diff --git a/e2e/collision_trajectory_qa.cjs b/e2e/collision_trajectory_qa.cjs index ffdd0d2..35bbaab 100644 --- a/e2e/collision_trajectory_qa.cjs +++ b/e2e/collision_trajectory_qa.cjs @@ -1,7 +1,14 @@ const WebSocket = require('ws') + +const DRAW_VEL = 2.38 +const TAKEOUT_VEL = DRAW_VEL * 1.4 // ~3.33 — enough to move the stationary stone const room = 'COLQA' + Math.floor(Math.random() * 1000) const url = '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() { return new Promise((resolve, reject) => { const ws = new WebSocket(url) @@ -24,46 +31,83 @@ function waitFor(messages, pred, timeout = 10000) { }) } +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 c = await connect() await waitFor(c.messages, () => { - const last = c.messages[c.messages.length - 1] - return last && last.type === 'game_state' && last.phase === 'playing' + const st = latestState(c.messages) + return st && st.phase === 'playing' }) - // First throw: weight 7 so it stays in house. - let state = c.messages[c.messages.length - 1] - c.ws.send(JSON.stringify({ type: 'throw', team: state.turn_team, broom_x: 0.0, broom_y: 38.5, weight: 7, curl: 0, friction: 1.0 })) - await waitFor(c.messages, () => c.messages.filter(m => m.type === 'game_state').length > 1) - state = c.messages[c.messages.length - 1] + // First throw: DRAW_VEL so it stays in house. + let state = latestState(c.messages) + const firstTeam = state.turn_team + throwFor(c.ws, firstTeam, 0.0, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(c.messages, () => { + const st = latestState(c.messages) + return st && st.stones && st.stones.length === 1 && st.phase === 'playing' + }, 15000) + state = latestState(c.messages) console.log('After first throw stones:', state.stones.map(s => ({ id: s.id, x: s.x, y: s.y }))) if (state.stones.length !== 1) throw new Error('expected first stone in play') - const trajCountBefore = c.messages.filter(m => m.type === 'trajectory').length - console.log('trajectory count before second throw:', trajCountBefore) + const firstStone = state.stones[0] + const firstIdKey = stoneIdKey(firstStone.id) + const trajCountBefore = c.messages.filter(m => m.type === 'trajectories').length + console.log('trajectories count before second throw:', trajCountBefore) - // Second throw aimed slightly off-center so it hits the first stone. - c.ws.send(JSON.stringify({ type: 'throw', team: state.turn_team, broom_x: 0.25, broom_y: 38.5, weight: 7, curl: 0, friction: 1.0 })) - await waitFor(c.messages, () => c.messages.filter(m => m.type === 'trajectory').length > trajCountBefore) + // Second throw aimed at first stone so they collide. + const secondTeam = state.turn_team + throwFor(c.ws, secondTeam, firstStone.x, firstStone.y, TAKEOUT_VEL, 0, 1.0) + await waitFor(c.messages, () => c.messages.filter(m => m.type === 'trajectories').length > trajCountBefore, 15000) - const traj = c.messages.filter(m => m.type === 'trajectory').pop() - console.log('Trajectory paths count:', traj.paths.length) - for (const p of traj.paths) { - console.log('stone_id', p.stone_id, 'path length', p.path.length, 'first', p.path[0], 'last', p.path[p.path.length - 1]) + const traj = c.messages.filter(m => m.type === 'trajectories').pop() + if (!traj.stones || !Array.isArray(traj.stones)) { + throw new Error('trajectories message must have stones[]') + } + console.log('Trajectory stones count:', traj.stones.length) + for (const p of traj.stones) { + console.log( + 'stone_id', p.stone_id, + 'team', p.team, + 'path length', p.trajectory?.length, + 'first', p.trajectory?.[0], + 'last', p.trajectory?.[p.trajectory.length - 1] + ) } - const ids = traj.paths.map(p => p.stone_id).sort((a, b) => a - b) - if (ids.length !== 2 || ids[0] !== 1 || ids[1] !== 2) throw new Error('expected both stone ids in trajectory, got ' + JSON.stringify(ids)) - for (const p of traj.paths) { - if (p.path.length < 5) throw new Error('path too short for stone ' + p.stone_id) + const ids = traj.stones.map(p => stoneIdKey(p.stone_id)).sort() + if (ids.length < 2) { + throw new Error('expected both stones in trajectories, got ' + JSON.stringify(ids)) + } + if (!ids.includes(firstIdKey)) { + throw new Error('expected first stone in trajectories, got ' + JSON.stringify(ids)) + } + for (const p of traj.stones) { + if (!p.trajectory || p.trajectory.length < 5) { + throw new Error('path too short for stone ' + JSON.stringify(p.stone_id)) + } + if (typeof p.stone_id !== 'object' || !p.stone_id.team || typeof p.stone_id.n !== 'number') { + throw new Error('stone_id must be {team,n}, got ' + JSON.stringify(p.stone_id)) + } } // Verify the first stone actually moved because of collision. - const stone1Path = traj.paths.find(p => p.stone_id === 1).path + const stone1Path = traj.stones.find(p => stoneIdKey(p.stone_id) === firstIdKey).trajectory const first = stone1Path[0] const last = stone1Path[stone1Path.length - 1] - const dist = Math.sqrt((last[0]-first[0])**2 + (last[1]-first[1])**2) - console.log('stone1 moved', dist, 'm') + const dist = Math.sqrt((last[0] - first[0]) ** 2 + (last[1] - first[1]) ** 2) + console.log('first stone moved', dist, 'm') if (dist < 0.05) throw new Error('expected first stone to move after collision') console.log('COLLISION TRAJECTORY QA PASSED') diff --git a/e2e/e2e_end_score.cjs b/e2e/e2e_end_score.cjs index 99ecd62..81a8c36 100644 --- a/e2e/e2e_end_score.cjs +++ b/e2e/e2e_end_score.cjs @@ -1,7 +1,12 @@ 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)) @@ -27,37 +32,80 @@ function waitFor(messages, pred, timeout = 30000) { }) } +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 getTurn = () => { - const st = p1.messages.slice(-1)[0] - return st && st.type === 'game_state' ? st.turn_team : null - } - - let stateCount = p1.messages.filter(m => m.type === 'game_state').length + 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 last = p1.messages.slice(-1)[0] - return last && last.type === 'game_state' && last.phase === 'playing' - }, 5000) - const turn = getTurn() + 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') - const broomX = (Math.random() - 0.5) * 0.6 - p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: broomX, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 })) + // 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 - await waitFor(p1.messages, () => p1.messages.filter(m => m.type === 'game_state').length > prevStateCount, 15000) + 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) } - await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'end_scored'), 20000) - const final = p1.messages.slice(-1)[0] - console.log('Final game_state:', final) - if (final.end <= 1) throw new Error('end did not advance') - console.log('End scored event received; end advanced to', final.end) + // 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) diff --git a/e2e/e2e_multi_client.cjs b/e2e/e2e_multi_client.cjs index 839a915..b2b84c8 100644 --- a/e2e/e2e_multi_client.cjs +++ b/e2e/e2e_multi_client.cjs @@ -1,8 +1,13 @@ const WebSocket = require('ws') +const DRAW_VEL = 2.38 const room = 'MULTI' + 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) @@ -49,16 +54,15 @@ function waitFor(client, pred, timeout = 10000) { const state = p1.messages.find(m => m.type === 'game_state') const turn = state.turn_team console.log(`p1 throwing for team ${turn}`) - p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: 0.5, broom_y: 39, weight: 7, curl: 1, friction: 1.0 })) + throwFor(p1.ws, turn, 0.0, 38.5, DRAW_VEL, 0, 1.0) - // All 3 clients eventually see trajectory or updated game_state - await waitFor(p1, msgs => msgs.some(m => m.type === 'trajectory'), 15000) - await waitFor(p2, msgs => msgs.some(m => m.type === 'trajectory'), 15000) - await waitFor(p3, msgs => msgs.some(m => m.type === 'trajectory'), 15000) - console.log('All 3 clients received trajectory') + // All 3 clients eventually see trajectories (plural) or updated game_state + await waitFor(p1, msgs => msgs.some(m => m.type === 'trajectories'), 15000) + await waitFor(p2, msgs => msgs.some(m => m.type === 'trajectories'), 15000) + await waitFor(p3, msgs => msgs.some(m => m.type === 'trajectories'), 15000) + console.log('All 3 clients received trajectories') // All 3 see an updated game_state after the throw - const lastIdx = p1.messages.length - 1 await waitFor(p1, msgs => msgs.slice(-1)[0]?.type === 'game_state', 15000) await waitFor(p2, msgs => msgs.slice(-1)[0]?.type === 'game_state', 15000) await waitFor(p3, msgs => msgs.slice(-1)[0]?.type === 'game_state', 15000) @@ -72,4 +76,4 @@ function waitFor(client, pred, timeout = 10000) { })().catch(err => { console.error(err) process.exit(1) -}) \ No newline at end of file +}) diff --git a/e2e/e2e_persistence.cjs b/e2e/e2e_persistence.cjs index e49567a..dfcfc0c 100644 --- a/e2e/e2e_persistence.cjs +++ b/e2e/e2e_persistence.cjs @@ -1,8 +1,13 @@ 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) @@ -36,39 +41,61 @@ function latestState(messages) { 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')) - // Determine the current player and throw two stones in the same end. let state = latestState(p1.messages) - console.log('Initial state', state) + console.log('Initial state', { + turn_team: state.turn_team, + stones_remaining: state.stones_remaining, + scoreboard: state.scoreboard, + }) - // First throw: current turn team. + // First throw: DRAW_VEL + curl 0 + house center keeps stone in play. let turn = state.turn_team - p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: -0.3, broom_y: 39, weight: 7, curl: 1, friction: 1.0 })) + 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) + console.log('After first throw:', state.stones) - // Second throw: other team. + // Second throw: other team, offset so both stay. turn = state.turn_team - p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: 0.3, broom_y: 39, weight: 7, curl: -1, friction: 1.0 })) + 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) + 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}`) - // 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}`) + // 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 ${s.id} is out of house: y=${s.y}`) + 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') diff --git a/e2e/e2e_score.cjs b/e2e/e2e_score.cjs index 81b37a9..7e7bdf9 100644 --- a/e2e/e2e_score.cjs +++ b/e2e/e2e_score.cjs @@ -1,6 +1,11 @@ const WebSocket = require('ws') -const base = 'ws://127.0.0.1:3000/ws?room=SCOREQA' +const DRAW_VEL = 2.38 +const base = 'ws://127.0.0.1:3000/ws?room=SCOREQA' + Math.floor(Math.random() * 10000) + +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) => { @@ -27,25 +32,41 @@ function waitFor(condFn, timeout = 5000) { }) } +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 === 'game_state')) - let state = p1.messages.find(m => m.type === 'game_state') - console.log('start turn', state.turn_team, 'hammer', state.hammer) + let state = latestState(p1.messages) + console.log('start turn', state.turn_team, 'hammer', state.hammer, 'stones_remaining', state.stones_remaining) const thrower1 = state.turn_team - p1.ws.send(JSON.stringify({ type: 'throw', team: thrower1, broom_x: 0.2, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 })) - await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000) - await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000) - await new Promise(r => setTimeout(r, 500)) - state = p1.messages.slice(-1)[0] - console.log('After first throw:', state) + throwFor(p1.ws, thrower1, 0.2, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(() => p1.messages.some(m => m.type === 'trajectories'), 15000) + await waitFor(() => { + const s = latestState(p1.messages) + return s && s.stones && s.stones.length >= 1 && s.phase === 'playing' + }, 15000) + await new Promise(r => setTimeout(r, 200)) + state = latestState(p1.messages) + console.log('After first throw stones:', state.stones.length, 'remaining', state.stones_remaining) + const thrower2 = state.turn_team - p1.ws.send(JSON.stringify({ type: 'throw', team: thrower2, broom_x: -0.1, broom_y: 38.8, weight: 9, curl: 1, friction: 1.0 })) - await waitFor(() => p1.messages.filter(m => m.type === 'trajectory').length >= 2, 15000) - await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000) - await new Promise(r => setTimeout(r, 500)) - console.log('Final stones', p1.messages.slice(-1)[0].stones) + throwFor(p1.ws, thrower2, -0.2, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(() => p1.messages.filter(m => m.type === 'trajectories').length >= 2, 15000) + await waitFor(() => { + const s = latestState(p1.messages) + return s && s.stones && s.stones.length >= 2 && s.phase === 'playing' + }, 15000) + await new Promise(r => setTimeout(r, 200)) + state = latestState(p1.messages) + console.log('Final stones', state.stones) + console.log('scoreboard', state.scoreboard, 'stones_remaining', state.stones_remaining) p1.ws.close() process.exit(0) })().catch(e => { diff --git a/e2e/e2e_test.cjs b/e2e/e2e_test.cjs index 192136f..4e5c72a 100644 --- a/e2e/e2e_test.cjs +++ b/e2e/e2e_test.cjs @@ -1,8 +1,13 @@ const WebSocket = require('ws') +const DRAW_VEL = 2.38 const room = 'QA' + 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) @@ -37,13 +42,25 @@ function waitFor(condFn, timeout = 5000) { const p1 = await connect('p1') await waitFor(() => p1.messages.some(m => m.type === 'game_state')) const state = p1.messages.find(m => m.type === 'game_state') - console.log('Game state', state) + console.log('Game state', { + end: state.end, + turn_team: state.turn_team, + stones_remaining: state.stones_remaining, + scoreboard: state.scoreboard, + }) const turn = state.turn_team - p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: 0.5, broom_y: 39, weight: 7, curl: 1, friction: 1.0 })) - await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000) + // DRAW_VEL + broom at house center keeps stone in play + throwFor(p1.ws, turn, 0.0, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(() => p1.messages.some(m => m.type === 'trajectories'), 15000) await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000) - console.log('Final state after throw:', p1.messages.slice(-1)[0]) + const final = p1.messages.slice(-1)[0] + console.log('Final state after throw:', { + type: final.type, + turn_team: final.turn_team, + stones: final.stones, + stones_remaining: final.stones_remaining, + }) p1.ws.close() process.exit(0) })().catch(err => { diff --git a/e2e/load_test.cjs b/e2e/load_test.cjs index 5162bff..201422a 100644 --- a/e2e/load_test.cjs +++ b/e2e/load_test.cjs @@ -1,28 +1,84 @@ const WebSocket = require('ws') +const DRAW_VEL = 2.38 const ROOMS = 10 const BASE = 'ws://127.0.0.1:3000/ws?room=' -function connect(name, 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(room) { return new Promise((resolve, reject) => { const ws = new WebSocket(BASE + room) - ws.on('open', () => resolve(ws)) + const messages = [] + ws.on('open', () => resolve({ ws, messages })) ws.on('error', reject) - ws.on('message', () => {}) + ws.on('message', (data) => { + try { + messages.push(JSON.parse(data.toString())) + } catch (_) {} + }) }) } +function waitFor(messages, pred, timeout = 20000) { + const start = Date.now() + return new Promise((resolve, reject) => { + const check = () => { + if (pred()) return resolve(undefined) + if (Date.now() - start > timeout) { + const errs = messages.filter(m => m.type === 'error') + return reject(new Error( + 'timeout types=' + messages.map(m => m.type).join(',') + + ' errs=' + JSON.stringify(errs) + )) + } + 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 +} + +function remainingSum(st) { + if (!st || !Array.isArray(st.stones_remaining)) return 16 + return st.stones_remaining[0] + st.stones_remaining[1] +} + async function runRoom(i) { - const room = `LOAD${i}` - const p1 = await connect('p1', room) - const p2 = await connect('p2', room) - await new Promise(r => setTimeout(r, 200)) - p1.send(JSON.stringify({ type: 'throw', broom_x: 0.2, broom_y: 38.7, weight: 5, curl: 1, friction: 1.0 })) - await new Promise(r => setTimeout(r, 800)) - p2.send(JSON.stringify({ type: 'throw', broom_x: -0.1, broom_y: 38.8, weight: 5, curl: 1, friction: 1.0 })) - await new Promise(r => setTimeout(r, 1000)) - p1.close() - p2.close() + const room = `LOAD${i}_${process.hrtime.bigint()}` + const p1 = await connect(room) + const p2 = await connect(room) + await waitFor(p1.messages, () => { + const st = latestState(p1.messages) + return st && st.phase === 'playing' + }, 8000) + + let st = latestState(p1.messages) + const gsBefore = p1.messages.filter(m => m.type === 'game_state').length + throwFor(p1.ws, st.turn_team, 0.2, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'trajectories'), 20000) + // Must wait for post-throw game_state (remaining decreased), not the pre-throw playing state. + await waitFor(p1.messages, () => { + const s = latestState(p1.messages) + return s && s.phase === 'playing' && remainingSum(s) < 16 && + p1.messages.filter(m => m.type === 'game_state').length > gsBefore + }, 20000) + + st = latestState(p1.messages) + const trajBefore = p1.messages.filter(m => m.type === 'trajectories').length + throwFor(p2.ws, st.turn_team, -0.2, 38.5, DRAW_VEL, 0, 1.0) + await waitFor(p1.messages, () => p1.messages.filter(m => m.type === 'trajectories').length > trajBefore, 20000) + + p1.ws.close() + p2.ws.close() } ;(async () => { @@ -30,4 +86,7 @@ async function runRoom(i) { await Promise.all(Array.from({ length: ROOMS }, (_, i) => runRoom(i))) console.log(`10 rooms played start-to-throw-to-close in ${Date.now() - start}ms`) process.exit(0) -})() +})().catch((e) => { + console.error(e) + process.exit(1) +})