Project P102

Keypad lock

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.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-16

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A simulated keypad on a door, with a clock. Type PINs, wait, change the PIN and read the log. You can also work out how long trying every possible PIN would take. The PIN is 4821, so you can see what each answer does. It is a simulation, not a real lock.

  • main.eml - the menu, PIN input, the status line, durations, the worst-case report and the settings
  • keypad.eml - the lock and its clock, PIN attempts, lockouts, changing the PIN, the log and the worst case

How each part works:

  • The states are locked, open and locked out. They extend the corpus case door-lock-keypad-simulator with a clock. Typing a PIN takes 3 seconds.
  • A wrong PIN adds to a streak. Three wrong in a row lock the keypad out: 30 seconds the first time, and twice as long each time after that, up to 16 minutes. A PIN typed while locked out is refused and not counted.
  • The right PIN opens the door for 10 seconds and clears the streak and the lockouts.
  • Changing the PIN asks for the current PIN, and that counts as a try: a wrong one adds to the streak, and the right one clears it. The door is left as it is.
  • The worst case: every PIN of 4 to 8 digits, with the right one last. That takes 3 seconds a try plus every lockout. The lockouts are summed up to the cap, and every one after that is the cap itself. It is set beside the same run with every lockout as short as the first. If every PIN is equally likely to be the right one, the average is about half of the worst case.
  • The settings: wrong PINs before a lockout (1 to 10), the first lockout and the longest.

What is checked: menu choices 1 to 7; a PIN of 4 to 8 digits; the two new PINs the same; a wait of 1 to 86,400 seconds; 4 to 8 digits for the worst case; each setting in its range. An empty answer cancels.

Sessions: sessions/basic.in gives: - three wrong PINs, a lockout of 30 seconds and a refused PIN; - after the wait, three more wrong PINs and a lockout of 60 seconds; - the right PIN and the door open, the PIN changed to 2580 while it is open, the old PIN now wrong and the new one right; - the log; - every 4-digit and every 8-digit PIN: 37 days against 1.5 days for 4 digits; - new settings (5 tries, 10 to 120 seconds) and the 4-digit case again.

sessions/bad-input.in gives: - menu choices 0 and x; - PINs too short, too long and with a letter, and a cancel; - waits of 0 and x, then a cancel; - changes of PIN: - with the two new PINs different; - with a wrong current PIN; - with the new PIN the same as the old; - three wrong current PINs in a row, which lock it out; - one more while locked out; - digits 3 and 9, tries 0 and 11, a longest lockout below the first; then cancels, and the log.

Built on the verified corpus case door-lock-keypad-simulator (a keypad whose wrong attempts in a row lead to a lockout).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Keypad lock ==
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.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 0
Pick a number from 1 to 7.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> x
Pick a number from 1 to 7.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 12
A PIN is 4 to 8 digits.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 123456789
A PIN is 4 to 8 digits.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 12a4
A PIN is 4 to 8 digits.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 
Cancelled.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 2
seconds (1 to 86400)> 0
Type a whole number from 1 to 86400.
seconds (1 to 86400)> x
Type a whole number from 1 to 86400.
seconds (1 to 86400)> 
Cancelled.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 
Cancelled.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 1111
new PIN> 2222
new PIN again> 3333
The two new PINs differ; nothing changed.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 1111
new PIN> 2222
new PIN again> 2222
PIN not changed: wrong PIN, 1 in a row.

Time 0:03 - locked, 1 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 4821
new PIN> 4821
new PIN again> 4821
PIN not changed: the new PIN is the old one.

Time 0:06 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 0000
new PIN> 1234
new PIN again> 1234
PIN not changed: wrong PIN, 1 in a row.

Time 0:09 - locked, 1 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 0000
new PIN> 1234
new PIN again> 1234
PIN not changed: wrong PIN, 2 in a row.

Time 0:12 - locked, 2 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 0000
new PIN> 1234
new PIN again> 1234
PIN not changed: wrong PIN, 3 in a row: locked out for 30 s, until 0:45.

