{
  "id": "P122",
  "slug": "turnstile",
  "key": "P122-turnstile",
  "title": "Turnstile",
  "summary": "A coin unlocks the gate and a push lets one person through: three machines that differ only in a coin while the gate is open, step by step, side by side, and over every short sequence of events.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P122 - Turnstile\n\nA coin unlocks the gate and a push lets one person through; a push on a\nlocked gate is refused. Three machines differ only in what a coin does\nwhen the gate is already open.\n\n- `main.eml` - the menu, the machines, one machine step by step, all three\n  side by side, and every short sequence of events\n- `gate.eml` - one step of each machine, a run of events with its totals,\n  and the count over every sequence\n\nHow each part works:\n\n- As in the corpus case `turnstile-simulator`, a state machine driven by\n  events, with running counts beside it. The state is the number of\n  passages paid for and not yet used.\n- A coin while the gate is open:\n  - classic keeps the coin, and the gate stays open for one person: the\n    coin buys nothing;\n  - credit counts it, so one more person may pass;\n  - refund gives it back.\n- On every machine and every sequence, coins paid = people passed + coins\n  lost + passages still owed.\n- Every sequence of 1 to 8 events: classic loses a coin in 369 of the 510,\n  and up to 7 in one; the other two never do. Classic loses a coin exactly\n  when a coin comes while the gate is open.\n\nWhat is checked: menu choices 1 to 5; one of the three machines; events\nof c and p, in either case, with or without spaces, up to 20. An empty\nanswer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the machines;\n- c p p c c p p on classic (a coin lost, a push refused) and on credit\n  (all three pass);\n- c c c p p p p and p c p c p side by side;\n- every sequence.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a cancel and turbo for the machine;\n- events: a cancel, c x p, and 21 coins, then CP CP on refund;\n- a cancel side by side.\n\nBuilt on the verified corpus case `turnstile-simulator` (a two-state\nmachine and its counters, driven by a list of events).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P122 turnstile: three machines, a list of coins and pushes run through\n# one of them step by step or through all three side by side, and every\n# short sequence of events counted.\nimport gate\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef state_text(owed):\n    if owed == 0:\n        return \"locked\"\n    if owed == 1:\n        return \"open\"\n    return \"open, \" + str(owed) + \" paid\"\n\ndef show():\n    \"Every machine: a coin opens a locked gate; a push through an open gate lets one person pass and locks it once nothing more is paid; a push on a locked gate is refused.\" ^0\n    \"A coin while the gate is already open:\" ^0\n    \"  classic  keeps the coin, and the gate stays open for one person\" ^0\n    \"  credit   counts it: one more person may pass\" ^0\n    \"  refund   gives it back\" ^0\n\ndef ask_events():\n    (\"Events: c for a coin, p for a push, up to 20, such as c p p c c p.\") ^0\n    plain(input(\"events> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return []\n    [] => out\n    for c in t:\n        if c == \"c\" or c == \"C\":\n            out + [\"coin\"] => out\n        elif c == \"p\" or c == \"P\":\n            out + [\"push\"] => out\n        elif c != \" \":\n            \"Type only c, p and spaces.\" ^0\n            return []\n    if len(out) > 20:\n        \"Up to 20 events.\" ^0\n        return []\n    return out\n\ndef what_text(w):\n    if w == \"taken\":\n        return \"coin taken\"\n    if w == \"refunded\":\n        return \"coin given back: the gate is already open\"\n    if w == \"passes\":\n        return \"one person passes\"\n    if w == \"refused\":\n        return \"refused: pay first\"\n    return \"coin kept, but the gate was already open: it buys nothing\"\n\ndef totals_text(t):\n    return \"paid \" + str(t[\"paid\"]) + \", passed \" + str(t[\"passes\"]) + \", refused \" + str(t[\"refused\"]) + \", given back \" + str(t[\"refunded\"]) + \", lost \" + str(t[\"lost\"]) + \", still owed \" + str(t[\"owed\"])\n\ndef ask_machine():\n    plain(input(\"machine (classic, credit or refund)> \")) => m\n    if m == \"\":\n        \"Cancelled.\" ^0\n        return \"\"\n    for x in gate.machines():\n        if x == m:\n            return m\n    \"Type classic, credit or refund.\" ^0\n    return \"\"\n\ndef one(machine):\n    ask_events() => evs\n    if len(evs) == 0:\n        return\n    gate.run(machine, evs) => r\n    (machine + \":\") ^0\n    for k in [0:len(evs) - 1]:\n        r[0][k] => e\n        state_text(e[1]) => a\n        state_text(e[2]) => b\n        if a == b:\n            (\"  \" + lpad(str(k + 1), 2) + \"  \" + pad(e[0], 5) + pad(a, 22) + what_text(e[3])) ^0\n        else:\n            (\"  \" + lpad(str(k + 1), 2) + \"  \" + pad(e[0], 5) + pad(a + \" -> \" + b, 22) + what_text(e[3])) ^0\n    (\"  \" + totals_text(r[1]) + \".\") ^0\n\ndef side():\n    ask_events() => evs\n    if len(evs) == 0:\n        return\n    for m in gate.machines():\n        (\"  \" + pad(m, 8) + totals_text(gate.run(m, evs)[1])) ^0\n    \"Coins paid = people passed + coins lost + passages still owed, on every machine.\" ^0\n\ndef every():\n    \"Every sequence of 1 to 8 events, 510 in all:\" ^0\n    for m in gate.machines():\n        gate.census(m, 8) => c\n        if c[1] == 0:\n            (\"  \" + pad(m, 8) + \"never loses a coin.\") ^0\n        else:\n            (\"  \" + pad(m, 8) + \"loses a coin in \" + str(c[1]) + \" of the \" + str(c[0]) + \", and up to \" + str(c[2]) + \" coins in one.\") ^0\n    \"Only classic loses coins, and exactly when a coin comes while the gate is open.\" ^0\n\n\"== Turnstile ==\" ^0\n\"A coin unlocks the gate and a push lets one person through. Three machines differ only in what a coin does when the gate is already open.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) machines  2) one machine  3) side by side  4) every sequence  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show()\n    elif choice == \"2\":\n        ask_machine() => m\n        if m != \"\":\n            one(m)\n    elif choice == \"3\":\n        side()\n    elif choice == \"4\":\n        every()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import gate\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef state_text(owed):\n    if owed == 0:\n        return \"locked\"\n    if owed == 1:\n        return \"open\"\n    return \"open, \" + str(owed) + \" paid\"\n\ndef show():\n    print(\"Every machine: a coin opens a locked gate; a push through an open gate lets one person pass and locks it once nothing more is paid; a push on a locked gate is refused.\")\n    print(\"A coin while the gate is already open:\")\n    print(\"  classic  keeps the coin, and the gate stays open for one person\")\n    print(\"  credit   counts it: one more person may pass\")\n    print(\"  refund   gives it back\")\n\ndef ask_events():\n    print(\"Events: c for a coin, p for a push, up to 20, such as c p p c c p.\")\n    t = plain(input(\"events> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return []\n    out = []\n    for c in t:\n        if c == \"c\" or c == \"C\":\n            out = out + [\"coin\"]\n        elif c == \"p\" or c == \"P\":\n            out = out + [\"push\"]\n        elif c != \" \":\n            print(\"Type only c, p and spaces.\")\n            return []\n    if len(out) > 20:\n        print(\"Up to 20 events.\")\n        return []\n    return out\n\ndef what_text(w):\n    if w == \"taken\":\n        return \"coin taken\"\n    if w == \"refunded\":\n        return \"coin given back: the gate is already open\"\n    if w == \"passes\":\n        return \"one person passes\"\n    if w == \"refused\":\n        return \"refused: pay first\"\n    return \"coin kept, but the gate was already open: it buys nothing\"\n\ndef totals_text(t):\n    return \"paid \" + str(t[\"paid\"]) + \", passed \" + str(t[\"passes\"]) + \", refused \" + str(t[\"refused\"]) + \", given back \" + str(t[\"refunded\"]) + \", lost \" + str(t[\"lost\"]) + \", still owed \" + str(t[\"owed\"])\n\ndef ask_machine():\n    m = plain(input(\"machine (classic, credit or refund)> \"))\n    if m == \"\":\n        print(\"Cancelled.\")\n        return \"\"\n    for x in gate.machines():\n        if x == m:\n            return m\n    print(\"Type classic, credit or refund.\")\n    return \"\"\n\ndef one(machine):\n    evs = ask_events()\n    if len(evs) == 0:\n        return\n    r = gate.run(machine, evs)\n    print(machine + \":\")\n    for k in range(0, len(evs)):\n        e = r[0][k]\n        a = state_text(e[1])\n        b = state_text(e[2])\n        if a == b:\n            print(\"  \" + lpad(str(k + 1), 2) + \"  \" + pad(e[0], 5) + pad(a, 22) + what_text(e[3]))\n        else:\n            print(\"  \" + lpad(str(k + 1), 2) + \"  \" + pad(e[0], 5) + pad(a + \" -> \" + b, 22) + what_text(e[3]))\n    print(\"  \" + totals_text(r[1]) + \".\")\n\ndef side():\n    evs = ask_events()\n    if len(evs) == 0:\n        return\n    for m in gate.machines():\n        print(\"  \" + pad(m, 8) + totals_text(gate.run(m, evs)[1]))\n    print(\"Coins paid = people passed + coins lost + passages still owed, on every machine.\")\n\ndef every():\n    print(\"Every sequence of 1 to 8 events, 510 in all:\")\n    for m in gate.machines():\n        c = gate.census(m, 8)\n        if c[1] == 0:\n            print(\"  \" + pad(m, 8) + \"never loses a coin.\")\n        else:\n            print(\"  \" + pad(m, 8) + \"loses a coin in \" + str(c[1]) + \" of the \" + str(c[0]) + \", and up to \" + str(c[2]) + \" coins in one.\")\n    print(\"Only classic loses coins, and exactly when a coin comes while the gate is open.\")\n\nprint(\"== Turnstile ==\")\nprint(\"A coin unlocks the gate and a push lets one person through. Three machines differ only in what a coin does when the gate is already open.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) machines  2) one machine  3) side by side  4) every sequence  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show()\n    elif choice == \"2\":\n        m = ask_machine()\n        if m != \"\":\n            one(m)\n    elif choice == \"3\":\n        side()\n    elif choice == \"4\":\n        every()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "gate.eml",
      "eml": "# P122 turnstile - a coin unlocks it, a push lets one person through and\n# locks it again; a push while locked is refused. As in the corpus case\n# turnstile-simulator, a two-state machine driven by events. Three machines\n# differ only in a coin that arrives while the gate is already open:\n#   classic  keeps the coin and stays open: one passage for two coins;\n#   credit   counts it: each coin is one passage, however they arrive;\n#   refund   gives it back.\n# The state is the number of passages paid for and not yet used (classic\n# and refund never hold more than one).\n\ndef machines():\n    return [\"classic\", \"credit\", \"refund\"]\n\ndef step(machine, owed, ev):\n    # [passages owed after, what happened]: what is \"taken\", \"refunded\",\n    # \"passes\", \"refused\" or \"lost\" (a coin taken that buys nothing)\n    if ev == \"coin\":\n        if owed == 0 or machine == \"credit\":\n            return [owed + 1, \"taken\"]\n        if machine == \"refund\":\n            return [owed, \"refunded\"]\n        return [owed, \"lost\"]\n    if owed > 0:\n        return [owed - 1, \"passes\"]\n    return [owed, \"refused\"]\n\ndef run(machine, events):\n    # [trace of [event, owed before, owed after, what], totals {name: n}]\n    0 => owed\n    [] => trace\n    {\"taken\": 0, \"refunded\": 0, \"passes\": 0, \"refused\": 0, \"lost\": 0} => tot\n    for ev in events:\n        step(machine, owed, ev) => r\n        trace + [[ev, owed, r[0], r[1]]] => trace\n        r[0] => owed\n        tot[r[1]] + 1 => tot[r[1]]\n    tot[\"taken\"] + tot[\"lost\"] => tot[\"paid\"]\n    owed => tot[\"owed\"]\n    return [trace, tot]\n\ndef sequences(n):\n    # every sequence of n events\n    [[]] => out\n    for k in [1:n]:\n        [] => more\n        for s in out:\n            more + [s + [\"coin\"]] => more\n            more + [s + [\"push\"]] => more\n        more => out\n    return out\n\ndef census(machine, n):\n    # over every sequence of 1 to n events: [sequences, sequences losing a\n    # coin, most coins lost in one, most passages refused in one]\n    0 => count\n    0 => losing\n    0 => most\n    0 => refused\n    for k in [1:n]:\n        for s in sequences(k):\n            run(machine, s)[1] => t\n            count + 1 => count\n            if t[\"lost\"] > 0:\n                losing + 1 => losing\n            if t[\"lost\"] > most:\n                t[\"lost\"] => most\n            if t[\"refused\"] > refused:\n                t[\"refused\"] => refused\n    return [count, losing, most, refused]\n",
      "python": "def machines():\n    return [\"classic\", \"credit\", \"refund\"]\n\ndef step(machine, owed, ev):\n    if ev == \"coin\":\n        if owed == 0 or machine == \"credit\":\n            return [owed + 1, \"taken\"]\n        if machine == \"refund\":\n            return [owed, \"refunded\"]\n        return [owed, \"lost\"]\n    if owed > 0:\n        return [owed - 1, \"passes\"]\n    return [owed, \"refused\"]\n\ndef run(machine, events):\n    owed = 0\n    trace = []\n    tot = {\"taken\": 0, \"refunded\": 0, \"passes\": 0, \"refused\": 0, \"lost\": 0}\n    for ev in events:\n        r = step(machine, owed, ev)\n        trace = trace + [[ev, owed, r[0], r[1]]]\n        owed = r[0]\n        tot[r[1]] = tot[r[1]] + 1\n    tot[\"paid\"] = tot[\"taken\"] + tot[\"lost\"]\n    tot[\"owed\"] = owed\n    return [trace, tot]\n\ndef sequences(n):\n    out = [[]]\n    for k in range(1, n+1):\n        more = []\n        for s in out:\n            more = more + [s + [\"coin\"]]\n            more = more + [s + [\"push\"]]\n        out = more\n    return out\n\ndef census(machine, n):\n    count = 0\n    losing = 0\n    most = 0\n    refused = 0\n    for k in range(1, n+1):\n        for s in sequences(k):\n            t = run(machine, s)[1]\n            count = count + 1\n            if t[\"lost\"] > 0:\n                losing = losing + 1\n            if t[\"lost\"] > most:\n                most = t[\"lost\"]\n            if t[\"refused\"] > refused:\n                refused = t[\"refused\"]\n    return [count, losing, most, refused]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\nturbo\n2\nclassic\n\n2\nclassic\nc x p\n2\nclassic\nc c c c c c c c c c c c c c c c c c c c c\n2\nrefund\nCP CP\n3\n\n5\n",
      "screen": "== Turnstile ==\nA coin unlocks the gate and a push lets one person through. Three machines differ only in what a coin does when the gate is already open.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> \nCancelled.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> turbo\nType classic, credit or refund.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> classic\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> \nCancelled.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> classic\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> c x p\nType only c, p and spaces.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> classic\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> c c c c c c c c c c c c c c c c c c c c c\nUp to 20 events.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> refund\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> CP CP\nrefund:\n   1  coin locked -> open        coin taken\n   2  push open -> locked        one person passes\n   3  coin locked -> open        coin taken\n   4  push open -> locked        one person passes\n  paid 2, passed 2, refused 0, given back 0, lost 0, still owed 0.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 3\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> \nCancelled.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\nclassic\nc p p c c p p\n2\ncredit\nc p p c c p p\n3\nc c c p p p p\n3\np c p c p\n4\n5\n",
      "screen": "== Turnstile ==\nA coin unlocks the gate and a push lets one person through. Three machines differ only in what a coin does when the gate is already open.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 1\nEvery machine: a coin opens a locked gate; a push through an open gate lets one person pass and locks it once nothing more is paid; a push on a locked gate is refused.\nA coin while the gate is already open:\n  classic  keeps the coin, and the gate stays open for one person\n  credit   counts it: one more person may pass\n  refund   gives it back\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> classic\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> c p p c c p p\nclassic:\n   1  coin locked -> open        coin taken\n   2  push open -> locked        one person passes\n   3  push locked                refused: pay first\n   4  coin locked -> open        coin taken\n   5  coin open                  coin kept, but the gate was already open: it buys nothing\n   6  push open -> locked        one person passes\n   7  push locked                refused: pay first\n  paid 3, passed 2, refused 2, given back 0, lost 1, still owed 0.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 2\nmachine (classic, credit or refund)> credit\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> c p p c c p p\ncredit:\n   1  coin locked -> open        coin taken\n   2  push open -> locked        one person passes\n   3  push locked                refused: pay first\n   4  coin locked -> open        coin taken\n   5  coin open -> open, 2 paid  coin taken\n   6  push open, 2 paid -> open  one person passes\n   7  push open -> locked        one person passes\n  paid 3, passed 3, refused 1, given back 0, lost 0, still owed 0.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 3\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> c c c p p p p\n  classic paid 3, passed 1, refused 3, given back 0, lost 2, still owed 0\n  credit  paid 3, passed 3, refused 1, given back 0, lost 0, still owed 0\n  refund  paid 1, passed 1, refused 3, given back 2, lost 0, still owed 0\nCoins paid = people passed + coins lost + passages still owed, on every machine.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 3\nEvents: c for a coin, p for a push, up to 20, such as c p p c c p.\nevents> p c p c p\n  classic paid 2, passed 2, refused 1, given back 0, lost 0, still owed 0\n  credit  paid 2, passed 2, refused 1, given back 0, lost 0, still owed 0\n  refund  paid 2, passed 2, refused 1, given back 0, lost 0, still owed 0\nCoins paid = people passed + coins lost + passages still owed, on every machine.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 4\nEvery sequence of 1 to 8 events, 510 in all:\n  classic loses a coin in 369 of the 510, and up to 7 coins in one.\n  credit  never loses a coin.\n  refund  never loses a coin.\nOnly classic loses coins, and exactly when a coin comes while the gate is open.\n\n1) machines  2) one machine  3) side by side  4) every sequence  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "turnstile-simulator",
      "caseId": "078-turnstile-simulator",
      "title": "Turnstile simulator"
    }
  ],
  "updated": "2026-10-17"
}
