summaryrefslogtreecommitdiffstats
path: root/web/play/src/blackjack.ts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--web/play/src/blackjack.ts152
1 files changed, 152 insertions, 0 deletions
diff --git a/web/play/src/blackjack.ts b/web/play/src/blackjack.ts
new file mode 100644
index 0000000..3537f45
--- /dev/null
+++ b/web/play/src/blackjack.ts
@@ -0,0 +1,152 @@
1import type { View } from "./types";
2import { cardFace, clear, money, node } from "./dom";
3
4function cardElement(card: string): HTMLElement {
5 if (card === "?") {
6 const back = node("div", "card back");
7 back.append(node("span", "rank", "?"), node("span", "suit", ""));
8 return back;
9 }
10 const face = cardFace(card);
11 const element = node("div", "card");
12 if (face.red) element.classList.add("red");
13 element.append(node("span", "rank", face.rank), node("span", "suit", face.suit));
14 return element;
15}
16
17function handBlock(label: string, cards: string[], total: number): HTMLElement {
18 const block = node("div", "hand");
19 block.append(node("div", "label", label));
20 const row = node("div", "cards");
21 for (const card of cards) row.append(cardElement(card));
22 const sum = node("div", "total");
23 sum.append(document.createTextNode("Total "), node("strong", undefined, String(total)));
24 block.append(row, sum);
25 return block;
26}
27
28function outcome(view: View, hand: any): string {
29 const result = String(hand?.result ?? "");
30 const winnings = Number(hand?.winnings ?? 0);
31 switch (result) {
32 case "blackjack":
33 return `Blackjack — won ${money(winnings)}`;
34 case "win":
35 return `You win — ${money(winnings)}`;
36 case "push":
37 return `Push — ${money(winnings)} returned`;
38 case "bust":
39 return "Bust — the bet is lost";
40 case "lose":
41 return "Dealer wins — the bet is lost";
42 case "forfeit":
43 return "Hand forfeited — the bet is lost";
44 case "playing":
45 return view.status === "player" ? "Your move." : "Dealing…";
46 default:
47 return "Ready to deal.";
48 }
49}
50
51export function renderBlackjack(
52 board: HTMLElement,
53 side: HTMLElement,
54 view: View,
55 send: (command: unknown) => void,
56): void {
57 clear(board);
58 clear(side);
59
60 const hand = view.hand;
61 const playerPhase = hand?.phase === "player" || view.status === "player";
62 const done = hand?.phase === "done" || view.status === "done";
63 const canDeal = !hand || done;
64
65 board.append(node("h2", undefined, "Blackjack"));
66 board.append(
67 node("p", "note", "Six decks. Dealer stands on all 17. Blackjack pays 3:2."),
68 );
69
70 if (hand) {
71 board.append(handBlock("Dealer", hand.dealer ?? [], Number(hand.dealer_total ?? 0)));
72 board.append(handBlock("You", hand.player ?? [], Number(hand.player_total ?? 0)));
73 } else {
74 const empty = node("div", "outcome");
75 empty.textContent = "Press DEAL to start a hand.";
76 board.append(empty);
77 }
78
79 if (done || (hand && !playerPhase)) {
80 const result = node("div", "outcome");
81 result.textContent = outcome(view, hand);
82 if (String(hand?.result) === "win" || String(hand?.result) === "blackjack" || String(hand?.result) === "push") {
83 result.classList.add("win-text");
84 }
85 board.append(result);
86 }
87
88 if (view.error) board.append(node("p", "error-text", String(view.error)));
89
90 // Sidebar
91 side.append(node("div", "label", "Blackjack"));
92 const bet = Number(hand?.bet ?? view.bet ?? 0);
93 const balance = view.balance ?? view.hand?.balance;
94 side.append(node("p", "note", "Hit, stand or double. Forfeiting loses the reserved bet."));
95
96 if (canDeal) {
97 const betRow = node("div", "row");
98 const minus = node("button", "btn small", "−10");
99 const amount = node("strong", "mono", money(bet));
100 const plus = node("button", "btn small", "+10");
101 minus.addEventListener("click", () => send({ AdjustBet: -10 }));
102 plus.addEventListener("click", () => send({ AdjustBet: 10 }));
103 betRow.append(minus, amount, plus);
104 side.append(node("div", "label", "Bet"), betRow);
105 } else {
106 const stat = node("div", "stat");
107 stat.append(node("span", "muted", "Bet"), node("strong", undefined, money(bet)));
108 side.append(stat);
109 }
110
111 if (balance !== undefined) {
112 const stat = node("div", "stat");
113 stat.append(node("span", "muted", "Balance"), node("strong", undefined, money(Number(balance))));
114 side.append(stat);
115 }
116
117 const actions = node("div", "actions");
118 if (playerPhase) {
119 const actionsRow = node("div", "row wrap");
120 for (const action of hand?.actions ?? []) {
121 const label = action === "hit" ? "HIT" : action === "stand" ? "STAND" : "DOUBLE";
122 const button =
123 action === "stand"
124 ? node("button", "btn primary", label)
125 : node("button", "btn", label);
126 button.addEventListener("click", () => {
127 if (action === "hit") send("Hit");
128 else if (action === "stand") send("Stand");
129 else send("Double");
130 });
131 actionsRow.append(button);
132 }
133 actions.append(actionsRow);
134 const forfeit = node("button", "btn danger wide", "Forfeit hand");
135 forfeit.addEventListener("click", () => {
136 if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit");
137 });
138 actions.append(forfeit);
139 } else if (canDeal) {
140 const deal = node("button", "btn primary wide big", "DEAL");
141 deal.addEventListener("click", () => send("Deal"));
142 actions.append(deal);
143 }
144
145 const leave = node("button", "btn ghost wide", playerPhase ? "Leave (forfeits)" : "Leave table");
146 leave.addEventListener("click", () => {
147 if (playerPhase && !window.confirm("Leaving forfeits this hand. Continue?")) return;
148 send("Leave");
149 });
150 actions.append(leave);
151 side.append(actions);
152}