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:
@@ -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);
|
||||
Reference in New Issue
Block a user