// 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')
})
})
}