Time 0:15 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 4821
new PIN> 1234
new PIN again> 1234
PIN not changed: locked out until 0:45.

Time 0:18 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 5
digits in the PIN (4 to 8)> 3
Type a whole number from 4 to 8.
digits in the PIN (4 to 8)> 9
Type a whole number from 4 to 8.
digits in the PIN (4 to 8)> 
Cancelled.

Time 0:18 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 6
wrong PINs in a row before a lockout (1 to 10)> 0
Type a whole number from 1 to 10.
wrong PINs in a row before a lockout (1 to 10)> 11
Type a whole number from 1 to 10.
wrong PINs in a row before a lockout (1 to 10)> 
Cancelled.

Time 0:18 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 6
wrong PINs in a row before a lockout (1 to 10)> 3
first lockout in seconds (1 to 3600)> 30
longest lockout in seconds (30 to 86400)> 10
Type a whole number from 30 to 86400.
longest lockout in seconds (30 to 86400)> 
Cancelled.

Time 0:18 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 4
  0:00  PIN not changed: wrong PIN, 1 in a row
  0:03  PIN not changed: the new PIN is the old one
  0:06  PIN not changed: wrong PIN, 1 in a row
  0:09  PIN not changed: wrong PIN, 2 in a row
  0:12  PIN not changed: wrong PIN, 3 in a row: locked out for 30 s, until 0:45
  0:15  PIN not changed: locked out until 0:45

Time 0:18 - locked out until 0:45.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 7
Bye.
What was typed (59 lines)
0
x
1
12
1
123456789
1
12a4
1

2
0
x

3

3
1111
2222
3333
3
1111
2222
2222
3
4821
4821
4821
3
0000
1234
1234
3
0000
1234
1234
3
0000
1234
1234
3
4821
1234
1234
5
3
9

6
0
11

6
3
30
10

4
7

basic

interpreter: byte-equal
== Keypad lock ==
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.

Time 0:00 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 1234
Wrong PIN, 1 in a row.

Time 0:03 - locked, 1 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 0000
Wrong PIN, 2 in a row.

Time 0:06 - locked, 2 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 9999
Wrong PIN, 3 in a row: locked out for 30 s, until 0:39.

Time 0:09 - locked out until 0:39.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 4821
Refused: locked out until 0:39; not counted.

Time 0:12 - locked out until 0:39.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 2
seconds (1 to 86400)> 30

Time 0:42 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 1111
Wrong PIN, 1 in a row.

Time 0:45 - locked, 1 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 2222
Wrong PIN, 2 in a row.

Time 0:48 - locked, 2 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 3333
Wrong PIN, 3 in a row: locked out for 60 s, until 1:51.

Time 0:51 - locked out until 1:51.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 2
seconds (1 to 86400)> 70

Time 2:01 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 4821
Right PIN: the door opens until 2:14.

Time 2:04 - open until 2:14.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 3
current PIN> 4821
new PIN> 2580
new PIN again> 2580
PIN changed.

Time 2:07 - open until 2:14.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 2
seconds (1 to 86400)> 10

Time 2:17 - locked.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 4821
Wrong PIN, 1 in a row.

Time 2:20 - locked, 1 wrong in a row.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 1
PIN> 2580
Right PIN: the door opens until 2:33.

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 4
  0:00  PIN 1234: wrong PIN, 1 in a row
  0:03  PIN 0000: wrong PIN, 2 in a row
  0:06  PIN 9999: wrong PIN, 3 in a row: locked out for 30 s, until 0:39
  0:09  PIN 4821: refused: locked out until 0:39; not counted
  0:42  waited 30 s
  0:42  PIN 1111: wrong PIN, 1 in a row
  0:45  PIN 2222: wrong PIN, 2 in a row
  0:48  PIN 3333: wrong PIN, 3 in a row: locked out for 60 s, until 1:51
  2:01  waited 70 s
  2:01  PIN 4821: right PIN: the door opens until 2:14
  2:04  PIN changed
  2:17  waited 10 s
  2:17  PIN 4821: wrong PIN, 1 in a row
  2:20  PIN 2580: right PIN: the door opens until 2:33

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 5
digits in the PIN (4 to 8)> 4
Every 4-digit PIN, the right one last: 10,000 tries, 3 seconds each, and 3,333 lockouts.
  lockouts doubling up to 960 s: 37 days 8 hours 3 minutes 30 seconds in all
  every lockout 30 s:            1 day 12 hours 6 minutes 30 seconds in all
