import { defineComponent } from "../../src/component"; import type { World, Entity } from "../../src/index"; import { query } from "../../src/query"; import { SUITS, RANKS, isBlackjack, type CardData } from "./game"; // ── Component definitions ──────────────────────────── /** Each card is its own entity. `order` tracks position within its collection. */ export const Card = defineComponent("card", { rank: "A", suit: "♠", order: 0, }); // ── Tag components — mark which collection a card belongs to ─ export const InDeck = defineComponent("inDeck", {}); export const InPlayerHand = defineComponent("inPlayerHand", {}); export const InDealerHand = defineComponent("inDealerHand", {}); // ── Dealer state ───────────────────────────────────── /** When present (as singleton), the dealer's hole card is hidden. */ export const HoleHidden = defineComponent("holeHidden", {}); // ── Score / state ──────────────────────────────────── export const Score = defineComponent("score", { wins: 0, losses: 0, pushes: 0, chips: 100, }); export const Bet = defineComponent("bet", { amount: 10, }); /** Current phase of the game. */ export type Phase = "betting" | "playerTurn" | "dealerTurn" | "roundOver"; export const GamePhase = defineComponent("gamePhase", { phase: "betting" as Phase, message: "", }); // ── Card helpers ───────────────────────────────────── export function createCardHelpers(world: World) { return { /** Create all 52 card entities with the InDeck tag. */ buildDeck(): void { let order = 0; for (const suit of SUITS) { for (const rank of RANKS) { const e = world.spawn(); world.add(e, Card, { rank, suit, order }); world.add(e, InDeck); order++; } } }, /** Shuffle the deck: collect all InDeck entities, shuffle, reassign order. */ shuffleDeck(): void { const entities = [...world.query(query(Card, InDeck))]; for (let i = entities.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [entities[i], entities[j]] = [entities[j], entities[i]]; } for (let i = 0; i < entities.length; i++) { world.get(entities[i], Card).order = i; } }, /** Draw the top card from the deck (highest order). Returns entity or null. */ drawCard(): Entity | null { const cards = [...world.query(query(Card, InDeck))]; if (cards.length === 0) return null; let top = cards[0]; let topOrder = world.get(top, Card).order; for (let i = 1; i < cards.length; i++) { const o = world.get(cards[i], Card).order; if (o > topOrder) { top = cards[i]; topOrder = o; } } world.remove(top, InDeck); return top; }, /** Move a card entity to a hand tag, assigning the next order. */ dealTo(cardEntity: Entity, tag: typeof InPlayerHand): void { const existing = [...world.query(query(Card, tag))]; const nextOrder = existing.length === 0 ? 0 : Math.max(...existing.map((e) => world.get(e, Card).order)) + 1; world.add(cardEntity, tag); world.get(cardEntity, Card).order = nextOrder; }, /** Collect CardData from a hand tag, sorted by order. */ getHand(tag: typeof InPlayerHand): CardData[] { return [...world.query(query(Card, tag))] .map((e) => { const c = world.get(e, Card); return { rank: c.rank, suit: c.suit, order: c.order }; }) .sort((a, b) => a.order - b.order) .map(({ rank, suit }) => ({ rank, suit })); }, /** Remove all cards from a hand tag (destroy the card entities). */ clearHand(tag: typeof InPlayerHand): void { for (const e of world.query(query(Card, tag))) { world.destroy(e); } }, /** Count cards remaining in the deck. */ deckCount(): number { return [...world.query(query(Card, InDeck))].length; }, /** Deal initial 4 cards and start a round. */ startRound(): void { const bet = world.getSingleton(Bet); const score = world.getSingleton(Score); score.chips -= bet.amount; if (this.deckCount() < 15) { const remaining = [...world.query(query(Card, InDeck))]; for (const e of remaining) { world.remove(e, InDeck); } for (const e of remaining) { world.add(e, InDeck); } this.shuffleDeck(); } this.clearHand(InPlayerHand); this.clearHand(InDealerHand); world.removeSingleton(HoleHidden); const c1 = this.drawCard(); const c2 = this.drawCard(); const c3 = this.drawCard(); const c4 = this.drawCard(); if (c1) this.dealTo(c1, InPlayerHand); if (c2) this.dealTo(c2, InDealerHand); if (c3) this.dealTo(c3, InPlayerHand); if (c4) this.dealTo(c4, InDealerHand); const playerCards = this.getHand(InPlayerHand); const dealerCards = this.getHand(InDealerHand); if (isBlackjack(playerCards)) { world.removeSingleton(HoleHidden); if (isBlackjack(dealerCards)) { score.chips += bet.amount; score.pushes++; world.setSingleton(GamePhase, { phase: "roundOver", message: "Both have Blackjack — Push! Press N for new round.", }); } else { const winnings = payout(0, "blackjack", bet.amount); score.chips += bet.amount + winnings; score.wins++; world.setSingleton(GamePhase, { phase: "roundOver", message: "Blackjack! You win 3:2! Press N for new round.", }); } } else { world.addSingleton(HoleHidden); world.setSingleton(GamePhase, { phase: "playerTurn", message: "Your turn — H to hit, S to stand.", }); } }, }; } // ── Internal ───────────────────────────────────────── function payout(outcome: string, 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; default: return 0; } }