rework that the sqlite and postgresql backend uses the same code to build the data for the dashboard
This commit is contained in:
@@ -479,20 +479,9 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
}
|
}
|
||||||
|
|
||||||
you_id = you.get("id")
|
you_id = you.get("id")
|
||||||
for snake in snakes:
|
for params in self._build_snake_turn_params(snakes, you_id, game_id, turn, previous_positions):
|
||||||
snake_id = snake.get("id")
|
(p_game_id, p_turn, p_snake_id, p_name, p_health, p_length,
|
||||||
head = snake.get("head", {})
|
p_head_x, p_head_y, p_body, p_is_you, p_inferred, p_latency) = params
|
||||||
head_x = head.get("x")
|
|
||||||
head_y = head.get("y")
|
|
||||||
if snake_id is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_head = (
|
|
||||||
(int(head_x), int(head_y))
|
|
||||||
if head_x is not None and head_y is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
inferred = self._infer_direction(previous_positions.get(snake_id), new_head)
|
|
||||||
await conn.execute("""
|
await conn.execute("""
|
||||||
INSERT INTO snake_turns (
|
INSERT INTO snake_turns (
|
||||||
game_id, turn, snake_id, snake_name, health, length,
|
game_id, turn, snake_id, snake_name, health, length,
|
||||||
@@ -509,18 +498,8 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
inferred_move = EXCLUDED.inferred_move,
|
inferred_move = EXCLUDED.inferred_move,
|
||||||
latency = EXCLUDED.latency
|
latency = EXCLUDED.latency
|
||||||
""",
|
""",
|
||||||
game_id,
|
p_game_id, p_turn, p_snake_id, p_name, p_health, p_length,
|
||||||
turn,
|
p_head_x, p_head_y, p_body, p_is_you, p_inferred, p_latency,
|
||||||
snake_id,
|
|
||||||
snake.get("name"),
|
|
||||||
snake.get("health"),
|
|
||||||
snake.get("length"),
|
|
||||||
head_x,
|
|
||||||
head_y,
|
|
||||||
snake.get("body", []),
|
|
||||||
snake_id == you_id,
|
|
||||||
inferred,
|
|
||||||
snake.get("latency"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await conn.execute("""
|
await conn.execute("""
|
||||||
@@ -532,14 +511,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def record_game_end(self, game_state:dict) -> None:
|
async def record_game_end(self, game_state:dict) -> None:
|
||||||
game = game_state.get("game", {})
|
game_id, winner_name, winner_you, turn = self._extract_game_end_params(game_state)
|
||||||
game_id = game.get("id")
|
|
||||||
board = game_state.get("board", {})
|
|
||||||
snakes = list(board.get("snakes", []))
|
|
||||||
you = self._extract_you(game_state)
|
|
||||||
winner_name = next((s.get("name") for s in snakes if s.get("name")), None)
|
|
||||||
you_id = you.get("id")
|
|
||||||
winner_you = any(s.get("id") == you_id for s in snakes)
|
|
||||||
|
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
@@ -555,7 +527,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
self._utc_now_ts(),
|
self._utc_now_ts(),
|
||||||
winner_name,
|
winner_name,
|
||||||
winner_you,
|
winner_you,
|
||||||
int(game_state.get("turn", 0)),
|
turn,
|
||||||
game_id,
|
game_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -614,13 +586,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
game_id, final_turn,
|
game_id, final_turn,
|
||||||
)
|
)
|
||||||
|
|
||||||
survivor_ids = [s["snake_id"] for s in snake_rows if s["snake_id"]]
|
winner_you, survivor_name = self._calculate_survivor(snake_rows, your_snake_id)
|
||||||
winner_you = bool(
|
|
||||||
your_snake_id
|
|
||||||
and your_snake_id in survivor_ids
|
|
||||||
and len(survivor_ids) == 1
|
|
||||||
)
|
|
||||||
survivor_name = next((s["snake_name"] for s in snake_rows if s["snake_name"]), None)
|
|
||||||
|
|
||||||
tag = await conn.execute("""
|
tag = await conn.execute("""
|
||||||
UPDATE games
|
UPDATE games
|
||||||
@@ -680,34 +646,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
max(1, int(recent_limit)),
|
max(1, int(recent_limit)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return self._build_summary_output(totals, by_type, recent)
|
||||||
"total_games": int(totals["total_games"] or 0),
|
|
||||||
"running_games": int(totals["running_games"] or 0),
|
|
||||||
"finished_games": int(totals["finished_games"] or 0),
|
|
||||||
"wins": int(totals["wins"] or 0),
|
|
||||||
"losses": int(totals["losses"] or 0),
|
|
||||||
"avg_turns_finished": round(float(totals["avg_turns"] or 0.0), 2),
|
|
||||||
"by_game_type": [{
|
|
||||||
"game_type": row["type_label"],
|
|
||||||
"total": int(row["total"]),
|
|
||||||
"wins": int(row["wins"]),
|
|
||||||
"losses": int(row["losses"]),
|
|
||||||
} for row in by_type],
|
|
||||||
"recent_games": [{
|
|
||||||
"game_id": row["game_id"],
|
|
||||||
"started_at": row["started_at"].isoformat() if row["started_at"] else None,
|
|
||||||
"ended_at": row["ended_at"].isoformat() if row["ended_at"] else None,
|
|
||||||
"map": row["map_name"],
|
|
||||||
"ruleset": row["ruleset_name"],
|
|
||||||
"game_type": row["game_type"],
|
|
||||||
"snake": row["your_snake_name"],
|
|
||||||
"snake_type": row["your_snake_type"],
|
|
||||||
"snake_version": row["your_snake_version"],
|
|
||||||
"winner_you": bool(row["winner_you"]),
|
|
||||||
"final_turn": int(row["final_turn"] or 0),
|
|
||||||
"status": row["status"],
|
|
||||||
} for row in recent],
|
|
||||||
}
|
|
||||||
|
|
||||||
async def list_games(self, limit:int=50) -> list[dict]:
|
async def list_games(self, limit:int=50) -> list[dict]:
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
@@ -723,22 +662,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
max(1, int(limit)),
|
max(1, int(limit)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [{
|
return self._build_game_list_output(rows)
|
||||||
"game_id": row["game_id"],
|
|
||||||
"started_at": row["started_at"].isoformat() if row["started_at"] else None,
|
|
||||||
"ended_at": row["ended_at"].isoformat() if row["ended_at"] else None,
|
|
||||||
"map": row["map_name"],
|
|
||||||
"source": row["source"],
|
|
||||||
"ruleset": row["ruleset_name"],
|
|
||||||
"game_type": row["game_type"],
|
|
||||||
"snake": row["your_snake_name"],
|
|
||||||
"snake_type": row["your_snake_type"],
|
|
||||||
"snake_version": row["your_snake_version"],
|
|
||||||
"winner_you": bool(row["winner_you"]),
|
|
||||||
"winner_name": row["winner_name"],
|
|
||||||
"final_turn": int(row["final_turn"] or 0),
|
|
||||||
"status": row["status"],
|
|
||||||
} for row in rows]
|
|
||||||
|
|
||||||
async def get_game_replay(self, game_id:str) -> dict|None:
|
async def get_game_replay(self, game_id:str) -> dict|None:
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
@@ -758,8 +682,12 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
turn_rows = await conn.fetch("""
|
turn_rows = await conn.fetch("""
|
||||||
SELECT turn, observed_at, my_move, my_thinking,
|
SELECT turn, observed_at, my_move,
|
||||||
board_state, food, hazards, you
|
my_thinking AS my_thinking_json,
|
||||||
|
board_state AS board_state_json,
|
||||||
|
food AS food_json,
|
||||||
|
hazards AS hazards_json,
|
||||||
|
you AS you_json
|
||||||
FROM turns
|
FROM turns
|
||||||
WHERE game_id = $1
|
WHERE game_id = $1
|
||||||
ORDER BY turn ASC
|
ORDER BY turn ASC
|
||||||
@@ -769,7 +697,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
|
|
||||||
snake_rows = await conn.fetch("""
|
snake_rows = await conn.fetch("""
|
||||||
SELECT turn, snake_id, snake_name, health, length, head_x, head_y,
|
SELECT turn, snake_id, snake_name, health, length, head_x, head_y,
|
||||||
body, is_you, inferred_move, latency
|
body AS body_json, is_you, inferred_move, latency
|
||||||
FROM snake_turns
|
FROM snake_turns
|
||||||
WHERE game_id = $1
|
WHERE game_id = $1
|
||||||
ORDER BY turn ASC, is_you DESC, snake_name ASC
|
ORDER BY turn ASC, is_you DESC, snake_name ASC
|
||||||
@@ -777,56 +705,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
|
|||||||
game_id,
|
game_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
snakes_by_turn:dict[int, list[dict]] = {}
|
return self._build_game_replay_output(game_row, turn_rows, snake_rows, decode_json=lambda x: x)
|
||||||
for row in snake_rows:
|
|
||||||
snakes_by_turn.setdefault(int(row["turn"]), []).append({
|
|
||||||
"snake_id": row["snake_id"],
|
|
||||||
"snake_name": row["snake_name"],
|
|
||||||
"health": row["health"],
|
|
||||||
"length": row["length"],
|
|
||||||
"head": {"x": row["head_x"], "y": row["head_y"]},
|
|
||||||
"body": row["body"] or [],
|
|
||||||
"is_you": bool(row["is_you"]),
|
|
||||||
"inferred_move": row["inferred_move"],
|
|
||||||
"latency": row["latency"],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"game": {
|
|
||||||
"game_id": game_row["game_id"],
|
|
||||||
"started_at": game_row["started_at"].isoformat() if game_row["started_at"] else None,
|
|
||||||
"ended_at": game_row["ended_at"].isoformat() if game_row["ended_at"] else None,
|
|
||||||
"width": game_row["width"],
|
|
||||||
"height": game_row["height"],
|
|
||||||
"source": game_row["source"],
|
|
||||||
"map": game_row["map_name"],
|
|
||||||
"ruleset_name": game_row["ruleset_name"],
|
|
||||||
"ruleset_version": game_row["ruleset_version"],
|
|
||||||
"game_type": game_row["game_type"],
|
|
||||||
"your_snake_id": game_row["your_snake_id"],
|
|
||||||
"your_snake_name": game_row["your_snake_name"],
|
|
||||||
"your_snake_type": game_row["your_snake_type"],
|
|
||||||
"your_snake_version": game_row["your_snake_version"],
|
|
||||||
"winner_name": game_row["winner_name"],
|
|
||||||
"winner_you": bool(game_row["winner_you"]),
|
|
||||||
"final_turn": int(game_row["final_turn"] or 0),
|
|
||||||
"status": game_row["status"],
|
|
||||||
},
|
|
||||||
"turns": [
|
|
||||||
{
|
|
||||||
"turn": int(row["turn"]),
|
|
||||||
"observed_at": row["observed_at"].isoformat() if row["observed_at"] else None,
|
|
||||||
"my_move": row["my_move"],
|
|
||||||
"my_thinking": row["my_thinking"],
|
|
||||||
"board": row["board_state"],
|
|
||||||
"food": row["food"] or [],
|
|
||||||
"hazards": row["hazards"] or [],
|
|
||||||
"you": row["you"] or {},
|
|
||||||
"snakes": snakes_by_turn.get(int(row["turn"]), []),
|
|
||||||
}
|
|
||||||
for row in turn_rows
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── lifecycle ──────────────────────────────────────────────────────────────
|
# ── lifecycle ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -285,19 +285,9 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
}
|
}
|
||||||
|
|
||||||
you_id = you.get("id")
|
you_id = you.get("id")
|
||||||
for snake in snakes:
|
for params in self._build_snake_turn_params(snakes, you_id, game_id, turn, previous_positions):
|
||||||
snake_id = snake.get("id")
|
(p_game_id, p_turn, p_snake_id, p_name, p_health, p_length,
|
||||||
head = snake.get("head", {})
|
p_head_x, p_head_y, p_body, p_is_you, p_inferred, p_latency) = params
|
||||||
head_x = head.get("x")
|
|
||||||
head_y = head.get("y")
|
|
||||||
if snake_id is None:
|
|
||||||
continue
|
|
||||||
new_head = (
|
|
||||||
(int(head_x), int(head_y))
|
|
||||||
if head_x is not None and head_y is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
inferred = self._infer_direction(previous_positions.get(snake_id), new_head)
|
|
||||||
connection.execute("""
|
connection.execute("""
|
||||||
INSERT INTO snake_turns (
|
INSERT INTO snake_turns (
|
||||||
game_id, turn, snake_id, snake_name, health, length,
|
game_id, turn, snake_id, snake_name, health, length,
|
||||||
@@ -315,18 +305,10 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
latency = excluded.latency
|
latency = excluded.latency
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
game_id,
|
p_game_id, p_turn, p_snake_id, p_name, p_health, p_length,
|
||||||
turn,
|
p_head_x, p_head_y, self._to_json(p_body),
|
||||||
snake_id,
|
1 if p_is_you else 0,
|
||||||
snake.get("name"),
|
p_inferred, p_latency,
|
||||||
snake.get("health"),
|
|
||||||
snake.get("length"),
|
|
||||||
head_x,
|
|
||||||
head_y,
|
|
||||||
self._to_json(snake.get("body", [])),
|
|
||||||
1 if snake_id == you_id else 0,
|
|
||||||
inferred,
|
|
||||||
snake.get("latency"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,14 +321,7 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _record_game_end_sync(self, game_state:dict) -> None:
|
def _record_game_end_sync(self, game_state:dict) -> None:
|
||||||
game = game_state.get("game", {})
|
game_id, winner_name, winner_you, turn = self._extract_game_end_params(game_state)
|
||||||
game_id = game.get("id")
|
|
||||||
board = game_state.get("board", {})
|
|
||||||
snakes = list(board.get("snakes", []))
|
|
||||||
you = self._extract_you(game_state)
|
|
||||||
winner_name = next((snake.get("name") for snake in snakes if snake.get("name")), None)
|
|
||||||
you_id = you.get("id")
|
|
||||||
winner_you = any(snake.get("id") == you_id for snake in snakes)
|
|
||||||
|
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
connection.execute("""
|
connection.execute("""
|
||||||
@@ -362,8 +337,8 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
self._utc_now(),
|
self._utc_now(),
|
||||||
winner_name,
|
winner_name,
|
||||||
1 if winner_you else 0,
|
1 if winner_you else 0,
|
||||||
int(game_state.get("turn", 0)),
|
turn,
|
||||||
int(game_state.get("turn", 0)),
|
turn,
|
||||||
game_id,
|
game_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -417,13 +392,7 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
(game_id, final_turn),
|
(game_id, final_turn),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
survivor_ids = [s["snake_id"] for s in snake_rows if s["snake_id"]]
|
winner_you, survivor_name = self._calculate_survivor(snake_rows, your_snake_id)
|
||||||
winner_you = bool(
|
|
||||||
your_snake_id
|
|
||||||
and your_snake_id in survivor_ids
|
|
||||||
and len(survivor_ids) == 1
|
|
||||||
)
|
|
||||||
survivor_name = next((s["snake_name"] for s in snake_rows if s["snake_name"]), None)
|
|
||||||
|
|
||||||
result = connection.execute("""
|
result = connection.execute("""
|
||||||
UPDATE games
|
UPDATE games
|
||||||
@@ -485,34 +454,7 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
(max(1, int(recent_limit)),),
|
(max(1, int(recent_limit)),),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
return {
|
return self._build_summary_output(totals, by_type, recent)
|
||||||
"total_games": int(totals["total_games"] or 0),
|
|
||||||
"running_games": int(totals["running_games"] or 0),
|
|
||||||
"finished_games": int(totals["finished_games"] or 0),
|
|
||||||
"wins": int(totals["wins"] or 0),
|
|
||||||
"losses": int(totals["losses"] or 0),
|
|
||||||
"avg_turns_finished": round(float(totals["avg_turns"] or 0.0), 2),
|
|
||||||
"by_game_type": [{
|
|
||||||
"game_type": row["type_label"],
|
|
||||||
"total": int(row["total"]),
|
|
||||||
"wins": int(row["wins"]),
|
|
||||||
"losses": int(row["losses"]),
|
|
||||||
} for row in by_type],
|
|
||||||
"recent_games": [{
|
|
||||||
"game_id": row["game_id"],
|
|
||||||
"started_at": row["started_at"],
|
|
||||||
"ended_at": row["ended_at"],
|
|
||||||
"map": row["map_name"],
|
|
||||||
"ruleset": row["ruleset_name"],
|
|
||||||
"game_type": row["game_type"],
|
|
||||||
"snake": row["your_snake_name"],
|
|
||||||
"snake_type": row["your_snake_type"],
|
|
||||||
"snake_version": row["your_snake_version"],
|
|
||||||
"winner_you": bool(row["winner_you"]),
|
|
||||||
"final_turn": int(row["final_turn"] or 0),
|
|
||||||
"status": row["status"],
|
|
||||||
} for row in recent],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _list_games_sync(self, limit:int=50) -> list[dict]:
|
def _list_games_sync(self, limit:int=50) -> list[dict]:
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
@@ -527,22 +469,7 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
(max(1, int(limit)),),
|
(max(1, int(limit)),),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
return [{
|
return self._build_game_list_output(rows)
|
||||||
"game_id": row["game_id"],
|
|
||||||
"started_at": row["started_at"],
|
|
||||||
"ended_at": row["ended_at"],
|
|
||||||
"map": row["map_name"],
|
|
||||||
"source": row["source"],
|
|
||||||
"ruleset": row["ruleset_name"],
|
|
||||||
"game_type": row["game_type"],
|
|
||||||
"snake": row["your_snake_name"],
|
|
||||||
"snake_type": row["your_snake_type"],
|
|
||||||
"snake_version": row["your_snake_version"],
|
|
||||||
"winner_you": bool(row["winner_you"]),
|
|
||||||
"winner_name": row["winner_name"],
|
|
||||||
"final_turn": int(row["final_turn"] or 0),
|
|
||||||
"status": row["status"],
|
|
||||||
} for row in rows]
|
|
||||||
|
|
||||||
def _get_game_replay_sync(self, game_id:str) -> dict|None:
|
def _get_game_replay_sync(self, game_id:str) -> dict|None:
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
@@ -580,56 +507,7 @@ class SqliteGameplayBackend(GameplayBackendTemplate):
|
|||||||
(game_id,),
|
(game_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
snakes_by_turn: dict[int, list[dict]] = {}
|
return self._build_game_replay_output(game_row, turn_rows, snake_rows, decode_json=self._from_json)
|
||||||
for row in snake_rows:
|
|
||||||
snakes_by_turn.setdefault(int(row["turn"]), []).append({
|
|
||||||
"snake_id": row["snake_id"],
|
|
||||||
"snake_name": row["snake_name"],
|
|
||||||
"health": row["health"],
|
|
||||||
"length": row["length"],
|
|
||||||
"head": {"x": row["head_x"], "y": row["head_y"]},
|
|
||||||
"body": self._from_json(row["body_json"]) or [],
|
|
||||||
"is_you": bool(row["is_you"]),
|
|
||||||
"inferred_move": row["inferred_move"],
|
|
||||||
"latency": row["latency"],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"game": {
|
|
||||||
"game_id": game_row["game_id"],
|
|
||||||
"started_at": game_row["started_at"],
|
|
||||||
"ended_at": game_row["ended_at"],
|
|
||||||
"width": game_row["width"],
|
|
||||||
"height": game_row["height"],
|
|
||||||
"source": game_row["source"],
|
|
||||||
"map": game_row["map_name"],
|
|
||||||
"ruleset_name": game_row["ruleset_name"],
|
|
||||||
"ruleset_version": game_row["ruleset_version"],
|
|
||||||
"game_type": game_row["game_type"],
|
|
||||||
"your_snake_id": game_row["your_snake_id"],
|
|
||||||
"your_snake_name": game_row["your_snake_name"],
|
|
||||||
"your_snake_type": game_row["your_snake_type"],
|
|
||||||
"your_snake_version": game_row["your_snake_version"],
|
|
||||||
"winner_name": game_row["winner_name"],
|
|
||||||
"winner_you": bool(game_row["winner_you"]),
|
|
||||||
"final_turn": int(game_row["final_turn"] or 0),
|
|
||||||
"status": game_row["status"],
|
|
||||||
},
|
|
||||||
"turns": [
|
|
||||||
{
|
|
||||||
"turn": int(row["turn"]),
|
|
||||||
"observed_at": row["observed_at"],
|
|
||||||
"my_move": row["my_move"],
|
|
||||||
"my_thinking": self._from_json(row["my_thinking_json"]),
|
|
||||||
"board": self._from_json(row["board_state_json"]),
|
|
||||||
"food": self._from_json(row["food_json"]) or [],
|
|
||||||
"hazards": self._from_json(row["hazards_json"]) or [],
|
|
||||||
"you": self._from_json(row["you_json"]) or {},
|
|
||||||
"snakes": snakes_by_turn.get(int(row["turn"]), []),
|
|
||||||
}
|
|
||||||
for row in turn_rows
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── public async interface ─────────────────────────────────────────────────
|
# ── public async interface ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -98,3 +98,184 @@ class GameplayBackendTemplate:
|
|||||||
return "duel"
|
return "duel"
|
||||||
|
|
||||||
return ruleset.get("name") or "standard"
|
return ruleset.get("name") or "standard"
|
||||||
|
|
||||||
|
# ── shared output builders ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _ts_to_str(self, value) -> str|None:
|
||||||
|
"""Normalize a timestamp: pass str through, call .isoformat() on datetime."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
return value.isoformat()
|
||||||
|
|
||||||
|
def _build_summary_output(self, totals, by_type_rows, recent_rows) -> dict:
|
||||||
|
return {
|
||||||
|
"total_games": int(totals["total_games"] or 0),
|
||||||
|
"running_games": int(totals["running_games"] or 0),
|
||||||
|
"finished_games": int(totals["finished_games"] or 0),
|
||||||
|
"wins": int(totals["wins"] or 0),
|
||||||
|
"losses": int(totals["losses"] or 0),
|
||||||
|
"avg_turns_finished": round(float(totals["avg_turns"] or 0.0), 2),
|
||||||
|
"by_game_type": [{
|
||||||
|
"game_type": row["type_label"],
|
||||||
|
"total": int(row["total"]),
|
||||||
|
"wins": int(row["wins"]),
|
||||||
|
"losses": int(row["losses"]),
|
||||||
|
} for row in by_type_rows],
|
||||||
|
"recent_games": [{
|
||||||
|
"game_id": row["game_id"],
|
||||||
|
"started_at": self._ts_to_str(row["started_at"]),
|
||||||
|
"ended_at": self._ts_to_str(row["ended_at"]),
|
||||||
|
"map": row["map_name"],
|
||||||
|
"ruleset": row["ruleset_name"],
|
||||||
|
"game_type": row["game_type"],
|
||||||
|
"snake": row["your_snake_name"],
|
||||||
|
"snake_type": row["your_snake_type"],
|
||||||
|
"snake_version": row["your_snake_version"],
|
||||||
|
"winner_you": bool(row["winner_you"]),
|
||||||
|
"final_turn": int(row["final_turn"] or 0),
|
||||||
|
"status": row["status"],
|
||||||
|
} for row in recent_rows],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_game_list_output(self, rows) -> list[dict]:
|
||||||
|
return [{
|
||||||
|
"game_id": row["game_id"],
|
||||||
|
"started_at": self._ts_to_str(row["started_at"]),
|
||||||
|
"ended_at": self._ts_to_str(row["ended_at"]),
|
||||||
|
"map": row["map_name"],
|
||||||
|
"source": row["source"],
|
||||||
|
"ruleset": row["ruleset_name"],
|
||||||
|
"game_type": row["game_type"],
|
||||||
|
"snake": row["your_snake_name"],
|
||||||
|
"snake_type": row["your_snake_type"],
|
||||||
|
"snake_version": row["your_snake_version"],
|
||||||
|
"winner_you": bool(row["winner_you"]),
|
||||||
|
"winner_name": row["winner_name"],
|
||||||
|
"final_turn": int(row["final_turn"] or 0),
|
||||||
|
"status": row["status"],
|
||||||
|
} for row in rows]
|
||||||
|
|
||||||
|
def _build_game_replay_output(self, game_row, turn_rows, snake_rows, decode_json) -> dict:
|
||||||
|
"""Build the full replay dict.
|
||||||
|
|
||||||
|
decode_json: callable applied to raw column values that may be JSON strings
|
||||||
|
(SQLite) or already-decoded objects (PostgreSQL). Pass self._from_json for
|
||||||
|
SQLite; pass (lambda x: x) for PostgreSQL.
|
||||||
|
"""
|
||||||
|
snakes_by_turn:dict[int, list[dict]] = {}
|
||||||
|
for row in snake_rows:
|
||||||
|
snakes_by_turn.setdefault(int(row["turn"]), []).append({
|
||||||
|
"snake_id": row["snake_id"],
|
||||||
|
"snake_name": row["snake_name"],
|
||||||
|
"health": row["health"],
|
||||||
|
"length": row["length"],
|
||||||
|
"head": {"x": row["head_x"], "y": row["head_y"]},
|
||||||
|
"body": decode_json(row["body_json"]) or [],
|
||||||
|
"is_you": bool(row["is_you"]),
|
||||||
|
"inferred_move": row["inferred_move"],
|
||||||
|
"latency": row["latency"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"game": {
|
||||||
|
"game_id": game_row["game_id"],
|
||||||
|
"started_at": self._ts_to_str(game_row["started_at"]),
|
||||||
|
"ended_at": self._ts_to_str(game_row["ended_at"]),
|
||||||
|
"width": game_row["width"],
|
||||||
|
"height": game_row["height"],
|
||||||
|
"source": game_row["source"],
|
||||||
|
"map": game_row["map_name"],
|
||||||
|
"ruleset_name": game_row["ruleset_name"],
|
||||||
|
"ruleset_version": game_row["ruleset_version"],
|
||||||
|
"game_type": game_row["game_type"],
|
||||||
|
"your_snake_id": game_row["your_snake_id"],
|
||||||
|
"your_snake_name": game_row["your_snake_name"],
|
||||||
|
"your_snake_type": game_row["your_snake_type"],
|
||||||
|
"your_snake_version": game_row["your_snake_version"],
|
||||||
|
"winner_name": game_row["winner_name"],
|
||||||
|
"winner_you": bool(game_row["winner_you"]),
|
||||||
|
"final_turn": int(game_row["final_turn"] or 0),
|
||||||
|
"status": game_row["status"],
|
||||||
|
},
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"turn": int(row["turn"]),
|
||||||
|
"observed_at": self._ts_to_str(row["observed_at"]),
|
||||||
|
"my_move": row["my_move"],
|
||||||
|
"my_thinking": decode_json(row["my_thinking_json"]),
|
||||||
|
"board": decode_json(row["board_state_json"]),
|
||||||
|
"food": decode_json(row["food_json"]) or [],
|
||||||
|
"hazards": decode_json(row["hazards_json"]) or [],
|
||||||
|
"you": decode_json(row["you_json"]) or {},
|
||||||
|
"snakes": snakes_by_turn.get(int(row["turn"]), []),
|
||||||
|
}
|
||||||
|
for row in turn_rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── shared write-path helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_snake_turn_params(
|
||||||
|
self,
|
||||||
|
snakes:list[dict],
|
||||||
|
you_id:str|None,
|
||||||
|
game_id:str|None,
|
||||||
|
turn:int,
|
||||||
|
previous_positions:dict,
|
||||||
|
) -> list[tuple]:
|
||||||
|
"""Return one parameter tuple per snake for a snake_turns INSERT."""
|
||||||
|
result = []
|
||||||
|
for snake in snakes:
|
||||||
|
snake_id = snake.get("id")
|
||||||
|
if snake_id is None:
|
||||||
|
continue
|
||||||
|
head = snake.get("head", {})
|
||||||
|
head_x = head.get("x")
|
||||||
|
head_y = head.get("y")
|
||||||
|
new_head = (
|
||||||
|
(int(head_x), int(head_y))
|
||||||
|
if head_x is not None and head_y is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
inferred = self._infer_direction(previous_positions.get(snake_id), new_head)
|
||||||
|
result.append((
|
||||||
|
game_id,
|
||||||
|
turn,
|
||||||
|
snake_id,
|
||||||
|
snake.get("name"),
|
||||||
|
snake.get("health"),
|
||||||
|
snake.get("length"),
|
||||||
|
head_x,
|
||||||
|
head_y,
|
||||||
|
snake.get("body", []),
|
||||||
|
snake_id == you_id,
|
||||||
|
inferred,
|
||||||
|
snake.get("latency"),
|
||||||
|
))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _extract_game_end_params(self, game_state:dict) -> tuple:
|
||||||
|
"""Return (game_id, winner_name, winner_you, turn) from a game_end payload."""
|
||||||
|
game = game_state.get("game", {})
|
||||||
|
game_id = game.get("id")
|
||||||
|
board = game_state.get("board", {})
|
||||||
|
snakes = list(board.get("snakes", []))
|
||||||
|
you = self._extract_you(game_state)
|
||||||
|
winner_name = next((s.get("name") for s in snakes if s.get("name")), None)
|
||||||
|
you_id = you.get("id")
|
||||||
|
winner_you = any(s.get("id") == you_id for s in snakes)
|
||||||
|
turn = int(game_state.get("turn", 0))
|
||||||
|
return game_id, winner_name, winner_you, turn
|
||||||
|
|
||||||
|
def _calculate_survivor(self, snake_rows, your_snake_id:str|None) -> tuple[bool, str|None]:
|
||||||
|
"""Return (winner_you, survivor_name) from a list of snake_turns rows."""
|
||||||
|
survivor_ids = [s["snake_id"] for s in snake_rows if s["snake_id"]]
|
||||||
|
winner_you = bool(
|
||||||
|
your_snake_id
|
||||||
|
and your_snake_id in survivor_ids
|
||||||
|
and len(survivor_ids) == 1
|
||||||
|
)
|
||||||
|
survivor_name = next((s["snake_name"] for s in snake_rows if s["snake_name"]), None)
|
||||||
|
return winner_you, survivor_name
|
||||||
|
|||||||
Reference in New Issue
Block a user