"""Bug pattern checklist for automated game code review.
Each pattern defines a common SimVX bug with a regex, severity, and suggested fix.
Used by the /playtest and /playtest-team commands for static code analysis.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
log = logging.getLogger(__name__)
__all__ = ["BugPattern", "PATTERNS"]
[docs]
@dataclass(frozen=True)
class BugPattern:
"""A detectable bug pattern in SimVX game code."""
name: str
category: str # "input", "lifecycle", "ui", "state"
severity: str # "high", "medium", "low"
description: str
regex: str # Python regex to search source files
fix: str
PATTERNS: list[BugPattern] = [
# --- Input Handling ---
BugPattern(
name="hover-vs-click",
category="input",
severity="high",
description=(
"is_mouse_button_pressed() in process() fires every frame instead of once."
" Use is_mouse_button_just_pressed()."
),
regex=r"def process\b.*\n(?:.*\n)*?.*is_mouse_button_pressed\(",
fix="Replace is_mouse_button_pressed() with is_mouse_button_just_pressed() for one-shot actions.",
),
BugPattern(
name="missing-just-pressed",
category="input",
severity="high",
description="Menu/toggle actions using continuous-press instead of edge trigger.",
regex=r"is_action_pressed\(.*(?:menu|toggle|pause|start|select|confirm|submit|restart)",
fix="Use is_action_just_pressed() for actions that should fire once per press.",
),
BugPattern(
name="unregistered-action",
category="input",
severity="high",
description="is_action_pressed/just_pressed used without a matching InputMap.add_action().",
regex=r'is_action_(?:just_)?pressed\(["\'](\w+)',
fix="Add InputMap.add_action() for every action name before querying it.",
),
BugPattern(
name="raw-key-int",
category="input",
severity="low",
description="Passing int literals instead of Key/MouseButton enums.",
regex=r"(?:is_key_|is_mouse_button_|PressKey\()(?:just_)?pressed\(\d+\)",
fix="Use Key.SPACE, MouseButton.LEFT, etc. instead of raw integers.",
),
# --- Node Lifecycle ---
BugPattern(
name="missing-ready",
category="lifecycle",
severity="medium",
description="Node has process() but accesses attributes that should be initialised in ready().",
regex=r"def process\(self.*\n(?:.*\n)*?.*self\.(?!_)(\w+).*(?:AttributeError|has no attribute)",
fix="Initialise instance attributes in ready() or __init__(), not first-access in process().",
),
BugPattern(
name="children-before-tree",
category="lifecycle",
severity="medium",
description="Accessing get_child()/find() in __init__ before the node enters the tree.",
regex=r"def __init__\(self.*\n(?:.*\n)*?.*(?:get_child|\.find\(|\.find_all\()",
fix="Move child/tree queries to ready() which runs after the node enters the scene tree.",
),
BugPattern(
name="no-super-ready",
category="lifecycle",
severity="medium",
description="Overriding ready() without calling super().ready() in a subclass.",
regex=r"def ready\(self\):\s*\n(?:(?!super\(\)\.ready)(?:.*\n))*?(?=\n def |\nclass |\Z)",
fix="Call super().ready() at the start of ready() to ensure parent initialisation runs.",
),
BugPattern(
name="signal-in-process",
category="lifecycle",
severity="high",
description=".connect() called in process() accumulates duplicate connections every frame.",
regex=r"def process\(self.*\n(?:.*\n)*?.*\.connect\(",
fix="Move .connect() calls to ready() so they run exactly once.",
),
# --- UI Integration ---
BugPattern(
name="widget-no-size",
category="ui",
severity="high",
description="Control created without explicit size and no layout parent — rect is (0,0,0,0), never clickable.",
regex=r"(?:Button|Label|Panel|TextEdit|LineEdit)\((?!.*(?:rect|size|width|height|min_size))",
fix="Set explicit rect/size or place inside a layout container (VBox, HBox, etc.).",
),
BugPattern(
name="no-camera",
category="ui",
severity="high",
description="3D scene with MeshInstance3D but no Camera3D — nothing will be visible.",
regex=r"MeshInstance3D\(",
fix="Add a Camera3D to the scene. Without it, the 3D renderer has no viewpoint.",
),
BugPattern(
name="display-not-updated",
category="ui",
severity="medium",
description="Game state changes but corresponding Label.text / Text2D.text is never updated.",
regex=r"self\.\w+\s*[+\-*/]?=\s*(?!.*\.text\s*=)",
fix="Update display text whenever the backing property changes (or use Property with a setter).",
),
# --- State Management ---
BugPattern(
name="state-not-reset",
category="state",
severity="medium",
description="Restart/new-game function doesn't clear all game state variables.",
regex=r"def (?:restart|reset|new_game)\(self\).*\n(?:.*\n)*?(?=\n def |\nclass |\Z)",
fix="Ensure all game state (score, lives, timers, collections) is reset on restart.",
),
BugPattern(
name="collision-layer-mismatch",
category="state",
severity="medium",
description="Objects that should collide are on different collision layers.",
regex=r"collision_layer\s*=\s*(\d+)",
fix="Verify collision_layer and collision_mask overlap for objects that should interact.",
),
]