Build and Push Docker Container / build-and-push (push) Successful in 3m52s
- Add compact storage package: codec, records, hydrate, replay blobs, cache. - Running games write game_turns and snake_states keyed by integer ids. - Encode moves as 1-byte codes and bodies as head plus step codes. - Store food and hazards only when they changed since the previous turn. - Strip reasoning fields that duplicate stored values and restore on read. - Pack finished games into one zstd blob per game in game_replays. - Merge late turns into packed blobs during the stale-game sweep. - Extract SQLite and PostgreSQL schemas into dedicated modules. - Add read/import game bundle and list_game_ids to the backend template. - Add legacy readers and converter for the old turns/snake_turns layout. - Add migrate_gameplay_to_compact.py with in-place or cross-DB migration. - Verify every migrated game and keep legacy rows on failure. - Support --since, --minimum-quality and guarded --drop-legacy. - Add just recipe migrate-gameplay-postgres. - Rewrite merge script to copy game by game via backend bundles. - Move benchmark state loading into benchmark_states for both backends. - Update PostgreSQL cleanup script to the new replay tables. - Drop sqlite-zstd build stages from the Dockerfile. - Remove old migration scripts, normalized_turn and their tests. - Add tests for compact storage, schema, legacy migration and PostgreSQL. - Document the compact layout and migration workflow in the README.
101 lines
3.3 KiB
Python
101 lines
3.3 KiB
Python
"""Rebuild the dashboard replay payload from compact records."""
|
|
|
|
from .codec import decode_latency
|
|
from .records import SnakeIdentity, SnakeState, TurnRecord, you_view
|
|
from .thinking import derived_values, restore_thinking
|
|
|
|
_DIRECTIONS = {(1, 0): "right", (-1, 0): "left", (0, 1): "up", (0, -1): "down"}
|
|
|
|
def _head(state:SnakeState) -> dict|None:
|
|
return state.body[0] if state.body else None
|
|
|
|
def _inferred_move(previous:SnakeState|None, current:SnakeState) -> str|None:
|
|
if previous is None:
|
|
return None
|
|
old, new = _head(previous), _head(current)
|
|
if old is None or new is None:
|
|
return None
|
|
return _DIRECTIONS.get((new["x"] - old["x"], new["y"] - old["y"]))
|
|
|
|
def _sort_key(identity:SnakeIdentity|None):
|
|
name = identity.name if identity else None
|
|
is_you = bool(identity and identity.is_you)
|
|
return (not is_you, name is None, name or "")
|
|
|
|
def hydrate_replay_turns(
|
|
game:dict,
|
|
identities:dict[str, SnakeIdentity],
|
|
turns:list[TurnRecord],
|
|
states:list[SnakeState],
|
|
) -> list[dict]:
|
|
states_by_turn:dict[int, dict[str, SnakeState]] = {}
|
|
for state in states:
|
|
states_by_turn.setdefault(state.turn, {})[state.snake_id] = state
|
|
|
|
width, height = game.get("width"), game.get("height")
|
|
you_id = game.get("your_snake_id")
|
|
food:list = []
|
|
hazards:list = []
|
|
output = []
|
|
for record in sorted(turns, key=lambda item: item.turn):
|
|
food = food if record.food is None else record.food
|
|
hazards = hazards if record.hazards is None else record.hazards
|
|
current = states_by_turn.get(record.turn, {})
|
|
previous = states_by_turn.get(record.turn - 1, {})
|
|
ordered = sorted(current.values(), key=lambda s: _sort_key(identities.get(s.snake_id)))
|
|
|
|
snakes = []
|
|
api_snakes = []
|
|
for state in ordered:
|
|
identity = identities.get(state.snake_id)
|
|
name = identity.name if identity else None
|
|
customizations = identity.customizations if identity else {}
|
|
latency = decode_latency(state.latency)
|
|
api_snake = {
|
|
"id": state.snake_id,
|
|
"name": name,
|
|
"health": state.health,
|
|
"length": len(state.body),
|
|
"head": _head(state) or {"x": None, "y": None},
|
|
"body": state.body,
|
|
"customizations": customizations,
|
|
}
|
|
if latency is not None:
|
|
api_snake["latency"] = latency
|
|
api_snakes.append(api_snake)
|
|
snakes.append({
|
|
"snake_id": state.snake_id,
|
|
"snake_name": name,
|
|
"health": state.health,
|
|
"length": len(state.body),
|
|
"head": api_snake["head"],
|
|
"body": state.body,
|
|
"customizations": customizations,
|
|
"is_you": bool(identity and identity.is_you),
|
|
"inferred_move": _inferred_move(previous.get(state.snake_id), state),
|
|
"latency": latency,
|
|
})
|
|
|
|
derived = derived_values(
|
|
record.turn, record.move, you_view(list(current.values()), you_id),
|
|
food, width, height, len(current),
|
|
)
|
|
output.append({
|
|
"turn": record.turn,
|
|
"observed_at": record.observed_at,
|
|
"my_move": record.move,
|
|
"my_thinking": restore_thinking(record.thinking, derived),
|
|
"board": {
|
|
"width": width,
|
|
"height": height,
|
|
"food": food,
|
|
"hazards": hazards,
|
|
"snakes": api_snakes,
|
|
},
|
|
"food": food,
|
|
"hazards": hazards,
|
|
"you": next((snake for snake in api_snakes if snake["id"] == you_id), {}),
|
|
"snakes": snakes,
|
|
})
|
|
return output
|