const WebSocket = require('ws') const DRAW_VEL = 2.38 const room = 'PERSIST' + Math.floor(Math.random() * 1000) const base = 'ws://127.0.0.1:3000/ws?room=' + room function throwFor(ws, team, broom_x, broom_y, velocity = DRAW_VEL, curl = 0, friction = 1.0) { ws.send(JSON.stringify({ type: 'throw', team, broom_x, broom_y, velocity, curl, friction })) } function connect(name) { return new Promise((resolve, reject) => { const ws = new WebSocket(base) const messages = [] ws.on('message', (data) => { const msg = JSON.parse(data.toString()) messages.push(msg) console.log(`[${name}]`, msg.type, msg.type === 'game_state' ? ` turn=${msg.turn_team} stones=${msg.stones.length}` : '') if (msg.type === 'joined') { resolve({ ws, messages }) } }) ws.on('open', () => console.log(`[${name}] open`)) ws.on('error', (e) => { console.error(`[${name}] error`, e.message); reject(e) }) ws.on('close', (code) => console.log(`[${name}] close`, code)) }) } async function waitFor(condFn, timeout = 5000) { const start = Date.now() while (!condFn()) { if (Date.now() - start > timeout) throw new Error('Timeout waiting') await new Promise(r => setTimeout(r, 50)) } } function latestState(messages) { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].type === 'game_state') return messages[i] } return null } function stoneIdKey(id) { return `${id.team}:${id.n}` } ;(async () => { const p1 = await connect('p1') await waitFor(() => p1.messages.some(m => m.type === 'game_state')) let state = latestState(p1.messages) console.log('Initial state', { turn_team: state.turn_team, stones_remaining: state.stones_remaining, scoreboard: state.scoreboard, }) // First throw: DRAW_VEL + curl 0 + house center keeps stone in play. let turn = state.turn_team const firstTeam = turn throwFor(p1.ws, turn, -0.3, 38.5, DRAW_VEL, 0, 1.0) await waitFor(() => latestState(p1.messages)?.stones?.length === 1, 15000) state = latestState(p1.messages) console.log('After first throw:', state.stones) // Second throw: other team, offset so both stay. turn = state.turn_team const secondTeam = turn throwFor(p1.ws, turn, 0.3, 38.5, DRAW_VEL, 0, 1.0) await waitFor(() => latestState(p1.messages)?.stones?.length === 2, 15000) state = latestState(p1.messages) console.log('After second throw:', state.stones) if (state.stones.length !== 2) throw new Error(`Expected 2 stones after second throw, got ${state.stones.length}`) // Stone ids are {team, n}, not flat numbers — each team's first stone is n=1. const ids = state.stones.map(s => stoneIdKey(s.id)).sort() const expected = [stoneIdKey({ team: firstTeam, n: 1 }), stoneIdKey({ team: secondTeam, n: 1 })].sort() if (ids[0] !== expected[0] || ids[1] !== expected[1]) { throw new Error(`Unexpected stone ids: ${JSON.stringify(ids)} expected ${JSON.stringify(expected)}`) } for (const s of state.stones) { if (typeof s.id !== 'object' || !s.id.team || typeof s.id.n !== 'number') { throw new Error(`Stone id must be {team,n}, got ${JSON.stringify(s.id)}`) } } // Both stones should be in play (near house). for (const s of state.stones) { if (s.y < 35 || s.y > 42) throw new Error(`Stone ${stoneIdKey(s.id)} is out of house: y=${s.y}`) } if (!Array.isArray(state.scoreboard)) throw new Error('scoreboard missing') if (!Array.isArray(state.stones_remaining) || state.stones_remaining.length !== 2) { throw new Error(`stones_remaining must be [u8,u8], got ${JSON.stringify(state.stones_remaining)}`) } console.log('PERSISTENCE E2E PASSED') p1.ws.close() process.exit(0) })().catch(err => { console.error(err) process.exit(1) })