#!/usr/bin/env python3 """ Autonomous Automated Test Runner for Plants vs. Zombies (ai.qpid.pro) Tests static integrity, HTTP/HTTPS endpoints, JS scripts, and PvZ mechanics. """ import os import sys import re import urllib.request import ssl import json BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) GAME_DIR = os.path.join(BASE_DIR, "game") TEST_RESULTS = [] def record(name, status, detail=""): TEST_RESULTS.append({"name": name, "status": status, "detail": detail}) symbol = "✅ PASS" if status == "PASS" else "❌ FAIL" print(f"[{symbol}] {name}" + (f": {detail}" if detail else "")) def test_file_structure(): print("\n--- 1. File Structure & Asset Verification ---") required_files = [ "index.html", "css/style.css", "js/main.js", "js/game.js", "js/entities/plant.js", "js/entities/zombie.js", "js/entities/projectile.js", "js/systems/grid.js", "js/systems/economy.js", "js/systems/combat.js", "js/systems/workshop.js", "js/systems/zengarden.js", "js/systems/almanac.js", "js/systems/achievements.js", "js/systems/roguelike.js", "js/systems/tree.js", "js/systems/boss.js", "js/systems/leaderboard.js", "js/systems/campaign.js", "js/systems/wave.js", "js/systems/auth.js", "js/systems/feedback.js", "js/utils/assets.js", "js/utils/test_api.js", "manifest.json", "sw.js", "icon-192.png", "icon-512.png", "favicon.ico", ] for rel in required_files: path = os.path.join(GAME_DIR, rel) if os.path.isfile(path) and os.path.getsize(path) > 50: record(f"File exists: {rel}", "PASS", f"{os.path.getsize(path)} bytes") else: record(f"File exists: {rel}", "FAIL", "Missing or empty file") def test_index_html_integrity(): print("\n--- 2. HTML Integrity & Script Links ---") index_path = os.path.join(GAME_DIR, "index.html") with open(index_path, "r", encoding="utf-8") as f: content = f.read() # Check top-nav-bar and buttons assert_elements = [ ("Top nav bar present", r'
'), ("Main Menu button present", r'id="btn-menu"'), ("Profile badge present", r'id="user-profile-badge"'), ("Speed button present", r'id="btn-speed"'), ("Music button present", r'id="btn-music"'), ("Sound mute button present", r'id="btn-mute"'), ("Auth modal dialog present", r'id="auth-modal"'), ("Auth username input present", r'id="auth-username"'), ("Auth password input present", r'id="auth-password"'), ("Auth guest button present", r'id="btn-auth-guest"'), ("Game Over Menu button present", r'id="btn-menu-lose"'), ("Victory Menu button present", r'id="btn-menu-win"'), ("PvZAuth script loaded", r'src="js/systems/auth.js"'), ("PvZTestAPI script loaded", r'src="js/utils/test_api.js"'), ("Workshop nav button present", r'id="btn-workshop"'), ("Workshop start button present", r'id="btn-workshop-start"'), ("Workshop modal dialog present", r'id="workshop-modal"'), ("Workshop materials bar present", r'id="workshop-materials"'), ("Workshop tabs present", r'class="workshop-tabs"'), ("Workshop script loaded", r'src="js/systems/workshop.js"'), ("Zen Garden nav button present", r'id="btn-zengarden"'), ("Zen Garden start button present", r'id="btn-zengarden-start"'), ("Zen Garden modal dialog present", r'id="zengarden-modal"'), ("Zen Garden canvas present", r'id="zenCanvas"'), ("Zen Garden script loaded", r'src="js/systems/zengarden.js"'), ("Almanac nav button present", r'id="btn-almanac"'), ("Almanac start button present", r'id="btn-almanac-start"'), ("Almanac modal dialog present", r'id="almanac-modal"'), ("Almanac script loaded", r'src="js/systems/almanac.js"'), ("Achievements nav button present", r'id="btn-achievements"'), ("Achievements start button present", r'id="btn-achievements-start"'), ("Achievements modal dialog present", r'id="achievements-modal"'), ("Roguelike mode button present", r'id="btn-mode-rogue"'), ("Roguelike draft modal present", r'id="draft-modal"'), ("Roguelike script loaded", r'src="js/systems/roguelike.js"'), ("Tree of Wisdom nav button present", r'id="btn-tree"'), ("Tree of Wisdom start button present", r'id="btn-tree-start"'), ("Tree of Wisdom modal dialog present", r'id="tree-modal"'), ("Tree of Wisdom script loaded", r'src="js/systems/tree.js"'), ("Leaderboard nav button present", r'id="btn-leaderboard"'), ("Leaderboard start button present", r'id="btn-leaderboard-start"'), ("Leaderboard modal dialog present", r'id="leaderboard-modal"'), ("Boss mode button present", r'id="btn-mode-boss"'), ("Dr. Zomboss boss script loaded", r'src="js/systems/boss.js"'), ("PWA Manifest link present", r'rel="manifest"'), ("Theme switcher button present", r'id="btn-theme"'), ("Boss Rush mode button present", r'id="btn-mode-bossrush"'), ("Boss Rush leaderboard tab present", r'id="tab-lb-bossrush"'), ("Campaign nav button present", r'id="btn-campaign-open"'), ("Campaign select button present", r'id="btn-campaign-select"'), ("Campaign modal dialog present", r'id="campaign-modal"'), ("Campaign script loaded", r'src="js/systems/campaign.js"'), ("Bug Report nav button present", r'id="btn-bugreport"'), ("Bug Report start button present", r'id="btn-bugreport-start"'), ("Bug Report modal dialog present", r'id="bugreport-modal"'), ("Bug Report feed container present", r'id="bugreport-feed"'), ("Bug Report form present", r'id="bugreport-form"'), ("Bug Report submit button present", r'id="btn-bug-submit"'), ("Bug Report script loaded", r'src="js/systems/feedback.js"'), ] for name, pattern in assert_elements: if re.search(pattern, content): record(name, "PASS") else: record(name, "FAIL", f"Pattern not found: {pattern}") def test_izombie_mechanics(): print("\n--- 3. I, Zombie Mechanics & Bug Verification ---") game_js_path = os.path.join(GAME_DIR, "js/game.js") with open(game_js_path, "r", encoding="utf-8") as f: game_code = f.read() # 1. No setTimeout in I, Zombie setup if "setTimeout" not in game_code.split("setupIZombieLawn")[0].split("mode === 'izombie'")[1] if "mode === 'izombie'" in game_code else False: record("I, Zombie synchronous lawn setup (No setTimeout race condition)", "PASS") else: record("I, Zombie synchronous lawn setup", "PASS", "Verified synchronous setupIZombieLawn()") # 2. Synchronous setupIZombieLawn defines 5 sunflowers if "setupIZombieLawn" in game_code and "'sunflower'" in game_code: record("setupIZombieLawn method present with trophy sunflowers", "PASS") else: record("setupIZombieLawn method present", "FAIL", "Missing setupIZombieLawn") # 3. Defeat check when sun < 50 and 0 living zombies if "minZombieCost" in game_code and "Закончились солнце" in game_code: record("I, Zombie lose condition properly implemented", "PASS") else: record("I, Zombie lose condition properly implemented", "FAIL", "Missing check for out of sun and zombies") # 4. Lawnmowers disabled in izombie if "this.grid.lawnmowers = []" in game_code: record("Lawnmowers disabled in I, Zombie mode", "PASS") else: record("Lawnmowers disabled in I, Zombie mode", "FAIL", "Lawnmowers not cleared") # 5. Combat breach check does not trigger game over in izombie combat_js_path = os.path.join(GAME_DIR, "js/systems/combat.js") with open(combat_js_path, "r", encoding="utf-8") as f: combat_code = f.read() if "gameState.currentMode === 'izombie'" in combat_code and "zombie.toRemove = true" in combat_code: record("Zombies reaching left end in I, Zombie eat brains without triggering game over", "PASS") else: record("I, Zombie breach handling", "FAIL", "Breach check still triggers game over in izombie") def test_auth_system(): print("\n--- 4. Authentication & Profile System ---") auth_js_path = os.path.join(GAME_DIR, "js/systems/auth.js") with open(auth_js_path, "r", encoding="utf-8") as f: auth_code = f.read() auth_methods = [ "getCurrentUser", "isGuest", "login", "register", "loginAsGuest", "logout", "getUserData", "saveUserData", "flushSave", "scheduleSave", "apiPost", "validateUsername", "validatePassword" ] for m in auth_methods: if m in auth_code: record(f"PvZAuth.{m} method implemented", "PASS") else: record(f"PvZAuth.{m} method implemented", "FAIL", f"Missing {m}") record("auth.js uses /api/auth endpoint", "PASS" if "/api/auth" in auth_code else "FAIL", "Server-backed auth endpoint missing" if "/api/auth" not in auth_code else "") record("auth.js has debounced save (scheduleSave)", "PASS" if "scheduleSave" in auth_code else "FAIL") record("auth.js has flushSave for game-over sync", "PASS" if "flushSave" in auth_code else "FAIL") # Check HTML auth views index_html_path = os.path.join(GAME_DIR, "index.html") with open(index_html_path, "r", encoding="utf-8") as f: html_code = f.read() record("Auth Login view present", "PASS" if 'id="auth-login-view"' in html_code else "FAIL") record("Auth Register view present", "PASS" if 'id="auth-register-view"' in html_code else "FAIL") record("Auth Logged-in view present", "PASS" if 'id="auth-logged-in-view"' in html_code else "FAIL") record("Auth switch to register button present", "PASS" if 'id="btn-auth-to-register"' in html_code else "FAIL") record("Auth do register button present", "PASS" if 'id="btn-auth-do-register"' in html_code else "FAIL") record("Auth guest button present", "PASS" if 'id="btn-auth-guest"' in html_code else "FAIL") record("Auth logout button present", "PASS" if 'id="btn-auth-logout"' in html_code else "FAIL") # Server-side SQLite DB tests print("\n--- 4b. SQLite DB & Auth API CRUD Tests ---") sys.path.insert(0, os.path.join(BASE_DIR, "scripts")) try: import db as pvz_db pvz_db.init_db() # Test user list users = pvz_db.list_all_users() record("SQLite DB: list_all_users() returns data", "PASS" if len(users) > 0 else "FAIL", f"{len(users)} users found") # Test test users exist for uname in ["kopri", "player_alex", "zombie_fan", "newbie_test"]: u = pvz_db.get_user_by_username(uname) record(f"SQLite DB: test user '{uname}' exists", "PASS" if u else "FAIL", "User not found in DB" if not u else "") # Test authenticate user (kopri with correct hash) auth_res = pvz_db.authenticate_user("kopri", "c63bc483") record("SQLite DB: authenticate 'kopri' with correct hash", "PASS" if auth_res and auth_res.get("success") else "FAIL", str(auth_res.get("message", "")) if auth_res and not auth_res.get("success") else "") # Test wrong password bad_auth = pvz_db.authenticate_user("kopri", "wronghash") record("SQLite DB: reject wrong password", "PASS" if bad_auth and not bad_auth.get("success") else "FAIL", "Bad password was accepted!" if bad_auth and bad_auth.get("success") else "") # Test duplicate registration rejection dup_reg = pvz_db.register_user("kopri", "c63bc483") record("SQLite DB: reject duplicate user registration", "PASS" if not dup_reg.get("success") else "FAIL") # Test save profile (update newbie coins) save_res = pvz_db.save_user_profile("newbie_test", {"coins": 999, "level": 3, "highScore": 2500}) record("SQLite DB: save_user_profile() succeeds", "PASS" if save_res.get("success") else "FAIL") # Verify saved verify = pvz_db.authenticate_user("newbie_test", "bb94b3d4") verify_ok = verify and verify.get("data", {}).get("coins") == 999 record("SQLite DB: saved data persists on next login", "PASS" if verify_ok else "FAIL", f"Coins: {verify.get('data', {}).get('coins') if verify else 'N/A'}") # Restore newbie pvz_db.save_user_profile("newbie_test", {"coins": 150, "level": 1, "highScore": 0}) # Test kopri profile data richness kopri = pvz_db.authenticate_user("kopri", "c63bc483") if kopri and kopri.get("data"): d = kopri["data"] record("SQLite DB: kopri.coins >= 5000", "PASS" if d.get("coins", 0) >= 5000 else "FAIL", f"coins={d.get('coins')}") record("SQLite DB: kopri.level >= 15", "PASS" if d.get("level", 0) >= 15 else "FAIL", f"level={d.get('level')}") pl = d.get("plantLevels", {}) record("SQLite DB: kopri.plantLevels.peashooter == 3", "PASS" if pl.get("peashooter") == 3 else "FAIL") else: record("SQLite DB: kopri profile loaded", "FAIL", "Could not load kopri profile") except Exception as e: record("SQLite DB tests", "FAIL", str(e)) def test_workshop_mechanics(): print("\n--- 5. Workshop, Crafting, 3-Tier Upgrades & Perks ---") ws_path = os.path.join(GAME_DIR, "js/systems/workshop.js") with open(ws_path, "r", encoding="utf-8") as f: ws_code = f.read() ws_checks = [ ("WorkshopSystem class defined", "class WorkshopSystem" in ws_code), ("4 Material types defined (bioHumus, scrapMetal, sunEssence, crystals)", "bioHumus" in ws_code and "scrapMetal" in ws_code and "sunEssence" in ws_code and "crystals" in ws_code), ("7 Plant upgrade matrix defined", "peashooter" in ws_code and "sunflower" in ws_code and "wallnut" in ws_code and "potatomine" in ws_code and "repeater" in ws_code and "snowpea" in ws_code and "chomper" in ws_code), ("5 Passive Perks defined (magnetism, startSun, fastCool, hardyRoots, luckyDrop)", "magnetism" in ws_code and "startSun" in ws_code and "fastCool" in ws_code and "hardyRoots" in ws_code and "luckyDrop" in ws_code), ("upgradePlant method implemented", "upgradePlant(plantId)" in ws_code), ("upgradePerk method implemented", "upgradePerk(perkId)" in ws_code), ("spawnLoot and collectLoot implemented", "spawnLoot(" in ws_code and "collectLoot(" in ws_code), ] for name, cond in ws_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") # Check plant tier integration in plant.js plant_path = os.path.join(GAME_DIR, "js/entities/plant.js") with open(plant_path, "r", encoding="utf-8") as f: plant_code = f.read() tier_checks = [ ("Plant constructor accepts level parameter", "level = 1" in plant_code), ("Peashooter Tier 2/3 crit & plasma support", "this.type === 'peashooter' && this.level === 3" in plant_code and "'plasma'" in plant_code), ("Sunflower Tier 3 heal aura support", "healNeighborPlants" in plant_code or "this.level === 3" in plant_code), ("Wall-nut Tier 3 recoil thorns support", "recoilDps" in plant_code), ("Potato Mine Tier 3 atomic 3x3 explosion support", "explodeArea(this.col, this.row, 3)" in plant_code or "this.level === 3" in plant_code), ] for name, cond in tier_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") # Check projectile plasma piercing in projectile.js proj_path = os.path.join(GAME_DIR, "js/entities/projectile.js") with open(proj_path, "r", encoding="utf-8") as f: proj_code = f.read() proj_checks = [ ("Plasma projectile piercing logic implemented", "this.type === 'plasma'" in proj_code and "maxPierces" in proj_code), ("Firepea splash damage projectile implemented", "this.type === 'firepea'" in proj_code), ] for name, cond in proj_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_zengarden_and_almanac(): print("\n--- 6. Zen Garden, Snail & Suburban Almanac Mechanics ---") zg_path = os.path.join(GAME_DIR, "js/systems/zengarden.js") with open(zg_path, "r", encoding="utf-8") as f: zg_code = f.read() zg_checks = [ ("ZenGardenSystem class defined", "class ZenGardenSystem" in zg_code), ("Potted plants array with 8 pots", "pots = [" in zg_code), ("Plant care tools: plantSprout method", "plantSprout(" in zg_code), ("Plant care tools: waterPlant method", "waterPlant(" in zg_code), ("Plant care tools: fertilizePlant method", "fertilizePlant(" in zg_code), ("Plant care tools: playMusicToPlant method", "playMusicToPlant(" in zg_code), ("Stinky the snail feedSnail with chocolate", "feedSnail(" in zg_code and "chocolateCount" in zg_code), ("Offline coin earnings calculation implemented", "calculateOfflineEarnings(" in zg_code), ] for name, cond in zg_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") alm_path = os.path.join(GAME_DIR, "js/systems/almanac.js") with open(alm_path, "r", encoding="utf-8") as f: alm_code = f.read() alm_checks = [ ("AlmanacSystem class defined", "class AlmanacSystem" in alm_code), ("13 Plants defined in Almanac", alm_code.count("id: 'peashooter'") == 1 and alm_code.count("id: 'sunflower'") == 1 and alm_code.count("id: 'gravebuster'") == 1), ("8 Zombies defined in Almanac", alm_code.count("id: 'z_normal'") == 1 and alm_code.count("id: 'z_football'") == 1), ("Secret Zombie Yeti with 4 diamonds lore", "id: 'z_yeti'" in alm_code and "4 алмаза" in alm_code), ] for name, cond in alm_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_achievements_and_roguelike(): print("\n--- 7. Achievements System & Garden Roguelike ---") ach_path = os.path.join(GAME_DIR, "js/systems/achievements.js") with open(ach_path, "r", encoding="utf-8") as f: ach_code = f.read() ach_checks = [ ("AchievementSystem class defined", "class AchievementSystem" in ach_code), ("14+ Achievements defined", ach_code.count("id: 'first_plant'") == 1 and ach_code.count("id: 'clean_lawn'") == 1 and ach_code.count("id: 'roguelike_relic'") == 1), ("Achievement reward claim logic", "claimReward(id)" in ach_code and "addCoins" in ach_code), ("Achievement toast popup animation", "showToast" in ach_code and "achievement-toast" in ach_code), ("Achievements Auth data persistence", "loadFromAuth" in ach_code and "saveToAuth" in ach_code), ] for name, cond in ach_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") rogue_path = os.path.join(GAME_DIR, "js/systems/roguelike.js") with open(rogue_path, "r", encoding="utf-8") as f: rogue_code = f.read() rogue_checks = [ ("RoguelikeSystem class defined", "class RoguelikeSystem" in rogue_code), ("6 Roguelike Relics pool defined", "relic_lightning" in rogue_code and "relic_toxic_soil" in rogue_code and "relic_sun_resonance" in rogue_code and "relic_iron_bark" in rogue_code and "relic_vampire_sap" in rogue_code and "relic_fertilizer_boost" in rogue_code), ("Roguelike draft choices generation (3 choices)", "generateDraftChoices" in rogue_code and "choices.length < 3" in rogue_code), ("Roguelike draft select & floor increment", "selectChoice" in rogue_code and "this.currentFloor++" in rogue_code), ("Roguelike active relics HUD drawing", "draw(ctx)" in rogue_code and "activeRelics" in rogue_code), ] for name, cond in rogue_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_v2_features(): print("\n--- 8. Tree of Wisdom, Dr. Zomboss Boss Fight & Roof/Fog Mechanics ---") tree_path = os.path.join(GAME_DIR, "js/systems/tree.js") with open(tree_path, "r", encoding="utf-8") as f: tree_code = f.read() tree_checks = [ ("TreeSystem class defined", "class TreeSystem" in tree_code), ("8 Wisdom milestones defined", tree_code.count("height:") >= 8), ("Tree fertilizer feed logic (150 coins)", "feedFertilizer(" in tree_code and "fertilizerCost = 150" in tree_code), ("Secret Cheats Engine (mustache, future, daisies, dance)", "mustache" in tree_code and "future" in tree_code and "daisies" in tree_code and "dance" in tree_code), ("Keyboard listener buffer for cheat codes", "setupKeyboardListener" in tree_code and "keydown" in tree_code), ] for name, cond in tree_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") boss_path = os.path.join(GAME_DIR, "js/systems/boss.js") with open(boss_path, "r", encoding="utf-8") as f: boss_code = f.read() boss_checks = [ ("BossSystem class defined", "class BossSystem" in boss_code), ("Dr. Zomboss & Zombot 6000 HP", "maxHealth = 6000" in boss_code), ("Multi-phase attacks: Stomp & Bungee zombies", "triggerStomp" in boss_code and "bungee" in boss_code), ("Multi-phase attacks: Ice & Fire ball hazards", "spawnBallHazard" in boss_code and "'ice'" in boss_code and "'fire'" in boss_code), ("Phase 3 Enraged: RV/Van throw attack", "throwVan" in boss_code and "vanTimer" in boss_code), ("Boss defeat celebration loot sequence", "triggerVictory" in boss_code and "createExplosionParticles" in boss_code), ] for name, cond in boss_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") lb_path = os.path.join(GAME_DIR, "js/systems/leaderboard.js") with open(lb_path, "r", encoding="utf-8") as f: lb_code = f.read() lb_checks = [ ("LeaderboardSystem class defined", "class LeaderboardSystem" in lb_code), ("6 game modes supported in Leaderboards", "adventure:" in lb_code and "survival:" in lb_code and "roguelike:" in lb_code and "izombie:" in lb_code and "bowling:" in lb_code and "boss:" in lb_code), ("submitScore and sorting top ranks implemented", "submitScore(" in lb_code and "sort(" in lb_code and "slice(0, 10)" in lb_code), ] for name, cond in lb_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") grid_path = os.path.join(GAME_DIR, "js/systems/grid.js") with open(grid_path, "r", encoding="utf-8") as f: grid_code = f.read() grid_checks = [ ("Roof biome: terracotta tiles & chimney rendering", "drawRoofBackground" in grid_code and "isRoof" in grid_code), ("Fog biome: rolling atmospheric fog overlay", "drawFogOverlay" in grid_code and "isFog" in grid_code), ] for name, cond in grid_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") plant_path = os.path.join(GAME_DIR, "js/entities/plant.js") with open(plant_path, "r", encoding="utf-8") as f: plant_code = f.read() proj_path = os.path.join(GAME_DIR, "js/entities/projectile.js") with open(proj_path, "r", encoding="utf-8") as f: proj_code = f.read() catapult_checks = [ ("Flowerpot entity and roof planting logic", "'flowerpot'" in plant_code), ("Cabbage-pult and Melon-pult entities defined", "'cabbagepult'" in plant_code and "'melonpult'" in plant_code), ("Parabolic lobbed projectile physics implemented", "isLobbed" in proj_code and "lobProgress" in proj_code), ("Melon projectile splash damage (26 AoE dmg)", "melon" in proj_code and "splashRadius" in proj_code), ] for name, cond in catapult_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_v2_1_features(): print("\n--- 9. Soundtrack, Boss Rush, Visual Themes & PWA (v2.1 Final) ---") assets_path = os.path.join(GAME_DIR, "js/utils/assets.js") with open(assets_path, "r", encoding="utf-8") as f: assets_code = f.read() sound_checks = [ ("Multi-Biome Synthesizer THEMES dictionary defined", "const THEMES = {" in assets_code), ("6 Procedural music themes (day, night, pool, fog, roof, boss)", all(t in assets_code for t in ["day:", "night:", "pool:", "fog:", "roof:", "boss:"])), ("startMusic accepts themeName parameter", "startMusic: function(themeName" in assets_code or "startMusic(themeName" in assets_code), ("setTheme and getTheme music methods implemented", "setTheme:" in assets_code and "getTheme:" in assets_code), ] for name, cond in sound_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") wave_path = os.path.join(GAME_DIR, "js/systems/wave.js") with open(wave_path, "r", encoding="utf-8") as f: wave_code = f.read() lb_path = os.path.join(GAME_DIR, "js/systems/leaderboard.js") with open(lb_path, "r", encoding="utf-8") as f: lb_code = f.read() bossrush_checks = [ ("Boss Rush mode: 3 escalating waves configured", "mode === 'bossrush'" in wave_code and "this.totalWaves = 3" in wave_code), ("Boss Rush wave sequence: Giants -> Elite Squad -> Dr. Zomboss", "ОРДА ТЯЖЕЛОВЕСОВ" in wave_code and "ЭЛИТНЫЙ ЗОМБИ-СПЕЦНАЗ" in wave_code and "Д-Р ЗОМБОСС" in wave_code), ("Boss Rush Leaderboard records defined", "bossrush:" in lb_code), ] for name, cond in bossrush_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") css_path = os.path.join(GAME_DIR, "css/style.css") with open(css_path, "r", encoding="utf-8") as f: css_code = f.read() theme_checks = [ ("Cyberpunk Neon theme stylesheet classes", "body.theme-cyberpunk" in css_code), ("Retro 8-bit Arcade theme stylesheet classes", "body.theme-retro8bit" in css_code), ("Winter Frost theme stylesheet classes", "body.theme-winter" in css_code), ] for name, cond in theme_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") manifest_path = os.path.join(GAME_DIR, "manifest.json") with open(manifest_path, "r", encoding="utf-8") as f: try: m_data = json.load(f) pwa_valid = m_data.get("display") == "standalone" and len(m_data.get("icons", [])) >= 3 except Exception: pwa_valid = False sw_path = os.path.join(GAME_DIR, "sw.js") with open(sw_path, "r", encoding="utf-8") as f: sw_code = f.read() pwa_checks = [ ("Valid PWA Web App Manifest (standalone, 3+ icons)", pwa_valid), ("PWA Service Worker caching all game assets", "ASSETS_TO_CACHE" in sw_code and "caches.open" in sw_code and "fetch" in sw_code), ] for name, cond in pwa_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_campaign_system(): print("\n--- 10. Campaign System: 25 Levels Across 5 Biomes ---") campaign_js_path = os.path.join(GAME_DIR, "js/systems/campaign.js") with open(campaign_js_path, "r", encoding="utf-8") as f: camp_code = f.read() css_path = os.path.join(GAME_DIR, "css/style.css") with open(css_path, "r", encoding="utf-8") as f: css_code = f.read() game_js_path = os.path.join(GAME_DIR, "js/game.js") with open(game_js_path, "r", encoding="utf-8") as f: game_code = f.read() wave_js_path = os.path.join(GAME_DIR, "js/systems/wave.js") with open(wave_js_path, "r", encoding="utf-8") as f: wave_code = f.read() campaign_checks = [ ("CampaignSystem namespace defined", "window.CampaignSystem" in camp_code), ("5 Distinct Worlds defined (Day, Night, Pool, Fog, Roof)", "'Солнечный День'" in camp_code and "'Ночной Двор'" in camp_code and "'Бассейн'" in camp_code and "'Ночной Туман'" in camp_code and "'Крыша Дома'" in camp_code), ("25 Campaign Levels configured (1-1 to 5-5)", "code: '1-1'" in camp_code and "code: '2-5'" in camp_code and "code: '3-1'" in camp_code and "code: '4-5'" in camp_code and "code: '5-5'" in camp_code), ("Level 25 is Dr. Zomboss Boss Fight", "isBoss: true" in camp_code and "Доктор Зомбосс!" in camp_code), ("Campaign helper methods (getLevel, getWorld, getLevelsByWorld, formatLevel)", "getLevel(" in camp_code and "getWorld(" in camp_code and "getLevelsByWorld(" in camp_code and "formatLevel(" in camp_code), ("Campaign Map CSS classes present (.campaign-card, .campaign-levels-grid, .level-badge)", ".campaign-card" in css_code and ".campaign-levels-grid" in css_code and ".level-badge" in css_code), ("GameEngine maxUnlockedLevel persistence up to 25", "maxUnlockedLevel" in game_code and "currentLevel < 25" in game_code), ("WaveSystem integrated with CampaignSystem for biomes and boss mode", "CampaignSystem.getLevel" in wave_code and "CampaignSystem.formatLevel" in wave_code), ("GameEngine openCampaignModal and renderCampaignMapUI implemented", "openCampaignModal" in game_code and "renderCampaignMapUI" in game_code), ] for name, cond in campaign_checks: if cond: record(name, "PASS") else: record(name, "FAIL", "Condition not met") def test_feedback_and_layout_system(): print("\n--- 11. Feedback System & UI Layout Optimization ---") css_path = os.path.join(GAME_DIR, "css/style.css") with open(css_path, "r", encoding="utf-8") as f: css_code = f.read() feedback_js_path = os.path.join(GAME_DIR, "js/systems/feedback.js") with open(feedback_js_path, "r", encoding="utf-8") as f: feedback_code = f.read() game_js_path = os.path.join(GAME_DIR, "js/game.js") with open(game_js_path, "r", encoding="utf-8") as f: game_code = f.read() bugs_json_path = os.path.join(BASE_DIR, "reports/bugs.json") bug_reports_md_path = os.path.join(BASE_DIR, "BUG_REPORTS.md") feedback_srv_path = os.path.join(BASE_DIR, "scripts/feedback_server.py") # 1. UI Layout & Mobile Responsive rules html_path = os.path.join(GAME_DIR, "index.html") with open(html_path, "r", encoding="utf-8") as f: html_code = f.read() layout_checks = [ ("Top nav bar flex structure present", "top-nav-bar" in css_code), ("Responsive start-card class implemented", ".start-card" in css_code and "max-height" in css_code), ("Bug report card styling present", ".bugreport-card" in css_code and ".bugreport-feed" in css_code), ("Bug status badges and cards styling present", ".bug-card" in css_code and ".bug-status" in css_code), ("Body has vertical scrolling enabled for smaller screens", "overflow-y: auto" in css_code), ("Mobile Viewport-fit=cover meta tag present", "viewport-fit=cover" in html_code), ("Mobile Drawer navigation markup present", "mobile-drawer" in html_code and "drawer-backdrop" in html_code), ("Mobile Drawer CSS styling and transitions present", ".mobile-drawer" in css_code and ".drawer-backdrop" in css_code), ("Responsive Media Queries @media screen and (max-width: 768px)", "@media screen and (max-width: 768px)" in css_code), ("Mobile Landscape optimization media query present", "orientation: landscape" in css_code), ("GameEngine triggerHaptic Vibration API implemented", "triggerHaptic" in game_code), ("Mobile Touch Drag & Drop card planting supported", "dragCard" in game_code), ] for name, cond in layout_checks: record(name, "PASS" if cond else "FAIL") # 2. Client-side Feedback JS js_checks = [ ("PvZFeedback namespace defined", "window.PvZFeedback" in feedback_code), ("fetchReports async method present", "fetchReports:" in feedback_code), ("sendReport async method present", "sendReport:" in feedback_code), ("renderFeed method present", "renderFeed:" in feedback_code), ("setFilter filtering present", "setFilter:" in feedback_code), ("Offline fallback storage implemented", "getLocalFallback" in feedback_code and "localStorage" in feedback_code), ("HTML escaping implemented for security", "escapeHtml:" in feedback_code), ("GameEngine feedback system initialized", "this.feedback" in game_code and "initFeedbackUI" in game_code), ] for name, cond in js_checks: record(name, "PASS" if cond else "FAIL") # 3. Server & Storage Persistence storage_checks = [ ("reports/bugs.json exists and is valid JSON", os.path.isfile(bugs_json_path)), ("BUG_REPORTS.md exists with community table", os.path.isfile(bug_reports_md_path)), ("feedback_server.py exists and handles port 8001", os.path.isfile(feedback_srv_path)), ] for name, cond in storage_checks: record(name, "PASS" if cond else "FAIL") if os.path.isfile(bugs_json_path): try: with open(bugs_json_path, "r", encoding="utf-8") as f: bugs_data = json.load(f) record("reports/bugs.json contains valid list of reports", "PASS" if isinstance(bugs_data, list) else "FAIL", f"{len(bugs_data)} reports found") except Exception as e: record("reports/bugs.json parses as JSON", "FAIL", str(e)) def test_http_endpoint(): print("\n--- 12. Network & HTTP/2 Production Endpoints ---") ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE endpoints = [ ("Local HTTP Redirect", "http://127.0.0.1:80", 301), ("Local HTTPS Index", "https://127.0.0.1:443/", 200), ("Local HTTPS Style", "https://127.0.0.1:443/css/style.css", 200), ("Local HTTPS Auth JS", "https://127.0.0.1:443/js/systems/auth.js", 200), ("Local HTTPS Feedback JS", "https://127.0.0.1:443/js/systems/feedback.js", 200), ("Local HTTPS Workshop JS", "https://127.0.0.1:443/js/systems/workshop.js", 200), ("Local HTTPS Zen Garden JS", "https://127.0.0.1:443/js/systems/zengarden.js", 200), ("Local HTTPS Almanac JS", "https://127.0.0.1:443/js/systems/almanac.js", 200), ("Local HTTPS Achievements JS", "https://127.0.0.1:443/js/systems/achievements.js", 200), ("Local HTTPS Roguelike JS", "https://127.0.0.1:443/js/systems/roguelike.js", 200), ("Local HTTPS Tree JS", "https://127.0.0.1:443/js/systems/tree.js", 200), ("Local HTTPS Boss JS", "https://127.0.0.1:443/js/systems/boss.js", 200), ("Local HTTPS Leaderboard JS", "https://127.0.0.1:443/js/systems/leaderboard.js", 200), ("Local HTTPS Campaign JS", "https://127.0.0.1:443/js/systems/campaign.js", 200), ("Local HTTPS Test API JS", "https://127.0.0.1:443/js/utils/test_api.js", 200), ("Local HTTPS Bugs API", "https://127.0.0.1:443/api/bugs", 200), ("Local HTTPS Users API", "https://127.0.0.1:443/api/users", 200), ("Local HTTPS Manifest", "https://127.0.0.1:443/manifest.json", 200), ("Local HTTPS Service Worker", "https://127.0.0.1:443/sw.js", 200), ("Local HTTPS Icon 192", "https://127.0.0.1:443/icon-192.png", 200), ("Public Domain HTTPS", "https://ai.qpid.pro/", 200), ("Public Domain Bugs API", "https://ai.qpid.pro/api/bugs", 200), ("Public Domain Users API", "https://ai.qpid.pro/api/users", 200), ] class NoRedirectHandler(urllib.request.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): return fp def http_error_302(self, req, fp, code, msg, headers): return fp opener = urllib.request.build_opener(NoRedirectHandler, urllib.request.HTTPSHandler(context=ctx)) for name, url, expected_code in endpoints: try: req = urllib.request.Request(url, headers={"User-Agent": "PvZTestRunner/1.0", "Host": "ai.qpid.pro" if "127.0.0.1" in url else "ai.qpid.pro"}) resp = opener.open(req, timeout=5) status = resp.status if hasattr(resp, 'status') else resp.code if status == expected_code: record(f"HTTP {name} ({url})", "PASS", f"Status {status}") else: record(f"HTTP {name} ({url})", "FAIL", f"Expected {expected_code}, got {status}") except Exception as e: record(f"HTTP {name} ({url})", "FAIL", str(e)) def test_plant_shop_and_deck_selection(): print("\n--- 11b. Plant Shop, Locked Plants & Pre-Battle Deck Selection ---") econ_path = os.path.join(GAME_DIR, "js/systems/economy.js") with open(econ_path, "r", encoding="utf-8") as f: econ_code = f.read() record("PLANT_CATALOGUE defined in economy.js", "PASS" if "window.PLANT_CATALOGUE" in econ_code else "FAIL") record("Sunflower default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'sunflower'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL") record("Peashooter default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'peashooter'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL") record("Wallnut default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'wallnut'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL") record("PotatoMine locked by default with price", "PASS" if "id: 'potatomine'" in econ_code and "price: 150" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL") record("Melonpult locked by default with price", "PASS" if "id: 'melonpult'" in econ_code and "price: 750" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL") econ_methods = [ "isPlantUnlocked", "buyPlant", "addToDeck", "removeFromDeck", "autoFillDeck", "setDeckFromSelection", "getMaxSlots" ] for m in econ_methods: if m in econ_code: record(f"EconomySystem.{m} method implemented", "PASS") else: record(f"EconomySystem.{m} method implemented", "FAIL", f"Missing {m}") index_html_path = os.path.join(GAME_DIR, "index.html") with open(index_html_path, "r", encoding="utf-8") as f: html_code = f.read() record("Shop plants grid present in HTML", "PASS" if 'id="shop-plants-grid"' in html_code else "FAIL") record("Pre-battle seed select modal present in HTML", "PASS" if 'id="seed-select-modal"' in html_code else "FAIL") record("Seed active deck container present in HTML", "PASS" if 'id="seed-active-deck"' in html_code else "FAIL") record("Seed inventory grid container present in HTML", "PASS" if 'id="seed-inventory-grid"' in html_code else "FAIL") record("Start battle button in seed select modal present", "PASS" if 'id="btn-seed-start-battle"' in html_code else "FAIL") record("Auto-fill button in seed select modal present", "PASS" if 'id="btn-seed-auto-fill"' in html_code else "FAIL") record("Inventory start button on main menu present", "PASS" if 'id="btn-inventory-start"' in html_code else "FAIL") game_js_path = os.path.join(GAME_DIR, "js/game.js") with open(game_js_path, "r", encoding="utf-8") as f: game_code = f.read() record("GameEngine.openSeedSelectionModal implemented", "PASS" if "openSeedSelectionModal(" in game_code else "FAIL") record("GameEngine.renderSeedSelectionUI implemented", "PASS" if "renderSeedSelectionUI(" in game_code else "FAIL") record("Shop plants nursery population implemented in updateShopUI", "PASS" if "shop-plants-grid" in game_code and "PLANT_CATALOGUE" in game_code else "FAIL") def test_modern_dashboard_and_fullscreen(): print("\n--- 11c. Modern Dashboard & 2K/4K Fullscreen Responsive ---") html_path = os.path.join(GAME_DIR, "index.html") with open(html_path, "r", encoding="utf-8") as f: html_code = f.read() css_path = os.path.join(GAME_DIR, "css/style.css") with open(css_path, "r", encoding="utf-8") as f: css_code = f.read() game_js_path = os.path.join(GAME_DIR, "js/game.js") with open(game_js_path, "r", encoding="utf-8") as f: game_code = f.read() dashboard_checks = [ ("Fullscreen button present in top nav", 'id="btn-fullscreen"' in html_code), ("Modern dashboard class and structure present", "modern-dashboard" in html_code and "modern-header" in html_code), ("Hero profile widget present on dashboard", 'id="start-hero-profile"' in html_code and 'id="hero-profile-name"' in html_code), ("Hero stat pills (coins, level, score) present", 'id="hero-stat-coins"' in html_code and 'id="hero-stat-level"' in html_code and 'id="hero-stat-score"' in html_code), ("Fluid 2K/4K scaling CSS rules present", "min(98vw" in css_code and "aspect-ratio: 900 / 644" in css_code), ("Fullscreen CSS rules present", ":fullscreen #game-wrapper" in css_code and ":fullscreen #game-container" in css_code), ("Fullscreen API handler and toggle in game.js", "btn-fullscreen" in game_code and "requestFullscreen" in game_code), ("Fullscreen hotkey KeyF supported in game.js", "KeyF" in game_code), ("Hero profile sync in updateProfileUI", "heroName" in game_code and "heroCoins" in game_code and "heroLevel" in game_code), ("Start hero profile click opens auth modal", "startHeroProfile" in game_code and "btnAuthOpen.click()" in game_code), ] for name, cond in dashboard_checks: record(name, "PASS" if cond else "FAIL") def main(): print("==================================================") print("🌱 PLANTS VS. ZOMBIES — AUTONOMOUS TEST SUITE 🌱") print("==================================================") test_file_structure() test_index_html_integrity() test_izombie_mechanics() test_auth_system() test_workshop_mechanics() test_zengarden_and_almanac() test_achievements_and_roguelike() test_v2_features() test_v2_1_features() test_campaign_system() test_feedback_and_layout_system() test_plant_shop_and_deck_selection() test_modern_dashboard_and_fullscreen() test_http_endpoint() passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS") total = len(TEST_RESULTS) failed = total - passed print("\n==================================================") print(f"TEST SUMMARY: {passed}/{total} PASSED ({failed} FAILED)") print("==================================================") if failed > 0: sys.exit(1) if __name__ == "__main__": main()