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.
- cerberus_game-0.1.0/Cerberus.py +5 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/PKG-INFO +6 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/SOURCES.txt +13 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/dependency_links.txt +1 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/entry_points.txt +2 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/requires.txt +1 -0
- cerberus_game-0.1.0/Cerberus_Game.egg-info/top_level.txt +2 -0
- cerberus_game-0.1.0/Game/BoneYard.py +333 -0
- cerberus_game-0.1.0/Game/Catacomb.py +777 -0
- cerberus_game-0.1.0/Game/Guardian.py +542 -0
- cerberus_game-0.1.0/Game/__init__.py +1 -0
- cerberus_game-0.1.0/PKG-INFO +6 -0
- cerberus_game-0.1.0/README.md +1 -0
- cerberus_game-0.1.0/pyproject.toml +22 -0
- cerberus_game-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography
|
|
@@ -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
|