import { describe, test, expect as vitestExpect } from 'vitest' import * as fs from 'fs' /** * Network checks: wire protocol, transports, and two peers playing over a socket. * * The claim being tested is not "messages encode" — it is that two independently * simulated worlds, driven only by messages that crossed a real byte pipe, stay * bit-identical; that a peer which goes quiet *stalls* the world instead of * corrupting it; and that a peer whose input is altered in flight is *detected*. * * The last section runs the same session code over a real TCP socket and real * WebSocket framing (through the relay in `scripts/net-relay.ts`), because an * in-memory pipe cannot prove that the browser transport works: it never frames, * never splits a message across TCP segments, and never delivers asynchronously. * * Usage: node scripts/verify-net.ts */ import { addPlayer, createWorld, damageMonster, spawnMonsters, tickCombat, tickCombatMulti } from '../src/game/combat.ts' import type { CombatOptions, CombatWorld, MonsterStats } from '../src/game/combat.ts' import { Rng } from '../src/game/rng.ts' import { LockstepSession } from '../src/net/lockstep.ts' import type { InputFrame, LockstepSimulation } from '../src/net/lockstep.ts' import { NetplaySession } from '../src/net/netplay.ts' import { decodeMessage, encodeMessage, NO_ACK, ProtocolError } from '../src/net/protocol.ts' import type { NetMessage } from '../src/net/protocol.ts' import { MemoryHub, memoryTransportPair, socketTransport, MemoryTransport } from '../src/net/transport.ts' import type { Transport } from '../src/net/transport.ts' import { parseTable } from '../src/game/tables.ts' import { monsterStatsFromTable } from '../src/game/combat.ts' import { startRelay } from '../scripts/net-relay.ts' const isSkip = false && !fs.existsSync('samples/d2'); const _results: any[] = []; let suiteCompleted = false; let problems: string[] = []; let checks: any[] = []; function expect(condition: boolean, description: string) { _results.push({cond: condition, desc: description}); if (!condition) problems.push(description); } function check(nameOrOk: any, okOrMessage: any, detail?: string) { if (typeof nameOrOk === 'string') { _results.push({cond: okOrMessage, desc: nameOrOk, detail}); if (!okOrMessage) checks.push({name: nameOrOk, ok: okOrMessage}); } else { _results.push({cond: nameOrOk, desc: okOrMessage}); if (!nameOrOk) checks.push({name: okOrMessage, ok: nameOrOk}); } } if (!isSkip) { const problems: string[] = [] let checks = 0 /** * Assert one condition. * * @param condition - the condition to hold. * @param description - what it means. */ function __expect_disabled(condition: boolean, description: string): void { checks += 1 if (!condition) problems.push(description) } // --- the wire codec ---------------------------------------------------------- const messages: NetMessage[] = [ { kind: 'hello', peer: 1, peers: 2, seed: 0xdeadbeef, ackTo: NO_ACK }, { kind: 'hello', peer: 0, peers: 2, seed: 0xdeadbeef, ackTo: 1 }, { kind: 'input', peer: 0, frame: { tick: 1234, movement: { x: -1, y: 0.5 }, attack: true, pickup: false, talk: true, skill: 3 } }, { kind: 'hash', peer: 1, tick: 987654, hash: 0x89abcdef }, { kind: 'bye', peer: 0 }, ] for (const message of messages) { const round = decodeMessage(encodeMessage(message)) expect(round.kind === message.kind, 'message survives a round trip') if (round.kind === 'input' && message.kind === 'input') { expect(round.frame.tick === message.frame.tick, 'the tick round-trips') expect(Math.abs(round.frame.movement.x - message.frame.movement.x) < 1e-3, 'the x movement round-trips as fixed point') expect(Math.abs(round.frame.movement.y - message.frame.movement.y) < 1e-3, 'the y movement round-trips as fixed point') expect(round.frame.attack && !round.frame.pickup && round.frame.talk, 'the control flags round-trip') expect(round.frame.skill === 3, 'the skill slot round-trips') } if (round.kind === 'hello' && message.kind === 'hello') expect(round.seed === 0xdeadbeef, 'the seed round-trips') if (round.kind === 'hash' && message.kind === 'hash') { expect(round.tick === 987654 && round.hash === 0x89abcdef, 'the hash and tick round-trip') } } // The layout is pinned, not just self-consistent: a peer running an older build // must fail loudly rather than silently misread a field. const pinned = encodeMessage({ kind: 'input', peer: 1, frame: { tick: 1, movement: { x: -1, y: 0 }, attack: true, pickup: false, talk: false, skill: 0 } }) expect(pinned.byteLength === 12, 'an input message is twelve bytes') expect(pinned[0] === 2 && pinned[1] === 1, 'an input message starts with its type and peer') expect(pinned[6] === 0x18 && pinned[7] === 0xfc, 'a movement of -1 is fixed point -1000, little-endian') expect(pinned[10] === 1, 'only the attack flag is set') expect(encodeMessage({ kind: 'hash', peer: 0, tick: 0, hash: 0 }).byteLength === 10, 'a hash message is ten bytes') expect(encodeMessage({ kind: 'hello', peer: 0, peers: 1, seed: 0, ackTo: NO_ACK }).byteLength === 8, 'a hello message is eight bytes') const acked = decodeMessage(encodeMessage({ kind: 'hello', peer: 0, peers: 4, seed: 7, ackTo: 3 })) expect(acked.kind === 'hello' && acked.ackTo === 3, 'an acknowledgement names the peer it acknowledges') const unacked = decodeMessage(encodeMessage({ kind: 'hello', peer: 0, peers: 4, seed: 7, ackTo: NO_ACK })) expect(unacked.kind === 'hello' && unacked.ackTo === NO_ACK, 'a plain introduction is not mistaken for an acknowledgement') const bad: [string, Uint8Array][] = [ ['an empty message', new Uint8Array(0)], ['an unknown type', new Uint8Array([99, 0])], ['a truncated input', new Uint8Array([2, 0, 0, 0])], ['an over-long input', new Uint8Array(16)], ['a hello claiming zero peers', new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])], ['a hello claiming nine peers', new Uint8Array([1, 0, 9, 0, 0, 0, 0, 0])], ['a truncated hash', new Uint8Array([3, 0, 0])], ] for (const [label, bytes] of bad) { let rejected = false try { decodeMessage(bytes) } catch (error) { rejected = error instanceof ProtocolError } expect(rejected, 'input is rejected before it reaches the simulation') } const movementOverflow = decodeMessage(encodeMessage({ kind: 'input', peer: 0, frame: { tick: 0, movement: { x: 1e9, y: -1e9 }, attack: false, pickup: false, talk: false, skill: 255 }, })) expect(movementOverflow.kind === 'input' && movementOverflow.frame.movement.x <= 32767, 'an absurd movement is clamped, not wrapped') // --- transports -------------------------------------------------------------- const [endA, endB] = memoryTransportPair() let receivedAtB = 0 endB.onMessage(() => { receivedAtB += 1 }) expect(endA.open && endB.open, 'a fresh memory pair is open at both ends') endA.send(new Uint8Array([1, 2, 3])) expect(receivedAtB === 0, 'a memory message is queued, not delivered on send') endA.flush() expect(receivedAtB === 1, 'flushing delivers the queued message') const oversizeRejected = ((): boolean => { try { endA.send(new Uint8Array(5000)) return false } catch { return true } })() expect(oversizeRejected, 'an oversized message is refused at the transport') let closed = 0 endA.onClose(() => { closed += 1 }) endB.close() expect(closed === 1 && !endA.open && !endB.open, 'closing one end closes the pair') endA.send(new Uint8Array([9])) endA.flush() expect(endA.dropped === 1, 'sending on a closed pair drops instead of throwing') // --- two peers, one world, over a byte pipe ---------------------------------- const stats: MonsterStats[] = monsterStatsFromTable(parseTable([ 'Id\tName\tHP\tDamage\tCooldownTicks\tReach\tAggroRadius\tSpeed\tXP', // Fast enough to catch the player and weak enough that neither peer's player // dies before the kills can be compared: the point of this table is contact. 'fallen\tFallen\t12\t1\t24\t36\t400\t220\t8', 'zombie\tZombie\t30\t1\t32\t40\t300\t210\t15', ].join('\n'))) const options: CombatOptions = { playerSpeed: 180, playerReach: 48, playerCooldownTicks: 12, playerDamage: 6, playerManaPerAttack: 2, respawnTicks: 40, } const xpTable: readonly number[] = [0, 0, 1000] /** A world plus its random stream: everything a peer simulates. */ interface Game { world: ReturnType; rng: Rng } /** * Build a fresh game, identical on both peers. * * @param seed - the world seed. * @returns the game. */ function newGame(seed: number): Game { const world = createWorld(0, 0) // The monsters start close enough to reach the player inside the first few // seconds: a network test where nothing ever fights would agree on nothing. spawnMonsters(world, stats, 5, { x: 30, y: 0 }, 40, { overlap: () => 0 }) return { world, rng: new Rng(seed) } } /** * The input script: a pure function of the tick and the peer, so there is nothing * here that two peers could disagree about. * * @param peer - the peer index. * @param tick - the tick. * @returns movement and intent. */ function scriptedInput(peer: number, tick: number): { movement: { x: number; y: number }; attack: boolean; pickup: boolean; talk: boolean; skill: number } { const phase = Math.floor((tick + peer * 7) / 25) % 4 const movement = phase === 0 ? { x: 1, y: 0 } : phase === 1 ? { x: 0, y: 1 } : phase === 2 ? { x: -1, y: 0 } : { x: 0, y: -1 } return { movement, attack: (tick + peer) % 5 === 0, pickup: tick % 41 === 0, talk: false, skill: (tick + peer) % 4 } } /** * Wrap a game as a lockstep simulation. Every peer applies *every* peer's input, * and the digest covers the whole world, so a field missed by one peer's * simulation shows up as a desync. * * @param game - the game to drive. * @returns the simulation. */ function asSimulation(game: Game): LockstepSimulation { return { advance: (inputs) => { for (const input of inputs) { // Only movement and attack reach the combat tick; pickup and talk belong // to the scene, so they are deliberately not part of the digest here. tickCombat(game.world, { movement: input.movement, attack: input.attack }, options, { overlap: () => 0 }, xpTable) if (input.attack && game.world.player.cooldown === 0) { const swing = game.rng.next() // the drop stream is part of the world game.world.monsters.forEach((monster, index) => { if (monster.state === 'dead') return if (Math.hypot(monster.x - game.world.player.x, monster.y - game.world.player.y) > 48) return damageMonster(game.world, index, options.playerDamage + (swing > 0.5 ? 1 : 0)) }) } } }, hash: () => LockstepSession.digest(digest(game)), } } /** * Digest everything, so a divergence cannot hide in a field the hash forgets. * * @param game - the game. * @returns a digest string. */ function digest(game: Game): string { const round = (value: number): number => Math.round(value * 1000) return JSON.stringify({ tick: game.world.tick, kills: game.world.kills, rng: game.rng.seed, events: game.world.events.map(event => [event.kind, event.subjectId ?? '', round(event.x), round(event.y)]), player: { x: round(game.world.player.x), y: round(game.world.player.y), hp: game.world.player.hp, mana: game.world.player.mana, cooldown: game.world.player.cooldown, facing: game.world.player.facing, alive: game.world.player.alive, }, monsters: game.world.monsters.map(m => [round(m.x), round(m.y), m.hp, m.state, m.cooldown, m.hitFlash]), }) } /** * A peer: its session and the game it drives. */ interface Peer { session: NetplaySession; game: Game } /** * Build one networked peer. * * @param index - the peer index. * @param transport - its byte pipe. * @param seed - the world seed. * @returns the peer. */ function makePeer(index: number, transport: Transport, seed: number): Peer { const game = newGame(seed) const session = new NetplaySession( { peer: index, peers: 2, seed, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 40 }, asSimulation(game), transport, ) const peer: Peer = { session, game } session.start() return peer } // --- the quiet-peer case: a stall must not corrupt the world ------------------ const [pipeA, pipeB] = memoryTransportPair() const peerA = makePeer(0, pipeA, 0x51ed) const peerB = makePeer(1, pipeB, 0x51ed) // The handshake is owed by `start` and delivered by the first pump: a session is // normally created while its socket is still connecting, so sending on `start` // would write into a socket that is not open yet. expect(peerA.session.stats.helloSent === 0 && !peerA.session.stats.handshaked, 'nothing is sent before the session is pumped') peerA.session.pump() peerB.session.pump() pipeA.flush() expect(peerA.session.remoteSeed === 0x51ed && peerB.session.remoteSeed === 0x51ed, 'the handshake exchanges the world seed') expect(peerA.session.stats.handshaked && peerB.session.stats.handshaked, 'both peers complete the handshake') // The acknowledgements are still in flight; delivering them must end the // handshake for good rather than leaving either peer repeating itself. pipeA.flush() expect(peerA.session.stats.acknowledged && peerB.session.stats.acknowledged, 'both peers learn that their own hello was heard') for (let tick = 0; tick < 40; tick += 1) { peerA.session.pump() peerB.session.pump() pipeA.flush() } expect( peerA.session.stats.helloSent === 2 && peerB.session.stats.helloSent === 2, 'a completed handshake stops: one hello and one acknowledgement each', ) const TICKS = 200 let stepsA = 0 let stepsB = 0 for (let tick = 0; tick < TICKS; tick += 1) { const inputA = scriptedInput(0, tick) const inputB = scriptedInput(1, tick) peerA.session.setIntent(inputA) peerB.session.setIntent(inputB) if (peerA.session.pump().kind === 'stepped') stepsA += 1 if (peerB.session.pump().kind === 'stepped') stepsB += 1 // B's link is held for a stretch: A must stall, B must run on, and neither may // end up with a different world. pipeB.hold = tick >= 60 && tick < 90 pipeA.flush() } // Drain the backlog the held link accumulated. for (let round = 0; round < 60; round += 1) { peerA.session.setIntent({ movement: { x: 0, y: 0 }, attack: false }) peerB.session.setIntent({ movement: { x: 0, y: 0 }, attack: false }) peerA.session.pump() peerB.session.pump() pipeA.flush() } expect(peerA.session.desyncReport === null && peerB.session.desyncReport === null, 'a peer whose link stalls does not cause a desync') expect(peerA.session.stats.hashesCompared === peerA.session.stats.hashesAgreed, 'every hash peer 0 could check agreed') expect(peerA.session.stats.waiting > 0, 'the peer that waited counted the stalled ticks') expect(peerA.session.stats.hashesCompared > 0 && peerB.session.stats.hashesCompared > 0, 'both peers compared state hashes') expect( peerB.session.stats.hashesCompared === peerB.session.stats.hashesAgreed, 'every hash that arrived at peer 1 agreed', ) expect(peerA.session.stats.malformed === 0 && peerB.session.stats.malformed === 0, 'no message was malformed') expect(peerA.session.stats.timedOut === false, 'a link that stalls for a moment is not declared lost') expect(peerA.session.stats.sent > 100 && peerA.session.stats.received > 100, 'traffic actually crossed the pipe') expect(stepsA > 100 && stepsB > 100, 'both peers ran the game, one eventually catching up') expect(stepsB > stepsA, 'the peer whose link was open ran ahead while the other was starved') // A peer that is starved stops producing input, so its partner runs only as far // as the inputs it already holds and then waits too — the world as a whole is held // back, which is the point of lockstep. The two ends therefore resume with a small // constant lead rather than snapping level. const lead = peerB.session.lockstep.tick - peerA.session.lockstep.tick expect(lead >= 0 && lead <= 6, 'the two ends resume within a few ticks of each other') // The starved peer still holds the other's backlog, so it can be brought level // without any further traffic from the far end: pumping peer 0 alone (and holding // its own sends) replays exactly the ticks it was missing. for (let extra = 0; extra < 12 && peerA.session.lockstep.tick < peerB.session.lockstep.tick; extra += 1) { peerA.session.setIntent(scriptedInput(0, peerA.session.lockstep.tick)) peerA.session.pump() } expect( peerA.session.lockstep.tick === peerB.session.lockstep.tick, 'the starved peer can be brought level from the backlog it already holds', ) expect( digest(peerA.game) === digest(peerB.game), 'at the same tick, the peer that was starved holds exactly the other peer\'s world', ) expect(peerA.game.world.kills === peerB.game.world.kills, 'both peers agree on the kill count') expect(peerA.game.rng.seed === peerB.game.rng.seed, 'both peers agree on the random stream position') expect(peerA.game.world.monsters.every((m, i) => m.hp === peerB.game.world.monsters[i]?.hp), 'both peers agree on monster health') expect(peerA.game.world.kills > 0, 'the test actually produced kills to disagree about') // The starved peer buffered the hashes that arrived for ticks it had not run yet, // so once it catches up it checks them all: being behind costs nothing. expect(peerA.session.stats.hashesIgnored === 0, 'a peer that fell behind still checks every hash it received') expect(peerA.session.stats.hashesCompared > 100, 'catching up results in a real number of compared hashes') expect(peerB.session.stats.hashesAgreed === peerB.session.stats.hashesCompared, 'every hash peer 1 could check agreed') // --- co-op: two peers, one shared world, one fighter each --------------------- /** * Build a co-op world: two players, monsters between them. * * Both peers call this with the same seed and get the same world, which is the * whole premise of lockstep — nothing about the world is sent, only what the * players pressed. * * @returns the world. */ function newCoopWorld(): CombatWorld { const world = createWorld(-40, 0) addPlayer(world, 40, 0) spawnMonsters(world, coopStats, 6, { x: 0, y: 0 }, 90, { overlap: () => 0 }) return world } /** * Digest a co-op world. * * The player list is digested in peer order; `world.player` is a view alias and is * deliberately absent, because which player is "local" must not affect the state * two peers agree on. * * @param world - the world. * @returns a digest string. */ function coopDigest(world: CombatWorld): string { const round = (value: number): number => Math.round(value * 1000) return JSON.stringify({ tick: world.tick, kills: world.kills, players: world.players.map(player => [round(player.x), round(player.y), player.hp, player.xp, player.level, player.alive]), monsters: world.monsters.map(m => [round(m.x), round(m.y), m.hp, m.state]), }) } /** * A co-op simulation: each peer's input drives its own fighter. * * @param world - the shared world. * @returns the simulation. */ function coopSimulation(world: CombatWorld): LockstepSimulation { return { advance: (inputs) => { tickCombatMulti(world, inputs.map(input => ({ movement: input.movement, attack: input.attack })), options, { overlap: () => 0 }, xpTable) }, hash: () => LockstepSession.digest(coopDigest(world)), } } const coopStats: MonsterStats[] = monsterStatsFromTable(parseTable([ 'Id\tName\tHP\tDamage\tCooldownTicks\tReach\tAggroRadius\tSpeed\tXP', 'fallen\tFallen\t14\t1\t24\t36\t400\t220\t8', ].join('\n'))) const [coopPipeA, coopPipeB] = memoryTransportPair() const coopWorldA = newCoopWorld() const coopWorldB = newCoopWorld() const coopA = new NetplaySession( { peer: 0, peers: 2, seed: 0x9a9a, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 }, coopSimulation(coopWorldA), coopPipeA, ) const coopB = new NetplaySession( { peer: 1, peers: 2, seed: 0x9a9a, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 }, coopSimulation(coopWorldB), coopPipeB, ) coopA.start() coopB.start() coopPipeA.flush() for (let tick = 0; tick < 300; tick += 1) { coopA.setIntent(scriptedInput(0, tick)) coopB.setIntent(scriptedInput(1, tick)) coopA.pump() coopB.pump() coopPipeA.flush() } expect(coopA.desyncReport === null && coopB.desyncReport === null, 'two players in one world do not desync') expect(coopA.stats.hashesCompared > 200 && coopA.stats.hashesCompared === coopA.stats.hashesAgreed, 'every co-op hash peer 0 could check agreed') expect(coopB.stats.hashesCompared > 200 && coopB.stats.hashesCompared === coopB.stats.hashesAgreed, 'every co-op hash peer 1 could check agreed') expect(coopA.lockstep.tick >= 295 && coopB.lockstep.tick === coopA.lockstep.tick, 'both peers ran the whole co-op run, tick for tick together') expect(coopDigest(coopWorldA) === coopDigest(coopWorldB), 'the two peers hold the same two-player world') expect(coopWorldA.players.length === 2, 'the world really has two players in it') expect(coopWorldA.kills > 0, 'the two players killed something together') expect(coopWorldA.players[0]!.xp !== coopWorldA.players[1]!.xp, 'experience is credited to the peer that landed the killing blow') expect(coopWorldA.players[0]!.x !== coopWorldB.players[0]!.x || coopWorldA.players[0]!.y === coopWorldB.players[0]!.y, 'player 0 moved under its own input') expect(coopDigest(coopWorldA) !== coopDigest(newCoopWorld()), 'the co-op world actually changed while it ran') // Monsters go for whoever is nearest, not for player 0 by seniority. const bait = createWorld(-300, 0) addPlayer(bait, 0, 0) bait.monsters.push({ index: 0, stats: { ...coopStats[0]!, aggroRadius: 400, reach: 40, speed: 0 }, x: 6, y: 0, hp: 50, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, }) const idle = { movement: { x: 0, y: 0 }, attack: false } for (let tick = 0; tick < 40; tick += 1) tickCombatMulti(bait, [idle, idle], options, { overlap: () => 0 }, xpTable) expect(bait.players[1]!.hp < bait.players[1]!.maxHp, 'a monster attacks the nearest player') expect(bait.players[0]!.hp === bait.players[0]!.maxHp, 'a monster 300 pixels away from the other player leaves it alone') // A dead peer must not stall the monsters or hide the living one. const bereaved = createWorld(-30, 0) addPlayer(bereaved, 30, 0) bereaved.players[1]!.hp = 0 bereaved.players[1]!.alive = false bereaved.players[1]!.respawnIn = 30 // The monster stands on top of the dead peer and within reach of the living one, // so a monster that still targeted corpses would attack the wrong player. bereaved.monsters.push({ index: 0, stats: { ...coopStats[0]!, aggroRadius: 400, reach: 80, speed: 0 }, x: 30, y: 0, hp: 50, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, }) for (let tick = 0; tick < 10; tick += 1) tickCombatMulti(bereaved, [idle, idle], options, { overlap: () => 0 }, xpTable) expect(bereaved.players[0]!.hp < bereaved.players[0]!.maxHp, 'with one peer dead the monsters hunt the other') expect(!bereaved.players[1]!.alive && bereaved.players[1]!.hp === 0, 'a dead peer stays dead while its respawn timer runs') // A hash whose tick has already fallen out of history is a different case: it // cannot be checked at all, and saying so is the honest answer. const [latePipeA, latePipeB] = memoryTransportPair() const lateA = makePeer(0, latePipeA, 0x6666) const lateB = makePeer(1, latePipeB, 0x6666) latePipeA.flush() for (let tick = 0; tick < 120; tick += 1) { lateA.session.setIntent(scriptedInput(0, tick)) lateB.session.setIntent(scriptedInput(1, tick)) lateA.session.pump() lateB.session.pump() latePipeA.flush() } latePipeB.send(encodeMessage({ kind: 'hash', peer: 1, tick: 0, hash: 0x1234 })) latePipeA.flush() for (let extra = 0; extra < 5; extra += 1) { lateA.session.pump(); latePipeA.flush() } expect(lateA.session.stats.hashesIgnored === 1, 'a hash for a tick that has left history is counted as uncheckable') expect(lateA.session.desyncReport === null, 'an uncheckable hash is not mistaken for a mismatch') expect(lateA.session.stats.hashesAgreed === lateA.session.stats.hashesCompared, 'a stale hash does not corrupt the agreement count') // --- four peers, one world, one fighter each -------------------------------- /** * Build a world with a given number of players, in peer order. * * @param count - how many players. * @returns the world. */ function newHubWorld(count: number): CombatWorld { const world = createWorld(-60, 0) for (let index = 1; index < count; index += 1) addPlayer(world, -60 + index * 40, 0) spawnMonsters(world, coopStats, 8, { x: 0, y: 0 }, 140, { overlap: () => 0 }) return world } const FOUR = 4 // The hub is grown rather than pre-sized: a peer that has not joined has no pipe // at all, which is the case worth testing — a transport that merely exists // handshakes immediately, because answering a hello is not a game action. const hub = new MemoryHub() const hubWorlds: CombatWorld[] = [] const hubSessions: NetplaySession[] = [] for (let index = 0; index < FOUR - 1; index += 1) { const world = newHubWorld(FOUR) hubWorlds.push(world) const session = new NetplaySession( { peer: index, peers: FOUR, seed: 0x4bee, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 }, coopSimulation(world), hub.attach(), ) session.start() hubSessions.push(session) } /** * Pump one round: every running peer once, then deliver. * * @param tick - the scripted tick. */ function pumpRound(tick: number): void { for (let index = 0; index < hubSessions.length; index += 1) { hubSessions[index]!.setIntent(scriptedInput(index, tick)) hubSessions[index]!.pump() } hub.flush() } // Three peers are up; the fourth has not joined. for (let tick = 0; tick < 60; tick += 1) pumpRound(tick) expect(hubSessions[0]!.stats.peersHeard === 2, 'a session knows which of its peers it has heard from') expect(hubSessions[0]!.stats.peersExpected === 3, 'a four-peer session expects three other peers') expect(!hubSessions[0]!.stats.ready, 'a session expecting four peers is not ready with three') expect(hubSessions[0]!.lockstep.tick === 0, 'with a peer still missing, nobody starts the game') expect(hubSessions[0]!.stats.stepped === 0, 'and no input is sent into the void before everyone is known') // The fourth peer joins late and nothing it missed was ever sent, because the // others refused to start without it. const lateWorld = newHubWorld(FOUR) const lateSession = new NetplaySession( { peer: 3, peers: FOUR, seed: 0x4bee, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 }, coopSimulation(lateWorld), hub.attach(), ) lateSession.start() hubWorlds.push(lateWorld) hubSessions.push(lateSession) for (let tick = 60; tick < 360; tick += 1) pumpRound(tick) expect(hubSessions.every(session => session.stats.handshaked), 'all four peers complete the handshake') expect(hubSessions.every(session => session.stats.acknowledged), 'every peer is acknowledged by all the others') expect(hubSessions.every(session => session.stats.ready), 'every peer becomes ready once the fourth arrives') expect(hubSessions.every(session => session.lockstep.tick >= 290), 'all four peers ran the whole session') expect( new Set(hubSessions.map(session => session.lockstep.tick)).size === 1, 'a late joiner runs level with the peers that waited for it, tick for tick', ) expect(hubWorlds.every(world => world.players.length === FOUR), 'each world holds one fighter per peer') const hubDigests = new Set(hubWorlds.map(world => coopDigest(world))) expect(hubDigests.size === 1, 'all four independently simulated worlds are identical') expect(hubSessions.every(session => session.desyncReport === null), 'four peers do not desync') expect( hubSessions.every(session => session.stats.hashesCompared > 200 && session.stats.hashesAgreed === session.stats.hashesCompared), 'every hash every peer could check agreed', ) expect( hubSessions.every(session => session.stats.malformed === 0), 'four peers exchange only well-formed messages', ) expect(hubWorlds[0]!.kills > 0, 'four fighters killed something together') expect( hubWorlds[0]!.players.some((player, index) => index > 0 && player.x !== hubWorlds[0]!.players[0]!.x), 'the fighters stand where their own peer drove them', ) expect(hubSessions[0]!.peerSeeds.length === 3, 'every peer seed is recorded, not just the first') expect(hubSessions[0]!.peerSeeds.every(entry => entry.seed === 0x4bee), 'every peer reports the same world seed') // One of the four goes quiet: the others must stall rather than run on without it. const beforeQuiet = hubSessions.map(session => session.lockstep.tick) hub.held.add(2) for (let tick = 360; tick < 420; tick += 1) pumpRound(tick) const stalledTicks = hubSessions.map((session, index) => session.lockstep.tick - beforeQuiet[index]!) expect(hubSessions.every(session => session.desyncReport === null), 'a quiet peer in a four-peer game causes no desync') expect( hubSessions[0]!.lockstep.tick <= hubSessions[2]!.lockstep.tick, 'the peers that lost a partner ran no further than the one they lost', ) expect( hubSessions.slice(0, 3).every(session => session.stats.waiting > 10), 'the peers that lost a partner counted the ticks they could not run', ) // Releasing the link leaves everyone the backlog they need to finish level. hub.held.delete(2) for (let tick = 420; tick < 520; tick += 1) pumpRound(tick) const target = Math.max(...hubSessions.map(session => session.lockstep.tick)) for (let round = 0; round < 40; round += 1) { for (let index = 0; index < FOUR; index += 1) { const session = hubSessions[index]! if (session.lockstep.tick >= target) continue session.setIntent(scriptedInput(index, session.lockstep.tick)) session.pump() } hub.flush() } expect( hubSessions.every(session => session.lockstep.tick === target), 'after the link recovers, every peer can be brought level from its own backlog', ) expect(new Set(hubWorlds.map(world => coopDigest(world))).size === 1, 'the four worlds are still identical after a stall') expect(stalledTicks[0]! < 60, 'the stall really did hold the other peers back') // --- a peer that alters input in flight is detected -------------------------- /** * A transport that rewrites the movement in the first input message it forwards. * * This is the shape of a real bug: not a malicious peer, but a corrupted or * mismatched build. The field is chosen so the change *must* matter — a movement * of half a step moves the player, whereas flipping the attack flag on a tick with * nothing in reach changes nothing, and a tamper that changes nothing proves * nothing. The first input message is the warm-up frame for tick 0, which the send * cursor fills in so that the first ticks have inputs at all; the world must * therefore diverge at tick 0. * * @param inner - the transport to wrap. * @returns the tampering transport. */ function tamperingTransport(inner: MemoryTransport): Transport { let tampered = false return { send: (data) => { if (!tampered && data[0] === 2) { tampered = true const copy = new Uint8Array(data) copy[6] = 500 & 0xff // movement x := 0.5, a value the script never sends copy[7] = (500 >> 8) & 0xff inner.send(copy) return } inner.send(data) }, onMessage: handler => { inner.onMessage(handler) }, onClose: handler => { inner.onClose(handler) }, close: () => { inner.close() }, get open(): boolean { return inner.open }, } } const [honestPipe, cheatPipe] = memoryTransportPair() const honest = makePeer(0, honestPipe, 0x2222) const cheat = makePeer(1, tamperingTransport(cheatPipe), 0x2222) honestPipe.flush() for (let tick = 0; tick < 40; tick += 1) { honest.session.setIntent(scriptedInput(0, tick)) cheat.session.setIntent(scriptedInput(1, tick)) honest.session.pump() cheat.session.pump() honestPipe.flush() } const report = honest.session.desyncReport ?? cheat.session.desyncReport expect(report !== null, 'a tampered input is detected as a desync') expect(report !== null && report.tick === 0, 'the desync is reported at the tick the tampered input applied to') expect(report !== null && report.local !== report.remote, 'the report carries both digests so the divergence is diagnosable') expect(honest.session.stats.hashesCompared > 0, 'hashes were exchanged before the divergence was found') // --- garbage on the wire is dropped, not fatal ------------------------------- const [cleanPipe, noisyPipe] = memoryTransportPair() const calm = makePeer(0, cleanPipe, 0x3333) const noisy = makePeer(1, noisyPipe, 0x3333) // One end receives an unknown type, the other an empty frame: both directions of // nonsense, delivered before either peer runs a tick. cleanPipe.send(new Uint8Array([200, 1, 2, 3])) noisyPipe.send(new Uint8Array(0)) cleanPipe.flush() for (let tick = 0; tick < 20; tick += 1) { calm.session.setIntent(scriptedInput(0, tick)) noisy.session.setIntent(scriptedInput(1, tick)) calm.session.pump() noisy.session.pump() cleanPipe.flush() } expect(noisy.session.stats.malformed === 1, 'an unknown message type is counted and dropped') expect(calm.session.stats.malformed === 1, 'an empty frame is counted and dropped at the other end') expect(noisy.session.stats.stepped > 10, 'the session keeps running after a malformed message') expect(calm.session.desyncReport === null && noisy.session.desyncReport === null, 'garbage does not desync the game') expect(digest(calm.game) === digest(noisy.game), 'a malformed message leaves both worlds identical') // --- a silent peer is a timeout, not a desync -------------------------------- // Peer 0's outbound link is held, so peer 1 hears nothing at all: that is the // "the other player vanished" case, and it must read as a loss, not a divergence. const [lostPipe, gonePipe] = memoryTransportPair() const heardFrom = makePeer(0, lostPipe, 0x4444) const starved = makePeer(1, gonePipe, 0x4444) lostPipe.flush() lostPipe.hold = true for (let tick = 0; tick < 60; tick += 1) { heardFrom.session.setIntent(scriptedInput(0, tick)) starved.session.setIntent(scriptedInput(1, tick)) heardFrom.session.pump() starved.session.pump() lostPipe.flush() } expect(starved.session.stats.timedOut, 'a peer nothing has been heard from is declared lost') expect(starved.session.stats.waiting > 20, 'the starved peer counted every tick it could not run') expect(starved.session.lockstep.tick === 0, 'the starved peer never fabricated the input it was missing') expect(starved.session.desyncReport === null && heardFrom.session.desyncReport === null, 'a lost peer is a timeout, never a desync') // The other end does *not* give up: its peer is still retrying its handshake, so // something is arriving. "No answer yet" and "no peer" are different states, and // the session is expected to tell them apart. expect(!heardFrom.session.stats.timedOut, 'a peer still trying to handshake is not declared lost') expect(starved.session.stats.helloSent > 1, 'a peer whose handshake goes unanswered keeps retrying it') // The other end hears those hellos and answers them, but its answers never // arrive, so its own handshake is never acknowledged and it keeps retrying too: // "no acknowledgement" is the condition for retrying, not "no answer". expect(heardFrom.session.stats.helloSent > 1, 'a peer whose own hello was never acknowledged keeps retrying it') expect(!heardFrom.session.stats.acknowledged, 'an unacknowledged handshake is never called complete') expect(heardFrom.session.lockstep.tick === 0, 'neither peer runs a tick before the handshake completes') // --- goodbye ----------------------------------------------------------------- const [byePipe] = memoryTransportPair() const leaving = makePeer(0, byePipe, 0x5555) leaving.session.pump() byePipe.flush() leaving.session.leave() expect(!leaving.session.stats.connected, 'leaving closes the pipe') expect(leaving.session.stats.sent === 2, 'leaving sends exactly the goodbye after the handshake') // --- the same sessions over a real socket ------------------------------------ const relay = await startRelay(0) const socketA = new WebSocket(relay.url) const socketB = new WebSocket(relay.url) await Promise.all([ new Promise(resolve => { socketA.addEventListener('open', () => { resolve() }) }), new Promise(resolve => { socketB.addEventListener('open', () => { resolve() }) }), ]) expect(relay.accepted === 2, 'the relay accepted both peers') const netA = makePeer(0, socketTransport(socketA as unknown as Parameters[0]), 0x7777) const netB = makePeer(1, socketTransport(socketB as unknown as Parameters[0]), 0x7777) const deadline = Date.now() + 4000 for (let tick = 0; tick < 150 && Date.now() < deadline; tick += 1) { netA.session.setIntent(scriptedInput(0, tick)) netB.session.setIntent(scriptedInput(1, tick)) netA.session.pump() netB.session.pump() // Real sockets deliver on the event loop, so the session is pumped more than // once per scripted tick: a peer that is ahead stalls until its peer catches up. await new Promise(resolve => { setTimeout(resolve, 8) }) netA.session.pump() netB.session.pump() await new Promise(resolve => { setTimeout(resolve, 1) }) } expect(netA.session.stats.handshaked && netB.session.stats.handshaked, 'the handshake crosses a real socket') expect(relay.forwarded > 200, 'the relay forwarded traffic in both directions') expect(netA.session.stats.stepped > 120 && netB.session.stats.stepped > 120, 'both peers ran the game over the socket') expect(Math.abs(netA.session.stats.stepped - netB.session.stats.stepped) <= 1, 'the peers stay within one tick of each other') expect(netA.session.desyncReport === null && netB.session.desyncReport === null, 'no desync over a real socket') expect( netA.session.stats.hashesCompared > 50 && netA.session.stats.hashesCompared === netA.session.stats.hashesAgreed, 'every hash that crossed the socket agreed', ) expect( netB.session.stats.hashesCompared > 50 && netB.session.stats.hashesCompared === netB.session.stats.hashesAgreed, 'every hash that crossed the socket agreed at the other peer', ) expect(digest(netA.game) === digest(netB.game), 'two worlds fed by a real socket end up identical') expect(netA.session.stats.malformed === 0 && netB.session.stats.malformed === 0, 'the socket transport framed every message correctly') // Closing the socket must be observed by the session rather than hanging it. netA.session.leave() await new Promise(resolve => { setTimeout(resolve, 40) }) expect(!netB.session.stats.connected, 'a peer leaving over a socket is observed by the other end') socketA.close() socketB.close() await relay.close() expect(relay.clients === 0, 'the relay closes every client') // --- three peers over real sockets ------------------------------------------- const relayThree = await startRelay(0) const THREE = 3 const threeWorlds = [newHubWorld(THREE), newHubWorld(THREE), newHubWorld(THREE)] const threeSessions: NetplaySession[] = [] for (let index = 0; index < THREE; index += 1) { const socket = new WebSocket(relayThree.url) await new Promise(resolve => { socket.addEventListener('open', () => { resolve() }) }) const session = new NetplaySession( { peer: index, peers: THREE, seed: 0x3eed, inputDelayTicks: 4, hashInterval: 1, timeoutTicks: 400 }, coopSimulation(threeWorlds[index]!), socketTransport(socket as unknown as Parameters[0]), ) session.start() threeSessions.push(session) } const threeSockets: WebSocket[] = [] expect(relayThree.accepted === THREE, 'the relay accepts all three peers') const threeDeadline = Date.now() + 6000 for (let tick = 0; tick < 160 && Date.now() < threeDeadline; tick += 1) { for (let index = 0; index < THREE; index += 1) { threeSessions[index]!.setIntent(scriptedInput(index, tick)) threeSessions[index]!.pump() } await new Promise(resolve => { setTimeout(resolve, 7) }) for (let index = 0; index < THREE; index += 1) threeSessions[index]!.pump() await new Promise(resolve => { setTimeout(resolve, 1) }) } expect(threeSessions.every(session => session.stats.handshaked), 'three peers complete the handshake over real sockets') expect(threeSessions.every(session => session.stats.acknowledged), 'three peers are acknowledged by each other') expect(threeSessions.every(session => session.desyncReport === null), 'three peers over sockets do not desync') expect(threeSessions.every(session => session.stats.malformed === 0), 'the socket transport frames every message for three peers') expect( threeSessions.every(session => session.stats.hashesCompared > 50 && session.stats.hashesAgreed === session.stats.hashesCompared), 'three peers agree on every hash they could check', ) // Level the peers that fell behind, then require byte-identical worlds. const threeTarget = Math.max(...threeSessions.map(session => session.lockstep.tick)) for (let round = 0; round < 40; round += 1) { for (let index = 0; index < THREE; index += 1) { const session = threeSessions[index]! if (session.lockstep.tick >= threeTarget) continue session.setIntent(scriptedInput(index, session.lockstep.tick)) session.pump() } await new Promise(resolve => { setTimeout(resolve, 4) }) } expect( threeSessions.every(session => session.lockstep.tick === threeTarget), 'three peers over sockets can be brought level from their backlog', ) expect( new Set(threeWorlds.map(world => coopDigest(world))).size === 1, 'three worlds fed by real sockets are identical', ) for (const session of threeSessions) session.leave() await new Promise(resolve => { setTimeout(resolve, 40) }) await relayThree.close() expect(relayThree.clients === 0, 'the three-peer relay closes every client') void threeSockets for (const problem of problems.slice(0, 12)) console.log(` - ${problem}`) console.log(problems.length === 0 ? 'RESULT protocol, transports and two-peer lockstep hold' : 'RESULT FAILED') // disabled exit: problems.length === 0 ? 0 : 1) suiteCompleted = true; } describe('verify-net.ts', () => { test.skipIf(isSkip)('evaluates script successfully', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); vitestExpect(problems).toEqual([]); }); test.skipIf(isSkip)('runs assertion', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('runs assertion')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the tick round-trips', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the tick round-trips')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the x movement round-trips as fixed point', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the x movement round-trips as fixed point')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the y movement round-trips as fixed point', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the y movement round-trips as fixed point')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the control flags round-trip', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the control flags round-trip')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the skill slot round-trips', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the skill slot round-trips')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the seed round-trips', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the seed round-trips')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the hash and tick round-trip', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the hash and tick round-trip')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an input message is twelve bytes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an input message is twelve bytes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an input message starts with its type and peer', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an input message starts with its type and peer')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a movement of -1 is fixed point -1000, little-endian', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a movement of -1 is fixed point -1000, little-endian')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('only the attack flag is set', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('only the attack flag is set')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a hash message is ten bytes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a hash message is ten bytes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a hello message is eight bytes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a hello message is eight bytes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an acknowledgement names the peer it acknowledges', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an acknowledgement names the peer it acknowledges')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a plain introduction is not mistaken for an acknowledgement', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a plain introduction is not mistaken for an acknowledgement')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an absurd movement is clamped, not wrapped', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an absurd movement is clamped, not wrapped')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a fresh memory pair is open at both ends', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a fresh memory pair is open at both ends')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a memory message is queued, not delivered on send', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a memory message is queued, not delivered on send')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('flushing delivers the queued message', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('flushing delivers the queued message')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an oversized message is refused at the transport', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an oversized message is refused at the transport')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('closing one end closes the pair', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('closing one end closes the pair')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('sending on a closed pair drops instead of throwing', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('sending on a closed pair drops instead of throwing')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('nothing is sent before the session is pumped', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('nothing is sent before the session is pumped')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the handshake exchanges the world seed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the handshake exchanges the world seed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers complete the handshake', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers complete the handshake')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers learn that their own hello was heard', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers learn that their own hello was heard')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a completed handshake stops: one hello and one acknowledgement each', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a completed handshake stops: one hello and one acknowledgement each')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer whose link stalls does not cause a desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose link stalls does not cause a desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash peer 0 could check agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash peer 0 could check agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the peer that waited counted the stalled ticks', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the peer that waited counted the stalled ticks')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers compared state hashes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers compared state hashes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash that arrived at peer 1 agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash that arrived at peer 1 agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('no message was malformed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('no message was malformed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a link that stalls for a moment is not declared lost', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a link that stalls for a moment is not declared lost')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('traffic actually crossed the pipe', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('traffic actually crossed the pipe')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers ran the game, one eventually catching up', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the game, one eventually catching up')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the peer whose link was open ran ahead while the other was starved', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the peer whose link was open ran ahead while the other was starved')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the two ends resume within a few ticks of each other', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the two ends resume within a few ticks of each other')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the starved peer can be brought level from the backlog it already holds', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer can be brought level from the backlog it already holds')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('at the same tick, the peer that was starved holds exactly the other peers world', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith("at the same tick, the peer that was starved holds exactly the other peer's world")); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers agree on the kill count', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on the kill count')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers agree on the random stream position', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on the random stream position')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers agree on monster health', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on monster health')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the test actually produced kills to disagree about', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the test actually produced kills to disagree about')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer that fell behind still checks every hash it received', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer that fell behind still checks every hash it received')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('catching up results in a real number of compared hashes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('catching up results in a real number of compared hashes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash peer 1 could check agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash peer 1 could check agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('two players in one world do not desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('two players in one world do not desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every co-op hash peer 0 could check agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every co-op hash peer 0 could check agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every co-op hash peer 1 could check agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every co-op hash peer 1 could check agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers ran the whole co-op run, tick for tick together', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the whole co-op run, tick for tick together')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the two peers hold the same two-player world', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the two peers hold the same two-player world')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the world really has two players in it', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the world really has two players in it')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the two players killed something together', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the two players killed something together')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('experience is credited to the peer that landed the killing blow', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('experience is credited to the peer that landed the killing blow')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('player 0 moved under its own input', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('player 0 moved under its own input')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the co-op world actually changed while it ran', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the co-op world actually changed while it ran')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a monster attacks the nearest player', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a monster attacks the nearest player')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a monster 300 pixels away from the other player leaves it alone', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a monster 300 pixels away from the other player leaves it alone')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('with one peer dead the monsters hunt the other', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('with one peer dead the monsters hunt the other')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a dead peer stays dead while its respawn timer runs', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a dead peer stays dead while its respawn timer runs')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a hash for a tick that has left history is counted as uncheckable', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a hash for a tick that has left history is counted as uncheckable')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an uncheckable hash is not mistaken for a mismatch', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an uncheckable hash is not mistaken for a mismatch')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a stale hash does not corrupt the agreement count', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a stale hash does not corrupt the agreement count')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a session knows which of its peers it has heard from', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a session knows which of its peers it has heard from')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a four-peer session expects three other peers', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a four-peer session expects three other peers')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a session expecting four peers is not ready with three', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a session expecting four peers is not ready with three')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('with a peer still missing, nobody starts the game', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('with a peer still missing, nobody starts the game')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('and no input is sent into the void before everyone is known', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('and no input is sent into the void before everyone is known')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('all four peers complete the handshake', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('all four peers complete the handshake')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every peer is acknowledged by all the others', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every peer is acknowledged by all the others')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every peer becomes ready once the fourth arrives', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every peer becomes ready once the fourth arrives')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('all four peers ran the whole session', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('all four peers ran the whole session')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a late joiner runs level with the peers that waited for it, tick for tick', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a late joiner runs level with the peers that waited for it, tick for tick')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('each world holds one fighter per peer', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('each world holds one fighter per peer')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('all four independently simulated worlds are identical', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('all four independently simulated worlds are identical')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('four peers do not desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('four peers do not desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash every peer could check agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash every peer could check agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('four peers exchange only well-formed messages', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('four peers exchange only well-formed messages')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('four fighters killed something together', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('four fighters killed something together')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the fighters stand where their own peer drove them', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the fighters stand where their own peer drove them')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every peer seed is recorded, not just the first', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every peer seed is recorded, not just the first')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every peer reports the same world seed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every peer reports the same world seed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a quiet peer in a four-peer game causes no desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a quiet peer in a four-peer game causes no desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the peers that lost a partner ran no further than the one they lost', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the peers that lost a partner ran no further than the one they lost')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the peers that lost a partner counted the ticks they could not run', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the peers that lost a partner counted the ticks they could not run')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('after the link recovers, every peer can be brought level from its own backlog', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('after the link recovers, every peer can be brought level from its own backlog')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the four worlds are still identical after a stall', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the four worlds are still identical after a stall')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the stall really did hold the other peers back', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the stall really did hold the other peers back')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a tampered input is detected as a desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a tampered input is detected as a desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the desync is reported at the tick the tampered input applied to', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the desync is reported at the tick the tampered input applied to')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the report carries both digests so the divergence is diagnosable', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the report carries both digests so the divergence is diagnosable')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('hashes were exchanged before the divergence was found', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('hashes were exchanged before the divergence was found')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an unknown message type is counted and dropped', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an unknown message type is counted and dropped')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an empty frame is counted and dropped at the other end', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an empty frame is counted and dropped at the other end')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the session keeps running after a malformed message', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the session keeps running after a malformed message')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('garbage does not desync the game', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('garbage does not desync the game')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a malformed message leaves both worlds identical', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a malformed message leaves both worlds identical')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer nothing has been heard from is declared lost', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer nothing has been heard from is declared lost')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the starved peer counted every tick it could not run', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer counted every tick it could not run')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the starved peer never fabricated the input it was missing', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer never fabricated the input it was missing')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a lost peer is a timeout, never a desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a lost peer is a timeout, never a desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer still trying to handshake is not declared lost', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer still trying to handshake is not declared lost')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer whose handshake goes unanswered keeps retrying it', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose handshake goes unanswered keeps retrying it')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer whose own hello was never acknowledged keeps retrying it', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose own hello was never acknowledged keeps retrying it')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('an unacknowledged handshake is never called complete', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('an unacknowledged handshake is never called complete')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('neither peer runs a tick before the handshake completes', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('neither peer runs a tick before the handshake completes')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('leaving closes the pipe', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('leaving closes the pipe')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('leaving sends exactly the goodbye after the handshake', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('leaving sends exactly the goodbye after the handshake')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the relay accepted both peers', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the relay accepted both peers')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the handshake crosses a real socket', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the handshake crosses a real socket')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the relay forwarded traffic in both directions', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the relay forwarded traffic in both directions')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('both peers ran the game over the socket', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the game over the socket')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the peers stay within one tick of each other', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the peers stay within one tick of each other')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('no desync over a real socket', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('no desync over a real socket')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash that crossed the socket agreed', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash that crossed the socket agreed')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('every hash that crossed the socket agreed at the other peer', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('every hash that crossed the socket agreed at the other peer')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('two worlds fed by a real socket end up identical', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('two worlds fed by a real socket end up identical')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the socket transport framed every message correctly', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the socket transport framed every message correctly')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('a peer leaving over a socket is observed by the other end', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('a peer leaving over a socket is observed by the other end')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the relay closes every client', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the relay closes every client')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the relay accepts all three peers', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the relay accepts all three peers')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three peers complete the handshake over real sockets', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three peers complete the handshake over real sockets')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three peers are acknowledged by each other', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three peers are acknowledged by each other')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three peers over sockets do not desync', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three peers over sockets do not desync')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the socket transport frames every message for three peers', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the socket transport frames every message for three peers')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three peers agree on every hash they could check', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three peers agree on every hash they could check')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three peers over sockets can be brought level from their backlog', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three peers over sockets can be brought level from their backlog')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('three worlds fed by real sockets are identical', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('three worlds fed by real sockets are identical')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); test.skipIf(isSkip)('the three-peer relay closes every client', () => { vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true); let resKey = _results.findIndex(x => x.desc.startsWith('the three-peer relay closes every client')); if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true); else vitestExpect(true).toBe(true); }); });