diff --git a/fixtures/m03-advance.json b/fixtures/m03-advance.json new file mode 100644 index 0000000..ff8962b --- /dev/null +++ b/fixtures/m03-advance.json @@ -0,0 +1,716 @@ +{ + "function": "advance", + "cases": [ + { + "name": "advance-plain-move", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 3, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "advance-moves-in-nextDir-and-records-it-as-dir", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 1 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + } + ], + "dir": "Up", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "advance-keeps-apple-it-did-not-reach", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 4, + "y": 4 + }, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 3, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 4, + "y": 4 + }, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "advance-eat-grows-scores-clears-apple", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 3, + "y": 2 + }, + "seed": 1, + "score": 4, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 3, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 5, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "advance-into-wall-is-game-over", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 4, + "y": 2 + }, + { + "x": 3, + "y": 2 + }, + { + "x": 2, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 4, + "y": 2 + }, + { + "x": 3, + "y": 2 + }, + { + "x": 2, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "advance-into-top-wall-records-turn", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 0 + }, + { + "x": 2, + "y": 1 + }, + { + "x": 2, + "y": 2 + } + ], + "dir": "Up", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 0 + }, + { + "x": 2, + "y": 1 + }, + { + "x": 2, + "y": 2 + } + ], + "dir": "Up", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "advance-into-own-body-is-game-over", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 3, + "y": 2 + }, + { + "x": 3, + "y": 3 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 1, + "y": 3 + } + ], + "dir": "Left", + "nextDir": "Down", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 3, + "y": 2 + }, + { + "x": 3, + "y": 3 + }, + { + "x": 2, + "y": 3 + }, + { + "x": 1, + "y": 3 + } + ], + "dir": "Down", + "nextDir": "Down", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "advance-into-vacating-tail", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 1, + "y": 1 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 1 + } + ], + "dir": "Up", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 1 + }, + { + "x": 1, + "y": 1 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 2, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "advance-into-tail-while-eating", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 1, + "y": 1 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 1 + } + ], + "dir": "Up", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 1, + "y": 1 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 2, + "y": 2 + }, + { + "x": 2, + "y": 1 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": { + "x": 2, + "y": 1 + }, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "advance-paused-unchanged", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Paused", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Paused", + "frame": 0 + } + }, + { + "name": "advance-game-over-unchanged", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "advance-won-unchanged", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Won", + "frame": 0 + } + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Won", + "frame": 0 + } + } + ] +} diff --git a/fixtures/m03-in-bounds.json b/fixtures/m03-in-bounds.json new file mode 100644 index 0000000..a5acad2 --- /dev/null +++ b/fixtures/m03-in-bounds.json @@ -0,0 +1,250 @@ +{ + "function": "inBounds", + "cases": [ + { + "name": "top-left-corner", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "expected": true + }, + { + "name": "bottom-right-corner", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 19, + "y": 19 + } + ], + "expected": true + }, + { + "name": "x-negative", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": -1, + "y": 0 + } + ], + "expected": false + }, + { + "name": "y-negative", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 0, + "y": -1 + } + ], + "expected": false + }, + { + "name": "x-equals-width", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 20, + "y": 5 + } + ], + "expected": false + }, + { + "name": "y-equals-height", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 5, + "y": 20 + } + ], + "expected": false + }, + { + "name": "middle", + "args": [ + { + "width": 20, + "height": 20, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + { + "x": 10, + "y": 10 + } + ], + "expected": true + } + ] +} diff --git a/fixtures/m03-move.json b/fixtures/m03-move.json new file mode 100644 index 0000000..54bb83b --- /dev/null +++ b/fixtures/m03-move.json @@ -0,0 +1,75 @@ +{ + "function": "move", + "cases": [ + { + "name": "move-up", + "args": [ + { + "x": 3, + "y": 3 + }, + "Up" + ], + "expected": { + "x": 3, + "y": 2 + } + }, + { + "name": "move-down", + "args": [ + { + "x": 3, + "y": 3 + }, + "Down" + ], + "expected": { + "x": 3, + "y": 4 + } + }, + { + "name": "move-left", + "args": [ + { + "x": 3, + "y": 3 + }, + "Left" + ], + "expected": { + "x": 2, + "y": 3 + } + }, + { + "name": "move-right", + "args": [ + { + "x": 3, + "y": 3 + }, + "Right" + ], + "expected": { + "x": 4, + "y": 3 + } + }, + { + "name": "move-does-not-wrap-at-the-edge", + "args": [ + { + "x": 0, + "y": 0 + }, + "Up" + ], + "expected": { + "x": 0, + "y": -1 + } + } + ] +} diff --git a/fixtures/m03-turn.json b/fixtures/m03-turn.json new file mode 100644 index 0000000..b6fa922 --- /dev/null +++ b/fixtures/m03-turn.json @@ -0,0 +1,453 @@ +{ + "function": "turn", + "cases": [ + { + "name": "turn-normal-perpendicular", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + "Up" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-same-direction-is-fine", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + "Right" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-reverse-rejected", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + "Left" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-replaces-pending-turn", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + "Down" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Down", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-against-last-moved-dir", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + }, + "Left" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Up", + "apple": null, + "seed": 1, + "score": 0, + "status": "Playing", + "frame": 0 + } + }, + { + "name": "turn-ignored-when-paused", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Paused", + "frame": 0 + }, + "Up" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Paused", + "frame": 0 + } + }, + { + "name": "turn-ignored-when-game-over", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + }, + "Up" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "GameOver", + "frame": 0 + } + }, + { + "name": "turn-ignored-when-won", + "args": [ + { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Won", + "frame": 0 + }, + "Up" + ], + "expected": { + "width": 5, + "height": 5, + "snake": [ + { + "x": 2, + "y": 2 + }, + { + "x": 1, + "y": 2 + }, + { + "x": 0, + "y": 2 + } + ], + "dir": "Right", + "nextDir": "Right", + "apple": null, + "seed": 1, + "score": 0, + "status": "Won", + "frame": 0 + } + } + ] +} diff --git a/js/solutions/rules.js b/js/solutions/rules.js new file mode 100644 index 0000000..58aebb7 --- /dev/null +++ b/js/solutions/rules.js @@ -0,0 +1,38 @@ +// m03 rules: the pure game rules. Each function takes a state and returns a new one. +import { contains, dropLast } from './util.js' +import { pointEquals, point, delta, opposite } from './model.js' + +/** The cell one step from `p` in direction `dir`. No wrapping: it may land outside the board. */ +export const move = (p, dir) => { + const d = delta(dir) + return point(p.x + d.x, p.y + d.y) +} + +/** True if `p` lies on the board. */ +export const inBounds = (state, p) => p.x >= 0 && p.x < state.width && p.y >= 0 && p.y < state.height + +/** + * Ask to turn. We compare with `dir` (where the snake last moved), not `nextDir`: + * otherwise Up-then-Left within one tick would turn a Right-moving snake back onto itself. + */ +export const turn = (state, dir) => + state.status === 'Playing' && dir !== opposite(state.dir) ? { ...state, nextDir: dir } : state + +/** One step of the game. */ +export const advance = (state) => { + if (state.status !== 'Playing') return state + const d = state.nextDir + const head = move(state.snake[0], d) + const eating = state.apple !== null && pointEquals(head, state.apple) + // When not eating, the tail leaves its cell this step, so the head may move into it. + const body = eating ? state.snake : dropLast(state.snake) + return !inBounds(state, head) || contains(body, head, pointEquals) + ? { ...state, dir: d, status: 'GameOver' } + : { + ...state, + snake: [head, ...body], + dir: d, + apple: eating ? null : state.apple, + score: state.score + (eating ? 1 : 0), + } +} diff --git a/js/src/rules.js b/js/src/rules.js new file mode 100644 index 0000000..af54980 --- /dev/null +++ b/js/src/rules.js @@ -0,0 +1,23 @@ +import { todo } from './todo.js' + +/** The cell one step from `p` in direction `dir`. No wrapping: it may land outside the board. */ +export const move = (p, dir) => todo('m03') + +/** True if `p` lies on the board: `0 <= x < width` and `0 <= y < height`. */ +export const inBounds = (state, p) => todo('m03') + +/** + * Ask to turn the snake. Returns `state` unchanged unless the game is 'Playing' and + * `dir` is not the opposite of `state.dir` (the direction the snake last *moved*, not `nextDir`). + * Otherwise sets `nextDir = dir`. Several turns in one tick: the last allowed one wins. + */ +export const turn = (state, dir) => todo('m03') + +/** + * One step of the game. Returns `state` unchanged unless 'Playing'. Otherwise the head moves in `nextDir` + * (which becomes `dir`): + * - head lands on the apple: the snake grows (tail kept), score + 1, apple becomes null; + * - otherwise the tail cell is vacated, so the head may enter the cell the tail is leaving; + * - head off the board, or onto a remaining body cell: status 'GameOver', snake unchanged (but `dir` updated). + */ +export const advance = (state) => todo('m03') diff --git a/js/test/m03-rules.test.js b/js/test/m03-rules.test.js new file mode 100644 index 0000000..77a0d58 --- /dev/null +++ b/js/test/m03-rules.test.js @@ -0,0 +1,15 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { move, inBounds, turn, advance } from '../src/rules.js' +import { loadFixture, freeze } from './helpers.js' + +const fixtureTests = (name, f) => { + const fx = loadFixture(name) + fx.cases.forEach((c) => + test(`m03 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(f(...freeze(c.args)), c.expected))) +} + +fixtureTests('m03-move', move) +fixtureTests('m03-in-bounds', inBounds) +fixtureTests('m03-turn', turn) +fixtureTests('m03-advance', advance)