# Texas hold 'em, heads-up, no-limit, with a fixed number of hands. The game is in the public
# domain; the rules are described here in our own words.
game: holdem
label: "Texas hold 'em"
players: "2 players"
num_players: 2
mode: competitive
summary: >
  Heads-up no-limit hold 'em over ten hands. Each hand you get two hidden cards; five community
  cards come out in three rounds (flop, turn, river) with a betting round before and after each.
  Fold, check or call, or raise (half the pot, the pot, or all-in). At showdown the best five-card
  hand from your two cards and the five on the table wins the pot. After ten hands, or when a player
  is out of chips, the bigger stack wins.
scope: >
  Blinds 1 and 2, stacks of 100, no antes, no side pots beyond heads-up all-ins (a call for less
  wins only what the smaller stack covered; the uncalled part goes back). Raise sizes are fixed
  choices, not any amount. An odd chip of a split pot goes to the player left of the button.

constants:
  STACK: 100
  SB: 1
  BB: 2
  HANDS: 10
  RANKS: ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"]
  SUITS: ["♠", "♥", "♦", "♣"]
  STREETS: [preflop, flop, turn, river]
  DEAL: {preflop: 0, flop: 3, turn: 1, river: 1}
  CATEGORY: ["high card", "a pair", "two pair", "three of a kind", "a straight", "a flush", "a full house", "four of a kind", "a straight flush"]

