# Sod Tori i — Architecture (pseudo-tables & relationships)

> Companion to `DESIGN.md` (the brand book) — the data model a developer builds
> from. Firebase project: **`hot-potato-games`** (Firestore native). Client:
> Flutter, BLoC, web-first. Status: v0.1 proposed, 2026-08-06 — Brett shreds.
>
> **Prime doctrine (inherited from x's parity rules): board state is never
> stored — it is DERIVED.** A match is its move list; every client runs the
> same pure-Dart engine over the same moves and must reach the same board.
> The preview, the resolution animation, and the stored truth are one engine.

---

## The tables

```
users/{uid}                          ← Anonymous Auth uid (upgradeable later)
  displayName   string               a generated potato name; editable
  goop          int                  THE forever counter (never decreases)
  wins          int
  losses        int
  createdAt     timestamp
  lastSeenAt    timestamp
```

```
matches/{matchId}
  code          string|null          4-char room code (friend mode); unique among open
  mode          'friend'|'compete'|'cpu'
  status        'open'|'active'|'complete'|'abandoned'
  players       { green: uid, red: uid|'cpu' }
  firstMove     'green'|'red'        the Tato-coin flip result, server-assigned
  moves         array<Move>          the event log — THE match (see Move below)
  matchGoop     { green: int, red: int }   summons this match — the stealable pot
  winner        'green'|'red'|null
  createdAt     timestamp
  updatedAt     timestamp
```

```
Move (embedded in matches.moves — append-only)
  n             int                  move number, 0-based; parity enforces turns
  by            'green'|'red'
  verb          'move'|'boop'|'goop'
  potuh         [col,row]            the acting Potuh's square
  target        [col,row]            move-destination / boop-target / goop-tile
  at            timestamp
```

```
queue/{uid}                          ← Compete matchmaking (one doc per waiting player)
  goop          int                  copied at enqueue; matched by |Δgoop|,
  enqueuedAt    timestamp            window widens with wait time (no elo yet)
```

```
scenarios (LOCAL — assets/scenarios.json, no Firestore)
  id, title, board[], objective, parMoves      Practice is offline
```

```
session events (EMIT-ONLY — shape per hotpotatogames/_status/SESSIONS-CONTRACT.md)
  { gameId: 'sod_tori_i', kind: 'play',    uid, matchId, at }   on match start
  { gameId: 'sod_tori_i', kind: 'session', uid, matchId, at }   on match complete
  cell_mobile owns the counting rig; sod_tori_i only emits.
```

## The relationships

```
users 1 ──── n matches        (via players.green / players.red)
users 1 ──── 0..1 queue       (enqueued at most once)
matches 1 ── n Move           (embedded, append-only, event-sourced)
matches ──→ users.goop        on complete: winner.goop += matchGoop[loser]
                              (ADDITIVE — loser keeps theirs; number never ↓)
matches ──→ users.wins/losses on complete
Move ──→ board state          NEVER stored; derived by engine(moves)
engine ──→ preview & replay   same function: preview = engine run ahead of time
cpu matches ──→ goop          summons count to YOUR counter; no stealable pot,
                              no win/loss record vs CPU in v1, no session credit
```

## The turn protocol (multiplayer)

1. Client validates the action locally with the engine (legal verbs, aiming).
2. Append one `Move` to `matches.moves` (transaction: `n` must equal
   `moves.length`, `by` must match parity + `firstMove`).
3. Both clients re-derive board state from the full move list; the resolution
   animation replays what the preview promised.
4. Security rules enforce: players only append to their own matches, one move
   per turn, no edits to history, `matchGoop`/`winner` written by the mover's
   same transaction and re-derivable (auditable) from moves.
5. Disconnect = resume from the move list. Abandon after timeout → `abandoned`,
   no goop steal.

## Firestore rules

Canonical file: `~/Potatuhs/.config/firestore.rules` (the symlink convention —
every hot-potato-games repo links it). Add a `sodtori_` namespace or
`matches`-scoped block there; deploy via `/deploy-rules`. Never a local copy.

## Hosting & deploy

- Hosting site: **`sod-tori-i`** on project `hot-potato-games`
  (x keeps `sod-tori`). `firebase.json` target added in
  `~/Potatuhs/.config/firebase.json`.
- Deploy script (sendit pattern): run the engine test suite → gate →
  `flutter build web --release` with BUILD_ID/BUILD_TIME dart-defines →
  `firebase deploy --only hosting:sod-tori-i --project=hot-potato-games`.
- The engine (`lib/engine/`) is pure Dart — no Flutter imports — with one test
  group per rulebook number (RB-1 … RB-12 in DESIGN.md). The rulebook IS the
  test plan.
