// Headless stress test: long runs, aggressive player, faction start, save/load. import { newGame, G, F, saveGame, loadGame, listSaves, factionTroopCount } from "../js/state.js"; import { endTurn, recruitToGarrison, moveArmy, raiseArmyFromCity } from "../js/sim.js"; import { applyEffect } from "../js/events.js"; // localStorage shim const store = new Map(); globalThis.localStorage = { getItem: k => store.get(k) ?? null, setItem: (k, v) => store.set(k, String(v)), removeItem: k => store.delete(k), key: i => [...store.keys()][i] ?? null, get length() { return store.size; }, }; function drainEvents(choose = 0) { let guard = 0; while (G.pendingEvents.length && guard++ < 80) { const ev = G.pendingEvents.shift(); const choice = ev.choices[Math.min(choose, ev.choices.length - 1)]; try { applyEffect(choice.effect); } catch (e) { console.log("EFFECT ERR", ev.title, e.message); } } } function run(label, opts, turns, playerBehavior) { newGame(opts); let battles = 0, sieges = 0, errors = []; for (let t = 0; t < turns; t++) { try { playerBehavior?.(t); endTurn(); battles += G.replays.filter(r => r.kind === "field").length; sieges += G.replays.filter(r => r.kind === "siege" || r.kind === "surrender").length; drainEvents(t % 4); if (G.gameOver) break; } catch (e) { errors.push(`turn ${t}: ${e.stack.split("\n").slice(0, 4).join(" | ")}`); break; } } console.log(`\n== ${label} == turns:${G.turnCount} over:${G.gameOver}(${G.victory ?? "-"}) battles:${battles} sieges:${sieges} errors:${errors.length}`); const top = Object.values(G.factions).filter(f => f.alive && f.id !== "neutral").sort((a, b) => b.cities.length - a.cities.length).slice(0, 5); console.log("top:", top.map(f => `${f.name}:${f.cities.length}`).join(", ")); if (errors.length) console.log(errors.join("\n")); return errors; } const custom = { mode: "campaign", difficulty: "normal", playerType: "custom", custom: { rulerName: "Minh", color: "#c2452d", emblem: "⚔", origin: "governor", cityId: "xuchang", factionName: "" } }; let errs = []; errs += run("passive 150t", custom, 150); // aggressive player: raise army & attack neighbors errs += run("aggressive 100t", { ...custom }, 100, (t) => { const pf = F(G.playerFaction); const capCity = G.cities[pf.capital]; // keep garrison fed if (capCity && capCity.levies > 300) recruitToGarrison(capCity, "spear", Math.min(capCity.levies, 400)); // raise army with best idle general const idle = Object.values(G.generals).filter(g => g.alive && g.faction === pf.id && !g.isLeader && typeof g.location === "string" && G.cities[g.location]?.owner === pf.id && !Object.values(G.armies).some(a => a.genId === g.id)); if (idle.length && capCity && totalTroopsLocal(capCity.garrison) > 1600) { const troops = {}; let rem = Math.floor(totalTroopsLocal(capCity.garrison) * 0.7); for (const tt of ["cav", "bow", "sword", "spear"]) { const take = Math.min(capCity.garrison[tt] || 0, rem); if (take > 0) { troops[tt] = take; rem -= take; } } raiseArmyFromCity(capCity, idle[0].id, troops); } // move armies toward adjacent hostile/neutral provinces for (const army of Object.values(G.armies)) { if (army.faction !== pf.id || army.moved) continue; const nbrs = provNbrsLocal(army.prov); for (const n of nbrs) { const res = moveArmy(army, n, { attackNeutral: true }); if (res.ok) break; } } }); // faction start errs += run("cao-campaign 120t", { mode: "campaign", difficulty: "hard", playerType: "faction", factionId: "cao" }, 120); // challenge errs += run("challenge 12t", { ...custom, mode: "challenge" }, 12); // save / load roundtrip mid-game newGame(custom); for (let t = 0; t < 10; t++) { endTurn(); drainEvents(); } saveGame("mid"); const ok = loadGame("mid"); console.log("\nsave/load roundtrip:", ok ? `ok (year ${ok.year})` : "FAILED"); if (!ok) errs.push("save/load failed"); function totalTroopsLocal(troops) { return Object.values(troops || {}).reduce((s, v) => s + v, 0); } import { PROV_ADJ } from "../js/world.js"; function provNbrsLocal(p) { return [...PROV_ADJ[p]]; } console.log(errs.length ? "\nSTRESS FAILED" : "\nSTRESS PASSED"); process.exit(errs.length ? 1 : 0);