Tonic commited on
Commit
e3cb489
·
verified ·
1 Parent(s): 3e66fcb

ci: deploy game_server (364d42a69efd)

Browse files
src/nutonic_server/guess_telemetry_store.py CHANGED
@@ -6,10 +6,12 @@ from dataclasses import dataclass
6
  from threading import Lock
7
  from typing import TYPE_CHECKING, Callable
8
 
9
- from sqlalchemy import Column, Float, Integer, MetaData, String, Table, insert, select
 
 
10
 
11
  if TYPE_CHECKING:
12
- from sqlalchemy.engine import Engine
13
 
14
  metadata = MetaData()
15
 
@@ -25,6 +27,9 @@ guess_telemetry_rows = Table(
25
  Column("client_distance_km", Float, nullable=True),
26
  Column("ruleset_version", String(128), nullable=True),
27
  Column("session_id", String(128), nullable=True),
 
 
 
28
  )
29
 
30
  guess_idempotency = Table(
@@ -46,6 +51,9 @@ class GuessTelemetryIn:
46
  client_distance_km: float | None = None
47
  ruleset_version: str | None = None
48
  session_id: str | None = None
 
 
 
49
 
50
 
51
  class GuessTelemetryStore:
@@ -56,14 +64,48 @@ class GuessTelemetryStore:
56
 
57
  def initialize_schema(self) -> None:
58
  metadata.create_all(self._engine)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  def record(
61
  self,
62
  row: GuessTelemetryIn,
63
  *,
64
  idempotency_key: str | None,
65
- ) -> int:
66
- """Insert telemetry row; idempotent repeat returns same row id."""
67
  key = idempotency_key.strip() if idempotency_key and idempotency_key.strip() else None
68
  with self._lock:
69
  with self._engine.begin() as conn:
@@ -72,10 +114,10 @@ class GuessTelemetryStore:
72
  select(guess_idempotency.c.row_id).where(
73
  guess_idempotency.c.map_id == row.map_id,
74
  guess_idempotency.c.idempotency_key == key,
75
- )
76
  ).scalar_one_or_none()
77
  if hit is not None:
78
- return int(hit)
79
 
80
  rid = conn.execute(
81
  insert(guess_telemetry_rows)
@@ -88,8 +130,11 @@ class GuessTelemetryStore:
88
  client_distance_km=row.client_distance_km,
89
  ruleset_version=row.ruleset_version,
90
  session_id=row.session_id,
 
 
 
91
  )
92
- .returning(guess_telemetry_rows.c.id)
93
  ).scalar_one()
94
  if key:
95
  conn.execute(
@@ -97,10 +142,10 @@ class GuessTelemetryStore:
97
  map_id=row.map_id,
98
  idempotency_key=key,
99
  row_id=rid,
100
- )
101
  )
102
  self._sync_after_write()
103
- return int(rid)
104
 
105
  def _sync_after_write(self) -> None:
106
  if self._on_write is None:
 
6
  from threading import Lock
7
  from typing import TYPE_CHECKING, Callable
8
 
9
+ from sqlalchemy import Column, Float, Integer, MetaData, String, Table, insert, select, text
10
+
11
+ from nutonic_server.schemas import GuessRecordOut
12
 
13
  if TYPE_CHECKING:
14
+ from sqlalchemy.engine import Connection, Engine
15
 
16
  metadata = MetaData()
17
 
 
27
  Column("client_distance_km", Float, nullable=True),
28
  Column("ruleset_version", String(128), nullable=True),
29
  Column("session_id", String(128), nullable=True),
30
+ Column("display_handle", String(64), nullable=True),
31
+ Column("score_points", Integer, nullable=True),
32
+ Column("player_role", String(32), nullable=True),
33
  )
34
 
