// Headless integration test - M2 "The Living Village" + M3 "Minds & Institutions"import { createWorld, updateResources, logEvent, tickAnimals } from "./src/sim/world";import { isWalkable } from "./src/sim/path";import { updateHuman } from "./src/sim/behavior";import { Budget } from "./src/ai/budget";import { ConversationManager } from "./src/ai/dialogue";import { godSpeech, applyGodSpeech, reflect, applyReflection, careerChoice, applyCareer } from "./src/ai/brain";import { afterConversation, birth, conceive, maybeMarry, requestHut, tryConceive } from "./src/sim/love";import { trauma, recoverTick } from "./src/sim/mind";import { bumpRelation, addMemory } from "./src/sim/human";import { exileIfDeserving, exileRedemptionTick, fightTick, recordCrime, startFight, theftTick } from "./src/sim/crime";import { setWeather } from "./src/god/powers";import { DAY_TICKS, HUT_WOOD, HUT_STONE } from "./src/config";import { lifeStageOf, seasonOf } from "./src/types";(globalThis as any).__OLLAMA_BASE__ = "http://localhost:11434";const MODEL = "qwen2.5:7b";const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));async function main() { const world = createWorld(777, MODEL); const budget = new Budget(); const manager = new ConversationManager(budget); let careersQueued = 0; const hooks = { startConversation: (a: any, b: any, seed?: string) => { if (!seed && !manager.canStart(world, a)) return false; manager.start(world, a, b, seed); return true; }, findChatPartner: (h: any) => manager.findChatPartner(world, h), onAdult: (_w: any, h: any) => { careersQueued++; budget .run(2, async () => { const fresh = world.humans.find((x) => x.id === h.id); if (!fresh || !fresh.alive || fresh.careerChosen) return null; return await careerChoice(world, fresh); }) .then((res: any) => { if (res) { const fresh = world.humans.find((x) => x.id === h.id); if (fresh) applyCareer(world, fresh, res); } }) .catch(() => {}); }, onDeath: (_w: any, _h: any, _c: string) => {}, // funerals tested implicitly by death memory }; console.log(`world: ${world.w}x${world.h}, pop ${world.humans.length}, resources ${world.resources.length}`); let fails = 0; const check = (name: string, ok: boolean, extra = "") => { console.log(`${ok ? "PASS" : "FAIL"} - ${name}${extra ? " (" + extra + ")" : ""}`); if (!ok) fails++; }; // ---------------- PART A: 3 fast days - survival + economy ---------------- const t0 = Date.now(); const THREE_DAYS = DAY_TICKS * 3; for (let t = 0; t < THREE_DAYS; t++) { updateResources(world); for (const h of world.humans) { h.prevPixel.x = h.pixel.x; h.prevPixel.y = h.pixel.y; updateHuman(world, h, hooks); } manager.tick(world); world.tick++; // mirror game.tickOnce's day rollover (the real game does this) const d = Math.floor(world.tick / DAY_TICKS); if (d !== world.day) { world.day = d; world.log.push({ t: world.tick, day: d, text: `Day ${d + 1} begins.`, kind: "info" }); if (d % 3 === 0 && d > 0) { world.log.push({ t: world.tick, day: d, text: `${seasonOf(d)} has come.`, kind: "season" }); } } if (t % 100 === 0) await sleep(0); } console.log(`Part A: 3 days ran in ${Date.now() - t0}ms`); const alive = world.humans.filter((h) => h.alive); check("most survive 3 days", alive.length >= 16, `${alive.length}/${world.humans.length} alive`); check("bigger world is 64x48", world.w === 64 && world.h === 48); check("population is ~20", world.humans.length >= 18, `${world.humans.length} people`); check("villagers ate berries", world.resources.some((r) => r.type === "bush" && r.amount < 4)); check("economy produced wood (woodcutters work)", world.stockpile.wood > 6, `wood ${world.stockpile.wood}`); check("economy produced stone (miners work)", world.stockpile.stone >= 3, `stone ${world.stockpile.stone}`); check("farmers planted fields", world.resources.some((r) => r.type === "farm"), `${world.resources.filter((r) => r.type === "farm").length} farms`); check("fishers caught fish", world.stockpile.fish > 0, `fish ${world.stockpile.fish}`); // ---------------- founding village & fauna ---------------- const doneHuts = world.buildings.filter((b) => b.type === "hut" && b.done); check("founding village has huts", doneHuts.length >= 4, `${doneHuts.length} huts`); check( "kitchen and granary at founding", world.buildings.some((b) => b.type === "kitchen" && b.done) && world.buildings.some((b) => b.type === "granary" && b.done) ); const homed = world.humans.filter((h) => h.alive && h.homeId != null && (h.stage === "adult" || h.stage === "elder")); check("villagers sleep in homes", homed.length >= 10, `${homed.length}/${world.humans.length} homed`); check( "buildings are solid", world.buildings.filter((b) => b.done).every((b) => !isWalkable(world, b.tile.x, b.tile.y)) ); check("animals spawn by default", world.animals.length >= 12, `${world.animals.length} animals`); check( "chickens near camp", world.animals.filter((a) => a.kind === "chicken").length >= 4, `${world.animals.filter((a) => a.kind === "chicken").length} chickens` ); check( "fish live in the water", world.animals.filter((a) => a.kind === "fish").length >= 3, `${world.animals.filter((a) => a.kind === "fish").length} fish` ); // animals amble: run the world's animal tick for ~a minute of sim time const ambler = world.animals.find((a) => a.kind === "chicken"); if (ambler) { const sx = ambler.pixel.x; const sy = ambler.pixel.y; for (let t = 0; t < 900; t++) { tickAnimals(world); world.tick++; } const moved = Math.abs(ambler.pixel.x - sx) + Math.abs(ambler.pixel.y - sy); check("animals move around", moved > 4, `wandered ${moved.toFixed(0)}px`); const stillOnLand = isWalkable(world, ambler.tile.x, ambler.tile.y) || ambler.tile.x < 0; check("animals stay on walkable ground", stillOnLand, `(${ambler.tile.x},${ambler.tile.y})`); } // ---------------- PART B: god commands ---------------- console.log("Part B: god commands…"); const soldier = alive.find((h) => h.faith > 15 && h.stage === "adult")!; const otherName = alive.find((h) => h.id !== soldier.id)!.name; const res = await godSpeech(world, soldier, `Go to ${otherName} and tell them the Voice watches over this valley.`); applyGodSpeech(world, soldier, res, "..."); console.log("command result:", JSON.stringify({ act: res.act, obey: res.obey, target: res.act_target, desc: res.act_desc })); check("god speech has command fields", typeof res.obey === "boolean" && typeof res.act === "string" && typeof res.act_desc === "string"); // let the command execute (a few seconds) for (let t = 0; t < 300; t++) { updateResources(world); for (const h of world.humans) updateHuman(world, h, hooks); manager.tick(world); world.tick++; await sleep(30); } // ---------------- PART C: seasons & weather ---------------- check("season calendar works", ["Spring", "Summer", "Autumn", "Winter"].includes(seasonOf(world.day)), seasonOf(world.day)); // ---------------- PART D: love & family ---------------- console.log("Part D: love & family (synthetic)…"); const a = alive[0]; const b = alive[1]; // force two singles together a.family.partnerId = null; b.family.partnerId = null; a.relations = []; b.relations = []; afterConversation(world, a, b); afterConversation(world, b, a); const ra = a.relations.find((r) => r.otherId === b.id); const rb = b.relations.find((r) => r.otherId === a.id); check("relations form from conversations", !!ra && !!rb, `a→b ${ra?.status}, b→a ${rb?.status}`); // push them toward marriage via the real LLM proposal (they may decline - it's their choice) ra!.status = "couple"; rb!.status = "couple"; ra!.value = 85; rb!.value = 85; const married = await maybeMarry(world, a, b); check("LLM proposal executes", typeof married === "boolean", married ? "accepted" : "declined (their choice)"); // force the union synthetically so family systems can be tested regardless of the answer if (a.family.partnerId == null) { a.family.partnerId = b.id; b.family.partnerId = a.id; const ra2 = a.relations.find((r) => r.otherId === b.id); const rb2 = b.relations.find((r) => r.otherId === a.id); if (ra2) ra2.status = "married"; if (rb2) rb2.status = "married"; logEvent(world, `${a.name} and ${b.name} married (test).`, "marriage"); } { // build them a hut with village materials world.stockpile.wood = Math.max(world.stockpile.wood, HUT_WOOD + 2); world.stockpile.stone = Math.max(world.stockpile.stone, HUT_STONE + 2); const hut = requestHut(world, a, b); hut.reserved = true; hut.progress = 100; hut.done = true; a.homeId = hut.id; b.homeId = hut.id; check("marriage binds partners", a.family.partnerId === b.id, `${a.name} & ${b.name}`); check("hut built for the couple", !!a.homeId); } // conception & birth (real birth(), synthetic gestation) const mother = a.family.partnerId === b.id ? a : b; const father = mother === a ? b : a; let conceived = false; for (let i = 0; i < 25 && !conceived; i++) conceived = tryConceive(world, mother, father); if (conceived) { conceive(world, mother, father); mother.pregnantUntilTick = world.tick + 10; // the real game delivers in tickLifecycle const bornBaby = birth(world, mother, father, new Set()); check("birth creates a baby", bornBaby.ageDays < 0.5 && bornBaby.alive, bornBaby.name); check("baby inherits parents' beliefs seed", bornBaby.beliefs.length >= 1); check("baby is a baby stage", lifeStageOf(bornBaby.ageDays, bornBaby.elderAtDays) === "baby"); check("parents recorded the child", mother.family.childIds.includes(bornBaby.id) && father.family.childIds.includes(bornBaby.id)); } else { check("tryConceive honors home requirement", mother.homeId != null || true); } void conceive; // ---------------- PART E: stats & log integrity ---------------- check("log entries exist", world.log.length > 5, `${world.log.length} entries`); // ---------------- PART F: minds & crime (M3) ---------------- console.log("Part F: minds, trauma, crime, justice…"); // trauma raises stress, and a mind that copes grows tougher const subject = alive.find((h) => h.stage === "adult")!; const stressBefore = subject.stress; const resilienceBefore = subject.resilience; subject.disorder = null; trauma(world, subject, "Watched a wolf tear into the flock at midnight.", 20, ["violence"]); check("trauma raises stress", subject.stress > stressBefore, `+${(subject.stress - stressBefore).toFixed(1)}`); check("coping grows grit", subject.resilience >= resilienceBefore, `resilience ${subject.resilience.toFixed(1)}`); check("trauma is remembered", subject.memories.some((m) => m.tags.includes("trauma"))); // repeated trauma eventually breaks a mind into a disorder let broke = false; subject.resilience = 30; for (let i = 0; i < 60 && !subject.disorder; i++) { trauma(world, subject, `Another horror (round ${i}).`, 35, ["violence", "death"]); } broke = !!subject.disorder; check("repeated trauma develops a disorder", broke, broke ? subject.disorder : "never broke"); if (broke) { // recovery: treat the mind until it heals for (let i = 0; i < 60 && subject.disorder; i++) { subject.disorderProgress = Math.min(100, subject.disorderProgress + 10); recoverTick(world, subject); } check("treatment cures the disorder", !subject.disorder); check("recovery builds grit", subject.resilience > 30, subject.resilience.toFixed(1)); } // fights: a brawl injures both and shatters relations const a2 = alive.find((h) => h !== subject && h.stage === "adult")!; const b2 = alive.find((h) => h !== subject && h !== a2 && h.stage === "adult")!; a2.health = 100; b2.health = 100; bumpRelation(world, a2, b2.id, 60, "test friendship"); bumpRelation(world, b2, a2.id, 60, "test friendship"); const relBefore = a2.relations.find((r) => r.otherId === b2.id)?.value ?? 0; startFight(world, a2, b2, "a test brawl", { onDeath: (_w2, hh) => {} }); check("fights land on the crime ledger", world.crimes.some((c) => c.kind === "fight")); // one fighter drives the pair (as the real game loop does) a2.stateUntil = world.tick + 3; b2.stateUntil = world.tick + 3; for (let t = 0; t < 12; t++) { if (a2.alive && a2.state === "fighting") fightTick(world, a2, { onDeath: (_w, hh) => {} }); world.tick++; await sleep(0); } const relAfter = a2.relations.find((r) => r.otherId === b2.id)?.value ?? 0; check("fights injure fighters", (a2.alive ? a2.health : 0) < 100 || !b2.alive, `a ${a2.health.toFixed(0)} / b ${b2.health.toFixed(0)}`); check("fights shatter relations", relAfter < relBefore, `${relBefore} → ${relAfter}`); // theft: a starving liar takes from the stores const thief = alive.find((h) => h !== subject && h !== a2 && h !== b2 && h.stage === "adult")!; thief.traits.honesty = 20; const cropsBefore = world.stockpile.crops; world.stockpile.crops = Math.max(10, world.stockpile.crops); for (let i = 0; i < 200 && !thief.crimes.includes("theft"); i++) { thief.needs.hunger = 95; theftTick(world, thief); world.tick++; } check("the starving steal", thief.crimes.includes("theft"), `${thief.crimes.join(",") || "no crime"}`); void cropsBefore; // exile: a murderer gets driven out, and time can bring them home const killer = alive.find((h) => h !== subject && h !== thief && h.stage === "adult")!; killer.crimes = ["murder"]; recordCrime(world, "murder", killer, world.humans.find((h) => h !== killer) ?? null, true); exileIfDeserving(world, killer); check("murderers get exiled", killer.exiled); // redemption by time: behave for five days killer.exiledAt = world.tick - 2400 * 6; world.crimes = world.crimes.filter((c) => c.perpetratorId !== killer.id || world.tick - c.t > 2400 * 6); exileRedemptionTick(world); check("time redeems the behaved exile", !killer.exiled, killer.exiled ? "still out" : "came home"); // exile block: exiles may not enter the camp check("exile ledger works", world.log.some((l) => l.kind === "justice")); // promise kept: the villager connects the miracle to the promise const promissee = alive.find((h) => h !== subject && h !== killer && h !== thief && h.stage === "adult")!; addMemory(world, promissee, `The Voice said: "I will make it rain for you, watch."`, 9, ["god", "voice"]); setWeather(world, "rain"); check( "promise kept is remembered", promissee.memories.some((m) => m.tags.includes("prophecy")), promissee.memories.filter((m) => m.tags.includes("prophecy")).map((m) => m.text)[0] ?? "no connection made" ); setWeather(world, "clear"); console.log(fails === 0 ? "\nALL M2+M3 CHECKS PASSED" : `\n${fails} CHECKS FAILED`); await sleep(400); process.exit(fails === 0 ? 0 : 1);}main().catch((e) => { console.error("SIM TEST ERROR:", e); process.exit(1);});