Hydra-Game 1.0.0__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.
- hydra_game-1.0.0/Game/Body.py +308 -0
- hydra_game-1.0.0/Game/Mutate.py +288 -0
- hydra_game-1.0.0/Game/Plexus.py +193 -0
- hydra_game-1.0.0/Game/Pulse.py +144 -0
- hydra_game-1.0.0/Hydra.py +7 -0
- hydra_game-1.0.0/Hydra_Game.egg-info/PKG-INFO +4 -0
- hydra_game-1.0.0/Hydra_Game.egg-info/SOURCES.txt +12 -0
- hydra_game-1.0.0/Hydra_Game.egg-info/dependency_links.txt +1 -0
- hydra_game-1.0.0/Hydra_Game.egg-info/entry_points.txt +2 -0
- hydra_game-1.0.0/Hydra_Game.egg-info/top_level.txt +2 -0
- hydra_game-1.0.0/PKG-INFO +4 -0
- hydra_game-1.0.0/README.md +80 -0
- hydra_game-1.0.0/pyproject.toml +14 -0
- hydra_game-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import termios
|
|
7
|
+
import threading
|
|
8
|
+
import tty
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from select import select
|
|
11
|
+
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
|
|
12
|
+
|
|
13
|
+
from .Plexus import GemName, Intent
|
|
14
|
+
from .Pulse import CursorLeft, Flicker1, Flicker2, Flicker3, Flicker4, Green, HideCursor, PadLine, ReadKey, Reset, ShowCursor, Step, Teal
|
|
15
|
+
|
|
16
|
+
class Heart(Protocol):
|
|
17
|
+
head: str
|
|
18
|
+
heads: List[str]
|
|
19
|
+
tail: Optional[Dict[str, Any]]
|
|
20
|
+
state: Any
|
|
21
|
+
|
|
22
|
+
def Snapshot(self) -> Dict[str, Any]: ...
|
|
23
|
+
def Emotions(self) -> Dict[str, Any]: ...
|
|
24
|
+
def Ingest(self, tailin: Dict[str, Any]) -> List[Intent]: ...
|
|
25
|
+
def Propose(self, tohead: str, amount: int) -> Dict[str, Any]: ...
|
|
26
|
+
def DreamState(self) -> Dict[str, Any]: ...
|
|
27
|
+
|
|
28
|
+
PrintLock = threading.Lock()
|
|
29
|
+
SeenMax = 4096
|
|
30
|
+
Command = Union[str, Tuple[str, str, int]]
|
|
31
|
+
WelcomeLines = [
|
|
32
|
+
"",
|
|
33
|
+
"",
|
|
34
|
+
f" {Teal}It's feeding time{Green}...{Reset}",
|
|
35
|
+
"",
|
|
36
|
+
f" {Teal}Go for it, let another set of jaws chomp on your tallies{Reset}",
|
|
37
|
+
f" {Teal}and give of yourself freely{Green}. {Teal}Nothing here is lost{Green}.{Reset}",
|
|
38
|
+
f" {Teal}If another takes too much, draw it back through the ichor{Green}.{Reset}",
|
|
39
|
+
f" {Teal}Hydra feels no pain{Green}. {Teal}It has no memory{Green}...{Reset}",
|
|
40
|
+
"",
|
|
41
|
+
f" {Teal}Use {Green}←{Teal} and {Green}→{Teal} to select a head, then {Green}↑{Teal} and {Green}↓{Teal} for an amount{Green}.{Reset}",
|
|
42
|
+
f" {Green}Enter{Teal}({Green}Feed!{Teal}) {Green}Ctrl{Teal}+{Green}X{Teal}({Green}Sever{Teal}) {Green}Ctrl{Teal}+{Green}C{Teal}({Green}Cauterize{Teal}){Reset}",
|
|
43
|
+
"",
|
|
44
|
+
f" {Teal}Heads that become {Green}Envious{Teal} must be {Green}Severed{Teal} and Rehydrated{Green}.{Reset}",
|
|
45
|
+
"",
|
|
46
|
+
"",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
class ExitSignal(Exception):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def FirstTarget(head: str, heads: List[str]) -> str:
|
|
54
|
+
for item in heads:
|
|
55
|
+
if item != head:
|
|
56
|
+
return item
|
|
57
|
+
return heads[0]
|
|
58
|
+
|
|
59
|
+
def NextTarget(head: str, heads: List[str], targethead: str, direction: int) -> str:
|
|
60
|
+
if not heads:
|
|
61
|
+
return targethead
|
|
62
|
+
index = heads.index(targethead)
|
|
63
|
+
for _ in range(len(heads)):
|
|
64
|
+
index = (index + direction) % len(heads)
|
|
65
|
+
candidate = heads[index]
|
|
66
|
+
if candidate != head or len(heads) == 1:
|
|
67
|
+
return candidate
|
|
68
|
+
return targethead
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Body:
|
|
72
|
+
head: str
|
|
73
|
+
heads: List[str]
|
|
74
|
+
sock: socket.socket
|
|
75
|
+
peers: List[Tuple[str, int]]
|
|
76
|
+
heart: Heart
|
|
77
|
+
lock: threading.Lock
|
|
78
|
+
targethead: str = ""
|
|
79
|
+
amount: int = 1
|
|
80
|
+
seen: Dict[str, None] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def Crown(self) -> int:
|
|
83
|
+
return int(self.heart.state.crown)
|
|
84
|
+
|
|
85
|
+
def EnsureTarget(self) -> None:
|
|
86
|
+
if not self.targethead or self.targethead not in self.heads:
|
|
87
|
+
self.targethead = FirstTarget(self.head, self.heads)
|
|
88
|
+
|
|
89
|
+
def MoveTarget(self, direction: int) -> None:
|
|
90
|
+
self.EnsureTarget()
|
|
91
|
+
self.targethead = NextTarget(self.head, self.heads, self.targethead, direction)
|
|
92
|
+
|
|
93
|
+
def TailItems(self, tallies: Dict[str, Any]) -> str:
|
|
94
|
+
return f"{Reset}{Green}:{Reset}".join(
|
|
95
|
+
f"{Teal}{item}{Reset}{(Flicker3() if index % 2 == 0 else Flicker4())}{tallies.get(item, 'x')}{Reset}"
|
|
96
|
+
for index, item in enumerate(self.heads)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def HudLine(self, crown: int, tallies: Dict[str, Any], envy: bool) -> Tuple[str, int]:
|
|
100
|
+
headchunk = f"{Flicker2()}Head{Reset}{Green}:{Reset}{Flicker2()}{self.head}{Reset}" if envy else f"{Teal}Head{Green}:{Reset}{Teal}{self.head}{Reset}"
|
|
101
|
+
visible = f".::Head:{self.head}:::Crown:{GemName(crown)}:::{self.targethead}:{self.amount}:::Tails:" + ".".join(f"{item}{tallies.get(item, 'x')}" for item in self.heads) + "::."
|
|
102
|
+
line = (
|
|
103
|
+
f" {Green}.::{Reset}{headchunk}{Green}:::{Reset}"
|
|
104
|
+
f"{Teal}Crown{Green}:{Reset}{Flicker1()}{GemName(crown)}{Reset}{Green}:::{Reset}"
|
|
105
|
+
f"{Flicker3()}{self.targethead}{Reset}{Green}:{Reset}{Flicker4()}{self.amount}{Reset}{Green}:::{Reset}"
|
|
106
|
+
f"{Teal}Tails{Green}:{Reset}{Flicker3()}{self.TailItems(tallies)}{Reset}{Green}::.{Reset}"
|
|
107
|
+
)
|
|
108
|
+
return line, len(visible)
|
|
109
|
+
|
|
110
|
+
def Paint(self, lines: List[str], cursorleft: int) -> None:
|
|
111
|
+
with PrintLock:
|
|
112
|
+
width, height = shutil.get_terminal_size(fallback=(80, 24))
|
|
113
|
+
built = [PadLine(line, max(1, width)) for line in lines]
|
|
114
|
+
if len(built) < height:
|
|
115
|
+
built.extend([" " * max(1, width)] * (height - len(built)))
|
|
116
|
+
sys.stdout.write("\x1b[H")
|
|
117
|
+
sys.stdout.write("\n".join(built[:height]))
|
|
118
|
+
sys.stdout.write(CursorLeft(cursorleft))
|
|
119
|
+
sys.stdout.flush()
|
|
120
|
+
|
|
121
|
+
def RenderStatus(self) -> None:
|
|
122
|
+
self.EnsureTarget()
|
|
123
|
+
snap = self.heart.Snapshot()
|
|
124
|
+
crown = int(snap.get("crown", 1) or 1)
|
|
125
|
+
tallies = dict(snap.get("tallies", {}) or {})
|
|
126
|
+
envy = bool(self.heart.Emotions().get("envy", False))
|
|
127
|
+
hudline, cursorleft = self.HudLine(crown, tallies, envy)
|
|
128
|
+
self.Paint(WelcomeLines + [hudline], cursorleft)
|
|
129
|
+
|
|
130
|
+
def SendMessage(self, message: Dict[str, Any], dstaddr: Optional[Tuple[str, int]] = None, skipaddr: Optional[Tuple[str, int]] = None) -> None:
|
|
131
|
+
payload = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
|
132
|
+
if dstaddr is not None:
|
|
133
|
+
try:
|
|
134
|
+
self.sock.sendto(payload, dstaddr)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
return
|
|
138
|
+
for host, port in self.peers:
|
|
139
|
+
if skipaddr is not None and (host, port) == skipaddr:
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
self.sock.sendto(payload, (host, port))
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def SendTail(self, tail: Dict[str, Any], srcaddr: Optional[Tuple[str, int]] = None) -> None:
|
|
147
|
+
self.SendMessage(tail, skipaddr=srcaddr)
|
|
148
|
+
|
|
149
|
+
def SendHunger(self, crown: int, needtail: bool = True) -> None:
|
|
150
|
+
self.SendMessage({"type": "HUNGER", "head": self.head, "crown": int(crown), "needtail": bool(needtail)})
|
|
151
|
+
|
|
152
|
+
def SendRoster(self, dstaddr: Optional[Tuple[str, int]] = None) -> None:
|
|
153
|
+
self.SendMessage({"type": "ROSTER", "head": self.head, "heads": list(self.heads)}, dstaddr=dstaddr)
|
|
154
|
+
|
|
155
|
+
def SendDream(self, dstaddr: Tuple[str, int]) -> None:
|
|
156
|
+
with self.lock:
|
|
157
|
+
self.SendMessage(dict(self.heart.DreamState()), dstaddr=dstaddr)
|
|
158
|
+
|
|
159
|
+
def SoftReboot(self) -> None:
|
|
160
|
+
with self.lock:
|
|
161
|
+
self.SendHunger(self.Crown(), needtail=True)
|
|
162
|
+
self.RenderStatus()
|
|
163
|
+
|
|
164
|
+
def MarkSeen(self, message: Dict[str, Any]) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
seenkey = json.dumps({
|
|
167
|
+
"head": message.get("head", ""),
|
|
168
|
+
"crown": int(message.get("crown", 1) or 1),
|
|
169
|
+
"tallies": dict(message.get("tallies", {}) or {}),
|
|
170
|
+
}, sort_keys=True, separators=(",", ":"))
|
|
171
|
+
except Exception:
|
|
172
|
+
return False
|
|
173
|
+
if not (message.get("is_dream") and self.heart.envy) and seenkey in self.seen:
|
|
174
|
+
return False
|
|
175
|
+
self.seen[seenkey] = None
|
|
176
|
+
if len(self.seen) > SeenMax:
|
|
177
|
+
self.seen.pop(next(iter(self.seen)))
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
def HandleSignal(self, message: Dict[str, Any], addr: Tuple[str, int]) -> bool:
|
|
181
|
+
messagetype = str(message.get("type", "") or "")
|
|
182
|
+
if messagetype == "HUNGER":
|
|
183
|
+
self.SendDream(addr)
|
|
184
|
+
elif messagetype == "AWAKE":
|
|
185
|
+
self.SendRoster(dstaddr=addr)
|
|
186
|
+
elif messagetype != "ROSTER":
|
|
187
|
+
return False
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def IngestMessage(self, message: Dict[str, Any], addr: Tuple[str, int]) -> None:
|
|
191
|
+
if "tallies" not in message or "crown" not in message:
|
|
192
|
+
return
|
|
193
|
+
with self.lock:
|
|
194
|
+
if not self.MarkSeen(message):
|
|
195
|
+
return
|
|
196
|
+
intents = self.heart.Ingest(dict(message))
|
|
197
|
+
self.ExecuteIntents(intents, srcaddr=addr)
|
|
198
|
+
|
|
199
|
+
def ExecuteIntents(self, intents: List[Intent], srcaddr: Optional[Tuple[str, int]] = None) -> None:
|
|
200
|
+
for intent in intents:
|
|
201
|
+
if intent.type == "Propagate":
|
|
202
|
+
tail = dict(intent.payload.get("tail", {}))
|
|
203
|
+
if tail:
|
|
204
|
+
self.SendTail(tail, srcaddr=srcaddr)
|
|
205
|
+
elif intent.type == "RequestSync":
|
|
206
|
+
crown = int(intent.payload.get("crown", 1) or 1)
|
|
207
|
+
if bool(intent.payload.get("needtail", False)) or bool(self.heart.envy):
|
|
208
|
+
self.SendHunger(crown, needtail=True)
|
|
209
|
+
self.RenderStatus()
|
|
210
|
+
|
|
211
|
+
class Receiver(threading.Thread):
|
|
212
|
+
def __init__(self, body: Body):
|
|
213
|
+
super().__init__(daemon=True)
|
|
214
|
+
self.body = body
|
|
215
|
+
|
|
216
|
+
def run(self) -> None:
|
|
217
|
+
while True:
|
|
218
|
+
try:
|
|
219
|
+
data, addr = self.body.sock.recvfrom(65535)
|
|
220
|
+
message = json.loads(data.decode("utf-8"))
|
|
221
|
+
if isinstance(message, dict) and self.body.HandleSignal(message, addr):
|
|
222
|
+
continue
|
|
223
|
+
if isinstance(message, dict):
|
|
224
|
+
self.body.IngestMessage(message, addr)
|
|
225
|
+
except Exception:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
def ReadCommand(body: Body) -> Command:
|
|
229
|
+
body.EnsureTarget()
|
|
230
|
+
filedescriptor = sys.stdin.fileno()
|
|
231
|
+
original = termios.tcgetattr(filedescriptor)
|
|
232
|
+
body.RenderStatus()
|
|
233
|
+
try:
|
|
234
|
+
tty.setcbreak(filedescriptor)
|
|
235
|
+
laststep = None
|
|
236
|
+
while True:
|
|
237
|
+
step = Step()
|
|
238
|
+
if step != laststep:
|
|
239
|
+
body.RenderStatus()
|
|
240
|
+
laststep = step
|
|
241
|
+
ready, _, _ = select([sys.stdin], [], [], 1 / 60)
|
|
242
|
+
if not ready:
|
|
243
|
+
continue
|
|
244
|
+
key = ReadKey()
|
|
245
|
+
if key == "":
|
|
246
|
+
raise EOFError
|
|
247
|
+
if key in ("\n", "\r"):
|
|
248
|
+
target = body.targethead
|
|
249
|
+
if target == body.head and len(body.heads) > 1:
|
|
250
|
+
body.MoveTarget(+1)
|
|
251
|
+
target = body.targethead
|
|
252
|
+
body.RenderStatus()
|
|
253
|
+
return ("FEED", target, body.amount)
|
|
254
|
+
if key == "\x03":
|
|
255
|
+
raise KeyboardInterrupt
|
|
256
|
+
if key == "\x18":
|
|
257
|
+
body.SoftReboot()
|
|
258
|
+
continue
|
|
259
|
+
if key in ("h", "H"):
|
|
260
|
+
return "HUNGER"
|
|
261
|
+
if key == "C":
|
|
262
|
+
body.MoveTarget(+1)
|
|
263
|
+
elif key == "D":
|
|
264
|
+
body.MoveTarget(-1)
|
|
265
|
+
elif key == "A":
|
|
266
|
+
body.amount = min(999, body.amount + 1)
|
|
267
|
+
elif key == "B":
|
|
268
|
+
body.amount = max(-999, body.amount - 1)
|
|
269
|
+
else:
|
|
270
|
+
continue
|
|
271
|
+
body.RenderStatus()
|
|
272
|
+
finally:
|
|
273
|
+
termios.tcsetattr(filedescriptor, termios.TCSADRAIN, original)
|
|
274
|
+
|
|
275
|
+
def RunBody(*, heart: Heart, head: str, port: int, peers: List[Tuple[str, int]], heads: List[str]) -> None:
|
|
276
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
277
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20)
|
|
278
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
|
|
279
|
+
if any(host == "255.255.255.255" for host, _ in peers):
|
|
280
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
281
|
+
sock.bind(("0.0.0.0", int(port)))
|
|
282
|
+
body = Body(head=str(head).upper(), heads=list(heads), sock=sock, peers=list(peers), heart=heart, lock=threading.Lock(), targethead=FirstTarget(head, heads))
|
|
283
|
+
Receiver(body).start()
|
|
284
|
+
with PrintLock:
|
|
285
|
+
sys.stdout.write("\x1b[2J\x1b[H")
|
|
286
|
+
sys.stdout.write(HideCursor)
|
|
287
|
+
sys.stdout.flush()
|
|
288
|
+
body.RenderStatus()
|
|
289
|
+
with body.lock:
|
|
290
|
+
body.SendHunger(body.Crown(), needtail=True)
|
|
291
|
+
try:
|
|
292
|
+
while True:
|
|
293
|
+
command = ReadCommand(body)
|
|
294
|
+
if command == "HUNGER":
|
|
295
|
+
with body.lock:
|
|
296
|
+
body.SendHunger(body.Crown(), needtail=True)
|
|
297
|
+
continue
|
|
298
|
+
if isinstance(command, tuple) and command[0] == "FEED":
|
|
299
|
+
_, tohead, amount = command
|
|
300
|
+
with body.lock:
|
|
301
|
+
intents = heart.Ingest(dict(heart.Propose(tohead, amount)))
|
|
302
|
+
body.ExecuteIntents(intents)
|
|
303
|
+
except (KeyboardInterrupt, EOFError) as exc:
|
|
304
|
+
raise ExitSignal() from exc
|
|
305
|
+
finally:
|
|
306
|
+
with PrintLock:
|
|
307
|
+
sys.stdout.write(ShowCursor)
|
|
308
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
import termios
|
|
6
|
+
import tty
|
|
7
|
+
from select import select
|
|
8
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
9
|
+
|
|
10
|
+
from .Plexus import Plexus
|
|
11
|
+
from .Body import ExitSignal, RunBody
|
|
12
|
+
from .Pulse import AwakeField, Clear, ExitLine, HideCursor, Index, Now, Phase, ReadKey, RenderCentered, RenderField, ShowCursor
|
|
13
|
+
|
|
14
|
+
BaseHeads = ["A", "B", "C", "D", "E"]
|
|
15
|
+
Fields = ["environment", "depth", "mutation", "head", "awakening"]
|
|
16
|
+
Labels = {
|
|
17
|
+
"environment": "ENVIRONMENT",
|
|
18
|
+
"depth": "DEPTH",
|
|
19
|
+
"mutation": "MUTATIONS",
|
|
20
|
+
"head": "HEADS",
|
|
21
|
+
"awakening": "AWAKENING",
|
|
22
|
+
}
|
|
23
|
+
Options = {
|
|
24
|
+
"environment": ["Den", "Swamp"],
|
|
25
|
+
"mutation": ["1", "2", "3", "4", "5"],
|
|
26
|
+
"head": list(BaseHeads),
|
|
27
|
+
}
|
|
28
|
+
RosterCache: Set[str] = set()
|
|
29
|
+
AwakeInterval = 0.75
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def BuildDen(heads: List[str], depth: int, head: str) -> Tuple[int, List[Tuple[str, int]]]:
|
|
33
|
+
ports = {item: depth + index for index, item in enumerate(heads)}
|
|
34
|
+
return ports[head], [("127.0.0.1", ports[item]) for item in heads if item != head]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def BuildSwamp(heads: List[str], depth: int, head: str) -> Tuple[int, List[Tuple[str, int]]]:
|
|
38
|
+
return depth, [("255.255.255.255", depth)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ExitScreen() -> None:
|
|
42
|
+
filedescriptor = sys.stdin.fileno()
|
|
43
|
+
original = termios.tcgetattr(filedescriptor)
|
|
44
|
+
tty.setcbreak(filedescriptor)
|
|
45
|
+
start = Now()
|
|
46
|
+
lastpulse = None
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
sys.stdout.write(HideCursor)
|
|
50
|
+
Clear()
|
|
51
|
+
while True:
|
|
52
|
+
phase = Phase(start)
|
|
53
|
+
pulse = Index(phase, 4)
|
|
54
|
+
if pulse != lastpulse:
|
|
55
|
+
RenderCentered([ExitLine(phase)], bias=0.5)
|
|
56
|
+
lastpulse = pulse
|
|
57
|
+
try:
|
|
58
|
+
ready, _, _ = select([sys.stdin], [], [], 1 / 60)
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
return
|
|
61
|
+
if ready:
|
|
62
|
+
try:
|
|
63
|
+
ReadKey()
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
pass
|
|
66
|
+
return
|
|
67
|
+
finally:
|
|
68
|
+
termios.tcsetattr(filedescriptor, termios.TCSADRAIN, original)
|
|
69
|
+
Clear()
|
|
70
|
+
sys.stdout.write(ShowCursor)
|
|
71
|
+
sys.stdout.flush()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def MutateShell() -> Dict[str, str]:
|
|
75
|
+
state = {"environment": "Den", "depth": "12321", "mutation": "1", "head": "A"}
|
|
76
|
+
fieldstep = 0
|
|
77
|
+
awakensock: Optional[socket.socket] = None
|
|
78
|
+
awakeheads: Set[str] = set()
|
|
79
|
+
expectedheads: Set[str] = set()
|
|
80
|
+
awakevalue = ""
|
|
81
|
+
awakenready = False
|
|
82
|
+
lastawakesent = 0.0
|
|
83
|
+
|
|
84
|
+
def CurrentHeads() -> List[str]:
|
|
85
|
+
return BaseHeads[:int(state["mutation"])]
|
|
86
|
+
|
|
87
|
+
def ClampHead() -> None:
|
|
88
|
+
heads = CurrentHeads()
|
|
89
|
+
if state["head"] not in heads:
|
|
90
|
+
state["head"] = heads[0]
|
|
91
|
+
|
|
92
|
+
def Network() -> Tuple[int, List[Tuple[str, int]]]:
|
|
93
|
+
heads = CurrentHeads()
|
|
94
|
+
depth = int(state["depth"])
|
|
95
|
+
return BuildDen(heads, depth, state["head"]) if state["environment"] == "Den" else BuildSwamp(heads, depth, state["head"])
|
|
96
|
+
|
|
97
|
+
def AwakeSend() -> None:
|
|
98
|
+
if awakensock is None:
|
|
99
|
+
return
|
|
100
|
+
message = json.dumps({"type": "AWAKE", "head": state["head"], "heads": CurrentHeads()}, separators=(",", ":")).encode("utf-8")
|
|
101
|
+
_, peers = Network()
|
|
102
|
+
for host, peerport in peers:
|
|
103
|
+
try:
|
|
104
|
+
awakensock.sendto(message, (host, peerport))
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def OpenAwakening() -> None:
|
|
109
|
+
nonlocal awakensock, awakeheads, expectedheads, awakevalue, awakenready, lastawakesent
|
|
110
|
+
global RosterCache
|
|
111
|
+
if awakensock is not None:
|
|
112
|
+
return
|
|
113
|
+
port, _ = Network()
|
|
114
|
+
awakensock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
115
|
+
awakensock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
116
|
+
if state["environment"] == "Swamp":
|
|
117
|
+
awakensock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
118
|
+
try:
|
|
119
|
+
awakensock.bind(("0.0.0.0", port))
|
|
120
|
+
except OSError as exc:
|
|
121
|
+
try:
|
|
122
|
+
awakensock.close()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
awakensock = None
|
|
126
|
+
raise ExitSignal() from exc
|
|
127
|
+
awakensock.setblocking(False)
|
|
128
|
+
expectedheads = set(CurrentHeads())
|
|
129
|
+
awakeheads = {item for item in RosterCache if item in expectedheads}
|
|
130
|
+
awakeheads.add(state["head"])
|
|
131
|
+
RosterCache = set(awakeheads)
|
|
132
|
+
awakevalue = AwakeField(awakeheads)
|
|
133
|
+
awakenready = False
|
|
134
|
+
lastawakesent = 0.0
|
|
135
|
+
for _ in range(3):
|
|
136
|
+
AwakeSend()
|
|
137
|
+
lastawakesent = Now()
|
|
138
|
+
|
|
139
|
+
def CloseAwakening() -> None:
|
|
140
|
+
nonlocal awakensock, awakeheads, expectedheads, awakevalue, awakenready, lastawakesent
|
|
141
|
+
global RosterCache
|
|
142
|
+
if awakeheads:
|
|
143
|
+
RosterCache = set(awakeheads)
|
|
144
|
+
if awakensock is not None:
|
|
145
|
+
try:
|
|
146
|
+
awakensock.close()
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
awakensock = None
|
|
150
|
+
awakeheads = set()
|
|
151
|
+
expectedheads = set()
|
|
152
|
+
awakevalue = ""
|
|
153
|
+
awakenready = False
|
|
154
|
+
lastawakesent = 0.0
|
|
155
|
+
|
|
156
|
+
def PollAwakening() -> None:
|
|
157
|
+
nonlocal awakevalue, awakenready, lastawakesent, expectedheads, awakeheads
|
|
158
|
+
if awakensock is None:
|
|
159
|
+
return
|
|
160
|
+
now = Now()
|
|
161
|
+
if not awakenready and now - lastawakesent >= AwakeInterval:
|
|
162
|
+
AwakeSend()
|
|
163
|
+
lastawakesent = now
|
|
164
|
+
while True:
|
|
165
|
+
try:
|
|
166
|
+
data, _ = awakensock.recvfrom(1024)
|
|
167
|
+
except BlockingIOError:
|
|
168
|
+
break
|
|
169
|
+
except Exception:
|
|
170
|
+
break
|
|
171
|
+
try:
|
|
172
|
+
message = json.loads(data.decode("utf-8"))
|
|
173
|
+
except Exception:
|
|
174
|
+
continue
|
|
175
|
+
if not isinstance(message, dict):
|
|
176
|
+
continue
|
|
177
|
+
incoming = str(message.get("head", "") or "").strip().upper()
|
|
178
|
+
incomingheads = {str(item).upper() for item in list(message.get("heads", []) or []) if str(item).strip()}
|
|
179
|
+
messagetype = str(message.get("type", "") or "")
|
|
180
|
+
if messagetype == "AWAKE":
|
|
181
|
+
if incomingheads:
|
|
182
|
+
expectedheads = set(incomingheads)
|
|
183
|
+
if incoming and incoming not in awakeheads:
|
|
184
|
+
awakeheads.add(incoming)
|
|
185
|
+
AwakeSend()
|
|
186
|
+
lastawakesent = Now()
|
|
187
|
+
continue
|
|
188
|
+
if messagetype == "ROSTER":
|
|
189
|
+
if incomingheads:
|
|
190
|
+
expectedheads = set(incomingheads)
|
|
191
|
+
awakeheads.update(incomingheads)
|
|
192
|
+
if incoming:
|
|
193
|
+
awakeheads.add(incoming)
|
|
194
|
+
awakevalue = AwakeField(awakeheads)
|
|
195
|
+
awakenready = bool(expectedheads) and awakeheads >= expectedheads
|
|
196
|
+
|
|
197
|
+
filedescriptor = sys.stdin.fileno()
|
|
198
|
+
original = termios.tcgetattr(filedescriptor)
|
|
199
|
+
tty.setcbreak(filedescriptor)
|
|
200
|
+
start = Now()
|
|
201
|
+
lastpulse = None
|
|
202
|
+
lastfield = None
|
|
203
|
+
lastvalue = None
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
sys.stdout.write(HideCursor)
|
|
207
|
+
Clear()
|
|
208
|
+
while True:
|
|
209
|
+
field = Fields[fieldstep]
|
|
210
|
+
if field == "awakening":
|
|
211
|
+
OpenAwakening()
|
|
212
|
+
PollAwakening()
|
|
213
|
+
phase = Phase(start)
|
|
214
|
+
value = awakevalue if field == "awakening" else state[field]
|
|
215
|
+
pulse = Index(phase, 9)
|
|
216
|
+
if pulse != lastpulse or field != lastfield or value != lastvalue:
|
|
217
|
+
RenderField("Mutate", Labels[field], value, phase)
|
|
218
|
+
lastpulse = pulse
|
|
219
|
+
lastfield = field
|
|
220
|
+
lastvalue = value
|
|
221
|
+
if field == "awakening" and awakenready:
|
|
222
|
+
CloseAwakening()
|
|
223
|
+
return state
|
|
224
|
+
try:
|
|
225
|
+
ready, _, _ = select([sys.stdin], [], [], 1 / 60)
|
|
226
|
+
except KeyboardInterrupt:
|
|
227
|
+
raise ExitSignal
|
|
228
|
+
if not ready:
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
key = ReadKey()
|
|
232
|
+
except KeyboardInterrupt:
|
|
233
|
+
raise ExitSignal
|
|
234
|
+
field = Fields[fieldstep]
|
|
235
|
+
if key == "\x03" or field == "awakening":
|
|
236
|
+
raise ExitSignal
|
|
237
|
+
if key in ("\n", "\r", "C"):
|
|
238
|
+
fieldstep = min(len(Fields) - 1, fieldstep + 1)
|
|
239
|
+
continue
|
|
240
|
+
if key == "D":
|
|
241
|
+
fieldstep = max(0, fieldstep - 1)
|
|
242
|
+
continue
|
|
243
|
+
if key in ("A", "B"):
|
|
244
|
+
direction = 1 if key == "A" else -1
|
|
245
|
+
if field == "depth":
|
|
246
|
+
state["depth"] = f"{max(0, min(99999, int(state['depth']) + direction)):05d}"
|
|
247
|
+
continue
|
|
248
|
+
fieldoptions = Options.get(field)
|
|
249
|
+
if fieldoptions is not None:
|
|
250
|
+
index = fieldoptions.index(state[field])
|
|
251
|
+
if field == "head":
|
|
252
|
+
direction = -direction
|
|
253
|
+
state[field] = fieldoptions[(index + direction) % len(fieldoptions)]
|
|
254
|
+
if field == "mutation":
|
|
255
|
+
ClampHead()
|
|
256
|
+
continue
|
|
257
|
+
if not key.isprintable():
|
|
258
|
+
continue
|
|
259
|
+
if field == "mutation" and key in "12345":
|
|
260
|
+
state["mutation"] = key
|
|
261
|
+
ClampHead()
|
|
262
|
+
elif field == "head":
|
|
263
|
+
typed = key.upper()
|
|
264
|
+
if typed in BaseHeads:
|
|
265
|
+
state["head"] = typed
|
|
266
|
+
ClampHead()
|
|
267
|
+
finally:
|
|
268
|
+
CloseAwakening()
|
|
269
|
+
termios.tcsetattr(filedescriptor, termios.TCSADRAIN, original)
|
|
270
|
+
Clear()
|
|
271
|
+
sys.stdout.write(ShowCursor)
|
|
272
|
+
sys.stdout.flush()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def Mutate() -> None:
|
|
276
|
+
try:
|
|
277
|
+
state = MutateShell()
|
|
278
|
+
heads = BaseHeads[:int(state["mutation"])]
|
|
279
|
+
head = state["head"] if state["head"] in heads else heads[0]
|
|
280
|
+
depth = int(state["depth"])
|
|
281
|
+
port, peers = BuildDen(heads, depth, head) if state["environment"] == "Den" else BuildSwamp(heads, depth, head)
|
|
282
|
+
RunBody(heart=Plexus(head=head, heads=heads), head=head, port=port, peers=peers, heads=heads)
|
|
283
|
+
except ExitSignal:
|
|
284
|
+
ExitScreen()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
Mutate()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Dict, List, Optional, Literal
|
|
4
|
+
|
|
5
|
+
Gems = {1: "Onyx", 2: "Jade", 3: "Opal"}
|
|
6
|
+
|
|
7
|
+
def GemName(g: int) -> str:
|
|
8
|
+
return Gems.get(int(g or 1), "G?")
|
|
9
|
+
|
|
10
|
+
def CrownNext(c: int) -> int:
|
|
11
|
+
return 1 if int(c) >= 3 else int(c) + 1
|
|
12
|
+
|
|
13
|
+
IntentType = Literal["Propagate", "RequestSync", "Envy"]
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Intent:
|
|
17
|
+
type: IntentType
|
|
18
|
+
payload: Dict[str, Any]
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Tetron:
|
|
22
|
+
tallies: Dict[str, int]
|
|
23
|
+
|
|
24
|
+
def Snapshot(self) -> Dict[str, Any]:
|
|
25
|
+
return {
|
|
26
|
+
"tallies": dict(self.tallies),
|
|
27
|
+
"is_dream": True,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PlexusState:
|
|
32
|
+
tallies: Dict[str, int]
|
|
33
|
+
crown: int
|
|
34
|
+
head: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
class Plexus:
|
|
37
|
+
def __init__(self, head: str, heads: List[str]) -> None:
|
|
38
|
+
self.head = str(head)
|
|
39
|
+
self.heads = list(heads)
|
|
40
|
+
|
|
41
|
+
tallies = {h: 10 for h in self.heads}
|
|
42
|
+
|
|
43
|
+
self.tetron = Tetron(tallies=dict(tallies))
|
|
44
|
+
self.state = PlexusState(
|
|
45
|
+
tallies=dict(tallies),
|
|
46
|
+
crown=1,
|
|
47
|
+
head=None,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.tail: Optional[Dict[str, Any]] = None
|
|
51
|
+
self.envy: bool = False
|
|
52
|
+
|
|
53
|
+
def Snapshot(self) -> Dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"head": self.head,
|
|
56
|
+
"tallies": dict(self.state.tallies),
|
|
57
|
+
"crown": int(self.state.crown),
|
|
58
|
+
"is_dream": False,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def Emotions(self) -> Dict[str, Any]:
|
|
62
|
+
return {"envy": bool(self.envy)}
|
|
63
|
+
|
|
64
|
+
def DreamState(self) -> Dict[str, Any]:
|
|
65
|
+
d = self.tetron.Snapshot()
|
|
66
|
+
d["crown"] = int(self.state.crown)
|
|
67
|
+
return d
|
|
68
|
+
|
|
69
|
+
def EnvyReanchor(self) -> Dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"head": self.head,
|
|
72
|
+
"tallies": dict(self.tetron.tallies),
|
|
73
|
+
"crown": int(self.state.crown),
|
|
74
|
+
"is_dream": True,
|
|
75
|
+
"mode": "Envy",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def ExpectedTotal(self) -> int:
|
|
79
|
+
return 10 * len(self.heads)
|
|
80
|
+
|
|
81
|
+
def TalliesTotal(self, tallies: Dict[str, Any]) -> int:
|
|
82
|
+
return sum(int(value or 0) for value in dict(tallies or {}).values())
|
|
83
|
+
|
|
84
|
+
def ValidTotal(self, tallies: Dict[str, Any]) -> bool:
|
|
85
|
+
return self.TalliesTotal(tallies) == self.ExpectedTotal()
|
|
86
|
+
|
|
87
|
+
def Propose(self, tohead: str, amount: int) -> Dict[str, Any]:
|
|
88
|
+
if self.envy:
|
|
89
|
+
return self.EnvyReanchor()
|
|
90
|
+
|
|
91
|
+
tallies = dict(self.state.tallies)
|
|
92
|
+
tallies[self.head] = tallies.get(self.head, 0) - int(amount)
|
|
93
|
+
tallies[tohead] = tallies.get(tohead, 0) + int(amount)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"head": self.head,
|
|
97
|
+
"tallies": tallies,
|
|
98
|
+
"crown": CrownNext(self.state.crown),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def Ingest(self, tailin: Dict[str, Any]) -> List[Intent]:
|
|
102
|
+
intents: List[Intent] = []
|
|
103
|
+
|
|
104
|
+
if tailin.get("is_dream"):
|
|
105
|
+
dreamtallies = dict(tailin.get("tallies", {}) or {})
|
|
106
|
+
|
|
107
|
+
# ================= LINCHPIN ================= #
|
|
108
|
+
if dreamtallies and not self.ValidTotal(dreamtallies):
|
|
109
|
+
# ============================================ #
|
|
110
|
+
if not self.envy:
|
|
111
|
+
self.envy = True
|
|
112
|
+
intents.append(Intent("Envy", {
|
|
113
|
+
"expectedtotal": self.ExpectedTotal(),
|
|
114
|
+
"incomingtotal": self.TalliesTotal(dreamtallies),
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
intents.append(Intent("RequestSync", {
|
|
118
|
+
"crown": int(self.state.crown),
|
|
119
|
+
"gem": GemName(self.state.crown),
|
|
120
|
+
"needtail": True,
|
|
121
|
+
}))
|
|
122
|
+
return intents
|
|
123
|
+
|
|
124
|
+
if dreamtallies and dreamtallies != self.state.tallies:
|
|
125
|
+
self.state.tallies = dict(dreamtallies)
|
|
126
|
+
self.tetron.tallies = dict(dreamtallies)
|
|
127
|
+
|
|
128
|
+
if self.envy:
|
|
129
|
+
self.envy = False
|
|
130
|
+
|
|
131
|
+
return intents
|
|
132
|
+
|
|
133
|
+
incomingtallies = dict(tailin.get("tallies", {}))
|
|
134
|
+
inccrown = int(tailin.get("crown", self.state.crown))
|
|
135
|
+
|
|
136
|
+
cur = int(self.state.crown)
|
|
137
|
+
exp = CrownNext(cur)
|
|
138
|
+
|
|
139
|
+
# ================= LINCHPIN ================= #
|
|
140
|
+
if inccrown not in (cur, exp):
|
|
141
|
+
# ============================================ #
|
|
142
|
+
if not self.envy:
|
|
143
|
+
self.envy = True
|
|
144
|
+
intents.append(Intent("Envy", {
|
|
145
|
+
"currentcrown": cur,
|
|
146
|
+
"incomingcrown": inccrown,
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
intents.append(Intent("RequestSync", {
|
|
150
|
+
"crown": cur,
|
|
151
|
+
"gem": GemName(cur),
|
|
152
|
+
}))
|
|
153
|
+
return intents
|
|
154
|
+
|
|
155
|
+
# ================= LINCHPIN ================= #
|
|
156
|
+
if not self.ValidTotal(incomingtallies):
|
|
157
|
+
# ============================================ #
|
|
158
|
+
if not self.envy:
|
|
159
|
+
self.envy = True
|
|
160
|
+
intents.append(Intent("Envy", {
|
|
161
|
+
"expectedtotal": self.ExpectedTotal(),
|
|
162
|
+
"incomingtotal": self.TalliesTotal(incomingtallies),
|
|
163
|
+
}))
|
|
164
|
+
|
|
165
|
+
intents.append(Intent("RequestSync", {
|
|
166
|
+
"crown": cur,
|
|
167
|
+
"gem": GemName(cur),
|
|
168
|
+
"needtail": True,
|
|
169
|
+
}))
|
|
170
|
+
return intents
|
|
171
|
+
|
|
172
|
+
if self.envy:
|
|
173
|
+
self.envy = False
|
|
174
|
+
|
|
175
|
+
if incomingtallies == self.state.tallies:
|
|
176
|
+
return intents
|
|
177
|
+
|
|
178
|
+
self.state.tallies = dict(incomingtallies)
|
|
179
|
+
self.state.crown = inccrown
|
|
180
|
+
self.state.head = str(tailin.get("head", "")) or None
|
|
181
|
+
|
|
182
|
+
self.tetron.tallies = dict(incomingtallies)
|
|
183
|
+
self.tail = dict(tailin)
|
|
184
|
+
|
|
185
|
+
intents.append(Intent("Propagate", {"tail": dict(self.tail)}))
|
|
186
|
+
|
|
187
|
+
if inccrown == exp:
|
|
188
|
+
intents.append(Intent("RequestSync", {
|
|
189
|
+
"crown": inccrown,
|
|
190
|
+
"gem": GemName(inccrown),
|
|
191
|
+
}))
|
|
192
|
+
|
|
193
|
+
return intents
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from typing import Iterable, Sequence, Set, Tuple
|
|
7
|
+
|
|
8
|
+
Reset = "\x1b[0m"
|
|
9
|
+
Ash = "\x1b[90m"
|
|
10
|
+
Blue = "\x1b[36m"
|
|
11
|
+
Green = "\x1b[92m"
|
|
12
|
+
Teal = "\x1b[38;2;0;150;130m"
|
|
13
|
+
HideCursor = "\x1b[?25l"
|
|
14
|
+
ShowCursor = "\x1b[?25h"
|
|
15
|
+
AnsiPattern = re.compile(r"\x1b\[[0-9;]*m")
|
|
16
|
+
PulseRate = 7.0
|
|
17
|
+
|
|
18
|
+
BubbleFrames = [
|
|
19
|
+
"",
|
|
20
|
+
"0",
|
|
21
|
+
"o0o",
|
|
22
|
+
".o0o.",
|
|
23
|
+
"o.o0o.o",
|
|
24
|
+
"0o.o0o.o0",
|
|
25
|
+
"o0o.o0o.o0o",
|
|
26
|
+
".o0o.o0o.o0o.",
|
|
27
|
+
"..o0o.o0o.o0o..",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
ExitFrames = ["", ".", "..", "..."]
|
|
31
|
+
|
|
32
|
+
def Now() -> float:
|
|
33
|
+
return time.monotonic()
|
|
34
|
+
|
|
35
|
+
def Step(rate: float = PulseRate) -> int:
|
|
36
|
+
return int(Now() * float(rate or 0.0))
|
|
37
|
+
|
|
38
|
+
def Phase(start: float, speed: float = PulseRate) -> float:
|
|
39
|
+
return (Now() - float(start or 0.0)) * float(speed or 0.0)
|
|
40
|
+
|
|
41
|
+
def Index(phase: float, frames: int) -> int:
|
|
42
|
+
if frames <= 1:
|
|
43
|
+
return 0
|
|
44
|
+
span = (frames * 2) - 2
|
|
45
|
+
index = int(phase) % span
|
|
46
|
+
if index >= frames:
|
|
47
|
+
index = span - index
|
|
48
|
+
return index
|
|
49
|
+
|
|
50
|
+
def FlickerCycle(sequence: Sequence[str], rate: float = PulseRate, fallback: str = Teal) -> str:
|
|
51
|
+
if not sequence:
|
|
52
|
+
return fallback
|
|
53
|
+
return sequence[Step(rate) % len(sequence)]
|
|
54
|
+
|
|
55
|
+
def Flicker1() -> str:
|
|
56
|
+
return FlickerCycle((Green, Blue, Teal))
|
|
57
|
+
|
|
58
|
+
def Flicker2() -> str:
|
|
59
|
+
return FlickerCycle((Green, Teal))
|
|
60
|
+
|
|
61
|
+
def Flicker3() -> str:
|
|
62
|
+
return FlickerCycle((Teal, Blue))
|
|
63
|
+
|
|
64
|
+
def Flicker4() -> str:
|
|
65
|
+
return FlickerCycle((Blue, Teal))
|
|
66
|
+
|
|
67
|
+
def VisibleLength(text: str) -> int:
|
|
68
|
+
return len(AnsiPattern.sub("", str(text or "")))
|
|
69
|
+
|
|
70
|
+
def PadLine(text: str, width: int) -> str:
|
|
71
|
+
built = str(text or "")
|
|
72
|
+
visible = VisibleLength(built)
|
|
73
|
+
if visible < width:
|
|
74
|
+
built += " " * (width - visible)
|
|
75
|
+
return built
|
|
76
|
+
|
|
77
|
+
def Center(text: str, width: int = 80) -> str:
|
|
78
|
+
line = str(text or "")
|
|
79
|
+
gap = max(0, (width - VisibleLength(line)) // 2)
|
|
80
|
+
built = (" " * gap) + line
|
|
81
|
+
visible = VisibleLength(built)
|
|
82
|
+
if visible < width:
|
|
83
|
+
built += " " * (width - visible)
|
|
84
|
+
return built
|
|
85
|
+
|
|
86
|
+
def TerminalSize() -> Tuple[int, int]:
|
|
87
|
+
size = shutil.get_terminal_size(fallback=(80, 24))
|
|
88
|
+
return size.columns, size.lines
|
|
89
|
+
|
|
90
|
+
def VerticalOffset(lines: int, height: int = 24, bias: float = 0.35) -> int:
|
|
91
|
+
return max(0, int((height - lines) * bias))
|
|
92
|
+
|
|
93
|
+
def Clear() -> None:
|
|
94
|
+
sys.stdout.write("\x1b[2J\x1b[H")
|
|
95
|
+
sys.stdout.flush()
|
|
96
|
+
|
|
97
|
+
def CursorLeft(count: int) -> str:
|
|
98
|
+
return f"\x1b[{count}D" if count > 0 else ""
|
|
99
|
+
|
|
100
|
+
def ReadKey() -> str:
|
|
101
|
+
key = sys.stdin.read(1)
|
|
102
|
+
if key == "\x1b" and sys.stdin.read(1) in ("[", "O"):
|
|
103
|
+
return sys.stdin.read(1)
|
|
104
|
+
return key
|
|
105
|
+
|
|
106
|
+
def RenderCentered(lines: Iterable[str], *, width: int = 80, minimumheight: int = 24, bias: float = 0.35) -> None:
|
|
107
|
+
linelist = list(lines)
|
|
108
|
+
_, terminalheight = TerminalSize()
|
|
109
|
+
height = max(minimumheight, terminalheight)
|
|
110
|
+
topgap = VerticalOffset(len(linelist), height, bias)
|
|
111
|
+
sys.stdout.write("\x1b[H")
|
|
112
|
+
if topgap:
|
|
113
|
+
sys.stdout.write("\n" * topgap)
|
|
114
|
+
sys.stdout.write("\n".join(Center(line, width) for line in linelist))
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
|
|
117
|
+
def TitleLine(text: str) -> str:
|
|
118
|
+
return f"{Green}.:{Reset}{Teal}{text}{Reset}{Green}:.{Reset}"
|
|
119
|
+
|
|
120
|
+
def LabelLine(text: str) -> str:
|
|
121
|
+
return f"{Ash}{text}{Reset}"
|
|
122
|
+
|
|
123
|
+
def DotField(value: str) -> str:
|
|
124
|
+
return f"{Green}.{Reset}{Teal}{value}{Reset}{Green}.{Reset}"
|
|
125
|
+
|
|
126
|
+
def AwakeField(heads: Set[str]) -> str:
|
|
127
|
+
return "" if not heads else f"{Teal}" + f"{Reset}{Green}.{Reset}{Teal}".join(sorted(heads))
|
|
128
|
+
|
|
129
|
+
def RenderField(title: str, label: str, value: str, phase: float, bias: float = 0.35) -> None:
|
|
130
|
+
RenderCentered([
|
|
131
|
+
TitleLine(title),
|
|
132
|
+
"",
|
|
133
|
+
BubbleLine(phase),
|
|
134
|
+
LabelLine(label),
|
|
135
|
+
DotField(value),
|
|
136
|
+
], bias=bias)
|
|
137
|
+
|
|
138
|
+
def BubbleLine(phase: float) -> str:
|
|
139
|
+
return f"{Ash}{BubbleFrames[Index(phase, len(BubbleFrames))]}{Reset}"
|
|
140
|
+
|
|
141
|
+
def ExitLine(phase: float) -> str:
|
|
142
|
+
dots = ExitFrames[Index(phase, len(ExitFrames))]
|
|
143
|
+
message = f"{Teal}Sniff{Green}.{Teal}Snort{Green}..{Teal}RAWR{Green}...{Teal}bye{Reset}"
|
|
144
|
+
return f"{Green}{dots}{Reset}{Teal}{message}{Reset}{Green}{dots}{Reset}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Hydra.py
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
Game/Body.py
|
|
5
|
+
Game/Mutate.py
|
|
6
|
+
Game/Plexus.py
|
|
7
|
+
Game/Pulse.py
|
|
8
|
+
Hydra_Game.egg-info/PKG-INFO
|
|
9
|
+
Hydra_Game.egg-info/SOURCES.txt
|
|
10
|
+
Hydra_Game.egg-info/dependency_links.txt
|
|
11
|
+
Hydra_Game.egg-info/entry_points.txt
|
|
12
|
+
Hydra_Game.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 🐍 Hydra 🐍
|
|
2
|
+
|
|
3
|
+
**Hands-on network toy where packets collide and state flows.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<img src="../Relics/Awake.gif"/>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🪵 Swamp Thing
|
|
12
|
+
|
|
13
|
+
In **Hydra**, the state is *fluid*. Nodes can leave, rejoin, and rehydrate instantly with almost no ceremony. Move between a **den** or a **swamp**, across machines or terminals.
|
|
14
|
+
|
|
15
|
+
As long as your **head is unique** and you share the same **depth**, your node will simply **snap to what is**. *What holds is what remains.*
|
|
16
|
+
|
|
17
|
+
Hydra is a distributed expression of the **Oblivious Compute system**. It is not a coordinated network, but a field of independent nodes sharing a single **admissible state**. Each node emits and each node observes, and what persists is simply what the network accepts.
|
|
18
|
+
|
|
19
|
+
**There is no leader and no history—only convergence.**
|
|
20
|
+
|
|
21
|
+
> Simply put, this is a distributed packet collider in under a thousand lines of code.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ⬇️ Install
|
|
26
|
+
|
|
27
|
+
To run Hydra, install it with:
|
|
28
|
+
```bash
|
|
29
|
+
pipx install Hydra-Braid
|
|
30
|
+
Hydra
|
|
31
|
+
```
|
|
32
|
+
You’ll need **Python 3.9 or newer** and an **80x24 UNIX-like terminal environment.**
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 🐧 Operating System Support
|
|
37
|
+
|
|
38
|
+
- ✅ Linux
|
|
39
|
+
- ✅ macOS
|
|
40
|
+
- ❌ Windows *(sorry, but not sorry)*
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🌐 Networking
|
|
45
|
+
|
|
46
|
+
Hydra runs in two modes.
|
|
47
|
+
|
|
48
|
+
**Den** is local—multiple terminals on the same machine. *(sandbox)*
|
|
49
|
+
**Swamp** runs across a LAN, allowing multiple machines to share the same **Braid**.
|
|
50
|
+
|
|
51
|
+
> *All nodes must use the same depth (port), and the same number of mutated heads.*
|
|
52
|
+
> *Each node must use a unique head.*
|
|
53
|
+
|
|
54
|
+
<img src="../Relics/Spam.gif"/>
|
|
55
|
+
|
|
56
|
+
> *Run Hydra in a swamp if you want to use it as a packet collider.*
|
|
57
|
+
> *Push state with multiple machines and see what holds.*
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
🎯 **Intent**
|
|
62
|
+
|
|
63
|
+
The Hydra Demo has been published as a **public technical disclosure**.
|
|
64
|
+
|
|
65
|
+
This demo exists to show that **oblivious convergence through an admissability gate** is possible.
|
|
66
|
+
|
|
67
|
+
If it fails, it fails cleanly.
|
|
68
|
+
If it works, it demonstrates a **new computational primitive**.
|
|
69
|
+
|
|
70
|
+
<img src="../Relics/Alpha.png" width="400"/>
|
|
71
|
+
|
|
72
|
+
<img src="../Relics/Bye.gif" width="400"/>
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📜 License
|
|
77
|
+
|
|
78
|
+
This project is released under the terms of the [LICENSE](../LICENSE).
|
|
79
|
+
|
|
80
|
+
Use it, study it, modify it—just respect the terms outlined there.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "Hydra-Game"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Terminal entrypoint for Hydra"
|
|
5
|
+
|
|
6
|
+
[project.scripts]
|
|
7
|
+
Hydra = "Hydra:main"
|
|
8
|
+
|
|
9
|
+
[tool.setuptools]
|
|
10
|
+
py-modules = ["Hydra"]
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages.find]
|
|
13
|
+
where = ["."]
|
|
14
|
+
include = ["Game"]
|