UI polish: opponent draw flash, compact house rules with suit separators, toast styling.

- 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 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-01-26 21:37:02 -05:00
parent 33e3f124ed
commit 36a71799b5
4 changed files with 278 additions and 86 deletions

View File

@ -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)

View File

@ -94,10 +94,10 @@
<summary>Advanced Options</summary>
<div class="advanced-options-grid">
<!-- Left Column: Variants & Jokers -->
<!-- Left Column: Gameplay & Jokers -->
<div class="options-column">
<div class="options-category">
<h4>Variants</h4>
<h4>Gameplay</h4>
<div class="checkbox-group">
<div class="select-option">
<label for="flip-mode">Flip on Discard</label>
@ -106,12 +106,17 @@
<option value="always">Speed Golf - MUST flip a card after discarding</option>
<option value="endgame">Endgame - Optional flip to help trailing players catch up</option>
</select>
<span class="rule-desc">What happens when you draw from deck and discard</span>
<span class="rule-desc">After discarding a drawn card</span>
</div>
<label class="checkbox-label">
<label class="checkbox-label inline">
<input type="checkbox" id="flip-as-action">
<span>Flip as Action</span>
<span class="rule-desc"><span class="suit suit-black"></span>flip instead of draw</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="knock-penalty">
<span>Knock Penalty</span>
<span class="rule-desc">+10 if you go out but don't have lowest</span>
<span class="rule-desc"><span class="suit suit-red"></span>+10 if not lowest</span>
</label>
</div>
</div>
@ -126,94 +131,84 @@
<label class="radio-label">
<input type="radio" name="joker-mode" value="standard">
<span>Standard</span>
<span class="rule-desc">2 per deck, -2 pts / 0 paired</span>
<span class="rule-desc"><span class="suit suit-black"></span>2 per deck, -2 / 0 paired</span>
</label>
<label class="radio-label">
<input type="radio" name="joker-mode" value="lucky-swing">
<span>Lucky Swing</span>
<span class="rule-desc">1-3 decks: 1 Joker, -5 pts!</span>
<span class="rule-desc"><span class="suit suit-red"></span>1 Joker total, -5!</span>
</label>
<label class="radio-label">
<input type="radio" name="joker-mode" value="eagle-eye">
<span>Eagle-Eyed</span>
<span class="rule-desc">2 per deck, +2 pts / -4 paired</span>
</label>
</div>
</div>
<div class="options-category">
<h4>Point Modifiers</h4>
<div class="checkbox-group">
<label class="checkbox-label inline">
<input type="checkbox" id="super-kings">
<span>Super Kings</span>
<span class="rule-desc">K = -2 pts</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="ten-penny">
<span>Ten Penny</span>
<span class="rule-desc">10 = 1 pt</span>
<span class="rule-desc"><span class="suit suit-black"></span>+2 / -4 paired</span>
</label>
</div>
</div>
</div>
<!-- Right Column: Bonuses & Gameplay -->
<!-- Right Column: Card Values & Bonuses -->
<div class="options-column">
<div class="options-category">
<h4>Bonuses & Penalties</h4>
<h4>Card Values</h4>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="knock-bonus">
<span>Knock Out Bonus</span>
<span class="rule-desc">-5 for going out first</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="underdog-bonus">
<span>Underdog Bonus</span>
<span class="rule-desc">-3 for lowest score each hole</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="tied-shame">
<span>Tied Shame</span>
<span class="rule-desc">+5 if you tie with someone</span>
<label class="checkbox-label inline">
<input type="checkbox" id="super-kings">
<span>Super Kings</span>
<span class="rule-desc"><span class="suit suit-red"></span>K = -2</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="blackjack">
<span>Blackjack</span>
<span class="rule-desc">21 pts = 0 pts</span>
<input type="checkbox" id="ten-penny">
<span>Ten Penny</span>
<span class="rule-desc"><span class="suit suit-black"></span>10 = 1</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="wolfpack">
<span>Wolfpack</span>
<span class="rule-desc">All 4 Jacks = -20 pts</span>
<input type="checkbox" id="one-eyed-jacks">
<span>One-Eyed Jacks</span>
<span class="rule-desc"><span class="suit suit-red"></span>J♥/J♠ = 0</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="negative-pairs-keep-value">
<span>Negative Pairs</span>
<span class="rule-desc"><span class="suit suit-black"></span>paired 2s/Jokers = -4</span>
</label>
</div>
</div>
<div class="options-category">
<h4>New Variants</h4>
<h4>Bonuses & Penalties</h4>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="flip-as-action">
<span>Flip as Action</span>
<span class="rule-desc">Use turn to flip a card without drawing</span>
<label class="checkbox-label inline">
<input type="checkbox" id="knock-bonus">
<span>Knock Bonus</span>
<span class="rule-desc"><span class="suit suit-red"></span>-5 going out first</span>
</label>
<label class="checkbox-label">
<label class="checkbox-label inline">
<input type="checkbox" id="underdog-bonus">
<span>Underdog</span>
<span class="rule-desc"><span class="suit suit-black"></span>-3 lowest score</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="tied-shame">
<span>Tied Shame</span>
<span class="rule-desc"><span class="suit suit-red"></span>+5 if tied</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="blackjack">
<span>Blackjack</span>
<span class="rule-desc"><span class="suit suit-black"></span>score 21 = 0</span>
</label>
<label class="checkbox-label inline">
<input type="checkbox" id="four-of-a-kind">
<span>Four of a Kind</span>
<span class="rule-desc">4 matching cards in 2 columns = -20 pts</span>
<span class="rule-desc"><span class="suit suit-red"></span>-20 bonus</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="negative-pairs-keep-value">
<span>Negative Pairs Keep Value</span>
<span class="rule-desc">Paired 2s/Jokers stay at -4 (not 0)</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="one-eyed-jacks">
<span>One-Eyed Jacks</span>
<span class="rule-desc">J♥ and J♠ worth 0 pts</span>
<label class="checkbox-label inline">
<input type="checkbox" id="wolfpack">
<span>Wolfpack</span>
<span class="rule-desc"><span class="suit suit-black"></span>4 Jacks = -20</span>
</label>
<p id="wolfpack-combo-note" class="combo-note hidden">🃏 4 Jacks = -40 (stacks!)</p>
</div>
</div>

View File

@ -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 {

View File

@ -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)