# ADR: Change GAME_ROUNDS from strings to numbers ## Status Accepted ## Date 2026-04-28 ## Context `GAME_ROUNDS` in `packages/shared/src/constants.ts` was typed as `["3", "10"] as const`, making `GameRounds` a string union (`"3" | "10"`). This meant `gameService.ts` had to cast the value with `Number(request.rounds)` deep in business logic — a type conversion happening far from the boundary where data enters the system. The type system was lying: `rounds` was described as a string everywhere but used as a number where it mattered. ## Decision Change `GAME_ROUNDS` to `[3, 10] as const` and update the Zod schema to use `z.literal(GAME_ROUNDS)` instead of `z.enum(GAME_ROUNDS)`. The single source of truth remains `constants.ts` — adding a new round count (e.g. `20`) requires only editing that file. ## Options considered ### Option A — Numbers everywhere ✅ Change `GAME_ROUNDS` to `[3, 10] as const`. Use `z.literal(GAME_ROUNDS)` in the schema. Update the frontend component state and `SettingGroup` props. Drop `Number()` cast in the service. Chosen because: JSON carries numbers natively, both ends of the wire are owned by this codebase, and type conversions belong at the boundary — not inside business logic. ### Option B — Keep strings, accept the cast Leave `GAME_ROUNDS` as `["3", "10"]`. The `Number()` cast stays in `gameService.ts`. Rejected because: it pushes type conversion into business logic and makes the inferred `GameRequest` type misleading. The cast has to live somewhere — the schema boundary is the right place. ### Option C — Coerce at the schema boundary Keep `GAME_ROUNDS` as numbers but use `z.coerce.number().pipe(z.literal(GAME_ROUNDS))` so the frontend can keep sending strings. Rejected because: coercion is for untrusted or uncontrolled inputs (form fields, query params, third-party clients). We control both ends of the wire. Coercing a self-inflicted type mismatch is treating a wound we gave ourselves. ## Consequences - `GameRounds` is now `3 | 10` instead of `"3" | "10"` - `Number(request.rounds)` cast removed from `gameService.ts` - `SettingGroup` in `GameSetup.tsx` now accepts `string | number` options - `useState` for rounds changed to `useState` - Adding a new round count requires only editing `GAME_ROUNDS` in `constants.ts` - `z.enum` cannot be used for number literals — `z.literal` must be used instead (this is a Zod constraint, not a project convention) ## Affected files - `packages/shared/src/constants.ts` - `packages/shared/src/schemas/game.ts` - `apps/api/src/services/gameService.ts` - `apps/api/src/services/gameService.test.ts` - `apps/api/src/controllers/gameController.test.ts` - `apps/web/src/components/game/GameSetup.tsx` ## References - [Zod literals](https://zod.dev/?id=literals) --- ## Setup guide / implementation notes 1. In `packages/shared/src/constants.ts`, change: ```ts export const GAME_ROUNDS = ["3", "10"] as const; ``` to: ```ts export const GAME_ROUNDS = [3, 10] as const; ``` 2. In `packages/shared/src/schemas/game.ts`, change: ```ts rounds: z.enum(GAME_ROUNDS), ``` to: ```ts rounds: z.literal(GAME_ROUNDS), ``` 3. In `apps/api/src/services/gameService.ts`, change: ```ts Number(request.rounds), ``` to: ```ts request.rounds, ``` 4. In `apps/api/src/services/gameService.test.ts`, change: ```ts rounds: "3", ``` to: ```ts rounds: 3, ``` 5. In `apps/api/src/controllers/gameController.test.ts`, change: ```ts rounds: "3", ``` to: ```ts rounds: 3, ``` Also add a pinning test before the refactor: ```ts it("returns 400 when rounds has an invalid value", async () => { const res = await request(app) .post("/api/v1/game/start") .send({ ...validBody, rounds: "invalid" }); expect(res.status).toBe(400); expect(res.body.success).toBe(false); }); ``` 6. In `apps/web/src/components/game/GameSetup.tsx`: - Update `SettingGroup` props to accept `string | number`: ```ts type SettingGroupProps = { options: readonly (string | number)[]; selected: string | number; onSelect: (value: string | number) => void; }; ``` - Update `LABELS` lookup to `LABELS[String(option)]` - Change rounds state from `useState` to `useState`