forked from eros/curltastic
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:
parent
95d04f8d9e
commit
56ab1fb2e3
@ -1,7 +1,14 @@
|
|||||||
const WebSocket = require('ws')
|
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 room = 'COLQA' + Math.floor(Math.random() * 1000)
|
||||||
const url = 'ws://127.0.0.1:3000/ws?room=' + room
|
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() {
|
function connect() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(url)
|
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 () => {
|
;(async () => {
|
||||||
const c = await connect()
|
const c = await connect()
|
||||||
await waitFor(c.messages, () => {
|
await waitFor(c.messages, () => {
|
||||||
const last = c.messages[c.messages.length - 1]
|
const st = latestState(c.messages)
|
||||||
return last && last.type === 'game_state' && last.phase === 'playing'
|
return st && st.phase === 'playing'
|
||||||
})
|
})
|
||||||
|
|
||||||
// First throw: weight 7 so it stays in house.
|
// First throw: DRAW_VEL so it stays in house.
|
||||||
let state = c.messages[c.messages.length - 1]
|
let state = latestState(c.messages)
|
||||||
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 }))
|
const firstTeam = state.turn_team
|
||||||
await waitFor(c.messages, () => c.messages.filter(m => m.type === 'game_state').length > 1)
|
throwFor(c.ws, firstTeam, 0.0, 38.5, DRAW_VEL, 0, 1.0)
|
||||||
state = c.messages[c.messages.length - 1]
|
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 })))
|
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')
|
if (state.stones.length !== 1) throw new Error('expected first stone in play')
|
||||||
|
|
||||||
const trajCountBefore = c.messages.filter(m => m.type === 'trajectory').length
|
const firstStone = state.stones[0]
|
||||||
console.log('trajectory count before second throw:', trajCountBefore)
|
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.
|
// Second throw aimed at first stone so they collide.
|
||||||
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 }))
|
const secondTeam = state.turn_team
|
||||||
await waitFor(c.messages, () => c.messages.filter(m => m.type === 'trajectory').length > trajCountBefore)
|
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()
|
const traj = c.messages.filter(m => m.type === 'trajectories').pop()
|
||||||
console.log('Trajectory paths count:', traj.paths.length)
|
if (!traj.stones || !Array.isArray(traj.stones)) {
|
||||||
for (const p of traj.paths) {
|
throw new Error('trajectories message must have stones[]')
|
||||||
console.log('stone_id', p.stone_id, 'path length', p.path.length, 'first', p.path[0], 'last', p.path[p.path.length - 1])
|
}
|
||||||
|
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)
|
const ids = traj.stones.map(p => stoneIdKey(p.stone_id)).sort()
|
||||||
if (ids.length !== 2 || ids[0] !== 1 || ids[1] !== 2) throw new Error('expected both stone ids in trajectory, got ' + JSON.stringify(ids))
|
if (ids.length < 2) {
|
||||||
for (const p of traj.paths) {
|
throw new Error('expected both stones in trajectories, got ' + JSON.stringify(ids))
|
||||||
if (p.path.length < 5) throw new Error('path too short for stone ' + p.stone_id)
|
}
|
||||||
|
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.
|
// 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 first = stone1Path[0]
|
||||||
const last = stone1Path[stone1Path.length - 1]
|
const last = stone1Path[stone1Path.length - 1]
|
||||||
const dist = Math.sqrt((last[0]-first[0])**2 + (last[1]-first[1])**2)
|
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')
|
if (dist < 0.05) throw new Error('expected first stone to move after collision')
|
||||||
|
|
||||||
console.log('COLLISION TRAJECTORY QA PASSED')
|
console.log('COLLISION TRAJECTORY QA PASSED')
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
const DRAW_VEL = 2.38
|
||||||
const base = (room) => `ws://127.0.0.1:3000/ws?room=${room}`
|
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) {
|
function connect(name, room) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(base(room))
|
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 () => {
|
;(async () => {
|
||||||
const room = 'ENDQA' + Math.floor(Math.random() * 1000)
|
const room = 'ENDQA' + Math.floor(Math.random() * 1000)
|
||||||
const p1 = await connect('p1', room)
|
const p1 = await connect('p1', room)
|
||||||
const p2 = await connect('p2', room)
|
const p2 = await connect('p2', room)
|
||||||
await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'game_state'), 5000)
|
await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'game_state'), 5000)
|
||||||
|
|
||||||
const getTurn = () => {
|
const initial = latestState(p1.messages)
|
||||||
const st = p1.messages.slice(-1)[0]
|
const startEnd = initial.end
|
||||||
return st && st.type === 'game_state' ? st.turn_team : null
|
const startScoreboardLen = (initial.scoreboard || []).length
|
||||||
}
|
console.log('start end=', startEnd, 'scoreboard len=', startScoreboardLen)
|
||||||
|
|
||||||
let stateCount = p1.messages.filter(m => m.type === 'game_state').length
|
|
||||||
|
|
||||||
|
// 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++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
await waitFor(p1.messages, () => {
|
await waitFor(p1.messages, () => {
|
||||||
const last = p1.messages.slice(-1)[0]
|
const st = latestState(p1.messages)
|
||||||
return last && last.type === 'game_state' && last.phase === 'playing'
|
return st && st.phase === 'playing'
|
||||||
}, 5000)
|
}, 15000)
|
||||||
const turn = getTurn()
|
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')
|
if (!turn) throw new Error('no turn')
|
||||||
const broomX = (Math.random() - 0.5) * 0.6
|
// Keep broom near house center so draws stay in play for scoring.
|
||||||
p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: broomX, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 }))
|
const broomX = ((i % 8) - 3.5) * 0.08
|
||||||
const prevStateCount = p1.messages.filter(m => m.type === 'game_state').length
|
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)
|
// Wait for scoreboard entry + end advance (no end_scored type).
|
||||||
const final = p1.messages.slice(-1)[0]
|
await waitFor(p1.messages, () => {
|
||||||
console.log('Final game_state:', final)
|
const st = latestState(p1.messages)
|
||||||
if (final.end <= 1) throw new Error('end did not advance')
|
if (!st) return false
|
||||||
console.log('End scored event received; end advanced to', final.end)
|
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()
|
p1.ws.close()
|
||||||
p2.ws.close()
|
p2.ws.close()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
const DRAW_VEL = 2.38
|
||||||
const room = 'MULTI' + Math.floor(Math.random() * 1000)
|
const room = 'MULTI' + Math.floor(Math.random() * 1000)
|
||||||
const base = 'ws://127.0.0.1:3000/ws?room=' + room
|
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) {
|
function connect(name) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(base)
|
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 state = p1.messages.find(m => m.type === 'game_state')
|
||||||
const turn = state.turn_team
|
const turn = state.turn_team
|
||||||
console.log(`p1 throwing for team ${turn}`)
|
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
|
// All 3 clients eventually see trajectories (plural) or updated game_state
|
||||||
await waitFor(p1, msgs => msgs.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(p1, msgs => msgs.some(m => m.type === 'trajectories'), 15000)
|
||||||
await waitFor(p2, msgs => msgs.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(p2, msgs => msgs.some(m => m.type === 'trajectories'), 15000)
|
||||||
await waitFor(p3, msgs => msgs.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(p3, msgs => msgs.some(m => m.type === 'trajectories'), 15000)
|
||||||
console.log('All 3 clients received trajectory')
|
console.log('All 3 clients received trajectories')
|
||||||
|
|
||||||
// All 3 see an updated game_state after the throw
|
// 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(p1, msgs => msgs.slice(-1)[0]?.type === 'game_state', 15000)
|
||||||
await waitFor(p2, 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)
|
await waitFor(p3, msgs => msgs.slice(-1)[0]?.type === 'game_state', 15000)
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
const DRAW_VEL = 2.38
|
||||||
const room = 'PERSIST' + Math.floor(Math.random() * 1000)
|
const room = 'PERSIST' + Math.floor(Math.random() * 1000)
|
||||||
const base = 'ws://127.0.0.1:3000/ws?room=' + room
|
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) {
|
function connect(name) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(base)
|
const ws = new WebSocket(base)
|
||||||
@ -36,39 +41,61 @@ function latestState(messages) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stoneIdKey(id) {
|
||||||
|
return `${id.team}:${id.n}`
|
||||||
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
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)
|
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
|
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)
|
await waitFor(() => latestState(p1.messages)?.stones?.length === 1, 15000)
|
||||||
|
|
||||||
state = latestState(p1.messages)
|
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
|
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)
|
await waitFor(() => latestState(p1.messages)?.stones?.length === 2, 15000)
|
||||||
|
|
||||||
state = latestState(p1.messages)
|
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}`)
|
if (state.stones.length !== 2) throw new Error(`Expected 2 stones after second throw, got ${state.stones.length}`)
|
||||||
|
|
||||||
// Check that ids are monotonic.
|
// Stone ids are {team, n}, not flat numbers — each team's first stone is n=1.
|
||||||
const ids = state.stones.map(s => s.id).sort((a, b) => a - b)
|
const ids = state.stones.map(s => stoneIdKey(s.id)).sort()
|
||||||
if (ids[0] !== 1 || ids[1] !== 2) throw new Error(`Unexpected stone ids: ${ids}`)
|
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).
|
// Both stones should be in play (near house).
|
||||||
for (const s of state.stones) {
|
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')
|
console.log('PERSISTENCE E2E PASSED')
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
const WebSocket = require('ws')
|
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) {
|
function connect(name) {
|
||||||
return new Promise((resolve, reject) => {
|
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 () => {
|
;(async () => {
|
||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
||||||
let state = p1.messages.find(m => m.type === 'game_state')
|
let state = latestState(p1.messages)
|
||||||
console.log('start turn', state.turn_team, 'hammer', state.hammer)
|
console.log('start turn', state.turn_team, 'hammer', state.hammer, 'stones_remaining', state.stones_remaining)
|
||||||
|
|
||||||
const thrower1 = state.turn_team
|
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 }))
|
throwFor(p1.ws, thrower1, 0.2, 38.5, DRAW_VEL, 0, 1.0)
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(() => p1.messages.some(m => m.type === 'trajectories'), 15000)
|
||||||
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
await waitFor(() => {
|
||||||
await new Promise(r => setTimeout(r, 500))
|
const s = latestState(p1.messages)
|
||||||
state = p1.messages.slice(-1)[0]
|
return s && s.stones && s.stones.length >= 1 && s.phase === 'playing'
|
||||||
console.log('After first throw:', state)
|
}, 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
|
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 }))
|
throwFor(p1.ws, thrower2, -0.2, 38.5, DRAW_VEL, 0, 1.0)
|
||||||
await waitFor(() => p1.messages.filter(m => m.type === 'trajectory').length >= 2, 15000)
|
await waitFor(() => p1.messages.filter(m => m.type === 'trajectories').length >= 2, 15000)
|
||||||
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
await waitFor(() => {
|
||||||
await new Promise(r => setTimeout(r, 500))
|
const s = latestState(p1.messages)
|
||||||
console.log('Final stones', p1.messages.slice(-1)[0].stones)
|
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()
|
p1.ws.close()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})().catch(e => {
|
})().catch(e => {
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
const DRAW_VEL = 2.38
|
||||||
const room = 'QA' + Math.floor(Math.random() * 1000)
|
const room = 'QA' + Math.floor(Math.random() * 1000)
|
||||||
const base = 'ws://127.0.0.1:3000/ws?room=' + room
|
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) {
|
function connect(name) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(base)
|
const ws = new WebSocket(base)
|
||||||
@ -37,13 +42,25 @@ function waitFor(condFn, timeout = 5000) {
|
|||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
||||||
const state = p1.messages.find(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
|
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 }))
|
// DRAW_VEL + broom at house center keeps stone in play
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
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)
|
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()
|
p1.ws.close()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
|
|||||||
@ -1,28 +1,84 @@
|
|||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
const DRAW_VEL = 2.38
|
||||||
const ROOMS = 10
|
const ROOMS = 10
|
||||||
const BASE = 'ws://127.0.0.1:3000/ws?room='
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(BASE + room)
|
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('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) {
|
async function runRoom(i) {
|
||||||
const room = `LOAD${i}`
|
const room = `LOAD${i}_${process.hrtime.bigint()}`
|
||||||
const p1 = await connect('p1', room)
|
const p1 = await connect(room)
|
||||||
const p2 = await connect('p2', room)
|
const p2 = await connect(room)
|
||||||
await new Promise(r => setTimeout(r, 200))
|
await waitFor(p1.messages, () => {
|
||||||
p1.send(JSON.stringify({ type: 'throw', broom_x: 0.2, broom_y: 38.7, weight: 5, curl: 1, friction: 1.0 }))
|
const st = latestState(p1.messages)
|
||||||
await new Promise(r => setTimeout(r, 800))
|
return st && st.phase === 'playing'
|
||||||
p2.send(JSON.stringify({ type: 'throw', broom_x: -0.1, broom_y: 38.8, weight: 5, curl: 1, friction: 1.0 }))
|
}, 8000)
|
||||||
await new Promise(r => setTimeout(r, 1000))
|
|
||||||
p1.close()
|
let st = latestState(p1.messages)
|
||||||
p2.close()
|
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 () => {
|
;(async () => {
|
||||||
@ -30,4 +86,7 @@ async function runRoom(i) {
|
|||||||
await Promise.all(Array.from({ length: ROOMS }, (_, i) => 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`)
|
console.log(`10 rooms played start-to-throw-to-close in ${Date.now() - start}ms`)
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})()
|
})().catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user