Remove hints; gate undo/redo behind a setting and limit it to your turn
Three changes to how much the client helps you: - The hint feature is gone -- button, board highlights, the whole path that asked the policy for your best move. Playing against the model shouldn't come with the model telling you what to do. - Undo/redo is off by default and enabled from a menu toggle. Taking moves back is a training aid, not how the game is played, so the honest game is the default. The switch is reactive: flipping it on mid-game shows the controls immediately, no restart. - When on, undo/redo is limited to the current turn. The floor is the most recent committed move -- a rival move or your own draw -- so you can revise a card selection or placement before you draw, but you can't rewind into the rival's move or an earlier turn. Verified in a browser: undo is disabled at turn start, enabled after selecting a card, and disabled again once you've drawn and the rival has answered. Board and Card lose their now-dead hint props and the hint CSS goes with them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
This commit is contained in:
+40
-81
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Board, type BoardHint } from "./components/Board";
|
import { Board } from "./components/Board";
|
||||||
import { Card, CardBack } from "./components/Card";
|
import { Card, CardBack } from "./components/Card";
|
||||||
import { ResultCard } from "./components/ResultCard";
|
import { ResultCard } from "./components/ResultCard";
|
||||||
import { describeAction } from "./game/actions";
|
|
||||||
import { cardColor, cardName } from "./game/cards";
|
import { cardColor, cardName } from "./game/cards";
|
||||||
import {
|
import {
|
||||||
boardScore,
|
boardScore,
|
||||||
@@ -39,10 +38,13 @@ function loadMode(): Mode {
|
|||||||
return window.localStorage.getItem(MODE_KEY) === "3" ? 3 : 1;
|
return window.localStorage.getItem(MODE_KEY) === "3" ? 3 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Hint {
|
// Off by default: taking moves back is a training aid, not how the game is played,
|
||||||
action: number;
|
// and the default should be the honest game. When on, it is limited to the current
|
||||||
text: string;
|
// turn (see turnFloor) -- you can revise a misclick, not rewind the rival.
|
||||||
probability: number | null;
|
const UNDO_KEY = "lost-cities.undo";
|
||||||
|
|
||||||
|
function loadUndoEnabled(): boolean {
|
||||||
|
return window.localStorage.getItem(UNDO_KEY) === "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
type Selection = { handSlot: number | null; placeType: PlaceType | null };
|
type Selection = { handSlot: number | null; placeType: PlaceType | null };
|
||||||
@@ -139,8 +141,7 @@ function App() {
|
|||||||
const [thinking, setThinking] = useState(false);
|
const [thinking, setThinking] = useState(false);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth);
|
const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth);
|
||||||
const [hint, setHint] = useState<Hint | null>(null);
|
const [undoEnabled, setUndoEnabled] = useState(loadUndoEnabled);
|
||||||
const [hintPending, setHintPending] = useState(false);
|
|
||||||
const generation = useRef(0);
|
const generation = useRef(0);
|
||||||
const gameId = useRef(newGameId());
|
const gameId = useRef(newGameId());
|
||||||
const startedAt = useRef(new Date().toISOString());
|
const startedAt = useRef(new Date().toISOString());
|
||||||
@@ -150,13 +151,26 @@ function App() {
|
|||||||
const { state: match, selection } = frames[cursor];
|
const { state: match, selection } = frames[cursor];
|
||||||
// The board on screen is the round in play; the match is what decides the game.
|
// The board on screen is the round in play; the match is what decides the game.
|
||||||
const state = match.round;
|
const state = match.round;
|
||||||
const canUndo = cursor > 0;
|
// The start of the current turn: the most recent committed move at or before the
|
||||||
const canRedo = cursor < frames.length - 1;
|
// cursor (a rival move, or your own draw). Undo cannot cross it, so you can take
|
||||||
|
// back a card selection or placement within your turn but never rewind into the
|
||||||
|
// rival's move or an earlier turn.
|
||||||
|
const turnFloor = useMemo(() => {
|
||||||
|
for (let index = cursor; index > 0; index -= 1) {
|
||||||
|
if (frames[index].move) return index;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [frames, cursor]);
|
||||||
|
const canUndo = undoEnabled && cursor > turnFloor;
|
||||||
|
const canRedo = undoEnabled && cursor < frames.length - 1;
|
||||||
const latestMatch = useRef(match);
|
const latestMatch = useRef(match);
|
||||||
|
|
||||||
useEffect(() => { publishSeed(seed); }, [seed]);
|
useEffect(() => { publishSeed(seed); }, [seed]);
|
||||||
useEffect(() => { latestMatch.current = match; }, [match]);
|
useEffect(() => { latestMatch.current = match; }, [match]);
|
||||||
useEffect(() => { window.localStorage.setItem(MODE_KEY, String(mode)); }, [mode]);
|
useEffect(() => { window.localStorage.setItem(MODE_KEY, String(mode)); }, [mode]);
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(UNDO_KEY, undoEnabled ? "on" : "off");
|
||||||
|
}, [undoEnabled]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved: SavedGame = { version: 2, seed, mode, frames, cursor, resultOpen };
|
const saved: SavedGame = { version: 2, seed, mode, frames, cursor, resultOpen };
|
||||||
window.localStorage.setItem(SAVED_GAME_KEY, JSON.stringify(saved));
|
window.localStorage.setItem(SAVED_GAME_KEY, JSON.stringify(saved));
|
||||||
@@ -258,7 +272,6 @@ function App() {
|
|||||||
setCursor(0);
|
setCursor(0);
|
||||||
setSeedDraft("");
|
setSeedDraft("");
|
||||||
setResultOpen(true);
|
setResultOpen(true);
|
||||||
setHint(null);
|
|
||||||
setThinking(false);
|
setThinking(false);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
}
|
}
|
||||||
@@ -272,49 +285,12 @@ function App() {
|
|||||||
if (next < 0 || next >= frames.length) return;
|
if (next < 0 || next >= frames.length) return;
|
||||||
generation.current += 1;
|
generation.current += 1;
|
||||||
setCursor(next);
|
setCursor(next);
|
||||||
setHint(null);
|
|
||||||
setThinking(false);
|
setThinking(false);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const undo = () => moveCursor(cursor - 1);
|
const undo = () => { if (canUndo) moveCursor(cursor - 1); };
|
||||||
const redo = () => moveCursor(cursor + 1);
|
const redo = () => { if (canRedo) moveCursor(cursor + 1); };
|
||||||
|
|
||||||
/** Leave the reviewed position as the live one and let the rival play on. */
|
|
||||||
function resumeFromHere() {
|
|
||||||
setFrames((items) => items.slice(0, cursor + 1));
|
|
||||||
setMenuOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ask the policy driving the rival what it would do in your seat. */
|
|
||||||
async function requestHint() {
|
|
||||||
if (!policy || match.done || state.toMove !== 0 || hintPending) return;
|
|
||||||
const position = match;
|
|
||||||
setHintPending(true);
|
|
||||||
setMenuOpen(false);
|
|
||||||
try {
|
|
||||||
const ranked = await policy.rank(position).catch(async (error) => {
|
|
||||||
console.error("hint inference failed; falling back to heuristic", error);
|
|
||||||
return fallbackHeuristicPolicy().rank(position);
|
|
||||||
});
|
|
||||||
const [best] = ranked;
|
|
||||||
// The position can move on while inference runs — a hint for a stale
|
|
||||||
// position would point at the wrong hand slot.
|
|
||||||
if (best === undefined || latestMatch.current !== position) return;
|
|
||||||
setHint({
|
|
||||||
action: best.action,
|
|
||||||
text: describeAction(position.round, best.action),
|
|
||||||
probability: best.probability,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("hint failed", error);
|
|
||||||
} finally {
|
|
||||||
setHintPending(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A hint answers one position; drop it as soon as anything is done or undone. */
|
|
||||||
useEffect(() => { setHint(null); }, [cursor, frames]);
|
|
||||||
|
|
||||||
function chooseCard(handSlot: number) {
|
function chooseCard(handSlot: number) {
|
||||||
if (state.toMove !== 0 || match.done) return;
|
if (state.toMove !== 0 || match.done) return;
|
||||||
@@ -374,17 +350,6 @@ function App() {
|
|||||||
(_, draw) => legal[encodeAction(selection.handSlot!, PLAY, draw)],
|
(_, draw) => legal[encodeAction(selection.handSlot!, PLAY, draw)],
|
||||||
).some(Boolean);
|
).some(Boolean);
|
||||||
|
|
||||||
const hintMove = hint === null ? null : decodeAction(hint.action);
|
|
||||||
const hintedCard = hintMove === null ? undefined : humanHand[hintMove.handSlot];
|
|
||||||
const boardHint: BoardHint | null = hintMove === null || hintedCard === undefined ? null : {
|
|
||||||
color: cardColor(hintedCard),
|
|
||||||
place: hintMove.placeType,
|
|
||||||
drawSource: hintMove.drawSource,
|
|
||||||
};
|
|
||||||
const hintConfidence = hint !== null && hint.probability !== null
|
|
||||||
? `${Math.round(hint.probability * 100)}%`
|
|
||||||
: null;
|
|
||||||
const canHint = policy !== null && !match.done && state.toMove === 0;
|
|
||||||
|
|
||||||
const outcome = scores[0] === scores[1]
|
const outcome = scores[0] === scores[1]
|
||||||
? "DRAW"
|
? "DRAW"
|
||||||
@@ -520,6 +485,14 @@ function App() {
|
|||||||
</label>
|
</label>
|
||||||
<button onClick={loadSeed} disabled={normalizeSeed(seedDraft) === ""}>DEAL THIS SEED</button>
|
<button onClick={loadSeed} disabled={normalizeSeed(seedDraft) === ""}>DEAL THIS SEED</button>
|
||||||
<span>THE SEED SHUFFLES THE DECK ONLY · THE POLICY IS DETERMINISTIC</span>
|
<span>THE SEED SHUFFLES THE DECK ONLY · THE POLICY IS DETERMINISTIC</span>
|
||||||
|
<label className="menu-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={undoEnabled}
|
||||||
|
onChange={(event) => setUndoEnabled(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>ENABLE UNDO / REDO · WITHIN YOUR TURN</span>
|
||||||
|
</label>
|
||||||
<span>{policy ? `${policy.provider.toUpperCase()} POLICY` : "LOADING POLICY"}</span>
|
<span>{policy ? `${policy.provider.toUpperCase()} POLICY` : "LOADING POLICY"}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -550,7 +523,6 @@ function App() {
|
|||||||
selectedPlace={selection.placeType}
|
selectedPlace={selection.placeType}
|
||||||
canPlay={canPlaySelected}
|
canPlay={canPlaySelected}
|
||||||
canDraw={legalDraw}
|
canDraw={legalDraw}
|
||||||
hint={boardHint}
|
|
||||||
cardRef={cardRef}
|
cardRef={cardRef}
|
||||||
onChoosePlace={choosePlace}
|
onChoosePlace={choosePlace}
|
||||||
onCancelPlace={cancelPlace}
|
onCancelPlace={cancelPlace}
|
||||||
@@ -560,7 +532,7 @@ function App() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`deck-stack ${legalDraw(DRAW_DECK) ? "is-draw-target" : ""} ${hintMove?.drawSource === DRAW_DECK ? "is-hinted" : ""}`}
|
className={`deck-stack ${legalDraw(DRAW_DECK) ? "is-draw-target" : ""}`}
|
||||||
disabled={!legalDraw(DRAW_DECK)}
|
disabled={!legalDraw(DRAW_DECK)}
|
||||||
onClick={() => commit(DRAW_DECK)}
|
onClick={() => commit(DRAW_DECK)}
|
||||||
aria-label={`Draw from deck, ${N_CARDS - state.drawPtr} cards left`}
|
aria-label={`Draw from deck, ${N_CARDS - state.drawPtr} cards left`}
|
||||||
@@ -572,7 +544,7 @@ function App() {
|
|||||||
|
|
||||||
<div className="prompt-row">
|
<div className="prompt-row">
|
||||||
<p className={`turn-prompt ${thinking ? "turn-prompt--thinking" : ""} ${reviewingRival ? "turn-prompt--review" : ""}`}>
|
<p className={`turn-prompt ${thinking ? "turn-prompt--thinking" : ""} ${reviewingRival ? "turn-prompt--review" : ""}`}>
|
||||||
{hint ? `Hint — ${hint.text}${hintConfidence ? ` (${hintConfidence})` : ""}` : status}
|
{status}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -582,7 +554,6 @@ function App() {
|
|||||||
card={card}
|
card={card}
|
||||||
key={card}
|
key={card}
|
||||||
selected={selection.handSlot === slot}
|
selected={selection.handSlot === slot}
|
||||||
hinted={hintedCard === card}
|
|
||||||
disabled={state.toMove !== 0 || match.done}
|
disabled={state.toMove !== 0 || match.done}
|
||||||
onClick={() => chooseCard(slot)}
|
onClick={() => chooseCard(slot)}
|
||||||
style={position === 0 ? undefined : { marginLeft: handCardMarginLeft }}
|
style={position === 0 ? undefined : { marginLeft: handCardMarginLeft }}
|
||||||
@@ -596,28 +567,16 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="control-stack">
|
<div className="control-stack">
|
||||||
<button
|
{undoEnabled && (
|
||||||
type="button"
|
<>
|
||||||
className={`hint-button ${hint ? "is-active" : ""}`}
|
|
||||||
onClick={requestHint}
|
|
||||||
disabled={!canHint || hintPending}
|
|
||||||
>
|
|
||||||
{hintPending ? "THINKING…" : "HINT"}
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={undo} disabled={!canUndo} aria-label="Undo one action" title="Undo (←)">
|
<button type="button" onClick={undo} disabled={!canUndo} aria-label="Undo one action" title="Undo (←)">
|
||||||
↶ <span>UNDO</span>
|
↶ <span>UNDO</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={redo} disabled={!canRedo} aria-label="Redo one action" title="Redo (→)">
|
<button type="button" onClick={redo} disabled={!canRedo} aria-label="Redo one action" title="Redo (→)">
|
||||||
↷ <span>REDO</span>
|
↷ <span>REDO</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
</>
|
||||||
type="button"
|
)}
|
||||||
className="control-stack__resume"
|
|
||||||
onClick={resumeFromHere}
|
|
||||||
disabled={!rivalSuspended}
|
|
||||||
>
|
|
||||||
PLAY FROM HERE
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`control-stack__score ${state.done && !resultOpen ? "is-available" : ""}`}
|
className={`control-stack__score ${state.done && !resultOpen ? "is-available" : ""}`}
|
||||||
|
|||||||
@@ -9,20 +9,12 @@ import {
|
|||||||
import { cardsOnBoard } from "../game/engine";
|
import { cardsOnBoard } from "../game/engine";
|
||||||
import { DISCARD, PLAY, type GameState, type PlaceType } from "../game/types";
|
import { DISCARD, PLAY, type GameState, type PlaceType } from "../game/types";
|
||||||
|
|
||||||
/** Where the hint says to put the card and where to draw from, if hinting. */
|
|
||||||
export interface BoardHint {
|
|
||||||
color: number;
|
|
||||||
place: PlaceType;
|
|
||||||
drawSource: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BoardProps {
|
interface BoardProps {
|
||||||
state: GameState;
|
state: GameState;
|
||||||
selectedColor: number | null;
|
selectedColor: number | null;
|
||||||
selectedPlace: PlaceType | null;
|
selectedPlace: PlaceType | null;
|
||||||
canPlay: boolean;
|
canPlay: boolean;
|
||||||
canDraw: (source: number) => boolean;
|
canDraw: (source: number) => boolean;
|
||||||
hint: BoardHint | null;
|
|
||||||
cardRef: (card: number) => (element: HTMLElement | null) => void;
|
cardRef: (card: number) => (element: HTMLElement | null) => void;
|
||||||
onChoosePlace: (place: PlaceType) => void;
|
onChoosePlace: (place: PlaceType) => void;
|
||||||
onCancelPlace: () => void;
|
onCancelPlace: () => void;
|
||||||
@@ -58,7 +50,6 @@ export function Board({
|
|||||||
selectedPlace,
|
selectedPlace,
|
||||||
canPlay,
|
canPlay,
|
||||||
canDraw,
|
canDraw,
|
||||||
hint,
|
|
||||||
cardRef,
|
cardRef,
|
||||||
onChoosePlace,
|
onChoosePlace,
|
||||||
onCancelPlace,
|
onCancelPlace,
|
||||||
@@ -75,9 +66,6 @@ export function Board({
|
|||||||
const drawTarget = selectedPlace !== null && canDraw(color + 1);
|
const drawTarget = selectedPlace !== null && canDraw(color + 1);
|
||||||
const discardChosen = selectedPlace === DISCARD && selectedColor === color;
|
const discardChosen = selectedPlace === DISCARD && selectedColor === color;
|
||||||
const playChosen = selectedPlace === PLAY && selectedColor === color;
|
const playChosen = selectedPlace === PLAY && selectedColor === color;
|
||||||
const hintPlay = hint?.place === PLAY && hint.color === color;
|
|
||||||
const hintDiscard = hint?.place === DISCARD && hint.color === color;
|
|
||||||
const hintDraw = hint?.drawSource === color + 1;
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className="lane"
|
className="lane"
|
||||||
@@ -97,7 +85,7 @@ export function Board({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`lane__discard ${discardTarget ? "is-target" : ""} ${drawTarget ? "is-draw-target" : ""} ${discardChosen ? "is-chosen" : ""} ${hintDiscard || hintDraw ? "is-hinted" : ""}`}
|
className={`lane__discard ${discardTarget ? "is-target" : ""} ${drawTarget ? "is-draw-target" : ""} ${discardChosen ? "is-chosen" : ""}`}
|
||||||
onClick={() => drawTarget ? onDraw(color + 1) : discardChosen ? onCancelPlace() : discardTarget ? onChoosePlace(DISCARD) : undefined}
|
onClick={() => drawTarget ? onDraw(color + 1) : discardChosen ? onCancelPlace() : discardTarget ? onChoosePlace(DISCARD) : undefined}
|
||||||
disabled={!discardTarget && !drawTarget && !discardChosen}
|
disabled={!discardTarget && !drawTarget && !discardChosen}
|
||||||
aria-label={discardChosen ? `Cancel discarding to ${name} pile` : `${name} discard pile`}
|
aria-label={discardChosen ? `Cancel discarding to ${name} pile` : `${name} discard pile`}
|
||||||
@@ -113,7 +101,7 @@ export function Board({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`lane__zone lane__zone--mine ${playTarget ? "is-target" : ""} ${playChosen ? "is-chosen" : ""} ${hintPlay ? "is-hinted" : ""}`}
|
className={`lane__zone lane__zone--mine ${playTarget ? "is-target" : ""} ${playChosen ? "is-chosen" : ""}`}
|
||||||
onClick={() => playChosen ? onCancelPlace() : playTarget ? onChoosePlace(PLAY) : undefined}
|
onClick={() => playChosen ? onCancelPlace() : playTarget ? onChoosePlace(PLAY) : undefined}
|
||||||
disabled={!playTarget && !playChosen}
|
disabled={!playTarget && !playChosen}
|
||||||
aria-label={playChosen ? `Cancel playing to your ${name} expedition` : `Your ${name} expedition`}
|
aria-label={playChosen ? `Cancel playing to your ${name} expedition` : `Your ${name} expedition`}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ interface CardProps {
|
|||||||
* hand row is only as tall as a card back, so a full card would burst it. */
|
* hand row is only as tall as a card back, so a full card would burst it. */
|
||||||
mini?: boolean;
|
mini?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
hinted?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
@@ -26,7 +25,6 @@ export function Card({
|
|||||||
compact = false,
|
compact = false,
|
||||||
mini = false,
|
mini = false,
|
||||||
selected = false,
|
selected = false,
|
||||||
hinted = false,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onClick,
|
onClick,
|
||||||
style: styleOverride,
|
style: styleOverride,
|
||||||
@@ -35,7 +33,7 @@ export function Card({
|
|||||||
const color = cardColor(card);
|
const color = cardColor(card);
|
||||||
const value = cardValue(card);
|
const value = cardValue(card);
|
||||||
const handshake = isHandshake(card);
|
const handshake = isHandshake(card);
|
||||||
const className = `card ${compact ? "card--compact" : ""} ${mini ? "card--mini" : ""} ${selected ? "card--selected" : ""} ${hinted ? "card--hinted" : ""}`;
|
const className = `card ${compact ? "card--compact" : ""} ${mini ? "card--mini" : ""} ${selected ? "card--selected" : ""}`;
|
||||||
const style = { "--card-color": COLOR_HEX[color], ...styleOverride } as React.CSSProperties;
|
const style = { "--card-color": COLOR_HEX[color], ...styleOverride } as React.CSSProperties;
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+12
-37
@@ -333,28 +333,10 @@ button.lane__discard:disabled { opacity: 1; }
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The hinted card, its destination, and its draw source. */
|
|
||||||
.card--hinted { box-shadow: 0 0 0 3px var(--gold), 0 0 26px rgba(201, 163, 75, 0.35); }
|
|
||||||
.lane__zone.is-hinted,
|
|
||||||
.lane__discard.is-hinted,
|
|
||||||
.deck-stack.is-hinted { animation: target-pulse 1.6s ease-in-out infinite; }
|
|
||||||
.lane__zone.is-hinted .lane__ghost,
|
|
||||||
.lane__discard.is-hinted .lane__ghost {
|
|
||||||
border-color: var(--gold);
|
|
||||||
border-style: dashed;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.lane__zone.is-hinted .card,
|
|
||||||
.lane__discard.is-hinted .card,
|
|
||||||
.deck-stack.is-hinted .card { box-shadow: 0 0 0 2px var(--gold), 0 0 26px rgba(201, 163, 75, 0.3); }
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.lane__zone.is-target,
|
.lane__zone.is-target,
|
||||||
.lane__discard.is-target,
|
.lane__discard.is-target,
|
||||||
.lane__discard.is-draw-target,
|
.lane__discard.is-draw-target,
|
||||||
.lane__zone.is-hinted,
|
|
||||||
.lane__discard.is-hinted,
|
|
||||||
.deck-stack.is-hinted,
|
|
||||||
.deck-stack.is-draw-target { animation: none; }
|
.deck-stack.is-draw-target { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,19 +523,6 @@ button.card:disabled { cursor: default; }
|
|||||||
.turn-prompt--thinking { color: #c6b887; }
|
.turn-prompt--thinking { color: #c6b887; }
|
||||||
.turn-prompt--review { color: #9fb4c8; }
|
.turn-prompt--review { color: #9fb4c8; }
|
||||||
|
|
||||||
.hint-button {
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid var(--gold-dim);
|
|
||||||
border-radius: 9px;
|
|
||||||
background: rgba(7, 13, 12, 0.9);
|
|
||||||
font: 700 9px Arial, sans-serif;
|
|
||||||
letter-spacing: 0.16em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.hint-button:hover:not(:disabled) { border-color: var(--gold); }
|
|
||||||
.hint-button:disabled { opacity: 0.35; cursor: default; }
|
|
||||||
.hint-button.is-active { border-color: var(--gold); color: var(--gold); }
|
|
||||||
|
|
||||||
.human-hand {
|
.human-hand {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -682,12 +651,6 @@ button.card:disabled { cursor: default; }
|
|||||||
.control-stack button span { font: 700 9px Arial, sans-serif; letter-spacing: 0.14em; }
|
.control-stack button span { font: 700 9px Arial, sans-serif; letter-spacing: 0.14em; }
|
||||||
.control-stack button:hover:not(:disabled) { border-color: var(--gold); }
|
.control-stack button:hover:not(:disabled) { border-color: var(--gold); }
|
||||||
.control-stack button:disabled { opacity: 0.3; cursor: default; }
|
.control-stack button:disabled { opacity: 0.3; cursor: default; }
|
||||||
.control-stack__resume {
|
|
||||||
border-color: var(--gold) !important;
|
|
||||||
color: var(--gold);
|
|
||||||
font: 700 9px Arial, sans-serif;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
}
|
|
||||||
.control-stack__score { visibility: hidden; }
|
.control-stack__score { visibility: hidden; }
|
||||||
.control-stack__score.is-available { visibility: visible; }
|
.control-stack__score.is-available { visibility: visible; }
|
||||||
|
|
||||||
@@ -801,3 +764,15 @@ button.card:disabled { cursor: default; }
|
|||||||
color: #f0d9a6;
|
color: #f0d9a6;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The undo/redo settings switch. Off by default: taking moves back is a training
|
||||||
|
aid, and the honest game is the sensible default. */
|
||||||
|
.menu-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.menu-toggle input { width: 15px; height: 15px; accent-color: var(--gold); cursor: pointer; }
|
||||||
|
.menu-toggle span { padding: 0; color: #9a9d97; }
|
||||||
|
|||||||
Reference in New Issue
Block a user