From e734a6a4a7b64ff752fa874e6e644d8e72dce097 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 23 Aug 2026 00:25:01 +0000 Subject: [PATCH] Add Golden Starbloom headless test (Session 11) --- golden-bloom-test.mjs | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 golden-bloom-test.mjs diff --git a/golden-bloom-test.mjs b/golden-bloom-test.mjs new file mode 100644 index 0000000..4d89325 --- /dev/null +++ b/golden-bloom-test.mjs @@ -0,0 +1,131 @@ +/** + * Headless test for Session 11 - Golden Starbloom random event. + * Verifies golden bloom scheduling, claiming, and the production boost. + */ + +// Minimal DOM stubs (only what our code touches) +global.document = { + getElementById: () => ({ getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 200 }) }), + querySelector: () => null, + addEventListener: () => {}, + querySelectorAll: () => [], +}; + +// UI stub capturing notifications + spawn/remove calls +const notifications = []; +global.UI = { + showNotification: (msg) => notifications.push(msg), + update: () => {}, + spawnGoldenBloom: () => { global.__spawned = (global.__spawned || 0) + 1; }, + removeGoldenBloom: () => { global.__removed = (global.__removed || 0) + 1; }, + updateGoldenBloomBadge: () => {}, +}; + +// Audio / Particles stubs +global.Audio = { playGoldenBloom: () => {} }; +global.Particles = { createGoldenEffect: (x, y) => { global.__goldX = x; global.__goldY = y; } }; + +// Load the real game engine source and eval it in this scope. +import { readFileSync } from 'fs'; +const src = readFileSync('/opt/data/idle-game/js/game.js', 'utf8') + .replace(/window\.Game = Game;/, '') + + '\nglobalThis.Game = Game;\n'; + +eval(src); + +// Build stubs for the data modules needed by the engine. +global.BUILDINGS = { + starSprout: { id: 'starSprout', baseCost: 15, baseProduction: 0.1 }, + moonBloom: { id: 'moonBloom', baseCost: 100, baseProduction: 0.5 }, +}; +global.UPGRADES = {}; + +// Minimal prestige stub returning x1 multiplier (no bonus) so we can isolate +// golden-bloom math from the real prestige system. +global.PRESTIGE = { getMultiplier: () => 1 }; +global.PRESTIGE_UPGRADES = {}; + +// Achievements stub - check() must be a no-op for these tests. +global.Achievements = { check: () => {} }; + +let passed = 0, failed = 0; +function assert(cond, msg) { + if (cond) { passed++; console.log(` OK ${msg}`); } + else { failed++; console.error(` FAIL ${msg}`); } +} + +console.log('Test 1: no bloom active initially -> remaining is 0'); +Game.state.goldenBlooms.active = false; +assert(Game.getGoldenBloomRemaining() === 0, `remaining is 0 (got ${Game.getGoldenBloomRemaining()})`); + +console.log('Test 2: claiming with no active bloom returns 0 and does not award stardust'); +const before = Game.state.stardust; +Game.state.goldenBlooms.active = false; +const none = Game.claimGoldenBloom(10, 10); +assert(none === 0, `claim returned 0 when inactive (got ${none})`); +assert(Game.state.stardust === before, 'stardust unchanged when no bloom'); + +console.log('Test 3: golden bloom bonus = floor(production * mult), production boost applied'); +// Give the player some buildings so production is deterministic. +Game.state.buildings = { starSprout: 10 }; // 10 * 0.1 = 1.0 /s (prestige multiplier = 1) +Game.state.productionBoostUntil = 0; +const remaining0 = Game.getGoldenBloomRemaining(); +Game.state.goldenBlooms.active = true; +Game.state.goldenBlooms.timer = 5; +assert(Game.getGoldenBloomRemaining() === 5, `remaining reflects timer (got ${Game.getGoldenBloomRemaining()})`); + +const prodBefore = Game.getProductionPerSecond(); +assert(Math.abs(prodBefore - 1.0) < 1e-9, `production is 1.0 before boost (got ${prodBefore})`); + +// Activate the boost and check the x1.5 multiplier. +Game.state.productionBoostUntil = Date.now() + 30000; +const prodBoosted = Game.getProductionPerSecond(); +assert(Math.abs(prodBoosted - 1.5) < 1e-9, `production is 1.5 during boost (got ${prodBoosted})`); + +const boostInfo = Game.getProductionBoostInfo(); +assert(boostInfo.active === true, 'boost info reports active'); +assert(boostInfo.remaining > 0 && boostInfo.remaining <= 30, `boost remaining in range (${boostInfo.remaining})`); + +// Now claim the bloom. Bonus should be floor(1.5 * 15) = 22. +Game.state.productionBoostUntil = Date.now() + 30000; // keep boost on so bonus uses boosted prod +const earned = Game.claimGoldenBloom(42, 84); +assert(earned === Math.floor(1.5 * 15), `bonus is floor(prod*mult)=${Math.floor(1.5*15)} (got ${earned})`); +assert(Game.state.stardust === before + earned, 'stardust increased by exactly the bonus'); +assert(Game.state.goldenBlooms.totalClaimed === 1, 'totalClaimed incremented to 1'); +assert(Game.state.goldenBlooms.active === false, 'bloom no longer active after claim'); +assert(global.__goldX === 42 && global.__goldY === 84, `particle coords passed through (${global.__goldX}, ${global.__goldY})`); +assert(notifications.some(n => /Golden Starbloom/.test(n)), 'golden bloom notification shown'); + +console.log('Test 4: boost expires correctly via getProductionBoostInfo'); +Game.state.productionBoostUntil = Date.now() - 1000; // already expired +const expired = Game.getProductionBoostInfo(); +assert(expired.active === false && expired.remaining === 0, 'boost inactive after expiry'); +const prodExpired = Game.getProductionPerSecond(); +assert(Math.abs(prodExpired - 1.0) < 1e-9, `production back to 1.0 after boost expires (got ${prodExpired})`); + +console.log('Test 5: updateGoldenBloom countdown decrements active bloom and vanishes'); +Game.state.goldenBlooms.active = true; +Game.state.goldenBlooms.timer = 2.0; +Game.updateGoldenBloom(1.0); // advance 1s +assert(Game.state.goldenBlooms.active === true && Game.state.goldenBlooms.timer === 1.0, `countdown decremented (timer=${Game.state.goldenBlooms.timer})`); +Game.updateGoldenBloom(1.5); // more than remaining -> vanishes +assert(Game.state.goldenBlooms.active === false && Game.state.goldenBlooms.timer === 0, 'bloom vanishes when timer hits 0'); + +console.log('Test 6: bloom spawns after elapsed reaches maxMs (forced)'); +Game.state.goldenBlooms.active = false; +Game.state.goldenBlooms.lastSpawn = Date.now() - (Game.config.goldenBloomMaxMs + 1); // past maximum +global.__spawned = 0; +Game.updateGoldenBloom(0.1); +assert(Game.state.goldenBlooms.active === true, 'bloom spawns once maxMs has elapsed'); +assert(Game.state.goldenBlooms.timer === Game.config.goldenBloomDuration, `timer set to duration (${Game.state.goldenBlooms.timer})`); +assert(global.__spawned >= 1, `UI.spawnGoldenBloom called (${global.__spawned}x)`); + +console.log('Test 7: bloom does NOT spawn before minMs elapsed'); +Game.state.goldenBlooms.active = false; +Game.state.goldenBlooms.lastSpawn = Date.now(); // just now +const spawnedBefore = global.__spawned || 0; +Game.updateGoldenBloom(5); // advance a few seconds (well under minMs) +assert(Game.state.goldenBlooms.active === false, 'no premature spawn before minMs'); + +console.log(`\nResult: ${passed} passed, ${failed} failed`); +process.exit(failed === 0 ? 0 : 1);