{
  "id": "P102",
  "slug": "keypad-lock",
  "key": "P102-keypad-lock",
  "title": "Keypad lock",
  "summary": "A simulated door keypad with a clock: wrong PINs in a row lock it out for longer each time, the right PIN opens the door for ten seconds, the PIN can be changed, every event is logged, and the time to try every possible PIN is worked out.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P102 - Keypad lock\n\nA simulated keypad on a door, with a clock. Type PINs, wait, change the\nPIN and read the log. You can also work out how long trying every\npossible PIN would take. The PIN is 4821, so you can see what each answer\ndoes. It is a simulation, not a real lock.\n\n- `main.eml` - the menu, PIN input, the status line, durations, the\n  worst-case report and the settings\n- `keypad.eml` - the lock and its clock, PIN attempts, lockouts, changing\n  the PIN, the log and the worst case\n\nHow each part works:\n\n- The states are locked, open and locked out. They extend the corpus case\n  `door-lock-keypad-simulator` with a clock. Typing a PIN takes 3 seconds.\n- A wrong PIN adds to a streak. Three wrong in a row lock the keypad out:\n  30 seconds the first time, and twice as long each time after that, up to\n  16 minutes. A PIN typed while locked out is refused and not counted.\n- The right PIN opens the door for 10 seconds and clears the streak and\n  the lockouts.\n- Changing the PIN asks for the current PIN, and that counts as a try: a\n  wrong one adds to the streak, and the right one clears it. The door is\n  left as it is.\n- The worst case: every PIN of 4 to 8 digits, with the right one last.\n  That takes 3 seconds a try plus every lockout. The lockouts are summed\n  up to the cap, and every one after that is the cap itself.\n  It is set beside the same run with every lockout as short as the first.\n  If every PIN is equally likely to be the right one, the average is about\n  half of the worst case.\n- The settings: wrong PINs before a lockout (1 to 10), the first lockout\n  and the longest.\n\nWhat is checked: menu choices 1 to 7; a PIN of 4 to 8 digits; the two new\nPINs the same; a wait of 1 to 86,400 seconds; 4 to 8 digits for the worst\ncase; each setting in its range. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- three wrong PINs, a lockout of 30 seconds and a refused PIN;\n- after the wait, three more wrong PINs and a lockout of 60 seconds;\n- the right PIN and the door open, the PIN changed to 2580 while it is\n  open, the old PIN now wrong and the new one right;\n- the log;\n- every 4-digit and every 8-digit PIN: 37 days against 1.5 days for 4\n  digits;\n- new settings (5 tries, 10 to 120 seconds) and the 4-digit case again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- PINs too short, too long and with a letter, and a cancel;\n- waits of 0 and x, then a cancel;\n- changes of PIN:\n  - with the two new PINs different;\n  - with a wrong current PIN;\n  - with the new PIN the same as the old;\n  - three wrong current PINs in a row, which lock it out;\n  - one more while locked out;\n- digits 3 and 9, tries 0 and 11, a longest lockout below the first; then\n  cancels, and the log.\n\nBuilt on the verified corpus case `door-lock-keypad-simulator` (a keypad\nwhose wrong attempts in a row lead to a lockout).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P102 keypad lock: type PINs against a keypad that locks out after too\n# many wrong ones, each lockout longer than the last; change the PIN; read\n# the log; and see how long trying every PIN would take.\nimport keypad\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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef ask(prompt, low, high):\n    while True:\n        plain(input(prompt + \" (\" + str(low) + \" to \" + str(high) + \")> \")) => a\n        if a == \"\":\n            return -1\n        number(a, 6) => n\n        if n >= low and n <= high:\n            return n\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n\ndef ask_pin(prompt):\n    # 4 to 8 digits, \"\" to cancel, \"!\" after a complaint\n    plain(input(prompt + \"> \")) => p\n    if p == \"\":\n        return \"\"\n    if len(p) < 4 or len(p) > 8 or number(p, 8) == -1:\n        \"A PIN is 4 to 8 digits.\" ^0\n        return \"!\"\n    return p\n\ndef cap(t):\n    # the first letter raised\n    \"abcdefghijklmnopqrstuvwxyz\" => low\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => up\n    for i in [0:25]:\n        if len(t) > 0 and t[0] == low[i]:\n            return up[i] + t[1:len(t)]\n    return t\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef grouped(n):\n    str(n) => s\n    \"\" => out\n    0 => k\n    for i in [0:len(s) - 1]:\n        s[len(s) - 1 - i] + out => out\n        k + 1 => k\n        if k % 3 == 0 and i < len(s) - 1:\n            \",\" + out => out\n    return out\n\ndef duration(t):\n    # days, hours, minutes and seconds, leaving out what is zero\n    t % 60 => s\n    int((t - s) / 60) => m\n    m % 60 => mm\n    int((m - mm) / 60) => h\n    h % 24 => hh\n    int((h - hh) / 24) => d\n    \"\" => out\n    for part in [[d, \"day\"], [hh, \"hour\"], [mm, \"minute\"], [s, \"second\"]]:\n        if part[0] > 0:\n            if out != \"\":\n                out + \" \" => out\n            out + grouped(part[0]) + \" \" + part[1] => out\n            if part[0] != 1:\n                out + \"s\" => out\n    if out == \"\":\n        return \"0 seconds\"\n    return out\n\ndef status(lock):\n    keypad.state(lock) => st\n    \"Time \" + keypad.clock_text(lock[4]) + \" - \" => head\n    if st == \"open\":\n        (head + \"open until \" + keypad.clock_text(lock[5]) + \".\") ^0\n    elif st == \"locked out\":\n        (head + \"locked out until \" + keypad.clock_text(lock[3]) + \".\") ^0\n    elif lock[1] == 0:\n        (head + \"locked.\") ^0\n    else:\n        (head + \"locked, \" + str(lock[1]) + \" wrong in a row.\") ^0\n\ndef show_worst(cfg):\n    ask(\"digits in the PIN\", 4, 8) => d\n    if d == -1:\n        \"Cancelled.\" ^0\n        return\n    keypad.worst(cfg, d) => w\n    keypad.worst([cfg[0], cfg[1], cfg[1]], d) => flat\n    (\"Every \" + str(d) + \"-digit PIN, the right one last: \" + grouped(w[0]) + \" tries, 3 seconds each, and \" + grouped(w[1]) + \" lockouts.\") ^0\n    \"lockouts doubling up to \" + grouped(cfg[2]) + \" s\" => a\n    \"every lockout \" + grouped(cfg[1]) + \" s\" => b\n    len(a) => width\n    if len(b) > width:\n        len(b) => width\n    (\"  \" + pad(a + \":\", width + 2) + duration(w[3]) + \" in all\") ^0\n    (\"  \" + pad(b + \":\", width + 2) + duration(flat[3]) + \" in all\") ^0\n    \"If every PIN is equally likely to be the right one, the average is about half of that.\" ^0\n\ndef settings(cfg):\n    ask(\"wrong PINs in a row before a lockout\", 1, 10) => t\n    if t == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    ask(\"first lockout in seconds\", 1, 3600) => f\n    if f == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    ask(\"longest lockout in seconds\", f, 86400) => c\n    if c == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    (str(t) + \" wrong in a row lock the keypad for \" + str(f) + \" s, doubling each time up to \" + str(c) + \" s.\") ^0\n    return [t, f, c]\n\n[3, 30, 960] => cfg\nkeypad.new_lock(\"4821\") => lock\n\n\"== Keypad lock ==\" ^0\n\"A simulated keypad: the PIN is 4821. Three wrong PINs in a row lock it for 30 seconds; until the right PIN is typed, each lockout after that is twice as long as the one before, up to 16 minutes.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    status(lock)\n    \"1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        ask_pin(\"PIN\") => p\n        if p == \"\":\n            \"Cancelled.\" ^0\n        elif p != \"!\":\n            (cap(keypad.attempt(lock, p, cfg)) + \".\") ^0\n    elif choice == \"2\":\n        ask(\"seconds\", 1, 86400) => s\n        if s == -1:\n            \"Cancelled.\" ^0\n        else:\n            keypad.wait(lock, s)\n    elif choice == \"3\":\n        ask_pin(\"current PIN\") => old\n        if old == \"\":\n            \"Cancelled.\" ^0\n        elif old != \"!\":\n            ask_pin(\"new PIN\") => new\n            if new == \"\":\n                \"Cancelled.\" ^0\n            elif new != \"!\":\n                ask_pin(\"new PIN again\") => again\n                if again == \"\":\n                    \"Cancelled.\" ^0\n                elif again != \"!\":\n                    if again != new:\n                        \"The two new PINs differ; nothing changed.\" ^0\n                    else:\n                        (cap(keypad.change(lock, old, new, cfg)) + \".\") ^0\n    elif choice == \"4\":\n        if len(lock[6]) == 0:\n            \"Nothing has happened yet.\" ^0\n        for e in lock[6]:\n            (\"  \" + keypad.clock_text(e[0]) + \"  \" + e[1]) ^0\n    elif choice == \"5\":\n        show_worst(cfg)\n    elif choice == \"6\":\n        settings(cfg) => cfg\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import keypad\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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef ask(prompt, low, high):\n    while True:\n        a = plain(input(prompt + \" (\" + str(low) + \" to \" + str(high) + \")> \"))\n        if a == \"\":\n            return -1\n        n = number(a, 6)\n        if n >= low and n <= high:\n            return n\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n\ndef ask_pin(prompt):\n    p = plain(input(prompt + \"> \"))\n    if p == \"\":\n        return \"\"\n    if len(p) < 4 or len(p) > 8 or number(p, 8) == -1:\n        print(\"A PIN is 4 to 8 digits.\")\n        return \"!\"\n    return p\n\ndef cap(t):\n    low = \"abcdefghijklmnopqrstuvwxyz\"\n    up = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    for i in range(0, 26):\n        if len(t) > 0 and t[0] == low[i]:\n            return up[i] + t[1:len(t)]\n    return t\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef grouped(n):\n    s = str(n)\n    out = \"\"\n    k = 0\n    for i in range(0, len(s)):\n        out = s[len(s) - 1 - i] + out\n        k = k + 1\n        if k % 3 == 0 and i < len(s) - 1:\n            out = \",\" + out\n    return out\n\ndef duration(t):\n    s = t % 60\n    m = int((t - s) / 60)\n    mm = m % 60\n    h = int((m - mm) / 60)\n    hh = h % 24\n    d = int((h - hh) / 24)\n    out = \"\"\n    for part in [[d, \"day\"], [hh, \"hour\"], [mm, \"minute\"], [s, \"second\"]]:\n        if part[0] > 0:\n            if out != \"\":\n                out = out + \" \"\n            out = out + grouped(part[0]) + \" \" + part[1]\n            if part[0] != 1:\n                out = out + \"s\"\n    if out == \"\":\n        return \"0 seconds\"\n    return out\n\ndef status(lock):\n    st = keypad.state(lock)\n    head = \"Time \" + keypad.clock_text(lock[4]) + \" - \"\n    if st == \"open\":\n        print(head + \"open until \" + keypad.clock_text(lock[5]) + \".\")\n    elif st == \"locked out\":\n        print(head + \"locked out until \" + keypad.clock_text(lock[3]) + \".\")\n    elif lock[1] == 0:\n        print(head + \"locked.\")\n    else:\n        print(head + \"locked, \" + str(lock[1]) + \" wrong in a row.\")\n\ndef show_worst(cfg):\n    d = ask(\"digits in the PIN\", 4, 8)\n    if d == -1:\n        print(\"Cancelled.\")\n        return\n    w = keypad.worst(cfg, d)\n    flat = keypad.worst([cfg[0], cfg[1], cfg[1]], d)\n    print(\"Every \" + str(d) + \"-digit PIN, the right one last: \" + grouped(w[0]) + \" tries, 3 seconds each, and \" + grouped(w[1]) + \" lockouts.\")\n    a = \"lockouts doubling up to \" + grouped(cfg[2]) + \" s\"\n    b = \"every lockout \" + grouped(cfg[1]) + \" s\"\n    width = len(a)\n    if len(b) > width:\n        width = len(b)\n    print(\"  \" + pad(a + \":\", width + 2) + duration(w[3]) + \" in all\")\n    print(\"  \" + pad(b + \":\", width + 2) + duration(flat[3]) + \" in all\")\n    print(\"If every PIN is equally likely to be the right one, the average is about half of that.\")\n\ndef settings(cfg):\n    t = ask(\"wrong PINs in a row before a lockout\", 1, 10)\n    if t == -1:\n        print(\"Cancelled.\")\n        return cfg\n    f = ask(\"first lockout in seconds\", 1, 3600)\n    if f == -1:\n        print(\"Cancelled.\")\n        return cfg\n    c = ask(\"longest lockout in seconds\", f, 86400)\n    if c == -1:\n        print(\"Cancelled.\")\n        return cfg\n    print(str(t) + \" wrong in a row lock the keypad for \" + str(f) + \" s, doubling each time up to \" + str(c) + \" s.\")\n    return [t, f, c]\n\ncfg = [3, 30, 960]\nlock = keypad.new_lock(\"4821\")\nprint(\"== Keypad lock ==\")\nprint(\"A simulated keypad: the PIN is 4821. Three wrong PINs in a row lock it for 30 seconds; until the right PIN is typed, each lockout after that is twice as long as the one before, up to 16 minutes.\")\nrunning = True\nwhile running:\n    print(\"\")\n    status(lock)\n    print(\"1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        p = ask_pin(\"PIN\")\n        if p == \"\":\n            print(\"Cancelled.\")\n        elif p != \"!\":\n            print(cap(keypad.attempt(lock, p, cfg)) + \".\")\n    elif choice == \"2\":\n        s = ask(\"seconds\", 1, 86400)\n        if s == -1:\n            print(\"Cancelled.\")\n        else:\n            keypad.wait(lock, s)\n    elif choice == \"3\":\n        old = ask_pin(\"current PIN\")\n        if old == \"\":\n            print(\"Cancelled.\")\n        elif old != \"!\":\n            new = ask_pin(\"new PIN\")\n            if new == \"\":\n                print(\"Cancelled.\")\n            elif new != \"!\":\n                again = ask_pin(\"new PIN again\")\n                if again == \"\":\n                    print(\"Cancelled.\")\n                elif again != \"!\":\n                    if again != new:\n                        print(\"The two new PINs differ; nothing changed.\")\n                    else:\n                        print(cap(keypad.change(lock, old, new, cfg)) + \".\")\n    elif choice == \"4\":\n        if len(lock[6]) == 0:\n            print(\"Nothing has happened yet.\")\n        for e in lock[6]:\n            print(\"  \" + keypad.clock_text(e[0]) + \"  \" + e[1])\n    elif choice == \"5\":\n        show_worst(cfg)\n    elif choice == \"6\":\n        cfg = settings(cfg)\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "keypad.eml",
      "eml": "# P102 keypad lock - the corpus case door-lock-keypad-simulator's states\n# (locked, open, locked out) with a clock: a wrong PIN adds to a streak,\n# and `tries` wrong in a row lock the keypad out, each lockout twice as\n# long as the one before, up to a cap. A right PIN opens the door for 10\n# seconds and clears the streak and the lockouts. Typing a PIN takes 3\n# seconds.\n#\n# A lock is [pin, streak, lockouts, until, clock, open_until, log]; cfg is\n# [tries, first lockout, cap] in seconds.\n\ndef new_lock(pin):\n    return [pin, 0, 0, 0, 0, -1, []]\n\ndef lockout_length(k, cfg):\n    # the k-th lockout in a row: first x 2^(k-1), no longer than the cap\n    cfg[1] => s\n    for i in [2:k]:\n        if s < cfg[2]:\n            s * 2 => s\n    if s > cfg[2]:\n        cfg[2] => s\n    return s\n\ndef clock_text(t):\n    # h:mm:ss, or m:ss under an hour\n    t % 60 => s\n    (t - s) / 60 => m\n    int(m) => m\n    m % 60 => mm\n    int((m - mm) / 60) => h\n    \":\" + str(s) => tail\n    if s < 10:\n        \":0\" + str(s) => tail\n    if h == 0:\n        return str(mm) + tail\n    \":\" + str(mm) => mid\n    if mm < 10:\n        \":0\" + str(mm) => mid\n    return str(h) + mid + tail\n\ndef state(lock):\n    if lock[5] > lock[4]:\n        return \"open\"\n    if lock[3] > lock[4]:\n        return \"locked out\"\n    return \"locked\"\n\ndef note(lock, text):\n    lock[6] + [[lock[4], text]] => lock[6]\n\ndef attempt(lock, pin, cfg):\n    # one PIN typed at the current time; the clock moves on 3 seconds\n    state(lock) => st\n    \"\" => text\n    if st == \"open\":\n        \"the door is already open\" => text\n    elif st == \"locked out\":\n        \"refused: locked out until \" + clock_text(lock[3]) + \"; not counted\" => text\n    elif pin == lock[0]:\n        0 => lock[1]\n        0 => lock[2]\n        lock[4] + 3 + 10 => lock[5]\n        \"right PIN: the door opens until \" + clock_text(lock[5]) => text\n    else:\n        wrong(lock, cfg) => text\n    note(lock, \"PIN \" + pin + \": \" + text)\n    lock[4] + 3 => lock[4]\n    return text\n\ndef wrong(lock, cfg):\n    # one more wrong PIN in a row; the lockout starts once it is typed\n    lock[1] + 1 => lock[1]\n    if lock[1] >= cfg[0]:\n        lock[2] + 1 => lock[2]\n        lockout_length(lock[2], cfg) => n\n        lock[4] + 3 + n => lock[3]\n        0 => lock[1]\n        return \"wrong PIN, \" + str(cfg[0]) + \" in a row: locked out for \" + str(n) + \" s, until \" + clock_text(lock[3])\n    return \"wrong PIN, \" + str(lock[1]) + \" in a row\"\n\ndef wait(lock, s):\n    lock[4] + s => lock[4]\n    note(lock, \"waited \" + str(s) + \" s\")\n\ndef change(lock, old, new, cfg):\n    # the old PIN is checked as a try, open door or not: a wrong one adds\n    # to the streak; the right one clears it and leaves the door as it is\n    \"\" => why\n    if state(lock) == \"locked out\":\n        \"locked out until \" + clock_text(lock[3]) => why\n    elif old != lock[0]:\n        wrong(lock, cfg) => why\n    else:\n        0 => lock[1]\n        0 => lock[2]\n        if new == old:\n            \"the new PIN is the old one\" => why\n        else:\n            new => lock[0]\n    \"PIN changed\" => text\n    if why != \"\":\n        \"PIN not changed: \" + why => text\n    note(lock, text)\n    lock[4] + 3 => lock[4]\n    return text\n\ndef worst(cfg, digits):\n    # trying every PIN of this many digits, the right one last:\n    # [attempts, lockouts, seconds locked out, seconds in all]\n    1 => n\n    for i in [1:digits]:\n        n * 10 => n\n    n - 1 => wrong\n    (wrong - wrong % cfg[0]) / cfg[0] => k\n    int(k) => k\n    # the lockouts double until they reach the cap; every later one is\n    # the cap itself\n    0 => locked\n    0 => done\n    while done < k and lockout_length(done + 1, cfg) < cfg[2]:\n        locked + lockout_length(done + 1, cfg) => locked\n        done + 1 => done\n    locked + (k - done) * cfg[2] => locked\n    return [n, k, locked, n * 3 + locked]\n",
      "python": "def new_lock(pin):\n    return [pin, 0, 0, 0, 0, -1, []]\n\ndef lockout_length(k, cfg):\n    s = cfg[1]\n    for i in range(2, k+1):\n        if s < cfg[2]:\n            s = s * 2\n    if s > cfg[2]:\n        s = cfg[2]\n    return s\n\ndef clock_text(t):\n    s = t % 60\n    m = (t - s) / 60\n    m = int(m)\n    mm = m % 60\n    h = int((m - mm) / 60)\n    tail = \":\" + str(s)\n    if s < 10:\n        tail = \":0\" + str(s)\n    if h == 0:\n        return str(mm) + tail\n    mid = \":\" + str(mm)\n    if mm < 10:\n        mid = \":0\" + str(mm)\n    return str(h) + mid + tail\n\ndef state(lock):\n    if lock[5] > lock[4]:\n        return \"open\"\n    if lock[3] > lock[4]:\n        return \"locked out\"\n    return \"locked\"\n\ndef note(lock, text):\n    lock[6] = lock[6] + [[lock[4], text]]\n\ndef attempt(lock, pin, cfg):\n    st = state(lock)\n    text = \"\"\n    if st == \"open\":\n        text = \"the door is already open\"\n    elif st == \"locked out\":\n        text = \"refused: locked out until \" + clock_text(lock[3]) + \"; not counted\"\n    elif pin == lock[0]:\n        lock[1] = 0\n        lock[2] = 0\n        lock[5] = lock[4] + 3 + 10\n        text = \"right PIN: the door opens until \" + clock_text(lock[5])\n    else:\n        text = wrong(lock, cfg)\n    note(lock, \"PIN \" + pin + \": \" + text)\n    lock[4] = lock[4] + 3\n    return text\n\ndef wrong(lock, cfg):\n    lock[1] = lock[1] + 1\n    if lock[1] >= cfg[0]:\n        lock[2] = lock[2] + 1\n        n = lockout_length(lock[2], cfg)\n        lock[3] = lock[4] + 3 + n\n        lock[1] = 0\n        return \"wrong PIN, \" + str(cfg[0]) + \" in a row: locked out for \" + str(n) + \" s, until \" + clock_text(lock[3])\n    return \"wrong PIN, \" + str(lock[1]) + \" in a row\"\n\ndef wait(lock, s):\n    lock[4] = lock[4] + s\n    note(lock, \"waited \" + str(s) + \" s\")\n\ndef change(lock, old, new, cfg):\n    why = \"\"\n    if state(lock) == \"locked out\":\n        why = \"locked out until \" + clock_text(lock[3])\n    elif old != lock[0]:\n        why = wrong(lock, cfg)\n    else:\n        lock[1] = 0\n        lock[2] = 0\n        if new == old:\n            why = \"the new PIN is the old one\"\n        else:\n            lock[0] = new\n    text = \"PIN changed\"\n    if why != \"\":\n        text = \"PIN not changed: \" + why\n    note(lock, text)\n    lock[4] = lock[4] + 3\n    return text\n\ndef worst(cfg, digits):\n    n = 1\n    for i in range(1, digits+1):\n        n = n * 10\n    wrong = n - 1\n    k = (wrong - wrong % cfg[0]) / cfg[0]\n    k = int(k)\n    locked = 0\n    done = 0\n    while done < k and lockout_length(done + 1, cfg) < cfg[2]:\n        locked = locked + lockout_length(done + 1, cfg)\n        done = done + 1\n    locked = locked + (k - done) * cfg[2]\n    return [n, k, locked, n * 3 + locked]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\n12\n1\n123456789\n1\n12a4\n1\n\n2\n0\nx\n\n3\n\n3\n1111\n2222\n3333\n3\n1111\n2222\n2222\n3\n4821\n4821\n4821\n3\n0000\n1234\n1234\n3\n0000\n1234\n1234\n3\n0000\n1234\n1234\n3\n4821\n1234\n1234\n5\n3\n9\n\n6\n0\n11\n\n6\n3\n30\n10\n\n4\n7\n",
      "screen": "== Keypad lock ==\nA simulated keypad: the PIN is 4821. Three wrong PINs in a row lock it for 30 seconds; until the right PIN is typed, each lockout after that is twice as long as the one before, up to 16 minutes.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> x\nPick a number from 1 to 7.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 12\nA PIN is 4 to 8 digits.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 123456789\nA PIN is 4 to 8 digits.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 12a4\nA PIN is 4 to 8 digits.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> \nCancelled.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 2\nseconds (1 to 86400)> 0\nType a whole number from 1 to 86400.\nseconds (1 to 86400)> x\nType a whole number from 1 to 86400.\nseconds (1 to 86400)> \nCancelled.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> \nCancelled.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 1111\nnew PIN> 2222\nnew PIN again> 3333\nThe two new PINs differ; nothing changed.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 1111\nnew PIN> 2222\nnew PIN again> 2222\nPIN not changed: wrong PIN, 1 in a row.\n\nTime 0:03 - locked, 1 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 4821\nnew PIN> 4821\nnew PIN again> 4821\nPIN not changed: the new PIN is the old one.\n\nTime 0:06 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 0000\nnew PIN> 1234\nnew PIN again> 1234\nPIN not changed: wrong PIN, 1 in a row.\n\nTime 0:09 - locked, 1 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 0000\nnew PIN> 1234\nnew PIN again> 1234\nPIN not changed: wrong PIN, 2 in a row.\n\nTime 0:12 - locked, 2 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 0000\nnew PIN> 1234\nnew PIN again> 1234\nPIN not changed: wrong PIN, 3 in a row: locked out for 30 s, until 0:45.\n\nTime 0:15 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 4821\nnew PIN> 1234\nnew PIN again> 1234\nPIN not changed: locked out until 0:45.\n\nTime 0:18 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 5\ndigits in the PIN (4 to 8)> 3\nType a whole number from 4 to 8.\ndigits in the PIN (4 to 8)> 9\nType a whole number from 4 to 8.\ndigits in the PIN (4 to 8)> \nCancelled.\n\nTime 0:18 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 6\nwrong PINs in a row before a lockout (1 to 10)> 0\nType a whole number from 1 to 10.\nwrong PINs in a row before a lockout (1 to 10)> 11\nType a whole number from 1 to 10.\nwrong PINs in a row before a lockout (1 to 10)> \nCancelled.\n\nTime 0:18 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 6\nwrong PINs in a row before a lockout (1 to 10)> 3\nfirst lockout in seconds (1 to 3600)> 30\nlongest lockout in seconds (30 to 86400)> 10\nType a whole number from 30 to 86400.\nlongest lockout in seconds (30 to 86400)> \nCancelled.\n\nTime 0:18 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 4\n  0:00  PIN not changed: wrong PIN, 1 in a row\n  0:03  PIN not changed: the new PIN is the old one\n  0:06  PIN not changed: wrong PIN, 1 in a row\n  0:09  PIN not changed: wrong PIN, 2 in a row\n  0:12  PIN not changed: wrong PIN, 3 in a row: locked out for 30 s, until 0:45\n  0:15  PIN not changed: locked out until 0:45\n\nTime 0:18 - locked out until 0:45.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n1234\n1\n0000\n1\n9999\n1\n4821\n2\n30\n1\n1111\n1\n2222\n1\n3333\n2\n70\n1\n4821\n3\n4821\n2580\n2580\n2\n10\n1\n4821\n1\n2580\n4\n5\n4\n5\n8\n6\n5\n10\n120\n5\n4\n7\n",
      "screen": "== Keypad lock ==\nA simulated keypad: the PIN is 4821. Three wrong PINs in a row lock it for 30 seconds; until the right PIN is typed, each lockout after that is twice as long as the one before, up to 16 minutes.\n\nTime 0:00 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 1234\nWrong PIN, 1 in a row.\n\nTime 0:03 - locked, 1 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 0000\nWrong PIN, 2 in a row.\n\nTime 0:06 - locked, 2 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 9999\nWrong PIN, 3 in a row: locked out for 30 s, until 0:39.\n\nTime 0:09 - locked out until 0:39.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 4821\nRefused: locked out until 0:39; not counted.\n\nTime 0:12 - locked out until 0:39.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 2\nseconds (1 to 86400)> 30\n\nTime 0:42 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 1111\nWrong PIN, 1 in a row.\n\nTime 0:45 - locked, 1 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 2222\nWrong PIN, 2 in a row.\n\nTime 0:48 - locked, 2 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 3333\nWrong PIN, 3 in a row: locked out for 60 s, until 1:51.\n\nTime 0:51 - locked out until 1:51.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 2\nseconds (1 to 86400)> 70\n\nTime 2:01 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 4821\nRight PIN: the door opens until 2:14.\n\nTime 2:04 - open until 2:14.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 3\ncurrent PIN> 4821\nnew PIN> 2580\nnew PIN again> 2580\nPIN changed.\n\nTime 2:07 - open until 2:14.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 2\nseconds (1 to 86400)> 10\n\nTime 2:17 - locked.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 4821\nWrong PIN, 1 in a row.\n\nTime 2:20 - locked, 1 wrong in a row.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 1\nPIN> 2580\nRight PIN: the door opens until 2:33.\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 4\n  0:00  PIN 1234: wrong PIN, 1 in a row\n  0:03  PIN 0000: wrong PIN, 2 in a row\n  0:06  PIN 9999: wrong PIN, 3 in a row: locked out for 30 s, until 0:39\n  0:09  PIN 4821: refused: locked out until 0:39; not counted\n  0:42  waited 30 s\n  0:42  PIN 1111: wrong PIN, 1 in a row\n  0:45  PIN 2222: wrong PIN, 2 in a row\n  0:48  PIN 3333: wrong PIN, 3 in a row: locked out for 60 s, until 1:51\n  2:01  waited 70 s\n  2:01  PIN 4821: right PIN: the door opens until 2:14\n  2:04  PIN changed\n  2:17  waited 10 s\n  2:17  PIN 4821: wrong PIN, 1 in a row\n  2:20  PIN 2580: right PIN: the door opens until 2:33\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 5\ndigits in the PIN (4 to 8)> 4\nEvery 4-digit PIN, the right one last: 10,000 tries, 3 seconds each, and 3,333 lockouts.\n  lockouts doubling up to 960 s: 37 days 8 hours 3 minutes 30 seconds in all\n  every lockout 30 s:            1 day 12 hours 6 minutes 30 seconds in all\nIf every PIN is equally likely to be the right one, the average is about half of that.\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 5\ndigits in the PIN (4 to 8)> 8\nEvery 8-digit PIN, the right one last: 100,000,000 tries, 3 seconds each, and 33,333,333 lockouts.\n  lockouts doubling up to 960 s: 373,842 days 13 hours 3 minutes 30 seconds in all\n  every lockout 30 s:            15,046 days 7 hours 6 minutes 30 seconds in all\nIf every PIN is equally likely to be the right one, the average is about half of that.\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 6\nwrong PINs in a row before a lockout (1 to 10)> 5\nfirst lockout in seconds (1 to 3600)> 10\nlongest lockout in seconds (10 to 86400)> 120\n5 wrong in a row lock the keypad for 10 s, doubling each time up to 120 s.\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 5\ndigits in the PIN (4 to 8)> 4\nEvery 4-digit PIN, the right one last: 10,000 tries, 3 seconds each, and 1,999 lockouts.\n  lockouts doubling up to 120 s: 3 days 2 hours 52 minutes 30 seconds in all\n  every lockout 10 s:            13 hours 53 minutes 10 seconds in all\nIf every PIN is equally likely to be the right one, the average is about half of that.\n\nTime 2:23 - open until 2:33.\n1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "door-lock-keypad-simulator",
      "caseId": "069-door-lock-keypad-simulator",
      "title": "Door lock keypad simulator"
    }
  ],
  "updated": "2026-10-16"
}
