├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs-assets └── thumbnail.jpg ├── package-lock.json ├── package.json ├── public ├── CNAME ├── clashjs-com.jpg ├── favicon.ico ├── index.html ├── robots.txt └── static │ ├── bg-m.jpg │ ├── bg.jpg │ ├── diamond.png │ ├── rockets │ ├── cristine.svg │ ├── cristine2.svg │ ├── figma.svg │ ├── rocket0.png │ ├── rocket1.png │ ├── rocket10.png │ ├── rocket2.png │ ├── rocket3.png │ ├── rocket4.png │ ├── rocket5.png │ ├── rocket6.png │ ├── rocket7.png │ ├── rocket8.png │ ├── rocket9.png │ └── svg-a.svg │ ├── star.png │ └── svg │ ├── 0.svg │ ├── 1.svg │ ├── 2.svg │ └── 3.svg ├── spec_assets ├── airunner-blackbox.png ├── game-blackbox.png └── screenshot.jpg └── src ├── Players.js ├── app.jsx ├── clashjs ├── ClashCore.js ├── PlayerClass.js └── executeMovementHelper.js ├── components ├── Ammos.js ├── ClashRender.js ├── Notifications.js ├── PlayersRender.js ├── Shoots.js ├── Stats.js └── Tiles.js ├── index.js ├── lib ├── codingpains-logic.js ├── config.js ├── string-tools.js └── utils.js ├── mixins └── deepSetState.js ├── players ├── codingpains.js ├── elperron.js ├── ericku.js ├── horror.js ├── javierbyte.js ├── manuelmhtr.js ├── margeux.js ├── siegfried.js ├── xmontoya.js └── yuno.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .vercel 26 | 27 | .eslintcache -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.0 2 | * Small internal refactor, `ClashJS` was renamed to `ClashCore` and `ClashRender` to differentiate the game state runner and the render. 3 | * Update UI to show better scores. 4 | 5 | # 1.2.0 6 | 7 | * Upgrades to `react` `16.13`. 8 | * Fixes readme getting started instructions, -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Javier Bórquez 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ClashJS.com](https://clashjs.com/) 2 | 3 | [![](public/clashjs-com.jpg)](https://clashjs.com/) 4 | 5 | [Demo Online](https://clashjs.com/) 6 | 7 | This is an experiment. The idea is to create a battle game, where the participants code their AI, and then we make them fight! You can play by adding your own AI to the game! 8 | 9 | # How to run the demo. 10 | 11 | You'll need nodejs [https://nodejs.org/en/download/](https://nodejs.org/en/download/), tested with current LTS version `12.x`. 12 | 13 | 1. Clone the repo. 14 | 15 | ``` 16 | git clone git@github.com:javierbyte/clashjs.git 17 | ``` 18 | 19 | 2. Move to the repo directory, install dependencies. 20 | 21 | ```bash 22 | cd clashjs 23 | npm install 24 | ``` 25 | 26 | 3. Run the repo! 27 | 28 | ``` 29 | npm start 30 | ``` 31 | 32 | It should open your browser with the `http://localhost:3000/` demo URL. 33 | 34 | The current configured players will start to play right away, at this point you can edit them on `src/players/.js` and the browser should refresh on save. 35 | 36 | # How to participate. 37 | 38 | Add your player as specificed in [player definition](#player-definition) in 39 | 40 | ``` 41 | /src/players/YOU.js 42 | ``` 43 | 44 | And then require yourself in 45 | 46 | ``` 47 | /src/Players.js 48 | ``` 49 | 50 | And run the demo again. Have fun! 51 | 52 | Read the [game definitions](#game-definitions) to learn how to create your player. Have fun! 53 | 54 | # Game. Functional Spec. 55 | 56 | ## Introduction. 57 | 58 | Games and coding are fun! So I want to make a game where we can confront AI vs AI in javascript. 59 | 60 | The game is simple: we will put all the players in a battle arena, and then make them fight to death. There will be ammo in the arena so they can shoot each other. The last player alive wins! 61 | 62 | ### Game Rules. 63 | 64 | - Every player will have a position and direction on the grid. A player can not go over the grid limits, and can only face north, east, south or west. 65 | - The game will be turn based. Every turn we will excecute the AI of every player passing as arguments: 66 | - The state of the player. 67 | - The state of all other players. 68 | - A environment configuration option with: 69 | - Grid size. 70 | - The position of the ammo. 71 | - Every turn a player must execute one of the following actions: 72 | - Move one step in its current direction. (`move`). 73 | - Turn into any of the four directions. (`north`, `east`, `south`, `west`). 74 | - Shoot. (`shoot`). 75 | - A player can shoot to try to destroy another player. 76 | - A player can collect ammo in the moment it steps over it. New ammo may appear in any moment of the game. 77 | 78 | ## Game Definitions. 79 | 80 | ### Player Definition. 81 | 82 | Let the _player definition_ (`playerDefinition`) be an object with the player info and its AI function. 83 | 84 | ```js 85 | { 86 | info: { 87 | name: 'javierbyte', 88 | style: 2 // one of the 6 styles (0 to 5) 89 | }, 90 | ai: function(playerState, enemiesStates, gameEnvironment) { 91 | // think... 92 | return 'move'; 93 | } 94 | } 95 | ``` 96 | 97 | The AI function will receive [`playerState`](#player-state), `enemiesStates` (array of all the other players `playerState`s), and [`gameEnvironment`](#game-environment) as arguments, and must return one of the following strings: 98 | 99 | - `move`: To move one tile in the current direction. 100 | - `north`, `east`, `south` or `west`: To turn to that direction. 101 | - `shoot`. To shoot if the user has enough ammo. 102 | 103 | Any other response, trying to move outside the arena size (`gameEnvironment.gridSize`) or trying to shoot without ammo, will result in a no-op. 104 | 105 | ### Player State. 106 | 107 | Let the _player state_ (`playerState`) be an object with a player information like the following: 108 | 109 | ```js 110 | { 111 | position: `[, ]`, 112 | direction: ``, // One of 'north', 'east', 'south' or 'west' 113 | ammo: ``, 114 | isAlive: `` 115 | } 116 | ``` 117 | 118 | ### Game Environment. 119 | 120 | Let the _game environment_ (`gameEnvironment`) be a configuration object like the following: 121 | 122 | ```js 123 | { 124 | gridSize: [, ], 125 | ammoPosition: , ] arrays> 126 | } 127 | ``` 128 | 129 | ### Game State. 130 | 131 | Let the _game state_ (`gameState`) be an object with the array of all user states, and the game environment. 132 | 133 | ```js 134 | { 135 | playerStates: , 136 | gameEnvironment: <`gameEnvironment`> 137 | } 138 | ``` 139 | 140 | # Game Technical Spec. 141 | 142 | ## Problem. 143 | 144 | We should make an app that can take functions provided by the users, execute them, and render the game as specified in the functional spec. 145 | 146 | ## Constraints. 147 | 148 | - The game mechanics should avoid accidentally benefitting players by its random nature. The order of execution of the AIs should not favor any particular player. The position of newly created coins should be fair for all players. 149 | - Be safe. A player code should not be able to modify anything other than itself. 150 | - Be resilient as possible. If a player crashes or stop responding, the show must go on. 151 | 152 | ## How this was made. 153 | 154 | ### Architecture. 155 | 156 | We can divide the problem in 3 big steps. 157 | 158 | - **AI Runner**. This will take all the user provided functions and the current game state, and execute every function. 159 | - This will take care of catch errors on the functions, and stop non-responding functions to hang the window. 160 | - **Game Core**. This will take the responses that the AI Runners sends, and apply the game logic on them. 161 | - Handle killed players. 162 | - Move and turn players. 163 | - Collect and count coins. 164 | - Generate new coins if necessary. 165 | - Set the paralized turns to players that shot. 166 | - Count if too many inactive turns have passed. 167 | - Stop the game when it ends. 168 | - **Render**. This will take the game state and render it nicely. 169 | 170 | They will interact as follows: 171 | 172 | ![](spec_assets/game-blackbox.png) 173 | 174 | 185 | 186 | # AI Runner. Spec. 187 | 188 | ## Problem. 189 | 190 | The AI runner should execute all the functions that the players provided, with the current user state, all user states, and game enrivonment as arguments. 191 | 192 | ## Constraints. 193 | 194 | - Isolate the user functions. They should not be allowed to modify any other code. 195 | - Catch executions errors, and simply return `null` as response to the Game Core. 196 | - Detect if any functions gets stuck in an infinite loop, and return `null` as response. 197 | 198 | ## Hypothesis. 199 | 200 | We can run the functions as WebWorkers because: 201 | 202 | - They can not access the dom and modify things. 203 | - Runs in a sandbox. If they crash or stop responding we can detect it. 204 | - Bonus: We can parallelise the excecution. 205 | 206 | The game is designed to make irrelevant the order of execution of the AIs. We are safe in running all this asynchronously. 207 | 208 | ## Solution. 209 | 210 | To prevent the functions from taking too much time processing (probably because an infinite loop), we will create an array of `null`s, where we will put the responses of the workers as they arrive. If `X` seconds passes (enough processing time for almost everything, except infinite loops, of couse) then we will pass the `null`ified response of that worker, and the Game Core will kill that player. 211 | 212 | ![](spec_assets/airunner-blackbox.png) 213 | 214 | 229 | 230 | # Game Core. 231 | 232 | ## Player Class. 233 | 234 | This javascript class will recive a `playerDefinition` and return a player instance. 235 | 236 | ### Arguments: 237 | 238 | - [`playerDefinition`](#player-definition). 239 | - [`evtCallback`] A callback that will receive the arguments `evt` and `data`. 240 | 241 | ### Methods: 242 | 243 | - `getInfo`. Will return the player info. 244 | - `execute`. Will receive the following arguments: 245 | - [`playerState`](#player-state). The current player state. 246 | - `enemiesStates`. An array of all the other live players `playerState`s. 247 | - [`gameEnvironment`](#game-environment). The game environment object. 248 | 249 | ## CashJS Class. 250 | 251 | This class will receive all the player definitions, generate the game states, and execute the players AIs. 252 | 253 | ### Arguments: 254 | 255 | - `playerDefinitionArray`. An array of [`playerDefinition`](#player-definition) objects. 256 | 257 | ### Methods: 258 | 259 | - `getState`. Will return the current [`gameState`](#game-state). 260 | - `nextStep`. Will execute a step for every player (all individual plys). Will return the game state. 261 | - `nextPly`. Will execute the AI for the player in turn. Will return the game state. 262 | 263 | # Render. 264 | 265 | React. 266 | -------------------------------------------------------------------------------- /docs-assets/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/docs-assets/thumbnail.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clashjs", 3 | "version": "1.3.0", 4 | "private": false, 5 | "homepage": "https://clashjs.com/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/javierbyte/clashjs.git" 9 | }, 10 | "author": "Javier Bórquez (http://github.com/javierbyte)", 11 | "license": "BSD-3-Clause", 12 | "dependencies": { 13 | "lodash": "^4.17.21", 14 | "react": "^18.1.0", 15 | "react-dom": "^18.1.0", 16 | "react-scripts": "^5.0.1", 17 | "wolfy87-eventemitter": "^5.2.9" 18 | }, 19 | "scripts": { 20 | "dev": "react-scripts start", 21 | "build": "react-scripts build", 22 | "predeploy": "npm run build", 23 | "deploy": "gh-pages -d build" 24 | }, 25 | "eslintConfig": { 26 | "ignorePatterns": [ 27 | "src/players/*.js" 28 | ], 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "gh-pages": "^3.2.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | clashjs.com -------------------------------------------------------------------------------- /public/clashjs-com.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/clashjs-com.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ClashJS | AI Javascript Battle Game 10 | 11 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 50 | 51 | 57 | 89 | 90 |
91 |
ClashJS. Javascript AI battle game.
92 |
Play by coding your own spaceship with javascript!
93 |
94 | Learn more 95 | github.com/javierbyte/clashjs, by javierbyte. 98 |
99 |
100 | 101 | 102 | 106 | 115 | 116 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/static/bg-m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/bg-m.jpg -------------------------------------------------------------------------------- /public/static/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/bg.jpg -------------------------------------------------------------------------------- /public/static/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/diamond.png -------------------------------------------------------------------------------- /public/static/rockets/cristine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/static/rockets/cristine2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/static/rockets/figma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/static/rockets/rocket0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket0.png -------------------------------------------------------------------------------- /public/static/rockets/rocket1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket1.png -------------------------------------------------------------------------------- /public/static/rockets/rocket10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket10.png -------------------------------------------------------------------------------- /public/static/rockets/rocket2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket2.png -------------------------------------------------------------------------------- /public/static/rockets/rocket3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket3.png -------------------------------------------------------------------------------- /public/static/rockets/rocket4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket4.png -------------------------------------------------------------------------------- /public/static/rockets/rocket5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket5.png -------------------------------------------------------------------------------- /public/static/rockets/rocket6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket6.png -------------------------------------------------------------------------------- /public/static/rockets/rocket7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket7.png -------------------------------------------------------------------------------- /public/static/rockets/rocket8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket8.png -------------------------------------------------------------------------------- /public/static/rockets/rocket9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/rockets/rocket9.png -------------------------------------------------------------------------------- /public/static/rockets/svg-a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 464 | 465 | 466 | 467 | 469 | 470 | 471 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | -------------------------------------------------------------------------------- /public/static/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/public/static/star.png -------------------------------------------------------------------------------- /public/static/svg/0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/static/svg/1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/static/svg/2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /public/static/svg/3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /spec_assets/airunner-blackbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/spec_assets/airunner-blackbox.png -------------------------------------------------------------------------------- /spec_assets/game-blackbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/spec_assets/game-blackbox.png -------------------------------------------------------------------------------- /spec_assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/spec_assets/screenshot.jpg -------------------------------------------------------------------------------- /src/Players.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // codingpains: require('./players/codingpains.js'), 3 | manuelmhtr: require('./players/manuelmhtr.js'), 4 | ericku: require('./players/ericku.js'), 5 | siegfried: require('./players/siegfried.js'), 6 | horror: require('./players/horror.js'), 7 | elperron: require('./players/elperron.js'), 8 | yuno: require('./players/yuno.js'), 9 | xmontoya: require('./players/xmontoya.js'), 10 | margeux: require('./players/margeux.js') 11 | }; 12 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import Clash from "./components/ClashRender.js"; 2 | export default Clash; 3 | -------------------------------------------------------------------------------- /src/clashjs/ClashCore.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | import PlayerClass from "./PlayerClass.js"; 4 | import executeMovementHelper from "./executeMovementHelper.js"; 5 | 6 | import EventEmitter from "wolfy87-eventemitter"; 7 | 8 | const DIRECTIONS = ["north", "east", "south", "west"]; 9 | const TOTAL_ROUNDS = 10; 10 | const SUDDEN_DEATH_PLY = 210; 11 | 12 | const MAP_SIZE = window.innerWidth > 720 ? 13 : 11; 13 | 14 | const ClashEmitter = new EventEmitter(); 15 | 16 | let STATE = { 17 | running: false, 18 | 19 | currentPlayer: 0, 20 | 21 | gameEnvironment: { 22 | gridSize: MAP_SIZE, 23 | ammoPosition: [], 24 | }, 25 | suddenDeathCount: 0, 26 | totalRounds: TOTAL_ROUNDS, 27 | rounds: 0, 28 | 29 | playerInstances: [], 30 | playerStates: [], 31 | 32 | playerDefinition: {}, 33 | gameStats: {}, 34 | }; 35 | 36 | function ClashJS(playerDefinitionArray) { 37 | STATE.playerInstances = playerDefinitionArray.map((playerDefinition) => { 38 | const player = new PlayerClass(playerDefinition); 39 | STATE.gameStats[player.id] = { 40 | id: player.id, 41 | name: player.name, 42 | kills: 0, 43 | wins: 0, 44 | }; 45 | return player; 46 | }); 47 | 48 | function emit(name, payload) { 49 | console.info("ClashCore: Emit", { name, payload }); 50 | ClashEmitter.emit("CLASHJS", { name, payload }); 51 | } 52 | 53 | function handleExecuteMovementCallback(event, payload) { 54 | emit(event, payload); 55 | } 56 | 57 | function handleCoreAction(action, data) { 58 | if (action === "KILL") { 59 | let { killer, killed } = data; 60 | 61 | STATE.gameStats[killer.id].kills += killed.length; 62 | 63 | _.forEach(STATE.playerInstances, (player) => { 64 | let stats = STATE.gameStats[player.id]; 65 | if (killed.indexOf(player) > -1) { 66 | STATE.alivePlayerCount--; 67 | stats.deaths++; 68 | } 69 | if (stats.deaths) { 70 | stats.kdr = stats.kills / stats.deaths; 71 | } else { 72 | stats.kdr = stats.kills; 73 | } 74 | }); 75 | STATE.suddenDeathCount = 0; 76 | } 77 | if (action === "WIN") { 78 | STATE.gameStats[data.winner.id].wins++; 79 | STATE.suddenDeathCount = 0; 80 | 81 | if (STATE.rounds >= STATE.totalRounds) { 82 | emit("GAME_OVER"); 83 | return; 84 | } 85 | } 86 | if (action === "DRAW") { 87 | STATE.suddenDeathCount = 0; 88 | 89 | if (STATE.rounds >= STATE.totalRounds) { 90 | emit("GAME_OVER"); 91 | return; 92 | } 93 | } 94 | } 95 | 96 | function savePlayerAction(playerIndex, playerAction) { 97 | STATE.playerStates = executeMovementHelper({ 98 | playerIndex: playerIndex, 99 | playerAction: playerAction, 100 | playerStates: STATE.playerStates, 101 | playerInstances: STATE.playerInstances, 102 | gameEnvironment: STATE.gameEnvironment, 103 | evtCallback: handleExecuteMovementCallback, 104 | coreCallback: handleCoreAction, 105 | }); 106 | } 107 | 108 | function createAmmo() { 109 | const newAmmoPosition = [ 110 | Math.floor(Math.random() * STATE.gameEnvironment.gridSize), 111 | Math.floor(Math.random() * STATE.gameEnvironment.gridSize), 112 | ]; 113 | 114 | if ( 115 | STATE.gameEnvironment.ammoPosition.some((el) => { 116 | return el[0] === newAmmoPosition[0] && el[1] === newAmmoPosition[1]; 117 | }) 118 | ) { 119 | createAmmo(); 120 | return; 121 | } 122 | 123 | STATE.gameEnvironment.ammoPosition.push(newAmmoPosition); 124 | } 125 | 126 | return { 127 | newGame() { 128 | STATE.gameEnvironment = { 129 | gridSize: MAP_SIZE, 130 | ammoPosition: [], 131 | }; 132 | STATE.rounds = STATE.rounds + 1; 133 | STATE.suddenDeathCount = 0; 134 | STATE.playerInstances = _.shuffle(_.cloneDeep(STATE.playerInstances)); 135 | STATE.playerStates = STATE.playerInstances.map((playerInstance) => { 136 | const gridSize = STATE.gameEnvironment.gridSize; 137 | const directionAngle = Math.floor(Math.random() * 4); 138 | 139 | return { 140 | name: playerInstance.name, 141 | id: playerInstance.id, 142 | style: playerInstance.info.style, 143 | position: [Math.floor(Math.random() * gridSize), Math.floor(Math.random() * gridSize)], 144 | direction: DIRECTIONS[directionAngle], 145 | directionAngle: directionAngle, 146 | ammo: 0, 147 | isAlive: true, 148 | }; 149 | }); 150 | STATE.currentPlayer = 0; 151 | STATE = { ...STATE }; 152 | createAmmo(); 153 | }, 154 | 155 | getAlivePlayerCount() { 156 | return STATE.playerStates.filter((player) => player.isAlive).length; 157 | }, 158 | 159 | getState() { 160 | const derivedStats = STATE.gameStats; 161 | 162 | for (const playerKey in derivedStats) { 163 | const player = derivedStats[playerKey]; 164 | player.score = player.wins * 5 + player.kills; 165 | player.isAlive = STATE.playerStates.find( 166 | (playerState) => playerState.id === playerKey 167 | ).isAlive; 168 | } 169 | 170 | return JSON.parse( 171 | JSON.stringify({ 172 | gameEnvironment: STATE.gameEnvironment, 173 | gameStats: derivedStats, 174 | rounds: STATE.rounds, 175 | totalRounds: STATE.totalRounds, 176 | playerStates: STATE.playerStates, 177 | playerInstances: STATE.playerInstances, 178 | }) 179 | ); 180 | }, 181 | 182 | nextPly() { 183 | let clonedStates = _.cloneDeep(STATE.playerStates); 184 | 185 | STATE.suddenDeathCount++; 186 | 187 | if (STATE.suddenDeathCount === SUDDEN_DEATH_PLY - 35) { 188 | emit("PRE_DRAW"); 189 | } 190 | 191 | if (STATE.suddenDeathCount > SUDDEN_DEATH_PLY) { 192 | emit("DRAW"); 193 | if (STATE.rounds >= STATE.totalRounds) { 194 | emit("GAME_OVER"); 195 | return; 196 | } 197 | } 198 | 199 | const otherPlayers = clonedStates.filter((currentEnemyFilter, index) => { 200 | if (index === STATE.currentPlayer) return false; 201 | return currentEnemyFilter.isAlive; 202 | }); 203 | 204 | if (STATE.playerStates[STATE.currentPlayer].isAlive) { 205 | savePlayerAction( 206 | STATE.currentPlayer, 207 | STATE.playerInstances[STATE.currentPlayer].execute( 208 | clonedStates[STATE.currentPlayer], 209 | otherPlayers, 210 | _.cloneDeep(STATE.gameEnvironment) 211 | ) 212 | ); 213 | } 214 | 215 | STATE.currentPlayer = (STATE.currentPlayer + 1) % STATE.playerInstances.length; 216 | 217 | if ( 218 | STATE.gameEnvironment.ammoPosition.length < STATE.playerStates.length / 1.3 && 219 | Math.random() > 0.93 220 | ) { 221 | createAmmo(); 222 | } 223 | 224 | if (Math.random() > 0.98) { 225 | createAmmo(); 226 | } 227 | 228 | return this.getState(); 229 | }, 230 | 231 | addListener(callback) { 232 | ClashEmitter.addListener("CLASHJS", callback); 233 | }, 234 | }; 235 | } 236 | 237 | export default ClashJS; 238 | -------------------------------------------------------------------------------- /src/clashjs/PlayerClass.js: -------------------------------------------------------------------------------- 1 | import generateId from "./../lib/string-tools"; 2 | 3 | class PlayerClass { 4 | constructor(options) { 5 | this.id = generateId.generateBase32String(8); 6 | this.info = options.info; 7 | this.name = options.info.name; 8 | this.ai = options.ai; 9 | } 10 | 11 | execute(playerState, enemyState, gameEnvironment) { 12 | try { 13 | return this.ai(playerState, enemyState, gameEnvironment); 14 | } catch (e) { 15 | console.error("!", e); 16 | } 17 | } 18 | } 19 | 20 | export default PlayerClass; 21 | -------------------------------------------------------------------------------- /src/clashjs/executeMovementHelper.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const utils = require("../lib/utils.js"); 3 | const DIRECTIONS = ["north", "east", "south", "west"]; 4 | 5 | const safeMovement = (value, size) => { 6 | if (value < 0) return 0; 7 | if (value > size - 1) return size - 1; 8 | return value; 9 | }; 10 | 11 | const clashCoreUtils = (data) => { 12 | const { 13 | playerIndex, 14 | playerAction, 15 | playerStates, 16 | playerInstances, 17 | gameEnvironment, 18 | evtCallback, 19 | coreCallback, 20 | } = data; 21 | const currentPlayerState = playerStates[playerIndex]; 22 | 23 | if (DIRECTIONS.indexOf(playerAction) !== -1) { 24 | const newDirection = DIRECTIONS.indexOf(playerAction); 25 | const currentDirection = DIRECTIONS.indexOf(currentPlayerState.direction); 26 | 27 | // alert(newDirection, currentPlayerState.direction) 28 | 29 | let directionDifference = ((newDirection + 4) % 4) - ((currentDirection + 4) % 4); 30 | if (directionDifference === 3) directionDifference = -1; 31 | if (directionDifference === -3) directionDifference = 1; 32 | 33 | playerStates[playerIndex].direction = playerAction; 34 | playerStates[playerIndex].directionAngle = 35 | playerStates[playerIndex].directionAngle + directionDifference; 36 | return playerStates; 37 | } 38 | 39 | if (playerAction === "move") { 40 | switch (currentPlayerState.direction) { 41 | case DIRECTIONS[0]: 42 | currentPlayerState.position[0]--; 43 | break; 44 | case DIRECTIONS[1]: 45 | currentPlayerState.position[1]++; 46 | break; 47 | case DIRECTIONS[2]: 48 | currentPlayerState.position[0]++; 49 | break; 50 | case DIRECTIONS[3]: 51 | currentPlayerState.position[1]--; 52 | break; 53 | default: 54 | break; 55 | } 56 | 57 | // prevent the player to go over the world 58 | currentPlayerState.position[0] = safeMovement( 59 | currentPlayerState.position[0], 60 | gameEnvironment.gridSize 61 | ); 62 | currentPlayerState.position[1] = safeMovement( 63 | currentPlayerState.position[1], 64 | gameEnvironment.gridSize 65 | ); 66 | 67 | // check if the player collected ammo 68 | gameEnvironment.ammoPosition.forEach((el, index) => { 69 | if (el[0] === currentPlayerState.position[0] && el[1] === currentPlayerState.position[1]) { 70 | gameEnvironment.ammoPosition.splice(index, 1); 71 | currentPlayerState.ammo += 1; 72 | } 73 | }); 74 | } 75 | 76 | if (playerAction === "shoot" && currentPlayerState.ammo > 0) { 77 | currentPlayerState.ammo -= 1; 78 | 79 | let kills = []; 80 | let survivors = []; 81 | 82 | evtCallback("SHOOT", { 83 | shooter: playerIndex, 84 | origin: currentPlayerState.position, 85 | direction: currentPlayerState.direction, 86 | }); 87 | 88 | playerStates.forEach((enemyObject, enemyIndex) => { 89 | if ( 90 | enemyObject.isAlive && 91 | utils.isVisible( 92 | currentPlayerState.position, 93 | enemyObject.position, 94 | currentPlayerState.direction 95 | ) 96 | ) { 97 | kills.push(enemyIndex); 98 | enemyObject.isAlive = false; 99 | } 100 | }); 101 | 102 | if (kills.length) { 103 | survivors = _.filter(playerStates, (player) => player.isAlive); 104 | coreCallback("KILL", { 105 | killer: playerInstances[playerIndex], 106 | killed: _.map(kills, (index) => playerInstances[index]), 107 | }); 108 | evtCallback("KILL", { 109 | killer: playerInstances[playerIndex], 110 | killed: _.map(kills, (index) => playerInstances[index]), 111 | }); 112 | 113 | if (survivors.length === 1) { 114 | coreCallback("WIN", { 115 | winner: playerInstances[playerIndex], 116 | }); 117 | evtCallback("WIN", { 118 | winner: playerInstances[playerIndex], 119 | }); 120 | } 121 | } 122 | } 123 | 124 | return playerStates; 125 | }; 126 | 127 | export default clashCoreUtils; 128 | -------------------------------------------------------------------------------- /src/components/Ammos.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | 4 | class Ammos extends React.Component { 5 | render() { 6 | var { gridSize, ammoPosition } = this.props; 7 | 8 | var tileSize = 100 / gridSize; 9 | 10 | var ammoRender = _.map(ammoPosition, (ammoPos, ammoIndex) => { 11 | return ( 12 |
22 | ); 23 | }); 24 | 25 | return ( 26 |
27 | {ammoRender} 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Ammos; 34 | -------------------------------------------------------------------------------- /src/components/ClashRender.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import Tiles from "./Tiles.js"; 4 | import Ammos from "./Ammos.js"; 5 | import PlayersRender from "./PlayersRender.js"; 6 | import Stats from "./Stats.js"; 7 | import Shoots from "./Shoots.js"; 8 | import Notifications from "./Notifications.js"; 9 | 10 | import ClashJS from "../clashjs/ClashCore.js"; 11 | 12 | import playerObjects from "../Players.js"; 13 | const playerDefinitionArray = _.shuffle(_.map(playerObjects, (el) => el)); 14 | 15 | const DEBUG = document.location.search.includes("debug"); 16 | 17 | const DEFAULT_SPEED = DEBUG ? 32 : 200; 18 | const MAX_SPEED = DEBUG ? 32 : 100; 19 | // const DEFAULT_SPEED = 4; 20 | // const MAX_SPEED = 4; 21 | 22 | const EXPIRE_NOTIF_TIME = 7 * 1000; 23 | 24 | const ClashInstance = ClashJS(playerDefinitionArray); 25 | ClashInstance.newGame(); 26 | 27 | class Clash extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | clashState: null, 33 | shoots: [], 34 | speed: DEFAULT_SPEED, 35 | notifications: [], 36 | finished: false, 37 | }; 38 | } 39 | 40 | componentWillMount() { 41 | ClashInstance.addListener(({ name, payload }) => { 42 | if (name === "SHOOT") { 43 | this.setState((state) => { 44 | return { 45 | shoots: [ 46 | ...state.shoots, 47 | { 48 | direction: payload.direction, 49 | origin: payload.origin.slice(), 50 | time: new Date().getTime(), 51 | }, 52 | ], 53 | }; 54 | }); 55 | return; 56 | } 57 | 58 | if (name === "KILL") return this.handleKill(payload); 59 | if (name === "WIN") { 60 | const winner = _.get(payload, ["winner", "info", "name"]); 61 | if (winner) { 62 | this.pushNotification({ 63 | text: ( 64 |
65 | {winner} won this one! 66 |
67 | ), 68 | }); 69 | } 70 | this.newGame(); 71 | return; 72 | } 73 | if (name === "DRAW") { 74 | this.pushNotification({ 75 | text: We got a draw!, 76 | }); 77 | this.newGame(); 78 | return; 79 | } 80 | if (name === "PRE_DRAW") { 81 | this.pushNotification({ 82 | text: `They are too strong!`, 83 | }); 84 | } 85 | if (name === "GAME_OVER") return this.endGame(); 86 | }); 87 | 88 | this.nextTurn(); 89 | } 90 | 91 | newGame() { 92 | if (ClashInstance.getState().rounds >= ClashInstance.getState().totalRounds) { 93 | return; 94 | } 95 | 96 | this.pushNotification({ 97 | text: `Starting a new game!`, 98 | }); 99 | 100 | this.setState( 101 | { 102 | speed: DEFAULT_SPEED, 103 | }, 104 | () => { 105 | ClashInstance.newGame(); 106 | } 107 | ); 108 | } 109 | 110 | nextTurn() { 111 | const { speed, finished } = this.state; 112 | 113 | if (finished) { 114 | return; 115 | } 116 | 117 | this.setState( 118 | { 119 | clashState: ClashInstance.nextPly(), 120 | speed: this.state.speed > MAX_SPEED ? parseInt(this.state.speed * 0.98, 10) : MAX_SPEED, 121 | }, 122 | () => { 123 | const alivePlayerCount = ClashInstance.getAlivePlayerCount(); 124 | 125 | if (alivePlayerCount >= 2) { 126 | window.setTimeout(() => { 127 | this.nextTurn(); 128 | }, speed); 129 | } 130 | } 131 | ); 132 | } 133 | 134 | handleKill({ killer, killed }) { 135 | this.pushNotification({ 136 | text: `${killer.name} eliminated ${_.map(killed, (player) => player.name).join(",")}`, 137 | }); 138 | } 139 | 140 | endGame() { 141 | const clashState = ClashInstance.getState(); 142 | 143 | const winner = _.sortBy( 144 | clashState.gameStats, 145 | (playerStats) => playerStats.score * -1 + playerStats.wins * -0.1 146 | )[0]; 147 | 148 | this.pushNotification({ 149 | expire: Infinity, 150 | text: Congratulations {winner.name}!, 151 | }); 152 | 153 | this.pushNotification({ 154 | expire: Infinity, 155 | text: ( 156 |
157 | 170 |
171 | ), 172 | }); 173 | 174 | this.setState({ 175 | shoots: [], 176 | finished: true, 177 | }); 178 | } 179 | 180 | pushNotification({ text, expire }) { 181 | this.setState((state) => { 182 | return { 183 | notifications: [ 184 | ...state.notifications, 185 | { 186 | expire: expire || new Date().getTime() + EXPIRE_NOTIF_TIME, 187 | date: new Date().getTime(), 188 | text: text, 189 | id: state.notifications.length, 190 | }, 191 | ], 192 | }; 193 | }); 194 | } 195 | 196 | render() { 197 | if (!this.state) return null; 198 | 199 | const { shoots, speed, notifications } = this.state; 200 | const { 201 | gameStats, 202 | playerStates, 203 | playerInstances, 204 | rounds, 205 | totalRounds, 206 | gameEnvironment, 207 | } = ClashInstance.getState(); 208 | 209 | return ( 210 |
211 | 212 | 213 | 214 | 220 | 221 | 222 | {DEBUG && ( 223 |
224 |             playerInstances
225 |             {JSON.stringify(playerInstances, 0, 2)}
226 |             
227 | playerStates 228 | {JSON.stringify(playerStates, 0, 2)} 229 |
230 | )} 231 |
232 | ); 233 | } 234 | } 235 | 236 | export default Clash; 237 | -------------------------------------------------------------------------------- /src/components/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | 3 | const MAX_NOTIF = 7; 4 | 5 | function useInterval(callback, delay) { 6 | const savedCallback = useRef(); 7 | 8 | useEffect(() => { 9 | savedCallback.current = callback; 10 | }); 11 | 12 | useEffect(() => { 13 | function tick() { 14 | savedCallback.current(); 15 | } 16 | 17 | let id = setInterval(tick, delay); 18 | return () => clearInterval(id); 19 | }, [delay]); 20 | } 21 | 22 | function Notifications(props) { 23 | const [dateTime, setDateTime] = React.useState(new Date().getTime()); 24 | const { notifications } = props; 25 | 26 | const filteredKeys = notifications 27 | .filter((el) => el && el.text) 28 | .filter((el) => dateTime < el.expire) 29 | .sort((a, b) => a.date - b.date) 30 | .slice(MAX_NOTIF * -1); 31 | 32 | useInterval(() => { 33 | setDateTime(new Date().getTime()); 34 | }, 50); 35 | 36 | return ( 37 |
38 | {filteredKeys.map((notificationData, notificationIdx) => { 39 | const hideByLength = notificationIdx <= filteredKeys.length - MAX_NOTIF; 40 | const showByTime = dateTime < notificationData.expire - 500; 41 | return ( 42 | 47 | ); 48 | })} 49 |
50 | ); 51 | } 52 | 53 | function Notification(props) { 54 | const [rendered, setRendered] = React.useState(false); 55 | const { hide } = props; 56 | 57 | useInterval(() => { 58 | setRendered(true); 59 | }, 16); 60 | 61 | return ( 62 |
66 |
{props.text}
67 |
68 | ); 69 | } 70 | 71 | export default Notifications; 72 | -------------------------------------------------------------------------------- /src/components/PlayersRender.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | 4 | function getSpaceArt(info) { 5 | if (_.isPlainObject(info)) { 6 | return `url(static/rockets/${info.type})`; 7 | } 8 | 9 | return `url(static/rockets/rocket${info}.png)`; 10 | } 11 | 12 | function PlayersRender(props) { 13 | const { gridSize, playerStates, playerInstances, speed } = props; 14 | const playerDirections = playerStates.map((el) => el.directionAngle); 15 | const tileSize = 100 / gridSize; 16 | 17 | const playerRender = _.map(playerStates, (playerData, playerIndex) => { 18 | const playerUUID = playerData.id; 19 | const playerInfo = playerInstances.find((player) => player.id === playerUUID).info; 20 | 21 | return ( 22 |
35 |
52 |
55 | {playerInfo.name} 56 | {new Array(Math.min(playerData.ammo, 3)).fill("🚀").join("")} 57 |
58 |
59 | ); 60 | }); 61 | 62 | return
{playerRender}
; 63 | } 64 | 65 | export default PlayersRender; 66 | -------------------------------------------------------------------------------- /src/components/Shoots.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | 4 | var DIRECTIONS = ["north", "east", "south", "west"]; 5 | 6 | class Shoots extends React.Component { 7 | // shouldComponentUpdate(nextProps) { 8 | // return nextProps.shoots.length !== this.props.shoots.length; 9 | // } 10 | 11 | render() { 12 | var { shoots, gridSize } = this.props; 13 | 14 | var tileSize = 100 / gridSize; 15 | 16 | var shootsRender = _.map(shoots, (el, index) => { 17 | return ( 18 |
36 | ); 37 | }); 38 | 39 | return
{shootsRender}
; 40 | } 41 | } 42 | 43 | export default Shoots; 44 | -------------------------------------------------------------------------------- /src/components/Stats.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | 4 | function Stats(props) { 5 | const [expand, setExpand] = React.useState(false); 6 | 7 | const isToggable = window.innerHeight < 1080; 8 | 9 | const isExpanded = expand || !isToggable; 10 | 11 | const { gameStats, rounds, total } = props; 12 | 13 | const orderedStats = Object.keys(gameStats) 14 | .map((playerId) => { 15 | return { ...gameStats[playerId], id: playerId }; 16 | }) 17 | .sort((a, b) => b.score - a.score); 18 | 19 | return ( 20 |
{setExpand(!expand)}}> 21 | {isExpanded ? ( 22 |
28 | Round {rounds} of {total} 29 |
30 | ) : ( 31 |
Click to see stats
32 | )} 33 | {isExpanded && ( 34 | 35 | 36 | 37 | 38 | 39 | 42 | 45 | 48 | 49 | 50 | 51 | {_.map(orderedStats, (playerStats, index) => { 52 | return ( 53 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | ); 65 | })} 66 | 67 |
Player 40 | E 41 | 43 | W 44 | 46 | S 47 |
{playerStats.name} 56 | 57 | 💀 58 | 59 | {playerStats.kills}{playerStats.wins}{playerStats.score}
68 | )} 69 |
70 | ); 71 | } 72 | 73 | export default Stats; 74 | -------------------------------------------------------------------------------- /src/components/Tiles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Tiles extends React.Component { 4 | // shouldComponentUpdate() { 5 | // return false; 6 | // } 7 | 8 | render() { 9 | var { gridSize } = this.props; 10 | 11 | var tileSize = 100 / gridSize; 12 | var i; 13 | 14 | var tileRender = []; 15 | for (i = 0; i < gridSize * gridSize; i++) { 16 | tileRender.push( 17 |
22 | ); 23 | } 24 | 25 | return
{tileRender}
; 26 | } 27 | } 28 | 29 | export default Tiles; 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import App from "./app.jsx"; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById("root")); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/lib/codingpains-logic.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | var utils = require('./utils.js'); 4 | var DIRECTIONS = ['north', 'east', 'south', 'west']; 5 | 6 | var inDanger = function(player, enemies) { 7 | if (!enemies.length) return false; 8 | var pos = player.position; 9 | return _.some(enemies, e => sameY(pos, e.position) || sameX(pos, e.position)); 10 | }; 11 | 12 | var sameY = function(start, end) { 13 | return start[0] === end[0]; 14 | }; 15 | 16 | var sameX = function(start, end) { 17 | return start[1] === end[1]; 18 | }; 19 | 20 | var canMoveTowards = function(direction, player, map) { 21 | var canDo = false; 22 | switch (direction) { 23 | case 'north': 24 | canDo = player.position[0] > 0; 25 | break; 26 | case 'east': 27 | canDo = player.position[1] < map.gridSize; 28 | break; 29 | case 'south': 30 | canDo = player.position[0] < map.gridSize - 1; 31 | break; 32 | case 'west': 33 | canDo = player.position[1] > 0; 34 | break; 35 | } 36 | return canDo; 37 | }; 38 | 39 | var canDie = function(player, enemies) { 40 | return enemies 41 | .map(function(enemy) { 42 | return enemy.ammo > 0 && utils.isVisible(enemy.position, player.position, enemy.direction); 43 | }) 44 | .filter(function(result) { 45 | return result === true; 46 | }).length > 0; 47 | }; 48 | 49 | var getClosestAmmo = function(player, ammoPosition) { 50 | var closest; 51 | 52 | if (!ammoPosition.length) return; 53 | 54 | closest = ammoPosition[0]; 55 | 56 | ammoPosition.forEach(function(ammo) { 57 | var isCloser = utils.getDistance(player.position, ammo) < utils.getDistance(player.position, closest); 58 | if (isCloser) { 59 | closest = ammo; 60 | } 61 | }); 62 | 63 | return closest; 64 | }; 65 | 66 | var getReachableAmmo = function(player, enemies, map) { 67 | var reachable = map.ammoPosition.filter(function(ammo) { 68 | var distance = utils.getDistance(player.position, ammo); 69 | 70 | return !enemies.some(function(enemy) { 71 | return utils.getDistance(enemy.position, ammo) < distance; 72 | }); 73 | }); 74 | 75 | return reachable; 76 | }; 77 | 78 | var shouldMoveForAmmo = function(player, enemies, map) { 79 | var ammo = getReachableAmmo(player, enemies, map); 80 | var closest; 81 | var direction; 82 | 83 | if (!ammo.length) return false; 84 | 85 | closest = getClosestAmmo(player, ammo); 86 | direction = utils.fastGetDirection(player.position, closest); 87 | 88 | if (direction !== player.direction) { 89 | return direction; 90 | } 91 | 92 | return 'move'; 93 | }; 94 | 95 | var isMovementSafe = function(action, player, enemies, map) { 96 | var futureState = JSON.parse(JSON.stringify(player)); 97 | 98 | if (action === 'move') { 99 | switch (player.direction) { 100 | case DIRECTIONS[0]: 101 | if (futureState.position[0] > 0) { 102 | futureState.position[0]--; 103 | } 104 | break; 105 | case DIRECTIONS[1]: 106 | if (futureState.position[1] < map.gridSize) { 107 | futureState.position[1]++; 108 | } 109 | break; 110 | case DIRECTIONS[2]: 111 | if (futureState.position[0] < map.gridSize) { 112 | futureState.position[0]++; 113 | } 114 | break; 115 | case DIRECTIONS[3]: 116 | if (futureState.position[1] > 0) { 117 | futureState.position[1]--; 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | } 124 | 125 | if (canDie(futureState, enemies)) { 126 | return false; 127 | } else { 128 | return true; 129 | } 130 | }; 131 | 132 | var getSafestMove = function(player, enemies, map) { 133 | var safest; 134 | var isSafeHere = isMovementSafe('north', player, enemies, map); 135 | var isSafeToMove = isMovementSafe('move', player, enemies, map); 136 | 137 | if (isSafeHere) { 138 | if (player.ammo) { 139 | return turnToKill(player, enemies) || chaseEnemy(player, enemies, map); 140 | } 141 | } 142 | if (isSafeToMove) { 143 | return 'move'; 144 | } 145 | 146 | return; 147 | }; 148 | 149 | var goToCenter = function(player, map) { 150 | var center = [map.gridSize, map.gridSize].map(coord => Math.floor(coord / 2)); 151 | var movement = utils.fastGetDirection(player.position, center); 152 | 153 | if (movement === player.direction) { 154 | movement = 'move'; 155 | } 156 | 157 | return movement; 158 | }; 159 | 160 | var getClosestEnemy = function(player, enemies) { 161 | var clonedStates = enemies.slice(0, enemies.length); 162 | var closest; 163 | 164 | clonedStates = clonedStates.filter(function(enemy) { 165 | return enemy.isAlive; 166 | }); 167 | 168 | closest = clonedStates[0]; 169 | 170 | clonedStates.forEach(function(enemy) { 171 | if (utils.getDistance(player, enemy) < utils.getDistance(player, closest)) { 172 | closest = enemy; 173 | } 174 | }); 175 | 176 | return closest; 177 | }; 178 | 179 | var getBackPosition = function(enemy) { 180 | var back = enemy.position.slice(0, 2); 181 | switch (enemy.direction) { 182 | case 'north': 183 | back[0]++; 184 | break; 185 | case 'south': 186 | back[0]--; 187 | break; 188 | case 'west': 189 | back[1]++; 190 | break; 191 | case 'east': 192 | back[1]--; 193 | break; 194 | } 195 | 196 | return back; 197 | }; 198 | 199 | var sneakyGetDirection = function(player, enemy) { 200 | var diffVertical = Math.abs(player.position[0] - player.position[0]); 201 | 202 | if (diffVertical && enemy.position !== 'north' && enemy.position !== 'south') { 203 | return player.position[0] - enemy.position[0] > 0 ? 'north' : 'south'; 204 | } 205 | return player.position[1] - enemy.position[1] > 0 ? 'west' : 'east'; 206 | }; 207 | 208 | var verticalDelta = function(start, end) { 209 | return start[0] - end[0]; 210 | }; 211 | 212 | var absVerticalDelta = function(start, end) { 213 | return Math.abs(verticalDelta(start, end)); 214 | }; 215 | 216 | var horizontalDelta = function(start, end) { 217 | return start[1] - end[1]; 218 | }; 219 | 220 | var absHorizontalDelta = function(start, end) { 221 | return Math.abs(horizontalDelta(start, end)); 222 | }; 223 | 224 | var isVertical = function(direction) { 225 | return ['north', 'south'].indexOf(direction) > -1; 226 | }; 227 | 228 | var isHorizontal = function(direction) { 229 | return ['west', 'east'].indexOf(direction) > -1; 230 | }; 231 | 232 | var opositeDirection = function(direction) { 233 | var ret; 234 | switch (direction) { 235 | case 'north': 236 | ret = 'south'; 237 | break; 238 | case 'south': 239 | ret = 'north'; 240 | break; 241 | case 'west': 242 | ret = 'east'; 243 | break; 244 | case 'east': 245 | ret = 'west'; 246 | break; 247 | } 248 | return ret; 249 | }; 250 | 251 | var chaseEnemy = function(player, enemies, map) { 252 | var closest = getClosestEnemy(player, enemies); 253 | var back = getBackPosition(closest); 254 | var direction = sneakyGetDirection(player, closest); 255 | 256 | if (direction !== player.direction) { 257 | return direction; 258 | } 259 | 260 | if (!isMovementSafe('move', player, [closest], map)) { 261 | if (isVertical(player.direction) && absHorizontalDelta(player.position, closest.position) === 1) return 'hold'; 262 | if (isHorizontal(player.direction) && absVerticalDelta(player.position, closest.position) === 1) return 'hold'; 263 | return opositeDirection(closest.direction); 264 | } 265 | 266 | return 'move'; 267 | }; 268 | 269 | var turnToKill = function(player, enemies) { 270 | var turn = false; 271 | var mockState = JSON.parse(JSON.stringify(player)); 272 | 273 | DIRECTIONS.forEach(function(direction) { 274 | mockState.direction = direction; 275 | 276 | if (utils.canKill(mockState, enemies)) { 277 | turn = direction; 278 | } 279 | }); 280 | 281 | return turn; 282 | }; 283 | 284 | var turnToAmbush = function(player, enemies) { 285 | var killables = enemies.filter(function(enemy) { 286 | switch (enemy.direction) { 287 | case 'north': 288 | return verticalDelta(player.position, enemy.position) === -1; 289 | break; 290 | case 'east': 291 | return verticalDelta(player.position, enemy.position) === 1; 292 | break; 293 | case 'south': 294 | return verticalDelta(player.position, enemy.position) === 1; 295 | break; 296 | case 'west': 297 | return verticalDelta(player.position, enemy.position) === -1; 298 | break; 299 | default: 300 | return false; 301 | } 302 | }); 303 | var enemy; 304 | 305 | if (!killables.length) return; 306 | enemy = killables[0]; 307 | if (isVertical(enemy.direction)) { 308 | if (horizontalDelta(player.position, enemy.position) < 0) return 'east'; 309 | return 'west'; 310 | } 311 | 312 | if (verticalDelta(player.position, enemy.position) < 0) return 'south'; 313 | return 'north'; 314 | }; 315 | 316 | var canKillMany = function(player, enemies) { 317 | let { position, direction } = player; 318 | var targets = _.filter(enemies, enemy => utils.isVisible(position, enemy.position, direction)); 319 | return targets.length > 2; 320 | }; 321 | 322 | var canKillAll = function(player, enemies) { 323 | if (!player.ammo) return false; 324 | var killable = enemies.filter(enemy => utils.canKill(player, [enemy])); 325 | 326 | return enemies.length === killable.length; 327 | }; 328 | 329 | var getImmediateThreats = function(player, enemies) { 330 | return enemies.filter(enemy => enemy.ammo > 0 && utils.isVisible(enemy.position, player.position, enemy.direction)); 331 | }; 332 | 333 | var getDangerousEnemies = function(enemies) { 334 | var dangerous = enemies.filter(enemy => enemy.ammo); 335 | if (dangerous.length) return dangerous; 336 | return enemies; 337 | }; 338 | 339 | module.exports = { 340 | inDanger, 341 | sameY, 342 | sameX, 343 | canDie, 344 | canMoveTowards, 345 | canKillMany, 346 | getClosestAmmo, 347 | getReachableAmmo, 348 | shouldMoveForAmmo, 349 | isMovementSafe, 350 | getSafestMove, 351 | goToCenter, 352 | getClosestEnemy, 353 | getBackPosition, 354 | sneakyGetDirection, 355 | isHorizontal, 356 | isVertical, 357 | absVerticalDelta, 358 | absHorizontalDelta, 359 | opositeDirection, 360 | chaseEnemy, 361 | turnToKill, 362 | turnToAmbush, 363 | canKillAll, 364 | getImmediateThreats, 365 | getDangerousEnemies 366 | }; 367 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /src/lib/string-tools.js: -------------------------------------------------------------------------------- 1 | var digits = '0123456789'; 2 | var lowercase = 'abcdefghijklmnoprstuvxuyz'; 3 | var uppercase = lowercase.toUpperCase(); 4 | var special = '+/'; 5 | var base64Chars; 6 | var base62Chars; 7 | var base32Chars; 8 | var generateBaseString; 9 | 10 | module.exports = (exports = {}); 11 | 12 | base64Chars = function() { 13 | return [digits, lowercase, uppercase, special].join(''); 14 | }; 15 | 16 | base62Chars = function() { 17 | return [digits, lowercase, uppercase].join(''); 18 | }; 19 | 20 | base32Chars = function() { 21 | return [uppercase, digits].join(''); 22 | }; 23 | 24 | generateBaseString = function(pool, length) { 25 | var output = ''; 26 | var i; 27 | 28 | for (i = 0; i < length; i += 1) { 29 | output += pool[Math.ceil(Math.random() * 100 % pool.length || 1) - 1]; 30 | } 31 | 32 | return output; 33 | }; 34 | 35 | exports.generateBase64String = function(length) { 36 | return generateBaseString(base64Chars(), length); 37 | }; 38 | 39 | exports.generateBase62String = function(length) { 40 | return generateBaseString(base62Chars(), length); 41 | }; 42 | 43 | exports.generateBase32String = function(length) { 44 | return generateBaseString(base32Chars(), length); 45 | }; 46 | 47 | exports.generateBase10String = function(length) { 48 | return generateBaseString(digits, length); 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | var DIRECTIONS = ['north', 'east', 'south', 'west']; 2 | var movements = ['north', 'east', 'south', 'west', 'shoot']; 3 | 4 | var randomMove = () => { 5 | return Math.random() > 0.33 ? 'move' : movements[Math.floor(Math.random() * movements.length)]; 6 | }; 7 | 8 | var safeRandomMove = () => { 9 | return Math.random() > 0.33 ? 'move' : DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)]; 10 | }; 11 | 12 | var turn = (currentPosition = [], howMuchTurn) => { 13 | var currentPositionIndex = DIRECTIONS.indexOf(currentPosition); 14 | return DIRECTIONS[(currentPositionIndex + howMuchTurn) % 4]; 15 | }; 16 | 17 | var getDirection = (start = [], end = []) => { 18 | start = start || []; 19 | end = end || []; 20 | 21 | var diffVertical = Math.abs(start[0] - end[0]); 22 | var diffHorizontal = Math.abs(start[1] - end[1]); 23 | 24 | if (diffVertical > diffHorizontal) { 25 | return start[0] - end[0] > 0 ? 'north' : 'south'; 26 | } 27 | return start[1] - end[1] > 0 ? 'west' : 'east'; 28 | }; 29 | 30 | var getDistance = (start = [], end = []) => { 31 | var diffVertical = Math.abs(start[0] - end[0]); 32 | var diffHorizontal = Math.abs(start[1] - end[1]); 33 | 34 | return diffHorizontal + diffVertical; 35 | }; 36 | 37 | var fastGetDirection = (start = [], end = []) => { 38 | var diffVertical = Math.abs(start[0] - end[0]); 39 | // var diffHorizontal = Math.abs(start[1] - end[1]); 40 | 41 | if (diffVertical) { 42 | return start[0] - end[0] > 0 ? 'north' : 'south'; 43 | } 44 | return start[1] - end[1] > 0 ? 'west' : 'east'; 45 | }; 46 | 47 | var isVisible = (originalPosition = [], finalPosition = [], direction = []) => { 48 | switch (direction) { 49 | case DIRECTIONS[0]: 50 | return originalPosition[1] === finalPosition[1] && originalPosition[0] > finalPosition[0]; 51 | case DIRECTIONS[1]: 52 | return originalPosition[0] === finalPosition[0] && originalPosition[1] < finalPosition[1]; 53 | case DIRECTIONS[2]: 54 | return originalPosition[1] === finalPosition[1] && originalPosition[0] < finalPosition[0]; 55 | case DIRECTIONS[3]: 56 | return originalPosition[0] === finalPosition[0] && originalPosition[1] > finalPosition[1]; 57 | default: 58 | break; 59 | } 60 | }; 61 | 62 | var canKill = (currentPlayerState = {}, enemiesStates = []) => { 63 | return enemiesStates.some(enemyObject => { 64 | return enemyObject.isAlive && 65 | isVisible(currentPlayerState.position, enemyObject.position, currentPlayerState.direction); 66 | }); 67 | }; 68 | 69 | module.exports = { 70 | randomMove, 71 | getDirection, 72 | isVisible, 73 | canKill, 74 | safeRandomMove, 75 | fastGetDirection, 76 | turn, 77 | getDistance 78 | }; 79 | -------------------------------------------------------------------------------- /src/mixins/deepSetState.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = { 4 | deepSetState(value, callback) { 5 | this.state = _.merge(this.state, value, value); 6 | this.forceUpdate(); 7 | if (callback) callback(this.state); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/players/codingpains.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | var utils = require('./../lib/utils'); 4 | var logic = require('./../lib/codingpains-logic'); 5 | 6 | var codingpains = { 7 | info: { 8 | name: 'HGE-7', 9 | style: 8 10 | }, 11 | 12 | ai: function(player, enemies, map) { 13 | var armedEnemies = _.filter(enemies, (enemy) => enemy.ammo > 0); 14 | if (logic.inDanger(player, armedEnemies)) return codingpains._eluder(player, armedEnemies, map); 15 | if (player.ammo) return codingpains._hunter(player, enemies, map); 16 | return codingpains._gatherer(player, enemies, map); 17 | }, 18 | 19 | _gatherer: function(player, enemies, map) { 20 | var ammoMove; 21 | var safestMove; 22 | var centerMove; 23 | codingpains.info.mode = 'g'; 24 | if (!map.ammoPosition.length) { 25 | centerMove = logic.goToCenter(player, map); 26 | if (logic.isMovementSafe(centerMove, player, enemies, map)) return centerMove; 27 | } else { 28 | ammoMove = logic.shouldMoveForAmmo(player, enemies, map); 29 | if (ammoMove && logic.isMovementSafe(ammoMove, player, enemies, map)) return ammoMove; 30 | 31 | centerMove = logic.goToCenter(player, map); 32 | if (logic.isMovementSafe(centerMove, player, enemies, map)) return centerMove; 33 | } 34 | 35 | safestMove = logic.getSafestMove(player, enemies, map); 36 | if (safestMove && logic.isMovementSafe(safestMove, player, enemies, map)) return safestMove; 37 | 38 | return utils.safeRandomMove(); 39 | }, 40 | 41 | _hunter: function(player, enemies, map) { 42 | var turnMove; 43 | var ammoMove; 44 | var chaseMove; 45 | var safestMove; 46 | 47 | codingpains.info.mode = 'h'; 48 | 49 | enemies = logic.getDangerousEnemies(enemies); 50 | 51 | if (utils.canKill(player, enemies)) return 'shoot'; 52 | 53 | turnMove = logic.turnToAmbush(player, enemies); 54 | if (turnMove) return turnMove; 55 | 56 | turnMove = logic.turnToKill(player, enemies); 57 | if (turnMove && logic.isMovementSafe(turnMove, player, enemies, map)) return turnMove; 58 | 59 | chaseMove = logic.chaseEnemy(player, enemies, map); 60 | if (chaseMove && logic.isMovementSafe(chaseMove, player, enemies, map)) return chaseMove; 61 | 62 | safestMove = logic.getSafestMove(player, enemies, map); 63 | if (safestMove) return safestMove; 64 | 65 | return false; 66 | }, 67 | 68 | _eluder: function(player, enemies, map) { 69 | var killers = logic.getImmediateThreats(player, enemies); 70 | var closeThreats = {y: [], x:[]}; 71 | codingpains.info.mode = 'e'; 72 | if (killers.length) { 73 | if (logic.canKillAll(player, killers)) { 74 | return 'shoot'; 75 | } else if (logic.isMovementSafe('move', player, killers, map)) { 76 | return 'move'; 77 | } else { 78 | // Here I would activate a shield!, for now just die :( 79 | return false; 80 | } 81 | } 82 | 83 | if (player.ammo) return this._hunter(player, enemies, map); 84 | _.forEach(enemies, function(e) { 85 | if (logic.sameY(player.position, e.position)) { 86 | closeThreats.y.push(e); 87 | } 88 | if (logic.sameX(player.position, e.position)) { 89 | closeThreats.x.push(e); 90 | } 91 | }); 92 | 93 | if (closeThreats.x.length) { 94 | if (player.ammo && utils.canKill(player, closeThreats.x)) return 'shoot'; 95 | if (logic.isHorizontal(player.direction) && logic.canMoveTowards(player.direction, player, map)) { 96 | return 'move'; 97 | } else { 98 | return logic.opositeDirection(closeThreats.x[0].direction); 99 | } 100 | } else { 101 | if (player.ammo && utils.canKill(player, closeThreats.y)) return 'shoot'; 102 | if (logic.isVertical(player.direction) && logic.canMoveTowards(player.direction, player, map)) { 103 | return 'move'; 104 | } else { 105 | return logic.opositeDirection(closeThreats.y[0].direction); 106 | } 107 | } 108 | 109 | } 110 | }; 111 | 112 | module.exports = codingpains; 113 | -------------------------------------------------------------------------------- /src/players/elperron.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/clashjs/e0d34f5f25061153d33f45a55208b9fd2ae1f6e9/src/players/elperron.js -------------------------------------------------------------------------------- /src/players/ericku.js: -------------------------------------------------------------------------------- 1 | var utils = require("../lib/utils.js"); 2 | var DIRECTIONS = ["north", "east", "south", "west"]; 3 | 4 | var canDie = function (player, enemies) { 5 | return ( 6 | enemies 7 | .filter((enemy) => enemy.isAlive) 8 | .map((enemy) => utils.canKill(enemy, [player], enemy.direction)) 9 | .filter((result) => result === true).length > 0 10 | ); 11 | }; 12 | 13 | var getClosestAmmo = function (player, map) { 14 | if (!map.ammoPosition.length) { 15 | return; 16 | } 17 | var playerPos = player.position; 18 | var closestAmmo = map.ammoPosition[0]; 19 | 20 | map.ammoPosition.forEach(function (pos) { 21 | var isCloser = utils.getDistance(playerPos, pos) < utils.getDistance(playerPos, closestAmmo); 22 | if (isCloser) { 23 | closestAmmo = pos; 24 | } 25 | }); 26 | 27 | return closestAmmo; 28 | }; 29 | 30 | var shouldMoveForAmmo = function (player, map) { 31 | var closest = getClosestAmmo(player, map); 32 | var direction; 33 | 34 | if (!closest) { 35 | return; 36 | } 37 | 38 | direction = utils.fastGetDirection(player.position, closest); 39 | 40 | if (direction !== player.direction) { 41 | return direction; 42 | } 43 | 44 | return "move"; 45 | }; 46 | 47 | var isMovementSafe = function (action, player, enemies, map) { 48 | var futureState = JSON.parse(JSON.stringify(player)); 49 | 50 | if (action === "move") { 51 | switch (player.direction) { 52 | case DIRECTIONS[0]: 53 | if (futureState.position[0] > 0) { 54 | futureState.position[0]--; 55 | } 56 | break; 57 | case DIRECTIONS[1]: 58 | if (futureState.position[1] < map.gridSize[1]) { 59 | futureState.position[1]++; 60 | } 61 | break; 62 | case DIRECTIONS[2]: 63 | if (futureState.position[0] < map.gridSize[0]) futureState.position[0]++; 64 | break; 65 | case DIRECTIONS[3]: 66 | if (futureState[1] > 0) futureState.position[1]--; 67 | break; 68 | default: 69 | break; 70 | } 71 | } 72 | 73 | if (canDie(futureState, enemies)) { 74 | return false; 75 | } 76 | return true; 77 | }; 78 | 79 | var getSafestMove = function (player, enemies, map) { 80 | var safest; 81 | var isSafeHere = isMovementSafe("north", player, enemies, map); 82 | var isSafeToMove = isMovementSafe("move", player, enemies, map); 83 | 84 | if (isSafeHere) { 85 | if (player.ammo) { 86 | return turnToKill(player, enemies) || chaseEnemy(player, enemies); 87 | } 88 | } 89 | if (isSafeToMove) { 90 | return "move"; 91 | } 92 | 93 | return; 94 | }; 95 | 96 | var getClosestEnemy = function (player, enemies) { 97 | var clonedStates = enemies.slice(0, enemies.length); 98 | var closest; 99 | 100 | clonedStates = clonedStates.filter(function (enemy) { 101 | return enemy.isAlive; 102 | }); 103 | 104 | closest = clonedStates[0]; 105 | 106 | clonedStates.forEach(function (enemy) { 107 | if (utils.getDistance(player, enemy) < utils.getDistance(player, closest)) { 108 | closest = enemy; 109 | } 110 | }); 111 | 112 | return closest; 113 | }; 114 | 115 | var chaseEnemy = function (player, enemies) { 116 | var closestEnemy = getClosestEnemy(player, enemies); 117 | var direction = utils.fastGetDirection(player.position, closestEnemy.position); 118 | 119 | if (direction !== player.direction) { 120 | return direction; 121 | } 122 | 123 | return "move"; 124 | }; 125 | 126 | var turnToKill = function (player, enemies) { 127 | var turn = player.direction === "north" ? "south" : "north"; 128 | var mockState = JSON.parse(JSON.stringify(player)); 129 | 130 | enemies = enemies.filter(function (enemy) { 131 | return enemy.isAlive; 132 | }); 133 | 134 | DIRECTIONS.forEach(function (direction) { 135 | mockState.direction = direction; 136 | 137 | if (utils.canKill(mockState, enemies)) { 138 | turn = direction; 139 | } 140 | }); 141 | 142 | return turn; 143 | }; 144 | 145 | var hunter = function (player, enemies, map) { 146 | var turnMove; 147 | var ammoMove; 148 | var safestMove; 149 | 150 | if (utils.canKill(player, enemies)) return "shoot"; 151 | 152 | turnMove = turnToKill(player, enemies); 153 | if (turnMove && isMovementSafe(turnMove, player, enemies, map)) return turnMove; 154 | 155 | safestMove = getSafestMove(player, enemies, map); 156 | if (safestMove) return safestMove; 157 | 158 | return "stay"; 159 | }; 160 | 161 | var gatherer = function (player, enemies, map) { 162 | var ammoMove; 163 | var safestMove; 164 | 165 | ammoMove = shouldMoveForAmmo(player, map); 166 | if (ammoMove && isMovementSafe(ammoMove, player, enemies, map)) return ammoMove; 167 | 168 | safestMove = getSafestMove(player, enemies, map); 169 | if (safestMove) return safestMove; 170 | 171 | return utils.safeRandomMove(); 172 | }; 173 | 174 | var kills = 0; 175 | 176 | var ericku_ = { 177 | info: { 178 | name: "Gurren", 179 | style: { 180 | type: "figma.svg", 181 | hue: -90 182 | }, 183 | }, 184 | ai: function (player, enemies, map) { 185 | if (player.ammo) return hunter(player, enemies, map); 186 | if (Math.random() > 0.8) return "move"; 187 | return gatherer(player, enemies, map); 188 | }, 189 | }; 190 | 191 | module.exports = ericku_; 192 | -------------------------------------------------------------------------------- /src/players/horror.js: -------------------------------------------------------------------------------- 1 | var utils = require('../lib/utils.js'); 2 | 3 | var MUSOLINI = { 4 | info: { 5 | name: 'Horror', 6 | style: 10 7 | }, 8 | ai: (playerState, enemiesStates, gameEnvironment) => { 9 | var directionToAmmo; 10 | 11 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) { 12 | return 'shoot'; 13 | } 14 | if (gameEnvironment.ammoPosition.length) { 15 | directionToAmmo = utils.fastGetDirection(playerState.position, gameEnvironment.ammoPosition[0]); 16 | 17 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 18 | return 'move'; 19 | } 20 | return utils.safeRandomMove(); 21 | } 22 | }; 23 | 24 | module.exports = MUSOLINI; 25 | -------------------------------------------------------------------------------- /src/players/javierbyte.js: -------------------------------------------------------------------------------- 1 | const utils = require("../lib/utils.js"); 2 | 3 | const javierbyte = { 4 | info: { 5 | name: "javierbyte", 6 | style: 2, 7 | }, 8 | ai: (playerState, enemiesState, gameEnvironment) => { 9 | const directionToAmmo; 10 | 11 | if (Math.random() > 0.9) return "shoot"; 12 | 13 | if (gameEnvironment.ammoPosition.length) { 14 | directionToAmmo = utils.getDirection(playerState.position, gameEnvironment.ammoPosition[0]); 15 | 16 | if (directionToAmmo !== playerState.direction) { 17 | return directionToAmmo; 18 | } 19 | return "move"; 20 | } 21 | 22 | return utils.randomMove(); 23 | }, 24 | }; 25 | 26 | module.exports = javierbyte; 27 | -------------------------------------------------------------------------------- /src/players/manuelmhtr.js: -------------------------------------------------------------------------------- 1 | var utils = require('../lib/utils.js'); 2 | var ORIENTATION = {north: 'vertical', east: 'horizontal', south: 'vertical', west: 'horizontal'}; 3 | 4 | var manuelmhtr = { 5 | info: { 6 | name: 'Manuelmhtr', 7 | style: 3 8 | }, 9 | ai: function(playerState, enemiesStates, gameEnvironment) { 10 | var response; 11 | var enemies = []; 12 | var params = { 13 | vulnerabilityLevel: null, 14 | canKill: null, 15 | nearestAmmo: null, 16 | nearestEnemy: null, 17 | canMove: null 18 | }; 19 | 20 | // Parse enemies 21 | enemiesStates.forEach(function(enemy) { 22 | if (enemy.isAlive === true) { 23 | enemy.nearestAmmoDistance = calculateDistanceToNearestAmmo(enemy.position); 24 | enemies.push(enemy); 25 | } 26 | }); 27 | 28 | // Process parameters 29 | params.vulnerabilityLevel = calculateVulnerabilityLevel(playerState.position); 30 | params.canKill = playerState.ammo > 0 && utils.canKill(playerState, enemiesStates); 31 | params.nearestAmmo = getNearestAmmo(playerState.position); 32 | params.nearestEnemy = getNearestEnemy(playerState.position); 33 | params.canMove = canMove(playerState.position, playerState.direction); 34 | 35 | function calculateVulnerabilityLevel(targetPosition) { 36 | var vulnerabilityLevel = 0.0; 37 | enemies.forEach(function(enemy) { 38 | if (utils.isVisible(enemy.position, targetPosition, enemy.direction) && enemy.ammo > 0) { 39 | vulnerabilityLevel = Math.max(vulnerabilityLevel, 1.0); 40 | } else if (isAligned(enemy.position, targetPosition) && (enemy.ammo > 0 || enemy.nearestAmmoDistance === 1)) { 41 | vulnerabilityLevel = Math.max(vulnerabilityLevel, 0.5); 42 | } 43 | }); 44 | 45 | if (vulnerabilityLevel === 0.0) { 46 | // Check if other enemies are near 47 | var northEast = [targetPosition[0] + 1, targetPosition[1] + 1]; 48 | var southWest = [targetPosition[0] - 1, targetPosition[1] - 1]; 49 | enemies.forEach(function(enemy) { 50 | if (enemy.ammo > 0) { 51 | if (isAligned(enemy.position, northEast) || isAligned(enemy.position, southWest)) { 52 | vulnerabilityLevel = Math.max(vulnerabilityLevel, 0.25); 53 | } 54 | } 55 | }); 56 | } 57 | 58 | return vulnerabilityLevel; 59 | } 60 | 61 | function isAligned(originalPosition, finalPosition) { 62 | var aligned = false; 63 | aligned = aligned || originalPosition[1] === finalPosition[1] && originalPosition[0] > finalPosition[0]; 64 | aligned = aligned || originalPosition[0] === finalPosition[0] && originalPosition[1] < finalPosition[1]; 65 | aligned = aligned || originalPosition[1] === finalPosition[1] && originalPosition[0] < finalPosition[0]; 66 | aligned = aligned || originalPosition[0] === finalPosition[0] && originalPosition[1] > finalPosition[1]; 67 | return aligned; 68 | } 69 | 70 | function getNearestAmmo(position) { 71 | var nearestAmmo = null; 72 | var nearestDistance = null; 73 | gameEnvironment.ammoPosition.forEach(function(ammo) { 74 | var distance = utils.getDistance(position, ammo); 75 | if (nearestDistance === null || distance < nearestDistance) { 76 | nearestDistance = distance; 77 | nearestAmmo = { 78 | position: ammo, 79 | distance: nearestDistance 80 | }; 81 | } 82 | }); 83 | return nearestAmmo; 84 | } 85 | 86 | function calculateDistanceToNearestAmmo(position) { 87 | var nearestDistance = null; 88 | gameEnvironment.ammoPosition.forEach(function(ammo) { 89 | var distance = utils.getDistance(position, ammo); 90 | if (nearestDistance === null || distance < nearestDistance) { 91 | nearestDistance = distance; 92 | } 93 | }); 94 | return nearestDistance; 95 | } 96 | 97 | function getNearestEnemy(position) { 98 | var nearestEnemy = null; 99 | var nearestDistance = null; 100 | enemies.forEach(function(enemy) { 101 | var distance = calculateEnemyDistance(position, enemy); 102 | 103 | if (nearestDistance === null || distance < nearestDistance) { 104 | nearestDistance = distance; 105 | nearestEnemy = { 106 | instance: enemy, 107 | distance: nearestDistance 108 | }; 109 | } 110 | }); 111 | return nearestEnemy; 112 | } 113 | 114 | function calculateEnemyDistance(position, enemy) { 115 | var diffVertical = Math.abs(position[0] - enemy.position[0]); 116 | var diffHorizontal = Math.abs(position[1] - enemy.position[1]); 117 | return Math.min(diffHorizontal, diffVertical); 118 | } 119 | 120 | function defend() { 121 | if (params.vulnerabilityLevel === 1.0) { 122 | var canRun = params.canMove; 123 | var attacker; 124 | 125 | // Find attacker 126 | enemies.forEach(function(enemy) { 127 | if (enemy.ammo > 0 && utils.isVisible(enemy.position, playerState.position, enemy.direction)) { 128 | attacker = enemy; 129 | } 130 | }); 131 | 132 | // Check if can run 133 | if (attacker && ORIENTATION[attacker.direction] === ORIENTATION[playerState.direction]) { 134 | canRun = false; 135 | } 136 | 137 | if (canRun) { 138 | return 'move'; 139 | } else if (attacker && playerState.ammo > 0) { 140 | // Counterattack 141 | return huntEnemy(attacker); 142 | } else { 143 | return getSafestDirection(); 144 | } 145 | } else { 146 | return getSafestDirection(); 147 | } 148 | } 149 | 150 | function getSafestDirection() { 151 | var safestDirection = playerState.direction; 152 | var lowestVulnerability = params.vulnerabilityLevel; 153 | var maxDistanceLeft = 0; 154 | 155 | var options = [{ 156 | direction: 'north', 157 | position: [playerState.position[0] - 1, playerState.position[1]] 158 | }, { 159 | direction: 'east', 160 | position: [playerState.position[0], playerState.position[1] + 1] 161 | }, { 162 | direction: 'south', 163 | position: [playerState.position[0] + 1, playerState.position[1]] 164 | }, { 165 | direction: 'west', 166 | position: [playerState.position[0], playerState.position[1] - 1] 167 | } 168 | ]; 169 | 170 | // Process options 171 | options.forEach(function(option) { 172 | option.vulnerability = calculateVulnerabilityLevel(option.position); 173 | option.distanceLeft = calculateDistanceLeft(option.direction); 174 | option.canMove = canMove(playerState.position, option.direction); 175 | var isBetterOption = option.vulnerability < lowestVulnerability || (option.vulnerability === lowestVulnerability && option.distanceLeft > maxDistanceLeft); 176 | if (option.canMove && isBetterOption) { 177 | safestDirection = option.direction; 178 | lowestVulnerability = option.vulnerability; 179 | maxDistanceLeft = option.distanceLeft; 180 | } 181 | }); 182 | 183 | if (safestDirection === playerState.direction) { 184 | return moveSafely(); 185 | } else { 186 | return safestDirection; 187 | } 188 | 189 | function calculateDistanceLeft(direction) { 190 | if (direction === 'north') { 191 | return playerState.position[0]; 192 | } else if (direction === 'east') { 193 | return gameEnvironment.gridSize - playerState.position[1]; 194 | } else if (direction === 'south') { 195 | return gameEnvironment.gridSize - playerState.position[0]; 196 | } else if (direction === 'west') { 197 | return playerState.position[1]; 198 | } 199 | } 200 | } 201 | 202 | function canMove(position, direction) { 203 | if (direction === 'north') { 204 | return position[0] > 0; 205 | } else if (direction === 'east') { 206 | return position[1] < gameEnvironment.gridSize; 207 | } else if (direction === 'south') { 208 | return position[0] < gameEnvironment.gridSize; 209 | } else if (direction === 'west') { 210 | return position[1] > 0; 211 | } 212 | } 213 | 214 | function attack() { 215 | return 'shoot'; 216 | } 217 | 218 | function moveWisely() { 219 | if (playerState.ammo === 0 && params.nearestAmmo) { 220 | return getAmmo(params.nearestAmmo.position); 221 | } else if (playerState.ammo > 0 && params.nearestEnemy) { 222 | if (params.nearestAmmo && params.nearestAmmo.distance < params.nearestEnemy.distance) { 223 | return getAmmo(params.nearestAmmo.position); 224 | } else { 225 | return huntEnemy(params.nearestEnemy.instance); 226 | } 227 | } else { 228 | return getSafestDirection(); 229 | } 230 | } 231 | 232 | function huntEnemy(enemy) { 233 | if (utils.isVisible(playerState.position, enemy.position, playerState.direction)) { 234 | return attack(); 235 | } else { 236 | var attackDirection = getAttackDirection(); 237 | var enemyDistance = calculateEnemyDistance(playerState.position, enemy); 238 | 239 | if (attackDirection === playerState.direction && (enemyDistance > 1 || enemy.ammo === 0)) { 240 | return 'move'; 241 | } else { 242 | return attackDirection; 243 | } 244 | } 245 | 246 | function getAttackDirection() { 247 | var isEnemyAligned = isAligned(playerState.position, enemy.position); 248 | var enemyOrientation = ORIENTATION[enemy.direction]; 249 | 250 | if (isEnemyAligned) { 251 | return utils.fastGetDirection(playerState.position, enemy.position); 252 | } else { 253 | if (enemyOrientation === 'vertical') { 254 | return enemy.position[1] > playerState.position[1] ? 'east' : 'west'; 255 | } else { 256 | return enemy.position[0] > playerState.position[0] ? 'south' : 'north'; 257 | } 258 | } 259 | } 260 | } 261 | 262 | function moveSafely() { 263 | var destination; 264 | var vulnerabilityOnMove; 265 | 266 | if (playerState.direction === 'north') { 267 | destination = [playerState.position[0] - 1, playerState.position[1]]; 268 | } else if (playerState.direction === 'east') { 269 | destination = [playerState.position[0], playerState.position[1] + 1]; 270 | } else if (playerState.direction === 'south') { 271 | destination = [playerState.position[0] + 1, playerState.position[1]]; 272 | } else if (playerState.direction === 'west') { 273 | destination = [playerState.position[0], playerState.position[1] - 1]; 274 | } 275 | 276 | vulnerabilityOnMove = calculateVulnerabilityLevel(destination); 277 | 278 | if (vulnerabilityOnMove === 1.0) { 279 | return null; 280 | } else { 281 | return 'move'; 282 | } 283 | } 284 | 285 | function getAmmo(ammoPosition) { 286 | // Get direction to turn 287 | var goToDirection = utils.fastGetDirection(playerState.position, ammoPosition); 288 | 289 | // If same direction, move 290 | if (goToDirection === playerState.direction) { 291 | return moveSafely(); 292 | } else { 293 | return goToDirection; 294 | } 295 | } 296 | 297 | // Decide movement 298 | if (params.vulnerabilityLevel === 1.0 || params.vulnerabilityLevel >= 0.5 && params.canKill !== true) { 299 | response = defend(); 300 | } else if (params.canKill === true) { 301 | response = attack(); 302 | } else { 303 | response = moveWisely(); 304 | } 305 | return response; 306 | 307 | } 308 | }; 309 | 310 | module.exports = manuelmhtr; 311 | -------------------------------------------------------------------------------- /src/players/margeux.js: -------------------------------------------------------------------------------- 1 | var utils = require('../lib/utils.js'); 2 | var noAmmo = true; 3 | var margeux = { 4 | info: { 5 | name: 'margeux', 6 | style: 5 7 | }, 8 | ai: (playerState, enemiesStates, gameEnvironment) => { 9 | var directionToAmmo; 10 | var directionToEnemy; 11 | if (playerState.ammo >0){ 12 | noAmmo = false; 13 | } 14 | if (playerState.ammo == 0){ 15 | noAmmo= true; 16 | } 17 | 18 | if (gameEnvironment.ammoPosition.length && noAmmo) { 19 | directionToAmmo = utils.fastGetDirection(playerState.position, gameEnvironment.ammoPosition[0]); 20 | 21 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 22 | return 'move'; 23 | } 24 | 25 | //LOOK for an enemy 26 | if(!noAmmo){ 27 | directionToEnemy = utils.fastGetDirection(playerState.position, enemiesStates[0].position); 28 | if (directionToEnemy !== playerState.direction){ 29 | var directionToMargeus = utils.fastGetDirection(enemiesStates[0].position, playerState.position); 30 | if (directionToMargeus !== enemiesStates[0].position){ 31 | return directionToEnemy; 32 | }else{ 33 | return utils.safeRandomMove(); 34 | } 35 | } 36 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) { 37 | return 'shoot'; 38 | }else{ 39 | return utils.safeRandomMove(); 40 | } 41 | } 42 | 43 | 44 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) { 45 | return 'shoot'; 46 | } 47 | if (gameEnvironment.ammoPosition.length ) { 48 | directionToAmmo = utils.fastGetDirection(playerState.position, gameEnvironment.ammoPosition[0]); 49 | 50 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 51 | return 'move'; 52 | } 53 | return utils.safeRandomMove(); 54 | } 55 | }; 56 | 57 | module.exports = margeux; 58 | -------------------------------------------------------------------------------- /src/players/siegfried.js: -------------------------------------------------------------------------------- 1 | var utils = require('../lib/utils.js'); 2 | 3 | var siegfried = { 4 | info: { 5 | name: 'Siegfried', 6 | style: 4 7 | }, 8 | ai: (playerState, enemiesStates, gameEnvironment) => { 9 | var directionToAmmo; 10 | 11 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) { 12 | return 'shoot'; 13 | } 14 | 15 | if (gameEnvironment.ammoPosition.length) { 16 | directionToAmmo = utils.getDirection(playerState.position, gameEnvironment.ammoPosition[0]); 17 | 18 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 19 | return 'move'; 20 | } 21 | return utils.randomMove(); 22 | } 23 | }; 24 | 25 | module.exports = siegfried; 26 | -------------------------------------------------------------------------------- /src/players/xmontoya.js: -------------------------------------------------------------------------------- 1 | var utils = require('../lib/utils.js'); 2 | 3 | var xmontoya = { 4 | info: { 5 | name: 'Xmontoya89', 6 | style: 1 7 | }, 8 | ai: (playerState, enemiesStates, gameEnvironment) => { 9 | var directionToAmmo; 10 | 11 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) { 12 | return 'shoot'; 13 | } 14 | 15 | if (gameEnvironment.ammoPosition.length) { 16 | directionToAmmo = utils.fastGetDirection(playerState.position, gameEnvironment.ammoPosition[0]); 17 | 18 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 19 | return utils.safeRandomMove(); 20 | } 21 | 22 | return 'move'; 23 | } 24 | }; 25 | 26 | module.exports = xmontoya; 27 | -------------------------------------------------------------------------------- /src/players/yuno.js: -------------------------------------------------------------------------------- 1 | var utils = require("../lib/utils.js"); 2 | var DIRECTIONS = ["north", "east", "south", "west"]; 3 | 4 | var getShortestDirection = (start, endArray) => { 5 | var reducedArray = endArray.reduce( 6 | (reduced, currentPosition) => { 7 | if (reduced[0] === -1 || utils.getDistance(start, currentPosition) < reduced[0]) { 8 | reduced[0] = utils.getDistance(start, currentPosition); 9 | reduced[1] = currentPosition; 10 | } 11 | 12 | return reduced; 13 | }, 14 | [-1, 0] 15 | ); 16 | 17 | return utils.fastGetDirection(start, reducedArray[1]); 18 | }; 19 | 20 | var turnToKill = (originalPosition, positionArray) => { 21 | return DIRECTIONS.reduce((result, currentDirection) => { 22 | if (result) return result; 23 | return positionArray.reduce((resultPositions, currentEnemyPosition) => { 24 | if (resultPositions) return resultPositions; 25 | return utils.isVisible(originalPosition, currentEnemyPosition, currentDirection) ? currentDirection : null; 26 | }, null); 27 | }, null); 28 | }; 29 | 30 | var Yuno = { 31 | info: { 32 | name: "Yuno", 33 | style: 9 34 | }, 35 | ai: (playerState, enemiesStates, gameEnvironment) => { 36 | var directionToAmmo; 37 | var directionToPlayer; 38 | 39 | if (utils.canKill(playerState, enemiesStates) && playerState.ammo) return "shoot"; 40 | 41 | if (playerState.ammo) { 42 | directionToPlayer = turnToKill(playerState.position, enemiesStates.map(el => el.position)); 43 | if (directionToPlayer) { 44 | return directionToPlayer; 45 | } 46 | 47 | directionToPlayer = getShortestDirection(playerState.position, enemiesStates.map(el => el.position)); 48 | if (directionToPlayer !== playerState.direction) return directionToPlayer; 49 | return "move"; 50 | } 51 | 52 | if (gameEnvironment.ammoPosition.length) { 53 | directionToAmmo = getShortestDirection(playerState.position, gameEnvironment.ammoPosition); 54 | 55 | if (directionToAmmo !== playerState.direction) return directionToAmmo; 56 | return "move"; 57 | } 58 | 59 | // if (Math.random() > 0.9) return "move"; 60 | 61 | return utils.safeRandomMove(); 62 | } 63 | }; 64 | 65 | module.exports = Yuno; 66 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, 5 | *:before, 6 | *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | * { 11 | position: relative; 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | html { 17 | font-size: 16px; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | html, 26 | body { 27 | width: 100vw; 28 | height: 100vh; 29 | overflow: hidden; 30 | } 31 | 32 | body { 33 | background: url("../public/static/bg-m.jpg") no-repeat center #2c3e50; 34 | background-size: cover; 35 | color: #fff; 36 | } 37 | 38 | body, 39 | table { 40 | font: 1rem/1.618 din-2014, sans-serif; 41 | font-weight: 200; 42 | } 43 | 44 | b { 45 | font-weight: 500; 46 | } 47 | 48 | .instructions { 49 | position: fixed; 50 | font-size: 1rem; 51 | bottom: 0; 52 | left: 0; 53 | padding: 1rem; 54 | color: #eee; 55 | z-index: 10000; 56 | } 57 | .instructions a { 58 | color: #fff; 59 | } 60 | 61 | .clash { 62 | top: calc(50vh - 50vmin); 63 | width: 100vmin; 64 | height: 100vmin; 65 | margin: 0 auto; 66 | background: rgba(255, 255, 255, 0.03); 67 | } 68 | 69 | @media screen and (max-width: 400px) and (min-height: 400px) { 70 | .clash { 71 | top: 30vmin; 72 | } 73 | } 74 | 75 | .clash-tile { 76 | display: inline-block; 77 | float: left; 78 | } 79 | .clash-tile:nth-child(odd) { 80 | background: rgba(255, 255, 255, 0.03); 81 | } 82 | 83 | .clash-ammo { 84 | position: absolute; 85 | background: url("../public/static/star.png") no-repeat center / 80%; 86 | } 87 | .clash-ammo:hover { 88 | transform: rotate(360deg); 89 | } 90 | 91 | .clash-player { 92 | position: absolute; 93 | top: 0; 94 | left: 0; 95 | background-size: contain; 96 | background-position: center; 97 | background-repeat: no-repeat; 98 | } 99 | .clash-player-container { 100 | position: absolute; 101 | top: 0; 102 | left: 0; 103 | } 104 | .clash-player-name { 105 | top: -2rem; 106 | width: 8rem; 107 | left: 50%; 108 | margin-left: -4rem; 109 | text-transform: uppercase; 110 | text-align: center; 111 | font-size: 0.75rem; 112 | } 113 | 114 | @media screen and (max-width: 640px) { 115 | .clash-player-name { 116 | font-size: 0.55rem; 117 | } 118 | } 119 | 120 | .clash-shoot { 121 | position: absolute; 122 | width: 100em; 123 | transform-origin: center left; 124 | } 125 | 126 | .clash-layer { 127 | position: absolute; 128 | top: 0; 129 | left: 0; 130 | width: 100vmin; 131 | height: 100vmin; 132 | } 133 | 134 | .player-dead-emoji { 135 | font-size: 1rem; 136 | line-height: 1rem; 137 | opacity: 0; 138 | transform: scale(0); 139 | transition: opacity 0.3s, transform 0.3s; 140 | } 141 | 142 | .player-stats.-dead td { 143 | color: #55555; 144 | } 145 | 146 | td.player-name { 147 | text-align: left; 148 | } 149 | 150 | .player-stats.-dead .player-name { 151 | text-decoration: line-through; 152 | } 153 | .player-stats.-dead .player-dead-emoji { 154 | transform: scale(1); 155 | opacity: 1; 156 | } 157 | 158 | .notifications, 159 | .stats, 160 | .debugger { 161 | background: rgba(255, 255, 255, 0.05); 162 | /*box-shadow: rgba(0, 0, 0, 0.05) 0 1px 0, rgba(0, 0, 0, 0.1) 0 8px 16px;*/ 163 | color: #fff; 164 | backdrop-filter: blur(16px); 165 | -webkit-backdrop-filter: blur(16px); 166 | width: 280px; 167 | margin: 16px; 168 | border-radius: 10px; 169 | overflow: hidden; 170 | } 171 | 172 | .notifications-element { 173 | /*padding: 0.5rem 1rem;*/ 174 | padding: 0; 175 | margin: 0; 176 | max-height: 4rem; 177 | opacity: 1; 178 | transition: max-height 0.3s, opacity 0.3s; 179 | } 180 | 181 | .notifications-element.-hide { 182 | opacity: 0; 183 | overflow: hidden; 184 | max-height: 0; 185 | /*height: 2rem;*/ 186 | } 187 | 188 | .notifications-element-text { 189 | padding: 0.25rem 1rem; 190 | } 191 | 192 | /*.notifications-element:not(:first-of-type) { 193 | border-top: 1px solid rgba(255,255,255,0.1); 194 | }*/ 195 | 196 | .notifications-element.-alt { 197 | background-color: rgba(0, 0, 0, 0.2); 198 | } 199 | 200 | @media screen and (max-width: 640px) { 201 | .notifications { 202 | display: none; 203 | } 204 | } 205 | 206 | .stats { 207 | position: fixed; 208 | top: 0; 209 | left: 0; 210 | padding: 0.5rem 0; 211 | z-index: 10; 212 | } 213 | 214 | .stats-title { 215 | text-align: center; 216 | } 217 | 218 | .stats table { 219 | width: 100%; 220 | } 221 | 222 | .stats td, 223 | .stats th { 224 | padding: 0.05rem 1rem; 225 | text-align: left; 226 | } 227 | .stats td.stats-results { 228 | text-align: right; 229 | } 230 | 231 | .notifications { 232 | position: fixed; 233 | bottom: 0%; 234 | right: 0%; 235 | z-index: 9000; 236 | } 237 | 238 | .animation-shot { 239 | animation: shotKeyFrame 0.3s; 240 | } 241 | 242 | .debugger { 243 | padding: 1rem; 244 | font-size: 0.8rem; 245 | line-height: 0.8rem; 246 | position: fixed; 247 | top: 60px; 248 | left: 0; 249 | } 250 | 251 | @keyframes shotKeyFrame { 252 | 0% { 253 | background: #f00; 254 | margin-top: -0.05em; 255 | height: 0.1em; 256 | } 257 | 100% { 258 | background: #ff0; 259 | height: 0.7em; 260 | margin-top: -0.35em; 261 | } 262 | } 263 | 264 | .animation-glow { 265 | animation: glowKeyFrame 1s infinite alternate; 266 | } 267 | @keyframes glowKeyFrame { 268 | 0% { 269 | -webkit-filter: saturate(0); 270 | } 271 | 100% { 272 | -webkit-filter: saturate(1); 273 | } 274 | } 275 | 276 | .-clickable { 277 | cursor: pointer; 278 | -webkit-user-select: none; 279 | user-select: none; 280 | } 281 | --------------------------------------------------------------------------------