Add hints, per-action undo/redo, and card motion
The rival's policy now exposes a ranking, which drives both its own move and a new HINT button: it highlights the card to play, where to put it, and where to draw, with the model's confidence. Undo and redo work per action rather than per turn — picking a card, choosing its destination, and drawing are separate steps, as are the rival's moves — so a finished game can be stepped back through from the result screen. The rival is suspended while undone moves are pending, and PLAY FROM HERE resumes from the reviewed position. Cards now travel between zones instead of teleporting: a motion layer measures each card's old and new position and animates the difference, flying cards out of the deck face-down and flipping them over, and back into it on undo. Also: the result screen gets a per-expedition score breakdown mirroring human_play.py, and a rival card revealed by a discard-pile draw no longer renders at full size in the card-back-sized rival hand row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LmyprzuzanXRhpomc3Ga1i
This commit is contained in:
+193
-31
@@ -289,31 +289,55 @@ button.lane__discard:disabled { opacity: 1; }
|
||||
.lane__discard.is-chosen .card { box-shadow: 0 0 0 2px var(--gold), 0 0 28px rgba(201, 163, 75, 0.25); }
|
||||
@keyframes target-pulse { 50% { filter: brightness(1.22); } }
|
||||
|
||||
/* Cards are keyed by card id, so only a card that just moved into a zone
|
||||
mounts there — these entry animations are its motion between zones. */
|
||||
@keyframes card-place {
|
||||
from { transform: translateY(-30px) scale(0.94) rotate(-1.5deg); opacity: 0; }
|
||||
to { transform: none; opacity: 1; }
|
||||
/* Card travel between zones is played by the motion layer (src/ui/useCardMotion),
|
||||
which measures each card's old and new position and animates the difference.
|
||||
Only the flip cover it fades in and out lives here. */
|
||||
.card__flip-cover {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
z-index: 12;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid #67552d;
|
||||
border-radius: 11px;
|
||||
background: repeating-linear-gradient(135deg, #20252a 0 5px, #181c20 5px 10px);
|
||||
color: var(--gold);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes card-draw {
|
||||
from { transform: translateY(34px) scale(0.9); opacity: 0; }
|
||||
to { transform: none; opacity: 1; }
|
||||
.card--compact .card__flip-cover { border-radius: 9px; }
|
||||
.card__flip-cover i { font: 24px/1 Georgia, serif; font-style: normal; }
|
||||
.card--back .card__flip-cover { display: none; }
|
||||
|
||||
.motion-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* `backwards`, not `both`: a lingering final transform would outrank the
|
||||
hover-lift and .card--selected transforms once the animation ends. */
|
||||
.card-stack .card,
|
||||
.lane__discard .card { animation: card-place 0.26s ease-out backwards; }
|
||||
.human-hand .card,
|
||||
.opponent-hand .card { animation: card-draw 0.3s ease-out backwards; }
|
||||
|
||||
/* 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) {
|
||||
.card-stack .card,
|
||||
.lane__discard .card,
|
||||
.human-hand .card,
|
||||
.opponent-hand .card { animation: none; }
|
||||
.lane__zone.is-target,
|
||||
.lane__discard.is-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; }
|
||||
}
|
||||
|
||||
@@ -386,6 +410,23 @@ button.card:disabled { cursor: default; }
|
||||
.card--compact .card__center--handshake i { font-size: 31px; }
|
||||
.card--compact .card__center small { font-size: 6px; }
|
||||
|
||||
/* Same footprint as a card back, so a revealed rival card sits in the hand row
|
||||
instead of bursting out of it. */
|
||||
.card--mini {
|
||||
width: 44px;
|
||||
height: 58px;
|
||||
border-width: 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.card--mini .card__corner { display: none; }
|
||||
.card--mini .card__center { inset: 4px; }
|
||||
.card--mini .card__center b { font-size: 20px; }
|
||||
.card--mini .card__center i { margin-top: 2px; font-size: 11px; }
|
||||
.card--mini .card__center--handshake i { font-size: 19px; }
|
||||
.card--mini .card__center small { display: none; }
|
||||
.card--mini::after { inset: 3px; border-radius: 3px; }
|
||||
.card--mini .card__flip-cover { border-radius: 5px; }
|
||||
|
||||
.card--back {
|
||||
width: 44px;
|
||||
height: 58px;
|
||||
@@ -431,12 +472,20 @@ button.card:disabled { cursor: default; }
|
||||
.deck-stack > span { margin-top: 5px; color: #7e817c; font: 700 9px Arial, sans-serif; letter-spacing: 0.2em; }
|
||||
.deck-stack.is-draw-target .card { box-shadow: 0 0 0 3px var(--gold), 7px 8px 0 #171b1e, 0 0 32px rgba(201, 163, 75, 0.3); }
|
||||
|
||||
.turn-prompt {
|
||||
/* Insets keep the row clear of the history bar parked at the right. */
|
||||
.prompt-row {
|
||||
position: absolute;
|
||||
right: 260px;
|
||||
right: 330px;
|
||||
bottom: 202px;
|
||||
left: 260px;
|
||||
left: 330px;
|
||||
z-index: 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.turn-prompt {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #dedbd3;
|
||||
@@ -446,6 +495,22 @@ button.card:disabled { cursor: default; }
|
||||
white-space: nowrap;
|
||||
}
|
||||
.turn-prompt--thinking { color: #c6b887; }
|
||||
.turn-prompt--review { color: #9fb4c8; }
|
||||
|
||||
.hint-button {
|
||||
flex: 0 0 auto;
|
||||
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 {
|
||||
position: absolute;
|
||||
@@ -471,18 +536,113 @@ button.card:disabled { cursor: default; }
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.result-card {
|
||||
width: min(430px, calc(100% - 40px));
|
||||
padding: 42px;
|
||||
width: min(520px, calc(100% - 40px));
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow-y: auto;
|
||||
padding: 32px 34px 26px;
|
||||
border: 1px solid var(--gold-dim);
|
||||
border-radius: 18px;
|
||||
background: #0b1211;
|
||||
text-align: center;
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.result-card__head { text-align: center; }
|
||||
.result-card__head p { margin: 0; color: #8d8d84; font: 700 9px Arial, sans-serif; letter-spacing: 0.2em; }
|
||||
.result-card__head h1 { margin: 10px 0 14px; color: #eee9dd; font: 700 32px Georgia, serif; }
|
||||
.result-card__head strong { display: block; color: var(--gold); font: 700 26px Georgia, serif; }
|
||||
.result-card__head i { color: #716b5b; font-style: normal; }
|
||||
|
||||
.result-table {
|
||||
width: 100%;
|
||||
margin: 24px 0 20px;
|
||||
border-collapse: collapse;
|
||||
font: 12px Arial, sans-serif;
|
||||
}
|
||||
.result-table th,
|
||||
.result-table td { padding: 7px 6px; text-align: right; }
|
||||
.result-table thead th {
|
||||
color: #8d8d84;
|
||||
font: 700 9px Arial, sans-serif;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
.result-table thead tr:first-child th { border-bottom: 1px solid rgba(226, 220, 202, 0.14); }
|
||||
.result-table__subhead th { padding-top: 6px; color: #6d716e; }
|
||||
.result-table tbody th[scope="row"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #cfcabd;
|
||||
font: 700 11px Arial, sans-serif;
|
||||
letter-spacing: 0.08em;
|
||||
text-align: left;
|
||||
}
|
||||
.result-table tbody th[scope="row"] i {
|
||||
color: var(--lane-color);
|
||||
font: 15px/1 Georgia, serif;
|
||||
font-style: normal;
|
||||
}
|
||||
.result-table tbody td { color: #ddd8ca; font: 700 13px Georgia, serif; }
|
||||
.result-table__detail { color: #7d817c !important; font: 11px Arial, sans-serif !important; }
|
||||
.result-table td.is-positive { color: #86c08c; }
|
||||
.result-table td.is-negative { color: #c98b7f; }
|
||||
.result-table tfoot th,
|
||||
.result-table tfoot td {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(226, 220, 202, 0.14);
|
||||
color: var(--gold);
|
||||
font: 700 15px Georgia, serif;
|
||||
}
|
||||
.result-table tfoot th { text-align: left; letter-spacing: 0.1em; }
|
||||
|
||||
.result-card__foot { display: grid; gap: 14px; justify-items: center; }
|
||||
.result-card__foot small { color: #6d716e; font: 700 9px Arial, sans-serif; letter-spacing: 0.14em; }
|
||||
.result-card__foot div { display: flex; gap: 10px; }
|
||||
.result-card button {
|
||||
height: 42px;
|
||||
padding: 0 24px;
|
||||
border: 1px solid var(--gold-dim);
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #ece8dc;
|
||||
font: 700 10px Arial, sans-serif;
|
||||
letter-spacing: 0.12em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-card button:hover { border-color: var(--gold); }
|
||||
.result-card button.is-primary { border-color: var(--gold); color: var(--gold); }
|
||||
|
||||
/* Sits in the band above the hand: the hand row is centered and its width is
|
||||
computed against the score plaques, so a wide bar down at the hand's level
|
||||
would overlap the leftmost/rightmost cards. */
|
||||
.history-bar {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 196px;
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.history-bar button {
|
||||
height: 32px;
|
||||
padding: 0 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--gold-dim);
|
||||
border-radius: 9px;
|
||||
background: rgba(7, 13, 12, 0.92);
|
||||
font: 700 13px/1 Georgia, serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
.history-bar button span { font: 700 9px Arial, sans-serif; letter-spacing: 0.14em; }
|
||||
.history-bar button:hover:not(:disabled) { border-color: var(--gold); }
|
||||
.history-bar button:disabled { opacity: 0.3; cursor: default; }
|
||||
.history-bar__resume {
|
||||
border-color: var(--gold) !important;
|
||||
color: var(--gold);
|
||||
font: 700 9px Arial, sans-serif;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
.result-card p { color: #8d8d84; font: 700 9px Arial, sans-serif; letter-spacing: 0.2em; }
|
||||
.result-card h1 { margin: 16px 0; color: #eee9dd; font: 700 34px Georgia, serif; }
|
||||
.result-card strong { display: block; margin: 24px; color: var(--gold); font: 700 28px Georgia, serif; }
|
||||
.result-card i { color: #716b5b; font-style: normal; }
|
||||
.result-card button { height: 42px; padding: 0 28px; border: 1px solid var(--gold); border-radius: 10px; background: transparent; color: #ece8dc; font: 700 10px Arial, sans-serif; letter-spacing: 0.12em; cursor: pointer; }
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.table-center { width: 600px; }
|
||||
@@ -490,7 +650,8 @@ button.card:disabled { cursor: default; }
|
||||
.lane__ghost, .card--compact { width: 78px; height: 110px; }
|
||||
.human-hand .card { width: 98px; height: 142px; margin-left: 7px; }
|
||||
.human-hand { height: 146px; }
|
||||
.turn-prompt { bottom: 177px; }
|
||||
.prompt-row { bottom: 177px; }
|
||||
.history-bar { bottom: 172px; }
|
||||
.table-center { bottom: 210px; }
|
||||
/* Narrow viewports: the plaques are the keep-out that squeezes the hand row,
|
||||
so they become compact chips (model-name line dropped) to give the cards
|
||||
@@ -513,7 +674,8 @@ button.card:disabled { cursor: default; }
|
||||
.table-center { top: 64px; bottom: 196px; }
|
||||
.human-hand { bottom: 18px; height: 138px; }
|
||||
.human-hand .card { width: 94px; height: 134px; }
|
||||
.turn-prompt { bottom: 165px; }
|
||||
.prompt-row { bottom: 165px; }
|
||||
.history-bar { bottom: 160px; }
|
||||
.score-plaque--human { bottom: 9px; }
|
||||
.deck-stack { transform: scale(0.86); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user