├── .gitignore ├── README.md ├── frontpage.png ├── goal.png ├── index.html ├── index.ts ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.sh 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bomb-guy 2 | 3 | In this kata your task is to refactor the code for a small game. When finished it should be easier to implement suggested extensions below. 4 | 5 | The code already abides by the most common principles "Don't Repeat Yourself", "Keep It Simple, Stupid", and there are only very few magic literals. There are no poorly structured nor deeply nested `if`s. 6 | 7 | This is *not* an easy exercise. 8 | 9 | # Inspiration for extensions 10 | 11 | 1. Make bombs round 12 | 2. Bomb range upgrade 13 | 3. Bombs trigger each other 14 | 4. Draw upgrades with images 15 | 5. Add lives, incl. extra life upgrade 16 | 6. Add eyes to the monster so you can see which way it is facing 17 | 7. Add more types of monsters 18 | 8. Add a slipery tile, when the player steps on it they goes as far as they can in that direction 19 | 9. Make the fire not take up a whole tile, but still connects to fire next to it 20 | 21 | # How to build it 22 | Assuming that you have the Typescript compiler installed: Open a terminal in this directory, then run `tsc`. There should now be a `index.js` file in this directory. 23 | 24 | # How to run it 25 | To run the game you need to first build it, see above. Then simply open `index.html` in a browser. Use the arrows to move the player. 26 | 27 | # Thank you! 28 | If you like this kata please consider giving the repo a star. You might also consider purchasing a copy of my book where I show a simple way to tackle code like this: [Five Lines of Code](https://www.manning.com/books/five-lines-of-code). 29 | 30 | [![Five Lines of Code](frontpage.png)](https://www.manning.com/books/five-lines-of-code) 31 | 32 | If you have feedback or comments on this repo don't hesitate to write me a message or send me a pull request. 33 | 34 | Thank you for checking it out. 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedrlambda/bomb-guy/198b59310c17cc80bde8462b557a0c86cf49af9c/frontpage.png -------------------------------------------------------------------------------- /goal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedrlambda/bomb-guy/198b59310c17cc80bde8462b557a0c86cf49af9c/goal.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bomb Guy 8 | 9 | 35 | 36 | 37 | 38 | 39 | 40 |

How to play

41 |

42 | The user controls the player square using the arrow keys, and place bombs using the space key. 43 |

44 | 45 | 55 | 56 |

57 | Bombs have a slight delay before they explode, the fire can blow up destructible walls. If the player touches the 58 | fire or the monster they lose. Initially the player can only place one bomb at a time, but as they collect power ups 59 | they can place more. 60 |

61 | 62 |

How to win

63 |

64 | The objective of the game is to clear the map, like this: 65 |

