- Stylized 3D ink-painting map of China (12 provinces, 32 cities, 17 factions) - Custom warlord creation (8 origins, banner, starting city) or historical factions - City management: 7 buildings, 5 dev tiers, recruitment from levies - Character system: stats, traits, loyalty, relationships, wounds, capture, death, succession - Turn-based tactical battles with formations, stances, hero skills, cinematic 3D replay - Sieges: assault, starvation, bribery, infiltration - Diplomacy with trust memory, alliances, NAPs, trade, marriage, espionage, betrayal - Scripted diverging history (Dong Zhuo, Guandu, Red Cliffs...) + world crises + court events - AI factions with distinct personalities; prisoners (execute/release/recruit/ransom) - Procedural guqin/taiko WebAudio score; save/load; victory + dynasty chronicle screens - View-relative camera controls; headless test suites (smoke, stress, map validator)
101 lines
4.2 KiB
JavaScript
101 lines
4.2 KiB
JavaScript
// 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);
|