├── .gitignore ├── README.md ├── frontpage.png ├── game.png ├── goal.png ├── index.html ├── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.sh 3 | .vscode 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # five-lines 2 | 3 | In this kata your task is to refactor the code for a small game. When finished it should be easy to add new tile types, or make the key draw as a circle, so we can easily distinguish it from the lock. 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 | # About the Game 10 | In the game, you are a red square and have to get the box (brown) to the lower right corner. Obstacles include falling stones (blue), walls (gray), and a lock (yellow, right) that can be unlocked with the key (yellow, left). You can push one stone or box at a time, and only if it is not falling. The flux (greenish) holds up boxes and stones but can be 'eaten' by the player. 11 | 12 |  13 | 14 | # How to Build It 15 | 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. 16 | 17 | # How to Run It 18 | 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. 19 | 20 | # Thank You! 21 | 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), available through the Manning Early Access Program. 22 | 23 | [](https://www.manning.com/books/five-lines-of-code) 24 | 25 | If you have feedback or comments on this repo don't hesitate to write me a message or send me a pull request. 26 | 27 | Thank you for checking it out. 28 | 29 | -------------------------------------------------------------------------------- /frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedrlambda/five-lines/6835d5850b1ca80e6126f3cd1fb42afa416d344c/frontpage.png -------------------------------------------------------------------------------- /game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedrlambda/five-lines/6835d5850b1ca80e6126f3cd1fb42afa416d344c/game.png -------------------------------------------------------------------------------- /goal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedrlambda/five-lines/6835d5850b1ca80e6126f3cd1fb42afa416d344c/goal.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |42 | The user controls the player square using the arrow keys. 43 |
44 | 45 |59 | If a box or stone is not supported by anything, it falls. The player can push one stone or box at a time, provided 60 | it 61 | is not obstructed or falling. The path between the box and the lower-right corner is initially obstructed by a lock, 62 | so the player has to get a key to remove it. Flux can be "eaten" (removed) by the player by stepping on it. 63 |
64 | 65 |67 | The objective of the game is to get the box to the lower-right corner, like this: 68 |
69 |72 | Solution (put your cursor here): 73 | 74 | 75 | → → → ↓ 76 | ↓ ↓ ← ↑ 77 | ↓ ← → ↑ 78 | ← ← → → 79 | ↓ → → 80 | 81 | 82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | 2 | const TILE_SIZE = 30; 3 | const FPS = 30; 4 | const SLEEP = 1000 / FPS; 5 | 6 | enum Tile { 7 | AIR, 8 | FLUX, 9 | UNBREAKABLE, 10 | PLAYER, 11 | STONE, FALLING_STONE, 12 | BOX, FALLING_BOX, 13 | KEY1, LOCK1, 14 | KEY2, LOCK2 15 | } 16 | 17 | enum Input { 18 | UP, DOWN, LEFT, RIGHT 19 | } 20 | 21 | let playerx = 1; 22 | let playery = 1; 23 | let map: Tile[][] = [ 24 | [2, 2, 2, 2, 2, 2, 2, 2], 25 | [2, 3, 0, 1, 1, 2, 0, 2], 26 | [2, 4, 2, 6, 1, 2, 0, 2], 27 | [2, 8, 4, 1, 1, 2, 0, 2], 28 | [2, 4, 1, 1, 1, 9, 0, 2], 29 | [2, 2, 2, 2, 2, 2, 2, 2], 30 | ]; 31 | 32 | let inputs: Input[] = []; 33 | 34 | function remove(tile: Tile) { 35 | for (let y = 0; y < map.length; y++) { 36 | for (let x = 0; x < map[y].length; x++) { 37 | if (map[y][x] === tile) { 38 | map[y][x] = Tile.AIR; 39 | } 40 | } 41 | } 42 | } 43 | 44 | function moveToTile(newx: number, newy: number) { 45 | map[playery][playerx] = Tile.AIR; 46 | map[newy][newx] = Tile.PLAYER; 47 | playerx = newx; 48 | playery = newy; 49 | } 50 | 51 | function moveHorizontal(dx: number) { 52 | if (map[playery][playerx + dx] === Tile.FLUX 53 | || map[playery][playerx + dx] === Tile.AIR) { 54 | moveToTile(playerx + dx, playery); 55 | } else if ((map[playery][playerx + dx] === Tile.STONE 56 | || map[playery][playerx + dx] === Tile.BOX) 57 | && map[playery][playerx + dx + dx] === Tile.AIR 58 | && map[playery + 1][playerx + dx] !== Tile.AIR) { 59 | map[playery][playerx + dx + dx] = map[playery][playerx + dx]; 60 | moveToTile(playerx + dx, playery); 61 | } else if (map[playery][playerx + dx] === Tile.KEY1) { 62 | remove(Tile.LOCK1); 63 | moveToTile(playerx + dx, playery); 64 | } else if (map[playery][playerx + dx] === Tile.KEY2) { 65 | remove(Tile.LOCK2); 66 | moveToTile(playerx + dx, playery); 67 | } 68 | } 69 | 70 | function moveVertical(dy: number) { 71 | if (map[playery + dy][playerx] === Tile.FLUX 72 | || map[playery + dy][playerx] === Tile.AIR) { 73 | moveToTile(playerx, playery + dy); 74 | } else if (map[playery + dy][playerx] === Tile.KEY1) { 75 | remove(Tile.LOCK1); 76 | moveToTile(playerx, playery + dy); 77 | } else if (map[playery + dy][playerx] === Tile.KEY2) { 78 | remove(Tile.LOCK2); 79 | moveToTile(playerx, playery + dy); 80 | } 81 | } 82 | 83 | function update() { 84 | while (inputs.length > 0) { 85 | let current = inputs.pop(); 86 | if (current === Input.LEFT) 87 | moveHorizontal(-1); 88 | else if (current === Input.RIGHT) 89 | moveHorizontal(1); 90 | else if (current === Input.UP) 91 | moveVertical(-1); 92 | else if (current === Input.DOWN) 93 | moveVertical(1); 94 | } 95 | 96 | for (let y = map.length - 1; y >= 0; y--) { 97 | for (let x = 0; x < map[y].length; x++) { 98 | if ((map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE) 99 | && map[y + 1][x] === Tile.AIR) { 100 | map[y + 1][x] = Tile.FALLING_STONE; 101 | map[y][x] = Tile.AIR; 102 | } else if ((map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX) 103 | && map[y + 1][x] === Tile.AIR) { 104 | map[y + 1][x] = Tile.FALLING_BOX; 105 | map[y][x] = Tile.AIR; 106 | } else if (map[y][x] === Tile.FALLING_STONE) { 107 | map[y][x] = Tile.STONE; 108 | } else if (map[y][x] === Tile.FALLING_BOX) { 109 | map[y][x] = Tile.BOX; 110 | } 111 | } 112 | } 113 | } 114 | 115 | function draw() { 116 | let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement; 117 | let g = canvas.getContext("2d"); 118 | 119 | g.clearRect(0, 0, canvas.width, canvas.height); 120 | 121 | // Draw map 122 | for (let y = 0; y < map.length; y++) { 123 | for (let x = 0; x < map[y].length; x++) { 124 | if (map[y][x] === Tile.FLUX) 125 | g.fillStyle = "#ccffcc"; 126 | else if (map[y][x] === Tile.UNBREAKABLE) 127 | g.fillStyle = "#999999"; 128 | else if (map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE) 129 | g.fillStyle = "#0000cc"; 130 | else if (map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX) 131 | g.fillStyle = "#8b4513"; 132 | else if (map[y][x] === Tile.KEY1 || map[y][x] === Tile.LOCK1) 133 | g.fillStyle = "#ffcc00"; 134 | else if (map[y][x] === Tile.KEY2 || map[y][x] === Tile.LOCK2) 135 | g.fillStyle = "#00ccff"; 136 | 137 | if (map[y][x] !== Tile.AIR && map[y][x] !== Tile.PLAYER) 138 | g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); 139 | } 140 | } 141 | 142 | // Draw player 143 | g.fillStyle = "#ff0000"; 144 | g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE); 145 | } 146 | 147 | function gameLoop() { 148 | let before = Date.now(); 149 | update(); 150 | draw(); 151 | let after = Date.now(); 152 | let frameTime = after - before; 153 | let sleep = SLEEP - frameTime; 154 | setTimeout(() => gameLoop(), sleep); 155 | } 156 | 157 | window.onload = () => { 158 | gameLoop(); 159 | } 160 | 161 | const LEFT_KEY = "ArrowLeft"; 162 | const UP_KEY = "ArrowUp"; 163 | const RIGHT_KEY = "ArrowRight"; 164 | const DOWN_KEY = "ArrowDown"; 165 | window.addEventListener("keydown", e => { 166 | if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT); 167 | else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP); 168 | else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT); 169 | else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN); 170 | }); 171 | 172 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "sourceMap": false 7 | } 8 | } --------------------------------------------------------------------------------