Files
repterra-web/test/e2e.js
T
deepseek 8fbe70d0b0 Repterra Web — full game: base building, power grid, taming & breeding, aquatic raiders, day/night, save/load
- Isometric canvas RTS vs dinosaur waves (fan demake of Repterra)
- Economy: houses/taxes, farms, foresters, quarries; colonist staffing
- Power grid: generators extend build range; brownout + recovery
- Defense: walls/gates, watchtowers (AA), cannon towers (ground-only)
- Taming: Primal Pen + Tamers collar weakened dinos; pets obey commands
- Breeding: tamed pairs incubate eggs at the pen; hatchlings grow up
- 7 dino species incl. flying Pteranodons and lake-raiding Suchomimus
- Telegraphed waves with direction arrows; day-15 final horde; 3 difficulties
- Day/night cycle, fog of war, minimap, synth audio, 1x-3x speeds
- Save/Load/Continue + dawn autosave (full JSON state snapshots)
- Tests: 80-assertion headless suite, browser boot + E2E, balance harness
2026-08-23 07:00:23 +00:00

138 lines
6.1 KiB
JavaScript

/* Deeper E2E: pixels, HUD updates, placement ghost, wave banner. */
'use strict';
const { chromium } = require('/tmp/node_modules/playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell',
args: ['--no-sandbox', '--disable-gpu'],
});
const page = await browser.newPage({ viewport: { width: 1440, height: 860 } });
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
let fails = 0;
const ok = (c, m) => { if (c) console.log(' ✔ ' + m); else { fails++; console.error(' ✘ ' + m); } };
await page.goto('http://127.0.0.1:8933/', { waitUntil: 'load' });
await page.waitForTimeout(800);
await page.click('[data-diff="normal"]');
await page.waitForTimeout(2000);
// keep the test window peaceful — wave destruction timing would make the
// economy assertions flaky (this suite tests UI/HUD, not combat)
await page.evaluate(() => { RTS.sim.state().waves.forEach(w => { w.day += 40; }); });
// ---- canvas pixel richness ----
const pix = await page.evaluate(() => {
const cv = document.getElementById('game');
const ctx = cv.getContext('2d');
const img = ctx.getImageData(0, 0, cv.width, cv.height).data;
const colors = new Set();
let nonBlack = 0, total = 0;
for (let i = 0; i < img.length; i += 40) { // sample
const r = img[i], g = img[i + 1], b = img[i + 2];
colors.add((r >> 4) + ',' + (g >> 4) + ',' + (b >> 4));
if (r + g + b > 30) nonBlack++;
total++;
}
return { uniqueColors: colors.size, nonBlackRatio: nonBlack / total };
});
ok(pix.uniqueColors > 60, 'canvas is richly colored (' + pix.uniqueColors + ' quantized colors)');
ok(pix.nonBlackRatio > 0.5, 'canvas mostly rendered (' + Math.round(pix.nonBlackRatio * 100) + '% non-dark)');
// ---- HUD present ----
const hud1 = await page.evaluate(() => ({
gold: document.querySelector('#res-gold .v').textContent,
day: document.getElementById('daylabel').textContent,
time: RTS.sim.state().time,
}));
ok(hud1.gold === '550', 'HUD shows starting gold (' + hud1.gold + ')');
ok(/Day 1/.test(hud1.day), 'day label renders (' + hud1.day + ')'); // sun/moon icon prefix
// ---- placement via UI ----
await page.click('#palette .palcol:first-child .palbtn:first-child'); // house
await page.waitForTimeout(150);
const ghost = await page.evaluate(() => !!RTS.ui.placing && RTS.ui.placing.defId);
ok(ghost === 'house', 'palette activates ghost placement (' + ghost + ')');
// find a valid tile near HQ on the right side of screen
const spot = await page.evaluate(() => {
const hq = RTS.sim.hq();
for (let r = 2; r < 8; r++) {
for (let a = 0; a < 12; a++) {
const x = Math.round(hq.x + r * Math.cos(a / 12 * 6.28));
const y = Math.round(hq.y + r * Math.sin(a / 12 * 6.28));
if (RTS.sim.canPlace('house', x, y).ok) {
const sp = RTS.render.worldToScreen(x, y);
return { sx: sp.x, sy: sp.y, x, y };
}
}
}
return null;
});
ok(spot, 'found valid house spot');
if (spot) {
await page.mouse.click(spot.sx, spot.sy);
await page.waitForTimeout(300);
const placed = await page.evaluate(({ x, y }) => {
const s = RTS.sim.state();
return s.buildings.some(b => b.defId === 'house' && b.x === x && b.y === y);
}, { x: spot.x, y: spot.y });
ok(placed, 'house placed by clicking at (' + spot.x + ',' + spot.y + ')');
}
await page.keyboard.press('Escape');
// ---- income flows into HUD once a house exists ----
await page.click('#topbtns .tbtn:nth-child(4)'); // 3x
await page.waitForTimeout(9000);
const hud2 = await page.evaluate(() => ({
gold: document.querySelector('#res-gold .v').textContent,
rate: document.querySelector('#res-gold .r').textContent,
pop: document.querySelector('#res-pop .v').textContent,
}));
ok(hud2.rate !== '+0.00/s' && hud2.rate.startsWith('+'), 'tax income shown in HUD (rate ' + hud2.rate + ', gold ' + hud2.gold + ')');
ok(hud2.pop !== '4/0', 'population grows under housing (' + hud2.pop + ')');
// ---- minimap: terrain lit + fog dark both present ----
const mm = await page.evaluate(() => {
const cv = document.getElementById('minimap');
const d = cv.getContext('2d').getImageData(0, 0, cv.width, cv.height).data;
let lit = 0, dark = 0;
for (let i = 0; i < d.length; i += 40) {
const g = d[i + 1];
if (g > 60) lit++;
else if (g < 20) dark++;
}
return { lit, dark };
});
ok(mm.lit > 60 && mm.dark > 500, 'minimap renders terrain + fog (' + mm.lit + ' lit / ' + mm.dark + ' fog samples)');
// ---- run to first wave: restore real schedule, day 2 = 40s game time; 3x speed ----
await page.evaluate(() => {
const s = RTS.sim.state();
s.waves.forEach(w => { w.day -= 40; w.warned = false; w.spawned = false; });
s.waveIdx = 0;
s.warnT = 0;
});
await page.click('#topbtns .tbtn:nth-child(4)'); // 3x
let bannerSeen = false, waveSpawned = false;
for (let i = 0; i < 40 && !waveSpawned; i++) {
await page.waitForTimeout(1000);
bannerSeen = bannerSeen || await page.evaluate(() => document.getElementById('wavebanner').style.display === 'flex');
waveSpawned = await page.evaluate(() => RTS.sim.state().waveIdx > 0);
}
ok(bannerSeen, 'wave warning banner appeared');
ok(waveSpawned, 'first wave spawned');
await page.screenshot({ path: '/root/dinorts/shots/06-wave.png' });
const waveState = await page.evaluate(() => {
const s = RTS.sim.state();
return { dinos: s.dinos.length, kills: s.stats.kills, day: s.day, wave: s.waveIdx };
});
console.log(' wave state:', JSON.stringify(waveState));
console.log('console errors:', errors.length ? errors : 'none');
await browser.close();
if (errors.length || fails) { console.error('E2E FAILED: ' + fails + ' assertion(s), ' + errors.length + ' console errors'); process.exit(1); }
console.log('E2E PASSED ✔');
})().catch(e => { console.error('TEST CRASH:', e); process.exit(1); });