Files
snake-python/server/database/compact/codec.py
T
daniel156161 a2414f0b3d
Build and Push Docker Container / build-and-push (push) Successful in 2m18s
feat: add Zenith strategy and move latency tracing
- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
2026-11-05 01:11:31 +01:00

154 lines
4.8 KiB
Python

"""Byte-level encodings for compact gameplay storage.
All encodings are lossless. Values that do not fit the compact form (for
example coordinates outside 0..255) fall back to a JSON payload, marked by the
leading format byte, so unusual maps can never corrupt stored data.
"""
from datetime import datetime, timezone
import json
MOVES = ("up", "down", "left", "right")
_MOVE_CODES = {move: index + 1 for index, move in enumerate(MOVES)}
_FORMAT_JSON = 0
_FORMAT_COMPACT = 1
# Body step codes relative to the previous segment.
_SAME, _UP, _DOWN, _LEFT, _RIGHT, _ABSOLUTE = range(6)
_STEPS = {(0, 0): _SAME, (0, 1): _UP, (0, -1): _DOWN, (-1, 0): _LEFT, (1, 0): _RIGHT}
_STEP_DELTAS = {code: delta for delta, code in _STEPS.items()}
def encode_move(move:str|None) -> int|None:
return _MOVE_CODES.get(move) if move is not None else None
def decode_move(code:int|None) -> str|None:
if not code:
return None
return MOVES[int(code) - 1]
# The engine reports latency as a string; "" means no response was measured.
_EMPTY_LATENCY = -32768
def encode_latency(latency:object) -> int|None:
if latency is None:
return None
if latency == "":
return _EMPTY_LATENCY
try:
value = int(str(latency))
except ValueError:
return None
return value if _EMPTY_LATENCY < value <= 32767 else None
def decode_latency(value:int|None) -> str|None:
if value is None:
return None
return "" if value == _EMPTY_LATENCY else str(value)
def _point(point:object) -> tuple[int, int]|None:
if not isinstance(point, dict):
return None
x, y = point.get("x"), point.get("y")
if type(x) is not int or type(y) is not int:
return None
return x, y
def _fits_byte(*values:int) -> bool:
return all(0 <= value <= 255 for value in values)
def _json_fallback(value:object) -> bytes:
return bytes([_FORMAT_JSON]) + json.dumps(value, separators=(",", ":")).encode()
def encode_body(body:list) -> bytes:
"""Head as absolute byte pair, then one step code per following segment."""
points = [_point(segment) for segment in body]
if any(point is None for point in points) or not all(_fits_byte(*point) for point in points):
return _json_fallback(body)
output = bytearray([_FORMAT_COMPACT])
previous = None
for x, y in points:
if previous is None:
output += bytes((x, y))
else:
step = _STEPS.get((x - previous[0], y - previous[1]))
output += bytes((step,)) if step is not None else bytes((_ABSOLUTE, x, y))
previous = (x, y)
return bytes(output)
def decode_body(data:bytes|memoryview|None) -> list[dict]:
if not data:
return []
data = bytes(data)
if data[0] == _FORMAT_JSON:
return json.loads(data[1:])
body = []
index = 1
if len(data) >= 3:
x, y = data[1], data[2]
body.append({"x": x, "y": y})
index = 3
while index < len(data):
code = data[index]
if code == _ABSOLUTE:
x, y = data[index + 1], data[index + 2]
index += 3
else:
dx, dy = _STEP_DELTAS[code]
x, y = x + dx, y + dy
index += 1
body.append({"x": x, "y": y})
return body
def encode_points(points:list|None) -> bytes|None:
"""Food/hazard lists as byte pairs. ``None`` means "unchanged"."""
if points is None:
return None
pairs = [_point(point) for point in points]
if any(pair is None for pair in pairs) or not all(_fits_byte(*pair) for pair in pairs):
return _json_fallback(points)
output = bytearray([_FORMAT_COMPACT])
for x, y in pairs:
output += bytes((x, y))
return bytes(output)
def decode_points(data:bytes|memoryview|None) -> list[dict]|None:
if data is None:
return None
data = bytes(data)
if not data:
return []
if data[0] == _FORMAT_JSON:
return json.loads(data[1:])
return [{"x": data[i], "y": data[i + 1]} for i in range(1, len(data) - 1, 2)]
def flatten_points(points:list[dict]) -> list:
"""Compact JSON form used inside replay blobs: [x0, y0, x1, y1, ...]."""
pairs = [_point(point) for point in points]
if any(pair is None for pair in pairs):
return [points]
return [value for pair in pairs for value in pair]
def unflatten_points(values:list) -> list[dict]:
if len(values) == 1 and isinstance(values[0], list):
return values[0]
return [{"x": values[i], "y": values[i + 1]} for i in range(0, len(values), 2)]
def to_epoch_us(value:datetime|str|None) -> int|None:
if value is None:
return None
if isinstance(value, str):
value = datetime.fromisoformat(value.replace("Z", "+00:00"))
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
delta = value - datetime(1970, 1, 1, tzinfo=timezone.utc)
return (delta.days * 86_400 + delta.seconds) * 1_000_000 + delta.microseconds
def from_epoch_us(value:int|None) -> datetime|None:
if value is None:
return None
seconds, micros = divmod(int(value), 1_000_000)
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(microsecond=micros)