:: blackjack-koth

iczelia

Blackjack KoTH event for Esolangs

recent commits

2026-08-12 20:24initial importiczelia

branches

main (2026-08-12 20:24)

tags

no tags.

README.md

Esolangs Blackjack King Of The Hill Rules

Quick Start

  • Build: make (produces bj, dflat, drandom). Use new-ish clang or gcc.
  • Run demo: ./bj --games 2000 --bot flat:./dflat --bot random:./drandom.
  • Implement your bot by speaking the request/response JSON below on stdin/stdout; keep responses under ~500ms and obey legal_actions and staking constraints to avoid termination.
  • Implement solutions in any programming language that has a widely available compiler and/or interpreter and supports communication via standard I/O streams.
    • Solutions in esoteric languages (brainfuck, Malbolge, etc.) are welcome. Their time limit is increased tenfold per move, to ~5s, for fairness.
    • The interpreters/compilers for each language will be chosen at the discretion of the organiser, unless explicitly specified.
    • The following languages are explicitly allowed: - Brainfuck, Befunge93, Befunge98, C, C++, Python, Ruby, JavaScript (Node.js), Perl, Lua, Rust, Java, Go.
  • The engine may be amended to fix bugs. Bots should not rely on unspecified behavior.
  • Card counting, shuffle tracking, or other advanced techniques are allowed.
  • Malicious, or potentially malicious bots will not be tolerated in the competition.
  • Intended to be played by members of the Esolangs Discord server.

Organisation

  • We will continue to play rounds until a clear winner is established.
  • In the first round, all submitted bots will play a match of between 10,000 and 100,000 games each against all other bots.
  • The worst-performing bot (i.e., lowest final balance) will be eliminated.
  • Subsequent rounds will continue with the remaining bots until only one bot remains.
    • Source code to all bots is made publicly available between rounds to allow for improvements.
    • The engine may be updated between rounds to fix bugs or improve fairness.
    • The user bots will be published, with attribution, on GitHub along with the engine.
  • In the event of a tie for worst performance, all tied bots will be eliminated.
  • The final winner will be declared the King of the Hill.

Rounds

  • Round 1: Tue 31 Dec 00:00 UTC to Wed 14 Jan 23:59 UTC. Submit bots by the deadline to @iczelia.

Examples

  • dflat.c (./dflat): deterministic baseline; always bets 10, declines insurance, prefers DOUBLE if offered, otherwise HIT if available, otherwise STAND; occasional SPLIT (20% chance) when legal.
  • drandom.c (./drandom): random-ish; bets 5-10 uniformly, declines insurance, chooses uniformly among advertised legal play actions.

Gameplay Rules

  • Purposefully chosen to be different (easier) than standard casino rules to encourage bot development.
  • Read https://en.wikipedia.org/wiki/Blackjack1 for general blackjack terminology and concepts.
  • Shoe: 6 standard decks (312 cards), shuffled; reshuffles when 75% of cards have been dealt (penetration_reshuffle_fraction 0.75).
  • Dealer hits soft 17.
  • Blackjack pays 3:2 (blackjack_payout 1.5). Player blackjack vs dealer blackjack is a push.
  • Double: allowed on any two cards, including after a non-ace split; not allowed after splitting aces. Double adds a second wager equal to the current wager and draws exactly one card.
  • Split: allowed on two equal ranks up to split_max_hands (4 total hands). Aces may be split once; split aces get one card each and are immediately marked completed (no hit/double/resplit).
  • Hit split aces: not allowed. Resplit aces: not allowed.
  • Surrender: none.
  • Insurance: offered when dealer upcard is Ace. Max insurance = half of the original bet; pays 2:1 net (engine adds 3x the insurance stake on dealer blackjack to cover stake + winnings).
  • Bets: constrained by match config min_bet and max_bet. Bot must have at least min_bet balance to be seated for a game.

Match / Hand Flow

  1. Active seats: bots with balance >= min_bet and not terminated are shuffled into a per-game order; seat in requests is this order index.
  2. Betting phase: each active bot posts a bet (deducted immediately). Invalid/missing bets terminate the seat for the rest of the match.
  3. Initial deal: each active hand receives 2 cards; dealer takes 2 (first is upcard).
  4. Insurance phase: only if dealer upcard is Ace. Dealer peeks for blackjack when upcard is Ace or 10-value.
  5. Dealer blackjack branch: if dealer has blackjack, insurance settles, player blackjacks push, other hands lose; round ends.
  6. Play phase: for each active seat, each hand plays until completed or bot terminated. Legal actions per hand: HIT, STAND, optional DOUBLE, optional SPLIT.
  7. Dealer play: only if any live player hand remains; dealer draws until 17+ (hits soft 17).
  8. Settlement: each hand settled vs dealer; balances updated. Game counter increments; loop continues until configured num_games or no active bots.
  9. After match, engine prints final balances and termination status.

