Version 2.0.0: Animation fixes, timing improvements, and E2E test suite

Animation fixes:
- Fix held card positioning bug (was appearing at bottom of page)
- Fix discard pile blank/white flash on turn transitions
- Fix blank card at round end by skipping animations during round_over/game_over
- Set card content before triggering flip animation to prevent flash
- Center suit symbol on 10 cards

Timing improvements:
- Reduce post-discard delay from 700ms to 500ms
- Reduce post-swap delay from 1800ms to 1000ms
- Speed up swap flip animation from 1150ms to 550ms
- Reduce CPU initial thinking delay from 150-250ms to 80-150ms
- Pause now happens after swap completes (showing result) instead of before

E2E test suite:
- Add Playwright-based test bot that plays full games
- State parser extracts game state from DOM for validation
- AI brain ports decision logic for automated play
- Freeze detector monitors for UI hangs
- Visual validator checks CSS states
- Full game, stress, and visual test specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-29 18:33:28 -05:00
parent 724bf87c43
commit 6950769bc3
29 changed files with 5153 additions and 348 deletions

View File

@@ -195,12 +195,13 @@ body {
grid-template-columns: 220px 1fr;
gap: 25px;
align-items: start;
padding-top: 70px;
}
.waiting-left-col {
display: flex;
flex-direction: column;
gap: 15px;
gap: 10px;
}
.waiting-left-col .players-list {
@@ -227,7 +228,7 @@ body {
/* Basic settings in a row */
.basic-settings-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 15px;
align-items: end;
@@ -248,14 +249,29 @@ body {
padding: 8px 4px;
}
.basic-settings-row .cpu-controls {
display: flex;
gap: 5px;
/* CPU Controls Section - below players list */
.cpu-controls-section {
background: rgba(0,0,0,0.2);
border-radius: 8px;
padding: 10px 12px;
}
.basic-settings-row .cpu-controls .btn {
.cpu-controls-section h4 {
margin: 0 0 6px 0;
font-size: 0.8rem;
color: #f4a460;
}
.cpu-controls-section .cpu-controls {
display: flex;
gap: 6px;
}
.cpu-controls-section .cpu-controls .btn {
flex: 1;
padding: 8px 0;
padding: 6px 0;
font-size: 1rem;
font-weight: bold;
}
#waiting-message {
@@ -282,14 +298,14 @@ body {
left: 20px;
z-index: 100;
background: linear-gradient(180deg, #d4845a 0%, #c4723f 50%, #b8663a 100%);
padding: 12px 16px 20px;
padding: 10px 14px 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
gap: 6px;
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.3);
/* Ribbon forked end (snake tongue style) */
clip-path: polygon(0 0, 100% 0, 100% 100%, 50% calc(100% - 12px), 0 100%);
clip-path: polygon(0 0, 100% 0, 100% 100%, 50% calc(100% - 10px), 0 100%);
}
.room-code-banner::before {
@@ -302,39 +318,27 @@ body {
background: linear-gradient(180deg, rgba(255,255,255,0.3) 0%, transparent 100%);
}
.room-code-label {
font-size: 0.55rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.25em;
color: rgba(255, 255, 255, 0.85);
text-align: center;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.room-code-value {
font-size: 1.6rem;
font-size: 1.5rem;
font-weight: 800;
font-family: 'Courier New', monospace;
letter-spacing: 0.2em;
letter-spacing: 0.15em;
color: #fff;
text-shadow: 0 2px 2px rgba(0, 0, 0, 0.3);
padding: 2px 0;
}
.room-code-buttons {
display: flex;
gap: 6px;
margin-top: 2px;
gap: 5px;
}
.room-code-copy {
background: rgba(255, 255, 255, 0.85);
border: none;
border-radius: 4px;
padding: 4px 8px;
padding: 4px 6px;
cursor: pointer;
font-size: 0.9rem;
font-size: 0.85rem;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
@@ -349,6 +353,7 @@ body {
}
h1 {
font-size: 3rem;
text-align: center;
@@ -463,6 +468,15 @@ input::placeholder {
width: auto;
}
.game-buttons .btn-next-round {
padding: 10px 20px;
font-size: 1rem;
font-weight: 600;
width: 100%;
background: #f4a460;
color: #1a472a;
}
.btn.disabled,
.btn:disabled {
opacity: 0.4;
@@ -625,7 +639,7 @@ input::placeholder {
/* Game Screen */
.game-header {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-columns: auto 1fr auto;
align-items: center;
padding: 10px 20px;
background: rgba(0,0,0,0.35);
@@ -653,8 +667,18 @@ input::placeholder {
}
.header-col-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
min-width: max-content;
}
#game-logout-btn {
padding: 4px 8px;
font-size: 0.75rem;
}
.game-header .round-info {
@@ -773,6 +797,7 @@ input::placeholder {
background: #fff;
border: 2px solid #ddd;
color: #333;
text-align: center;
}
.card-front.red {
@@ -962,7 +987,73 @@ input::placeholder {
.deck-area {
display: flex;
gap: 15px;
align-items: center;
align-items: flex-start;
}
/* Gentle pulse when it's your turn to draw */
.deck-area.your-turn-to-draw {
animation: deckAreaPulse 2s ease-in-out infinite;
}
@keyframes deckAreaPulse {
0%, 100% {
filter: brightness(1);
transform: scale(1);
}
50% {
filter: brightness(1.08);
transform: scale(1.02);
}
}
/* Held card slot - hidden, using floating card over discard instead */
.held-card-slot {
display: none !important;
}
/* Held card floating over discard pile (larger, closer to viewer) */
.held-card-floating {
position: absolute;
top: 0;
left: 0;
z-index: 100;
transform: scale(1.2) translateY(-12px);
border: 3px solid #f4a460 !important;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 0 25px rgba(244, 164, 96, 0.7) !important;
pointer-events: none;
transition: opacity 0.15s ease-out, transform 0.15s ease-out;
}
.held-card-floating.hidden {
opacity: 0;
transform: scale(1) translateY(0);
pointer-events: none;
}
/* Animate floating card dropping to discard pile (when drawn from discard) */
.held-card-floating.dropping {
transform: scale(1) translateY(0);
border-color: transparent !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
transition: transform 0.25s ease-out, border-color 0.25s ease-out, box-shadow 0.25s ease-out;
}
/* Swoop animation for deck → immediate discard */
.held-card-floating.swooping {
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1),
top 0.35s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.35s ease-out,
border-color 0.35s ease-out,
box-shadow 0.35s ease-out;
transform: scale(1.15) rotate(-8deg);
border-color: rgba(244, 164, 96, 0.8) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4), 0 0 20px rgba(244, 164, 96, 0.6) !important;
}
.held-card-floating.swooping.landed {
transform: scale(1) rotate(0deg);
border-color: transparent !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
.deck-area .card {
@@ -989,11 +1080,19 @@ input::placeholder {
transform: scale(1.05);
}
/* Picked-up state - showing card underneath after drawing from discard */
#discard.picked-up {
opacity: 0.5;
filter: grayscale(40%);
transform: scale(0.95);
}
.discard-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
position: relative;
}
.discard-stack .btn {
@@ -1016,7 +1115,7 @@ input::placeholder {
/* Highlight flash when opponent draws from a pile */
#deck.draw-pulse,
#discard.draw-pulse {
animation: draw-highlight 0.4s ease-out;
animation: draw-highlight 0.45s ease-out;
z-index: 100;
}
@@ -1044,7 +1143,7 @@ input::placeholder {
/* Card flip animation for discard pile */
.card-flip-in {
animation: cardFlipIn 0.5s ease-out;
animation: cardFlipIn 0.56s ease-out;
}
@keyframes cardFlipIn {
@@ -1069,6 +1168,26 @@ input::placeholder {
}
}
/* Discard pile pulse when card lands */
#discard.discard-land {
animation: discardLand 0.46s ease-out;
}
@keyframes discardLand {
0% {
transform: scale(1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
40% {
transform: scale(1.18);
box-shadow: 0 0 25px rgba(244, 164, 96, 0.9);
}
100% {
transform: scale(1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
}
/* Swap animation overlay */
.swap-animation {
position: fixed;
@@ -1092,12 +1211,16 @@ input::placeholder {
perspective: 1000px;
}
.swap-card.hidden {
display: none;
}
.swap-card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.4s ease-in-out;
transition: transform 0.54s ease-in-out;
}
.swap-card.flipping .swap-card-inner {
@@ -1163,7 +1286,7 @@ input::placeholder {
}
.swap-card.moving {
transition: top 0.4s cubic-bezier(0.4, 0, 0.2, 1), left 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s ease-out;
transition: top 0.45s cubic-bezier(0.4, 0, 0.2, 1), left 0.45s cubic-bezier(0.4, 0, 0.2, 1), transform 0.45s ease-out;
transform: scale(1.1) rotate(-5deg);
filter: drop-shadow(0 0 25px rgba(244, 164, 96, 1));
}
@@ -1180,6 +1303,31 @@ input::placeholder {
transition: opacity 0.2s;
}
/* Subtle swap pulse for face-to-face swaps (no flip needed) */
.card.swap-pulse {
animation: swapPulse 0.4s ease-out;
}
@keyframes swapPulse {
0% {
transform: scale(1);
filter: brightness(1);
}
20% {
transform: scale(0.92);
filter: brightness(0.85);
}
50% {
transform: scale(1.08);
filter: brightness(1.15);
box-shadow: 0 0 12px rgba(255, 255, 255, 0.4);
}
100% {
transform: scale(1);
filter: brightness(1);
}
}
/* Player Area */
.player-section {
text-align: center;
@@ -1483,14 +1631,14 @@ input::placeholder {
}
.game-buttons {
margin-top: 8px;
margin-bottom: 8px;
display: flex;
flex-direction: column;
gap: 5px;
}
.game-buttons .btn {
font-size: 0.7rem;
font-size: 0.8rem;
padding: 6px 8px;
width: 100%;
}
@@ -1728,11 +1876,27 @@ input::placeholder {
}
.game-header {
display: flex;
flex-direction: column;
text-align: center;
gap: 3px;
}
.header-col-right {
justify-content: center;
}
#game-logout-btn,
#leave-game-btn {
padding: 3px 6px;
font-size: 0.7rem;
}
.game-username {
font-size: 0.7rem;
max-width: 60px;
}
.table-center {
padding: 10px 15px;
}
@@ -1793,7 +1957,7 @@ input::placeholder {
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.4s ease-in-out;
transition: transform 0.54s ease-in-out;
}
.real-card .card-inner.flipped {
@@ -1867,9 +2031,9 @@ input::placeholder {
.real-card.moving,
.real-card.anim-card.moving {
z-index: 600;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s ease-out;
transition: left 0.27s cubic-bezier(0.4, 0, 0.2, 1),
top 0.27s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.27s ease-out;
filter: drop-shadow(0 0 20px rgba(244, 164, 96, 0.8));
transform: scale(1.08) rotate(-3deg);
}
@@ -1881,7 +2045,7 @@ input::placeholder {
}
.real-card.anim-card .card-inner {
transition: transform 0.4s ease-in-out;
transition: transform 0.54s ease-in-out;
}
.real-card.holding {
@@ -2881,11 +3045,30 @@ input::placeholder {
display: none;
}
/* Hide global auth-bar when game screen is active */
#app:has(#game-screen.active) > .auth-bar {
display: none !important;
}
#auth-username {
color: #f4a460;
font-weight: 500;
}
/* Username in game header */
.game-username {
color: #f4a460;
font-weight: 500;
font-size: 0.75rem;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
}
.game-username.hidden {
display: none;
}
/* Auth buttons in lobby */
.auth-buttons {
display: flex;