test(e2e): align suite with PR-B protocol and physics

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Dekarske 2026-07-10 23:47:17 -07:00
parent 95d04f8d9e
commit 56ab1fb2e3
7 changed files with 313 additions and 93 deletions

View File

@ -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')
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')

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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 => {

View File

@ -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 => {

View File

@ -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)
})