├── .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 | [](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 |
46 | - The green square is the player.
47 | - Blue squares are destructible walls.
48 | - Red squares are bombs.
49 | - Yellow squares are fire.
50 | - Dark green squares are extra bomb power-ups.
51 |
52 | - Gray squares are indestructible walls.
53 | - The purple square is an enemy monster.
54 |
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 |
--------------------------------------------------------------------------------