import type { View } from "./types"; import { cardFace, clear, money, node } from "./dom"; let lastAction = 0; function actionAllowed(): boolean { if (Date.now() - lastAction < 400) return false; lastAction = Date.now(); return true; } function cardElement(card: string): HTMLElement { if (card === "?") { const back = node("div", "card back"); back.append(node("span", "rank", "?"), node("span", "suit", "")); return back; } const face = cardFace(card); const element = node("div", "card"); if (face.red) element.classList.add("red"); element.append(node("span", "rank", face.rank), node("span", "suit", face.suit)); return element; } function handBlock(label: string, cards: string[], total: number): HTMLElement { const block = node("div", "hand"); const head = node("div", "hand-head"); head.append(node("span", "label", label), node("strong", "total", String(total))); const row = node("div", "cards"); for (const card of cards) row.append(cardElement(card)); block.append(head, row); return block; } function outcome(view: View, hand: any): string { const result = String(hand?.result ?? ""); const winnings = Number(hand?.winnings ?? 0); switch (result) { case "blackjack": return `Blackjack — won ${money(winnings)}`; case "win": return `You win — ${money(winnings)}`; case "push": return `Push — ${money(winnings)} returned`; case "bust": return "Bust — the bet is lost"; case "lose": return "Dealer wins — the bet is lost"; case "forfeit": return "Hand forfeited — the bet is lost"; case "playing": return view.status === "player" ? "Your move." : "Dealing…"; default: return "Ready to deal."; } } export function renderBlackjack( board: HTMLElement, side: HTMLElement, view: View, send: (command: unknown) => void, ): void { clear(board); clear(side); const hand = view.hand; const playerPhase = hand?.phase === "player" || view.status === "player"; const done = hand?.phase === "done" || view.status === "done"; const canDeal = !hand || done; const table = node("div", "bj-table"); if (hand) { table.append(handBlock("DEALER", hand.dealer ?? [], Number(hand.dealer_total ?? 0))); table.append(node("div", "bj-divider")); table.append(handBlock("YOU", hand.player ?? [], Number(hand.player_total ?? 0))); } else { const empty = node("div", "empty-table", "Place a bet and deal a hand."); table.append(empty); } board.append(table); const foot = node("div", "board-foot"); foot.append( node("span", "label", "Hand"), node("span", undefined, hand ? outcome(view, hand) : "Waiting"), ); board.append(foot); if (view.error) board.append(node("p", "error-text", String(view.error))); // Sidebar side.append(node("h2", undefined, "Blackjack")); const bet = Number(hand?.bet ?? view.bet ?? 0); const balance = view.balance ?? view.hand?.balance; side.append(node("p", "note", "Get closer to 21 without going over.")); if (canDeal) { const betRow = node("div", "row"); const minus = node("button", "btn small", "−10"); const amount = node("strong", "mono", money(bet)); const plus = node("button", "btn small", "+10"); minus.addEventListener("click", () => send({ AdjustBet: -10 })); plus.addEventListener("click", () => send({ AdjustBet: 10 })); betRow.append(minus, amount, plus); side.append(node("div", "label", "Bet"), betRow); } else { const stat = node("div", "stat"); stat.append(node("span", "muted", "Bet"), node("strong", undefined, money(bet))); side.append(stat); } if (balance !== undefined) { const stat = node("div", "stat"); stat.append(node("span", "muted", "Balance"), node("strong", undefined, money(Number(balance)))); side.append(stat); } if (done || (hand && !playerPhase)) { const resultLine = node("p", "note win-text"); resultLine.textContent = `${String(hand?.result ?? "").toUpperCase()} ${money(Number(hand?.winnings ?? 0))}`; if (!["win", "blackjack", "push"].includes(String(hand?.result))) { resultLine.className = "note"; } side.append(resultLine); } else if (playerPhase) { const actionsText = (hand?.actions ?? []).includes("double") ? "Hit, stand or double." : "Hit or stand."; side.append(node("p", "note", actionsText)); } else if (!hand) { side.append(node("p", "note", "Press DEAL to start a hand.")); } const actions = node("div", "actions"); if (playerPhase) { const actionsRow = node("div", "row wrap"); for (const action of hand?.actions ?? []) { const label = action === "hit" ? "HIT" : action === "stand" ? "STAND" : "DOUBLE"; const button = action === "stand" ? node("button", "btn primary", label) : node("button", "btn", label); button.addEventListener("click", () => { if (!actionAllowed()) return; if (action === "hit") send("Hit"); else if (action === "stand") send("Stand"); else send("Double"); }); actionsRow.append(button); } actions.append(actionsRow); const forfeit = node("button", "btn danger wide", "Forfeit hand"); forfeit.addEventListener("click", () => { if (!actionAllowed()) return; if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit"); }); actions.append(forfeit); } else if (canDeal) { const deal = node("button", "btn primary wide big", "DEAL"); deal.addEventListener("click", () => { if (actionAllowed()) send("Deal"); }); actions.append(deal); } const leave = node("button", "btn ghost wide", playerPhase ? "Leave (forfeits)" : "Leave table"); leave.addEventListener("click", () => { if (playerPhase && !window.confirm("Leaving forfeits this hand. Continue?")) return; send("Leave"); }); actions.append(leave); side.append(actions); }