Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
3 changes: 3 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__))
Expand All @@ -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)


Expand Down
33 changes: 33 additions & 0 deletions src/main_desktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down