# Yahtzee: five dice, three rolls a turn, thirteen boxes to fill. The classic dice game (the name is
# a trademark of Hasbro; the rules of the public-domain game "Yacht" family are described here in
# our own words).
game: yahtzee
label: Yahtzee
players: "2 players"
num_players: 2
mode: competitive
summary: >
  Thirteen rounds. On your turn you roll five dice, and may reroll any of them twice more. Then you
  write the dice into one of your thirteen boxes: ones to sixes score the pips of that face (63 or
  more over the six boxes earns a 35 bonus), three and four of a kind score all pips, a full house
  25, a small straight 30, a large straight 40, a Yahtzee (five alike) 50, and chance all pips. A box
  that does not fit scores 0. After thirteen rounds the highest total wins.
scope: >
  Extra Yahtzees score a 100 bonus. As a joker, such a roll must go into its upper box if that is
  still open; otherwise it may be written into any lower box, also the full house or a straight for
  full points. No multiple-player Yahtzee variants.

constants:
  ROUNDS: 13
  UPPER: [ones, twos, threes, fours, fives, sixes]
  LOWER: [three_kind, four_kind, full_house, small_straight, large_straight, yahtzee, chance]
  NAMES:
    ones: Ones
    twos: Twos
    threes: Threes
    fours: Fours
    fives: Fives
    sixes: Sixes
    three_kind: Three of a kind
    four_kind: Four of a kind
    full_house: Full house
    small_straight: Small straight
    large_straight: Large straight
    yahtzee: Yahtzee
    chance: Chance
  FACE_OF: {ones: 1, twos: 2, threes: 3, fours: 4, fives: 5, sixes: 6}
  UPPER_BONUS_AT: 63
  UPPER_BONUS: 35
  YAHTZEE_BONUS: 100
  PIP: ["", "⚀", "⚁", "⚂", "⚃", "⚄", "⚅"]

functions:
  counts:
    text: "how often each face shows: a list for faces 1 to 6 (index 0 unused)"
    params: [d]
    x: "[0] + [d.count(f) for f in range(1, 7)]"
  best_face:
    text: "the face that shows most often (the highest on a tie)"
    params: [d]
    x: "[f for f in range(6, 0, -1) if counts(d)[f] == max(counts(d))][0]"
  is_yahtzee:
    params: [d]
    x: "max(counts(d)) == 5"
  is_full:
    text: "a full house: three of one face and two of another"
    params: [d]
    x: "sorted(counts(d))[-1] == 3 and sorted(counts(d))[-2] == 2"
  straight_len:
    text: "the longest run of consecutive faces"
    params: [d]
    x: >-
      max([len([f for f in range(s, s + n) if f in d]) for s in range(1, 7) for n in range(1, 8 - s)
           if all(f in d for f in range(s, s + n))] + [0])
  joker:
    text: "a Yahtzee that may be written anywhere for full points (the Yahtzee box is already used)"
    params: [d, sc]
    x: "is_yahtzee(d) and sc.get('yahtzee') is not None"
  points:
    text: "what dice d score in box b, on score sheet sc"
    params: [b, d, sc]
    x: >-
      (counts(d)[FACE_OF[b]] * FACE_OF[b] if b in FACE_OF
       else sum(d) if b in ('three_kind', 'four_kind') and (max(counts(d)) >= (3 if b == 'three_kind' else 4) or joker(d, sc))
       else 25 if b == 'full_house' and (is_full(d) or joker(d, sc))
       else 30 if b == 'small_straight' and (straight_len(d) >= 4 or joker(d, sc))
       else 40 if b == 'large_straight' and (straight_len(d) >= 5 or joker(d, sc))
       else 50 if b == 'yahtzee' and is_yahtzee(d)
       else sum(d) if b == 'chance'
       else 0)
  open_boxes:
    text: "the boxes still open; a joker Yahtzee must go into its own upper box while that is open"
    params: [sc, d]
    x: >-
      [UPPER[d[0] - 1]] if joker(d, sc) and sc.get(UPPER[d[0] - 1]) is None
      else [b for b in UPPER + LOWER if sc.get(b) is None]
  upper_total:
    params: [sc]
    x: "sum(sc.get(b) or 0 for b in UPPER)"
  total:
    text: "the sheet's total: boxes, the upper bonus and Yahtzee bonuses"
    params: [sc, yb]
    x: "sum(v or 0 for v in sc.values()) + (UPPER_BONUS if upper_total(sc) >= UPPER_BONUS_AT else 0) + YAHTZEE_BONUS * yb"
  keep_options:
    text: "tap a die to keep it (or to roll it again), then roll the others, or stop and keep all five"
    params: [d, k]
    x: >-
      [{'label': ('roll ' if i in k else 'keep ') + PIP[d[i]], 'cell': i, 'die': i} for i in range(len(d))]
      + ([{'label': 'roll ' + str(len(d) - len(k)) + (' die' if len(d) - len(k) == 1 else ' dice'), 'act': 'roll'}] if len(k) < len(d) else [])
      + [{'label': 'stop rolling', 'act': 'stop'}]
content:
  boxes:
    - {id: ones, label: Ones, status: verified}
    - {id: twos, label: Twos, status: verified}
    - {id: threes, label: Threes, status: verified}
    - {id: fours, label: Fours, status: verified}
    - {id: fives, label: Fives, status: verified}
    - {id: sixes, label: Sixes, status: verified}
    - {id: three_kind, label: Three of a kind, status: verified}
    - {id: four_kind, label: Four of a kind, status: verified}
    - {id: full_house, label: Full house, status: verified}
    - {id: small_straight, label: Small straight, status: verified}
    - {id: large_straight, label: Large straight, status: verified}
    - {id: yahtzee, label: Yahtzee, status: verified}
    - {id: chance, label: Chance, status: verified}

