IMPERIUM ARCANUM — web-based Caesar 3-like city builder with heroes, magic & monsters

- Full classic layer: roads/walkers, 11-tier housing evolution, food chain,
  industry chains, trade routes, taxes/wages, ratings, fires & collapse
- Fantasy layer: heroes (XP/levels/auras), mana & 7 spells, monsters/invasions,
  walls/towers/legions/golems, campaign of 5 scenarios + endless sandbox
- Canvas2D isometric engine with procedural sprites, zero runtime deps
- Save/load slots + autosave, WebAudio synth music/SFX, HUD/menus/minimap
- Tests: headless sim smoke test + Playwright E2E (build/combat/save)
This commit is contained in:
deepseek
2026-08-23 07:00:05 +00:00
commit b642726428
38 changed files with 7288 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
// Deep E2E: start sandbox, build a working town via __IA hook, fast-forward months, assert state.
import { chromium } from 'playwright-core';
const exe = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell';
const errors = [];
const browser = await chromium.launch({ executablePath: exe, args: ['--no-sandbox', '--disable-gpu'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
const BASE = process.env.BASE || 'http://127.0.0.1:5173/';
await page.goto(BASE, { waitUntil: 'networkidle' });
await page.waitForTimeout(800);
await page.click('#mm-sandbox');
await page.waitForTimeout(1200);
// Build town programmatically
const result = await page.evaluate(() => {
const I = window.__IA;
const c = I.app.city;
// find flat grass area
let ox = -1, oy = -1;
outer:
for (let y = 8; y < c.H - 12; y++) for (let x = 4; x < c.W - 24; x++) {
let ok = true, fertileNear = false;
for (let dy = -2; dy < 9 && ok; dy++) for (let dx = -2; dx < 21; dx++) {
const t = c.tiles[(y + dy) * c.W + (x + dx)];
if (!t || t.terrain === 'water') { ok = false; break; }
if (t.terrain === 'fertile' && t.fert > 30) fertileNear = true;
}
if (ok && fertileNear) { ox = x; oy = y; break outer; }
}
if (ox < 0) return { fail: 'no spot' };
for (let i = 0; i < 20; i++) I.place('road', ox + i, oy);
for (let i = 1; i < 19; i++) I.place('house', ox + i, oy - 1);
I.place('well', ox + 2, oy + 1);
I.place('market', ox + 4, oy + 1);
I.place('granary', ox + 7, oy + 1);
I.place('prefecture', ox + 11, oy + 1);
I.place('forum', ox + 12, oy - 2) || I.place('forum', ox + 13, oy - 3);
// farm directly below the road row -> top edge touches road => connected
const fr = I.place('farmWheat', ox + 15, oy + 1);
const farmPlaced = !!fr.ok;
if (!farmPlaced) {
for (const [fdx, fdy] of [[13, 1], [17, 1], [15, -4], [12, 1], [18, 1]]) {
if (I.place('farmWheat', ox + fdx, oy + fdy).ok) { farmPlaced = true; break; }
}
}
if (!farmPlaced) return { fail: 'no road-connected farm spot' };
return { ox, oy, funds: Math.round(c.funds), pop: c.population };
});
console.log('build result:', JSON.stringify(result));
if (result.fail) { console.log('BUILD FAILED'); process.exit(1); }
// Fast-forward 30 months synchronously
const ff = await page.evaluate(() => {
const I = window.__IA;
for (let i = 0; i < 30 * 20; i++) I.step(0.5);
const c = I.app.city;
const houses = [...c.buildings.values()].filter(b => b.def === 'house');
return {
month: c.month, pop: c.population, funds: Math.round(c.funds),
employed: c.employed, laborers: c.laborers,
sentiment: c.sentiment, ratings: c.ratings,
avgLevel: (houses.reduce((a, b) => a + b.houseLevel, 0) / Math.max(1, houses.length)).toFixed(2),
walkers: c.walkers.size,
granaryWheat: [...c.buildings.values()].filter(b => b.def === 'granary').reduce((a, b) => a + (b.inv.wheat ?? 0), 0),
};
});
console.log('after 30mo:', JSON.stringify(ff));
// Now test spells & heroes UI presence
const ui = await page.evaluate(() => ({
spellCount: document.querySelectorAll('#spellbar .spell').length,
catTabs: document.querySelectorAll('#cattabs .cattab').length,
minimap: !!document.getElementById('minimap'),
topFunds: document.getElementById('t-funds')?.textContent,
}));
console.log('ui:', JSON.stringify(ui));
await page.screenshot({ path: '/tmp/shot_town.png' });
console.log('CONSOLE ERRORS:', errors.length);
errors.slice(0, 10).forEach(e => console.log(' -', e.slice(0, 200)));
const pass = ff.pop > 40 && ff.month >= 28 && ui.spellCount === 7 && errors.length === 0;
console.log(pass ? '✅ E2E PASS' : '❌ E2E FAIL');
await browser.close();
process.exit(pass ? 0 : 1);