From 36a71799b5ccedc7ed8d01a85cdb4b560b07fb81 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Mon, 26 Jan 2026 21:37:02 -0500 Subject: [PATCH] UI polish: opponent draw flash, compact house rules with suit separators, toast styling. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Opponent draw highlight: scale + outline flash animation - House rules reorganized: Gameplay, Jokers, Card Values, Bonuses & Penalties - Compact inline rule descriptions with alternating suit separators (♣♦♠♥) - Wolfpack + Four of a Kind combo note when both selected - Toast notifications now yellow/green with charcoal text - Brief pause after AI draw for visual feedback Co-Authored-By: Claude Opus 4.5 --- client/app.js | 16 +++- client/index.html | 119 ++++++++++++------------ client/style.css | 225 +++++++++++++++++++++++++++++++++++++++++----- server/ai.py | 4 +- 4 files changed, 278 insertions(+), 86 deletions(-) diff --git a/client/app.js b/client/app.js index acb9a5d..7e859a1 100644 --- a/client/app.js +++ b/client/app.js @@ -168,6 +168,7 @@ class GolfGame { this.fourOfAKindCheckbox = document.getElementById('four-of-a-kind'); this.negativePairsCheckbox = document.getElementById('negative-pairs-keep-value'); this.oneEyedJacksCheckbox = document.getElementById('one-eyed-jacks'); + this.wolfpackComboNote = document.getElementById('wolfpack-combo-note'); this.startGameBtn = document.getElementById('start-game-btn'); this.leaveRoomBtn = document.getElementById('leave-room-btn'); this.addCpuBtn = document.getElementById('add-cpu-btn'); @@ -249,6 +250,17 @@ class GolfGame { this.updateDeckRecommendation(playerCount); }); + // Show combo note when wolfpack + four-of-a-kind are both selected + const updateWolfpackCombo = () => { + if (this.wolfpackCheckbox.checked && this.fourOfAKindCheckbox.checked) { + this.wolfpackComboNote.classList.remove('hidden'); + } else { + this.wolfpackComboNote.classList.add('hidden'); + } + }; + this.wolfpackCheckbox.addEventListener('change', updateWolfpackCombo); + this.fourOfAKindCheckbox.addEventListener('change', updateWolfpackCombo); + // Toggle scoreboard collapse on mobile const scoreboardTitle = this.scoreboard.querySelector('h4'); if (scoreboardTitle) { @@ -932,7 +944,7 @@ class GolfGame { // The swap animation handles showing the card at the correct position } - // Pulse animation on deck or discard pile to show where opponent drew from + // Flash animation on deck or discard pile to show where opponent drew from pulseDrawPile(source) { const pile = source === 'discard' ? this.discard : this.deck; pile.classList.remove('draw-pulse'); @@ -940,7 +952,7 @@ class GolfGame { void pile.offsetWidth; pile.classList.add('draw-pulse'); // Remove class after animation completes - setTimeout(() => pile.classList.remove('draw-pulse'), 600); + setTimeout(() => pile.classList.remove('draw-pulse'), 400); } // Fire animation for discard without swap (card goes deck -> discard) diff --git a/client/index.html b/client/index.html index 804ed92..59ff8ed 100644 --- a/client/index.html +++ b/client/index.html @@ -94,10 +94,10 @@ Advanced Options
- +
-

Variants

+

Gameplay

@@ -106,12 +106,17 @@ - What happens when you draw from deck and discard + After discarding a drawn card
-
@@ -126,94 +131,84 @@ -
-
- -
-

Point Modifiers

-
- -
- +
-

Bonuses & Penalties

+

Card Values

- - -
-

New Variants

+

Bonuses & Penalties

