9Volt 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 9volt-0.0.1/9Volt.egg-info/PKG-INFO +6 -0
- 9volt-0.0.1/9Volt.egg-info/SOURCES.txt +23 -0
- 9volt-0.0.1/9Volt.egg-info/dependency_links.txt +1 -0
- 9volt-0.0.1/9Volt.egg-info/entry_points.txt +2 -0
- 9volt-0.0.1/9Volt.egg-info/requires.txt +1 -0
- 9volt-0.0.1/9Volt.egg-info/top_level.txt +2 -0
- 9volt-0.0.1/HaltingMachine.py +7 -0
- 9volt-0.0.1/Machine/Spacewalk.py +50 -0
- 9volt-0.0.1/Machine/__init__.py +1 -0
- 9volt-0.0.1/Machine/field.py +75 -0
- 9volt-0.0.1/Machine/inverter.py +14 -0
- 9volt-0.0.1/Machine/observer.py +134 -0
- 9volt-0.0.1/PKG-INFO +6 -0
- 9volt-0.0.1/pyproject.toml +17 -0
- 9volt-0.0.1/setup.cfg +4 -0
- 9volt-0.0.1/tests/test_display_feet.py +85 -0
- 9volt-0.0.1/tests/test_genesis_default.py +22 -0
- 9volt-0.0.1/tests/test_observer_wire.py +139 -0
- 9volt-0.0.1/tests/test_trinity.py +66 -0
- 9volt-0.0.1/tests/test_v22_timing.py +31 -0
- 9volt-0.0.1/tests/test_v23_layout.py +43 -0
- 9volt-0.0.1/tests/test_v24_cadence.py +52 -0
- 9volt-0.0.1/tests/test_v25_toggle_and_pacing.py +48 -0
- 9volt-0.0.1/tests/test_v26_pingpong.py +46 -0
- 9volt-0.0.1/tests/test_v27_three_seconds.py +58 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
HaltingMachine.py
|
|
2
|
+
pyproject.toml
|
|
3
|
+
9Volt.egg-info/PKG-INFO
|
|
4
|
+
9Volt.egg-info/SOURCES.txt
|
|
5
|
+
9Volt.egg-info/dependency_links.txt
|
|
6
|
+
9Volt.egg-info/entry_points.txt
|
|
7
|
+
9Volt.egg-info/requires.txt
|
|
8
|
+
9Volt.egg-info/top_level.txt
|
|
9
|
+
Machine/Spacewalk.py
|
|
10
|
+
Machine/__init__.py
|
|
11
|
+
Machine/field.py
|
|
12
|
+
Machine/inverter.py
|
|
13
|
+
Machine/observer.py
|
|
14
|
+
tests/test_display_feet.py
|
|
15
|
+
tests/test_genesis_default.py
|
|
16
|
+
tests/test_observer_wire.py
|
|
17
|
+
tests/test_trinity.py
|
|
18
|
+
tests/test_v22_timing.py
|
|
19
|
+
tests/test_v23_layout.py
|
|
20
|
+
tests/test_v24_cadence.py
|
|
21
|
+
tests/test_v25_toggle_and_pacing.py
|
|
22
|
+
tests/test_v26_pingpong.py
|
|
23
|
+
tests/test_v27_three_seconds.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wcwidth>=0.2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
from Machine.field import main
|
|
3
|
+
if __name__ == "__main__":
|
|
4
|
+
if sys.argv[1:] not in ([], ["Spacewalk.py"], ["Spacewalk"]):
|
|
5
|
+
raise SystemExit("Usage: python HaltingMachine.py [Spacewalk.py]")
|
|
6
|
+
os.environ["HALTING_SPACEWALK"] = "1" if sys.argv[1:] else "0"
|
|
7
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Change only this value to adjust Spacewalk's proposal and doorway pace.
|
|
2
|
+
INTERVAL = 3.0
|
|
3
|
+
|
|
4
|
+
POSITIVE = ('000', '100', '110', '111')
|
|
5
|
+
NEGATIVE = ('000', '001', '011', '111')
|
|
6
|
+
LATERAL = frozenset(('010', '101'))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Spacewalk:
|
|
10
|
+
def __init__(self, inverted=False):
|
|
11
|
+
self.inverted = inverted
|
|
12
|
+
self.state = self.previous = '000'
|
|
13
|
+
self.doorway_at = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def lane(self):
|
|
17
|
+
return NEGATIVE if self.inverted else POSITIVE
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def shell(self):
|
|
21
|
+
return self.state.count('1')
|
|
22
|
+
|
|
23
|
+
def propose(self):
|
|
24
|
+
position = self.lane.index(self.state)
|
|
25
|
+
return self.lane[position + 1] if position < 3 else None
|
|
26
|
+
|
|
27
|
+
def accept(self, incoming, now=0):
|
|
28
|
+
successor = self.propose()
|
|
29
|
+
# ================= LINCHPIN ================= #
|
|
30
|
+
if incoming != successor or successor is None:
|
|
31
|
+
return None
|
|
32
|
+
# ============================================ #
|
|
33
|
+
assert sum(a != b for a, b in zip(self.state, successor)) == 1
|
|
34
|
+
self.previous, self.state = self.state, successor
|
|
35
|
+
if successor == '111': self.doorway_at = now + INTERVAL
|
|
36
|
+
return successor
|
|
37
|
+
|
|
38
|
+
def doorway(self):
|
|
39
|
+
assert self.state == '111'
|
|
40
|
+
self.previous = self.state
|
|
41
|
+
self.inverted = not self.inverted
|
|
42
|
+
self.state, self.doorway_at = '000', None
|
|
43
|
+
return self.state
|
|
44
|
+
|
|
45
|
+
def display(self):
|
|
46
|
+
emblems = ('🪨', '✂️', '📜', '🪨') if self.inverted else ('🪨', '📜', '✂️', '🪨')
|
|
47
|
+
icons = ''.join(glyph + (' ' if glyph == '✂️' else '') for glyph in emblems)
|
|
48
|
+
pointer = ' ' * (4 + 2 * self.shell)
|
|
49
|
+
left, right = (self.state, self.previous) if self.state == '000' and self.previous == '111' else (self.previous, self.state)
|
|
50
|
+
return pointer + '🔻', f'{left} {icons} {right}', pointer + '🔺'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HaltingMachine: observer, inverter, and field."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os, platform, random, shlex, shutil, signal, subprocess, sys, termios, tty
|
|
2
|
+
from wcwidth import wcswidth
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def paint():
|
|
6
|
+
sys.stdout.write("\033[40m\033[97m\033[2J\033[H\033[?25l")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clear():
|
|
10
|
+
sys.stdout.write("\033[?25h\033[0m\033[2J\033[H"); sys.stdout.flush()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def center(text):
|
|
14
|
+
return " " * max(0, (os.get_terminal_size().columns - wcswidth(text)) // 2) + text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def choose(label, lo, hi, value):
|
|
18
|
+
old = termios.tcgetattr(sys.stdin); tty.setcbreak(sys.stdin.fileno())
|
|
19
|
+
try:
|
|
20
|
+
while True:
|
|
21
|
+
paint(); print("\n\n" + center("HaltingMachine"), "\n", center(label), "", center(str(value)), sep="\n", flush=True)
|
|
22
|
+
key = os.read(sys.stdin.fileno(), 3)
|
|
23
|
+
if key in (b"\r", b"\n"): return value
|
|
24
|
+
if key == b"\x1b[A": value = min(hi, value + 1)
|
|
25
|
+
elif key == b"\x1b[B": value = max(lo, value - 1)
|
|
26
|
+
finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def terminal_command(cmd):
|
|
30
|
+
names = ("kitty", "konsole", "gnome-terminal", "xfce4-terminal", "alacritty")
|
|
31
|
+
terminal = next((shutil.which(x) for x in names if shutil.which(x)), None)
|
|
32
|
+
terminal = terminal or shutil.which(os.environ.get("TERMINAL", ""))
|
|
33
|
+
terminal = terminal or shutil.which("x-terminal-emulator") or shutil.which("xterm")
|
|
34
|
+
if not terminal: raise SystemExit("No terminal emulator found")
|
|
35
|
+
name = os.path.basename(os.path.realpath(terminal))
|
|
36
|
+
if name == "gnome-terminal": return [terminal, "--", *cmd]
|
|
37
|
+
if name == "xfce4-terminal": return [terminal, "--command", shlex.join(cmd)]
|
|
38
|
+
if name == "alacritty": return [terminal, "-T", "HaltingMachine", "-e", *cmd]
|
|
39
|
+
if name == "kitty": return [terminal, "--title", "HaltingMachine", *cmd]
|
|
40
|
+
return [terminal, "-e", *cmd]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def spawn(module, index, total):
|
|
44
|
+
cmd = [sys.executable, "-m", module, str(index), str(total)]
|
|
45
|
+
env = os.environ.copy(); env["PYTHONIOENCODING"] = "utf-8"
|
|
46
|
+
if platform.system() == "Darwin":
|
|
47
|
+
line = shlex.join(cmd).replace('"', '\\"')
|
|
48
|
+
subprocess.Popen(["osascript", "-e", f'tell application "Terminal" to do script "{line}"'])
|
|
49
|
+
else: subprocess.Popen(terminal_command(cmd), env=env)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
old_handlers = {s: signal.getsignal(s) for s in (signal.SIGTSTP, signal.SIGQUIT)}
|
|
54
|
+
for s in old_handlers: signal.signal(s, signal.SIG_IGN)
|
|
55
|
+
try:
|
|
56
|
+
total = choose("OBSERVERS", 1, 10, 3)
|
|
57
|
+
inverted = choose("INVERTERS", 0, total, min(1, total))
|
|
58
|
+
first = total - inverted
|
|
59
|
+
index = random.randrange(total)
|
|
60
|
+
for i in range(total):
|
|
61
|
+
if i != index:
|
|
62
|
+
spawn("Machine.inverter" if i >= first else "Machine.observer", i, total)
|
|
63
|
+
if index >= first:
|
|
64
|
+
from .inverter import run
|
|
65
|
+
else:
|
|
66
|
+
from .observer import run
|
|
67
|
+
run(index, total)
|
|
68
|
+
except (KeyboardInterrupt, EOFError):
|
|
69
|
+
pass
|
|
70
|
+
finally:
|
|
71
|
+
for s, h in old_handlers.items(): signal.signal(s, h)
|
|
72
|
+
clear()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__": main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .observer import REVERSE, run as observe
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run(index, total):
|
|
6
|
+
observe(index, total, step=REVERSE, invert_controls=True, invert_mode=True)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
index, total = map(int, sys.argv[1:3])
|
|
11
|
+
run(index, total)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__": main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os, select, signal, socket, sys, termios, time, tty
|
|
2
|
+
from wcwidth import wcswidth
|
|
3
|
+
|
|
4
|
+
ROCK, PAPER, SCISSORS = "🪨", "📜", "✂️"
|
|
5
|
+
FORWARD = {ROCK: PAPER, PAPER: SCISSORS, SCISSORS: ROCK}
|
|
6
|
+
REVERSE = {v: k for k, v in FORWARD.items()}
|
|
7
|
+
ART = {ROCK: ROCK * 4, PAPER: PAPER * 4, SCISSORS: "✂️ " * 4}
|
|
8
|
+
GREEK = ("Γ", "Δ", "Ε", "Θ", "Λ", "Π", "Σ", "Φ", "Ψ", "Ω")
|
|
9
|
+
BASE = 43120
|
|
10
|
+
|
|
11
|
+
def paint():
|
|
12
|
+
sys.stdout.write("\033[40m\033[97m\033[2J\033[H\033[?25l")
|
|
13
|
+
|
|
14
|
+
def clear():
|
|
15
|
+
sys.stdout.write("\033[?25h\033[0m\033[2J\033[H"); sys.stdout.flush()
|
|
16
|
+
|
|
17
|
+
def center(text, width=None):
|
|
18
|
+
width = wcswidth(text) if width is None else width
|
|
19
|
+
return " " * max(0, (os.get_terminal_size().columns - width) // 2) + text
|
|
20
|
+
|
|
21
|
+
def draw(identity, mode, state, inverted=False, genesis=None, cube=None):
|
|
22
|
+
paint(); print("\n\n\n\n" + center(f"{identity} HaltingMachine {identity}"), "\n")
|
|
23
|
+
if genesis is not None:
|
|
24
|
+
print(center("GENESIS"), "\n", center(genesis), flush=True); return
|
|
25
|
+
face = "🙃" if inverted else "🙂"
|
|
26
|
+
halt = f"{face}HALT{face}" if mode == 0 else "HALT"
|
|
27
|
+
repeat = f"{face}REPEAT{face}" if mode else "REPEAT"
|
|
28
|
+
visuals = cube.display() if cube else (ART[state],)
|
|
29
|
+
positions = [center(text, 16 if cube else 8) for text in visuals]
|
|
30
|
+
if cube:
|
|
31
|
+
print(center(halt), center(repeat), *positions, ' ' + center("SPACE TO EXIT"), sep="\n", flush=True)
|
|
32
|
+
else:
|
|
33
|
+
print(center(halt), center(repeat), '', *positions, '', ' ' + center("SPACE TO EXIT"), sep="\n", flush=True)
|
|
34
|
+
|
|
35
|
+
def send(sock, total, payload, skip):
|
|
36
|
+
data = payload.encode("utf-8")
|
|
37
|
+
for i in range(total):
|
|
38
|
+
if i != skip:
|
|
39
|
+
try: sock.sendto(data, ("127.0.0.1", BASE + i))
|
|
40
|
+
except OSError: pass
|
|
41
|
+
|
|
42
|
+
def run(index, total, step=FORWARD, invert_controls=False, invert_mode=False):
|
|
43
|
+
cube = None
|
|
44
|
+
if os.environ.get("HALTING_SPACEWALK") == "1":
|
|
45
|
+
from .Spacewalk import Spacewalk, INTERVAL
|
|
46
|
+
cube = Spacewalk(invert_mode)
|
|
47
|
+
interval = INTERVAL if cube else 1.0
|
|
48
|
+
identity = GREEK[index]
|
|
49
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
50
|
+
sock.setblocking(False); sock.bind(("127.0.0.1", BASE + index))
|
|
51
|
+
seen, state, mode = {index}, ROCK, 0
|
|
52
|
+
def show(genesis=None):
|
|
53
|
+
draw(identity, mode, state, cube.inverted if cube else invert_controls, genesis, cube)
|
|
54
|
+
old = termios.tcgetattr(sys.stdin); tty.setcbreak(sys.stdin.fileno())
|
|
55
|
+
ignored = (signal.SIGTSTP, signal.SIGQUIT)
|
|
56
|
+
handlers = {s: signal.getsignal(s) for s in ignored}
|
|
57
|
+
for s in ignored: signal.signal(s, signal.SIG_IGN)
|
|
58
|
+
hello_at = ready_at = 0.0; emit_at = None if cube else 0.0; shown_seen = -1
|
|
59
|
+
try:
|
|
60
|
+
while True:
|
|
61
|
+
now = time.monotonic()
|
|
62
|
+
if cube and cube.doorway_at is not None and now >= cube.doorway_at:
|
|
63
|
+
cube.doorway()
|
|
64
|
+
mode = int(cube.inverted)
|
|
65
|
+
emit_at = None # Doorway resets to an effectively HALTed relay.
|
|
66
|
+
show()
|
|
67
|
+
if now >= hello_at:
|
|
68
|
+
send(sock, total, f"H{index}", index); hello_at = now + .25
|
|
69
|
+
while True:
|
|
70
|
+
try: msg = sock.recvfrom(64)[0].decode("utf-8")
|
|
71
|
+
except BlockingIOError: break
|
|
72
|
+
if msg.startswith("H"):
|
|
73
|
+
seen.add(int(msg[1:]))
|
|
74
|
+
elif cube or now >= ready_at:
|
|
75
|
+
if cube:
|
|
76
|
+
admitted = cube.accept(msg, now)
|
|
77
|
+
active = (1 - mode) if cube.inverted else mode
|
|
78
|
+
if admitted is not None and active:
|
|
79
|
+
emit_at = now + interval # Admission rephases the repeater.
|
|
80
|
+
elif (admitted is None and active and emit_at is None
|
|
81
|
+
and len(msg) == 3 and set(msg) <= {'0', '1'}):
|
|
82
|
+
emit_at = now + interval # One delayed reply to a rejected Trinity.
|
|
83
|
+
else:
|
|
84
|
+
# ================= LINCHPIN ================= #
|
|
85
|
+
admissible = msg == step[state]
|
|
86
|
+
# ============================================ #
|
|
87
|
+
admitted = msg if admissible else None
|
|
88
|
+
if admitted is not None:
|
|
89
|
+
if not cube:
|
|
90
|
+
state = admitted
|
|
91
|
+
ready_at = emit_at = now + 1.0
|
|
92
|
+
send(sock, total, admitted, index)
|
|
93
|
+
show()
|
|
94
|
+
if len(seen) < total:
|
|
95
|
+
if len(seen) != shown_seen:
|
|
96
|
+
show(f"{len(seen)} / {total}"); shown_seen = len(seen)
|
|
97
|
+
time.sleep(.05); continue
|
|
98
|
+
if ready_at == 0.0:
|
|
99
|
+
show(); ready_at = now + interval
|
|
100
|
+
if not cube: emit_at = ready_at
|
|
101
|
+
elif cube and cube.inverted: emit_at = emit_at or ready_at
|
|
102
|
+
active = (1 - mode) if (cube.inverted if cube else invert_mode) else mode
|
|
103
|
+
if cube:
|
|
104
|
+
if active and emit_at is not None and now >= emit_at:
|
|
105
|
+
proposal = cube.propose()
|
|
106
|
+
if proposal is not None: send(sock, total, proposal, index)
|
|
107
|
+
emit_at = None # One proposal per encounter; listen for another.
|
|
108
|
+
elif active and now >= ready_at and now >= emit_at:
|
|
109
|
+
send(sock, total, step[state], index)
|
|
110
|
+
emit_at = now + 1.0
|
|
111
|
+
ready, _, _ = select.select([sys.stdin], [], [], .05)
|
|
112
|
+
if ready:
|
|
113
|
+
key = os.read(sys.stdin.fileno(), 3)
|
|
114
|
+
if key == b" ": break
|
|
115
|
+
if key in (b"\x1b[A", b"\x1b[B"):
|
|
116
|
+
mode = 1 - mode
|
|
117
|
+
if cube:
|
|
118
|
+
active = (1 - mode) if cube.inverted else mode
|
|
119
|
+
emit_at = time.monotonic() + interval if active else None
|
|
120
|
+
else:
|
|
121
|
+
emit_at = time.monotonic() + 1.0
|
|
122
|
+
show()
|
|
123
|
+
except (KeyboardInterrupt, EOFError):
|
|
124
|
+
pass
|
|
125
|
+
finally:
|
|
126
|
+
for s, h in handlers.items(): signal.signal(s, h)
|
|
127
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
128
|
+
clear(); sock.close()
|
|
129
|
+
|
|
130
|
+
def main():
|
|
131
|
+
index, total = map(int, sys.argv[1:3])
|
|
132
|
+
run(index, total)
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__": main()
|
9volt-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "9Volt"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A tiny Oblivious Compute halting-machine specimen"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["wcwidth>=0.2"]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
HaltingMachine = "HaltingMachine:main"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
py-modules = ["HaltingMachine"]
|
|
17
|
+
packages = ["Machine"]
|
9volt-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Spacewalk's two pointers flank a centered four-shell icon strip."""
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
from Machine.Spacewalk import Spacewalk, POSITIVE, NEGATIVE
|
|
7
|
+
from Machine import observer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DisplayFeetTests(unittest.TestCase):
|
|
11
|
+
def test_previous_and_current_vertices_flank_four_icons(self):
|
|
12
|
+
for inverted, lane in ((False, POSITIVE), (True, NEGATIVE)):
|
|
13
|
+
node = Spacewalk(inverted)
|
|
14
|
+
for i in range(4):
|
|
15
|
+
top, middle, bottom = node.display()
|
|
16
|
+
self.assertEqual(node.previous, lane[max(0, i - 1)])
|
|
17
|
+
self.assertEqual(node.state, lane[i])
|
|
18
|
+
self.assertEqual(middle[:4], node.previous + ' ')
|
|
19
|
+
self.assertTrue(middle.endswith(' ' + node.state))
|
|
20
|
+
self.assertEqual(middle[4:-4], '🪨✂️ 📜🪨' if inverted else '🪨📜✂️ 🪨')
|
|
21
|
+
self.assertEqual(top, ' ' * (4 + 2 * i) + '🔻')
|
|
22
|
+
self.assertEqual(bottom, ' ' * (4 + 2 * i) + '🔺')
|
|
23
|
+
if i < 3:
|
|
24
|
+
self.assertEqual(node.accept(node.propose()), lane[i + 1])
|
|
25
|
+
|
|
26
|
+
def test_doorway_reverses_icons_and_retains_last_vertex(self):
|
|
27
|
+
for old_inverted in (False, True):
|
|
28
|
+
node = Spacewalk(old_inverted)
|
|
29
|
+
for _ in range(3):
|
|
30
|
+
node.accept(node.propose())
|
|
31
|
+
self.assertEqual(node.state, '111')
|
|
32
|
+
self.assertEqual(node.doorway(), '000')
|
|
33
|
+
self.assertEqual(node.previous, '111')
|
|
34
|
+
self.assertEqual(node.inverted, not old_inverted)
|
|
35
|
+
top, middle, bottom = node.display()
|
|
36
|
+
self.assertTrue(middle.startswith('000 '))
|
|
37
|
+
self.assertTrue(middle.endswith(' 111'))
|
|
38
|
+
self.assertEqual(top, ' 🔻')
|
|
39
|
+
self.assertEqual(bottom, ' 🔺')
|
|
40
|
+
self.assertEqual(node.lane, POSITIVE if old_inverted else NEGATIVE)
|
|
41
|
+
|
|
42
|
+
def test_doorway_labels_reorient_without_mutating_internal_state(self):
|
|
43
|
+
for starting_inverted, first_step in ((False, '001'), (True, '100')):
|
|
44
|
+
node = Spacewalk(starting_inverted)
|
|
45
|
+
for _ in range(3): node.accept(node.propose())
|
|
46
|
+
node.doorway()
|
|
47
|
+
self.assertEqual((node.previous, node.state), ('111', '000'))
|
|
48
|
+
self.assertEqual(node.display()[1][:4], '000 ')
|
|
49
|
+
self.assertTrue(node.display()[1].endswith(' 111'))
|
|
50
|
+
self.assertEqual(node.accept(first_step), first_step)
|
|
51
|
+
self.assertTrue(node.display()[1].startswith('000 '))
|
|
52
|
+
self.assertTrue(node.display()[1].endswith(' ' + first_step))
|
|
53
|
+
|
|
54
|
+
def test_rejection_does_not_change_display(self):
|
|
55
|
+
node = Spacewalk()
|
|
56
|
+
first = node.display()
|
|
57
|
+
self.assertIsNone(node.accept('101'))
|
|
58
|
+
self.assertEqual(node.display(), first)
|
|
59
|
+
|
|
60
|
+
def test_terminal_alignment_in_both_orientations(self):
|
|
61
|
+
for inverted in (False, True):
|
|
62
|
+
node = Spacewalk(inverted)
|
|
63
|
+
for shell in range(4):
|
|
64
|
+
out = io.StringIO()
|
|
65
|
+
with patch.object(observer.sys, 'stdout', out), \
|
|
66
|
+
patch.object(observer, 'paint'), \
|
|
67
|
+
patch.object(observer.os, 'get_terminal_size',
|
|
68
|
+
return_value=os.terminal_size((80, 24))):
|
|
69
|
+
observer.draw('Γ', 1 if inverted else 0, '🪨', inverted, cube=node)
|
|
70
|
+
lines = out.getvalue().splitlines()
|
|
71
|
+
icons = next(line for line in lines if '🪨' in line)
|
|
72
|
+
upper = next(line for line in lines if '🔻' in line)
|
|
73
|
+
lower = next(line for line in lines if '🔺' in line)
|
|
74
|
+
# Four columns for the previous Trinity + a space.
|
|
75
|
+
self.assertEqual(len(icons) - len(icons.lstrip(' ')), 32)
|
|
76
|
+
self.assertEqual(icons.index('🪨'), 36)
|
|
77
|
+
self.assertEqual(upper.index('🔻'), 36 + 2 * shell)
|
|
78
|
+
self.assertEqual(lower.index('🔺'), 36 + 2 * shell)
|
|
79
|
+
self.assertTrue(icons.rstrip().endswith(' ' + node.state))
|
|
80
|
+
if shell < 3:
|
|
81
|
+
node.accept(node.propose())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == '__main__':
|
|
85
|
+
unittest.main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Confirm the Genesis screen defaults, without launching real terminals."""
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from Machine import field
|
|
5
|
+
|
|
6
|
+
class GenesisDefaultTest(unittest.TestCase):
|
|
7
|
+
def test_three_observers_one_inverter(self):
|
|
8
|
+
selections = []
|
|
9
|
+
def fake_choose(label, low, high, default):
|
|
10
|
+
selections.append((label, low, high, default))
|
|
11
|
+
return default
|
|
12
|
+
with patch.object(field, 'choose', side_effect=fake_choose), \
|
|
13
|
+
patch.object(field, 'spawn'), \
|
|
14
|
+
patch.object(field.random, 'randrange', return_value=0), \
|
|
15
|
+
patch('Machine.observer.run'), \
|
|
16
|
+
patch.object(field, 'clear'), \
|
|
17
|
+
patch.object(field.signal, 'signal'):
|
|
18
|
+
field.main()
|
|
19
|
+
self.assertEqual(selections[0], ('OBSERVERS', 1, 10, 3))
|
|
20
|
+
self.assertEqual(selections[1], ('INVERTERS', 0, 3, 1))
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__': unittest.main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""V17: only expected-next admissions, repeating proposals, quiet doorways."""
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from Machine import observer, Spacewalk as cube_module
|
|
5
|
+
|
|
6
|
+
class FakeSocket:
|
|
7
|
+
def __init__(self, clock, incoming):
|
|
8
|
+
self.clock, self.incoming, self.delivered = clock, incoming, set()
|
|
9
|
+
def setblocking(self, *_): pass
|
|
10
|
+
def bind(self, *_): pass
|
|
11
|
+
def close(self): pass
|
|
12
|
+
def recvfrom(self, *_):
|
|
13
|
+
t = self.clock[0]
|
|
14
|
+
if t in self.incoming and t not in self.delivered:
|
|
15
|
+
self.delivered.add(t)
|
|
16
|
+
return self.incoming[t].encode(), ('127.0.0.1', 9999)
|
|
17
|
+
raise BlockingIOError
|
|
18
|
+
|
|
19
|
+
class WireTests(unittest.TestCase):
|
|
20
|
+
def execute(self, inverted=False, hypercube=True, inputs=None, stop=12,
|
|
21
|
+
arrow_at=None, arrows=None, interval=1.0):
|
|
22
|
+
clock, packets, snapshots, modes = [0], [], [], []
|
|
23
|
+
sock = FakeSocket(clock, {2:'H1', **(inputs or {})})
|
|
24
|
+
def tick():
|
|
25
|
+
clock[0] += 1
|
|
26
|
+
return float(clock[0])
|
|
27
|
+
def capture(_sock, _total, payload, _index):
|
|
28
|
+
packets.append((clock[0], payload))
|
|
29
|
+
def draw(_identity, mode, _state, face, genesis=None, cube=None):
|
|
30
|
+
if genesis is None:
|
|
31
|
+
modes.append((clock[0], mode, face))
|
|
32
|
+
if cube and genesis is None:
|
|
33
|
+
snapshots.append((clock[0], cube.state, cube.inverted, mode, face, cube.previous))
|
|
34
|
+
keys = arrows or ({arrow_at: b'\x1b[B'} if arrow_at is not None else {})
|
|
35
|
+
def ready(*_):
|
|
36
|
+
return ([0], [], []) if clock[0] >= stop or clock[0] in keys else ([], [], [])
|
|
37
|
+
def read(*_):
|
|
38
|
+
return keys.get(clock[0], b' ')
|
|
39
|
+
env = {'HALTING_SPACEWALK':'1'} if hypercube else {'HALTING_SPACEWALK':'0'}
|
|
40
|
+
# Older fixtures exercise the V26 one-second timing. Pass interval=None
|
|
41
|
+
# to test V27's actual configured three-second interval instead.
|
|
42
|
+
step = cube_module.INTERVAL if interval is None else interval
|
|
43
|
+
with patch.object(cube_module, 'INTERVAL', step), patch.dict('os.environ', env), \
|
|
44
|
+
patch.object(observer.socket, 'socket', return_value=sock), \
|
|
45
|
+
patch.object(observer.time, 'monotonic', side_effect=tick), \
|
|
46
|
+
patch.object(observer.select, 'select', side_effect=ready), \
|
|
47
|
+
patch.object(observer.termios, 'tcgetattr', return_value=[0]*7), \
|
|
48
|
+
patch.object(observer.termios, 'tcsetattr'), \
|
|
49
|
+
patch.object(observer.tty, 'setcbreak'), \
|
|
50
|
+
patch.object(observer.os, 'read', side_effect=read), \
|
|
51
|
+
patch.object(observer, 'draw', side_effect=draw), \
|
|
52
|
+
patch.object(observer, 'clear'), \
|
|
53
|
+
patch.object(observer, 'send', side_effect=capture), \
|
|
54
|
+
patch.object(observer.signal, 'signal'):
|
|
55
|
+
observer.run(0,2,step=observer.REVERSE if inverted else observer.FORWARD,
|
|
56
|
+
invert_controls=inverted, invert_mode=inverted)
|
|
57
|
+
self.modes = modes
|
|
58
|
+
return [x for x in packets if not x[1].startswith('H')], snapshots
|
|
59
|
+
|
|
60
|
+
def test_original_healthy_wraps_and_physically_repeats_to_halt(self):
|
|
61
|
+
packets, snaps = self.execute(inputs={4:'100',6:'110',8:'111'})
|
|
62
|
+
self.assertEqual(packets[:3], [(4,'100'),(6,'110'),(8,'111')])
|
|
63
|
+
self.assertNotIn((8,'000'), packets) # No extra same-window projection
|
|
64
|
+
self.assertNotIn((9,'000'), packets) # Quiet entry to inverted cube
|
|
65
|
+
self.assertNotIn((10,'000'), packets) # No periodic repeat after crossing
|
|
66
|
+
self.assertIn((9,'000',True,1,True,'111'), snaps) # Inverted wiring: REPEAT is HALT
|
|
67
|
+
self.assertEqual(snaps[-1][2], True)
|
|
68
|
+
|
|
69
|
+
def test_original_inverter_unwraps_to_true_healthy_halt(self):
|
|
70
|
+
packets, snaps = self.execute(inverted=True, inputs={4:'001',6:'011',8:'111'})
|
|
71
|
+
self.assertIn((4,'001'), packets)
|
|
72
|
+
self.assertIn((6,'011'), packets)
|
|
73
|
+
self.assertIn((8,'111'), packets)
|
|
74
|
+
self.assertNotIn((9,'000'), packets)
|
|
75
|
+
self.assertNotIn((10,'000'), packets) # Original inverter now effectively halted
|
|
76
|
+
self.assertIn((9,'000',False,0,False,'111'), snaps) # Actual physical HALT
|
|
77
|
+
|
|
78
|
+
def test_manual_repeat_after_doorway_uses_current_cube_wiring(self):
|
|
79
|
+
for original_inverter, path in ((False, ('000','100','110','111')),
|
|
80
|
+
(True, ('000','001','011','111'))):
|
|
81
|
+
data = {4:path[1], 6:path[2], 8:path[3]}
|
|
82
|
+
packets, snaps = self.execute(inverted=original_inverter, inputs=data,
|
|
83
|
+
arrow_at=10, stop=12)
|
|
84
|
+
self.assertNotIn((9,'000'), packets) # Both doorway directions are silent
|
|
85
|
+
target = '100' if original_inverter else '001'
|
|
86
|
+
self.assertTrue(any(t>=11 and word==target for t,word in packets))
|
|
87
|
+
# Physical down => effective REPEAT using the *current* wrapper.
|
|
88
|
+
self.assertTrue(any(s[1:] == ('000', not original_inverter,
|
|
89
|
+
1 if original_inverter else 0,
|
|
90
|
+
not original_inverter, '111') for s in snaps))
|
|
91
|
+
|
|
92
|
+
def test_double_doorway_rewraps_and_unwraps_for_both_origins(self):
|
|
93
|
+
# Include both complete 3-edge walks to exercise wrap -> unwrap.
|
|
94
|
+
for original_inverter, first, second in (
|
|
95
|
+
(False, ('000','100','110'), ('000','001','011')),
|
|
96
|
+
(True, ('000','001','011'), ('000','100','110')),
|
|
97
|
+
):
|
|
98
|
+
timeline = dict(zip((4,6,8,10,12,14), first[1:] + ('111',) + second[1:] + ('111',)))
|
|
99
|
+
packets, snaps = self.execute(inverted=original_inverter, inputs=timeline,
|
|
100
|
+
stop=18)
|
|
101
|
+
self.assertIn((9,'000',not original_inverter,
|
|
102
|
+
int(not original_inverter),not original_inverter,'111'), snaps)
|
|
103
|
+
self.assertIn((15,'000',original_inverter,
|
|
104
|
+
int(original_inverter),original_inverter,'111'), snaps)
|
|
105
|
+
self.assertIn((8,'111'), packets)
|
|
106
|
+
self.assertIn((14,'111'), packets)
|
|
107
|
+
# Active inverter may also PROPOSE 111 ahead of its acceptance.
|
|
108
|
+
if original_inverter:
|
|
109
|
+
self.assertIn((7,'111'), packets)
|
|
110
|
+
# The final machine genuinely has its original, restored
|
|
111
|
+
# physical HALT/REPEAT interpretation and remains silent.
|
|
112
|
+
self.assertNotIn((16,'000'), packets)
|
|
113
|
+
self.assertNotIn((17,'000'), packets)
|
|
114
|
+
|
|
115
|
+
def test_repeat_proposes_without_self_transition_and_waits_for_echo(self):
|
|
116
|
+
# Manual REPEAT at t=3, proposal at t=5, no incoming echo until t=8.
|
|
117
|
+
packets, snaps = self.execute(inputs={8:'100'}, arrow_at=3, stop=11)
|
|
118
|
+
first_proposal = next(t for t,p in packets if p=='100')
|
|
119
|
+
self.assertLess(first_proposal,8)
|
|
120
|
+
self.assertFalse(any(t<8 and st!='000' for t,st,*_ in snaps))
|
|
121
|
+
self.assertTrue(any(t==8 and st=='100' for t,st,*_ in snaps))
|
|
122
|
+
self.assertIn((8,'100'), packets) # reprojects only upon admission
|
|
123
|
+
|
|
124
|
+
def test_plain_mode_inverter_still_uses_original_reverse_cycle(self):
|
|
125
|
+
packets, snaps = self.execute(hypercube=False, inverted=True,
|
|
126
|
+
inputs={4:observer.SCISSORS})
|
|
127
|
+
self.assertIn((4, observer.SCISSORS), packets)
|
|
128
|
+
self.assertFalse(snaps)
|
|
129
|
+
|
|
130
|
+
def test_plain_mode_inverter_repeat_mapping_is_unchanged(self):
|
|
131
|
+
packets, _ = self.execute(hypercube=False, inverted=True, stop=10)
|
|
132
|
+
self.assertTrue(any(payload == observer.SCISSORS for _,payload in packets))
|
|
133
|
+
|
|
134
|
+
def test_plain_mode_still_admits_old_expected_rps(self):
|
|
135
|
+
packets, snaps = self.execute(hypercube=False, inputs={4:observer.PAPER})
|
|
136
|
+
self.assertIn((4,observer.PAPER), packets)
|
|
137
|
+
self.assertFalse(snaps)
|
|
138
|
+
|
|
139
|
+
if __name__ == '__main__': unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Deterministic regression checks for Trinity geometry and admission."""
|
|
2
|
+
import unittest
|
|
3
|
+
from Machine.Spacewalk import Spacewalk, POSITIVE, NEGATIVE, LATERAL
|
|
4
|
+
|
|
5
|
+
class TrinityTests(unittest.TestCase):
|
|
6
|
+
def test_all_eight_vertices_covered_exactly_once(self):
|
|
7
|
+
self.assertEqual(set(POSITIVE) & set(NEGATIVE), {'000', '111'})
|
|
8
|
+
self.assertEqual((set(POSITIVE) | set(NEGATIVE)) & LATERAL, set())
|
|
9
|
+
self.assertEqual((set(POSITIVE) | set(NEGATIVE)) | LATERAL,
|
|
10
|
+
{f'{x:03b}' for x in range(8)})
|
|
11
|
+
self.assertEqual(LATERAL, {'010', '101'})
|
|
12
|
+
|
|
13
|
+
def test_each_path_has_only_valid_one_bit_edges(self):
|
|
14
|
+
for lane in (POSITIVE, NEGATIVE):
|
|
15
|
+
self.assertEqual([sum(a != b for a, b in zip(u, v))
|
|
16
|
+
for u,v in zip(lane, lane[1:])], [1,1,1])
|
|
17
|
+
self.assertEqual([s.count('1') for s in lane], [0,1,2,3])
|
|
18
|
+
|
|
19
|
+
def test_mirror(self):
|
|
20
|
+
self.assertEqual(tuple(s[::-1] for s in POSITIVE), NEGATIVE)
|
|
21
|
+
|
|
22
|
+
def test_matching_state_is_inert_and_proposal_has_no_side_effect(self):
|
|
23
|
+
for inverted, expected in ((False, POSITIVE), (True, NEGATIVE)):
|
|
24
|
+
node = Spacewalk(inverted)
|
|
25
|
+
for vertex in expected[1:]:
|
|
26
|
+
current = node.state
|
|
27
|
+
self.assertIsNone(node.accept(current))
|
|
28
|
+
self.assertEqual(node.state, current)
|
|
29
|
+
self.assertEqual(node.propose(), vertex)
|
|
30
|
+
self.assertEqual(node.state, current) # proposal != admission
|
|
31
|
+
self.assertEqual(node.accept(vertex), vertex)
|
|
32
|
+
self.assertEqual(node.state, vertex)
|
|
33
|
+
self.assertIsNone(node.propose())
|
|
34
|
+
self.assertIsNone(node.accept('111'))
|
|
35
|
+
self.assertEqual(node.state, '111')
|
|
36
|
+
|
|
37
|
+
def test_expected_successor_admitted(self):
|
|
38
|
+
for inverted, path in ((False, POSITIVE), (True, NEGATIVE)):
|
|
39
|
+
node = Spacewalk(inverted)
|
|
40
|
+
for vertex in path[1:]:
|
|
41
|
+
self.assertEqual(node.accept(vertex), vertex)
|
|
42
|
+
|
|
43
|
+
def test_off_lane_including_lateral_is_rejected(self):
|
|
44
|
+
for inverted in (False, True):
|
|
45
|
+
node = Spacewalk(inverted)
|
|
46
|
+
for packet in ('010','101','xyz','111','1000'):
|
|
47
|
+
self.assertIsNone(node.accept(packet))
|
|
48
|
+
self.assertEqual(node.state,'000')
|
|
49
|
+
|
|
50
|
+
def test_no_sender_orientation_is_transmitted(self):
|
|
51
|
+
self.assertEqual(Spacewalk(False).state, Spacewalk(True).state)
|
|
52
|
+
self.assertEqual(Spacewalk(False).state, '000')
|
|
53
|
+
|
|
54
|
+
def test_four_shell_emblems_switch_and_one_dot(self):
|
|
55
|
+
a,b = Spacewalk(), Spacewalk(True)
|
|
56
|
+
self.assertEqual(a.display()[1].count('📜'),1)
|
|
57
|
+
self.assertEqual(b.display()[1].count('📜'),1)
|
|
58
|
+
self.assertNotEqual(a.display()[1],b.display()[1])
|
|
59
|
+
for node in (a,b):
|
|
60
|
+
for i in range(4):
|
|
61
|
+
self.assertEqual(node.shell,i)
|
|
62
|
+
self.assertEqual(node.display()[0].count('🔻'),1)
|
|
63
|
+
self.assertEqual(node.display()[2].count('🔺'),1)
|
|
64
|
+
if node.propose() is not None: node.accept(node.propose())
|
|
65
|
+
|
|
66
|
+
if __name__=='__main__': unittest.main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Regression: no flickering startup loop or endlessly deferred proposals."""
|
|
2
|
+
import unittest
|
|
3
|
+
import test_observer_wire as wire
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SpacewalkTimingTests(unittest.TestCase):
|
|
7
|
+
def test_halted_spacewalk_renders_once_at_startup(self):
|
|
8
|
+
data, snapshots = wire.WireTests().execute(stop=14)
|
|
9
|
+
self.assertEqual(data, [])
|
|
10
|
+
self.assertEqual(len(snapshots), 1)
|
|
11
|
+
|
|
12
|
+
def test_inverter_initial_proposal_then_listens_until_next_encounter(self):
|
|
13
|
+
data, snapshots = wire.WireTests().execute(inverted=True, stop=14)
|
|
14
|
+
self.assertEqual(len(snapshots), 1)
|
|
15
|
+
self.assertEqual(data, [(3, '001')])
|
|
16
|
+
|
|
17
|
+
def test_healthy_manual_repeat_perturbs_field_once_and_listens(self):
|
|
18
|
+
data, snapshots = wire.WireTests().execute(arrow_at=3, stop=14)
|
|
19
|
+
self.assertEqual(data, [(5, '100')])
|
|
20
|
+
self.assertEqual({state for _, state, *_ in snapshots}, {'000'})
|
|
21
|
+
|
|
22
|
+
def test_admissions_do_not_wait_for_one_second_in_spacewalk(self):
|
|
23
|
+
data, snapshots = wire.WireTests().execute(inputs={4: '100', 5: '110', 6: '111'}, stop=11)
|
|
24
|
+
admissions = [(t, state) for t, state, *_ in snapshots if state != '000']
|
|
25
|
+
self.assertEqual(admissions[:3], [(4, '100'), (5, '110'), (6, '111')])
|
|
26
|
+
self.assertIn((5, '110'), data)
|
|
27
|
+
self.assertIn((6, '111'), data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
unittest.main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Keep original RPS spacing independent from Spacewalk's compact layout."""
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
from Machine import observer
|
|
7
|
+
from Machine.Spacewalk import Spacewalk
|
|
8
|
+
|
|
9
|
+
class LayoutTests(unittest.TestCase):
|
|
10
|
+
def render(self, cube):
|
|
11
|
+
capture = io.StringIO()
|
|
12
|
+
with patch.object(observer.sys, 'stdout', capture), \
|
|
13
|
+
patch.object(observer, 'paint'), \
|
|
14
|
+
patch.object(observer.os, 'get_terminal_size',
|
|
15
|
+
return_value=os.terminal_size((80, 24))):
|
|
16
|
+
observer.draw('Γ', 0, observer.ROCK, cube=cube)
|
|
17
|
+
return capture.getvalue().splitlines()
|
|
18
|
+
|
|
19
|
+
def test_both_modes_shift_two_lines_down_as_one_cluster(self):
|
|
20
|
+
for cube in (None, Spacewalk(), Spacewalk(True)):
|
|
21
|
+
lines = self.render(cube)
|
|
22
|
+
title = next(i for i, line in enumerate(lines) if 'HaltingMachine' in line)
|
|
23
|
+
self.assertEqual(title, 4) # V27 started at line index 2.
|
|
24
|
+
self.assertEqual(lines[:4], ['', '', '', ''])
|
|
25
|
+
|
|
26
|
+
def test_plain_layout_has_both_original_blank_rows(self):
|
|
27
|
+
lines = self.render(None)
|
|
28
|
+
repeat = next(i for i,line in enumerate(lines) if line.strip() == 'REPEAT')
|
|
29
|
+
self.assertEqual(lines[repeat + 1], '')
|
|
30
|
+
self.assertIn(observer.ROCK * 4, lines[repeat + 2])
|
|
31
|
+
self.assertEqual(lines[repeat + 3], '')
|
|
32
|
+
self.assertIn('SPACE TO EXIT', lines[repeat + 4])
|
|
33
|
+
|
|
34
|
+
def test_spacewalk_has_no_blank_rows_between_controls_and_exit(self):
|
|
35
|
+
lines = self.render(Spacewalk())
|
|
36
|
+
repeat = next(i for i,line in enumerate(lines) if line.strip() == 'REPEAT')
|
|
37
|
+
self.assertIn('🔻', lines[repeat + 1])
|
|
38
|
+
self.assertIn('🪨', lines[repeat + 2])
|
|
39
|
+
self.assertIn('🔺', lines[repeat + 3])
|
|
40
|
+
self.assertIn('SPACE TO EXIT', lines[repeat + 4])
|
|
41
|
+
self.assertNotIn('', lines[repeat + 1:repeat + 5])
|
|
42
|
+
|
|
43
|
+
if __name__ == '__main__': unittest.main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""V26: each admissibility encounter can re-arm a one-shot repeater."""
|
|
2
|
+
import unittest
|
|
3
|
+
import test_observer_wire as wire
|
|
4
|
+
from Machine import observer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CadenceTests(unittest.TestCase):
|
|
8
|
+
def test_inadmissible_trinity_resets_emission_without_changing_state(self):
|
|
9
|
+
packets, snapshots = wire.WireTests().execute(
|
|
10
|
+
inverted=True, inputs={4: '111', 5: '111', 6: '111'}, stop=10)
|
|
11
|
+
proposal_times = [t for t, msg in packets if msg == '001']
|
|
12
|
+
self.assertEqual(proposal_times[:3], [3, 5, 7])
|
|
13
|
+
self.assertEqual({state for _, state, *_ in snapshots}, {'000'})
|
|
14
|
+
self.assertEqual(len(snapshots), 1) # no redraw on rejection
|
|
15
|
+
|
|
16
|
+
def test_idempotent_and_lateral_trinity_also_regulate_cadence(self):
|
|
17
|
+
packets, snapshots = wire.WireTests().execute(
|
|
18
|
+
inverted=True, inputs={4: '000', 5: '010', 6: '101'}, stop=10)
|
|
19
|
+
self.assertEqual([t for t, msg in packets if msg == '001'][:3], [3, 5, 7])
|
|
20
|
+
self.assertEqual({state for _, state, *_ in snapshots}, {'000'})
|
|
21
|
+
|
|
22
|
+
def test_discovery_packets_do_not_regulate_cadence(self):
|
|
23
|
+
packets, _ = wire.WireTests().execute(
|
|
24
|
+
inverted=True, inputs={4: 'H1', 5: 'H1', 6: 'H1'}, stop=10)
|
|
25
|
+
self.assertEqual([t for t, msg in packets if msg == '001'], [3])
|
|
26
|
+
|
|
27
|
+
def test_nontrinity_payload_does_not_regulate_cadence(self):
|
|
28
|
+
packets, _ = wire.WireTests().execute(
|
|
29
|
+
inverted=True, inputs={4: 'xyz', 5: '1000', 6: 'bad'}, stop=10)
|
|
30
|
+
self.assertEqual([t for t, msg in packets if msg == '001'], [3])
|
|
31
|
+
|
|
32
|
+
def test_no_ongoing_repeater_without_a_new_trinity(self):
|
|
33
|
+
packets, _ = wire.WireTests().execute(inverted=True, inputs={4:'111'}, stop=20)
|
|
34
|
+
self.assertEqual([t for t, msg in packets if msg == '001'], [3, 5])
|
|
35
|
+
|
|
36
|
+
def test_original_haltingmachine_is_unchanged_by_inadmissible_packets(self):
|
|
37
|
+
packets, _ = wire.WireTests().execute(
|
|
38
|
+
inverted=True, hypercube=False,
|
|
39
|
+
inputs={4: observer.ROCK, 5: observer.ROCK, 6: observer.ROCK}, stop=10)
|
|
40
|
+
self.assertEqual([t for t, msg in packets if msg == observer.SCISSORS][:3],
|
|
41
|
+
[3, 4, 5])
|
|
42
|
+
|
|
43
|
+
def test_admitted_trinity_reprojects_immediately_without_admission_gate(self):
|
|
44
|
+
packets, snapshots = wire.WireTests().execute(
|
|
45
|
+
inputs={4:'100', 5:'110', 6:'111'}, stop=9)
|
|
46
|
+
self.assertEqual([(t, state) for t,state,*_ in snapshots if state != '000'],
|
|
47
|
+
[(4,'100'), (5,'110'), (6,'111')])
|
|
48
|
+
self.assertIn((5, '110'), packets)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == '__main__':
|
|
52
|
+
unittest.main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""V26 one-shot reception pacing, original physical switches, and spacing."""
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
from Machine import observer
|
|
7
|
+
from Machine.Spacewalk import Spacewalk
|
|
8
|
+
import test_observer_wire as wire
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class V25Tests(unittest.TestCase):
|
|
12
|
+
def test_receiving_every_tick_cannot_starve_inverter(self):
|
|
13
|
+
stream = {t: '111' for t in range(4, 21)}
|
|
14
|
+
packets, snapshots = wire.WireTests().execute(
|
|
15
|
+
inverted=True, inputs=stream, stop=22)
|
|
16
|
+
times = [t for t, payload in packets if payload == '001']
|
|
17
|
+
self.assertGreaterEqual(len(times), 8, times)
|
|
18
|
+
self.assertEqual(times[:4], [3,5,7,9])
|
|
19
|
+
self.assertEqual({state for _,state,*_ in snapshots}, {'000'})
|
|
20
|
+
|
|
21
|
+
def test_one_response_per_packet_window_then_listen(self):
|
|
22
|
+
packets, _ = wire.WireTests().execute(
|
|
23
|
+
inverted=True, inputs={4:'111', 5:'111', 6:'111', 7:'111'}, stop=11)
|
|
24
|
+
self.assertEqual([t for t,p in packets if p=='001'], [3,5,7])
|
|
25
|
+
|
|
26
|
+
def test_arrows_wrap_both_physical_controls_in_both_modes(self):
|
|
27
|
+
for inverted in (False, True):
|
|
28
|
+
for cube in (False, True):
|
|
29
|
+
for arrow in (b'\x1b[A', b'\x1b[B'):
|
|
30
|
+
test = wire.WireTests()
|
|
31
|
+
test.execute(inverted=inverted, hypercube=cube, stop=8,
|
|
32
|
+
arrows={4:arrow, 6:arrow})
|
|
33
|
+
toggles = [mode for _,mode,_ in test.modes]
|
|
34
|
+
self.assertEqual(toggles, [0,1,0])
|
|
35
|
+
|
|
36
|
+
def test_plain_exit_text_shifted_one_column_spacewalk_unchanged(self):
|
|
37
|
+
def render(cube):
|
|
38
|
+
out = io.StringIO()
|
|
39
|
+
with patch.object(observer.sys, 'stdout', out), patch.object(observer, 'paint'), \
|
|
40
|
+
patch.object(observer.os,'get_terminal_size', return_value=os.terminal_size((80,24))):
|
|
41
|
+
observer.draw('Γ',0,observer.ROCK,cube=cube)
|
|
42
|
+
return next(l for l in out.getvalue().splitlines() if 'SPACE TO EXIT' in l)
|
|
43
|
+
plain = render(None)
|
|
44
|
+
spacewalk = render(Spacewalk())
|
|
45
|
+
self.assertEqual(plain, spacewalk)
|
|
46
|
+
self.assertEqual(len(plain)-len(plain.lstrip()), 34)
|
|
47
|
+
|
|
48
|
+
if __name__ == '__main__': unittest.main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""V26: each active repeater earns one delayed proposal per encounter."""
|
|
2
|
+
import unittest
|
|
3
|
+
import test_observer_wire as wire
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PingPongTests(unittest.TestCase):
|
|
7
|
+
def test_halted_relay_never_proposes_from_rejected_packets(self):
|
|
8
|
+
packets, frames = wire.WireTests().execute(
|
|
9
|
+
inputs={4:'001',5:'111',6:'010'}, stop=11)
|
|
10
|
+
self.assertEqual(packets, [])
|
|
11
|
+
self.assertEqual({state for _,state,*_ in frames}, {'000'})
|
|
12
|
+
self.assertEqual(len(frames), 1)
|
|
13
|
+
|
|
14
|
+
def test_halted_relay_admits_each_successor_without_a_timer(self):
|
|
15
|
+
packets, frames = wire.WireTests().execute(
|
|
16
|
+
inputs={4:'100',5:'110',6:'111'}, stop=13)
|
|
17
|
+
self.assertEqual(packets, [(4,'100'),(5,'110'),(6,'111')])
|
|
18
|
+
self.assertEqual([st for _,st,*_ in frames][:4],
|
|
19
|
+
['000','100','110','111'])
|
|
20
|
+
|
|
21
|
+
def test_repeater_admission_echoes_immediately_and_resets_timer(self):
|
|
22
|
+
packets, frames = wire.WireTests().execute(
|
|
23
|
+
inverted=True, inputs={4:'001',5:'011'}, stop=12)
|
|
24
|
+
self.assertEqual(packets, [(3,'001'),(4,'001'),(5,'011'),(6,'111')])
|
|
25
|
+
self.assertEqual([st for _,st,*_ in frames], ['000','001','011'])
|
|
26
|
+
|
|
27
|
+
def test_admission_starts_one_proposal_even_without_another_packet(self):
|
|
28
|
+
packets, _ = wire.WireTests().execute(
|
|
29
|
+
inverted=True, inputs={4:'001'}, stop=12)
|
|
30
|
+
self.assertEqual(packets, [(3,'001'),(4,'001'),(5,'011')])
|
|
31
|
+
|
|
32
|
+
def test_multiple_rejections_in_same_window_do_not_restart_countdown(self):
|
|
33
|
+
packets, frames = wire.WireTests().execute(
|
|
34
|
+
inverted=True, inputs={4:'000',5:'000'}, stop=12)
|
|
35
|
+
self.assertEqual(packets, [(3,'001'),(5,'001')])
|
|
36
|
+
self.assertEqual(len(frames), 1)
|
|
37
|
+
|
|
38
|
+
def test_manual_perturbation_speaks_once_then_admission_drives_next(self):
|
|
39
|
+
packets, frames = wire.WireTests().execute(
|
|
40
|
+
arrow_at=3, inputs={6:'100',7:'110'}, stop=12)
|
|
41
|
+
self.assertEqual(packets, [(5,'100'),(6,'100'),(7,'110'),(8,'111')])
|
|
42
|
+
self.assertEqual([st for _,st,*_ in frames], ['000','000','100','110'])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == '__main__':
|
|
46
|
+
unittest.main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""V27 ships a 3-second Spacewalk interval without touching RPS or admission."""
|
|
2
|
+
import unittest
|
|
3
|
+
from Machine.Spacewalk import Spacewalk, INTERVAL
|
|
4
|
+
from Machine import observer
|
|
5
|
+
import test_observer_wire as wire
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThreeSecondTests(unittest.TestCase):
|
|
9
|
+
def test_pace_is_three_seconds(self):
|
|
10
|
+
self.assertEqual(INTERVAL, 3.0)
|
|
11
|
+
|
|
12
|
+
def test_doorway_wait_is_three_seconds_from_admission(self):
|
|
13
|
+
for inverted, lane in ((False, ('100', '110', '111')),
|
|
14
|
+
(True, ('001', '011', '111'))):
|
|
15
|
+
walk = Spacewalk(inverted)
|
|
16
|
+
for t, vertex in enumerate(lane, start=4):
|
|
17
|
+
self.assertEqual(walk.accept(vertex, now=t), vertex)
|
|
18
|
+
self.assertEqual(walk.doorway_at, 6.0 + INTERVAL)
|
|
19
|
+
self.assertEqual(walk.state, '111')
|
|
20
|
+
|
|
21
|
+
def test_first_inverted_proposal_is_after_three_seconds(self):
|
|
22
|
+
packets, frames = wire.WireTests().execute(inverted=True, stop=12, interval=None)
|
|
23
|
+
self.assertEqual(packets, [(5, '001')])
|
|
24
|
+
self.assertEqual(len(frames), 1)
|
|
25
|
+
|
|
26
|
+
def test_manual_repeat_waits_three_seconds_then_listens(self):
|
|
27
|
+
packets, frames = wire.WireTests().execute(arrow_at=3, stop=12, interval=None)
|
|
28
|
+
self.assertEqual(packets, [(7, '100')])
|
|
29
|
+
self.assertTrue(all(st == '000' for _, st, *_ in frames))
|
|
30
|
+
|
|
31
|
+
def test_rejected_packet_arms_one_response_and_does_not_keep_postponing(self):
|
|
32
|
+
packets, frames = wire.WireTests().execute(
|
|
33
|
+
inverted=True, inputs={4: '000', 6: '111', 7: '010'}, stop=13, interval=None)
|
|
34
|
+
self.assertEqual(packets, [(5, '001'), (9, '001')])
|
|
35
|
+
self.assertEqual({state for _, state, *_ in frames}, {'000'})
|
|
36
|
+
|
|
37
|
+
def test_admission_echo_immediate_and_restarts_three_second_timer(self):
|
|
38
|
+
packets, frames = wire.WireTests().execute(
|
|
39
|
+
inverted=True, inputs={4: '001', 5: '011'}, stop=12, interval=None)
|
|
40
|
+
self.assertEqual(packets, [(4, '001'), (5, '011'), (8, '111')])
|
|
41
|
+
self.assertEqual([state for _,state,*_ in frames], ['000', '001', '011'])
|
|
42
|
+
|
|
43
|
+
def test_halted_relay_still_admits_immediately_and_crosses_in_three_seconds(self):
|
|
44
|
+
packets, frames = wire.WireTests().execute(
|
|
45
|
+
inputs={4: '100', 5: '110', 6: '111'}, stop=13, interval=None)
|
|
46
|
+
self.assertEqual(packets, [(4, '100'), (5, '110'), (6, '111')])
|
|
47
|
+
self.assertIn((9, '000', True, 1, True, '111'), frames)
|
|
48
|
+
|
|
49
|
+
def test_plain_rps_still_uses_one_second_timer(self):
|
|
50
|
+
packets, _ = wire.WireTests().execute(
|
|
51
|
+
inverted=True, hypercube=False, stop=9, interval=None)
|
|
52
|
+
self.assertEqual(packets[:3], [(3, observer.SCISSORS),
|
|
53
|
+
(4, observer.SCISSORS),
|
|
54
|
+
(5, observer.SCISSORS)])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
unittest.main()
|