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:
@@ -0,0 +1,74 @@
|
||||
// Headless smoke test: create a game, simulate many turns.
|
||||
const shim = new Map();
|
||||
globalThis.localStorage = {
|
||||
getItem: k => shim.get(k) ?? null,
|
||||
setItem: (k, v) => shim.set(k, String(v)),
|
||||
removeItem: k => shim.delete(k),
|
||||
get length() { return shim.size; },
|
||||
key: i => [...shim.keys()][i] ?? null,
|
||||
};
|
||||
|
||||
import { newGame, G, F, saveGame, loadGame, listSaves } from "../js/state.js";
|
||||
import { endTurn, recruitToGarrison } from "../js/sim.js";
|
||||
import { factionTroopCount } from "../js/state.js";
|
||||
import { applyEffect } from "../js/events.js";
|
||||
|
||||
const opts = {
|
||||
mode: "campaign", difficulty: "normal", playerType: "custom",
|
||||
custom: { rulerName: "Minh", color: "#c2452d", emblem: "⚔", origin: "governor", cityId: "xuchang", factionName: "" },
|
||||
};
|
||||
let g = newGame(opts);
|
||||
console.log("factions:", Object.keys(g.factions).length);
|
||||
console.log("generals:", Object.keys(g.generals).length);
|
||||
console.log("cities:", Object.keys(g.cities).length);
|
||||
const pf = F("player");
|
||||
console.log("player cities:", pf.cities, "gold:", pf.gold, "gens:", Object.values(g.generals).filter(x => x.faction === "player").map(x => x.name));
|
||||
|
||||
// sanity: leaders assigned
|
||||
for (const f of Object.values(g.factions)) {
|
||||
if (f.id === "neutral") continue;
|
||||
if (!f.leader) console.log(`!! faction ${f.id} has no leader`);
|
||||
}
|
||||
|
||||
// resolve any pending events each turn by choosing first option
|
||||
function drainEvents() {
|
||||
let guard = 0;
|
||||
while (g.pendingEvents.length && guard++ < 50) {
|
||||
const ev = g.pendingEvents.shift();
|
||||
const choice = ev.choices[0];
|
||||
try { applyEffect(choice.effect); } catch (e) { console.log("EFFECT ERR", ev.title, e.message); }
|
||||
}
|
||||
}
|
||||
|
||||
let battles = 0, errors = [];
|
||||
for (let t = 0; t < 60; t++) {
|
||||
try {
|
||||
// simple player activity: recruit sometimes
|
||||
if (t % 3 === 0) { const c = g.cities[pf.cities[0]]; if (c) recruitToGarrison(c, "spear", Math.min(c.levies, 200)); }
|
||||
endTurn();
|
||||
battles += g.replays.filter(r => r.rounds?.length || r.kind === "surrender").length;
|
||||
drainEvents();
|
||||
} catch (e) {
|
||||
errors.push(`turn ${t}: ${e.stack.split("\n").slice(0, 3).join(" | ")}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log("\nafter 60 turns => year", g.year, "/", g.month);
|
||||
for (const f of Object.values(g.factions)) {
|
||||
if (!f.alive) { console.log(` ✝ ${f.name} destroyed`); continue; }
|
||||
console.log(` ${f.name.padEnd(16)} cities:${String(f.cities.length).padStart(2)} gold:${String(f.gold).padStart(6)} food:${String(f.food).padStart(6)} troops:${factionTroopCount(f.id)} legit:${f.legitimacy} rank:${f.rank}`);
|
||||
}
|
||||
console.log("battles fought:", battles);
|
||||
console.log("journal entries:", g.journal.length);
|
||||
console.log("chronicle:", g.chronicle.slice(0, 6).map(c => `${c.y}/${c.m} ${c.text}`));
|
||||
if (errors.length) { console.log("ERRORS:\n" + errors.join("\n")); process.exit(1); }
|
||||
|
||||
// save/load roundtrip
|
||||
saveGame("test");
|
||||
const saves = listSaves();
|
||||
console.log("saves:", saves.map(s => s.slot));
|
||||
const loaded = loadGame("test");
|
||||
console.log("loaded ok:", loaded.year === g.year && loaded.playerFaction === "player");
|
||||
|
||||
// battle sanity
|
||||
console.log("\nSMOKE TEST PASSED");
|
||||
Reference in New Issue
Block a user