feat: Initial commit Plants vs Zombies Web Edition v2.4

- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels)
- Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss
- Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements
- Backend: REST API + SQLite Accounts & Save Synchronization
- Infrastructure: Nginx HTTPS & Docker deployment
This commit is contained in:
2026-09-17 09:30:52 +00:00
commit 6457a8904e
50 changed files with 14486 additions and 0 deletions
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""
SQLite Database Manager for Plants vs. Zombies Game (ai.qpid.pro)
Zero external dependencies. Manages users, game progress profiles, leaderboards, and bug reports.
"""
import sqlite3
import json
import os
from datetime import datetime
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")
DB_PATH = os.path.join(DATA_DIR, "pvz.db")
os.makedirs(DATA_DIR, exist_ok=True)
def get_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL COLLATE NOCASE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS profiles (
user_id INTEGER PRIMARY KEY,
coins INTEGER DEFAULT 150,
level INTEGER DEFAULT 1,
max_unlocked_level INTEGER DEFAULT 1,
high_score INTEGER DEFAULT 0,
theme TEXT DEFAULT 'classic',
upgrades TEXT DEFAULT '{}',
materials TEXT DEFAULT '{}',
plant_levels TEXT DEFAULT '{}',
perks TEXT DEFAULT '{}',
zen_garden TEXT DEFAULT '{}',
tree_of_wisdom TEXT DEFAULT '{}',
achievements TEXT DEFAULT '{}',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS leaderboards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
player_name TEXT NOT NULL,
mode TEXT NOT NULL,
score INTEGER NOT NULL,
detail TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS bugs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author TEXT NOT NULL,
category TEXT NOT NULL,
text TEXT NOT NULL,
status TEXT DEFAULT 'новый',
timestamp TEXT NOT NULL,
admin_reply TEXT
)
""")
conn.commit()
conn.close()
def get_default_profile_dict():
return {
"coins": 150,
"level": 1,
"maxUnlockedLevel": 1,
"highScore": 0,
"theme": "classic",
"upgrades": {"extraSlot": False, "hasRake": False},
"materials": {"bioHumus": 10, "scrapMetal": 5, "sunEssence": 20, "crystals": 0},
"plantLevels": {"peashooter": 1, "sunflower": 1, "wallnut": 1, "potatomine": 1, "repeater": 1, "snowpea": 1, "chomper": 1},
"perks": {"magnetism": 0, "startSun": 0, "fastCool": 0, "hardyRoots": 0, "luckyDrop": 0},
"zenGarden": {
"pots": [
{"id": 1, "plantType": "sunflower", "stage": 2, "watered": True, "fertilized": True, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 2, "plantType": "peashooter", "stage": 1, "watered": True, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 3, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 4, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 5, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 6, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 7, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0},
{"id": 8, "plantType": None, "stage": 0, "watered": False, "fertilized": False, "needWaterTime": 0, "matureTime": 0, "dropsCollected": 0}
],
"fertilizerCount": 5,
"chocolateCount": 2,
"snail": {"name": "Стинки", "active": False, "speed": 60, "boostTimer": 0, "x": 50, "direction": 1},
"lastVisit": int(datetime.now().timestamp() * 1000)
},
"treeOfWisdom": {"height": 0, "cheats": {}},
"achievements": {}
}
def get_user_by_username(username):
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ? COLLATE NOCASE", (username.strip(),))
user = cursor.fetchone()
conn.close()
return user
def register_user(username, password_hash, initial_data=None):
username = username.strip()
conn = get_connection()
cursor = conn.cursor()
try:
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"INSERT INTO users (username, password_hash, created_at, last_login) VALUES (?, ?, ?, ?)",
(username, password_hash, now_str, now_str)
)
user_id = cursor.lastrowid
profile_data = get_default_profile_dict()
if initial_data and isinstance(initial_data, dict):
for k, v in initial_data.items():
if v is not None:
profile_data[k] = v
cursor.execute("""
INSERT INTO profiles (
user_id, coins, level, max_unlocked_level, high_score, theme,
upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id,
profile_data.get("coins", 150),
profile_data.get("level", 1),
profile_data.get("maxUnlockedLevel", 1),
profile_data.get("highScore", 0),
profile_data.get("theme", "classic"),
json.dumps(profile_data.get("upgrades", {})),
json.dumps(profile_data.get("materials", {})),
json.dumps(profile_data.get("plantLevels", {})),
json.dumps(profile_data.get("perks", {})),
json.dumps(profile_data.get("zenGarden", {})),
json.dumps(profile_data.get("treeOfWisdom", {})),
json.dumps(profile_data.get("achievements", {})),
now_str
))
conn.commit()
conn.close()
return {"success": True, "user_id": user_id, "username": username, "data": profile_data}
except sqlite3.IntegrityError:
conn.close()
return {"success": False, "message": "Пользователь с таким именем уже существует"}
except Exception as e:
conn.close()
return {"success": False, "message": str(e)}
def authenticate_user(username, password_hash):
username = username.strip()
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ? COLLATE NOCASE", (username,))
user = cursor.fetchone()
if not user:
conn.close()
return None
if user["password_hash"] != password_hash:
conn.close()
return {"success": False, "message": "Неверный пароль"}
# Update last login
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("UPDATE users SET last_login = ? WHERE id = ?", (now_str, user["id"]))
conn.commit()
# Fetch profile
cursor.execute("SELECT * FROM profiles WHERE user_id = ?", (user["id"],))
p = cursor.fetchone()
conn.close()
if not p:
profile_dict = get_default_profile_dict()
else:
profile_dict = {
"coins": p["coins"],
"level": p["level"],
"maxUnlockedLevel": p["max_unlocked_level"],
"highScore": p["high_score"],
"theme": p["theme"],
"upgrades": json.loads(p["upgrades"] or "{}"),
"materials": json.loads(p["materials"] or "{}"),
"plantLevels": json.loads(p["plant_levels"] or "{}"),
"perks": json.loads(p["perks"] or "{}"),
"zenGarden": json.loads(p["zen_garden"] or "{}"),
"treeOfWisdom": json.loads(p["tree_of_wisdom"] or "{}"),
"achievements": json.loads(p["achievements"] or "{}")
}
return {"success": True, "user_id": user["id"], "username": user["username"], "data": profile_dict}
def save_user_profile(username, data):
username = username.strip()
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT id FROM users WHERE username = ? COLLATE NOCASE", (username,))
user = cursor.fetchone()
if not user:
conn.close()
return {"success": False, "message": "Пользователь не найден"}
user_id = user["id"]
cursor.execute("SELECT * FROM profiles WHERE user_id = ?", (user_id,))
current = cursor.fetchone()
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
coins = data.get("coins", current["coins"] if current else 150)
level = data.get("level", current["level"] if current else 1)
max_unlocked = data.get("maxUnlockedLevel", current["max_unlocked_level"] if current else 1)
high_score = data.get("highScore", current["high_score"] if current else 0)
theme = data.get("theme", current["theme"] if current else "classic")
# Merge JSON dictionaries
def merge_json(key, default):
cur_obj = json.loads(current[key] or "{}") if current and current[key] else default
new_val = data.get(key)
if new_val is not None and isinstance(new_val, dict):
cur_obj.update(new_val)
return json.dumps(cur_obj)
upgrades_json = merge_json("upgrades", {})
materials_json = merge_json("materials", {})
plant_levels_json = merge_json("plant_levels", {}) if "plant_levels" in data else (json.dumps(data["plantLevels"]) if "plantLevels" in data else (current["plant_levels"] if current else "{}"))
perks_json = merge_json("perks", {})
zen_garden_json = json.dumps(data["zenGarden"]) if "zenGarden" in data else (current["zen_garden"] if current else "{}")
tree_json = json.dumps(data["treeOfWisdom"]) if "treeOfWisdom" in data else (current["tree_of_wisdom"] if current else "{}")
achievements_json = json.dumps(data["achievements"]) if "achievements" in data else (current["achievements"] if current else "{}")
if current:
cursor.execute("""
UPDATE profiles SET
coins = ?, level = ?, max_unlocked_level = ?, high_score = ?, theme = ?,
upgrades = ?, materials = ?, plant_levels = ?, perks = ?,
zen_garden = ?, tree_of_wisdom = ?, achievements = ?, updated_at = ?
WHERE user_id = ?
""", (
coins, level, max_unlocked, high_score, theme,
upgrades_json, materials_json, plant_levels_json, perks_json,
zen_garden_json, tree_json, achievements_json, now_str,
user_id
))
else:
cursor.execute("""
INSERT INTO profiles (
user_id, coins, level, max_unlocked_level, high_score, theme,
upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id, coins, level, max_unlocked, high_score, theme,
upgrades_json, materials_json, plant_levels_json, perks_json,
zen_garden_json, tree_json, achievements_json, now_str
))
conn.commit()
conn.close()
return {"success": True}
def list_all_users():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT
u.id, u.username, u.created_at, u.last_login,
p.coins, p.level, p.max_unlocked_level, p.high_score, p.theme,
p.materials, p.plant_levels, p.perks, p.upgrades, p.zen_garden, p.tree_of_wisdom, p.achievements
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
ORDER BY u.id ASC
""")
rows = cursor.fetchall()
conn.close()
result = []
for r in rows:
result.append({
"id": r["id"],
"username": r["username"],
"createdAt": r["created_at"],
"lastLogin": r["last_login"],
"coins": r["coins"] if r["coins"] is not None else 150,
"level": r["level"] if r["level"] is not None else 1,
"maxUnlockedLevel": r["max_unlocked_level"] if r["max_unlocked_level"] is not None else 1,
"highScore": r["high_score"] if r["high_score"] is not None else 0,
"theme": r["theme"] or "classic",
"materials": json.loads(r["materials"] or "{}"),
"plantLevels": json.loads(r["plant_levels"] or "{}"),
"perks": json.loads(r["perks"] or "{}"),
"upgrades": json.loads(r["upgrades"] or "{}"),
"zenGarden": json.loads(r["zen_garden"] or "{}"),
"treeOfWisdom": json.loads(r["tree_of_wisdom"] or "{}"),
"achievements": json.loads(r["achievements"] or "{}")
})
return result
if __name__ == "__main__":
init_db()
users = list_all_users()
print(f"Database initialized. Total registered users: {len(users)}")
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""
Backend Server for Plants vs. Zombies (ai.qpid.pro)
Zero external dependencies.
Manages:
- SQLite User Accounts & Game Progression Profiles (/api/auth/login, /api/auth/save, /api/users)
- Community Bug Reports & Feedback (/api/bugs)
"""
import http.server
import json
import os
import sys
from datetime import datetime
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
import db
PORT = 8001
REPORTS_DIR = os.path.join(BASE_DIR, "reports")
BUGS_JSON = os.path.join(REPORTS_DIR, "bugs.json")
BUGS_MD = os.path.join(BASE_DIR, "BUG_REPORTS.md")
os.makedirs(REPORTS_DIR, exist_ok=True)
db.init_db()
DEFAULT_BUGS = [
{
"id": 1,
"author": "Player_Kopri",
"category": "bug",
"text": "Зомби после поедания растения не шел дальше, а оставался стоять на месте.",
"status": "исправлено",
"timestamp": "2026-09-16 09:30",
"adminReply": "Исправлено в v1.6: сброшен флаг isEating и таймер поедания."
},
{
"id": 2,
"author": "DaveFan",
"category": "bug",
"text": "Режим 'Я, Зомби' не работал: сразу после старта выводилось сообщение 'победа'.",
"status": "исправлено",
"timestamp": "2026-09-16 09:45",
"adminReply": "Исправлено в v1.6: устранено состояние гонки при инициализации газона."
},
{
"id": 3,
"author": "ZombossHater",
"category": "visual",
"text": "Элементы главного меню и кнопки вываливаются за границы экрана на небольших дисплеях.",
"status": "исправлено",
"timestamp": "2026-09-16 10:50",
"adminReply": "Исправлено в v2.2: адаптивная панель навигации, компактные карточки и вертикальный скролл."
}
]
def load_bugs():
if not os.path.exists(BUGS_JSON):
save_bugs(DEFAULT_BUGS)
return DEFAULT_BUGS
try:
with open(BUGS_JSON, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return DEFAULT_BUGS
def save_bugs(bugs):
with open(BUGS_JSON, "w", encoding="utf-8") as f:
json.dump(bugs, f, ensure_ascii=False, indent=2)
sync_markdown(bugs)
def sync_markdown(bugs):
lines = [
"# Журнал баг-репортов и обратной связи от игроков",
"",
"> Данный файл обновляется автоматически при поступлении новых баг-репортов из игры.",
"> При каждом обновлении и новой итерации разработки ассистент анализирует этот список.",
"",
f"**Всего репортов:** {len(bugs)} | **Последнее обновление:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
"| ID | Дата | Автор | Категория | Статус | Описание | Ответ / Фикс |",
"|:---:|:---:|:---|:---:|:---:|:---|:---|"
]
for b in reversed(bugs):
bid = b.get("id", 0)
ts = b.get("timestamp", "")
author = b.get("author", "Аноним").replace("|", "/")
cat = b.get("category", "bug")
cat_icon = "🔴 Баг" if cat == "bug" else ("💡 Идея" if cat == "feature" else ("🎨 UI" if cat == "visual" else "⚖️ Баланс"))
st = b.get("status", "новый")
st_badge = "✅ Исправлено" if st == "исправлено" else ("🟡 В работе" if st == "в работе" else "🔴 Новый")
text = b.get("text", "").replace("|", "/").replace("\n", " ")
reply = (b.get("adminReply") or "—").replace("|", "/").replace("\n", " ")
lines.append(f"| {bid} | {ts} | {author} | {cat_icon} | {st_badge} | {text} | {reply} |")
lines.append("")
with open(BUGS_MD, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
class FeedbackHandler(http.server.BaseHTTPRequestHandler):
def _set_cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def do_OPTIONS(self):
self.send_response(204)
self._set_cors()
self.end_headers()
def do_GET(self):
path = self.path.split("?")[0]
# 1. Health check
if path == "/health":
self.send_response(200)
self._set_cors()
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"OK")
return
# 2. List Registered Users in Database
if path in ["/api/users", "/api/bugs/users", "/api/auth/users"]:
users = db.list_all_users()
data = json.dumps({"success": True, "users": users}, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
# 3. Bug Reports
if path.startswith("/api/bugs") or path.startswith("/bugs"):
bugs = load_bugs()
data = json.dumps(bugs, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
self.send_response(404)
self._set_cors()
self.end_headers()
def do_POST(self):
path = self.path.split("?")[0]
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8") if length > 0 else "{}"
try:
payload = json.loads(body) if body else {}
except Exception as e:
self.send_response(400)
self._set_cors()
self.end_headers()
self.wfile.write(json.dumps({"error": f"Invalid JSON: {e}"}).encode("utf-8"))
return
# 1. User Authentication (Login & Auto-Registration)
if path in ["/api/auth/login", "/api/bugs/auth/login", "/api/auth/register"]:
username = str(payload.get("username", "")).strip()
password_hash = str(payload.get("passwordHash", "")).strip()
initial_data = payload.get("initialData")
if not username or not password_hash:
self.send_response(400)
self._set_cors()
self.end_headers()
self.wfile.write(json.dumps({"success": False, "message": "Имя пользователя и пароль обязательны"}).encode("utf-8"))
return
# Check if user exists
user = db.get_user_by_username(username)
if user:
res = db.authenticate_user(username, password_hash)
if not res or not res.get("success"):
self.send_response(401)
self._set_cors()
self.end_headers()
self.wfile.write(json.dumps(res or {"success": False, "message": "Неверный пароль"}).encode("utf-8"))
return
resp_json = res
else:
# Auto-register new user
res = db.register_user(username, password_hash, initial_data)
resp_json = res
data = json.dumps(resp_json, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
# 2. User Progress Profile Save & Sync
if path in ["/api/auth/save", "/api/bugs/auth/save", "/api/auth/sync"]:
username = str(payload.get("username", "")).strip()
user_data = payload.get("data", {})
if not username or not user_data:
self.send_response(400)
self._set_cors()
self.end_headers()
self.wfile.write(json.dumps({"success": False, "message": "Username and data are required"}).encode("utf-8"))
return
res = db.save_user_profile(username, user_data)
data = json.dumps(res, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
# 3. Bug Report Submission
if path.startswith("/api/bugs") or path.startswith("/bugs"):
text = str(payload.get("text", "")).strip()
if not text:
self.send_response(400)
self._set_cors()
self.end_headers()
self.wfile.write(json.dumps({"error": "Empty report text"}).encode("utf-8"))
return
author = str(payload.get("author", "Гость")).strip() or "Гость"
category = str(payload.get("category", "bug")).strip()
if category not in ["bug", "feature", "visual", "balance"]:
category = "bug"
bugs = load_bugs()
new_id = max([b.get("id", 0) for b in bugs] + [0]) + 1
new_bug = {
"id": new_id,
"author": author[:30],
"category": category,
"text": text[:1000],
"status": "новый",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"adminReply": None
}
bugs.append(new_bug)
save_bugs(bugs)
resp_data = json.dumps({"success": True, "report": new_bug, "total": len(bugs)}, ensure_ascii=False).encode("utf-8")
self.send_response(201)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(resp_data)))
self.end_headers()
self.wfile.write(resp_data)
return
self.send_response(404)
self._set_cors()
self.end_headers()
def log_message(self, format, *args):
sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] {self.address_string()} - {format%args}\n")
if __name__ == "__main__":
load_bugs()
server = http.server.HTTPServer(("0.0.0.0", PORT), FeedbackHandler)
print(f"PvZ Backend Server running on port {PORT} with SQLite database...")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server.")
server.server_close()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
import os
import zlib
import struct
import math
def make_png(w, h, get_pixel):
def chunk(tag, data):
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data) & 0xffffffff)
ihdr = struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0) # RGBA 8-bit
raw_rows = []
for y in range(h):
row = [0] # filter type 0
for x in range(w):
r, g, b, a = get_pixel(x, y, w, h)
row.extend([r, g, b, a])
raw_rows.append(bytes(row))
raw_data = b''.join(raw_rows)
idat = zlib.compress(raw_data, 9)
return b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', ihdr) + chunk(b'IDAT', idat) + chunk(b'IEND', b'')
def render_pvz_icon(x, y, w, h):
# Normalized coords (-1 to 1)
nx = (x - w / 2) / (w / 2)
ny = (y - h / 2) / (h / 2)
dist = math.hypot(nx, ny)
# Outer dark green circle border
if dist > 0.96:
return 0, 0, 0, 0
if dist > 0.90:
return 27, 94, 32, 255
# Background circle (lawn green gradient)
if dist <= 0.90:
# Sunflower petals or Peashooter head
# Let's draw stylized bright yellow sunflower with green peashooter center!
angle = math.atan2(ny, nx)
petal_r = 0.55 + 0.15 * math.sin(angle * 8)
# Petal check
if dist < petal_r and dist > 0.40:
return 255, 213, 79, 255 # Yellow sunflower petals
# Peashooter green head in center
center_dist = math.hypot(nx + 0.08, ny)
if center_dist < 0.35:
# Snout pointing right
snout = (nx > 0.05 and nx < 0.35 and abs(ny) < 0.12)
if snout:
return 76, 175, 80, 255
# Eye
eye_dist = math.hypot(nx - (-0.05), ny - (-0.12))
if eye_dist < 0.06:
return 255, 255, 255, 255
if eye_dist < 0.03:
return 0, 0, 0, 255
return 67, 160, 71, 255
# Green lawn background inside circle
grad = int(46 + (1.0 - dist) * 60)
return 27, min(255, grad + 80), 32, 255
def main():
game_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "game")
# Generate 192x192
p192 = make_png(192, 192, render_pvz_icon)
with open(os.path.join(game_dir, "icon-192.png"), "wb") as f:
f.write(p192)
print("Generated icon-192.png:", len(p192), "bytes")
# Generate 512x512
p512 = make_png(512, 512, render_pvz_icon)
with open(os.path.join(game_dir, "icon-512.png"), "wb") as f:
f.write(p512)
print("Generated icon-512.png:", len(p512), "bytes")
# Generate favicon.ico (valid 32x32 PNG inside or 32x32 favicon)
p32 = make_png(32, 32, render_pvz_icon)
with open(os.path.join(game_dir, "favicon.ico"), "wb") as f:
f.write(p32)
print("Generated favicon.ico:", len(p32), "bytes")
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
CLI Tool to list, inspect, and manage registered users in Plants vs. Zombies database.
Usage:
python3 scripts/manage_users.py # List all registered users in a formatted table
python3 scripts/manage_users.py <user> # Show detailed full JSON profile for a specific user
"""
import sys
import json
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
import db
def print_user_table(users):
if not users:
print("\nВ базе данных пока нет зарегистрированных пользователей.")
print("Чтобы зарегистрироваться, откройте игру на https://ai.qpid.pro/ и нажмите кнопку «Войти».\n")
return
print("\n" + "=" * 90)
print(f"{'ID':<4} | {'Имя пользователя':<18} | {'Монеты':<8} | {'Уровень':<8} | {'Макс Ур':<8} | {'Рекорд':<8} | {'Создан':<19}")
print("-" * 90)
for u in users:
print(f"{u['id']:<4} | {u['username']:<18} | {u['coins']:<8} | {u['level']:<8} | {u['maxUnlockedLevel']:<8} | {u['highScore']:<8} | {u['createdAt']:<19}")
print("=" * 90)
print(f"Всего зарегистрировано пользователей: {len(users)}\n")
def print_user_detail(username):
users = db.list_all_users()
target = next((u for u in users if u["username"].lower() == username.lower()), None)
if not target:
print(f"\nПользователь '{username}' не найден в базе данных.\n")
return
print("\n" + "=" * 60)
print(f" Профиль пользователя: {target['username']} (ID: {target['id']})")
print("=" * 60)
print(f" Дата регистрации: {target['createdAt']}")
print(f" Последний вход: {target['lastLogin']}")
print(f" Текущий уровень: {target['level']}")
print(f" Макс. открытый: {target['maxUnlockedLevel']}")
print(f" Всего монет: {target['coins']}")
print(f" Рекорд очков: {target['highScore']}")
print(f" Тема оформления: {target['theme']}")
print("-" * 60)
print(" Материалы мастерской:")
for mat, count in target['materials'].items():
print(f" - {mat}: {count}")
print("-" * 60)
print(" Уровни растений:")
for plant, lvl in target['plantLevels'].items():
print(f" - {plant}: Уровень {lvl}")
print("-" * 60)
print(" Перки:")
for perk, lvl in target['perks'].items():
print(f" - {perk}: Уровень {lvl}")
print("-" * 60)
print(" Дерево Мудрости:")
print(f" - Высота: {target['treeOfWisdom'].get('height', 0)} футов")
print("=" * 60 + "\n")
if __name__ == "__main__":
db.init_db()
if len(sys.argv) > 1:
print_user_detail(sys.argv[1])
else:
users = db.list_all_users()
print_user_table(users)
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
echo "[$(date)] Checking and renewing SSL certificates..."
docker compose run --rm certbot renew --webroot --webroot-path=/var/www/certbot
docker compose exec nginx nginx -s reload || docker compose restart nginx
echo "[$(date)] Certificate check finished."
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
set -e
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
DOMAIN="ai.qpid.pro"
EMAIL="admin@qpid.pro"
echo "=== [1/5] Проверка и создание директорий Certbot ==="
mkdir -p "$PROJECT_DIR/certbot/conf" "$PROJECT_DIR/certbot/www"
chmod -R 755 "$PROJECT_DIR/certbot/www"
echo "=== [2/5] Проверка существующих SSL сертификатов ==="
CERT_PATH="$PROJECT_DIR/certbot/conf/live/$DOMAIN/fullchain.pem"
if [ -f "$CERT_PATH" ]; then
echo "Сертификат для $DOMAIN уже существует."
cp "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf.ssl" "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf"
docker compose up -d --build
echo "Nginx и Certbot запущены с существующим SSL."
else
echo "Сертификат не найден. Запуск временного HTTP-сервера для верификации ACME..."
cp "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf.http" "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf"
# Сборка и запуск nginx в HTTP-режиме
docker compose up -d --build nginx
echo "Ожидание готовности Nginx на порту 80..."
sleep 3
echo "=== [3/5] Запрос SSL-сертификата через Certbot ==="
SUCCESS=0
for ATTEMPT in 1 2 3; do
echo "Попытка $ATTEMPT получения сертификата через Certbot..."
if docker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
-d "$DOMAIN" \
--email "$EMAIL" \
--agree-tos \
--no-eff-email \
--non-interactive; then
SUCCESS=1
echo "Сертификат успешно получен!"
break
else
echo "Попытка $ATTEMPT не удалась. Пауза 5 секунд..."
sleep 5
fi
done
if [ "$SUCCESS" -ne 1 ]; then
echo "❌ Ошибка: Не удалось получить SSL-сертификат для $DOMAIN."
echo "Проверьте, что DNS A-запись для $DOMAIN указывает на публичный IP этого сервера."
exit 1
fi
echo "=== [4/5] Применение полной SSL-конфигурации Nginx ==="
cp "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf.ssl" "$PROJECT_DIR/nginx/conf.d/ai.qpid.pro.conf"
docker compose restart nginx
fi
echo "=== [5/5] Настройка регулярного обновления сертификатов ==="
chmod +x "$PROJECT_DIR/scripts/renew-certs.sh"
# Добавление задачи обновления в cron (если еще не добавлена)
CRON_JOB="0 3 * * * $PROJECT_DIR/scripts/renew-certs.sh >> /var/log/certbot-renew.log 2>&1"
(crontab -l 2>/dev/null | grep -Fv "$PROJECT_DIR/scripts/renew-certs.sh" ; echo "$CRON_JOB") | crontab - || true
echo "=== Развертывание и настройка SSL успешно завершены! ==="
+738
View File
@@ -0,0 +1,738 @@
#!/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",
"loginAsGuest",
"logout",
"getUserData",
"saveUserData",
"flushSave",
"scheduleSave",
"apiPost",
]
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")
# 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 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 rules
layout_checks = [
("Top nav bar flex-wrap enabled for zero overflow", "flex-wrap: wrap" 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),
]
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()