"""Convert rows of the legacy layout (turns/snake_turns) into a compact bundle.""" import json from server.database.compact.codec import encode_latency from server.database.compact.games import normalize_game from server.database.compact.records import ( GameBundle, PointsState, SnakeIdentity, SnakeState, compact_turn, ) class LegacyDataError(ValueError): """Legacy rows that cannot be converted without losing data.""" def parse_json(value:object, default:object=None) -> object: if value is None or value == "": return default if not isinstance(value, str): return value try: return json.loads(value) except (json.JSONDecodeError, TypeError) as exc: raise LegacyDataError(f"Invalid JSON value: {value[:120]}") from exc def _state_from_row(row:dict) -> SnakeState: body = parse_json(row.get("body"), []) or [] head_x, head_y, length = row.get("head_x"), row.get("head_y"), row.get("length") if length is not None and int(length) != len(body): raise LegacyDataError(f"snake {row['snake_id']} turn {row['turn']}: length {length} != body {len(body)}") if body and head_x is not None and (head_x, head_y) != (body[0].get("x"), body[0].get("y")): raise LegacyDataError(f"snake {row['snake_id']} turn {row['turn']}: head differs from body[0]") return SnakeState( snake_id=row["snake_id"], turn=int(row["turn"]), health=row.get("health"), latency=encode_latency(row.get("latency")), body=body, ) def _states_from_board(board:dict, turn:int) -> list[SnakeState]: return [ SnakeState( snake_id=snake["id"], turn=turn, health=snake.get("health"), latency=encode_latency(snake.get("latency")), body=list(snake.get("body", [])), ) for snake in board.get("snakes", []) if snake.get("id") is not None ] def _identities(game:dict, identity_rows:list[dict], snake_rows:list[dict], board_snakes:list[dict]) -> list[SnakeIdentity]: identities:dict[str, SnakeIdentity] = {} for row in identity_rows: identities[row["snake_id"]] = SnakeIdentity( row["snake_id"], row.get("snake_name"), bool(row.get("is_you")), parse_json(row.get("customizations"), {}) or {}, ) # Older databases have an empty or partial game_snakes table. for row in snake_rows + board_snakes: snake_id = row.get("snake_id") or row.get("id") if snake_id is None or snake_id in identities: continue name = row.get("snake_name") or row.get("name") is_you = bool(row.get("is_you")) or snake_id == game.get("your_snake_id") identities[snake_id] = SnakeIdentity(snake_id, name, is_you, row.get("customizations") or {}) for snake_id, identity in list(identities.items()): if identity.name is None: name = next((row.get("snake_name") for row in snake_rows if row.get("snake_id") == snake_id and row.get("snake_name")), None) if name: identities[snake_id] = SnakeIdentity(snake_id, name, identity.is_you, identity.customizations) return list(identities.values()) def build_legacy_bundle(game_row:dict, identity_rows:list[dict], turn_rows:list[dict], snake_rows:list[dict]) -> GameBundle: game = normalize_game(game_row) states_by_turn:dict[int, list[SnakeState]] = {} for row in snake_rows: state = _state_from_row(row) states_by_turn.setdefault(state.turn, []).append(state) turns = [] states = [] board_snakes = [] previous:PointsState|None = None for row in sorted(turn_rows, key=lambda item: int(item["turn"])): turn = int(row["turn"]) board = parse_json(row.get("board_state"), {}) or {} food = board["food"] if "food" in board else parse_json(row.get("food"), []) or [] hazards = board["hazards"] if "hazards" in board else parse_json(row.get("hazards"), []) or [] turn_states = states_by_turn.pop(turn, None) if turn_states is None: turn_states = _states_from_board(board, turn) board_snakes.extend(board.get("snakes", [])) states.extend(turn_states) record, previous = compact_turn( turn, row.get("observed_at"), row.get("my_move"), parse_json(row.get("my_thinking")), food, hazards, turn_states, game.get("your_snake_id"), game.get("width"), game.get("height"), previous, ) turns.append(record) # Snake rows without a turn row are kept as well. for orphan in states_by_turn.values(): states.extend(orphan) return GameBundle(game, _identities(game, identity_rows, snake_rows, board_snakes), turns, states)