Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8252b665b5 | ||
|
|
7d50cba061 | ||
|
|
e70f65f112 | ||
|
|
e38cc5933f |
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to **History** are listed here, newest first.
|
All notable changes to **History** are listed here, newest first.
|
||||||
|
|
||||||
|
## v0.10.0 — 2026-10-11 21:25
|
||||||
|
- **Fix world map style (#2)** — made the choropleth fill fully opaque (`fill-opacity` 0.85 → 1.0) and darkened the Natural Earth terrain raster (`raster-opacity` 0.5 → 0.15), so the raster's flat water tint and fine 1:10m coastline no longer bleed through countries and compete with the borders.
|
||||||
|
- **Strengthened country border lines** — border width 0.7 → 1.25, and the per-region stroke alphas raised (owned 0.10 → 0.30, unowned 0.06 → 0.14) so the vector borders read clearly as the dominant edge, with owned regions still more prominent than unowned ones.
|
||||||
|
- Fixes #2.
|
||||||
|
|
||||||
|
## v0.9.0 — 2026-10-11 20:57
|
||||||
|
- **Reduced turn latency (prompt-level):** capped `recentEvents` in the turn prompt from the last 10 to the last 5 events, and added a HARD CONSTRAINT asking the reasoning model to keep its internal analysis brief before emitting the JSON. This shortens the chain-of-thought (the 170–223s turn driver) and lowers the risk of exhausting the 4000-token output budget before the answer (the original 502 failure mode). Reasoning stays enabled (`reasoning_effort: low`); the deterministic stub, the `TurnResult` contract, `max_tokens`, and `reasoning_effort` are all unchanged.
|
||||||
|
|
||||||
## v0.8.0 — 2026-10-11 20:29
|
## v0.8.0 — 2026-10-11 20:29
|
||||||
|
|
||||||
- **Remove a saved game (#20)** — every saved game now has a delete control: a trash button on its card in the home screen's "Saved games" section opens a confirmation dialog ("Delete this game?" showing the scenario/turn/date) with Cancel and a destructive Delete. The new `DELETE /api/games/:id` route removes the game's row from SQLite and returns `{ deleted: true }` (404 if the id is unknown).
|
- **Remove a saved game (#20)** — every saved game now has a delete control: a trash button on its card in the home screen's "Saved games" section opens a confirmation dialog ("Delete this game?" showing the scenario/turn/date) with Cancel and a destructive Delete. The new `DELETE /api/games/:id` route removes the game's row from SQLite and returns `{ deleted: true }` (404 if the id is unknown).
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "history",
|
"name": "history",
|
||||||
"version": "0.8.0",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).",
|
"description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).",
|
||||||
|
|||||||
+40
-1
@@ -1,10 +1,11 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { GameState, Preset, RoundItem } from '../shared/types';
|
import type { GameState, Preset, RoundItem } from '../shared/types';
|
||||||
import type { TurnResult } from '../shared/protocol';
|
import { makeEvent, type TurnResult } from '../shared/protocol';
|
||||||
import type { LlmClient } from './llm';
|
import type { LlmClient } from './llm';
|
||||||
import { makeStubLlmClient } from './stub';
|
import { makeStubLlmClient } from './stub';
|
||||||
import {
|
import {
|
||||||
applyTurnResult,
|
applyTurnResult,
|
||||||
|
buildSystemPrompt,
|
||||||
buildUserPrompt,
|
buildUserPrompt,
|
||||||
isMeaningful,
|
isMeaningful,
|
||||||
parseTurnResult,
|
parseTurnResult,
|
||||||
@@ -279,6 +280,44 @@ describe('buildUserPrompt (batched round)', () => {
|
|||||||
expect(parsed.roundInstruction).toMatch(/no action/i);
|
expect(parsed.roundInstruction).toMatch(/no action/i);
|
||||||
expect(parsed.roundInstruction).toMatch(/own momentum/i);
|
expect(parsed.roundInstruction).toMatch(/own momentum/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('caps recentEvents at the last 5 (in order) when the state has more than 5 events', () => {
|
||||||
|
const state = baseState();
|
||||||
|
const events = Array.from({ length: 8 }, (_, i) =>
|
||||||
|
makeEvent(
|
||||||
|
{
|
||||||
|
nation: 'uk',
|
||||||
|
type: 'political',
|
||||||
|
title: `Event ${i + 1}`,
|
||||||
|
text: `Narrative ${i + 1}`,
|
||||||
|
outcome: 'neutral',
|
||||||
|
},
|
||||||
|
i + 1,
|
||||||
|
`1939-09-0${i + 1}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
state.events = events;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(buildUserPrompt(state, [])) as {
|
||||||
|
state: { recentEvents: Array<{ title: string; turn: number; date: string }> };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parsed.state.recentEvents).toHaveLength(5);
|
||||||
|
// the last 5 of the 8 input events, in order
|
||||||
|
expect(parsed.state.recentEvents[0].title).toBe(events[3].title); // 'Event 4'
|
||||||
|
expect(parsed.state.recentEvents[4].title).toBe(events[7].title); // 'Event 8'
|
||||||
|
expect(parsed.state.recentEvents.map((e) => e.title)).toEqual([
|
||||||
|
events[3].title,
|
||||||
|
events[4].title,
|
||||||
|
events[5].title,
|
||||||
|
events[6].title,
|
||||||
|
events[7].title,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asks the model to keep its internal reasoning brief (concise-CoT constraint)', () => {
|
||||||
|
expect(buildSystemPrompt(preset)).toMatch(/reasoning brief|a few sentences/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('queueRoundItem', () => {
|
describe('queueRoundItem', () => {
|
||||||
|
|||||||
+2
-1
@@ -59,6 +59,7 @@ export function buildSystemPrompt(preset: Preset): string {
|
|||||||
'HARD CONSTRAINTS',
|
'HARD CONSTRAINTS',
|
||||||
'- Use only nation ids and ISO_A3 codes that exist in the provided state.',
|
'- Use only nation ids and ISO_A3 codes that exist in the provided state.',
|
||||||
'- Prefer 1-4 events per turn; keep each text 1-3 sentences.',
|
'- Prefer 1-4 events per turn; keep each text 1-3 sentences.',
|
||||||
|
'- Keep your internal reasoning brief (a few sentences) and go straight to the JSON result; do not over-deliberate or re-derive facts already present in the state.',
|
||||||
'- Keep every stat value between 0 and 100.',
|
'- Keep every stat value between 0 and 100.',
|
||||||
'- Only change borders when war or a decisive political shift justifies it.',
|
'- Only change borders when war or a decisive political shift justifies it.',
|
||||||
'- If a field is not relevant this turn, set it to an empty array.',
|
'- If a field is not relevant this turn, set it to an empty array.',
|
||||||
@@ -95,7 +96,7 @@ export function buildUserPrompt(state: GameState, items: RoundItem[]): string {
|
|||||||
nations: Object.fromEntries(
|
nations: Object.fromEntries(
|
||||||
Object.entries(state.nations).map(([id, n]) => [id, nationForPrompt(n)]),
|
Object.entries(state.nations).map(([id, n]) => [id, nationForPrompt(n)]),
|
||||||
),
|
),
|
||||||
recentEvents: state.events.slice(-10).map((e) => ({
|
recentEvents: state.events.slice(-5).map((e) => ({
|
||||||
turn: e.turn,
|
turn: e.turn,
|
||||||
date: e.date,
|
date: e.date,
|
||||||
nation: e.nation,
|
nation: e.nation,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const INLINE_STYLE: maplibregl.StyleSpecification = {
|
|||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: 'terrain',
|
source: 'terrain',
|
||||||
paint: {
|
paint: {
|
||||||
'raster-opacity': 0.5,
|
'raster-opacity': 0.15,
|
||||||
'raster-brightness-max': 0.8,
|
'raster-brightness-max': 0.8,
|
||||||
'raster-fade-duration': 200,
|
'raster-fade-duration': 200,
|
||||||
},
|
},
|
||||||
@@ -124,11 +124,11 @@ function applyRegions(geo: FeatureCollection, regions: RegionMap, playerNationId
|
|||||||
f.properties.fill = r.color;
|
f.properties.fill = r.color;
|
||||||
f.properties.owner = r.owner;
|
f.properties.owner = r.owner;
|
||||||
f.properties.ownerId = r.ownerId;
|
f.properties.ownerId = r.ownerId;
|
||||||
f.properties.stroke = 'rgba(255,255,255,0.10)';
|
f.properties.stroke = 'rgba(255,255,255,0.30)';
|
||||||
f.properties.isPlayer = r.ownerId === playerNationId ? 1 : 0;
|
f.properties.isPlayer = r.ownerId === playerNationId ? 1 : 0;
|
||||||
} else {
|
} else {
|
||||||
f.properties.fill = 'rgba(148,163,184,0.05)';
|
f.properties.fill = 'rgba(148,163,184,0.05)';
|
||||||
f.properties.stroke = 'rgba(255,255,255,0.06)';
|
f.properties.stroke = 'rgba(255,255,255,0.14)';
|
||||||
f.properties.owner = '';
|
f.properties.owner = '';
|
||||||
f.properties.ownerId = '';
|
f.properties.ownerId = '';
|
||||||
f.properties.isPlayer = 0;
|
f.properties.isPlayer = 0;
|
||||||
@@ -411,7 +411,7 @@ export default function WorldMap({
|
|||||||
id: FILL_LAYER,
|
id: FILL_LAYER,
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: COUNTRIES_SOURCE,
|
source: COUNTRIES_SOURCE,
|
||||||
paint: { 'fill-color': ['get', 'fill'], 'fill-opacity': 0.85 },
|
paint: { 'fill-color': ['get', 'fill'], 'fill-opacity': 1.0 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!map.getLayer(LINE_LAYER)) {
|
if (!map.getLayer(LINE_LAYER)) {
|
||||||
@@ -419,7 +419,7 @@ export default function WorldMap({
|
|||||||
id: LINE_LAYER,
|
id: LINE_LAYER,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: COUNTRIES_SOURCE,
|
source: COUNTRIES_SOURCE,
|
||||||
paint: { 'line-color': ['get', 'stroke'], 'line-width': 0.7, 'line-opacity': 0.95 },
|
paint: { 'line-color': ['get', 'stroke'], 'line-width': 1.25, 'line-opacity': 0.95 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// NOTE: layers are added in bottom→top order (each appends on top). The
|
// NOTE: layers are added in bottom→top order (each appends on top). The
|
||||||
|
|||||||
Reference in New Issue
Block a user