Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
thedrlambda committed Jul 8, 2021
0 parents commit 6835d58
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
*.sh
.vscode
28 changes: 28 additions & 0 deletions README.md
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.

Binary file added frontpage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added goal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions index.html
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">
&nbsp;
&rarr; &rarr; &rarr; &darr; &nbsp; &nbsp;
&darr; &darr; &larr; &uarr; &nbsp; &nbsp;
&darr; &larr; &rarr; &uarr; &nbsp; &nbsp;
&larr; &larr; &rarr; &rarr; &nbsp; &nbsp;
&darr; &rarr; &rarr;
&nbsp;
</span>
</p>
</body>

</html>
171 changes: 171 additions & 0 deletions index.ts
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);
});

8 changes: 8 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"noImplicitAny": true,
"sourceMap": false
}
}

0 comments on commit 6835d58

Please sign in to comment.