const WebSocket = require('ws') const DRAW_VEL = 2.38 const base = (room) => `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, room) { return new Promise((resolve, reject) => { const ws = new WebSocket(base(room)) const messages = [] ws.on('message', (data) => { const msg = JSON.parse(data.toString()) messages.push(msg) if (msg.type === 'joined') resolve({ ws, messages }) }) ws.on('error', reject) }) } function waitFor(messages, pred, timeout = 30000) { const start = Date.now() return new Promise((resolve, reject) => { const check = () => { if (pred()) return resolve(undefined) if (Date.now() - start > timeout) return reject(new Error('timeout')) setTimeout(check, 50) } check() }) } 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 room = 'ENDQA' + Math.floor(Math.random() * 1000) const p1 = await connect('p1', room) const p2 = await connect('p2', room) await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'game_state'), 5000) const initial = latestState(p1.messages) const startEnd = initial.end const startScoreboardLen = (initial.scoreboard || []).length console.log('start end=', startEnd, 'scoreboard len=', startScoreboardLen) // 16 throws (8 per team) complete one end. NO end_scored message — // assert scoreboard length increase + end advance on game_state. for (let i = 0; i < 16; i++) { await waitFor(p1.messages, () => { const st = latestState(p1.messages) return st && st.phase === 'playing' }, 15000) const st = latestState(p1.messages) // If end already advanced mid-loop, stop early. if ((st.scoreboard || []).length > startScoreboardLen && st.end > startEnd) { console.log('end advanced early at throw', i) break } const turn = st.turn_team if (!turn) throw new Error('no turn') // Keep broom near house center so draws stay in play for scoring. const broomX = ((i % 8) - 3.5) * 0.08 const prevStateCount = p1.messages.filter(m => m.type === 'game_state').length throwFor(p1.ws, turn, broomX, 38.5, DRAW_VEL, 0, 1.0) await waitFor(p1.messages, () => { const n = p1.messages.filter(m => m.type === 'game_state').length const err = p1.messages.filter(m => m.type === 'error').pop() if (err && n <= prevStateCount) throw new Error('throw error: ' + err.message) return n > prevStateCount }, 30000) } // Wait for scoreboard entry + end advance (no end_scored type). await waitFor(p1.messages, () => { const st = latestState(p1.messages) if (!st) return false const sb = st.scoreboard || [] return sb.length > startScoreboardLen && st.end > startEnd }, 30000) // Ensure we never saw legacy end_scored if (p1.messages.some(m => m.type === 'end_scored')) { throw new Error('legacy end_scored message must not appear') } const final = latestState(p1.messages) console.log('Final game_state:', { end: final.end, scores: final.scores, scoreboard: final.scoreboard, phase: final.phase, }) if (!final.scoreboard || final.scoreboard.length < 1) { throw new Error('expected scoreboard entry after end') } const entry = final.scoreboard[0] if (typeof entry.end !== 'number' || entry.end < 1) { throw new Error(`bad scoreboard entry: ${JSON.stringify(entry)}`) } if (final.end <= startEnd) throw new Error('end did not advance') console.log('Scoreboard entry received; end advanced to', final.end) p1.ws.close() p2.ws.close() process.exit(0) })().catch((e) => { console.error(e) process.exit(1) })