66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | const TILE_SIZE = 30; 2 | const FPS = 30; 3 | const SLEEP = 1000 / FPS; 4 | const TPS = 2; 5 | const DELAY = FPS / TPS; 6 | 7 | enum Tile { 8 | AIR, 9 | UNBREAKABLE, 10 | STONE, 11 | BOMB, 12 | BOMB_CLOSE, 13 | BOMB_REALLY_CLOSE, 14 | TMP_FIRE, 15 | FIRE, 16 | EXTRA_BOMB, 17 | MONSTER_UP, 18 | MONSTER_RIGHT, 19 | TMP_MONSTER_RIGHT, 20 | MONSTER_DOWN, 21 | TMP_MONSTER_DOWN, 22 | MONSTER_LEFT, 23 | } 24 | 25 | enum Input { 26 | UP, 27 | DOWN, 28 | LEFT, 29 | RIGHT, 30 | PLACE, 31 | } 32 | 33 | let playerx = 1; 34 | let playery = 1; 35 | let map: Tile[][] = [ 36 | [1, 1, 1, 1, 1, 1, 1, 1, 1], 37 | [1, 0, 0, 2, 2, 2, 2, 2, 1], 38 | [1, 0, 1, 2, 1, 2, 1, 2, 1], 39 | [1, 2, 2, 2, 2, 2, 2, 2, 1], 40 | [1, 2, 1, 2, 1, 2, 1, 2, 1], 41 | [1, 2, 2, 2, 2, 0, 0, 0, 1], 42 | [1, 2, 1, 2, 1, 0, 1, 0, 1], 43 | [1, 2, 2, 2, 2, 0, 0, 10, 1], 44 | [1, 1, 1, 1, 1, 1, 1, 1, 1], 45 | ]; 46 | 47 | let inputs: Input[] = []; 48 | 49 | let delay = 0; 50 | let bombs = 1; 51 | let gameOver = false; 52 | 53 | function explode(x: number, y: number, type: Tile) { 54 | if (map[y][x] === Tile.STONE) { 55 | if (Math.random() < 0.1) map[y][x] = Tile.EXTRA_BOMB; 56 | else map[y][x] = type; 57 | } else if (map[y][x] !== Tile.UNBREAKABLE) { 58 | if ( 59 | map[y][x] === Tile.BOMB || 60 | map[y][x] === Tile.BOMB_CLOSE || 61 | map[y][x] === Tile.BOMB_REALLY_CLOSE 62 | ) 63 | bombs++; 64 | map[y][x] = type; 65 | } 66 | } 67 | 68 | function move(x: number, y: number) { 69 | if ( 70 | map[playery + y][playerx + x] === Tile.AIR || 71 | map[playery + y][playerx + x] === Tile.FIRE 72 | ) { 73 | playery += y; 74 | playerx += x; 75 | } else if (map[playery + y][playerx + x] === Tile.EXTRA_BOMB) { 76 | playery += y; 77 | playerx += x; 78 | bombs++; 79 | map[playery][playerx] = Tile.AIR; 80 | } 81 | } 82 | 83 | function placeBomb() { 84 | if (bombs > 0) { 85 | map[playery][playerx] = Tile.BOMB; 86 | bombs--; 87 | } 88 | } 89 | 90 | function update() { 91 | while (!gameOver && inputs.length > 0) { 92 | let current = inputs.pop(); 93 | if (current === Input.LEFT) move(-1, 0); 94 | else if (current === Input.RIGHT) move(1, 0); 95 | else if (current === Input.UP) move(0, -1); 96 | else if (current === Input.DOWN) move(0, 1); 97 | else if (current === Input.PLACE) placeBomb(); 98 | } 99 | 100 | if ( 101 | map[playery][playerx] === Tile.FIRE || 102 | map[playery][playerx] === Tile.MONSTER_DOWN || 103 | map[playery][playerx] === Tile.MONSTER_UP || 104 | map[playery][playerx] === Tile.MONSTER_LEFT || 105 | map[playery][playerx] === Tile.MONSTER_RIGHT 106 | ) 107 | gameOver = true; 108 | 109 | if (--delay > 0) return; 110 | delay = DELAY; 111 | 112 | for (let y = 1; y < map.length; y++) { 113 | for (let x = 1; x < map[y].length; x++) { 114 | if (map[y][x] === Tile.BOMB) { 115 | map[y][x] = Tile.BOMB_CLOSE; 116 | } else if (map[y][x] === Tile.BOMB_CLOSE) { 117 | map[y][x] = Tile.BOMB_REALLY_CLOSE; 118 | } else if (map[y][x] === Tile.BOMB_REALLY_CLOSE) { 119 | explode(x + 0, y - 1, Tile.FIRE); 120 | explode(x + 0, y + 1, Tile.TMP_FIRE); 121 | explode(x - 1, y + 0, Tile.FIRE); 122 | explode(x + 1, y + 0, Tile.TMP_FIRE); 123 | map[y][x] = Tile.FIRE; 124 | bombs++; 125 | } else if (map[y][x] === Tile.TMP_FIRE) { 126 | map[y][x] = Tile.FIRE; 127 | } else if (map[y][x] === Tile.FIRE) { 128 | map[y][x] = Tile.AIR; 129 | } else if (map[y][x] === Tile.TMP_MONSTER_DOWN) { 130 | map[y][x] = Tile.MONSTER_DOWN; 131 | } else if (map[y][x] === Tile.TMP_MONSTER_RIGHT) { 132 | map[y][x] = Tile.MONSTER_RIGHT; 133 | } else if (map[y][x] === Tile.MONSTER_RIGHT) { 134 | if (map[y][x + 1] === Tile.AIR) { 135 | map[y][x] = Tile.AIR; 136 | map[y][x + 1] = Tile.TMP_MONSTER_RIGHT; 137 | } else { 138 | map[y][x] = Tile.MONSTER_DOWN; 139 | } 140 | } else if (map[y][x] === Tile.MONSTER_DOWN) { 141 | if (map[y + 1][x] === Tile.AIR) { 142 | map[y][x] = Tile.AIR; 143 | map[y + 1][x] = Tile.TMP_MONSTER_DOWN; 144 | } else { 145 | map[y][x] = Tile.MONSTER_LEFT; 146 | } 147 | } else if (map[y][x] === Tile.MONSTER_LEFT) { 148 | if (map[y][x - 1] === Tile.AIR) { 149 | map[y][x] = Tile.AIR; 150 | map[y][x - 1] = Tile.MONSTER_LEFT; 151 | } else { 152 | map[y][x] = Tile.MONSTER_UP; 153 | } 154 | } else if (map[y][x] === Tile.MONSTER_UP) { 155 | if (map[y - 1][x] === Tile.AIR) { 156 | map[y][x] = Tile.AIR; 157 | map[y - 1][x] = Tile.MONSTER_UP; 158 | } else { 159 | map[y][x] = Tile.MONSTER_RIGHT; 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | function draw() { 167 | let canvas = document.getElementById("GameCanvas"); 168 | let g = canvas.getContext("2d"); 169 | 170 | g.clearRect(0, 0, canvas.width, canvas.height); 171 | 172 | // Draw map 173 | for (let y = 0; y < map.length; y++) { 174 | for (let x = 0; x < map[y].length; x++) { 175 | if (map[y][x] === Tile.UNBREAKABLE) g.fillStyle = "#999999"; 176 | else if (map[y][x] === Tile.STONE) g.fillStyle = "#0000cc"; 177 | else if (map[y][x] === Tile.EXTRA_BOMB) g.fillStyle = "#00cc00"; 178 | else if (map[y][x] === Tile.FIRE) g.fillStyle = "#ffcc00"; 179 | else if ( 180 | map[y][x] === Tile.MONSTER_UP || 181 | map[y][x] === Tile.MONSTER_LEFT || 182 | map[y][x] === Tile.MONSTER_RIGHT || 183 | map[y][x] === Tile.MONSTER_DOWN 184 | ) 185 | g.fillStyle = "#cc00cc"; 186 | else if (map[y][x] === Tile.BOMB) g.fillStyle = "#770000"; 187 | else if (map[y][x] === Tile.BOMB_CLOSE) g.fillStyle = "#cc0000"; 188 | else if (map[y][x] === Tile.BOMB_REALLY_CLOSE) g.fillStyle = "#ff0000"; 189 | 190 | if (map[y][x] !== Tile.AIR) 191 | g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); 192 | } 193 | } 194 | 195 | // Draw player 196 | g.fillStyle = "#00ff00"; 197 | if (!gameOver) 198 | g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE); 199 | } 200 | 201 | function gameLoop() { 202 | let before = Date.now(); 203 | update(); 204 | draw(); 205 | let after = Date.now(); 206 | let frameTime = after - before; 207 | let sleep = SLEEP - frameTime; 208 | setTimeout(() => gameLoop(), sleep); 209 | } 210 | 211 | window.onload = () => { 212 | gameLoop(); 213 | }; 214 | 215 | const LEFT_KEY = "ArrowLeft"; 216 | const UP_KEY = "ArrowUp"; 217 | const RIGHT_KEY = "ArrowRight"; 218 | const DOWN_KEY = "ArrowDown"; 219 | window.addEventListener("keydown", (e) => { 220 | if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT); 221 | else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP); 222 | else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT); 223 | else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN); 224 | else if (e.key === " ") inputs.push(Input.PLACE); 225 | }); 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynablaster", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "sourceMap": false 7 | } 8 | } 9 | --------------------------------------------------------------------------------