ecs-observable/examples/blackjack/commands.ts

148 lines
3.9 KiB
TypeScript

import { defineComponent } from "../../src/component";
import type { World, Entity } from "../../src/index";
import { query } from "../../src/query";
import type { CommandQueue } from "../../src/commands/index";
import {
Card,
InDeck,
InPlayerHand,
InDealerHand,
HoleHidden,
Score,
Bet,
GamePhase,
} from "./components";
import {
handValue,
isBust,
isBlackjack,
determineOutcome,
payout,
} from "./game";
// ── Command definitions ──────────────────────────────
/** Player takes another card. */
export const Hit = defineComponent("hit", {});
/** Player stands (ends their turn). */
export const Stand = defineComponent("stand", {});
/** Start a new round. */
export const NewRound = defineComponent("newRound", {});
/** Increase the bet. */
export const BetMore = defineComponent("betMore", {});
/** Decrease the bet. */
export const BetLess = defineComponent("betLess", {});
// ── Command handlers ─────────────────────────────────
export function registerCommands(
world: World,
commands: CommandQueue,
helpers: ReturnType<typeof import("./components").createCardHelpers>,
): void {
commands.handle(Hit, () => {
const phase = world.getSingleton(GamePhase);
if (phase.phase !== "playerTurn") return;
const cardEntity = helpers.drawCard();
if (cardEntity) {
helpers.dealTo(cardEntity, InPlayerHand);
}
if (isBust(helpers.getHand(InPlayerHand))) {
world.removeSingleton(HoleHidden);
resolveRound(world, helpers);
}
});
commands.handle(Stand, () => {
const phase = world.getSingleton(GamePhase);
if (phase.phase !== "playerTurn") return;
world.removeSingleton(HoleHidden);
world.setSingleton(GamePhase, {
phase: "dealerTurn",
message: "Dealer's turn...",
});
});
commands.handle(NewRound, () => {
const phase = world.getSingleton(GamePhase);
if (phase.phase !== "roundOver" && phase.phase !== "betting") return;
const score = world.getSingleton(Score);
if (score.chips <= 0) {
world.setSingleton(GamePhase, {
phase: "roundOver",
message: "You're out of chips! Restart the program to play again.",
});
return;
}
helpers.startRound();
});
commands.handle(BetMore, () => {
const phase = world.getSingleton(GamePhase);
if (phase.phase !== "betting" && phase.phase !== "roundOver") return;
const bet = world.getSingleton(Bet);
const score = world.getSingleton(Score);
bet.amount = Math.min(bet.amount + 10, score.chips);
});
commands.handle(BetLess, () => {
const phase = world.getSingleton(GamePhase);
if (phase.phase !== "betting" && phase.phase !== "roundOver") return;
const bet = world.getSingleton(Bet);
bet.amount = Math.max(bet.amount - 10, 10);
});
}
// ── Internal ─────────────────────────────────────────
export function resolveRound(
world: World,
helpers: ReturnType<typeof import("./components").createCardHelpers>,
): void {
const playerCards = helpers.getHand(InPlayerHand);
const dealerCards = helpers.getHand(InDealerHand);
const score = world.getSingleton(Score);
const bet = world.getSingleton(Bet);
const outcome = determineOutcome(playerCards, dealerCards);
const winnings = payout(outcome, bet.amount);
score.chips += bet.amount + winnings;
switch (outcome) {
case "blackjack":
case "win":
score.wins++;
break;
case "lose":
score.losses++;
break;
case "push":
score.pushes++;
break;
}
const messages: Record<string, string> = {
win: "You win!",
lose: "Dealer wins.",
push: "Push — tie!",
blackjack: "Blackjack! You win 3:2!",
};
world.setSingleton(GamePhase, {
phase: "roundOver",
message: `${messages[outcome]} Press N for new round.`,
});
}