Files
ai-qpid-pvz/scripts/test_game.py
T

770 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'<header class="top-nav-bar">'),
("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 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_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()