// Conway's Life (life.html) on every track, judged against the pure model. import { test, expect, TRACKS } from '../lib/app.mjs' import { FRAME } from '../lib/clock.mjs' import { GEN_MS, LCELL } from '../lib/oracle.mjs' const GREEN = '#7cb342' const lastScene = (shapes) => shapes.slice(Math.max(0, shapes.findLastIndex((s) => s.tag === 'Rect' && s.x === 0 && s.y === 0 && s.w === 640))) // Live cells as the page drew them: the green squares, back in cell coordinates. const cellsOf = (shapes) => lastScene(shapes).filter((s) => s.tag === 'Rect' && s.color === GREEN) .map((s) => ({ x: (s.x - 1) / LCELL, y: (s.y - 1) / LCELL })) const generationOf = (shapes) => Number(/^Generation (\d+)$/.exec(lastScene(shapes).find((s) => s.tag === 'Text' && s.text.startsWith('Generation')).text)[1]) const rowMajor = (cells) => [...cells].sort((p, q) => p.y - q.y || p.x - q.x) const beats = (ms, a, b) => Math.floor(b / ms) - Math.floor(a / ms) const lastFrameTime = (a) => Math.floor(a.clock.now / FRAME) * FRAME - a.clock.t0 for (const track of TRACKS) { test.describe(`${track}: life`, () => { test('clicks on a CSS-scaled canvas with a border toggle the cells the model says', async ({ app, page }) => { const a = await (await app(track, 'life')).start() // Show the 640 x 504 canvas 1.5 times as large; its 2 px border stays. Clicks must still land on the right cells. await page.locator('canvas').evaluate((c) => { c.style.width = `${c.width * 1.5}px` }) expect(await page.locator('canvas').evaluate((c) => [c.clientWidth, c.clientHeight, c.clientLeft])).toEqual([960, 756, 2]) await a.expectInSync() const c = (x, y) => [x * LCELL + 8, y * LCELL + 8] // the centre of cell (x, y) const clicks = [ ...[c(10, 10), c(11, 10), c(12, 10)], // cell centres [0, 0], // the first pixel of the board: cell (0, 0) [639, 479], // the last pixel of the board: cell (39, 29) [5 * LCELL, 7 * LCELL + 15], // the left edge of cell (5, 7), bottom row of its pixels [6 * LCELL - 1, 7 * LCELL], // the right edge of the same cell, top row: switches it off again [6 * LCELL, 7 * LCELL], // one pixel further right: cell (6, 7) [320, 490], // the status strip under the board: no cell ] for (const [x, y] of clicks) { await a.click(x, y) await a.expectInSync(`click at canvas pixel (${x}, ${y})`) } await a.advance(200) const drawn = await a.expectInSync() const want = rowMajor([{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 11, y: 10 }, { x: 12, y: 10 }, { x: 39, y: 29 }, { x: 6, y: 7 }]) expect(cellsOf(drawn)).toEqual(want) expect(a.state.cells).toEqual(want) }) test('N steps, Space runs one generation per 150 ms, C clears, R fills the board as the model says', async ({ app }) => { const a = await (await app(track, 'life')).start() for (const x of [10, 11, 12]) await a.click(x * LCELL + 8, 10 * LCELL + 8) await a.press('n') const stepped = await a.expectInSync('after N') expect(cellsOf(stepped)).toEqual([{ x: 11, y: 9 }, { x: 11, y: 10 }, { x: 11, y: 11 }]) expect(generationOf(stepped)).toBe(1) await a.advance(40) await a.press(' ') const startedAt = lastFrameTime(a) await a.expectInSync('running') for (let i = 0; i < 8; i++) { await a.advance(GEN_MS / 2) const f = await a.expectInSync(`running, ${i}`) expect(generationOf(f)).toBe(1 + beats(GEN_MS, startedAt, lastFrameTime(a))) } await a.press(' ') const pausedGen = generationOf(await a.expectInSync('paused')) await a.advance(600) expect(generationOf(await a.expectInSync('paused for 600 ms'))).toBe(pausedGen) await a.press('c') const cleared = await a.expectInSync('after C') expect(cellsOf(cleared)).toEqual([]) expect(generationOf(cleared)).toBe(0) await a.press('r') const filled = await a.expectInSync('after R') expect(cellsOf(filled)).toEqual(a.state.cells) expect(cellsOf(filled).length).toBeGreaterThan(100) await a.press('n') await a.advance(300) await a.expectInSync('after R and N') }) }) }