35
  guess_idempotency = Table(
 
51
  client_distance_km: float | None = None
52
  ruleset_version: str | None = None
53
  session_id: str | None = None
54
+ display_handle: str | None = None
55
+ score_points: int | None = None
56
+ player_role: str | None = None
57
 
58
 
59
  class GuessTelemetryStore:
 
64
 
65
  def initialize_schema(self) -> None:
66
  metadata.create_all(self._engine)
67
+ self._migrate_legacy_sqlite()
68
+
69
+ def _migrate_legacy_sqlite(self) -> None:
70
+ stmts = [
71
+ "ALTER TABLE guess_telemetry_rows ADD COLUMN display_handle VARCHAR(64)",
72
+ "ALTER TABLE guess_telemetry_rows ADD COLUMN score_points INTEGER",
73
+ "ALTER TABLE guess_telemetry_rows ADD COLUMN player_role VARCHAR(32)",
74
+ ]
75
+ try:
76
+ with self._engine.begin() as conn:
77
+ for stmt in stmts:
78
+ try:
79
+ conn.execute(text(stmt))
80
+ except Exception:
81
+ pass
82
+ except Exception:
83
+ pass
84
+
85
+ def _row_ack(self, conn: Connection, row_id: int) -> GuessRecordOut:
86
+ row = conn.execute(
87
+ select(
88
+ guess_telemetry_rows.c.id,
89
+ guess_telemetry_rows.c.client_distance_km,
90
+ guess_telemetry_rows.c.score_points,
91
+ guess_telemetry_rows.c.display_handle,
92
+ ).where(guess_telemetry_rows.c.id == row_id),
93
+ ).mappings().one()
94
+ return GuessRecordOut(
95
+ id=int(row["id"]),
96
+ recorded=True,
97
+ distance_km=row["client_distance_km"],
98
+ score_points=row["score_points"],
99
+ display_handle=row["display_handle"],
100
+ )
101
 
102
  def record(
103
  self,
104
  row: GuessTelemetryIn,
105
  *,
106
  idempotency_key: str | None,
107
+ ) -> GuessRecordOut:
108
+ """Insert telemetry row; idempotent repeat returns the same acknowledgement payload."""
109
  key = idempotency_key.strip() if idempotency_key and idempotency_key.strip() else None
110
  with self._lock:
111
  with self._engine.begin() as conn:
 
114
  select(guess_idempotency.c.row_id).where(
115
  guess_idempotency.c.map_id == row.map_id,
116
  guess_idempotency.c.idempotency_key == key,
117
+ ),
118
  ).scalar_one_or_none()
119
  if hit is not None:
120
+ return self._row_ack(conn, int(hit))
121
 
122
  rid = conn.execute(
123
  insert(guess_telemetry_rows)
 
130
  client_distance_km=row.client_distance_km,
131
  ruleset_version=row.ruleset_version,
132
  session_id=row.session_id,
133
+ display_handle=row.display_handle,
134
+ score_points=row.score_points,
135
+ player_role=row.player_role,
136
  )
137
+ .returning(guess_telemetry_rows.c.id),
138
  ).scalar_one()
139
  if key:
140
  conn.execute(
 
142
  map_id=row.map_id,
143
  idempotency_key=key,
144
  row_id=rid,
145
+ ),
146
  )
147
  self._sync_after_write()
148
+ return self._row_ack(conn, int(rid))
149
 
150
  def _sync_after_write(self) -> None:
151
  if self._on_write is None:
src/nutonic_server/main.py CHANGED
@@ -285,10 +285,12 @@ def post_guess_record(
285
  client_distance_km=body.client_distance_km,
286
  ruleset_version=body.ruleset_version.strip()[:128] if body.ruleset_version else None,
287
  session_id=sid or None,
 
 
 
288
  )
289
  key = idempotency_key.strip() if idempotency_key and idempotency_key.strip() else None
290
- rid = _guess_telemetry_store.record(row, idempotency_key=key)
291
- return GuessRecordOut(id=rid, recorded=True)
292
 
293
 
294
  @app.post("/api/v1/ranked/rounds/start", tags=["ranked"], response_model=RankedRoundStartOut)
 
285
  client_distance_km=body.client_distance_km,
286
  ruleset_version=body.ruleset_version.strip()[:128] if body.ruleset_version else None,
287
  session_id=sid or None,
288
+ display_handle=body.display_handle.strip()[:64] if body.display_handle else None,
289
+ score_points=body.score_points,
290
+ player_role=body.player_role.strip()[:32] if body.player_role else None,
291
  )
292
  key = idempotency_key.strip() if idempotency_key and idempotency_key.strip() else None
293
+ return _guess_telemetry_store.record(row, idempotency_key=key)
 
294
 
295
 
296
  @app.post("/api/v1/ranked/rounds/start", tags=["ranked"], response_model=RankedRoundStartOut)
src/nutonic_server/schemas.py CHANGED
@@ -91,11 +91,17 @@ class GuessRecordIn(BaseModel):
91
  guess_lon: float = Field(ge=-180.0, le=180.0)
92
  client_distance_km: float | None = Field(default=None, ge=0.0, le=40_000.0)
93
  ruleset_version: str | None = Field(default=None, max_length=128)
 
 
 
94
 
95
 
96
  class GuessRecordOut(BaseModel):
97
  id: int
98
  recorded: bool = True
 
 
 
99
 
100
 
101
  class RankedRoundStartIn(BaseModel):
 
91
  guess_lon: float = Field(ge=-180.0, le=180.0)
92
  client_distance_km: float | None = Field(default=None, ge=0.0, le=40_000.0)
93
  ruleset_version: str | None = Field(default=None, max_length=128)
94
+ display_handle: str | None = Field(default=None, max_length=64)
95
+ score_points: int | None = Field(default=None, ge=0, le=2_000_000_000)
96
+ player_role: str | None = Field(default=None, max_length=32)
97
 
98
 
99
  class GuessRecordOut(BaseModel):
100
  id: int
101
  recorded: bool = True
102
+ distance_km: float | None = None
103
+ score_points: int | None = None
104
+ display_handle: str | None = None
105
 
106
 
107
  class RankedRoundStartIn(BaseModel):