diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..1bad26d --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,99 @@ +name: Pull Request CI + +on: + push: + branches: '**' + +jobs: + test-desktop: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsdl2-2.0-0 libsdl2-ttf-2.0-0 libsdl2-image-2.0-0 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install pygame pillow + + - name: Translate source (English) + run: | + python tools/translate.py --lang en src build/desktop-en + + - name: Verify translated output + run: | + test -f build/desktop-en/main_desktop.py + echo "Translation OK" + + - name: Headless import test + shell: bash + run: | + cd build/desktop-en + export SDL_VIDEODRIVER=dummy + export SDL_AUDIODRIVER=dummy + python3 << 'PYEOF' + import os, sys, types + + for mod in ('espnow_manager', 'espnow_handler', 'visit_manager', + 'sleep_manager', 'wifi_tracker', 'machine'): + sys.modules[mod] = types.ModuleType(mod) + + import json + sys.modules['ujson'] = json + sys.modules['uos'] = os + + import pygame + pygame.init() + + import config_desktop + sys.modules['config'] = config_desktop + + import renderer_desktop + sys.modules['renderer'] = renderer_desktop + + import input_desktop + sys.modules['input'] = input_desktop + + import framebuf + + from context import GameContext + from scene_manager import SceneManager + from weather_system import WeatherSystem + from time_system import TimeSystem + from splash import show_splash + + print('All game modules imported successfully') + pygame.quit() + PYEOF + + - name: Headless game smoke test (with screenshot) + shell: bash + run: | + export SDL_VIDEODRIVER=dummy + export SDL_AUDIODRIVER=dummy + python run.py --lang en --screenshot + # Verify screenshot was created + if [ -f build/desktop-en/screenshot.png ]; then + echo "Screenshot captured: $(file build/desktop-en/screenshot.png)" + else + echo "Warning: screenshot not found" + fi + + - name: Upload screenshot artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: game-screenshot + path: build/desktop-en/screenshot.png + if-no-files-found: warn diff --git a/README.md b/README.md index b8dabc2..ab07e5f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Catode32 - A virtual pet for your ESP32 +[![Dekstop](https://github.com/moonbench/catode32/actions/workflows/pull-request.yml/badge.svg)](https://github.com/moonbench/catode32/actions/workflows/pull-request.yml) + ![catstars](https://github.com/user-attachments/assets/2ffc652a-f392-42e7-9a13-d7fb91f3770d) ![spookycat](https://github.com/user-attachments/assets/c1f8b6eb-b90c-46ad-b652-80093db97f83) diff --git a/run.py b/run.py index d0e4d73..773daa6 100644 --- a/run.py +++ b/run.py @@ -17,6 +17,7 @@ def main(): parser = argparse.ArgumentParser(description='Run Catode32 desktop emulator.') parser.add_argument('--lang', default='en', help='Language code (default: en)') + parser.add_argument('--screenshot', action='store_true', help='Capture a screenshot after boot and save as screenshot.png') args = parser.parse_args() root = os.path.dirname(os.path.abspath(__file__)) @@ -35,6 +36,8 @@ def main(): env = os.environ.copy() env['CATODE32_SRC'] = src_dir + if args.screenshot: + env['CATODE32_SCREENSHOT'] = '1' os.execve(sys.executable, [sys.executable, main_script], env) diff --git a/src/main_desktop.py b/src/main_desktop.py index 1cbca1e..fe682b5 100644 --- a/src/main_desktop.py +++ b/src/main_desktop.py @@ -22,6 +22,14 @@ import pygame pygame.init() +# ── Screenshot support (optional, enabled via CATODE32_SCREENSHOT env var) ── +_SCREENSHOT_ENABLED = os.environ.get('CATODE32_SCREENSHOT') == '1' +_SCREENSHOT_FRAME = 30 # capture after this many frames +_SCREENSHOT_COUNTER = 0 +_SCREENSHOT_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'screenshot.png' +) + # ── Swap in desktop versions before anything else imports the originals ── # We rename the desktop modules to the names the game expects. import config_desktop @@ -96,6 +104,21 @@ def _ticks_diff(a, b): from splash import show_splash import config +# ── Screenshot helper ────────────────────────────────────────────────────── +def _save_screenshot(surface): + """Save the pygame surface as a PNG file.""" + try: + from PIL import Image + # pygame surface -> raw RGB bytes -> PIL Image + raw = pygame.image.tostring(surface, 'RGB') + img = Image.frombytes('RGB', surface.get_size(), raw) + img.save(_SCREENSHOT_PATH) + print(f"==> Screenshot saved: {_SCREENSHOT_PATH} " + f"({surface.get_width()}x{surface.get_height()})") + except Exception as e: + print(f"[Screenshot] Failed: {e}") + + # ── Game class (trimmed version of main.py — no ESP-Now, no deep sleep) ─── class Game: @@ -155,6 +178,16 @@ def run(self): self.scene_manager.draw() self.renderer.show() + # ── screenshot capture (CI only) ────────────────────────── + global _SCREENSHOT_COUNTER + if _SCREENSHOT_ENABLED: + _SCREENSHOT_COUNTER += 1 + if _SCREENSHOT_COUNTER == _SCREENSHOT_FRAME: + _save_screenshot(self.renderer.display._surface) + if _SCREENSHOT_ENABLED: + print("==> Screenshot captured, auto-quitting...") + self._quit() + self.last_frame_time = now clock.tick(config.FPS)