Rewrite core game logic to fix 10 critical bugs violating poker rules: - Add betMatched flag to separate chip tracking from bet-matching state - Implement last-aggressor tracking for proper betting round completion - Rewrite all action functions with validation enforcement - Add side pot support for multi-level all-in scenarios - Replace nested setTimeout AI turns with async promise chain - Add aiActing guard to prevent race conditions during AI play - Fix currentTurn advancement to always land on active players
121 lines
2.9 KiB
Svelte
121 lines
2.9 KiB
Svelte
<script lang="ts">
|
|
import type { GameState } from '$lib/types/game-state';
|
|
|
|
let { gameState, onAction, disabled = false }: {
|
|
gameState: GameState;
|
|
onAction: (type: string, amount?: number) => void;
|
|
disabled?: boolean;
|
|
} = $props();
|
|
|
|
const humanPlayer = $derived(gameState.players[0]);
|
|
const isMyTurn = $derived(
|
|
!disabled &&
|
|
gameState.currentTurn === 0 &&
|
|
humanPlayer?.status === 'active' &&
|
|
gameState.bettingRound !== 'idle' &&
|
|
gameState.bettingRound !== 'showdown'
|
|
);
|
|
|
|
const canCheck = $derived(humanPlayer && humanPlayer.currentBet >= gameState.currentBet);
|
|
const callAmount = $derived(humanPlayer ? gameState.currentBet - humanPlayer.currentBet : 0);
|
|
const minRaise = $derived(gameState.currentBet + (gameState.lastRaiseAmount || gameState.bigBlind));
|
|
|
|
let raiseAmount = $derived(minRaise);
|
|
</script>
|
|
|
|
<div class="bet-controls" class:hidden={!isMyTurn}>
|
|
{#if isMyTurn}
|
|
<button class="btn check" onclick={() => onAction('check')} disabled={!canCheck}>
|
|
Check
|
|
</button>
|
|
|
|
{#if !canCheck}
|
|
<button class="btn call" onclick={() => onAction('call')}>
|
|
Call ${callAmount}
|
|
</button>
|
|
{/if}
|
|
|
|
<div class="raise-group">
|
|
<input
|
|
type="number"
|
|
bind:value={raiseAmount}
|
|
min={minRaise}
|
|
max={humanPlayer.chips + humanPlayer.currentBet}
|
|
class="raise-input"
|
|
aria-label="Raise amount"
|
|
/>
|
|
<button class="btn raise" onclick={() => onAction('raise', raiseAmount)}>
|
|
Raise
|
|
</button>
|
|
</div>
|
|
|
|
{#if humanPlayer && (humanPlayer.chips > callAmount)}
|
|
<button class="btn all-in" onclick={() => onAction('all-in')}>
|
|
All-In ${humanPlayer.chips}
|
|
</button>
|
|
{/if}
|
|
|
|
<button class="btn fold" onclick={() => onAction('fold')}>
|
|
Fold
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.bet-controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.bet-controls.hidden {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn:not(:disabled):hover {
|
|
transform: translateY(-1px);
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
.check { background: #3498db; color: white; }
|
|
.call { background: #2ecc71; color: white; }
|
|
.raise { background: #e67e22; color: white; }
|
|
.fold { background: #e74c3c; color: white; }
|
|
.all-in { background: #9b59b6; color: white; }
|
|
|
|
.raise-group {
|
|
display: flex;
|
|
gap: 4px;
|
|
align-items: center;
|
|
}
|
|
|
|
.raise-input {
|
|
width: 70px;
|
|
padding: 8px;
|
|
border: 2px solid #555;
|
|
border-radius: 6px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: white;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
}
|
|
</style>
|