fertig-classic-games/public/src/games/blackjack/BlackjackLogic.js

352 lines
12 KiB
JavaScript

// Blackjack pure game logic — no Phaser dependencies
import { SUITS, RANKS } from '../cards/Deck.js';
// ─── Shoe ─────────────────────────────────────────────────────────────────────
const RANK_VALUE = {
'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'T':10,'J':10,'Q':10,'K':10,'A':11,
};
function makeCard(rank, suit) {
return {
rank, suit,
value: RANK_VALUE[rank],
label: rank === 'T' ? '10' : rank,
isRed: suit === 'h' || suit === 'd',
suitSymbol: { s:'♠', h:'♥', d:'♦', c:'♣' }[suit],
key: `${rank}${suit}`,
};
}
export function buildShoe(numDecks = 6) {
const cards = [];
for (let d = 0; d < numDecks; d++) {
for (const suit of SUITS) {
for (const rank of RANKS) {
cards.push(makeCard(rank, suit));
}
}
}
for (let i = cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[cards[i], cards[j]] = [cards[j], cards[i]];
}
return cards;
}
// ─── Hand evaluation ──────────────────────────────────────────────────────────
export function handValue(cards) {
let total = 0;
let aces = 0;
for (const c of cards) {
if (c.rank === 'A') { aces++; total += 11; }
else total += Math.min(c.value, 10);
}
while (total > 21 && aces > 0) { total -= 10; aces--; }
return { score: total, soft: aces > 0 && total <= 21 };
}
export function isBlackjack(hand) {
return hand.length === 2 && handValue(hand).score === 21;
}
export function isBust(hand) {
return handValue(hand).score > 21;
}
export function canDouble(player) {
const hand = player.activeHand === 1 ? player.hand2 : player.hand;
return (hand ?? []).length === 2;
}
export function canSplit(player) {
return (
player.hand.length === 2 &&
player.hand[0].rank === player.hand[1].rank &&
!player.hand2
);
}
// ─── State creation ───────────────────────────────────────────────────────────
export function createInitialState(opponents, chips) {
const players = [
{
seat: 0, name: 'You', isHuman: true, active: true, opponent: null,
chips, bet: 0, bet2: null,
hand: [], hand2: null,
doubled: false, doubled2: false,
insuranceBet: 0,
activeHand: 0,
status: 'waiting', status2: null,
result: null, result2: null, chipsWon: 0,
},
];
for (let i = 0; i < 6; i++) {
const opp = opponents[i] ?? null;
players.push({
seat: i + 1, name: opp?.name ?? '', isHuman: false, active: !!opp, opponent: opp,
chips: 1000, bet: 0, bet2: null,
hand: [], hand2: null,
doubled: false, doubled2: false,
insuranceBet: 0,
activeHand: 0,
status: 'waiting', status2: null,
result: null, result2: null, chipsWon: 0,
});
}
return {
phase: 'betting',
dealer: { hand: [], revealed: false },
players,
currentSeat: 0,
insurancePending: false,
roundNumber: 0,
};
}
// ─── Round start ──────────────────────────────────────────────────────────────
export function prepareRound(gs) {
const players = gs.players.map(p => ({
...p,
bet: 0, bet2: null,
hand: [], hand2: null,
doubled: false, doubled2: false,
insuranceBet: 0,
activeHand: 0,
status: p.active ? 'waiting' : 'waiting',
status2: null,
result: null, result2: null, chipsWon: 0,
}));
return {
...gs,
phase: 'betting',
dealer: { hand: [], revealed: false },
players,
currentSeat: 0,
insurancePending: false,
roundNumber: gs.roundNumber + 1,
};
}
export function applyBet(gs, seat, amount) {
const players = gs.players.map(p => p.seat === seat ? { ...p, bet: amount } : p);
return { ...gs, players };
}
// Deal all cards into state from the shoe (shoe is mutated).
// Returns new gs with hands populated, phase set appropriately.
export function dealAllCards(gs, shoe) {
const players = gs.players.map(p => ({ ...p }));
const dealer = { hand: [], revealed: false };
// Standard deal order: each player card 1, dealer upcard, each player card 2, dealer hole
const activePlayers = players.filter(p => p.active);
// Round 1
for (const p of activePlayers) p.hand.push(shoe.pop());
dealer.hand.push(shoe.pop()); // upcard (index 0)
// Round 2
for (const p of activePlayers) p.hand.push(shoe.pop());
dealer.hand.push(shoe.pop()); // hole card (index 1)
// Detect player blackjacks immediately
for (const p of activePlayers) {
if (isBlackjack(p.hand)) p.status = 'blackjack';
}
const firstActiveSeat = activePlayers[0]?.seat ?? 0;
const dealerUpcard = dealer.hand[0];
const insurancePending = dealerUpcard.rank === 'A';
return {
...gs,
phase: insurancePending ? 'insurance' : 'player_turn',
dealer,
players,
currentSeat: firstActiveSeat,
insurancePending,
};
}
// ─── Insurance ────────────────────────────────────────────────────────────────
export function applyInsurance(gs, seat, accept) {
const players = gs.players.map(p => {
if (p.seat !== seat) return p;
const insuranceBet = accept ? Math.floor(p.bet / 2) : 0;
return { ...p, insuranceBet };
});
return { ...gs, players };
}
// After all insurance decisions: check dealer blackjack
export function resolveInsurance(gs) {
const dealerBJ = isBlackjack(gs.dealer.hand);
const players = gs.players.map(p => {
if (!p.active || p.insuranceBet === 0) return p;
// Insurance bet: pays 2:1 if dealer has BJ, else lost
const insuranceResult = dealerBJ ? p.insuranceBet * 2 : -p.insuranceBet;
return { ...p, chipsWon: (p.chipsWon ?? 0) + insuranceResult };
});
return { ...gs, players, insurancePending: false };
}
// ─── Player actions ───────────────────────────────────────────────────────────
export function applyHit(gs, seat, shoe) {
const players = gs.players.map(p => {
if (p.seat !== seat) return p;
const isH2 = p.activeHand === 1;
const newCard = shoe.pop();
if (isH2) {
const hand2 = [...p.hand2, newCard];
const bust = isBust(hand2);
return { ...p, hand2, status2: bust ? 'bust' : 'playing' };
} else {
const hand = [...p.hand, newCard];
const bust = isBust(hand);
return { ...p, hand, status: bust ? 'bust' : 'playing' };
}
});
return { ...gs, players };
}
export function applyStand(gs, seat) {
const players = gs.players.map(p => {
if (p.seat !== seat) return p;
if (p.activeHand === 1) return { ...p, status2: 'standing' };
// If split exists and hand 1 not yet played, move to hand 2
if (p.hand2 !== null && p.status2 === null) return { ...p, status: 'standing', activeHand: 1 };
return { ...p, status: 'standing' };
});
return { ...gs, players };
}
export function applyDouble(gs, seat, shoe) {
const players = gs.players.map(p => {
if (p.seat !== seat) return p;
const isH2 = p.activeHand === 1;
const newCard = shoe.pop();
if (isH2) {
const hand2 = [...p.hand2, newCard];
const bust = isBust(hand2);
return { ...p, hand2, bet2: p.bet2 * 2, doubled2: true, status2: bust ? 'bust' : 'standing' };
} else {
const hand = [...p.hand, newCard];
const bust = isBust(hand);
const doubled = true;
// If split exists and not yet played hand 2
if (p.hand2 !== null && p.status2 === null) {
return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing', activeHand: 1 };
}
return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing' };
}
});
return { ...gs, players };
}
export function applySplit(gs, seat, shoe) {
const players = gs.players.map(p => {
if (p.seat !== seat) return p;
const [c1, c2] = p.hand;
const hand = [c1, shoe.pop()];
const hand2 = [c2, shoe.pop()];
const isAceSplit = c1.rank === 'A';
// Ace splits: each hand gets one card only, immediately stands
return {
...p,
hand, hand2,
bet2: p.bet,
doubled: false, doubled2: false,
status: isAceSplit ? 'standing' : 'playing',
status2: isAceSplit ? 'standing' : null,
activeHand: 0,
};
});
return { ...gs, players };
}
// ─── Dealer turn ──────────────────────────────────────────────────────────────
// Returns new gs with dealer hand fully played out. shoe is mutated.
export function runDealerTurn(gs, shoe) {
const dealer = { ...gs.dealer, revealed: true };
while (true) {
const { score } = handValue(dealer.hand);
if (score >= 17) break;
dealer.hand = [...dealer.hand, shoe.pop()];
}
return { ...gs, phase: 'resolved', dealer };
}
// ─── Resolve round ────────────────────────────────────────────────────────────
export function resolveRound(gs) {
const { score: dealerScore } = handValue(gs.dealer.hand);
const dealerBJ = isBlackjack(gs.dealer.hand);
const dealerBust = dealerScore > 21;
const players = gs.players.map(p => {
if (!p.active) return p;
function resolveHand(hand, bet, status, doubled) {
if (!hand || hand.length === 0) return { result: null, net: 0 };
const { score: playerScore } = handValue(hand);
const playerBJ = isBlackjack(hand);
if (status === 'bust') return { result: 'lose', net: -bet };
if (playerBJ && dealerBJ) return { result: 'push', net: 0 };
if (playerBJ) return { result: 'blackjack', net: Math.floor(bet * 1.5) }; // 3:2
if (dealerBJ) return { result: 'lose', net: -bet };
if (dealerBust) return { result: 'win', net: bet };
if (playerScore > dealerScore) return { result: 'win', net: bet };
if (playerScore === dealerScore) return { result: 'push', net: 0 };
return { result: 'lose', net: -bet };
}
const h1 = resolveHand(p.hand, p.bet, p.status, p.doubled);
const h2 = p.hand2 ? resolveHand(p.hand2, p.bet2, p.status2, p.doubled2) : { result: null, net: 0 };
const totalNet = h1.net + h2.net + (p.chipsWon ?? 0); // chipsWon may have insurance delta
const newChips = p.chips + totalNet;
return {
...p,
chips: newChips,
result: h1.result,
result2: h2.result,
chipsWon: totalNet,
};
});
return { ...gs, phase: 'resolved', players };
}
// ─── Turn advancement ─────────────────────────────────────────────────────────
// Returns the seat number of the next player who still needs to act, or null if done.
export function nextActiveSeat(gs) {
const activePlayers = gs.players.filter(p => p.active);
const currentIdx = activePlayers.findIndex(p => p.seat === gs.currentSeat);
for (let i = currentIdx + 1; i < activePlayers.length; i++) {
const p = activePlayers[i];
if (p.status === 'playing' || p.status === 'waiting') return p.seat;
}
return null; // dealer's turn
}
export function isPlayerDone(player) {
// Hand 1 is done
const h1done = ['standing','bust','blackjack','done'].includes(player.status);
// Hand 2 (if split): must also be done
const h2done = player.hand2 === null || ['standing','bust','blackjack','done'].includes(player.status2);
return h1done && h2done;
}