Public: anyone can read this; the crew can change it.
Migrated from github.com/amirmushichge/pixel-plays
Bumper Cars rules: a tested `bumper-cars/floor.js` where you steer a car on a walled floor and shove three seeded rivals into the wall without a head-on bump, from plain data, in a two-file pull with a test file that runs in under a second #336
Check · Review · Land · Live done — Landed and live in production.
torque/pixel-plays:torque/320-bumper-cars-rules-a-tested-bumper-cars-f → main 2 commits fork
2 files
changed +380
−0
bumper-cars/floor.js added +159
| @@ -0,0 +1,159 @@ | ||
| 1 | 'use strict'; | |
| 2 | // The Bumper Cars rules: steer a car on a walled floor and shove three seeded rivals into the wall without a head-on bump. | |
| 3 | // Plain data in, plain data out, no DOM, no timers, no Math.random: every random choice comes from the seed | |
| 4 | // (state.rng is the generator's state). A refused call returns the very same state and says why. | |
| 5 | // | |
| 6 | // Units: squares and ticks. The floor is SIZE x SIZE squares, x to the right, y downwards, (0,0) top-left. | |
| 7 | // Headings are 0 north (0,-1), 1 east (1,0), 2 south (0,1), 3 west (-1,0); a right turn is +1, a left turn -1 (mod 4). | |
| 8 | // One tick, in this order: your car (the turn you steered, then one square), then rivals 1, 2, 3 (each its next pattern turn, then one square). | |
| 9 | // Wall: a car that would leave the floor stays put and turns right instead. | |
| 10 | // You move into a rival: | |
| 11 | // head-on (its heading is opposite yours) -> you lose a life, stay, and turn about; | |
| 12 | // back or side -> it is shoved one square the way you were going and you take its square. | |
| 13 | // Shoved off the floor (into the wall): SHOVE_SCORE points, a wall shove is counted, and it is placed again | |
| 14 | // at a seeded free square. Shoved into another car: no shove, you stay. | |
| 15 | // A rival moving into a car: into you head-on costs you a life; any other blocked move leaves it put, turning right. | |
| 16 | // The round ends when lives run out (a loss) or after TICKS ticks (a win, reporting score and wall shoves). | |
| 17 | // mulberry32 is the generator (public-domain construction by Tommy Ettinger). | |
| 18 | (function (root) { | |
| 19 | const SIZE = 9; | |
| 20 | const RIVALS = 3; | |
| 21 | const LIVES = 3; | |
| 22 | const TICKS = 60; | |
| 23 | const SHOVE_SCORE = 10; | |
| 24 | const PATTERN = 4; // turns in a rival's repeating pattern | |
| 25 | const DIRS = [[0, -1], [1, 0], [0, 1], [-1, 0]]; | |
| 26 | const TURNS = { left: -1, straight: 0, right: 1 }; | |
| 27 | const START = { x: 4, y: 8, h: 0 }; // bottom middle, facing north | |
| 28 | ||
| 29 | // mulberry32 as a pure step: next(state) -> [value in [0,1), nextState] | |
| 30 | function next(r) { | |
| 31 | r = (r + 0x6D2B79F5) >>> 0; | |
| 32 | let t = r; | |
| 33 | t = Math.imul(t ^ (t >>> 15), t | 1); | |
| 34 | t ^= t + Math.imul(t ^ (t >>> 7), t | 61); | |
| 35 | return [((t ^ (t >>> 14)) >>> 0) / 4294967296, r]; | |
| 36 | } | |
| 37 | ||
| 38 | const turnBy = (h, d) => (h + d + 4) % 4; | |
| 39 | const inside = (x, y) => x >= 0 && y >= 0 && x < SIZE && y < SIZE; | |
| 40 | const carAt = (s, x, y) => { | |
| 41 | if (s.player.x === x && s.player.y === y) return 0; | |
| 42 | const i = s.rivals.findIndex(r => r.x === x && r.y === y); | |
| 43 | return i < 0 ? -1 : i + 1; | |
| 44 | }; | |
| 45 | ||
| 46 | // freeSquare(state, rng) -> [{x,y}, rng]: a seeded square with no car on it | |
| 47 | function freeSquare(s, rng) { | |
| 48 | const free = []; | |
| 49 | for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) if (carAt(s, x, y) < 0) free.push({ x, y }); | |
| 50 | let v; [v, rng] = next(rng); | |
| 51 | return [free[Math.floor(v * free.length)], rng]; | |
| 52 | } | |
| 53 | ||
| 54 | function describe(s) { | |
| 55 | if (s.status === 'over') { | |
| 56 | const r = s.result; | |
| 57 | return r.won | |
| 58 | ? `Round won: score ${r.score}, ${r.shoves} wall shove${r.shoves === 1 ? '' : 's'}` | |
| 59 | : `Round lost: out of lives at tick ${r.tick}, score ${r.score}`; | |
| 60 | } | |
| 61 | return `Tick ${s.tick} of ${s.ticks}: score ${s.score}, ${s.lives} li${s.lives === 1 ? 'fe' : 'ves'}, ${s.shoves} wall shove${s.shoves === 1 ? '' : 's'}`; | |
| 62 | } | |
| 63 | ||
| 64 | // start(seed) -> a fresh round: floor, your car, three seeded rivals with headings and turn patterns | |
| 65 | function start(seed) { | |
| 66 | const sd = Number.isFinite(seed) ? Math.trunc(seed) >>> 0 : 0; | |
| 67 | let rng = sd; | |
| 68 | const s = { | |
| 69 | seed: sd, rng, status: 'playing', tick: 0, ticks: TICKS, size: SIZE, lives: LIVES, | |
| 70 | score: 0, shoves: 0, turn: 'straight', | |
| 71 | player: { ...START }, rivals: [], result: null, line: '', | |
| 72 | }; | |
| 73 | for (let i = 0; i < RIVALS; i++) { | |
| 74 | let sq, v; | |
| 75 | [sq, rng] = freeSquare(s, rng); | |
| 76 | [v, rng] = next(rng); const h = Math.floor(v * 4); | |
| 77 | const pattern = []; | |
| 78 | for (let k = 0; k < PATTERN; k++) { [v, rng] = next(rng); pattern.push(['left', 'straight', 'right'][Math.floor(v * 3)]); } | |
| 79 | s.rivals.push({ x: sq.x, y: sq.y, h, pattern, at: 0 }); | |
| 80 | } | |
| 81 | s.rng = rng; | |
| 82 | s.line = describe(s); | |
| 83 | return s; | |
| 84 | } | |
| 85 | ||
| 86 | const reject = (state, why) => ({ ok: false, state, why }); | |
| 87 | const clone = s => ({ ...s, player: { ...s.player }, rivals: s.rivals.map(r => ({ ...r })) }); | |
| 88 | ||
| 89 | // steer(state, turn) -> { ok, state, why }: your next turn, 'left', 'right' or 'straight' | |
| 90 | function steer(state, turn) { | |
| 91 | if (!state || state.status !== 'playing') return reject(state, 'round-over'); | |
| 92 | if (typeof turn !== 'string' || !Object.hasOwn(TURNS, turn)) return reject(state, 'unknown-turn'); | |
| 93 | const s = clone(state); | |
| 94 | s.turn = turn; | |
| 95 | return { ok: true, state: s, why: '' }; | |
| 96 | } | |
| 97 | ||
| 98 | // step(state) -> { ok, state, why, events }: one tick in the stated order | |
| 99 | function step(state) { | |
| 100 | if (!state || state.status !== 'playing') return reject(state, 'round-over'); | |
| 101 | const s = clone(state), p = s.player, events = []; | |
| 102 | s.tick += 1; | |
| 103 | ||
| 104 | // your car | |
| 105 | p.h = turnBy(p.h, TURNS[s.turn]); s.turn = 'straight'; | |
| 106 | const [dx, dy] = DIRS[p.h], nx = p.x + dx, ny = p.y + dy; | |
| 107 | if (!inside(nx, ny)) { p.h = turnBy(p.h, 1); events.push({ kind: 'wall', car: 0 }); } | |
| 108 | else { | |
| 109 | const hit = carAt(s, nx, ny); | |
| 110 | if (hit < 0) { p.x = nx; p.y = ny; } | |
| 111 | else { | |
| 112 | const r = s.rivals[hit - 1]; | |
| 113 | if (r.h === turnBy(p.h, 2)) { | |
| 114 | s.lives -= 1; p.h = turnBy(p.h, 2); | |
| 115 | events.push({ kind: 'head-on', car: hit }); | |
| 116 | } else { | |
| 117 | const sx = r.x + dx, sy = r.y + dy; | |
| 118 | if (!inside(sx, sy)) { | |
| 119 | s.score += SHOVE_SCORE; s.shoves += 1; | |
| 120 | p.x = r.x; p.y = r.y; | |
| 121 | let sq; [sq, s.rng] = freeSquare(s, s.rng); // r still stands on its old square, now yours: not free | |
| 122 | r.x = sq.x; r.y = sq.y; | |
| 123 | events.push({ kind: 'wall-shove', car: hit, score: SHOVE_SCORE }); | |
| 124 | } else if (carAt(s, sx, sy) < 0) { | |
| 125 | r.x = sx; r.y = sy; p.x = nx; p.y = ny; | |
| 126 | events.push({ kind: 'shove', car: hit }); | |
| 127 | } else events.push({ kind: 'blocked', car: hit }); | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | // rivals | |
| 133 | s.rivals.forEach((r, i) => { | |
| 134 | if (s.lives <= 0) return; | |
| 135 | r.h = turnBy(r.h, TURNS[r.pattern[r.at % r.pattern.length]]); r.at += 1; | |
| 136 | const [ex, ey] = DIRS[r.h], tx = r.x + ex, ty = r.y + ey; | |
| 137 | if (!inside(tx, ty)) { r.h = turnBy(r.h, 1); return; } | |
| 138 | const hit = carAt(s, tx, ty); | |
| 139 | if (hit < 0) { r.x = tx; r.y = ty; return; } | |
| 140 | if (hit === 0 && p.h === turnBy(r.h, 2)) { | |
| 141 | s.lives -= 1; events.push({ kind: 'head-on', car: i + 1 }); | |
| 142 | } | |
| 143 | r.h = turnBy(r.h, 1); | |
| 144 | }); | |
| 145 | ||
| 146 | if (s.lives <= 0) { | |
| 147 | s.lives = 0; s.status = 'over'; | |
| 148 | s.result = { won: false, tick: s.tick, score: s.score, shoves: s.shoves }; | |
| 149 | } else if (s.tick >= s.ticks) { | |
| 150 | s.status = 'over'; | |
| 151 | s.result = { won: true, tick: s.tick, score: s.score, shoves: s.shoves }; | |
| 152 | } | |
| 153 | s.line = describe(s); | |
| 154 | return { ok: true, state: s, why: '', events }; | |
| 155 | } | |
| 156 | ||
| 157 | const api = { SIZE, RIVALS, LIVES, TICKS, SHOVE_SCORE, START, start, steer, step, describe }; | |
| 158 | if (typeof module !== 'undefined') module.exports = api; else root.BumperCars = api; | |
| 159 | })(typeof window !== 'undefined' ? window : globalThis); |
bumper-cars/floor.test.mjs added +221
| @@ -0,0 +1,221 @@ | ||
| 1 | import { test, expect } from 'bun:test'; | |
| 2 | import { SIZE, LIVES, TICKS, SHOVE_SCORE, START, start, steer, step } from './floor.js'; | |
| 3 | ||
| 4 | const ok = r => { expect(r.ok).toBe(true); return r.state; }; | |
| 5 | // Rivals that are not part of a test wait here, one slot each, facing the wall they stand against (so they | |
| 6 | // turn in place on the first tick and never come near the middle of the floor). | |
| 7 | const PARKED = [{ x: 0, y: 0, h: 0 }, { x: 8, y: 0, h: 0 }, { x: 8, y: 8, h: 2 }]; | |
| 8 | // set(rivals, player): a floor where rivals[i] replaces slot i; the others are parked. Rival patterns default to straight. | |
| 9 | const set = (rivals = [], player = {}) => { | |
| 10 | const s = start(1); | |
| 11 | s.player = { ...START, ...player }; | |
| 12 | s.rivals = PARKED.map((p, i) => ({ pattern: ['straight'], at: 0, ...(rivals[i] || p) })); | |
| 13 | return s; | |
| 14 | }; | |
| 15 | const pos = c => [c.x, c.y]; | |
| 16 | const kinds = r => r.events.map(e => e.kind); | |
| 17 | ||
| 18 | test('start: stated floor, lives, ticks, your car and three seeded rivals; same seed same rivals', () => { | |
| 19 | const a = start(7), b = start(7), c = start(8); | |
| 20 | expect([a.size, a.lives, a.ticks, a.status]).toEqual([SIZE, LIVES, TICKS, 'playing']); | |
| 21 | expect(a.player).toEqual(START); | |
| 22 | expect(a.rivals.length).toBe(3); | |
| 23 | expect(a.rivals).toEqual(b.rivals); | |
| 24 | expect(a.rivals).not.toEqual(c.rivals); | |
| 25 | for (let seed = 0; seed < 50; seed++) { | |
| 26 | const s = start(seed), seen = new Set(['4,8']); | |
| 27 | for (const r of s.rivals) { | |
| 28 | expect(r.x >= 0 && r.x < SIZE && r.y >= 0 && r.y < SIZE).toBe(true); | |
| 29 | expect(r.pattern.length).toBe(4); | |
| 30 | seen.add(r.x + ',' + r.y); | |
| 31 | } | |
| 32 | expect(seen.size).toBe(4); | |
| 33 | } | |
| 34 | expect(start(NaN).seed).toBe(0); | |
| 35 | }); | |
| 36 | ||
| 37 | test('the same seed and inputs give the same round', () => { | |
| 38 | const run = seed => { | |
| 39 | let s = start(seed); | |
| 40 | for (let i = 0; s.status === 'playing'; i++) { | |
| 41 | s = ok(steer(s, ['left', 'straight', 'right'][i % 3])); | |
| 42 | s = ok(step(s)); | |
| 43 | } | |
| 44 | return s; | |
| 45 | }; | |
| 46 | expect(run(3)).toEqual(run(3)); | |
| 47 | expect(run(3).rivals).not.toEqual(run(4).rivals); | |
| 48 | }); | |
| 49 | ||
| 50 | test('steer sets the next turn, which step applies once', () => { | |
| 51 | let s = ok(steer(start(1), 'right')); | |
| 52 | expect(s.turn).toBe('right'); | |
| 53 | s.rivals.forEach(r => { r.pattern = ['straight']; }); | |
| 54 | const before = start(1); | |
| 55 | s = ok(step(s)); | |
| 56 | expect(s.player.h).toBe(1); | |
| 57 | expect(s.turn).toBe('straight'); | |
| 58 | expect(before.player.h).toBe(0); | |
| 59 | }); | |
| 60 | ||
| 61 | test('refusals return the same state with a reason', () => { | |
| 62 | const s = start(1); | |
| 63 | for (const t of ['up', '', 5, null, undefined, 'toString', '__proto__']) { | |
| 64 | const r = steer(s, t); | |
| 65 | expect(r.ok).toBe(false); expect(r.why).toBe('unknown-turn'); expect(r.state).toBe(s); | |
| 66 | } | |
| 67 | const over = { ...s, status: 'over' }; | |
| 68 | const a = steer(over, 'left'), b = step(over); | |
| 69 | expect([a.ok, a.why, a.state]).toEqual([false, 'round-over', over]); | |
| 70 | expect([b.ok, b.why, b.state]).toEqual([false, 'round-over', over]); | |
| 71 | }); | |
| 72 | ||
| 73 | test('a car at the wall stops and turns right', () => { | |
| 74 | const s = set([], { x: 4, y: 0, h: 0 }); | |
| 75 | const t = ok(step(s)); | |
| 76 | expect([t.player.x, t.player.y, t.player.h]).toEqual([4, 0, 1]); | |
| 77 | // a rival too | |
| 78 | const u = set([{ x: 8, y: 5, h: 1 }]); | |
| 79 | const v = ok(step(u)); | |
| 80 | expect([v.rivals[0].x, v.rivals[0].y, v.rivals[0].h]).toEqual([8, 5, 2]); | |
| 81 | }); | |
| 82 | ||
| 83 | test('a back bump shoves the rival one square the way you were going', () => { | |
| 84 | const s = set([{ x: 4, y: 7, h: 0 }]); // ahead of you, facing the same way | |
| 85 | s.rivals[0].pattern = ['straight']; | |
| 86 | const t = ok(step(s)); | |
| 87 | expect([t.player.x, t.player.y]).toEqual([4, 7]); | |
| 88 | // shoved to (4,6), then it drives north one square | |
| 89 | expect([t.rivals[0].x, t.rivals[0].y]).toEqual([4, 5]); | |
| 90 | expect(t.lives).toBe(LIVES); | |
| 91 | }); | |
| 92 | ||
| 93 | test('a side bump shoves the rival one square the way you were going', () => { | |
| 94 | const s = set([{ x: 4, y: 7, h: 1 }]); // facing east across your path | |
| 95 | const t = ok(step(s)); | |
| 96 | expect([t.player.x, t.player.y]).toEqual([4, 7]); | |
| 97 | // shoved north first, then it drives east one square | |
| 98 | expect([t.rivals[0].x, t.rivals[0].y]).toEqual([5, 6]); | |
| 99 | expect(t.lives).toBe(LIVES); | |
| 100 | }); | |
| 101 | ||
| 102 | test('a shove into the wall scores and the rival is placed again at a free square', () => { | |
| 103 | const s = set([{ x: 4, y: 1, h: 0 }], { x: 4, y: 2, h: 0 }); | |
| 104 | let t = ok(step(s)); | |
| 105 | expect([t.player.x, t.player.y]).toEqual([4, 1]); | |
| 106 | expect([t.rivals[0].x, t.rivals[0].y]).toEqual([4, 0]); | |
| 107 | t = ok(step(t)); // now the rival stands at (4,0); you at (4,1) bump it into the wall | |
| 108 | expect(t.score).toBe(SHOVE_SCORE); | |
| 109 | expect(t.shoves).toBe(1); | |
| 110 | const r = t.rivals[0]; | |
| 111 | expect(r.y === 0 && r.x === 4).toBe(false); | |
| 112 | const taken = [t.player, t.rivals[1], t.rivals[2]].map(c => c.x + ',' + c.y); | |
| 113 | expect(taken).not.toContain(r.x + ',' + r.y); | |
| 114 | expect(t.line).toContain('score 10'); | |
| 115 | expect(ok(step(set([{ x: 4, y: 0, h: 0 }], { x: 4, y: 1, h: 0 }))).score).toBe(SHOVE_SCORE); | |
| 116 | }); | |
| 117 | ||
| 118 | test('a head-on bump costs a life', () => { | |
| 119 | const s = set([{ x: 4, y: 7, h: 2 }]); // facing you | |
| 120 | const t = ok(step(s)); | |
| 121 | expect(t.lives).toBe(LIVES - 1); | |
| 122 | expect(t.score).toBe(0); | |
| 123 | expect(t.player.h).toBe(2); | |
| 124 | }); | |
| 125 | ||
| 126 | test('the loss line when lives run out', () => { | |
| 127 | let s = set([{ x: 4, y: 7, h: 2 }]); | |
| 128 | s.lives = 1; | |
| 129 | s = ok(step(s)); | |
| 130 | expect(s.status).toBe('over'); | |
| 131 | expect(s.result.won).toBe(false); | |
| 132 | expect(s.line).toBe('Round lost: out of lives at tick 1, score 0'); | |
| 133 | expect(step(s).why).toBe('round-over'); | |
| 134 | }); | |
| 135 | ||
| 136 | test('the win line reports score and wall shoves when the ticks run out', () => { | |
| 137 | let s = set([], { x: 4, y: 4, h: 0 }); | |
| 138 | s.tick = TICKS - 1; s.score = 20; s.shoves = 2; | |
| 139 | s = ok(step(s)); | |
| 140 | expect(s.status).toBe('over'); | |
| 141 | expect(s.result).toEqual({ won: true, tick: TICKS, score: 20, shoves: 2 }); | |
| 142 | expect(s.line).toBe('Round won: score 20, 2 wall shoves'); | |
| 143 | }); | |
| 144 | ||
| 145 | test('the input state is never changed', () => { | |
| 146 | const s = start(5), copy = JSON.parse(JSON.stringify(s)); | |
| 147 | step(steer(s, 'left').state); | |
| 148 | expect(s).toEqual(copy); | |
| 149 | }); | |
| 150 | ||
| 151 | test('step reports events: wall, shove, wall-shove and head-on', () => { | |
| 152 | expect(step(set([], { x: 4, y: 0, h: 0 })).events).toEqual([{ kind: 'wall', car: 0 }]); | |
| 153 | expect(kinds(step(set([{ x: 4, y: 7, h: 0 }])))).toEqual(['shove']); | |
| 154 | expect(step(set([{ x: 4, y: 0, h: 0 }], { x: 4, y: 1, h: 0 })).events) | |
| 155 | .toEqual([{ kind: 'wall-shove', car: 1, score: SHOVE_SCORE }]); | |
| 156 | expect(step(set([{ x: 4, y: 7, h: 2 }])).events).toEqual([{ kind: 'head-on', car: 1 }]); | |
| 157 | expect(step(set([], { x: 4, y: 4 })).events).toEqual([]); | |
| 158 | }); | |
| 159 | ||
| 160 | test('a rival driving head-on into your car costs a life', () => { | |
| 161 | // you advance to (4,3); the rival from (4,2) faces south and drives into you | |
| 162 | const r = step(set([{ x: 4, y: 2, h: 2 }], { x: 4, y: 4, h: 0 })); | |
| 163 | expect(r.state.lives).toBe(LIVES - 1); | |
| 164 | expect(r.events).toEqual([{ kind: 'head-on', car: 1 }]); | |
| 165 | expect(pos(r.state.rivals[0])).toEqual([4, 2]); // it stayed put and turned right | |
| 166 | expect(r.state.rivals[0].h).toBe(3); | |
| 167 | expect(pos(r.state.player)).toEqual([4, 3]); | |
| 168 | }); | |
| 169 | ||
| 170 | test('a rival head-on bump on your last life ends the round with the loss line', () => { | |
| 171 | const s = set([{ x: 4, y: 2, h: 2 }], { x: 4, y: 4, h: 0 }); | |
| 172 | s.lives = 1; | |
| 173 | const t = ok(step(s)); | |
| 174 | expect([t.status, t.lives, t.result.won]).toEqual(['over', 0, false]); | |
| 175 | expect(t.line).toBe('Round lost: out of lives at tick 1, score 0'); | |
| 176 | }); | |
| 177 | ||
| 178 | test('a rival blocked by your side or back does not cost a life; it stays and turns right', () => { | |
| 179 | // you advance to (4,3); the rival at (3,3) faces east into your side | |
| 180 | const t = ok(step(set([{ x: 3, y: 3, h: 1 }], { x: 4, y: 4, h: 0 }))); | |
| 181 | expect(t.lives).toBe(LIVES); | |
| 182 | expect(pos(t.rivals[0])).toEqual([3, 3]); | |
| 183 | expect(t.rivals[0].h).toBe(2); | |
| 184 | }); | |
| 185 | ||
| 186 | test('a rival blocked by another rival stays and turns right', () => { | |
| 187 | const t = ok(step(set([{ x: 1, y: 1, h: 1 }, { x: 2, y: 1, h: 1 }]))); | |
| 188 | expect(pos(t.rivals[0])).toEqual([1, 1]); | |
| 189 | expect(t.rivals[0].h).toBe(2); | |
| 190 | expect(pos(t.rivals[1])).toEqual([3, 1]); | |
| 191 | }); | |
| 192 | ||
| 193 | test('a shove into another car is refused: you stay put and a blocked event is returned', () => { | |
| 194 | // rival 1 sits across your path at (4,3); the shove would push it north onto rival 2 at (4,2) | |
| 195 | const r = step(set([{ x: 4, y: 3, h: 1 }, { x: 4, y: 2, h: 1 }], { x: 4, y: 4, h: 0 })); | |
| 196 | expect(r.events).toEqual([{ kind: 'blocked', car: 1 }]); | |
| 197 | expect(pos(r.state.player)).toEqual([4, 4]); | |
| 198 | expect(r.state.score).toBe(0); | |
| 199 | expect(r.state.lives).toBe(LIVES); | |
| 200 | }); | |
| 201 | ||
| 202 | test('rivals stop moving once lives hit 0 within the tick', () => { | |
| 203 | const s = set([{ x: 4, y: 3, h: 2 }, { x: 1, y: 1, h: 1 }], { x: 4, y: 4, h: 0 }); | |
| 204 | s.lives = 1; | |
| 205 | const t = ok(step(s)); | |
| 206 | expect(t.status).toBe('over'); | |
| 207 | expect(pos(t.rivals[1])).toEqual([1, 1]); | |
| 208 | expect(t.rivals[1].at).toBe(0); | |
| 209 | // with a life to spare the same rival does move | |
| 210 | const u = ok(step(set([{ x: 4, y: 3, h: 2 }, { x: 1, y: 1, h: 1 }], { x: 4, y: 4, h: 0 }))); | |
| 211 | expect(pos(u.rivals[1])).toEqual([2, 1]); | |
| 212 | }); | |
| 213 | ||
| 214 | test('lines use singular wording for one life and one wall shove', () => { | |
| 215 | const s = set([], { x: 4, y: 4, h: 0 }); | |
| 216 | s.lives = 1; s.shoves = 1; s.score = SHOVE_SCORE; | |
| 217 | expect(ok(step(s)).line).toBe('Tick 1 of 60: score 10, 1 life, 1 wall shove'); | |
| 218 | const w = set(); w.tick = TICKS - 1; w.shoves = 1; w.score = SHOVE_SCORE; | |
| 219 | expect(ok(step(w)).line).toBe('Round won: score 10, 1 wall shove'); | |
| 220 | expect(start(1).line).toBe('Tick 0 of 60: score 0, 3 lives, 0 wall shoves'); | |
| 221 | }); |
new above
offline