If every PIN is equally likely to be the right one, the average is about half of that.

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 5
digits in the PIN (4 to 8)> 8
Every 8-digit PIN, the right one last: 100,000,000 tries, 3 seconds each, and 33,333,333 lockouts.
  lockouts doubling up to 960 s: 373,842 days 13 hours 3 minutes 30 seconds in all
  every lockout 30 s:            15,046 days 7 hours 6 minutes 30 seconds in all
If every PIN is equally likely to be the right one, the average is about half of that.

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 6
wrong PINs in a row before a lockout (1 to 10)> 5
first lockout in seconds (1 to 3600)> 10
longest lockout in seconds (10 to 86400)> 120
5 wrong in a row lock the keypad for 10 s, doubling each time up to 120 s.

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 5
digits in the PIN (4 to 8)> 4
Every 4-digit PIN, the right one last: 10,000 tries, 3 seconds each, and 1,999 lockouts.
  lockouts doubling up to 120 s: 3 days 2 hours 52 minutes 30 seconds in all
  every lockout 10 s:            13 hours 53 minutes 10 seconds in all
If every PIN is equally likely to be the right one, the average is about half of that.

Time 2:23 - open until 2:33.
1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit
choice> 7
Bye.
What was typed (42 lines)
1
1234
1
0000
1
9999
1
4821
2
30
1
1111
1
2222
1
3333
2
70
1
4821
3
4821
2580
2580
2
10
1
4821
1
2580
4
5
4
5
8
6
5
10
120
5
4
7

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P102 keypad lock: type PINs against a keypad that locks out after too
# many wrong ones, each lockout longer than the last; change the PIN; read
# the log; and see how long trying every PIN would take.
import keypad

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def number(s, most):
    if len(s) == 0 or len(s) > most:
        return -1
    0 => n
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    return n

