diff --git a/web/src/App.tsx b/web/src/App.tsx index 87cf0fa..1ea5b60 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Board } from "./components/Board"; import { Card, CardBack } from "./components/Card"; -import { ResultCard } from "./components/ResultCard"; +import { ResultCard, RoundOverlay } from "./components/Scoreboard"; import { cardColor, cardName } from "./game/cards"; import { boardScore, @@ -13,10 +13,12 @@ import { } from "./game/engine"; import { N_ROUNDS, + matchAdvanceRound, matchFromOrders, matchLegalActionMask, matchScore, matchStep, + roundScore, type MatchState, } from "./game/match"; import { MODEL_CODENAME, MODEL_HASH } from "./model/policy"; @@ -35,7 +37,8 @@ function loadMode(): Mode { const requested = new URLSearchParams(window.location.search).get("rounds"); if (requested === "3") return 3; if (requested === "1") return 1; - return window.localStorage.getItem(MODE_KEY) === "3" ? 3 : 1; + // The classic three-round match is the default; a stored "1" opts down to a deal. + return window.localStorage.getItem(MODE_KEY) === "1" ? 1 : 3; } // Off by default: taking moves back is a training aid, not how the game is played, @@ -137,7 +140,7 @@ function App() { const [seedDraft, setSeedDraft] = useState(""); const [resultOpen, setResultOpen] = useState(initial.resultOpen); const [policy, setPolicy] = useState(null); - const [modelMessage, setModelMessage] = useState("LOADING BOREALIS"); + const [modelMessage, setModelMessage] = useState("BOREALIS 불러오는 중"); const [thinking, setThinking] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth); @@ -209,7 +212,7 @@ function App() { loadPolicy().then(({ policy: loaded, warning }) => { if (cancelled) return; setPolicy(loaded); - setModelMessage(warning ?? `${loaded.provider.toUpperCase()} · BOREALIS`); + setModelMessage(warning ?? "BOREALIS"); }); return () => { cancelled = true; }; }, []); @@ -226,7 +229,8 @@ function App() { const rivalSuspended = canRedo; useEffect(() => { - if (!policy || rivalSuspended || match.done || state.toMove !== 1) return; + // No move while a finished round is being shown -- the player advances it. + if (!policy || rivalSuspended || match.done || match.roundComplete || state.toMove !== 1) return; const currentGeneration = generation.current; const timer = window.setTimeout(async () => { setThinking(true); @@ -240,7 +244,7 @@ function App() { action = await fallback.action(match); if (generation.current !== currentGeneration) return; setPolicy(fallback); - setModelMessage("HEURISTIC FALLBACK (MODEL ERROR)"); + setModelMessage("휴리스틱 대체 (모델 오류)"); } if (generation.current !== currentGeneration) return; pushFrame({ @@ -250,7 +254,7 @@ function App() { }); } catch (error) { console.error("AI action failed even with heuristic fallback", error); - setModelMessage("MODEL INFERENCE ERROR"); + setModelMessage("모델 추론 오류"); } finally { if (generation.current === currentGeneration) setThinking(false); } @@ -258,6 +262,17 @@ function App() { return () => window.clearTimeout(timer); }, [policy, rivalSuspended, match, state.toMove]); + /** Deal the next round from the round-complete pause. */ + function advanceRound() { + generation.current += 1; + resetMotion(); + setFrames((items) => [...items.slice(0, cursor + 1), { + state: matchAdvanceRound(match), + selection: EMPTY_SELECTION, + }]); + setCursor(cursor + 1); + } + /** Deal a game. Without a seed this rolls a fresh one; the same seed always * reproduces the same deal, so `restart(seed)` also replays the current one. */ function restart(nextSeed: string = randomSeed(), nextMode: Mode = mode) { @@ -352,8 +367,8 @@ function App() { const outcome = scores[0] === scores[1] - ? "DRAW" - : scores[0] > scores[1] ? "YOU WIN" : "THE RIVAL WINS"; + ? "무승부" + : scores[0] > scores[1] ? "승리" : "패배"; useEffect(() => { if (!match.done || loggedGame.current === gameId.current) return; @@ -402,7 +417,7 @@ function App() { coinFlips: match.coinFlips.slice(0, mode), moves, finalScores: scores, - roundScores: match.carry, + roundScores: match.roundHistory, outcome, finalState: match, policy: modelMessage, @@ -424,24 +439,26 @@ function App() { // turn the normal prompt still applies: you can simply play on from here. const reviewingRival = rivalSuspended && (match.done || state.toMove === 1); const status = reviewingRival - ? `Reviewing ply ${state.stepCount} — redo, or play on from here` + ? "되돌리는 중 — 다시 진행하려면 앞으로" : match.done ? outcome - : state.toMove === 1 - ? thinking ? "The rival is thinking ···" : "The rival's turn" - : selection.handSlot === null - ? "Play or discard a card" - : selection.placeType === null - ? `Choose a destination for ${cardName(selectedCard!)}` - : "Draw — deck or a discard pile"; + : match.roundComplete + ? `${match.roundIdx + 1}라운드 종료` + : state.toMove === 1 + ? thinking ? "상대가 생각 중이에요" : "상대 차례" + : selection.handSlot === null + ? "낼 카드나 버릴 카드를 고르세요" + : selection.placeType === null + ? `${cardName(selectedCard!)} 놓을 곳을 고르세요` + : "카드를 뽑으세요 — 덱 또는 버림패"; return (
-
-
THE RIVAL{modelMessage}
{scores[1]} +
+
상대{modelMessage}
{scores[1]}
-
+
{opponentHand.map((card) => ( state.handPublic[card] ? @@ -450,56 +467,50 @@ function App() {
- + {menuOpen && (
- - -
- + + +
+
- - A MATCH IS DECIDED ON THE SUMMED TOTAL · WHOEVER LEADS ON POINTS - OPENS THE NEXT ROUND - - - THE SEED SHUFFLES THE DECK ONLY · THE POLICY IS DETERMINISTIC + - {policy ? `${policy.provider.toUpperCase()} POLICY` : "LOADING POLICY"}
)}
{mode === N_ROUNDS && ( -
+
{Array.from({ length: N_ROUNDS }, (_, round) => ( - R{round + 1} + {round + 1} ))} - - {match.carry[0]} : {match.carry[1]} BANKED - + 누적 {match.carry[0]} : {match.carry[1]}
)} @@ -535,11 +544,11 @@ function App() { className={`deck-stack ${legalDraw(DRAW_DECK) ? "is-draw-target" : ""}`} disabled={!legalDraw(DRAW_DECK)} onClick={() => commit(DRAW_DECK)} - aria-label={`Draw from deck, ${N_CARDS - state.drawPtr} cards left`} + aria-label={`덱에서 뽑기, ${N_CARDS - state.drawPtr}장 남음`} > {N_CARDS - state.drawPtr} - CARDS LEFT + 남은 카드
@@ -548,7 +557,7 @@ function App() {

-
+
{renderedHand.map(({ card, slot }, position) => ( -
-
YOU {state.toMove === 0 && !match.done ? "YOUR TURN" : "EXPEDITION LEAD"}
{scores[0]} +
+
{state.toMove === 0 && !match.done ? "내 차례" : "현재 점수"}
{scores[0]}
{undoEnabled && ( <> - - )}
@@ -592,15 +601,22 @@ function App() { after React has already removed them from the table. */}