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
1 change: 1 addition & 0 deletions frontend/src/components/DeckShowcase.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function renderShowcase(overrides: Partial<Parameters<typeof DeckShowcase>[0]> =
colorIdentityByDeckName: {},
winTally: null,
winTurns: null,
winTurnSums: null,
gamesPlayed: 0,
totalSimulations: 20,
jobStatus: 'RUNNING',
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/DeckShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface DeckShowcaseProps {
colorIdentityByDeckName: Record<string, string[]>;
winTally: Record<string, number> | null;
winTurns: Record<string, number[]> | null;
winTurnSums: Record<string, number> | null;
gamesPlayed: number;
totalSimulations: number;
deckLinks?: Record<string, string | null>;
Expand Down Expand Up @@ -48,6 +49,7 @@ export const DeckShowcase = memo(function DeckShowcase({
colorIdentityByDeckName,
winTally,
winTurns,
winTurnSums,
gamesPlayed,
totalSimulations,
deckLinks,
Expand Down Expand Up @@ -77,12 +79,12 @@ export const DeckShowcase = memo(function DeckShowcase({
const turns = winTurns?.[name] ?? [];
stats[name] = {
winPct: gamesPlayed > 0 ? ((wins / gamesPlayed) * 100).toFixed(0) : null,
avgTurn: turns.length > 0 ? (turns.reduce((acc, t) => acc + t, 0) / turns.length).toFixed(1) : null,
avgTurn: turns.length > 0 && winTurnSums ? (winTurnSums[name] / turns.length).toFixed(1) : null,
};
}

return { sorted: s, maxWins: m, deckStats: stats };
}, [deckNames, winTally, winTurns, gamesPlayed]);
}, [deckNames, winTally, winTurns, winTurnSums, gamesPlayed]);

return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/useWinData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,13 @@ describe('resolveEffectiveWins', () => {
const noSims: ReturnType<typeof computeSimWins> = {
simWinTally: null,
simWinTurns: null,
simWinTurnSums: null,
simGamesCompleted: 0,
};
const noStructured: ReturnType<typeof computeStructuredWins> = {
structuredWinTally: null,
structuredWinTurns: null,
structuredWinTurnSums: null,
};

it('prefers server results when present', () => {
Expand Down
29 changes: 21 additions & 8 deletions frontend/src/hooks/useWinData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { StructuredGame } from '@shared/types/log';
export interface WinData {
winTally: Record<string, number> | null;
winTurns: Record<string, number[]> | null;
winTurnSums: Record<string, number> | null;
gamesPlayed: number;
simGamesCompleted: number;
}
Expand All @@ -18,6 +19,7 @@ export interface WinData {
export interface SimWinResult {
simWinTally: Record<string, number> | null;
simWinTurns: Record<string, number[]> | null;
simWinTurnSums: Record<string, number> | null;
simGamesCompleted: number;
}

Expand All @@ -30,7 +32,7 @@ export function computeSimWins(
deckNames: string[],
): SimWinResult {
if (simulations.length === 0) {
return { simWinTally: null, simWinTurns: null, simGamesCompleted: 0 };
return { simWinTally: null, simWinTurns: null, simWinTurnSums: null, simGamesCompleted: 0 };
}

const completedSims = simulations.filter((s) => s.state === 'COMPLETED');
Expand All @@ -42,15 +44,17 @@ export function computeSimWins(
const gamesCompleted = completedSims.reduce(
(sum, s) => sum + (s.winners && s.winners.length > 0 ? s.winners.length : 1), 0,
) || completedSims.length;
return { simWinTally: null, simWinTurns: null, simGamesCompleted: gamesCompleted };
return { simWinTally: null, simWinTurns: null, simWinTurnSums: null, simGamesCompleted: gamesCompleted };
}

const tally: Record<string, number> = {};
const turns: Record<string, number[]> = {};
const turnSums: Record<string, number> = {};

for (const name of deckNames) {
tally[name] = 0;
turns[name] = [];
turnSums[name] = 0;
}

for (const sim of completedSims) {
Expand All @@ -76,6 +80,7 @@ export function computeSimWins(
if (i < simTurns.length && simTurns[i] !== undefined) {
if (!turns[matchedDeck]) turns[matchedDeck] = [];
turns[matchedDeck].push(simTurns[i]);
turnSums[matchedDeck] = (turnSums[matchedDeck] || 0) + simTurns[i];
}
}
}
Expand All @@ -90,12 +95,13 @@ export function computeSimWins(
(sum, s) => sum + (s.winners && s.winners.length > 0 ? s.winners.length : 1), 0,
) || completedSims.length;

return { simWinTally: tally, simWinTurns: turns, simGamesCompleted: gamesCompleted };
return { simWinTally: tally, simWinTurns: turns, simWinTurnSums: turnSums, simGamesCompleted: gamesCompleted };
}

export interface StructuredWinResult {
structuredWinTally: Record<string, number> | null;
structuredWinTurns: Record<string, number[]> | null;
structuredWinTurnSums: Record<string, number> | null;
}

/**
Expand All @@ -106,16 +112,18 @@ export function computeStructuredWins(
logDeckNames: string[] | null,
): StructuredWinResult {
if (!structuredGames || structuredGames.length === 0) {
return { structuredWinTally: null, structuredWinTurns: null };
return { structuredWinTally: null, structuredWinTurns: null, structuredWinTurnSums: null };
}

const tally: Record<string, number> = {};
const turns: Record<string, number[]> = {};
const turnSums: Record<string, number> = {};

if (logDeckNames) {
for (const name of logDeckNames) {
tally[name] = 0;
turns[name] = [];
turnSums[name] = 0;
}
}

Expand All @@ -131,6 +139,7 @@ export function computeStructuredWins(
if (game.winningTurn !== undefined) {
if (!turns[matchedDeck]) turns[matchedDeck] = [];
turns[matchedDeck].push(game.winningTurn);
turnSums[matchedDeck] = (turnSums[matchedDeck] || 0) + game.winningTurn;
}
}
}
Expand All @@ -139,7 +148,7 @@ export function computeStructuredWins(
turns[deck].sort((a, b) => a - b);
}

return { structuredWinTally: tally, structuredWinTurns: turns };
return { structuredWinTally: tally, structuredWinTurns: turns, structuredWinTurnSums: turnSums };
}

/**
Expand All @@ -151,8 +160,8 @@ export function resolveEffectiveWins(
structuredResult: StructuredWinResult,
structuredGames: StructuredGame[] | null,
): WinData {
const { structuredWinTally, structuredWinTurns } = structuredResult;
const { simWinTally, simWinTurns, simGamesCompleted } = simResult;
const { structuredWinTally, structuredWinTurns, structuredWinTurnSums } = structuredResult;
const { simWinTally, simWinTurns, simWinTurnSums, simGamesCompleted } = simResult;

const winTally =
jobResults?.wins ??
Expand All @@ -166,13 +175,17 @@ export function resolveEffectiveWins(
structuredWinTally && Object.keys(structuredWinTally).length > 0
? structuredWinTurns
: simWinTurns;
const winTurnSums =
structuredWinTally && Object.keys(structuredWinTally).length > 0
? structuredWinTurnSums
: simWinTurnSums;
const gamesPlayed =
jobResults?.gamesPlayed ??
(structuredGames && structuredGames.length > 0
? structuredGames.length
: simGamesCompleted);

return { winTally, winTurns, gamesPlayed, simGamesCompleted };
return { winTally, winTurns, winTurnSums, gamesPlayed, simGamesCompleted };
}

// ---------------------------------------------------------------------------
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/JobStatus.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import { screen, waitFor, fireEvent, act } from '@testing-library/react';
import { renderWithRouter } from '../test/render';
import JobStatusPage from './JobStatus';
import { useJobStream } from '../hooks/useJobStream';
Expand Down Expand Up @@ -53,6 +53,7 @@ vi.mock('@tanstack/react-query', async () => {
const defaultWinData = {
winTally: null,
winTurns: null,
winTurnSums: null,
gamesPlayed: 0,
simGamesCompleted: 0,
};
Expand Down Expand Up @@ -408,7 +409,9 @@ describe('JobStatus β€” Run Again', () => {
});
expect(screen.getByText('Submitting...').closest('button')).toBeDisabled();

resolveResponse({ ok: true, json: () => Promise.resolve({ id: 'job-new' }) } as unknown as Response);
await act(async () => {
resolveResponse({ ok: true, json: () => Promise.resolve({ id: 'job-new' }) } as unknown as Response);
});
});

it('shows error when resubmit fails', async () => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/JobStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function JobStatusPage() {
// Data hooks
const { job, simulations, error: streamError } = useJobStream(id);
const logs = useJobLogs(id, job, { showLogPanel, loadStructured: loadStructuredLogs });
const { winTally, winTurns, gamesPlayed, simGamesCompleted } = useWinData(
const { winTally, winTurns, winTurnSums, gamesPlayed, simGamesCompleted } = useWinData(
job, simulations, logs.structuredGames, logs.deckNames,
);

Expand Down Expand Up @@ -201,6 +201,7 @@ export default function JobStatusPage() {
colorIdentityByDeckName={logs.colorIdentityByDeckName}
winTally={winTally}
winTurns={winTurns}
winTurnSums={winTurnSums}
gamesPlayed={gamesPlayed}
totalSimulations={job.simulations}
deckLinks={job.deckLinks}
Expand Down
Loading