def ask(prompt, low, high):
    while True:
        plain(input(prompt + " (" + str(low) + " to " + str(high) + ")> ")) => a
        if a == "":
            return -1
        number(a, 6) => n
        if n >= low and n <= high:
            return n
        ("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0

def ask_pin(prompt):
    # 4 to 8 digits, "" to cancel, "!" after a complaint
    plain(input(prompt + "> ")) => p
    if p == "":
        return ""
    if len(p) < 4 or len(p) > 8 or number(p, 8) == -1:
        "A PIN is 4 to 8 digits." ^0
        return "!"
    return p

def cap(t):
    # the first letter raised
    "abcdefghijklmnopqrstuvwxyz" => low
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => up
    for i in [0:25]:
        if len(t) > 0 and t[0] == low[i]:
            return up[i] + t[1:len(t)]
    return t

def pad(s, n):
    while len(s) < n:
        s + " " => s
    return s

def grouped(n):
    str(n) => s
    "" => out
    0 => k
    for i in [0:len(s) - 1]:
        s[len(s) - 1 - i] + out => out
        k + 1 => k
        if k % 3 == 0 and i < len(s) - 1:
            "," + out => out
    return out

def duration(t):
    # days, hours, minutes and seconds, leaving out what is zero
    t % 60 => s
    int((t - s) / 60) => m
    m % 60 => mm
    int((m - mm) / 60) => h
    h % 24 => hh
    int((h - hh) / 24) => d
    "" => out
    for part in [[d, "day"], [hh, "hour"], [mm, "minute"], [s, "second"]]:
        if part[0] > 0:
            if out != "":
                out + " " => out
            out + grouped(part[0]) + " " + part[1] => out
            if part[0] != 1:
                out + "s" => out
    if out == "":
        return "0 seconds"
    return out

def status(lock):
    keypad.state(lock) => st
    "Time " + keypad.clock_text(lock[4]) + " - " => head
    if st == "open":
        (head + "open until " + keypad.clock_text(lock[5]) + ".") ^0
    elif st == "locked out":
        (head + "locked out until " + keypad.clock_text(lock[3]) + ".") ^0
    elif lock[1] == 0:
        (head + "locked.") ^0
    else:
        (head + "locked, " + str(lock[1]) + " wrong in a row.") ^0

def show_worst(cfg):
    ask("digits in the PIN", 4, 8) => d
    if d == -1:
        "Cancelled." ^0
        return
    keypad.worst(cfg, d) => w
    keypad.worst([cfg[0], cfg[1], cfg[1]], d) => flat
    ("Every " + str(d) + "-digit PIN, the right one last: " + grouped(w[0]) + " tries, 3 seconds each, and " + grouped(w[1]) + " lockouts.") ^0
    "lockouts doubling up to " + grouped(cfg[2]) + " s" => a
    "every lockout " + grouped(cfg[1]) + " s" => b
    len(a) => width
    if len(b) > width:
        len(b) => width
    ("  " + pad(a + ":", width + 2) + duration(w[3]) + " in all") ^0
    ("  " + pad(b + ":", width + 2) + duration(flat[3]) + " in all") ^0
    "If every PIN is equally likely to be the right one, the average is about half of that." ^0

def settings(cfg):
    ask("wrong PINs in a row before a lockout", 1, 10) => t
    if t == -1:
        "Cancelled." ^0
        return cfg
    ask("first lockout in seconds", 1, 3600) => f
    if f == -1:
        "Cancelled." ^0
        return cfg
    ask("longest lockout in seconds", f, 86400) => c
    if c == -1:
        "Cancelled." ^0
        return cfg
    (str(t) + " wrong in a row lock the keypad for " + str(f) + " s, doubling each time up to " + str(c) + " s.") ^0
    return [t, f, c]

[3, 30, 960] => cfg
keypad.new_lock("4821") => lock

"== Keypad lock ==" ^0
"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
True => running
while running:
    "" ^0
    status(lock)
    "1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        ask_pin("PIN") => p
        if p == "":
            "Cancelled." ^0
        elif p != "!":
            (cap(keypad.attempt(lock, p, cfg)) + ".") ^0
    elif choice == "2":
        ask("seconds", 1, 86400) => s
        if s == -1:
            "Cancelled." ^0
        else:
            keypad.wait(lock, s)
    elif choice == "3":
        ask_pin("current PIN") => old
        if old == "":
            "Cancelled." ^0
        elif old != "!":
            ask_pin("new PIN") => new
            if new == "":
                "Cancelled." ^0
            elif new != "!":
                ask_pin("new PIN again") => again
                if again == "":
                    "Cancelled." ^0
                elif again != "!":
                    if again != new:
                        "The two new PINs differ; nothing changed." ^0
                    else:
                        (cap(keypad.change(lock, old, new, cfg)) + ".") ^0
    elif choice == "4":
        if len(lock[6]) == 0:
            "Nothing has happened yet." ^0
        for e in lock[6]:
            ("  " + keypad.clock_text(e[0]) + "  " + e[1]) ^0
    elif choice == "5":
        show_worst(cfg)
    elif choice == "6":
        settings(cfg) => cfg
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
Python projection (main.py)
import keypad

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def number(s, most):
    if len(s) == 0 or len(s) > most:
        return -1
    n = 0
    for c in s:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    return n

def ask(prompt, low, high):
    while True:
        a = plain(input(prompt + " (" + str(low) + " to " + str(high) + ")> "))
        if a == "":
            return -1
        n = number(a, 6)
        if n >= low and n <= high:
            return n
        print("Type a whole number from " + str(low) + " to " + str(high) + ".")

def ask_pin(prompt):
    p = plain(input(prompt + "> "))
    if p == "":
        return ""
    if len(p) < 4 or len(p) > 8 or number(p, 8) == -1:
        print("A PIN is 4 to 8 digits.")
        return "!"
    return p

def cap(t):
    low = "abcdefghijklmnopqrstuvwxyz"
    up = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    for i in range(0, 26):
        if len(t) > 0 and t[0] == low[i]:
            return up[i] + t[1:len(t)]
    return t

def pad(s, n):
    while len(s) < n:
        s = s + " "
    return s

def grouped(n):
    s = str(n)
    out = ""
    k = 0
    for i in range(0, len(s)):
        out = s[len(s) - 1 - i] + out
        k = k + 1
        if k % 3 == 0 and i < len(s) - 1:
            out = "," + out
    return out

def duration(t):
    s = t % 60
    m = int((t - s) / 60)
    mm = m % 60
    h = int((m - mm) / 60)
    hh = h % 24
    d = int((h - hh) / 24)
    out = ""
    for part in [[d, "day"], [hh, "hour"], [mm, "minute"], [s, "second"]]:
        if part[0] > 0:
            if out != "":
                out = out + " "
            out = out + grouped(part[0]) + " " + part[1]
            if part[0] != 1:
                out = out + "s"
    if out == "":
        return "0 seconds"
    return out

def status(lock):
    st = keypad.state(lock)
    head = "Time " + keypad.clock_text(lock[4]) + " - "
    if st == "open":
        print(head + "open until " + keypad.clock_text(lock[5]) + ".")
    elif st == "locked out":
        print(head + "locked out until " + keypad.clock_text(lock[3]) + ".")
    elif lock[1] == 0:
        print(head + "locked.")
    else:
        print(head + "locked, " + str(lock[1]) + " wrong in a row.")

def show_worst(cfg):
    d = ask("digits in the PIN", 4, 8)
    if d == -1:
        print("Cancelled.")
        return
    w = keypad.worst(cfg, d)
    flat = keypad.worst([cfg[0], cfg[1], cfg[1]], d)
    print("Every " + str(d) + "-digit PIN, the right one last: " + grouped(w[0]) + " tries, 3 seconds each, and " + grouped(w[1]) + " lockouts.")
    a = "lockouts doubling up to " + grouped(cfg[2]) + " s"
    b = "every lockout " + grouped(cfg[1]) + " s"
    width = len(a)
    if len(b) > width:
        width = len(b)
    print("  " + pad(a + ":", width + 2) + duration(w[3]) + " in all")
    print("  " + pad(b + ":", width + 2) + duration(flat[3]) + " in all")
    print("If every PIN is equally likely to be the right one, the average is about half of that.")

def settings(cfg):
    t = ask("wrong PINs in a row before a lockout", 1, 10)
    if t == -1:
        print("Cancelled.")
        return cfg
    f = ask("first lockout in seconds", 1, 3600)
    if f == -1:
        print("Cancelled.")
        return cfg
    c = ask("longest lockout in seconds", f, 86400)
    if c == -1:
        print("Cancelled.")
        return cfg
    print(str(t) + " wrong in a row lock the keypad for " + str(f) + " s, doubling each time up to " + str(c) + " s.")
    return [t, f, c]

cfg = [3, 30, 960]
lock = keypad.new_lock("4821")
print("== Keypad lock ==")
print("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.")
running = True
while running:
    print("")
    status(lock)
    print("1) type a PIN  2) wait  3) change the PIN  4) log  5) try every PIN  6) settings  7) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        p = ask_pin("PIN")
        if p == "":
            print("Cancelled.")
        elif p != "!":
            print(cap(keypad.attempt(lock, p, cfg)) + ".")
    elif choice == "2":
        s = ask("seconds", 1, 86400)
        if s == -1:
            print("Cancelled.")
        else:
            keypad.wait(lock, s)
    elif choice == "3":
        old = ask_pin("current PIN")
        if old == "":
            print("Cancelled.")
        elif old != "!":
            new = ask_pin("new PIN")
            if new == "":
                print("Cancelled.")
            elif new != "!":
                again = ask_pin("new PIN again")
                if again == "":
                    print("Cancelled.")
                elif again != "!":
                    if again != new:
                        print("The two new PINs differ; nothing changed.")
                    else:
                        print(cap(keypad.change(lock, old, new, cfg)) + ".")
    elif choice == "4":
        if len(lock[6]) == 0:
            print("Nothing has happened yet.")
        for e in lock[6]:
            print("  " + keypad.clock_text(e[0]) + "  " + e[1])
    elif choice == "5":
        show_worst(cfg)
    elif choice == "6":
        cfg = settings(cfg)
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")

keypad.eml

eml
# P102 keypad lock - the corpus case door-lock-keypad-simulator's states
# (locked, open, locked out) with a clock: a wrong PIN adds to a streak,
# and `tries` wrong in a row lock the keypad out, each lockout twice as
# long as the one before, up to a cap. A right PIN opens the door for 10
# seconds and clears the streak and the lockouts. Typing a PIN takes 3
# seconds.
#
# A lock is [pin, streak, lockouts, until, clock, open_until, log]; cfg is
# [tries, first lockout, cap] in seconds.

def new_lock(pin):
    return [pin, 0, 0, 0, 0, -1, []]

def lockout_length(k, cfg):
    # the k-th lockout in a row: first x 2^(k-1), no longer than the cap
    cfg[1] => s
    for i in [2:k]:
        if s < cfg[2]:
            s * 2 => s
    if s > cfg[2]:
        cfg[2] => s
    return s

def clock_text(t):
    # h:mm:ss, or m:ss under an hour
    t % 60 => s
    (t - s) / 60 => m
    int(m) => m
    m % 60 => mm
    int((m - mm) / 60) => h
    ":" + str(s) => tail
    if s < 10:
        ":0" + str(s) => tail
    if h == 0:
        return str(mm) + tail
    ":" + str(mm) => mid
    if mm < 10:
        ":0" + str(mm) => mid
    return str(h) + mid + tail

def state(lock):
    if lock[5] > lock[4]:
        return "open"
    if lock[3] > lock[4]:
        return "locked out"
    return "locked"

def note(lock, text):
    lock[6] + [[lock[4], text]] => lock[6]

def attempt(lock, pin, cfg):
    # one PIN typed at the current time; the clock moves on 3 seconds
    state(lock) => st
    "" => text
    if st == "open":
        "the door is already open" => text
    elif st == "locked out":
        "refused: locked out until " + clock_text(lock[3]) + "; not counted" => text
    elif pin == lock[0]:
        0 => lock[1]
        0 => lock[2]
        lock[4] + 3 + 10 => lock[5]
        "right PIN: the door opens until " + clock_text(lock[5]) => text
    else:
        wrong(lock, cfg) => text
    note(lock, "PIN " + pin + ": " + text)
    lock[4] + 3 => lock[4]
    return text

def wrong(lock, cfg):
    # one more wrong PIN in a row; the lockout starts once it is typed
    lock[1] + 1 => lock[1]
    if lock[1] >= cfg[0]:
        lock[2] + 1 => lock[2]
        lockout_length(lock[2], cfg) => n
        lock[4] + 3 + n => lock[3]
        0 => lock[1]
        return "wrong PIN, " + str(cfg[0]) + " in a row: locked out for " + str(n) + " s, until " + clock_text(lock[3])
    return "wrong PIN, " + str(lock[1]) + " in a row"

def wait(lock, s):
    lock[4] + s => lock[4]
    note(lock, "waited " + str(s) + " s")

def change(lock, old, new, cfg):
    # the old PIN is checked as a try, open door or not: a wrong one adds
    # to the streak; the right one clears it and leaves the door as it is
    "" => why
    if state(lock) == "locked out":
        "locked out until " + clock_text(lock[3]) => why
    elif old != lock[0]:
        wrong(lock, cfg) => why
    else:
        0 => lock[1]
        0 => lock[2]
        if new == old:
            "the new PIN is the old one" => why
        else:
            new => lock[0]
    "PIN changed" => text
    if why != "":
        "PIN not changed: " + why => text
    note(lock, text)
    lock[4] + 3 => lock[4]
    return text

def worst(cfg, digits):
    # trying every PIN of this many digits, the right one last:
    # [attempts, lockouts, seconds locked out, seconds in all]
    1 => n
    for i in [1:digits]:
        n * 10 => n
    n - 1 => wrong
    (wrong - wrong % cfg[0]) / cfg[0] => k
    int(k) => k
    # the lockouts double until they reach the cap; every later one is
    # the cap itself
    0 => locked
    0 => done
    while done < k and lockout_length(done + 1, cfg) < cfg[2]:
        locked + lockout_length(done + 1, cfg) => locked
        done + 1 => done
    locked + (k - done) * cfg[2] => locked
    return [n, k, locked, n * 3 + locked]
Python projection (keypad.py)
def new_lock(pin):
    return [pin, 0, 0, 0, 0, -1, []]

def lockout_length(k, cfg):
    s = cfg[1]
    for i in range(2, k+1):
        if s < cfg[2]:
            s = s * 2
    if s > cfg[2]:
        s = cfg[2]
    return s

def clock_text(t):
    s = t % 60
    m = (t - s) / 60
    m = int(m)
    mm = m % 60
    h = int((m - mm) / 60)
    tail = ":" + str(s)
    if s < 10:
        tail = ":0" + str(s)
    if h == 0:
        return str(mm) + tail
    mid = ":" + str(mm)
    if mm < 10:
        mid = ":0" + str(mm)
    return str(h) + mid + tail

def state(lock):
    if lock[5] > lock[4]:
        return "open"
    if lock[3] > lock[4]:
        return "locked out"
    return "locked"

def note(lock, text):
    lock[6] = lock[6] + [[lock[4], text]]

def attempt(lock, pin, cfg):
    st = state(lock)
    text = ""
    if st == "open":
        text = "the door is already open"
    elif st == "locked out":
        text = "refused: locked out until " + clock_text(lock[3]) + "; not counted"
    elif pin == lock[0]:
        lock[1] = 0
        lock[2] = 0
        lock[5] = lock[4] + 3 + 10
        text = "right PIN: the door opens until " + clock_text(lock[5])
    else:
        text = wrong(lock, cfg)
    note(lock, "PIN " + pin + ": " + text)
    lock[4] = lock[4] + 3
    return text

def wrong(lock, cfg):
    lock[1] = lock[1] + 1
    if lock[1] >= cfg[0]:
        lock[2] = lock[2] + 1
        n = lockout_length(lock[2], cfg)
        lock[3] = lock[4] + 3 + n
        lock[1] = 0
        return "wrong PIN, " + str(cfg[0]) + " in a row: locked out for " + str(n) + " s, until " + clock_text(lock[3])
    return "wrong PIN, " + str(lock[1]) + " in a row"

def wait(lock, s):
    lock[4] = lock[4] + s
    note(lock, "waited " + str(s) + " s")

def change(lock, old, new, cfg):
    why = ""
    if state(lock) == "locked out":
        why = "locked out until " + clock_text(lock[3])
    elif old != lock[0]:
        why = wrong(lock, cfg)
    else:
        lock[1] = 0
        lock[2] = 0
        if new == old:
            why = "the new PIN is the old one"
        else:
            lock[0] = new
    text = "PIN changed"
    if why != "":
        text = "PIN not changed: " + why
    note(lock, text)
    lock[4] = lock[4] + 3
    return text

def worst(cfg, digits):
    n = 1
    for i in range(1, digits+1):
        n = n * 10
    wrong = n - 1
    k = (wrong - wrong % cfg[0]) / cfg[0]
    k = int(k)
    locked = 0
    done = 0
    while done < k and lockout_length(done + 1, cfg) < cfg[2]:
        locked = locked + lockout_length(done + 1, cfg)
        done = done + 1
    locked = locked + (k - done) * cfg[2]
    return [n, k, locked, n * 3 + locked]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.