- Introduced a new JSON file containing non-regression test states with detailed unit information, including positions, ages, and movement directions across multiple frames. - Added a shell script for Bluetooth diagnostics that checks system information, Bluetooth binaries, running processes, D-Bus status, Bluetooth controller details, and audio stack status, providing a comprehensive overview for troubleshooting.master
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 129 KiB |
@ -0,0 +1,73 @@
|
||||
# Game constants and configuration |
||||
|
||||
LEVEL_MUSIC_CONFIG = "level_music" |
||||
START_MENU_MUSIC = "High_Score_Garden.mp3" |
||||
RUN_COMPLETE_MUSIC = "Sunset_At_Pixel_Gardens.mp3" |
||||
START_MENU_ANIMATION = "anim/start_mice.gif" |
||||
SUPPORTED_MUSIC_EXTENSIONS = {".mp3", ".ogg", ".wav"} |
||||
BASE_INITIAL_RATS = 5 |
||||
DEFAULT_DIFFICULTY = "easy" |
||||
DIFFICULTY_ALIASES = { |
||||
"medium": "normal", |
||||
"normale": "normal", |
||||
} |
||||
START_MENU_AUDIO_OPTIONS = ( |
||||
("sound_volume", "Suono"), |
||||
("music_volume", "Musica"), |
||||
) |
||||
VOLUME_STEP = 5 |
||||
|
||||
GAME_END_LEVEL_CLEAR = "level_clear" |
||||
GAME_END_DEFEAT = "defeat" |
||||
GAME_END_RUN_COMPLETE = "run_complete" |
||||
|
||||
DIFFICULTY_OPTIONS = ( |
||||
{ |
||||
"key": "easy", |
||||
"label": "Easy", |
||||
"starting_rats_multiplier": 1, |
||||
"speed_multiplier": 1.0, |
||||
"fill": (235, 246, 234), |
||||
"accent": (88, 148, 82), |
||||
}, |
||||
{ |
||||
"key": "normal", |
||||
"label": "Normal", |
||||
"starting_rats_multiplier": 2, |
||||
"speed_multiplier": 1.5, |
||||
"fill": (252, 242, 223), |
||||
"accent": (204, 146, 44), |
||||
}, |
||||
{ |
||||
"key": "hard", |
||||
"label": "Hard", |
||||
"starting_rats_multiplier": 3, |
||||
"speed_multiplier": 2.0, |
||||
"fill": (251, 229, 229), |
||||
"accent": (188, 68, 68), |
||||
}, |
||||
) |
||||
|
||||
DIFFICULTY_OPTIONS_BY_KEY = {option["key"]: option for option in DIFFICULTY_OPTIONS} |
||||
|
||||
START_MENU_COLORS = { |
||||
"panel_fill": (255, 255, 255), |
||||
"panel_border": (52, 52, 52), |
||||
"header_fill": (255, 255, 255), |
||||
"text": (24, 24, 24), |
||||
"muted": (82, 82, 82), |
||||
"hint_fill": (255, 255, 255), |
||||
"track_fill": (212, 215, 216), |
||||
"card_fill": (255, 255, 255), |
||||
} |
||||
|
||||
START_MENU_AUDIO_STYLES = { |
||||
"sound_volume": { |
||||
"accent": (214, 146, 62), |
||||
"fill": (251, 241, 225), |
||||
}, |
||||
"music_volume": { |
||||
"accent": (91, 122, 208), |
||||
"fill": (230, 235, 248), |
||||
}, |
||||
} |
||||
@ -0,0 +1,42 @@
|
||||
from enum import Enum, auto |
||||
|
||||
class GameState(Enum): |
||||
START_MENU = auto() |
||||
PLAYING = auto() |
||||
PAUSED = auto() |
||||
GAME_OVER = auto() |
||||
VICTORY = auto() |
||||
|
||||
class StateMachine: |
||||
def __init__(self, game): |
||||
self.game = game |
||||
self.current_state = GameState.START_MENU |
||||
|
||||
def transition_to(self, new_state): |
||||
print(f"[state] Transitioning from {self.current_state} to {new_state}") |
||||
self.current_state = new_state |
||||
|
||||
# Sincronizzazione per compatibilità con KeyBindings e logica esistente |
||||
if new_state == GameState.PLAYING: |
||||
self.game.game_status = "game" |
||||
self.game.menu_screen = None |
||||
elif new_state == GameState.PAUSED: |
||||
self.game.game_status = "paused" |
||||
self.game.menu_screen = None |
||||
elif new_state == GameState.START_MENU: |
||||
self.game.game_status = "start_menu" |
||||
self.game.menu_screen = "start" |
||||
elif new_state in [GameState.GAME_OVER, GameState.VICTORY]: |
||||
self.game.game_status = "paused" # Legacy status used for key handling in end screens |
||||
self.game.menu_screen = None |
||||
|
||||
def update(self): |
||||
# Dispatch alla logica originale ripristinata in MiceMaze/Graphics |
||||
if self.current_state == GameState.START_MENU: |
||||
self.game.graphics.render_start_menu() |
||||
elif self.current_state == GameState.PAUSED: |
||||
self.game.graphics.render_pause_menu() |
||||
elif self.current_state in [GameState.GAME_OVER, GameState.VICTORY]: |
||||
# Delega al metodo game_over originale che gestisce i dialoghi specifici |
||||
self.game.game_over() |
||||
|
||||
@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash |
||||
# Deploy Mice! to a Koriki CFW device over SSH. |
||||
# |
||||
# Requirements on host: |
||||
# sshpass, tar, ssh |
||||
# |
||||
# Target: koriki@<IP> (default 10.0.0.199) |
||||
# - ARMv7l Linux, glibc 2.28 |
||||
# - Python 3.11 archive already present at /mnt/SDCARD/python_armv7.tar.gz |
||||
# - SDL2 libs in /mnt/SDCARD/Koriki/lib/ |
||||
# - Internet access via WiFi |
||||
# |
||||
# Usage: |
||||
# ./packaging/deploy_koriki.sh [TARGET_IP] |
||||
|
||||
set -euo pipefail |
||||
|
||||
TARGET_IP="${1:-10.0.0.199}" |
||||
TARGET_USER="${TARGET_USER:-koriki}" |
||||
TARGET_PASS="${TARGET_PASS:-koriki}" |
||||
SDCARD="/mnt/SDCARD" |
||||
PYTHON_ARCHIVE="${SDCARD}/python_armv7.tar.gz" |
||||
PYTHON_DIR="${SDCARD}/python" |
||||
GAME_DIR="${SDCARD}/Ports/mice" |
||||
KORIKI_LIB="${SDCARD}/Koriki/lib" |
||||
VENDOR_DIR="${GAME_DIR}/vendor" |
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) |
||||
|
||||
ssh_cmd() { |
||||
sshpass -p "$TARGET_PASS" ssh \ |
||||
-o StrictHostKeyChecking=no \ |
||||
-o ConnectTimeout=10 \ |
||||
-o PreferredAuthentications=password \ |
||||
-o PubkeyAuthentication=no \ |
||||
"${TARGET_USER}@${TARGET_IP}" "$@" |
||||
} |
||||
|
||||
log() { printf '==> %s\n' "$*"; } |
||||
|
||||
# ── 1. Estrai Python 3.11 ────────────────────────────────────────────────────── |
||||
log "Step 1: Extracting Python 3.11 on device" |
||||
ssh_cmd " |
||||
if [ ! -f '${PYTHON_DIR}/bin/python3.11' ]; then |
||||
echo 'Extracting Python archive...' |
||||
tar -xzf '${PYTHON_ARCHIVE}' -C '${SDCARD}' |
||||
echo 'Done' |
||||
else |
||||
echo 'Python 3.11 already extracted, skipping' |
||||
fi |
||||
" |
||||
|
||||
# ── 1b. Fix FAT32: i symlink non possono essere creati su vfat; li sostituiamo |
||||
# con copie reali dei file critici |
||||
log "Step 1b: Fixing missing symlinks on FAT32 (copying critical binaries)" |
||||
ssh_cmd " |
||||
PYBIN='${PYTHON_DIR}/bin' |
||||
PYLIB='${PYTHON_DIR}/lib' |
||||
# Copie necessarie all'interprete |
||||
[ ! -f \"\${PYBIN}/python3\" ] && cp \"\${PYBIN}/python3.11\" \"\${PYBIN}/python3\" |
||||
[ ! -f \"\${PYBIN}/python\" ] && cp \"\${PYBIN}/python3.11\" \"\${PYBIN}/python\" |
||||
[ ! -f \"\${PYBIN}/pip3\" ] && [ -f \"\${PYBIN}/pip3.11\" ] && cp \"\${PYBIN}/pip3.11\" \"\${PYBIN}/pip3\" |
||||
# Libreria condivisa |
||||
[ ! -f \"\${PYLIB}/libpython3.11.so\" ] && cp \"\${PYLIB}/libpython3.11.so.1.0\" \"\${PYLIB}/libpython3.11.so\" |
||||
echo 'Symlink workaround done' |
||||
" |
||||
|
||||
# ── 2. Installa dipendenze Python ────────────────────────────────────────────── |
||||
log "Step 2: Installing Python dependencies (wheel download + extract)" |
||||
ssh_cmd " |
||||
PYTHON='${PYTHON_DIR}/bin/python3.11' |
||||
export TMPDIR='${GAME_DIR}/tmp' |
||||
export LD_LIBRARY_PATH='${PYTHON_DIR}/lib:${KORIKI_LIB}' |
||||
# pip --target su FAT32 puo fallire (rename di file temporanei). |
||||
# Workaround: scarichiamo wheel in /tmp e li estraiamo manualmente in vendor. |
||||
mkdir -p '${VENDOR_DIR}' '${GAME_DIR}/tmp' '${GAME_DIR}/tmp/mice_wheels' |
||||
rm -f '${GAME_DIR}/tmp/mice_wheels'/*.whl |
||||
\"\$PYTHON\" -m pip download --no-cache-dir \ |
||||
--extra-index-url https://www.piwheels.org/simple/ \ |
||||
--dest '${GAME_DIR}/tmp/mice_wheels' \ |
||||
pysdl2 \ |
||||
'Pillow>=10.0' \ |
||||
'numpy>=1.26' \ |
||||
pyaml \ |
||||
requests |
||||
\"\$PYTHON\" - << 'PY' |
||||
import glob |
||||
import os |
||||
import zipfile |
||||
|
||||
vendor = '${VENDOR_DIR}' |
||||
wheels = sorted(glob.glob('${GAME_DIR}/tmp/mice_wheels/*.whl')) |
||||
if not wheels: |
||||
raise SystemExit('No wheels downloaded') |
||||
|
||||
for whl in wheels: |
||||
print('Extracting', os.path.basename(whl)) |
||||
with zipfile.ZipFile(whl) as zf: |
||||
zf.extractall(vendor) |
||||
|
||||
print('Dependencies extracted to', vendor) |
||||
PY |
||||
echo 'Dependencies installed (wheel extraction)' |
||||
" |
||||
|
||||
# ── 3. Crea directory di gioco ───────────────────────────────────────────────── |
||||
log "Step 3: Creating game directory ${GAME_DIR}" |
||||
ssh_cmd "mkdir -p '${GAME_DIR}'" |
||||
|
||||
# ── 4. Trasferisci il gioco ──────────────────────────────────────────────────── |
||||
# Pipe diretta tar → tar: evita di scrivere file intermedi in /tmp (tmpfs 49MB) |
||||
log "Step 4: Transferring game files via direct pipe (no tmp file)" |
||||
tar czf - \ |
||||
-C "$ROOT_DIR" \ |
||||
--exclude='.git' \ |
||||
--exclude='.venv' \ |
||||
--exclude='venv' \ |
||||
--exclude='__pycache__' \ |
||||
--exclude='*.pyc' \ |
||||
--exclude='dist' \ |
||||
--exclude='logs' \ |
||||
--exclude='packaging' \ |
||||
--exclude='tools' \ |
||||
--exclude='server' \ |
||||
--exclude='*.tar.gz' \ |
||||
. \ |
||||
| sshpass -p "$TARGET_PASS" ssh \ |
||||
-o StrictHostKeyChecking=no \ |
||||
"${TARGET_USER}@${TARGET_IP}" \ |
||||
"mkdir -p '${GAME_DIR}' && tar -xzf - -C '${GAME_DIR}' && echo 'Game extracted'" |
||||
|
||||
# ── 5. Crea il launcher ──────────────────────────────────────────────────────── |
||||
log "Step 5: Creating launcher script" |
||||
|
||||
LAUNCHER_CONTENT="#!/bin/sh |
||||
# Mice! launcher for Koriki CFW |
||||
|
||||
export PYTHON_DIR=\"${PYTHON_DIR}\" |
||||
export PATH=\"\${PYTHON_DIR}/bin:\$PATH\" |
||||
export PYTHONPATH=\"${VENDOR_DIR}:\${PYTHON_DIR}/lib/python3.11/site-packages\" |
||||
|
||||
# Puntiamo PySDL2 alle librerie SDL2 di Koriki |
||||
export PYSDL2_DLL_PATH=\"${KORIKI_LIB}\" |
||||
export LD_LIBRARY_PATH=\"${KORIKI_LIB}:\${LD_LIBRARY_PATH:-}\" |
||||
|
||||
# Root del progetto e dati persistenti |
||||
export MICE_PROJECT_ROOT=\"${GAME_DIR}\" |
||||
export MICE_DATA_DIR=\"\${SDCARD}/.mice_data\" |
||||
|
||||
mkdir -p \"\${MICE_DATA_DIR}\" |
||||
|
||||
cd \"\${MICE_PROJECT_ROOT}\" |
||||
exec \"\${PYTHON_DIR}/bin/python3\" rats.py \"\$@\" |
||||
" |
||||
|
||||
ssh_cmd "cat > '${SDCARD}/Ports/mice.sh'" <<< "$LAUNCHER_CONTENT" |
||||
ssh_cmd "chmod +x '${SDCARD}/Ports/mice.sh'" |
||||
|
||||
log "Launcher written to ${SDCARD}/Ports/mice.sh" |
||||
|
||||
# ── 6. Test rapido ───────────────────────────────────────────────────────────── |
||||
log "Step 6: Quick smoke test" |
||||
ssh_cmd " |
||||
export PATH='${PYTHON_DIR}/bin:\$PATH' |
||||
export PYSDL2_DLL_PATH='${KORIKI_LIB}' |
||||
export LD_LIBRARY_PATH='${PYTHON_DIR}/lib:${KORIKI_LIB}' |
||||
export PYTHONPATH='${VENDOR_DIR}:${PYTHON_DIR}/lib/python3.11/site-packages' |
||||
python3 -c \" |
||||
import sys |
||||
print('Python', sys.version) |
||||
import sdl2; print('PySDL2 OK:', sdl2.__version__) |
||||
import numpy; print('NumPy OK:', numpy.__version__) |
||||
import PIL; print('Pillow OK:', PIL.__version__) |
||||
import yaml; print('pyaml OK') |
||||
import requests; print('requests OK') |
||||
print('All dependencies OK') |
||||
\" |
||||
" |
||||
|
||||
log "Deployment complete!" |
||||
log "Run the game from Ports > mice on Koriki, or manually:" |
||||
log " ssh ${TARGET_USER}@${TARGET_IP} '${SDCARD}/Ports/mice.sh'" |
||||
@ -0,0 +1,88 @@
|
||||
|
||||
import os |
||||
import sys |
||||
import unittest |
||||
import random |
||||
from pathlib import Path |
||||
|
||||
# Add current directory to path |
||||
sys.path.append(os.getcwd()) |
||||
|
||||
# Set SDL to use dummy video driver for headless environments |
||||
os.environ["SDL_VIDEODRIVER"] = "dummy" |
||||
os.environ["SDL_AUDIODRIVER"] = "dummy" |
||||
os.environ["MICE_DISABLE_JOYSTICK"] = "1" |
||||
|
||||
from rats import MiceMaze |
||||
from engine.state_machine import GameState |
||||
from engine.sdl2 import GameWindow |
||||
|
||||
def mock_init_audio(self): |
||||
self.music_enabled = False |
||||
self.audio_devs = {"base": 0, "effects": 0, "music": 0} |
||||
self.sound_volume = 0 |
||||
self.music_volume = 0 |
||||
|
||||
class TestGameOverFlow(unittest.TestCase): |
||||
@classmethod |
||||
def setUpClass(cls): |
||||
GameWindow.show_intro = lambda *args, **kwargs: None |
||||
GameWindow.show_loading_screen = lambda *args, **kwargs: None |
||||
GameWindow._init_audio_system = mock_init_audio |
||||
GameWindow.play_sound = lambda *args, **kwargs: None |
||||
GameWindow.stop_sound = lambda *args, **kwargs: None |
||||
|
||||
def setUp(self): |
||||
random.seed(42) |
||||
# Load a standard level |
||||
self.game = MiceMaze("assets/Rat/level.dat", level_index=0) |
||||
self.game.game_status = "game" |
||||
self.game.state_machine.transition_to(GameState.PLAYING) |
||||
|
||||
def test_defeat_triggers_game_over_state(self): |
||||
"""Verify that having > 200 rats triggers GAME_OVER state, not PAUSED.""" |
||||
print("\nTesting defeat condition (> 200 rats)...") |
||||
|
||||
# Manually inject > 200 rats to trigger defeat |
||||
from units.rat import Male |
||||
for i in range(210): |
||||
self.game.unit_manager.spawn_unit(Male, (1, 1)) |
||||
|
||||
# Run one update cycle |
||||
self.game.update_maze() |
||||
|
||||
# Check end condition |
||||
self.assertTrue(self.game.game_end[0], "Game should be marked as ended") |
||||
self.assertEqual(self.game.game_end[1], "defeat", "End reason should be defeat") |
||||
|
||||
# CRITICAL CHECK: State must be GAME_OVER, not PAUSED |
||||
current_state = self.game.state_machine.current_state |
||||
print(f"Current State: {current_state}") |
||||
print(f"Legacy game_status: {self.game.game_status}") |
||||
|
||||
self.assertEqual(current_state, GameState.GAME_OVER, |
||||
f"Game should be in GAME_OVER state, but was in {current_state}") |
||||
|
||||
def test_victory_triggers_victory_state(self): |
||||
"""Verify that clearing all rats triggers VICTORY state.""" |
||||
print("\nTesting victory condition (0 rats)...") |
||||
|
||||
# Clear all units |
||||
self.game.units.clear() |
||||
|
||||
# Run one update cycle |
||||
self.game.update_maze() |
||||
|
||||
# Check end condition |
||||
self.assertTrue(self.game.game_end[0], "Game should be marked as ended") |
||||
self.assertEqual(self.game.game_end[1], "level_clear", "End reason should be level_clear") |
||||
|
||||
# State must be VICTORY |
||||
current_state = self.game.state_machine.current_state |
||||
print(f"Current State: {current_state}") |
||||
|
||||
self.assertEqual(current_state, GameState.VICTORY, |
||||
f"Game should be in VICTORY state, but was in {current_state}") |
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
||||
@ -0,0 +1,148 @@
|
||||
|
||||
import os |
||||
import sys |
||||
import random |
||||
import unittest |
||||
import json |
||||
import time |
||||
from pathlib import Path |
||||
from PIL import Image |
||||
|
||||
# Add current directory to path |
||||
sys.path.append(os.getcwd()) |
||||
|
||||
# Set SDL to use dummy video driver for headless environments |
||||
os.environ["SDL_VIDEODRIVER"] = "dummy" |
||||
os.environ["SDL_AUDIODRIVER"] = "dummy" |
||||
os.environ["MICE_DISABLE_JOYSTICK"] = "1" |
||||
|
||||
from rats import MiceMaze |
||||
from engine.sdl2 import GameWindow |
||||
|
||||
def mock_init_audio(self): |
||||
self.music_enabled = False |
||||
self.audio_devs = {"base": 0, "effects": 0, "music": 0} |
||||
self.sound_volume = 0 |
||||
self.music_volume = 0 |
||||
|
||||
import uuid |
||||
|
||||
# Global counter for deterministic UUIDs |
||||
_uuid_counter = 0 |
||||
def mock_uuid4(): |
||||
global _uuid_counter |
||||
val = _uuid_counter |
||||
_uuid_counter += 1 |
||||
return val |
||||
|
||||
class NonRegressionTest(unittest.TestCase): |
||||
@classmethod |
||||
def setUpClass(cls): |
||||
# Deterministic UUIDs |
||||
uuid.uuid4 = mock_uuid4 |
||||
# Monkeypatch SDL2 methods that block or show windows |
||||
GameWindow.show_intro = lambda *args, **kwargs: None |
||||
GameWindow.show_loading_screen = lambda *args, **kwargs: None |
||||
# Disable audio and its effects |
||||
GameWindow._init_audio_system = mock_init_audio |
||||
GameWindow.play_sound = lambda *args, **kwargs: None |
||||
GameWindow.stop_sound = lambda *args, **kwargs: None |
||||
|
||||
def setUp(self): |
||||
global _uuid_counter |
||||
_uuid_counter = 0 |
||||
|
||||
# Initial seed for constructor |
||||
random.seed(42) |
||||
|
||||
# Initialize game |
||||
self.game = MiceMaze("assets/Rat/level.dat", level_index=0) |
||||
|
||||
# Re-seed again to clear entropy consumed by asset loading (blood stains etc) |
||||
# This ensures the game logic starts from a consistent random state |
||||
random.seed(42) |
||||
_uuid_counter = 0 # Reset UUIDs too for initial spawns |
||||
self.game.start_game() |
||||
|
||||
# Trigger background generation once to consume those random calls |
||||
# before the simulation starts, ensuring stability. |
||||
self.game.graphics.draw_maze() |
||||
|
||||
# Override dynamic attributes |
||||
self.game.start_menu_animation_started_at = 0 |
||||
|
||||
# Skip menu and start gameplay |
||||
self.game.game_status = "game" |
||||
self.game.menu_screen = None |
||||
|
||||
def test_simulation_run(self): |
||||
steps = 200 |
||||
states = [] |
||||
|
||||
# Output directory |
||||
output_dir = Path("tests/non_regression_output") |
||||
output_dir.mkdir(parents=True, exist_ok=True) |
||||
|
||||
print(f"Starting simulation for {steps} steps...") |
||||
|
||||
for i in range(steps): |
||||
# Advance game state |
||||
self.game.update_maze() |
||||
|
||||
# Every 50 steps, record state and screenshot |
||||
if i % 50 == 0 or i == steps - 1: |
||||
state = self.dump_game_state() |
||||
state["frame"] = i |
||||
states.append(state) |
||||
|
||||
# Visual snapshot |
||||
# Note: In dummy driver, RenderReadPixels might return empty/black |
||||
# but we'll try anyway. If it fails, we rely on the state JSON. |
||||
try: |
||||
self.game.graphics.draw_maze() |
||||
# Manually draw units because we are not in mainloop |
||||
for unit in list(self.game.units.values()): |
||||
unit.draw() |
||||
|
||||
img = self.game.render_engine.capture_frame() |
||||
img_path = output_dir / f"frame_{i:04d}.png" |
||||
img.save(img_path) |
||||
except Exception as e: |
||||
print(f"Warning: Could not capture frame at step {i}: {e}") |
||||
|
||||
# Save states to JSON |
||||
states_path = output_dir / "states.json" |
||||
with open(states_path, "w") as f: |
||||
json.dump(states, f, indent=2) |
||||
|
||||
print(f"Simulation complete. Outputs saved to {output_dir}") |
||||
|
||||
def dump_game_state(self): |
||||
state = { |
||||
"points": self.game.points, |
||||
"unit_count": len(self.game.units), |
||||
"units": [] |
||||
} |
||||
# Sort units by ID to be deterministic |
||||
sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0])) |
||||
|
||||
for uid, unit in sorted_units: |
||||
unit_state = { |
||||
"id": str(uid), |
||||
"type": unit.__class__.__name__, |
||||
"pos": list(unit.position), |
||||
"pos_before": list(unit.position_before), |
||||
"partial_move": float(unit.partial_move), |
||||
"age": int(unit.age), |
||||
} |
||||
if hasattr(unit, "sex"): |
||||
unit_state["sex"] = unit.sex |
||||
if hasattr(unit, "direction"): |
||||
unit_state["direction"] = unit.direction |
||||
|
||||
state["units"].append(unit_state) |
||||
|
||||
return state |
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
||||
@ -0,0 +1,123 @@
|
||||
|
||||
import os |
||||
import sys |
||||
import random |
||||
import unittest |
||||
import json |
||||
from pathlib import Path |
||||
from PIL import Image |
||||
import numpy as np |
||||
|
||||
# Add current directory to path |
||||
sys.path.append(os.getcwd()) |
||||
|
||||
# Set SDL to use dummy video driver for headless environments |
||||
os.environ["SDL_VIDEODRIVER"] = "dummy" |
||||
os.environ["SDL_AUDIODRIVER"] = "dummy" |
||||
os.environ["MICE_DISABLE_JOYSTICK"] = "1" |
||||
|
||||
from rats import MiceMaze |
||||
from engine.sdl2 import GameWindow |
||||
|
||||
def mock_init_audio(self): |
||||
self.music_enabled = False |
||||
self.audio_devs = {"base": 0, "effects": 0, "music": 0} |
||||
self.sound_volume = 0 |
||||
self.music_volume = 0 |
||||
|
||||
import uuid |
||||
|
||||
# Global counter for deterministic UUIDs |
||||
_uuid_counter = 0 |
||||
def mock_uuid4(): |
||||
global _uuid_counter |
||||
val = _uuid_counter |
||||
_uuid_counter += 1 |
||||
return val |
||||
|
||||
class NonRegressionVerification(unittest.TestCase): |
||||
@classmethod |
||||
def setUpClass(cls): |
||||
# Deterministic UUIDs |
||||
uuid.uuid4 = mock_uuid4 |
||||
GameWindow.show_intro = lambda *args, **kwargs: None |
||||
GameWindow.show_loading_screen = lambda *args, **kwargs: None |
||||
GameWindow._init_audio_system = mock_init_audio |
||||
GameWindow.play_sound = lambda *args, **kwargs: None |
||||
GameWindow.stop_sound = lambda *args, **kwargs: None |
||||
|
||||
def setUp(self): |
||||
global _uuid_counter |
||||
_uuid_counter = 0 |
||||
random.seed(42) |
||||
self.game = MiceMaze("assets/Rat/level.dat", level_index=0) |
||||
|
||||
# Re-seed again to clear entropy consumed by asset loading |
||||
random.seed(42) |
||||
_uuid_counter = 0 |
||||
self.game.start_game() |
||||
|
||||
# Trigger background generation once to consume those random calls |
||||
# before the simulation starts, ensuring stability. |
||||
self.game.graphics.draw_maze() |
||||
|
||||
self.game.game_status = "game" |
||||
self.game.menu_screen = None |
||||
|
||||
def test_verify_against_golden_master(self): |
||||
golden_master_dir = Path("tests/golden_master") |
||||
if not golden_master_dir.exists(): |
||||
self.skipTest("Golden master not found. Run recording first.") |
||||
|
||||
with open(golden_master_dir / "states.json", "r") as f: |
||||
golden_states = json.load(f) |
||||
|
||||
steps = 200 |
||||
golden_idx = 0 |
||||
|
||||
print(f"Verifying against golden master for {steps} steps...") |
||||
|
||||
for i in range(steps): |
||||
self.game.update_maze() |
||||
|
||||
if i % 50 == 0 or i == steps - 1: |
||||
current_state = self.dump_game_state() |
||||
golden_state = golden_states[golden_idx] |
||||
|
||||
# Compare unit count |
||||
self.assertEqual(current_state["unit_count"], golden_state["unit_count"], |
||||
f"Unit count mismatch at step {i}") |
||||
|
||||
# Compare units |
||||
for u_idx, (curr_u, gold_u) in enumerate(zip(current_state["units"], golden_state["units"])): |
||||
self.assertEqual(curr_u["id"], gold_u["id"], f"Unit ID mismatch at step {i}, index {u_idx}") |
||||
self.assertEqual(curr_u["type"], gold_u["type"], f"Unit type mismatch at step {i}, unit {curr_u['id']}") |
||||
self.assertEqual(curr_u["pos"], gold_u["pos"], f"Unit pos mismatch at step {i}, unit {curr_u['id']}") |
||||
self.assertAlmostEqual(curr_u["partial_move"], gold_u["partial_move"], places=5, |
||||
msg=f"Unit partial_move mismatch at step {i}, unit {curr_u['id']}") |
||||
|
||||
golden_idx += 1 |
||||
|
||||
print("Verification successful! No regressions detected.") |
||||
|
||||
def dump_game_state(self): |
||||
state = { |
||||
"points": self.game.points, |
||||
"unit_count": len(self.game.units), |
||||
"units": [] |
||||
} |
||||
# Sort units by ID to be deterministic |
||||
sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0])) |
||||
|
||||
for uid, unit in sorted_units: |
||||
unit_state = { |
||||
"id": str(uid), |
||||
"type": unit.__class__.__name__, |
||||
"pos": list(unit.position), |
||||
"partial_move": float(unit.partial_move), |
||||
} |
||||
state["units"].append(unit_state) |
||||
return state |
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,437 @@
|
||||
[ |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
12, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
13, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
8, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
7, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
19, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
20, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
9, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
10, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 0 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
7, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
8, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
6, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
13, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
12, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
18, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 50 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
6, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
6, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "FEMALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
4, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
5 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
17, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
19, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
3, |
||||
15 |
||||
], |
||||
"pos_before": [ |
||||
4, |
||||
15 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 100 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
10, |
||||
6 |
||||
], |
||||
"pos_before": [ |
||||
10, |
||||
5 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "FEMALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
1, |
||||
3 |
||||
], |
||||
"pos_before": [ |
||||
1, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
22, |
||||
29 |
||||
], |
||||
"pos_before": [ |
||||
22, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
15, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
16, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
15 |
||||
], |
||||
"pos_before": [ |
||||
3, |
||||
15 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "FEMALE", |
||||
"direction": "RIGHT" |
||||
} |
||||
], |
||||
"frame": 150 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
11, |
||||
7 |
||||
], |
||||
"pos_before": [ |
||||
11, |
||||
8 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "FEMALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
3, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
2, |
||||
1 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
23, |
||||
26 |
||||
], |
||||
"pos_before": [ |
||||
22, |
||||
26 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
13, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
13, |
||||
7 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
4, |
||||
12 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "FEMALE", |
||||
"direction": "UP" |
||||
} |
||||
], |
||||
"frame": 199 |
||||
} |
||||
] |
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,437 @@
|
||||
[ |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
12, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
13, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
8, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
7, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
19, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
20, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
9, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
10, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 1, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 0 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
7, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
8, |
||||
1 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
6, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
13, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
12, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
18, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "MALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
11 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 51, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 50 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
6, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
6, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "FEMALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
4, |
||||
5 |
||||
], |
||||
"pos_before": [ |
||||
5, |
||||
5 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
30 |
||||
], |
||||
"pos_before": [ |
||||
17, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
18, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
19, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
3, |
||||
15 |
||||
], |
||||
"pos_before": [ |
||||
4, |
||||
15 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 101, |
||||
"sex": "FEMALE", |
||||
"direction": "LEFT" |
||||
} |
||||
], |
||||
"frame": 100 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
10, |
||||
6 |
||||
], |
||||
"pos_before": [ |
||||
10, |
||||
5 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "FEMALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
1, |
||||
3 |
||||
], |
||||
"pos_before": [ |
||||
1, |
||||
4 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
22, |
||||
29 |
||||
], |
||||
"pos_before": [ |
||||
22, |
||||
30 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
15, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
16, |
||||
8 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "MALE", |
||||
"direction": "LEFT" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
15 |
||||
], |
||||
"pos_before": [ |
||||
3, |
||||
15 |
||||
], |
||||
"partial_move": 0.1, |
||||
"age": 151, |
||||
"sex": "FEMALE", |
||||
"direction": "RIGHT" |
||||
} |
||||
], |
||||
"frame": 150 |
||||
}, |
||||
{ |
||||
"points": 0, |
||||
"unit_count": 5, |
||||
"units": [ |
||||
{ |
||||
"id": "0", |
||||
"type": "Female", |
||||
"pos": [ |
||||
11, |
||||
7 |
||||
], |
||||
"pos_before": [ |
||||
11, |
||||
8 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "FEMALE", |
||||
"direction": "UP" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"type": "Male", |
||||
"pos": [ |
||||
3, |
||||
1 |
||||
], |
||||
"pos_before": [ |
||||
2, |
||||
1 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"type": "Male", |
||||
"pos": [ |
||||
23, |
||||
26 |
||||
], |
||||
"pos_before": [ |
||||
22, |
||||
26 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "RIGHT" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"type": "Male", |
||||
"pos": [ |
||||
13, |
||||
8 |
||||
], |
||||
"pos_before": [ |
||||
13, |
||||
7 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "MALE", |
||||
"direction": "DOWN" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"type": "Female", |
||||
"pos": [ |
||||
4, |
||||
11 |
||||
], |
||||
"pos_before": [ |
||||
4, |
||||
12 |
||||
], |
||||
"partial_move": 0.95, |
||||
"age": 200, |
||||
"sex": "FEMALE", |
||||
"direction": "UP" |
||||
} |
||||
], |
||||
"frame": 199 |
||||
} |
||||
] |
||||
@ -0,0 +1,61 @@
|
||||
#!/bin/sh |
||||
set -u |
||||
|
||||
section() { |
||||
printf '\n===== %s =====\n' "$1" |
||||
} |
||||
|
||||
run_cmd() { |
||||
printf '\n$ %s\n' "$*" |
||||
"$@" 2>&1 || printf '[exit=%s]\n' "$?" |
||||
} |
||||
|
||||
run_shell() { |
||||
printf '\n$ %s\n' "$1" |
||||
sh -c "$1" 2>&1 || printf '[exit=%s]\n' "$?" |
||||
} |
||||
|
||||
section "system" |
||||
run_cmd date |
||||
run_cmd uname -a |
||||
run_shell 'cat /etc/os-release 2>/dev/null || true' |
||||
|
||||
section "bluetooth binaries" |
||||
run_shell 'for path in /bin/bluetoothctl /usr/bin/bluetoothctl /usr/sbin/bluetoothd /usr/libexec/bluetooth/bluetoothd /usr/bin/dbus-daemon /bin/dbus-daemon /usr/bin/dbus-send /bin/dbus-send /usr/bin/btmgmt /bin/btmgmt /usr/bin/hciconfig /bin/hciconfig /usr/sbin/rfkill /bin/rfkill; do if [ -e "$path" ]; then ls -l "$path"; fi; done' |
||||
|
||||
section "processes" |
||||
run_shell 'pidof bluetoothd || true' |
||||
run_shell 'pidof dbus-daemon || true' |
||||
run_shell 'ps w | grep -E "bluetoothd|dbus-daemon|pipewire|wireplumber|bluealsa" | grep -v grep || true' |
||||
|
||||
section "dbus" |
||||
run_shell 'ls -l /run/dbus/system_bus_socket /var/run/dbus/system_bus_socket 2>/dev/null || true' |
||||
run_shell 'dbus-send --system --dest=org.freedesktop.DBus --type=method_call --print-reply / org.freedesktop.DBus.ListNames 2>/dev/null | head -n 40' |
||||
|
||||
section "bluetoothctl" |
||||
run_shell 'timeout 8 bluetoothctl --version 2>/dev/null || bluetoothctl --version 2>/dev/null || true' |
||||
run_shell 'timeout 8 bluetoothctl show' |
||||
|
||||
section "kernel" |
||||
run_shell 'lsmod | grep -i -E "bluetooth|btusb|btmtk|hci_uart|rtl" || true' |
||||
run_shell 'dmesg | grep -i -E "bluetooth|bluez|hci|rtl_bt|btusb|btmtk" | tail -n 120 || true' |
||||
|
||||
section "controllers" |
||||
run_shell 'hciconfig -a' |
||||
run_shell 'btmgmt info' |
||||
run_shell 'rfkill list' |
||||
|
||||
section "sysfs" |
||||
run_shell 'find /sys/class/bluetooth -maxdepth 2 -type f 2>/dev/null | head -n 50 || true' |
||||
run_shell 'find /sys/class/rfkill -maxdepth 2 -type f 2>/dev/null | head -n 50 || true' |
||||
|
||||
section "firmware" |
||||
run_shell 'find /lib/firmware -maxdepth 3 -type f 2>/dev/null | grep -i -E "rtlbt|bluetooth|mt76|mt79|mediatek" | head -n 120 || true' |
||||
|
||||
section "audio stack" |
||||
run_shell 'ps w | grep -E "pipewire|wireplumber|pulseaudio|bluealsa" | grep -v grep || true' |
||||
run_shell 'command -v wpctl >/dev/null 2>&1 && wpctl status || true' |
||||
run_shell 'command -v pactl >/dev/null 2>&1 && pactl info || true' |
||||
|
||||
section "done" |
||||
printf '\nDiagnostic complete.\n' |
||||