diff options
Diffstat (limited to 'web/play/src/blackjack.ts')
| -rw-r--r-- | web/play/src/blackjack.ts | 152 |
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 @@ | |||
| 1 | import type { View } from "./types"; | ||
| 2 | import { cardFace, clear, money, node } from "./dom"; | ||
| 3 | |||
| 4 | function 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 | |||
| 17 | function 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 | |||
| 28 | function 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 | |||
| 51 | export 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 | } | ||
