Three Kingdoms: Warlord's Fate — complete playable game

- 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)
This commit is contained in:
deepseek
2026-08-23 06:59:40 +00:00
commit f040bb6be0
29 changed files with 60362 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
// 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);