-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 6835d58
Showing
8 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.js | ||
*.sh | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# five-lines | ||
|
||
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. | ||
|
||
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. | ||
|
||
This is *not* an easy exercise. | ||
|
||
# About the Game | ||
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. | ||
|
||
![Screenshot of the game](game.png) | ||
|
||
# How to Build It | ||
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. | ||
|
||
# How to Run It | ||
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. | ||
|
||
# Thank You! | ||
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. | ||
|
||
[![Five Lines of Code](frontpage.png)](https://www.manning.com/books/five-lines-of-code) | ||
|
||
If you have feedback or comments on this repo don't hesitate to write me a message or send me a pull request. | ||
|
||
Thank you for checking it out. | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
<html> | ||
|
||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | ||
<title>Gatherer</title> | ||
<script src="index.js"></script> | ||
<style> | ||
body { | ||
font-family: Arial, Helvetica, sans-serif; | ||
} | ||
|
||
.box { | ||
width: 10px; | ||
height: 11px; | ||
display: inline-block; | ||
border: 1px solid black; | ||
} | ||
|
||
.spoiler { | ||
background-color: #333; | ||
color: #333; | ||
padding: 5px; | ||
} | ||
|
||
.spoiler:hover { | ||
color: white; | ||
} | ||
|
||
p { | ||
max-width: 800px; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<canvas width="300" height="200" id="GameCanvas"></canvas> | ||
|
||
<h3>How to play</h3> | ||
<p> | ||
The user controls the player square using the arrow keys. | ||
</p> | ||
|
||
<ul> | ||
<li> <span class="box" style="background-color: red;"></span> The red square is the <b>player</b>.</li> | ||
<li> <span class="box" style="background-color: #8b4513;"></span> Brown squares are <b>boxes</b>.</li> | ||
<li> <span class="box" style="background-color: blue;"></span> Blue squares are <b>stones</b>.</li> | ||
<li> <span class="box" style="background-color: #fc0;"></span> Yellow squares are <b>keys</b> <i>or</i> <b>locks</b> | ||
-- we fix | ||
this | ||
in the book.</li> | ||
<li> <span class="box" style="background-color: #ccffcc;"></span> Greenish squares are called <b>flux</b>.</li> | ||
<li> <span class="box" style="background-color: gray;"></span> Gray squares are <b>walls</b>.</li> | ||
<li> <span class="box" style=""></span> White squares are <b>air</b> (empty).</li> | ||
</ul> | ||
|
||
<p> | ||
If a box or stone is not supported by anything, it falls. The player can push one stone or box at a time, provided | ||
it | ||
is not obstructed or falling. The path between the box and the lower-right corner is initially obstructed by a lock, | ||
so the player has to get a key to remove it. Flux can be "eaten" (removed) by the player by stepping on it. | ||
</p> | ||
|
||
<h3>How to win</h3> | ||
<p> | ||
The objective of the game is to get the box to the lower-right corner, like this: | ||
</p> | ||
<img src="goal.png" width=150 /> | ||
|
||
<p> | ||
Solution (put your cursor here): | ||
<span class="spoiler"> | ||
| ||
→ → → ↓ | ||
↓ ↓ ← ↑ | ||
↓ ← → ↑ | ||
← ← → → | ||
↓ → → | ||
| ||
</span> | ||
</p> | ||
</body> | ||
|
||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
|
||
const TILE_SIZE = 30; | ||
const FPS = 30; | ||
const SLEEP = 1000 / FPS; | ||
|
||
enum Tile { | ||
AIR, | ||
FLUX, | ||
UNBREAKABLE, | ||
PLAYER, | ||
STONE, FALLING_STONE, | ||
BOX, FALLING_BOX, | ||
KEY1, LOCK1, | ||
KEY2, LOCK2 | ||
} | ||
|
||
enum Input { | ||
UP, DOWN, LEFT, RIGHT | ||
} | ||
|
||
let playerx = 1; | ||
let playery = 1; | ||
let map: Tile[][] = [ | ||
[2, 2, 2, 2, 2, 2, 2, 2], | ||
[2, 3, 0, 1, 1, 2, 0, 2], | ||
[2, 4, 2, 6, 1, 2, 0, 2], | ||
[2, 8, 4, 1, 1, 2, 0, 2], | ||
[2, 4, 1, 1, 1, 9, 0, 2], | ||
[2, 2, 2, 2, 2, 2, 2, 2], | ||
]; | ||
|
||
let inputs: Input[] = []; | ||
|
||
function remove(tile: Tile) { | ||
for (let y = 0; y < map.length; y++) { | ||
for (let x = 0; x < map[y].length; x++) { | ||
if (map[y][x] === tile) { | ||
map[y][x] = Tile.AIR; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function moveToTile(newx: number, newy: number) { | ||
map[playery][playerx] = Tile.AIR; | ||
map[newy][newx] = Tile.PLAYER; | ||
playerx = newx; | ||
playery = newy; | ||
} | ||
|
||
function moveHorizontal(dx: number) { | ||
if (map[playery][playerx + dx] === Tile.FLUX | ||
|| map[playery][playerx + dx] === Tile.AIR) { | ||
moveToTile(playerx + dx, playery); | ||
} else if ((map[playery][playerx + dx] === Tile.STONE | ||
|| map[playery][playerx + dx] === Tile.BOX) | ||
&& map[playery][playerx + dx + dx] === Tile.AIR | ||
&& map[playery + 1][playerx + dx] !== Tile.AIR) { | ||
map[playery][playerx + dx + dx] = map[playery][playerx + dx]; | ||
moveToTile(playerx + dx, playery); | ||
} else if (map[playery][playerx + dx] === Tile.KEY1) { | ||
remove(Tile.LOCK1); | ||
moveToTile(playerx + dx, playery); | ||
} else if (map[playery][playerx + dx] === Tile.KEY2) { | ||
remove(Tile.LOCK2); | ||
moveToTile(playerx + dx, playery); | ||
} | ||
} | ||
|
||
function moveVertical(dy: number) { | ||
if (map[playery + dy][playerx] === Tile.FLUX | ||
|| map[playery + dy][playerx] === Tile.AIR) { | ||
moveToTile(playerx, playery + dy); | ||
} else if (map[playery + dy][playerx] === Tile.KEY1) { | ||
remove(Tile.LOCK1); | ||
moveToTile(playerx, playery + dy); | ||
} else if (map[playery + dy][playerx] === Tile.KEY2) { | ||
remove(Tile.LOCK2); | ||
moveToTile(playerx, playery + dy); | ||
} | ||
} | ||
|
||
function update() { | ||
while (inputs.length > 0) { | ||
let current = inputs.pop(); | ||
if (current === Input.LEFT) | ||
moveHorizontal(-1); | ||
else if (current === Input.RIGHT) | ||
moveHorizontal(1); | ||
else if (current === Input.UP) | ||
moveVertical(-1); | ||
else if (current === Input.DOWN) | ||
moveVertical(1); | ||
} | ||
|
||
for (let y = map.length - 1; y >= 0; y--) { | ||
for (let x = 0; x < map[y].length; x++) { | ||
if ((map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE) | ||
&& map[y + 1][x] === Tile.AIR) { | ||
map[y + 1][x] = Tile.FALLING_STONE; | ||
map[y][x] = Tile.AIR; | ||
} else if ((map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX) | ||
&& map[y + 1][x] === Tile.AIR) { | ||
map[y + 1][x] = Tile.FALLING_BOX; | ||
map[y][x] = Tile.AIR; | ||
} else if (map[y][x] === Tile.FALLING_STONE) { | ||
map[y][x] = Tile.STONE; | ||
} else if (map[y][x] === Tile.FALLING_BOX) { | ||
map[y][x] = Tile.BOX; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function draw() { | ||
let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement; | ||
let g = canvas.getContext("2d"); | ||
|
||
g.clearRect(0, 0, canvas.width, canvas.height); | ||
|
||
// Draw map | ||
for (let y = 0; y < map.length; y++) { | ||
for (let x = 0; x < map[y].length; x++) { | ||
if (map[y][x] === Tile.FLUX) | ||
g.fillStyle = "#ccffcc"; | ||
else if (map[y][x] === Tile.UNBREAKABLE) | ||
g.fillStyle = "#999999"; | ||
else if (map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE) | ||
g.fillStyle = "#0000cc"; | ||
else if (map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX) | ||
g.fillStyle = "#8b4513"; | ||
else if (map[y][x] === Tile.KEY1 || map[y][x] === Tile.LOCK1) | ||
g.fillStyle = "#ffcc00"; | ||
else if (map[y][x] === Tile.KEY2 || map[y][x] === Tile.LOCK2) | ||
g.fillStyle = "#00ccff"; | ||
|
||
if (map[y][x] !== Tile.AIR && map[y][x] !== Tile.PLAYER) | ||
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); | ||
} | ||
} | ||
|
||
// Draw player | ||
g.fillStyle = "#ff0000"; | ||
g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE); | ||
} | ||
|
||
function gameLoop() { | ||
let before = Date.now(); | ||
update(); | ||
draw(); | ||
let after = Date.now(); | ||
let frameTime = after - before; | ||
let sleep = SLEEP - frameTime; | ||
setTimeout(() => gameLoop(), sleep); | ||
} | ||
|
||
window.onload = () => { | ||
gameLoop(); | ||
} | ||
|
||
const LEFT_KEY = "ArrowLeft"; | ||
const UP_KEY = "ArrowUp"; | ||
const RIGHT_KEY = "ArrowRight"; | ||
const DOWN_KEY = "ArrowDown"; | ||
window.addEventListener("keydown", e => { | ||
if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT); | ||
else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP); | ||
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT); | ||
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"target": "es5", | ||
"noImplicitAny": true, | ||
"sourceMap": false | ||
} | ||
} |