A campaign starts knowing its own ground

The map was blank everywhere on a fresh start, including behind your
own lines, which read as amnesia rather than insurgency. You are a
local with a car: the pocket your side holds is the part of the map you
would obviously know, and the war is the part you would not.

It also fixes the board. offersAt splits its offers into targets you
have visited and targets you have not, and with nothing visited every
offer came up "new" - so the known-against-new comparison the whole
mission design turns on was not on screen until you had wandered around
for a while. The first board of a fresh campaign now reads: a known
target at 0.14 parts against new ones at 0.31 and 0.41.

Strictly bounded to liberated ground. Contested ground is not yours to
know, and none of this survives the line moving - the map still only
remembers who held a place when you last stood in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dejvino 2026-08-09 07:20:28 +02:00
parent 35f5b1c465
commit 3787350d4a
3 changed files with 89 additions and 2 deletions

View File

@ -5,7 +5,7 @@ import { areaAt, createAreas, createHeat, effectiveHeat, speedAttention, stepHea
import { ROAD_ATTENTION, routeAt, segmentAt } from './sim/roads'; import { ROAD_ATTENTION, routeAt, segmentAt } from './sim/roads';
import { baseAt, placeBases } from './sim/bases'; import { baseAt, placeBases } from './sim/bases';
import { buildGraph } from './sim/routing'; import { buildGraph } from './sim/routing';
import { createIntel, observe, reveal, survey } from './sim/intel'; import { createIntel, observe, reveal, revealHomeGround, survey } from './sim/intel';
import { import {
applyMissionImpact, applyMissionImpact,
controlAt, controlAt,
@ -293,6 +293,11 @@ async function boot() {
heatProps.finished(segment.id); heatProps.finished(segment.id);
} }
} }
} else {
// A fresh campaign starts knowing its own ground. You are a local with a
// car, not an amnesiac: the pocket your side holds is the part of the map
// you would obviously know, and the war is the part you would not.
revealHomeGround(intel, heat, model.roads, elapsed, controlOf);
} }
// Chassis transform at the end of the last two fixed steps, for interpolation. // Chassis transform at the end of the last two fixed steps, for interpolation.

View File

@ -137,6 +137,51 @@ export function survey(
return count; return count;
} }
/**
* What you already know at the start of a campaign: your own ground.
*
* A fresh map was blank everywhere, including behind your own lines, which made
* the first several sessions read as amnesia rather than as insurgency. You are
* a local with a car the pocket your own side holds is the part of the map you
* would obviously know, and the war is the part you would not.
*
* It also fixes the board. `offersAt` splits its offers into targets you have
* visited and targets you have not, and with nothing visited every offer came
* up "new" so the known-against-new comparison the whole mission design turns
* on simply was not on screen until you had wandered for a while.
*
* Strictly bounded to liberated ground. Contested ground is not yours to know.
*/
export function revealHomeGround(
intel: Intel,
heat: HeatState,
roads: RoadNetwork,
now: number,
controlOf: (x: number, z: number) => Control,
): void {
for (let row = 0; row < intel.cellsPerSide; row++) {
for (let col = 0; col < intel.cellsPerSide; col++) {
const x = intel.gridOrigin + (col + 0.5) * CELL_SIZE;
const z = intel.gridOrigin + (row + 0.5) * CELL_SIZE;
if (controlOf(x, z) !== 'liberated') continue;
const index = row * intel.cellsPerSide + col;
intel.explored[index] = 1;
intel.rememberedControl[index] = CONTROLS.indexOf('liberated');
}
}
for (const s of roads.segments) {
if (controlOf((s.ax + s.bx) / 2, (s.az + s.bz) / 2) !== 'liberated') continue;
observe(intel, heat, s.id, now);
}
// Junctions on home ground count as places you have been, which is what makes
// them eligible as the board's "known target".
for (const n of roads.nodes) {
if (controlOf(n.x, n.z) === 'liberated') intel.visitedNodes.add(n.id);
}
}
export interface RouteIntel { export interface RouteIntel {
/** Worst level remembered anywhere on the route. */ /** Worst level remembered anywhere on the route. */
worst: HeatLevel; worst: HeatLevel;

View File

@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
import { generateWorld } from './world'; import { generateWorld } from './world';
import { placeBases, baseAt } from './bases'; import { placeBases, baseAt } from './bases';
import { buildGraph, findRoute } from './routing'; import { buildGraph, findRoute } from './routing';
import { createIntel, observe, summariseRoute, survey, hasSeen } from './intel'; import { createIntel, observe, revealHomeGround, summariseRoute, survey, hasSeen } from './intel';
import { createAreas, createHeat, stepHeat } from './heat'; import { createAreas, createHeat, stepHeat } from './heat';
import { controlAt, createFront } from './regions';
import { import {
accept, accept,
createQuests, createQuests,
@ -233,3 +234,39 @@ describe('mission lifecycle', () => {
expect(state.active!.worked).toBe(0); expect(state.active!.worked).toBe(0);
}); });
}); });
describe('what a campaign starts out knowing', () => {
const started = () => {
const intel = createIntel(world.roads, world.extent);
const heat = createHeat(world.roads, areas);
const front = createFront(1337, world.extent, world.spawn);
revealHomeGround(intel, heat, world.roads, 0, (x, z) => controlAt(front, x, z));
return { intel, heat, front };
};
it('knows its own ground and nothing past the line', () => {
const { intel, front } = started();
let home = 0;
let away = 0;
for (const s of world.roads.segments) {
const liberated =
controlAt(front, (s.ax + s.bx) / 2, (s.az + s.bz) / 2) === 'liberated';
if (liberated) home += hasSeen(intel, s.id) ? 1 : 0;
else away += hasSeen(intel, s.id) ? 1 : 0;
}
expect(home).toBeGreaterThan(3);
// The war is the part you do not know. Nothing past the line is filled in.
expect(away).toBe(0);
});
it('gives the board a known target to weigh a new one against', () => {
// With nothing visited, offersAt has no "visited" pool at all and every
// offer comes up new — so the comparison the mission design turns on was
// not on screen until the player had wandered around for a while.
const { intel, front } = started();
const offers = offersAt(createQuests(), world.roads, graph, intel, bases[0]!, 0, 99);
expect(offers.some((o) => !o.novel)).toBe(true);
expect(offers.some((o) => o.novel)).toBe(true);
void front;
});
});