Cerberus-Game 0.1.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.
@@ -0,0 +1,5 @@
1
+ from Game.Guardian import Run
2
+
3
+
4
+ if __name__ == "__main__":
5
+ Run()
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: Cerberus-Game
3
+ Version: 0.1.0
4
+ Summary: Terminal entrypoint for Cerberus
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: cryptography
@@ -0,0 +1,13 @@
1
+ Cerberus.py
2
+ README.md
3
+ pyproject.toml
4
+ Cerberus_Game.egg-info/PKG-INFO
5
+ Cerberus_Game.egg-info/SOURCES.txt
6
+ Cerberus_Game.egg-info/dependency_links.txt
7
+ Cerberus_Game.egg-info/entry_points.txt
8
+ Cerberus_Game.egg-info/requires.txt
9
+ Cerberus_Game.egg-info/top_level.txt
10
+ Game/BoneYard.py
11
+ Game/Catacomb.py
12
+ Game/Guardian.py
13
+ Game/__init__.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ Cerberus = Cerberus:main
@@ -0,0 +1 @@
1
+ cryptography
@@ -0,0 +1,2 @@
1
+ Cerberus
2
+ Game
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import asdict
5
+ import hashlib
6
+ import json
7
+ import socket
8
+ from typing import Callable, Iterable, Optional
9
+
10
+ from .Catacomb import (
11
+ Bone,
12
+ BonePile,
13
+ BonePileHash,
14
+ Head,
15
+ Result,
16
+ Tag,
17
+ VerifyDigest,
18
+ )
19
+
20
+ Host = "127.0.0.1"
21
+ BonePilePort = 9000
22
+ Burst = 3
23
+ CacheLimit = 4096
24
+
25
+
26
+ def BoneKey(bone: Bone) -> tuple[object, ...]:
27
+ return (bone.head, bone.key, bone.target, bone.bones, bone.tag.parent, bone.tag.child, bone.locksign, bone.sign)
28
+
29
+
30
+ def BoneToWire(bone: Bone) -> dict[str, object]:
31
+ return asdict(bone)
32
+
33
+
34
+ def BoneFromWire(value: object) -> Bone:
35
+ if not isinstance(value, dict) or not isinstance(value.get("tag"), dict):
36
+ raise ValueError("Bone has bad shape")
37
+ tag = value["tag"]
38
+ return Bone(
39
+ head=str(value.get("head", "")).upper(),
40
+ key=str(value.get("key", "")),
41
+ target=str(value.get("target", "")).upper(),
42
+ bones=int(value.get("bones", 0)),
43
+ tag=Tag(str(tag.get("parent", "")), str(tag.get("child", ""))),
44
+ locksign=str(value.get("locksign", "")),
45
+ sign=str(value.get("sign", "")),
46
+ )
47
+
48
+
49
+ def HeadToWire(cell: Head) -> dict[str, object]:
50
+ return asdict(cell)
51
+
52
+
53
+ def HeadFromWire(value: object) -> Head:
54
+ if not isinstance(value, dict) or not isinstance(value.get("tag"), dict):
55
+ raise ValueError("BonePile Head has bad shape")
56
+ tag = value["tag"]
57
+ receipts = value.get("receipts", [])
58
+ if not isinstance(receipts, (list, tuple)) or len(receipts) > 2:
59
+ raise ValueError("BonePile Head has bad receipts")
60
+ return Head(
61
+ head=str(value.get("head", "")).upper(),
62
+ key=str(value.get("key", "")),
63
+ bones=int(value.get("bones", -1)),
64
+ tag=Tag(str(tag.get("parent", "")), str(tag.get("child", ""))),
65
+ locksign=str(value.get("locksign", "")),
66
+ receipts=tuple(BoneFromWire(item) for item in receipts),
67
+ clawcount=value.get("clawcount"),
68
+ )
69
+
70
+
71
+ def BonePileToWire(pile: BonePile) -> dict[str, object]:
72
+ return {head: HeadToWire(cell) for head, cell in pile.items()}
73
+
74
+
75
+ def BonePileFromWire(value: object, heads: Iterable[str]) -> BonePile:
76
+ expected = tuple(heads)
77
+ if not isinstance(value, dict) or set(value) != set(expected):
78
+ raise ValueError("BonePile has the wrong heads")
79
+ pile = {head: HeadFromWire(value[head]) for head in expected}
80
+ if any(pile[head].head != head for head in expected):
81
+ raise ValueError("BonePile Cell label does not match its slot")
82
+ return pile
83
+
84
+
85
+ class BoneYard:
86
+
87
+ def __init__(
88
+ self,
89
+ ring: str,
90
+ *,
91
+ HeadCountIn: Optional[Callable[[object], bool]] = None,
92
+ NoticeOut: Optional[Callable[[str], None]] = None,
93
+ ) -> None:
94
+ self.mask = hashlib.sha256(str(ring).encode("utf-8")).digest()
95
+ self.HeadCountIn = HeadCountIn
96
+ self.NoticeOut = NoticeOut
97
+
98
+ self.mouthcount = 0
99
+ self.bindport: Optional[int] = None
100
+ self.sock: Optional[socket.socket] = None
101
+
102
+ self.heads: tuple[str, ...] = ()
103
+ self.expected: set[str] = set()
104
+ self.count = 0
105
+ self.head = ""
106
+ self.ready = False
107
+
108
+ self.CatacombIn: Optional[Callable[[Bone], Result]] = None
109
+ self.BonePileIn: Optional[Callable[[BonePile, str], Result]] = None
110
+ self.BonePileOut: Optional[Callable[[], BonePile]] = None
111
+ self.BonePileSignOut: Optional[Callable[[BonePile], str]] = None
112
+
113
+ self.seenorder: deque[tuple[object, ...]] = deque()
114
+ self.seen: set[tuple[object, ...]] = set()
115
+
116
+ def Open(self, count: int) -> None:
117
+ if self.sock is not None:
118
+ return
119
+ self.mouthcount = max(1, int(count))
120
+ lasterror: Optional[Exception] = None
121
+ for port in self.Mouths():
122
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
123
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20)
124
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
125
+ try:
126
+ sock.bind((Host, port))
127
+ sock.setblocking(False)
128
+ self.bindport = port
129
+ self.sock = sock
130
+ return
131
+ except OSError as exc:
132
+ lasterror = exc
133
+ sock.close()
134
+ raise RuntimeError(f"No clean mouth available in {self.Mouths()}.") from lasterror
135
+
136
+ def Mouths(self) -> list[int]:
137
+ return [BonePilePort + index for index in range(self.mouthcount)]
138
+
139
+ def Peers(self) -> list[int]:
140
+ return [port for port in self.Mouths() if port != self.bindport]
141
+
142
+ def Close(self) -> None:
143
+ sock = self.sock
144
+ self.sock = None
145
+ if sock is None:
146
+ return
147
+ try:
148
+ sock.close()
149
+ except Exception:
150
+ pass
151
+
152
+ def Encrypt(self, message: dict[str, object]) -> bytes:
153
+ body = json.dumps(message, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
154
+ return bytes(byte ^ self.mask[index % len(self.mask)] for index, byte in enumerate(body))
155
+
156
+ def Decrypt(self, raw: bytes) -> dict[str, object]:
157
+ body = bytes(byte ^ self.mask[index % len(self.mask)] for index, byte in enumerate(raw))
158
+ message = json.loads(body.decode("utf-8"))
159
+ if not isinstance(message, dict):
160
+ raise TypeError("packet must decode to dict")
161
+ return message
162
+
163
+ def Send(self, message: dict[str, object]) -> None:
164
+ if self.sock is None:
165
+ return
166
+ raw = self.Encrypt(message)
167
+ for port in self.Peers():
168
+ for _shot in range(Burst):
169
+ try:
170
+ self.sock.sendto(raw, (Host, port))
171
+ except OSError:
172
+ pass
173
+
174
+ def Receive(self) -> list[dict[str, object]]:
175
+ if self.sock is None:
176
+ return []
177
+ messages = []
178
+ while True:
179
+ try:
180
+ raw, address = self.sock.recvfrom(65535)
181
+ except (BlockingIOError, OSError):
182
+ break
183
+ if address[0] != Host:
184
+ continue
185
+ try:
186
+ messages.append(self.Decrypt(raw))
187
+ except Exception:
188
+ continue
189
+ return messages
190
+
191
+ def HeadCount(self, headcount: object) -> None:
192
+ self.Send({"type": "HEADCOUNT", "headcount": headcount})
193
+
194
+ def Attach(
195
+ self,
196
+ heads: Iterable[str],
197
+ head: str,
198
+ *,
199
+ CatacombIn: Callable[[Bone], Result],
200
+ BonePileIn: Callable[[BonePile, str], Result],
201
+ BonePileOut: Callable[[], BonePile],
202
+ BonePileSignOut: Callable[[BonePile], str],
203
+ ) -> None:
204
+ heads = tuple(str(item).upper() for item in heads)
205
+ head = str(head).upper()
206
+ if head not in heads:
207
+ raise ValueError("local head is not in this BoneYard")
208
+ self.heads = heads
209
+ self.expected = set(heads)
210
+ self.count = len(heads)
211
+ self.head = head
212
+ self.CatacombIn = CatacombIn
213
+ self.BonePileIn = BonePileIn
214
+ self.BonePileOut = BonePileOut
215
+ self.BonePileSignOut = BonePileSignOut
216
+ self.ready = True
217
+
218
+ def SendBonePile(self, pile: Optional[BonePile] = None) -> None:
219
+ if not self.ready or self.BonePileOut is None or self.BonePileSignOut is None:
220
+ return
221
+ pile = self.BonePileOut() if pile is None else pile
222
+ if set(pile) != self.expected:
223
+ return
224
+ statehash = BonePileHash(self.head, pile)
225
+ self.Send({
226
+ "type": "BONEPILE",
227
+ "count": self.count,
228
+ "head": self.head,
229
+ "statehash": statehash,
230
+ "statesign": self.BonePileSignOut(pile),
231
+ "bonepile": BonePileToWire(pile),
232
+ })
233
+
234
+ def Hunger(self) -> None:
235
+ if self.ready:
236
+ self.Send({"type": "HUNGER", "count": self.count, "head": self.head})
237
+
238
+ def Seen(self, bone: Bone) -> bool:
239
+ return BoneKey(bone) in self.seen
240
+
241
+ def Remember(self, bone: Bone) -> None:
242
+ key = BoneKey(bone)
243
+ if key in self.seen:
244
+ return
245
+ self.seen.add(key)
246
+ self.seenorder.append(key)
247
+ while len(self.seenorder) > CacheLimit:
248
+ self.seen.discard(self.seenorder.popleft())
249
+
250
+ def Catacomb(self, bone: Bone, result: Result) -> None:
251
+ if not self.ready or not isinstance(bone, Bone):
252
+ return
253
+ if result.status == "GROWL":
254
+ self.Remember(bone)
255
+ self.Send({"type": "BONE", "count": self.count, "head": self.head, "bone": BoneToWire(bone)})
256
+ return
257
+ if result.snapshot is not None:
258
+ self.SendBonePile(result.snapshot)
259
+ if not result.changed:
260
+ return
261
+ self.Remember(bone)
262
+ self.Send({"type": "BONE", "count": self.count, "head": self.head, "bone": BoneToWire(bone)})
263
+ if result.reproject:
264
+ self.SendBonePile()
265
+
266
+ def Pump(self) -> bool:
267
+ redraw = False
268
+ for message in self.Receive():
269
+ redraw = self.Handle(message) or redraw
270
+ return redraw
271
+
272
+ def Handle(self, message: dict[str, object]) -> bool:
273
+ kind = str(message.get("type", "")).upper()
274
+ if kind == "HEADCOUNT":
275
+ return bool(self.HeadCountIn and self.HeadCountIn(message.get("headcount")))
276
+ if not self.ready:
277
+ return False
278
+ try:
279
+ if int(message.get("count", 0)) != self.count:
280
+ return False
281
+ except Exception:
282
+ return False
283
+ claimed = str(message.get("head", "")).upper()
284
+
285
+ if kind == "BONE":
286
+ try:
287
+ bone = BoneFromWire(message.get("bone"))
288
+ except Exception:
289
+ if self.NoticeOut:
290
+ self.NoticeOut("BAD BONE")
291
+ return True
292
+ if self.Seen(bone):
293
+ return False
294
+ if self.CatacombIn is None:
295
+ return False
296
+ result = self.CatacombIn(bone)
297
+ if result.status == "BAD BONE":
298
+ if self.NoticeOut:
299
+ self.NoticeOut("BAD BONE")
300
+ return True
301
+ if result.status == "HUNGRY":
302
+ return True
303
+ self.Remember(bone)
304
+ return False if result.status in ("IDEMPOTENT", "DOGHOUSE") else bool(result.changed)
305
+
306
+ if kind == "BONEPILE":
307
+ try:
308
+ pile = BonePileFromWire(message.get("bonepile"), self.heads)
309
+ statehash = str(message.get("statehash", ""))
310
+ statesign = str(message.get("statesign", ""))
311
+ if claimed not in self.expected or statehash != BonePileHash(claimed, pile):
312
+ raise ValueError("BonePile projector/hash mismatch")
313
+ VerifyDigest(pile[claimed].key, statehash, statesign)
314
+ except Exception:
315
+ if self.NoticeOut:
316
+ self.NoticeOut("BAD BONEPILE")
317
+ return True
318
+ if self.BonePileIn is None:
319
+ return False
320
+ result = self.BonePileIn(pile, claimed)
321
+ if result.status in ("LOCKED", "DOGHOUSE"):
322
+ return False
323
+ if result.status == "IDEMPOTENT":
324
+ return True
325
+ if result.status == "BAD BONEPILE":
326
+ if self.NoticeOut:
327
+ self.NoticeOut("BAD BONEPILE")
328
+ return True
329
+ return bool(result.changed)
330
+
331
+ if kind == "HUNGER":
332
+ self.SendBonePile()
333
+ return False