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.
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 settingskeypad.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-simulatorwith 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]