batched round: queue actions/diplomacy, jump forward commits one turn (v0.2.0)
CI / build + unit + e2e (pull_request) Successful in 1m51s
CI / build + unit + e2e (pull_request) Successful in 1m51s
This commit is contained in:
@@ -6,19 +6,20 @@ Guidance for AI agents (and humans) working in this repo.
|
||||
|
||||
A self-contained, AI-driven **alternate-history simulator** in the style of _Pax Historia_:
|
||||
the LLM is the world engine. You pick a scenario (one of 5 seed presets) and a nation to lead,
|
||||
then take actions / send diplomatic messages / ask an advisor; each move is sent to the model,
|
||||
which returns a structured `TurnResult` (events + stat/relation/territory deltas) that the engine
|
||||
validates, clamps, and applies to the world state. A MapLibre choropleth renders territorial
|
||||
ownership; the whole thing persists to SQLite and streams over SSE on a single port.
|
||||
then queue actions and diplomatic messages into a pending round; a single "Jump forward" sends
|
||||
the whole batch to the model, which returns a structured `TurnResult` (events + stat/relation/territory
|
||||
deltas) that the engine validates, clamps, and applies to the world state. A MapLibre choropleth
|
||||
renders territorial ownership; the whole thing persists to SQLite and streams over SSE on a single port.
|
||||
|
||||
## Change process (mandatory)
|
||||
|
||||
1. **Small, incremental, well-tested changes** — work is done one change at a time, in smaller parts; each part is tested before moving on. Connected tasks that touch the same area of code MAY be done together on one branch/PR with a single version bump (e.g. two tweaks to the same panel).
|
||||
2. **Feature branch + PR** — every change lands on a feature branch off `master` (no direct pushes to `master`). Open a pull request against `master` on Gitea (`https://git.kkhost.pl`, repo `OpenCode/History`).
|
||||
3. **Changelog entry** — every change is listed in `CHANGELOG.md` with a version, date and time (format: `## v<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
||||
4. **Version bump** — the version is updated in `package.json`.
|
||||
5. **CI must pass** — branch protection on `master` requires the CI status check to be green before merge. Merge via auto-merge (`POST /repos/OpenCode/History/pulls/{n}/merge` with body `{"do":"merge","merge_style":"merge","merge_when_checks_succeed":true}`) so the PR merges by itself once CI is green. **CI never deploys to production.**
|
||||
6. **Tag + manual deploy after merge** — once merged, tag the version on `master` (e.g. `v0.1.0`). Production (`https://history.dev.kkhost.pl`) is updated manually afterwards on the host: `docker compose up -d --build`, then verify the site.
|
||||
2. **Gitea issue first** — file an issue on Gitea (`https://git.kkhost.pl`, repo `OpenCode/History`) describing the change (title, scope, acceptance criteria) before or alongside creating the branch. The issue is the single source of intent; the PR references it.
|
||||
3. **Feature branch + PR** — every change lands on a feature branch off `master` (no direct pushes to `master`). Open a pull request against `master` on Gitea; reference the issue in the PR description.
|
||||
4. **Changelog entry** — every change is listed in `CHANGELOG.md` with a version, date and time (format: `## v<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
||||
5. **Version bump** — the version is updated in `package.json`.
|
||||
6. **CI must pass** — branch protection on `master` requires the CI status check to be green before merge. Merge via auto-merge (`POST /repos/OpenCode/History/pulls/{n}/merge` with body `{"do":"merge","merge_style":"merge","merge_when_checks_succeed":true}`) so the PR merges by itself once CI is green. **CI never deploys to production.**
|
||||
7. **Tag + manual deploy after merge** — once merged, tag the version on `master` (e.g. `v0.2.0`). Production (`https://history.dev.kkhost.pl`) is updated manually afterwards on the host: `docker compose up -d --build`, then verify the site.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -49,11 +50,11 @@ history/
|
||||
├── public/
|
||||
│ └── countries.geo.json # 177 features, id=ISO_A3 (committed; Vite copies to dist/)
|
||||
├── shared/ # NO runtime deps — the single source of truth
|
||||
│ ├── types.ts # domain types (Nation, GameState, Preset, …)
|
||||
│ └── protocol.ts # LLM TurnResult schema + client↔server messages + pure helpers
|
||||
│ ├── types.ts # domain types (Nation, GameState, RoundItem, Preset, …)
|
||||
│ └── protocol.ts # LLM TurnResult schema + AddAction/AddDiplomacy/Jump request types + pure helpers
|
||||
├── server/
|
||||
│ ├── index.ts # Express: /api/*, /sse/:id, static+SPA; reads PORT/LLM_*/DATA_DIR
|
||||
│ ├── engine.ts # pure: buildPrompt / parse+validate / applyTurnResult / runTurn / advisor
|
||||
│ ├── index.ts # Express: /api/* (action|diplomacy|jump|advisor|games|presets), /sse/:id; reads PORT/LLM_*/DATA_DIR
|
||||
│ ├── engine.ts # pure: buildPrompt / parse+validate / applyTurnResult / runTurn / queueRoundItem / advisor
|
||||
│ ├── llm.ts # LlmClient (injectable transport, retry+backoff, logging) + env config
|
||||
│ ├── stub.ts # deterministic LLM transport (tests/e2e) + ADVISOR_MARKER
|
||||
│ ├── map.ts # GameState → RegionMap (ISO_A3 → owner + stable color)
|
||||
@@ -61,7 +62,7 @@ history/
|
||||
│ ├── db.ts # SQLite open/save/load
|
||||
│ └── *.test.ts # unit tests (vitest)
|
||||
└── src/
|
||||
├── App.tsx # screen router: home → pick → game; owns game/regions/advisors state
|
||||
├── App.tsx # screen router: home → pick → game; owns game/regions/advisors/busy state
|
||||
├── main.tsx # React root + ThemeProvider(dark)
|
||||
├── index.css # Tailwind v4 + dark "command-center" theme
|
||||
├── lib/
|
||||
@@ -73,7 +74,7 @@ history/
|
||||
├── PresetPicker.tsx / NationSelect.tsx # home + nation pick screens
|
||||
├── WorldMap.tsx # MapLibre basemap + choropleth + click popup + player highlight
|
||||
├── NationPanel.tsx # a nation's stats/territory/diplomacy dossier
|
||||
├── ActionPanel.tsx # tabs: Action / Diplomacy / Advisor + "Jump forward"
|
||||
├── ActionPanel.tsx # tabs: Action / Diplomacy / Advisor + pending-queue list + "Jump forward"
|
||||
├── EventFeed.tsx # scrolling event log (newest first)
|
||||
├── StatBar.tsx
|
||||
└── ui/ # 14 shadcn/ui primitives
|
||||
@@ -97,12 +98,18 @@ history/
|
||||
## Conventions
|
||||
|
||||
- **The server is authoritative.** All world state lives in SQLite (`server/db.ts`). The client
|
||||
renders state and sends intents (`/api/games/:id/turn|jump|advisor`); it never computes outcomes.
|
||||
`shared/protocol.ts` is the single source of truth for the LLM `TurnResult` and the
|
||||
renders state and sends intents (`/api/games/:id/action|diplomacy|jump|advisor`); it never computes
|
||||
outcomes. `shared/protocol.ts` is the single source of truth for the LLM `TurnResult` and the
|
||||
client↔server message shapes — change it there first, then update both sides.
|
||||
- **No hardcoded origins.** The client uses same-origin relative paths (`/api/...`, `/sse/...`).
|
||||
In dev, Vite proxies them to :4321; in prod/Traefik they're the same origin. So the app works
|
||||
unchanged behind the proxy — never bake in `http://host:port`.
|
||||
- **Batched round model.** Actions and diplomatic messages are queued in `game.pending` via
|
||||
`POST /action` and `POST /diplomacy` (fast, no LLM call, no turn advance). A single
|
||||
`POST /jump` ("Jump forward") commits the whole pending batch in one LLM turn: `engine.runTurn`
|
||||
sends the entire `pending` array to the model, applies the `TurnResult`, clears `pending`, and
|
||||
advances the turn. An empty round (no items queued) is allowed and lets the world advance on its
|
||||
own. This design reduces LLM call frequency and makes each call a coherent batch of work.
|
||||
- **Defensive turn handling, never crash.** `engine.runTurn` strips fences, extracts the first
|
||||
balanced `{...}`, `JSON.parse`s, validates (ignores invalid ops, clamps stats to 0–100), retries
|
||||
once with a nudge if the result is empty/invalid, and if it's still unusable emits a generic
|
||||
@@ -110,11 +117,13 @@ history/
|
||||
(model down) surfaces as an HTTP 502 to the client (shown as a toast).
|
||||
- **Stable per-nation colors.** `shared.nationColor(id)` hashes a nation id → hue, shared by the
|
||||
server's `RegionMap` and any client coloring so the choropleth always agrees.
|
||||
- **Deterministic stub.** `server/stub.ts` answers the model contract deterministically: the player
|
||||
nation's `military` is set to `(current + 5)` (clamped) each turn, one event is emitted
|
||||
(action→military, chat→diplomatic, jump→political "The world moves on"), it allies the player with
|
||||
the first other nation, and the date advances 2 days. Unit + e2e assertions rely on these exact
|
||||
values, so keep the stub deterministic.
|
||||
- **Deterministic stub.** `server/stub.ts` answers the model contract deterministically per
|
||||
queued round item: each `action` item → one military/success event, each `diplomacy` item → one
|
||||
diplomatic/partial event, an empty round → one political "The world moves on" event. The player
|
||||
nation's `military` is set to `(current + 5)` (clamped) once per committed turn (not per item),
|
||||
the player is allied with the first other nation (by key order in the `nations` record), and the
|
||||
date advances 2 days. Unit + e2e assertions rely on these exact values, so keep the stub
|
||||
deterministic.
|
||||
- Styling: Tailwind utility classes + shadcn/ui only; dark "command-center" theme (slate bg, amber
|
||||
primary, cyan accent). No CSS beyond the Tailwind import in `src/index.css`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user