diff --git a/fixtures/m04-tick.json b/fixtures/m04-tick.json new file mode 100644 index 0000000..76d8ce6 --- /dev/null +++ b/fixtures/m04-tick.json @@ -0,0 +1,189 @@ +{ + "function": "tick", + "cases": [ + { + "name": "eat-then-respawn", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 0 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 1 + }, + "seed": 1831565813, + "score": 1, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "plain-move-no-respawn", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "wall-hit-is-game-over-no-spawn", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 2, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 0, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 2, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 0, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "game-over-state-unchanged", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "GameOver", + "frame": 0 + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "GameOver", + "frame": 0 + } + } + ] +} diff --git a/fixtures/m05-history.json b/fixtures/m05-history.json new file mode 100644 index 0000000..d7d3a06 --- /dev/null +++ b/fixtures/m05-history.json @@ -0,0 +1,172 @@ +{ + "function": "history", + "cases": [ + { + "name": "no-actions-gives-just-the-start", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + [] + ], + "expected": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + ] + }, + { + "name": "every-intermediate-state-is-kept", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + [ + { + "tag": "Turn", + "dir": "Down" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + } + ] + ], + "expected": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Down", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 1 + } + ], + "dir": "Down", + "nextDir": "Down", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 1 + } + ], + "dir": "Down", + "nextDir": "Down", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "GameOver", + "frame": 0 + } + ] + } + ] +} diff --git a/fixtures/m05-replay.json b/fixtures/m05-replay.json new file mode 100644 index 0000000..8e80b39 --- /dev/null +++ b/fixtures/m05-replay.json @@ -0,0 +1,172 @@ +{ + "function": "replay", + "cases": [ + { + "name": "twelve-actions-eat-one-apple", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 10, + "y": 10 + }, + { + "x": 9, + "y": 10 + }, + { + "x": 8, + "y": 10 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 9 + }, + "seed": 1831565855, + "score": 0, + "status": "Playing", + "frame": 0 + }, + [ + { + "tag": "Turn", + "dir": "Up" + }, + { + "tag": "Tick" + }, + { + "tag": "Turn", + "dir": "Left" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + }, + { + "tag": "Tick" + } + ] + ], + "expected": { + "width": 20, + "height": 20, + "snake": [ + { + "x": 1, + "y": 9 + }, + { + "x": 2, + "y": 9 + }, + { + "x": 3, + "y": 9 + }, + { + "x": 4, + "y": 9 + } + ], + "dir": "Left", + "nextDir": "Left", + "apple": { + "x": 6, + "y": 5 + }, + "seed": 3663131668, + "score": 1, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "no-actions-returns-the-start", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 10, + "y": 10 + }, + { + "x": 9, + "y": 10 + }, + { + "x": 8, + "y": 10 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 9 + }, + "seed": 1831565855, + "score": 0, + "status": "Playing", + "frame": 0 + }, + [] + ], + "expected": { + "width": 20, + "height": 20, + "snake": [ + { + "x": 10, + "y": 10 + }, + { + "x": 9, + "y": 10 + }, + { + "x": 8, + "y": 10 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 9 + }, + "seed": 1831565855, + "score": 0, + "status": "Playing", + "frame": 0 + } + } + ] +} diff --git a/fixtures/m05-update.json b/fixtures/m05-update.json new file mode 100644 index 0000000..36499b8 --- /dev/null +++ b/fixtures/m05-update.json @@ -0,0 +1,348 @@ +{ + "function": "update", + "cases": [ + { + "name": "tick-moves-the-snake", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "tag": "Tick" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "tick-eating-respawns-apple", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 0 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "tag": "Tick" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 1, + "y": 1 + }, + "seed": 1831565813, + "score": 1, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "tick-into-wall-is-game-over", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 2, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 0, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "tag": "Tick" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 2, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 0, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "tick-while-paused-does-nothing", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Paused", + "frame": 0 + }, + { + "tag": "Tick" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Paused", + "frame": 0 + } + }, + { + "name": "turn-sets-next-dir", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "tag": "Turn", + "dir": "Down" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Down", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-against-direction-is-ignored", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "tag": "Turn", + "dir": "Left" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-while-paused-is-ignored", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Paused", + "frame": 0 + }, + { + "tag": "Turn", + "dir": "Down" + } + ], + "expected": { + "width": 3, + "height": 2, + "snake": [ + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 0, + "status": "Paused", + "frame": 0 + } + } + ] +} diff --git a/fixtures/m06-view.json b/fixtures/m06-view.json new file mode 100644 index 0000000..2277eb8 --- /dev/null +++ b/fixtures/m06-view.json @@ -0,0 +1,398 @@ +{ + "function": "view", + "cases": [ + { + "name": "playing-with-apple", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 5, + "status": "Playing", + "frame": 0 + } + ], + "expected": [ + { + "tag": "Rect", + "x": 0, + "y": 0, + "w": 60, + "h": 40, + "color": "#111111" + }, + { + "tag": "Rect", + "x": 41, + "y": 21, + "w": 18, + "h": 18, + "color": "#e53935" + }, + { + "tag": "Rect", + "x": 21, + "y": 1, + "w": 18, + "h": 18, + "color": "#7cb342" + }, + { + "tag": "Rect", + "x": 1, + "y": 1, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Text", + "x": 6, + "y": 18, + "text": "Score: 5", + "color": "#ffffff", + "size": 16, + "align": "left" + } + ] + }, + { + "name": "playing-without-apple-has-no-apple-rect", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 0, + "score": 5, + "status": "Playing", + "frame": 0 + } + ], + "expected": [ + { + "tag": "Rect", + "x": 0, + "y": 0, + "w": 60, + "h": 40, + "color": "#111111" + }, + { + "tag": "Rect", + "x": 21, + "y": 1, + "w": 18, + "h": 18, + "color": "#7cb342" + }, + { + "tag": "Rect", + "x": 1, + "y": 1, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Text", + "x": 6, + "y": 18, + "text": "Score: 5", + "color": "#ffffff", + "size": 16, + "align": "left" + } + ] + }, + { + "name": "paused-overlay", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 0, + "score": 5, + "status": "Paused", + "frame": 0 + } + ], + "expected": [ + { + "tag": "Rect", + "x": 0, + "y": 0, + "w": 60, + "h": 40, + "color": "#111111" + }, + { + "tag": "Rect", + "x": 41, + "y": 21, + "w": 18, + "h": 18, + "color": "#e53935" + }, + { + "tag": "Rect", + "x": 21, + "y": 1, + "w": 18, + "h": 18, + "color": "#7cb342" + }, + { + "tag": "Rect", + "x": 1, + "y": 1, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Text", + "x": 6, + "y": 18, + "text": "Score: 5", + "color": "#ffffff", + "size": 16, + "align": "left" + }, + { + "tag": "Text", + "x": 30, + "y": 20, + "text": "Paused", + "color": "#ffffff", + "size": 32, + "align": "center" + }, + { + "tag": "Text", + "x": 30, + "y": 50, + "text": "Press Space to resume", + "color": "#ffffff", + "size": 16, + "align": "center" + } + ] + }, + { + "name": "game-over-overlay-on-20x20-board", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 10, + "y": 10 + }, + { + "x": 9, + "y": 10 + }, + { + "x": 8, + "y": 10 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + ], + "expected": [ + { + "tag": "Rect", + "x": 0, + "y": 0, + "w": 400, + "h": 400, + "color": "#111111" + }, + { + "tag": "Rect", + "x": 201, + "y": 201, + "w": 18, + "h": 18, + "color": "#7cb342" + }, + { + "tag": "Rect", + "x": 181, + "y": 201, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Rect", + "x": 161, + "y": 201, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Text", + "x": 6, + "y": 18, + "text": "Score: 0", + "color": "#ffffff", + "size": 16, + "align": "left" + }, + { + "tag": "Text", + "x": 200, + "y": 200, + "text": "Game Over", + "color": "#ffffff", + "size": 32, + "align": "center" + }, + { + "tag": "Text", + "x": 200, + "y": 230, + "text": "Press R to restart", + "color": "#ffffff", + "size": 16, + "align": "center" + } + ] + }, + { + "name": "won-overlay", + "args": [ + { + "width": 3, + "height": 2, + "snake": [ + { + "x": 1, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 0, + "score": 5, + "status": "Won", + "frame": 0 + } + ], + "expected": [ + { + "tag": "Rect", + "x": 0, + "y": 0, + "w": 60, + "h": 40, + "color": "#111111" + }, + { + "tag": "Rect", + "x": 21, + "y": 1, + "w": 18, + "h": 18, + "color": "#7cb342" + }, + { + "tag": "Rect", + "x": 1, + "y": 1, + "w": 18, + "h": 18, + "color": "#43a047" + }, + { + "tag": "Text", + "x": 6, + "y": 18, + "text": "Score: 5", + "color": "#ffffff", + "size": 16, + "align": "left" + }, + { + "tag": "Text", + "x": 30, + "y": 20, + "text": "You Win!", + "color": "#ffffff", + "size": 32, + "align": "center" + }, + { + "tag": "Text", + "x": 30, + "y": 50, + "text": "Press R to restart", + "color": "#ffffff", + "size": 16, + "align": "center" + } + ] + } + ] +} diff --git a/js/solutions/game.js b/js/solutions/game.js new file mode 100644 index 0000000..6f9e527 --- /dev/null +++ b/js/solutions/game.js @@ -0,0 +1,21 @@ +// m05 game: one `update` function turns every action into a new state; replay is a fold over actions. +import { turn } from './rules.js' +import { tick } from './spawn.js' + +/** Tick and Turn are handled here; other actions leave the state alone (m10 adds more). */ +export const update = (state, action) => { + switch (action.tag) { + case 'Tick': return tick(state) + case 'Turn': return turn(state, action.dir) + default: return state + } +} + +/** Fold the actions over the state, left to right. */ +export const replay = (state, actions) => actions.reduce(update, state) + +/** Like replay, but keeps each intermediate state. Recursion instead of mutation: no array is ever pushed to. */ +export const history = (state, actions) => + actions.length === 0 + ? [state] + : [state, ...history(update(state, actions[0]), actions.slice(1))] diff --git a/js/solutions/view.js b/js/solutions/view.js new file mode 100644 index 0000000..a0c6272 --- /dev/null +++ b/js/solutions/view.js @@ -0,0 +1,30 @@ +// m06 view: the picture is plain data (a list of shapes); drawing it is somebody else's job (m07). + +/** Pixels per board cell. */ +export const CELL = 20 + +const rect = (x, y, w, h, color) => ({ tag: 'Rect', x, y, w, h, color }) +const text = (x, y, str, size, align) => ({ tag: 'Text', x, y, text: str, color: '#ffffff', size, align }) + +// One cell, inset by 1px so neighbouring segments show a gap. +const cell = (p, color) => rect(p.x * CELL + 1, p.y * CELL + 1, CELL - 2, CELL - 2, color) + +const overlay = (state) => { + const cx = (state.width * CELL) / 2 + const cy = (state.height * CELL) / 2 + const banner = (title, hint) => [text(cx, cy, title, 32, 'center'), text(cx, cy + 30, hint, 16, 'center')] + switch (state.status) { + case 'Paused': return banner('Paused', 'Press Space to resume') + case 'GameOver': return banner('Game Over', 'Press R to restart') + case 'Won': return banner('You Win!', 'Press R to restart') + default: return [] + } +} + +export const view = (state) => [ + rect(0, 0, state.width * CELL, state.height * CELL, '#111111'), + ...(state.apple === null ? [] : [cell(state.apple, '#e53935')]), + ...state.snake.map((p, i) => cell(p, i === 0 ? '#7cb342' : '#43a047')), + text(6, 18, `Score: ${state.score}`, 16, 'left'), + ...overlay(state), +] diff --git a/js/src/game.js b/js/src/game.js new file mode 100644 index 0000000..5015dd1 --- /dev/null +++ b/js/src/game.js @@ -0,0 +1,14 @@ +import { todo } from './todo.js' + +/** + * The one function that says how the game changes: `(state, action) -> state`. + * `Tick` runs a game step (`tick`), `Turn` asks to change direction (`turn`); + * every other action returns the state unchanged for now. + */ +export const update = (state, action) => todo('m05') + +/** Apply all actions in order, starting from `state`, and return the final state. */ +export const replay = (state, actions) => todo('m05') + +/** Like `replay`, but keep every state: `[state, afterFirst, afterSecond, ...]`, so its length is `actions.length + 1`. */ +export const history = (state, actions) => todo('m05') diff --git a/js/src/view.js b/js/src/view.js new file mode 100644 index 0000000..21155c3 --- /dev/null +++ b/js/src/view.js @@ -0,0 +1,17 @@ +import { todo } from './todo.js' + +/** Pixels per board cell. */ +export const CELL = 20 + +/** + * Describe the picture as data: an array of shapes, drawn in order. + * `{tag:'Rect', x, y, w, h, color}` and `{tag:'Text', x, y, text, color, size, align}`. + * Order: background Rect(0,0,width*CELL,height*CELL,'#111111'); the apple (if any) as + * Rect(ax*CELL+1, ay*CELL+1, CELL-2, CELL-2, '#e53935'); each snake segment in list order, same size, + * head '#7cb342' and body '#43a047'; Text(6,18,'Score: ','#ffffff',16,'left'); + * then an overlay centred at (width*CELL/2, height*CELL/2), all '#ffffff': + * Paused: 'Paused' (size 32) and, 30px lower, 'Press Space to resume' (size 16); + * GameOver: 'Game Over' + 'Press R to restart'; Won: 'You Win!' + 'Press R to restart'; Playing: none. + * Text in the overlay uses align 'center'. + */ +export const view = (state) => todo('m06') diff --git a/js/test/m04-spawn.test.js b/js/test/m04-spawn.test.js index b2fc45d..8ceaef5 100644 --- a/js/test/m04-spawn.test.js +++ b/js/test/m04-spawn.test.js @@ -13,6 +13,7 @@ const fixtureTests = (name, f) => { fixtureTests('m04-free-cells', freeCells) fixtureTests('m04-spawn', spawnApple) fixtureTests('m04-new-game', newGame) +fixtureTests('m04-tick', tick) // 3x2 board, one-cell snake, about to eat the apple at (1,0) const eating = freeze({ diff --git a/js/test/m05-game.test.js b/js/test/m05-game.test.js new file mode 100644 index 0000000..6747eaa --- /dev/null +++ b/js/test/m05-game.test.js @@ -0,0 +1,26 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { update, replay, history } from '../src/game.js' +import { loadFixture, freeze } from './helpers.js' + +const fixtureTests = (name, f) => { + const fx = loadFixture(name) + fx.cases.forEach((c) => + test(`m05 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(f(...freeze(c.args)), c.expected))) +} + +fixtureTests('m05-update', update) +fixtureTests('m05-replay', replay) +fixtureTests('m05-history', history) + +test('m05 history has one more state than there are actions', () => { + const [state, actions] = loadFixture('m05-replay').cases[0].args + assert.equal(history(freeze(state), freeze(actions)).length, actions.length + 1) +}) + +test('m05 history starts at the start state and ends where replay ends', () => { + const [state, actions] = freeze(loadFixture('m05-replay').cases[0].args) + const states = history(state, actions) + assert.deepStrictEqual(states[0], state) + assert.deepStrictEqual(states[states.length - 1], replay(state, actions)) +}) diff --git a/js/test/m06-view.test.js b/js/test/m06-view.test.js new file mode 100644 index 0000000..0c68bb8 --- /dev/null +++ b/js/test/m06-view.test.js @@ -0,0 +1,10 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { CELL, view } from '../src/view.js' +import { loadFixture, freeze } from './helpers.js' + +test('m06 CELL is 20 pixels', () => assert.equal(CELL, 20)) + +const fx = loadFixture('m06-view') +fx.cases.forEach((c) => + test(`m06 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(view(...freeze(c.args)), c.expected)))