From fb8d90223f8450a47765919441d475da8fc11924 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 09:55:30 -0400 Subject: [PATCH 01/11] Onefile attempt --- .github/workflows/main.yml | 94 ++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69859d4..9a666fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,13 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-22.04 + platform: linux + python-version: "3.11" + - os: windows-latest + platform: windows + python-version: "3.11" steps: - name: Check-out repository @@ -35,58 +41,78 @@ jobs: disable-plugins: tk-inter,dill-compat,eventlet,gevent,pyqt5,pyqt6,pyside2,pyside6,delvewheel,pywebview,matplotlib,spacy,enum-compat,pbr-compat,gevent,pmw-freezer,transformers,upx,kivy,options-nanny,multiprocessing,gi include-data-dir: assets=assets include-data-files: CREDITS=CREDITS - mode: standalone + mode: onefile output-file: GameOfLife - - name: Zip Build Output - shell: bash + - name: Locate and rename executable (Linux) + if: matrix.os == 'ubuntu-22.04' run: | - mkdir -p zip_output - if [ "${{ runner.os }}" = "Windows" ]; then - powershell.exe -Command "Compress-Archive -Path 'build/run.dist/*' -DestinationPath 'zip_output/GameOfLife-${{ runner.os }}.zip'" - else - cd build/run.dist - zip -r "../../zip_output/GameOfLife-${{ runner.os }}.zip" . + set -euo pipefail + echo "Searching for built Linux binary..." + # List to help debugging when paths change + ls -laR . | head -n 500 || true + BIN=$(find . -maxdepth 4 -type f -name 'GameOfLife*' -perm -u+x | head -n1 || true) + if [ -z "${BIN}" ]; then + echo "ERROR: No Linux binary found after build" + exit 1 fi + echo "Found: ${BIN}" + mkdir -p build_output + cp "${BIN}" build_output/GameOfLife.bin + chmod +x build_output/GameOfLife.bin + echo "Executable ready: build_output/GameOfLife.bin" + shell: bash - - name: Upload Zipped Build Artifact + - name: Locate and rename executable (Windows) + if: matrix.os == 'windows-latest' + run: | + Write-Host "Searching for built Windows binary..." + Get-ChildItem -Recurse -File -Filter 'lasergates*.exe' | Select-Object -First 1 | ForEach-Object { + Write-Host ("Found: " + $_.FullName) + New-Item -ItemType Directory -Force -Path build_output | Out-Null + Copy-Item $_.FullName "build_output\GameOfLife.exe" + Write-Host "Executable ready: build_output\GameOfLife.exe" + } + if (!(Test-Path build_output\GameOfLife.exe)) { + Write-Error "ERROR: No Windows binary found after build" + exit 1 + } + shell: pwsh + + - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: GameOfLife-${{ runner.os }}.zip - path: zip_output/GameOfLife-${{ runner.os }}.zip + name: ${{ matrix.platform }} + path: build_output/GameOfLife.* + release: - name: Create GitHub Release - needs: build runs-on: ubuntu-latest + needs: build steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download All Zipped Builds + - name: Download All Build Artifacts uses: actions/download-artifact@v4 with: path: downloads - - name: Delete Old Release (if exists) - continue-on-error: true - run: gh release delete latest -y + - name: Create release (if missing) and upload artifacts to tag env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Delete Git tag (if exists) - continue-on-error: true run: | - git push origin :refs/tags/latest - git tag -d latest - - - name: Recreate Git tag at HEAD - run: | - git tag latest - git push origin latest - - - name: Create the new release - run: gh release create latest downloads/**/GameOfLife-*.zip --title "Latest Build" --notes "Most recent multi-platform builds of Game Of Life" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + set -euo pipefail + TAG="${{ github.ref_name }}" + echo "Target release tag: $TAG" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists; will upload assets with --clobber" + else + gh release create "$TAG" \ + --title "$TAG" \ + --notes "Automated build for $TAG" + fi + # Upload the executables directly (no zip files) + gh release upload "$TAG" downloads/linux/GameOfLife.bin --clobber + gh release upload "$TAG" downloads/windows/GameOfLife.exe --clobber From 2588aa3a9d7047d158d155724a399b8af9f24397 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 09:56:41 -0400 Subject: [PATCH 02/11] Formatting --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9a666fe..4992f02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -83,7 +83,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }} - path: build_output/GameOfLife.* + path: build_output/GameOfLife.* release: runs-on: ubuntu-latest From 7441c48f69c703b6c713a15c6d64090a85986999 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 10:19:52 -0400 Subject: [PATCH 03/11] Windows executable name typo --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4992f02..fce801d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: if: matrix.os == 'windows-latest' run: | Write-Host "Searching for built Windows binary..." - Get-ChildItem -Recurse -File -Filter 'lasergates*.exe' | Select-Object -First 1 | ForEach-Object { + Get-ChildItem -Recurse -File -Filter 'GameOfLife*.exe' | Select-Object -First 1 | ForEach-Object { Write-Host ("Found: " + $_.FullName) New-Item -ItemType Directory -Force -Path build_output | Out-Null Copy-Item $_.FullName "build_output\GameOfLife.exe" From 28f481ce878f7f6006edc7d70c9f50b88f954533 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 11:01:50 -0400 Subject: [PATCH 04/11] Resolving filepaths in both onefile and standalone modes for Nuitka --- run.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/run.py b/run.py index 65854f4..e748dd2 100644 --- a/run.py +++ b/run.py @@ -4,6 +4,12 @@ pyglet.options.debug_gl = False import logging, datetime, os, json, sys, arcade +# Set up paths BEFORE importing modules that load assets +script_dir = os.path.dirname(os.path.abspath(__file__)) +pyglet.resource.path.append(script_dir) +pyglet.font.add_directory(os.path.join(script_dir, 'assets', 'fonts')) + + from utils.utils import get_closest_resolution, print_debug_info, on_exception from utils.constants import log_dir, menu_background_color from menus.main import Main @@ -12,9 +18,6 @@ from arcade.experimental.controller_window import ControllerWindow sys.excepthook = on_exception -pyglet.resource.path.append(os.getcwd()) -pyglet.font.add_directory('./assets/fonts') - __builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args))) if not log_dir in os.listdir(): From 189e345f1f5a68ae111a0829b0ca44cfa3f0f956 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 11:19:00 -0400 Subject: [PATCH 05/11] Changed preload.py to use absolute paths for onefile --- utils/preload.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/utils/preload.py b/utils/preload.py index 094e5dd..1d75a38 100644 --- a/utils/preload.py +++ b/utils/preload.py @@ -1,9 +1,14 @@ import arcade.gui, arcade +import os -button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png")) -button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png")) -cursor_texture = arcade.load_texture("assets/graphics/cursor.png") +# Get the directory where this module is located +_module_dir = os.path.dirname(os.path.abspath(__file__)) +_assets_dir = os.path.join(os.path.dirname(_module_dir), 'assets') -create_sound = arcade.Sound("assets/sound/create.mp3") -destroy_sound = arcade.Sound("assets/sound/destroy.mp3") -theme_sound = arcade.Sound("assets/sound/music.mp3") \ No newline at end of file +button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button.png'))) +button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button_hovered.png'))) +cursor_texture = arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'cursor.png')) + +create_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'create.mp3')) +destroy_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'destroy.mp3')) +theme_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'music.mp3')) \ No newline at end of file From 88a699bbcdbf7bdd741c7078f22072516e1238ed Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 11:41:43 -0400 Subject: [PATCH 06/11] Fallback to arcade.Window on Linux to avoid exception --- run.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index e748dd2..0528a9d 100644 --- a/run.py +++ b/run.py @@ -76,8 +76,12 @@ else: if settings.get("music", True): theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True) -window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) - +try: + window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) +except (FileNotFoundError, PermissionError) as e: + logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.") + window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) + if vsync: window.set_vsync(True) display_mode = window.display.get_default_screen().get_mode() From 021c922571fa14e8dba1763548e74085eebeba4a Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 12:11:15 -0400 Subject: [PATCH 07/11] Change 2nd window display behavior if Wayland detected --- run.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/run.py b/run.py index 0528a9d..c7fd9f3 100644 --- a/run.py +++ b/run.py @@ -2,7 +2,7 @@ import pyglet pyglet.options.debug_gl = False -import logging, datetime, os, json, sys, arcade +import logging, datetime, os, json, sys, arcade, platform # Set up paths BEFORE importing modules that load assets script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -47,6 +47,13 @@ if os.path.exists('settings.json'): else: antialiasing = 0 + # Wayland workaround (can be overridden with environment variable) + if (platform.system() == "Linux" and + os.environ.get("WAYLAND_DISPLAY") and + not os.environ.get("ARCADE_FORCE_MSAA")): + logging.info("Wayland detected - disabling MSAA (set ARCADE_FORCE_MSAA=1 to override)") + antialiasing = 0 + fullscreen = settings['window_mode'] == 'Fullscreen' style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT vsync = settings['vsync'] @@ -54,6 +61,14 @@ if os.path.exists('settings.json'): else: resolution = get_closest_resolution() antialiasing = 4 + + # Wayland workaround (can be overridden with environment variable) + if (platform.system() == "Linux" and + os.environ.get("WAYLAND_DISPLAY") and + not os.environ.get("ARCADE_FORCE_MSAA")): + logging.info("Wayland detected - disabling MSAA (set ARCADE_FORCE_MSAA=1 to override)") + antialiasing = 0 + fullscreen = False style = arcade.Window.WINDOW_STYLE_DEFAULT vsync = True From 309cb0e2c5c40b39e52833f628daec81ca0b79fa Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 13:02:56 -0400 Subject: [PATCH 08/11] Turn off shadow window --- run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/run.py b/run.py index c7fd9f3..0c767fa 100644 --- a/run.py +++ b/run.py @@ -1,5 +1,6 @@ import pyglet +pyglet.options['shadow_window'] = False # Fix double window issue on Wayland pyglet.options.debug_gl = False import logging, datetime, os, json, sys, arcade, platform From 3cb59bd70660014ae9b72f027099a2e8952f62c6 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 13:29:37 -0400 Subject: [PATCH 09/11] Fixed double window issue on Linux --- run.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index 0c767fa..8b59a05 100644 --- a/run.py +++ b/run.py @@ -93,10 +93,10 @@ if settings.get("music", True): theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True) try: - window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) + window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) except (FileNotFoundError, PermissionError) as e: logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.") - window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) + window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) if vsync: window.set_vsync(True) @@ -121,6 +121,9 @@ main = Main() window.show_view(main) +# Make window visible after all setup is complete (helps prevent double window on Wayland) +window.set_visible(True) + logging.debug('Game started.') arcade.run() From 3c82a74fba0dd1434c3582cbb3b77e1c2a2acd7c Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 13:55:27 -0400 Subject: [PATCH 10/11] Handle missing ControllerWindow --- game/play.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/game/play.py b/game/play.py index 8ae0011..0adaa5c 100644 --- a/game/play.py +++ b/game/play.py @@ -76,7 +76,8 @@ class Game(arcade.gui.UIView): self.save_button.on_click = lambda event: self.save() self.anchor.add(self.save_button, anchor_x="right", anchor_y="bottom", align_x=-5, align_y=5) - if self.window.get_controllers(): + # Check if window has controller support (ControllerWindow has get_controllers, regular Window doesn't) + if hasattr(self.window, 'get_controllers') and self.window.get_controllers(): self.spritelist = arcade.SpriteList() self.cursor_sprite = arcade.Sprite(cursor_texture) self.spritelist.append(self.cursor_sprite) From 5c311f60ca4a9d6023d2ae528769c76c767f4fd3 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 1 Nov 2025 18:47:05 -0400 Subject: [PATCH 11/11] formatting --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fce801d..3a0fd8f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: disable-plugins: tk-inter,dill-compat,eventlet,gevent,pyqt5,pyqt6,pyside2,pyside6,delvewheel,pywebview,matplotlib,spacy,enum-compat,pbr-compat,gevent,pmw-freezer,transformers,upx,kivy,options-nanny,multiprocessing,gi include-data-dir: assets=assets include-data-files: CREDITS=CREDITS - mode: onefile + mode: onefile output-file: GameOfLife - name: Locate and rename executable (Linux)