Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions devtools/demo-live-attest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ try {
await page.goto(baseUrl, { waitUntil: "domcontentloaded" });

await page.evaluate(async () => {
const [{ createStore }, manifestMod, logicMod, rendererMod, uiMod, idsMod, presetsMod] = await Promise.all([
const [{ createStore }, webDriverMod, manifestMod, logicMod, rendererMod, uiMod, idsMod, presetsMod] = await Promise.all([
import("/src/kernel/store/createStore.js"),
import("/src/platform/persistence/webDriver.js"),
import("/src/game/manifest.js"),
import("/src/game/runtime/index.js"),
import("/src/game/render/renderer.js"),
Expand All @@ -176,7 +177,11 @@ try {
canvas.width = Math.floor(1200 * dpr);
canvas.height = Math.floor(800 * dpr);

const store = createStore(manifestMod.manifest, { reducer: logicMod.reducer, simStep: logicMod.simStepPatch });
const store = createStore(
manifestMod.manifest,
{ reducer: logicMod.reducer, simStep: logicMod.simStepPatch },
{ storageDriver: webDriverMod.getDefaultWebDriver() },
);
const render = rendererMod.render;
const ui = new uiMod.UI(store, canvas);

Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
- `tools/llm/*`: dev-only LLM adapters, read models and gate tooling. Runtime code must not import them.

## Current Cleanup Slice
- Sim runtime extraction continued: active-order execution, navigation, infra helper logic and shared state counters now live under `src/game/sim/runtime/*`; `src/game/runtime/*` keeps compatibility re-export facades only.
- UI/sim decoupling continued: UI no longer imports `src/game/sim/*` directly; read decisions for builder tiles, seed display and tile interaction now flow through `src/game/viewmodel/*` selectors.
- UI state access consolidation started: run-phase/running/grid/brush context reads now route through `src/game/viewmodel/uiStateSelectors.js` instead of ad-hoc inline state field access.
- Kernel/platform split continued: browser persistence drivers moved to `src/platform/persistence/webDriver.js`; kernel persistence now stays platform-neutral and app/test callsites inject storage drivers explicitly.
- Foundation eligibility now belongs to `src/game/runtime/foundationEligibility.js`; `src/game/sim/foundationEligibility.js` is compatibility-only.
- Fog read-model shaping now belongs to `src/game/viewmodel/fogIntel.js`; `src/game/render/fogOfWar.js` is render-only again.
- Lage-panel read helpers now belong to `src/game/viewmodel/lageStats.js` instead of being embedded inside the UI renderer.
Expand Down Expand Up @@ -64,6 +68,7 @@
- Operative reducer path remains `src/game/sim/reducer/index.js`.
- `src/game/sim/reducer.js` remains the compatibility facade.
- `src/game/runtime/index.js` remains the stable public sim-step surface even though active order execution now delegates into runtime helper modules.
- `src/game/runtime/*` helper modules are now compatibility facades that re-export canonical implementations from `src/game/sim/runtime/*`.
- `src/game/sim/worldPresets.js`, `src/game/sim/mapspec.js`, and `src/game/sim/worldgen.js` remain the stable path-pinned surfaces consumed by runtime and tests.
- Boot still dispatches `GEN_WORLD`, but world boot now compiles through `map.spec`.
- Renderer orchestration in `src/app/main.js` and `src/game/render/renderer.js` remains canonical and reusable.
Expand Down
9 changes: 9 additions & 0 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# STATUS - Current Head

## Snapshot (2026-03-20)
- Runtime/sim boundary cleanup continued (2026-03-26): active-order execution, order navigation, infra helpers and shared state counters moved from `src/game/runtime/*` into canonical `src/game/sim/runtime/*`; runtime paths remain compatibility re-exports.
- UI decoupling continued (2026-03-26): direct `ui -> sim` imports were removed in favor of `src/game/viewmodel/*` selector surfaces (`builderSelectors`, `tileInteractionSelectors`, `uiStateSelectors`).
- Kernel/platform split continued (2026-03-26): browser persistence moved to `src/platform/persistence/webDriver.js`; kernel store now defaults to explicit neutral driver behavior and app/tests inject web drivers where needed.
- Determinism guard coverage expanded (2026-03-26): new `tests/test-deterministic-patch-chain.mjs` verifies same-seed/same-action replay emits identical reducer/simStep patch chains and signature-material hashes.
- Runtime now boots directly against canonical `src/game/*` and `src/kernel/*` modules; legacy `src/project/*` and `src/core/kernel/*` facades were removed.
- Dev-only LLM helpers now live under `tools/llm/*`, and runtime/UI imports no longer depend on LLM modules.
- Sim cleanup follow-up landed: foundation eligibility moved to `src/game/runtime/foundationEligibility.js`, fog intel moved to `src/game/viewmodel/fogIntel.js`, and Lage-panel stat helpers moved to `src/game/viewmodel/lageStats.js`.
Expand Down Expand Up @@ -56,11 +60,16 @@
- `src/game/render/fogOfWar.js` now contains render-only fog logic; advisor fog shaping moved into `src/game/viewmodel/fogIntel.js`.
- `src/game/sim/reducer/index.js` now re-exports `shouldAdvanceSimulation` while consuming extracted gate and order command modules instead of defining them inline.
- `src/game/sim/reducer/index.js` now delegates active order execution through `src/game/runtime/processActiveOrderRuntime.js` while keeping the runtime/public export surface stable.
- `src/game/sim/reducer/core.js` now consumes canonical simulation runtime helpers from `src/game/sim/runtime/*` and no longer imports helper logic from `src/game/runtime/*`.
- `src/game/runtime/stateCounts.js` now owns shared role/mask counting helpers used by both `src/game/sim/reducer/index.js` and `src/game/sim/reducer/winConditions.js`.
- `src/game/runtime/infraRuntime.js` now owns shared infra staging helpers used by `src/game/sim/reducer/index.js` for candidate-mask cloning and committed-anchor checks.
- `src/game/runtime/infraRuntime.js`, `src/game/runtime/stateCounts.js`, `src/game/runtime/orderNavigation.js`, and `src/game/runtime/processActiveOrderRuntime.js` now act as compatibility re-export facades to `src/game/sim/runtime/*`.
- `src/game/viewmodel/uiStateSelectors.js` now centralizes run-phase/running/grid/brush-context reads for UI modules.
- `src/platform/persistence/webDriver.js` now owns browser persistence drivers; `src/kernel/store/persistence.js` stays platform-neutral.
- `src/game/sim/world/presetCatalog.js`, `src/game/sim/world/presetRuntime.js`, `src/game/sim/world/generationRuntime.js`, and `src/game/sim/mapspec/runtime.js` now own the moved preset/worldgen/MapSpec logic while the old top-level sim paths remain stable facades.
- `src/game/sim/grid/index.js` now owns shared 8-neighbor founder connectivity checks used by `src/game/runtime/foundationEligibility.js` and `src/game/sim/gates/phaseGates.js`.
- `tests/test-active-order-runtime.mjs` now hardens blocked, wait, harvest-progress, and harvest-complete branches for the extracted active-order runtime.
- `tests/test-deterministic-patch-chain.mjs` now verifies same-seed action replay patch-chain equivalence (reducer + simStep phases) and signature-material stability.

## Traceability Added
- `docs/traceability/rebuild-preparation-inventory.md`
Expand Down
3 changes: 3 additions & 0 deletions src/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { reducer, simStepPatch, shouldAdvanceSimulation } from "../game/runtime/
import { render } from "../game/render/renderer.js";
import { UI } from "../game/ui/ui.js";
import { hashString } from "../kernel/determinism/rng.js";
import { getDefaultWebDriver } from "../platform/persistence/webDriver.js";
import { createWorldStateLog } from "./runtime/worldStateLog.js";
import { bindBootStatusErrorHooks, setBootStatus } from "./runtime/bootStatus.js";

Expand Down Expand Up @@ -66,6 +67,8 @@ const DebugRuntime = {
const store = createStore(runtimeManifest, {
reducer,
simStep: simStepPatch,
}, {
storageDriver: getDefaultWebDriver(),
});
globalThis.__LIFEGAMELAB_STORE__ = store;
// Initialize Log with Seed from Store
Expand Down
18 changes: 1 addition & 17 deletions src/game/runtime/infraRuntime.js
Original file line number Diff line number Diff line change
@@ -1,17 +1 @@
import { ZONE_ROLE } from "../contracts/ids.js";
import { cloneTypedArray } from "../sim/shared.js";
import { hasAdjacentRoleTile4, isRoleMarked } from "../sim/grid/index.js";

export function getInfraCandidateMask(world, size) {
if (world?.infraCandidateMask && ArrayBuffer.isView(world.infraCandidateMask)) {
return cloneTypedArray(world.infraCandidateMask);
}
return new Uint8Array(size);
}

export function touchesCommittedInfraAnchor(world, idx, w, h) {
return isRoleMarked(world?.zoneRole, idx, ZONE_ROLE.CORE)
|| isRoleMarked(world?.zoneRole, idx, ZONE_ROLE.DNA)
|| isRoleMarked(world?.zoneRole, idx, ZONE_ROLE.INFRA)
|| hasAdjacentRoleTile4(world?.zoneRole, idx, w, h, [ZONE_ROLE.CORE, ZONE_ROLE.DNA, ZONE_ROLE.INFRA]);
}
export { getInfraCandidateMask, touchesCommittedInfraAnchor } from "../sim/runtime/infraRuntime.js";
70 changes: 1 addition & 69 deletions src/game/runtime/orderNavigation.js
Original file line number Diff line number Diff line change
@@ -1,69 +1 @@
export function findNextStepBfs4(world, fromIdx, targetIdx, w, h) {
if (fromIdx === targetIdx) return fromIdx;
const total = w * h;
if (fromIdx < 0 || targetIdx < 0 || fromIdx >= total || targetIdx >= total) return -1;
const alive = world?.alive;
if (!alive || !ArrayBuffer.isView(alive)) return -1;

const prev = new Int32Array(total);
prev.fill(-1);
const seen = new Uint8Array(total);
const queue = new Int32Array(total);
let qh = 0;
let qt = 0;
queue[qt++] = fromIdx;
seen[fromIdx] = 1;

while (qh < qt) {
const idx = queue[qh++];
if (idx === targetIdx) break;
const x = idx % w;
const y = (idx / w) | 0;
const candidates = [
[x - 1, y],
[x + 1, y],
[x, y - 1],
[x, y + 1],
];
for (const [nx, ny] of candidates) {
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
const nIdx = ny * w + nx;
if (seen[nIdx]) continue;
if (nIdx !== targetIdx && (Number(alive[nIdx] || 0) | 0) === 1) continue;
seen[nIdx] = 1;
prev[nIdx] = idx;
queue[qt++] = nIdx;
}
}

if (!seen[targetIdx]) return -1;
let step = targetIdx;
while (prev[step] !== -1 && prev[step] !== fromIdx) {
step = prev[step];
}
return prev[step] === fromIdx ? step : targetIdx;
}

export function moveEntityTile(world, fromIdx, toIdx) {
if (fromIdx === toIdx) return;
const scalarKeys = ["E", "reserve", "link", "lineageId", "hue", "age", "born", "died", "W", "clusterField", "superId"];
for (const key of scalarKeys) {
const arr = world?.[key];
if (!arr || !ArrayBuffer.isView(arr)) continue;
arr[toIdx] = arr[fromIdx];
arr[fromIdx] = 0;
}
if (world?.alive && ArrayBuffer.isView(world.alive)) {
world.alive[toIdx] = 1;
world.alive[fromIdx] = 0;
}
const trait = world?.trait;
if (trait && ArrayBuffer.isView(trait)) {
const fromOff = fromIdx * 7;
const toOff = toIdx * 7;
for (let i = 0; i < 7; i++) {
trait[toOff + i] = trait[fromOff + i];
trait[fromOff + i] = 0;
}
}
}
export { findNextStepBfs4, moveEntityTile } from "../sim/runtime/orderNavigation.js";
149 changes: 1 addition & 148 deletions src/game/runtime/processActiveOrderRuntime.js
Original file line number Diff line number Diff line change
@@ -1,148 +1 @@
import { createEmptyActiveOrder, HARVEST_TICKS } from "../sim/commands/orderCommands.js";
import { findNextStepBfs4, moveEntityTile } from "./orderNavigation.js";

export function processActiveOrderRuntime({
worldMutable,
preStepAlive,
simOut,
meta,
ticksPerSecond = 24,
harvestTicks = HARVEST_TICKS,
}) {
const activeOrder = simOut.activeOrder;
if (!activeOrder?.active) return { worldMutable, simOut };

const w = Number(worldMutable?.w || meta?.gridW || 0) | 0;
const h = Number(worldMutable?.h || meta?.gridH || 0) | 0;
const fromX = Number(activeOrder.fromX) | 0;
const fromY = Number(activeOrder.fromY) | 0;
const targetX = Number(activeOrder.targetX) | 0;
const targetY = Number(activeOrder.targetY) | 0;
const targetIdx = targetY * w + targetX;
const playerLineageId = Number(meta?.playerLineageId || 1) | 0;
let unitIdx = Number(simOut.selectedUnit ?? -1) | 0;
if (unitIdx < 0 || unitIdx >= w * h || (Number(worldMutable.alive?.[unitIdx] || 0) | 0) !== 1) {
unitIdx = fromY * w + fromX;
}
const validUnit =
unitIdx >= 0 &&
unitIdx < w * h &&
(Number(worldMutable.alive?.[unitIdx] || 0) | 0) === 1 &&
(Number(worldMutable.lineageId?.[unitIdx] || 0) | 0) === playerLineageId;

if (!validUnit || targetX < 0 || targetY < 0 || targetX >= w || targetY >= h) {
simOut.unitOrder = { active: false, fromX: -1, fromY: -1, targetX: -1, targetY: -1 };
simOut.activeOrder = createEmptyActiveOrder();
simOut.selectedUnit = -1;
simOut.lastAutoAction = "ORDER_ABORTED";
return { worldMutable, simOut };
}

if (unitIdx === targetIdx) {
const maxProgress = Math.max(1, Number(activeOrder.maxProgress || harvestTicks) | 0);
const nextProgress = Math.min(maxProgress, (Number(activeOrder.progress || 0) | 0) + 1);
if (nextProgress < maxProgress) {
simOut.activeOrder = {
...activeOrder,
active: true,
type: "HARVEST",
fromX: unitIdx % w,
fromY: (unitIdx / w) | 0,
targetX,
targetY,
progress: nextProgress,
maxProgress,
};
simOut.unitOrder = { active: true, fromX: unitIdx % w, fromY: (unitIdx / w) | 0, targetX, targetY };
simOut.selectedUnit = unitIdx;
simOut.lastAutoAction = `HARVEST_PROGRESS:${nextProgress}/${maxProgress}`;
} else {
simOut.playerDNA = Number(simOut.playerDNA || 0) + 1;
simOut.totalHarvested = Number(simOut.totalHarvested || 0) + 1;
simOut.unitOrder = { active: false, fromX: -1, fromY: -1, targetX: -1, targetY: -1 };
simOut.activeOrder = createEmptyActiveOrder();
simOut.selectedUnit = unitIdx;
simOut.lastAutoAction = `HARVEST_AUTO:${targetX},${targetY}`;
}
return { worldMutable, simOut };
}

const travelTicks = Math.max(1, ticksPerSecond | 0);
const travelProgress = (Number(activeOrder.progress || 0) | 0) + 1;
if (travelProgress < travelTicks) {
simOut.unitOrder = {
active: true,
fromX: unitIdx % w,
fromY: (unitIdx / w) | 0,
targetX,
targetY,
};
simOut.activeOrder = {
...activeOrder,
active: true,
type: "HARVEST",
fromX: unitIdx % w,
fromY: (unitIdx / w) | 0,
targetX,
targetY,
progress: travelProgress,
maxProgress: Math.max(1, Number(activeOrder.maxProgress || harvestTicks) | 0),
};
simOut.selectedUnit = unitIdx;
simOut.lastAutoAction = `MOVE_WAIT:${travelProgress}/${travelTicks}`;
return { worldMutable, simOut };
}

const navigationWorld = preStepAlive ? { ...worldMutable, alive: preStepAlive } : worldMutable;
const nextIdx = findNextStepBfs4(navigationWorld, unitIdx, targetIdx, w, h);
const occupiedAtTickStart = nextIdx >= 0 && (Number(preStepAlive?.[nextIdx] || 0) | 0) === 1;
const hardBlocked = nextIdx < 0 || (occupiedAtTickStart && nextIdx !== targetIdx);
if (hardBlocked) {
simOut.unitOrder = {
active: true,
fromX: unitIdx % w,
fromY: (unitIdx / w) | 0,
targetX,
targetY,
};
simOut.activeOrder = {
...activeOrder,
active: true,
type: "HARVEST",
fromX: unitIdx % w,
fromY: (unitIdx / w) | 0,
targetX,
targetY,
progress: 0,
maxProgress: Math.max(1, Number(activeOrder.maxProgress || harvestTicks) | 0),
};
simOut.selectedUnit = unitIdx;
simOut.lastAutoAction = "ORDER_WAIT_BLOCKED";
return { worldMutable, simOut };
}

moveEntityTile(worldMutable, unitIdx, nextIdx);
const nx = nextIdx % w;
const ny = (nextIdx / w) | 0;
simOut.selectedUnit = nextIdx;
simOut.unitOrder = {
active: true,
fromX: nx,
fromY: ny,
targetX,
targetY,
};
simOut.activeOrder = {
...activeOrder,
active: true,
type: "HARVEST",
fromX: nx,
fromY: ny,
targetX,
targetY,
progress: 0,
maxProgress: Math.max(1, Number(activeOrder.maxProgress || harvestTicks) | 0),
};
simOut.lastAutoAction = `MOVE_STEP:${nx},${ny}`;
return { worldMutable, simOut };
}
export { processActiveOrderRuntime } from "../sim/runtime/processActiveOrderRuntime.js";
38 changes: 1 addition & 37 deletions src/game/runtime/stateCounts.js
Original file line number Diff line number Diff line change
@@ -1,37 +1 @@
export function countAlivePlayerRoleCells(world, playerLineageId, roleId) {
const zoneRole = world?.zoneRole;
const alive = world?.alive;
const lineageId = world?.lineageId;
if (!zoneRole || !alive || !lineageId) return 0;
let count = 0;
for (let i = 0; i < zoneRole.length; i++) {
if ((Number(zoneRole[i]) | 0) !== (roleId | 0)) continue;
if ((Number(alive[i]) | 0) !== 1) continue;
if ((Number(lineageId[i]) | 0) !== (playerLineageId | 0)) continue;
count++;
}
return count;
}

export function countAlivePlayerMaskedCells(mask, world, playerLineageId) {
const alive = world?.alive;
const lineageId = world?.lineageId;
if (!mask || !alive || !lineageId) return 0;
let count = 0;
for (let i = 0; i < mask.length; i++) {
if ((Number(mask[i]) | 0) !== 1) continue;
if ((Number(alive[i]) | 0) !== 1) continue;
if ((Number(lineageId[i]) | 0) !== (playerLineageId | 0)) continue;
count++;
}
return count;
}

export function countMaskOnes(mask) {
if (!mask || !ArrayBuffer.isView(mask)) return 0;
let count = 0;
for (let i = 0; i < mask.length; i++) {
if ((Number(mask[i]) | 0) === 1) count++;
}
return count;
}
export { countAlivePlayerRoleCells, countAlivePlayerMaskedCells, countMaskOnes } from "../sim/runtime/stateCounts.js";
6 changes: 3 additions & 3 deletions src/game/sim/reducer/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ import { derivePatternBonuses, derivePatternCatalog } from "../patterns.js";
import {
getInfraCandidateMask,
touchesCommittedInfraAnchor,
} from "../../runtime/infraRuntime.js";
} from "../runtime/infraRuntime.js";
import {
countAlivePlayerMaskedCells,
countAlivePlayerRoleCells,
} from "../../runtime/stateCounts.js";
} from "../runtime/stateCounts.js";
import {
collectMaskIndices,
hasAdjacentMarkedTile,
Expand All @@ -79,7 +79,7 @@ import {
HARVEST_TICKS,
parseWorkerEntityId,
} from "../commands/orderCommands.js";
import { processActiveOrderRuntime } from "../../runtime/processActiveOrderRuntime.js";
import { processActiveOrderRuntime } from "../runtime/processActiveOrderRuntime.js";
import { reduceEconomyActions } from "./economy.js";
import { reduceRunActions } from "./run.js";
import { reduceZoneActions } from "./zones.js";
Expand Down
2 changes: 1 addition & 1 deletion src/game/sim/reducer/winConditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
WIN_MODE,
deriveGoalCode,
} from "../../contracts/ids.js";
import { countAlivePlayerRoleCells, countMaskOnes } from "../../runtime/stateCounts.js";
import { countAlivePlayerRoleCells, countMaskOnes } from "../runtime/stateCounts.js";
import { deriveGoalCodeWithPresetBias } from "./progression.js";

function deriveDominantTopology(cellPatternCounts) {
Expand Down
Loading