204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
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("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;
|
|
}
|
|
}
|