// Autonomous Game Testing API for Plants vs. Zombies window.PvZTestAPI = (function() { let engine = null; return { setEngine: function(e) { engine = e; }, getEngine: function() { return engine; }, getState: function() { if (!engine) return null; return { isRunning: engine.isRunning, isGameOver: engine.isGameOver, isVictory: engine.isVictory, currentMode: engine.currentMode, currentLevel: engine.currentLevel, sun: engine.economy.sun, coins: engine.economy.totalCoins, plantsCount: engine.plants.length, zombiesCount: engine.zombies.length, projectilesCount: engine.projectiles.length, plants: engine.plants.map(p => ({ type: p.type, col: p.col, row: p.row, health: p.health })), zombies: engine.zombies.map(z => ({ type: z.type, row: z.row, x: Math.round(z.x), health: z.health, isEating: z.isEating })) }; }, clearField: function() { if (!engine) return; engine.plants = []; engine.zombies = []; engine.projectiles = []; for (let r = 0; r < engine.grid.rows; r++) { for (let c = 0; c < engine.grid.cols; c++) { engine.grid.grid[r][c] = null; } } }, spawnZombie: function(row, type = 'normal', x = 850) { if (!engine) return null; const z = new window.Zombie(row, type, x); const b = engine.grid.getCellBounds(0, row); z.y = b.centerY - 5; engine.zombies.push(z); return z; }, plant: function(col, row, type) { if (!engine) return null; const b = engine.grid.getCellBounds(col, row); const p = new window.Plant(type, col, row, b.x, b.y, b.w, b.h); engine.grid.setPlant(col, row, p); engine.plants.push(p); return p; }, step: function(dt = 0.05, ticks = 1) { if (!engine) return; for (let i = 0; i < ticks; i++) { engine.update(dt); } }, runAllTests: function() { const results = []; let passed = 0; function assert(name, condition, errorMsg) { if (condition) { results.push({ name, status: 'PASS' }); passed++; } else { results.push({ name, status: 'FAIL', error: errorMsg }); } } if (!engine) { return { total: 0, passed: 0, failed: 1, error: 'Engine not initialized' }; } const origMode = engine.currentMode; const origLevel = engine.currentLevel; try { // Test 1: Peashooter Combat Test this.clearField(); engine.startLevel(1, 'adventure'); this.clearField(); const p = this.plant(0, 0, 'peashooter'); const z = this.spawnZombie(0, 'normal', 500); const startHealth = z.health; // Step simulation until pea hits this.step(0.1, 40); // 4 seconds of game time assert('Peashooter damages zombie', z.health < startHealth, `Zombie health did not decrease: ${z.health}`); // Test 2: Zombie Eating & Resume Movement Bug Test this.clearField(); const testNut = this.plant(2, 1, 'wallnut'); testNut.health = 40; // low health so it gets eaten fast const eatingZombie = this.spawnZombie(1, 'normal', testNut.x + 20); // Step until zombie touches and eats nut this.step(0.1, 20); assert('Zombie begins eating plant', eatingZombie.isEating === true || testNut.health <= 0, 'Zombie did not start eating'); // Step until nut is consumed this.step(0.1, 40); const nutGone = engine.plants.length === 0 || testNut.health <= 0; assert('Plant is consumed', nutGone, 'Plant was not consumed'); assert('Zombie resumes walking after plant death (Bug Fix)', eatingZombie.isEating === false, `isEating remained true!`); // Test 3: Potato Mine Arming and Spudow Explosion this.clearField(); const mine = this.plant(1, 2, 'potatomine'); assert('Potato mine starts unarmed', mine.isArmed === false, 'Mine started armed'); mine.armTimer = mine.armTime; // fast-forward arming this.step(0.1, 2); assert('Potato mine is armed', mine.isArmed === true, 'Mine did not arm'); const boomZombie = this.spawnZombie(2, 'normal', mine.x + 20); this.step(0.1, 10); assert('Potato mine explodes on contact', mine.toRemove === true || boomZombie.health <= 0, 'Mine did not explode'); // Test 4: Lawnmower Defense this.clearField(); engine.grid.initLawnmowers(); const lawnZombie = this.spawnZombie(0, 'normal', 110); // triggers mower at x<=115 this.step(0.1, 10); assert('Lawnmower activates on breach', engine.grid.lawnmowers[0].active === true || engine.grid.lawnmowers[0].used === true, 'Mower did not activate'); assert('Lawnmower kills intruder', lawnZombie.health <= 0 || lawnZombie.toRemove === true, 'Mower did not kill zombie'); // Test 5: I, Zombie Mode (No Instant Victory Bug Test) this.clearField(); engine.startLevel(1, 'izombie'); assert('I, Zombie does NOT instantly trigger victory', engine.isVictory === false, 'Instant victory bug detected!'); assert('I, Zombie spawns 5 trophy sunflowers', engine.plants.filter(pl => pl.type === 'sunflower').length === 5, 'Incorrect sunflowers count'); assert('I, Zombie uses zombie deck', engine.economy.cards.some(c => c.id.startsWith('z_')), 'Zombie deck not equipped'); // Step simulation 10 ticks: game should remain running without instant win this.step(0.1, 10); assert('I, Zombie stays active during play', engine.isVictory === false && engine.isRunning === true, 'Unexpected victory or stop'); // Win simulation: destroy all sunflowers for (const pl of engine.plants) { if (pl.type === 'sunflower') pl.toRemove = true; } this.step(0.1, 2); assert('I, Zombie triggers victory when all sunflowers are destroyed', engine.isVictory === true, 'Victory did not trigger upon sunflower defeat'); // Test 6: Auth & Profiles window.PvZAuth.loginAsGuest(); assert('Guest mode detected', window.PvZAuth.isGuest() === true, 'Failed to login as guest'); const userRes = window.PvZAuth.login('autotester', 'testpass123'); assert('User registration & login succeeds', userRes.success === true && window.PvZAuth.getCurrentUser() === 'autotester', 'Login failed'); window.PvZAuth.saveUserData({ coins: 999, level: 3, highScore: 500, upgrades: {} }); const loaded = window.PvZAuth.getUserData(); assert('User profile data persists', loaded.coins === 999 && loaded.level === 3, 'Data did not persist'); window.PvZAuth.loginAsGuest(); // restore guest // Test 7: Main Menu Navigation engine.openMainMenu(); assert('Main menu pauses game', engine.isRunning === false, 'Game was not paused on menu open'); const startMod = document.getElementById('start-modal'); assert('Main menu opens start modal', startMod && !startMod.classList.contains('hidden'), 'Start modal was not shown'); // Test 8: Workshop, Materials & Plant Upgrades if (engine.workshop) { const ws = engine.workshop; ws.materials.bioHumus = 50; ws.materials.scrapMetal = 50; ws.materials.crystals = 5; engine.economy.totalCoins = 2000; const pLevelBefore = ws.getPlantLevel('peashooter'); const upgradedT2 = ws.upgradePlant('peashooter'); assert('Workshop upgrades plant to Tier 2', upgradedT2 === true && ws.getPlantLevel('peashooter') === 2, 'Failed to upgrade to Tier 2'); const upgradedT3 = ws.upgradePlant('peashooter'); assert('Workshop upgrades plant to Tier 3', upgradedT3 === true && ws.getPlantLevel('peashooter') === 3, 'Failed to upgrade to Tier 3'); // Check perk upgrade const perkUpgraded = ws.upgradePerk('hardyRoots'); assert('Workshop upgrades passive perk', perkUpgraded === true && ws.getPerkLevel('hardyRoots') === 1, 'Failed to upgrade perk'); // Check plant creation with upgraded tier and hardy roots const cellB = engine.grid.getCellBounds(1, 0); const t3Plant = engine.createPlant('peashooter', 1, 0, cellB); assert('Planted plant inherits Tier 3 level', t3Plant.level === 3, `Expected level 3, got ${t3Plant.level}`); assert('Hardy Roots perk boosts plant max HP', t3Plant.maxHealth > 300, `Expected boosted HP, got ${t3Plant.maxHealth}`); // Restore peashooter level and perk for clean state ws.plantLevels['peashooter'] = 1; ws.perks['hardyRoots'] = 0; } else { assert('WorkshopSystem exists on engine', false, 'engine.workshop is undefined'); } // Test 9: Zen Garden and Suburban Almanac if (engine.zengarden) { const zg = engine.zengarden; zg.chocolateCount = 2; zg.fertilizerCount = 3; const feedRes = zg.feedSnail(); assert('Feeding Stinky snail with chocolate wakes and boosts snail', feedRes === true && zg.snail.isAwake === true && zg.snail.boostTimer > 0, 'Snail feed failed'); // Plant sprout in empty pot const emptyIdx = zg.pots.findIndex(p => !p.plantType); if (emptyIdx !== -1) { const planted = zg.plantSprout('chomper'); assert('Zen Garden plants new sprout in empty pot', planted === true && zg.pots[emptyIdx].plantType === 'chomper', 'Sprout planting failed'); } // Water thirsty plant const thirstyPot = zg.pots.find(p => p.plantType && p.thirstTimer > 0); if (thirstyPot) { const watered = zg.waterPlant(thirstyPot.id); assert('Watering thirsty plant succeeds', watered === true && thirstyPot.thirstTimer === 0, 'Watering failed'); } else { zg.pots[0].thirstTimer = 10; const watered = zg.waterPlant(zg.pots[0].id); assert('Watering plant clears thirst timer', watered === true && zg.pots[0].thirstTimer === 0, 'Watering failed'); } } else { assert('ZenGardenSystem exists on engine', false, 'engine.zengarden is undefined'); } if (engine.almanac) { const alm = engine.almanac; assert('Almanac has 13 plants defined', alm.plants.length >= 13, `Expected >= 13 plants, got ${alm.plants.length}`); assert('Almanac has 8 zombies defined', alm.zombies.length >= 8, `Expected >= 8 zombies, got ${alm.zombies.length}`); const yeti = alm.getItem('z_yeti'); assert('Almanac contains secret Zombie Yeti', yeti && yeti.id === 'z_yeti', 'Yeti not found in almanac'); } else { assert('AlmanacSystem exists on engine', false, 'engine.almanac is undefined'); } // Test 10: Achievements System if (engine.achievements) { const achs = engine.achievements; assert('Achievements list contains 14+ achievements', achs.achievements.length >= 14, `Expected >= 14, got ${achs.achievements.length}`); achs.trigger('first_plant'); const ach = achs.achievements.find(a => a.id === 'first_plant'); assert('first_plant achievement triggers and completes', ach && ach.completed === true, 'Achievement not completed'); const coinsBefore = engine.economy.totalCoins; if (ach && !ach.claimed) { const claimed = achs.claimReward('first_plant'); assert('Claiming achievement grants coins reward', claimed === true && engine.economy.totalCoins > coinsBefore, 'Reward claiming failed'); } else { assert('Achievement reward already claimed or validated', true); } } else { assert('AchievementSystem exists on engine', false, 'engine.achievements is undefined'); } // Test 11: Garden Roguelike System if (engine.roguelike) { const rgl = engine.roguelike; rgl.reset(); assert('Roguelike starts at floor 1 with 0 relics', rgl.currentFloor === 1 && rgl.activeRelics.length === 0, 'Reset failed'); const choices = rgl.generateDraftChoices(); assert('generateDraftChoices generates exactly 3 choices', choices && choices.length === 3, `Expected 3 choices, got ${choices ? choices.length : 0}`); const selected = rgl.selectChoice(0); assert('selectChoice successfully picks choice and increments floor', selected === true && rgl.currentFloor === 2, 'Select choice failed'); } else { assert('RoguelikeSystem exists on engine', false, 'engine.roguelike is undefined'); } // Test 12: Tree of Wisdom & Secret Cheats if (engine.tree) { const tree = engine.tree; assert('TreeSystem has wisdom milestones defined', tree.wisdomList && tree.wisdomList.length >= 8, 'Wisdom list too small'); const initialHeight = tree.height; engine.economy.totalCoins += 300; const fed = tree.feedFertilizer(); assert('feedFertilizer increases height by 1', fed === true && tree.height === initialHeight + 1, 'Tree feeding failed'); const cheatState = tree.toggleCheat('mustache'); assert('toggleCheat toggles cheat state', typeof cheatState === 'boolean', 'Cheat toggle failed'); } else { assert('TreeSystem exists on engine', false, 'engine.tree is undefined'); } // Test 13: Dr. Zomboss Boss Fight if (engine.boss) { const boss = engine.boss; boss.reset(); assert('Boss starts with 6000 HP and isActive true', boss.health === 6000 && boss.isActive === true, 'Boss init failed'); boss.takeDamage(500); assert('Boss takeDamage accurately decreases HP', boss.health === 5500, `Expected 5500, got ${boss.health}`); engine.economy.setBossDeck(); const hasPots = engine.economy.cards.some(c => c.id === 'flowerpot'); const hasMelon = engine.economy.cards.some(c => c.id === 'melonpult'); assert('Boss conveyor deck contains flowerpots and melon-pults', hasPots && hasMelon, 'Boss deck missing items'); } else { assert('BossSystem exists on engine', false, 'engine.boss is undefined'); } // Test 14: Leaderboard System if (engine.leaderboards) { const lb = engine.leaderboards; assert('Leaderboards has 6 modes', Object.keys(lb.records).length >= 6, 'Missing leaderboard modes'); lb.submitScore('survival', 99, '99 Флагов (Тест)'); const entries = lb.getEntries('survival'); const top = entries[0]; assert('Leaderboard records and sorts top score', top && top.score === 99, 'Score submission failed'); } else { assert('LeaderboardSystem exists on engine', false, 'engine.leaderboards is undefined'); } // Test 15: Roof & Fog Biomes and Lobbed Projectiles engine.grid.initBiome('roof'); assert('Roof biome initialized', engine.grid.biome === 'roof' && engine.grid.isRoof() === true, 'Roof biome init failed'); engine.grid.initBiome('fog'); assert('Fog biome initialized', engine.grid.biome === 'fog' && engine.grid.isFog() === true, 'Fog biome init failed'); const lobbedProj = new window.Projectile(100, 200, 1, 'melon', 80, 500, 200); assert('Lobbed projectile initializes with arc parameters', lobbedProj.isLobbed === true && lobbedProj.targetX === 500, 'Lobbed proj init failed'); lobbedProj.update(0.1); assert('Lobbed projectile progresses along trajectory', lobbedProj.lobProgress > 0, 'Trajectory update failed'); // Test 16: v2.1 Soundtrack, Themes & Boss Rush Mode if (window.PvZAudio) { window.PvZAudio.setTheme('boss'); assert('PvZAudio supports multi-biome themes', window.PvZAudio.getTheme() === 'boss', 'Theme switch failed'); window.PvZAudio.setTheme('day'); } engine.applyTheme('cyberpunk'); assert('Visual themes switch to cyberpunk', engine.currentTheme === 'cyberpunk' && document.body.classList.contains('theme-cyberpunk'), 'Cyberpunk theme failed'); engine.applyTheme('winter'); assert('Visual themes switch to winter', engine.currentTheme === 'winter' && document.body.classList.contains('theme-winter'), 'Winter theme failed'); engine.applyTheme('classic'); assert('Visual themes reset to classic', engine.currentTheme === 'classic', 'Classic theme reset failed'); engine.wave.setLevel(1, 'bossrush'); assert('Boss Rush mode initializes 3 waves', engine.wave.totalWaves === 3 && engine.wave.biome === 'roof', 'Boss Rush init failed'); if (engine.leaderboards) { assert('Leaderboard includes bossrush mode records', Array.isArray(engine.leaderboards.getEntries('bossrush')), 'Missing bossrush leaderboard'); } // Test 17: Campaign System & 25 Levels Multi-Biome Progression if (window.CampaignSystem) { const worlds = window.CampaignSystem.WORLDS; const levels = window.CampaignSystem.LEVELS; assert('CampaignSystem defines 5 worlds', Array.isArray(worlds) && worlds.length === 5, `Expected 5 worlds, got ${worlds.length}`); assert('CampaignSystem defines 25 levels', Array.isArray(levels) && levels.length === 25, `Expected 25 levels, got ${levels.length}`); const lvl1 = window.CampaignSystem.getLevel(1); const lvl11 = window.CampaignSystem.getLevel(11); const lvl16 = window.CampaignSystem.getLevel(16); const lvl25 = window.CampaignSystem.getLevel(25); assert('Level 1-1 is Day Lawn biome', lvl1.code === '1-1' && lvl1.biome === 'day', 'Level 1 biome mismatch'); assert('Level 3-1 is Pool biome', lvl11.code === '3-1' && lvl11.biome === 'pool', 'Level 11 biome mismatch'); assert('Level 4-1 is Fog biome', lvl16.code === '4-1' && lvl16.biome === 'fog', 'Level 16 biome mismatch'); assert('Level 5-5 is Dr. Zomboss Boss fight', lvl25.code === '5-5' && lvl25.isBoss === true && lvl25.biome === 'roof', 'Level 25 boss mismatch'); engine.wave.setLevel(11, 'adventure'); assert('WaveSystem sets pool biome on level 11', engine.wave.biome === 'pool', 'Wave biome mismatch for lvl 11'); engine.wave.setLevel(16, 'adventure'); assert('WaveSystem sets fog biome on level 16', engine.wave.biome === 'fog', 'Wave biome mismatch for lvl 16'); engine.wave.setLevel(25, 'adventure'); assert('WaveSystem configures boss mode on level 25', engine.wave.mode === 'boss' && engine.wave.biome === 'roof', 'Wave boss mismatch for lvl 25'); const fmt = window.CampaignSystem.formatLevel(11); assert('formatLevel formats code and world icon', fmt.fullLabel.includes('3-1') && fmt.worldIcon === '🏊', 'Format level mismatch'); } else { assert('CampaignSystem is globally defined', false, 'CampaignSystem not found'); } } catch (err) { results.push({ name: 'Exception during test run', status: 'FAIL', error: err.toString() }); } finally { // Restore original state engine.startLevel(origLevel, origMode); } return { total: results.length, passed, failed: results.length - passed, results }; } }; })();