~/maber.io · main open to work · remote · colorado springs, co
02.

Blackjack

Vegas-rules Blackjack with persistent balance tracking, built twice in two different stacks. The Python/Tkinter version was a forcing function for learning a stateful GUI primitive; the browser port is plain DOM + CSS to prove the same gameplay loop without a framework.

TYPE
Game · Two stacks
YEAR
2025
STACK
Python · Tkinter · JavaScript · HTML · CSS
STATUS
Live · Maintained
ROLE
Solo · Game logic, UI, persistence
[ LIVE DEMO ] See it running blackjack.maber.io view source on github
/architecture

architecture

flowchart TB
  Engine["Game engine<br/>shoe · hand · settle"]
  PyUI["Tkinter UI<br/>desktop"]
  WebUI["DOM + CSS UI<br/>browser"]
  LS[("Local persistence<br/>balance + settings")]

  Engine --> PyUI
  Engine --> WebUI
  PyUI --> LS
  WebUI --> LS

Both implementations share the same core engine shape — a shoe of N decks, a hand evaluator that handles soft 17, and a settlement function. The UI layer is the only thing that changes between desktop and browser. Balance and settings persist locally per implementation (file on Tkinter, localStorage in the browser) so neither version needs a backend.

/decisions

decisions worth reading

A:

The point was to learn two GUI primitives, not just to ship one game. Building it cold in Tkinter and again in plain JS forced me to keep the engine engine and the UI UI — the parts that didn't need to change between stacks didn't change.

A:

The whole game state fits in one object. Reaching for a framework would have added more ceremony than logic. Plain `addEventListener` + a single render function makes every state transition explicit, which is also nicer for a learning artifact.

A:

It's the player-friendlier rule (S17), which keeps the game beatable for casual sessions. Was tempted by H17 for a marginally more interesting decision tree on the dealer side, but ultimately picked S17 because it's the one most casinos still use on the strip.

/code

code highlights

Hand value handling soft aces

Treats aces as 11 by default, then demotes them to 1 only as needed to keep the hand under 22. Same logic powers both the player and dealer evaluators.

function handValue(cards) {
  let total = 0;
  let aces = 0;

  for (const card of cards) {
    if (card.rank === 'A') {
      aces += 1;
      total += 11;
    } else if (['J', 'Q', 'K'].includes(card.rank)) {
      total += 10;
    } else {
      total += Number(card.rank);
    }
  }

  while (total > 21 && aces > 0) {
    total -= 10;
    aces -= 1;
  }

  return total;
}
view full file on github