Additive 'Create scenario' path (v0.31): the home screen's create-scenario card opens a ScenarioForm (pick one of 3 light era templates + a free-text description + an editable, era-prefilled start year) whose 'Generate world' posts the draft request; the CheckpointReview screen shows the generated world (per-nation stats/territory/relations + backstory) and the advisor's checkpoint list as an approve/remove checklist, then the player picks a nation to lead and finalizes into a saved, playable game. Both LLM steps ride the scenario client (deterministic stub under LLM_STUB=1); a failure toasts and stays on the current screen. The turn stub and static presets are untouched. New e2e covers the full deterministic flow (near-future 2035, remove checkpoint 3, lead Boreas, finalize, one empty jump) and the 3-template manifest.
126 lines
5.4 KiB
TypeScript
126 lines
5.4 KiB
TypeScript
// Client-side API helper. All requests are same-origin relative paths (they are
|
|
// proxied by Vite in dev and served by Express in prod), so no origin is ever
|
|
// hardcoded — this works unchanged behind the Traefik proxy.
|
|
import type { GameState, Preset, PresetMeta } from '@shared/types';
|
|
import type {
|
|
AddActionResponse,
|
|
AddDiplomacyResponse,
|
|
AdvisorResponse,
|
|
ConfirmNationOpResponse,
|
|
CreateGameResponse,
|
|
DeleteGameResponse,
|
|
GameSummary,
|
|
RejectNationOpResponse,
|
|
RemoveQueueItemResponse,
|
|
TurnResponse,
|
|
} from '@shared/protocol';
|
|
import type {
|
|
ScenarioCommitRequest,
|
|
ScenarioCommitResponse,
|
|
ScenarioDraftRequest,
|
|
ScenarioDraftResponse,
|
|
TemplateMeta,
|
|
} from '@shared/scenario';
|
|
|
|
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
headers: { 'content-type': 'application/json' },
|
|
...init,
|
|
});
|
|
if (!res.ok) {
|
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export const api = {
|
|
listPresets: () => req<{ presets: PresetMeta[] }>('/api/presets'),
|
|
getPreset: (id: string) => req<{ preset: Preset }>(`/api/presets/${id}`),
|
|
createGame: (presetId: string, playerNationId: string) =>
|
|
req<CreateGameResponse>('/api/games', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ presetId, playerNationId }),
|
|
}),
|
|
getGame: (id: string) => req<{ game: GameState }>(`/api/games/${id}`),
|
|
// Delete a saved game entirely. Fast, non-LLM. Returns { deleted: true }.
|
|
deleteGame: (id: string) =>
|
|
req<DeleteGameResponse>(`/api/games/${id}`, { method: 'DELETE' }),
|
|
// Every saved game as a lightweight summary, newest-updated first (for the
|
|
// home screen's "Saved games" section / resume affordances).
|
|
listGames: () => req<{ games: GameSummary[] }>('/api/games'),
|
|
// Queue a free-text player action onto the pending round. No LLM call — the
|
|
// round is only committed by `jump`. Returns the updated game (with pending).
|
|
addAction: (id: string, text: string) =>
|
|
req<AddActionResponse>(`/api/games/${id}/action`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ text }),
|
|
}),
|
|
// Queue a diplomatic message to a target nation onto the pending round.
|
|
addDiplomacy: (id: string, to: string, message: string) =>
|
|
req<AddDiplomacyResponse>(`/api/games/${id}/diplomacy`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ to, message }),
|
|
}),
|
|
// Remove one queued item from the pending round, by its index in `game.pending`.
|
|
// Fast, non-LLM — no turn advance. Returns the updated game.
|
|
removeQueueItem: (id: string, index: number) =>
|
|
req<RemoveQueueItemResponse>(`/api/games/${id}/queue/remove`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ index }),
|
|
}),
|
|
// (v0.23, #16 P4a; v0.24, #16 P4b) Confirm a pending nation-op proposal by
|
|
// its index in `game.pendingNationOps`: a rename takes effect (`edits.name`
|
|
// overrides the proposed name), a split partitions the nation's territory
|
|
// (`edits.playerPart` picks which part the player leads, default 0), and a
|
|
// merge folds its inputs into the survivor (`edits.name` overrides the
|
|
// merged name). Fast, non-LLM — no turn advance. Returns the updated game.
|
|
confirmNationOp: (id: string, index: number, edits?: { name?: string; playerPart?: number }) =>
|
|
req<ConfirmNationOpResponse>(`/api/games/${id}/nation-op/confirm`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ index, ...(edits ? { edits } : {}) }),
|
|
}),
|
|
// (v0.23, #16 P4a) Reject (drop) a pending nation-op proposal by index,
|
|
// without applying it. Fast, non-LLM — no turn advance. Returns the updated game.
|
|
rejectNationOp: (id: string, index: number) =>
|
|
req<RejectNationOpResponse>(`/api/games/${id}/nation-op/reject`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ index }),
|
|
}),
|
|
// Commit the whole pending round (or an empty one) in a single LLM turn.
|
|
jump: (id: string) =>
|
|
req<TurnResponse>(`/api/games/${id}/jump`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({}),
|
|
}),
|
|
advisor: (id: string, prompt?: string) =>
|
|
req<AdvisorResponse>(`/api/games/${id}/advisor`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ prompt }),
|
|
}),
|
|
map: (id: string) =>
|
|
req<{ regions: Record<string, { color: string; owner: string; ownerId: string }> }>(
|
|
`/api/games/${id}/map`,
|
|
),
|
|
// --- Dynamic scenario generation (v0.31, #52) ---------------------------
|
|
// The additive "Create scenario" path: a two-step LLM flow (design a draft
|
|
// world + checkpoint list, then finalize it into a saved, playable game).
|
|
// The era-template manifest for the form's cards (id / name / year / blurb).
|
|
listScenarioTemplates: () => req<{ templates: TemplateMeta[] }>('/api/scenarios/templates'),
|
|
// Step 1: design a draft world + an advisor checkpoint list from an era
|
|
// template + a free-text description. An LLM call — can be slow; a 502
|
|
// means the model is down or returned nothing usable (the caller toasts).
|
|
draftScenario: (body: ScenarioDraftRequest) =>
|
|
req<ScenarioDraftResponse>('/api/scenarios/draft', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
// Step 2: finalize the approved draft (constrained to the approved
|
|
// checkpoints) into a new, playable game and save it. Returns the id.
|
|
commitScenario: (body: ScenarioCommitRequest) =>
|
|
req<ScenarioCommitResponse>('/api/scenarios/commit', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
};
|