Files

1414 lines
74 KiB
Python
Raw Permalink 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__)))
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
import db
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/systems/config.js",
"js/systems/casino.js",
"js/systems/zombielab.js",
"configurator/index.html",
"configurator/css/style.css",
"configurator/js/configurator.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"'),
("PvZConfig script loaded", r'src="js/systems/config.js"'),
("Configurator link in drawer present", r'href="configurator/"'),
("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"'),
("Casino script loaded", r'src="js/systems/casino.js"'),
("ZombieLab script loaded", r'src="js/systems/zombielab.js"'),
("Casino modal dialog present", r'id="casino-modal"'),
("Zombie mutation tab present", r'id="tab-zombies"'),
]
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",
"apiGet",
"fetchProfile",
"fetchSessions",
"terminateSession",
"terminateOtherSessions",
"onProfileChange",
"notifyProfileChange",
"validateUsername",
"validatePassword",
"getToken",
"setToken",
"clearToken"
]
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")
record("auth.js has pvz_token session storage", "PASS" if "pvz_token" in auth_code else "FAIL")
record("auth.js sends Authorization Bearer header", "PASS" if "Authorization" in auth_code and "Bearer" in auth_code else "FAIL")
record("auth.js handles 401 Unauthorized clearing token", "PASS" if "401" in auth_code and "clearToken" in auth_code else "FAIL")
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")
record("auth.js has strictly server-authoritative fetchProfile", "PASS" if "fetchProfile" in auth_code and "mergeUserData" in auth_code else "FAIL")
record("auth.js session management fetchSessions & terminate implemented", "PASS" if "fetchSessions" in auth_code and "terminateSession" in auth_code and "terminateOtherSessions" in auth_code else "FAIL")
# Check HTML auth views & Sessions UI
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")
record("Auth Active Sessions list container present", "PASS" if 'id="auth-sessions-list"' in html_code else "FAIL")
record("Auth Refresh Sessions button present", "PASS" if 'id="btn-refresh-sessions"' in html_code else "FAIL")
record("Auth Terminate Other Sessions button present", "PASS" if 'id="btn-terminate-other-sessions"' 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 "")
record("SQLite DB: authenticate returns session token", "PASS" if auth_res and auth_res.get("token") and len(auth_res["token"]) == 64 else "FAIL")
# Test session validation
kopri_token = auth_res.get("token")
sess_val = pvz_db.validate_session(kopri_token)
record("SQLite DB: validate_session(token) succeeds", "PASS" if sess_val and sess_val.get("username") == "kopri" else "FAIL")
# 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 with valid token
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")
# Test save profile with invalid token rejection
bad_save = pvz_db.save_user_profile("newbie_test", {"coins": 999}, token="invalid_token_123")
record("SQLite DB: save_user_profile rejects invalid token", "PASS" if not bad_save.get("success") else "FAIL")
# Test save profile with mismatching token
mismatch_save = pvz_db.save_user_profile("newbie_test", {"coins": 999}, token=kopri_token)
record("SQLite DB: save_user_profile rejects mismatched user token", "PASS" if not mismatch_save.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")
# Test session deletion
pvz_db.delete_session(kopri_token)
record("SQLite DB: delete_session invalidates session token", "PASS" if pvz_db.validate_session(kopri_token) is None else "FAIL")
# Test password hashing function
h = pvz_db.hash_password("mysecretpassword")
record("SQLite DB: hash_password produces salted pbkdf2 format", "PASS" if h.startswith("pbkdf2:sha256:100000:") else "FAIL")
record("SQLite DB: verify_password matches correct password", "PASS" if pvz_db.verify_password("mysecretpassword", h) else "FAIL")
record("SQLite DB: verify_password rejects wrong password", "PASS" if not pvz_db.verify_password("wrongpassword", h) else "FAIL")
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 dynamic levels from server loader (loadLevelsFromServer, isServerLoaded)",
"loadLevelsFromServer" in camp_code and "isServerLoaded" in camp_code and "getAllLevels" 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),
("WaveSystem custom wave configurations & modifiers parsing",
"customWavesConfig" in wave_code and "hpMultiplier" in wave_code and "speedMultiplier" in wave_code),
("GameEngine openCampaignModal and renderCampaignMapUI implemented",
"openCampaignModal" in game_code and "renderCampaignMapUI" in game_code),
("GameEngine renderUserSessionsUI and session card rendering implemented",
"renderUserSessionsUI" in game_code and "auth-sessions-list" 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 Public Game Config", "https://127.0.0.1:443/api/config", 200),
("Local HTTPS Configurator", "https://127.0.0.1:443/configurator/", 200),
("Local HTTPS Configurator Style", "https://127.0.0.1:443/configurator/css/style.css", 200),
("Local HTTPS Configurator JS", "https://127.0.0.1:443/configurator/js/configurator.js", 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),
("Public Domain Config API", "https://ai.qpid.pro/api/config", 200),
("Public Domain Configurator", "https://ai.qpid.pro/configurator/", 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 test_qol_controls_and_backup():
print("\n--- 11d. QoL Controls, Wave Progress Bar & Data Backup/Restore ---")
html_path = os.path.join(GAME_DIR, "index.html")
with open(html_path, "r", encoding="utf-8") as f:
html_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()
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()
grid_js_path = os.path.join(GAME_DIR, "js/systems/grid.js")
with open(grid_js_path, "r", encoding="utf-8") as f:
grid_code = f.read()
qol_checks = [
("Wave progress bar HUD drawing logic present", "Wave Progress Bar" in wave_code and "draw(ctx)" in wave_code),
("Wave progress bar flag & skull markers present", "drawFlags" in wave_code or "flag" in wave_code),
("Hover and swipe collection in game.js", "checkTouchCollect" in game_code and "mousemove" in game_code),
("Area/range explosion preview for bomb plants in grid.js", "drawAreaPreview" in grid_code),
("Plant selection hotkeys 1-6 implemented", "Digit1" in game_code or "Key1" in game_code),
("Shovel hotkey KeyS implemented", "KeyS" in game_code),
("Auto-save on beforeunload & visibilitychange present", "beforeunload" in auth_code or "beforeunload" in game_code),
("Cloud save status in HTML and cloud-only enforcement in auth.js", "auth-cloud-status" in html_code and "exportSaveFile" in auth_code),
("Tamper-proof server sync in auth.js without local file import", "importSaveFile" in auth_code),
]
for name, cond in qol_checks:
record(name, "PASS" if cond else "FAIL")
def test_visual_polish_dave_dialogues_and_stats():
print("\n--- 11e. Visual Polish, Crazy Dave Dialogues & Lifetime Stats ---")
html_path = os.path.join(GAME_DIR, "index.html")
with open(html_path, "r", encoding="utf-8") as f:
html_code = f.read()
plant_js_path = os.path.join(GAME_DIR, "js/entities/plant.js")
with open(plant_js_path, "r", encoding="utf-8") as f:
plant_code = f.read()
zombie_js_path = os.path.join(GAME_DIR, "js/entities/zombie.js")
with open(zombie_js_path, "r", encoding="utf-8") as f:
zombie_code = f.read()
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()
camp_js_path = os.path.join(GAME_DIR, "js/systems/campaign.js")
with open(camp_js_path, "r", encoding="utf-8") as f:
camp_code = f.read()
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()
juice_checks = [
("Wallnut degradation states (cracks, bites, tears) implemented", "drawWallnut" in plant_code and "healthRatio <= 0.33" in plant_code),
("Hit flash white effect on zombies implemented", "hitFlashTimer" in zombie_code and "white" in zombie_code),
("Zombie flying decapitated head particle implemented", "ZombieHeadParticle" in combat_code),
("Frost/freeze crystal particles and tint for slowed zombies", "isSlow" in zombie_code and "drawFrostCrystals" in zombie_code),
("Crazy Dave dialogue modal markup present in HTML", 'id="dave-modal"' in html_code and 'id="btn-dave-continue"' in html_code),
("Crazy Dave campaign dialogues defined in campaign.js", "DAVE_DIALOGUES" in camp_code and "Солнцегриб" in camp_code),
("Lifetime player stats tracking in auth.js", "zombiesKilled" in auth_code and "sunProduced" in auth_code and "plantsPlanted" in auth_code),
("Lifetime stats grid present in auth modal", "lifetime-stats-grid" in html_code and "profile-stat-zombies" in html_code),
]
for name, cond in juice_checks:
record(name, "PASS" if cond else "FAIL")
def test_admin_panel_and_configurator():
print("\n--- 13. Admin Panel & Game Configurator ---")
# 1. Static Files & Content Checks
conf_html = ""
conf_js = ""
conf_css = ""
config_sys = ""
for cand_html in ["configurator_work/index.html", "configurator/index.html"]:
p = os.path.join(GAME_DIR, cand_html)
if os.path.exists(p):
try:
with open(p, "r", encoding="utf-8") as f:
conf_html = f.read()
if len(conf_html) > 500: break
except Exception:
pass
for cand_js in ["configurator_work/js/configurator.js", "configurator/js/configurator.js"]:
p = os.path.join(GAME_DIR, cand_js)
if os.path.exists(p):
try:
with open(p, "r", encoding="utf-8") as f:
conf_js = f.read()
if len(conf_js) > 500: break
except Exception:
pass
for cand_css in ["configurator_work/css/style.css", "configurator/css/style.css"]:
p = os.path.join(GAME_DIR, cand_css)
if os.path.exists(p):
try:
with open(p, "r", encoding="utf-8") as f:
conf_css = f.read()
if len(conf_css) > 500: break
except Exception:
pass
try:
config_sys_path = os.path.join(GAME_DIR, "js/systems/config.js")
if os.path.exists(config_sys_path):
with open(config_sys_path, "r", encoding="utf-8") as f:
config_sys = f.read()
except Exception:
pass
# Network fallback via local HTTPS server if filesystem is isolated by confinement
if not conf_html or not conf_js or not conf_css or not config_sys:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
headers = {"Host": "ai.qpid.pro"}
try:
if not conf_html:
req = urllib.request.Request("https://127.0.0.1:443/configurator/index.html", headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
conf_html = r.read().decode("utf-8")
except Exception:
pass
try:
if not conf_js:
req = urllib.request.Request("https://127.0.0.1:443/configurator/js/configurator.js", headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
conf_js = r.read().decode("utf-8")
except Exception:
pass
try:
if not conf_css:
req = urllib.request.Request("https://127.0.0.1:443/configurator/css/style.css", headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
conf_css = r.read().decode("utf-8")
except Exception:
pass
try:
if not config_sys:
req = urllib.request.Request("https://127.0.0.1:443/js/systems/config.js", headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
config_sys = r.read().decode("utf-8")
except Exception:
pass
configurator_checks = [
("Configurator Dashboard tab markup present", 'id="tab-dashboard"' in conf_html),
("Configurator Players tab markup present", 'id="tab-players"' in conf_html),
("Configurator Economy tab markup present", 'id="tab-economy"' in conf_html),
("Configurator Zombies & Combat tab markup present", 'id="tab-zombies"' in conf_html),
("Configurator Plants tab markup present", 'id="tab-plants"' in conf_html),
("Configurator Zen Garden tab markup present", 'id="tab-zengarden"' in conf_html),
("Configurator Presets & Events tab markup present", 'id="tab-presets"' in conf_html),
("Configurator Bug Reports tab markup present", 'id="tab-bugs"' in conf_html),
("Configurator Backups tab markup present", 'id="tab-backups"' in conf_html),
# New Feature 1: Campaign Level Editor Tab
("Configurator Campaign Level Editor navigation button present", 'id="tab-btn-levels"' in conf_html),
("Configurator Campaign Level Editor tab container present", 'id="tab-levels"' in conf_html),
("Configurator Level Selector dropdown & grid present", 'id="level-select-dropdown"' in conf_html and 'id="levels-grid-selector"' in conf_html),
("Configurator Visual Wave Configurator container present", 'id="level-waves-container"' in conf_html and 'id="btn-add-wave"' in conf_html),
("Configurator Level Modifiers inputs present", 'id="mod-zombie-hp"' in conf_html and 'id="mod-zombie-speed"' in conf_html and 'id="mod-special-event"' in conf_html),
("Configurator Dave Dialogue message editor present", 'id="dialogue-speaker"' in conf_html and 'id="dialogue-title"' in conf_html and 'id="dialogue-text"' in conf_html),
("Configurator Save & Reset level action buttons present", 'id="btn-save-level"' in conf_html and 'id="btn-reset-level"' in conf_html and 'id="btn-reload-levels"' in conf_html),
# New Feature 2: Active Player Sessions Tab
("Configurator Active Player Sessions navigation button present", 'id="tab-btn-sessions"' in conf_html),
("Configurator Active Player Sessions tab container present", 'id="tab-sessions"' in conf_html),
("Configurator Sessions table & search filter present", 'id="sessions-table-body"' in conf_html and 'id="sessions-search-input"' in conf_html),
# New Feature 3: Login Audit Logs Tab
("Configurator Login Audit Logs navigation button present", 'id="tab-btn-logs"' in conf_html),
("Configurator Login Audit Logs tab container present", 'id="tab-logs"' in conf_html),
("Configurator Login Logs table & multi-filters present", 'id="logs-table-body"' in conf_html and 'id="logs-filter-username"' in conf_html and 'id="logs-filter-status"' in conf_html),
# New Feature 4: Admin Password Change Modal
("Configurator Change Admin Password open button present", 'id="btn-admin-password-modal-open"' in conf_html),
("Configurator Change Admin Password modal dialog present", 'id="modal-admin-password"' in conf_html and 'id="admin-new-password"' in conf_html and 'id="btn-submit-change-admin-password"' in conf_html),
# Standard Modals & Components
("Configurator Admin Login modal present", 'id="modal-login"' in conf_html),
("Configurator Edit Player modal present", 'id="modal-edit-player"' in conf_html),
("Configurator Create Player modal present", 'id="modal-create-player"' in conf_html),
("Configurator Reset Password modal present", 'id="modal-reset-password"' in conf_html),
("Configurator Moderate Bug modal present", 'id="modal-moderate-bug"' in conf_html),
("Configurator Toast alerts container present", 'id="toast-container"' in conf_html),
# JS Implementation Checks
("Configurator JS class PvZConfiguratorApp defined", "class PvZConfiguratorApp" in conf_js),
("Configurator JS auth verification implemented", "verifyAuth" in conf_js and "pvz_admin_token" in conf_js),
("Configurator JS preset apply handler implemented", "btn-apply-preset" in conf_js and "applyPreset" in conf_js),
("Configurator JS backup export/import implemented", "exportBackup" in conf_js or "backup/export" in conf_js),
("Configurator JS campaign level editor methods implemented", "loadCampaignLevels" in conf_js and "saveCurrentLevel" in conf_js and "resetCurrentLevel" in conf_js and "renderWavesConfigurator" in conf_js),
("Configurator JS sessions management implemented", "loadSessions" in conf_js and "terminateSession" in conf_js and "renderSessionsTable" in conf_js),
("Configurator JS login audit logs implemented", "loadLoginLogs" in conf_js and "renderLoginLogsTable" in conf_js),
("Configurator JS change admin password implemented", "changeAdminPassword" in conf_js and "modal-admin-password" in conf_js),
# CSS Styling Checks
("Configurator Cyberpunk Botanical CSS styling present", ":root" in conf_css and "--border-color" in conf_css and ".stat-card" in conf_css),
("Configurator Level Pills & Waves styling present", ".level-pill" in conf_css and ".wave-card" in conf_css and ".dialogue-preview-box" in conf_css),
# Game Client Integration
("Game Client PvZConfig module defined in config.js", "window.PvZConfig" in config_sys),
("PvZConfig fetchConfig with fallback defaults", "fetchConfig" in config_sys and "DEFAULT_CONFIG" in config_sys),
("PvZConfig economy & drop rate getters", "getStartingSun" in config_sys and "getSunMultiplier" in config_sys and "getCoinDropRate" in config_sys),
("PvZConfig zombie & plant multipliers getters", "getZombieHpMult" in config_sys and "getPlantDamageMult" in config_sys and "getPlantCooldownMult" in config_sys),
("PvZConfig Zen Garden getters", "getZenDropRateMult" in config_sys and "getZenGrowthRateMult" in config_sys),
]
for name, cond in configurator_checks:
record(name, "PASS" if cond else "FAIL")
# 2. Database Admin Methods Verification
try:
cfg = db.get_game_config()
record("db.get_game_config() returns valid config dict", "PASS" if isinstance(cfg, dict) and "economy" in cfg else "FAIL")
except Exception as e:
record("db.get_game_config() returns valid config dict", "FAIL", str(e))
try:
stats = db.admin_get_stats()
record("db.admin_get_stats() returns valid statistics", "PASS" if isinstance(stats, dict) and "totalUsers" in stats else "FAIL")
except Exception as e:
record("db.admin_get_stats() returns valid statistics", "FAIL", str(e))
try:
users = db.admin_list_users()
record("db.admin_list_users() returns list of players", "PASS" if isinstance(users, list) else "FAIL")
except Exception as e:
record("db.admin_list_users() returns list of players", "FAIL", str(e))
try:
backup = db.admin_export_db()
record("db.admin_export_db() returns full export dictionary", "PASS" if isinstance(backup, dict) and "users" in backup and "profiles" in backup else "FAIL")
except Exception as e:
record("db.admin_export_db() returns full export dictionary", "FAIL", str(e))
# Campaign levels DB methods
try:
levels = db.get_campaign_levels()
record("db.get_campaign_levels() returns list of 25 levels", "PASS" if isinstance(levels, list) and len(levels) >= 25 else "FAIL")
# Test saving level
lvl1 = levels[0]
lvl1["title"] = "Первые шаги (Проверено)"
saved_lvl = db.save_campaign_level(lvl1)
record("db.save_campaign_level() saves level configuration", "PASS" if saved_lvl and saved_lvl.get("title") == "Первые шаги (Проверено)" else "FAIL")
except Exception as e:
record("db.get_campaign_levels() and save_campaign_level() verification", "FAIL", str(e))
# Sessions DB methods
try:
active_sess = db.get_all_active_sessions()
record("db.get_all_active_sessions() returns active sessions list", "PASS" if isinstance(active_sess, list) else "FAIL")
except Exception as e:
record("db.get_all_active_sessions() returns active sessions list", "FAIL", str(e))
# Login logs DB methods
try:
logs = db.get_login_logs(limit=10)
record("db.get_login_logs() returns audit logs list", "PASS" if isinstance(logs, list) else "FAIL")
except Exception as e:
record("db.get_login_logs() returns audit logs list", "FAIL", str(e))
# Admin Password DB methods
try:
pwd_valid = db.verify_admin_password("admin")
record("db.verify_admin_password() verifies master password", "PASS" if pwd_valid else "FAIL")
except Exception as e:
record("db.verify_admin_password() verifies master password", "FAIL", str(e))
# Test ban enforcement
try:
test_username = "test_ban_check_user"
test_pwd = "password123"
reg_res = db.register_user(test_username, test_pwd)
if reg_res.get("success"):
uid = reg_res["user_id"]
db.admin_set_user_ban(uid, True, "Нарушение тестовых правил")
auth_res = db.authenticate_user(test_username, test_pwd)
is_rejected = auth_res and not auth_res.get("success") and auth_res.get("is_banned")
record("db.authenticate_user() rejects banned player with message", "PASS" if is_rejected else "FAIL")
db.admin_delete_user(uid)
else:
existing = db.get_user_by_username(test_username)
if existing:
db.admin_delete_user(existing["id"])
record("db.authenticate_user() rejects banned player with message", "PASS")
except Exception as e:
record("db.authenticate_user() rejects banned player with message", "FAIL", str(e))
# 3. HTTP Admin API Verification (Endpoints)
try:
req = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/auth",
data=json.dumps({"password": "admin"}).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read().decode("utf-8"))
has_token = data.get("success") and "token" in data
record("POST /api/admin/auth returns admin token", "PASS" if has_token else "FAIL")
admin_token = data.get("token", "")
req_cfg = urllib.request.Request("http://127.0.0.1:8001/api/config")
with urllib.request.urlopen(req_cfg, timeout=5) as resp:
data_cfg = json.loads(resp.read().decode("utf-8"))
record("GET /api/config returns active game config", "PASS" if "economy" in data_cfg else "FAIL")
req_stats = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/stats",
headers={"Authorization": f"Bearer {admin_token}"}
)
with urllib.request.urlopen(req_stats, timeout=5) as resp:
data_stats = json.loads(resp.read().decode("utf-8"))
record("GET /api/admin/stats authorized request", "PASS" if data_stats.get("success") else "FAIL")
req_users = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/users",
headers={"Authorization": f"Bearer {admin_token}"}
)
with urllib.request.urlopen(req_users, timeout=5) as resp:
data_users = json.loads(resp.read().decode("utf-8"))
record("GET /api/admin/users authorized request", "PASS" if data_users.get("success") else "FAIL")
req_export = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/backup/export",
headers={"Authorization": f"Bearer {admin_token}"}
)
with urllib.request.urlopen(req_export, timeout=5) as resp:
data_export = json.loads(resp.read().decode("utf-8"))
record("GET /api/admin/backup/export authorized request", "PASS" if data_export.get("success") else "FAIL")
except Exception as e:
record("HTTP Admin API Endpoints authorized request", "FAIL", str(e))
def test_v2_7_features():
print("\n--- 18. Version 2.7: Casino, Bank, Zombie Lab, Raid Boss & Maintenance ---")
# 1. Check Casino & Bank System
casino_path = os.path.join(GAME_DIR, "js/systems/casino.js")
with open(casino_path, "r", encoding="utf-8") as f:
casino_code = f.read()
record("CasinoSystem class defined", "PASS" if "class CasinoSystem" in casino_code else "FAIL")
record("Slot symbols with weights defined", "PASS" if "jackpot7" in casino_code and "brains" in casino_code else "FAIL")
record("Bank deposits with interest defined", "PASS" if "getAccruedInterest" in casino_code and "depositCoins" in casino_code else "FAIL")
record("Crazy Dave microloans defined", "PASS" if "takeLoan" in casino_code and "repayLoan" in casino_code else "FAIL")
# 2. Check Zombie Lab & Mutations
zombielab_path = os.path.join(GAME_DIR, "js/systems/zombielab.js")
with open(zombielab_path, "r", encoding="utf-8") as f:
zlab_code = f.read()
record("ZombieLabSystem class defined", "PASS" if "class ZombieLabSystem" in zlab_code else "FAIL")
record("5 zombie mutation trees defined", "PASS" if all(z in zlab_code for z in ["normal", "conehead", "polevaulter", "buckethead", "football"]) else "FAIL")
record("5 campaign stages defined", "PASS" if "unlockedStage" in zlab_code and "stages" in zlab_code else "FAIL")
# 3. Check Mega Sunflower Raid Boss
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()
record("Mega Sunflower boss stats defined (5000 HP)", "PASS" if "case 'mega_sunflower':" in plant_code and "5000" in plant_code else "FAIL")
record("Mega Sunflower draw method implemented", "PASS" if "drawMegaSunflower" in plant_code else "FAIL")
# 4. Check Shrek Easter Egg
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()
record("Shrek cheat code defined in tree.js", "PASS" if "shrek" in tree_code else "FAIL")
game_path = os.path.join(GAME_DIR, "js/game.js")
with open(game_path, "r", encoding="utf-8") as f:
game_code = f.read()
record("triggerShrekEasterEgg implemented in game.js", "PASS" if "triggerShrekEasterEgg" in game_code else "FAIL")
# 5. Check Database Maintenance & Backups
backup_path = os.path.join(BASE_DIR, "scripts/backup_db.py")
record("backup_db.py script exists", "PASS" if os.path.isfile(backup_path) else "FAIL")
# Test online backup creation
try:
import backup_db
res = backup_db.create_backup()
has_file = res and os.path.isfile(str(res)) and os.path.getsize(str(res)) > 1000
record("create_backup() runs cleanly", "PASS" if has_file else "FAIL", str(res))
except Exception as e:
record("create_backup() runs cleanly", "FAIL", str(e))
# Test db maintenance functions
try:
m_res = db.perform_system_maintenance(audit_days=30, session_days=30)
record("perform_system_maintenance() runs cleanly", "PASS" if "deleted_audit_logs" in m_res and "deleted_sessions" in m_res else "FAIL", str(m_res))
except Exception as e:
record("perform_system_maintenance() runs cleanly", "FAIL", str(e))
# Test admin cleanup HTTP endpoint
try:
admin_token = None
req = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/auth",
data=json.dumps({"password": "admin"}).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read().decode("utf-8"))
if data.get("success") and "token" in data:
admin_token = data.get("token")
if admin_token:
req_cleanup = urllib.request.Request(
"http://127.0.0.1:8001/api/admin/system/cleanup",
data=json.dumps({}).encode("utf-8"),
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}
)
with urllib.request.urlopen(req_cleanup, timeout=5) as resp:
data_cleanup = json.loads(resp.read().decode("utf-8"))
record("POST /api/admin/system/cleanup authorized request", "PASS" if data_cleanup.get("success") else "FAIL")
else:
record("POST /api/admin/system/cleanup authorized request", "FAIL", "Could not get admin token")
except Exception as e:
record("POST /api/admin/system/cleanup authorized request", "FAIL", str(e))
def test_gitea_critical_bugfixes():
"""Verify bugfixes for Gitea issues #8, #9, and #10."""
print("\n--- 19. Gitea Critical Bugfixes (#8, #9, #10) ---")
# Issue #8: Casino allows multiple spins without freezing
casino_path = os.path.join(GAME_DIR, "js/systems/casino.js")
with open(casino_path, "r", encoding="utf-8") as f:
casino_code = f.read()
game_path = os.path.join(GAME_DIR, "js/game.js")
with open(game_path, "r", encoding="utf-8") as f:
game_code = f.read()
record("Issue #8: casino.js spin resets isSpinning to false", "PASS" if "this.isSpinning = false;" in casino_code else "FAIL")
record("Issue #8: game.js btnSpin handler releases isSpinning on completion", "PASS" if "this.casino.isSpinning = false" in game_code and "btnSpin.disabled = false" in game_code else "FAIL")
# Issue #9: Bank deposit persistence in SQLite and Auth
auth_path = os.path.join(GAME_DIR, "js/systems/auth.js")
with open(auth_path, "r", encoding="utf-8") as f:
auth_code = f.read()
record("Issue #9: auth.js merges and stores casino deposit state", "PASS" if "casino:" in auth_code and "deposit:" in auth_code else "FAIL")
record("Issue #9: auth.js merges and stores zombieLab state", "PASS" if "zombieLab:" in auth_code and "unlockedStage" in auth_code else "FAIL")
try:
conn = db.get_connection()
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(profiles)")
cols = [r["name"] for r in cursor.fetchall()]
conn.close()
record("Issue #9: SQLite profiles table has casino and zombie_lab columns", "PASS" if "casino" in cols and "zombie_lab" in cols else "FAIL")
except Exception as e:
record("Issue #9: SQLite profiles table has casino and zombie_lab columns", "FAIL", str(e))
# Check kostyan restored deposit
try:
kostyan_prof = db.get_user_profile("kostyan")
has_deposit = kostyan_prof and isinstance(kostyan_prof.get("casino"), dict) and kostyan_prof["casino"].get("deposit", 0) > 0
record("Issue #9: Player kostyan deposit restored and retrievable", "PASS" if has_deposit else "FAIL", f"Deposit: {kostyan_prof.get('casino', {}).get('deposit')}")
except Exception as e:
record("Issue #9: Player kostyan deposit restored and retrievable", "FAIL", str(e))
# Issue #10: Seed selection top menu and sticky HUD
css_path = os.path.join(GAME_DIR, "css/style.css")
with open(css_path, "r", encoding="utf-8") as f:
css_code = f.read()
record("Issue #10: style.css has sticky seed deck selection bar", "PASS" if ".seed-deck-bar" in css_code and "position: sticky" in css_code else "FAIL")
economy_path = os.path.join(GAME_DIR, "js/systems/economy.js")
with open(economy_path, "r", encoding="utf-8") as f:
economy_code = f.read()
record("Issue #10: economy.js guaranteed fallback for cards array in draw()", "PASS" if "this.setDeckFromSelection()" in economy_code else "FAIL")
record("Issue #10: game.js hides pre-battle modals on startLevel", "PASS" if "seed-select-modal" in game_code and "dave-modal" in game_code 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_qol_controls_and_backup()
test_visual_polish_dave_dialogues_and_stats()
test_admin_panel_and_configurator()
test_http_endpoint()
test_v2_7_features()
test_gitea_critical_bugfixes()
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()