diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5decd05..68cbb25 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,118 +1,118 @@
-name: Build and Release
-
-on: push
-
-jobs:
- build:
- name: Build on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- 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
- uses: actions/checkout@v4
-
- - name: Setup Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.11"
- architecture: "x64"
- cache: "pip"
- cache-dependency-path: |
- **/requirements*.txt
-
- - name: Install Dependencies
- run: pip install -r requirements.txt
-
- - name: Build Executable
- uses: Nuitka/Nuitka-Action@main
- with:
- nuitka-version: main
- script-name: run.py
- nofollow-import-to: "*tk*,_codecs,encodings,multiprocessing,gi"
- 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
- output-file: ConnectTheCurrent
-
- - name: Locate and rename executable (Linux)
- if: matrix.os == 'ubuntu-22.04'
- run: |
- 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 'ConnectTheCurrent*' -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/ConnectTheCurrent.bin
- chmod +x build_output/ConnectTheCurrent.bin
- echo "Executable ready: build_output/ConnectTheCurrent.bin"
- shell: bash
-
- - name: Locate and rename executable (Windows)
- if: matrix.os == 'windows-latest'
- run: |
- Write-Host "Searching for built Windows binary..."
- Get-ChildItem -Recurse -File -Filter 'ConnectTheCurrent*.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\ConnectTheCurrent.exe"
- Write-Host "Executable ready: build_output\ConnectTheCurrent.exe"
- }
- if (!(Test-Path build_output\ConnectTheCurrent.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: ${{ matrix.platform }}
- path: build_output/ConnectTheCurrent.*
-
- release:
- runs-on: ubuntu-latest
- needs: build
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Download All Build Artifacts
- uses: actions/download-artifact@v4
- with:
- path: downloads
-
- - name: Create release (if missing) and upload artifacts to tag
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- set -euo pipefail
- TAG="latest"
- 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/ConnectTheCurrent.bin --clobber
- gh release upload "$TAG" downloads/windows/ConnectTheCurrent.exe --clobber
+name: Build and Release
+
+on: push
+
+jobs:
+ build:
+ name: Build on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ 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
+ uses: actions/checkout@v4
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ architecture: "x64"
+ cache: "pip"
+ cache-dependency-path: |
+ **/requirements*.txt
+
+ - name: Install Dependencies
+ run: pip install -r requirements.txt
+
+ - name: Build Executable
+ uses: Nuitka/Nuitka-Action@main
+ with:
+ nuitka-version: main
+ script-name: run.py
+ nofollow-import-to: "*tk*,_codecs,encodings,multiprocessing,gi"
+ 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
+ output-file: ConnectTheCurrent
+
+ - name: Locate and rename executable (Linux)
+ if: matrix.os == 'ubuntu-22.04'
+ run: |
+ 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 'ConnectTheCurrent*' -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/ConnectTheCurrent.bin
+ chmod +x build_output/ConnectTheCurrent.bin
+ echo "Executable ready: build_output/ConnectTheCurrent.bin"
+ shell: bash
+
+ - name: Locate and rename executable (Windows)
+ if: matrix.os == 'windows-latest'
+ run: |
+ Write-Host "Searching for built Windows binary..."
+ Get-ChildItem -Recurse -File -Filter 'ConnectTheCurrent*.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\ConnectTheCurrent.exe"
+ Write-Host "Executable ready: build_output\ConnectTheCurrent.exe"
+ }
+ if (!(Test-Path build_output\ConnectTheCurrent.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: ${{ matrix.platform }}
+ path: build_output/ConnectTheCurrent.*
+
+ release:
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Download All Build Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: downloads
+
+ - name: Create release (if missing) and upload artifacts to tag
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+ TAG="latest"
+ 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/ConnectTheCurrent.bin --clobber
+ gh release upload "$TAG" downloads/windows/ConnectTheCurrent.exe --clobber
diff --git a/.gitignore b/.gitignore
index 81c71f9..e0264d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,182 +1,182 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
-
-.vscode
-
-test*.py
-.zed/
-logs/
-logs
-settings.json
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+.vscode
+
+test*.py
+.zed/
+logs/
+logs
+settings.json
diff --git a/.python-version b/.python-version
index 2c07333..37504c5 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.11
+3.11
diff --git a/CREDITS b/CREDITS
index ac6e134..f3c536f 100644
--- a/CREDITS
+++ b/CREDITS
@@ -1,14 +1,14 @@
-Sound Effect by freesound_community from Pixabay (cut to the important part)
-https://pixabay.com/sound-effects/cutting-clipping-wire-copper-80373/
-
-The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information.
-
-Huge Thanks to Python for being the programming language used in this game.
-https://www.python.org/
-
-Huge thanks to Arcade and Pyglet for being the graphical engines used in this game.
-https://arcade.academy/
-https://pyglet.readthedocs.io/en/latest/
-
-Thanks to the following other libraries used in this game:
-pypresence - https://github.com/qwertyquerty/pypresence - Used for Discord Rich Presence
+Sound Effect by freesound_community from Pixabay (cut to the important part)
+https://pixabay.com/sound-effects/cutting-clipping-wire-copper-80373/
+
+The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information.
+
+Huge Thanks to Python for being the programming language used in this game.
+https://www.python.org/
+
+Huge thanks to Arcade and Pyglet for being the graphical engines used in this game.
+https://arcade.academy/
+https://pyglet.readthedocs.io/en/latest/
+
+Thanks to the following other libraries used in this game:
+pypresence - https://github.com/qwertyquerty/pypresence - Used for Discord Rich Presence
diff --git a/LICENSE b/LICENSE
index f288702..3877ae0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,674 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
index 2a392ec..7c5c622 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-CTC: Connect The Current is a game where you have a power source, and you have to direct the power lines to houses by rotating them into the correct direction.
-
-Tutorial:
-In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
-- Every line has to be connected on all of it's sides.
-- When needed, you might have to create loops of power or branches with no house linked to them.
-(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
-- To rotate a line, just click on it and it will change its rotation.
+CTC: Connect The Current is a game where you have a power source, and you have to direct the power lines to houses by rotating them into the correct direction.
+
+Tutorial:
+In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
+- Every line has to be connected on all of it's sides.
+- When needed, you might have to create loops of power or branches with no house linked to them.
+(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
+- To rotate a line, just click on it and it will change its rotation.
- Maps are randomly generated, difficulty(size, source count, house count) depends on what you pick and grows exponentially.
\ No newline at end of file
diff --git a/game/cell.py b/game/cell.py
index 0620781..8c1515c 100644
--- a/game/cell.py
+++ b/game/cell.py
@@ -1,60 +1,60 @@
-import arcade, arcade.gui
-
-from utils.constants import ROTATIONS, NEIGHBOURS
-from utils.preload import TEXTURE_MAP, wire_sound_effect
-
-def get_opposite(direction):
- if direction == "l":
- return "r"
- elif direction == "r":
- return "l"
- elif direction == "t":
- return "b"
- elif direction == "b":
- return "t"
-
-class Cell(arcade.Sprite):
- def __init__(self, cell_type, x, y, left_neighbour, top_neighbour):
- super().__init__(TEXTURE_MAP[cell_type, ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross", cell_type == "power_source"], center_x=x, center_y=y)
-
- self.rotation = ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross"
- self.cell_type = cell_type
- self.powered = False
- self.left_neighbour, self.top_neighbour = left_neighbour, top_neighbour
- self.right_neighbour, self.bottom_neighbour = None, None
-
- def get_neighbour(self, name):
- if name == "l":
- return self.left_neighbour
- elif name == "r":
- return self.right_neighbour
- elif name == "b":
- return self.bottom_neighbour
- elif name == "t":
- return self.top_neighbour
-
- def get_connected_neighbours(self, include_houses=False):
- return [
- self.get_neighbour(neighbour_direction) for neighbour_direction in NEIGHBOURS[self.rotation]
- if (
- self.get_neighbour(neighbour_direction) and
- (include_houses or self.get_neighbour(neighbour_direction).cell_type != "house") and
- get_opposite(neighbour_direction) in NEIGHBOURS[self.get_neighbour(neighbour_direction).rotation]
- )
- ]
-
- def update_visual(self):
- self.texture = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)]
-
- def next_rotation(self, sfx, sfx_volume):
- if sfx:
- wire_sound_effect.play(volume=sfx_volume / 50)
-
- current_index = ROTATIONS[self.cell_type].index(self.rotation)
-
- if current_index + 1 == len(ROTATIONS[self.cell_type]):
- self.rotation = ROTATIONS[self.cell_type][0]
- else:
- self.rotation = ROTATIONS[self.cell_type][current_index + 1]
-
+import arcade, arcade.gui
+
+from utils.constants import ROTATIONS, NEIGHBOURS
+from utils.preload import TEXTURE_MAP, wire_sound_effect
+
+def get_opposite(direction):
+ if direction == "l":
+ return "r"
+ elif direction == "r":
+ return "l"
+ elif direction == "t":
+ return "b"
+ elif direction == "b":
+ return "t"
+
+class Cell(arcade.Sprite):
+ def __init__(self, cell_type, x, y, left_neighbour, top_neighbour):
+ super().__init__(TEXTURE_MAP[cell_type, ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross", cell_type == "power_source"], center_x=x, center_y=y)
+
+ self.rotation = ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross"
+ self.cell_type = cell_type
+ self.powered = False
+ self.left_neighbour, self.top_neighbour = left_neighbour, top_neighbour
+ self.right_neighbour, self.bottom_neighbour = None, None
+
+ def get_neighbour(self, name):
+ if name == "l":
+ return self.left_neighbour
+ elif name == "r":
+ return self.right_neighbour
+ elif name == "b":
+ return self.bottom_neighbour
+ elif name == "t":
+ return self.top_neighbour
+
+ def get_connected_neighbours(self, include_houses=False):
+ return [
+ self.get_neighbour(neighbour_direction) for neighbour_direction in NEIGHBOURS[self.rotation]
+ if (
+ self.get_neighbour(neighbour_direction) and
+ (include_houses or self.get_neighbour(neighbour_direction).cell_type != "house") and
+ get_opposite(neighbour_direction) in NEIGHBOURS[self.get_neighbour(neighbour_direction).rotation]
+ )
+ ]
+
+ def update_visual(self):
+ self.texture = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)]
+
+ def next_rotation(self, sfx, sfx_volume):
+ if sfx:
+ wire_sound_effect.play(volume=sfx_volume / 50)
+
+ current_index = ROTATIONS[self.cell_type].index(self.rotation)
+
+ if current_index + 1 == len(ROTATIONS[self.cell_type]):
+ self.rotation = ROTATIONS[self.cell_type][0]
+ else:
+ self.rotation = ROTATIONS[self.cell_type][current_index + 1]
+
self.update_visual()
\ No newline at end of file
diff --git a/game/cells.py b/game/cells.py
index 94cab49..5165866 100644
--- a/game/cells.py
+++ b/game/cells.py
@@ -1,17 +1,17 @@
-from game.cell import Cell
-
-class House(Cell):
- def __init__(self, x, y, left_neighbour, top_neighbour):
- super().__init__("house", x, y, left_neighbour, top_neighbour)
-
-class PowerSource(Cell):
- def __init__(self, x, y, left_neighbour, top_neighbour):
- super().__init__("power_source", x, y, left_neighbour, top_neighbour)
- self.on_click = lambda e: self.next_rotation()
-
-class PowerLine(Cell):
- def __init__(self, cell_type, x, y, left_neighbour, top_neighbour):
- super().__init__(cell_type, x, y, left_neighbour, top_neighbour)
-
- if not cell_type == "cross":
+from game.cell import Cell
+
+class House(Cell):
+ def __init__(self, x, y, left_neighbour, top_neighbour):
+ super().__init__("house", x, y, left_neighbour, top_neighbour)
+
+class PowerSource(Cell):
+ def __init__(self, x, y, left_neighbour, top_neighbour):
+ super().__init__("power_source", x, y, left_neighbour, top_neighbour)
+ self.on_click = lambda e: self.next_rotation()
+
+class PowerLine(Cell):
+ def __init__(self, cell_type, x, y, left_neighbour, top_neighbour):
+ super().__init__(cell_type, x, y, left_neighbour, top_neighbour)
+
+ if not cell_type == "cross":
self.on_click = lambda e: self.next_rotation()
\ No newline at end of file
diff --git a/game/level_generator.py b/game/level_generator.py
index 730abc1..94d8226 100644
--- a/game/level_generator.py
+++ b/game/level_generator.py
@@ -1,130 +1,130 @@
-import random
-from utils.constants import ROTATIONS, NEIGHBOURS, DIRECTIONS
-
-def in_bounds(x, y, size):
- return 0 <= x < size and 0 <= y < size
-
-def classify_tile(conns):
- for rotation, connections in NEIGHBOURS.items():
- if conns == connections:
- for cell_type, rotations in ROTATIONS.items():
- if rotation in rotations:
- return cell_type
-
- print(f"Unknown: {conns}")
- return "cross"
-
-def add_cycles(conns, num_cycles):
- size = len(conns)
- added = 0
- attempts = 0
- max_attempts = num_cycles * 20
-
- while added < num_cycles and attempts < max_attempts:
- attempts += 1
- x, y = random.randint(0, size-1), random.randint(0, size-1)
-
- dirs = list(DIRECTIONS.items())
- random.shuffle(dirs)
-
- for d, (dx, dy, opposite) in dirs:
- nx, ny = x + dx, y + dy
-
- if in_bounds(nx, ny, size) and d not in conns[y][x]:
- conns[y][x].add(d)
- conns[ny][nx].add(opposite)
- added += 1
- break
-
- return conns
-
-def pick_random_cells(size, count, avoid=None):
- all_cells = [(x, y) for y in range(size) for x in range(size)]
-
- if avoid:
- all_cells = [c for c in all_cells if c not in avoid]
-
- random.shuffle(all_cells)
- return all_cells[:count]
-
-def generate_spanning_tree_with_dead_ends(size, num_dead_ends):
- if num_dead_ends > size * size - 1:
- num_dead_ends = size * size - 1
-
- grid = [[set() for _ in range(size)] for _ in range(size)]
- all_cells = [(x, y) for y in range(size) for x in range(size)]
- random.shuffle(all_cells)
-
- start = all_cells[0]
- stack = [start]
- visited = {start}
- leaf_candidates = []
-
- while len(visited) < size * size:
- if not stack:
- unvisited = [c for c in all_cells if c not in visited]
- if unvisited:
- stack.append(unvisited[0])
- visited.add(unvisited[0])
-
- x, y = stack[-1]
- dirs = list(DIRECTIONS.items())
- random.shuffle(dirs)
-
- found = False
- for d, (dx, dy, opposite) in dirs:
- nx, ny = x + dx, y + dy
- if in_bounds(nx, ny, size) and (nx, ny) not in visited:
- grid[y][x].add(d)
- grid[ny][nx].add(opposite)
- visited.add((nx, ny))
- stack.append((nx, ny))
- found = True
- break
-
- if not found:
- if len(grid[y][x]) == 1:
- leaf_candidates.append((x, y))
- stack.pop()
-
- leaf_nodes = leaf_candidates[:num_dead_ends]
-
- for y in range(size):
- for x in range(size):
- if (x, y) not in leaf_nodes and len(grid[y][x]) < 2:
- dirs = list(DIRECTIONS.items())
- random.shuffle(dirs)
-
- for d, (dx, dy, opposite) in dirs:
- nx, ny = x + dx, y + dy
-
- if in_bounds(nx, ny, size) and d not in grid[y][x]:
- grid[y][x].add(d)
- grid[ny][nx].add(opposite)
- if len(grid[y][x]) >= 2:
- break
-
- return grid, leaf_nodes
-
-def generate_map(size, source_count, house_count, cycles=15):
- conns, dead_ends = generate_spanning_tree_with_dead_ends(size, house_count)
- conns = add_cycles(conns, cycles)
-
- houses = dead_ends[:house_count]
- available_cells = [(x, y) for y in range(size) for x in range(size) if (x, y) not in houses]
- random.shuffle(available_cells)
- sources = available_cells[:source_count]
-
- grid = []
- for y in range(size):
- grid.append([])
-
- for x in range(size):
- if (x, y) in sources:
- grid[-1].append("power_source")
- elif (x, y) in houses:
- grid[-1].append("house")
- else:
- grid[-1].append(classify_tile(conns[y][x]))
-
+import random
+from utils.constants import ROTATIONS, NEIGHBOURS, DIRECTIONS
+
+def in_bounds(x, y, size):
+ return 0 <= x < size and 0 <= y < size
+
+def classify_tile(conns):
+ for rotation, connections in NEIGHBOURS.items():
+ if conns == connections:
+ for cell_type, rotations in ROTATIONS.items():
+ if rotation in rotations:
+ return cell_type
+
+ print(f"Unknown: {conns}")
+ return "cross"
+
+def add_cycles(conns, num_cycles):
+ size = len(conns)
+ added = 0
+ attempts = 0
+ max_attempts = num_cycles * 20
+
+ while added < num_cycles and attempts < max_attempts:
+ attempts += 1
+ x, y = random.randint(0, size-1), random.randint(0, size-1)
+
+ dirs = list(DIRECTIONS.items())
+ random.shuffle(dirs)
+
+ for d, (dx, dy, opposite) in dirs:
+ nx, ny = x + dx, y + dy
+
+ if in_bounds(nx, ny, size) and d not in conns[y][x]:
+ conns[y][x].add(d)
+ conns[ny][nx].add(opposite)
+ added += 1
+ break
+
+ return conns
+
+def pick_random_cells(size, count, avoid=None):
+ all_cells = [(x, y) for y in range(size) for x in range(size)]
+
+ if avoid:
+ all_cells = [c for c in all_cells if c not in avoid]
+
+ random.shuffle(all_cells)
+ return all_cells[:count]
+
+def generate_spanning_tree_with_dead_ends(size, num_dead_ends):
+ if num_dead_ends > size * size - 1:
+ num_dead_ends = size * size - 1
+
+ grid = [[set() for _ in range(size)] for _ in range(size)]
+ all_cells = [(x, y) for y in range(size) for x in range(size)]
+ random.shuffle(all_cells)
+
+ start = all_cells[0]
+ stack = [start]
+ visited = {start}
+ leaf_candidates = []
+
+ while len(visited) < size * size:
+ if not stack:
+ unvisited = [c for c in all_cells if c not in visited]
+ if unvisited:
+ stack.append(unvisited[0])
+ visited.add(unvisited[0])
+
+ x, y = stack[-1]
+ dirs = list(DIRECTIONS.items())
+ random.shuffle(dirs)
+
+ found = False
+ for d, (dx, dy, opposite) in dirs:
+ nx, ny = x + dx, y + dy
+ if in_bounds(nx, ny, size) and (nx, ny) not in visited:
+ grid[y][x].add(d)
+ grid[ny][nx].add(opposite)
+ visited.add((nx, ny))
+ stack.append((nx, ny))
+ found = True
+ break
+
+ if not found:
+ if len(grid[y][x]) == 1:
+ leaf_candidates.append((x, y))
+ stack.pop()
+
+ leaf_nodes = leaf_candidates[:num_dead_ends]
+
+ for y in range(size):
+ for x in range(size):
+ if (x, y) not in leaf_nodes and len(grid[y][x]) < 2:
+ dirs = list(DIRECTIONS.items())
+ random.shuffle(dirs)
+
+ for d, (dx, dy, opposite) in dirs:
+ nx, ny = x + dx, y + dy
+
+ if in_bounds(nx, ny, size) and d not in grid[y][x]:
+ grid[y][x].add(d)
+ grid[ny][nx].add(opposite)
+ if len(grid[y][x]) >= 2:
+ break
+
+ return grid, leaf_nodes
+
+def generate_map(size, source_count, house_count, cycles=15):
+ conns, dead_ends = generate_spanning_tree_with_dead_ends(size, house_count)
+ conns = add_cycles(conns, cycles)
+
+ houses = dead_ends[:house_count]
+ available_cells = [(x, y) for y in range(size) for x in range(size) if (x, y) not in houses]
+ random.shuffle(available_cells)
+ sources = available_cells[:source_count]
+
+ grid = []
+ for y in range(size):
+ grid.append([])
+
+ for x in range(size):
+ if (x, y) in sources:
+ grid[-1].append("power_source")
+ elif (x, y) in houses:
+ grid[-1].append("house")
+ else:
+ grid[-1].append(classify_tile(conns[y][x]))
+
return grid
\ No newline at end of file
diff --git a/game/play.py b/game/play.py
index 6c8f3a4..67efcf1 100644
--- a/game/play.py
+++ b/game/play.py
@@ -1,150 +1,150 @@
-import arcade, arcade.gui, json, time
-
-from utils.constants import button_style, NEIGHBOURS
-from utils.preload import button_texture, button_hovered_texture
-
-from collections import deque
-
-from game.level_generator import generate_map
-from game.cells import *
-
-class Game(arcade.gui.UIView):
- def __init__(self, pypresence_client, grid_size, source_count=None, house_count=None):
- super().__init__()
-
- self.pypresence_client = pypresence_client
- self.pypresence_client.update(state='In Game', start=self.pypresence_client.start_time)
-
- self.grid_size = grid_size
- self.source_count = source_count
- self.house_count = house_count
-
- self.start = time.perf_counter()
- self.wire_rotations = 0
- self.cells = []
- self.power_sources = []
- self.houses = []
-
- self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
- self.map = generate_map(self.grid_size, int((self.grid_size * self.grid_size) / 10) if not source_count else source_count, int((self.grid_size * self.grid_size) / 5) if not house_count else house_count)
-
- self.spritelist = arcade.SpriteList()
-
- with open("settings.json", "r") as file:
- self.settings = json.load(file)
-
- def on_show_view(self):
- super().on_show_view()
-
- self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
- self.back_button.on_click = lambda event: self.main_exit()
- self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
-
- self.won_label = self.anchor.add(arcade.gui.UILabel(text="You won!", font_size=48), anchor_x="center", anchor_y="center")
- self.won_label.visible = False
-
- self.info_label = self.anchor.add(arcade.gui.UILabel("Time spent: 0s Wire Rotations: 0", font_size=24), anchor_x="center", anchor_y="top")
-
- x = (self.window.width / 2) - (self.grid_size * 64) / 2
- y = (self.window.height / 2) + (self.grid_size * 64) / 2
-
- for row in range(self.grid_size):
- self.cells.append([])
-
- for col in range(self.grid_size):
- left_neighbour = self.cells[row][col - 1] if col > 0 else None
- top_neighbour = self.cells[row - 1][col] if row > 0 else None
-
- cell_type = self.map[row][col]
-
- if cell_type in ["line", "corner", "t_junction", "cross"]:
- cell = PowerLine(cell_type, x, y, left_neighbour, top_neighbour)
- elif cell_type == "power_source":
- cell = PowerSource(x, y, left_neighbour, top_neighbour)
- self.power_sources.append(cell)
- elif cell_type == "house":
- cell = House(x, y, left_neighbour, top_neighbour)
- self.houses.append(cell)
-
- self.spritelist.append(cell)
- self.cells[row].append(cell)
-
- if left_neighbour:
- left_neighbour.right_neighbour = cell
- if top_neighbour:
- top_neighbour.bottom_neighbour = cell
-
- x += 64
- x = (self.window.width / 2) - (self.grid_size * 64) / 2
- y -= 64
-
- arcade.schedule(self.update_grid, 1 / 8)
-
- def update_grid(self, _):
- for row in self.cells:
- for power_line in row:
- if power_line.cell_type != "power_source":
- power_line.powered = False
-
- queue = deque(self.power_sources)
- visited = set()
-
- while queue:
- current = queue.popleft()
-
- if id(current) in visited:
- continue
-
- visited.add(id(current))
-
- current.powered = True
-
- for connected_neighbour in current.get_connected_neighbours():
- if id(connected_neighbour) not in visited:
- queue.append(connected_neighbour)
-
- for row in self.cells:
- for cell in row:
- cell.update_visual()
-
- self.check_win()
-
- def check_win(self):
- for row in self.cells:
- for cell in row:
- if cell.cell_type == "power_source":
- continue
- elif cell.cell_type == "house":
- if not len(cell.get_connected_neighbours()) >= 1:
- return
- else:
- continue
-
- if len(cell.get_connected_neighbours(True)) != len(NEIGHBOURS[cell.rotation]):
- return
-
- self.won_label.visible = True
- self.spritelist.visible = False
-
- arcade.unschedule(self.update_grid)
-
- def on_mouse_press(self, x, y, button, modifiers):
- for row in self.cells:
- for cell in row:
- if cell.cell_type in ["house", "power_source"]:
- continue
-
- if cell.rect.point_in_rect((x, y)):
- self.wire_rotations += 1
- cell.next_rotation(self.settings["sfx"], self.settings.get("sfx_volume", 50))
-
- def on_draw(self):
- super().on_draw()
- self.spritelist.draw()
-
- def on_update(self, delta_time):
- self.info_label.text = f"Time left: {int(time.perf_counter() - self.start)}s Wire Rotations: {self.wire_rotations}"
-
- def main_exit(self):
- from menus.main import Main
+import arcade, arcade.gui, json, time
+
+from utils.constants import button_style, NEIGHBOURS
+from utils.preload import button_texture, button_hovered_texture
+
+from collections import deque
+
+from game.level_generator import generate_map
+from game.cells import *
+
+class Game(arcade.gui.UIView):
+ def __init__(self, pypresence_client, grid_size, source_count=None, house_count=None):
+ super().__init__()
+
+ self.pypresence_client = pypresence_client
+ self.pypresence_client.update(state='In Game', start=self.pypresence_client.start_time)
+
+ self.grid_size = grid_size
+ self.source_count = source_count
+ self.house_count = house_count
+
+ self.start = time.perf_counter()
+ self.wire_rotations = 0
+ self.cells = []
+ self.power_sources = []
+ self.houses = []
+
+ self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
+ self.map = generate_map(self.grid_size, int((self.grid_size * self.grid_size) / 10) if not source_count else source_count, int((self.grid_size * self.grid_size) / 5) if not house_count else house_count)
+
+ self.spritelist = arcade.SpriteList()
+
+ with open("settings.json", "r") as file:
+ self.settings = json.load(file)
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
+ self.back_button.on_click = lambda event: self.main_exit()
+ self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
+
+ self.won_label = self.anchor.add(arcade.gui.UILabel(text="You won!", font_size=48), anchor_x="center", anchor_y="center")
+ self.won_label.visible = False
+
+ self.info_label = self.anchor.add(arcade.gui.UILabel("Time spent: 0s Wire Rotations: 0", font_size=24), anchor_x="center", anchor_y="top")
+
+ x = (self.window.width / 2) - (self.grid_size * 64) / 2
+ y = (self.window.height / 2) + (self.grid_size * 64) / 2
+
+ for row in range(self.grid_size):
+ self.cells.append([])
+
+ for col in range(self.grid_size):
+ left_neighbour = self.cells[row][col - 1] if col > 0 else None
+ top_neighbour = self.cells[row - 1][col] if row > 0 else None
+
+ cell_type = self.map[row][col]
+
+ if cell_type in ["line", "corner", "t_junction", "cross"]:
+ cell = PowerLine(cell_type, x, y, left_neighbour, top_neighbour)
+ elif cell_type == "power_source":
+ cell = PowerSource(x, y, left_neighbour, top_neighbour)
+ self.power_sources.append(cell)
+ elif cell_type == "house":
+ cell = House(x, y, left_neighbour, top_neighbour)
+ self.houses.append(cell)
+
+ self.spritelist.append(cell)
+ self.cells[row].append(cell)
+
+ if left_neighbour:
+ left_neighbour.right_neighbour = cell
+ if top_neighbour:
+ top_neighbour.bottom_neighbour = cell
+
+ x += 64
+ x = (self.window.width / 2) - (self.grid_size * 64) / 2
+ y -= 64
+
+ arcade.schedule(self.update_grid, 1 / 8)
+
+ def update_grid(self, _):
+ for row in self.cells:
+ for power_line in row:
+ if power_line.cell_type != "power_source":
+ power_line.powered = False
+
+ queue = deque(self.power_sources)
+ visited = set()
+
+ while queue:
+ current = queue.popleft()
+
+ if id(current) in visited:
+ continue
+
+ visited.add(id(current))
+
+ current.powered = True
+
+ for connected_neighbour in current.get_connected_neighbours():
+ if id(connected_neighbour) not in visited:
+ queue.append(connected_neighbour)
+
+ for row in self.cells:
+ for cell in row:
+ cell.update_visual()
+
+ self.check_win()
+
+ def check_win(self):
+ for row in self.cells:
+ for cell in row:
+ if cell.cell_type == "power_source":
+ continue
+ elif cell.cell_type == "house":
+ if not len(cell.get_connected_neighbours()) >= 1:
+ return
+ else:
+ continue
+
+ if len(cell.get_connected_neighbours(True)) != len(NEIGHBOURS[cell.rotation]):
+ return
+
+ self.won_label.visible = True
+ self.spritelist.visible = False
+
+ arcade.unschedule(self.update_grid)
+
+ def on_mouse_press(self, x, y, button, modifiers):
+ for row in self.cells:
+ for cell in row:
+ if cell.cell_type in ["house", "power_source"]:
+ continue
+
+ if cell.rect.point_in_rect((x, y)):
+ self.wire_rotations += 1
+ cell.next_rotation(self.settings["sfx"], self.settings.get("sfx_volume", 50))
+
+ def on_draw(self):
+ super().on_draw()
+ self.spritelist.draw()
+
+ def on_update(self, delta_time):
+ self.info_label.text = f"Time left: {int(time.perf_counter() - self.start)}s Wire Rotations: {self.wire_rotations}"
+
+ def main_exit(self):
+ from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
\ No newline at end of file
diff --git a/menus/custom_difficulty.py b/menus/custom_difficulty.py
index 1141b6b..ea8246e 100644
--- a/menus/custom_difficulty.py
+++ b/menus/custom_difficulty.py
@@ -1,41 +1,41 @@
-import arcade, arcade.gui
-
-from utils.constants import CUSTOM_DIFFICULTY_SETTINGS, slider_style, button_style
-from utils.preload import button_texture, button_hovered_texture
-
-class CustomDifficulty(arcade.gui.UIView):
- def __init__(self, pypresence_client):
- super().__init__()
-
- self.pypresence_client = pypresence_client
- self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
- self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_between=self.window.height / 10), anchor_x="center", anchor_y="top")
-
- self.custom_settings = {}
- self.custom_setting_labels = {}
-
- def set_custom_setting(self, key, value):
- value = int(value)
- self.custom_settings[key] = value
- self.custom_setting_labels[key].text = f"{next(setting_list[1] for setting_list in CUSTOM_DIFFICULTY_SETTINGS if setting_list[0] == key)}: {value}"
-
- def on_show_view(self):
- super().on_show_view()
-
- self.box.add(arcade.gui.UILabel(text="Custom Difficulty Selector", font_size=32))
- self.box.add(arcade.gui.UISpace(height=self.window.height / 20))
-
- for custom_setting_key, custom_setting_name, min_value, max_value in CUSTOM_DIFFICULTY_SETTINGS:
- self.custom_settings[custom_setting_key] = int((max_value - min_value) / 2)
- self.custom_setting_labels[custom_setting_key] = self.box.add(arcade.gui.UILabel(text=f"{custom_setting_name}: {int((max_value - min_value) / 2)}", font_size=28))
-
- slider = self.box.add(arcade.gui.UISlider(step=1, min_value=min_value, max_value=max_value, value=int((max_value - min_value) / 2), style=slider_style, width=self.window.width / 2, height=self.window.height / 15))
- slider._render_steps = lambda surface: None
- slider.on_change = lambda event, key=custom_setting_key: self.set_custom_setting(key, event.new_value)
-
- self.play_button = self.anchor.add(arcade.gui.UITextureButton(text="Play", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10), anchor_x="center", anchor_y="bottom")
- self.play_button.on_click = lambda event: self.play()
-
- def play(self):
- from game.play import Game
- self.window.show_view(Game(self.pypresence_client, self.custom_settings["size"], self.custom_settings["source_count"], self.custom_settings["house_count"]))
+import arcade, arcade.gui
+
+from utils.constants import CUSTOM_DIFFICULTY_SETTINGS, slider_style, button_style
+from utils.preload import button_texture, button_hovered_texture
+
+class CustomDifficulty(arcade.gui.UIView):
+ def __init__(self, pypresence_client):
+ super().__init__()
+
+ self.pypresence_client = pypresence_client
+ self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
+ self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_between=self.window.height / 10), anchor_x="center", anchor_y="top")
+
+ self.custom_settings = {}
+ self.custom_setting_labels = {}
+
+ def set_custom_setting(self, key, value):
+ value = int(value)
+ self.custom_settings[key] = value
+ self.custom_setting_labels[key].text = f"{next(setting_list[1] for setting_list in CUSTOM_DIFFICULTY_SETTINGS if setting_list[0] == key)}: {value}"
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.box.add(arcade.gui.UILabel(text="Custom Difficulty Selector", font_size=32))
+ self.box.add(arcade.gui.UISpace(height=self.window.height / 20))
+
+ for custom_setting_key, custom_setting_name, min_value, max_value in CUSTOM_DIFFICULTY_SETTINGS:
+ self.custom_settings[custom_setting_key] = int((max_value - min_value) / 2)
+ self.custom_setting_labels[custom_setting_key] = self.box.add(arcade.gui.UILabel(text=f"{custom_setting_name}: {int((max_value - min_value) / 2)}", font_size=28))
+
+ slider = self.box.add(arcade.gui.UISlider(step=1, min_value=min_value, max_value=max_value, value=int((max_value - min_value) / 2), style=slider_style, width=self.window.width / 2, height=self.window.height / 15))
+ slider._render_steps = lambda surface: None
+ slider.on_change = lambda event, key=custom_setting_key: self.set_custom_setting(key, event.new_value)
+
+ self.play_button = self.anchor.add(arcade.gui.UITextureButton(text="Play", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10), anchor_x="center", anchor_y="bottom")
+ self.play_button.on_click = lambda event: self.play()
+
+ def play(self):
+ from game.play import Game
+ self.window.show_view(Game(self.pypresence_client, self.custom_settings["size"], self.custom_settings["source_count"], self.custom_settings["house_count"]))
diff --git a/menus/difficulty_selector.py b/menus/difficulty_selector.py
index e8a5a2f..8fa1117 100644
--- a/menus/difficulty_selector.py
+++ b/menus/difficulty_selector.py
@@ -1,43 +1,43 @@
-import arcade, arcade.gui
-
-from utils.preload import button_texture, button_hovered_texture
-from utils.constants import big_button_style, button_style
-
-class DifficultySelector(arcade.gui.UIView):
- def __init__(self, pypresence_client):
- super().__init__()
-
- self.anchor = self.add_widget(arcade.gui.UIAnchorLayout())
- self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center')
-
- self.pypresence_client = pypresence_client
- self.pypresence_client.update(state='In Menu', details='In Difficulty Selector', start=self.pypresence_client.start_time)
-
- def on_show_view(self):
- super().on_show_view()
-
- self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
- self.back_button.on_click = lambda event: self.main_exit()
- self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
-
- self.box.add(arcade.gui.UILabel(text="Difficulty Selector", font_size=32))
-
- for difficulty in ["7x7", "8x8", "9x9", "10x10", "11x11", "12x12", "Custom"]:
- button = self.box.add(arcade.gui.UITextureButton(text=difficulty, width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=big_button_style))
-
- if not difficulty == "Custom":
- button.on_click = lambda e, difficulty=difficulty: self.play(int(difficulty.split("x")[0]))
- else:
- button.on_click = lambda e: self.custom_difficulty()
-
- def custom_difficulty(self):
- from menus.custom_difficulty import CustomDifficulty
- self.window.show_view(CustomDifficulty(self.pypresence_client))
-
- def play(self, difficulty):
- from game.play import Game
- self.window.show_view(Game(self.pypresence_client, difficulty))
-
- def main_exit(self):
- from menus.main import Main
+import arcade, arcade.gui
+
+from utils.preload import button_texture, button_hovered_texture
+from utils.constants import big_button_style, button_style
+
+class DifficultySelector(arcade.gui.UIView):
+ def __init__(self, pypresence_client):
+ super().__init__()
+
+ self.anchor = self.add_widget(arcade.gui.UIAnchorLayout())
+ self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center')
+
+ self.pypresence_client = pypresence_client
+ self.pypresence_client.update(state='In Menu', details='In Difficulty Selector', start=self.pypresence_client.start_time)
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
+ self.back_button.on_click = lambda event: self.main_exit()
+ self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
+
+ self.box.add(arcade.gui.UILabel(text="Difficulty Selector", font_size=32))
+
+ for difficulty in ["7x7", "8x8", "9x9", "10x10", "11x11", "12x12", "Custom"]:
+ button = self.box.add(arcade.gui.UITextureButton(text=difficulty, width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=big_button_style))
+
+ if not difficulty == "Custom":
+ button.on_click = lambda e, difficulty=difficulty: self.play(int(difficulty.split("x")[0]))
+ else:
+ button.on_click = lambda e: self.custom_difficulty()
+
+ def custom_difficulty(self):
+ from menus.custom_difficulty import CustomDifficulty
+ self.window.show_view(CustomDifficulty(self.pypresence_client))
+
+ def play(self, difficulty):
+ from game.play import Game
+ self.window.show_view(Game(self.pypresence_client, difficulty))
+
+ def main_exit(self):
+ from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
\ No newline at end of file
diff --git a/menus/main.py b/menus/main.py
index f7bf635..3610840 100644
--- a/menus/main.py
+++ b/menus/main.py
@@ -1,74 +1,74 @@
-import arcade, arcade.gui, asyncio, pypresence, time, copy, json
-from utils.preload import button_texture, button_hovered_texture
-from utils.constants import big_button_style, discord_presence_id
-from utils.utils import FakePyPresence
-
-class Main(arcade.gui.UIView):
- def __init__(self, pypresence_client=None):
- super().__init__()
-
- self.anchor = self.add_widget(arcade.gui.UIAnchorLayout())
- self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center')
-
- self.pypresence_client = pypresence_client
-
- with open("settings.json", "r") as file:
- self.settings_dict = json.load(file)
-
- if self.settings_dict.get('discord_rpc', True):
- if self.pypresence_client == None: # Game has started
- try:
- asyncio.get_event_loop()
- except:
- asyncio.set_event_loop(asyncio.new_event_loop())
- try:
- self.pypresence_client = pypresence.Presence(discord_presence_id)
- self.pypresence_client.connect()
- self.pypresence_client.start_time = time.time()
- except:
- self.pypresence_client = FakePyPresence()
- self.pypresence_client.start_time = time.time()
-
- elif isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
- # get start time from old object
- start_time = copy.deepcopy(self.pypresence_client.start_time)
- try:
- self.pypresence_client = pypresence.Presence(discord_presence_id)
- self.pypresence_client.connect()
- self.pypresence_client.start_time = start_time
- except:
- self.pypresence_client = FakePyPresence()
- self.pypresence_client.start_time = start_time
-
- self.pypresence_client.update(state='In Menu', details='In Main Menu', start=self.pypresence_client.start_time)
- else: # game has started, but the user has disabled RPC in the settings.
- self.pypresence_client = FakePyPresence()
- self.pypresence_client.start_time = time.time()
-
- self.pypresence_client.update(state='In Menu', details='In Main Menu', start=self.pypresence_client.start_time)
-
- def on_show_view(self):
- super().on_show_view()
-
- self.title_label = self.box.add(arcade.gui.UILabel(text="Connect the Current", font_name="Roboto", font_size=48))
-
- self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
- self.play_button.on_click = lambda event: self.play()
-
- self.tutorial_button = self.box.add(arcade.gui.UITextureButton(text="Tutorial", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
- self.tutorial_button.on_click = lambda event: self.tutorial()
-
- self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
- self.settings_button.on_click = lambda event: self.settings()
-
- def play(self):
- from menus.difficulty_selector import DifficultySelector
- self.window.show_view(DifficultySelector(self.pypresence_client))
-
- def tutorial(self):
- from menus.tutorial import Tutorial
- self.window.show_view(Tutorial(self.pypresence_client))
-
- def settings(self):
- from menus.settings import Settings
- self.window.show_view(Settings(self.pypresence_client))
+import arcade, arcade.gui, asyncio, pypresence, time, copy, json
+from utils.preload import button_texture, button_hovered_texture
+from utils.constants import big_button_style, discord_presence_id
+from utils.utils import FakePyPresence
+
+class Main(arcade.gui.UIView):
+ def __init__(self, pypresence_client=None):
+ super().__init__()
+
+ self.anchor = self.add_widget(arcade.gui.UIAnchorLayout())
+ self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center')
+
+ self.pypresence_client = pypresence_client
+
+ with open("settings.json", "r") as file:
+ self.settings_dict = json.load(file)
+
+ if self.settings_dict.get('discord_rpc', True):
+ if self.pypresence_client == None: # Game has started
+ try:
+ asyncio.get_event_loop()
+ except:
+ asyncio.set_event_loop(asyncio.new_event_loop())
+ try:
+ self.pypresence_client = pypresence.Presence(discord_presence_id)
+ self.pypresence_client.connect()
+ self.pypresence_client.start_time = time.time()
+ except:
+ self.pypresence_client = FakePyPresence()
+ self.pypresence_client.start_time = time.time()
+
+ elif isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
+ # get start time from old object
+ start_time = copy.deepcopy(self.pypresence_client.start_time)
+ try:
+ self.pypresence_client = pypresence.Presence(discord_presence_id)
+ self.pypresence_client.connect()
+ self.pypresence_client.start_time = start_time
+ except:
+ self.pypresence_client = FakePyPresence()
+ self.pypresence_client.start_time = start_time
+
+ self.pypresence_client.update(state='In Menu', details='In Main Menu', start=self.pypresence_client.start_time)
+ else: # game has started, but the user has disabled RPC in the settings.
+ self.pypresence_client = FakePyPresence()
+ self.pypresence_client.start_time = time.time()
+
+ self.pypresence_client.update(state='In Menu', details='In Main Menu', start=self.pypresence_client.start_time)
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.title_label = self.box.add(arcade.gui.UILabel(text="Connect the Current", font_name="Roboto", font_size=48))
+
+ self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
+ self.play_button.on_click = lambda event: self.play()
+
+ self.tutorial_button = self.box.add(arcade.gui.UITextureButton(text="Tutorial", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
+ self.tutorial_button.on_click = lambda event: self.tutorial()
+
+ self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
+ self.settings_button.on_click = lambda event: self.settings()
+
+ def play(self):
+ from menus.difficulty_selector import DifficultySelector
+ self.window.show_view(DifficultySelector(self.pypresence_client))
+
+ def tutorial(self):
+ from menus.tutorial import Tutorial
+ self.window.show_view(Tutorial(self.pypresence_client))
+
+ def settings(self):
+ from menus.settings import Settings
+ self.window.show_view(Settings(self.pypresence_client))
diff --git a/menus/settings.py b/menus/settings.py
index 3c0fd96..18548b6 100644
--- a/menus/settings.py
+++ b/menus/settings.py
@@ -1,284 +1,284 @@
-import copy, pypresence, json, os
-
-import arcade, arcade.gui
-
-from utils.constants import button_style, dropdown_style, slider_style, settings, discord_presence_id, settings_start_category
-from utils.utils import FakePyPresence
-from utils.preload import button_texture, button_hovered_texture
-
-from arcade.gui import UIBoxLayout, UIAnchorLayout
-
-class Settings(arcade.gui.UIView):
- def __init__(self, pypresence_client, *args):
- super().__init__()
-
- self.args = args
-
- with open("settings.json", "r") as file:
- self.settings_dict = json.load(file)
-
- self.pypresence_client = pypresence_client
- self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=self.pypresence_client.start_time)
-
- self.slider_labels = {}
- self.sliders = {}
-
- self.on_radiobuttons = {}
- self.off_radiobuttons = {}
-
- self.current_category = settings_start_category
-
- self.modified_settings = {}
-
- def create_layouts(self):
- self.anchor = self.add_widget(UIAnchorLayout(size_hint=(1, 1)))
-
- self.box = UIBoxLayout(space_between=50, align="center", vertical=False)
- self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75)
-
- self.top_box = UIBoxLayout(space_between=self.window.width / 160, vertical=False)
- self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10)
-
- self.key_layout = self.box.add(UIBoxLayout(space_between=20, align='left'))
- self.value_layout = self.box.add(UIBoxLayout(space_between=13, align='left'))
-
- def on_show_view(self):
- super().on_show_view()
-
- self.create_layouts()
-
- self.ui.push_handlers(self)
-
- self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
- self.back_button.on_click = lambda event: self.main_exit()
- self.top_box.add(self.back_button)
-
- self.display_categories()
-
- self.display_category(settings_start_category)
-
- def display_categories(self):
- for category in settings:
- category_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50)
-
- if not category == "Credits":
- category_button.on_click = lambda event, category=category: self.display_category(category)
- else:
- category_button.on_click = lambda event: self.credits()
-
- self.top_box.add(category_button)
-
- def display_category(self, category):
- if hasattr(self, 'apply_button'):
- self.anchor.remove(self.apply_button)
- del self.apply_button
-
- if hasattr(self, 'credits_label'):
- self.anchor.remove(self.credits_label)
- del self.credits_label
-
- self.current_category = category
-
- self.key_layout.clear()
- self.value_layout.clear()
-
- for setting in settings[category]:
- label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.WHITE )
- self.key_layout.add(label)
-
- setting_dict = settings[category][setting]
-
- if setting_dict['type'] == "option":
- dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=dropdown_style, dropdown_style=dropdown_style, primary_style=dropdown_style)
- dropdown.on_change = lambda _, setting=setting, dropdown=dropdown: self.update(setting, dropdown.value, "option")
- self.value_layout.add(dropdown)
-
- elif setting_dict['type'] == "bool":
- button_layout = self.value_layout.add(arcade.gui.UIBoxLayout(space_between=50, vertical=False))
-
- on_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50)
- self.on_radiobuttons[setting] = on_radiobutton
- on_radiobutton.on_click = lambda _, setting=setting: self.update(setting, True, "bool")
- button_layout.add(on_radiobutton)
-
- off_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50)
- self.off_radiobuttons[setting] = off_radiobutton
- off_radiobutton.on_click = lambda _, setting=setting: self.update(setting, False, "bool")
- button_layout.add(off_radiobutton)
-
- if self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]):
- self.set_highlighted_style(on_radiobutton)
- self.set_normal_style(off_radiobutton)
- else:
- self.set_highlighted_style(off_radiobutton)
- self.set_normal_style(on_radiobutton)
-
- elif setting_dict['type'] == "slider":
- if setting == "FPS Limit":
- if self.settings_dict.get(setting_dict["config_key"]) == 0:
- label_text = "FPS Limit: Disabled"
- else:
- label_text = f"FPS Limit: {self.settings_dict.get(setting_dict['config_key'], setting_dict['default'])}"
- else:
- label_text = f"{setting}: {int(self.settings_dict.get(setting_dict['config_key'], setting_dict['default']))}"
-
- label.text = label_text
-
- self.slider_labels[setting] = label
-
- slider = arcade.gui.UISlider(width=400, height=50, value=self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]), min_value=setting_dict['min'], max_value=setting_dict['max'], style=slider_style)
- slider.on_change = lambda _, setting=setting, slider=slider: self.update(setting, slider.value, "slider")
-
- self.sliders[setting] = slider
- self.value_layout.add(slider)
-
- self.apply_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100)
- self.apply_button.on_click = lambda event: self.apply_settings()
- self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10)
-
- def apply_settings(self):
- for config_key, value in self.modified_settings.items():
- self.settings_dict[config_key] = value
-
- if self.settings_dict['window_mode'] == "Fullscreen":
- self.window.set_fullscreen(True)
- else:
- self.window.set_fullscreen(False)
- width, height = map(int, self.settings_dict['resolution'].split('x'))
- self.window.set_size(width, height)
-
- if self.settings_dict['vsync']:
- self.window.set_vsync(True)
- display_mode = self.window.display.get_default_screen().get_mode()
- refresh_rate = display_mode.rate
- self.window.set_update_rate(1 / refresh_rate)
- self.window.set_draw_rate(1 / refresh_rate)
-
- elif not self.settings_dict['fps_limit'] == 0:
- self.window.set_vsync(False)
- self.window.set_update_rate(1 / self.settings_dict['fps_limit'])
- self.window.set_draw_rate(1 / self.settings_dict['fps_limit'])
-
- else:
- self.window.set_vsync(False)
- self.window.set_update_rate(1 / 99999999)
- self.window.set_draw_rate(1 / 99999999)
-
- if self.settings_dict['discord_rpc']:
- if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
- start_time = copy.deepcopy(self.pypresence_client.start_time)
- self.pypresence_client.close()
- del self.pypresence_client
- try:
- self.pypresence_client = pypresence.Presence(discord_presence_id)
- self.pypresence_client.connect()
- self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=start_time)
- self.pypresence_client.start_time = start_time
- except:
- self.pypresence_client = FakePyPresence()
- self.pypresence_client.start_time = start_time
- else:
- if not isinstance(self.pypresence_client, FakePyPresence):
- start_time = copy.deepcopy(self.pypresence_client.start_time)
- self.pypresence_client.update()
- self.pypresence_client.close()
- del self.pypresence_client
- self.pypresence_client = FakePyPresence()
- self.pypresence_client.start_time = start_time
-
- self.ui_cleanup()
-
- self.ui = arcade.gui.UIManager()
- self.ui.enable()
-
- self.create_layouts()
-
- self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
- self.back_button.on_click = lambda event: self.main_exit()
- self.top_box.add(self.back_button)
-
- self.display_categories()
-
- self.display_category(self.current_category)
-
- with open("settings.json", "w") as file:
- file.write(json.dumps(self.settings_dict, indent=4))
-
- def update(self, setting=None, button_state=None, setting_type="bool"):
- setting_dict = settings[self.current_category][setting]
- config_key = settings[self.current_category][setting]["config_key"]
-
- if setting_type == "option":
- self.modified_settings[config_key] = button_state
-
- elif setting_type == "bool":
- self.modified_settings[config_key] = button_state
-
- if button_state:
- self.set_highlighted_style(self.on_radiobuttons[setting])
- self.set_normal_style(self.off_radiobuttons[setting])
- else:
- self.set_highlighted_style(self.off_radiobuttons[setting])
- self.set_normal_style(self.on_radiobuttons[setting])
-
- elif setting_type == "slider":
- new_value = int(button_state)
-
- self.modified_settings[config_key] = new_value
- self.sliders[setting].value = new_value
-
- if setting == "FPS Limit":
- if new_value == 0:
- label_text = "FPS Limit: Disabled"
- else:
- label_text = f"FPS Limit: {str(new_value).rjust(8)}"
- else:
- label_text = f"{setting}: {str(new_value).rjust(8)}"
-
- self.slider_labels[setting].text = label_text
-
- def credits(self):
- if hasattr(self, 'apply_button'):
- self.anchor.remove(self.apply_button)
- del self.apply_button
-
- if hasattr(self, 'credits_label'):
- self.anchor.remove(self.credits_label)
- del self.credits_label
-
- self.key_layout.clear()
- self.value_layout.clear()
-
- with open('CREDITS', 'r') as file:
- text = file.read()
-
- if self.window.width == 3840:
- font_size = 30
- elif self.window.width == 2560:
- font_size = 20
- elif self.window.width == 1920:
- font_size = 17
- elif self.window.width >= 1440:
- font_size = 14
- else:
- font_size = 12
-
- self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Roboto", font_size=font_size, align="center", multiline=True)
-
- self.key_layout.add(self.credits_label)
-
- def set_highlighted_style(self, element):
- element.texture = button_hovered_texture
- element.texture_hovered = button_texture
-
- def set_normal_style(self, element):
- element.texture_hovered = button_hovered_texture
- element.texture = button_texture
-
- def main_exit(self):
- from menus.main import Main
- self.window.show_view(Main(self.pypresence_client, *self.args))
-
- def ui_cleanup(self):
- self.ui.clear()
- del self.ui
+import copy, pypresence, json, os
+
+import arcade, arcade.gui
+
+from utils.constants import button_style, dropdown_style, slider_style, settings, discord_presence_id, settings_start_category
+from utils.utils import FakePyPresence
+from utils.preload import button_texture, button_hovered_texture
+
+from arcade.gui import UIBoxLayout, UIAnchorLayout
+
+class Settings(arcade.gui.UIView):
+ def __init__(self, pypresence_client, *args):
+ super().__init__()
+
+ self.args = args
+
+ with open("settings.json", "r") as file:
+ self.settings_dict = json.load(file)
+
+ self.pypresence_client = pypresence_client
+ self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=self.pypresence_client.start_time)
+
+ self.slider_labels = {}
+ self.sliders = {}
+
+ self.on_radiobuttons = {}
+ self.off_radiobuttons = {}
+
+ self.current_category = settings_start_category
+
+ self.modified_settings = {}
+
+ def create_layouts(self):
+ self.anchor = self.add_widget(UIAnchorLayout(size_hint=(1, 1)))
+
+ self.box = UIBoxLayout(space_between=50, align="center", vertical=False)
+ self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75)
+
+ self.top_box = UIBoxLayout(space_between=self.window.width / 160, vertical=False)
+ self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10)
+
+ self.key_layout = self.box.add(UIBoxLayout(space_between=20, align='left'))
+ self.value_layout = self.box.add(UIBoxLayout(space_between=13, align='left'))
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.create_layouts()
+
+ self.ui.push_handlers(self)
+
+ self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
+ self.back_button.on_click = lambda event: self.main_exit()
+ self.top_box.add(self.back_button)
+
+ self.display_categories()
+
+ self.display_category(settings_start_category)
+
+ def display_categories(self):
+ for category in settings:
+ category_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50)
+
+ if not category == "Credits":
+ category_button.on_click = lambda event, category=category: self.display_category(category)
+ else:
+ category_button.on_click = lambda event: self.credits()
+
+ self.top_box.add(category_button)
+
+ def display_category(self, category):
+ if hasattr(self, 'apply_button'):
+ self.anchor.remove(self.apply_button)
+ del self.apply_button
+
+ if hasattr(self, 'credits_label'):
+ self.anchor.remove(self.credits_label)
+ del self.credits_label
+
+ self.current_category = category
+
+ self.key_layout.clear()
+ self.value_layout.clear()
+
+ for setting in settings[category]:
+ label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.WHITE )
+ self.key_layout.add(label)
+
+ setting_dict = settings[category][setting]
+
+ if setting_dict['type'] == "option":
+ dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=dropdown_style, dropdown_style=dropdown_style, primary_style=dropdown_style)
+ dropdown.on_change = lambda _, setting=setting, dropdown=dropdown: self.update(setting, dropdown.value, "option")
+ self.value_layout.add(dropdown)
+
+ elif setting_dict['type'] == "bool":
+ button_layout = self.value_layout.add(arcade.gui.UIBoxLayout(space_between=50, vertical=False))
+
+ on_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50)
+ self.on_radiobuttons[setting] = on_radiobutton
+ on_radiobutton.on_click = lambda _, setting=setting: self.update(setting, True, "bool")
+ button_layout.add(on_radiobutton)
+
+ off_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50)
+ self.off_radiobuttons[setting] = off_radiobutton
+ off_radiobutton.on_click = lambda _, setting=setting: self.update(setting, False, "bool")
+ button_layout.add(off_radiobutton)
+
+ if self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]):
+ self.set_highlighted_style(on_radiobutton)
+ self.set_normal_style(off_radiobutton)
+ else:
+ self.set_highlighted_style(off_radiobutton)
+ self.set_normal_style(on_radiobutton)
+
+ elif setting_dict['type'] == "slider":
+ if setting == "FPS Limit":
+ if self.settings_dict.get(setting_dict["config_key"]) == 0:
+ label_text = "FPS Limit: Disabled"
+ else:
+ label_text = f"FPS Limit: {self.settings_dict.get(setting_dict['config_key'], setting_dict['default'])}"
+ else:
+ label_text = f"{setting}: {int(self.settings_dict.get(setting_dict['config_key'], setting_dict['default']))}"
+
+ label.text = label_text
+
+ self.slider_labels[setting] = label
+
+ slider = arcade.gui.UISlider(width=400, height=50, value=self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]), min_value=setting_dict['min'], max_value=setting_dict['max'], style=slider_style)
+ slider.on_change = lambda _, setting=setting, slider=slider: self.update(setting, slider.value, "slider")
+
+ self.sliders[setting] = slider
+ self.value_layout.add(slider)
+
+ self.apply_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100)
+ self.apply_button.on_click = lambda event: self.apply_settings()
+ self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10)
+
+ def apply_settings(self):
+ for config_key, value in self.modified_settings.items():
+ self.settings_dict[config_key] = value
+
+ if self.settings_dict['window_mode'] == "Fullscreen":
+ self.window.set_fullscreen(True)
+ else:
+ self.window.set_fullscreen(False)
+ width, height = map(int, self.settings_dict['resolution'].split('x'))
+ self.window.set_size(width, height)
+
+ if self.settings_dict['vsync']:
+ self.window.set_vsync(True)
+ display_mode = self.window.display.get_default_screen().get_mode()
+ refresh_rate = display_mode.rate
+ self.window.set_update_rate(1 / refresh_rate)
+ self.window.set_draw_rate(1 / refresh_rate)
+
+ elif not self.settings_dict['fps_limit'] == 0:
+ self.window.set_vsync(False)
+ self.window.set_update_rate(1 / self.settings_dict['fps_limit'])
+ self.window.set_draw_rate(1 / self.settings_dict['fps_limit'])
+
+ else:
+ self.window.set_vsync(False)
+ self.window.set_update_rate(1 / 99999999)
+ self.window.set_draw_rate(1 / 99999999)
+
+ if self.settings_dict['discord_rpc']:
+ if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
+ start_time = copy.deepcopy(self.pypresence_client.start_time)
+ self.pypresence_client.close()
+ del self.pypresence_client
+ try:
+ self.pypresence_client = pypresence.Presence(discord_presence_id)
+ self.pypresence_client.connect()
+ self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=start_time)
+ self.pypresence_client.start_time = start_time
+ except:
+ self.pypresence_client = FakePyPresence()
+ self.pypresence_client.start_time = start_time
+ else:
+ if not isinstance(self.pypresence_client, FakePyPresence):
+ start_time = copy.deepcopy(self.pypresence_client.start_time)
+ self.pypresence_client.update()
+ self.pypresence_client.close()
+ del self.pypresence_client
+ self.pypresence_client = FakePyPresence()
+ self.pypresence_client.start_time = start_time
+
+ self.ui_cleanup()
+
+ self.ui = arcade.gui.UIManager()
+ self.ui.enable()
+
+ self.create_layouts()
+
+ self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
+ self.back_button.on_click = lambda event: self.main_exit()
+ self.top_box.add(self.back_button)
+
+ self.display_categories()
+
+ self.display_category(self.current_category)
+
+ with open("settings.json", "w") as file:
+ file.write(json.dumps(self.settings_dict, indent=4))
+
+ def update(self, setting=None, button_state=None, setting_type="bool"):
+ setting_dict = settings[self.current_category][setting]
+ config_key = settings[self.current_category][setting]["config_key"]
+
+ if setting_type == "option":
+ self.modified_settings[config_key] = button_state
+
+ elif setting_type == "bool":
+ self.modified_settings[config_key] = button_state
+
+ if button_state:
+ self.set_highlighted_style(self.on_radiobuttons[setting])
+ self.set_normal_style(self.off_radiobuttons[setting])
+ else:
+ self.set_highlighted_style(self.off_radiobuttons[setting])
+ self.set_normal_style(self.on_radiobuttons[setting])
+
+ elif setting_type == "slider":
+ new_value = int(button_state)
+
+ self.modified_settings[config_key] = new_value
+ self.sliders[setting].value = new_value
+
+ if setting == "FPS Limit":
+ if new_value == 0:
+ label_text = "FPS Limit: Disabled"
+ else:
+ label_text = f"FPS Limit: {str(new_value).rjust(8)}"
+ else:
+ label_text = f"{setting}: {str(new_value).rjust(8)}"
+
+ self.slider_labels[setting].text = label_text
+
+ def credits(self):
+ if hasattr(self, 'apply_button'):
+ self.anchor.remove(self.apply_button)
+ del self.apply_button
+
+ if hasattr(self, 'credits_label'):
+ self.anchor.remove(self.credits_label)
+ del self.credits_label
+
+ self.key_layout.clear()
+ self.value_layout.clear()
+
+ with open('CREDITS', 'r') as file:
+ text = file.read()
+
+ if self.window.width == 3840:
+ font_size = 30
+ elif self.window.width == 2560:
+ font_size = 20
+ elif self.window.width == 1920:
+ font_size = 17
+ elif self.window.width >= 1440:
+ font_size = 14
+ else:
+ font_size = 12
+
+ self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Roboto", font_size=font_size, align="center", multiline=True)
+
+ self.key_layout.add(self.credits_label)
+
+ def set_highlighted_style(self, element):
+ element.texture = button_hovered_texture
+ element.texture_hovered = button_texture
+
+ def set_normal_style(self, element):
+ element.texture_hovered = button_hovered_texture
+ element.texture = button_texture
+
+ def main_exit(self):
+ from menus.main import Main
+ self.window.show_view(Main(self.pypresence_client, *self.args))
+
+ def ui_cleanup(self):
+ self.ui.clear()
+ del self.ui
diff --git a/menus/tutorial.py b/menus/tutorial.py
index 3f71d04..c512f65 100644
--- a/menus/tutorial.py
+++ b/menus/tutorial.py
@@ -1,37 +1,37 @@
-import arcade, arcade.gui
-
-from utils.preload import button_texture, button_hovered_texture
-from utils.constants import button_style
-
-TUTORIAL_TEXT = """
-In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
-- Every line has to be connected on all of it's sides.
-- When needed, you might have to create loops of power or branches with no house linked to them.
-(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
-- To rotate a line, just click on it and it will change its rotation.
-- Maps are randomly generated, difficulty(size, source count, house count) depends on what you pick and grows exponentially.
-"""
-
-class Tutorial(arcade.gui.UIView):
- def __init__(self, pypresence_client):
- super().__init__()
-
- self.pypresence_client = pypresence_client
- self.pypresence_client.update(state="Checking Tutorial")
-
- self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
- self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=20), anchor_x="center", anchor_y="top")
-
- def main_exit(self):
- from menus.main import Main
- self.window.show_view(Main(self.pypresence_client))
-
- def on_show_view(self):
- super().on_show_view()
-
- self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
- self.back_button.on_click = lambda event: self.main_exit()
- self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
-
- self.box.add(arcade.gui.UILabel(text="CTC Tutorial", font_size=40))
+import arcade, arcade.gui
+
+from utils.preload import button_texture, button_hovered_texture
+from utils.constants import button_style
+
+TUTORIAL_TEXT = """
+In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
+- Every line has to be connected on all of it's sides.
+- When needed, you might have to create loops of power or branches with no house linked to them.
+(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
+- To rotate a line, just click on it and it will change its rotation.
+- Maps are randomly generated, difficulty(size, source count, house count) depends on what you pick and grows exponentially.
+"""
+
+class Tutorial(arcade.gui.UIView):
+ def __init__(self, pypresence_client):
+ super().__init__()
+
+ self.pypresence_client = pypresence_client
+ self.pypresence_client.update(state="Checking Tutorial")
+
+ self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
+ self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=20), anchor_x="center", anchor_y="top")
+
+ def main_exit(self):
+ from menus.main import Main
+ self.window.show_view(Main(self.pypresence_client))
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
+ self.back_button.on_click = lambda event: self.main_exit()
+ self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
+
+ self.box.add(arcade.gui.UILabel(text="CTC Tutorial", font_size=40))
self.box.add(arcade.gui.UILabel(text=TUTORIAL_TEXT, font_size=20, multiline=True))
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 2ae9690..92af725 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,10 @@
-[project]
-name = "connect-the-current"
-version = "0.1.0"
-description = "Connect the Current"
-readme = "README.md"
-requires-python = ">=3.11"
-dependencies = [
- "arcade==3.2.0",
- "pypresence>=4.3.0",
-]
+[project]
+name = "connect-the-current"
+version = "0.1.0"
+description = "Connect the Current"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "arcade==3.2.0",
+ "pypresence>=4.3.0",
+]
diff --git a/requirements.txt b/requirements.txt
index 65d8e29..18d2afc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,22 +1,22 @@
-# This file was autogenerated by uv via the following command:
-# uv pip compile pyproject.toml -o requirements.txt
-arcade==3.2.0
- # via connect-the-current (pyproject.toml)
-attrs==25.3.0
- # via pytiled-parser
-cffi==1.17.1
- # via pymunk
-pillow==11.0.0
- # via arcade
-pycparser==2.22
- # via cffi
-pyglet==2.1.6
- # via arcade
-pymunk==6.9.0
- # via arcade
-pypresence==4.3.0
- # via connect-the-current (pyproject.toml)
-pytiled-parser==2.2.9
- # via arcade
-typing-extensions==4.14.1
- # via pytiled-parser
+# This file was autogenerated by uv via the following command:
+# uv pip compile pyproject.toml -o requirements.txt
+arcade==3.2.0
+ # via connect-the-current (pyproject.toml)
+attrs==25.3.0
+ # via pytiled-parser
+cffi==1.17.1
+ # via pymunk
+pillow==11.0.0
+ # via arcade
+pycparser==2.22
+ # via cffi
+pyglet==2.1.6
+ # via arcade
+pymunk==6.9.0
+ # via arcade
+pypresence==4.3.0
+ # via connect-the-current (pyproject.toml)
+pytiled-parser==2.2.9
+ # via arcade
+typing-extensions==4.14.1
+ # via pytiled-parser
diff --git a/run.py b/run.py
index dcc4d23..9838911 100644
--- a/run.py
+++ b/run.py
@@ -1,128 +1,128 @@
-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
-
-# 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
-from arcade.experimental.controller_window import ControllerWindow
-
-sys.excepthook = on_exception
-
-if not log_dir in os.listdir():
- os.makedirs(log_dir)
-
-while len(os.listdir(log_dir)) >= 5:
- files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
- oldest_file = sorted(files, key=lambda x: x[1])[0][0]
- os.remove(os.path.join(log_dir, oldest_file))
-
-timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
-log_filename = f"debug_{timestamp}.log"
-logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG)
-
-for logger_name_to_disable in ['arcade']:
- logging.getLogger(logger_name_to_disable).propagate = False
- logging.getLogger(logger_name_to_disable).disabled = True
-
-if os.path.exists('settings.json'):
- with open('settings.json', 'r') as settings_file:
- settings = json.load(settings_file)
-
- resolution = list(map(int, settings['resolution'].split('x')))
-
- if not settings.get("anti_aliasing", "4x MSAA") == "None":
- antialiasing = int(settings.get("anti_aliasing", "4x MSAA").split('x')[0])
- 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']
- fps_limit = settings['fps_limit']
-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
- fps_limit = 0
-
- settings = {
- "music": True,
- "music_volume": 50,
- "resolution": f"{resolution[0]}x{resolution[1]}",
- "antialiasing": "4x MSAA",
- "window_mode": "Windowed",
- "vsync": True,
- "fps_limit": 60,
- "discord_rpc": True
- }
-
- with open("settings.json", "w") as file:
- file.write(json.dumps(settings))
-
-# 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='Connect the Current', 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='Connect the Current', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
-
-if vsync:
- window.set_vsync(True)
- display_mode = window.display.get_default_screen().get_mode()
- if display_mode:
- refresh_rate = display_mode.rate
- else:
- refresh_rate = 60
- window.set_update_rate(1 / refresh_rate)
- window.set_draw_rate(1 / refresh_rate)
-elif not fps_limit == 0:
- window.set_update_rate(1 / fps_limit)
- window.set_draw_rate(1 / fps_limit)
-else:
- window.set_update_rate(1 / 99999999)
- window.set_draw_rate(1 / 99999999)
-
-arcade.set_background_color(menu_background_color)
-
-print_debug_info()
-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()
-
-logging.info('Exited with error code 0.')
+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
+
+# 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
+from arcade.experimental.controller_window import ControllerWindow
+
+sys.excepthook = on_exception
+
+if not log_dir in os.listdir():
+ os.makedirs(log_dir)
+
+while len(os.listdir(log_dir)) >= 5:
+ files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
+ oldest_file = sorted(files, key=lambda x: x[1])[0][0]
+ os.remove(os.path.join(log_dir, oldest_file))
+
+timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
+log_filename = f"debug_{timestamp}.log"
+logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG)
+
+for logger_name_to_disable in ['arcade']:
+ logging.getLogger(logger_name_to_disable).propagate = False
+ logging.getLogger(logger_name_to_disable).disabled = True
+
+if os.path.exists('settings.json'):
+ with open('settings.json', 'r') as settings_file:
+ settings = json.load(settings_file)
+
+ resolution = list(map(int, settings['resolution'].split('x')))
+
+ if not settings.get("anti_aliasing", "4x MSAA") == "None":
+ antialiasing = int(settings.get("anti_aliasing", "4x MSAA").split('x')[0])
+ 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']
+ fps_limit = settings['fps_limit']
+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
+ fps_limit = 0
+
+ settings = {
+ "music": True,
+ "music_volume": 50,
+ "resolution": f"{resolution[0]}x{resolution[1]}",
+ "antialiasing": "4x MSAA",
+ "window_mode": "Windowed",
+ "vsync": True,
+ "fps_limit": 60,
+ "discord_rpc": True
+ }
+
+ with open("settings.json", "w") as file:
+ file.write(json.dumps(settings))
+
+# 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='Connect the Current', 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='Connect the Current', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
+
+if vsync:
+ window.set_vsync(True)
+ display_mode = window.display.get_default_screen().get_mode()
+ if display_mode:
+ refresh_rate = display_mode.rate
+ else:
+ refresh_rate = 60
+ window.set_update_rate(1 / refresh_rate)
+ window.set_draw_rate(1 / refresh_rate)
+elif not fps_limit == 0:
+ window.set_update_rate(1 / fps_limit)
+ window.set_draw_rate(1 / fps_limit)
+else:
+ window.set_update_rate(1 / 99999999)
+ window.set_draw_rate(1 / 99999999)
+
+arcade.set_background_color(menu_background_color)
+
+print_debug_info()
+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()
+
+logging.info('Exited with error code 0.')
diff --git a/utils/constants.py b/utils/constants.py
index 571ec6f..a2be9df 100644
--- a/utils/constants.py
+++ b/utils/constants.py
@@ -1,71 +1,71 @@
-import arcade.color
-from arcade.types import Color
-from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
-from arcade.gui.widgets.slider import UISliderStyle
-
-CUSTOM_DIFFICULTY_SETTINGS = [
- ["source_count", "Source Count", 1, 20],
- ["house_count", "House Count", 1, 20],
- ["size", "Size", 3, 30]
-]
-
-ROTATIONS = {
- "line": ["vertical", "horizontal"],
- "corner": ["right_bottom", "left_bottom", "left_top", "right_top"],
- "t_junction": ["top_bottom_right", "left_right_bottom", "top_bottom_left", "left_right_top"],
- "cross": ["cross"]
-}
-
-NEIGHBOURS = {
- "vertical": {"b", "t"},
- "horizontal": {"l", "r"},
- "left_bottom": {"l", "b"},
- "right_bottom": {"r", "b"},
- "left_top": {"l", "t"},
- "right_top": {"r", "t"},
- "top_bottom_right": {"t", "b", "r"},
- "top_bottom_left": {"t", "b", "l"},
- "left_right_bottom": {"l", "r", "b"},
- "left_right_top": {"l", "r", "t"},
- "cross": {"l", "r", "t", "b"}
-}
-
-DIRECTIONS = {"t": (0, -1, "b"), "b": (0, 1, "t"), "l": (-1, 0, "r"), "r": (1, 0, "l")}
-
-menu_background_color = (30, 30, 47)
-log_dir = 'logs'
-discord_presence_id = 1435687634960777266
-
-button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
- 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
-big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
- 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26)}
-
-dropdown_style = {'normal': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)),
- 'press': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))}
-
-slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
-slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
-
-slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'press': slider_hover_style, 'disabled': slider_default_style}
-
-settings = {
- "Graphics": {
- "Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},
- "Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
- "Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"},
- "VSync": {"type": "bool", "config_key": "vsync", "default": True},
- "FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60},
- },
- "Sound": {
- "Music": {"type": "bool", "config_key": "music", "default": True},
- "SFX": {"type": "bool", "config_key": "sfx", "default": True},
- "Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50},
- "SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
- },
- "Miscellaneous": {
- "Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},
- },
- "Credits": {}
-}
-settings_start_category = "Graphics"
+import arcade.color
+from arcade.types import Color
+from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
+from arcade.gui.widgets.slider import UISliderStyle
+
+CUSTOM_DIFFICULTY_SETTINGS = [
+ ["source_count", "Source Count", 1, 20],
+ ["house_count", "House Count", 1, 20],
+ ["size", "Size", 3, 30]
+]
+
+ROTATIONS = {
+ "line": ["vertical", "horizontal"],
+ "corner": ["right_bottom", "left_bottom", "left_top", "right_top"],
+ "t_junction": ["top_bottom_right", "left_right_bottom", "top_bottom_left", "left_right_top"],
+ "cross": ["cross"]
+}
+
+NEIGHBOURS = {
+ "vertical": {"b", "t"},
+ "horizontal": {"l", "r"},
+ "left_bottom": {"l", "b"},
+ "right_bottom": {"r", "b"},
+ "left_top": {"l", "t"},
+ "right_top": {"r", "t"},
+ "top_bottom_right": {"t", "b", "r"},
+ "top_bottom_left": {"t", "b", "l"},
+ "left_right_bottom": {"l", "r", "b"},
+ "left_right_top": {"l", "r", "t"},
+ "cross": {"l", "r", "t", "b"}
+}
+
+DIRECTIONS = {"t": (0, -1, "b"), "b": (0, 1, "t"), "l": (-1, 0, "r"), "r": (1, 0, "l")}
+
+menu_background_color = (30, 30, 47)
+log_dir = 'logs'
+discord_presence_id = 1435687634960777266
+
+button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
+ 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
+big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
+ 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26)}
+
+dropdown_style = {'normal': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)),
+ 'press': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))}
+
+slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
+slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
+
+slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'press': slider_hover_style, 'disabled': slider_default_style}
+
+settings = {
+ "Graphics": {
+ "Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},
+ "Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
+ "Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"},
+ "VSync": {"type": "bool", "config_key": "vsync", "default": True},
+ "FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60},
+ },
+ "Sound": {
+ "Music": {"type": "bool", "config_key": "music", "default": True},
+ "SFX": {"type": "bool", "config_key": "sfx", "default": True},
+ "Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50},
+ "SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
+ },
+ "Miscellaneous": {
+ "Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},
+ },
+ "Credits": {}
+}
+settings_start_category = "Graphics"
diff --git a/utils/preload.py b/utils/preload.py
index 87dc6cf..c8e2c34 100644
--- a/utils/preload.py
+++ b/utils/preload.py
@@ -1,46 +1,46 @@
-import arcade.gui, arcade
-
-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"))
-
-wire_sound_effect = arcade.Sound("assets/sound/wire.mp3")
-
-TEXTURE_MAP = {
- ("line", "vertical", True): arcade.load_texture("assets/graphics/powered_lines/line/vertical.png"),
- ("line", "vertical", False): arcade.load_texture("assets/graphics/unpowered_lines/line/vertical.png"),
-
- ("line", "horizontal", True): arcade.load_texture("assets/graphics/powered_lines/line/horizontal.png"),
- ("line", "horizontal", False): arcade.load_texture("assets/graphics/unpowered_lines/line/horizontal.png"),
-
- ("corner", "left_bottom", True): arcade.load_texture("assets/graphics/powered_lines/corner/left_bottom.png"),
- ("corner", "left_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/left_bottom.png"),
-
- ("corner", "left_top", True): arcade.load_texture("assets/graphics/powered_lines/corner/left_top.png"),
- ("corner", "left_top", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/left_top.png"),
-
- ("corner", "right_bottom", True): arcade.load_texture("assets/graphics/powered_lines/corner/right_bottom.png"),
- ("corner", "right_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/right_bottom.png"),
-
- ("corner", "right_top", True): arcade.load_texture("assets/graphics/powered_lines/corner/right_top.png"),
- ("corner", "right_top", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/right_top.png"),
-
- ("t_junction", "left_right_bottom", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/left_right_bottom.png"),
- ("t_junction", "left_right_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/left_right_bottom.png"),
-
- ("t_junction", "left_right_top", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/left_right_top.png"),
- ("t_junction", "left_right_top", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/left_right_top.png"),
-
- ("t_junction", "top_bottom_left", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/top_bottom_left.png"),
- ("t_junction", "top_bottom_left", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/top_bottom_left.png"),
-
- ("t_junction", "top_bottom_right", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/top_bottom_right.png"),
- ("t_junction", "top_bottom_right", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/top_bottom_right.png"),
-
- ("cross", "cross", True): arcade.load_texture("assets/graphics/powered_lines/cross/cross.png"),
- ("cross", "cross", False): arcade.load_texture("assets/graphics/unpowered_lines/cross/cross.png"),
-
- ("power_source", "cross", True): arcade.load_texture("assets/graphics/power_source.png"),
-
- ("house", "cross", True): arcade.load_texture("assets/graphics/powered_lines/cross/cross.png"),
- ("house", "cross", False): arcade.load_texture("assets/graphics/unpowered_lines/cross/cross.png"),
-}
+import arcade.gui, arcade
+
+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"))
+
+wire_sound_effect = arcade.Sound("assets/sound/wire.mp3")
+
+TEXTURE_MAP = {
+ ("line", "vertical", True): arcade.load_texture("assets/graphics/powered_lines/line/vertical.png"),
+ ("line", "vertical", False): arcade.load_texture("assets/graphics/unpowered_lines/line/vertical.png"),
+
+ ("line", "horizontal", True): arcade.load_texture("assets/graphics/powered_lines/line/horizontal.png"),
+ ("line", "horizontal", False): arcade.load_texture("assets/graphics/unpowered_lines/line/horizontal.png"),
+
+ ("corner", "left_bottom", True): arcade.load_texture("assets/graphics/powered_lines/corner/left_bottom.png"),
+ ("corner", "left_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/left_bottom.png"),
+
+ ("corner", "left_top", True): arcade.load_texture("assets/graphics/powered_lines/corner/left_top.png"),
+ ("corner", "left_top", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/left_top.png"),
+
+ ("corner", "right_bottom", True): arcade.load_texture("assets/graphics/powered_lines/corner/right_bottom.png"),
+ ("corner", "right_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/right_bottom.png"),
+
+ ("corner", "right_top", True): arcade.load_texture("assets/graphics/powered_lines/corner/right_top.png"),
+ ("corner", "right_top", False): arcade.load_texture("assets/graphics/unpowered_lines/corner/right_top.png"),
+
+ ("t_junction", "left_right_bottom", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/left_right_bottom.png"),
+ ("t_junction", "left_right_bottom", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/left_right_bottom.png"),
+
+ ("t_junction", "left_right_top", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/left_right_top.png"),
+ ("t_junction", "left_right_top", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/left_right_top.png"),
+
+ ("t_junction", "top_bottom_left", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/top_bottom_left.png"),
+ ("t_junction", "top_bottom_left", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/top_bottom_left.png"),
+
+ ("t_junction", "top_bottom_right", True): arcade.load_texture("assets/graphics/powered_lines/t_junction/top_bottom_right.png"),
+ ("t_junction", "top_bottom_right", False): arcade.load_texture("assets/graphics/unpowered_lines/t_junction/top_bottom_right.png"),
+
+ ("cross", "cross", True): arcade.load_texture("assets/graphics/powered_lines/cross/cross.png"),
+ ("cross", "cross", False): arcade.load_texture("assets/graphics/unpowered_lines/cross/cross.png"),
+
+ ("power_source", "cross", True): arcade.load_texture("assets/graphics/power_source.png"),
+
+ ("house", "cross", True): arcade.load_texture("assets/graphics/powered_lines/cross/cross.png"),
+ ("house", "cross", False): arcade.load_texture("assets/graphics/unpowered_lines/cross/cross.png"),
+}
diff --git a/utils/utils.py b/utils/utils.py
index f956521..e2aa726 100644
--- a/utils/utils.py
+++ b/utils/utils.py
@@ -1,90 +1,90 @@
-import logging, arcade, arcade.gui, sys, traceback
-
-from utils.constants import menu_background_color
-
-import pyglet.info, pyglet.event
-
-def dump_platform():
- import platform
- logging.debug(f'Platform: {platform.platform()}')
- logging.debug(f'Release: {platform.release()}')
- logging.debug(f'Machine: {platform.machine()}')
- logging.debug(f'Architecture: {platform.architecture()}')
-
-def dump_gl(context=None):
- if context is not None:
- info = context.get_info()
- else:
- from pyglet.gl import gl_info as info
- logging.debug(f'gl_info.get_version(): {info.get_version()}')
- logging.debug(f'gl_info.get_vendor(): {info.get_vendor()}')
- logging.debug(f'gl_info.get_renderer(): {info.get_renderer()}')
-
-def print_debug_info():
- logging.debug('########################## DEBUG INFO ##########################')
- logging.debug('')
- dump_platform()
- dump_gl()
- logging.debug('')
- logging.debug(f'Number of screens: {len(pyglet.display.get_display().get_screens())}')
- logging.debug('')
- for n, screen in enumerate(pyglet.display.get_display().get_screens()):
- logging.debug(f"Screen #{n+1}:")
- logging.debug(f'DPI: {screen.get_dpi()}')
- logging.debug(f'Scale: {screen.get_scale()}')
- logging.debug(f'Size: {screen.width}, {screen.height}')
- logging.debug(f'Position: {screen.x}, {screen.y}')
- logging.debug('')
- logging.debug('########################## DEBUG INFO ##########################')
- logging.debug('')
-
-class ErrorView(arcade.gui.UIView):
- def __init__(self, message, title):
- super().__init__()
-
- self.message = message
- self.title = title
-
- def exit(self):
- logging.fatal('Exited with error code 1.')
- sys.exit(1)
-
- def on_show_view(self):
- super().on_show_view()
-
- self.window.set_caption('Connect the Current - Error')
- self.window.set_mouse_visible(True)
- self.window.set_exclusive_mouse(False)
- arcade.set_background_color(menu_background_color)
-
- msgbox = arcade.gui.UIMessageBox(width=self.window.width / 2, height=self.window.height / 2, message_text=self.message, title=self.title)
- msgbox.on_action = lambda _: self.exit()
- self.add_widget(msgbox)
-
-def on_exception(*exc_info):
- logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
-
-def get_closest_resolution():
- allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
- screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height
- if (screen_width, screen_height) in allowed_resolutions:
- if not allowed_resolutions.index((screen_width, screen_height)) == 0:
- closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1]
- else:
- closest_resolution = (screen_width, screen_height)
- else:
- target_width, target_height = screen_width // 2, screen_height // 2
-
- closest_resolution = min(
- allowed_resolutions,
- key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height)
- )
- return closest_resolution
-
-class FakePyPresence():
- def __init__(self):
- ...
- def update(self, *args, **kwargs):
- ...
- def close(self, *args, **kwargs):
- ...
+import logging, arcade, arcade.gui, sys, traceback
+
+from utils.constants import menu_background_color
+
+import pyglet.info, pyglet.event
+
+def dump_platform():
+ import platform
+ logging.debug(f'Platform: {platform.platform()}')
+ logging.debug(f'Release: {platform.release()}')
+ logging.debug(f'Machine: {platform.machine()}')
+ logging.debug(f'Architecture: {platform.architecture()}')
+
+def dump_gl(context=None):
+ if context is not None:
+ info = context.get_info()
+ else:
+ from pyglet.gl import gl_info as info
+ logging.debug(f'gl_info.get_version(): {info.get_version()}')
+ logging.debug(f'gl_info.get_vendor(): {info.get_vendor()}')
+ logging.debug(f'gl_info.get_renderer(): {info.get_renderer()}')
+
+def print_debug_info():
+ logging.debug('########################## DEBUG INFO ##########################')
+ logging.debug('')
+ dump_platform()
+ dump_gl()
+ logging.debug('')
+ logging.debug(f'Number of screens: {len(pyglet.display.get_display().get_screens())}')
+ logging.debug('')
+ for n, screen in enumerate(pyglet.display.get_display().get_screens()):
+ logging.debug(f"Screen #{n+1}:")
+ logging.debug(f'DPI: {screen.get_dpi()}')
+ logging.debug(f'Scale: {screen.get_scale()}')
+ logging.debug(f'Size: {screen.width}, {screen.height}')
+ logging.debug(f'Position: {screen.x}, {screen.y}')
+ logging.debug('')
+ logging.debug('########################## DEBUG INFO ##########################')
+ logging.debug('')
+
+class ErrorView(arcade.gui.UIView):
+ def __init__(self, message, title):
+ super().__init__()
+
+ self.message = message
+ self.title = title
+
+ def exit(self):
+ logging.fatal('Exited with error code 1.')
+ sys.exit(1)
+
+ def on_show_view(self):
+ super().on_show_view()
+
+ self.window.set_caption('Connect the Current - Error')
+ self.window.set_mouse_visible(True)
+ self.window.set_exclusive_mouse(False)
+ arcade.set_background_color(menu_background_color)
+
+ msgbox = arcade.gui.UIMessageBox(width=self.window.width / 2, height=self.window.height / 2, message_text=self.message, title=self.title)
+ msgbox.on_action = lambda _: self.exit()
+ self.add_widget(msgbox)
+
+def on_exception(*exc_info):
+ logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
+
+def get_closest_resolution():
+ allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
+ screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height
+ if (screen_width, screen_height) in allowed_resolutions:
+ if not allowed_resolutions.index((screen_width, screen_height)) == 0:
+ closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1]
+ else:
+ closest_resolution = (screen_width, screen_height)
+ else:
+ target_width, target_height = screen_width // 2, screen_height // 2
+
+ closest_resolution = min(
+ allowed_resolutions,
+ key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height)
+ )
+ return closest_resolution
+
+class FakePyPresence():
+ def __init__(self):
+ ...
+ def update(self, *args, **kwargs):
+ ...
+ def close(self, *args, **kwargs):
+ ...
diff --git a/uv.lock b/uv.lock
index 8d6c4e6..2375e8c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,220 +1,220 @@
-version = 1
-revision = 2
-requires-python = ">=3.11"
-
-[[package]]
-name = "arcade"
-version = "3.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pillow" },
- { name = "pyglet" },
- { name = "pymunk" },
- { name = "pytiled-parser" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e5/39/87eaffdfc50ec9d4b4573652ef8b80cca0592e5ccafb5fc5bc8612b1445d/arcade-3.2.0.tar.gz", hash = "sha256:1c2c56181560665f6542157b9ab316b9551274a9ee8468bae017ed5b8fee18fd", size = 41941030, upload_time = "2025-05-09T20:16:20.112Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/9a/ac86f5cbccfe5455a28308fcf2d7179af8d9c3087ad4eb45706c2a7b089b/arcade-3.2.0-py3-none-any.whl", hash = "sha256:7bb47cf643b43272e4300d8a5ca5f1b1e9e131b0f3f1d3fad013cb29528d3062", size = 42635264, upload_time = "2025-05-09T20:16:15.98Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "25.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" },
-]
-
-[[package]]
-name = "cffi"
-version = "1.17.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload_time = "2024-09-04T20:43:51.124Z" },
- { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload_time = "2024-09-04T20:43:52.872Z" },
- { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload_time = "2024-09-04T20:43:56.123Z" },
- { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload_time = "2024-09-04T20:43:57.891Z" },
- { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload_time = "2024-09-04T20:44:00.18Z" },
- { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload_time = "2024-09-04T20:44:01.585Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload_time = "2024-09-04T20:44:03.467Z" },
- { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload_time = "2024-09-04T20:44:05.023Z" },
- { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload_time = "2024-09-04T20:44:06.444Z" },
- { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload_time = "2024-09-04T20:44:08.206Z" },
- { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload_time = "2024-09-04T20:44:09.481Z" },
- { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload_time = "2024-09-04T20:44:10.873Z" },
- { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" },
- { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" },
- { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" },
- { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" },
- { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" },
- { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" },
- { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" },
- { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" },
- { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" },
- { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" },
- { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" },
- { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" },
- { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" },
- { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" },
- { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" },
- { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" },
- { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" },
- { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" },
- { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" },
-]
-
-[[package]]
-name = "connect-the-current"
-version = "0.1.0"
-source = { virtual = "." }
-dependencies = [
- { name = "arcade" },
- { name = "pypresence" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "arcade", specifier = "==3.2.0" },
- { name = "pypresence", specifier = ">=4.3.0" },
-]
-
-[[package]]
-name = "pillow"
-version = "11.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload_time = "2024-10-15T14:24:29.672Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705, upload_time = "2024-10-15T14:22:15.419Z" },
- { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222, upload_time = "2024-10-15T14:22:17.681Z" },
- { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220, upload_time = "2024-10-15T14:22:19.826Z" },
- { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399, upload_time = "2024-10-15T14:22:22.129Z" },
- { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709, upload_time = "2024-10-15T14:22:23.953Z" },
- { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556, upload_time = "2024-10-15T14:22:25.706Z" },
- { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187, upload_time = "2024-10-15T14:22:27.362Z" },
- { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468, upload_time = "2024-10-15T14:22:29.093Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249, upload_time = "2024-10-15T14:22:31.268Z" },
- { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769, upload_time = "2024-10-15T14:22:32.974Z" },
- { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611, upload_time = "2024-10-15T14:22:35.496Z" },
- { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload_time = "2024-10-15T14:22:37.736Z" },
- { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload_time = "2024-10-15T14:22:39.654Z" },
- { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload_time = "2024-10-15T14:22:41.598Z" },
- { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762, upload_time = "2024-10-15T14:22:45.952Z" },
- { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468, upload_time = "2024-10-15T14:22:47.789Z" },
- { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824, upload_time = "2024-10-15T14:22:49.668Z" },
- { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436, upload_time = "2024-10-15T14:22:51.911Z" },
- { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714, upload_time = "2024-10-15T14:22:53.967Z" },
- { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631, upload_time = "2024-10-15T14:22:56.404Z" },
- { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533, upload_time = "2024-10-15T14:22:58.087Z" },
- { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890, upload_time = "2024-10-15T14:22:59.918Z" },
- { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300, upload_time = "2024-10-15T14:23:01.855Z" },
- { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742, upload_time = "2024-10-15T14:23:03.749Z" },
- { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349, upload_time = "2024-10-15T14:23:06.055Z" },
- { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714, upload_time = "2024-10-15T14:23:07.919Z" },
- { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514, upload_time = "2024-10-15T14:23:10.19Z" },
- { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055, upload_time = "2024-10-15T14:23:12.08Z" },
- { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751, upload_time = "2024-10-15T14:23:13.836Z" },
- { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378, upload_time = "2024-10-15T14:23:15.735Z" },
- { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588, upload_time = "2024-10-15T14:23:17.905Z" },
- { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509, upload_time = "2024-10-15T14:23:19.643Z" },
- { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791, upload_time = "2024-10-15T14:23:21.601Z" },
- { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854, upload_time = "2024-10-15T14:23:23.91Z" },
- { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369, upload_time = "2024-10-15T14:23:27.184Z" },
- { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703, upload_time = "2024-10-15T14:23:28.979Z" },
- { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550, upload_time = "2024-10-15T14:23:30.846Z" },
- { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038, upload_time = "2024-10-15T14:23:32.687Z" },
- { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload_time = "2024-10-15T14:23:35.309Z" },
- { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload_time = "2024-10-15T14:23:37.33Z" },
- { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload_time = "2024-10-15T14:23:39.826Z" },
-]
-
-[[package]]
-name = "pycparser"
-version = "2.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" },
-]
-
-[[package]]
-name = "pyglet"
-version = "2.1.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f7/bc/0533ccb30566ee59b540d700dbbf916dafa89132a4d582d0fd1fe158243d/pyglet-2.1.6.tar.gz", hash = "sha256:18483880b1411b39692eaf7756819285797b1aaf9ef63d40eb9f9b5d01c63416", size = 6546705, upload_time = "2025-04-27T01:12:30.995Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/ad/e16f9b56c4a935934341e385753d0d0a2a83b7d320e52906b44f32698feb/pyglet-2.1.6-py3-none-any.whl", hash = "sha256:52ef9e75f3969b6a28bfa5c223e50ff03a05c2baa67bfe00d2a9eec4e831a7c5", size = 983998, upload_time = "2025-04-27T01:12:26.307Z" },
-]
-
-[[package]]
-name = "pymunk"
-version = "6.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ac/08/1513c868bc2a6bfa22d47acded27f5525c1db10bf1db4fdfa39160991616/pymunk-6.9.0.tar.gz", hash = "sha256:765f7c561a859a1b565bc517a47cc3992d6258e860f9174c533033c218af63c3", size = 3104088, upload_time = "2024-10-13T09:02:40.008Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/ba/34524aac6c57990aa9561c4a949543794e5f7128a0b01537ed061bdaed08/pymunk-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:536cf3ef9a3add0ea04d83a4c01fe090ff137fb591c3b6fff6e69102384ec5d5", size = 364338, upload_time = "2024-10-13T08:58:08.889Z" },
- { url = "https://files.pythonhosted.org/packages/19/9a/0d4931e3114495c31b600a17f27d5541f2ee35883e7c693199e1ccdf1ab0/pymunk-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e474bb748ded01d96d6eac8e282446baef324b67e0280213b495b1f936c06e7", size = 346937, upload_time = "2024-10-13T08:58:10.604Z" },
- { url = "https://files.pythonhosted.org/packages/61/d0/acd6a6cd8266ac0333792ac3ae36558a58859ca806e0add8f5ea01627b24/pymunk-6.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54bd14512ca5fed0e77f964b1de4e7da1a31386dbf125e33482874d69bb6537", size = 1065273, upload_time = "2024-10-13T08:58:13.012Z" },
- { url = "https://files.pythonhosted.org/packages/5d/d3/2e5763d2eea69e8953782da83fe81a0235650339c22a4f8c65ecdd07cec0/pymunk-6.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc8d6fe79f77f3ed6e2f33682d355eedb6864684120b845a3501fdf2d3efdcb6", size = 988611, upload_time = "2024-10-13T08:58:15.262Z" },
- { url = "https://files.pythonhosted.org/packages/ac/db/ff2cfa5b87d3e60992b2264a03ffedc738de64d0107b4ce96c623f9098e7/pymunk-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:70ee413899672c2d7d2ffbecabee133dba49a109867b520d77c829c0d9b3fe92", size = 974971, upload_time = "2024-10-13T08:58:17.706Z" },
- { url = "https://files.pythonhosted.org/packages/ff/44/8fd8677048aa864d91915702522c70c5aaadedfd7cd95000b75d7aabeffd/pymunk-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d163dcba2e5814bc5f1274e0ee6ec2a7e06bed8bf0050f30f22b604634bf7dbc", size = 1037097, upload_time = "2024-10-13T08:58:20.264Z" },
- { url = "https://files.pythonhosted.org/packages/19/fc/e6b8bf53255f2012dbdf4a2b063b6c02f8c13ce13b21fdfd84dda64fea80/pymunk-6.9.0-cp311-cp311-win32.whl", hash = "sha256:5d3ae7df3d39afe5b11633496cd464b198d5c62bec69f767f3b61f9fe7f09b98", size = 315321, upload_time = "2024-10-13T08:58:22.475Z" },
- { url = "https://files.pythonhosted.org/packages/bc/3c/925a0193bbcca7203f46fc531f4f0703885c102c1e2c118c8db35816aee3/pymunk-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:2db4797ecec3668d51bc112a37192ee1836e236bbacdf5ed12f5a994cf1bae33", size = 366711, upload_time = "2024-10-13T08:58:24.796Z" },
- { url = "https://files.pythonhosted.org/packages/93/96/d8505f4e9661c0e5343db5492895b90b2ada6ec4547fdc7a2df50eb0cdf2/pymunk-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02bb0fbbbce2b12c18a033e2cec747e6c4b0db93d2cb9a20f45e569b571ba184", size = 364703, upload_time = "2024-10-13T08:58:27.144Z" },
- { url = "https://files.pythonhosted.org/packages/54/3e/610a2f2b0c6c14038168f6f862148cb245aef867b01906ce18704acafe1c/pymunk-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6aae4f93ac686d5e2ec60b01faa1b3722a8ab630464d0c127e16462e7bef6292", size = 347056, upload_time = "2024-10-13T08:58:29.39Z" },
- { url = "https://files.pythonhosted.org/packages/4a/dd/4e12fb3671a6c4f2c0604420f0f15b5402b05c4964bba001088a3d92e3b9/pymunk-6.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7734d13e490e84665b1f03e616270b248d5279ed34e03859267f67868f1b94c", size = 1071014, upload_time = "2024-10-13T08:58:32.274Z" },
- { url = "https://files.pythonhosted.org/packages/91/f8/0618a9204aff896da8b2a9df44179390b178bf00b189851affd4809b1f03/pymunk-6.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b05dbfa58d366dea860f7259ca48483922a83620ab6a19effaa74e85a4251966", size = 990358, upload_time = "2024-10-13T08:58:35.295Z" },
- { url = "https://files.pythonhosted.org/packages/af/67/ea2ff4a26b66acad394e4f28e4e316fbe306d34909eca401baae211ca182/pymunk-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cb9520c52c043de4b2b1f83979f0d097929f6ff13c8a4059d9d211b98ae25887", size = 976300, upload_time = "2024-10-13T08:58:37.339Z" },
- { url = "https://files.pythonhosted.org/packages/91/d9/a69b268712dceacf227cfff74401e2292b53050383661d456605a1928a84/pymunk-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:da0e153d321073cd07a48380cfc1b7bd8d40bf4ee1b14a7ede33d90a69ee0452", size = 1042511, upload_time = "2024-10-13T08:58:40.044Z" },
- { url = "https://files.pythonhosted.org/packages/f0/40/21c2a08b027d99f351b75daa36f8a2e2385daba45098078d225811275ff8/pymunk-6.9.0-cp312-cp312-win32.whl", hash = "sha256:8325c9092345764876b1c3855126cb14450dc83dc5b141ff54983a7c77fbae52", size = 315339, upload_time = "2024-10-13T09:01:37.995Z" },
- { url = "https://files.pythonhosted.org/packages/78/b4/0a18c632f96924f969924cc5903689afcaf474d4c472305805dab391b247/pymunk-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:13246a79b599c44d174f5619596c62b656d8539797f28bdb2797c4b700c90a33", size = 366671, upload_time = "2024-10-13T09:01:39.965Z" },
- { url = "https://files.pythonhosted.org/packages/b6/5a/c76904d21f3fdb0b713b3a8056622733a0b773f7e55ef974fa4546068cbd/pymunk-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c59e5cf904e148dd0d35cffb7bafe146835042de9280672cafecc3a41caf7a3", size = 364703, upload_time = "2024-10-13T09:01:42.628Z" },
- { url = "https://files.pythonhosted.org/packages/63/b2/378d54b79812da5312b10de272c27aa0ac621498e059aa50eb4eec33ab52/pymunk-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4cbc2d37f69d85fedc1097af64edc8f4c43973a13429d51004883cbb9342875e", size = 347058, upload_time = "2024-10-13T09:01:44.529Z" },
- { url = "https://files.pythonhosted.org/packages/ba/a8/c7ea141a1d0e3f5b08ad653f0b5a4ebc0e5854f92bc7049a2a921fbe0d65/pymunk-6.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd64ef76e9e47fda929a2961fe98759ac46b5a7b6126d1ba3e6f04493da6519b", size = 1070851, upload_time = "2024-10-13T09:01:46.638Z" },
- { url = "https://files.pythonhosted.org/packages/10/a2/f40bcc9be90c2af1fe8cf4ba4281385b48d9f5667f03f6834c49aba600fd/pymunk-6.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c568c7402acd5d9a55e3965565ae0a596e4603ba8a7b7b7f0952efadd0e69524", size = 990371, upload_time = "2024-10-13T09:01:49.622Z" },
- { url = "https://files.pythonhosted.org/packages/01/ae/ff7fdf1c8d32ba89d1ccada39b5f7ed66e35420b8d31bdc9af6d5d20ea2f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f6cbe3d06e468be11a615d4facecc4a870bf58c1a27c365e655b5a85685ec942", size = 976294, upload_time = "2024-10-13T09:01:52.196Z" },
- { url = "https://files.pythonhosted.org/packages/c6/90/64ef000011f0c930b42354f0d91a07b4bc7f70819ec5b6034b84198bf53f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65a9a93a51dbaf1c77efa4d2425549888a1eda9f5c9cd9a5a89b7ca66310968a", size = 1042493, upload_time = "2024-10-13T09:01:55.665Z" },
- { url = "https://files.pythonhosted.org/packages/e5/fb/6516bd5fe565ea51a88308869632dfc896ca6b05b2579b016ffa8047a8ec/pymunk-6.9.0-cp313-cp313-win32.whl", hash = "sha256:a78b37bb360e715657c76caedaf40cdaaf6dab354d497eda481a976cc5cab3d7", size = 315341, upload_time = "2024-10-13T09:01:58.049Z" },
- { url = "https://files.pythonhosted.org/packages/e0/7c/1542df7ffbff70a4523ccb02c9241c9fe4dc24c77b747e2c16fb94891156/pymunk-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d6419e1531df80ff0bb6f1f8215e044f57415514386b7b212dc148919ca629ed", size = 366673, upload_time = "2024-10-13T09:01:59.733Z" },
-]
-
-[[package]]
-name = "pypresence"
-version = "4.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3", size = 10696, upload_time = "2023-07-08T00:33:53.49Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/40/1d30b30e18f81eb71365681223971a9822a89b3d6ee5269dd2aa955bc228/pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f", size = 11778, upload_time = "2023-07-08T00:33:52.018Z" },
-]
-
-[[package]]
-name = "pytiled-parser"
-version = "2.2.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/79/62/0d8a2220ee0747522f3b73e4f38bea7c78aefdf707afb86decf26f799fc5/pytiled_parser-2.2.9.tar.gz", hash = "sha256:225269fdd37afcbcd3b76ea3e2cab6b1e742387027106055990db43fd7451ebd", size = 45958, upload_time = "2025-01-23T18:43:30.538Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d7/f7/6b6c51b50ed8681a31146e5e7ac325b78fe776ff48b1ec8f56d7e4995d72/pytiled_parser-2.2.9-py2.py3-none-any.whl", hash = "sha256:37f73d31950bf4d02ee3bda59f3d6123c55194dc8d8e876821dd2080af5f1f91", size = 44452, upload_time = "2025-01-23T18:43:28.207Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.14.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" },
-]
+version = 1
+revision = 2
+requires-python = ">=3.11"
+
+[[package]]
+name = "arcade"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pillow" },
+ { name = "pyglet" },
+ { name = "pymunk" },
+ { name = "pytiled-parser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/39/87eaffdfc50ec9d4b4573652ef8b80cca0592e5ccafb5fc5bc8612b1445d/arcade-3.2.0.tar.gz", hash = "sha256:1c2c56181560665f6542157b9ab316b9551274a9ee8468bae017ed5b8fee18fd", size = 41941030, upload_time = "2025-05-09T20:16:20.112Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/9a/ac86f5cbccfe5455a28308fcf2d7179af8d9c3087ad4eb45706c2a7b089b/arcade-3.2.0-py3-none-any.whl", hash = "sha256:7bb47cf643b43272e4300d8a5ca5f1b1e9e131b0f3f1d3fad013cb29528d3062", size = 42635264, upload_time = "2025-05-09T20:16:15.98Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload_time = "2024-09-04T20:43:51.124Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload_time = "2024-09-04T20:43:52.872Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload_time = "2024-09-04T20:43:56.123Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload_time = "2024-09-04T20:43:57.891Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload_time = "2024-09-04T20:44:00.18Z" },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload_time = "2024-09-04T20:44:01.585Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload_time = "2024-09-04T20:44:03.467Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload_time = "2024-09-04T20:44:05.023Z" },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload_time = "2024-09-04T20:44:06.444Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload_time = "2024-09-04T20:44:08.206Z" },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload_time = "2024-09-04T20:44:09.481Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload_time = "2024-09-04T20:44:10.873Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "connect-the-current"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "arcade" },
+ { name = "pypresence" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "arcade", specifier = "==3.2.0" },
+ { name = "pypresence", specifier = ">=4.3.0" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload_time = "2024-10-15T14:24:29.672Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705, upload_time = "2024-10-15T14:22:15.419Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222, upload_time = "2024-10-15T14:22:17.681Z" },
+ { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220, upload_time = "2024-10-15T14:22:19.826Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399, upload_time = "2024-10-15T14:22:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709, upload_time = "2024-10-15T14:22:23.953Z" },
+ { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556, upload_time = "2024-10-15T14:22:25.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187, upload_time = "2024-10-15T14:22:27.362Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468, upload_time = "2024-10-15T14:22:29.093Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249, upload_time = "2024-10-15T14:22:31.268Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769, upload_time = "2024-10-15T14:22:32.974Z" },
+ { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611, upload_time = "2024-10-15T14:22:35.496Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload_time = "2024-10-15T14:22:37.736Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload_time = "2024-10-15T14:22:39.654Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload_time = "2024-10-15T14:22:41.598Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762, upload_time = "2024-10-15T14:22:45.952Z" },
+ { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468, upload_time = "2024-10-15T14:22:47.789Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824, upload_time = "2024-10-15T14:22:49.668Z" },
+ { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436, upload_time = "2024-10-15T14:22:51.911Z" },
+ { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714, upload_time = "2024-10-15T14:22:53.967Z" },
+ { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631, upload_time = "2024-10-15T14:22:56.404Z" },
+ { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533, upload_time = "2024-10-15T14:22:58.087Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890, upload_time = "2024-10-15T14:22:59.918Z" },
+ { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300, upload_time = "2024-10-15T14:23:01.855Z" },
+ { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742, upload_time = "2024-10-15T14:23:03.749Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349, upload_time = "2024-10-15T14:23:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714, upload_time = "2024-10-15T14:23:07.919Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514, upload_time = "2024-10-15T14:23:10.19Z" },
+ { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055, upload_time = "2024-10-15T14:23:12.08Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751, upload_time = "2024-10-15T14:23:13.836Z" },
+ { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378, upload_time = "2024-10-15T14:23:15.735Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588, upload_time = "2024-10-15T14:23:17.905Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509, upload_time = "2024-10-15T14:23:19.643Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791, upload_time = "2024-10-15T14:23:21.601Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854, upload_time = "2024-10-15T14:23:23.91Z" },
+ { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369, upload_time = "2024-10-15T14:23:27.184Z" },
+ { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703, upload_time = "2024-10-15T14:23:28.979Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550, upload_time = "2024-10-15T14:23:30.846Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038, upload_time = "2024-10-15T14:23:32.687Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload_time = "2024-10-15T14:23:35.309Z" },
+ { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload_time = "2024-10-15T14:23:37.33Z" },
+ { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload_time = "2024-10-15T14:23:39.826Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pyglet"
+version = "2.1.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/bc/0533ccb30566ee59b540d700dbbf916dafa89132a4d582d0fd1fe158243d/pyglet-2.1.6.tar.gz", hash = "sha256:18483880b1411b39692eaf7756819285797b1aaf9ef63d40eb9f9b5d01c63416", size = 6546705, upload_time = "2025-04-27T01:12:30.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/ad/e16f9b56c4a935934341e385753d0d0a2a83b7d320e52906b44f32698feb/pyglet-2.1.6-py3-none-any.whl", hash = "sha256:52ef9e75f3969b6a28bfa5c223e50ff03a05c2baa67bfe00d2a9eec4e831a7c5", size = 983998, upload_time = "2025-04-27T01:12:26.307Z" },
+]
+
+[[package]]
+name = "pymunk"
+version = "6.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/08/1513c868bc2a6bfa22d47acded27f5525c1db10bf1db4fdfa39160991616/pymunk-6.9.0.tar.gz", hash = "sha256:765f7c561a859a1b565bc517a47cc3992d6258e860f9174c533033c218af63c3", size = 3104088, upload_time = "2024-10-13T09:02:40.008Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/ba/34524aac6c57990aa9561c4a949543794e5f7128a0b01537ed061bdaed08/pymunk-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:536cf3ef9a3add0ea04d83a4c01fe090ff137fb591c3b6fff6e69102384ec5d5", size = 364338, upload_time = "2024-10-13T08:58:08.889Z" },
+ { url = "https://files.pythonhosted.org/packages/19/9a/0d4931e3114495c31b600a17f27d5541f2ee35883e7c693199e1ccdf1ab0/pymunk-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e474bb748ded01d96d6eac8e282446baef324b67e0280213b495b1f936c06e7", size = 346937, upload_time = "2024-10-13T08:58:10.604Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d0/acd6a6cd8266ac0333792ac3ae36558a58859ca806e0add8f5ea01627b24/pymunk-6.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54bd14512ca5fed0e77f964b1de4e7da1a31386dbf125e33482874d69bb6537", size = 1065273, upload_time = "2024-10-13T08:58:13.012Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/d3/2e5763d2eea69e8953782da83fe81a0235650339c22a4f8c65ecdd07cec0/pymunk-6.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc8d6fe79f77f3ed6e2f33682d355eedb6864684120b845a3501fdf2d3efdcb6", size = 988611, upload_time = "2024-10-13T08:58:15.262Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/db/ff2cfa5b87d3e60992b2264a03ffedc738de64d0107b4ce96c623f9098e7/pymunk-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:70ee413899672c2d7d2ffbecabee133dba49a109867b520d77c829c0d9b3fe92", size = 974971, upload_time = "2024-10-13T08:58:17.706Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/44/8fd8677048aa864d91915702522c70c5aaadedfd7cd95000b75d7aabeffd/pymunk-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d163dcba2e5814bc5f1274e0ee6ec2a7e06bed8bf0050f30f22b604634bf7dbc", size = 1037097, upload_time = "2024-10-13T08:58:20.264Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fc/e6b8bf53255f2012dbdf4a2b063b6c02f8c13ce13b21fdfd84dda64fea80/pymunk-6.9.0-cp311-cp311-win32.whl", hash = "sha256:5d3ae7df3d39afe5b11633496cd464b198d5c62bec69f767f3b61f9fe7f09b98", size = 315321, upload_time = "2024-10-13T08:58:22.475Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/3c/925a0193bbcca7203f46fc531f4f0703885c102c1e2c118c8db35816aee3/pymunk-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:2db4797ecec3668d51bc112a37192ee1836e236bbacdf5ed12f5a994cf1bae33", size = 366711, upload_time = "2024-10-13T08:58:24.796Z" },
+ { url = "https://files.pythonhosted.org/packages/93/96/d8505f4e9661c0e5343db5492895b90b2ada6ec4547fdc7a2df50eb0cdf2/pymunk-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02bb0fbbbce2b12c18a033e2cec747e6c4b0db93d2cb9a20f45e569b571ba184", size = 364703, upload_time = "2024-10-13T08:58:27.144Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3e/610a2f2b0c6c14038168f6f862148cb245aef867b01906ce18704acafe1c/pymunk-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6aae4f93ac686d5e2ec60b01faa1b3722a8ab630464d0c127e16462e7bef6292", size = 347056, upload_time = "2024-10-13T08:58:29.39Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/dd/4e12fb3671a6c4f2c0604420f0f15b5402b05c4964bba001088a3d92e3b9/pymunk-6.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7734d13e490e84665b1f03e616270b248d5279ed34e03859267f67868f1b94c", size = 1071014, upload_time = "2024-10-13T08:58:32.274Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f8/0618a9204aff896da8b2a9df44179390b178bf00b189851affd4809b1f03/pymunk-6.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b05dbfa58d366dea860f7259ca48483922a83620ab6a19effaa74e85a4251966", size = 990358, upload_time = "2024-10-13T08:58:35.295Z" },
+ { url = "https://files.pythonhosted.org/packages/af/67/ea2ff4a26b66acad394e4f28e4e316fbe306d34909eca401baae211ca182/pymunk-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cb9520c52c043de4b2b1f83979f0d097929f6ff13c8a4059d9d211b98ae25887", size = 976300, upload_time = "2024-10-13T08:58:37.339Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d9/a69b268712dceacf227cfff74401e2292b53050383661d456605a1928a84/pymunk-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:da0e153d321073cd07a48380cfc1b7bd8d40bf4ee1b14a7ede33d90a69ee0452", size = 1042511, upload_time = "2024-10-13T08:58:40.044Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/40/21c2a08b027d99f351b75daa36f8a2e2385daba45098078d225811275ff8/pymunk-6.9.0-cp312-cp312-win32.whl", hash = "sha256:8325c9092345764876b1c3855126cb14450dc83dc5b141ff54983a7c77fbae52", size = 315339, upload_time = "2024-10-13T09:01:37.995Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b4/0a18c632f96924f969924cc5903689afcaf474d4c472305805dab391b247/pymunk-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:13246a79b599c44d174f5619596c62b656d8539797f28bdb2797c4b700c90a33", size = 366671, upload_time = "2024-10-13T09:01:39.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/5a/c76904d21f3fdb0b713b3a8056622733a0b773f7e55ef974fa4546068cbd/pymunk-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c59e5cf904e148dd0d35cffb7bafe146835042de9280672cafecc3a41caf7a3", size = 364703, upload_time = "2024-10-13T09:01:42.628Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b2/378d54b79812da5312b10de272c27aa0ac621498e059aa50eb4eec33ab52/pymunk-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4cbc2d37f69d85fedc1097af64edc8f4c43973a13429d51004883cbb9342875e", size = 347058, upload_time = "2024-10-13T09:01:44.529Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/a8/c7ea141a1d0e3f5b08ad653f0b5a4ebc0e5854f92bc7049a2a921fbe0d65/pymunk-6.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd64ef76e9e47fda929a2961fe98759ac46b5a7b6126d1ba3e6f04493da6519b", size = 1070851, upload_time = "2024-10-13T09:01:46.638Z" },
+ { url = "https://files.pythonhosted.org/packages/10/a2/f40bcc9be90c2af1fe8cf4ba4281385b48d9f5667f03f6834c49aba600fd/pymunk-6.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c568c7402acd5d9a55e3965565ae0a596e4603ba8a7b7b7f0952efadd0e69524", size = 990371, upload_time = "2024-10-13T09:01:49.622Z" },
+ { url = "https://files.pythonhosted.org/packages/01/ae/ff7fdf1c8d32ba89d1ccada39b5f7ed66e35420b8d31bdc9af6d5d20ea2f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f6cbe3d06e468be11a615d4facecc4a870bf58c1a27c365e655b5a85685ec942", size = 976294, upload_time = "2024-10-13T09:01:52.196Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/90/64ef000011f0c930b42354f0d91a07b4bc7f70819ec5b6034b84198bf53f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65a9a93a51dbaf1c77efa4d2425549888a1eda9f5c9cd9a5a89b7ca66310968a", size = 1042493, upload_time = "2024-10-13T09:01:55.665Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/fb/6516bd5fe565ea51a88308869632dfc896ca6b05b2579b016ffa8047a8ec/pymunk-6.9.0-cp313-cp313-win32.whl", hash = "sha256:a78b37bb360e715657c76caedaf40cdaaf6dab354d497eda481a976cc5cab3d7", size = 315341, upload_time = "2024-10-13T09:01:58.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/7c/1542df7ffbff70a4523ccb02c9241c9fe4dc24c77b747e2c16fb94891156/pymunk-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d6419e1531df80ff0bb6f1f8215e044f57415514386b7b212dc148919ca629ed", size = 366673, upload_time = "2024-10-13T09:01:59.733Z" },
+]
+
+[[package]]
+name = "pypresence"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3", size = 10696, upload_time = "2023-07-08T00:33:53.49Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/40/1d30b30e18f81eb71365681223971a9822a89b3d6ee5269dd2aa955bc228/pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f", size = 11778, upload_time = "2023-07-08T00:33:52.018Z" },
+]
+
+[[package]]
+name = "pytiled-parser"
+version = "2.2.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/62/0d8a2220ee0747522f3b73e4f38bea7c78aefdf707afb86decf26f799fc5/pytiled_parser-2.2.9.tar.gz", hash = "sha256:225269fdd37afcbcd3b76ea3e2cab6b1e742387027106055990db43fd7451ebd", size = 45958, upload_time = "2025-01-23T18:43:30.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/f7/6b6c51b50ed8681a31146e5e7ac325b78fe776ff48b1ec8f56d7e4995d72/pytiled_parser-2.2.9-py2.py3-none-any.whl", hash = "sha256:37f73d31950bf4d02ee3bda59f3d6123c55194dc8d8e876821dd2080af5f1f91", size = 44452, upload_time = "2025-01-23T18:43:28.207Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" },
+]