curltastic/e2e/e2e_end_score.cjs
Jason Dekarske 56ab1fb2e3 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>
2026-07-10 23:47:17 -07:00

116 lines
3.9 KiB
JavaScript

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