const WebSocket = require('ws') const room = 'PERSIST' + Math.floor(Math.random() * 1000) const base = 'ws://127.0.0.1:3000/ws?room=' + room 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, team: msg.team }) } }) 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 } ;(async () => { const p1 = await connect('p1') await waitFor(() => p1.messages.some(m => m.type === 'waiting')) const p2 = await connect('p2') await waitFor(() => p2.messages.some(m => m.type === 'game_state')) // Determine the current player and throw two stones in the same end. let state = latestState(p1.messages) console.log('Initial state', state) // First throw: yellow aims slightly left of center. let turn = state.turn_team let turnPlayer = turn === p1.team ? p1 : p2 turnPlayer.ws.send(JSON.stringify({ type: 'throw', broom_x: -0.3, broom_y: 39, weight: 7, curl: 1, friction: 1.0 })) await waitFor(() => latestState(p1.messages)?.stones?.length === 1, 15000) state = latestState(p1.messages) console.log('After first throw:', state) // Second throw: red aims slightly right of center. turn = state.turn_team turnPlayer = turn === p1.team ? p1 : p2 turnPlayer.ws.send(JSON.stringify({ type: 'throw', broom_x: 0.3, broom_y: 39, weight: 7, curl: -1, friction: 1.0 })) await waitFor(() => latestState(p1.messages)?.stones?.length === 2, 15000) state = latestState(p1.messages) console.log('After second throw:', state) if (state.stones.length !== 2) throw new Error(`Expected 2 stones after second throw, got ${state.stones.length}`) // Check that ids are monotonic. const ids = state.stones.map(s => s.id).sort((a, b) => a - b) if (ids[0] !== 1 || ids[1] !== 2) throw new Error(`Unexpected stone ids: ${ids}`) // 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 ${s.id} is out of house: y=${s.y}`) } console.log('PERSISTENCE E2E PASSED') p1.ws.close() p2.ws.close() process.exit(0) })().catch(err => { console.error(err) process.exit(1) })