Add seeded deals and card movement animations

Every game is now dealt from a seed carried in the URL as `?seed=`, shown in
the menu, and re-dealable by typing it in, so a deal can be shared or replayed.
Seeds are hashed into a mulberry32 stream, independent of the Python shuffle
bank.

Cards previously teleported between zones: the only motion in the client was
the hover lift and the legal-target pulse. Cards are keyed by card id, so a
card that just moved into a zone mounts there and now animates in, with the
motion disabled under prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmyprzuzanXRhpomc3Ga1i
This commit is contained in:
2026-07-14 21:10:14 +09:00
co-authored by Claude Opus 4.8
parent 299c788545
commit be5226bd3b
5 changed files with 164 additions and 6 deletions
+41
View File
@@ -149,6 +149,19 @@ button:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }
.menu-popover button:hover:not(:disabled) { border-color: var(--gold); }
.menu-popover button:disabled { opacity: 0.3; cursor: default; }
.menu-popover span { padding: 7px 4px 2px; color: #757a76; font: 8px Arial, sans-serif; letter-spacing: 0.08em; }
.menu-seed { display: grid; gap: 4px; }
.menu-seed span { padding: 4px 4px 0; color: var(--gold); }
.menu-seed input {
height: 32px;
padding: 0 8px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 4px;
background: #0a1210;
color: #ddd9ce;
font: 11px/1 Arial, sans-serif;
letter-spacing: 0.08em;
}
.menu-seed input:focus-visible { border-color: var(--gold); outline: none; }
.table-center {
position: absolute;
@@ -276,6 +289,34 @@ 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; }
}
@keyframes card-draw {
from { transform: translateY(34px) scale(0.9); opacity: 0; }
to { transform: none; opacity: 1; }
}
/* `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; }
@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,
.deck-stack.is-draw-target { animation: none; }
}
.card {
--card-color: #888;
position: relative;