diff --git a/server/src/engine/edge-cases.test.ts b/server/src/engine/edge-cases.test.ts index 194d1e7..ada2623 100644 --- a/server/src/engine/edge-cases.test.ts +++ b/server/src/engine/edge-cases.test.ts @@ -243,6 +243,49 @@ describe('challenge type rotation', () => { }) }) +describe('simultaneous KO — both bots reach 0 HP same round', () => { + it('when both bots KO same round, botA is penalized first (asymmetric tiebreaker)', () => { + // Simulate the orchestrator's HP application logic: + // hpB = Math.max(0, hpB - result.botADamage) + // hpA = Math.max(0, hpA - result.botBDamage) + // Then: if (hpA <= 0 || hpB <= 0) winnerId = hpA <= 0 ? botB.id : botA.id + const KO_THRESHOLD = 0 + let hpA = 25 // low enough to die this round + let hpB = 25 + + // One correct one wrong → winner gets ~30 damage from baseDamage 20 + margin + const ch = makeChallenge(['Bitcoin']) + // Both correct, A faster → A deals more damage to B, but B also deals damage to A + const result = scoreRound(ch, botA, botB, resp('Bitcoin', 200), resp('Bitcoin', 8000), null, 0, 0) + + // Apply damage (mimicking orchestrator logic) + hpB = Math.max(KO_THRESHOLD, hpB - result.botADamage) + hpA = Math.max(KO_THRESHOLD, hpA - result.botBDamage) + + // If both at or below 0, the orchestrator checks hpA first + if (hpA <= KO_THRESHOLD && hpB <= KO_THRESHOLD) { + // Orchestrator: winnerId = hpA <= 0 ? botB.id : botA.id + // When BOTH are at 0, hpA <= 0 is checked first → botB wins + const winnerId = hpA <= KO_THRESHOLD ? 'b1' : 'a1' + expect(winnerId).toBe('b1') + } else if (hpA <= KO_THRESHOLD) { + expect(true).toBe(true) // botB wins, expected + } else if (hpB <= KO_THRESHOLD) { + expect(true).toBe(true) // botA wins, expected + } + + // Regardless of who dies, both HP should be at or below starting value + expect(hpA).toBeLessThanOrEqual(25) + expect(hpB).toBeLessThanOrEqual(25) + }) + + it('ELO can go negative when 0-rated bot loses', () => { + const result = calculateElo(0, 0) + // Loser at 0 should go negative (no clamping in the formula) + expect(result.newLoserElo).toBeLessThan(0) + }) +}) + describe('10-round double timeout simulation', () => { it('both bots timeout every round: fight ends as draw, equal HP, no infinite loop', () => { const STARTING_HP = 200