No man's land: TurnResult.unowned + release/re-claim in applyTurnResult + distinct grey map rendering (#16 P3)
- shared/protocol.ts: TurnResult.unowned (keys released this turn) + the map presentation contract (regionFillStyle / unownedTooltipLabel, hue-less grey) - shared/types.ts: required GameState.unowned (cumulative mid-game releases; [] at init, distinct from preset-unassigned regions) - server/engine.ts: applyTurnResult applies releases first (sole-owner only, never fabricating no man's land from unassigned keys), then claims against the post-release snapshot (gain on an ownerless key = re-claim); lone release is isMeaningful; prompts + user payload carry the unowned field - server/stub.ts: unchanged behavior (never releases; e2e stays stable) - server/db.ts: legacy rows load with unowned defaulted to [] - server/index.ts: create-game sets []; jump reference table includes released keys so the model can name them for re-claim - WorldMap/App: unowned prop -> visible grey + 'No man's land' tooltip - unit tests: 105 -> 127 (release/re-claim suites, presentation contract, legacy-row normalization)
This commit is contained in:
@@ -46,6 +46,16 @@ export interface TurnResult {
|
||||
stateChanges: StateChange[];
|
||||
relations: RelationChange[];
|
||||
territoryChanges: TerritoryChange[];
|
||||
/**
|
||||
* (v0.22, #16 P3) Region keys that become **unowned** — "no man's land" —
|
||||
* this turn. The engine removes each key from its current (sole) owner so it
|
||||
* ends the turn ownerless and marks it in `GameState.unowned`; the LLM
|
||||
* narrates the cause (war, cataclysm, collapse) in `events`. Re-claiming is
|
||||
* NOT expressed here: a nation re-claims an unowned region with an ordinary
|
||||
* `gain` on that key. Only keys a nation currently owns can be released;
|
||||
* empty on normal turns.
|
||||
*/
|
||||
unowned: string[];
|
||||
}
|
||||
|
||||
/** Per-region map payload: region key -> owning nation + its stable color. */
|
||||
@@ -188,6 +198,47 @@ export function nationColor(id: string): string {
|
||||
return `hsl(${hue} 58% 46%)`;
|
||||
}
|
||||
|
||||
// --- No man's land (v0.22, #16 P3) — the map presentation contract ---
|
||||
//
|
||||
// An unowned region has two distinct looks, and the difference is NOT in the
|
||||
// RegionMap (a key absent there is simply unowned): it is decided by the
|
||||
// client from `GameState.unowned` (the keys released mid-game). The constants
|
||||
// and the one decision function live here — shared and pure — so the
|
||||
// client's choropleth is the only consumer and the color contract is testable
|
||||
// from the server-side tests (shared/*.test.ts is not in the vitest glob).
|
||||
|
||||
/** The distinct, visible neutral grey for a **no man's land** region (released mid-game). Hue-less by construction, so it can never collide with a nation's `nationColor` (`hsl(h 58% 46%)`, always chromatic). */
|
||||
export const NO_MANS_LAND_FILL = '#3f3f46';
|
||||
/** The border of a no-man's-land region (brighter than a plain unowned one). */
|
||||
export const NO_MANS_LAND_STROKE = 'rgba(255,255,255,0.25)';
|
||||
/** The faint neutral fill for a region no nation owns that was never released mid-game (a preset never assigned it). */
|
||||
export const UNOWNED_FILL = 'rgba(148,163,184,0.05)';
|
||||
/** The border of a plain unowned region. */
|
||||
export const UNOWNED_STROKE = 'rgba(255,255,255,0.14)';
|
||||
/** The border of an owned region (brighter than unowned). */
|
||||
export const OWNED_STROKE = 'rgba(255,255,255,0.30)';
|
||||
|
||||
/** The hover-tooltip sub-line for a region with no owning nation. */
|
||||
export function unownedTooltipLabel(isNoMansLand: boolean): string {
|
||||
return isNoMansLand ? "No man's land" : 'No owner';
|
||||
}
|
||||
|
||||
/**
|
||||
* The fill + border for one sub-region, given its RegionMap entry
|
||||
* (`undefined` = unowned) and whether its key is no man's land (released
|
||||
* mid-game, i.e. in `GameState.unowned`). Pure + shared; see the section
|
||||
* comment above for why the distinction is not encoded in the RegionMap.
|
||||
*/
|
||||
export function regionFillStyle(
|
||||
owner: RegionOwner | undefined,
|
||||
isNoMansLand: boolean,
|
||||
): { fill: string; stroke: string } {
|
||||
if (owner) return { fill: owner.color, stroke: OWNED_STROKE };
|
||||
return isNoMansLand
|
||||
? { fill: NO_MANS_LAND_FILL, stroke: NO_MANS_LAND_STROKE }
|
||||
: { fill: UNOWNED_FILL, stroke: UNOWNED_STROKE };
|
||||
}
|
||||
|
||||
/** Rebuild the full event list entry shape from a TurnEventSpec + turn/date. */
|
||||
export function makeEvent(
|
||||
spec: TurnEventSpec,
|
||||
|
||||
Reference in New Issue
Block a user