testsAndMisc-archive/bucket_catch/packages/frontend/src/App.tsx
Krzysztof kuhy Rudnicki 031bff898e bucket_catch: make the done phase carry its result
Removes the last fixable non-null assertion. GamePhase goes from a string
union to a tagged one whose "done" arm carries `result: GameResult`, so
narrowing on phase.kind yields a non-null result and `fileResult!`
disappears. GameResult - previously an untagged union nothing consumed -
is now tagged, so the render picks a screen from the discriminant instead
of inferring it from which of two nullable states was set.

Three earlier attempts at this site were reverted: a null guard returning
null, a widened ReactElement | null return, and a tagged GameResult with
phase left as a string. Each either added a branch no test could reach -
dropping the 100% gate - or left the value nullable in the fall-through.
The invariant lived in the phase, so the fix had to as well.

Coverage stays 100%. Statements 585 -> 579 and lines 536 -> 530, exactly
the two useState declarations and their setters that the collapse
removed; branches unchanged at 217, so nothing new went untested. All 10
App tests pass unmodified - they drive the app through mocked onDone
callbacks, so both done arms are exercised through the new union.

eslint 5 -> 4 (45 -> 4 for the session). The remaining four are reported
in the previous commit's evidence. usePuzzleGameLoop's Map.get assertion
is not among the fixable ones: `?? Infinity` was tried and measured at
219 branches / 99.54%, since the default side is unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 11:55:05 +02:00

87 lines
2.5 KiB
TypeScript

import React, { useCallback, useState } from "react";
import type {
GamePhase,
FileGameResult,
PuzzleGameResult,
TransferMode,
} from "./types";
import { DropZone } from "./components/DropZone";
import { ModeSelect } from "./components/ModeSelect";
import { GameCanvas } from "./components/GameCanvas";
import { PuzzleCanvas } from "./components/PuzzleCanvas";
import { ScoreScreen } from "./components/ScoreScreen";
import { PuzzleResult } from "./components/PuzzleResult";
export default function App(): React.ReactElement {
const [phase, setPhase] = useState<GamePhase>({ kind: "drop" });
const [files, setFiles] = useState<File[]>([]);
const [mode, setMode] = useState<TransferMode>("download");
const [puzzleGridSize, setPuzzleGridSize] = useState(4);
const handleFiles = useCallback((incoming: File[]) => {
setFiles(incoming);
setPhase({ kind: "mode" });
}, []);
const handlePuzzleDirect = useCallback(
(imageFile: File, gridSize: number) => {
setFiles([imageFile]);
setMode("puzzle");
setPuzzleGridSize(gridSize);
setPhase({ kind: "playing" });
},
[],
);
const handleStart = useCallback((selected: TransferMode, gridSize = 4) => {
setMode(selected);
if (selected === "puzzle") setPuzzleGridSize(gridSize);
setPhase({ kind: "playing" });
}, []);
const handleFileDone = useCallback((value: FileGameResult) => {
setPhase({ kind: "done", result: { kind: "file", value } });
}, []);
const handlePuzzleDone = useCallback((value: PuzzleGameResult) => {
setPhase({ kind: "done", result: { kind: "puzzle", value } });
}, []);
const handleRestart = useCallback(() => {
setFiles([]);
setPhase({ kind: "drop" });
}, []);
if (phase.kind === "drop") {
return <DropZone onFiles={handleFiles} onPuzzle={handlePuzzleDirect} />;
}
if (phase.kind === "mode") {
return <ModeSelect files={files} onStart={handleStart} />;
}
if (phase.kind === "playing") {
if (mode === "puzzle") {
return (
<PuzzleCanvas
imageFile={files[0]}
gridSize={puzzleGridSize}
onDone={handlePuzzleDone}
/>
);
}
return <GameCanvas files={files} onDone={handleFileDone} />;
}
// done phase: the phase carries its result, so both arms are non-null.
if (phase.result.kind === "puzzle") {
return (
<PuzzleResult result={phase.result.value} onRestart={handleRestart} />
);
}
return (
<ScoreScreen
result={phase.result.value}
mode={mode}
onRestart={handleRestart}
/>
);
}