ecs-observable/examples/blackjack/game.ts

105 lines
2.7 KiB
TypeScript
Raw Normal View History

// ── Blackjack game logic (pure functions, no ECS dependency) ──
export const SUITS = ["♠", "♥", "♦", "♣"] as const;
export const RANKS = [
"A",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"J",
"Q",
"K",
] as const;
export interface CardData {
rank: string;
suit: string;
}
// ── Hand evaluation ──────────────────────────────────
/** Numeric value of a single card rank. */
export function rankValue(rank: string): number {
if (rank === "A") return 11;
if (rank === "K" || rank === "Q" || rank === "J") return 10;
return parseInt(rank, 10);
}
/** Best total for a hand (aces count as 1 or 11). */
export function handValue(cards: CardData[]): number {
let total = 0;
let aces = 0;
for (const c of cards) {
total += rankValue(c.rank);
if (c.rank === "A") aces++;
}
while (total > 21 && aces > 0) {
total -= 10;
aces--;
}
return total;
}
export function isBust(cards: CardData[]): boolean {
return handValue(cards) > 21;
}
export function isBlackjack(cards: CardData[]): boolean {
return cards.length === 2 && handValue(cards) === 21;
}
export function isSoft(cards: CardData[]): boolean {
let total = 0;
let aces = 0;
for (const c of cards) {
total += rankValue(c.rank);
if (c.rank === "A") aces++;
}
return aces > 0 && total <= 21;
}
// ── Dealer logic ─────────────────────────────────────
/** Dealer must hit on soft 17 in this variant. */
export function dealerShouldHit(cards: CardData[]): boolean {
const val = handValue(cards);
if (val < 17) return true;
if (val === 17 && isSoft(cards)) return true;
return false;
}
// ── Outcome ──────────────────────────────────────────
export type Outcome = "win" | "lose" | "push" | "blackjack";
export function determineOutcome(
playerCards: CardData[],
dealerCards: CardData[],
): Outcome {
if (isBust(playerCards)) return "lose";
if (isBust(dealerCards)) return "win";
if (isBlackjack(playerCards) && !isBlackjack(dealerCards)) return "blackjack";
const pv = handValue(playerCards);
const dv = handValue(dealerCards);
if (pv > dv) return "win";
if (pv < dv) return "lose";
return "push";
}
/** Payout multiplier. Blackjack pays 3:2, win pays 1:1. */
export function payout(outcome: Outcome, bet: number): number {
switch (outcome) {
case "blackjack":
return Math.floor(bet * 1.5);
case "win":
return bet;
case "push":
return 0;
case "lose":
return -bet;
}
}