-
diff --git a/client/style.css b/client/style.css index c771b78..38dd162 100644 --- a/client/style.css +++ b/client/style.css @@ -4,6 +4,10 @@ padding: 0; } +html { + scroll-behavior: smooth; +} + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; /* Dark emerald pool table felt */ @@ -582,9 +586,6 @@ input::placeholder { white-space: nowrap; } -.radio-label .rule-desc::before { - content: "— "; -} /* Settings */ .settings { @@ -985,21 +986,32 @@ input::placeholder { box-shadow: none; } -/* Pulse animation when opponent draws from a pile */ +/* Highlight flash when opponent draws from a pile */ #deck.draw-pulse, #discard.draw-pulse { - animation: draw-pulse 0.6s ease-out; + animation: draw-highlight 0.4s ease-out; + z-index: 100; } -@keyframes draw-pulse { +@keyframes draw-highlight { 0% { - box-shadow: 0 0 0 0 rgba(244, 164, 96, 0.8); + transform: scale(1); + outline: 0px solid rgba(255, 220, 100, 0); } - 50% { - box-shadow: 0 0 0 12px rgba(244, 164, 96, 0.4); + 15% { + transform: scale(1.08); + outline: 3px solid rgba(255, 220, 100, 1); + outline-offset: 2px; + } + 40% { + transform: scale(1.04); + outline: 3px solid rgba(255, 200, 80, 0.7); + outline-offset: 4px; } 100% { - box-shadow: 0 0 0 20px rgba(244, 164, 96, 0); + transform: scale(1); + outline: 3px solid rgba(255, 200, 80, 0); + outline-offset: 8px; } } @@ -1244,8 +1256,8 @@ input::placeholder { } .status-message.your-turn { - background: linear-gradient(135deg, #f4a460 0%, #e8914d 100%); - color: #1a472a; + background: linear-gradient(135deg, #b5d484 0%, #9ab973 100%); + color: #2d3436; } /* Final turn badge - separate indicator */ @@ -2127,8 +2139,28 @@ input::placeholder { margin-left: 0; } -.checkbox-label.inline .rule-desc::before { - content: "— "; + +/* Suit separators for rule descriptions */ +.suit { + font-size: 0.95em; + margin-right: 6px; +} +.suit-red { color: #e74c3c; } +.suit-black { color: #888; } + +/* Combo note for stacking rules */ +.combo-note { + font-size: 0.8rem; + color: #ffd700; + background: rgba(255, 215, 0, 0.1); + border-left: 2px solid #ffd700; + padding: 4px 8px; + margin: 4px 0 8px 0; + border-radius: 0 4px 4px 0; +} + +.combo-note.hidden { + display: none; } /* Rule description */ @@ -2376,28 +2408,114 @@ input::placeholder { } .rules-container { + position: relative; background: rgba(0, 0, 0, 0.3); border-radius: 12px; padding: 20px 35px; border: 1px solid rgba(255, 255, 255, 0.1); margin-top: 0; + scroll-behavior: smooth; } -.rules-container h1 { +/* Rules back button */ +.rules-back-btn { + padding: 4px 12px; + font-size: 0.8rem; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.7); + margin-bottom: 15px; +} + +.rules-back-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border-color: rgba(255, 255, 255, 0.5); +} + +.golfer-logo { + display: inline-block; + transform: scaleX(-1); +} + +/* Rules header */ +.rules-header { text-align: center; - margin-bottom: 30px; + margin-bottom: 25px; + padding-bottom: 20px; + border-bottom: 2px solid rgba(244, 164, 96, 0.3); +} + +.rules-header h1 { color: #f4a460; font-size: 2rem; + margin-bottom: 8px; } -.rules-container .back-btn { - margin-bottom: 20px; +.rules-subtitle { + color: rgba(255, 255, 255, 0.7); + font-size: 1rem; + margin: 0; +} + +/* Table of Contents */ +.rules-toc { + background: linear-gradient(135deg, rgba(244, 164, 96, 0.15) 0%, rgba(244, 164, 96, 0.05) 100%); + border: 1px solid rgba(244, 164, 96, 0.3); + border-radius: 10px; + padding: 18px 22px; + margin-bottom: 30px; +} + +.toc-title { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 1.5px; + color: rgba(244, 164, 96, 0.9); + margin-bottom: 14px; + font-weight: 600; +} + +.toc-links { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.toc-link { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + color: rgba(255, 255, 255, 0.85); + text-decoration: none; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.toc-link:hover { + background: rgba(244, 164, 96, 0.25); + border-color: rgba(244, 164, 96, 0.4); + color: #fff; + transform: translateY(-1px); +} + +.toc-icon { + font-size: 1rem; +} + +.toc-text { + font-weight: 500; } .rules-section { margin-bottom: 35px; padding-bottom: 25px; border-bottom: 1px solid rgba(255, 255, 255, 0.15); + scroll-margin-top: 20px; } .rules-section:last-child { @@ -2554,6 +2672,45 @@ input::placeholder { margin-bottom: 15px; } +/* House rule items */ +.house-rule { + background: rgba(0, 0, 0, 0.15); + border-radius: 6px; + padding: 12px 16px; + margin: 12px 0; + border-left: 3px solid rgba(244, 164, 96, 0.5); +} + +.house-rule h4 { + margin: 0 0 6px 0; + color: #f4a460; + font-size: 1rem; +} + +.house-rule p { + margin: 0 0 8px 0; + line-height: 1.5; +} + +.house-rule p:last-child { + margin-bottom: 0; +} + +.strategic-impact { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.75); + font-style: italic; +} + +.combo-note { + background: rgba(244, 164, 96, 0.1); + border-radius: 4px; + padding: 10px 14px; + margin-top: 15px; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); +} + /* FAQ items */ .faq-item { background: rgba(0, 0, 0, 0.2); @@ -2626,11 +2783,37 @@ input::placeholder { /* Mobile adjustments for rules */ @media (max-width: 600px) { .rules-container { - padding: 20px; + padding: 15px; } - .rules-container h1 { - font-size: 1.6rem; + + .rules-header h1 { + font-size: 1.5rem; + } + + .rules-subtitle { + font-size: 0.9rem; + } + + .rules-toc { + padding: 14px 16px; + } + + .toc-title { + font-size: 0.75rem; + } + + .toc-links { + gap: 8px; + } + + .toc-link { + padding: 6px 10px; + font-size: 0.8rem; + } + + .toc-icon { + font-size: 0.9rem; } .rules-section h2 { diff --git a/server/ai.py b/server/ai.py index a0a1298..666e40f 100644 --- a/server/ai.py +++ b/server/ai.py @@ -1073,7 +1073,9 @@ async def process_cpu_turn( return await broadcast_callback() - await asyncio.sleep(0.4 + random.uniform(0, 0.4)) + # Brief pause after draw to let the flash animation register visually + await asyncio.sleep(0.08) + await asyncio.sleep(0.35 + random.uniform(0, 0.35)) # Decide whether to swap or discard swap_pos = GolfAI.choose_swap_or_discard(drawn, cpu_player, profile, game)