Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions client/src/hooks/__tests__/liveOpponents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";

import { countLiveOpponents } from "../liveOpponents";

describe("countLiveOpponents", () => {
it("counts the live seats other than the local human", () => {
expect(
countLiveOpponents([
{ id: 0, is_eliminated: false },
{ id: 1, is_eliminated: false },
{ id: 2, is_eliminated: false },
]),
).toBe(2);
});

it("does not subtract the local seat when seat 0 is eliminated", () => {
// Regression: the old `Math.max(0, liveCount - 1)` undercounted this to 1.
expect(
countLiveOpponents([
{ id: 0, is_eliminated: true },
{ id: 1, is_eliminated: false },
{ id: 2, is_eliminated: false },
]),
).toBe(2);
});

it("excludes eliminated opponents", () => {
expect(
countLiveOpponents([
{ id: 0, is_eliminated: false },
{ id: 1, is_eliminated: true },
{ id: 2, is_eliminated: false },
]),
).toBe(1);
});

it("is zero when only the local human remains", () => {
expect(countLiveOpponents([{ id: 0, is_eliminated: false }])).toBe(0);
});

it("is zero when everyone is eliminated", () => {
expect(
countLiveOpponents([
{ id: 0, is_eliminated: true },
{ id: 1, is_eliminated: true },
]),
).toBe(0);
});
});
22 changes: 22 additions & 0 deletions client/src/hooks/liveOpponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** A player as the resumable-match summary sees it (a structural subset of the
* engine `Player`, whose `is_eliminated` is optional on the wire — an absent
* flag means "not eliminated"). */
export interface ResumableSummaryPlayer {
id: number;
is_eliminated?: boolean;
}

/**
* Number of live opponents for the local human (seat 0) in a resumable match.
*
* Per CR 800.4 an eliminated player is out, so opponents are the live seats
* other than you. Only subtract the local seat when it is itself still alive:
* if seat 0 has been eliminated but the match is still resumable (a multi-seat
* local / FFA game), it is not among the live seats, so every live seat is an
* opponent and there is nothing to subtract.
*/
export function countLiveOpponents(players: ResumableSummaryPlayer[]): number {
const you = players.find((p) => p.id === 0);
const liveCount = players.filter((p) => !p.is_eliminated).length;
return liveCount - (you && !you.is_eliminated ? 1 : 0);
}
4 changes: 2 additions & 2 deletions client/src/hooks/useResumables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";

import { countLiveOpponents } from "./liveOpponents";
import { loadP2PSession } from "../services/p2pSession";
import { loadWsSession } from "../services/multiplayerSession";
import {
Expand Down Expand Up @@ -86,12 +87,11 @@ export function useResumables(): Resumables {
// CR 800.4: eliminated players are out — only live seats count as
// opponents. Seat 0 is the local human in AI/host matches.
const you = state.players.find((p) => p.id === 0);
const liveCount = state.players.filter((p) => !p.is_eliminated).length;
setMatchSummary({
turn: state.turn_number,
isYourTurn: state.active_player === 0,
yourLife: you && !you.is_eliminated ? you.life : null,
opponentCount: Math.max(0, liveCount - 1),
opponentCount: countLiveOpponents(state.players),
});
} else {
clearActiveGame();
Expand Down
Loading