diff --git a/bucket_catch/packages/frontend/src/App.tsx b/bucket_catch/packages/frontend/src/App.tsx index 0e3413a..f9a356f 100644 --- a/bucket_catch/packages/frontend/src/App.tsx +++ b/bucket_catch/packages/frontend/src/App.tsx @@ -13,55 +13,52 @@ import { ScoreScreen } from "./components/ScoreScreen"; import { PuzzleResult } from "./components/PuzzleResult"; export default function App(): React.ReactElement { - const [phase, setPhase] = useState("drop"); + const [phase, setPhase] = useState({ kind: "drop" }); const [files, setFiles] = useState([]); const [mode, setMode] = useState("download"); const [puzzleGridSize, setPuzzleGridSize] = useState(4); - const [fileResult, setFileResult] = useState(null); - const [puzzleResult, setPuzzleResult] = useState(null); const handleFiles = useCallback((incoming: File[]) => { setFiles(incoming); - setPhase("mode"); + setPhase({ kind: "mode" }); }, []); - const handlePuzzleDirect = useCallback((imageFile: File, gridSize: number) => { - setFiles([imageFile]); - setMode("puzzle"); - setPuzzleGridSize(gridSize); - setPhase("playing"); - }, []); + 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("playing"); + setPhase({ kind: "playing" }); }, []); - const handleFileDone = useCallback((result: FileGameResult) => { - setFileResult(result); - setPhase("done"); + const handleFileDone = useCallback((value: FileGameResult) => { + setPhase({ kind: "done", result: { kind: "file", value } }); }, []); - const handlePuzzleDone = useCallback((result: PuzzleGameResult) => { - setPuzzleResult(result); - setPhase("done"); + const handlePuzzleDone = useCallback((value: PuzzleGameResult) => { + setPhase({ kind: "done", result: { kind: "puzzle", value } }); }, []); const handleRestart = useCallback(() => { setFiles([]); - setFileResult(null); - setPuzzleResult(null); - setPhase("drop"); + setPhase({ kind: "drop" }); }, []); - if (phase === "drop") { + if (phase.kind === "drop") { return ; } - if (phase === "mode") { + if (phase.kind === "mode") { return ; } - if (phase === "playing") { + if (phase.kind === "playing") { if (mode === "puzzle") { return ( ; } - // done phase — puzzleResult is non-null iff mode was "puzzle" - if (puzzleResult !== null) { - return ; + // done phase: the phase carries its result, so both arms are non-null. + if (phase.result.kind === "puzzle") { + return ( + + ); } return ( diff --git a/bucket_catch/packages/frontend/src/types.ts b/bucket_catch/packages/frontend/src/types.ts index c071d28..796ac8d 100644 --- a/bucket_catch/packages/frontend/src/types.ts +++ b/bucket_catch/packages/frontend/src/types.ts @@ -1,4 +1,12 @@ -export type GamePhase = "drop" | "mode" | "playing" | "done"; +/** + * Where the app is. The done phase carries its result, so "finished with no + * result" cannot be represented and the render never has to assert one away. + */ +export type GamePhase = + | { kind: "drop" } + | { kind: "mode" } + | { kind: "playing" } + | { kind: "done"; result: GameResult }; export type TransferMode = "download" | "upload" | "puzzle"; @@ -52,4 +60,7 @@ export interface PuzzleGameResult { gridSize: number; } -export type GameResult = FileGameResult | PuzzleGameResult; +/** The outcome of a finished game, tagged by which mode produced it. */ +export type GameResult = + | { kind: "file"; value: FileGameResult } + | { kind: "puzzle"; value: PuzzleGameResult };