state:
  derived:
    table_dice: {text: "the dice as pips, a tick on the ones kept", x: "[PIP[dice[i]] + (' ✓' if i in keep else '') for i in range(len(dice))]"}
  zones:
    sheet_cards: {kind: set, owner: player, visibility: public, holds: box, init_x: "[list(content['boxes']) for q in players]", note: "the score sheet: a card per box, filled in with its points"}
  vars:
    current: {scope: global, type: ref, init: 0}
    round_no: {scope: global, type: int, init: 1}
    dice: {scope: global, type: list, init: [], note: "the five dice of the current turn"}
    rolls_left: {scope: global, type: int, init: 0}
    keep: {scope: global, type: list, init: [], note: "positions kept for the next roll (kept dice stay kept after a roll)"}
    sheet: {scope: player, type: dict, init_x: "[{} for q in players]", note: "box → points (missing: open)"}
    yahtzee_bonus: {scope: player, type: int, init: 0, note: "extra Yahtzees"}
    score: {scope: player, type: int, init: 0}

flow:
  id: game
  label: Game
  kind: loop
  until: thirteen rounds are played
  until_x: "round_no > ROUNDS"
  children:
    - id: round
      label: Round
      kind: loop
      over: the players
      over_x: players
      as: current
      once: true
      children:
        - {id: first_roll, label: Roll the five dice, kind: step, do: first_roll}
        - id: rerolls
          label: Reroll up to twice
          kind: loop
          until: no rolls left
          until_x: "rolls_left == 0"
          children:
            - {id: keep_step, label: Choose the dice to keep, kind: step, actor: current player, actor_x: current, do: choose_keep}
        - {id: score_step, label: Write the dice into a box, kind: step, actor: current player, actor_x: current, do: score_box}
    - {id: next_round, label: Next round, kind: step, do: next_round}

actions:
  first_roll:
    effect:
      - {op: set, var: dice, value: []}
      - {op: set, var: keep, value: []}
      - {op: set, var: rolls_left, value: 2}
      - {op: run, flow: roll_dice}
  choose_keep:
    label: Keep dice, roll the others
    effect:
      - {op: choose, options: "a die to keep or roll again, or roll, or stop", options_x: "keep_options(dice, keep)", bind: k}
      - op: if
        cond: a die was tapped
        cond_x: "k.get('die') is not None"
        then:
          - {op: set, var: keep, value_x: "sorted([i for i in keep if i != k['die']] + ([] if k['die'] in keep else [k['die']]))"}
        else:
          - op: if
            cond: stop rolling
            cond_x: "k['act'] == 'stop'"
            then:
              - {op: set, var: rolls_left, value: 0}
            else:
              - {op: set, var: dice, value_x: "[dice[i] for i in keep]"}
              - {op: set, var: keep, value_x: "list(range(len(keep)))"}
              - {op: add, var: rolls_left, amount: -1}
              - {op: run, flow: roll_dice}
  score_box:
    label: Write the dice into a box
    effect:
      - op: choose
        options: "an open box (with the points it would score)"
        options_x: >-
          [{'label': NAMES[b] + ': ' + str(points(b, dice, sheet[current])), 'box': b, 'on': [c for c in sheet_cards[current] if c['id'] == b][0]}
           for b in open_boxes(sheet[current], dice)]
        bind: choice
      - op: if
        cond: an extra Yahtzee
        cond_x: "is_yahtzee(dice) and sheet[current].get('yahtzee') == 50"
        then:
          - {op: add, var: "yahtzee_bonus[current]", amount: 1}
      - {op: set, var: "sheet[current]", value_x: "{**sheet[current], choice['box']: points(choice['box'], dice, sheet[current])}"}
      - {op: set, var: "score[current]", value_x: "total(sheet[current], yahtzee_bonus[current])"}
      - {op: set, of_x: "[c for c in sheet_cards[current] if c['id'] == choice['box']][0]", attr: points, value_x: "sheet[current][choice['box']]"}
      - {op: set, of_x: "[c for c in sheet_cards[current] if c['id'] == choice['box']][0]", attr: filled, value: true}
      - {op: emit, event: box_written}
  next_round:
    effect:
      - {op: add, var: round_no, amount: 1}
  roll_missing:
    effect:
      - op: repeat
        until: five dice are on the table
        until_x: "len(dice) == 5"
        body:
          - {op: roll, die: [1, 2, 3, 4, 5, 6], bind: f}
          - {op: set, var: dice, value_x: "dice + [f]"}

flows:
  roll_dice:
    id: roll_dice
    label: Roll the dice that were not kept
    kind: sequence
    children:
      - {id: roll_one, label: Roll, kind: step, do: roll_missing}

end:
  - id: done
    if: "thirteen rounds are played: the highest total wins"
    if_x: "round_no > ROUNDS"
    result: score
    returns_x: "[1 if score[q] == max(score) and score.count(max(score)) == 1 else (0 if score[q] == max(score) else -1) for q in players]"

# Shown while playing, the first time the step comes up (learn as you play)
tutorial:
  - {at: keep_step, text: "Tap the dice you want to keep (a tick shows them; tap again to roll that die after all), then roll the others. You may roll twice more each turn, or stop whenever you like what you have."}
  - {at: score_step, text: "Tap a box on your score sheet (or choose it below): the number is what these dice score there. A box that does not fit scores 0, and every box is used once."}