functions:
  ranks_of:
    params: [cs]
    x: "sorted([c['rank'] for c in cs], reverse=True)"
  groups:
    text: "ranks grouped: [(count, rank)] highest count first, then highest rank"
    params: [cs]
    x: "sorted([(ranks_of(cs).count(r), r) for r in set(ranks_of(cs))], reverse=True)"
  flush_suit:
    params: [cs]
    x: "[s for s in SUITS if sum(1 for c in cs if c['suit'] == s) >= 5]"
  straight_high:
    text: "the top of the best straight in these ranks, or 0 (the ace counts low in A-2-3-4-5)"
    params: [rs]
    x: "max([h for h in range(14, 4, -1) if all((h - i if h - i >= 2 else 14) in rs for i in range(5))] + [0])"
  strength:
    text: "the value of the best five cards in cs: [category, tiebreaks...], higher is better (also for fewer cards, such as the board alone)"
    params: [cs]
    x: >-
      [([8, straight_high([c['rank'] for c in cs if c['suit'] == fs[0]])] if fs and straight_high([c['rank'] for c in cs if c['suit'] == fs[0]]) > 0
        else [7, g[0][1], max([r for n, r in g if r != g[0][1]] + [0])] if g[0][0] == 4
        else [6, g[0][1], g[1][1]] if g[0][0] == 3 and g[1][0] >= 2
        else [5] + sorted([c['rank'] for c in cs if c['suit'] == fs[0]], reverse=True)[:5] if fs
        else [4, straight_high(rs)] if straight_high(rs) > 0
        else [3, g[0][1]] + [r for n, r in g[1:]][:2] if g[0][0] == 3
        else [2, g[0][1], g[1][1], max([r for n, r in g[2:]] + [0])] if g[0][0] == 2 and g[1][0] == 2
        else [1, g[0][1]] + [r for n, r in g[1:]][:3] if g[0][0] == 2
        else [0] + rs[:5])
       for g in [groups(cs) + [(0, 0), (0, 0)]] for fs in [flush_suit(cs)] for rs in [ranks_of(cs)]][0]
  describe:
    params: [cs]
    x: "CATEGORY[strength(cs)[0]]"
  pot_now:
    x: "pot + bet[0] + bet[1]"
  to_call:
    params: [p]
    x: "min(bet[1 - p] - bet[p], stack[p])"
  raise_options:
    text: "the raise sizes on offer: half the pot, the pot, all-in (each at least a min-raise and at most the stack)"
    params: [p]
    x: >-
      [{'label': ('all-in (' + str(amt) + ')' if amt == bet[p] + stack[p] else 'raise to ' + str(amt)), 'act': 'raise', 'to': amt}
       for amt in sorted(set([min(bet[p] + stack[p], max(bet[1 - p] + max(last_raise, BB), bet[1 - p] + (pot_now() + to_call(p)) // 2)),
                               min(bet[p] + stack[p], max(bet[1 - p] + max(last_raise, BB), bet[1 - p] + pot_now() + to_call(p))),
                               bet[p] + stack[p]]))
       if amt > bet[1 - p] and amt - bet[p] <= stack[p] and stack[1 - p] + bet[1 - p] > bet[1 - p]]
  options_for:
    text: "fold, check or call, and the raises"
    params: [p]
    x: >-
      ([{'label': 'fold', 'act': 'fold'}] if to_call(p) > 0 else [])
      + ([{'label': 'check', 'act': 'call'}] if to_call(p) == 0 else [{'label': 'call ' + str(to_call(p)), 'act': 'call'}])
      + (raise_options(p) if stack[p] > to_call(p) and stack[1 - p] > 0 else [])

state:
  entity_types:
    card: {attrs: {rank: int, suit: str}}
  zones:
    deck: {kind: stack, owner: global, visibility: hidden, shuffled: true, holds: card, init_x: "[{'id': r + s, 'label': r + s, 'rank': i + 2, 'suit': s} for i, r in enumerate(RANKS) for s in SUITS]"}
    hole: {kind: set, owner: player, visibility: owner, holds: card}
    board: {kind: set, owner: global, visibility: public, holds: card, note: "the community cards"}
  vars:
    stack: {scope: player, type: int, init_x: "[STACK, STACK]"}
    bet: {scope: player, type: int, init: 0, note: "put in on this street"}
    pot: {scope: global, type: int, init: 0, note: "from earlier streets"}
    dealer: {scope: global, type: ref, init: 0, note: "the button: posts the small blind and acts first before the flop"}
    hand_no: {scope: global, type: int, init: 1}
    street: {scope: global, type: str, init: preflop}
    to_act: {scope: global, type: ref, init: 0}
    need: {scope: player, type: bool, init: false, note: "still has to act on this street"}
    folded: {scope: global, type: ref, init: null, note: "who folded this hand"}
    last_raise: {scope: global, type: int, init: 0}
    winner: {scope: global, type: ref, init: null}
    shown: {scope: global, type: str, init: "", note: "what won the last showdown"}

flow:
  id: match
  label: Match
  kind: loop
  until: ten hands are played or a player is out of chips
  until_x: "hand_no > HANDS or (min(stack) == 0 and pot == 0 and bet[0] == 0 and bet[1] == 0)"
  children:
    - {id: deal, label: "New hand: shuffle, deal two cards each, post the blinds", kind: step, do: new_hand}
    - id: streets
      label: Betting rounds
      kind: loop
      over: preflop, flop, turn, river
      over_x: STREETS
      as: street
      once: true
      note: "after a fold, or once someone is all-in and called, the remaining streets are skipped (the board is completed at the showdown)"
      children:
        - id: street_open
          label: The hand goes on?
          kind: branch
          switch: nobody folded and nobody all-in
          cases:
            - when: the hand is still open
              when_x: "folded is None and min(stack) > 0"
              children:
                - {id: open_street, label: Deal the community cards, kind: step, do: open_street}
        - id: betting
          label: Betting
          kind: loop
          until: everyone has acted and the bets are equal
          until_x: "folded is not None or not any(need)"
          children:
            - {id: act, label: "Fold, check or call, or raise", kind: step, actor_x: to_act, do: act}
    - {id: showdown, label: Showdown, kind: step, do: settle}

actions:
  new_hand:
    effect:
      - {op: move, what: the hole cards, what_x: "list(hole[0])", from: "hole[0]", to: deck}
      - {op: move, what: the hole cards, what_x: "list(hole[1])", from: "hole[1]", to: deck}
      - {op: move, what: the community cards, what_x: "list(board)", from: board, to: deck}
      - {op: shuffle, zone: deck}
      - {op: set, var: dealer, value_x: "(hand_no + 1) % 2"}
      - {op: set, var: folded, value: null}
      - {op: set, var: winner, value: null}
      - {op: set, var: pot, value: 0}
      - {op: set, var: bet, value_x: "[0, 0]"}
      - {op: draw, n: 2, from: deck, to: "hole[0]"}
      - {op: draw, n: 2, from: deck, to: "hole[1]"}
      - {op: set, var: "bet[dealer]", value_x: "min(SB, stack[dealer])"}
      - {op: set, var: "bet[1 - dealer]", value_x: "min(BB, stack[1 - dealer])"}
      - {op: set, var: "stack[dealer]", value_x: "stack[dealer] - bet[dealer]"}
      - {op: set, var: "stack[1 - dealer]", value_x: "stack[1 - dealer] - bet[1 - dealer]"}
      - {op: set, var: last_raise, value_x: "BB"}
      - {op: emit, event: hand_dealt}
  open_street:
    effect:
      - op: if
        cond: a later street
        cond_x: "street != 'preflop'"
        then:
          - {op: set, var: pot, value_x: "pot + bet[0] + bet[1]"}
          - {op: set, var: bet, value_x: "[0, 0]"}
          - {op: set, var: last_raise, value_x: "BB"}
          - {op: draw, n: "the street's cards", n_x: "DEAL[street]", from: deck, to: board}
      - {op: set, var: to_act, value_x: "dealer if street == 'preflop' else 1 - dealer"}
      - {op: set, var: need, value_x: "[stack[0] > 0 and stack[1] > 0, stack[0] > 0 and stack[1] > 0]"}
  act:
    label: Act
    effect:
      - {op: choose, options: "fold, check or call, or a raise", options_x: "options_for(to_act)", bind: o}
      - op: if
        cond: fold
        cond_x: "o['act'] == 'fold'"
        then:
          - {op: set, var: folded, value_x: to_act}
          - {op: set, var: need, value_x: "[False, False]"}
        else:
          - op: if
            cond: call or check
            cond_x: "o['act'] == 'call'"
            then:
              - {op: let, bind: c, value_x: "to_call(to_act)"}
              - {op: set, var: "stack[to_act]", value_x: "stack[to_act] - c"}
              - {op: set, var: "bet[to_act]", value_x: "bet[to_act] + c"}
              - {op: set, var: "need[to_act]", value: false}
            else:
              - {op: let, bind: add, value_x: "o['to'] - bet[to_act]"}
              - {op: set, var: last_raise, value_x: "o['to'] - bet[1 - to_act]"}
              - {op: set, var: "stack[to_act]", value_x: "stack[to_act] - add"}
              - {op: set, var: "bet[to_act]", value_x: "o['to']"}
              - {op: set, var: "need[to_act]", value: false}
              - {op: set, var: "need[1 - to_act]", value_x: "stack[1 - to_act] > 0"}
          - {op: set, var: to_act, value_x: "1 - to_act"}
      - {op: emit, event: acted}
  settle:
    effect:
      # an uncalled part of a bet (the bigger stack went all-in for more than the other could call) goes back
      - {op: let, bind: matched, value_x: "min(bet[0], bet[1])"}
      - {op: set, var: stack, value_x: "[stack[0] + bet[0] - matched, stack[1] + bet[1] - matched]"}
      - {op: set, var: pot, value_x: "pot + 2 * matched"}
      - op: if
        cond: someone folded
        cond_x: "folded is not None"
        then:
          - {op: set, var: winner, value_x: "1 - folded"}
          - {op: set, var: shown, value: the other player folded, value_x: "'the other player folded'"}
        else:
          - op: if
            cond: the board is not complete (both all-in early)
            cond_x: "len(board) < 5"
            then:
              - {op: draw, n: "the rest of the board", n_x: "5 - len(board)", from: deck, to: board}
          - {op: let, bind: s0, value_x: "strength(list(hole[0]) + list(board))"}
          - {op: let, bind: s1, value_x: "strength(list(hole[1]) + list(board))"}
          - {op: reveal, what: both hands, what_x: "list(hole[0]) + list(hole[1])"}
          - {op: set, var: winner, value_x: "0 if s0 > s1 else 1 if s1 > s0 else None"}
          - {op: set, var: shown, value_x: "describe(list(hole[0]) + list(board)) + ' vs ' + describe(list(hole[1]) + list(board))"}
      - op: if
        cond: a winner
        cond_x: "winner is not None"
        then:
          - {op: set, var: "stack[winner]", value_x: "stack[winner] + pot"}
        else:
          - {op: set, var: stack, value_x: "[stack[0] + pot // 2 + (pot % 2 if dealer == 1 else 0), stack[1] + pot // 2 + (pot % 2 if dealer == 0 else 0)]"}
      - {op: set, var: pot, value: 0}
      - {op: set, var: bet, value_x: "[0, 0]"}
      - {op: add, var: hand_no, amount: 1}
      - {op: emit, event: hand_over}

end:
  - id: done
    if: "ten hands played, or a player is out of chips: the bigger stack wins"
    if_x: "hand_no > HANDS or (min(stack) == 0 and pot == 0 and bet[0] == 0 and bet[1] == 0)"
    result: score
    returns_x: "[1 if stack[q] > stack[1 - q] else -1 if stack[q] < stack[1 - q] else 0 for q in players]"

tutorial:
  - {at: act, text: "Before the flop the dealer (small blind) acts first; after it the other player does. Call matches the bet, check passes when nothing is owed, a raise sets a new price to stay in. Fold gives up the hand and whatever you put in."}
  - {at: settle, text: "Showdown: the best five cards out of your two and the five on the table. Pair beats high card, two pair beats a pair, then three of a kind, straight, flush, full house, four of a kind, straight flush."}
