feat(#50): make the four 0-100 stats consequential
CI / build + unit + e2e (pull_request) Successful in 2m8s
CI / build + unit + e2e (pull_request) Successful in 2m8s
- Add a hard military conquest gate (canConquer, CONQUEST_MILITARY_MARGIN=15) enforced in applyTurnResult: a contested invasion is repelled when the attacker's military is more than 15 below the defender's; re-claims of unowned regions are never gated. Emits a new 'repelled' event. - Document the soft levers in the system prompt: stability<30 + gathering marker -> non-military takeover; economy growth/decay drives other stats. - Emit bounded frontierMilitary (per-neighbour balance) and takeoverRisks hints in the user payload (only when present). - repelled outcome color (#9f1239); full unit test coverage; stub untouched. - Bump to v0.30.0. Fixes #50
This commit is contained in:
@@ -452,6 +452,42 @@ export function nationColor(id: string): string {
|
||||
return `hsl(${hue} 58% 46%)`;
|
||||
}
|
||||
|
||||
// --- Combat: the military hard gate (v0.30, #50) ------------------------------
|
||||
//
|
||||
// The one rule the engine enforces on the player's behalf instead of trusting
|
||||
// the LLM: a contested conquest only succeeds when the attacker is not MORE
|
||||
// than `CONQUEST_MILITARY_MARGIN` below the defender. The LLM still proposes
|
||||
// the transfer; the engine decides whether it holds (see `applyTurnResult`).
|
||||
// Re-claims of unowned / no-man's-land regions are never gated.
|
||||
|
||||
/**
|
||||
* (v0.30, #50) The margin of military superiority an attacker must NOT be more
|
||||
* than below the defender for a contested conquest to succeed. A gap of exactly
|
||||
* this many points is still enough to win; a gap one larger is repelled.
|
||||
*/
|
||||
export const CONQUEST_MILITARY_MARGIN = 15;
|
||||
|
||||
/**
|
||||
* (v0.30, #50) Hard military gate for a **contested** conquest (a region
|
||||
* currently owned by a different nation). Returns `true` — the invasion
|
||||
* proceeds — unless the attacker's military is MORE than `margin` below the
|
||||
* defender's: equal, or within `margin` (default {@link CONQUEST_MILITARY_MARGIN}),
|
||||
* is enough to win, while a gap larger than `margin` is repelled.
|
||||
*
|
||||
* The comparison is intentionally one-sided (it only protects the *defender*
|
||||
* from an overwhelming odds gap, not the attacker from a small disadvantage):
|
||||
* `attacker < defender - margin` → repel; everything else (equal, within the
|
||||
* margin, or ahead) → proceed. Pure + shared so the engine, the tests, and the
|
||||
* prompt's frontier-balance hint all agree on the outcome.
|
||||
*/
|
||||
export function canConquer(
|
||||
attackerMil: number,
|
||||
defenderMil: number,
|
||||
margin: number = CONQUEST_MILITARY_MARGIN,
|
||||
): boolean {
|
||||
return !(attackerMil < defenderMil - margin);
|
||||
}
|
||||
|
||||
// --- 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
|
||||
|
||||
+9
-1
@@ -79,7 +79,15 @@ export const EVENT_TYPES: EventType[] = [
|
||||
'cultural',
|
||||
];
|
||||
|
||||
export type EventOutcome = 'success' | 'failure' | 'partial' | 'neutral';
|
||||
/**
|
||||
* The outcome of a narrative event. `success` / `failure` / `partial` /
|
||||
* `neutral` are the four the LLM is asked to emit. `repelled` (v0.30, #50) is
|
||||
* reserved for the engine: it is emitted automatically when the military hard
|
||||
* gate rejects a contested conquest (an invasion whose attacker was more than
|
||||
* `CONQUEST_MILITARY_MARGIN` below the defender) — the LLM never produces it,
|
||||
* and the client renders it in a distinct muted-red failure tone.
|
||||
*/
|
||||
export type EventOutcome = 'success' | 'failure' | 'partial' | 'neutral' | 'repelled';
|
||||
|
||||
// --- Map markers (v0.27, #44) -------------------------------------------------
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user