105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
// ── 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;
|
|
}
|
|
}
|