Source code for simvx.graphics.playtest_patterns

"""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.", ), ]