Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
117 lines
4.2 KiB
JavaScript
117 lines
4.2 KiB
JavaScript
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)
|
|
const messages = []
|
|
ws.on('open', () => resolve({ ws, messages }))
|
|
ws.on('message', (data) => messages.push(JSON.parse(data.toString())))
|
|
ws.on('error', reject)
|
|
})
|
|
}
|
|
|
|
function waitFor(messages, pred, timeout = 10000) {
|
|
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
|
|
}
|
|
|
|
function stoneIdKey(id) {
|
|
return `${id.team}:${id.n}`
|
|
}
|
|
|
|
;(async () => {
|
|
const c = await connect()
|
|
await waitFor(c.messages, () => {
|
|
const st = latestState(c.messages)
|
|
return st && st.phase === 'playing'
|
|
})
|
|
|
|
// 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 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 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 === '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.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.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('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')
|
|
c.ws.close()
|
|
process.exit(0)
|
|
})().catch(e => { console.error(e); process.exit(1) })
|