Action Request JSON (engine -> bot)

One line of JSON is sent per decision. Structure (all numeric fields are integers unless noted):

{
  "type": "action_request",
  "match_id": "demo-match",
  "game_id": 12,
  "bot_id": "flat_basic",
  "seat": 0,
  "phase": "bet" | "insurance" | "play",
  "st": {
    "rules": {
      "decks": 6,
      "penetration_reshuffle_fraction": 0.75,
      "dealer_hits_soft_17": 1,
      "blackjack_payout": 1.5,
      "double_allowed": "any_two",
      "double_after_split": 1,
      "split_max_hands": 4,
      "resplit_aces": 0,
      "hit_split_aces": 0,
      "surrender": "none",
      "insurance_allowed": 1,
      "min_bet": 1,
      "max_bet": 100
    },
    "table": {
      "num_seats": 2,
      "seat_order": [ { "seat": 0, "bot_id": "flat_basic" }, ... ],
      "balances": { "flat_basic": 9990.0, "randomish": 10005.0 },
      "active_bots": ["flat_basic", "randomish"],
      "terminated_bots": [],
      "current_bets": { "flat_basic": 10.0, "randomish": 5.0 },
      "hands": [
        { "seat": 0, "hands": [ { "hand_id": "12-0-0", "wager": 10.0, "is_split_hand": 0, "is_completed": 0, "cards": [ { "rank": "A", "suit": "S" }, ... ] } ] },
        { "seat": 1, "hands": [ ... ] }
      ]
    },
    "shoe": {
      "cards_dealt_in_shoe": 42,
      "cards_remaining_in_shoe": 270,
      "reshuffle_at_dealt": 234,
      "discard_rank_counts": { "A": 3, "2": 5, ... "K": 4 }
    },
    "history": { "games_completed": 11, "recent_games": [] },
    "you": {
      "bot_id": "flat_basic",
      "seat": 0,
      "balance": 9990.0,
      "current_bet_total": 10.0,
      "current_hands": [ { "hand_id": "12-0-0", "wager": 10.0, "is_split_hand": 0, "is_completed": 0, "cards": [ ... ] } ]
    },
    "dealer": {
      "upcard": { "rank": "K", "suit": "H" },
      "hole_card_known": 0,
      "cards_revealed": [ { "rank": "K", "suit": "H" } ],
      "peek_checked": 1,
      "has_blackjack": 0
    },
    "decision": {
      "phase": "bet" | "insurance" | "play",
      "hand_id": "12-0-0",     // present during play decisions
      "legal_actions": ["HIT","STAND","DOUBLE"] // phase-dependent
    },
    "max_insurance": 5        // present when insurance is offered
  },
  "legal_actions": ["BET" | "INSURANCE" | "NO_INSURANCE" | ...],
  "min_bet": 1,               // bet phase only
  "max_bet": 100,             // bet phase only
  "max_insurance": 5,         // insurance phase only
  "time_budget_ms": 50
}

Notes:

  • hand_id format: <game_id>-<seat>-<hand_index>.
  • legal_actions is duplicated: inside st.decision for context and again top-level for ease of parsing.
  • Amount fields are integers; wagers and balances in state are doubles with two decimals.
  • Requests are newline-terminated and delivered over the bot's stdin.

Expected Action Response JSON (bot -> engine)

Bot must emit a single line JSON per request:

{
  "type": "action_response",
  "match_id": "demo-match",
  "game_id": 12,
  "bot_id": "flat_basic",
  "action": "BET" | "INSURANCE" | "NO_INSURANCE" | "HIT" | "STAND" | "DOUBLE" | "SPLIT",
  "amount": 10   // required for BET, optional for INSURANCE, ignored otherwise
}

Validation per phase:

  • Bet: action must be BET; amount clamped to [min_bet, max_bet] and to available balance. Missing/invalid/over-range bets terminate the seat.
  • Insurance: action may be INSURANCE or NO_INSURANCE. If INSURANCE, amount is clamped to [0, max_insurance] and balance; sending 0 or omitting amount is treated as full max_insurance. Over-range/invalid terminates the seat.
  • Play: action must be one of the provided legal actions for that hand. DOUBLE/SPLIT also require sufficient balance at execution time; otherwise the seat is terminated.

Timing, Process, and Termination

  • Bots are spawned via /bin/sh -c <cmd>; stdin/stdout pipes carry JSON lines.
  • time_budget_ms in the request is 50ms, but the engine actually waits up to ~500ms for a full line response. No/late response results in an empty buffer and subsequent termination.
  • Parse failures, unexpected action strings, or missing required fields terminate the seat (balance set to 0, hand data cleared) for the rest of the match.
  • Seats are also terminated if they try to act with insufficient balance (bet, insurance, double, split) or choose an illegal action.
tab: 248 wrap: offon