diff --git a/codecov.yml b/.codecov.yml
similarity index 60%
rename from codecov.yml
rename to .codecov.yml
index dd31e005..79c5a87d 100644
--- a/codecov.yml
+++ b/.codecov.yml
@@ -3,6 +3,7 @@ coverage:
project:
default:
target: auto
- threshold: 0.1
+ threshold: "0.1"
base: auto
-comment: off
+comment:
+ require_changes: true
diff --git a/.editorconfig b/.editorconfig
index 2cd81a2a..bd4f19f2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -26,4 +26,3 @@ indent_size = 2
[*.json]
indent_style = space
indent_size = 4
-insert_final_newline = false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..ac5a1634
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
+
+github: HexDecimal
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dc89cbfd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,28 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior. A minimal code example is recommended.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. Windows 11]
+ - Python version [e.g. Python 3.11, PyPy 3.9]
+ - tcod version [e.g. 16.0.2]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..11fc491e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 1127e239..95e391c2 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -6,109 +6,125 @@ name: Package
on:
push:
pull_request:
+ types: [opened, reopened]
defaults:
run:
shell: bash
+env:
+ git-depth: 0 # Depth to search for tags.
+
jobs:
black:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Install Black
run: pip install black
- name: Run Black
run: black --check --diff examples/ scripts/ tcod/ tests/ *.py
isort:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Install isort
run: pip install isort
- name: isort
- uses: liskin/gh-problem-matcher-wrap@v1
- with:
- linters: isort
- run: isort scripts/ tcod/ tests/ --check --diff
- - name: isort (examples)
- uses: liskin/gh-problem-matcher-wrap@v1
+ uses: liskin/gh-problem-matcher-wrap@v2
with:
linters: isort
- run: isort examples/ --check --diff --thirdparty tcod
-
- flake8:
- runs-on: ubuntu-20.04
- steps:
- - uses: actions/checkout@v2
- - name: Install Flake8
- run: pip install Flake8
- - name: Flake8
- uses: liskin/gh-problem-matcher-wrap@v1
- with:
- linters: flake8
- run: flake8 scripts/ tcod/ tests/
+ run: isort scripts/ tcod/ tests/ examples/ --check --diff
mypy:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- - name: Install Python dependencies
+ - name: Install typing dependencies
run: pip install mypy pytest -r requirements.txt
- - name: Fake initialize package
- run: |
- echo '__version__ = ""' > tcod/version.py
- name: Mypy
- uses: liskin/gh-problem-matcher-wrap@v1
+ uses: liskin/gh-problem-matcher-wrap@v2
with:
linters: mypy
- run: mypy --show-column-numbers .
+ run: mypy --show-column-numbers
+
+ sdist:
+ runs-on: ubuntu-latest
+ steps:
+ - name: APT update
+ run: sudo apt-get update
+ - name: Install APT dependencies
+ run: sudo apt-get install libsdl2-dev
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: ${{ env.git-depth }}
+ - name: Checkout submodules
+ run: git submodule update --init --recursive --depth 1
+ - name: Install build
+ run: pip install build
+ - name: Build source distribution
+ run: python -m build --sdist
+ - uses: actions/upload-artifact@v3
+ with:
+ name: sdist
+ path: dist/tcod-*.tar.gz
+ retention-days: 7
+
# This makes sure that the latest versions of the SDL headers parse correctly.
parse_sdl:
+ needs: [black, isort, mypy]
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: ["windows-2019", "macos-11"]
+ os: ["windows-latest", "macos-latest"]
sdl-version: ["2.0.14", "2.0.16"]
fail-fast: true
steps:
- - uses: actions/checkout@v1 # v1 required to build package.
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- - name: Build package
- run: ./setup.py build
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+ - name: Install build dependencies
+ run: pip install -r requirements.txt
+ - name: Test SDL parsing
+ run: python build_sdl.py
env:
SDL_VERSION: ${{ matrix.sdl-version }}
build:
+ needs: [black, isort, mypy]
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: ["ubuntu-20.04", "windows-2019"]
+ os: ["ubuntu-latest", "windows-latest"]
python-version: ["3.7", "3.8", "3.9", "pypy-3.7"]
architecture: ["x64"]
include:
- - os: "windows-2019"
+ - os: "windows-latest"
python-version: "3.7"
architecture: "x86"
- - os: "windows-2019"
+ - os: "windows-latest"
python-version: "pypy-3.7"
architecture: "x86"
fail-fast: false
steps:
- # v2 breaks `git describe` so v1 is needed.
- # https://github.com/actions/checkout/issues/272
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: |
git submodule update --init --recursive --depth 1
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
@@ -120,49 +136,42 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
- pip install pytest pytest-cov pytest-benchmark wheel twine
+ pip install pytest pytest-cov pytest-benchmark pytest-timeout build
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Initialize package
run: |
- python setup.py check # Creates tcod/version.py.
- - name: Build package.
+ pip install -e . # Install the package in-place.
+ - name: Build package
run: |
- python setup.py build sdist develop bdist_wheel --py-limited-api cp36 # Install the package in-place.
+ python -m build
- name: Test with pytest
if: runner.os == 'Windows'
run: |
- pytest --cov-report=xml
+ pytest --cov-report=xml --timeout=300
- name: Test with pytest (Xvfb)
if: always() && runner.os != 'Windows'
run: |
- xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml
+ xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml --timeout=300
- name: Xvfb logs
if: runner.os != 'Windows'
run: cat /tmp/xvfb.log
- - uses: codecov/codecov-action@v2
- - name: Upload to PyPI
- if: startsWith(github.ref, 'refs/tags/') && runner.os != 'Linux'
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: |
- twine upload --skip-existing dist/*
- - uses: actions/upload-artifact@v2
- if: runner.os == 'Linux'
+ - uses: codecov/codecov-action@v3
+ - uses: actions/upload-artifact@v3
+ if: runner.os == 'Windows'
with:
- name: sdist
- path: dist/tcod-*.tar.gz
- retention-days: 3
+ name: wheels-windows
+ path: dist/*.whl
+ retention-days: 7
isolated: # Test installing the package from source.
- needs: build
+ needs: [black, isort, mypy, sdist]
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: ["ubuntu-20.04", "windows-2019"]
+ os: ["ubuntu-latest", "windows-latest"]
steps:
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install Python dependencies
@@ -174,7 +183,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- - uses: actions/download-artifact@v2
+ - uses: actions/download-artifact@v3
with:
name: sdist
- name: Build package in isolation
@@ -182,85 +191,117 @@ jobs:
pip install tcod-*.tar.gz
- name: Confirm package import
run: |
- python -c "import tcod"
+ python -c "import tcod.context"
linux-wheels:
- runs-on: "ubuntu-20.04"
+ needs: [black, isort, mypy]
+ runs-on: "ubuntu-latest"
+ strategy:
+ matrix:
+ arch: ["x86_64", "aarch64"]
+ build: ["cp37-manylinux*", "pp37-manylinux*"]
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: ${{ env.git-depth }}
+ - name: Set up QEMU
+ if: ${{ matrix.arch == 'aarch64' }}
+ uses: docker/setup-qemu-action@v2
- name: Checkout submodules
run: |
git submodule update --init --recursive --depth 1
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
- pip install twine cibuildwheel==2.0.0
+ pip install cibuildwheel==2.3.1
- name: Build wheels
run: |
python -m cibuildwheel --output-dir wheelhouse
env:
- CIBW_BUILD: cp36-* pp*
- CIBW_ARCHS_LINUX: "x86_64"
- CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
+ CIBW_BUILD: ${{ matrix.build }}
+ CIBW_ARCHS_LINUX: ${{ matrix.arch }}
+ CIBW_MANYLINUX_*_IMAGE: manylinux2014
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux2014
- CIBW_BEFORE_ALL_LINUX: yum install -y SDL2-devel
+ CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux2014
+ CIBW_BEFORE_ALL_LINUX: >
+ yum install -y epel-release &&
+ yum-config-manager --enable epel &&
+ yum install -y SDL2-devel
CIBW_BEFORE_TEST: pip install numpy
- CIBW_TEST_COMMAND: python -c "import tcod"
+ CIBW_TEST_COMMAND: python -c "import tcod.context"
+ # Skip test on emulated architectures
+ CIBW_TEST_SKIP: "*_aarch64"
- name: Archive wheel
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
- name: wheel-linux
+ name: wheels-linux
path: wheelhouse/*.whl
- retention-days: 1
- - name: Upload to PyPI
- if: startsWith(github.ref, 'refs/tags/')
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: |
- twine upload --skip-existing wheelhouse/*
+ retention-days: 7
build-macos:
- runs-on: "macos-10.15"
+ needs: [black, isort, mypy]
+ runs-on: "macos-11"
strategy:
fail-fast: true
matrix:
python: ["cp38-*_universal2", "cp38-*_x86_64", "cp38-*_arm64", "pp37-*"]
steps:
- # v2 breaks `git describe` so v1 is needed.
- # https://github.com/actions/checkout/issues/272
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- - name: Print git describe
- run: git describe
- name: Install Python dependencies
- run: pip3 install wheel twine -r requirements.txt
+ run: pip3 install -r requirements.txt
- name: Prepare package
# Downloads SDL2 for the later step.
- run: python3 setup.py check
+ run: python3 build_sdl.py
- name: Build wheels
- uses: pypa/cibuildwheel@v2.0.0a4
+ uses: pypa/cibuildwheel@v2.12.3
env:
CIBW_BUILD: ${{ matrix.python }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
CIBW_BEFORE_BUILD_MACOS: pip install --upgrade delocate
CIBW_BEFORE_TEST: pip install numpy
- CIBW_TEST_COMMAND: python -c "import tcod"
+ CIBW_TEST_COMMAND: python -c "import tcod.context"
CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64"
- name: Archive wheel
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
- name: wheel-macos
+ name: wheels-macos
path: wheelhouse/*.whl
- retention-days: 1
- - name: Upload to PyPI
- if: startsWith(github.ref, 'refs/tags/')
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: twine upload --skip-existing wheelhouse/*
+ retention-days: 7
+
+ publish:
+ needs: [sdist, build, build-macos, linux-wheels]
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ environment:
+ name: release
+ url: https://pypi.org/p/tcod
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v3
+ with:
+ name: sdist
+ path: dist/
+ - uses: actions/download-artifact@v3
+ with:
+ name: wheels-windows
+ path: dist/
+ - uses: actions/download-artifact@v3
+ with:
+ name: wheels-macos
+ path: dist/
+ - uses: actions/download-artifact@v3
+ with:
+ name: wheels-linux
+ path: dist/
+ - uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ skip-existing: true
diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml
index fd5e5377..95cf02d3 100644
--- a/.github/workflows/release-on-tag.yml
+++ b/.github/workflows/release-on-tag.yml
@@ -9,20 +9,17 @@ jobs:
build:
name: Create Release
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Generate body
run: |
scripts/get_release_description.py | tee release_body.md
- name: Create Release
id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ uses: ncipollo/release-action@v1
with:
- tag_name: ${{ github.ref }}
- release_name: ""
- body_path: release_body.md
- draft: false
- prerelease: false
+ name: ""
+ bodyFile: release_body.md
diff --git a/.gitignore b/.gitignore
index 85bf65c5..6ec1f7fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,4 @@ debian/python*
.pytest_cache
Thumbs.db
.mypy_cache/
+.ruff_cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..54a9a88e
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,26 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+ - id: check-shebang-scripts-are-executable
+ - id: check-merge-conflict
+ - id: check-toml
+ - id: debug-statements
+ - id: fix-byte-order-marker
+ - id: detect-private-key
+ - repo: https://github.com/psf/black
+ rev: 23.3.0
+ hooks:
+ - id: black
+ - repo: https://github.com/pycqa/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+default_language_version:
+ python: python3.11
diff --git a/.pyup.yml b/.pyup.yml
index bf632f0b..fa58aa6e 100644
--- a/.pyup.yml
+++ b/.pyup.yml
@@ -1,4 +1,4 @@
-# autogenerated pyup.io config file
+# autogenerated pyup.io config file
# see https://pyup.io/docs/configuration/ for all available options
update: false
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..390d6115
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,31 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+ apt_packages:
+ - libsdl2-dev
+
+submodules:
+ include: all
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+# If using Sphinx, optionally build your docs in additional formats such as PDF
+# formats:
+# - pdf
+
+# Optionally declare the Python requirements required to build your docs
+python:
+ install:
+ - requirements: docs/requirements.txt
+ - method: pip
+ path: .
diff --git a/.readthedocs.yml b/.readthedocs.yml
deleted file mode 100644
index 2521036c..00000000
--- a/.readthedocs.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-# .readthedocs.yml
-# Read the Docs configuration file
-# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
-
-version: 2
-
-build:
- image: latest
-
-sphinx:
- builder: html
- configuration: docs/conf.py
- fail_on_warning: true
-
-formats:
- - htmlzip
- - pdf
- - epub
-
-python:
- version: "3.8"
- install:
- - requirements: requirements.txt
- - requirements: docs/requirements.txt
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 59e34caf..d589600c 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -6,6 +6,7 @@
"austin.code-gnu-global",
"editorconfig.editorconfig",
"ms-python.python",
+ "ms-python.black-formatter",
"ms-python.vscode-pylance",
"ms-vscode.cpptools",
"redhat.vscode-yaml",
@@ -15,4 +16,4 @@
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
-}
\ No newline at end of file
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 4bd84cbe..0f237387 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,8 +9,7 @@
"type": "python",
"request": "launch",
"program": "${file}",
- "console": "integratedTerminal",
- "preLaunchTask": "develop python-tcod",
+ "console": "internalConsole",
},
{
// Run the Python samples.
@@ -20,8 +19,7 @@
"request": "launch",
"program": "${workspaceFolder}/examples/samples_tcod.py",
"cwd": "${workspaceFolder}/examples",
- "console": "integratedTerminal",
- "preLaunchTask": "develop python-tcod",
+ "console": "internalConsole",
},
{
"name": "Python: Run tests",
@@ -47,4 +45,4 @@
"preLaunchTask": "build documentation",
}
]
-}
\ No newline at end of file
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6e892bc5..6ddcc5ef 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,25 +7,30 @@
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
+ "files.trimFinalNewlines": true,
+ "files.insertFinalNewline": true,
+ "files.trimTrailingWhitespace": true,
"python.linting.enabled": true,
- "python.linting.flake8Enabled": true,
+ "python.linting.flake8Enabled": false,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--follow-imports=silent",
"--show-column-numbers"
],
- "python.formatting.provider": "black",
+ "python.formatting.provider": "none",
"files.associations": {
"*.spec": "python",
},
"cSpell.words": [
"ADDA",
+ "ADDALPHA",
"addressof",
"addsub",
"addx",
"addy",
"algo",
"ALPH",
+ "ALTERASE",
"arange",
"ARCHS",
"asarray",
@@ -34,7 +39,13 @@
"astype",
"atexit",
"AUDIOCVT",
+ "AUDIOFASTFORWARD",
+ "AUDIOMUTE",
+ "AUDIONEXT",
+ "AUDIOPLAY",
"AUDIOPREV",
+ "AUDIOREWIND",
+ "AUDIOSTOP",
"autoclass",
"autofunction",
"autogenerated",
@@ -45,17 +56,26 @@
"bezier",
"bfade",
"bgcolor",
+ "bitmask",
+ "bitorder",
+ "BITSIZE",
"BKGND",
"Blit",
"blits",
"blitting",
+ "BORDERLESS",
"bresenham",
"Bresenham",
+ "BRIGHTNESSDOWN",
"BRIGHTNESSUP",
"bysource",
"caeldera",
+ "CAPSLOCK",
+ "caxis",
+ "cbutton",
"ccoef",
"cdef",
+ "cdevice",
"cffi",
"cflags",
"CFLAGS",
@@ -63,6 +83,8 @@
"Chebyshev",
"cibuildwheel",
"CIBW",
+ "CLEARAGAIN",
+ "CLEARENTRY",
"CMWC",
"Codacy",
"Codecov",
@@ -71,15 +93,28 @@
"coef",
"Coef",
"COLCTRL",
+ "COMPILEDVERSION",
"consolas",
+ "contextdata",
+ "CONTROLLERAXISMOTION",
+ "CONTROLLERBUTTONDOWN",
+ "CONTROLLERBUTTONUP",
+ "CONTROLLERDEVICEADDED",
+ "CONTROLLERDEVICEREMAPPED",
+ "CONTROLLERDEVICEREMOVED",
"cplusplus",
"CPLUSPLUS",
"CRSEL",
"ctypes",
+ "CURRENCYSUBUNIT",
+ "CURRENCYUNIT",
"currentmodule",
"datas",
+ "DBLAMPERSAND",
+ "DBLVERTICALBAR",
"dcost",
"DCROSS",
+ "DECIMALSEPARATOR",
"dejavu",
"delocate",
"deque",
@@ -87,7 +122,10 @@
"DESATURATED",
"devel",
"DHLINE",
+ "DISPLAYSWITCH",
"dlopen",
+ "Doryen",
+ "DPAD",
"DTEEE",
"DTEEN",
"DTEES",
@@ -100,6 +138,7 @@
"epub",
"EQUALSAS",
"errorvf",
+ "EXCLAM",
"EXSEL",
"favicon",
"ffade",
@@ -109,8 +148,12 @@
"fmean",
"fontx",
"fonty",
+ "freetype",
+ "frombuffer",
"fullscreen",
"fwidth",
+ "GAMECONTROLLER",
+ "gamepad",
"genindex",
"GFORCE",
"GLES",
@@ -120,13 +163,17 @@
"guass",
"heapify",
"heightmap",
+ "heightmaps",
"hflip",
+ "hiddenimports",
+ "HIGHDPI",
"hillclimb",
"hline",
"horiz",
"howto",
"htbp",
"htmlzip",
+ "IBEAM",
"ifdef",
"ifndef",
"iinfo",
@@ -136,17 +183,33 @@
"INROW",
"isinstance",
"isort",
+ "itemsize",
"itleref",
"ivar",
+ "jaxis",
+ "jball",
+ "jbutton",
+ "jdevice",
+ "jhat",
"jice",
"jieba",
+ "JOYAXISMOTION",
+ "JOYBALLMOTION",
+ "JOYBUTTONDOWN",
+ "JOYBUTTONUP",
+ "JOYDEVICEADDED",
+ "JOYDEVICEREMOVED",
+ "JOYHATMOTION",
"Kaczor",
"KBDILLUMDOWN",
"KBDILLUMTOGGLE",
"KBDILLUMUP",
+ "kernelsize",
"keychar",
+ "KEYDOWN",
"keyname",
"keypress",
+ "keysym",
"KEYUP",
"KMOD",
"KPADD",
@@ -160,16 +223,25 @@
"lbutton",
"LCTRL",
"LDFLAGS",
+ "LEFTBRACE",
+ "LEFTBRACKET",
+ "LEFTDOWN",
"LEFTPAREN",
+ "LEFTSHOULDER",
+ "LEFTSTICK",
+ "LEFTUP",
+ "LEFTX",
"lerp",
"LGUI",
"libsdl",
"libtcod",
"libtcodpy",
+ "linspace",
"liskin",
"LMASK",
"lmeta",
"lodepng",
+ "LPAREN",
"LTCG",
"lucida",
"LWIN",
@@ -177,8 +249,17 @@
"maxarray",
"maxdepth",
"mbutton",
+ "MEDIASELECT",
"MEIPASS",
+ "MEMADD",
+ "MEMCLEAR",
+ "MEMDIVIDE",
+ "MEMMULTIPLY",
+ "MEMRECALL",
+ "MEMSTORE",
+ "MEMSUBTRACT",
"mersenne",
+ "meshgrid",
"mgrid",
"milli",
"minmax",
@@ -186,16 +267,22 @@
"mipmaps",
"MMASK",
"modindex",
+ "MOUSEBUTTONDOWN",
"MOUSEBUTTONUP",
+ "MOUSEMOTION",
+ "MOUSESTATE",
"msilib",
"MSVC",
"msvcr",
+ "mult",
"mulx",
"muly",
"mypy",
"namegen",
+ "ncipollo",
"ndarray",
"ndim",
+ "newaxis",
"newh",
"neww",
"noarchive",
@@ -213,13 +300,23 @@
"onefile",
"OPENGL",
"OPER",
+ "packbits",
+ "PAGEDOWN",
"PAGEUP",
+ "PATCHLEVEL",
"pathfinding",
"pathlib",
+ "pcpp",
+ "PERLIN",
"PILCROW",
"pilmode",
+ "PIXELFORMAT",
+ "PLUSMINUS",
+ "PRESENTVSYNC",
"PRINTF",
"printn",
+ "PRINTSCREEN",
+ "propname",
"pycall",
"pycparser",
"pyinstaller",
@@ -231,17 +328,26 @@
"PYTHONOPTIMIZE",
"Pyup",
"quickstart",
+ "QUOTEDBL",
"RALT",
"randomizer",
"rbutton",
"RCTRL",
+ "rects",
"redist",
"Redistributable",
"redistributables",
"repr",
"rgba",
"RGUI",
+ "RIGHTBRACE",
+ "RIGHTBRACKET",
+ "RIGHTDOWN",
"RIGHTPAREN",
+ "RIGHTSHOULDER",
+ "RIGHTSTICK",
+ "RIGHTUP",
+ "RIGHTX",
"RMASK",
"rmeta",
"roguelike",
@@ -249,57 +355,103 @@
"RRGGBB",
"rtype",
"RWIN",
+ "RWOPS",
"scalex",
"scaley",
"Scancode",
+ "scancodes",
"scipy",
"scoef",
+ "SCROLLLOCK",
"sdist",
"SDL's",
"SDLCALL",
"sdlevent",
"SDLK",
+ "seealso",
"servernum",
"setuptools",
"SHADOWCAST",
+ "SIZEALL",
+ "SIZENESW",
+ "SIZENS",
+ "SIZENWSE",
+ "SIZEWE",
"SMILIE",
+ "snprintf",
+ "soundfile",
"stdeb",
"struct",
"structs",
"SUBP",
"SYSREQ",
"tablefmt",
+ "TARGETTEXTURE",
"tcod",
"tcoddoc",
"TCODK",
"TCODLIB",
"TEEE",
"TEEW",
+ "TEXTUREACCESS",
"thirdparty",
+ "THOUSANDSSEPARATOR",
"Tileset",
"tilesets",
"tilesheet",
+ "tilesheets",
"timeit",
"toctree",
"todos",
"tolist",
+ "touchpad",
+ "TRIGGERLEFT",
+ "TRIGGERRIGHT",
"tris",
"truetype",
+ "typestr",
"undoc",
"Unifont",
+ "unraisable",
+ "unraisablehook",
+ "unraiseable",
"upscaling",
+ "userdata",
"VAFUNC",
+ "VALUELIST",
"vcoef",
"venv",
"vertic",
+ "VERTICALBAR",
"vflip",
"vline",
+ "VOLUMEDOWN",
"VOLUMEUP",
"voronoi",
"VRAM",
"vsync",
+ "VULKAN",
+ "WAITARROW",
"WASD",
+ "waterlevel",
+ "windowclose",
+ "windowenter",
+ "WINDOWEVENT",
+ "windowexposed",
+ "windowfocusgained",
+ "windowfocuslost",
+ "windowhidden",
+ "windowhittest",
+ "windowleave",
+ "windowmaximized",
+ "windowminimized",
+ "windowmoved",
+ "WINDOWPOS",
"WINDOWRESIZED",
+ "windowrestored",
+ "windowshown",
+ "windowsizechanged",
+ "windowtakefocus",
"xdst",
"xrel",
"xvfb",
@@ -308,5 +460,11 @@
],
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
- "python.testing.pytestEnabled": true
-}
\ No newline at end of file
+ "python.testing.pytestEnabled": true,
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.black-formatter"
+ },
+ "cSpell.enableFiletypes": [
+ "github-actions-workflow"
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index ba6b05d3..5e1a0fb2 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -41,4 +41,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..c9f0e36e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,1318 @@
+# Changelog
+Changes relevant to the users of python-tcod are documented here.
+
+This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`.
+
+## [Unreleased]
+
+## [16.0.2] - 2023-06-02
+### Fixed
+- Joystick/controller device events would raise `RuntimeError` when accessed after removal.
+
+## [16.0.1] - 2023-05-28
+### Fixed
+- `AudioDevice.stopped` was inverted.
+- Fixed the audio mixer stop and fadeout methods.
+- Exceptions raised in the audio mixer callback no longer cause a messy crash, they now go to `sys.unraisablehook`.
+
+## [16.0.0] - 2023-05-27
+### Added
+- Added PathLike support to more libtcodpy functions.
+- New `tcod.sdl.mouse.show` function for querying or setting mouse visibility.
+- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`.
+- `tcod.sdl.audio.AudioDevice` is now a context manager.
+
+### Changed
+- SDL audio conversion will now pass unconvertible floating types as float32 instead of raising.
+
+### Deprecated
+- Deprecated the libtcodpy functions for images and noise generators.
+
+### Removed
+- `tcod.console_set_custom_font` can no longer take bytes as the file path.
+
+### Fixed
+- Fix `tcod.sdl.mouse.warp_in_window` function.
+- Fix `TypeError: '_AudioCallbackUserdata' object is not callable` when using an SDL audio device callback.
+ [#128](https://github.com/libtcod/python-tcod/issues/128)
+
+## [15.0.3] - 2023-05-25
+### Deprecated
+- Deprecated all libtcod color constants. Replace these with your own manually defined colors.
+ Using a color will tell you the color values of the deprecated color in the warning.
+- Deprecated older scancode and keysym constants. These were replaced with the Scancode and KeySym enums.
+
+### Fixed
+- DLL loader could fail to load `SDL2.dll` when other tcod namespace packages were installed.
+
+## [15.0.1] - 2023-03-30
+### Added
+- Added support for `tcod.sdl` namespace packages.
+
+### Fixed
+- ``Renderer.read_pixels`` method was completely broken.
+
+## [15.0.0] - 2023-01-04
+### Changed
+- Modified the letter case of window event types to match their type annotations.
+ This may cause regressions. Run Mypy to check for ``[comparison-overlap]`` errors.
+- Mouse event attributes have been changed ``.pixel -> .position`` and ``.pixel_motion -> .motion``.
+- `Context.convert_event` now returns copies of events with mouse coordinates converted into tile positions.
+
+### Deprecated
+- Mouse event pixel and tile attributes have been deprecated.
+
+## [14.0.0] - 2022-12-09
+### Added
+- Added explicit support for namespace packages.
+
+### Changed
+- Using `libtcod 1.23.1`.
+- Bundle `SDL 2.26.0` on Windows and MacOS.
+- Code Page 437: Character 0x7F is now assigned to 0x2302 (HOUSE).
+- Forced all renderers to ``RENDERER_SDL2`` to fix rare graphical artifacts with OpenGL.
+
+### Deprecated
+- The `renderer` parameter of new contexts is now deprecated.
+
+## [13.8.1] - 2022-09-23
+### Fixed
+- `EventDispatch` was missing new event names.
+
+## [13.8.0] - 2022-09-22
+### Added
+- Ported SDL2 joystick handing as `tcod.sdl.joystick`.
+- New joystick related events.
+
+### Changed
+- Using `libtcod 1.22.3`.
+- Bundle `SDL 2.24.0` on Windows and MacOS.
+
+### Deprecated
+- Renderers other than `tcod.RENDERER_SDL2` are now discouraged.
+
+### Fixed
+- Fixed double present bug in non-context flush functions.
+ This was affecting performance and also caused a screen flicker whenever the global fade color was active.
+- Fixed the parsing of SDL 2.24.0 headers on Windows.
+
+## [13.7.0] - 2022-08-07
+### Added
+- You can new use `SDLConsoleRender.atlas` to access the `SDLTilesetAtlas` used to create it.
+ [#121](https://github.com/libtcod/python-tcod/issues/121)
+
+### Fixed
+- Fixed the parsing of SDL 2.0.22 headers. Specifically `SDL_FLT_EPSILON`.
+
+## [13.6.2] - 2022-05-02
+### Fixed
+- SDL renderers were ignoring tiles where only the background red channel was changed.
+
+## [13.6.1] - 2022-03-29
+### Changed
+- The SDL2 renderer has had a major performance update when compiled with SDL 2.0.18.
+- SDL2 is now the default renderer to avoid rare issues with the OpenGL 2 renderer.
+
+## [13.6.0] - 2022-02-19
+### Added
+- `BasicMixer` and `Channel` classes added to `tcod.sdl.audio`. These handle simple audio mixing.
+- `AudioDevice.convert` added to handle simple conversions to the active devices format.
+- `tcod.sdl.audio.convert_audio` added to handle any other conversions needed.
+
+## [13.5.0] - 2022-02-11
+### Added
+- `tcod.sdl.audio`, a new module exposing SDL audio devices. This does not include an audio mixer yet.
+- `tcod.sdl.mouse`, for SDL mouse and cursor handing.
+- `Context.sdl_atlas`, which provides the relevant `SDLTilesetAtlas` when one is being used by the context.
+- Several missing features were added to `tcod.sdl.render`.
+- `Window.mouse_rect` added to SDL windows to set the mouse confinement area.
+### Changed
+- `Texture.access` and `Texture.blend_mode` properties now return enum instances.
+ You can still set `blend_mode` with `int` but Mypy will complain.
+
+## [13.4.0] - 2022-02-04
+### Added
+- Adds `sdl_window` and `sdl_renderer` properties to tcod contexts.
+- Adds `tcod.event.add_watch` and `tcod.event.remove_watch` to handle SDL events via callback.
+- Adds the `tcod.sdl.video` module to handle SDL windows.
+- Adds the `tcod.sdl.render` module to handle SDL renderers.
+- Adds the `tcod.render` module which gives more control over the rendering of consoles and tilesets.
+### Fixed
+- Fixed handling of non-Path PathLike parameters and filepath encodings.
+
+## [13.3.0] - 2022-01-07
+### Added
+- New experimental renderer `tcod.context.RENDERER_XTERM`.
+### Changed
+- Using `libtcod 1.20.1`.
+### Fixed
+- Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type.
+- BDF files with blank lines no longer fail to load with an "Unknown keyword" error.
+
+## [13.2.0] - 2021-12-24
+### Added
+- New `console` parameter in `tcod.context.new` which sets parameters from an existing Console.
+
+### Changed
+- Using `libtcod 1.20.0`.
+
+### Fixed
+- Fixed segfault when an OpenGL2 context fails to load.
+- Gaussian number generation no longer affects the results of unrelated RNG's.
+- Gaussian number generation is now reentrant and thread-safe.
+- Fixed potential crash in PNG image loading.
+
+## [13.1.0] - 2021-10-22
+### Added
+- Added the `tcod.tileset.procedural_block_elements` function.
+
+### Removed
+- Python 3.6 is no longer supported.
+
+## [13.0.0] - 2021-09-20
+### Changed
+- Console print and drawing functions now always use absolute coordinates for negative numbers.
+
+## [12.7.3] - 2021-08-13
+### Deprecated
+- `tcod.console_is_key_pressed` was replaced with `tcod.event.get_keyboard_state`.
+- `tcod.console_from_file` is deprecated.
+- The `.asc` and `.apf` formats are no longer actively supported.
+
+### Fixed
+- Fixed the parsing of SDL 2.0.16 headers.
+
+## [12.7.2] - 2021-07-01
+### Fixed
+- *Scancode* and *KeySym* enums no longer crash when SDL returns an unexpected value.
+
+## [12.7.1] - 2021-06-30
+### Added
+- Started uploading wheels for ARM64 macOS.
+
+## [12.7.0] - 2021-06-29
+### Added
+- *tcod.image* and *tcod.tileset* now support *pathlib*.
+
+### Fixed
+- Wheels for 32-bit Windows now deploy again.
+
+## [12.6.2] - 2021-06-15
+### Fixed
+- Git is no longer required to install from source.
+
+## [12.6.1] - 2021-06-09
+### Fixed
+- Fixed version mismatch when building from sources.
+
+## [12.6.0] - 2021-06-09
+### Added
+- Added the *decoration* parameter to *Console.draw_frame*.
+ You may use this parameter to designate custom glyphs as the frame border.
+
+### Deprecated
+- The handling of negative indexes given to console drawing and printing
+ functions will be changed to be used as absolute coordinates in the future.
+
+## [12.5.1] - 2021-05-30
+### Fixed
+- The setup script should no longer fail silently when cffi is unavailable.
+
+## [12.5.0] - 2021-05-21
+### Changed
+- `KeyboardEvent`'s '`scancode`, `sym`, and `mod` attributes now use their respective enums.
+
+## [12.4.0] - 2021-05-21
+### Added
+- Added modernized REXPaint saving/loading functions.
+ - `tcod.console.load_xp`
+ - `tcod.console.save_xp`
+
+### Changed
+- Using `libtcod 1.18.1`.
+- `tcod.event.KeySym` and `tcod.event.Scancode` can now be hashed.
+
+## [12.3.2] - 2021-05-15
+### Changed
+- Using `libtcod 1.17.1`.
+
+### Fixed
+- Fixed regression with loading PNG images.
+
+## [12.3.1] - 2021-05-13
+### Fixed
+- Fix Windows deployment.
+
+## [12.3.0] - 2021-05-13
+### Added
+- New keyboard enums:
+ - `tcod.event.KeySym`
+ - `tcod.event.Scancode`
+ - `tcod.event.Modifier`
+- New functions:
+ - `tcod.event.get_keyboard_state`
+ - `tcod.event.get_modifier_state`
+- Added `tcod.console.rgb_graphic` and `tcod.console.rgba_graphic` dtypes.
+- Another name for the Console array attributes: `Console.rgb` and `Console.rgba`.
+
+### Changed
+- Using `libtcod 1.17.0`.
+
+### Deprecated
+- `Console_tiles_rgb` is being renamed to `Console.rgb`.
+- `Console_tiles` being renamed to `Console.rgba`.
+
+### Fixed
+- Contexts now give a more useful error when pickled.
+- Fixed regressions with `tcod.console_print_frame` and `Console.print_frame`
+ when given empty strings as the banner.
+
+## [12.2.0] - 2021-04-09
+### Added
+- Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums.
+- Added `tcod.noise.grid` helper function.
+
+### Deprecated
+- The non-enum noise implementation names have been deprecated.
+
+### Fixed
+- Indexing Noise classes now works with the FBM implementation.
+
+## [12.1.0] - 2021-04-01
+### Added
+- Added package-level PyInstaller hook.
+
+### Changed
+- Using `libtcod 1.16.7`.
+- `tcod.path.dijkstra2d` now returns the output and accepts an `out` parameter.
+
+### Deprecated
+- In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given.
+
+### Fixed
+- Fixed crashes from loading tilesets with non-square tile sizes.
+- Tilesets with a size of 0 should no longer crash when used.
+- Prevent division by zero from recommended-console-size functions.
+
+## [12.0.0] - 2021-03-05
+### Added
+- Now includes PyInstaller hooks within the package itself.
+
+### Deprecated
+- The Random class will now warn if the seed it's given will not used
+ deterministically. It will no longer accept non-integer seeds in the future.
+
+### Changed
+- Now bundles SDL 2.0.14 for MacOS.
+- `tcod.event` can now detect and will warn about uninitialized tile
+ attributes on mouse events.
+
+### Removed
+- Python 3.5 is no longer supported.
+- The `tdl` module has been dropped.
+
+## [11.19.3] - 2021-01-07
+### Fixed
+- Some wheels had broken version metadata.
+
+## [11.19.2] - 2020-12-30
+### Changed
+- Now bundles SDL 2.0.10 for MacOS and SDL 2.0.14 for Windows.
+
+### Fixed
+- MacOS wheels were failing to bundle dependencies for SDL2.
+
+## [11.19.1] - 2020-12-29
+### Fixed
+- MacOS wheels failed to deploy for the previous version.
+
+## [11.19.0] - 2020-12-29
+### Added
+- Added the important `order` parameter to `Context.new_console`.
+
+## [11.18.3] - 2020-12-28
+### Changed
+- Now bundles SDL 2.0.14 for Windows/MacOS.
+
+### Deprecated
+- Support for Python 3.5 will be dropped.
+- `tcod.console_load_xp` has been deprecated, `tcod.console_from_xp` can load
+ these files without modifying an existing console.
+
+### Fixed
+- `tcod.console_from_xp` now has better error handling (instead of crashing.)
+- Can now compile with SDL 2.0.14 headers.
+
+## [11.18.2] - 2020-12-03
+### Fixed
+- Fixed missing `tcod.FOV_SYMMETRIC_SHADOWCAST` constant.
+- Fixed regression in `tcod.sys_get_current_resolution` behavior. This
+ function now returns the monitor resolution as was previously expected.
+
+## [11.18.1] - 2020-11-30
+### Fixed
+- Code points from the Private Use Area will now print correctly.
+
+## [11.18.0] - 2020-11-13
+### Added
+- New context method `Context.new_console`.
+
+### Changed
+- Using `libtcod 1.16.0-alpha.15`.
+
+## [11.17.0] - 2020-10-30
+### Added
+- New FOV implementation: `tcod.FOV_SYMMETRIC_SHADOWCAST`.
+
+### Changed
+- Using `libtcod 1.16.0-alpha.14`.
+
+## [11.16.1] - 2020-10-28
+### Deprecated
+- Changed context deprecations to PendingDeprecationWarning to reduce mass
+ panic from tutorial followers.
+
+### Fixed
+- Fixed garbled titles and crashing on some platforms.
+
+## [11.16.0] - 2020-10-23
+### Added
+- Added `tcod.context.new` function.
+- Contexts now support a CLI.
+- You can now provide the window x,y position when making contexts.
+- `tcod.noise.Noise` instances can now be indexed to generate noise maps.
+
+### Changed
+- Using `libtcod 1.16.0-alpha.13`.
+- The OpenGL 2 renderer can now use `SDL_HINT_RENDER_SCALE_QUALITY` to
+ determine the tileset upscaling filter.
+- Improved performance of the FOV_BASIC algorithm.
+
+### Deprecated
+- `tcod.context.new_window` and `tcod.context.new_terminal` have been replaced
+ by `tcod.context.new`.
+
+### Fixed
+- Pathfinders will now work with boolean arrays.
+- Console blits now ignore alpha compositing which would result in division by
+ zero.
+- `tcod.console_is_key_pressed` should work even if libtcod events are ignored.
+- The `TCOD_RENDERER` and `TCOD_VSYNC` environment variables should work now.
+- `FOV_PERMISSIVE` algorithm is now reentrant.
+
+## [11.15.3] - 2020-07-30
+### Fixed
+- `tcod.tileset.Tileset.remap`, codepoint and index were swapped.
+
+## [11.15.2] - 2020-07-27
+### Fixed
+- `tcod.path.dijkstra2d`, fixed corrupted output with int8 arrays.
+
+## [11.15.1] - 2020-07-26
+### Changed
+- `tcod.event.EventDispatch` now uses the absolute names for event type hints
+ so that IDE's can better auto-complete method overrides.
+
+### Fixed
+- Fixed libtcodpy heightmap data alignment issues on non-square maps.
+
+## [11.15.0] - 2020-06-29
+### Added
+- `tcod.path.SimpleGraph` for pathfinding on simple 2D arrays.
+
+### Changed
+- `tcod.path.CustomGraph` now accepts an `order` parameter.
+
+## [11.14.0] - 2020-06-23
+### Added
+- New `tcod.los` module for NumPy-based line-of-sight algorithms.
+ Includes `tcod.los.bresenham`.
+
+### Deprecated
+- `tcod.line_where` and `tcod.line_iter` have been deprecated.
+
+## [11.13.6] - 2020-06-19
+### Deprecated
+- `console_init_root` and `console_set_custom_font` have been replaced by the
+ modern API.
+- All functions which handle SDL windows without a context are deprecated.
+- All functions which modify a globally active tileset are deprecated.
+- `tcod.map.Map` is deprecated, NumPy arrays should be passed to functions
+ directly instead of through this class.
+
+## [11.13.5] - 2020-06-15
+### Fixed
+- Install requirements will no longer try to downgrade `cffi`.
+
+## [11.13.4] - 2020-06-15
+
+## [11.13.3] - 2020-06-13
+### Fixed
+- `cffi` requirement has been updated to version `1.13.0`.
+ The older versions raise TypeError's.
+
+## [11.13.2] - 2020-06-12
+### Fixed
+- SDL related errors during package installation are now more readable.
+
+## [11.13.1] - 2020-05-30
+### Fixed
+- `tcod.event.EventDispatch`: `ev_*` methods now allow `Optional[T]` return
+ types.
+
+## [11.13.0] - 2020-05-22
+### Added
+- `tcod.path`: New `Pathfinder` and `CustomGraph` classes.
+
+### Changed
+- Added `edge_map` parameter to `tcod.path.dijkstra2d` and
+ `tcod.path.hillclimb2d`.
+
+### Fixed
+- tcod.console_init_root` and context initializing functions were not
+ raising exceptions on failure.
+
+## [11.12.1] - 2020-05-02
+### Fixed
+- Prevent adding non-existent 2nd halves to potential double-wide charterers.
+
+## [11.12.0] - 2020-04-30
+### Added
+- Added `tcod.context` module. You now have more options for making libtcod
+ controlled contexts.
+- `tcod.tileset.load_tilesheet`: Load a simple tilesheet as a Tileset.
+- `Tileset.remap`: Reassign codepoints to tiles on a Tileset.
+- `tcod.tileset.CHARMAP_CP437`: Character mapping for `load_tilesheet`.
+- `tcod.tileset.CHARMAP_TCOD`: Older libtcod layout.
+
+### Changed
+- `EventDispatch.dispatch` can now return the values returned by the `ev_*`
+ methods. The class is now generic to support type checking these values.
+- Event mouse coordinates are now strictly int types.
+- Submodules are now implicitly imported.
+
+## [11.11.4] - 2020-04-26
+### Changed
+- Using `libtcod 1.16.0-alpha.10`.
+
+### Fixed
+- Fixed characters being dropped when color codes were used.
+
+## [11.11.3] - 2020-04-24
+### Changed
+- Using `libtcod 1.16.0-alpha.9`.
+
+### Fixed
+- `FOV_DIAMOND` and `FOV_RESTRICTIVE` algorithms are now reentrant.
+ [libtcod#48](https://github.com/libtcod/libtcod/pull/48)
+- The `TCOD_VSYNC` environment variable was being ignored.
+
+## [11.11.2] - 2020-04-22
+
+## [11.11.1] - 2020-04-03
+### Changed
+- Using `libtcod 1.16.0-alpha.8`.
+
+### Fixed
+- Changing the active tileset now redraws tiles correctly on the next frame.
+
+## [11.11.0] - 2020-04-02
+### Added
+- Added `Console.close` as a more obvious way to close the active window of a
+ root console.
+
+### Changed
+- GCC is no longer needed to compile the library on Windows.
+- Using `libtcod 1.16.0-alpha.7`.
+- `tcod.console_flush` will now accept an RGB tuple as a `clear_color`.
+
+### Fixed
+- Changing the active tileset will now properly show it on the next render.
+
+## [11.10.0] - 2020-03-26
+### Added
+- Added `tcod.tileset.load_bdf`, you can now load BDF fonts.
+- `tcod.tileset.set_default` and `tcod.tileset.get_default` are now stable.
+
+### Changed
+- Using `libtcod 1.16.0-alpha.6`.
+
+### Deprecated
+- The `snap_to_integer` parameter in `tcod.console_flush` has been deprecated
+ since it can cause minor scaling issues which don't exist when using
+ `integer_scaling` instead.
+
+## [11.9.2] - 2020-03-17
+### Fixed
+- Fixed segfault after the Tileset returned by `tcod.tileset.get_default` goes
+ out of scope.
+
+## [11.9.1] - 2020-02-28
+### Changed
+- Using `libtcod 1.16.0-alpha.5`.
+- Mouse tile coordinates are now always zero before the first call to
+ `tcod.console_flush`.
+
+## [11.9.0] - 2020-02-22
+### Added
+- New method `Tileset.render` renders an RGBA NumPy array from a tileset and
+ a console.
+
+## [11.8.2] - 2020-02-22
+### Fixed
+- Prevent KeyError when representing unusual keyboard symbol constants.
+
+## [11.8.1] - 2020-02-22
+### Changed
+- Using `libtcod 1.16.0-alpha.4`.
+
+### Fixed
+- Mouse tile coordinates are now correct on any resized window.
+
+## [11.8.0] - 2020-02-21
+### Added
+- Added `tcod.console.recommended_size` for when you want to change your main
+ console size at runtime.
+- Added `Console.tiles_rgb` as a replacement for `Console.tiles2`.
+
+### Changed
+- Using `libtcod 1.16.0-alpha.3`.
+- Added parameters to `tcod.console_flush`, you can now manually provide a
+ console and adjust how it is presented.
+
+### Deprecated
+- `Console.tiles2` is deprecated in favour of `Console.tiles_rgb`.
+- `Console.buffer` is now deprecated in favour of `Console.tiles`, instead of
+ the other way around.
+
+### Fixed
+- Fixed keyboard state and mouse state functions losing state when events were
+ flushed.
+
+## [11.7.2] - 2020-02-16
+### Fixed
+- Fixed regression in `tcod.console_clear`.
+
+## [11.7.1] - 2020-02-16
+### Fixed
+- Fixed regression in `Console.draw_frame`.
+- The wavelet noise generator now excludes -1.0f and 1.0f as return values.
+- Fixed console fading color regression.
+
+## [11.7.0] - 2020-02-14
+### Changed
+- Using `libtcod 1.16.0-alpha.2`.
+- When a renderer fails to load it will now fallback to a different one.
+ The order is: OPENGL2 -> OPENGL -> SDL2.
+- The default renderer is now SDL2.
+- The SDL and OPENGL renderers are no longer deprecated, but they now point to
+ slightly different backward compatible implementations.
+
+### Deprecated
+- The use of `libtcod.cfg` and `terminal.png` is deprecated.
+
+### Fixed
+- `tcod.sys_update_char` now works with the newer renderers.
+- Fixed buffer overflow in name generator.
+- `tcod.image_from_console` now works with the newer renderers.
+- New renderers now auto-load fonts from `libtcod.cfg` or `terminal.png`.
+
+## [11.6.0] - 2019-12-05
+### Changed
+- Console blit operations now perform per-cell alpha transparency.
+
+## [11.5.1] - 2019-11-23
+### Fixed
+- Python 3.8 wheels failed to deploy.
+
+## [11.5.0] - 2019-11-22
+### Changed
+- Quarter block elements are now rendered using Unicode instead of a custom
+ encoding.
+
+### Fixed
+- `OPENGL` and `GLSL` renderers were not properly clearing space characters.
+
+## [11.4.1] - 2019-10-15
+### Added
+- Uploaded Python 3.8 wheels to PyPI.
+
+## [11.4.0] - 2019-09-20
+### Added
+- Added `__array_interface__` to the Image class.
+- Added `Console.draw_semigraphics` as a replacement for blit_2x functions.
+ `draw_semigraphics` can handle array-like objects.
+- `Image.from_array` class method creates an Image from an array-like object.
+- `tcod.image.load` loads a PNG file as an RGBA array.
+
+### Changed
+- `Console.tiles` is now named `Console.buffer`.
+
+## [11.3.0] - 2019-09-06
+### Added
+- New attribute `Console.tiles2` is similar to `Console.tiles` but without an
+ alpha channel.
+
+## [11.2.2] - 2019-08-25
+### Fixed
+- Fixed a regression preventing PyInstaller distributions from loading SDL2.
+
+## [11.2.1] - 2019-08-25
+
+## [11.2.0] - 2019-08-24
+### Added
+- `tcod.path.dijkstra2d`: Computes Dijkstra from an arbitrary initial state.
+- `tcod.path.hillclimb2d`: Returns a path from a distance array.
+- `tcod.path.maxarray`: Creates arrays filled with maximum finite values.
+
+### Fixed
+- Changing the tiles of an active tileset on OPENGL2 will no longer leave
+ temporary artifact tiles.
+- It's now harder to accidentally import tcod's internal modules.
+
+## [11.1.2] - 2019-08-02
+### Changed
+- Now bundles SDL 2.0.10 for Windows/MacOS.
+
+### Fixed
+- Can now parse SDL 2.0.10 headers during installation without crashing.
+
+## [11.1.1] - 2019-08-01
+### Deprecated
+- Using an out-of-bounds index for field-of-view operations now raises a
+ warning, which will later become an error.
+
+### Fixed
+- Changing the tiles of an active tileset will now work correctly.
+
+## [11.1.0] - 2019-07-05
+### Added
+- You can now set the `TCOD_RENDERER` and `TCOD_VSYNC` environment variables to
+ force specific options to be used.
+ Example: ``TCOD_RENDERER=sdl2 TCOD_VSYNC=1``
+
+### Changed
+- `tcod.sys_set_renderer` now raises an exception if it fails.
+
+### Fixed
+- `tcod.console_map_ascii_code_to_font` functions will now work when called
+ before `tcod.console_init_root`.
+
+## [11.0.2] - 2019-06-21
+### Changed
+- You no longer need OpenGL to build python-tcod.
+
+## [11.0.1] - 2019-06-21
+### Changed
+- Better runtime checks for Windows dependencies should now give distinct
+ errors depending on if the issue is SDL2 or missing redistributables.
+
+### Fixed
+- Changed NumPy type hints from `np.array` to `np.ndarray` which should
+ resolve issues.
+
+## [11.0.0] - 2019-06-14
+### Changed
+- `tcod.map.compute_fov` now takes a 2-item tuple instead of separate `x` and
+ `y` parameters. This causes less confusion over how axes are aligned.
+
+## [10.1.1] - 2019-06-02
+### Changed
+- Better string representations for `tcod.event.Event` subclasses.
+
+### Fixed
+- Fixed regressions in text alignment for non-rectangle print functions.
+
+## [10.1.0] - 2019-05-24
+### Added
+- `tcod.console_init_root` now has an optional `vsync` parameter.
+
+## [10.0.5] - 2019-05-17
+### Fixed
+- Fixed shader compilation issues in the OPENGL2 renderer.
+- Fallback fonts should fail less on Linux.
+
+## [10.0.4] - 2019-05-17
+### Changed
+- Now depends on cffi 0.12 or later.
+
+### Fixed
+- `tcod.console_init_root` and `tcod.console_set_custom_font` will raise
+ exceptions instead of terminating.
+- Fixed issues preventing `tcod.event` from working on 32-bit Windows.
+
+## [10.0.3] - 2019-05-10
+### Fixed
+- Corrected bounding box issues with the `Console.print_box` method.
+
+## [10.0.2] - 2019-04-26
+### Fixed
+- Resolved Color warnings when importing tcod.
+- When compiling, fixed a name conflict with endianness macros on FreeBSD.
+
+## [10.0.1] - 2019-04-19
+### Fixed
+- Fixed horizontal alignment for TrueType fonts.
+- Fixed taking screenshots with the older SDL renderer.
+
+## [10.0.0] - 2019-03-29
+### Added
+- New `Console.tiles` array attribute.
+### Changed
+- `Console.DTYPE` changed to add alpha to its color types.
+### Fixed
+- Console printing was ignoring color codes at the beginning of a string.
+
+## [9.3.0] - 2019-03-15
+### Added
+- The SDL2/OPENGL2 renderers can potentially use a fall-back font when none
+ are provided.
+- New function `tcod.event.get_mouse_state`.
+- New function `tcod.map.compute_fov` lets you get a visibility array directly
+ from a transparency array.
+### Deprecated
+- The following functions and classes have been deprecated.
+ - `tcod.Key`
+ - `tcod.Mouse`
+ - `tcod.mouse_get_status`
+ - `tcod.console_is_window_closed`
+ - `tcod.console_check_for_keypress`
+ - `tcod.console_wait_for_keypress`
+ - `tcod.console_delete`
+ - `tcod.sys_check_for_event`
+ - `tcod.sys_wait_for_event`
+- The SDL, OPENGL, and GLSL renderers have been deprecated.
+- Many libtcodpy functions have been marked with PendingDeprecationWarning's.
+### Fixed
+- To be more compatible with libtcodpy `tcod.console_init_root` will default
+ to the SDL render, but will raise warnings when an old renderer is used.
+
+## [9.2.5] - 2019-03-04
+### Fixed
+- Fixed `tcod.namegen_generate_custom`.
+
+## [9.2.4] - 2019-03-02
+### Fixed
+- The `tcod` package is has been marked as typed and will now work with MyPy.
+
+## [9.2.3] - 2019-03-01
+### Deprecated
+- The behavior for negative indexes on the new print functions may change in
+ the future.
+- Methods and functionality preventing `tcod.Color` from behaving like a tuple
+ have been deprecated.
+
+## [9.2.2] - 2019-02-26
+### Fixed
+- `Console.print_box` wasn't setting the background color by default.
+
+## [9.2.1] - 2019-02-25
+### Fixed
+- `tcod.sys_get_char_size` fixed on the new renderers.
+
+## [9.2.0] - 2019-02-24
+### Added
+- New `tcod.console.get_height_rect` function, which can be used to get the
+ height of a print call without an existing console.
+- New `tcod.tileset` module, with a `set_truetype_font` function.
+### Fixed
+- The new print methods now handle alignment according to how they were
+ documented.
+- `SDL2` and `OPENGL2` now support screenshots.
+- Windows and MacOS builds now restrict exported SDL2 symbols to only
+ SDL 2.0.5; This will avoid hard to debug import errors when the wrong
+ version of SDL is dynamically linked.
+- The root console now starts with a white foreground.
+
+## [9.1.0] - 2019-02-23
+### Added
+- Added the `tcod.random.MULTIPLY_WITH_CARRY` constant.
+### Changed
+- The overhead for warnings has been reduced when running Python with the
+ optimize `-O` flag.
+- `tcod.random.Random` now provides a default algorithm.
+
+## [9.0.0] - 2019-02-17
+### Changed
+- New console methods now default to an `fg` and `bg` of None instead of
+ white-on-black.
+
+## [8.5.0] - 2019-02-15
+### Added
+- `tcod.console.Console` now supports `str` and `repr`.
+- Added new Console methods which are independent from the console defaults.
+- You can now give an array when initializing a `tcod.console.Console`
+ instance.
+- `Console.clear` can now take `ch`, `fg`, and `bg` parameters.
+### Changed
+- Updated libtcod to 1.10.6
+- Printing generates more compact layouts.
+### Deprecated
+- Most libtcodpy console functions have been replaced by the tcod.console
+ module.
+- Deprecated the `set_key_color` functions. You can pass key colors to
+ `Console.blit` instead.
+- `Console.clear` should be given the colors to clear with as parameters,
+ rather than by using `default_fg` or `default_bg`.
+- Most functions which depend on console default values have been deprecated.
+ The new deprecation warnings will give details on how to make default values
+ explicit.
+### Fixed
+- `tcod.console.Console.blit` was ignoring the key color set by
+ `Console.set_key_color`.
+- The `SDL2` and `OPENGL2` renders can now large numbers of tiles.
+
+## [8.4.3] - 2019-02-06
+### Changed
+- Updated libtcod to 1.10.5
+- The SDL2/OPENGL2 renderers will now auto-detect a custom fonts key-color.
+
+## [8.4.2] - 2019-02-05
+### Deprecated
+- The tdl module has been deprecated.
+- The libtcodpy parser functions have been deprecated.
+### Fixed
+- `tcod.image_is_pixel_transparent` and `tcod.image_get_alpha` now return
+ values.
+- `Console.print_frame` was clearing tiles outside if its bounds.
+- The `FONT_LAYOUT_CP437` layout was incorrect.
+
+## [8.4.1] - 2019-02-01
+### Fixed
+- Window event types were not upper-case.
+- Fixed regression where libtcodpy mouse wheel events unset mouse coordinates.
+
+## [8.4.0] - 2019-01-31
+### Added
+- Added tcod.event module, based off of the sdlevent.py shim.
+### Changed
+- Updated libtcod to 1.10.3
+### Fixed
+- Fixed libtcodpy `struct_add_value_list` function.
+- Use correct math for tile-based delta in mouse events.
+- New renderers now support tile-based mouse coordinates.
+- SDL2 renderer will now properly refresh after the window is resized.
+
+## [8.3.2] - 2018-12-28
+### Fixed
+- Fixed rare access violations for some functions which took strings as
+ parameters, such as `tcod.console_init_root`.
+
+## [8.3.1] - 2018-12-28
+### Fixed
+- libtcodpy key and mouse functions will no longer accept the wrong types.
+- The `new_struct` method was not being called for libtcodpy's custom parsers.
+
+## [8.3.0] - 2018-12-08
+### Added
+- Added BSP traversal methods in tcod.bsp for parity with libtcodpy.
+### Deprecated
+- Already deprecated bsp functions are now even more deprecated.
+
+## [8.2.0] - 2018-11-27
+### Added
+- New layout `tcod.FONT_LAYOUT_CP437`.
+### Changed
+- Updated libtcod to 1.10.2
+- `tcod.console_print_frame` and `Console.print_frame` now support Unicode
+ strings.
+### Deprecated
+- Deprecated using bytes strings for all printing functions.
+### Fixed
+- Console objects are now initialized with spaces. This fixes some blit
+ operations.
+- Unicode code-points above U+FFFF will now work on all platforms.
+
+## [8.1.1] - 2018-11-16
+### Fixed
+- Printing a frame with an empty string no longer displays a title bar.
+
+## [8.1.0] - 2018-11-15
+### Changed
+- Heightmap functions now support 'F_CONTIGUOUS' arrays.
+- `tcod.heightmap_new` now has an `order` parameter.
+- Updated SDL to 2.0.9
+### Deprecated
+- Deprecated heightmap functions which sample noise grids, this can be done
+ using the `Noise.sample_ogrid` method.
+
+## [8.0.0] - 2018-11-02
+### Changed
+- The default renderer can now be anything if not set manually.
+- Better error message for when a font file isn't found.
+
+## [7.0.1] - 2018-10-27
+### Fixed
+- Building from source was failing because `console_2tris.glsl*` was missing
+ from source distributions.
+
+## [7.0.0] - 2018-10-25
+### Added
+- New `RENDERER_SDL2` and `RENDERER_OPENGL2` renderers.
+### Changed
+- Updated libtcod to 1.9.0
+- Now requires SDL 2.0.5, which is not trivially installable on
+ Ubuntu 16.04 LTS.
+### Removed
+- Dropped support for Python versions before 3.5
+- Dropped support for MacOS versions before 10.9 Mavericks.
+
+## [6.0.7] - 2018-10-24
+### Fixed
+- The root console no longer loses track of buffers and console defaults on a
+ renderer change.
+
+## [6.0.6] - 2018-10-01
+### Fixed
+- Replaced missing wheels for older and 32-bit versions of MacOS.
+
+## [6.0.5] - 2018-09-28
+### Fixed
+- Resolved CDefError error during source installs.
+
+## [6.0.4] - 2018-09-11
+### Fixed
+- tcod.Key right-hand modifiers are now set independently at initialization,
+ instead of mirroring the left-hand modifier value.
+
+## [6.0.3] - 2018-09-05
+### Fixed
+- tcod.Key and tcod.Mouse no longer ignore initiation parameters.
+
+## [6.0.2] - 2018-08-28
+### Fixed
+- Fixed color constants missing at build-time.
+
+## [6.0.1] - 2018-08-24
+### Fixed
+- Source distributions were missing C++ source files.
+
+## [6.0.0] - 2018-08-23
+### Changed
+- Project renamed to tcod on PyPI.
+### Deprecated
+- Passing bytes strings to libtcodpy print functions is deprecated.
+### Fixed
+- Fixed libtcodpy print functions not accepting bytes strings.
+- libtcod constants are now generated at build-time fixing static analysis
+ tools.
+
+## [5.0.1] - 2018-07-08
+### Fixed
+- tdl.event no longer crashes with StopIteration on Python 3.7
+
+## [5.0.0] - 2018-07-05
+### Changed
+- tcod.path: all classes now use `shape` instead of `width` and `height`.
+- tcod.path now respects NumPy array shape, instead of assuming that arrays
+ need to be transposed from C memory order. From now on `x` and `y` mean
+ 1st and 2nd axis. This doesn't affect non-NumPy code.
+- tcod.path now has full support of non-contiguous memory.
+
+## [4.6.1] - 2018-06-30
+### Added
+- New function `tcod.line_where` for indexing NumPy arrays using a Bresenham
+ line.
+### Deprecated
+- Python 2.7 support will be dropped in the near future.
+
+## [4.5.2] - 2018-06-29
+### Added
+- New wheels for Python3.7 on Windows.
+### Fixed
+- Arrays from `tcod.heightmap_new` are now properly zeroed out.
+
+## [4.5.1] - 2018-06-23
+### Deprecated
+- Deprecated all libtcodpy map functions.
+### Fixed
+- `tcod.map_copy` could break the `tcod.map.Map` class.
+- `tcod.map_clear` `transparent` and `walkable` parameters were reversed.
+- When multiple SDL2 headers were installed, the wrong ones would be used when
+ the library is built.
+- Fails to build via pip unless Numpy is installed first.
+
+## [4.5.0] - 2018-06-12
+### Changed
+- Updated libtcod to v1.7.0
+- Updated SDL to v2.0.8
+- Error messages when failing to create an SDL window should be a less vague.
+- You no longer need to initialize libtcod before you can print to an
+ off-screen console.
+### Fixed
+- Avoid crashes if the root console has a character code higher than expected.
+### Removed
+- No more debug output when loading fonts.
+
+## [4.4.0] - 2018-05-02
+### Added
+- Added the libtcodpy module as an alias for tcod. Actual use of it is
+ deprecated, it exists primarily for backward compatibility.
+- Adding missing libtcodpy functions `console_has_mouse_focus` and
+ `console_is_active`.
+### Changed
+- Updated libtcod to v1.6.6
+
+## [4.3.2] - 2018-03-18
+### Deprecated
+- Deprecated the use of falsy console parameters with libtcodpy functions.
+### Fixed
+- Fixed libtcodpy image functions not supporting falsy console parameters.
+- Fixed tdl `Window.get_char` method. (Kaczor2704)
+
+## [4.3.1] - 2018-03-07
+### Fixed
+- Fixed cffi.api.FFIError "unsupported expression: expected a simple numeric
+ constant" error when building on platforms with an older cffi module and
+ newer SDL headers.
+- tcod/tdl Map and Console objects were not saving stride data when pickled.
+
+## [4.3.0] - 2018-02-01
+### Added
+- You can now set the numpy memory order on tcod.console.Console,
+ tcod.map.Map, and tdl.map.Map objects well as from the
+ tcod.console_init_root function.
+### Changed
+- The `console_init_root` `title` parameter is now optional.
+### Fixed
+- OpenGL renderer alpha blending is now consistent with all other render
+ modes.
+
+## [4.2.3] - 2018-01-06
+### Fixed
+- Fixed setup.py regression that could prevent building outside of the git
+ repository.
+
+## [4.2.2] - 2018-01-06
+### Fixed
+- The Windows dynamic linker will now prefer the bundled version of SDL.
+ This fixes:
+ "ImportError: DLL load failed: The specified procedure could not be found."
+- `key.c` is no longer set when `key.vk == KEY_TEXT`, this fixes a regression
+ which was causing events to be heard twice in the libtcod/Python tutorial.
+
+## [4.2.0] - 2018-01-02
+### Changed
+- Updated libtcod backend to v1.6.4
+- Updated SDL to v2.0.7 for Windows/MacOS.
+### Removed
+- Source distributions no longer include tests, examples, or fonts.
+ [Find these on GitHub.](https://github.com/libtcod/python-tcod)
+### Fixed
+- Fixed "final link failed: Nonrepresentable section on output" error
+ when compiling for Linux.
+- `tcod.console_init_root` defaults to the SDL renderer, other renderers
+ cause issues with mouse movement events.
+
+## [4.1.1] - 2017-11-02
+### Fixed
+- Fixed `ConsoleBuffer.blit` regression.
+- Console defaults corrected, the root console's blend mode and alignment is
+ the default value for newly made Console's.
+- You can give a byte string as a filename to load parsers.
+
+## [4.1.0] - 2017-07-19
+### Added
+- tdl Map class can now be pickled.
+### Changed
+- Added protection to the `transparent`, `walkable`, and `fov`
+ attributes in tcod and tdl Map classes, to prevent them from being
+ accidentally overridden.
+- tcod and tdl Map classes now use numpy arrays as their attributes.
+
+## [4.0.1] - 2017-07-12
+### Fixed
+- tdl: Fixed NameError in `set_fps`.
+
+## [4.0.0] - 2017-07-08
+### Changed
+- tcod.bsp: `BSP.split_recursive` parameter `random` is now `seed`.
+- tcod.console: `Console.blit` parameters have been rearranged.
+ Most of the parameters are now optional.
+- tcod.noise: `Noise.__init__` parameter `rand` is now named `seed`.
+- tdl: Changed `set_fps` parameter name to `fps`.
+### Fixed
+- tcod.bsp: Corrected spelling of max_vertical_ratio.
+
+## [3.2.0] - 2017-07-04
+### Changed
+- Merged libtcod-cffi dependency with TDL.
+### Fixed
+- Fixed boolean related crashes with Key 'text' events.
+- tdl.noise: Fixed crash when given a negative seed. As well as cases
+ where an instance could lose its seed being pickled.
+
+## [3.1.0] - 2017-05-28
+### Added
+- You can now pass tdl Console instances as parameters to libtcod-cffi
+ functions expecting a tcod Console.
+### Changed
+- Dependencies updated: `libtcod-cffi>=2.5.0,<3`
+- The `Console.tcod_console` attribute is being renamed to
+ `Console.console_c`.
+### Deprecated
+- The tdl.noise and tdl.map modules will be deprecated in the future.
+### Fixed
+- Resolved crash-on-exit issues for Windows platforms.
+
+## [3.0.2] - 2017-04-13
+### Changed
+- Dependencies updated: `libtcod-cffi>=2.4.3,<3`
+- You can now create Console instances before a call to `tdl.init`.
+### Removed
+- Dropped support for Python 3.3
+### Fixed
+- Resolved issues with MacOS builds.
+- 'OpenGL' and 'GLSL' renderers work again.
+
+## [3.0.1] - 2017-03-22
+### Changed
+- `KeyEvent`'s with `text` now have all their modifier keys set to False.
+### Fixed
+- Undefined behavior in text events caused crashes on 32-bit builds.
+
+## [3.0.0] - 2017-03-21
+### Added
+- `KeyEvent` supports libtcod text and meta keys.
+### Changed
+- `KeyEvent` parameters have been moved.
+- This version requires `libtcod-cffi>=2.3.0`.
+### Deprecated
+- `KeyEvent` camel capped attribute names are deprecated.
+### Fixed
+- Crashes with key-codes undefined by libtcod.
+- `tdl.map` typedef issues with libtcod-cffi.
+
+
+## [2.0.1] - 2017-02-22
+### Fixed
+- `tdl.init` renderer was defaulted to OpenGL which is not supported in the
+ current version of libtcod.
+
+## [2.0.0] - 2017-02-15
+### Changed
+- Dependencies updated, tdl now requires libtcod-cffi 2.x.x
+- Some event behaviors have changed with SDL2, event keys might be different
+ than what you expect.
+### Removed
+- Key repeat functions were removed from SDL2.
+ `set_key_repeat` is now stubbed, and does nothing.
+
+## [1.6.0] - 2016-11-18
+- Console.blit methods can now take fg_alpha and bg_alpha parameters.
+
+## [1.5.3] - 2016-06-04
+- set_font no longer crashes when loading a file without the implied font
+ size in its name
+
+## [1.5.2] - 2016-03-11
+- Fixed non-square Map instances
+
+## [1.5.1] - 2015-12-20
+- Fixed errors with Unicode and non-Unicode literals on Python 2
+- Fixed attribute error in compute_fov
+
+## [1.5.0] - 2015-07-13
+- python-tdl distributions are now universal builds
+- New Map class
+- map.bresenham now returns a list
+- This release will require libtcod-cffi v0.2.3 or later
+
+## [1.4.0] - 2015-06-22
+- The DLL's have been moved into another library which you can find at
+ https://github.com/HexDecimal/libtcod-cffi
+ You can use this library to have some raw access to libtcod if you want.
+ Plus it can be used alongside TDL.
+- The libtcod console objects in Console instances have been made public.
+- Added tdl.event.wait function. This function can called with a timeout and
+ can automatically call tdl.flush.
+
+## [1.3.1] - 2015-06-19
+- Fixed pathfinding regressions.
+
+## [1.3.0] - 2015-06-19
+- Updated backend to use python-cffi instead of ctypes. This gives decent
+ boost to speed in CPython and a drastic to boost in speed in PyPy.
+
+## [1.2.0] - 2015-06-06
+- The set_colors method now changes the default colors used by the draw_*
+ methods. You can use Python's Ellipsis to explicitly select default colors
+ this way.
+- Functions and Methods renamed to match Python's style-guide PEP 8, the old
+ function names still exist and are depreciated.
+- The fgcolor and bgcolor parameters have been shortened to fg and bg.
+
+## [1.1.7] - 2015-03-19
+- Noise generator now seeds properly.
+- The OS event queue will now be handled during a call to tdl.flush. This
+ prevents a common newbie programmer hang where events are handled
+ infrequently during long animations, simulations, or early development.
+- Fixed a major bug that would cause a crash in later versions of Python 3
+
+## [1.1.6] - 2014-06-27
+- Fixed a race condition when importing on some platforms.
+- Fixed a type issue with quickFOV on Linux.
+- Added a bresenham function to the tdl.map module.
+
+## [1.1.5] - 2013-11-10
+- A for loop can iterate over all coordinates of a Console.
+- drawStr can be configured to scroll or raise an error.
+- You can now configure or disable key repeating with tdl.event.setKeyRepeat
+- Typewriter class removed, use a Window instance for the same functionality.
+- setColors method fixed.
+
+## [1.1.4] - 2013-03-06
+- Merged the Typewriter and MetaConsole classes,
+ You now have a virtual cursor with Console and Window objects.
+- Fixed the clear method on the Window class.
+- Fixed screenshot function.
+- Fixed some drawing operations with unchanging backgrounds.
+- Instances of Console and Noise can be pickled and copied.
+- Added KeyEvent.keychar
+- Fixed event.keyWait, and now converts window closed events into Alt+F4.
+
+## [1.1.3] - 2012-12-17
+- Some of the setFont parameters were incorrectly labeled and documented.
+- setFont can auto-detect tilesets if the font sizes are in the filenames.
+- Added some X11 unicode tilesets, including Unifont.
+
+## [1.1.2] - 2012-12-13
+- Window title now defaults to the running scripts filename.
+- Fixed incorrect deltaTime for App.update
+- App will no longer call tdl.flush on its own, you'll need to call this
+ yourself.
+- tdl.noise module added.
+- clear method now defaults to black on black.
+
+## [1.1.1] - 2012-12-05
+- Map submodule added with AStar class and quickFOV function.
+- New Typewriter class.
+- Most console functions can use Python-style negative indexes now.
+- New App.runOnce method.
+- Rectangle geometry is less strict.
+
+## [1.1.0] - 2012-10-04
+- KeyEvent.keyname is now KeyEvent.key
+- MouseButtonEvent.button now behaves like KeyEvent.keyname does.
+- event.App class added.
+- Drawing methods no longer have a default for the character parameter.
+- KeyEvent.ctrl is now KeyEvent.control
+
+## [1.0.8] - 2010-04-07
+- No longer works in Python 2.5 but now works in 3.x and has been partly
+ tested.
+- Many bug fixes.
+
+## [1.0.5] - 2010-04-06
+- Got rid of setuptools dependency, this will make it much more compatible
+ with Python 3.x
+- Fixed a typo with the MacOS library import.
+
+## [1.0.4] - 2010-04-06
+- All constant colors (C_*) have been removed, they may be put back in later.
+- Made some type assertion failures show the value they received to help in
+ general debugging. Still working on it.
+- Added MacOS and 64-bit Linux support.
+
+## [1.0.0] - 2009-01-31
+- First public release.
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
deleted file mode 100755
index 6c2b3cbc..00000000
--- a/CHANGELOG.rst
+++ /dev/null
@@ -1,1318 +0,0 @@
-===========
- Changelog
-===========
-Changes relevant to the users of python-tcod are documented here.
-
-This project adheres to `Semantic Versioning `_ since
-v2.0.0
-
-Unreleased
-------------------
-
-13.1.0 - 2021-10-22
--------------------
-Added
- - Added the `tcod.tileset.procedural_block_elements` function.
-Removed
- - Python 3.6 is no longer supported.
-
-13.0.0 - 2021-09-20
--------------------
-Changed
- - Console print and drawing functions now always use absolute coordinates for negative numbers.
-
-12.7.3 - 2021-08-13
--------------------
-Deprecated
- - `tcod.console_is_key_pressed` was replaced with `tcod.event.get_keyboard_state`.
- - `tcod.console_from_file` is deprecated.
- - The `.asc` and `.apf` formats are no longer actively supported.
-
-Fixed
- - Fixed the parsing of SDL 2.0.16 headers.
-
-12.7.2 - 2021-07-01
--------------------
-Fixed
- - *Scancode* and *KeySym* enums no longer crash when SDL returns an unexpected value.
-
-12.7.1 - 2021-06-30
--------------------
-Added
- - Started uploading wheels for ARM64 macOS.
-
-12.7.0 - 2021-06-29
--------------------
-Added
- - *tcod.image* and *tcod.tileset* now support *pathlib*.
-
-Fixed
- - Wheels for 32-bit Windows now deploy again.
-
-12.6.2 - 2021-06-15
--------------------
-Fixed
- - Git is no longer required to install from source.
-
-12.6.1 - 2021-06-09
--------------------
-Fixed
- - Fixed version mismatch when building from sources.
-
-12.6.0 - 2021-06-09
--------------------
-Added
- - Added the *decoration* parameter to *Console.draw_frame*.
- You may use this parameter to designate custom glyphs as the frame border.
-
-Deprecated
- - The handling of negative indexes given to console drawing and printing
- functions will be changed to be used as absolute coordinates in the future.
-
-12.5.1 - 2021-05-30
--------------------
-Fixed
- - The setup script should no longer fail silently when cffi is unavailable.
-
-12.5.0 - 2021-05-21
--------------------
-Changed
- - `KeyboardEvent`'s '`scancode`, `sym`, and `mod` attributes now use their respective enums.
-
-12.4.0 - 2021-05-21
--------------------
-Added
- - Added modernized REXPaint saving/loading functions.
- - `tcod.console.load_xp`
- - `tcod.console.save_xp`
-
-Changed
- - Using `libtcod 1.18.1`.
- - `tcod.event.KeySym` and `tcod.event.Scancode` can now be hashed.
-
-12.3.2 - 2021-05-15
--------------------
-Changed
- - Using `libtcod 1.17.1`.
-
-Fixed
- - Fixed regression with loading PNG images.
-
-12.3.1 - 2021-05-13
--------------------
-Fixed
- - Fix Windows deployment.
-
-12.3.0 - 2021-05-13
--------------------
-Added
- - New keyboard enums:
- - `tcod.event.KeySym`
- - `tcod.event.Scancode`
- - `tcod.event.Modifier`
- - New functions:
- - `tcod.event.get_keyboard_state`
- - `tcod.event.get_modifier_state`
- - Added `tcod.console.rgb_graphic` and `tcod.console.rgba_graphic` dtypes.
- - Another name for the Console array attributes: `Console.rgb` and `Console.rgba`.
-
-Changed
- - Using `libtcod 1.17.0`.
-
-Deprecated
- - `Console_tiles_rgb` is being renamed to `Console.rgb`.
- - `Console_tiles` being renamed to `Console.rgba`.
-
-Fixed
- - Contexts now give a more useful error when pickled.
- - Fixed regressions with `tcod.console_print_frame` and `Console.print_frame`
- when given empty strings as the banner.
-
-12.2.0 - 2021-04-09
--------------------
-Added
- - Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums.
- - Added `tcod.noise.grid` helper function.
-
-Deprecated
- - The non-enum noise implementation names have been deprecated.
-
-Fixed
- - Indexing Noise classes now works with the FBM implementation.
-
-12.1.0 - 2021-04-01
--------------------
-Added
- - Added package-level PyInstaller hook.
-
-Changed
- - Using `libtcod 1.16.7`.
- - `tcod.path.dijkstra2d` now returns the output and accepts an `out` parameter.
-
-Deprecated
- - In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given.
-
-Fixed
- - Fixed crashes from loading tilesets with non-square tile sizes.
- - Tilesets with a size of 0 should no longer crash when used.
- - Prevent division by zero from recommended-console-size functions.
-
-12.0.0 - 2021-03-05
--------------------
-Deprecated
- - The Random class will now warn if the seed it's given will not used
- deterministically. It will no longer accept non-integer seeds in the future.
-
-Changed
- - Now bundles SDL 2.0.14 for MacOS.
- - `tcod.event` can now detect and will warn about uninitialized tile
- attributes on mouse events.
-
-Removed
- - Python 3.5 is no longer supported.
- - The `tdl` module has been dropped.
-
-11.19.3 - 2021-01-07
---------------------
-Fixed
- - Some wheels had broken version metadata.
-
-11.19.2 - 2020-12-30
---------------------
-Changed
- - Now bundles SDL 2.0.10 for MacOS and SDL 2.0.14 for Windows.
-
-Fixed
- - MacOS wheels were failing to bundle dependencies for SDL2.
-
-11.19.1 - 2020-12-29
---------------------
-Fixed
- - MacOS wheels failed to deploy for the previous version.
-
-11.19.0 - 2020-12-29
---------------------
-Added
- - Added the important `order` parameter to `Context.new_console`.
-
-11.18.3 - 2020-12-28
---------------------
-Changed
- - Now bundles SDL 2.0.14 for Windows/MacOS.
-
-Deprecated
- - Support for Python 3.5 will be dropped.
- - `tcod.console_load_xp` has been deprecated, `tcod.console_from_xp` can load
- these files without modifying an existing console.
-
-Fixed
- - `tcod.console_from_xp` now has better error handling (instead of crashing.)
- - Can now compile with SDL 2.0.14 headers.
-
-11.18.2 - 2020-12-03
---------------------
-Fixed
- - Fixed missing `tcod.FOV_SYMMETRIC_SHADOWCAST` constant.
- - Fixed regression in `tcod.sys_get_current_resolution` behavior. This
- function now returns the monitor resolution as was previously expected.
-
-11.18.1 - 2020-11-30
---------------------
-Fixed
- - Code points from the Private Use Area will now print correctly.
-
-11.18.0 - 2020-11-13
---------------------
-Added
- - New context method `Context.new_console`.
-
-Changed
- - Using `libtcod 1.16.0-alpha.15`.
-
-11.17.0 - 2020-10-30
---------------------
-Added
- - New FOV implementation: `tcod.FOV_SYMMETRIC_SHADOWCAST`.
-
-Changed
- - Using `libtcod 1.16.0-alpha.14`.
-
-11.16.1 - 2020-10-28
---------------------
-Deprecated
- - Changed context deprecations to PendingDeprecationWarning to reduce mass
- panic from tutorial followers.
-
-Fixed
- - Fixed garbled titles and crashing on some platforms.
-
-11.16.0 - 2020-10-23
---------------------
-Added
- - Added `tcod.context.new` function.
- - Contexts now support a CLI.
- - You can now provide the window x,y position when making contexts.
- - `tcod.noise.Noise` instances can now be indexed to generate noise maps.
-
-Changed
- - Using `libtcod 1.16.0-alpha.13`.
- - The OpenGL 2 renderer can now use `SDL_HINT_RENDER_SCALE_QUALITY` to
- determine the tileset upscaling filter.
- - Improved performance of the FOV_BASIC algorithm.
-
-Deprecated
- - `tcod.context.new_window` and `tcod.context.new_terminal` have been replaced
- by `tcod.context.new`.
-
-Fixed
- - Pathfinders will now work with boolean arrays.
- - Console blits now ignore alpha compositing which would result in division by
- zero.
- - `tcod.console_is_key_pressed` should work even if libtcod events are ignored.
- - The `TCOD_RENDERER` and `TCOD_VSYNC` environment variables should work now.
- - `FOV_PERMISSIVE` algorithm is now reentrant.
-
-11.15.3 - 2020-07-30
---------------------
-Fixed
- - `tcod.tileset.Tileset.remap`, codepoint and index were swapped.
-
-11.15.2 - 2020-07-27
---------------------
-Fixed
- - `tcod.path.dijkstra2d`, fixed corrupted output with int8 arrays.
-
-11.15.1 - 2020-07-26
---------------------
-Changed
- - `tcod.event.EventDispatch` now uses the absolute names for event type hints
- so that IDE's can better auto-complete method overrides.
-
-Fixed
- - Fixed libtcodpy heightmap data alignment issues on non-square maps.
-
-11.15.0 - 2020-06-29
---------------------
-Added
- - `tcod.path.SimpleGraph` for pathfinding on simple 2D arrays.
-
-Changed
- - `tcod.path.CustomGraph` now accepts an `order` parameter.
-
-11.14.0 - 2020-06-23
---------------------
-Added
- - New `tcod.los` module for NumPy-based line-of-sight algorithms.
- Includes `tcod.los.bresenham`.
-
-Deprecated
- - `tcod.line_where` and `tcod.line_iter` have been deprecated.
-
-11.13.6 - 2020-06-19
---------------------
-Deprecated
- - `console_init_root` and `console_set_custom_font` have been replaced by the
- modern API.
- - All functions which handle SDL windows without a context are deprecated.
- - All functions which modify a globally active tileset are deprecated.
- - `tcod.map.Map` is deprecated, NumPy arrays should be passed to functions
- directly instead of through this class.
-
-11.13.5 - 2020-06-15
---------------------
-Fixed
- - Install requirements will no longer try to downgrade `cffi`.
-
-11.13.4 - 2020-06-15
---------------------
-
-11.13.3 - 2020-06-13
---------------------
-Fixed
- - `cffi` requirement has been updated to version `1.13.0`.
- The older versions raise TypeError's.
-
-11.13.2 - 2020-06-12
---------------------
-Fixed
- - SDL related errors during package installation are now more readable.
-
-11.13.1 - 2020-05-30
---------------------
-Fixed
- - `tcod.event.EventDispatch`: `ev_*` methods now allow `Optional[T]` return
- types.
-
-11.13.0 - 2020-05-22
---------------------
-Added
- - `tcod.path`: New `Pathfinder` and `CustomGraph` classes.
-
-Changed
- - Added `edge_map` parameter to `tcod.path.dijkstra2d` and
- `tcod.path.hillclimb2d`.
-
-Fixed
- - tcod.console_init_root` and context initializing functions were not
- raising exceptions on failure.
-
-11.12.1 - 2020-05-02
---------------------
-Fixed
- - Prevent adding non-existent 2nd halves to potential double-wide charterers.
-
-11.12.0 - 2020-04-30
---------------------
-Added
- - Added `tcod.context` module. You now have more options for making libtcod
- controlled contexts.
- - `tcod.tileset.load_tilesheet`: Load a simple tilesheet as a Tileset.
- - `Tileset.remap`: Reassign codepoints to tiles on a Tileset.
- - `tcod.tileset.CHARMAP_CP437`: Character mapping for `load_tilesheet`.
- - `tcod.tileset.CHARMAP_TCOD`: Older libtcod layout.
-
-Changed
- - `EventDispatch.dispatch` can now return the values returned by the `ev_*`
- methods. The class is now generic to support type checking these values.
- - Event mouse coordinates are now strictly int types.
- - Submodules are now implicitly imported.
-
-11.11.4 - 2020-04-26
---------------------
-Changed
- - Using `libtcod 1.16.0-alpha.10`.
-
-Fixed
- - Fixed characters being dropped when color codes were used.
-
-11.11.3 - 2020-04-24
---------------------
-Changed
- - Using `libtcod 1.16.0-alpha.9`.
-
-Fixed
- - `FOV_DIAMOND` and `FOV_RESTRICTIVE` algorithms are now reentrant.
- `libtcod#48 `_
- - The `TCOD_VSYNC` environment variable was being ignored.
-
-11.11.2 - 2020-04-22
---------------------
-
-11.11.1 - 2020-04-03
---------------------
-Changed
- - Using `libtcod 1.16.0-alpha.8`.
-
-Fixed
- - Changing the active tileset now redraws tiles correctly on the next frame.
-
-11.11.0 - 2020-04-02
---------------------
-Added
- - Added `Console.close` as a more obvious way to close the active window of a
- root console.
-
-Changed
- - GCC is no longer needed to compile the library on Windows.
- - Using `libtcod 1.16.0-alpha.7`.
- - `tcod.console_flush` will now accept an RGB tuple as a `clear_color`.
-
-Fixed
- - Changing the active tileset will now properly show it on the next render.
-
-11.10.0 - 2020-03-26
---------------------
-Added
- - Added `tcod.tileset.load_bdf`, you can now load BDF fonts.
- - `tcod.tileset.set_default` and `tcod.tileset.get_default` are now stable.
-
-Changed
- - Using `libtcod 1.16.0-alpha.6`.
-
-Deprecated
- - The `snap_to_integer` parameter in `tcod.console_flush` has been deprecated
- since it can cause minor scaling issues which don't exist when using
- `integer_scaling` instead.
-
-11.9.2 - 2020-03-17
--------------------
-Fixed
- - Fixed segfault after the Tileset returned by `tcod.tileset.get_default` goes
- out of scope.
-
-11.9.1 - 2020-02-28
--------------------
-Changed
- - Using `libtcod 1.16.0-alpha.5`.
- - Mouse tile coordinates are now always zero before the first call to
- `tcod.console_flush`.
-
-11.9.0 - 2020-02-22
--------------------
-Added
- - New method `Tileset.render` renders an RGBA NumPy array from a tileset and
- a console.
-
-11.8.2 - 2020-02-22
--------------------
-Fixed
- - Prevent KeyError when representing unusual keyboard symbol constants.
-
-11.8.1 - 2020-02-22
--------------------
-Changed
- - Using `libtcod 1.16.0-alpha.4`.
-
-Fixed
- - Mouse tile coordinates are now correct on any resized window.
-
-11.8.0 - 2020-02-21
--------------------
-Added
- - Added `tcod.console.recommended_size` for when you want to change your main
- console size at runtime.
- - Added `Console.tiles_rgb` as a replacement for `Console.tiles2`.
-
-Changed
- - Using `libtcod 1.16.0-alpha.3`.
- - Added parameters to `tcod.console_flush`, you can now manually provide a
- console and adjust how it is presented.
-
-Deprecated
- - `Console.tiles2` is deprecated in favour of `Console.tiles_rgb`.
- - `Console.buffer` is now deprecated in favour of `Console.tiles`, instead of
- the other way around.
-
-Fixed
- - Fixed keyboard state and mouse state functions losing state when events were
- flushed.
-
-11.7.2 - 2020-02-16
--------------------
-Fixed
- - Fixed regression in `tcod.console_clear`.
-
-11.7.1 - 2020-02-16
--------------------
-Fixed
- - Fixed regression in `Console.draw_frame`.
- - The wavelet noise generator now excludes -1.0f and 1.0f as return values.
- - Fixed console fading color regression.
-
-11.7.0 - 2020-02-14
--------------------
-Changed
- - Using `libtcod 1.16.0-alpha.2`.
- - When a renderer fails to load it will now fallback to a different one.
- The order is: OPENGL2 -> OPENGL -> SDL2.
- - The default renderer is now SDL2.
- - The SDL and OPENGL renderers are no longer deprecated, but they now point to
- slightly different backward compatible implementations.
-
-Deprecated
- - The use of `libtcod.cfg` and `terminal.png` is deprecated.
-
-Fixed
- - `tcod.sys_update_char` now works with the newer renderers.
- - Fixed buffer overflow in name generator.
- - `tcod.image_from_console` now works with the newer renderers.
- - New renderers now auto-load fonts from `libtcod.cfg` or `terminal.png`.
-
-11.6.0 - 2019-12-05
--------------------
-Changed
- - Console blit operations now perform per-cell alpha transparency.
-
-11.5.1 - 2019-11-23
--------------------
-Fixed
- - Python 3.8 wheels failed to deploy.
-
-11.5.0 - 2019-11-22
--------------------
-Changed
- - Quarter block elements are now rendered using Unicode instead of a custom
- encoding.
-
-Fixed
- - `OPENGL` and `GLSL` renderers were not properly clearing space characters.
-
-11.4.1 - 2019-10-15
--------------------
-Added
- - Uploaded Python 3.8 wheels to PyPI.
-
-11.4.0 - 2019-09-20
--------------------
-Added
- - Added `__array_interface__` to the Image class.
- - Added `Console.draw_semigraphics` as a replacement for blit_2x functions.
- `draw_semigraphics` can handle array-like objects.
- - `Image.from_array` class method creates an Image from an array-like object.
- - `tcod.image.load` loads a PNG file as an RGBA array.
-
-Changed
- - `Console.tiles` is now named `Console.buffer`.
-
-11.3.0 - 2019-09-06
--------------------
-Added
- - New attribute `Console.tiles2` is similar to `Console.tiles` but without an
- alpha channel.
-
-11.2.2 - 2019-08-25
--------------------
-Fixed
- - Fixed a regression preventing PyInstaller distributions from loading SDL2.
-
-11.2.1 - 2019-08-25
--------------------
-
-11.2.0 - 2019-08-24
--------------------
-Added
- - `tcod.path.dijkstra2d`: Computes Dijkstra from an arbitrary initial state.
- - `tcod.path.hillclimb2d`: Returns a path from a distance array.
- - `tcod.path.maxarray`: Creates arrays filled with maximum finite values.
-
-Fixed
- - Changing the tiles of an active tileset on OPENGL2 will no longer leave
- temporary artifact tiles.
- - It's now harder to accidentally import tcod's internal modules.
-
-11.1.2 - 2019-08-02
--------------------
-Changed
- - Now bundles SDL 2.0.10 for Windows/MacOS.
-
-Fixed
- - Can now parse SDL 2.0.10 headers during installation without crashing.
-
-11.1.1 - 2019-08-01
--------------------
-Deprecated
- - Using an out-of-bounds index for field-of-view operations now raises a
- warning, which will later become an error.
-
-Fixed
- - Changing the tiles of an active tileset will now work correctly.
-
-11.1.0 - 2019-07-05
--------------------
-Added
- - You can now set the `TCOD_RENDERER` and `TCOD_VSYNC` environment variables to
- force specific options to be used.
- Example: ``TCOD_RENDERER=sdl2 TCOD_VSYNC=1``
-
-Changed
- - `tcod.sys_set_renderer` now raises an exception if it fails.
-
-Fixed
- - `tcod.console_map_ascii_code_to_font` functions will now work when called
- before `tcod.console_init_root`.
-
-11.0.2 - 2019-06-21
--------------------
-Changed
- - You no longer need OpenGL to build python-tcod.
-
-11.0.1 - 2019-06-21
--------------------
-Changed
- - Better runtime checks for Windows dependencies should now give distinct
- errors depending on if the issue is SDL2 or missing redistributables.
-
-Fixed
- - Changed NumPy type hints from `np.array` to `np.ndarray` which should
- resolve issues.
-
-11.0.0 - 2019-06-14
--------------------
-Changed
- - `tcod.map.compute_fov` now takes a 2-item tuple instead of separate `x` and
- `y` parameters. This causes less confusion over how axes are aligned.
-
-10.1.1 - 2019-06-02
--------------------
-Changed
- - Better string representations for `tcod.event.Event` subclasses.
-
-Fixed
- - Fixed regressions in text alignment for non-rectangle print functions.
-
-10.1.0 - 2019-05-24
--------------------
-Added
- - `tcod.console_init_root` now has an optional `vsync` parameter.
-
-10.0.5 - 2019-05-17
--------------------
-Fixed
- - Fixed shader compilation issues in the OPENGL2 renderer.
- - Fallback fonts should fail less on Linux.
-
-10.0.4 - 2019-05-17
--------------------
-Changed
- - Now depends on cffi 0.12 or later.
-
-Fixed
- - `tcod.console_init_root` and `tcod.console_set_custom_font` will raise
- exceptions instead of terminating.
- - Fixed issues preventing `tcod.event` from working on 32-bit Windows.
-
-10.0.3 - 2019-05-10
--------------------
-Fixed
- - Corrected bounding box issues with the `Console.print_box` method.
-
-10.0.2 - 2019-04-26
--------------------
-Fixed
- - Resolved Color warnings when importing tcod.
- - When compiling, fixed a name conflict with endianness macros on FreeBSD.
-
-10.0.1 - 2019-04-19
--------------------
-Fixed
- - Fixed horizontal alignment for TrueType fonts.
- - Fixed taking screenshots with the older SDL renderer.
-
-10.0.0 - 2019-03-29
--------------------
-Added
- - New `Console.tiles` array attribute.
-Changed
- - `Console.DTYPE` changed to add alpha to its color types.
-Fixed
- - Console printing was ignoring color codes at the beginning of a string.
-
-9.3.0 - 2019-03-15
-------------------
-Added
- - The SDL2/OPENGL2 renderers can potentially use a fall-back font when none
- are provided.
- - New function `tcod.event.get_mouse_state`.
- - New function `tcod.map.compute_fov` lets you get a visibility array directly
- from a transparency array.
-Deprecated
- - The following functions and classes have been deprecated.
- - `tcod.Key`
- - `tcod.Mouse`
- - `tcod.mouse_get_status`
- - `tcod.console_is_window_closed`
- - `tcod.console_check_for_keypress`
- - `tcod.console_wait_for_keypress`
- - `tcod.console_delete`
- - `tcod.sys_check_for_event`
- - `tcod.sys_wait_for_event`
- - The SDL, OPENGL, and GLSL renderers have been deprecated.
- - Many libtcodpy functions have been marked with PendingDeprecationWarning's.
-Fixed
- - To be more compatible with libtcodpy `tcod.console_init_root` will default
- to the SDL render, but will raise warnings when an old renderer is used.
-
-9.2.5 - 2019-03-04
-------------------
-Fixed
- - Fixed `tcod.namegen_generate_custom`.
-
-9.2.4 - 2019-03-02
-------------------
-Fixed
- - The `tcod` package is has been marked as typed and will now work with MyPy.
-
-9.2.3 - 2019-03-01
-------------------
-Deprecated
- - The behavior for negative indexes on the new print functions may change in
- the future.
- - Methods and functionality preventing `tcod.Color` from behaving like a tuple
- have been deprecated.
-
-9.2.2 - 2019-02-26
-------------------
-Fixed
- - `Console.print_box` wasn't setting the background color by default.
-
-9.2.1 - 2019-02-25
-------------------
-Fixed
- - `tcod.sys_get_char_size` fixed on the new renderers.
-
-9.2.0 - 2019-02-24
-------------------
-Added
- - New `tcod.console.get_height_rect` function, which can be used to get the
- height of a print call without an existing console.
- - New `tcod.tileset` module, with a `set_truetype_font` function.
-Fixed
- - The new print methods now handle alignment according to how they were
- documented.
- - `SDL2` and `OPENGL2` now support screenshots.
- - Windows and MacOS builds now restrict exported SDL2 symbols to only
- SDL 2.0.5; This will avoid hard to debug import errors when the wrong
- version of SDL is dynamically linked.
- - The root console now starts with a white foreground.
-
-9.1.0 - 2019-02-23
-------------------
-Added
- - Added the `tcod.random.MULTIPLY_WITH_CARRY` constant.
-Changed
- - The overhead for warnings has been reduced when running Python with the
- optimize `-O` flag.
- - `tcod.random.Random` now provides a default algorithm.
-
-9.0.0 - 2019-02-17
-------------------
-Changed
- - New console methods now default to an `fg` and `bg` of None instead of
- white-on-black.
-
-8.5.0 - 2019-02-15
-------------------
-Added
- - `tcod.console.Console` now supports `str` and `repr`.
- - Added new Console methods which are independent from the console defaults.
- - You can now give an array when initializing a `tcod.console.Console`
- instance.
- - `Console.clear` can now take `ch`, `fg`, and `bg` parameters.
-Changed
- - Updated libtcod to 1.10.6
- - Printing generates more compact layouts.
-Deprecated
- - Most libtcodpy console functions have been replaced by the tcod.console
- module.
- - Deprecated the `set_key_color` functions. You can pass key colors to
- `Console.blit` instead.
- - `Console.clear` should be given the colors to clear with as parameters,
- rather than by using `default_fg` or `default_bg`.
- - Most functions which depend on console default values have been deprecated.
- The new deprecation warnings will give details on how to make default values
- explicit.
-Fixed
- - `tcod.console.Console.blit` was ignoring the key color set by
- `Console.set_key_color`.
- - The `SDL2` and `OPENGL2` renders can now large numbers of tiles.
-
-8.4.3 - 2019-02-06
-------------------
-Changed
- - Updated libtcod to 1.10.5
- - The SDL2/OPENGL2 renderers will now auto-detect a custom fonts key-color.
-
-8.4.2 - 2019-02-05
-------------------
-Deprecated
- - The tdl module has been deprecated.
- - The libtcodpy parser functions have been deprecated.
-Fixed
- - `tcod.image_is_pixel_transparent` and `tcod.image_get_alpha` now return
- values.
- - `Console.print_frame` was clearing tiles outside if its bounds.
- - The `FONT_LAYOUT_CP437` layout was incorrect.
-
-8.4.1 - 2019-02-01
-------------------
-Fixed
- - Window event types were not upper-case.
- - Fixed regression where libtcodpy mouse wheel events unset mouse coordinates.
-
-8.4.0 - 2019-01-31
-------------------
-Added
- - Added tcod.event module, based off of the sdlevent.py shim.
-Changed
- - Updated libtcod to 1.10.3
-Fixed
- - Fixed libtcodpy `struct_add_value_list` function.
- - Use correct math for tile-based delta in mouse events.
- - New renderers now support tile-based mouse coordinates.
- - SDL2 renderer will now properly refresh after the window is resized.
-
-8.3.2 - 2018-12-28
-------------------
-Fixed
- - Fixed rare access violations for some functions which took strings as
- parameters, such as `tcod.console_init_root`.
-
-8.3.1 - 2018-12-28
-------------------
-Fixed
- - libtcodpy key and mouse functions will no longer accept the wrong types.
- - The `new_struct` method was not being called for libtcodpy's custom parsers.
-
-8.3.0 - 2018-12-08
-------------------
-Added
- - Added BSP traversal methods in tcod.bsp for parity with libtcodpy.
-Deprecated
- - Already deprecated bsp functions are now even more deprecated.
-
-8.2.0 - 2018-11-27
-------------------
-Added
- - New layout `tcod.FONT_LAYOUT_CP437`.
-Changed
- - Updated libtcod to 1.10.2
- - `tcod.console_print_frame` and `Console.print_frame` now support Unicode
- strings.
-Deprecated
- - Deprecated using bytes strings for all printing functions.
-Fixed
- - Console objects are now initialized with spaces. This fixes some blit
- operations.
- - Unicode code-points above U+FFFF will now work on all platforms.
-
-8.1.1 - 2018-11-16
-------------------
-Fixed
- - Printing a frame with an empty string no longer displays a title bar.
-
-8.1.0 - 2018-11-15
-------------------
-Changed
- - Heightmap functions now support 'F_CONTIGUOUS' arrays.
- - `tcod.heightmap_new` now has an `order` parameter.
- - Updated SDL to 2.0.9
-Deprecated
- - Deprecated heightmap functions which sample noise grids, this can be done
- using the `Noise.sample_ogrid` method.
-
-8.0.0 - 2018-11-02
-------------------
-Changed
- - The default renderer can now be anything if not set manually.
- - Better error message for when a font file isn't found.
-
-7.0.1 - 2018-10-27
-------------------
-Fixed
- - Building from source was failing because `console_2tris.glsl*` was missing
- from source distributions.
-
-7.0.0 - 2018-10-25
-------------------
-Added
- - New `RENDERER_SDL2` and `RENDERER_OPENGL2` renderers.
-Changed
- - Updated libtcod to 1.9.0
- - Now requires SDL 2.0.5, which is not trivially installable on
- Ubuntu 16.04 LTS.
-Removed
- - Dropped support for Python versions before 3.5
- - Dropped support for MacOS versions before 10.9 Mavericks.
-
-6.0.7 - 2018-10-24
-------------------
-Fixed
- - The root console no longer loses track of buffers and console defaults on a
- renderer change.
-
-6.0.6 - 2018-10-01
-------------------
-Fixed
- - Replaced missing wheels for older and 32-bit versions of MacOS.
-
-6.0.5 - 2018-09-28
-------------------
-Fixed
- - Resolved CDefError error during source installs.
-
-6.0.4 - 2018-09-11
-------------------
-Fixed
- - tcod.Key right-hand modifiers are now set independently at initialization,
- instead of mirroring the left-hand modifier value.
-
-6.0.3 - 2018-09-05
-------------------
-Fixed
- - tcod.Key and tcod.Mouse no longer ignore initiation parameters.
-
-6.0.2 - 2018-08-28
-------------------
-Fixed
- - Fixed color constants missing at build-time.
-
-6.0.1 - 2018-08-24
-------------------
-Fixed
- - Source distributions were missing C++ source files.
-
-6.0.0 - 2018-08-23
-------------------
-Changed
- - Project renamed to tcod on PyPI.
-Deprecated
- - Passing bytes strings to libtcodpy print functions is deprecated.
-Fixed
- - Fixed libtcodpy print functions not accepting bytes strings.
- - libtcod constants are now generated at build-time fixing static analysis
- tools.
-
-5.0.1 - 2018-07-08
-------------------
-Fixed
- - tdl.event no longer crashes with StopIteration on Python 3.7
-
-5.0.0 - 2018-07-05
-------------------
-Changed
- - tcod.path: all classes now use `shape` instead of `width` and `height`.
- - tcod.path now respects NumPy array shape, instead of assuming that arrays
- need to be transposed from C memory order. From now on `x` and `y` mean
- 1st and 2nd axis. This doesn't affect non-NumPy code.
- - tcod.path now has full support of non-contiguous memory.
-
-4.6.1 - 2018-06-30
-------------------
-Added
- - New function `tcod.line_where` for indexing NumPy arrays using a Bresenham
- line.
-Deprecated
- - Python 2.7 support will be dropped in the near future.
-
-4.5.2 - 2018-06-29
-------------------
-Added
- - New wheels for Python3.7 on Windows.
-Fixed
- - Arrays from `tcod.heightmap_new` are now properly zeroed out.
-
-4.5.1 - 2018-06-23
-------------------
-Deprecated
- - Deprecated all libtcodpy map functions.
-Fixed
- - `tcod.map_copy` could break the `tcod.map.Map` class.
- - `tcod.map_clear` `transparent` and `walkable` parameters were reversed.
- - When multiple SDL2 headers were installed, the wrong ones would be used when
- the library is built.
- - Fails to build via pip unless Numpy is installed first.
-
-4.5.0 - 2018-06-12
-------------------
-Changed
- - Updated libtcod to v1.7.0
- - Updated SDL to v2.0.8
- - Error messages when failing to create an SDL window should be a less vague.
- - You no longer need to initialize libtcod before you can print to an
- off-screen console.
-Fixed
- - Avoid crashes if the root console has a character code higher than expected.
-Removed
- - No more debug output when loading fonts.
-
-4.4.0 - 2018-05-02
-------------------
-Added
- - Added the libtcodpy module as an alias for tcod. Actual use of it is
- deprecated, it exists primarily for backward compatibility.
- - Adding missing libtcodpy functions `console_has_mouse_focus` and
- `console_is_active`.
-Changed
- - Updated libtcod to v1.6.6
-
-4.3.2 - 2018-03-18
-------------------
-Deprecated
- - Deprecated the use of falsy console parameters with libtcodpy functions.
-Fixed
- - Fixed libtcodpy image functions not supporting falsy console parameters.
- - Fixed tdl `Window.get_char` method. (Kaczor2704)
-
-4.3.1 - 2018-03-07
-------------------
-Fixed
- - Fixed cffi.api.FFIError "unsupported expression: expected a simple numeric
- constant" error when building on platforms with an older cffi module and
- newer SDL headers.
- - tcod/tdl Map and Console objects were not saving stride data when pickled.
-
-4.3.0 - 2018-02-01
-------------------
-Added
- - You can now set the numpy memory order on tcod.console.Console,
- tcod.map.Map, and tdl.map.Map objects well as from the
- tcod.console_init_root function.
-Changed
- - The `console_init_root` `title` parameter is now optional.
-Fixed
- - OpenGL renderer alpha blending is now consistent with all other render
- modes.
-
-4.2.3 - 2018-01-06
-------------------
-Fixed
- - Fixed setup.py regression that could prevent building outside of the git
- repository.
-
-4.2.2 - 2018-01-06
-------------------
-Fixed
- - The Windows dynamic linker will now prefer the bundled version of SDL.
- This fixes:
- "ImportError: DLL load failed: The specified procedure could not be found."
- - `key.c` is no longer set when `key.vk == KEY_TEXT`, this fixes a regression
- which was causing events to be heard twice in the libtcod/Python tutorial.
-
-4.2.0 - 2018-01-02
-------------------
-Changed
- - Updated libtcod backend to v1.6.4
- - Updated SDL to v2.0.7 for Windows/MacOS.
-Removed
- - Source distributions no longer include tests, examples, or fonts.
- `Find these on GitHub. `_
-Fixed
- - Fixed "final link failed: Nonrepresentable section on output" error
- when compiling for Linux.
- - `tcod.console_init_root` defaults to the SDL renderer, other renderers
- cause issues with mouse movement events.
-
-4.1.1 - 2017-11-02
-------------------
-Fixed
- - Fixed `ConsoleBuffer.blit` regression.
- - Console defaults corrected, the root console's blend mode and alignment is
- the default value for newly made Console's.
- - You can give a byte string as a filename to load parsers.
-
-4.1.0 - 2017-07-19
-------------------
-Added
- - tdl Map class can now be pickled.
-Changed
- - Added protection to the `transparent`, `walkable`, and `fov`
- attributes in tcod and tdl Map classes, to prevent them from being
- accidentally overridden.
- - tcod and tdl Map classes now use numpy arrays as their attributes.
-
-4.0.1 - 2017-07-12
-------------------
-Fixed
- - tdl: Fixed NameError in `set_fps`.
-
-4.0.0 - 2017-07-08
-------------------
-Changed
- - tcod.bsp: `BSP.split_recursive` parameter `random` is now `seed`.
- - tcod.console: `Console.blit` parameters have been rearranged.
- Most of the parameters are now optional.
- - tcod.noise: `Noise.__init__` parameter `rand` is now named `seed`.
- - tdl: Changed `set_fps` parameter name to `fps`.
-Fixed
- - tcod.bsp: Corrected spelling of max_vertical_ratio.
-
-3.2.0 - 2017-07-04
-------------------
-Changed
- - Merged libtcod-cffi dependency with TDL.
-Fixed
- - Fixed boolean related crashes with Key 'text' events.
- - tdl.noise: Fixed crash when given a negative seed. As well as cases
- where an instance could lose its seed being pickled.
-
-3.1.0 - 2017-05-28
-------------------
-Added
- - You can now pass tdl Console instances as parameters to libtcod-cffi
- functions expecting a tcod Console.
-Changed
- - Dependencies updated: `libtcod-cffi>=2.5.0,<3`
- - The `Console.tcod_console` attribute is being renamed to
- `Console.console_c`.
-Deprecated
- - The tdl.noise and tdl.map modules will be deprecated in the future.
-Fixed
- - Resolved crash-on-exit issues for Windows platforms.
-
-3.0.2 - 2017-04-13
-------------------
-Changed
- - Dependencies updated: `libtcod-cffi>=2.4.3,<3`
- - You can now create Console instances before a call to `tdl.init`.
-Removed
- - Dropped support for Python 3.3
-Fixed
- - Resolved issues with MacOS builds.
- - 'OpenGL' and 'GLSL' renderers work again.
-
-3.0.1 - 2017-03-22
-------------------
-Changed
- - `KeyEvent`'s with `text` now have all their modifier keys set to False.
-Fixed
- - Undefined behavior in text events caused crashes on 32-bit builds.
-
-3.0.0 - 2017-03-21
-------------------
-Added
- - `KeyEvent` supports libtcod text and meta keys.
-Changed
- - `KeyEvent` parameters have been moved.
- - This version requires `libtcod-cffi>=2.3.0`.
-Deprecated
- - `KeyEvent` camel capped attribute names are deprecated.
-Fixed
- - Crashes with key-codes undefined by libtcod.
- - `tdl.map` typedef issues with libtcod-cffi.
-
-
-2.0.1 - 2017-02-22
-------------------
-Fixed
- - `tdl.init` renderer was defaulted to OpenGL which is not supported in the
- current version of libtcod.
-
-2.0.0 - 2017-02-15
-------------------
-Changed
- - Dependencies updated, tdl now requires libtcod-cffi 2.x.x
- - Some event behaviors have changed with SDL2, event keys might be different
- than what you expect.
-Removed
- - Key repeat functions were removed from SDL2.
- `set_key_repeat` is now stubbed, and does nothing.
-
-1.6.0 - 2016-11-18
-------------------
-- Console.blit methods can now take fg_alpha and bg_alpha parameters.
-
-1.5.3 - 2016-06-04
-------------------
-- set_font no longer crashes when loading a file without the implied font
- size in its name
-
-1.5.2 - 2016-03-11
-------------------
-- Fixed non-square Map instances
-
-1.5.1 - 2015-12-20
-------------------
-- Fixed errors with Unicode and non-Unicode literals on Python 2
-- Fixed attribute error in compute_fov
-
-1.5.0 - 2015-07-13
-------------------
-- python-tdl distributions are now universal builds
-- New Map class
-- map.bresenham now returns a list
-- This release will require libtcod-cffi v0.2.3 or later
-
-1.4.0 - 2015-06-22
-------------------
-- The DLL's have been moved into another library which you can find at
- https://github.com/HexDecimal/libtcod-cffi
- You can use this library to have some raw access to libtcod if you want.
- Plus it can be used alongside TDL.
-- The libtcod console objects in Console instances have been made public.
-- Added tdl.event.wait function. This function can called with a timeout and
- can automatically call tdl.flush.
-
-1.3.1 - 2015-06-19
-------------------
-- Fixed pathfinding regressions.
-
-1.3.0 - 2015-06-19
-------------------
-- Updated backend to use python-cffi instead of ctypes. This gives decent
- boost to speed in CPython and a drastic to boost in speed in PyPy.
-
-1.2.0 - 2015-06-06
-------------------
-- The set_colors method now changes the default colors used by the draw_*
- methods. You can use Python's Ellipsis to explicitly select default colors
- this way.
-- Functions and Methods renamed to match Python's style-guide PEP 8, the old
- function names still exist and are depreciated.
-- The fgcolor and bgcolor parameters have been shortened to fg and bg.
-
-1.1.7 - 2015-03-19
-------------------
-- Noise generator now seeds properly.
-- The OS event queue will now be handled during a call to tdl.flush. This
- prevents a common newbie programmer hang where events are handled
- infrequently during long animations, simulations, or early development.
-- Fixed a major bug that would cause a crash in later versions of Python 3
-
-1.1.6 - 2014-06-27
-------------------
-- Fixed a race condition when importing on some platforms.
-- Fixed a type issue with quickFOV on Linux.
-- Added a bresenham function to the tdl.map module.
-
-1.1.5 - 2013-11-10
-------------------
-- A for loop can iterate over all coordinates of a Console.
-- drawStr can be configured to scroll or raise an error.
-- You can now configure or disable key repeating with tdl.event.setKeyRepeat
-- Typewriter class removed, use a Window instance for the same functionality.
-- setColors method fixed.
-
-1.1.4 - 2013-03-06
-------------------
-- Merged the Typewriter and MetaConsole classes,
- You now have a virtual cursor with Console and Window objects.
-- Fixed the clear method on the Window class.
-- Fixed screenshot function.
-- Fixed some drawing operations with unchanging backgrounds.
-- Instances of Console and Noise can be pickled and copied.
-- Added KeyEvent.keychar
-- Fixed event.keyWait, and now converts window closed events into Alt+F4.
-
-1.1.3 - 2012-12-17
-------------------
-- Some of the setFont parameters were incorrectly labeled and documented.
-- setFont can auto-detect tilesets if the font sizes are in the filenames.
-- Added some X11 unicode tilesets, including Unifont.
-
-1.1.2 - 2012-12-13
-------------------
-- Window title now defaults to the running scripts filename.
-- Fixed incorrect deltaTime for App.update
-- App will no longer call tdl.flush on its own, you'll need to call this
- yourself.
-- tdl.noise module added.
-- clear method now defaults to black on black.
-
-1.1.1 - 2012-12-05
-------------------
-- Map submodule added with AStar class and quickFOV function.
-- New Typewriter class.
-- Most console functions can use Python-style negative indexes now.
-- New App.runOnce method.
-- Rectangle geometry is less strict.
-
-1.1.0 - 2012-10-04
-------------------
-- KeyEvent.keyname is now KeyEvent.key
-- MouseButtonEvent.button now behaves like KeyEvent.keyname does.
-- event.App class added.
-- Drawing methods no longer have a default for the character parameter.
-- KeyEvent.ctrl is now KeyEvent.control
-
-1.0.8 - 2010-04-07
-------------------
-- No longer works in Python 2.5 but now works in 3.x and has been partly
- tested.
-- Many bug fixes.
-
-1.0.5 - 2010-04-06
-------------------
-- Got rid of setuptools dependency, this will make it much more compatible
- with Python 3.x
-- Fixed a typo with the MacOS library import.
-
-1.0.4 - 2010-04-06
-------------------
-- All constant colors (C_*) have been removed, they may be put back in later.
-- Made some type assertion failures show the value they received to help in
- general debugging. Still working on it.
-- Added MacOS and 64-bit Linux support.
-
-1.0.0 - 2009-01-31
-------------------
-- First public release.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 58da55b3..c4f7d5ea 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,10 +1,12 @@
## Code style
-New and refactored Python code should follow the
-[PEP 8](https://www.python.org/dev/peps/pep-0008/) guidelines.
+Code styles are enforced using black and linters.
+These are best enabled with a pre-commit which you can setup with:
-It's recommended to use an editor supporting
-[EditorConfig](https://editorconfig.org/).
+```sh
+pip install pre-commit
+pre-commit install
+```
## Building python-tcod
diff --git a/LICENSE.txt b/LICENSE.txt
index c00f298e..d91bf759 100755
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
BSD 2-Clause License
-Copyright (c) 2009-2021, Kyle Benesch and the python-tcod contributors.
+Copyright (c) 2009-2023, Kyle Benesch and the python-tcod contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/MANIFEST.in b/MANIFEST.in
index 54b46459..9fc63cae 100755
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,8 +1,23 @@
-include *.py *.cfg *.txt *.rst *.toml
+global-exclude .*
+prune .*
+
+include *.py
+include *.txt
+include *.rst
+include *.toml
+include *.md
recursive-include tcod *.py *.c *.h
+prune libtcod
recursive-include libtcod/src *.glsl* *.c *.h
-include libtcod/*.txt libtcod/*.md
+include libtcod/*.txt
+include libtcod/*.md
+
+prune docs
+prune examples
+prune fonts
+prune scripts
+prune tests
-exclude tcod/*/SDL2.dll
+global-exclude *.dll
diff --git a/README.rst b/README.rst
index 3c38bea3..f84370ee 100755
--- a/README.rst
+++ b/README.rst
@@ -6,7 +6,7 @@
========
|VersionsBadge| |ImplementationBadge| |LicenseBadge|
-|PyPI| |RTD| |Codecov| |Pyup|
+|PyPI| |RTD| |Codecov| |Pyup| |CommitsSinceLastRelease|
=======
About
@@ -48,14 +48,14 @@ For the most part it's just::
==============
* Python 3.7+
* Windows, Linux, or MacOS X 10.9+.
-* On Linux, requires libsdl2 (2.0.5+).
+* On Linux, requires libsdl2 (2.0.10+).
===========
Changelog
===========
You can find the most recent changelog
-`here `_.
+`here `_.
=========
License
@@ -100,3 +100,6 @@ python-tcod is distributed under the `Simplified 2-clause FreeBSD license
.. |Pyup| image:: https://pyup.io/repos/github/libtcod/python-tcod/shield.svg
:target: https://pyup.io/repos/github/libtcod/python-tcod/
:alt: Updates
+
+.. |CommitsSinceLastRelease| image:: https://img.shields.io/github/commits-since/libtcod/python-tcod/latest
+ :target: https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md
diff --git a/build_libtcod.py b/build_libtcod.py
old mode 100644
new mode 100755
index a9de366b..3f5d2772
--- a/build_libtcod.py
+++ b/build_libtcod.py
@@ -1,36 +1,28 @@
#!/usr/bin/env python3
+"""Parse and compile libtcod and SDL sources for CFFI."""
+from __future__ import annotations
+import contextlib
import glob
import os
import platform
import re
-import shutil
-import subprocess
import sys
-import zipfile
-from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union
+from pathlib import Path
+from typing import Any, Iterable, Iterator
-try:
- from urllib import urlretrieve # type: ignore
-except ImportError:
- from urllib.request import urlretrieve
+from cffi import FFI
-from cffi import FFI # type: ignore
+sys.path.append(str(Path(__file__).parent)) # Allow importing local modules.
-sys.path.append(os.path.dirname(__file__))
+import build_sdl # noqa: E402
-import parse_sdl2 # noqa: E402
-
-# The SDL2 version to parse and export symbols from.
-SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.5")
-
-# The SDL2 version to include in binary distributions.
-SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.14")
+Py_LIMITED_API = 0x03060000
HEADER_PARSE_PATHS = ("tcod/", "libtcod/src/libtcod/")
HEADER_PARSE_EXCLUDES = ("gl2_ext_.h", "renderer_gl_internal.h", "event.h")
-BITSIZE, LINKAGE = platform.architecture()
+BIT_SIZE, LINKAGE = platform.architecture()
# Regular expressions to parse the headers for cffi.
RE_COMMENT = re.compile(r"\s*/\*.*?\*/|\s*//*?$", re.DOTALL | re.MULTILINE)
@@ -38,10 +30,11 @@
RE_PREPROCESSOR = re.compile(r"(?!#define\s+\w+\s+\d+$)#.*?(? None:
- self.path = path = os.path.normpath(path)
- directory = os.path.dirname(path)
+ def __init__(self, path: Path) -> None:
+ """Initialize and organize a header file."""
+ self.path = path = path.resolve(True)
+ directory = path.parent
depends = set()
- with open(self.path, "r", encoding="utf-8") as f:
- header = f.read()
+ header = self.path.read_text(encoding="utf-8")
header = RE_COMMENT.sub("", header)
header = RE_CPLUSPLUS.sub("", header)
for dependency in RE_INCLUDE.findall(header):
- depends.add(os.path.normpath(os.path.join(directory, dependency)))
+ depends.add((directory / str(dependency)).resolve(True))
header = RE_PREPROCESSOR.sub("", header)
header = RE_TAGS.sub("", header)
header = RE_VAFUNC.sub("", header)
@@ -72,40 +65,40 @@ def __init__(self, path: str) -> None:
self.depends = frozenset(depends)
self.all_headers[self.path] = self
- def parsed_depends(self) -> Iterator["ParsedHeader"]:
+ def parsed_depends(self) -> Iterator[ParsedHeader]:
"""Return dependencies excluding ones that were not loaded."""
for dep in self.depends:
- try:
+ with contextlib.suppress(KeyError):
yield self.all_headers[dep]
- except KeyError:
- pass
def __str__(self) -> str:
- return "Parsed harder at '%s'\n Depends on: %s" % (
+ """Return useful info on this object."""
+ return "Parsed harder at '{}'\n Depends on: {}".format(
self.path,
- "\n\t".join(self.depends),
+ "\n\t".join(str(d) for d in self.depends),
)
def __repr__(self) -> str:
- return "ParsedHeader(%s)" % (self.path,)
+ """Return the representation of this object."""
+ return f"ParsedHeader({self.path!r})"
def walk_includes(directory: str) -> Iterator[ParsedHeader]:
"""Parse all the include files in a directory and subdirectories."""
- for path, dirs, files in os.walk(directory):
+ for path, _dirs, files in os.walk(directory):
for file in files:
if file in HEADER_PARSE_EXCLUDES:
continue
if file.endswith(".h"):
- yield ParsedHeader(os.path.join(path, file))
+ yield ParsedHeader(Path(path, file).resolve(True))
def resolve_dependencies(
includes: Iterable[ParsedHeader],
-) -> List[ParsedHeader]:
+) -> list[ParsedHeader]:
"""Sort headers by their correct include order."""
unresolved = set(includes)
- resolved = set() # type: Set[ParsedHeader]
+ resolved: set[ParsedHeader] = set()
result = []
while unresolved:
for item in unresolved:
@@ -113,90 +106,57 @@ def resolve_dependencies(
resolved.add(item)
result.append(item)
if not unresolved & resolved:
- raise RuntimeError(
- "Could not resolve header load order.\n"
- "Possible cyclic dependency with the unresolved headers:\n%s" % (unresolved,)
+ msg = (
+ "Could not resolve header load order."
+ "\nPossible cyclic dependency with the unresolved headers:"
+ f"\n{unresolved}"
)
+ raise RuntimeError(msg)
unresolved -= resolved
return result
-def parse_includes() -> List[ParsedHeader]:
+def parse_includes() -> list[ParsedHeader]:
"""Collect all parsed header files and return them.
- Reads HEADER_PARSE_PATHS and HEADER_PARSE_EXCLUDES."""
- includes = [] # type: List[ParsedHeader]
+ Reads HEADER_PARSE_PATHS and HEADER_PARSE_EXCLUDES.
+ """
+ includes: list[ParsedHeader] = []
for dirpath in HEADER_PARSE_PATHS:
includes.extend(walk_includes(dirpath))
return resolve_dependencies(includes)
def walk_sources(directory: str) -> Iterator[str]:
- for path, dirs, files in os.walk(directory):
+ """Iterate over the C sources of a directory recursively."""
+ for path, _dirs, files in os.walk(directory):
for source in files:
if source.endswith(".c"):
- yield os.path.join(path, source)
-
-
-def get_sdl2_file(version: str) -> str:
- if sys.platform == "win32":
- sdl2_file = "SDL2-devel-%s-VC.zip" % (version,)
- else:
- assert sys.platform == "darwin"
- sdl2_file = "SDL2-%s.dmg" % (version,)
- sdl2_local_file = os.path.join("dependencies", sdl2_file)
- sdl2_remote_file = "https://www.libsdl.org/release/%s" % sdl2_file
- if not os.path.exists(sdl2_local_file):
- print("Downloading %s" % sdl2_remote_file)
- os.makedirs("dependencies/", exist_ok=True)
- urlretrieve(sdl2_remote_file, sdl2_local_file)
- return sdl2_local_file
-
-
-def unpack_sdl2(version: str) -> str:
- sdl2_path = "dependencies/SDL2-%s" % (version,)
- if sys.platform == "darwin":
- sdl2_dir = sdl2_path
- sdl2_path += "/SDL2.framework"
- if os.path.exists(sdl2_path):
- return sdl2_path
- sdl2_arc = get_sdl2_file(version)
- print("Extracting %s" % sdl2_arc)
- if sdl2_arc.endswith(".zip"):
- with zipfile.ZipFile(sdl2_arc) as zf:
- zf.extractall("dependencies/")
- elif sys.platform == "darwin":
- assert sdl2_arc.endswith(".dmg")
- subprocess.check_call(["hdiutil", "mount", sdl2_arc])
- subprocess.check_call(["mkdir", "-p", sdl2_dir])
- subprocess.check_call(["cp", "-r", "/Volumes/SDL2/SDL2.framework", sdl2_dir])
- subprocess.check_call(["hdiutil", "unmount", "/Volumes/SDL2"])
- return sdl2_path
+ yield str(Path(path, source))
includes = parse_includes()
module_name = "tcod._libtcod"
-include_dirs = [
+include_dirs: list[str] = [
".",
"libtcod/src/vendor/",
"libtcod/src/vendor/utf8proc",
"libtcod/src/vendor/zlib/",
+ *build_sdl.include_dirs,
]
-extra_parse_args = []
-extra_compile_args = []
-extra_link_args = []
-sources = [] # type: List[str]
+extra_compile_args: list[str] = [*build_sdl.extra_compile_args]
+extra_link_args: list[str] = [*build_sdl.extra_link_args]
+sources: list[str] = []
-libraries = []
-library_dirs: List[str] = []
-define_macros = [("Py_LIMITED_API", 0x03060000)] # type: List[Tuple[str, Any]]
+libraries: list[str] = [*build_sdl.libraries]
+library_dirs: list[str] = [*build_sdl.library_dirs]
+define_macros: list[tuple[str, Any]] = [("Py_LIMITED_API", Py_LIMITED_API)]
sources += walk_sources("tcod/")
sources += walk_sources("libtcod/src/libtcod/")
sources += ["libtcod/src/vendor/stb.c"]
-sources += ["libtcod/src/vendor/glad.c"]
sources += ["libtcod/src/vendor/lodepng.c"]
sources += ["libtcod/src/vendor/utf8proc/utf8proc.c"]
sources += glob.glob("libtcod/src/vendor/zlib/*.c")
@@ -206,82 +166,19 @@ def unpack_sdl2(version: str) -> str:
define_macros.append(("TCODLIB_API", ""))
define_macros.append(("_CRT_SECURE_NO_WARNINGS", None))
-if sys.platform == "darwin":
- extra_link_args += ["-framework", "SDL2"]
-else:
- libraries += ["SDL2"]
-
-# included SDL headers are for whatever OS's don't easily come with them
-
if sys.platform in ["win32", "darwin"]:
- SDL2_PARSE_PATH = unpack_sdl2(SDL2_PARSE_VERSION)
- SDL2_BUNDLE_PATH = unpack_sdl2(SDL2_BUNDLE_VERSION)
include_dirs.append("libtcod/src/zlib/")
-if sys.platform == "win32":
- SDL2_INCLUDE = os.path.join(SDL2_PARSE_PATH, "include")
-elif sys.platform == "darwin":
- SDL2_INCLUDE = os.path.join(SDL2_PARSE_PATH, "Versions/A/Headers")
-else:
- matches = re.findall(
- r"-I(\S+)",
- subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True),
- )
- assert matches
-
- SDL2_INCLUDE = None
- for match in matches:
- if os.path.isfile(os.path.join(match, "SDL_stdinc.h")):
- SDL2_INCLUDE = match
- assert SDL2_INCLUDE
-
-if sys.platform == "win32":
- include_dirs.append(SDL2_INCLUDE)
- ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"}
- SDL2_LIB_DIR = os.path.join(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE])
- library_dirs.append(SDL2_LIB_DIR)
- SDL2_LIB_DEST = os.path.join("tcod", ARCH_MAPPING[BITSIZE])
- if not os.path.exists(SDL2_LIB_DEST):
- os.mkdir(SDL2_LIB_DEST)
- shutil.copy(os.path.join(SDL2_LIB_DIR, "SDL2.dll"), SDL2_LIB_DEST)
-
-
-def fix_header(filepath: str) -> None:
- """Removes leading whitespace from a MacOS header file.
-
- This whitespace is causing issues with directives on some platforms.
- """
- with open(filepath, "r+", encoding="utf-8") as f:
- current = f.read()
- fixed = "\n".join(line.strip() for line in current.split("\n"))
- if current == fixed:
- return
- f.seek(0)
- f.truncate()
- f.write(fixed)
-
if sys.platform == "darwin":
- HEADER_DIR = os.path.join(SDL2_PARSE_PATH, "Headers")
- fix_header(os.path.join(HEADER_DIR, "SDL_assert.h"))
- fix_header(os.path.join(HEADER_DIR, "SDL_config_macosx.h"))
- include_dirs.append(HEADER_DIR)
- extra_link_args += ["-F%s/.." % SDL2_BUNDLE_PATH]
- extra_link_args += ["-rpath", "%s/.." % SDL2_BUNDLE_PATH]
- extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"]
-
# Fix "implicit declaration of function 'close'" in zlib.
define_macros.append(("HAVE_UNISTD_H", 1))
-if sys.platform not in ["win32", "darwin"]:
- extra_parse_args += subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True).strip().split()
- extra_compile_args += extra_parse_args
- extra_link_args += subprocess.check_output(["sdl2-config", "--libs"], universal_newlines=True).strip().split()
tdl_build = os.environ.get("TDL_BUILD", "RELEASE").upper()
MSVC_CFLAGS = {"DEBUG": ["/Od"], "RELEASE": ["/GL", "/O2", "/GS-", "/wd4996"]}
-MSVC_LDFLAGS: Dict[str, List[str]] = {"DEBUG": [], "RELEASE": ["/LTCG"]}
+MSVC_LDFLAGS: dict[str, list[str]] = {"DEBUG": [], "RELEASE": ["/LTCG"]}
GCC_CFLAGS = {
"DEBUG": ["-std=c99", "-Og", "-g", "-fPIC"],
"RELEASE": [
@@ -303,13 +200,13 @@ def fix_header(filepath: str) -> None:
extra_link_args.extend(GCC_CFLAGS[tdl_build])
ffi = FFI()
-parse_sdl2.add_to_ffi(ffi, SDL2_INCLUDE)
+ffi.cdef(build_sdl.get_cdef())
for include in includes:
try:
ffi.cdef(include.header)
except Exception:
# Print the source, for debugging.
- print("Error with: %s" % include.path)
+ print(f"Error with: {include.path}")
for i, line in enumerate(include.header.split("\n"), 1):
print("%03i %s" % (i, line))
raise
@@ -346,7 +243,7 @@ def fix_header(filepath: str) -> None:
'''
-def find_sdl_attrs(prefix: str) -> Iterator[Tuple[str, Union[int, str, Any]]]:
+def find_sdl_attrs(prefix: str) -> Iterator[tuple[str, int | str | Any]]:
"""Return names and values from `tcod.lib`.
`prefix` is used to filter out which names to copy.
@@ -364,19 +261,21 @@ def find_sdl_attrs(prefix: str) -> Iterator[Tuple[str, Union[int, str, Any]]]:
yield attr[name_starts_at:], getattr(lib, attr)
-def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]:
- """Return the name/value pairs, and the final dictionary string for the
- library attributes with `prefix`.
+def parse_sdl_attrs(prefix: str, all_names: list[str] | None) -> tuple[str, str]:
+ """Return the name/value pairs, and the final dictionary string for the library attributes with `prefix`.
Append matching names to the `all_names` list.
"""
names = []
lookup = []
for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]):
- all_names.append(name)
- names.append("%s = %s" % (name, value))
- lookup.append('%s: "%s"' % (value, name))
- return "\n".join(names), "{\n %s,\n}" % (",\n ".join(lookup),)
+ if name == "KMOD_RESERVED":
+ continue
+ if all_names is not None:
+ all_names.append(name)
+ names.append(f"{name} = {value}")
+ lookup.append(f'{value}: "{name}"')
+ return "\n".join(names), "{{\n {},\n}}".format(",\n ".join(lookup))
EXCLUDE_CONSTANTS = [
@@ -388,6 +287,7 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]:
"TCOD_KEY_TEXT_SIZE",
"TCOD_NOISE_MAX_DIMENSIONS",
"TCOD_NOISE_MAX_OCTAVES",
+ "TCOD_FALLBACK_FONT_SIZE",
]
EXCLUDE_CONSTANT_PREFIXES = [
@@ -395,30 +295,29 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]:
"TCOD_HEAP_",
"TCOD_LEX_",
"TCOD_CHARMAP_",
+ "TCOD_LOG_",
]
-def update_module_all(filename: str, new_all: str) -> None:
+def update_module_all(filename: Path, new_all: str) -> None:
"""Update the __all__ of a file with the constants from new_all."""
RE_CONSTANTS_ALL = re.compile(
r"(.*# --- From constants.py ---).*(# --- End constants.py ---.*)",
re.DOTALL,
)
- with open(filename, "r", encoding="utf-8") as f:
- match = RE_CONSTANTS_ALL.match(f.read())
- assert match, "Can't determine __all__ subsection in %s!" % (filename,)
+ match = RE_CONSTANTS_ALL.match(filename.read_text(encoding="utf-8"))
+ assert match, f"Can't determine __all__ subsection in {filename}!"
header, footer = match.groups()
- with open(filename, "w", encoding="utf-8") as f:
- f.write("%s\n %s,\n %s" % (header, new_all, footer))
+ filename.write_text(f"{header}\n {new_all},\n {footer}", encoding="utf-8")
def generate_enums(prefix: str) -> Iterator[str]:
"""Generate attribute assignments suitable for a Python enum."""
- for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]):
- name = name.split("_", 1)[1]
+ for symbol, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]):
+ _, name = symbol.split("_", 1)
if name.isdigit():
name = f"N{name}"
- if name in "IOl": # Handle Flake8 warnings.
+ if name in "IOl": # Ignore ambiguous variable name warnings.
yield f"{name} = {value} # noqa: E741"
else:
yield f"{name} = {value}"
@@ -429,7 +328,7 @@ def write_library_constants() -> None:
import tcod.color
from tcod._libtcod import ffi, lib
- with open("tcod/constants.py", "w", encoding="utf-8") as f:
+ with Path("tcod/constants.py").open("w", encoding="utf-8") as f:
all_names = []
f.write(CONSTANT_MODULE_HEADER)
for name in dir(lib):
@@ -444,14 +343,14 @@ def write_library_constants() -> None:
value = getattr(lib, name)
if name[:5] == "TCOD_":
if name.isupper(): # const names
- f.write("%s = %r\n" % (name[5:], value))
+ f.write(f"{name[5:]} = {value!r}\n")
all_names.append(name[5:])
elif name.startswith("FOV"): # fov const names
- f.write("%s = %r\n" % (name, value))
+ f.write(f"{name} = {value!r}\n")
all_names.append(name)
elif name[:6] == "TCODK_": # key name
- f.write("KEY_%s = %r\n" % (name[6:], value))
- all_names.append("KEY_%s" % name[6:])
+ f.write(f"KEY_{name[6:]} = {value!r}\n")
+ all_names.append(f"KEY_{name[6:]}")
f.write("\n# --- colors ---\n")
for name in dir(lib):
@@ -463,33 +362,31 @@ def write_library_constants() -> None:
if ffi.typeof(value) != ffi.typeof("TCOD_color_t"):
continue
color = tcod.color.Color._new_from_cdata(value)
- f.write("%s = %r\n" % (name[5:], color))
- all_names.append(name[5:])
+ f.write(f"{name[5:]} = {color!r}\n")
- all_names_merged = ",\n ".join('"%s"' % name for name in all_names)
- f.write("\n__all__ = [\n %s,\n]\n" % (all_names_merged,))
- update_module_all("tcod/__init__.py", all_names_merged)
- update_module_all("tcod/libtcodpy.py", all_names_merged)
+ all_names_merged = ",\n ".join(f'"{name}"' for name in all_names)
+ f.write(f"\n__all__ = [\n {all_names_merged},\n]\n")
+ update_module_all(Path("tcod/__init__.py"), all_names_merged)
+ update_module_all(Path("tcod/libtcodpy.py"), all_names_merged)
- with open("tcod/event_constants.py", "w", encoding="utf-8") as f:
+ with Path("tcod/event_constants.py").open("w", encoding="utf-8") as f:
all_names = []
f.write(EVENT_CONSTANT_MODULE_HEADER)
f.write("\n# --- SDL scancodes ---\n")
- f.write(f"""{parse_sdl_attrs("SDL_SCANCODE", all_names)[0]}\n""")
+ f.write(f"""{parse_sdl_attrs("SDL_SCANCODE", None)[0]}\n""")
f.write("\n# --- SDL keyboard symbols ---\n")
- f.write(f"""{parse_sdl_attrs("SDLK", all_names)[0]}\n""")
+ f.write(f"""{parse_sdl_attrs("SDLK", None)[0]}\n""")
f.write("\n# --- SDL keyboard modifiers ---\n")
- f.write("%s\n_REVERSE_MOD_TABLE = %s\n" % parse_sdl_attrs("KMOD", all_names))
+ f.write("{}\n_REVERSE_MOD_TABLE = {}\n".format(*parse_sdl_attrs("KMOD", all_names)))
f.write("\n# --- SDL wheel ---\n")
- f.write("%s\n_REVERSE_WHEEL_TABLE = %s\n" % parse_sdl_attrs("SDL_MOUSEWHEEL", all_names))
- all_names_merged = ",\n ".join('"%s"' % name for name in all_names)
- f.write("\n__all__ = [\n %s,\n]\n" % (all_names_merged,))
+ f.write("{}\n_REVERSE_WHEEL_TABLE = {}\n".format(*parse_sdl_attrs("SDL_MOUSEWHEEL", all_names)))
+ all_names_merged = ",\n ".join(f'"{name}"' for name in all_names)
+ f.write(f"\n__all__ = [\n {all_names_merged},\n]\n")
- with open("tcod/event.py", "r", encoding="utf-8") as f:
- event_py = f.read()
+ event_py = Path("tcod/event.py").read_text(encoding="utf-8")
event_py = re.sub(
r"(?<=# --- SDL scancodes ---\n ).*?(?=\n # --- end ---\n)",
@@ -504,8 +401,7 @@ def write_library_constants() -> None:
flags=re.DOTALL,
)
- with open("tcod/event.py", "w", encoding="utf-8") as f:
- f.write(event_py)
+ Path("tcod/event.py").write_text(event_py, encoding="utf-8")
if __name__ == "__main__":
diff --git a/build_sdl.py b/build_sdl.py
new file mode 100755
index 00000000..ef3dd4be
--- /dev/null
+++ b/build_sdl.py
@@ -0,0 +1,311 @@
+#!/usr/bin/env python3
+"""Build script to parse SDL headers and generate CFFI bindings."""
+from __future__ import annotations
+
+import io
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import zipfile
+from pathlib import Path
+from typing import Any
+
+import pcpp # type: ignore
+import requests
+
+# ruff: noqa: S603, S607 # This script calls a lot of programs.
+
+BIT_SIZE, LINKAGE = platform.architecture()
+
+# Reject versions of SDL older than this, update the requirements in the readme if you change this.
+SDL_MIN_VERSION = (2, 0, 10)
+# The SDL2 version to parse and export symbols from.
+SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20")
+# The SDL2 version to include in binary distributions.
+SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.26.0")
+
+
+# Used to remove excessive newlines in debug outputs.
+RE_NEWLINES = re.compile(r"\n\n+")
+# Functions using va_list need to be culled.
+RE_VAFUNC = re.compile(r"^.*?\([^()]*va_list[^()]*\);$", re.MULTILINE)
+# Static inline functions need to be culled.
+RE_INLINE = re.compile(r"^static inline.*?^}$", re.MULTILINE | re.DOTALL)
+# Most SDL_PIXELFORMAT names need their values scrubbed.
+RE_PIXELFORMAT = re.compile(r"(?PSDL_PIXELFORMAT_\w+) =[^,}]*")
+# Most SDLK names need their values scrubbed.
+RE_SDLK = re.compile(r"(?PSDLK_\w+) =.*?(?=,\n|}\n)")
+# Remove compile time assertions from the cdef.
+RE_ASSERT = re.compile(r"^.*SDL_compile_time_assert.*$", re.MULTILINE)
+# Padding values need to be scrubbed.
+RE_PADDING = re.compile(r"padding\[[^;]*\];")
+
+# These structs have an unusual size when packed by SDL on 32-bit platforms.
+FLEXIBLE_STRUCTS = (
+ "SDL_AudioCVT",
+ "SDL_TouchFingerEvent",
+ "SDL_MultiGestureEvent",
+ "SDL_DollarGestureEvent",
+)
+
+# Other defined names which sometimes cause issues when parsed.
+IGNORE_DEFINES = frozenset(
+ (
+ "SDL_DEPRECATED",
+ "SDL_INLINE",
+ "SDL_FORCE_INLINE",
+ "SDL_FALLTHROUGH",
+ # Might show up in parsing and not in source.
+ "SDL_ANDROID_EXTERNAL_STORAGE_READ",
+ "SDL_ANDROID_EXTERNAL_STORAGE_WRITE",
+ "SDL_ASSEMBLY_ROUTINES",
+ "SDL_RWOPS_VITAFILE",
+ # Prevent double definition.
+ "SDL_FALSE",
+ "SDL_TRUE",
+ # Ignore floating point symbols.
+ "SDL_FLT_EPSILON",
+ # Conditional config flags which might be missing.
+ "SDL_VIDEO_RENDER_D3D12",
+ "SDL_SENSOR_WINDOWS",
+ "SDL_SENSOR_DUMMY",
+ )
+)
+
+
+def check_sdl_version() -> None:
+ """Check the local SDL version on Linux distributions."""
+ if not sys.platform.startswith("linux"):
+ return
+ needed_version = f"{SDL_MIN_VERSION[0]}.{SDL_MIN_VERSION[1]}.{SDL_MIN_VERSION[2]}"
+ try:
+ sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip()
+ except FileNotFoundError as exc:
+ msg = (
+ "libsdl2-dev or equivalent must be installed on your system and must be at least version"
+ f" {needed_version}.\nsdl2-config must be on PATH."
+ )
+ raise RuntimeError(msg) from exc
+ print(f"Found SDL {sdl_version_str}.")
+ sdl_version = tuple(int(s) for s in sdl_version_str.split("."))
+ if sdl_version < SDL_MIN_VERSION:
+ msg = f"SDL version must be at least {needed_version}, (found {sdl_version_str})"
+ raise RuntimeError(msg)
+
+
+def get_sdl2_file(version: str) -> Path:
+ """Return a path to an SDL2 archive for the current platform. The archive is downloaded if missing."""
+ if sys.platform == "win32":
+ sdl2_file = f"SDL2-devel-{version}-VC.zip"
+ else:
+ assert sys.platform == "darwin"
+ sdl2_file = f"SDL2-{version}.dmg"
+ sdl2_local_file = Path("dependencies", sdl2_file)
+ sdl2_remote_file = f"https://www.libsdl.org/release/{sdl2_file}"
+ if not sdl2_local_file.exists():
+ print(f"Downloading {sdl2_remote_file}")
+ Path("dependencies/").mkdir(parents=True, exist_ok=True)
+ with requests.get(sdl2_remote_file) as response: # noqa: S113
+ response.raise_for_status()
+ sdl2_local_file.write_bytes(response.content)
+ return sdl2_local_file
+
+
+def unpack_sdl2(version: str) -> Path:
+ """Return the path to an extracted SDL distribution. Creates it if missing."""
+ sdl2_path = Path(f"dependencies/SDL2-{version}")
+ if sys.platform == "darwin":
+ sdl2_dir = sdl2_path
+ sdl2_path /= "SDL2.framework"
+ if sdl2_path.exists():
+ return sdl2_path
+ sdl2_arc = get_sdl2_file(version)
+ print(f"Extracting {sdl2_arc}")
+ if sdl2_arc.suffix == ".zip":
+ with zipfile.ZipFile(sdl2_arc) as zf:
+ zf.extractall("dependencies/")
+ elif sys.platform == "darwin":
+ assert sdl2_arc.suffix == ".dmg"
+ subprocess.check_call(["hdiutil", "mount", sdl2_arc])
+ subprocess.check_call(["mkdir", "-p", sdl2_dir])
+ subprocess.check_call(["cp", "-r", "/Volumes/SDL2/SDL2.framework", sdl2_dir])
+ subprocess.check_call(["hdiutil", "unmount", "/Volumes/SDL2"])
+ return sdl2_path
+
+
+class SDLParser(pcpp.Preprocessor): # type: ignore
+ """A modified preprocessor to output code in a format for CFFI."""
+
+ def __init__(self) -> None:
+ """Initialise the object with empty values."""
+ super().__init__()
+ self.line_directive = None # Don't output line directives.
+ self.known_string_defines: dict[str, str] = {}
+ self.known_defines: set[str] = set()
+
+ def get_output(self) -> str:
+ """Return this objects current tokens as a string."""
+ with io.StringIO() as buffer:
+ self.write(buffer)
+ for name in self.known_defines:
+ buffer.write(f"#define {name} ...\n")
+ return buffer.getvalue()
+
+ def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curdir: str, includepath: str) -> None:
+ """Remove bad includes such as stddef.h and stdarg.h."""
+ raise pcpp.OutputDirective(pcpp.Action.IgnoreAndRemove)
+
+ def _should_track_define(self, tokens: list[Any]) -> bool:
+ if len(tokens) < 3:
+ return False
+ if tokens[0].value in IGNORE_DEFINES:
+ return False
+ if not tokens[0].value.isupper():
+ return False # Function-like name, such as SDL_snprintf.
+ if tokens[0].value.startswith("_") or tokens[0].value.endswith("_"):
+ return False # Private name.
+ if tokens[2].value.startswith("_") or tokens[2].value.endswith("_"):
+ return False # Likely calls a private function.
+ if tokens[1].type == "CPP_LPAREN":
+ return False # Function-like macro.
+ if len(tokens) >= 4 and tokens[2].type == "CPP_INTEGER" and tokens[3].type == "CPP_DOT":
+ return False # Value is a floating point number.
+ if tokens[0].value.startswith("SDL_PR") and (tokens[0].value.endswith("32") or tokens[0].value.endswith("64")):
+ return False # Data type for printing, which is not needed.
+ return bool(
+ tokens[0].value.startswith("KMOD_")
+ or tokens[0].value.startswith("SDL_")
+ or tokens[0].value.startswith("AUDIO_")
+ )
+
+ def on_directive_handle(
+ self, directive: Any, tokens: list[Any], if_passthru: bool, preceding_tokens: list[Any] # noqa: ANN401
+ ) -> Any: # noqa: ANN401
+ """Catch and store definitions."""
+ if directive.value == "define" and self._should_track_define(tokens):
+ if tokens[2].type == "CPP_STRING":
+ self.known_string_defines[tokens[0].value] = tokens[2].value
+ else:
+ self.known_defines.add(tokens[0].value)
+ return super().on_directive_handle(directive, tokens, if_passthru, preceding_tokens)
+
+
+check_sdl_version()
+
+if sys.platform in ["win32", "darwin"]:
+ SDL2_PARSE_PATH = unpack_sdl2(SDL2_PARSE_VERSION)
+ SDL2_BUNDLE_PATH = unpack_sdl2(SDL2_BUNDLE_VERSION)
+
+SDL2_INCLUDE: Path
+if sys.platform == "win32":
+ SDL2_INCLUDE = SDL2_PARSE_PATH / "include"
+elif sys.platform == "darwin":
+ SDL2_INCLUDE = SDL2_PARSE_PATH / "Versions/A/Headers"
+else: # Unix
+ matches = re.findall(
+ r"-I(\S+)",
+ subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True),
+ )
+ assert matches
+
+ for match in matches:
+ if Path(match, "SDL_stdinc.h").is_file():
+ SDL2_INCLUDE = match
+ assert SDL2_INCLUDE
+
+
+EXTRA_CDEF = """
+#define SDLK_SCANCODE_MASK ...
+
+extern "Python" {
+// SDL_AudioCallback callback.
+void _sdl_audio_callback(void* userdata, Uint8* stream, int len);
+// SDL to Python log function.
+void _sdl_log_output_function(void *userdata, int category, SDL_LogPriority priority, const char *message);
+// Generic event watcher callback.
+int _sdl_event_watcher(void* userdata, SDL_Event* event);
+}
+"""
+
+
+def get_cdef() -> str:
+ """Return the parsed code of SDL for CFFI."""
+ parser = SDLParser()
+ parser.add_path(SDL2_INCLUDE)
+ parser.parse(
+ """
+ // Remove extern keyword.
+ #define extern
+ // Ignore some SDL assert statements.
+ #define DOXYGEN_SHOULD_IGNORE_THIS
+
+ #define _SIZE_T_DEFINED_
+ typedef int... size_t;
+
+ // Skip these headers.
+ #define SDL_atomic_h_
+ #define SDL_thread_h_
+
+ #include
+ """
+ )
+ sdl2_cdef = parser.get_output()
+ sdl2_cdef = RE_VAFUNC.sub("", sdl2_cdef)
+ sdl2_cdef = RE_INLINE.sub("", sdl2_cdef)
+ sdl2_cdef = RE_PIXELFORMAT.sub(r"\g = ...", sdl2_cdef)
+ sdl2_cdef = RE_SDLK.sub(r"\g = ...", sdl2_cdef)
+ sdl2_cdef = RE_NEWLINES.sub("\n", sdl2_cdef)
+ sdl2_cdef = RE_ASSERT.sub("", sdl2_cdef)
+ sdl2_cdef = RE_PADDING.sub("padding[...];", sdl2_cdef)
+ sdl2_cdef = (
+ sdl2_cdef.replace("int SDL_main(int argc, char *argv[]);", "")
+ .replace("typedef unsigned int uintptr_t;", "typedef int... uintptr_t;")
+ .replace("typedef unsigned int size_t;", "typedef int... size_t;")
+ )
+ for name in FLEXIBLE_STRUCTS:
+ sdl2_cdef = sdl2_cdef.replace(f"}} {name};", f"...;}} {name};")
+ return sdl2_cdef + EXTRA_CDEF
+
+
+include_dirs: list[str] = []
+extra_compile_args: list[str] = []
+extra_link_args: list[str] = []
+
+libraries: list[str] = []
+library_dirs: list[str] = []
+
+
+if sys.platform == "darwin":
+ extra_link_args += ["-framework", "SDL2"]
+else:
+ libraries += ["SDL2"]
+
+# Bundle the Windows SDL2 DLL.
+if sys.platform == "win32":
+ include_dirs.append(str(SDL2_INCLUDE))
+ ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"}
+ SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BIT_SIZE])
+ library_dirs.append(str(SDL2_LIB_DIR))
+ SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BIT_SIZE])
+ SDL2_LIB_DEST.mkdir(exist_ok=True)
+ SDL2_LIB_DEST_FILE = SDL2_LIB_DEST / "SDL2.dll"
+ SDL2_LIB_FILE = SDL2_LIB_DIR / "SDL2.dll"
+ if not SDL2_LIB_DEST_FILE.exists() or SDL2_LIB_FILE.read_bytes() != SDL2_LIB_DEST_FILE.read_bytes():
+ shutil.copy(SDL2_LIB_FILE, SDL2_LIB_DEST_FILE)
+
+# Link to the SDL2 framework on MacOS.
+# Delocate will bundle the binaries in a later step.
+if sys.platform == "darwin":
+ HEADER_DIR = Path(SDL2_PARSE_PATH, "Headers")
+ include_dirs.append(HEADER_DIR)
+ extra_link_args += [f"-F{SDL2_BUNDLE_PATH}/.."]
+ extra_link_args += ["-rpath", f"{SDL2_BUNDLE_PATH}/.."]
+ extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"]
+
+# Use sdl2-config to link to SDL2 on Linux.
+if sys.platform not in ["win32", "darwin"]:
+ extra_compile_args += subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True).strip().split()
+ extra_link_args += subprocess.check_output(["sdl2-config", "--libs"], universal_newlines=True).strip().split()
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 1b4f389c..2f021a4b 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -3,4 +3,4 @@
===========
You can find the most recent changelog
-`here `_.
+`here `_.
diff --git a/docs/conf.py b/docs/conf.py
index 45d78195..de4337ee 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,7 +60,7 @@
# General information about the project.
project = "python-tcod"
-copyright = "2009-2021, Kyle Benesch"
+copyright = "2009-2023, Kyle Benesch"
author = "Kyle Benesch"
# The version info for the project you're documenting, acts as replacement for
@@ -85,7 +85,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
diff --git a/docs/faq.rst b/docs/faq.rst
index 109ac981..ed7987fe 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -6,7 +6,7 @@ How do you set a frames-per-second while using contexts?
You'll need to use an external tool to manage the framerate.
This can either be your own custom tool or you can copy the Clock class from the
-`framerate.py `_
+`framerate.py `_
example.
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 5380de58..3cfb22a7 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -10,6 +10,17 @@ Glossary
These have been deprecated since version `8.5`.
+ tcod
+ `tcod` on its own is shorthand for both :term:`libtcod` and all of its bindings including :term:`python-tcod`.
+
+ It originated as an acronym for the game the library was first created for:
+ `The Chronicles Of Doryen `_
+
+ libtcod
+ This is the original C library which contains the implementations and algorithms used by C programs.
+
+ :term:`python-tcod` includes a statically linked version of this library.
+
libtcod-cffi
This is the `cffi` implementation of libtcodpy, the original was
made using `ctypes` which was more difficult to maintain.
@@ -19,8 +30,10 @@ Glossary
implemented.
python-tcod
- `python-tcod` is a superset of the :term:`libtcodpy` API. The major
- additions include class functionality in returned objects, no manual
+ `python-tcod` is the main Python port of :term:`libtcod`.
+
+ Originally a superset of the :term:`libtcodpy` API. The major
+ additions included class functionality in returned objects, no manual
memory management, pickle-able objects, and `numpy` array attributes
in most objects.
@@ -38,14 +51,13 @@ Glossary
This left it impractical for any real use as a roguelike library.
Currently no new features are planned for `tdl`, instead new features
- are added to `libtcod` itself and then ported to :term:`python-tcod`.
+ are added to :term:`libtcod` itself and then ported to :term:`python-tcod`.
:term:`python-tdl` and :term:`libtcodpy` are included in installations
of `python-tcod`.
libtcodpy
- `libtcodpy` is more or less a direct port of `libtcod`'s C API to
- Python.
+ :term:`libtcodpy` is more or less a direct port of :term:`libtcod`'s C API to Python.
This caused a handful of issues including instances needing to be
freed manually or else a memory leak would occur, and many functions
performing badly in Python due to the need to call them frequently.
diff --git a/docs/index.rst b/docs/index.rst
index 1ae35b97..c9760f9f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -36,8 +36,14 @@ Contents:
tcod/noise
tcod/path
tcod/random
+ tcod/render
tcod/tileset
libtcodpy
+ sdl/audio
+ sdl/joystick
+ sdl/render
+ sdl/mouse
+ sdl/video
Indices and tables
==================
@@ -45,4 +51,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
-
diff --git a/docs/installation.rst b/docs/installation.rst
index 109601cc..4b8e04ce 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -50,9 +50,9 @@ You can then verify that ``tcod`` is importable from the Python interpreter::
>python
- >>> import tcod
+ >>> import tcod.context
-If ``import tcod`` doesn't throw an ``ImportError`` then ``tcod`` has been installed correctly to your system libraries.
+If ``import tcod.context`` doesn't throw an ``ImportError`` then ``tcod`` has been installed correctly to your system libraries.
Some IDE's such as PyCharm will create a virtual environment which will ignore your system libraries and require tcod to be installed again in that new environment.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 6cdabebb..f25589d5 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,2 +1,2 @@
-sphinx>=4.2
-sphinx_rtd_theme
+sphinx>=5.0.2,!=5.1.0,<6.1 # https://github.com/readthedocs/sphinx_rtd_theme/issues/1463
+sphinx_rtd_theme>=1.2.1
diff --git a/docs/sdl/audio.rst b/docs/sdl/audio.rst
new file mode 100644
index 00000000..702bd470
--- /dev/null
+++ b/docs/sdl/audio.rst
@@ -0,0 +1,5 @@
+tcod.sdl.audio - SDL Audio
+==========================
+
+.. automodule:: tcod.sdl.audio
+ :members:
diff --git a/docs/sdl/joystick.rst b/docs/sdl/joystick.rst
new file mode 100644
index 00000000..e1b22ca2
--- /dev/null
+++ b/docs/sdl/joystick.rst
@@ -0,0 +1,11 @@
+tcod.sdl.joystick - SDL Joystick Support
+========================================
+
+.. automodule:: tcod.sdl.joystick
+ :members:
+ :exclude-members:
+ Power
+
+.. autoclass:: tcod.sdl.joystick.Power
+ :members:
+ :member-order: bysource
diff --git a/docs/sdl/mouse.rst b/docs/sdl/mouse.rst
new file mode 100644
index 00000000..932f4d9f
--- /dev/null
+++ b/docs/sdl/mouse.rst
@@ -0,0 +1,5 @@
+tcod.sdl.mouse - SDL Mouse Functions
+====================================
+
+.. automodule:: tcod.sdl.mouse
+ :members:
diff --git a/docs/sdl/render.rst b/docs/sdl/render.rst
new file mode 100644
index 00000000..0dc462cd
--- /dev/null
+++ b/docs/sdl/render.rst
@@ -0,0 +1,5 @@
+tcod.sdl.render - SDL Rendering
+===============================
+
+.. automodule:: tcod.sdl.render
+ :members:
diff --git a/docs/sdl/video.rst b/docs/sdl/video.rst
new file mode 100644
index 00000000..56d43408
--- /dev/null
+++ b/docs/sdl/video.rst
@@ -0,0 +1,5 @@
+tcod.sdl.video - SDL Window and Display API
+===========================================
+
+.. automodule:: tcod.sdl.video
+ :members:
diff --git a/docs/tcod/charmap-reference.rst b/docs/tcod/charmap-reference.rst
index 0fa8150a..3ad42b16 100644
--- a/docs/tcod/charmap-reference.rst
+++ b/docs/tcod/charmap-reference.rst
@@ -173,7 +173,7 @@ https://en.wikipedia.org/wiki/Code_page_437
124 0x7C \'\|\' VERTICAL LINE
125 0x7D \'}\' RIGHT CURLY BRACKET
126 0x7E \'~\' TILDE
- 127 0x7F \'\\x7f\'
+ 127 0x2302 \'⌂\' HOUSE
128 0xC7 \'Ç\' LATIN CAPITAL LETTER C WITH CEDILLA
129 0xFC \'ü\' LATIN SMALL LETTER U WITH DIAERESIS
130 0xE9 \'é\' LATIN SMALL LETTER E WITH ACUTE
diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst
index ddca121c..b277f864 100644
--- a/docs/tcod/getting-started.rst
+++ b/docs/tcod/getting-started.rst
@@ -23,7 +23,10 @@ Example::
#!/usr/bin/env python3
# Make sure 'dejavu10x10_gs_tc.png' is in the same directory as this script.
- import tcod
+ import tcod.console
+ import tcod.context
+ import tcod.event
+ import tcod.tileset
WIDTH, HEIGHT = 80, 60 # Console width and height in tiles.
@@ -35,7 +38,7 @@ Example::
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD,
)
# Create the main console.
- console = tcod.Console(WIDTH, HEIGHT, order="F")
+ console = tcod.console.Console(WIDTH, HEIGHT, order="F")
# Create a window based on this console and tileset.
with tcod.context.new( # New window for a console of size columns×rows.
columns=console.width, rows=console.height, tileset=tileset,
@@ -45,6 +48,8 @@ Example::
console.print(x=0, y=0, string="Hello World!")
context.present(console) # Show the console.
+ # This event loop will wait until at least one event is processed before exiting.
+ # For a non-blocking event loop replace `tcod.event.wait` with `tcod.event.get`.
for event in tcod.event.wait():
context.convert_event(event) # Sets tile coordinates for mouse events.
print(event) # Print event names and attributes.
@@ -85,7 +90,8 @@ clearing the console every frame and replacing it only on resizing the window.
Example::
#!/usr/bin/env python3
- import tcod
+ import tcod.context
+ import tcod.event
WIDTH, HEIGHT = 720, 480 # Window pixel resolution (when not maximized.)
FLAGS = tcod.context.SDL_WINDOW_RESIZABLE | tcod.context.SDL_WINDOW_MAXIMIZED
diff --git a/docs/tcod/render.rst b/docs/tcod/render.rst
new file mode 100644
index 00000000..605d52f2
--- /dev/null
+++ b/docs/tcod/render.rst
@@ -0,0 +1,5 @@
+tcod.render - Console Rendering Extension
+=========================================
+
+.. automodule:: tcod.render
+ :members:
diff --git a/examples/.isort.cfg b/examples/.isort.cfg
deleted file mode 100644
index 9e0911c7..00000000
--- a/examples/.isort.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[isort]
-profile= black
-py_version = 36
-skip_gitignore = true
-line_length = 120
diff --git a/examples/DejaVuSerif.ttf b/examples/DejaVuSerif.ttf
new file mode 100644
index 00000000..0b803d20
Binary files /dev/null and b/examples/DejaVuSerif.ttf differ
diff --git a/examples/README.md b/examples/README.md
index 48fc5209..0594efe1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,7 +1,7 @@
This directory contains a few example scripts for using python-tcod.
`samples_tcod.py` is the mail example which uses most of the newer API. This
-can be compared to `samokes_libtcodpy.py` which mostly uses deprecated
+can be compared to `samples_libtcodpy.py` which mostly uses deprecated
functions from the old API.
Examples in the `distribution/` folder show how to distribute projects made
diff --git a/examples/cavegen.py b/examples/cavegen.py
old mode 100644
new mode 100755
diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py
index b396d536..e1c6090d 100755
--- a/examples/distribution/PyInstaller/main.py
+++ b/examples/distribution/PyInstaller/main.py
@@ -3,20 +3,25 @@
# copyright and related or neighboring rights for the "hello world" PyInstaller
# example script. This work is published from: United States.
# https://creativecommons.org/publicdomain/zero/1.0/
-import os.path
+"""PyInstaller main script example."""
import sys
+from pathlib import Path
-import tcod
+import tcod.console
+import tcod.context
+import tcod.event
+import tcod.tileset
WIDTH, HEIGHT = 80, 60
# The base directory, this is sys._MEIPASS when in one-file mode.
-BASE_DIR = getattr(sys, "_MEIPASS", ".")
+BASE_DIR = Path(getattr(sys, "_MEIPASS", "."))
-FONT_PATH = os.path.join(BASE_DIR, "data/terminal8x8_gs_ro.png")
+FONT_PATH = BASE_DIR / "data/terminal8x8_gs_ro.png"
def main() -> None:
+ """Entry point function."""
tileset = tcod.tileset.load_tilesheet(FONT_PATH, 16, 16, tcod.tileset.CHARMAP_CP437)
with tcod.context.new(columns=WIDTH, rows=HEIGHT, tileset=tileset) as context:
while True:
diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py
index 6323407a..8f608af7 100755
--- a/examples/distribution/cx_Freeze/main.py
+++ b/examples/distribution/cx_Freeze/main.py
@@ -1,11 +1,16 @@
#!/usr/bin/env python3
-import tcod
+"""cx_Freeze main script example."""
+import tcod.console
+import tcod.context
+import tcod.event
+import tcod.tileset
WIDTH, HEIGHT = 80, 60
console = None
def main() -> None:
+ """Entry point function."""
tileset = tcod.tileset.load_tilesheet("data/terminal8x8_gs_ro.png", 16, 16, tcod.tileset.CHARMAP_CP437)
with tcod.context.new(columns=WIDTH, rows=HEIGHT, tileset=tileset) as context:
while True:
diff --git a/examples/eventget.py b/examples/eventget.py
index 55d6a58b..774ad3b1 100755
--- a/examples/eventget.py
+++ b/examples/eventget.py
@@ -3,20 +3,24 @@
# copyright and related or neighboring rights for this example. This work is
# published from: United States.
# https://creativecommons.org/publicdomain/zero/1.0/
-"""An demonstration of event handling using the tcod.event module.
-"""
-from typing import List
+"""An demonstration of event handling using the tcod.event module."""
+from typing import List, Set
-import tcod
+import tcod.context
+import tcod.event
+import tcod.sdl.joystick
+import tcod.sdl.sys
WIDTH, HEIGHT = 720, 480
def main() -> None:
- """Example program for tcod.event"""
-
+ """Example program for tcod.event."""
event_log: List[str] = []
motion_desc = ""
+ tcod.sdl.joystick.init()
+ controllers: Set[tcod.sdl.joystick.GameController] = set()
+ joysticks: Set[tcod.sdl.joystick.Joystick] = set()
with tcod.context.new(width=WIDTH, height=HEIGHT) as context:
console = context.new_console()
@@ -39,9 +43,19 @@ def main() -> None:
raise SystemExit()
if isinstance(event, tcod.event.WindowResized) and event.type == "WINDOWRESIZED":
console = context.new_console()
+ if isinstance(event, tcod.event.ControllerDevice):
+ if event.type == "CONTROLLERDEVICEADDED":
+ controllers.add(event.controller)
+ elif event.type == "CONTROLLERDEVICEREMOVED":
+ controllers.remove(event.controller)
+ if isinstance(event, tcod.event.JoystickDevice):
+ if event.type == "JOYDEVICEADDED":
+ joysticks.add(event.joystick)
+ elif event.type == "JOYDEVICEREMOVED":
+ joysticks.remove(event.joystick)
if isinstance(event, tcod.event.MouseMotion):
motion_desc = str(event)
- else:
+ else: # Log all events other than MouseMotion.
event_log.append(str(event))
diff --git a/examples/framerate.py b/examples/framerate.py
old mode 100644
new mode 100755
index 39ef6ee6..3a9e9f81
--- a/examples/framerate.py
+++ b/examples/framerate.py
@@ -3,8 +3,7 @@
# copyright and related or neighboring rights for this example. This work is
# published from: United States.
# https://creativecommons.org/publicdomain/zero/1.0/
-"""A system to control time since the original libtcod tools are deprecated.
-"""
+"""A system to control time since the original libtcod tools are deprecated."""
import statistics
import time
from collections import deque
@@ -23,6 +22,7 @@ class Clock:
"""
def __init__(self) -> None:
+ """Initialize this object with empty data."""
self.last_time = time.perf_counter() # Last time this was synced.
self.time_samples: Deque[float] = deque() # Delta time samples.
self.max_samples = 64 # Number of fps samples to log. Can be changed.
@@ -138,7 +138,7 @@ def main() -> None:
context.convert_event(event) # Set tile coordinates for event.
if isinstance(event, tcod.event.Quit):
raise SystemExit()
- elif isinstance(event, tcod.event.MouseWheel):
+ if isinstance(event, tcod.event.MouseWheel):
desired_fps = max(1, desired_fps + event.y)
diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py
index 6b5b05a7..ab68eb91 100755
--- a/examples/samples_libtcodpy.py
+++ b/examples/samples_libtcodpy.py
@@ -4,6 +4,7 @@
# This code demonstrates various usages of libtcod modules
# It's in the public domain.
#
+# ruff: noqa
from __future__ import division
import math
@@ -930,6 +931,8 @@ def render_path(first, key, mouse):
# if true, there is always a wall on north & west side of a room
bsp_room_walls = True
bsp_map = None
+
+
# draw a vertical line
def vline(m, x, y1, y2):
if y1 > y2:
@@ -1266,7 +1269,7 @@ def render_name(first, key, mouse):
if ng_nbsets == 0:
# parse all *.cfg files in data/namegen
for file in os.listdir(get_data("namegen")):
- if file.find(b".cfg") > 0:
+ if file.find(".cfg") > 0:
libtcod.namegen_parse(get_data(os.path.join("namegen", file)))
# get the sets list
ng_sets = libtcod.namegen_get_sets()
@@ -1422,8 +1425,8 @@ def render_py(first, key, mouse):
) + libtcod.noise_get_fbm(noise2d, [1 - u / float(RES_U), tex_v], 32.0)
if use_numpy:
# squared distance from center, clipped to sensible minimum and maximum values
- sqr_dist = xc ** 2 + yc ** 2
- sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V ** 2)
+ sqr_dist = xc**2 + yc**2
+ sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V**2)
# one coordinate into the texture, represents depth in the tunnel
v = TEX_STRETCH * float(RES_V) / sqr_dist + frac_t
@@ -1444,8 +1447,8 @@ def render_py(first, key, mouse):
for y in range(-HALF_H, HALF_H):
for x in range(-HALF_W, HALF_W):
# squared distance from center, clipped to sensible minimum and maximum values
- sqr_dist = x ** 2 + y ** 2
- sqr_dist = min(max(sqr_dist, 1.0 / RES_V), RES_V ** 2)
+ sqr_dist = x**2 + y**2
+ sqr_dist = min(max(sqr_dist, 1.0 / RES_V), RES_V**2)
# one coordinate into the texture, represents depth in the tunnel
v = TEX_STRETCH * float(RES_V) / sqr_dist + frac_t
@@ -1595,7 +1598,7 @@ def __init__(self, name, func):
libtcod.console_set_default_foreground(None, libtcod.grey)
libtcod.console_set_default_background(None, libtcod.black)
libtcod.console_print_ex(None, 42, 46 - (libtcod.NB_RENDERERS + 1), libtcod.BKGND_SET, libtcod.LEFT, "Renderer :")
- for i in range(libtcod.NB_RENDERERS):
+ for i in range(len(renderer_name)):
if i == cur_renderer:
libtcod.console_set_default_foreground(None, libtcod.white)
libtcod.console_set_default_background(None, libtcod.light_blue)
diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py
index 3ca8b1a5..35e1832d 100755
--- a/examples/samples_tcod.py
+++ b/examples/samples_tcod.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python3
-"""
-This code demonstrates various usages of python-tcod.
-"""
+"""This code demonstrates various usages of python-tcod."""
# To the extent possible under law, the libtcod maintainers have waived all
# copyright and related or neighboring rights to these samples.
# https://creativecommons.org/publicdomain/zero/1.0/
@@ -9,30 +7,34 @@
import copy
import math
-import os
import random
import sys
import time
import warnings
-from typing import List
+from pathlib import Path
+from typing import Any
import numpy as np
-import tcod
from numpy.typing import NDArray
+import tcod
+import tcod.constants
+import tcod.event
+import tcod.libtcodpy
+import tcod.noise
+import tcod.render
+import tcod.sdl.mouse
+import tcod.sdl.render
+
+# ruff: noqa: S311
+
if not sys.warnoptions:
warnings.simplefilter("default") # Show all warnings.
+DATA_DIR = Path(__file__).parent / "../libtcod/data"
+"""Path of the samples data directory."""
-def get_data(path: str) -> str:
- """Return the path to a resource in the libtcod data directory,"""
- SCRIPT_DIR = os.path.dirname(__file__)
- DATA_DIR = os.path.join(SCRIPT_DIR, "../libtcod/data")
- assert os.path.exists(DATA_DIR), (
- "Data directory is missing," " did you forget to run `git submodule update --init`?"
- )
- return os.path.join(DATA_DIR, path)
-
+assert DATA_DIR.exists(), "Data directory is missing, did you forget to run `git submodule update --init`?"
WHITE = (255, 255, 255)
GREY = (127, 127, 127)
@@ -44,12 +46,14 @@ def get_data(path: str) -> str:
SAMPLE_SCREEN_HEIGHT = 20
SAMPLE_SCREEN_X = 20
SAMPLE_SCREEN_Y = 10
-FONT = get_data("fonts/dejavu10x10_gs_tc.png")
+FONT = DATA_DIR / "fonts/dejavu10x10_gs_tc.png"
# Mutable global names.
context: tcod.context.Context
tileset: tcod.tileset.Tileset
-root_console = tcod.Console(80, 50, order="F")
+console_render: tcod.render.SDLConsoleRender # Optional SDL renderer.
+sample_minimap: tcod.sdl.render.Texture # Optional minimap texture.
+root_console = tcod.console.Console(80, 50, order="F")
sample_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT, order="F")
cur_sample = 0 # Current selected sample.
frame_times = [time.perf_counter()]
@@ -67,18 +71,18 @@ def on_draw(self) -> None:
pass
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- global cur_sample, context
- if event.sym == tcod.event.K_DOWN:
+ global cur_sample
+ if event.sym == tcod.event.KeySym.DOWN:
cur_sample = (cur_sample + 1) % len(SAMPLES)
SAMPLES[cur_sample].on_enter()
draw_samples_menu()
- elif event.sym == tcod.event.K_UP:
+ elif event.sym == tcod.event.KeySym.UP:
cur_sample = (cur_sample - 1) % len(SAMPLES)
SAMPLES[cur_sample].on_enter()
draw_samples_menu()
- elif event.sym == tcod.event.K_RETURN and event.mod & tcod.event.KMOD_LALT:
+ elif event.sym == tcod.event.KeySym.RETURN and event.mod & tcod.event.KMOD_LALT:
tcod.console_set_fullscreen(not tcod.console_is_fullscreen())
- elif event.sym == tcod.event.K_PRINTSCREEN or event.sym == ord("p"):
+ elif event.sym == tcod.event.KeySym.PRINTSCREEN or event.sym == tcod.event.KeySym.p:
print("screenshot")
if event.mod & tcod.event.KMOD_LALT:
tcod.console_save_apf(root_console, "samples.apf")
@@ -86,12 +90,11 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
else:
tcod.sys_save_screenshot()
print("png")
- elif event.sym == tcod.event.K_ESCAPE:
+ elif event.sym == tcod.event.KeySym.ESCAPE:
raise SystemExit()
elif event.sym in RENDERER_KEYS:
# Swap the active context for one with a different renderer.
- context.close()
- context = init_context(RENDERER_KEYS[event.sym])
+ init_context(RENDERER_KEYS[event.sym])
def ev_quit(self, event: tcod.event.Quit) -> None:
raise SystemExit()
@@ -101,14 +104,14 @@ class TrueColorSample(Sample):
def __init__(self) -> None:
self.name = "True colors"
# corner colors
- self.colors = np.array(
+ self.colors: NDArray[np.int16] = np.array(
[(50, 40, 150), (240, 85, 5), (50, 35, 240), (10, 200, 130)],
dtype=np.int16,
)
# color shift direction
- self.slide_dir = np.array([[1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, 1, -1]], dtype=np.int16)
+ self.slide_dir: NDArray[np.int16] = np.array([[1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, 1, -1]], dtype=np.int16)
# corner indexes
- self.corners = np.array([0, 1, 2, 3])
+ self.corners: NDArray[np.int16] = np.array([0, 1, 2, 3], dtype=np.int16)
def on_draw(self) -> None:
self.slide_corner_colors()
@@ -193,7 +196,7 @@ def __init__(self) -> None:
"You can render to an offscreen console and blit in on another " "one, simulating alpha transparency.",
fg=WHITE,
bg=None,
- alignment=tcod.CENTER,
+ alignment=tcod.constants.CENTER,
)
def on_enter(self) -> None:
@@ -229,7 +232,6 @@ def on_draw(self) -> None:
class LineDrawingSample(Sample):
-
FLAG_NAMES = [
"BKGND_NONE",
"BKGND_SET",
@@ -248,8 +250,8 @@ class LineDrawingSample(Sample):
def __init__(self) -> None:
self.name = "Line drawing"
- self.mk_flag = tcod.BKGND_SET
- self.bk_flag = tcod.BKGND_SET
+ self.mk_flag = tcod.constants.BKGND_SET
+ self.bk_flag = tcod.constants.BKGND_SET
self.bk = tcod.console.Console(sample_console.width, sample_console.height, order="F")
# initialize the colored background
@@ -259,7 +261,7 @@ def __init__(self) -> None:
self.bk.ch[:] = ord(" ")
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym in (tcod.event.K_RETURN, tcod.event.K_KP_ENTER):
+ if event.sym in (tcod.event.KeySym.RETURN, tcod.event.KeySym.KP_ENTER):
self.bk_flag += 1
if (self.bk_flag & 0xFF) > tcod.BKGND_ALPH:
self.bk_flag = tcod.BKGND_NONE
@@ -294,7 +296,7 @@ def on_draw(self) -> None:
yd = int(sample_console.height // 2 - sin_angle * sample_console.width // 2)
# draw the line
# in python the easiest way is to use the line iterator
- for x, y in tcod.line_iter(xo, yo, xd, yd):
+ for x, y in tcod.los.bresenham((xo, yo), (xd, yd)).tolist():
if 0 <= x < sample_console.width and 0 <= y < sample_console.height:
tcod.console_set_char_background(sample_console, x, y, LIGHT_BLUE, self.bk_flag)
sample_console.print(
@@ -362,10 +364,10 @@ def __init__(self) -> None:
self.dy = 0.0
self.octaves = 4.0
self.zoom = 3.0
- self.hurst = tcod.NOISE_DEFAULT_HURST
- self.lacunarity = tcod.NOISE_DEFAULT_LACUNARITY
+ self.hurst = tcod.libtcodpy.NOISE_DEFAULT_HURST
+ self.lacunarity = tcod.libtcodpy.NOISE_DEFAULT_LACUNARITY
self.noise = self.get_noise()
- self.img = tcod.image_new(SAMPLE_SCREEN_WIDTH * 2, SAMPLE_SCREEN_HEIGHT * 2)
+ self.img = tcod.image.Image(SAMPLE_SCREEN_WIDTH * 2, SAMPLE_SCREEN_HEIGHT * 2)
@property
def algorithm(self) -> int:
@@ -449,30 +451,30 @@ def on_draw(self) -> None:
)
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if ord("9") >= event.sym >= ord("1"):
- self.func = event.sym - ord("1")
+ if tcod.event.KeySym.N9 >= event.sym >= tcod.event.KeySym.N1:
+ self.func = event.sym - tcod.event.KeySym.N1
self.noise = self.get_noise()
- elif event.sym == ord("e"):
+ elif event.sym == tcod.event.KeySym.e:
self.hurst += 0.1
self.noise = self.get_noise()
- elif event.sym == ord("d"):
+ elif event.sym == tcod.event.KeySym.d:
self.hurst -= 0.1
self.noise = self.get_noise()
- elif event.sym == ord("r"):
+ elif event.sym == tcod.event.KeySym.r:
self.lacunarity += 0.5
self.noise = self.get_noise()
- elif event.sym == ord("f"):
+ elif event.sym == tcod.event.KeySym.f:
self.lacunarity -= 0.5
self.noise = self.get_noise()
- elif event.sym == ord("t"):
+ elif event.sym == tcod.event.KeySym.t:
self.octaves += 0.5
self.noise.octaves = self.octaves
- elif event.sym == ord("g"):
+ elif event.sym == tcod.event.KeySym.g:
self.octaves -= 0.5
self.noise.octaves = self.octaves
- elif event.sym == ord("y"):
+ elif event.sym == tcod.event.KeySym.y:
self.zoom += 0.2
- elif event.sym == ord("h"):
+ elif event.sym == tcod.event.KeySym.h:
self.zoom -= 0.2
else:
super().ev_keydown(event)
@@ -509,7 +511,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
"##############################################",
]
-SAMPLE_MAP = np.array([list(line) for line in SAMPLE_MAP_]).transpose()
+SAMPLE_MAP: NDArray[Any] = np.array([list(line) for line in SAMPLE_MAP_]).transpose()
FOV_ALGO_NAMES = [
"BASIC ",
@@ -540,22 +542,22 @@ def __init__(self) -> None:
self.player_y = 10
self.torch = False
self.light_walls = True
- self.algo_num = 0
+ self.algo_num = tcod.constants.FOV_SYMMETRIC_SHADOWCAST
self.noise = tcod.noise.Noise(1) # 1D noise for the torch flickering.
map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
- self.walkable = np.zeros(map_shape, dtype=bool, order="F")
+ self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
self.walkable[:] = SAMPLE_MAP[:] == " "
- self.transparent = np.zeros(map_shape, dtype=bool, order="F")
+ self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
self.transparent[:] = self.walkable[:] | (SAMPLE_MAP == "=")
# Lit background colors for the map.
- self.light_map_bg = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B")
+ self.light_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B")
self.light_map_bg[SAMPLE_MAP[:] == "#"] = LIGHT_WALL
# Dark background colors for the map.
- self.dark_map_bg = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B")
+ self.dark_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B")
self.dark_map_bg[SAMPLE_MAP[:] == "#"] = DARK_WALL
def draw_ui(self) -> None:
@@ -563,10 +565,9 @@ def draw_ui(self) -> None:
1,
1,
"IJKL : move around\n"
- "T : torch fx %s\n"
- "W : light walls %s\n"
- "+-: algo %s"
- % (
+ "T : torch fx {}\n"
+ "W : light walls {}\n"
+ "+-: algo {}".format(
"on " if self.torch else "off",
"on " if self.light_walls else "off",
FOV_ALGO_NAMES[self.algo_num],
@@ -581,7 +582,7 @@ def on_draw(self) -> None:
self.draw_ui()
sample_console.print(self.player_x, self.player_y, "@")
# Draw windows.
- sample_console.tiles_rgb["ch"][SAMPLE_MAP == "="] = tcod.CHAR_DHLINE
+ sample_console.tiles_rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL
sample_console.tiles_rgb["fg"][SAMPLE_MAP == "="] = BLACK
# Get a 2D boolean array of visible cells.
@@ -608,7 +609,7 @@ def on_draw(self) -> None:
x = x.astype(np.float32) - torch_x
y = y.astype(np.float32) - torch_y
- distance_squared = x ** 2 + y ** 2 # 2D squared distance array.
+ distance_squared = x**2 + y**2 # 2D squared distance array.
# Get the currently visible cells.
visible = (distance_squared < SQUARED_TORCH_RADIUS) & fov
@@ -621,8 +622,8 @@ def on_draw(self) -> None:
light[~visible] = 0 # Set non-visible areas to darkness.
# Setup background colors for floating point math.
- light_bg = self.light_map_bg.astype(np.float16)
- dark_bg = self.dark_map_bg.astype(np.float16)
+ light_bg: NDArray[np.float16] = self.light_map_bg.astype(np.float16)
+ dark_bg: NDArray[np.float16] = self.dark_map_bg.astype(np.float16)
# Linear interpolation between colors.
sample_console.tiles_rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis]
@@ -631,25 +632,25 @@ def on_draw(self) -> None:
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
MOVE_KEYS = {
- ord("i"): (0, -1),
- ord("j"): (-1, 0),
- ord("k"): (0, 1),
- ord("l"): (1, 0),
+ tcod.event.KeySym.i: (0, -1),
+ tcod.event.KeySym.j: (-1, 0),
+ tcod.event.KeySym.k: (0, 1),
+ tcod.event.KeySym.l: (1, 0),
}
FOV_SELECT_KEYS = {
- ord("-"): -1,
- ord("="): 1,
- tcod.event.K_KP_MINUS: -1,
- tcod.event.K_KP_PLUS: 1,
+ tcod.event.KeySym.MINUS: -1,
+ tcod.event.KeySym.EQUALS: 1,
+ tcod.event.KeySym.KP_MINUS: -1,
+ tcod.event.KeySym.KP_PLUS: 1,
}
if event.sym in MOVE_KEYS:
x, y = MOVE_KEYS[event.sym]
if self.walkable[self.player_x + x, self.player_y + y]:
self.player_x += x
self.player_y += y
- elif event.sym == ord("t"):
+ elif event.sym == tcod.event.KeySym.t:
self.torch = not self.torch
- elif event.sym == ord("w"):
+ elif event.sym == tcod.event.KeySym.w:
self.light_walls = not self.light_walls
elif event.sym in FOV_SELECT_KEYS:
self.algo_num += FOV_SELECT_KEYS[event.sym]
@@ -672,17 +673,19 @@ def __init__(self) -> None:
self.busy = 0.0
self.oldchar = " "
- self.map = tcod.map_new(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
+ self.map = tcod.map.Map(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
for y in range(SAMPLE_SCREEN_HEIGHT):
for x in range(SAMPLE_SCREEN_WIDTH):
if SAMPLE_MAP[x, y] == " ":
# ground
- tcod.map_set_properties(self.map, x, y, True, True)
+ self.map.walkable[y, x] = True
+ self.map.transparent[y, x] = True
elif SAMPLE_MAP[x, y] == "=":
# window
- tcod.map_set_properties(self.map, x, y, True, False)
- self.path = tcod.path_new_using_map(self.map)
- self.dijkstra = tcod.dijkstra_new(self.map)
+ self.map.walkable[y, x] = False
+ self.map.transparent[y, x] = True
+ self.path = tcod.path.AStar(self.map)
+ self.dijkstra = tcod.path.Dijkstra(self.map)
def on_enter(self) -> None:
# we draw the foreground only the first time.
@@ -775,7 +778,7 @@ def on_draw(self) -> None:
self.recalculate = True
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym == ord("i") and self.dy > 0:
+ if event.sym == tcod.event.KeySym.i and self.dy > 0:
# destination move north
tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE)
self.dy -= 1
@@ -783,7 +786,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE)
if SAMPLE_MAP[self.dx, self.dy] == " ":
self.recalculate = True
- elif event.sym == ord("k") and self.dy < SAMPLE_SCREEN_HEIGHT - 1:
+ elif event.sym == tcod.event.KeySym.k and self.dy < SAMPLE_SCREEN_HEIGHT - 1:
# destination move south
tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE)
self.dy += 1
@@ -791,7 +794,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE)
if SAMPLE_MAP[self.dx, self.dy] == " ":
self.recalculate = True
- elif event.sym == ord("j") and self.dx > 0:
+ elif event.sym == tcod.event.KeySym.j and self.dx > 0:
# destination move west
tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE)
self.dx -= 1
@@ -799,7 +802,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE)
if SAMPLE_MAP[self.dx, self.dy] == " ":
self.recalculate = True
- elif event.sym == ord("l") and self.dx < SAMPLE_SCREEN_WIDTH - 1:
+ elif event.sym == tcod.event.KeySym.l and self.dx < SAMPLE_SCREEN_WIDTH - 1:
# destination move east
tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE)
self.dx += 1
@@ -807,7 +810,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE)
if SAMPLE_MAP[self.dx, self.dy] == " ":
self.recalculate = True
- elif event.sym == tcod.event.K_TAB:
+ elif event.sym == tcod.event.KeySym.TAB:
self.using_astar = not self.using_astar
if self.using_astar:
tcod.console_print(sample_console, 1, 4, "Using : A* ")
@@ -905,50 +908,48 @@ def traverse_node(bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP) -> None:
left, right = node.children
node.x = min(left.x, right.x)
node.y = min(left.y, right.y)
- node.w = max(left.x + left.w, right.x + right.w) - node.x
- node.h = max(left.y + left.h, right.y + right.h) - node.y
+ node.width = max(left.x + left.width, right.x + right.width) - node.x
+ node.height = max(left.y + left.height, right.y + right.height) - node.y
# create a corridor between the two lower nodes
if node.horizontal:
# vertical corridor
- if left.x + left.w - 1 < right.x or right.x + right.w - 1 < left.x:
+ if left.x + left.width - 1 < right.x or right.x + right.width - 1 < left.x:
# no overlapping zone. we need a Z shaped corridor
- x1 = random.randint(left.x, left.x + left.w - 1)
- x2 = random.randint(right.x, right.x + right.w - 1)
- y = random.randint(left.y + left.h, right.y)
+ x1 = random.randint(left.x, left.x + left.width - 1)
+ x2 = random.randint(right.x, right.x + right.width - 1)
+ y = random.randint(left.y + left.height, right.y)
vline_up(bsp_map, x1, y - 1)
hline(bsp_map, x1, y, x2)
vline_down(bsp_map, x2, y + 1)
else:
# straight vertical corridor
min_x = max(left.x, right.x)
- max_x = min(left.x + left.w - 1, right.x + right.w - 1)
+ max_x = min(left.x + left.width - 1, right.x + right.width - 1)
x = random.randint(min_x, max_x)
vline_down(bsp_map, x, right.y)
vline_up(bsp_map, x, right.y - 1)
+ elif left.y + left.height - 1 < right.y or right.y + right.height - 1 < left.y: # horizontal corridor
+ # no overlapping zone. we need a Z shaped corridor
+ y1 = random.randint(left.y, left.y + left.height - 1)
+ y2 = random.randint(right.y, right.y + right.height - 1)
+ x = random.randint(left.x + left.width, right.x)
+ hline_left(bsp_map, x - 1, y1)
+ vline(bsp_map, x, y1, y2)
+ hline_right(bsp_map, x + 1, y2)
else:
- # horizontal corridor
- if left.y + left.h - 1 < right.y or right.y + right.h - 1 < left.y:
- # no overlapping zone. we need a Z shaped corridor
- y1 = random.randint(left.y, left.y + left.h - 1)
- y2 = random.randint(right.y, right.y + right.h - 1)
- x = random.randint(left.x + left.w, right.x)
- hline_left(bsp_map, x - 1, y1)
- vline(bsp_map, x, y1, y2)
- hline_right(bsp_map, x + 1, y2)
- else:
- # straight horizontal corridor
- min_y = max(left.y, right.y)
- max_y = min(left.y + left.h - 1, right.y + right.h - 1)
- y = random.randint(min_y, max_y)
- hline_left(bsp_map, right.x - 1, y)
- hline_right(bsp_map, right.x, y)
+ # straight horizontal corridor
+ min_y = max(left.y, right.y)
+ max_y = min(left.y + left.height - 1, right.y + right.height - 1)
+ y = random.randint(min_y, max_y)
+ hline_left(bsp_map, right.x - 1, y)
+ hline_right(bsp_map, right.x, y)
class BSPSample(Sample):
def __init__(self) -> None:
self.name = "Bsp toolkit"
self.bsp = tcod.bsp.BSP(1, 1, SAMPLE_SCREEN_WIDTH - 1, SAMPLE_SCREEN_HEIGHT - 1)
- self.bsp_map = np.zeros((SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT), dtype=bool, order="F")
+ self.bsp_map: NDArray[np.bool_] = np.zeros((SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT), dtype=bool, order="F")
self.bsp_generate()
def bsp_generate(self) -> None:
@@ -999,28 +1000,28 @@ def on_draw(self) -> None:
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
global bsp_random_room, bsp_room_walls, bsp_depth, bsp_min_room_size
- if event.sym in (tcod.event.K_RETURN, tcod.event.K_KP_ENTER):
+ if event.sym in (tcod.event.KeySym.RETURN, tcod.event.KeySym.KP_ENTER):
self.bsp_generate()
- elif event.sym == ord(" "):
+ elif event.sym == tcod.event.KeySym.SPACE:
self.bsp_refresh()
- elif event.sym in (tcod.event.K_EQUALS, tcod.event.K_KP_PLUS):
+ elif event.sym in (tcod.event.KeySym.EQUALS, tcod.event.KeySym.KP_PLUS):
bsp_depth += 1
self.bsp_generate()
- elif event.sym in (tcod.event.K_MINUS, tcod.event.K_KP_MINUS):
+ elif event.sym in (tcod.event.KeySym.MINUS, tcod.event.KeySym.KP_MINUS):
bsp_depth = max(1, bsp_depth - 1)
self.bsp_generate()
- elif event.sym in (tcod.event.K_8, tcod.event.K_KP_MULTIPLY):
+ elif event.sym in (tcod.event.KeySym.N8, tcod.event.KeySym.KP_MULTIPLY):
bsp_min_room_size += 1
self.bsp_generate()
- elif event.sym in (tcod.event.K_SLASH, tcod.event.K_KP_DIVIDE):
+ elif event.sym in (tcod.event.KeySym.SLASH, tcod.event.KeySym.KP_DIVIDE):
bsp_min_room_size = max(2, bsp_min_room_size - 1)
self.bsp_generate()
- elif event.sym in (tcod.event.K_1, tcod.event.K_KP_1):
+ elif event.sym in (tcod.event.KeySym.N1, tcod.event.KeySym.KP_1):
bsp_random_room = not bsp_random_room
if not bsp_random_room:
bsp_room_walls = True
self.bsp_refresh()
- elif event.sym in (tcod.event.K_2, tcod.event.K_KP_2):
+ elif event.sym in (tcod.event.KeySym.N2, tcod.event.KeySym.KP_2):
bsp_room_walls = not bsp_room_walls
self.bsp_refresh()
else:
@@ -1031,9 +1032,9 @@ class ImageSample(Sample):
def __init__(self) -> None:
self.name = "Image toolkit"
- self.img = tcod.image_load(get_data("img/skull.png"))
+ self.img = tcod.image.Image.from_file(DATA_DIR / "img/skull.png")
self.img.set_key_color(BLACK)
- self.circle = tcod.image_load(get_data("img/circle.png"))
+ self.circle = tcod.image.Image.from_file(DATA_DIR / "img/circle.png")
def on_draw(self) -> None:
sample_console.clear()
@@ -1067,11 +1068,13 @@ def __init__(self) -> None:
self.motion = tcod.event.MouseMotion()
self.mouse_left = self.mouse_middle = self.mouse_right = 0
- self.log: List[str] = []
+ self.log: list[str] = []
def on_enter(self) -> None:
- tcod.mouse_move(320, 200)
- tcod.mouse_show_cursor(True)
+ sdl_window = context.sdl_window
+ if sdl_window:
+ tcod.sdl.mouse.warp_in_window(sdl_window, 320, 200)
+ tcod.sdl.mouse.show(True)
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
self.motion = event
@@ -1104,8 +1107,8 @@ def on_draw(self) -> None:
"Right button : %s\n"
"Middle button : %s\n"
% (
- self.motion.pixel.x,
- self.motion.pixel.y,
+ self.motion.position.x,
+ self.motion.position.y,
self.motion.tile.x,
self.motion.tile.y,
self.motion.tile_motion.x,
@@ -1126,10 +1129,10 @@ def on_draw(self) -> None:
)
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym == ord("1"):
- tcod.mouse_show_cursor(False)
- elif event.sym == ord("2"):
- tcod.mouse_show_cursor(True)
+ if event.sym == tcod.event.KeySym.N1:
+ tcod.sdl.mouse.show(False)
+ elif event.sym == tcod.event.KeySym.N2:
+ tcod.sdl.mouse.show(True)
else:
super().ev_keydown(event)
@@ -1140,15 +1143,15 @@ def __init__(self) -> None:
self.curset = 0
self.delay = 0.0
- self.names: List[str] = []
- self.sets: List[str] = []
+ self.names: list[str] = []
+ self.sets: list[str] = []
def on_draw(self) -> None:
if not self.sets:
# parse all *.cfg files in data/namegen
- for file in os.listdir(get_data("namegen")):
- if file.find(".cfg") > 0:
- tcod.namegen_parse(get_data(os.path.join("namegen", file)))
+ for file in (DATA_DIR / "namegen").iterdir():
+ if file.suffix == ".cfg":
+ tcod.namegen_parse(file)
# get the sets list
self.sets = tcod.namegen_get_sets()
print(self.sets)
@@ -1177,10 +1180,10 @@ def on_draw(self) -> None:
self.names.append(tcod.namegen_generate(self.sets[self.curset]))
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym == ord("="):
+ if event.sym == tcod.event.KeySym.EQUALS:
self.curset += 1
self.names.append("======")
- elif event.sym == ord("-"):
+ elif event.sym == tcod.event.KeySym.MINUS:
self.curset -= 1
self.names.append("======")
else:
@@ -1214,12 +1217,12 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
# xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]
# yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]
if numpy_available:
- (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H)) # type: ignore
+ (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H))
# translate coordinates of all pixels to center
xc = xc - HALF_W
yc = yc - HALF_H
-noise2d = tcod.noise_new(2, 0.5, 2.0)
+noise2d = tcod.noise.Noise(2, hurst=0.5, lacunarity=2.0)
if numpy_available: # the texture starts empty
texture = np.zeros((RES_U, RES_V))
@@ -1253,7 +1256,7 @@ def on_enter(self) -> None:
self.frac_t: float = RES_V - 1
self.abs_t: float = RES_V - 1
# light and current color of the tunnel texture
- self.lights: List[Light] = []
+ self.lights: list[Light] = []
self.tex_r = 0.0
self.tex_g = 0.0
self.tex_b = 0.0
@@ -1299,8 +1302,8 @@ def on_draw(self) -> None:
# squared distance from center,
# clipped to sensible minimum and maximum values
- sqr_dist = xc ** 2 + yc ** 2
- sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V ** 2)
+ sqr_dist = xc**2 + yc**2
+ sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V**2)
# one coordinate into the texture, represents depth in the tunnel
vv = TEX_STRETCH * float(RES_V) / sqr_dist + self.frac_t
@@ -1363,11 +1366,11 @@ def on_draw(self) -> None:
#############################################
RENDERER_KEYS = {
- tcod.event.K_F1: tcod.RENDERER_GLSL,
- tcod.event.K_F2: tcod.RENDERER_OPENGL,
- tcod.event.K_F3: tcod.RENDERER_SDL,
- tcod.event.K_F4: tcod.RENDERER_SDL2,
- tcod.event.K_F5: tcod.RENDERER_OPENGL2,
+ tcod.event.KeySym.F1: tcod.constants.RENDERER_GLSL,
+ tcod.event.KeySym.F2: tcod.constants.RENDERER_OPENGL,
+ tcod.event.KeySym.F3: tcod.constants.RENDERER_SDL,
+ tcod.event.KeySym.F4: tcod.constants.RENDERER_SDL2,
+ tcod.event.KeySym.F5: tcod.constants.RENDERER_OPENGL2,
}
RENDERER_NAMES = (
@@ -1393,30 +1396,47 @@ def on_draw(self) -> None:
)
-def init_context(renderer: int) -> tcod.context.Context:
- """Return a new context with common parameters set.
+def init_context(renderer: int) -> None:
+ """Setup or reset a global context with common parameters set.
This function exists to more easily switch between renderers.
"""
+ global context, console_render, sample_minimap
+ if "context" in globals():
+ context.close()
libtcod_version = "%i.%i.%i" % (
tcod.lib.TCOD_MAJOR_VERSION,
tcod.lib.TCOD_MINOR_VERSION,
tcod.lib.TCOD_PATCHLEVEL,
)
- return tcod.context.new(
+ context = tcod.context.new(
columns=root_console.width,
rows=root_console.height,
- title=f"python-tcod samples" f" (python-tcod {tcod.__version__}, libtcod {libtcod_version})",
- renderer=renderer,
+ title=f"python-tcod samples (python-tcod {tcod.__version__}, libtcod {libtcod_version})",
vsync=False, # VSync turned off since this is for benchmarking.
tileset=tileset,
)
+ if context.sdl_renderer: # If this context supports SDL rendering.
+ # Start by setting the logical size so that window resizing doesn't break anything.
+ context.sdl_renderer.logical_size = (
+ tileset.tile_width * root_console.width,
+ tileset.tile_height * root_console.height,
+ )
+ assert context.sdl_atlas
+ # Generate the console renderer and minimap.
+ console_render = tcod.render.SDLConsoleRender(context.sdl_atlas)
+ sample_minimap = context.sdl_renderer.new_texture(
+ SAMPLE_SCREEN_WIDTH,
+ SAMPLE_SCREEN_HEIGHT,
+ format=tcod.lib.SDL_PIXELFORMAT_RGB24,
+ access=tcod.sdl.render.TextureAccess.STREAMING, # Updated every frame.
+ )
def main() -> None:
global context, tileset
tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD)
- context = init_context(tcod.RENDERER_SDL2)
+ init_context(tcod.constants.RENDERER_SDL2)
try:
SAMPLES[cur_sample].on_enter()
@@ -1429,7 +1449,25 @@ def main() -> None:
SAMPLES[cur_sample].on_draw()
sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y)
draw_stats()
- context.present(root_console)
+ if context.sdl_renderer:
+ # SDL renderer support, upload the sample console background to a minimap texture.
+ sample_minimap.update(sample_console.rgb.T["bg"])
+ # Render the root_console normally, this is the drawing step of context.present without presenting.
+ context.sdl_renderer.copy(console_render.render(root_console))
+ # Render the minimap to the screen.
+ context.sdl_renderer.copy(
+ sample_minimap,
+ dest=(
+ tileset.tile_width * 24,
+ tileset.tile_height * 36,
+ SAMPLE_SCREEN_WIDTH * 3,
+ SAMPLE_SCREEN_HEIGHT * 3,
+ ),
+ )
+ context.sdl_renderer.present()
+ else: # No SDL renderer, just use plain context rendering.
+ context.present(root_console)
+
handle_time()
handle_events()
finally:
@@ -1449,7 +1487,21 @@ def handle_time() -> None:
def handle_events() -> None:
for event in tcod.event.get():
- context.convert_event(event)
+ if context.sdl_renderer:
+ # Manual handing of tile coordinates since context.present is skipped.
+ if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)):
+ event.tile = tcod.event.Point(
+ event.position.x // tileset.tile_width, event.position.y // tileset.tile_height
+ )
+ if isinstance(event, tcod.event.MouseMotion):
+ prev_tile = (
+ (event.position[0] - event.motion[0]) // tileset.tile_width,
+ (event.position[1] - event.motion[1]) // tileset.tile_height,
+ )
+ event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1])
+ else:
+ context.convert_event(event)
+
SAMPLES[cur_sample].dispatch(event)
if isinstance(event, tcod.event.Quit):
raise SystemExit()
diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py
new file mode 100644
index 00000000..b9c07547
--- /dev/null
+++ b/examples/sdl-hello-world.py
@@ -0,0 +1,48 @@
+"""Hello world using tcod's SDL API and using Pillow for the TTF rendering."""
+from pathlib import Path
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont # type: ignore # pip install Pillow
+
+import tcod.event
+import tcod.sdl.render
+import tcod.sdl.video
+
+CURRENT_DIR = Path(__file__).parent # Directory of this script.
+font = ImageFont.truetype(bytes(CURRENT_DIR / "DejaVuSerif.ttf"), size=18) # Preloaded font file.
+
+
+def render_text(renderer: tcod.sdl.render.Renderer, text: str) -> tcod.sdl.render.Texture:
+ """Render text, upload it to VRAM, then return it as an SDL Texture."""
+ # Use Pillow normally to render the font. This code is standard.
+ width, height = font.getsize(text)
+ image = Image.new("RGBA", (width, height))
+ draw = ImageDraw.Draw(image)
+ draw.text((0, 0), text, font=font)
+ # Push to VRAM using SDL.
+ texture = renderer.upload_texture(np.asarray(image))
+ texture.blend_mode = tcod.sdl.render.BlendMode.BLEND # Enable alpha blending by default.
+ return texture
+
+
+def main() -> None:
+ """Show hello world until the window is closed."""
+ # Open an SDL window and renderer.
+ window = tcod.sdl.video.new_window(720, 480, flags=tcod.sdl.video.WindowFlags.RESIZABLE)
+ renderer = tcod.sdl.render.new_renderer(window, target_textures=True)
+ # Render the text once, then reuse the texture.
+ hello_world = render_text(renderer, "Hello World")
+ hello_world.color_mod = (64, 255, 64) # Set the color when copied.
+
+ while True:
+ renderer.draw_color = (0, 0, 0, 255)
+ renderer.clear()
+ renderer.copy(hello_world, dest=(0, 0, hello_world.width, hello_world.height))
+ renderer.present()
+ for event in tcod.event.get():
+ if isinstance(event, tcod.event.Quit):
+ raise SystemExit()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/termbox/termbox.py b/examples/termbox/termbox.py
index d97d9110..26ab2d7a 100755
--- a/examples/termbox/termbox.py
+++ b/examples/termbox/termbox.py
@@ -1,5 +1,4 @@
-"""
-Implementation of Termbox Python API in tdl.
+"""Implementation of Termbox Python API in tdl.
See README.md for details.
"""
@@ -18,10 +17,10 @@
class TermboxException(Exception):
- def __init__(self, msg):
+ def __init__(self, msg) -> None:
self.msg = msg
- def __str__(self):
+ def __str__(self) -> str:
return self.msg
@@ -153,7 +152,7 @@ def __str__(self):
class Event:
- """Aggregate for Termbox Event structure"""
+ """Aggregate for Termbox Event structure."""
type = None
ch = None
@@ -169,10 +168,11 @@ def gettuple(self):
class Termbox:
- def __init__(self, width=132, height=60):
+ def __init__(self, width=132, height=60) -> None:
global _instance
if _instance:
- raise TermboxException("It is possible to create only one instance of Termbox")
+ msg = "It is possible to create only one instance of Termbox"
+ raise TermboxException(msg)
try:
self.console = tdl.init(width, height)
@@ -183,7 +183,7 @@ def __init__(self, width=132, height=60):
_instance = self
- def __del__(self):
+ def __del__(self) -> None:
self.close()
def __exit__(self, *args): # t, value, traceback):
@@ -289,5 +289,6 @@ def poll_event(self):
if e.type == "KEYDOWN":
self.e.key = e.key
return self.e.gettuple()
+ return None
# return (e.type, uch, e.key, e.mod, e.w, e.h, e.x, e.y)
diff --git a/examples/termbox/termboxtest.py b/examples/termbox/termboxtest.py
index 9b43cfae..c0d57065 100755
--- a/examples/termbox/termboxtest.py
+++ b/examples/termbox/termboxtest.py
@@ -1,5 +1,4 @@
#!/usr/bin/python
-# -*- encoding: utf-8 -*-
import termbox
@@ -17,8 +16,8 @@ def print_line(t, msg, y, fg, bg):
t.change_cell(x + i, y, c, fg, bg)
-class SelectBox(object):
- def __init__(self, tb, choices, active=-1):
+class SelectBox:
+ def __init__(self, tb, choices, active=-1) -> None:
self.tb = tb
self.active = active
self.choices = choices
diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py
index 82d88358..d6fa54fd 100755
--- a/examples/thread_jobs.py
+++ b/examples/thread_jobs.py
@@ -20,7 +20,7 @@
import timeit
from typing import Callable, List, Tuple
-import tcod
+import tcod.map
THREADS = multiprocessing.cpu_count()
@@ -42,7 +42,7 @@ def test_fov_single(maps: List[tcod.map.Map]) -> None:
def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None:
- for result in executor.map(test_fov, maps):
+ for _result in executor.map(test_fov, maps):
pass
@@ -57,7 +57,7 @@ def test_astar_single(maps: List[tcod.map.Map]) -> None:
def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None:
- for result in executor.map(test_astar, maps):
+ for _result in executor.map(test_astar, maps):
pass
@@ -66,8 +66,7 @@ def run_test(
single_func: Callable[[List[tcod.map.Map]], None],
multi_func: Callable[[concurrent.futures.Executor, List[tcod.map.Map]], None],
) -> None:
- """Run a function designed for a single thread and compare it to a threaded
- version.
+ """Run a function designed for a single thread and compare it to a threaded version.
This prints the results of these tests.
"""
diff --git a/examples/ttf.py b/examples/ttf.py
old mode 100644
new mode 100755
index 36434916..e1c585d5
--- a/examples/ttf.py
+++ b/examples/ttf.py
@@ -12,7 +12,12 @@
import freetype # type: ignore # pip install freetype-py
import numpy as np
-import tcod
+from numpy.typing import NDArray
+
+import tcod.console
+import tcod.context
+import tcod.event
+import tcod.tileset
FONT = "VeraMono.ttf"
@@ -36,10 +41,10 @@ def load_ttf(path: str, size: Tuple[int, int]) -> tcod.tileset.Tileset:
ttf.load_glyph(glyph_index)
bitmap = ttf.glyph.bitmap
assert bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY
- bitmap_array = np.asarray(bitmap.buffer).reshape((bitmap.width, bitmap.rows), order="F")
+ bitmap_array: NDArray[np.uint8] = np.asarray(bitmap.buffer).reshape((bitmap.width, bitmap.rows), order="F")
if bitmap_array.size == 0:
continue # Skip blank glyphs.
- output_image = np.zeros(size, dtype=np.uint8, order="F")
+ output_image: NDArray[np.uint8] = np.zeros(size, dtype=np.uint8, order="F")
out_slice = output_image
# Adjust the position to center this glyph on the tile.
@@ -57,7 +62,8 @@ def load_ttf(path: str, size: Tuple[int, int]) -> tcod.tileset.Tileset:
def main() -> None:
- console = tcod.Console(16, 12, order="F")
+ """True-type font example script."""
+ console = tcod.console.Console(16, 12, order="F")
with tcod.context.new(
columns=console.width,
rows=console.height,
diff --git a/fonts/libtcod/README.txt b/fonts/libtcod/README.txt
index c9726fbc..772f3efc 100755
--- a/fonts/libtcod/README.txt
+++ b/fonts/libtcod/README.txt
@@ -1,5 +1,5 @@
This directory contains antialiased fonts for libtcod.
-These fonts are in public domain.
+These fonts are in public domain.
The file names are composed with :
__.png
diff --git a/libtcod b/libtcod
index 40cfd7c4..168ab8ce 160000
--- a/libtcod
+++ b/libtcod
@@ -1 +1 @@
-Subproject commit 40cfd7c47bbd12b5f0845befbe9a156c5514e233
+Subproject commit 168ab8ce054f84087e04595a54ad02093c31a5a9
diff --git a/libtcodpy.py b/libtcodpy.py
index bb1a8cc8..63e2e0b0 100644
--- a/libtcodpy.py
+++ b/libtcodpy.py
@@ -1,9 +1,11 @@
-"""This module just an alias for tcod"""
+"""Module alias for tcod."""
import warnings
+from tcod import * # noqa: F4
+from tcod.libtcodpy import __getattr__ # noqa: F401
+
warnings.warn(
"'import tcod as libtcodpy' is preferred.",
DeprecationWarning,
stacklevel=2,
)
-from tcod import * # noqa: F4
diff --git a/parse_sdl2.py b/parse_sdl2.py
deleted file mode 100644
index bfdc0125..00000000
--- a/parse_sdl2.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import os.path
-import platform
-import re
-import sys
-from typing import Any, Dict, Iterator
-
-import cffi # type: ignore
-
-# Various poorly made regular expressions, these will miss code which isn't
-# supported by cffi.
-RE_COMMENT = re.compile(r" */\*.*?\*/", re.DOTALL)
-RE_REMOVALS = re.compile(
- r"#ifndef DOXYGEN_SHOULD_IGNORE_THIS.*" r"#endif /\* DOXYGEN_SHOULD_IGNORE_THIS \*/",
- re.DOTALL,
-)
-RE_DEFINE = re.compile(r"#define \w+(?!\() (?:.*?(?:\\\n)?)*$", re.MULTILINE)
-RE_TYPEDEF = re.compile(r"^typedef[^{;#]*?(?:{[^}]*\n}[^;]*)?;", re.MULTILINE)
-RE_ENUM = re.compile(r"^(?:typedef )?enum[^{;#]*?(?:{[^}]*\n}[^;]*)?;", re.MULTILINE)
-RE_DECL = re.compile(r"^extern[^#(]*\([^#]*?\);$", re.MULTILINE | re.DOTALL)
-RE_ENDIAN = re.compile(r"#if SDL_BYTEORDER == SDL_LIL_ENDIAN(.*?)#else(.*?)#endif", re.DOTALL)
-RE_ENDIAN2 = re.compile(r"#if SDL_BYTEORDER == SDL_BIG_ENDIAN(.*?)#else(.*?)#endif", re.DOTALL)
-RE_DEFINE_TRUNCATE = re.compile(r"(#define\s+\w+\s+).+$", flags=re.DOTALL)
-RE_TYPEDEF_TRUNCATE = re.compile(r"(typedef\s+\w+\s+\w+)\s*{.*\n}(?=.*;$)", flags=re.DOTALL | re.MULTILINE)
-RE_ENUM_TRUNCATE = re.compile(r"(\w+\s*=).+?(?=,$|})(?![^(']*\))", re.MULTILINE | re.DOTALL)
-RE_EVENT_PADDING = re.compile(r"Uint8 padding\[[^]]+\];", re.MULTILINE | re.DOTALL)
-
-
-def get_header(name: str) -> str:
- """Return the source of a header in a partially preprocessed state."""
- with open(name, "r", encoding="utf-8") as f:
- header = f.read()
- # Remove Doxygen code.
- header = RE_REMOVALS.sub("", header)
- # Remove comments.
- header = RE_COMMENT.sub("", header)
- # Deal with endianness in "SDL_audio.h".
- header = RE_ENDIAN.sub(r"\1" if sys.byteorder == "little" else r"\2", header)
- header = RE_ENDIAN2.sub(r"\1" if sys.byteorder != "little" else r"\2", header)
-
- # Ignore bad ARM compiler typedef.
- header = header.replace("typedef int SDL_bool;", "")
- return header
-
-
-# Remove non-integer definitions.
-DEFINE_BLACKLIST = [
- "SDL_AUDIOCVT_PACKED",
- "SDL_BlitScaled",
- "SDL_BlitSurface",
- "SDL_Colour",
- "SDL_IPHONE_MAX_GFORCE",
-]
-
-
-def parse(header: str, NEEDS_PACK4: bool) -> Iterator[str]:
- """Pull individual sections from a header, processing them as needed."""
- for define in RE_DEFINE.findall(header):
- if any(item in define for item in DEFINE_BLACKLIST):
- continue # Remove non-integer definitions.
- if '"' in define:
- continue # Ignore definitions with strings.
- # Replace various definitions with "..." since cffi is limited here.
- yield RE_DEFINE_TRUNCATE.sub(r"\1 ...", define)
-
- for enum in RE_ENUM.findall(header):
- yield RE_ENUM_TRUNCATE.sub(r"\1 ...", enum)
- header = header.replace(enum, "")
-
- for typedef in RE_TYPEDEF.findall(header):
- # Special case for SDL window flags enum.
- if "SDL_WINDOW_FULLSCREEN_DESKTOP" in typedef:
- typedef = typedef.replace("( SDL_WINDOW_FULLSCREEN | 0x00001000 )", "...")
- # Detect array sizes at compile time.
- typedef = typedef.replace("SDL_TEXTINPUTEVENT_TEXT_SIZE", "...")
- typedef = typedef.replace("SDL_TEXTEDITINGEVENT_TEXT_SIZE", "...")
- typedef = typedef.replace("SDL_AUDIOCVT_MAX_FILTERS + 1", "...")
-
- typedef = typedef.replace("SDLCALL", " ")
- typedef = typedef.replace("SDL_AUDIOCVT_PACKED ", "")
- typedef = RE_EVENT_PADDING.sub("Uint8 padding[...];", typedef)
-
- if NEEDS_PACK4 and "typedef struct SDL_AudioCVT" in typedef:
- typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef)
- if NEEDS_PACK4 and "typedef struct SDL_TouchFingerEvent" in typedef:
- typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef)
- if NEEDS_PACK4 and "typedef struct SDL_MultiGestureEvent" in typedef:
- typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef)
- if NEEDS_PACK4 and "typedef struct SDL_DollarGestureEvent" in typedef:
- typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef)
- yield typedef
-
- for decl in RE_DECL.findall(header):
- if "SDL_RWops" in decl:
- continue # Ignore SDL_RWops functions.
- if "va_list" in decl:
- continue
- decl = re.sub(r"SDL_PRINTF_VARARG_FUNC\(\w*\)", "", decl)
- decl = decl.replace("SDL_DEPRECATED", "")
- decl = decl.replace("SDLCALL", "")
- decl = re.sub(r"extern\s+DECLSPEC", "", decl)
- yield decl.replace("SDL_PRINTF_FORMAT_STRING ", "")
-
-
-# Parsed headers excluding "SDL_stdinc.h"
-HEADERS = [
- "SDL_rect.h",
- "SDL_pixels.h",
- "SDL_blendmode.h",
- "SDL_error.h",
- "SDL_surface.h",
- "SDL_video.h",
- "SDL_render.h",
- "SDL_audio.h",
- "SDL_clipboard.h",
- "SDL_touch.h",
- "SDL_gesture.h",
- "SDL_hints.h",
- "SDL_joystick.h",
- "SDL_haptic.h",
- "SDL_power.h",
- "SDL_log.h",
- "SDL_messagebox.h",
- "SDL_mouse.h",
- "SDL_timer.h",
- "SDL_keycode.h",
- "SDL_scancode.h",
- "SDL_keyboard.h",
- "SDL_events.h",
- "SDL.h",
- "SDL_version.h",
-]
-
-
-def add_to_ffi(ffi: cffi.FFI, path: str) -> None:
- BITS, _ = platform.architecture()
- cdef_args: Dict[str, Any] = {}
- NEEDS_PACK4 = False
- if sys.platform == "win32" and BITS == "32bit":
- NEEDS_PACK4 = True
- # The following line is required but cffi does not currently support
- # it for ABI mode.
- # cdef_args["pack"] = 4
-
- ffi.cdef(
- "\n".join(RE_TYPEDEF.findall(get_header(os.path.join(path, "SDL_stdinc.h")))).replace("SDLCALL ", ""),
- **cdef_args,
- )
- for header in HEADERS:
- try:
- for code in parse(get_header(os.path.join(path, header)), NEEDS_PACK4):
- if "typedef struct SDL_AudioCVT" in code and sys.platform != "win32" and not NEEDS_PACK4:
- # This specific struct needs to be packed.
- ffi.cdef(code, packed=1)
- continue
- ffi.cdef(code, **cdef_args)
- except Exception:
- print("Error parsing %r code:\n%s" % (header, code))
- raise
-
-
-def get_ffi(path: str) -> cffi.FFI:
- """Return an ffi for SDL2, needs to be compiled."""
- ffi = cffi.FFI()
- add_to_ffi(ffi, path)
- return ffi
diff --git a/pyproject.toml b/pyproject.toml
index df1a4a46..4c1c47a0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,80 @@
[build-system]
-requires = ["setuptools>=57.0.0", "wheel", "cffi~=1.13", "pycparser>=2.14"]
+requires = [
+ # Newer versions of setuptools break editable installs
+ # https://github.com/pypa/setuptools/issues/3548
+ "setuptools >=61.0.0, <64.0.0",
+ "setuptools_scm[toml]>=6.2",
+ "wheel>=0.37.1",
+ "cffi>=1.15",
+ "pycparser>=2.14",
+ "pcpp==1.30",
+ "requests>=2.28.1",
+]
build-backend = "setuptools.build_meta"
+[project]
+name = "tcod"
+dynamic = ["version"]
+description = "The official Python port of libtcod."
+authors = [{ name = "Kyle Benesch", email = "4b796c65+tcod@gmail.com" }]
+readme = "README.rst"
+requires-python = ">=3.7"
+license = { text = "Simplified BSD License" }
+dependencies = [
+ "cffi>=1.15",
+ 'numpy>=1.21.4; implementation_name != "pypy"',
+ "typing_extensions",
+]
+keywords = [
+ "roguelike",
+ "cffi",
+ "Unicode",
+ "libtcod",
+ "field-of-view",
+ "pathfinding",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Win32 (MS Windows)",
+ "Environment :: MacOS X",
+ "Environment :: X11 Applications",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Natural Language :: English",
+ "Operating System :: POSIX",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Games/Entertainment",
+ "Topic :: Multimedia :: Graphics",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Typing :: Typed",
+]
+
+[project.entry-points.pyinstaller40]
+hook-dirs = "tcod.__pyinstaller:get_hook_dirs"
+
+[project.urls]
+Homepage = "https://github.com/libtcod/python-tcod"
+Documentation = "https://python-tcod.readthedocs.io"
+Changelog = "https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md"
+Source = "https://github.com/libtcod/python-tcod"
+Tracker = "https://github.com/libtcod/python-tcod/issues"
+Forum = "https://github.com/libtcod/python-tcod/discussions"
+
+[tool.distutils.bdist_wheel]
+py-limited-api = "cp37"
+
+[tool.setuptools_scm]
+write_to = "tcod/version.py"
+
[tool.black]
line-length = 120
target-version = ["py37"]
@@ -14,7 +87,7 @@ line_length = 120
[tool.pytest.ini_options]
minversion = "6.0"
-required_plugins = ["pytest-cov"]
+required_plugins = ["pytest-cov", "pytest-benchmark"]
testpaths = ["tcod/", "tests/", "docs/"]
addopts = [
"--doctest-modules",
@@ -29,4 +102,91 @@ filterwarnings = [
"ignore::DeprecationWarning:tcod.libtcodpy",
"ignore::PendingDeprecationWarning:tcod.libtcodpy",
"ignore:This class may perform poorly and is no longer needed.::tcod.map",
+ "ignore:'import tcod as libtcodpy' is preferred.",
]
+
+[tool.mypy]
+files = ["."]
+python_version = 3.8
+warn_unused_configs = true
+show_error_codes = true
+disallow_subclassing_any = true
+disallow_any_generics = true
+disallow_untyped_calls = true
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+check_untyped_defs = true
+disallow_untyped_decorators = true
+no_implicit_optional = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_return_any = true
+implicit_reexport = false
+strict_equality = true
+exclude = [
+ "build/",
+ "venv/",
+ "libtcod/",
+ "docs/",
+ "distribution/",
+ "termbox/",
+ "samples_libtcodpy.py",
+]
+
+[[tool.mypy.overrides]]
+module = "numpy.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "tcod.version"
+ignore_missing_imports = true
+
+
+[tool.ruff]
+# https://beta.ruff.rs/docs/rules/
+select = [
+ "C90", # mccabe
+ "E", # pycodestyle
+ "W", # pycodestyle
+ "F", # Pyflakes
+ "I", # isort
+ "UP", # pyupgrade
+ "YTT", # flake8-2020
+ "ANN", # flake8-annotations
+ "S", # flake8-bandit
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "DTZ", # flake8-datetimez
+ "EM", # flake8-errmsg
+ "EXE", # flake8-executable
+ "RET", # flake8-return
+ "ICN", # flake8-import-conventions
+ "PIE", # flake8-pie
+ "PT", # flake8-pytest-style
+ "SIM", # flake8-simplify
+ "PTH", # flake8-use-pathlib
+ "PL", # Pylint
+ "TRY", # tryceratops
+ "RUF", # NumPy-specific rules
+ "G", # flake8-logging-format
+ "D", # pydocstyle
+]
+ignore = [
+ "E501", # line-too-long
+ "S101", # assert
+ "S301", # suspicious-pickle-usage
+ "S311", # suspicious-non-cryptographic-random-usage
+ "ANN101", # missing-type-self
+ "ANN102", # missing-type-cls
+ "D203", # one-blank-line-before-class
+ "D204", # one-blank-line-after-class
+ "D213", # multi-line-summary-second-line
+ "D407", # dashed-underline-after-section
+ "D408", # section-underline-after-name
+ "D409", # section-underline-matches-section-length
+]
+line-length = 120
+
+[tool.ruff.pydocstyle]
+# Use Google-style docstrings.
+convention = "google"
diff --git a/requirements.txt b/requirements.txt
index 3ce930b0..ee1ea8a0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,11 @@
-cffi~=1.13
-numpy>=1.20.3
+cffi>=1.15
+numpy>=1.21.4
pycparser>=2.14
-setuptools>=36.0.1
+requests>=2.28.1
+setuptools==65.5.1
+types-cffi
+types-requests
types-setuptools
types-tabulate
typing_extensions
+pcpp==1.30
diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py
index d4bd0b77..0591cb9b 100755
--- a/scripts/generate_charmap_table.py
+++ b/scripts/generate_charmap_table.py
@@ -3,6 +3,8 @@
Uses the tabulate module from PyPI.
"""
+from __future__ import annotations
+
import argparse
import unicodedata
from typing import Iterable, Iterator
@@ -51,6 +53,7 @@ def generate_table(charmap: Iterable[int]) -> str:
def main() -> None:
+ """Main entry point."""
parser = argparse.ArgumentParser(
description="Generate an RST table for a tcod character map.",
)
@@ -71,8 +74,9 @@ def main() -> None:
)
args = parser.parse_args()
charmap = getattr(tcod.tileset, f"CHARMAP_{args.charmap.upper()}")
+ output = generate_table(charmap)
with args.out_file as f:
- f.write(generate_table(charmap))
+ f.write(output)
if __name__ == "__main__":
diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py
index 0cd97f66..7d47d9f9 100755
--- a/scripts/get_release_description.py
+++ b/scripts/get_release_description.py
@@ -1,22 +1,21 @@
#!/usr/bin/env python3
"""Print the description used for GitHub Releases."""
+from __future__ import annotations
+
import re
+from pathlib import Path
-TAG_BANNER = r"\d+\.\d+\.\d+\S* - \d+-\d+-\d+\n-+\n"
+TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n"
-RE_BODY = re.compile(fr".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL)
-RE_SECTION = re.compile(r"^(\w+)$", re.MULTILINE)
+RE_BODY = re.compile(rf".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL)
def main() -> None:
- # Get the most recent tag.
- with open("CHANGELOG.rst", "r", encoding="utf-8") as f:
- match = RE_BODY.match(f.read())
+ """Output the most recently tagged changelog body to stdout."""
+ match = RE_BODY.match(Path("CHANGELOG.md").read_text(encoding="utf-8"))
assert match
body = match.groups()[0].strip()
- # Add Markdown formatting to sections.
- body = RE_SECTION.sub(r"### \1", body)
print(body)
diff --git a/scripts/tag_release.py b/scripts/tag_release.py
old mode 100644
new mode 100755
index 68dd07f4..70b77fb7
--- a/scripts/tag_release.py
+++ b/scripts/tag_release.py
@@ -1,10 +1,18 @@
#!/usr/bin/env python3
+"""Automate tagged releases of this project."""
+from __future__ import annotations
+
import argparse
import datetime
+import os
import re
import subprocess
import sys
-from typing import Any, Tuple
+from pathlib import Path
+
+# ruff: noqa: S603, S607
+
+PROJECT_DIR = Path(__file__).parent.parent
parser = argparse.ArgumentParser(description="Tags and releases the next version of this project.")
@@ -17,42 +25,66 @@
parser.add_argument("-v", "--verbose", action="store_true", help="Print debug information.")
-def parse_changelog(args: Any) -> Tuple[str, str]:
+def parse_changelog(args: argparse.Namespace) -> tuple[str, str]:
"""Return an updated changelog and and the list of changes."""
- with open("CHANGELOG.rst", "r", encoding="utf-8") as file:
- match = re.match(
- pattern=r"(.*?Unreleased\n---+\n)(.+?)(\n*[^\n]+\n---+\n.*)",
- string=file.read(),
- flags=re.DOTALL,
- )
+ match = re.match(
+ pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)",
+ string=(PROJECT_DIR / "CHANGELOG.md").read_text(encoding="utf-8"),
+ flags=re.DOTALL,
+ )
assert match
header, changes, tail = match.groups()
- tag = "%s - %s" % (args.tag, datetime.date.today().isoformat())
-
- tagged = "\n%s\n%s\n%s" % (tag, "-" * len(tag), changes)
+ tagged = "\n## [{}] - {}\n{}".format(
+ args.tag,
+ datetime.date.today().isoformat(), # Local timezone is fine, probably. # noqa: DTZ011
+ changes,
+ )
if args.verbose:
+ print("--- Tagged section:")
print(tagged)
return "".join((header, tagged, tail)), changes
+def replace_unreleased_tags(tag: str, dry_run: bool) -> None:
+ """Walk though sources and replace pending tags with the new tag."""
+ match = re.match(r"\d+\.\d+", tag)
+ assert match
+ short_tag = match.group()
+ for directory, _, files in os.walk(PROJECT_DIR / "tcod"):
+ for filename in files:
+ file = Path(directory, filename)
+ if file.suffix != ".py":
+ continue
+ text = file.read_text(encoding="utf-8")
+ new_text = re.sub(r":: *unreleased", rf":: {short_tag}", text, flags=re.IGNORECASE)
+ if text != new_text:
+ print(f"Update tags in {file}")
+ if not dry_run:
+ file.write_text(new_text, encoding="utf-8")
+
+
def main() -> None:
- if len(sys.argv) == 1:
+ """Entry function."""
+ if len(sys.argv) <= 1:
parser.print_help(sys.stderr)
sys.exit(1)
args = parser.parse_args()
- if args.verbose:
- print(args)
new_changelog, changes = parse_changelog(args)
+ if args.verbose:
+ print("--- New changelog:")
+ print(new_changelog)
+
+ replace_unreleased_tags(args.tag, args.dry_run)
+
if not args.dry_run:
- with open("CHANGELOG.rst", "w", encoding="utf-8") as f:
- f.write(new_changelog)
+ (PROJECT_DIR / "CHANGELOG.md").write_text(new_changelog, encoding="utf-8")
edit = ["-e"] if args.edit else []
- subprocess.check_call(["git", "commit", "-avm", "Prepare %s release." % args.tag] + edit)
- subprocess.check_call(["git", "tag", args.tag, "-am", "%s\n\n%s" % (args.tag, changes)] + edit)
+ subprocess.check_call(["git", "commit", "-avm", f"Prepare {args.tag} release.", *edit])
+ subprocess.check_call(["git", "tag", args.tag, "-am", f"{args.tag}\n\n{changes}", *edit])
if __name__ == "__main__":
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 4b26be03..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,34 +0,0 @@
-[options.entry_points]
-pyinstaller40 =
- hook-dirs = tcod.__pyinstaller:get_hook_dirs
-
-[bdist_wheel]
-py-limited-api = cp36
-
-[aliases]
-test=pytest
-
-[flake8]
-ignore = E203 W503
-max-line-length = 120
-
-[mypy]
-python_version = 3.8
-warn_unused_configs = True
-disallow_subclassing_any = True
-disallow_any_generics = True
-disallow_untyped_calls = True
-disallow_untyped_defs = True
-disallow_incomplete_defs = True
-check_untyped_defs = True
-disallow_untyped_decorators = True
-no_implicit_optional = True
-warn_redundant_casts = True
-warn_unused_ignores = True
-warn_return_any = True
-implicit_reexport = False
-strict_equality = True
-exclude = (build/|venv/|libtcod/|docs/|distribution/|termbox/|samples_libtcodpy.py)
-
-[mypy-numpy]
-ignore_missing_imports = True
diff --git a/setup.py b/setup.py
index f01db6f2..dc2139e7 100755
--- a/setup.py
+++ b/setup.py
@@ -1,55 +1,22 @@
#!/usr/bin/env python3
+"""Python-tcod setup script."""
+from __future__ import annotations
-import os
-import pathlib
import platform
-import re
import subprocess
import sys
-import warnings
-from typing import List
+from pathlib import Path
from setuptools import setup
SDL_VERSION_NEEDED = (2, 0, 5)
-PATH = pathlib.Path(__file__).parent # setup.py current directory
+SETUP_DIR = Path(__file__).parent # setup.py current directory
-def get_version() -> str:
- """Get the current version from a git tag, or by reading tcod/version.py"""
- if (PATH / ".git").exists():
- tag = subprocess.check_output(["git", "describe", "--abbrev=0"], universal_newlines=True).strip()
- assert not tag.startswith("v")
- version = tag
-
- # add .devNN if needed
- log = subprocess.check_output(["git", "log", f"{tag}..HEAD", "--oneline"], universal_newlines=True)
- commits_since_tag = log.count("\n")
- if commits_since_tag:
- version += ".dev%i" % commits_since_tag
-
- # update tcod/version.py
- with open(PATH / "tcod/version.py", "w", encoding="utf-8") as version_file:
- version_file.write(f'__version__ = "{version}"\n')
- return version
- else: # Not a Git repository.
- try:
- with open(PATH / "tcod/version.py", encoding="utf-8") as version_file:
- match = re.match(r'__version__ = "(\S+)"', version_file.read())
- assert match
- return match.groups()[0]
- except FileNotFoundError:
- warnings.warn("Unknown version: Not in a Git repository and not from a sdist bundle or wheel.")
- return "0.0.0"
-
-
-is_pypy = platform.python_implementation() == "PyPy"
-
-
-def get_package_data() -> List[str]:
- """get data files which will be included in the main tcod/ directory"""
- BITSIZE, _ = platform.architecture()
+def get_package_data() -> list[str]:
+ """Get data files which will be included in the main tcod/ directory."""
+ BIT_SIZE, _ = platform.architecture()
files = [
"py.typed",
"lib/LIBTCOD-CREDITS.txt",
@@ -57,7 +24,7 @@ def get_package_data() -> List[str]:
"lib/README-SDL.txt",
]
if "win32" in sys.platform:
- if BITSIZE == "32bit":
+ if BIT_SIZE == "32bit":
files += ["x86/SDL2.dll"]
else:
files += ["x64/SDL2.dll"]
@@ -66,95 +33,37 @@ def get_package_data() -> List[str]:
return files
-def get_long_description() -> str:
- """Return this projects description."""
- with open(PATH / "README.rst", "r", encoding="utf-8") as readme_file:
- return readme_file.read()
-
-
def check_sdl_version() -> None:
"""Check the local SDL version on Linux distributions."""
if not sys.platform.startswith("linux"):
return
- needed_version = "%i.%i.%i" % SDL_VERSION_NEEDED
+ needed_version = "{}.{}.{}".format(*SDL_VERSION_NEEDED)
try:
sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip()
- except FileNotFoundError:
- raise RuntimeError(
- "libsdl2-dev or equivalent must be installed on your system"
- " and must be at least version %s."
- "\nsdl2-config must be on PATH." % (needed_version,)
+ except FileNotFoundError as exc:
+ msg = (
+ f"libsdl2-dev or equivalent must be installed on your system and must be at least version {needed_version}."
+ "\nsdl2-config must be on PATH."
)
- print("Found SDL %s." % (sdl_version_str,))
+ raise RuntimeError(msg) from exc
+ print(f"Found SDL {sdl_version_str}.")
sdl_version = tuple(int(s) for s in sdl_version_str.split("."))
if sdl_version < SDL_VERSION_NEEDED:
- raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str))
+ msg = f"SDL version must be at least {needed_version}, (found {sdl_version_str})"
+ raise RuntimeError(msg)
-if not os.path.exists(PATH / "libtcod/src"):
+if not (SETUP_DIR / "libtcod/src").exists():
print("Libtcod submodule is uninitialized.")
print("Did you forget to run 'git submodule update --init'?")
sys.exit(1)
check_sdl_version()
-needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv)
-pytest_runner = ["pytest-runner"] if needs_pytest else []
-
setup(
- name="tcod",
- version=get_version(),
- author="Kyle Benesch",
- author_email="4b796c65+tcod@gmail.com",
- description="The official Python port of libtcod.",
- long_description=get_long_description(),
- url="https://github.com/libtcod/python-tcod",
- project_urls={
- "Documentation": "https://python-tcod.readthedocs.io",
- "Changelog": "https://github.com/libtcod/python-tcod/blob/develop/CHANGELOG.rst",
- "Source": "https://github.com/libtcod/python-tcod",
- "Tracker": "https://github.com/libtcod/python-tcod/issues",
- "Forum": "https://github.com/libtcod/python-tcod/discussions",
- },
py_modules=["libtcodpy"],
- packages=["tcod", "tcod.__pyinstaller"],
+ packages=["tcod", "tcod.sdl", "tcod.__pyinstaller"],
package_data={"tcod": get_package_data()},
- python_requires=">=3.7",
- setup_requires=[
- *pytest_runner,
- "cffi~=1.13",
- "pycparser>=2.14",
- ],
- install_requires=[
- "cffi~=1.13", # Also required by pyproject.toml.
- "numpy>=1.20.3" if not is_pypy else "",
- "typing_extensions",
- ],
cffi_modules=["build_libtcod.py:ffi"],
- tests_require=["pytest", "pytest-cov", "pytest-benchmark"],
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Environment :: Win32 (MS Windows)",
- "Environment :: MacOS X",
- "Environment :: X11 Applications",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: BSD License",
- "Natural Language :: English",
- "Operating System :: POSIX",
- "Operating System :: MacOS :: MacOS X",
- "Operating System :: Microsoft :: Windows",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Topic :: Games/Entertainment",
- "Topic :: Multimedia :: Graphics",
- "Topic :: Software Development :: Libraries :: Python Modules",
- ],
- keywords="roguelike cffi Unicode libtcod field-of-view pathfinding",
platforms=["Windows", "MacOS", "Linux"],
- license="Simplified BSD License",
)
diff --git a/tcod/__init__.py b/tcod/__init__.py
index 98c489b1..0082d521 100644
--- a/tcod/__init__.py
+++ b/tcod/__init__.py
@@ -8,29 +8,40 @@
"""
from __future__ import annotations
-import sys
import warnings
+from pkgutil import extend_path
-from tcod import bsp, color, console, context, event, image, los, map, noise, path, random, tileset
-from tcod.console import Console # noqa: F401
-from tcod.constants import * # noqa: F4
-from tcod.libtcodpy import * # noqa: F4
-from tcod.loader import __sdl_version__, ffi, lib # noqa: F4
+__path__ = extend_path(__path__, __name__)
+
+from tcod import bsp, color, console, constants, context, event, image, los, map, noise, path, random, tileset
+from tcod.console import Console
+from tcod.constants import * # noqa: F403
+from tcod.libtcodpy import * # noqa: F403
+from tcod.loader import __sdl_version__, ffi, lib
try:
from tcod.version import __version__
except ImportError: # Gets imported without version.py by ReadTheDocs
__version__ = ""
-if sys.version_info < (3, 6):
+
+def __getattr__(name: str, stacklevel: int = 1) -> color.Color:
+ """Mark access to color constants as deprecated."""
+ value: color.Color | None = getattr(constants, name, None)
+ if value is None:
+ msg = f"module {__name__!r} has no attribute {name!r}"
+ raise AttributeError(msg)
warnings.warn(
- "Support for Python 3.5 has been dropped from python-tcod.",
- DeprecationWarning,
- stacklevel=2,
+ f"Color constants will be removed from future releases.\nReplace `tcod.{name}` with `{tuple(value)}`.",
+ FutureWarning,
+ stacklevel=stacklevel + 1,
)
+ return value
+
__all__ = [ # noqa: F405
"__version__",
+ "__sdl_version__",
"lib",
"ffi",
"bsp",
@@ -556,6 +567,7 @@
"RENDERER_OPENGL2",
"RENDERER_SDL",
"RENDERER_SDL2",
+ "RENDERER_XTERM",
"RIGHT",
"RNG_CMWC",
"RNG_MT",
@@ -600,202 +612,5 @@
"TYPE_VALUELIST13",
"TYPE_VALUELIST14",
"TYPE_VALUELIST15",
- "amber",
- "azure",
- "black",
- "blue",
- "brass",
- "celadon",
- "chartreuse",
- "copper",
- "crimson",
- "cyan",
- "dark_amber",
- "dark_azure",
- "dark_blue",
- "dark_chartreuse",
- "dark_crimson",
- "dark_cyan",
- "dark_flame",
- "dark_fuchsia",
- "dark_gray",
- "dark_green",
- "dark_grey",
- "dark_han",
- "dark_lime",
- "dark_magenta",
- "dark_orange",
- "dark_pink",
- "dark_purple",
- "dark_red",
- "dark_sea",
- "dark_sepia",
- "dark_sky",
- "dark_turquoise",
- "dark_violet",
- "dark_yellow",
- "darker_amber",
- "darker_azure",
- "darker_blue",
- "darker_chartreuse",
- "darker_crimson",
- "darker_cyan",
- "darker_flame",
- "darker_fuchsia",
- "darker_gray",
- "darker_green",
- "darker_grey",
- "darker_han",
- "darker_lime",
- "darker_magenta",
- "darker_orange",
- "darker_pink",
- "darker_purple",
- "darker_red",
- "darker_sea",
- "darker_sepia",
- "darker_sky",
- "darker_turquoise",
- "darker_violet",
- "darker_yellow",
- "darkest_amber",
- "darkest_azure",
- "darkest_blue",
- "darkest_chartreuse",
- "darkest_crimson",
- "darkest_cyan",
- "darkest_flame",
- "darkest_fuchsia",
- "darkest_gray",
- "darkest_green",
- "darkest_grey",
- "darkest_han",
- "darkest_lime",
- "darkest_magenta",
- "darkest_orange",
- "darkest_pink",
- "darkest_purple",
- "darkest_red",
- "darkest_sea",
- "darkest_sepia",
- "darkest_sky",
- "darkest_turquoise",
- "darkest_violet",
- "darkest_yellow",
- "desaturated_amber",
- "desaturated_azure",
- "desaturated_blue",
- "desaturated_chartreuse",
- "desaturated_crimson",
- "desaturated_cyan",
- "desaturated_flame",
- "desaturated_fuchsia",
- "desaturated_green",
- "desaturated_han",
- "desaturated_lime",
- "desaturated_magenta",
- "desaturated_orange",
- "desaturated_pink",
- "desaturated_purple",
- "desaturated_red",
- "desaturated_sea",
- "desaturated_sky",
- "desaturated_turquoise",
- "desaturated_violet",
- "desaturated_yellow",
- "flame",
- "fuchsia",
- "gold",
- "gray",
- "green",
- "grey",
- "han",
- "light_amber",
- "light_azure",
- "light_blue",
- "light_chartreuse",
- "light_crimson",
- "light_cyan",
- "light_flame",
- "light_fuchsia",
- "light_gray",
- "light_green",
- "light_grey",
- "light_han",
- "light_lime",
- "light_magenta",
- "light_orange",
- "light_pink",
- "light_purple",
- "light_red",
- "light_sea",
- "light_sepia",
- "light_sky",
- "light_turquoise",
- "light_violet",
- "light_yellow",
- "lighter_amber",
- "lighter_azure",
- "lighter_blue",
- "lighter_chartreuse",
- "lighter_crimson",
- "lighter_cyan",
- "lighter_flame",
- "lighter_fuchsia",
- "lighter_gray",
- "lighter_green",
- "lighter_grey",
- "lighter_han",
- "lighter_lime",
- "lighter_magenta",
- "lighter_orange",
- "lighter_pink",
- "lighter_purple",
- "lighter_red",
- "lighter_sea",
- "lighter_sepia",
- "lighter_sky",
- "lighter_turquoise",
- "lighter_violet",
- "lighter_yellow",
- "lightest_amber",
- "lightest_azure",
- "lightest_blue",
- "lightest_chartreuse",
- "lightest_crimson",
- "lightest_cyan",
- "lightest_flame",
- "lightest_fuchsia",
- "lightest_gray",
- "lightest_green",
- "lightest_grey",
- "lightest_han",
- "lightest_lime",
- "lightest_magenta",
- "lightest_orange",
- "lightest_pink",
- "lightest_purple",
- "lightest_red",
- "lightest_sea",
- "lightest_sepia",
- "lightest_sky",
- "lightest_turquoise",
- "lightest_violet",
- "lightest_yellow",
- "lime",
- "magenta",
- "orange",
- "peach",
- "pink",
- "purple",
- "red",
- "sea",
- "sepia",
- "silver",
- "sky",
- "turquoise",
- "violet",
- "white",
- "yellow",
# --- End constants.py ---
]
diff --git a/tcod/__pyinstaller/__init__.py b/tcod/__pyinstaller/__init__.py
index a2a36e88..7224bb10 100644
--- a/tcod/__pyinstaller/__init__.py
+++ b/tcod/__pyinstaller/__init__.py
@@ -1,8 +1,9 @@
"""PyInstaller entry point for tcod."""
-import os
-from typing import List
+from __future__ import annotations
+from pathlib import Path
-def get_hook_dirs() -> List[str]:
+
+def get_hook_dirs() -> list[str]:
"""Return the current directory."""
- return [os.path.dirname(__file__)]
+ return [str(Path(__file__).parent)]
diff --git a/tcod/__pyinstaller/hook-tcod.py b/tcod/__pyinstaller/hook-tcod.py
index 2df121e6..9790d583 100644
--- a/tcod/__pyinstaller/hook-tcod.py
+++ b/tcod/__pyinstaller/hook-tcod.py
@@ -1,8 +1,8 @@
"""PyInstaller hook for tcod.
-Added here after tcod 12.0.0.
+There were added since tcod 12.0.0.
-If this hook is modified then the contributed hook needs to be removed from:
+If this hook is ever modified then the contributed hook needs to be removed from:
https://github.com/pyinstaller/pyinstaller-hooks-contrib
"""
from PyInstaller.utils.hooks import collect_dynamic_libs # type: ignore
diff --git a/tcod/_internal.py b/tcod/_internal.py
index e7dc033b..a9d2ba35 100644
--- a/tcod/_internal.py
+++ b/tcod/_internal.py
@@ -1,21 +1,23 @@
-"""This module internal helper functions used by the rest of the library.
-"""
+"""Internal helper functions used by the rest of the library."""
from __future__ import annotations
import functools
import warnings
-from typing import Any, AnyStr, Callable, TypeVar, cast
+from types import TracebackType
+from typing import Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast
import numpy as np
-from typing_extensions import Literal, NoReturn
+from numpy.typing import ArrayLike, NDArray
+from typing_extensions import Literal
from tcod.loader import ffi, lib
FuncType = Callable[..., Any]
F = TypeVar("F", bound=FuncType)
+T = TypeVar("T")
-def deprecate(message: str, category: Any = DeprecationWarning, stacklevel: int = 0) -> Callable[[F], F]:
+def deprecate(message: str, category: type[Warning] = DeprecationWarning, stacklevel: int = 0) -> Callable[[F], F]:
"""Return a decorator which adds a warning to functions."""
def decorator(func: F) -> F:
@@ -23,7 +25,7 @@ def decorator(func: F) -> F:
return func
@functools.wraps(func)
- def wrapper(*args, **kwargs): # type: ignore
+ def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
warnings.warn(message, category, stacklevel=stacklevel + 2)
return func(*args, **kwargs)
@@ -35,18 +37,19 @@ def wrapper(*args, **kwargs): # type: ignore
def pending_deprecate(
message: str = "This function may be deprecated in the future."
" Consider raising an issue on GitHub if you need this feature.",
- category: Any = PendingDeprecationWarning,
+ category: type[Warning] = PendingDeprecationWarning,
stacklevel: int = 0,
) -> Callable[[F], F]:
- """Like deprecate, but the default parameters are filled out for a generic
- pending deprecation warning."""
+ """Like deprecate, but the default parameters are filled out for a generic pending deprecation warning."""
return deprecate(message, category, stacklevel)
def verify_order(order: Literal["C", "F"]) -> Literal["C", "F"]:
+ """Verify and return a Numpy order string."""
order = order.upper() # type: ignore
if order not in ("C", "F"):
- raise TypeError("order must be 'C' or 'F', not %r" % (order,))
+ msg = f"order must be 'C' or 'F', not {order!r}"
+ raise TypeError(msg)
return order
@@ -62,7 +65,7 @@ def _check(error: int) -> int:
return error
-def _check_p(pointer: Any) -> Any:
+def _check_p(pointer: T) -> T:
"""Treats NULL pointers as errors and raises a libtcod exception."""
if not pointer:
_raise_tcod_error()
@@ -80,19 +83,19 @@ def _check_warn(error: int, stacklevel: int = 2) -> int:
return error
-def _unpack_char_p(char_p: Any) -> str:
+def _unpack_char_p(char_p: Any) -> str: # noqa: ANN401
if char_p == ffi.NULL:
return ""
return ffi.string(char_p).decode() # type: ignore
-def _int(int_or_str: Any) -> int:
- "return an integer where a single character string may be expected"
+def _int(int_or_str: SupportsInt | str | bytes) -> int:
+ """Return an integer where a single character string may be expected."""
if isinstance(int_or_str, str):
return ord(int_or_str)
if isinstance(int_or_str, bytes):
return int_or_str[0]
- return int(int_or_str) # check for __count__
+ return int(int_or_str)
def _bytes(string: AnyStr) -> bytes:
@@ -104,8 +107,8 @@ def _bytes(string: AnyStr) -> bytes:
def _unicode(string: AnyStr, stacklevel: int = 2) -> str:
if isinstance(string, bytes):
warnings.warn(
- ("Passing byte strings as parameters to Unicode functions is " "deprecated."),
- DeprecationWarning,
+ "Passing byte strings as parameters to Unicode functions is deprecated.",
+ FutureWarning,
stacklevel=stacklevel + 1,
)
return string.decode("latin-1")
@@ -115,8 +118,8 @@ def _unicode(string: AnyStr, stacklevel: int = 2) -> str:
def _fmt(string: str, stacklevel: int = 2) -> bytes:
if isinstance(string, bytes):
warnings.warn(
- ("Passing byte strings as parameters to Unicode functions is " "deprecated."),
- DeprecationWarning,
+ "Passing byte strings as parameters to Unicode functions is deprecated.",
+ FutureWarning,
stacklevel=stacklevel + 1,
)
string = string.decode("latin-1")
@@ -124,8 +127,9 @@ def _fmt(string: str, stacklevel: int = 2) -> bytes:
class _PropagateException:
- """Context manager designed to propagate exceptions outside of a cffi
- callback context. Normally cffi suppresses the exception.
+ """Context manager designed to propagate exceptions outside of a cffi callback context.
+
+ Normally cffi suppresses the exception.
When propagate is called this class will hold onto the error until the
control flow leaves the context, then the error will be raised.
@@ -135,53 +139,46 @@ class _PropagateException:
"""
def __init__(self) -> None:
- # (exception, exc_value, traceback)
- self.exc_info = None # type: Any
+ self.caught: BaseException | None = None
- def propagate(self, *exc_info: Any) -> None:
+ def propagate(self, *exc_info: Any) -> None: # noqa: ANN401
"""Set an exception to be raised once this context exits.
If multiple errors are caught, only keep the first exception raised.
"""
- if not self.exc_info:
- self.exc_info = exc_info
+ if self.caught is None:
+ self.caught = exc_info[1]
def __enter__(self) -> Callable[[Any], None]:
- """Once in context, only the propagate call is needed to use this
- class effectively.
- """
+ """Once in context, only the propagate call is needed to use this class effectively."""
return self.propagate
- def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
+ def __exit__(
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
+ ) -> None:
"""If we're holding on to an exception, raise it now.
- Prefers our held exception over any current raising error.
-
- self.exc_info is reset now in case of nested manager shenanigans.
+ self.caught is reset now in case of nested manager shenanigans.
"""
- if self.exc_info:
- type, value, traceback = self.exc_info
- self.exc_info = None
- if type:
- # Python 2/3 compatible throw
- exception = type(value)
- exception.__traceback__ = traceback
- raise exception
-
-
-class _CDataWrapper(object):
- def __init__(self, *args: Any, **kwargs: Any):
+ to_raise, self.caught = self.caught, None
+ if to_raise is not None:
+ raise to_raise from value
+
+
+class _CDataWrapper:
+ """A generally deprecated CData wrapper class used by libtcodpy."""
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
self.cdata = self._get_cdata_from_args(*args, **kwargs)
if self.cdata is None:
self.cdata = ffi.NULL
- super(_CDataWrapper, self).__init__()
+ super().__init__()
@staticmethod
- def _get_cdata_from_args(*args: Any, **kwargs: Any) -> Any:
+ def _get_cdata_from_args(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
if len(args) == 1 and isinstance(args[0], ffi.CData) and not kwargs:
return args[0]
- else:
- return None
+ return None
def __hash__(self) -> int:
return hash(self.cdata)
@@ -201,11 +198,11 @@ def __setattr__(self, attr: str, value: Any) -> None:
if hasattr(self, "cdata") and hasattr(self.cdata, attr):
setattr(self.cdata, attr, value)
else:
- super(_CDataWrapper, self).__setattr__(attr, value)
+ super().__setattr__(attr, value)
-def _console(console: Any) -> Any:
- """Return a cffi console."""
+def _console(console: Any) -> Any: # noqa: ANN401
+ """Return a cffi console pointer."""
try:
return console.console_c
except AttributeError:
@@ -221,14 +218,16 @@ def _console(console: Any) -> Any:
return ffi.NULL
-class TempImage(object):
+class TempImage:
"""An Image-like container for NumPy arrays."""
- def __init__(self, array: Any):
- self._array = np.ascontiguousarray(array, dtype=np.uint8)
+ def __init__(self, array: ArrayLike) -> None:
+ """Initialize an image from the given array. May copy or reference the array."""
+ self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8)
height, width, depth = self._array.shape
if depth != 3:
- raise TypeError("Array must have RGB channels. Shape is: %r" % (self._array.shape,))
+ msg = f"Array must have RGB channels. Shape is: {self._array.shape!r}"
+ raise TypeError(msg)
self._buffer = ffi.from_buffer("TCOD_color_t[]", self._array)
self._mipmaps = ffi.new(
"struct TCOD_mipmap_*",
diff --git a/tcod/bsp.py b/tcod/bsp.py
index 42f82460..8734c340 100644
--- a/tcod/bsp.py
+++ b/tcod/bsp.py
@@ -1,10 +1,11 @@
-"""
+r"""Libtcod's Binary Space Partitioning.
+
The following example shows how to traverse the BSP tree using Python. This
assumes `create_room` and `connect_rooms` will be replaced by custom code.
Example::
- import tcod
+ import tcod.bsp
bsp = tcod.bsp.BSP(x=0, y=0, width=80, height=60)
bsp.split_recursive(
@@ -19,22 +20,21 @@
for node in bsp.pre_order():
if node.children:
node1, node2 = node.children
- print('Connect the rooms:\\n%s\\n%s' % (node1, node2))
+ print('Connect the rooms:\n%s\n%s' % (node1, node2))
else:
print('Dig a room for %s.' % node)
"""
from __future__ import annotations
-from typing import Any, Iterator, List, Optional, Tuple, Union # noqa: F401
+from typing import Any, Iterator
import tcod.random
from tcod._internal import deprecate
from tcod.loader import ffi, lib
-class BSP(object):
- """A binary space partitioning tree which can be used for simple dungeon
- generation.
+class BSP:
+ """A binary space partitioning tree which can be used for simple dungeon generation.
Attributes:
x (int): Rectangle left coordinate.
@@ -56,7 +56,8 @@ class BSP(object):
height (int): Rectangle height.
"""
- def __init__(self, x: int, y: int, width: int, height: int):
+ def __init__(self, x: int, y: int, width: int, height: int) -> None:
+ """Initialize a root node of a BSP tree."""
self.x = x
self.y = y
self.width = width
@@ -66,11 +67,12 @@ def __init__(self, x: int, y: int, width: int, height: int):
self.position = 0
self.horizontal = False
- self.parent: Optional[BSP] = None
- self.children: Union[Tuple[()], Tuple[BSP, BSP]] = ()
+ self.parent: BSP | None = None
+ self.children: tuple[()] | tuple[BSP, BSP] = ()
@property
- def w(self) -> int:
+ @deprecate("This attribute has been renamed to `width`.", FutureWarning)
+ def w(self) -> int: # noqa: D102
return self.width
@w.setter
@@ -78,7 +80,8 @@ def w(self, value: int) -> None:
self.width = value
@property
- def h(self) -> int:
+ @deprecate("This attribute has been renamed to `height`.", FutureWarning)
+ def h(self) -> int: # noqa: D102
return self.height
@h.setter
@@ -93,7 +96,7 @@ def _as_cdata(self) -> Any:
cdata.level = self.level
return cdata
- def __str__(self) -> str:
+ def __repr__(self) -> str:
"""Provide a useful readout when printed."""
status = "leaf"
if self.children:
@@ -102,7 +105,7 @@ def __str__(self) -> str:
self.horizontal,
)
- return "<%s(x=%i,y=%i,width=%i,height=%i)level=%i,%s>" % (
+ return "<%s(x=%i,y=%i,width=%i,height=%i) level=%i %s>" % (
self.__class__.__name__,
self.x,
self.y,
@@ -132,21 +135,21 @@ def split_once(self, horizontal: bool, position: int) -> None:
"""Split this partition into 2 sub-partitions.
Args:
- horizontal (bool):
- position (int):
+ horizontal (bool): If True then the sub-partition is split into an upper and bottom half.
+ position (int): The position of where to put the divider relative to the current node.
"""
cdata = self._as_cdata()
lib.TCOD_bsp_split_once(cdata, horizontal, position)
self._unpack_bsp_tree(cdata)
- def split_recursive(
+ def split_recursive( # noqa: PLR0913
self,
depth: int,
min_width: int,
min_height: int,
max_horizontal_ratio: float,
max_vertical_ratio: float,
- seed: Optional[tcod.random.Random] = None,
+ seed: tcod.random.Random | None = None,
) -> None:
"""Divide this partition recursively.
@@ -230,7 +233,7 @@ def inverted_level_order(self) -> Iterator[BSP]:
.. versionadded:: 8.3
"""
- levels: List[List[BSP]] = []
+ levels: list[list[BSP]] = []
next = [self]
while next:
levels.append(next)
@@ -242,7 +245,7 @@ def inverted_level_order(self) -> Iterator[BSP]:
yield from levels.pop()
def contains(self, x: int, y: int) -> bool:
- """Returns True if this node contains these coordinates.
+ """Return True if this node contains these coordinates.
Args:
x (int): X position to check.
@@ -254,16 +257,16 @@ def contains(self, x: int, y: int) -> bool:
"""
return self.x <= x < self.x + self.width and self.y <= y < self.y + self.height
- def find_node(self, x: int, y: int) -> Optional[BSP]:
+ def find_node(self, x: int, y: int) -> BSP | None:
"""Return the deepest node which contains these coordinates.
Returns:
- Optional[BSP]: BSP object or None.
+ BSP object or None.
"""
if not self.contains(x, y):
return None
for child in self.children:
- found: Optional[BSP] = child.find_node(x, y)
+ found = child.find_node(x, y)
if found:
return found
return self
diff --git a/tcod/cdef.h b/tcod/cdef.h
index 70ed74f9..aecccc9c 100644
--- a/tcod/cdef.h
+++ b/tcod/cdef.h
@@ -16,7 +16,8 @@ float _pycall_path_dest_only(int x1, int y1, int x2, int y2, void* user_data);
void _pycall_sdl_hook(struct SDL_Surface*);
-int _pycall_event_watch(void* userdata, union SDL_Event* event);
-
void _pycall_cli_output(void* userdata, const char* output);
+
+// Libtcod log watch function.
+void _libtcod_log_watcher(const TCOD_LogMessage* message, void* userdata);
}
diff --git a/tcod/cffi.h b/tcod/cffi.h
index a1f3ce52..fc373dac 100644
--- a/tcod/cffi.h
+++ b/tcod/cffi.h
@@ -2,6 +2,7 @@
Anything included here will be accessible from tcod.loader.lib */
#include "../libtcod/src/libtcod/libtcod.h"
#include "../libtcod/src/libtcod/libtcod_int.h"
+#include "../libtcod/src/libtcod/renderer_xterm.h"
#include "../libtcod/src/libtcod/tileset_truetype.h"
#include "../libtcod/src/libtcod/wrappers.h"
#include "noise.h"
diff --git a/tcod/color.py b/tcod/color.py
index 43fe0aab..f5fed4c2 100644
--- a/tcod/color.py
+++ b/tcod/color.py
@@ -1,6 +1,4 @@
-"""
-
-"""
+"""Old libtcod color management."""
from __future__ import annotations
import warnings
@@ -11,7 +9,7 @@
class Color(List[int]):
- """
+ """Old-style libtcodpy color class.
Args:
r (int): Red value, from 0 to 255.
@@ -31,8 +29,8 @@ def r(self) -> int:
"""
return int(self[0])
- @r.setter # type: ignore
- @deprecate("Setting color attributes has been deprecated.")
+ @r.setter
+ @deprecate("Setting color attributes has been deprecated.", FutureWarning)
def r(self, value: int) -> None:
self[0] = value & 0xFF
@@ -45,8 +43,8 @@ def g(self) -> int:
"""
return int(self[1])
- @g.setter # type: ignore
- @deprecate("Setting color attributes has been deprecated.")
+ @g.setter
+ @deprecate("Setting color attributes has been deprecated.", FutureWarning)
def g(self, value: int) -> None:
self[1] = value & 0xFF
@@ -59,37 +57,36 @@ def b(self) -> int:
"""
return int(self[2])
- @b.setter # type: ignore
- @deprecate("Setting color attributes has been deprecated.")
+ @b.setter
+ @deprecate("Setting color attributes has been deprecated.", FutureWarning)
def b(self, value: int) -> None:
self[2] = value & 0xFF
@classmethod
- def _new_from_cdata(cls, cdata: Any) -> Color:
- """new in libtcod-cffi"""
+ def _new_from_cdata(cls, cdata: Any) -> Color: # noqa: ANN401
return cls(cdata.r, cdata.g, cdata.b)
- def __getitem__(self, index: Any) -> Any:
- """
+ def __getitem__(self, index: Any) -> Any: # noqa: ANN401
+ """Return a color channel.
+
.. deprecated:: 9.2
Accessing colors via a letter index is deprecated.
"""
- try:
- return super().__getitem__(index)
- except TypeError:
+ if isinstance(index, str):
warnings.warn(
"Accessing colors via a letter index is deprecated",
DeprecationWarning,
stacklevel=2,
)
return super().__getitem__("rgb".index(index))
+ return super().__getitem__(index)
- @deprecate("This class will not be mutable in the future.")
- def __setitem__(self, index: Any, value: Any) -> None:
- try:
- super().__setitem__(index, value)
- except TypeError:
+ @deprecate("This class will not be mutable in the future.", FutureWarning)
+ def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401
+ if isinstance(index, str):
super().__setitem__("rgb".index(index), value)
+ else:
+ super().__setitem__(index, value)
def __eq__(self, other: Any) -> bool:
"""Compare equality between colors.
@@ -101,8 +98,8 @@ def __eq__(self, other: Any) -> bool:
except TypeError:
return False
- @deprecate("Use NumPy instead for color math operations.")
- def __add__(self, other: Any) -> Color:
+ @deprecate("Use NumPy instead for color math operations.", FutureWarning)
+ def __add__(self, other: Any) -> Color: # type: ignore[override]
"""Add two colors together.
.. deprecated:: 9.2
@@ -110,7 +107,7 @@ def __add__(self, other: Any) -> Color:
"""
return Color._new_from_cdata(lib.TCOD_color_add(self, other))
- @deprecate("Use NumPy instead for color math operations.")
+ @deprecate("Use NumPy instead for color math operations.", FutureWarning)
def __sub__(self, other: Any) -> Color:
"""Subtract one color from another.
@@ -119,7 +116,7 @@ def __sub__(self, other: Any) -> Color:
"""
return Color._new_from_cdata(lib.TCOD_color_subtract(self, other))
- @deprecate("Use NumPy instead for color math operations.")
+ @deprecate("Use NumPy instead for color math operations.", FutureWarning)
def __mul__(self, other: Any) -> Color:
"""Multiply with a scaler or another color.
@@ -128,14 +125,8 @@ def __mul__(self, other: Any) -> Color:
"""
if isinstance(other, (Color, list, tuple)):
return Color._new_from_cdata(lib.TCOD_color_multiply(self, other))
- else:
- return Color._new_from_cdata(lib.TCOD_color_multiply_scalar(self, other))
+ return Color._new_from_cdata(lib.TCOD_color_multiply_scalar(self, other))
def __repr__(self) -> str:
"""Return a printable representation of the current color."""
- return "%s(%r, %r, %r)" % (
- self.__class__.__name__,
- self.r,
- self.g,
- self.b,
- )
+ return f"{self.__class__.__name__}({self.r!r}, {self.g!r}, {self.b!r})"
diff --git a/tcod/console.py b/tcod/console.py
index b6e6862a..f5147e23 100644
--- a/tcod/console.py
+++ b/tcod/console.py
@@ -1,14 +1,13 @@
-"""
-Libtcod consoles are a strictly tile-based representation of text and color.
+"""Libtcod consoles are a strictly tile-based representation of text and color.
To render a console you need a tileset and a window to render to.
See :ref:`getting-started` for info on how to set those up.
"""
from __future__ import annotations
-import os
import warnings
+from os import PathLike
from pathlib import Path
-from typing import Any, Iterable, Optional, Sequence, Tuple, Union
+from typing import Any, Iterable
import numpy as np
from numpy.typing import NDArray
@@ -45,8 +44,7 @@ def _fmt(string: str) -> bytes:
class Console:
- """A console object containing a grid of characters with
- foreground/background colors.
+ """A console object containing a grid of characters with foreground/background colors.
`width` and `height` are the size of the console (in tiles.)
@@ -117,14 +115,14 @@ def __init__(
width: int,
height: int,
order: Literal["C", "F"] = "C",
- buffer: Optional[NDArray[Any]] = None,
- ):
- self._key_color: Optional[Tuple[int, int, int]] = None
+ buffer: NDArray[Any] | None = None,
+ ) -> None:
+ self._key_color: tuple[int, int, int] | None = None
self._order = tcod._internal.verify_order(order)
if buffer is not None:
if self._order == "F":
buffer = buffer.transpose()
- self._tiles = np.ascontiguousarray(buffer, self.DTYPE)
+ self._tiles: NDArray[Any] = np.ascontiguousarray(buffer, self.DTYPE)
else:
self._tiles = np.ndarray((height, width), dtype=self.DTYPE)
@@ -153,7 +151,7 @@ def __init__(
self.clear()
@classmethod
- def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console:
+ def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console: # noqa: ANN401
"""Return a Console instance which wraps this `TCOD_Console*` object."""
if isinstance(cdata, cls):
return cdata
@@ -163,7 +161,7 @@ def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console:
return self
@classmethod
- def _get_root(cls, order: Optional[Literal["C", "F"]] = None) -> Console:
+ def _get_root(cls, order: Literal["C", "F"] | None = None) -> Console:
"""Return a root console singleton with valid buffers.
This function will also update an already active root console.
@@ -197,12 +195,12 @@ def _init_setup_console_data(self, order: Literal["C", "F"] = "C") -> None:
@property
def width(self) -> int:
- """int: The width of this Console. (read-only)"""
+ """The width of this Console."""
return lib.TCOD_console_get_width(self.console_c) # type: ignore
@property
def height(self) -> int:
- """int: The height of this Console. (read-only)"""
+ """The height of this Console."""
return lib.TCOD_console_get_height(self.console_c) # type: ignore
@property
@@ -243,9 +241,9 @@ def ch(self) -> NDArray[np.intc]:
Index this array with ``console.ch[i, j] # order='C'`` or
``console.ch[x, y] # order='F'``.
"""
- return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"] # type: ignore
+ return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"]
- @property # type: ignore
+ @property
@deprecate("This attribute has been renamed to `rgba`.")
def tiles(self) -> NDArray[Any]:
"""An array of this consoles raw tile data.
@@ -261,7 +259,7 @@ def tiles(self) -> NDArray[Any]:
"""
return self.rgba
- @property # type: ignore
+ @property
@deprecate("This attribute has been renamed to `rgba`.")
def buffer(self) -> NDArray[Any]:
"""An array of this consoles raw tile data.
@@ -273,7 +271,7 @@ def buffer(self) -> NDArray[Any]:
"""
return self.rgba
- @property # type: ignore
+ @property
@deprecate("This attribute has been renamed to `rgb`.")
def tiles_rgb(self) -> NDArray[Any]:
"""An array of this consoles data without the alpha channel.
@@ -285,7 +283,7 @@ def tiles_rgb(self) -> NDArray[Any]:
"""
return self.rgb
- @property # type: ignore
+ @property
@deprecate("This attribute has been renamed to `rgb`.")
def tiles2(self) -> NDArray[Any]:
"""This name is deprecated in favour of :any:`rgb`.
@@ -306,10 +304,11 @@ def rgba(self) -> NDArray[Any]:
Example:
>>> con = tcod.console.Console(10, 2)
+ >>> WHITE, BLACK = (255, 255, 255), (0, 0, 0)
>>> con.rgba[0, 0] = (
... ord("X"),
- ... (*tcod.white, 255),
- ... (*tcod.black, 255),
+ ... (*WHITE, 255),
+ ... (*BLACK, 255),
... )
>>> con.rgba[0, 0]
(88, [255, 255, 255, 255], [ 0, 0, 0, 255])
@@ -330,10 +329,11 @@ def rgb(self) -> NDArray[Any]:
Example:
>>> con = tcod.console.Console(10, 2)
- >>> con.rgb[0, 0] = ord("@"), tcod.yellow, tcod.black
+ >>> BLUE, YELLOW, BLACK = (0, 0, 255), (255, 255, 0), (0, 0, 0)
+ >>> con.rgb[0, 0] = ord("@"), YELLOW, BLACK
>>> con.rgb[0, 0]
(64, [255, 255, 0], [0, 0, 0])
- >>> con.rgb["bg"] = tcod.blue
+ >>> con.rgb["bg"] = BLUE
>>> con.rgb[0, 0]
(64, [255, 255, 0], [ 0, 0, 255])
@@ -342,25 +342,25 @@ def rgb(self) -> NDArray[Any]:
return self.rgba.view(self._DTYPE_RGB)
@property
- def default_bg(self) -> Tuple[int, int, int]:
+ def default_bg(self) -> tuple[int, int, int]:
"""Tuple[int, int, int]: The default background color."""
color = self._console_data.back
return color.r, color.g, color.b
- @default_bg.setter # type: ignore
+ @default_bg.setter
@deprecate("Console defaults have been deprecated.")
- def default_bg(self, color: Tuple[int, int, int]) -> None:
+ def default_bg(self, color: tuple[int, int, int]) -> None:
self._console_data.back = color
@property
- def default_fg(self) -> Tuple[int, int, int]:
+ def default_fg(self) -> tuple[int, int, int]:
"""Tuple[int, int, int]: The default foreground color."""
color = self._console_data.fore
return color.r, color.g, color.b
- @default_fg.setter # type: ignore
+ @default_fg.setter
@deprecate("Console defaults have been deprecated.")
- def default_fg(self, color: Tuple[int, int, int]) -> None:
+ def default_fg(self, color: tuple[int, int, int]) -> None:
self._console_data.fore = color
@property
@@ -368,7 +368,7 @@ def default_bg_blend(self) -> int:
"""int: The default blending mode."""
return self._console_data.bkgnd_flag # type: ignore
- @default_bg_blend.setter # type: ignore
+ @default_bg_blend.setter
@deprecate("Console defaults have been deprecated.")
def default_bg_blend(self, value: int) -> None:
self._console_data.bkgnd_flag = value
@@ -378,24 +378,24 @@ def default_alignment(self) -> int:
"""int: The default text alignment."""
return self._console_data.alignment # type: ignore
- @default_alignment.setter # type: ignore
+ @default_alignment.setter
@deprecate("Console defaults have been deprecated.")
def default_alignment(self, value: int) -> None:
self._console_data.alignment = value
- def __clear_warning(self, name: str, value: Tuple[int, int, int]) -> None:
+ def __clear_warning(self, name: str, value: tuple[int, int, int]) -> None:
"""Raise a warning for bad default values during calls to clear."""
warnings.warn(
- "Clearing with the console default values is deprecated.\n" "Add %s=%r to this call." % (name, value),
+ f"Clearing with the console default values is deprecated.\nAdd {name}={value!r} to this call.",
DeprecationWarning,
stacklevel=3,
)
def clear(
self,
- ch: int = ord(" "),
- fg: Tuple[int, int, int] = ..., # type: ignore
- bg: Tuple[int, int, int] = ..., # type: ignore
+ ch: int = 0x20,
+ fg: tuple[int, int, int] = ..., # type: ignore
+ bg: tuple[int, int, int] = ..., # type: ignore
) -> None:
"""Reset all values in this console to a single value.
@@ -460,25 +460,22 @@ def put_char(
13: "tcod.BKGND_DEFAULT",
}
- def __deprecate_defaults(
+ def __deprecate_defaults( # noqa: C901, PLR0912
self,
new_func: str,
- bg_blend: Any,
- alignment: Any = ...,
- clear: Any = ...,
+ bg_blend: Any, # noqa: ANN401
+ alignment: Any = ..., # noqa: ANN401
+ clear: Any = ..., # noqa: ANN401
) -> None:
"""Return the parameters needed to recreate the current default state."""
if not __debug__:
return
- fg: Optional[Tuple[int, int, int]] = self.default_fg
- bg: Optional[Tuple[int, int, int]] = self.default_bg
+ fg: tuple[int, int, int] | None = self.default_fg
+ bg: tuple[int, int, int] | None = self.default_bg
if bg_blend == tcod.constants.BKGND_NONE:
bg = None
- if bg_blend == tcod.constants.BKGND_DEFAULT:
- bg_blend = self.default_bg_blend
- else:
- bg_blend = None
+ bg_blend = self.default_bg_blend if bg_blend == tcod.constants.BKGND_DEFAULT else None
if bg_blend == tcod.constants.BKGND_NONE:
bg = None
bg_blend = None
@@ -498,22 +495,19 @@ def __deprecate_defaults(
if clear is False:
params.append("ch=0")
if fg is not None:
- params.append("fg=%s" % (fg,))
+ params.append(f"fg={fg}")
if bg is not None:
- params.append("bg=%s" % (bg,))
+ params.append(f"bg={bg}")
if bg_blend is not None:
- params.append("bg_blend=%s" % (self.__BG_BLEND_LOOKUP[bg_blend],))
+ params.append(f"bg_blend={self.__BG_BLEND_LOOKUP[bg_blend]}")
if alignment is not None:
- params.append("alignment=%s" % (self.__ALIGNMENT_LOOKUP[alignment],))
+ params.append(f"alignment={self.__ALIGNMENT_LOOKUP[alignment]}")
param_str = ", ".join(params)
- if not param_str:
- param_str = "."
- else:
- param_str = " and add the following parameters:\n%s" % (param_str,)
+ param_str = "." if not param_str else f" and add the following parameters:\n{param_str}"
warnings.warn(
"Console functions using default values have been deprecated.\n"
- "Replace this method with `Console.%s`%s" % (new_func, param_str),
- DeprecationWarning,
+ f"Replace this method with `Console.{new_func}`{param_str}",
+ FutureWarning,
stacklevel=3,
)
@@ -523,7 +517,7 @@ def print_(
y: int,
string: str,
bg_blend: int = tcod.constants.BKGND_DEFAULT,
- alignment: Optional[int] = None,
+ alignment: int | None = None,
) -> None:
"""Print a color formatted string on a console.
@@ -552,7 +546,7 @@ def print_rect(
height: int,
string: str,
bg_blend: int = tcod.constants.BKGND_DEFAULT,
- alignment: Optional[int] = None,
+ alignment: int | None = None,
) -> int:
"""Print a string constrained to a rectangle.
@@ -749,7 +743,7 @@ def blit(
height: int = 0,
fg_alpha: float = 1.0,
bg_alpha: float = 1.0,
- key_color: Optional[Tuple[int, int, int]] = None,
+ key_color: tuple[int, int, int] | None = None,
) -> None:
"""Blit from this console onto the ``dest`` console.
@@ -771,7 +765,7 @@ def blit(
None, or a (red, green, blue) tuple with values of 0-255.
.. versionchanged:: 4.0
- Parameters were rearraged and made optional.
+ Parameters were rearranged and made optional.
Previously they were:
`(x, y, width, height, dest, dest_x, dest_y, *)`
@@ -783,7 +777,7 @@ def blit(
"""
# The old syntax is easy to detect and correct.
if hasattr(src_y, "console_c"):
- (src_x, src_y, width, height, dest, dest_x, dest_y,) = (
+ (src_x, src_y, width, height, dest, dest_x, dest_y) = (
dest, # type: ignore
dest_x,
dest_y,
@@ -829,7 +823,7 @@ def blit(
)
@deprecate("Pass the key color to Console.blit instead of calling this function.")
- def set_key_color(self, color: Optional[Tuple[int, int, int]]) -> None:
+ def set_key_color(self, color: tuple[int, int, int] | None) -> None:
"""Set a consoles blit transparent color.
`color` is the (r, g, b) color, or None to disable key color.
@@ -841,7 +835,7 @@ def set_key_color(self, color: Optional[Tuple[int, int, int]]) -> None:
self._key_color = color
def __enter__(self) -> Console:
- """Returns this console in a managed context.
+ """Return this console in a managed context.
When the root console is used as a context, the graphical window will
close once the context is left as if :any:`tcod.console_delete` was
@@ -854,7 +848,8 @@ def __enter__(self) -> Console:
:any:`tcod.console_init_root`
"""
if self.console_c != ffi.NULL:
- raise NotImplementedError("Only the root console has a context.")
+ msg = "Only the root console has a context."
+ raise NotImplementedError(msg)
return self
def close(self) -> None:
@@ -866,24 +861,25 @@ def close(self) -> None:
.. versionadded:: 11.11
"""
if self.console_c != ffi.NULL:
- raise NotImplementedError("Only the root console can be used to close libtcod's window.")
+ msg = "Only the root console can be used to close libtcod's window."
+ raise NotImplementedError(msg)
lib.TCOD_console_delete(self.console_c)
def __exit__(self, *args: Any) -> None:
- """Closes the graphical window on exit.
+ """Close the graphical window on exit.
Some tcod functions may have undefined behavior after this point.
"""
self.close()
def __bool__(self) -> bool:
- """Returns False if this is the root console.
+ """Return False if this is the root console.
This mimics libtcodpy behavior.
"""
return bool(self.console_c != ffi.NULL)
- def __getstate__(self) -> Any:
+ def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
del state["console_c"]
state["_console_data"] = {
@@ -898,7 +894,7 @@ def __getstate__(self) -> Any:
state["_tiles"] = np.array(self._tiles, copy=True)
return state
- def __setstate__(self, state: Any) -> None:
+ def __setstate__(self, state: dict[str, Any]) -> None:
self._key_color = None
if "_tiles" not in state:
tiles: NDArray[Any] = np.ndarray((self.height, self.width), dtype=self.DTYPE)
@@ -922,7 +918,7 @@ def __repr__(self) -> str:
self.width,
self.height,
self._order,
- self.tiles,
+ self.rgba,
)
def __str__(self) -> str:
@@ -934,19 +930,19 @@ def print(
x: int,
y: int,
string: str,
- fg: Optional[Tuple[int, int, int]] = None,
- bg: Optional[Tuple[int, int, int]] = None,
+ fg: tuple[int, int, int] | None = None,
+ bg: tuple[int, int, int] | None = None,
bg_blend: int = tcod.constants.BKGND_SET,
alignment: int = tcod.constants.LEFT,
) -> None:
- """Print a string on a console with manual line breaks.
+ r"""Print a string on a console with manual line breaks.
`x` and `y` are the starting tile, with ``0,0`` as the upper-left
corner of the console.
`string` is a Unicode string which may include color control
characters. Strings which are too long will be truncated until the
- next newline character ``"\\n"``.
+ next newline character ``"\n"``.
`fg` and `bg` are the foreground text color and background tile color
respectfully. This is a 3-item tuple with (r, g, b) color values from
@@ -985,8 +981,8 @@ def print_box(
width: int,
height: int,
string: str,
- fg: Optional[Tuple[int, int, int]] = None,
- bg: Optional[Tuple[int, int, int]] = None,
+ fg: tuple[int, int, int] | None = None,
+ bg: tuple[int, int, int] | None = None,
bg_blend: int = tcod.constants.BKGND_SET,
alignment: int = tcod.constants.LEFT,
) -> int:
@@ -1045,13 +1041,13 @@ def draw_frame(
height: int,
title: str = "",
clear: bool = True,
- fg: Optional[Tuple[int, int, int]] = None,
- bg: Optional[Tuple[int, int, int]] = None,
+ fg: tuple[int, int, int] | None = None,
+ bg: tuple[int, int, int] | None = None,
bg_blend: int = tcod.constants.BKGND_SET,
*,
- decoration: Union[str, Tuple[int, int, int, int, int, int, int, int, int]] = "┌─┐│ │└─┘",
+ decoration: str | tuple[int, int, int, int, int, int, int, int, int] = "┌─┐│ │└─┘",
) -> None:
- """Draw a framed rectangle with an optional title.
+ r"""Draw a framed rectangle with an optional title.
`x` and `y` are the starting tile, with ``0,0`` as the upper-left
corner of the console.
@@ -1093,7 +1089,7 @@ def draw_frame(
Example::
- >>> console = tcod.Console(12, 6)
+ >>> console = tcod.console.Console(12, 6)
>>> console.draw_frame(x=0, y=0, width=3, height=3)
>>> console.draw_frame(x=3, y=0, width=3, height=3, decoration="╔═╗║ ║╚═╝")
>>> console.draw_frame(x=6, y=0, width=3, height=3, decoration="123456789")
@@ -1104,17 +1100,16 @@ def draw_frame(
>>> console.print_box(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=tcod.CENTER)
1
>>> print(console)
- <┌─┐╔═╗123/-\\
+ <┌─┐╔═╗123/-\
│ │║ ║456| |
- └─┘╚═╝789\\-/
+ └─┘╚═╝789\-/
┌─ Title ──┐
│ │
└─┤Lower├──┘>
"""
if title and decoration != "┌─┐│ │└─┘":
- raise TypeError(
- "The title and decoration parameters are mutually exclusive. You should print the title manually."
- )
+ msg = "The title and decoration parameters are mutually exclusive. You should print the title manually."
+ raise TypeError(msg)
if title:
warnings.warn(
"The title parameter will be removed in the future since the style is hard-coded.",
@@ -1136,13 +1131,10 @@ def draw_frame(
clear,
)
return
- decoration_: Sequence[int]
- if isinstance(decoration, str):
- decoration_ = [ord(c) for c in decoration]
- else:
- decoration_ = decoration
+ decoration_ = [ord(c) for c in decoration] if isinstance(decoration, str) else decoration
if len(decoration_) != 9:
- raise TypeError(f"Decoration must have a length of 9 (len(decoration) is {len(decoration_)}.)")
+ msg = f"Decoration must have a length of 9 (len(decoration) is {len(decoration_)}.)"
+ raise TypeError(msg)
_check(
lib.TCOD_console_draw_frame_rgb(
self.console_c,
@@ -1165,8 +1157,8 @@ def draw_rect(
width: int,
height: int,
ch: int,
- fg: Optional[Tuple[int, int, int]] = None,
- bg: Optional[Tuple[int, int, int]] = None,
+ fg: tuple[int, int, int] | None = None,
+ bg: tuple[int, int, int] | None = None,
bg_blend: int = tcod.constants.BKGND_SET,
) -> None:
"""Draw characters and colors over a rectangular region.
@@ -1235,7 +1227,7 @@ def get_height_rect(width: int, string: str) -> int:
@deprecate("This function does not support contexts.")
-def recommended_size() -> Tuple[int, int]:
+def recommended_size() -> tuple[int, int]:
"""Return the recommended size of a console for the current active window.
The return is determined from the active tileset size and active window
@@ -1254,7 +1246,8 @@ def recommended_size() -> Tuple[int, int]:
Use :any:`Context.recommended_console_size` instead.
"""
if not lib.TCOD_ctx.engine:
- raise RuntimeError("The libtcod engine was not initialized first.")
+ msg = "The libtcod engine was not initialized first."
+ raise RuntimeError(msg)
window = lib.TCOD_sys_get_sdl_window()
renderer = lib.TCOD_sys_get_sdl_renderer()
with ffi.new("int[2]") as xy:
@@ -1267,7 +1260,7 @@ def recommended_size() -> Tuple[int, int]:
return w, h
-def load_xp(path: Union[str, Path], order: Literal["C", "F"] = "C") -> Tuple[Console, ...]:
+def load_xp(path: str | PathLike[str], order: Literal["C", "F"] = "C") -> tuple[Console, ...]:
"""Load a REXPaint file as a tuple of consoles.
`path` is the name of the REXPaint file to load.
@@ -1281,7 +1274,8 @@ def load_xp(path: Union[str, Path], order: Literal["C", "F"] = "C") -> Tuple[Con
Example::
import numpy as np
- import tcod
+ import tcod.console
+ import tcod.tileset
path = "example.xp" # REXPaint file with one layer.
@@ -1300,16 +1294,15 @@ def load_xp(path: Union[str, Path], order: Literal["C", "F"] = "C") -> Tuple[Con
is_transparent = (console.rgb["bg"] == KEY_COLOR).all(axis=-1)
console.rgba[is_transparent] = (ord(" "), (0,), (0,))
"""
- if not os.path.exists(path):
- raise FileNotFoundError(f"File not found:\n\t{os.path.abspath(path)}")
- layers = _check(tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), 0, ffi.NULL))
+ path = Path(path).resolve(strict=True)
+ layers = _check(tcod.lib.TCOD_load_xp(bytes(path), 0, ffi.NULL))
consoles = ffi.new("TCOD_Console*[]", layers)
- _check(tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), layers, consoles))
+ _check(tcod.lib.TCOD_load_xp(bytes(path), layers, consoles))
return tuple(Console._from_cdata(console_p, order=order) for console_p in consoles)
def save_xp(
- path: Union[str, Path],
+ path: str | PathLike[str],
consoles: Iterable[Console],
compress_level: int = 9,
) -> None:
@@ -1331,9 +1324,10 @@ def save_xp(
Example::
import numpy as np
- import tcod
+ import tcod.console
+ import tcod.tileset
- console = tcod.Console(80, 24) # Example console.
+ console = tcod.console.Console(80, 24) # Example console.
# Convert from Unicode to REXPaint's encoding.
# Required to load this console correctly in the REXPaint tool.
@@ -1358,12 +1352,13 @@ def save_xp(
tcod.console.save_xp("example.xp", [console])
"""
+ path = Path(path)
consoles_c = ffi.new("TCOD_Console*[]", [c.console_c for c in consoles])
_check(
tcod.lib.TCOD_save_xp(
len(consoles_c),
consoles_c,
- str(path).encode("utf-8"),
+ bytes(path),
compress_level,
)
)
diff --git a/tcod/constants.py b/tcod/constants.py
index 41afe966..f0a596dc 100644
--- a/tcod/constants.py
+++ b/tcod/constants.py
@@ -247,7 +247,7 @@
KEY_PRESSED = 1
KEY_RELEASED = 2
LEFT = 0
-NB_RENDERERS = 5
+NB_RENDERERS = 6
NOISE_DEFAULT = 0
NOISE_PERLIN = 1
NOISE_SIMPLEX = 2
@@ -257,6 +257,7 @@
RENDERER_OPENGL2 = 4
RENDERER_SDL = 2
RENDERER_SDL2 = 3
+RENDERER_XTERM = 5
RIGHT = 1
RNG_CMWC = 1
RNG_MT = 0
@@ -755,6 +756,7 @@
"RENDERER_OPENGL2",
"RENDERER_SDL",
"RENDERER_SDL2",
+ "RENDERER_XTERM",
"RIGHT",
"RNG_CMWC",
"RNG_MT",
@@ -799,201 +801,4 @@
"TYPE_VALUELIST13",
"TYPE_VALUELIST14",
"TYPE_VALUELIST15",
- "amber",
- "azure",
- "black",
- "blue",
- "brass",
- "celadon",
- "chartreuse",
- "copper",
- "crimson",
- "cyan",
- "dark_amber",
- "dark_azure",
- "dark_blue",
- "dark_chartreuse",
- "dark_crimson",
- "dark_cyan",
- "dark_flame",
- "dark_fuchsia",
- "dark_gray",
- "dark_green",
- "dark_grey",
- "dark_han",
- "dark_lime",
- "dark_magenta",
- "dark_orange",
- "dark_pink",
- "dark_purple",
- "dark_red",
- "dark_sea",
- "dark_sepia",
- "dark_sky",
- "dark_turquoise",
- "dark_violet",
- "dark_yellow",
- "darker_amber",
- "darker_azure",
- "darker_blue",
- "darker_chartreuse",
- "darker_crimson",
- "darker_cyan",
- "darker_flame",
- "darker_fuchsia",
- "darker_gray",
- "darker_green",
- "darker_grey",
- "darker_han",
- "darker_lime",
- "darker_magenta",
- "darker_orange",
- "darker_pink",
- "darker_purple",
- "darker_red",
- "darker_sea",
- "darker_sepia",
- "darker_sky",
- "darker_turquoise",
- "darker_violet",
- "darker_yellow",
- "darkest_amber",
- "darkest_azure",
- "darkest_blue",
- "darkest_chartreuse",
- "darkest_crimson",
- "darkest_cyan",
- "darkest_flame",
- "darkest_fuchsia",
- "darkest_gray",
- "darkest_green",
- "darkest_grey",
- "darkest_han",
- "darkest_lime",
- "darkest_magenta",
- "darkest_orange",
- "darkest_pink",
- "darkest_purple",
- "darkest_red",
- "darkest_sea",
- "darkest_sepia",
- "darkest_sky",
- "darkest_turquoise",
- "darkest_violet",
- "darkest_yellow",
- "desaturated_amber",
- "desaturated_azure",
- "desaturated_blue",
- "desaturated_chartreuse",
- "desaturated_crimson",
- "desaturated_cyan",
- "desaturated_flame",
- "desaturated_fuchsia",
- "desaturated_green",
- "desaturated_han",
- "desaturated_lime",
- "desaturated_magenta",
- "desaturated_orange",
- "desaturated_pink",
- "desaturated_purple",
- "desaturated_red",
- "desaturated_sea",
- "desaturated_sky",
- "desaturated_turquoise",
- "desaturated_violet",
- "desaturated_yellow",
- "flame",
- "fuchsia",
- "gold",
- "gray",
- "green",
- "grey",
- "han",
- "light_amber",
- "light_azure",
- "light_blue",
- "light_chartreuse",
- "light_crimson",
- "light_cyan",
- "light_flame",
- "light_fuchsia",
- "light_gray",
- "light_green",
- "light_grey",
- "light_han",
- "light_lime",
- "light_magenta",
- "light_orange",
- "light_pink",
- "light_purple",
- "light_red",
- "light_sea",
- "light_sepia",
- "light_sky",
- "light_turquoise",
- "light_violet",
- "light_yellow",
- "lighter_amber",
- "lighter_azure",
- "lighter_blue",
- "lighter_chartreuse",
- "lighter_crimson",
- "lighter_cyan",
- "lighter_flame",
- "lighter_fuchsia",
- "lighter_gray",
- "lighter_green",
- "lighter_grey",
- "lighter_han",
- "lighter_lime",
- "lighter_magenta",
- "lighter_orange",
- "lighter_pink",
- "lighter_purple",
- "lighter_red",
- "lighter_sea",
- "lighter_sepia",
- "lighter_sky",
- "lighter_turquoise",
- "lighter_violet",
- "lighter_yellow",
- "lightest_amber",
- "lightest_azure",
- "lightest_blue",
- "lightest_chartreuse",
- "lightest_crimson",
- "lightest_cyan",
- "lightest_flame",
- "lightest_fuchsia",
- "lightest_gray",
- "lightest_green",
- "lightest_grey",
- "lightest_han",
- "lightest_lime",
- "lightest_magenta",
- "lightest_orange",
- "lightest_pink",
- "lightest_purple",
- "lightest_red",
- "lightest_sea",
- "lightest_sepia",
- "lightest_sky",
- "lightest_turquoise",
- "lightest_violet",
- "lightest_yellow",
- "lime",
- "magenta",
- "orange",
- "peach",
- "pink",
- "purple",
- "red",
- "sea",
- "sepia",
- "silver",
- "sky",
- "turquoise",
- "violet",
- "white",
- "yellow",
]
diff --git a/tcod/context.py b/tcod/context.py
index e2e69f12..79eb7df1 100644
--- a/tcod/context.py
+++ b/tcod/context.py
@@ -46,18 +46,23 @@
:any:`tcod.mouse_get_status`.
.. versionadded:: 11.12
-""" # noqa: E501
+"""
from __future__ import annotations
-import os
+import copy
import pickle
import sys
-from typing import Any, Iterable, List, Optional, Tuple
+import warnings
+from pathlib import Path
+from typing import Any, Iterable, NoReturn, TypeVar
-from typing_extensions import Literal, NoReturn
+from typing_extensions import Literal
-import tcod
+import tcod.console
import tcod.event
+import tcod.render
+import tcod.sdl.render
+import tcod.sdl.video
import tcod.tileset
from tcod._internal import _check, _check_warn, pending_deprecate
from tcod.loader import ffi, lib
@@ -80,8 +85,11 @@
"RENDERER_OPENGL2",
"RENDERER_SDL",
"RENDERER_SDL2",
+ "RENDERER_XTERM",
)
+_Event = TypeVar("_Event", bound=tcod.event.Event)
+
SDL_WINDOW_FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN
"""Exclusive fullscreen mode.
@@ -126,20 +134,33 @@
Rendering is decided by SDL2 and can be changed by using an SDL2 hint:
https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
"""
+RENDERER_XTERM = lib.TCOD_RENDERER_XTERM
+"""A renderer targeting modern terminals with 24-bit color support.
+
+This is an experimental renderer with partial support for XTerm and SSH.
+This will work best on those terminals.
+
+Terminal inputs and events will be passed to SDL's event system.
+
+There is poor support for ANSI escapes on Windows 10.
+It is not recommended to use this renderer on Windows.
+
+.. versionadded:: 13.3
+"""
-def _handle_tileset(tileset: Optional[tcod.tileset.Tileset]) -> Any:
+def _handle_tileset(tileset: tcod.tileset.Tileset | None) -> Any:
"""Get the TCOD_Tileset pointer from a Tileset or return a NULL pointer."""
return tileset._tileset_p if tileset else ffi.NULL
-def _handle_title(title: Optional[str]) -> Any:
+def _handle_title(title: str | None) -> Any:
"""Return title as a CFFI string.
If title is None then return a decent default title is returned.
"""
if title is None:
- title = os.path.basename(sys.argv[0])
+ title = Path(sys.argv[0]).name
return ffi.new("char[]", title.encode("utf-8"))
@@ -149,28 +170,39 @@ class Context:
Use :any:`tcod.context.new` to create a new context.
"""
- def __init__(self, context_p: Any):
- """Creates a context from a cffi pointer."""
+ def __init__(self, context_p: Any) -> None:
+ """Create a context from a cffi pointer."""
self._context_p = context_p
@classmethod
def _claim(cls, context_p: Any) -> Context:
+ """Return a new instance wrapping a context pointer."""
return cls(ffi.gc(context_p, lib.TCOD_context_delete))
+ @property
+ def _p(self) -> Any: # noqa: ANN401
+ """Return the context pointer or raise if it is missing."""
+ try:
+ return self._context_p
+ except AttributeError:
+ msg = "This context has been closed can no longer be used."
+ raise RuntimeError(msg) from None
+
def __enter__(self) -> Context:
- """This context can be used as a context manager."""
+ """Enter this context which will close on exiting."""
return self
def close(self) -> None:
- """Delete the context, closing any windows opened by this context.
+ """Close this context, closing any windows opened by this context.
- This instance is invalid after this call."""
+ Afterwards doing anything with this instance other than closing it again is invalid.
+ """
if hasattr(self, "_context_p"):
ffi.release(self._context_p)
del self._context_p
def __exit__(self, *args: Any) -> None:
- """The libtcod context is closed as this context manager exits."""
+ """Automatically close on the context on exit."""
self.close()
def present(
@@ -179,8 +211,8 @@ def present(
*,
keep_aspect: bool = False,
integer_scaling: bool = False,
- clear_color: Tuple[int, int, int] = (0, 0, 0),
- align: Tuple[float, float] = (0.5, 0.5),
+ clear_color: tuple[int, int, int] = (0, 0, 0),
+ align: tuple[float, float] = (0.5, 0.5),
) -> None:
"""Present a console to this context's display.
@@ -214,39 +246,71 @@ def present(
"align_y": align[1],
},
)
- _check(lib.TCOD_context_present(self._context_p, console.console_c, viewport_args))
+ _check(lib.TCOD_context_present(self._p, console.console_c, viewport_args))
- def pixel_to_tile(self, x: int, y: int) -> Tuple[int, int]:
+ def pixel_to_tile(self, x: int, y: int) -> tuple[int, int]:
"""Convert window pixel coordinates to tile coordinates."""
with ffi.new("int[2]", (x, y)) as xy:
- _check(lib.TCOD_context_screen_pixel_to_tile_i(self._context_p, xy, xy + 1))
+ _check(lib.TCOD_context_screen_pixel_to_tile_i(self._p, xy, xy + 1))
return xy[0], xy[1]
- def pixel_to_subtile(self, x: int, y: int) -> Tuple[float, float]:
+ def pixel_to_subtile(self, x: int, y: int) -> tuple[float, float]:
"""Convert window pixel coordinates to sub-tile coordinates."""
with ffi.new("double[2]", (x, y)) as xy:
- _check(lib.TCOD_context_screen_pixel_to_tile_d(self._context_p, xy, xy + 1))
+ _check(lib.TCOD_context_screen_pixel_to_tile_d(self._p, xy, xy + 1))
return xy[0], xy[1]
- def convert_event(self, event: tcod.event.Event) -> None:
- """Fill in the tile coordinates of a mouse event using this context."""
+ def convert_event(self, event: _Event) -> _Event:
+ """Return an event with mouse pixel coordinates converted into tile coordinates.
+
+ Example::
+
+ context: tcod.context.Context
+ for event in tcod.event.get():
+ event_tile = context.convert_event(event)
+ if isinstance(event, tcod.event.MouseMotion):
+ # Events start with pixel coordinates and motion.
+ print(f"Pixels: {event.position=}, {event.motion=}")
+ if isinstance(event_tile, tcod.event.MouseMotion):
+ # Tile coordinates are used in the returned event.
+ print(f"Tiles: {event_tile.position=}, {event_tile.motion=}")
+
+ .. versionchanged:: 15.0
+ Now returns a new event with the coordinates converted into tiles.
+ """
+ event_copy = copy.copy(event)
if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)):
- event.tile = tcod.event.Point(*self.pixel_to_tile(*event.pixel))
+ assert isinstance(event_copy, (tcod.event.MouseState, tcod.event.MouseMotion))
+ event_copy.position = event.tile = tcod.event.Point(*self.pixel_to_tile(*event.position))
if isinstance(event, tcod.event.MouseMotion):
+ assert isinstance(event_copy, tcod.event.MouseMotion)
prev_tile = self.pixel_to_tile(
- event.pixel[0] - event.pixel_motion[0],
- event.pixel[1] - event.pixel_motion[1],
+ event.position[0] - event.motion[0],
+ event.position[1] - event.motion[1],
+ )
+ event_copy.motion = event.tile_motion = tcod.event.Point(
+ event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1]
)
- event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1])
+ return event_copy
- def save_screenshot(self, path: Optional[str] = None) -> None:
+ def save_screenshot(self, path: str | None = None) -> None:
"""Save a screen-shot to the given file path."""
c_path = path.encode("utf-8") if path is not None else ffi.NULL
- _check(lib.TCOD_context_save_screenshot(self._context_p, c_path))
+ _check(lib.TCOD_context_save_screenshot(self._p, c_path))
+
+ def change_tileset(self, tileset: tcod.tileset.Tileset | None) -> None:
+ """Change the active tileset used by this context.
+
+ The new tileset will take effect on the next call to :any:`present`.
+ Contexts not using a renderer with an emulated terminal will be unaffected by this method.
+
+ This does not do anything to resize the window, keep this in mind if the tileset as a differing tile size.
+ Access the window with :any:`sdl_window` to resize it manually, if needed.
- def change_tileset(self, tileset: Optional[tcod.tileset.Tileset]) -> None:
- """Change the active tileset used by this context."""
- _check(lib.TCOD_context_change_tileset(self._context_p, _handle_tileset(tileset)))
+ Using this method only one tileset is active per-frame.
+ See :any:`tcod.render` if you want to renderer with multiple tilesets in a single frame.
+ """
+ _check(lib.TCOD_context_change_tileset(self._p, _handle_tileset(tileset)))
def new_console(
self,
@@ -282,31 +346,48 @@ def new_console(
.. seealso::
:any:`tcod.console.Console`
+
+ Example::
+
+ scale = 1 # Tile size scale. This example uses integers but floating point numbers are also valid.
+ context = tcod.context.new()
+ while True:
+ # Create a cleared, dynamically-sized console for each frame.
+ console = context.new_console(magnification=scale)
+ # This printed output will wrap if the window is shrunk.
+ console.print_box(0, 0, console.width, console.height, "Hello world")
+ # Use integer_scaling to prevent subpixel distortion.
+ # This may add padding around the rendered console.
+ context.present(console, integer_scaling=True)
+ for event in tcod.event.wait():
+ if isinstance(event, tcod.event.Quit):
+ raise SystemExit()
+ elif isinstance(event, tcod.event.MouseWheel):
+ # Use the mouse wheel to change the rendered tile size.
+ scale = max(1, scale + event.y)
"""
if magnification < 0:
raise ValueError("Magnification must be greater than zero. (Got %f)" % magnification)
size = ffi.new("int[2]")
- _check(lib.TCOD_context_recommended_console_size(self._context_p, magnification, size, size + 1))
+ _check(lib.TCOD_context_recommended_console_size(self._p, magnification, size, size + 1))
width, height = max(min_columns, size[0]), max(min_rows, size[1])
return tcod.console.Console(width, height, order=order)
- def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> Tuple[int, int]:
- """Return the recommended (columns, rows) of a console for this
- context.
+ def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> tuple[int, int]:
+ """Return the recommended (columns, rows) of a console for this context.
`min_columns`, `min_rows` are the lowest values which will be returned.
- If result is only used to create a new console then you may want to
- call :any:`Context.new_console` instead.
+ If result is only used to create a new console then you may want to call :any:`Context.new_console` instead.
"""
with ffi.new("int[2]") as size:
- _check(lib.TCOD_context_recommended_console_size(self._context_p, 1.0, size, size + 1))
+ _check(lib.TCOD_context_recommended_console_size(self._p, 1.0, size, size + 1))
return max(min_columns, size[0]), max(min_rows, size[1])
@property
def renderer_type(self) -> int:
"""Return the libtcod renderer type used by this context."""
- return _check(lib.TCOD_context_get_renderer_type(self._context_p))
+ return _check(lib.TCOD_context_get_renderer_type(self._p))
@property
def sdl_window_p(self) -> Any:
@@ -334,37 +415,85 @@ def toggle_fullscreen(context: tcod.context.Context) -> None:
context.sdl_window_p,
0 if fullscreen else tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP,
)
- ''' # noqa: E501
- return lib.TCOD_context_get_sdl_window(self._context_p)
- def __reduce__(self) -> NoReturn:
- """Contexts can not be pickled, so this class will raise
- :class:`pickle.PicklingError`.
+ '''
+ return lib.TCOD_context_get_sdl_window(self._p)
+
+ @property
+ def sdl_window(self) -> tcod.sdl.video.Window | None:
+ '''Return a :any:`tcod.sdl.video.Window` referencing this contexts SDL window if it exists.
+
+ Example::
+
+ import tcod
+ import tcod.sdl.video
+
+ def toggle_fullscreen(context: tcod.context.Context) -> None:
+ """Toggle a context window between fullscreen and windowed modes."""
+ window = context.sdl_window
+ if not window:
+ return
+ if window.fullscreen:
+ window.fullscreen = False
+ else:
+ window.fullscreen = tcod.sdl.video.WindowFlags.FULLSCREEN_DESKTOP
+
+ .. versionadded:: 13.4
+ '''
+ p = self.sdl_window_p
+ return tcod.sdl.video.Window(p) if p else None
+
+ @property
+ def sdl_renderer(self) -> tcod.sdl.render.Renderer | None:
+ """Return a :any:`tcod.sdl.render.Renderer` referencing this contexts SDL renderer if it exists.
+
+ .. versionadded:: 13.4
+ """
+ p = lib.TCOD_context_get_sdl_renderer(self._p)
+ return tcod.sdl.render.Renderer(p) if p else None
+
+ @property
+ def sdl_atlas(self) -> tcod.render.SDLTilesetAtlas | None:
+ """Return a :any:`tcod.render.SDLTilesetAtlas` referencing libtcod's SDL texture atlas if it exists.
+
+ .. versionadded:: 13.5
"""
- raise pickle.PicklingError("Python-tcod contexts can not be pickled.")
+ if self._p.type not in (lib.TCOD_RENDERER_SDL, lib.TCOD_RENDERER_SDL2):
+ return None
+ context_data = ffi.cast("struct TCOD_RendererSDL2*", self._context_p.contextdata_)
+ return tcod.render.SDLTilesetAtlas._from_ref(context_data.renderer, context_data.atlas)
+
+ def __reduce__(self) -> NoReturn:
+ """Contexts can not be pickled, so this class will raise :class:`pickle.PicklingError`."""
+ msg = "Python-tcod contexts can not be pickled."
+ raise pickle.PicklingError(msg)
@ffi.def_extern() # type: ignore
def _pycall_cli_output(catch_reference: Any, output: Any) -> None:
- """Callback for the libtcod context CLI. Catches the CLI output."""
- catch: List[str] = ffi.from_handle(catch_reference)
+ """Callback for the libtcod context CLI.
+
+ Catches the CLI output.
+ """
+ catch: list[str] = ffi.from_handle(catch_reference)
catch.append(ffi.string(output).decode("utf-8"))
def new(
*,
- x: Optional[int] = None,
- y: Optional[int] = None,
- width: Optional[int] = None,
- height: Optional[int] = None,
- columns: Optional[int] = None,
- rows: Optional[int] = None,
- renderer: Optional[int] = None,
- tileset: Optional[tcod.tileset.Tileset] = None,
+ x: int | None = None,
+ y: int | None = None,
+ width: int | None = None,
+ height: int | None = None,
+ columns: int | None = None,
+ rows: int | None = None,
+ renderer: int | None = None,
+ tileset: tcod.tileset.Tileset | None = None,
vsync: bool = True,
- sdl_window_flags: Optional[int] = None,
- title: Optional[str] = None,
- argv: Optional[Iterable[str]] = None,
+ sdl_window_flags: int | None = None,
+ title: str | None = None,
+ argv: Iterable[str] | None = None,
+ console: tcod.console.Console | None = None,
) -> Context:
"""Create a new context with the desired pixel size.
@@ -376,11 +505,12 @@ def new(
`columns` and `rows` is the desired size of the console. Can be left as
`None` when you're setting a context by a window size instead of a console.
+ `console` automatically fills in the `columns` and `rows` parameters from an existing :any:`tcod.console.Console`
+ instance.
+
Providing no size information at all is also acceptable.
- `renderer` is the desired libtcod renderer to use.
- Typical options are :any:`tcod.context.RENDERER_OPENGL2` for a faster
- renderer or :any:`tcod.context.RENDERER_SDL2` for a reliable renderer.
+ `renderer` now does nothing and should not be set. It may be removed in the future.
`tileset` is the font/tileset for the new context to render with.
The fall-back tileset available from passing None is useful for
@@ -409,17 +539,29 @@ def new(
the console which should be used.
.. versionadded:: 11.16
+
+ .. versionchanged:: 13.2
+ Added the `console` parameter.
"""
- if renderer is None:
- renderer = RENDERER_OPENGL2
+ if renderer is not None:
+ warnings.warn(
+ "The renderer parameter was deprecated and will likely be removed in a future version of libtcod. "
+ "Remove the renderer parameter to fix this warning.",
+ FutureWarning,
+ stacklevel=2,
+ )
+ renderer = RENDERER_SDL2
if sdl_window_flags is None:
sdl_window_flags = SDL_WINDOW_RESIZABLE
if argv is None:
argv = sys.argv
+ if console is not None:
+ columns = columns or console.width
+ rows = rows or console.height
argv_encoded = [ffi.new("char[]", arg.encode("utf-8")) for arg in argv] # Needs to be kept alive for argv_c.
argv_c = ffi.new("char*[]", argv_encoded)
- catch_msg: List[str] = []
+ catch_msg: list[str] = []
catch_handle = ffi.new_handle(catch_msg) # Keep alive.
title_p = _handle_title(title) # Keep alive.
@@ -459,11 +601,11 @@ def new_window(
width: int,
height: int,
*,
- renderer: Optional[int] = None,
- tileset: Optional[tcod.tileset.Tileset] = None,
+ renderer: int | None = None,
+ tileset: tcod.tileset.Tileset | None = None,
vsync: bool = True,
- sdl_window_flags: Optional[int] = None,
- title: Optional[str] = None,
+ sdl_window_flags: int | None = None,
+ title: str | None = None,
) -> Context:
"""Create a new context with the desired pixel size.
@@ -486,11 +628,11 @@ def new_terminal(
columns: int,
rows: int,
*,
- renderer: Optional[int] = None,
- tileset: Optional[tcod.tileset.Tileset] = None,
+ renderer: int | None = None,
+ tileset: tcod.tileset.Tileset | None = None,
vsync: bool = True,
- sdl_window_flags: Optional[int] = None,
- title: Optional[str] = None,
+ sdl_window_flags: int | None = None,
+ title: str | None = None,
) -> Context:
"""Create a new context with the desired console size.
diff --git a/tcod/event.py b/tcod/event.py
index e430ae49..1379c40c 100644
--- a/tcod/event.py
+++ b/tcod/event.py
@@ -1,21 +1,81 @@
-"""
-A light-weight implementation of event handling built on calls to SDL.
+"""A light-weight implementation of event handling built on calls to SDL.
Many event constants are derived directly from SDL.
-For example: ``tcod.event.K_UP`` and ``tcod.event.SCANCODE_A`` refer to
+For example: ``tcod.event.KeySym.UP`` and ``tcod.event.Scancode.A`` refer to
SDL's ``SDLK_UP`` and ``SDL_SCANCODE_A`` respectfully.
`See this table for all of SDL's keyboard constants.
`_
Printing any event will tell you its attributes in a human readable format.
-An events type attribute if omitted is just the classes name with all letters
-upper-case.
+An events type attribute if omitted is just the classes name with all letters upper-case.
+
+As a general guideline, you should use :any:`KeyboardEvent.sym` for command inputs,
+and :any:`TextInput.text` for name entry fields.
+
+Example::
+
+ import tcod
+
+ KEY_COMMANDS = {
+ tcod.event.KeySym.UP: "move N",
+ tcod.event.KeySym.DOWN: "move S",
+ tcod.event.KeySym.LEFT: "move W",
+ tcod.event.KeySym.RIGHT: "move E",
+ }
+
+ context = tcod.context.new()
+ while True:
+ console = context.new_console()
+ context.present(console, integer_scaling=True)
+ for event in tcod.event.wait():
+ context.convert_event(event) # Adds tile coordinates to mouse events.
+ if isinstance(event, tcod.event.Quit):
+ print(event)
+ raise SystemExit()
+ elif isinstance(event, tcod.event.KeyDown):
+ print(event) # Prints the Scancode and KeySym enums for this event.
+ if event.sym in KEY_COMMANDS:
+ print(f"Command: {KEY_COMMANDS[event.sym]}")
+ elif isinstance(event, tcod.event.MouseButtonDown):
+ print(event) # Prints the mouse button constant names for this event.
+ elif isinstance(event, tcod.event.MouseMotion):
+ print(event) # Prints the mouse button mask bits in a readable format.
+ else:
+ print(event) # Print any unhandled events.
-As a general guideline, you should use :any:`KeyboardEvent.sym` for command
-inputs, and :any:`TextInput.text` for name entry fields.
+Python 3.10 introduced `match statements `_
+which can be used to dispatch events more gracefully:
-Remember to add the line ``import tcod.event``, as importing this module is not
-implied by ``import tcod``.
+Example::
+
+ import tcod
+
+ KEY_COMMANDS = {
+ tcod.event.KeySym.UP: "move N",
+ tcod.event.KeySym.DOWN: "move S",
+ tcod.event.KeySym.LEFT: "move W",
+ tcod.event.KeySym.RIGHT: "move E",
+ }
+
+ context = tcod.context.new()
+ while True:
+ console = context.new_console()
+ context.present(console, integer_scaling=True)
+ for event in tcod.event.wait():
+ context.convert_event(event) # Adds tile coordinates to mouse events.
+ match event:
+ case tcod.event.Quit():
+ raise SystemExit()
+ case tcod.event.KeyDown(sym) if sym in KEY_COMMANDS:
+ print(f"Command: {KEY_COMMANDS[sym]}")
+ case tcod.event.KeyDown(sym=sym, scancode=scancode, mod=mod, repeat=repeat):
+ print(f"KeyDown: {sym=}, {scancode=}, {mod=}, {repeat=}")
+ case tcod.event.MouseButtonDown(button=button, pixel=pixel, tile=tile):
+ print(f"MouseButtonDown: {button=}, {pixel=}, {tile=}")
+ case tcod.event.MouseMotion(pixel=pixel, pixel_motion=pixel_motion, tile=tile, tile_motion=tile_motion):
+ print(f"MouseMotion: {pixel=}, {pixel_motion=}, {tile=}, {tile_motion=}")
+ case tcod.event.Event() as event:
+ print(event) # Show any unhandled events.
.. versionadded:: 8.4
"""
@@ -23,22 +83,24 @@
import enum
import warnings
-from typing import Any, Callable, Dict, Generic, Iterator, Mapping, NamedTuple, Optional, Tuple, TypeVar, Union
+from typing import Any, Callable, Generic, Iterator, Mapping, NamedTuple, TypeVar
import numpy as np
from numpy.typing import NDArray
from typing_extensions import Final, Literal
import tcod.event_constants
-from tcod.event_constants import * # noqa: F4
+import tcod.sdl.joystick
+from tcod.event_constants import * # noqa: F403
from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT
from tcod.loader import ffi, lib
+from tcod.sdl.joystick import _HAT_DIRECTIONS
T = TypeVar("T")
class _ConstantsWithPrefix(Mapping[int, str]):
- def __init__(self, constants: Mapping[int, str]):
+ def __init__(self, constants: Mapping[int, str]) -> None:
self.constants = constants
def __getitem__(self, key: int) -> str:
@@ -71,7 +133,7 @@ def _describe_bitmask(bits: int, table: Mapping[int, str], default: str = "0") -
return "|".join(result)
-def _pixel_to_tile(x: float, y: float) -> Optional[Tuple[float, float]]:
+def _pixel_to_tile(x: float, y: float) -> tuple[float, float] | None:
"""Convert pixel coordinates to tile coordinates."""
if not lib.TCOD_ctx.engine:
return None
@@ -93,7 +155,7 @@ class Point(NamedTuple):
"""A pixel or tile coordinate starting with zero as the top-most position."""
-def _verify_tile_coordinates(xy: Optional[Point]) -> Point:
+def _verify_tile_coordinates(xy: Point | None) -> Point:
"""Check if an events tile coordinate is initialized and warn if not.
Always returns a valid Point object for backwards compatibility.
@@ -123,7 +185,7 @@ def _init_sdl_video() -> None:
class Modifier(enum.IntFlag):
- """Keyboard modifier flags, a bitfield of held modifier keys.
+ """Keyboard modifier flags, a bit-field of held modifier keys.
Use `bitwise and` to check if a modifier key is held.
@@ -133,8 +195,6 @@ class Modifier(enum.IntFlag):
Example::
>>> mod = tcod.event.Modifier(4098)
- >>> mod
-
>>> mod & tcod.event.Modifier.SHIFT # Check if any shift key is held.
>>> mod & tcod.event.Modifier.LSHIFT # Check if left shift key is held.
@@ -231,19 +291,19 @@ class Event:
pointer. All sub-classes have this attribute.
"""
- def __init__(self, type: Optional[str] = None):
+ def __init__(self, type: str | None = None) -> None:
if type is None:
type = self.__class__.__name__.upper()
self.type: Final = type
self.sdl_event = None
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Any:
+ def from_sdl_event(cls, sdl_event: Any) -> Event:
"""Return a class instance from a python-cffi 'SDL_Event*' pointer."""
raise NotImplementedError()
def __str__(self) -> str:
- return "" % (self.type,)
+ return f""
class Quit(Event):
@@ -263,11 +323,12 @@ def from_sdl_event(cls, sdl_event: Any) -> Quit:
return self
def __repr__(self) -> str:
- return "tcod.event.%s()" % (self.__class__.__name__,)
+ return f"tcod.event.{self.__class__.__name__}()"
class KeyboardEvent(Event):
- """
+ """Base keyboard event.
+
Attributes:
type (str): Will be "KEYDOWN" or "KEYUP", depending on the event.
scancode (Scancode): The keyboard scan-code, this is the physical location
@@ -285,7 +346,7 @@ class KeyboardEvent(Event):
`scancode`, `sym`, and `mod` now use their respective enums.
"""
- def __init__(self, scancode: int, sym: int, mod: int, repeat: bool = False):
+ def __init__(self, scancode: int, sym: int, mod: int, repeat: bool = False) -> None:
super().__init__()
self.scancode = Scancode(scancode)
self.sym = KeySym(sym)
@@ -300,7 +361,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any:
return self
def __repr__(self) -> str:
- return "tcod.event.%s(scancode=%r, sym=%r, mod=%s%s)" % (
+ return "tcod.event.{}(scancode={!r}, sym={!r}, mod={}{})".format(
self.__class__.__name__,
self.scancode,
self.sym,
@@ -321,10 +382,11 @@ class KeyUp(KeyboardEvent):
class MouseState(Event):
- """
+ """Mouse state.
+
Attributes:
type (str): Always "MOUSESTATE".
- pixel (Point): The pixel coordinates of the mouse.
+ position (Point): The position coordinates of the mouse.
tile (Point): The integer tile coordinates of the mouse on the screen.
state (int): A bitmask of which mouse buttons are currently held.
@@ -337,50 +399,82 @@ class MouseState(Event):
* tcod.event.BUTTON_X2MASK
.. versionadded:: 9.3
+
+ .. versionchanged:: 15.0
+ Renamed `pixel` attribute to `position`.
"""
def __init__(
self,
- pixel: Tuple[int, int] = (0, 0),
- tile: Optional[Tuple[int, int]] = (0, 0),
+ position: tuple[int, int] = (0, 0),
+ tile: tuple[int, int] | None = (0, 0),
state: int = 0,
- ):
+ ) -> None:
super().__init__()
- self.pixel = Point(*pixel)
+ self.position = Point(*position)
self.__tile = Point(*tile) if tile is not None else None
self.state = state
+ @property
+ def pixel(self) -> Point:
+ warnings.warn(
+ "The mouse.pixel attribute is deprecated. Use mouse.position instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.position
+
+ @pixel.setter
+ def pixel(self, value: Point) -> None:
+ warnings.warn(
+ "The mouse.pixel attribute is deprecated. Use mouse.position instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.position = value
+
@property
def tile(self) -> Point:
+ warnings.warn(
+ "The mouse.tile attribute is deprecated. Use mouse.position of the event returned by context.convert_event instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return _verify_tile_coordinates(self.__tile)
@tile.setter
- def tile(self, xy: Tuple[int, int]) -> None:
+ def tile(self, xy: tuple[int, int]) -> None:
+ warnings.warn(
+ "The mouse.tile attribute is deprecated. Use mouse.position of the event returned by context.convert_event instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
self.__tile = Point(*xy)
def __repr__(self) -> str:
- return ("tcod.event.%s(pixel=%r, tile=%r, state=%s)") % (
+ return ("tcod.event.{}(position={!r}, tile={!r}, state={})").format(
self.__class__.__name__,
- tuple(self.pixel),
+ tuple(self.position),
tuple(self.tile),
_describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE_PREFIX),
)
def __str__(self) -> str:
- return ("<%s, pixel=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % (
+ return ("<%s, position=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % (
super().__str__().strip("<>"),
- *self.pixel,
+ *self.position,
*self.tile,
_describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE),
)
class MouseMotion(MouseState):
- """
+ """Mouse motion event.
+
Attributes:
type (str): Always "MOUSEMOTION".
- pixel (Point): The pixel coordinates of the mouse.
- pixel_motion (Point): The pixel delta.
+ position (Point): The pixel coordinates of the mouse.
+ motion (Point): The pixel delta.
tile (Point): The integer tile coordinates of the mouse on the screen.
tile_motion (Point): The integer tile delta.
state (int): A bitmask of which mouse buttons are currently held.
@@ -392,26 +486,60 @@ class MouseMotion(MouseState):
* tcod.event.BUTTON_RMASK
* tcod.event.BUTTON_X1MASK
* tcod.event.BUTTON_X2MASK
+
+ .. versionchanged:: 15.0
+ Renamed `pixel` attribute to `position`.
+ Renamed `pixel_motion` attribute to `motion`.
"""
def __init__(
self,
- pixel: Tuple[int, int] = (0, 0),
- pixel_motion: Tuple[int, int] = (0, 0),
- tile: Optional[Tuple[int, int]] = (0, 0),
- tile_motion: Optional[Tuple[int, int]] = (0, 0),
+ position: tuple[int, int] = (0, 0),
+ motion: tuple[int, int] = (0, 0),
+ tile: tuple[int, int] | None = (0, 0),
+ tile_motion: tuple[int, int] | None = (0, 0),
state: int = 0,
- ):
- super().__init__(pixel, tile, state)
- self.pixel_motion = Point(*pixel_motion)
+ ) -> None:
+ super().__init__(position, tile, state)
+ self.motion = Point(*motion)
self.__tile_motion = Point(*tile_motion) if tile_motion is not None else None
+ @property
+ def pixel_motion(self) -> Point:
+ warnings.warn(
+ "The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.motion
+
+ @pixel_motion.setter
+ def pixel_motion(self, value: Point) -> None:
+ warnings.warn(
+ "The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.motion = value
+
@property
def tile_motion(self) -> Point:
+ warnings.warn(
+ "The mouse.tile_motion attribute is deprecated."
+ " Use mouse.motion of the event returned by context.convert_event instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return _verify_tile_coordinates(self.__tile_motion)
@tile_motion.setter
- def tile_motion(self, xy: Tuple[int, int]) -> None:
+ def tile_motion(self, xy: tuple[int, int]) -> None:
+ warnings.warn(
+ "The mouse.tile_motion attribute is deprecated."
+ " Use mouse.motion of the event returned by context.convert_event instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
self.__tile_motion = Point(*xy)
@classmethod
@@ -434,29 +562,30 @@ def from_sdl_event(cls, sdl_event: Any) -> MouseMotion:
return self
def __repr__(self) -> str:
- return ("tcod.event.%s(pixel=%r, pixel_motion=%r, " "tile=%r, tile_motion=%r, state=%s)") % (
+ return ("tcod.event.{}(position={!r}, motion={!r}, tile={!r}, tile_motion={!r}, state={})").format(
self.__class__.__name__,
- tuple(self.pixel),
- tuple(self.pixel_motion),
+ tuple(self.position),
+ tuple(self.motion),
tuple(self.tile),
tuple(self.tile_motion),
_describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE_PREFIX),
)
def __str__(self) -> str:
- return ("<%s, pixel_motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % (
+ return ("<%s, motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % (
super().__str__().strip("<>"),
- *self.pixel_motion,
+ *self.motion,
*self.tile_motion,
)
class MouseButtonEvent(MouseState):
- """
+ """Mouse button event.
+
Attributes:
type (str): Will be "MOUSEBUTTONDOWN" or "MOUSEBUTTONUP",
depending on the event.
- pixel (Point): The pixel coordinates of the mouse.
+ position (Point): The pixel coordinates of the mouse.
tile (Point): The integer tile coordinates of the mouse on the screen.
button (int): Which mouse button.
@@ -467,14 +596,15 @@ class MouseButtonEvent(MouseState):
* tcod.event.BUTTON_RIGHT
* tcod.event.BUTTON_X1
* tcod.event.BUTTON_X2
+
"""
def __init__(
self,
- pixel: Tuple[int, int] = (0, 0),
- tile: Optional[Tuple[int, int]] = (0, 0),
+ pixel: tuple[int, int] = (0, 0),
+ tile: tuple[int, int] | None = (0, 0),
button: int = 0,
- ):
+ ) -> None:
super().__init__(pixel, tile, button)
@property
@@ -491,7 +621,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any:
pixel = button.x, button.y
subtile = _pixel_to_tile(*pixel)
if subtile is None:
- tile: Optional[Tuple[int, int]] = None
+ tile: tuple[int, int] | None = None
else:
tile = int(subtile[0]), int(subtile[1])
self = cls(pixel, tile, button.button)
@@ -499,17 +629,17 @@ def from_sdl_event(cls, sdl_event: Any) -> Any:
return self
def __repr__(self) -> str:
- return "tcod.event.%s(pixel=%r, tile=%r, button=%s)" % (
+ return "tcod.event.{}(position={!r}, tile={!r}, button={})".format(
self.__class__.__name__,
- tuple(self.pixel),
+ tuple(self.position),
tuple(self.tile),
_REVERSE_BUTTON_TABLE_PREFIX[self.button],
)
def __str__(self) -> str:
- return " None:
super().__init__()
self.x = x
self.y = y
@@ -566,13 +697,14 @@ def __str__(self) -> str:
class TextInput(Event):
- """
+ """SDL text input event.
+
Attributes:
type (str): Always "TEXTINPUT".
text (str): A Unicode string with the input.
"""
- def __init__(self, text: str):
+ def __init__(self, text: str) -> None:
super().__init__()
self.text = text
@@ -583,19 +715,16 @@ def from_sdl_event(cls, sdl_event: Any) -> TextInput:
return self
def __repr__(self) -> str:
- return "tcod.event.%s(text=%r)" % (self.__class__.__name__, self.text)
+ return f"tcod.event.{self.__class__.__name__}(text={self.text!r})"
def __str__(self) -> str:
- return "<%s, text=%r)" % (super().__str__().strip("<>"), self.text)
+ return "<{}, text={!r})".format(super().__str__().strip("<>"), self.text)
class WindowEvent(Event):
- """
- Attributes:
- type (str): A window event could mean various event types.
- """
+ """A window event."""
- type: Final[ # type: ignore[misc] # Narrowing contant type.
+ type: Final[ # type: ignore[misc] # Narrowing final type.
Literal[
"WindowShown",
"WindowHidden",
@@ -615,12 +744,13 @@ class WindowEvent(Event):
"WindowHitTest",
]
]
+ """The current window event. This can be one of various options."""
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Union[WindowEvent, Undefined]:
+ def from_sdl_event(cls, sdl_event: Any) -> WindowEvent | Undefined:
if sdl_event.window.event not in cls.__WINDOW_TYPES:
return Undefined.from_sdl_event(sdl_event)
- event_type: Final = cls.__WINDOW_TYPES[sdl_event.window.event].upper()
+ event_type: Final = cls.__WINDOW_TYPES[sdl_event.window.event]
self: WindowEvent
if sdl_event.window.event == lib.SDL_WINDOWEVENT_MOVED:
self = WindowMoved(sdl_event.window.data1, sdl_event.window.data2)
@@ -635,7 +765,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Union[WindowEvent, Undefined]:
return self
def __repr__(self) -> str:
- return "tcod.event.%s(type=%r)" % (self.__class__.__name__, self.type)
+ return f"tcod.event.{self.__class__.__name__}(type={self.type!r})"
__WINDOW_TYPES = {
lib.SDL_WINDOWEVENT_SHOWN: "WindowShown",
@@ -658,14 +788,15 @@ def __repr__(self) -> str:
class WindowMoved(WindowEvent):
- """
+ """Window moved event.
+
Attributes:
- type (str): Always "WINDOWMOVED".
x (int): Movement on the x-axis.
y (int): Movement on the y-axis.
"""
- type: Literal["WINDOWMOVED"] # type: ignore[assignment,misc]
+ type: Final[Literal["WINDOWMOVED"]] # type: ignore[assignment,misc]
+ """Always "WINDOWMOVED"."""
def __init__(self, x: int, y: int) -> None:
super().__init__(None)
@@ -673,7 +804,7 @@ def __init__(self, x: int, y: int) -> None:
self.y = y
def __repr__(self) -> str:
- return "tcod.event.%s(type=%r, x=%r, y=%r)" % (
+ return "tcod.event.{}(type={!r}, x={!r}, y={!r})".format(
self.__class__.__name__,
self.type,
self.x,
@@ -681,7 +812,7 @@ def __repr__(self) -> str:
)
def __str__(self) -> str:
- return "<%s, x=%r, y=%r)" % (
+ return "<{}, x={!r}, y={!r})".format(
super().__str__().strip("<>"),
self.x,
self.y,
@@ -689,14 +820,15 @@ def __str__(self) -> str:
class WindowResized(WindowEvent):
- """
+ """Window resized event.
+
Attributes:
- type (str): "WINDOWRESIZED" or "WINDOWSIZECHANGED"
width (int): The current width of the window.
height (int): The current height of the window.
"""
- type: Literal["WINDOWRESIZED", "WINDOWSIZECHANGED"] # type: ignore[assignment,misc]
+ type: Final[Literal["WINDOWRESIZED", "WINDOWSIZECHANGED"]] # type: ignore[assignment,misc]
+ """WINDOWRESIZED" or "WINDOWSIZECHANGED"""
def __init__(self, type: str, width: int, height: int) -> None:
super().__init__(type)
@@ -704,7 +836,7 @@ def __init__(self, type: str, width: int, height: int) -> None:
self.height = height
def __repr__(self) -> str:
- return "tcod.event.%s(type=%r, width=%r, height=%r)" % (
+ return "tcod.event.{}(type={!r}, width={!r}, height={!r})".format(
self.__class__.__name__,
self.type,
self.width,
@@ -712,18 +844,336 @@ def __repr__(self) -> str:
)
def __str__(self) -> str:
- return "<%s, width=%r, height=%r)" % (
+ return "<{}, width={!r}, height={!r})".format(
super().__str__().strip("<>"),
self.width,
self.height,
)
-class Undefined(Event):
- """This class is a place holder for SDL events without their own tcod.event
- class.
+class JoystickEvent(Event):
+ """A base class for joystick events.
+
+ .. versionadded:: 13.8
+ """
+
+ def __init__(self, type: str, which: int) -> None:
+ super().__init__(type)
+ self.which = which
+ """The ID of the joystick this event is for."""
+
+ @property
+ def joystick(self) -> tcod.sdl.joystick.Joystick:
+ if self.type == "JOYDEVICEADDED":
+ return tcod.sdl.joystick.Joystick._open(self.which)
+ return tcod.sdl.joystick.Joystick._from_instance_id(self.which)
+
+ def __repr__(self) -> str:
+ return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})"
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, which={self.which}>"
+
+
+class JoystickAxis(JoystickEvent):
+ """When a joystick axis changes in value.
+
+ .. versionadded:: 13.8
+
+ .. seealso::
+ :any:`tcod.sdl.joystick`
+ """
+
+ which: int
+ """The ID of the joystick this event is for."""
+
+ def __init__(self, type: str, which: int, axis: int, value: int) -> None:
+ super().__init__(type, which)
+ self.axis = axis
+ """The index of the changed axis."""
+ self.value = value
+ """The raw value of the axis in the range -32768 to 32767."""
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> JoystickAxis:
+ return cls("JOYAXISMOTION", sdl_event.jaxis.which, sdl_event.jaxis.axis, sdl_event.jaxis.value)
+
+ def __repr__(self) -> str:
+ return (
+ f"tcod.event.{self.__class__.__name__}"
+ f"(type={self.type!r}, which={self.which}, axis={self.axis}, value={self.value})"
+ )
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, axis={self.axis}, value={self.value}>"
+
+
+class JoystickBall(JoystickEvent):
+ """When a joystick ball is moved.
+
+ .. versionadded:: 13.8
+
+ .. seealso::
+ :any:`tcod.sdl.joystick`
+ """
+
+ which: int
+ """The ID of the joystick this event is for."""
+
+ def __init__(self, type: str, which: int, ball: int, dx: int, dy: int) -> None:
+ super().__init__(type, which)
+ self.ball = ball
+ """The index of the moved ball."""
+ self.dx = dx
+ """The X motion of the ball."""
+ self.dy = dy
+ """The Y motion of the ball."""
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> JoystickBall:
+ return cls(
+ "JOYBALLMOTION", sdl_event.jball.which, sdl_event.jball.ball, sdl_event.jball.xrel, sdl_event.jball.yrel
+ )
+
+ def __repr__(self) -> str:
+ return (
+ f"tcod.event.{self.__class__.__name__}"
+ f"(type={self.type!r}, which={self.which}, ball={self.ball}, dx={self.dx}, dy={self.dy})"
+ )
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, ball={self.ball}, dx={self.dx}, dy={self.dy}>"
+
+
+class JoystickHat(JoystickEvent):
+ """When a joystick hat changes direction.
+
+ .. versionadded:: 13.8
+
+ .. seealso::
+ :any:`tcod.sdl.joystick`
+ """
+
+ which: int
+ """The ID of the joystick this event is for."""
+
+ def __init__(self, type: str, which: int, x: Literal[-1, 0, 1], y: Literal[-1, 0, 1]) -> None:
+ super().__init__(type, which)
+ self.x = x
+ """The new X direction of the hat."""
+ self.y = y
+ """The new Y direction of the hat."""
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> JoystickHat:
+ return cls("JOYHATMOTION", sdl_event.jhat.which, *_HAT_DIRECTIONS[sdl_event.jhat.hat])
+
+ def __repr__(self) -> str:
+ return (
+ f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, x={self.x}, y={self.y})"
+ )
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, x={self.x}, y={self.y}>"
+
+
+class JoystickButton(JoystickEvent):
+ """When a joystick button is pressed or released.
+
+ .. versionadded:: 13.8
+
+ Example::
+
+ for event in tcod.event.get():
+ match event:
+ case JoystickButton(which=which, button=button, pressed=True):
+ print(f"Pressed {button=} on controller {which}.")
+ case JoystickButton(which=which, button=button, pressed=False):
+ print(f"Released {button=} on controller {which}.")
+ """
+
+ which: int
+ """The ID of the joystick this event is for."""
+
+ def __init__(self, type: str, which: int, button: int) -> None:
+ super().__init__(type, which)
+ self.button = button
+ """The index of the button this event is for."""
+
+ @property
+ def pressed(self) -> bool:
+ """True if the joystick button has been pressed, False when the button was released."""
+ return self.type == "JOYBUTTONDOWN"
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> JoystickButton:
+ type = {lib.SDL_JOYBUTTONDOWN: "JOYBUTTONDOWN", lib.SDL_JOYBUTTONUP: "JOYBUTTONUP"}[sdl_event.type]
+ return cls(type, sdl_event.jbutton.which, sdl_event.jbutton.button)
+
+ def __repr__(self) -> str:
+ return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, button={self.button})"
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, button={self.button}>"
+
+
+class JoystickDevice(JoystickEvent):
+ """An event for when a joystick is added or removed.
+
+ .. versionadded:: 13.8
+
+ Example::
+
+ joysticks: set[tcod.sdl.joystick.Joystick] = {}
+ for event in tcod.event.get():
+ match event:
+ case tcod.event.JoystickDevice(type="JOYDEVICEADDED", joystick=new_joystick):
+ joysticks.add(new_joystick)
+ case tcod.event.JoystickDevice(type="JOYDEVICEREMOVED", joystick=joystick):
+ joysticks.remove(joystick)
+ """
+
+ type: Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]] # type: ignore[misc]
+
+ which: int
+ """When type="JOYDEVICEADDED" this is the device ID.
+ When type="JOYDEVICEREMOVED" this is the instance ID.
+ """
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> JoystickDevice:
+ type = {lib.SDL_JOYDEVICEADDED: "JOYDEVICEADDED", lib.SDL_JOYDEVICEREMOVED: "JOYDEVICEREMOVED"}[sdl_event.type]
+ return cls(type, sdl_event.jdevice.which)
+
+
+class ControllerEvent(Event):
+ """Base class for controller events.
+
+ .. versionadded:: 13.8
+ """
+
+ def __init__(self, type: str, which: int) -> None:
+ super().__init__(type)
+ self.which = which
+ """The ID of the joystick this event is for."""
+
+ @property
+ def controller(self) -> tcod.sdl.joystick.GameController:
+ """The :any:`GameController` for this event."""
+ if self.type == "CONTROLLERDEVICEADDED":
+ return tcod.sdl.joystick.GameController._open(self.which)
+ return tcod.sdl.joystick.GameController._from_instance_id(self.which)
+
+ def __repr__(self) -> str:
+ return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})"
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, which={self.which}>"
+
+
+class ControllerAxis(ControllerEvent):
+ """When a controller axis is moved.
+
+ .. versionadded:: 13.8
"""
+ type: Final[Literal["CONTROLLERAXISMOTION"]] # type: ignore[misc]
+
+ def __init__(self, type: str, which: int, axis: tcod.sdl.joystick.ControllerAxis, value: int) -> None:
+ super().__init__(type, which)
+ self.axis = axis
+ """Which axis is being moved. One of :any:`ControllerAxis`."""
+ self.value = value
+ """The new value of this events axis.
+
+ This will be -32768 to 32767 for all axes except for triggers which are 0 to 32767 instead."""
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> ControllerAxis:
+ return cls(
+ "CONTROLLERAXISMOTION",
+ sdl_event.caxis.which,
+ tcod.sdl.joystick.ControllerAxis(sdl_event.caxis.axis),
+ sdl_event.caxis.value,
+ )
+
+ def __repr__(self) -> str:
+ return (
+ f"tcod.event.{self.__class__.__name__}"
+ f"(type={self.type!r}, which={self.which}, axis={self.axis}, value={self.value})"
+ )
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, axis={self.axis}, value={self.value}>"
+
+
+class ControllerButton(ControllerEvent):
+ """When a controller button is pressed or released.
+
+ .. versionadded:: 13.8
+ """
+
+ type: Final[Literal["CONTROLLERBUTTONDOWN", "CONTROLLERBUTTONUP"]] # type: ignore[misc]
+
+ def __init__(self, type: str, which: int, button: tcod.sdl.joystick.ControllerButton, pressed: bool) -> None:
+ super().__init__(type, which)
+ self.button = button
+ """The button for this event. One of :any:`ControllerButton`."""
+ self.pressed = pressed
+ """True if the button was pressed, False if it was released."""
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> ControllerButton:
+ type = {
+ lib.SDL_CONTROLLERBUTTONDOWN: "CONTROLLERBUTTONDOWN",
+ lib.SDL_CONTROLLERBUTTONUP: "CONTROLLERBUTTONUP",
+ }[sdl_event.type]
+ return cls(
+ type,
+ sdl_event.cbutton.which,
+ tcod.sdl.joystick.ControllerButton(sdl_event.cbutton.button),
+ sdl_event.cbutton.state == lib.SDL_PRESSED,
+ )
+
+ def __repr__(self) -> str:
+ return (
+ f"tcod.event.{self.__class__.__name__}"
+ f"(type={self.type!r}, which={self.which}, button={self.button}, pressed={self.pressed})"
+ )
+
+ def __str__(self) -> str:
+ prefix = super().__str__().strip("<>")
+ return f"<{prefix}, button={self.button}, pressed={self.pressed}>"
+
+
+class ControllerDevice(ControllerEvent):
+ """When a controller is added, removed, or remapped.
+
+ .. versionadded:: 13.8
+ """
+
+ type: Final[Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]] # type: ignore[misc]
+
+ @classmethod
+ def from_sdl_event(cls, sdl_event: Any) -> ControllerDevice:
+ type = {
+ lib.SDL_CONTROLLERDEVICEADDED: "CONTROLLERDEVICEADDED",
+ lib.SDL_CONTROLLERDEVICEREMOVED: "CONTROLLERDEVICEREMOVED",
+ lib.SDL_CONTROLLERDEVICEREMAPPED: "CONTROLLERDEVICEREMAPPED",
+ }[sdl_event.type]
+ return cls(type, sdl_event.cdevice.which)
+
+
+class Undefined(Event):
+ """This class is a place holder for SDL events without their own tcod.event class."""
+
def __init__(self) -> None:
super().__init__("")
@@ -739,7 +1189,7 @@ def __str__(self) -> str:
return ""
-_SDL_TO_CLASS_TABLE: Dict[int, Any] = {
+_SDL_TO_CLASS_TABLE: dict[int, type[Event]] = {
lib.SDL_QUIT: Quit,
lib.SDL_KEYDOWN: KeyDown,
lib.SDL_KEYUP: KeyUp,
@@ -749,54 +1199,36 @@ def __str__(self) -> str:
lib.SDL_MOUSEWHEEL: MouseWheel,
lib.SDL_TEXTINPUT: TextInput,
lib.SDL_WINDOWEVENT: WindowEvent,
+ lib.SDL_JOYAXISMOTION: JoystickAxis,
+ lib.SDL_JOYBALLMOTION: JoystickBall,
+ lib.SDL_JOYHATMOTION: JoystickHat,
+ lib.SDL_JOYBUTTONDOWN: JoystickButton,
+ lib.SDL_JOYBUTTONUP: JoystickButton,
+ lib.SDL_JOYDEVICEADDED: JoystickDevice,
+ lib.SDL_JOYDEVICEREMOVED: JoystickDevice,
+ lib.SDL_CONTROLLERAXISMOTION: ControllerAxis,
+ lib.SDL_CONTROLLERBUTTONDOWN: ControllerButton,
+ lib.SDL_CONTROLLERBUTTONUP: ControllerButton,
+ lib.SDL_CONTROLLERDEVICEADDED: ControllerDevice,
+ lib.SDL_CONTROLLERDEVICEREMOVED: ControllerDevice,
+ lib.SDL_CONTROLLERDEVICEREMAPPED: ControllerDevice,
}
-def get() -> Iterator[Any]:
- """Return an iterator for all pending events.
-
- Events are processed as the iterator is consumed. Breaking out of, or
- discarding the iterator will leave the remaining events on the event queue.
- It is also safe to call this function inside of a loop that is already
- handling events (the event iterator is reentrant.)
-
- Example::
+def _parse_event(sdl_event: Any) -> Event:
+ """Convert a C SDL_Event* type into a tcod Event sub-class."""
+ if sdl_event.type not in _SDL_TO_CLASS_TABLE:
+ return Undefined.from_sdl_event(sdl_event)
+ return _SDL_TO_CLASS_TABLE[sdl_event.type].from_sdl_event(sdl_event)
- context: tcod.context.Context # Context object initialized earlier.
- for event in tcod.event.get():
- context.convert_event(event) # Add tile coordinates to mouse events.
- if isinstance(event, tcod.event.Quit):
- print(event)
- raise SystemExit()
- elif isinstance(event, tcod.event.KeyDown):
- print(event) # Prints the Scancode and KeySym enums for this event.
- elif isinstance(event, tcod.event.MouseButtonDown):
- print(event) # Prints the mouse button constant names for this event.
- elif isinstance(event, tcod.event.MouseMotion):
- print(event) # Prints the mouse button mask bits in a readable format.
- else:
- print(event) # Print any unhandled events.
- # For loop exits after all current events are processed.
-
- Python 3.10 introduced `match statements `_
- which can be used to dispatch events more gracefully:
- Example::
+def get() -> Iterator[Any]:
+ """Return an iterator for all pending events.
- context: tcod.context.Context # Context object initialized earlier.
- for event in tcod.event.get():
- context.convert_event(event) # Add tile coordinates to mouse events.
- match event:
- case tcod.event.Quit():
- raise SystemExit()
- case tcod.event.KeyDown(sym, scancode, mod, repeat):
- print(f"KeyDown: {sym=}, {scancode=}, {mod=}, {repeat=}")
- case tcod.event.MouseButtonDown(button, pixel, tile):
- print(f"MouseButtonDown: {button=}, {pixel=}, {tile=}")
- case tcod.event.MouseMotion(pixel, pixel_motion, tile, tile_motion):
- print(f"MouseMotion: {pixel=}, {pixel_motion=}, {tile=}, {tile_motion=}")
- case tcod.event.Event() as event:
- print(event) # Show any unhandled events.
+ Events are processed as the iterator is consumed.
+ Breaking out of, or discarding the iterator will leave the remaining events on the event queue.
+ It is also safe to call this function inside of a loop that is already handling events
+ (the event iterator is reentrant.)
"""
sdl_event = ffi.new("SDL_Event*")
while lib.SDL_PollEvent(sdl_event):
@@ -806,7 +1238,7 @@ def get() -> Iterator[Any]:
yield Undefined.from_sdl_event(sdl_event)
-def wait(timeout: Optional[float] = None) -> Iterator[Any]:
+def wait(timeout: float | None = None) -> Iterator[Any]:
"""Block until events exist, then return an event iterator.
`timeout` is the maximum number of seconds to wait as a floating point
@@ -821,7 +1253,7 @@ def wait(timeout: Optional[float] = None) -> Iterator[Any]:
context: tcod.context.Context # Context object initialized earlier.
while True: # Main game-loop.
- console: tcod.Console # Console used for rendering.
+ console: tcod.console.Console # Console used for rendering.
... # Render the frame to `console` and then:
context.present(console) # Show the console to the display.
# The ordering to draw first before waiting for events is important.
@@ -838,15 +1270,14 @@ def wait(timeout: Optional[float] = None) -> Iterator[Any]:
class EventDispatch(Generic[T]):
- '''This class dispatches events to methods depending on the events type
- attribute.
+ '''Dispatches events to methods depending on the events type attribute.
- To use this class, make a sub-class and override the relevant `ev_*`
- methods. Then send events to the dispatch method.
+ To use this class, make a sub-class and override the relevant `ev_*` methods.
+ Then send events to the dispatch method.
.. versionchanged:: 11.12
- This is now a generic class. The type hists at the return value of
- :any:`dispatch` and the `ev_*` methods.
+ This is now a generic class.
+ The type hints at the return value of :any:`dispatch` and the `ev_*` methods.
Example::
@@ -854,35 +1285,35 @@ class EventDispatch(Generic[T]):
MOVE_KEYS = { # key_symbol: (x, y)
# Arrow keys.
- tcod.event.K_LEFT: (-1, 0),
- tcod.event.K_RIGHT: (1, 0),
- tcod.event.K_UP: (0, -1),
- tcod.event.K_DOWN: (0, 1),
- tcod.event.K_HOME: (-1, -1),
- tcod.event.K_END: (-1, 1),
- tcod.event.K_PAGEUP: (1, -1),
- tcod.event.K_PAGEDOWN: (1, 1),
- tcod.event.K_PERIOD: (0, 0),
+ tcod.event.KeySym.LEFT: (-1, 0),
+ tcod.event.KeySym.RIGHT: (1, 0),
+ tcod.event.KeySym.UP: (0, -1),
+ tcod.event.KeySym.DOWN: (0, 1),
+ tcod.event.KeySym.HOME: (-1, -1),
+ tcod.event.KeySym.END: (-1, 1),
+ tcod.event.KeySym.PAGEUP: (1, -1),
+ tcod.event.KeySym.PAGEDOWN: (1, 1),
+ tcod.event.KeySym.PERIOD: (0, 0),
# Numpad keys.
- tcod.event.K_KP_1: (-1, 1),
- tcod.event.K_KP_2: (0, 1),
- tcod.event.K_KP_3: (1, 1),
- tcod.event.K_KP_4: (-1, 0),
- tcod.event.K_KP_5: (0, 0),
- tcod.event.K_KP_6: (1, 0),
- tcod.event.K_KP_7: (-1, -1),
- tcod.event.K_KP_8: (0, -1),
- tcod.event.K_KP_9: (1, -1),
- tcod.event.K_CLEAR: (0, 0), # Numpad `clear` key.
+ tcod.event.KeySym.KP_1: (-1, 1),
+ tcod.event.KeySym.KP_2: (0, 1),
+ tcod.event.KeySym.KP_3: (1, 1),
+ tcod.event.KeySym.KP_4: (-1, 0),
+ tcod.event.KeySym.KP_5: (0, 0),
+ tcod.event.KeySym.KP_6: (1, 0),
+ tcod.event.KeySym.KP_7: (-1, -1),
+ tcod.event.KeySym.KP_8: (0, -1),
+ tcod.event.KeySym.KP_9: (1, -1),
+ tcod.event.KeySym.CLEAR: (0, 0), # Numpad `clear` key.
# Vi Keys.
- tcod.event.K_h: (-1, 0),
- tcod.event.K_j: (0, 1),
- tcod.event.K_k: (0, -1),
- tcod.event.K_l: (1, 0),
- tcod.event.K_y: (-1, -1),
- tcod.event.K_u: (1, -1),
- tcod.event.K_b: (-1, 1),
- tcod.event.K_n: (1, 1),
+ tcod.event.KeySym.h: (-1, 0),
+ tcod.event.KeySym.j: (0, 1),
+ tcod.event.KeySym.k: (0, -1),
+ tcod.event.KeySym.l: (1, 0),
+ tcod.event.KeySym.y: (-1, -1),
+ tcod.event.KeySym.u: (1, -1),
+ tcod.event.KeySym.b: (-1, 1),
+ tcod.event.KeySym.n: (1, 1),
}
@@ -908,7 +1339,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
if event.sym in MOVE_KEYS:
# Send movement keys to the cmd_move method with parameters.
self.cmd_move(*MOVE_KEYS[event.sym])
- elif event.sym == tcod.event.K_ESCAPE:
+ elif event.sym == tcod.event.KeySym.ESCAPE:
self.cmd_escape()
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> None:
@@ -940,9 +1371,9 @@ def cmd_quit(self) -> None:
tcod.console_flush()
for event in tcod.event.wait():
state.dispatch(event)
- ''' # noqa: E501
+ '''
- def dispatch(self, event: Any) -> Optional[T]:
+ def dispatch(self, event: Any) -> T | None:
"""Send an event to an `ev_*` method.
`*` will be the `event.type` attribute converted to lower-case.
@@ -961,93 +1392,175 @@ def dispatch(self, event: Any) -> Optional[T]:
stacklevel=2,
)
return None
- func: Callable[[Any], Optional[T]] = getattr(self, "ev_%s" % (event.type.lower(),))
+ func_name = f"ev_{event.type.lower()}"
+ func: Callable[[Any], T | None] | None = getattr(self, func_name, None)
+ if func is None:
+ warnings.warn(f"{func_name} is missing from this EventDispatch object.", RuntimeWarning, stacklevel=2)
+ return None
return func(event)
def event_get(self) -> None:
for event in get():
self.dispatch(event)
- def event_wait(self, timeout: Optional[float]) -> None:
+ def event_wait(self, timeout: float | None) -> None:
wait(timeout)
self.event_get()
- def ev_quit(self, event: tcod.event.Quit) -> Optional[T]:
+ def ev_quit(self, event: tcod.event.Quit) -> T | None:
"""Called when the termination of the program is requested."""
- def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[T]:
+ def ev_keydown(self, event: tcod.event.KeyDown) -> T | None:
"""Called when a keyboard key is pressed or repeated."""
- def ev_keyup(self, event: tcod.event.KeyUp) -> Optional[T]:
+ def ev_keyup(self, event: tcod.event.KeyUp) -> T | None:
"""Called when a keyboard key is released."""
- def ev_mousemotion(self, event: tcod.event.MouseMotion) -> Optional[T]:
+ def ev_mousemotion(self, event: tcod.event.MouseMotion) -> T | None:
"""Called when the mouse is moved."""
- def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[T]:
+ def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> T | None:
"""Called when a mouse button is pressed."""
- def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> Optional[T]:
+ def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> T | None:
"""Called when a mouse button is released."""
- def ev_mousewheel(self, event: tcod.event.MouseWheel) -> Optional[T]:
+ def ev_mousewheel(self, event: tcod.event.MouseWheel) -> T | None:
"""Called when the mouse wheel is scrolled."""
- def ev_textinput(self, event: tcod.event.TextInput) -> Optional[T]:
+ def ev_textinput(self, event: tcod.event.TextInput) -> T | None:
"""Called to handle Unicode input."""
- def ev_windowshown(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowshown(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window is shown."""
- def ev_windowhidden(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowhidden(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window is hidden."""
- def ev_windowexposed(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowexposed(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when a window is exposed, and needs to be refreshed.
This usually means a call to :any:`tcod.console_flush` is necessary.
"""
- def ev_windowmoved(self, event: "tcod.event.WindowMoved") -> Optional[T]:
+ def ev_windowmoved(self, event: tcod.event.WindowMoved) -> T | None:
"""Called when the window is moved."""
- def ev_windowresized(self, event: "tcod.event.WindowResized") -> Optional[T]:
+ def ev_windowresized(self, event: tcod.event.WindowResized) -> T | None:
"""Called when the window is resized."""
- def ev_windowsizechanged(self, event: "tcod.event.WindowResized") -> Optional[T]:
+ def ev_windowsizechanged(self, event: tcod.event.WindowResized) -> T | None:
"""Called when the system or user changes the size of the window."""
- def ev_windowminimized(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowminimized(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window is minimized."""
- def ev_windowmaximized(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowmaximized(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window is maximized."""
- def ev_windowrestored(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowrestored(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window is restored."""
- def ev_windowenter(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowenter(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window gains mouse focus."""
- def ev_windowleave(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowleave(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window loses mouse focus."""
- def ev_windowfocusgained(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowfocusgained(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window gains keyboard focus."""
- def ev_windowfocuslost(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowfocuslost(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window loses keyboard focus."""
- def ev_windowclose(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowclose(self, event: tcod.event.WindowEvent) -> T | None:
"""Called when the window manager requests the window to be closed."""
- def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> T | None:
pass
- def ev_windowhittest(self, event: tcod.event.WindowEvent) -> Optional[T]:
+ def ev_windowhittest(self, event: tcod.event.WindowEvent) -> T | None:
pass
- def ev_(self, event: Any) -> Optional[T]:
+ def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> T | None:
+ """Called when a joystick analog is moved.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> T | None:
+ """Called when a joystick ball is moved.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> T | None:
+ """Called when a joystick hat is moved.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> T | None:
+ """Called when a joystick button is pressed.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> T | None:
+ """Called when a joystick button is released.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> T | None:
+ """Called when a joystick is added.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> T | None:
+ """Called when a joystick is removed.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> T | None:
+ """Called when a controller analog is moved.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> T | None:
+ """Called when a controller button is pressed.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> T | None:
+ """Called when a controller button is released.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> T | None:
+ """Called when a standard controller is added.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice) -> T | None:
+ """Called when a standard controller is removed.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice) -> T | None:
+ """Called when a standard controller is remapped.
+
+ .. versionadded:: 13.8
+ """
+
+ def ev_(self, event: Any) -> T | None:
pass
@@ -1065,10 +1578,68 @@ def get_mouse_state() -> MouseState:
@ffi.def_extern() # type: ignore
-def _pycall_event_watch(userdata: Any, sdl_event: Any) -> int:
+def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int:
+ callback: Callable[[Event], None] = ffi.from_handle(userdata)
+ callback(_parse_event(sdl_event))
return 0
+_EventCallback = TypeVar("_EventCallback", bound=Callable[[Event], None])
+_event_watch_handles: dict[Callable[[Event], None], Any] = {} # Callbacks and their FFI handles.
+
+
+def add_watch(callback: _EventCallback) -> _EventCallback:
+ """Add a callback for watching events.
+
+ This function can be called with the callback to register, or be used as a decorator.
+
+ Callbacks added as event watchers can later be removed with :any:`tcod.event.remove_watch`.
+
+ .. warning::
+ How uncaught exceptions in a callback are handled is not currently defined by tcod.
+ They will likely be handled by :any:`sys.unraisablehook`.
+ This may be later changed to pass the exception to a :any:`tcod.event.get` or :any:`tcod.event.wait` call.
+
+ Args:
+ callback (Callable[[Event], None]):
+ A function which accepts :any:`Event` parameters.
+
+ Example::
+
+ import tcod.event
+
+ @tcod.event.add_watch
+ def handle_events(event: tcod.event.Event) -> None:
+ if isinstance(event, tcod.event.KeyDown):
+ print(event)
+
+ .. versionadded:: 13.4
+ """
+ if callback in _event_watch_handles:
+ warnings.warn(f"{callback} is already an active event watcher, nothing was added.", RuntimeWarning)
+ return callback
+ handle = _event_watch_handles[callback] = ffi.new_handle(callback)
+ lib.SDL_AddEventWatch(lib._sdl_event_watcher, handle)
+ return callback
+
+
+def remove_watch(callback: Callable[[Event], None]) -> None:
+ """Remove a callback as an event watcher.
+
+ Args:
+ callback (Callable[[Event], None]):
+ A function which has been previously registered with :any:`tcod.event.add_watch`.
+
+ .. versionadded:: 13.4
+ """
+ if callback not in _event_watch_handles:
+ warnings.warn(f"{callback} is not an active event watcher, nothing was removed.", RuntimeWarning)
+ return
+ handle = _event_watch_handles[callback]
+ lib.SDL_DelEventWatch(lib._sdl_event_watcher, handle)
+ del _event_watch_handles[callback]
+
+
def get_keyboard_state() -> NDArray[np.bool_]:
"""Return a boolean array with the current keyboard state.
@@ -1080,7 +1651,7 @@ def get_keyboard_state() -> NDArray[np.bool_]:
state = tcod.event.get_keyboard_state()
# Get a WASD movement vector:
- x = int(state[tcod.event.Scancode.E]) - int(state[tcod.event.Scancode.A])
+ x = int(state[tcod.event.Scancode.D]) - int(state[tcod.event.Scancode.A])
y = int(state[tcod.event.Scancode.S]) - int(state[tcod.event.Scancode.W])
# Key with 'z' glyph is held:
@@ -1089,9 +1660,9 @@ def get_keyboard_state() -> NDArray[np.bool_]:
.. versionadded:: 12.3
"""
- numkeys = ffi.new("int[1]")
- keyboard_state = lib.SDL_GetKeyboardState(numkeys)
- out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : numkeys[0]]), dtype=np.bool_) # type: ignore
+ num_keys = ffi.new("int[1]")
+ keyboard_state = lib.SDL_GetKeyboardState(num_keys)
+ out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : num_keys[0]]), dtype=np.bool_)
out.flags["WRITEABLE"] = False # This buffer is supposed to be const.
return out
@@ -1603,6 +2174,8 @@ class Scancode(enum.IntEnum):
SLEEP = 282
APP1 = 283
APP2 = 284
+ AUDIOREWIND = 285
+ AUDIOFASTFORWARD = 286
# --- end ---
@property
@@ -1638,7 +2211,7 @@ def scancode(self) -> Scancode:
return self
@classmethod
- def _missing_(cls, value: object) -> Optional[Scancode]:
+ def _missing_(cls, value: object) -> Scancode | None:
if not isinstance(value, int):
return None
result = cls(0)
@@ -1647,9 +2220,8 @@ def _missing_(cls, value: object) -> Optional[Scancode]:
def __eq__(self, other: Any) -> bool:
if isinstance(other, KeySym):
- raise TypeError(
- "Scancode and KeySym enums can not be compared directly." " Convert one or the other to the same type."
- )
+ msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type."
+ raise TypeError(msg)
return super().__eq__(other)
def __hash__(self) -> int:
@@ -2148,6 +2720,10 @@ class KeySym(enum.IntEnum):
KBDILLUMUP = 1073742104
EJECT = 1073742105
SLEEP = 1073742106
+ APP1 = 1073742107
+ APP2 = 1073742108
+ AUDIOREWIND = 1073742109
+ AUDIOFASTFORWARD = 1073742110
# --- end ---
@property
@@ -2189,7 +2765,7 @@ def scancode(self) -> Scancode:
return Scancode(lib.SDL_GetScancodeFromKey(self.value))
@classmethod
- def _missing_(cls, value: object) -> Optional[KeySym]:
+ def _missing_(cls, value: object) -> KeySym | None:
if not isinstance(value, int):
return None
result = cls(0)
@@ -2198,9 +2774,8 @@ def _missing_(cls, value: object) -> Optional[KeySym]:
def __eq__(self, other: Any) -> bool:
if isinstance(other, Scancode):
- raise TypeError(
- "Scancode and KeySym enums can not be compared directly." " Convert one or the other to the same type."
- )
+ msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type."
+ raise TypeError(msg)
return super().__eq__(other)
def __hash__(self) -> int:
@@ -2212,6 +2787,35 @@ def __repr__(self) -> str:
return f"tcod.event.{self.__class__.__name__}.{self.name}"
+def __getattr__(name: str) -> int:
+ """Migrate deprecated access of event constants."""
+ value: int | None = getattr(tcod.event_constants, name, None)
+ if not value:
+ msg = f"module {__name__!r} has no attribute {name!r}"
+ raise AttributeError(msg)
+ if name.startswith("SCANCODE_"):
+ scancode = name[9:]
+ if scancode.isdigit():
+ scancode = f"N{scancode}"
+ warnings.warn(
+ "Key constants have been replaced with enums.\n"
+ f"`tcod.event.{name}` should be replaced with `tcod.event.Scancode.{scancode}`",
+ FutureWarning,
+ stacklevel=2,
+ )
+ elif name.startswith("K_"):
+ sym = name[2:]
+ if sym.isdigit():
+ sym = f"N{sym}"
+ warnings.warn(
+ "Key constants have been replaced with enums.\n"
+ f"`tcod.event.{name}` should be replaced with `tcod.event.KeySym.{sym}`",
+ FutureWarning,
+ stacklevel=2,
+ )
+ return value
+
+
__all__ = [ # noqa: F405
"Modifier",
"Point",
@@ -2239,494 +2843,28 @@ def __repr__(self) -> str:
"WindowEvent",
"WindowMoved",
"WindowResized",
+ "JoystickEvent",
+ "JoystickAxis",
+ "JoystickBall",
+ "JoystickHat",
+ "JoystickButton",
+ "JoystickDevice",
+ "ControllerEvent",
+ "ControllerAxis",
+ "ControllerButton",
+ "ControllerDevice",
"Undefined",
"get",
"wait",
"get_mouse_state",
+ "add_watch",
+ "remove_watch",
"EventDispatch",
"get_keyboard_state",
"get_modifier_state",
"Scancode",
"KeySym",
# --- From event_constants.py ---
- "SCANCODE_UNKNOWN",
- "SCANCODE_A",
- "SCANCODE_B",
- "SCANCODE_C",
- "SCANCODE_D",
- "SCANCODE_E",
- "SCANCODE_F",
- "SCANCODE_G",
- "SCANCODE_H",
- "SCANCODE_I",
- "SCANCODE_J",
- "SCANCODE_K",
- "SCANCODE_L",
- "SCANCODE_M",
- "SCANCODE_N",
- "SCANCODE_O",
- "SCANCODE_P",
- "SCANCODE_Q",
- "SCANCODE_R",
- "SCANCODE_S",
- "SCANCODE_T",
- "SCANCODE_U",
- "SCANCODE_V",
- "SCANCODE_W",
- "SCANCODE_X",
- "SCANCODE_Y",
- "SCANCODE_Z",
- "SCANCODE_1",
- "SCANCODE_2",
- "SCANCODE_3",
- "SCANCODE_4",
- "SCANCODE_5",
- "SCANCODE_6",
- "SCANCODE_7",
- "SCANCODE_8",
- "SCANCODE_9",
- "SCANCODE_0",
- "SCANCODE_RETURN",
- "SCANCODE_ESCAPE",
- "SCANCODE_BACKSPACE",
- "SCANCODE_TAB",
- "SCANCODE_SPACE",
- "SCANCODE_MINUS",
- "SCANCODE_EQUALS",
- "SCANCODE_LEFTBRACKET",
- "SCANCODE_RIGHTBRACKET",
- "SCANCODE_BACKSLASH",
- "SCANCODE_NONUSHASH",
- "SCANCODE_SEMICOLON",
- "SCANCODE_APOSTROPHE",
- "SCANCODE_GRAVE",
- "SCANCODE_COMMA",
- "SCANCODE_PERIOD",
- "SCANCODE_SLASH",
- "SCANCODE_CAPSLOCK",
- "SCANCODE_F1",
- "SCANCODE_F2",
- "SCANCODE_F3",
- "SCANCODE_F4",
- "SCANCODE_F5",
- "SCANCODE_F6",
- "SCANCODE_F7",
- "SCANCODE_F8",
- "SCANCODE_F9",
- "SCANCODE_F10",
- "SCANCODE_F11",
- "SCANCODE_F12",
- "SCANCODE_PRINTSCREEN",
- "SCANCODE_SCROLLLOCK",
- "SCANCODE_PAUSE",
- "SCANCODE_INSERT",
- "SCANCODE_HOME",
- "SCANCODE_PAGEUP",
- "SCANCODE_DELETE",
- "SCANCODE_END",
- "SCANCODE_PAGEDOWN",
- "SCANCODE_RIGHT",
- "SCANCODE_LEFT",
- "SCANCODE_DOWN",
- "SCANCODE_UP",
- "SCANCODE_NUMLOCKCLEAR",
- "SCANCODE_KP_DIVIDE",
- "SCANCODE_KP_MULTIPLY",
- "SCANCODE_KP_MINUS",
- "SCANCODE_KP_PLUS",
- "SCANCODE_KP_ENTER",
- "SCANCODE_KP_1",
- "SCANCODE_KP_2",
- "SCANCODE_KP_3",
- "SCANCODE_KP_4",
- "SCANCODE_KP_5",
- "SCANCODE_KP_6",
- "SCANCODE_KP_7",
- "SCANCODE_KP_8",
- "SCANCODE_KP_9",
- "SCANCODE_KP_0",
- "SCANCODE_KP_PERIOD",
- "SCANCODE_NONUSBACKSLASH",
- "SCANCODE_APPLICATION",
- "SCANCODE_POWER",
- "SCANCODE_KP_EQUALS",
- "SCANCODE_F13",
- "SCANCODE_F14",
- "SCANCODE_F15",
- "SCANCODE_F16",
- "SCANCODE_F17",
- "SCANCODE_F18",
- "SCANCODE_F19",
- "SCANCODE_F20",
- "SCANCODE_F21",
- "SCANCODE_F22",
- "SCANCODE_F23",
- "SCANCODE_F24",
- "SCANCODE_EXECUTE",
- "SCANCODE_HELP",
- "SCANCODE_MENU",
- "SCANCODE_SELECT",
- "SCANCODE_STOP",
- "SCANCODE_AGAIN",
- "SCANCODE_UNDO",
- "SCANCODE_CUT",
- "SCANCODE_COPY",
- "SCANCODE_PASTE",
- "SCANCODE_FIND",
- "SCANCODE_MUTE",
- "SCANCODE_VOLUMEUP",
- "SCANCODE_VOLUMEDOWN",
- "SCANCODE_KP_COMMA",
- "SCANCODE_KP_EQUALSAS400",
- "SCANCODE_INTERNATIONAL1",
- "SCANCODE_INTERNATIONAL2",
- "SCANCODE_INTERNATIONAL3",
- "SCANCODE_INTERNATIONAL4",
- "SCANCODE_INTERNATIONAL5",
- "SCANCODE_INTERNATIONAL6",
- "SCANCODE_INTERNATIONAL7",
- "SCANCODE_INTERNATIONAL8",
- "SCANCODE_INTERNATIONAL9",
- "SCANCODE_LANG1",
- "SCANCODE_LANG2",
- "SCANCODE_LANG3",
- "SCANCODE_LANG4",
- "SCANCODE_LANG5",
- "SCANCODE_LANG6",
- "SCANCODE_LANG7",
- "SCANCODE_LANG8",
- "SCANCODE_LANG9",
- "SCANCODE_ALTERASE",
- "SCANCODE_SYSREQ",
- "SCANCODE_CANCEL",
- "SCANCODE_CLEAR",
- "SCANCODE_PRIOR",
- "SCANCODE_RETURN2",
- "SCANCODE_SEPARATOR",
- "SCANCODE_OUT",
- "SCANCODE_OPER",
- "SCANCODE_CLEARAGAIN",
- "SCANCODE_CRSEL",
- "SCANCODE_EXSEL",
- "SCANCODE_KP_00",
- "SCANCODE_KP_000",
- "SCANCODE_THOUSANDSSEPARATOR",
- "SCANCODE_DECIMALSEPARATOR",
- "SCANCODE_CURRENCYUNIT",
- "SCANCODE_CURRENCYSUBUNIT",
- "SCANCODE_KP_LEFTPAREN",
- "SCANCODE_KP_RIGHTPAREN",
- "SCANCODE_KP_LEFTBRACE",
- "SCANCODE_KP_RIGHTBRACE",
- "SCANCODE_KP_TAB",
- "SCANCODE_KP_BACKSPACE",
- "SCANCODE_KP_A",
- "SCANCODE_KP_B",
- "SCANCODE_KP_C",
- "SCANCODE_KP_D",
- "SCANCODE_KP_E",
- "SCANCODE_KP_F",
- "SCANCODE_KP_XOR",
- "SCANCODE_KP_POWER",
- "SCANCODE_KP_PERCENT",
- "SCANCODE_KP_LESS",
- "SCANCODE_KP_GREATER",
- "SCANCODE_KP_AMPERSAND",
- "SCANCODE_KP_DBLAMPERSAND",
- "SCANCODE_KP_VERTICALBAR",
- "SCANCODE_KP_DBLVERTICALBAR",
- "SCANCODE_KP_COLON",
- "SCANCODE_KP_HASH",
- "SCANCODE_KP_SPACE",
- "SCANCODE_KP_AT",
- "SCANCODE_KP_EXCLAM",
- "SCANCODE_KP_MEMSTORE",
- "SCANCODE_KP_MEMRECALL",
- "SCANCODE_KP_MEMCLEAR",
- "SCANCODE_KP_MEMADD",
- "SCANCODE_KP_MEMSUBTRACT",
- "SCANCODE_KP_MEMMULTIPLY",
- "SCANCODE_KP_MEMDIVIDE",
- "SCANCODE_KP_PLUSMINUS",
- "SCANCODE_KP_CLEAR",
- "SCANCODE_KP_CLEARENTRY",
- "SCANCODE_KP_BINARY",
- "SCANCODE_KP_OCTAL",
- "SCANCODE_KP_DECIMAL",
- "SCANCODE_KP_HEXADECIMAL",
- "SCANCODE_LCTRL",
- "SCANCODE_LSHIFT",
- "SCANCODE_LALT",
- "SCANCODE_LGUI",
- "SCANCODE_RCTRL",
- "SCANCODE_RSHIFT",
- "SCANCODE_RALT",
- "SCANCODE_RGUI",
- "SCANCODE_MODE",
- "SCANCODE_AUDIONEXT",
- "SCANCODE_AUDIOPREV",
- "SCANCODE_AUDIOSTOP",
- "SCANCODE_AUDIOPLAY",
- "SCANCODE_AUDIOMUTE",
- "SCANCODE_MEDIASELECT",
- "SCANCODE_WWW",
- "SCANCODE_MAIL",
- "SCANCODE_CALCULATOR",
- "SCANCODE_COMPUTER",
- "SCANCODE_AC_SEARCH",
- "SCANCODE_AC_HOME",
- "SCANCODE_AC_BACK",
- "SCANCODE_AC_FORWARD",
- "SCANCODE_AC_STOP",
- "SCANCODE_AC_REFRESH",
- "SCANCODE_AC_BOOKMARKS",
- "SCANCODE_BRIGHTNESSDOWN",
- "SCANCODE_BRIGHTNESSUP",
- "SCANCODE_DISPLAYSWITCH",
- "SCANCODE_KBDILLUMTOGGLE",
- "SCANCODE_KBDILLUMDOWN",
- "SCANCODE_KBDILLUMUP",
- "SCANCODE_EJECT",
- "SCANCODE_SLEEP",
- "SCANCODE_APP1",
- "SCANCODE_APP2",
- "K_UNKNOWN",
- "K_BACKSPACE",
- "K_TAB",
- "K_RETURN",
- "K_ESCAPE",
- "K_SPACE",
- "K_EXCLAIM",
- "K_QUOTEDBL",
- "K_HASH",
- "K_DOLLAR",
- "K_PERCENT",
- "K_AMPERSAND",
- "K_QUOTE",
- "K_LEFTPAREN",
- "K_RIGHTPAREN",
- "K_ASTERISK",
- "K_PLUS",
- "K_COMMA",
- "K_MINUS",
- "K_PERIOD",
- "K_SLASH",
- "K_0",
- "K_1",
- "K_2",
- "K_3",
- "K_4",
- "K_5",
- "K_6",
- "K_7",
- "K_8",
- "K_9",
- "K_COLON",
- "K_SEMICOLON",
- "K_LESS",
- "K_EQUALS",
- "K_GREATER",
- "K_QUESTION",
- "K_AT",
- "K_LEFTBRACKET",
- "K_BACKSLASH",
- "K_RIGHTBRACKET",
- "K_CARET",
- "K_UNDERSCORE",
- "K_BACKQUOTE",
- "K_a",
- "K_b",
- "K_c",
- "K_d",
- "K_e",
- "K_f",
- "K_g",
- "K_h",
- "K_i",
- "K_j",
- "K_k",
- "K_l",
- "K_m",
- "K_n",
- "K_o",
- "K_p",
- "K_q",
- "K_r",
- "K_s",
- "K_t",
- "K_u",
- "K_v",
- "K_w",
- "K_x",
- "K_y",
- "K_z",
- "K_DELETE",
- "K_SCANCODE_MASK",
- "K_CAPSLOCK",
- "K_F1",
- "K_F2",
- "K_F3",
- "K_F4",
- "K_F5",
- "K_F6",
- "K_F7",
- "K_F8",
- "K_F9",
- "K_F10",
- "K_F11",
- "K_F12",
- "K_PRINTSCREEN",
- "K_SCROLLLOCK",
- "K_PAUSE",
- "K_INSERT",
- "K_HOME",
- "K_PAGEUP",
- "K_END",
- "K_PAGEDOWN",
- "K_RIGHT",
- "K_LEFT",
- "K_DOWN",
- "K_UP",
- "K_NUMLOCKCLEAR",
- "K_KP_DIVIDE",
- "K_KP_MULTIPLY",
- "K_KP_MINUS",
- "K_KP_PLUS",
- "K_KP_ENTER",
- "K_KP_1",
- "K_KP_2",
- "K_KP_3",
- "K_KP_4",
- "K_KP_5",
- "K_KP_6",
- "K_KP_7",
- "K_KP_8",
- "K_KP_9",
- "K_KP_0",
- "K_KP_PERIOD",
- "K_APPLICATION",
- "K_POWER",
- "K_KP_EQUALS",
- "K_F13",
- "K_F14",
- "K_F15",
- "K_F16",
- "K_F17",
- "K_F18",
- "K_F19",
- "K_F20",
- "K_F21",
- "K_F22",
- "K_F23",
- "K_F24",
- "K_EXECUTE",
- "K_HELP",
- "K_MENU",
- "K_SELECT",
- "K_STOP",
- "K_AGAIN",
- "K_UNDO",
- "K_CUT",
- "K_COPY",
- "K_PASTE",
- "K_FIND",
- "K_MUTE",
- "K_VOLUMEUP",
- "K_VOLUMEDOWN",
- "K_KP_COMMA",
- "K_KP_EQUALSAS400",
- "K_ALTERASE",
- "K_SYSREQ",
- "K_CANCEL",
- "K_CLEAR",
- "K_PRIOR",
- "K_RETURN2",
- "K_SEPARATOR",
- "K_OUT",
- "K_OPER",
- "K_CLEARAGAIN",
- "K_CRSEL",
- "K_EXSEL",
- "K_KP_00",
- "K_KP_000",
- "K_THOUSANDSSEPARATOR",
- "K_DECIMALSEPARATOR",
- "K_CURRENCYUNIT",
- "K_CURRENCYSUBUNIT",
- "K_KP_LEFTPAREN",
- "K_KP_RIGHTPAREN",
- "K_KP_LEFTBRACE",
- "K_KP_RIGHTBRACE",
- "K_KP_TAB",
- "K_KP_BACKSPACE",
- "K_KP_A",
- "K_KP_B",
- "K_KP_C",
- "K_KP_D",
- "K_KP_E",
- "K_KP_F",
- "K_KP_XOR",
- "K_KP_POWER",
- "K_KP_PERCENT",
- "K_KP_LESS",
- "K_KP_GREATER",
- "K_KP_AMPERSAND",
- "K_KP_DBLAMPERSAND",
- "K_KP_VERTICALBAR",
- "K_KP_DBLVERTICALBAR",
- "K_KP_COLON",
- "K_KP_HASH",
- "K_KP_SPACE",
- "K_KP_AT",
- "K_KP_EXCLAM",
- "K_KP_MEMSTORE",
- "K_KP_MEMRECALL",
- "K_KP_MEMCLEAR",
- "K_KP_MEMADD",
- "K_KP_MEMSUBTRACT",
- "K_KP_MEMMULTIPLY",
- "K_KP_MEMDIVIDE",
- "K_KP_PLUSMINUS",
- "K_KP_CLEAR",
- "K_KP_CLEARENTRY",
- "K_KP_BINARY",
- "K_KP_OCTAL",
- "K_KP_DECIMAL",
- "K_KP_HEXADECIMAL",
- "K_LCTRL",
- "K_LSHIFT",
- "K_LALT",
- "K_LGUI",
- "K_RCTRL",
- "K_RSHIFT",
- "K_RALT",
- "K_RGUI",
- "K_MODE",
- "K_AUDIONEXT",
- "K_AUDIOPREV",
- "K_AUDIOSTOP",
- "K_AUDIOPLAY",
- "K_AUDIOMUTE",
- "K_MEDIASELECT",
- "K_WWW",
- "K_MAIL",
- "K_CALCULATOR",
- "K_COMPUTER",
- "K_AC_SEARCH",
- "K_AC_HOME",
- "K_AC_BACK",
- "K_AC_FORWARD",
- "K_AC_STOP",
- "K_AC_REFRESH",
- "K_AC_BOOKMARKS",
- "K_BRIGHTNESSDOWN",
- "K_BRIGHTNESSUP",
- "K_DISPLAYSWITCH",
- "K_KBDILLUMTOGGLE",
- "K_KBDILLUMDOWN",
- "K_KBDILLUMUP",
- "K_EJECT",
- "K_SLEEP",
"KMOD_NONE",
"KMOD_LSHIFT",
"KMOD_RSHIFT",
diff --git a/tcod/event_constants.py b/tcod/event_constants.py
index 6d6069be..d10eeb74 100644
--- a/tcod/event_constants.py
+++ b/tcod/event_constants.py
@@ -245,6 +245,8 @@
SCANCODE_SLEEP = 282
SCANCODE_APP1 = 283
SCANCODE_APP2 = 284
+SCANCODE_AUDIOREWIND = 285
+SCANCODE_AUDIOFASTFORWARD = 286
# --- SDL keyboard symbols ---
K_UNKNOWN = 0
@@ -484,6 +486,10 @@
K_KBDILLUMUP = 1073742104
K_EJECT = 1073742105
K_SLEEP = 1073742106
+K_APP1 = 1073742107
+K_APP2 = 1073742108
+K_AUDIOREWIND = 1073742109
+K_AUDIOFASTFORWARD = 1073742110
# --- SDL keyboard modifiers ---
KMOD_NONE = 0
@@ -502,7 +508,7 @@
KMOD_NUM = 4096
KMOD_CAPS = 8192
KMOD_MODE = 16384
-KMOD_RESERVED = 32768
+KMOD_SCROLL = 32768
_REVERSE_MOD_TABLE = {
0: "KMOD_NONE",
1: "KMOD_LSHIFT",
@@ -520,7 +526,7 @@
4096: "KMOD_NUM",
8192: "KMOD_CAPS",
16384: "KMOD_MODE",
- 32768: "KMOD_RESERVED",
+ 32768: "KMOD_SCROLL",
}
# --- SDL wheel ---
@@ -534,484 +540,6 @@
}
__all__ = [
- "SCANCODE_UNKNOWN",
- "SCANCODE_A",
- "SCANCODE_B",
- "SCANCODE_C",
- "SCANCODE_D",
- "SCANCODE_E",
- "SCANCODE_F",
- "SCANCODE_G",
- "SCANCODE_H",
- "SCANCODE_I",
- "SCANCODE_J",
- "SCANCODE_K",
- "SCANCODE_L",
- "SCANCODE_M",
- "SCANCODE_N",
- "SCANCODE_O",
- "SCANCODE_P",
- "SCANCODE_Q",
- "SCANCODE_R",
- "SCANCODE_S",
- "SCANCODE_T",
- "SCANCODE_U",
- "SCANCODE_V",
- "SCANCODE_W",
- "SCANCODE_X",
- "SCANCODE_Y",
- "SCANCODE_Z",
- "SCANCODE_1",
- "SCANCODE_2",
- "SCANCODE_3",
- "SCANCODE_4",
- "SCANCODE_5",
- "SCANCODE_6",
- "SCANCODE_7",
- "SCANCODE_8",
- "SCANCODE_9",
- "SCANCODE_0",
- "SCANCODE_RETURN",
- "SCANCODE_ESCAPE",
- "SCANCODE_BACKSPACE",
- "SCANCODE_TAB",
- "SCANCODE_SPACE",
- "SCANCODE_MINUS",
- "SCANCODE_EQUALS",
- "SCANCODE_LEFTBRACKET",
- "SCANCODE_RIGHTBRACKET",
- "SCANCODE_BACKSLASH",
- "SCANCODE_NONUSHASH",
- "SCANCODE_SEMICOLON",
- "SCANCODE_APOSTROPHE",
- "SCANCODE_GRAVE",
- "SCANCODE_COMMA",
- "SCANCODE_PERIOD",
- "SCANCODE_SLASH",
- "SCANCODE_CAPSLOCK",
- "SCANCODE_F1",
- "SCANCODE_F2",
- "SCANCODE_F3",
- "SCANCODE_F4",
- "SCANCODE_F5",
- "SCANCODE_F6",
- "SCANCODE_F7",
- "SCANCODE_F8",
- "SCANCODE_F9",
- "SCANCODE_F10",
- "SCANCODE_F11",
- "SCANCODE_F12",
- "SCANCODE_PRINTSCREEN",
- "SCANCODE_SCROLLLOCK",
- "SCANCODE_PAUSE",
- "SCANCODE_INSERT",
- "SCANCODE_HOME",
- "SCANCODE_PAGEUP",
- "SCANCODE_DELETE",
- "SCANCODE_END",
- "SCANCODE_PAGEDOWN",
- "SCANCODE_RIGHT",
- "SCANCODE_LEFT",
- "SCANCODE_DOWN",
- "SCANCODE_UP",
- "SCANCODE_NUMLOCKCLEAR",
- "SCANCODE_KP_DIVIDE",
- "SCANCODE_KP_MULTIPLY",
- "SCANCODE_KP_MINUS",
- "SCANCODE_KP_PLUS",
- "SCANCODE_KP_ENTER",
- "SCANCODE_KP_1",
- "SCANCODE_KP_2",
- "SCANCODE_KP_3",
- "SCANCODE_KP_4",
- "SCANCODE_KP_5",
- "SCANCODE_KP_6",
- "SCANCODE_KP_7",
- "SCANCODE_KP_8",
- "SCANCODE_KP_9",
- "SCANCODE_KP_0",
- "SCANCODE_KP_PERIOD",
- "SCANCODE_NONUSBACKSLASH",
- "SCANCODE_APPLICATION",
- "SCANCODE_POWER",
- "SCANCODE_KP_EQUALS",
- "SCANCODE_F13",
- "SCANCODE_F14",
- "SCANCODE_F15",
- "SCANCODE_F16",
- "SCANCODE_F17",
- "SCANCODE_F18",
- "SCANCODE_F19",
- "SCANCODE_F20",
- "SCANCODE_F21",
- "SCANCODE_F22",
- "SCANCODE_F23",
- "SCANCODE_F24",
- "SCANCODE_EXECUTE",
- "SCANCODE_HELP",
- "SCANCODE_MENU",
- "SCANCODE_SELECT",
- "SCANCODE_STOP",
- "SCANCODE_AGAIN",
- "SCANCODE_UNDO",
- "SCANCODE_CUT",
- "SCANCODE_COPY",
- "SCANCODE_PASTE",
- "SCANCODE_FIND",
- "SCANCODE_MUTE",
- "SCANCODE_VOLUMEUP",
- "SCANCODE_VOLUMEDOWN",
- "SCANCODE_KP_COMMA",
- "SCANCODE_KP_EQUALSAS400",
- "SCANCODE_INTERNATIONAL1",
- "SCANCODE_INTERNATIONAL2",
- "SCANCODE_INTERNATIONAL3",
- "SCANCODE_INTERNATIONAL4",
- "SCANCODE_INTERNATIONAL5",
- "SCANCODE_INTERNATIONAL6",
- "SCANCODE_INTERNATIONAL7",
- "SCANCODE_INTERNATIONAL8",
- "SCANCODE_INTERNATIONAL9",
- "SCANCODE_LANG1",
- "SCANCODE_LANG2",
- "SCANCODE_LANG3",
- "SCANCODE_LANG4",
- "SCANCODE_LANG5",
- "SCANCODE_LANG6",
- "SCANCODE_LANG7",
- "SCANCODE_LANG8",
- "SCANCODE_LANG9",
- "SCANCODE_ALTERASE",
- "SCANCODE_SYSREQ",
- "SCANCODE_CANCEL",
- "SCANCODE_CLEAR",
- "SCANCODE_PRIOR",
- "SCANCODE_RETURN2",
- "SCANCODE_SEPARATOR",
- "SCANCODE_OUT",
- "SCANCODE_OPER",
- "SCANCODE_CLEARAGAIN",
- "SCANCODE_CRSEL",
- "SCANCODE_EXSEL",
- "SCANCODE_KP_00",
- "SCANCODE_KP_000",
- "SCANCODE_THOUSANDSSEPARATOR",
- "SCANCODE_DECIMALSEPARATOR",
- "SCANCODE_CURRENCYUNIT",
- "SCANCODE_CURRENCYSUBUNIT",
- "SCANCODE_KP_LEFTPAREN",
- "SCANCODE_KP_RIGHTPAREN",
- "SCANCODE_KP_LEFTBRACE",
- "SCANCODE_KP_RIGHTBRACE",
- "SCANCODE_KP_TAB",
- "SCANCODE_KP_BACKSPACE",
- "SCANCODE_KP_A",
- "SCANCODE_KP_B",
- "SCANCODE_KP_C",
- "SCANCODE_KP_D",
- "SCANCODE_KP_E",
- "SCANCODE_KP_F",
- "SCANCODE_KP_XOR",
- "SCANCODE_KP_POWER",
- "SCANCODE_KP_PERCENT",
- "SCANCODE_KP_LESS",
- "SCANCODE_KP_GREATER",
- "SCANCODE_KP_AMPERSAND",
- "SCANCODE_KP_DBLAMPERSAND",
- "SCANCODE_KP_VERTICALBAR",
- "SCANCODE_KP_DBLVERTICALBAR",
- "SCANCODE_KP_COLON",
- "SCANCODE_KP_HASH",
- "SCANCODE_KP_SPACE",
- "SCANCODE_KP_AT",
- "SCANCODE_KP_EXCLAM",
- "SCANCODE_KP_MEMSTORE",
- "SCANCODE_KP_MEMRECALL",
- "SCANCODE_KP_MEMCLEAR",
- "SCANCODE_KP_MEMADD",
- "SCANCODE_KP_MEMSUBTRACT",
- "SCANCODE_KP_MEMMULTIPLY",
- "SCANCODE_KP_MEMDIVIDE",
- "SCANCODE_KP_PLUSMINUS",
- "SCANCODE_KP_CLEAR",
- "SCANCODE_KP_CLEARENTRY",
- "SCANCODE_KP_BINARY",
- "SCANCODE_KP_OCTAL",
- "SCANCODE_KP_DECIMAL",
- "SCANCODE_KP_HEXADECIMAL",
- "SCANCODE_LCTRL",
- "SCANCODE_LSHIFT",
- "SCANCODE_LALT",
- "SCANCODE_LGUI",
- "SCANCODE_RCTRL",
- "SCANCODE_RSHIFT",
- "SCANCODE_RALT",
- "SCANCODE_RGUI",
- "SCANCODE_MODE",
- "SCANCODE_AUDIONEXT",
- "SCANCODE_AUDIOPREV",
- "SCANCODE_AUDIOSTOP",
- "SCANCODE_AUDIOPLAY",
- "SCANCODE_AUDIOMUTE",
- "SCANCODE_MEDIASELECT",
- "SCANCODE_WWW",
- "SCANCODE_MAIL",
- "SCANCODE_CALCULATOR",
- "SCANCODE_COMPUTER",
- "SCANCODE_AC_SEARCH",
- "SCANCODE_AC_HOME",
- "SCANCODE_AC_BACK",
- "SCANCODE_AC_FORWARD",
- "SCANCODE_AC_STOP",
- "SCANCODE_AC_REFRESH",
- "SCANCODE_AC_BOOKMARKS",
- "SCANCODE_BRIGHTNESSDOWN",
- "SCANCODE_BRIGHTNESSUP",
- "SCANCODE_DISPLAYSWITCH",
- "SCANCODE_KBDILLUMTOGGLE",
- "SCANCODE_KBDILLUMDOWN",
- "SCANCODE_KBDILLUMUP",
- "SCANCODE_EJECT",
- "SCANCODE_SLEEP",
- "SCANCODE_APP1",
- "SCANCODE_APP2",
- "K_UNKNOWN",
- "K_BACKSPACE",
- "K_TAB",
- "K_RETURN",
- "K_ESCAPE",
- "K_SPACE",
- "K_EXCLAIM",
- "K_QUOTEDBL",
- "K_HASH",
- "K_DOLLAR",
- "K_PERCENT",
- "K_AMPERSAND",
- "K_QUOTE",
- "K_LEFTPAREN",
- "K_RIGHTPAREN",
- "K_ASTERISK",
- "K_PLUS",
- "K_COMMA",
- "K_MINUS",
- "K_PERIOD",
- "K_SLASH",
- "K_0",
- "K_1",
- "K_2",
- "K_3",
- "K_4",
- "K_5",
- "K_6",
- "K_7",
- "K_8",
- "K_9",
- "K_COLON",
- "K_SEMICOLON",
- "K_LESS",
- "K_EQUALS",
- "K_GREATER",
- "K_QUESTION",
- "K_AT",
- "K_LEFTBRACKET",
- "K_BACKSLASH",
- "K_RIGHTBRACKET",
- "K_CARET",
- "K_UNDERSCORE",
- "K_BACKQUOTE",
- "K_a",
- "K_b",
- "K_c",
- "K_d",
- "K_e",
- "K_f",
- "K_g",
- "K_h",
- "K_i",
- "K_j",
- "K_k",
- "K_l",
- "K_m",
- "K_n",
- "K_o",
- "K_p",
- "K_q",
- "K_r",
- "K_s",
- "K_t",
- "K_u",
- "K_v",
- "K_w",
- "K_x",
- "K_y",
- "K_z",
- "K_DELETE",
- "K_SCANCODE_MASK",
- "K_CAPSLOCK",
- "K_F1",
- "K_F2",
- "K_F3",
- "K_F4",
- "K_F5",
- "K_F6",
- "K_F7",
- "K_F8",
- "K_F9",
- "K_F10",
- "K_F11",
- "K_F12",
- "K_PRINTSCREEN",
- "K_SCROLLLOCK",
- "K_PAUSE",
- "K_INSERT",
- "K_HOME",
- "K_PAGEUP",
- "K_END",
- "K_PAGEDOWN",
- "K_RIGHT",
- "K_LEFT",
- "K_DOWN",
- "K_UP",
- "K_NUMLOCKCLEAR",
- "K_KP_DIVIDE",
- "K_KP_MULTIPLY",
- "K_KP_MINUS",
- "K_KP_PLUS",
- "K_KP_ENTER",
- "K_KP_1",
- "K_KP_2",
- "K_KP_3",
- "K_KP_4",
- "K_KP_5",
- "K_KP_6",
- "K_KP_7",
- "K_KP_8",
- "K_KP_9",
- "K_KP_0",
- "K_KP_PERIOD",
- "K_APPLICATION",
- "K_POWER",
- "K_KP_EQUALS",
- "K_F13",
- "K_F14",
- "K_F15",
- "K_F16",
- "K_F17",
- "K_F18",
- "K_F19",
- "K_F20",
- "K_F21",
- "K_F22",
- "K_F23",
- "K_F24",
- "K_EXECUTE",
- "K_HELP",
- "K_MENU",
- "K_SELECT",
- "K_STOP",
- "K_AGAIN",
- "K_UNDO",
- "K_CUT",
- "K_COPY",
- "K_PASTE",
- "K_FIND",
- "K_MUTE",
- "K_VOLUMEUP",
- "K_VOLUMEDOWN",
- "K_KP_COMMA",
- "K_KP_EQUALSAS400",
- "K_ALTERASE",
- "K_SYSREQ",
- "K_CANCEL",
- "K_CLEAR",
- "K_PRIOR",
- "K_RETURN2",
- "K_SEPARATOR",
- "K_OUT",
- "K_OPER",
- "K_CLEARAGAIN",
- "K_CRSEL",
- "K_EXSEL",
- "K_KP_00",
- "K_KP_000",
- "K_THOUSANDSSEPARATOR",
- "K_DECIMALSEPARATOR",
- "K_CURRENCYUNIT",
- "K_CURRENCYSUBUNIT",
- "K_KP_LEFTPAREN",
- "K_KP_RIGHTPAREN",
- "K_KP_LEFTBRACE",
- "K_KP_RIGHTBRACE",
- "K_KP_TAB",
- "K_KP_BACKSPACE",
- "K_KP_A",
- "K_KP_B",
- "K_KP_C",
- "K_KP_D",
- "K_KP_E",
- "K_KP_F",
- "K_KP_XOR",
- "K_KP_POWER",
- "K_KP_PERCENT",
- "K_KP_LESS",
- "K_KP_GREATER",
- "K_KP_AMPERSAND",
- "K_KP_DBLAMPERSAND",
- "K_KP_VERTICALBAR",
- "K_KP_DBLVERTICALBAR",
- "K_KP_COLON",
- "K_KP_HASH",
- "K_KP_SPACE",
- "K_KP_AT",
- "K_KP_EXCLAM",
- "K_KP_MEMSTORE",
- "K_KP_MEMRECALL",
- "K_KP_MEMCLEAR",
- "K_KP_MEMADD",
- "K_KP_MEMSUBTRACT",
- "K_KP_MEMMULTIPLY",
- "K_KP_MEMDIVIDE",
- "K_KP_PLUSMINUS",
- "K_KP_CLEAR",
- "K_KP_CLEARENTRY",
- "K_KP_BINARY",
- "K_KP_OCTAL",
- "K_KP_DECIMAL",
- "K_KP_HEXADECIMAL",
- "K_LCTRL",
- "K_LSHIFT",
- "K_LALT",
- "K_LGUI",
- "K_RCTRL",
- "K_RSHIFT",
- "K_RALT",
- "K_RGUI",
- "K_MODE",
- "K_AUDIONEXT",
- "K_AUDIOPREV",
- "K_AUDIOSTOP",
- "K_AUDIOPLAY",
- "K_AUDIOMUTE",
- "K_MEDIASELECT",
- "K_WWW",
- "K_MAIL",
- "K_CALCULATOR",
- "K_COMPUTER",
- "K_AC_SEARCH",
- "K_AC_HOME",
- "K_AC_BACK",
- "K_AC_FORWARD",
- "K_AC_STOP",
- "K_AC_REFRESH",
- "K_AC_BOOKMARKS",
- "K_BRIGHTNESSDOWN",
- "K_BRIGHTNESSUP",
- "K_DISPLAYSWITCH",
- "K_KBDILLUMTOGGLE",
- "K_KBDILLUMDOWN",
- "K_KBDILLUMUP",
- "K_EJECT",
- "K_SLEEP",
"KMOD_NONE",
"KMOD_LSHIFT",
"KMOD_RSHIFT",
@@ -1028,7 +556,7 @@
"KMOD_NUM",
"KMOD_CAPS",
"KMOD_MODE",
- "KMOD_RESERVED",
+ "KMOD_SCROLL",
"MOUSEWHEEL_NORMAL",
"MOUSEWHEEL_FLIPPED",
"MOUSEWHEEL",
diff --git a/tcod/image.py b/tcod/image.py
index 82d489dc..700c6290 100644
--- a/tcod/image.py
+++ b/tcod/image.py
@@ -1,25 +1,30 @@
-"""Functionality for handling images.
+"""Libtcod functionality for handling images.
-**Python-tcod is unable to render pixels to the screen directly.**
-If your image can't be represented as tiles then you'll need to use
-`an alternative library for graphics rendering
-`_.
+This module is generally seen as outdated.
+To load images you should typically use `Pillow `_ or
+`imageio `_ unless you need to use a feature exclusive to libtcod.
+
+**Python-tcod is unable to render pixels to consoles.**
+The best it can do with consoles is convert an image into semigraphics which can be shown on non-emulated terminals.
+For true pixel-based rendering you'll want to access the SDL rendering port at :any:`tcod.sdl.render`.
"""
from __future__ import annotations
+from os import PathLike
from pathlib import Path
-from typing import Any, Dict, Tuple, Union
+from typing import Any
import numpy as np
-from numpy.typing import NDArray
+from numpy.typing import ArrayLike, NDArray
import tcod.console
from tcod._internal import _console, deprecate
from tcod.loader import ffi, lib
-class Image(object):
- """
+class Image:
+ """A libtcod image.
+
Args:
width (int): Width of the new Image.
height (int): Height of the new Image.
@@ -29,7 +34,8 @@ class Image(object):
height (int): Read only height of this Image.
"""
- def __init__(self, width: int, height: int):
+ def __init__(self, width: int, height: int) -> None:
+ """Initialize a blank image."""
self.width, self.height = width, height
self.image_c = ffi.gc(lib.TCOD_image_new(width, height), lib.TCOD_image_delete)
@@ -41,7 +47,7 @@ def _from_cdata(cls, cdata: Any) -> Image:
return self
@classmethod
- def from_array(cls, array: Any) -> Image:
+ def from_array(cls, array: ArrayLike) -> Image:
"""Create a new Image from a copy of an array-like object.
Example:
@@ -52,14 +58,23 @@ def from_array(cls, array: Any) -> Image:
.. versionadded:: 11.4
"""
- array = np.asarray(array)
+ array = np.asarray(array, dtype=np.uint8)
height, width, depth = array.shape
image = cls(width, height)
- image_array = np.asarray(image)
+ image_array: NDArray[np.uint8] = np.asarray(image)
image_array[...] = array
return image
- def clear(self, color: Tuple[int, int, int]) -> None:
+ @classmethod
+ def from_file(cls, path: str | PathLike[str]) -> Image:
+ """Return a new Image loaded from the given `path`.
+
+ .. versionadded:: 16.0
+ """
+ path = Path(path).resolve(strict=True)
+ return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete))
+
+ def clear(self, color: tuple[int, int, int]) -> None:
"""Fill this entire Image with color.
Args:
@@ -98,7 +113,7 @@ def scale(self, width: int, height: int) -> None:
lib.TCOD_image_scale(self.image_c, width, height)
self.width, self.height = width, height
- def set_key_color(self, color: Tuple[int, int, int]) -> None:
+ def set_key_color(self, color: tuple[int, int, int]) -> None:
"""Set a color to be transparent during blitting functions.
Args:
@@ -134,7 +149,7 @@ def refresh_console(self, console: tcod.console.Console) -> None:
"""
lib.TCOD_image_refresh_console(self.image_c, _console(console))
- def _get_size(self) -> Tuple[int, int]:
+ def _get_size(self) -> tuple[int, int]:
"""Return the (width, height) for this Image.
Returns:
@@ -145,7 +160,7 @@ def _get_size(self) -> Tuple[int, int]:
lib.TCOD_image_get_size(self.image_c, w, h)
return w[0], h[0]
- def get_pixel(self, x: int, y: int) -> Tuple[int, int, int]:
+ def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
"""Get the color of a pixel in this Image.
Args:
@@ -160,7 +175,7 @@ def get_pixel(self, x: int, y: int) -> Tuple[int, int, int]:
color = lib.TCOD_image_get_pixel(self.image_c, x, y)
return color.r, color.g, color.b
- def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float) -> Tuple[int, int, int]:
+ def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float) -> tuple[int, int, int]:
"""Get the average color of a rectangle in this Image.
Parameters should stay within the following limits:
@@ -181,7 +196,7 @@ def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float)
color = lib.TCOD_image_get_mipmap_pixel(self.image_c, left, top, right, bottom)
return (color.r, color.g, color.b)
- def put_pixel(self, x: int, y: int, color: Tuple[int, int, int]) -> None:
+ def put_pixel(self, x: int, y: int, color: tuple[int, int, int]) -> None:
"""Change a pixel on this Image.
Args:
@@ -282,16 +297,19 @@ def blit_2x(
img_height,
)
- def save_as(self, filename: str) -> None:
+ def save_as(self, filename: str | PathLike[str]) -> None:
"""Save the Image to a 32-bit .bmp or .png file.
Args:
filename (Text): File path to same this Image.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- lib.TCOD_image_save(self.image_c, filename.encode("utf-8"))
+ lib.TCOD_image_save(self.image_c, bytes(Path(filename)))
@property
- def __array_interface__(self) -> Dict[str, Any]:
+ def __array_interface__(self) -> dict[str, Any]:
"""Return an interface for this images pixel buffer.
Use :any:`numpy.asarray` to get the read-write array of this Image.
@@ -310,7 +328,8 @@ def __array_interface__(self) -> Dict[str, Any]:
depth = 3
data = int(ffi.cast("size_t", self.image_c.mipmaps[0].buf))
else:
- raise TypeError("Image has no initialized data.")
+ msg = "Image has no initialized data."
+ raise TypeError(msg)
return {
"shape": (self.height, self.width, depth),
"typestr": "|u1",
@@ -325,7 +344,7 @@ def _get_format_name(format: int) -> str:
for attr in dir(lib):
if not attr.startswith("SDL_PIXELFORMAT"):
continue
- if not getattr(lib, attr) == format:
+ if getattr(lib, attr) != format:
continue
return attr
return str(format)
@@ -336,7 +355,7 @@ def _get_format_name(format: int) -> str:
" It's recommended to load images with a more complete image library such as python-Pillow or python-imageio.",
category=PendingDeprecationWarning,
)
-def load(filename: Union[str, Path]) -> NDArray[np.uint8]:
+def load(filename: str | PathLike[str]) -> NDArray[np.uint8]:
"""Load a PNG file as an RGBA array.
`filename` is the name of the file to load.
@@ -345,11 +364,11 @@ def load(filename: Union[str, Path]) -> NDArray[np.uint8]:
.. versionadded:: 11.4
"""
- image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(str(filename).encode()), lib.TCOD_image_delete))
+ image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(Path(filename))), lib.TCOD_image_delete))
array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8)
height, width, depth = array.shape
if depth == 3:
- array = np.concatenate( # type: ignore
+ array = np.concatenate(
(
array,
np.full((height, width, 1), fill_value=255, dtype=np.uint8),
diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py
index f4b607d8..4a258081 100644
--- a/tcod/libtcodpy.py
+++ b/tcod/libtcodpy.py
@@ -1,13 +1,13 @@
-"""This module handles backward compatibility with the ctypes libtcodpy module.
-"""
+"""This module handles backward compatibility with the ctypes libtcodpy module."""
from __future__ import annotations
import atexit
-import os
import sys
import threading
import warnings
-from typing import Any, AnyStr, Callable, Hashable, Iterable, Iterator, List, Optional, Sequence, Tuple, Union
+from os import PathLike
+from pathlib import Path
+from typing import Any, Callable, Hashable, Iterable, Iterator, Sequence
import numpy as np
from numpy.typing import NDArray
@@ -37,7 +37,7 @@
pending_deprecate,
)
from tcod.color import Color
-from tcod.constants import * # noqa: F4
+from tcod.constants import * # noqa: F403
from tcod.constants import (
BKGND_ADDA,
BKGND_ALPH,
@@ -51,6 +51,8 @@
)
from tcod.loader import ffi, lib
+# ruff: noqa: ANN401 PLR0913 # Functions are too deprecated to make changes.
+
Bsp = tcod.bsp.BSP
NB_FOV_ALGORITHMS = 13
@@ -71,9 +73,8 @@ def BKGND_ADDALPHA(a: int) -> int:
return BKGND_ADDA | (int(a * 255) << 8)
-class ConsoleBuffer(object):
- """Simple console that allows direct (fast) access to cells. simplifies
- use of the "fill" functions.
+class ConsoleBuffer:
+ """Simple console that allows direct (fast) access to cells. Simplifies use of the "fill" functions.
.. deprecated:: 6.0
Console array attributes perform better than this class.
@@ -102,8 +103,9 @@ def __init__(
fore_b: int = 0,
char: str = " ",
) -> None:
- """initialize with given width and height. values to fill the buffer
- are optional, defaults to black with no characters.
+ """Initialize with given width and height.
+
+ Values to fill the buffer are optional, defaults to black with no characters.
"""
warnings.warn(
"Console array attributes perform better than this class.",
@@ -124,8 +126,9 @@ def clear(
fore_b: int = 0,
char: str = " ",
) -> None:
- """Clears the console. Values to fill it with are optional, defaults
- to black with no characters.
+ """Clear the console.
+
+ Values to fill it with are optional, defaults to black with no characters.
Args:
back_r (int): Red background color, from 0 to 255.
@@ -146,7 +149,7 @@ def clear(
self.char = [ord(char)] * n
def copy(self) -> ConsoleBuffer:
- """Returns a copy of this ConsoleBuffer.
+ """Return a copy of this ConsoleBuffer.
Returns:
ConsoleBuffer: A new ConsoleBuffer copy.
@@ -247,7 +250,8 @@ def blit(
if not dest:
dest = tcod.console.Console._from_cdata(ffi.NULL)
if dest.width != self.width or dest.height != self.height:
- raise ValueError("ConsoleBuffer.blit: " "Destination console has an incorrect size.")
+ msg = "ConsoleBuffer.blit: Destination console has an incorrect size."
+ raise ValueError(msg)
if fill_back:
bg = dest.bg.ravel()
@@ -264,7 +268,7 @@ def blit(
class Dice(_CDataWrapper):
- """
+ """A libtcod dice object.
Args:
nb_dices (int): Number of dice.
@@ -283,7 +287,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
DeprecationWarning,
stacklevel=2,
)
- super(Dice, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
if self.cdata == ffi.NULL:
self._init(*args, **kwargs)
@@ -318,7 +322,7 @@ def __str__(self) -> str:
)
def __repr__(self) -> str:
- return "%s(nb_dices=%r,nb_faces=%r,multiplier=%r,addsub=%r)" % (
+ return "{}(nb_dices={!r},nb_faces={!r},multiplier={!r},addsub={!r})".format(
self.__class__.__name__,
self.nb_dices,
self.nb_faces,
@@ -332,7 +336,7 @@ def __repr__(self) -> str:
class Key(_CDataWrapper):
- """Key Event instance
+ r"""Key Event instance.
Attributes:
vk (int): TCOD_keycode_t key code
@@ -377,7 +381,7 @@ def __init__(
rctrl: bool = False,
rmeta: bool = False,
shift: bool = False,
- ):
+ ) -> None:
if isinstance(vk, ffi.CData):
self.cdata = vk
return
@@ -401,7 +405,7 @@ def __getattr__(self, attr: str) -> Any:
return ord(self.cdata.c)
if attr == "text":
return ffi.string(self.cdata.text).decode()
- return super(Key, self).__getattr__(attr)
+ return super().__getattr__(attr)
def __setattr__(self, attr: str, value: Any) -> None:
if attr == "c":
@@ -409,12 +413,12 @@ def __setattr__(self, attr: str, value: Any) -> None:
elif attr == "text":
self.cdata.text = value.encode()
else:
- super(Key, self).__setattr__(attr, value)
+ super().__setattr__(attr, value)
def __repr__(self) -> str:
"""Return a representation of this Key object."""
params = []
- params.append("pressed=%r, vk=tcod.%s" % (self.pressed, _LOOKUP_VK[self.vk]))
+ params.append(f"pressed={self.pressed!r}, vk=tcod.{_LOOKUP_VK[self.vk]}")
if self.c:
params.append("c=ord(%r)" % chr(self.c))
if self.text:
@@ -429,7 +433,7 @@ def __repr__(self) -> str:
"rmeta",
]:
if getattr(self, attr):
- params.append("%s=%r" % (attr, getattr(self, attr)))
+ params.append(f"{attr}={getattr(self, attr)!r}")
return "tcod.Key(%s)" % ", ".join(params)
@property
@@ -438,7 +442,7 @@ def key_p(self) -> Any:
class Mouse(_CDataWrapper):
- """Mouse event instance
+ """Mouse event instance.
Attributes:
x (int): Absolute mouse position at pixel x.
@@ -473,7 +477,7 @@ def __init__(
dcx: int = 0,
dcy: int = 0,
**kwargs: Any,
- ):
+ ) -> None:
if isinstance(x, ffi.CData):
self.cdata = x
return
@@ -495,7 +499,7 @@ def __repr__(self) -> str:
for attr in ["x", "y", "dx", "dy", "cx", "cy", "dcx", "dcy"]:
if getattr(self, attr) == 0:
continue
- params.append("%s=%r" % (attr, getattr(self, attr)))
+ params.append(f"{attr}={getattr(self, attr)!r}")
for attr in [
"lbutton",
"rbutton",
@@ -507,7 +511,7 @@ def __repr__(self) -> str:
"wheel_down",
]:
if getattr(self, attr):
- params.append("%s=%r" % (attr, getattr(self, attr)))
+ params.append(f"{attr}={getattr(self, attr)!r}")
return "tcod.Mouse(%s)" % ", ".join(params)
@property
@@ -515,7 +519,7 @@ def mouse_p(self) -> Any:
return self.cdata
-@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.")
+@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.", FutureWarning)
def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP:
"""Create a new BSP instance with the given rectangle.
@@ -534,35 +538,38 @@ def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP:
return Bsp(x, y, w, h)
-@deprecate("Call node.split_once instead.")
+@deprecate("Call node.split_once instead.", FutureWarning)
def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None:
- """
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.split_once` instead.
"""
node.split_once(horizontal, position)
-@deprecate("Call node.split_recursive instead.")
+@deprecate("Call node.split_recursive instead.", FutureWarning)
def bsp_split_recursive(
node: tcod.bsp.BSP,
- randomizer: Optional[tcod.random.Random],
+ randomizer: tcod.random.Random | None,
nb: int,
minHSize: int,
minVSize: int,
maxHRatio: float,
maxVRatio: float,
) -> None:
- """
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.split_recursive` instead.
"""
node.split_recursive(nb, minHSize, minVSize, maxHRatio, maxVRatio, randomizer)
-@deprecate("Assign values via attribute instead.")
+@deprecate("Assign values via attribute instead.", FutureWarning)
def bsp_resize(node: tcod.bsp.BSP, x: int, y: int, w: int, h: int) -> None:
- """
+ """Deprecated function.
+
.. deprecated:: 2.0
Assign directly to :any:`BSP` attributes instead.
"""
@@ -573,8 +580,9 @@ def bsp_resize(node: tcod.bsp.BSP, x: int, y: int, w: int, h: int) -> None:
@deprecate("Access children with 'node.children' instead.")
-def bsp_left(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]:
- """
+def bsp_left(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None:
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.children` instead.
"""
@@ -582,44 +590,49 @@ def bsp_left(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]:
@deprecate("Access children with 'node.children' instead.")
-def bsp_right(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]:
- """
+def bsp_right(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None:
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.children` instead.
"""
return None if not node.children else node.children[1]
-@deprecate("Get the parent with 'node.parent' instead.")
-def bsp_father(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]:
- """
+@deprecate("Get the parent with 'node.parent' instead.", FutureWarning)
+def bsp_father(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None:
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.parent` instead.
"""
return node.parent
-@deprecate("Check for children with 'bool(node.children)' instead.")
+@deprecate("Check for children with 'bool(node.children)' instead.", FutureWarning)
def bsp_is_leaf(node: tcod.bsp.BSP) -> bool:
- """
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.children` instead.
"""
return not node.children
-@deprecate("Use 'node.contains' instead.")
+@deprecate("Use 'node.contains' instead.", FutureWarning)
def bsp_contains(node: tcod.bsp.BSP, cx: int, cy: int) -> bool:
- """
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.contains` instead.
"""
return node.contains(cx, cy)
-@deprecate("Use 'node.find_node' instead.")
-def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> Optional[tcod.bsp.BSP]:
- """
+@deprecate("Use 'node.find_node' instead.", FutureWarning)
+def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> tcod.bsp.BSP | None:
+ """Deprecated function.
+
.. deprecated:: 2.0
Use :any:`BSP.find_node` instead.
"""
@@ -631,9 +644,7 @@ def _bsp_traverse(
callback: Callable[[tcod.bsp.BSP, Any], None],
userData: Any,
) -> None:
- """pack callback into a handle for use with the callback
- _pycall_bsp_callback
- """
+ """Pack callback into a handle for use with the callback _pycall_bsp_callback."""
for node in node_iter:
callback(node, userData)
@@ -722,7 +733,7 @@ def bsp_remove_sons(node: tcod.bsp.BSP) -> None:
node.children = ()
-@deprecate("libtcod objects are deleted automatically.")
+@deprecate("libtcod objects are deleted automatically.", FutureWarning)
def bsp_delete(node: tcod.bsp.BSP) -> None:
"""Exists for backward compatibility. Does nothing.
@@ -736,7 +747,7 @@ def bsp_delete(node: tcod.bsp.BSP) -> None:
@pending_deprecate()
-def color_lerp(c1: Tuple[int, int, int], c2: Tuple[int, int, int], a: float) -> Color:
+def color_lerp(c1: tuple[int, int, int], c2: tuple[int, int, int], a: float) -> Color:
"""Return the linear interpolation between two colors.
``a`` is the interpolation value, with 0 returning ``c1``,
@@ -773,7 +784,7 @@ def color_set_hsv(c: Color, h: float, s: float, v: float) -> None:
@pending_deprecate()
-def color_get_hsv(c: Tuple[int, int, int]) -> Tuple[float, float, float]:
+def color_get_hsv(c: tuple[int, int, int]) -> tuple[float, float, float]:
"""Return the (hue, saturation, value) of a color.
Args:
@@ -809,7 +820,7 @@ def color_scale_HSV(c: Color, scoef: float, vcoef: float) -> None:
@pending_deprecate()
-def color_gen_map(colors: Iterable[Tuple[int, int, int]], indexes: Iterable[int]) -> List[Color]:
+def color_gen_map(colors: Iterable[tuple[int, int, int]], indexes: Iterable[int]) -> list[Color]:
"""Return a smoothly defined scale of colors.
If ``indexes`` is [0, 3, 9] for example, the first color from ``colors``
@@ -844,11 +855,11 @@ def color_gen_map(colors: Iterable[Tuple[int, int, int]], indexes: Iterable[int]
def console_init_root(
w: int,
h: int,
- title: Optional[str] = None,
+ title: str | None = None,
fullscreen: bool = False,
- renderer: Optional[int] = None,
+ renderer: int | None = None,
order: Literal["C", "F"] = "C",
- vsync: Optional[bool] = None,
+ vsync: bool | None = None,
) -> tcod.console.Console:
"""Set up the primary display and return the root console.
@@ -915,7 +926,7 @@ def console_init_root(
"""
if title is None:
# Use the scripts filename as the title.
- title = os.path.basename(sys.argv[0])
+ title = Path(sys.argv[0]).name
if renderer is None:
renderer = tcod.constants.RENDERER_SDL2
elif renderer == tcod.constants.RENDERER_GLSL:
@@ -944,7 +955,7 @@ def console_init_root(
https://python-tcod.readthedocs.io/en/latest/tcod/getting-started.html"""
)
def console_set_custom_font(
- fontFile: AnyStr,
+ fontFile: str | PathLike[str],
flags: int = FONT_LAYOUT_ASCII_INCOL,
nb_char_horiz: int = 0,
nb_char_vertic: int = 0,
@@ -973,10 +984,12 @@ def console_set_custom_font(
.. deprecated:: 11.13
Load fonts using :any:`tcod.tileset.load_tilesheet` instead.
See :ref:`getting-started` for more info.
+
+ .. versionchanged:: 16.0
+ Added PathLike support. `fontFile` no longer takes bytes.
"""
- if not os.path.exists(_unicode(fontFile)):
- raise RuntimeError("File not found:\n\t%s" % (str(os.path.realpath(fontFile)),))
- _check(lib.TCOD_console_set_custom_font(_bytes(fontFile), flags, nb_char_horiz, nb_char_vertic))
+ fontFile = Path(fontFile).resolve(strict=True)
+ _check(lib.TCOD_console_set_custom_font(bytes(fontFile), flags, nb_char_horiz, nb_char_vertic))
@deprecate("Check `con.width` instead.")
@@ -1057,12 +1070,11 @@ def console_map_ascii_codes_to_font(firstAsciiCode: int, nbCodes: int, fontCharX
@deprecate("Setup fonts using the tcod.tileset module.")
def console_map_string_to_font(s: str, fontCharX: int, fontCharY: int) -> None:
- """Remap a string of codes to a contiguous set of tiles.
+ r"""Remap a string of codes to a contiguous set of tiles.
Args:
s (AnyStr): A string of character codes to map to new values.
- The null character `'\\x00'` will prematurely end this
- function.
+ Any null character `'\x00'` will prematurely end the printed text.
fontCharX (int): The starting X tile coordinate on the loaded tileset.
0 is the leftmost tile.
fontCharY (int): The starting Y tile coordinate on the loaded tileset.
@@ -1160,17 +1172,13 @@ def console_credits_render(x: int, y: int, alpha: bool) -> bool:
@deprecate("This function is not supported if contexts are being used.")
def console_flush(
- console: Optional[tcod.console.Console] = None,
+ console: tcod.console.Console | None = None,
*,
keep_aspect: bool = False,
integer_scaling: bool = False,
- snap_to_integer: Optional[bool] = None,
- clear_color: Union[Tuple[int, int, int], Tuple[int, int, int, int]] = (
- 0,
- 0,
- 0,
- ),
- align: Tuple[float, float] = (0.5, 0.5),
+ snap_to_integer: bool | None = None,
+ clear_color: tuple[int, int, int] | tuple[int, int, int, int] = (0, 0, 0),
+ align: tuple[float, float] = (0.5, 0.5),
) -> None:
"""Update the display to represent the root consoles current state.
@@ -1224,17 +1232,14 @@ def console_flush(
"align_x": align[0],
"align_y": align[1],
}
- if console is None:
- console_p = ffi.NULL
- else:
- console_p = _console(console)
+ console_p = ffi.NULL if console is None else _console(console)
with ffi.new("struct TCOD_ViewportOptions*", options) as viewport_opts:
_check(lib.TCOD_console_flush_ex(console_p, viewport_opts))
# drawing on a console
@deprecate("Set the `con.default_bg` attribute instead.")
-def console_set_default_background(con: tcod.console.Console, col: Tuple[int, int, int]) -> None:
+def console_set_default_background(con: tcod.console.Console, col: tuple[int, int, int]) -> None:
"""Change the default background color for a console.
Args:
@@ -1249,7 +1254,7 @@ def console_set_default_background(con: tcod.console.Console, col: Tuple[int, in
@deprecate("Set the `con.default_fg` attribute instead.")
-def console_set_default_foreground(con: tcod.console.Console, col: Tuple[int, int, int]) -> None:
+def console_set_default_foreground(con: tcod.console.Console, col: tuple[int, int, int]) -> None:
"""Change the default foreground color for a console.
Args:
@@ -1285,7 +1290,7 @@ def console_put_char(
con: tcod.console.Console,
x: int,
y: int,
- c: Union[int, str],
+ c: int | str,
flag: int = BKGND_DEFAULT,
) -> None:
"""Draw the character c at x,y using the default colors and a blend mode.
@@ -1305,9 +1310,9 @@ def console_put_char_ex(
con: tcod.console.Console,
x: int,
y: int,
- c: Union[int, str],
- fore: Tuple[int, int, int],
- back: Tuple[int, int, int],
+ c: int | str,
+ fore: tuple[int, int, int],
+ back: tuple[int, int, int],
) -> None:
"""Draw the character c at x,y using the colors fore and back.
@@ -1329,7 +1334,7 @@ def console_set_char_background(
con: tcod.console.Console,
x: int,
y: int,
- col: Tuple[int, int, int],
+ col: tuple[int, int, int],
flag: int = BKGND_SET,
) -> None:
"""Change the background color of x,y to col using a blend mode.
@@ -1346,7 +1351,7 @@ def console_set_char_background(
@deprecate("Directly access a consoles foreground color with `console.fg`")
-def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col: Tuple[int, int, int]) -> None:
+def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col: tuple[int, int, int]) -> None:
"""Change the foreground color of x,y to col.
Args:
@@ -1364,7 +1369,7 @@ def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col:
@deprecate("Directly access a consoles characters with `console.ch`")
-def console_set_char(con: tcod.console.Console, x: int, y: int, c: Union[int, str]) -> None:
+def console_set_char(con: tcod.console.Console, x: int, y: int, c: int | str) -> None:
"""Change the character at x,y to c, keeping the current colors.
Args:
@@ -1417,7 +1422,7 @@ def console_set_alignment(con: tcod.console.Console, alignment: int) -> None:
Args:
con (Console): Any Console instance.
- alignment (int):
+ alignment (int): The libtcod alignment constant.
.. deprecated:: 8.5
Set :any:`Console.default_alignment` instead.
@@ -1616,7 +1621,7 @@ def console_print_frame(
@pending_deprecate()
-def console_set_color_control(con: int, fore: Tuple[int, int, int], back: Tuple[int, int, int]) -> None:
+def console_set_color_control(con: int, fore: tuple[int, int, int], back: tuple[int, int, int]) -> None:
"""Configure :term:`color controls`.
Args:
@@ -1682,27 +1687,30 @@ def console_get_char(con: tcod.console.Console, x: int, y: int) -> int:
return lib.TCOD_console_get_char(_console(con), x, y) # type: ignore
-@deprecate("This function is not supported if contexts are being used.")
-def console_set_fade(fade: int, fadingColor: Tuple[int, int, int]) -> None:
- """
+@deprecate("This function is not supported if contexts are being used.", FutureWarning)
+def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None:
+ """Deprecated function.
+
.. deprecated:: 11.13
This function is not supported by contexts.
"""
lib.TCOD_console_set_fade(fade, fadingColor)
-@deprecate("This function is not supported if contexts are being used.")
+@deprecate("This function is not supported if contexts are being used.", FutureWarning)
def console_get_fade() -> int:
- """
+ """Deprecated function.
+
.. deprecated:: 11.13
This function is not supported by contexts.
"""
return int(lib.TCOD_console_get_fade())
-@deprecate("This function is not supported if contexts are being used.")
+@deprecate("This function is not supported if contexts are being used.", FutureWarning)
def console_get_fading_color() -> Color:
- """
+ """Deprecated function.
+
.. deprecated:: 11.13
This function is not supported by contexts.
"""
@@ -1737,7 +1745,8 @@ def console_wait_for_keypress(flush: bool) -> Key:
@deprecate("Use the tcod.event.get function to check for events.")
def console_check_for_keypress(flags: int = KEY_RELEASED) -> Key:
- """
+ """Return a recently pressed key.
+
.. deprecated:: 9.3
Use the :any:`tcod.event.get` function to check for events.
@@ -1752,9 +1761,10 @@ def console_check_for_keypress(flags: int = KEY_RELEASED) -> Key:
return key
-@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.")
+@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.", FutureWarning)
def console_is_key_pressed(key: int) -> bool:
- """
+ """Return True if a key is held.
+
.. deprecated:: 12.7
Use :any:`tcod.event.get_keyboard_state` to check if a key is held.
"""
@@ -1774,7 +1784,7 @@ def console_new(w: int, h: int) -> tcod.console.Console:
@deprecate("This loading method is no longer supported, use tcod.console_load_xp instead.")
-def console_from_file(filename: str) -> tcod.console.Console:
+def console_from_file(filename: str | PathLike[str]) -> tcod.console.Console:
"""Return a new console object from a filename.
The file format is automatically determined. This can load REXPaint `.xp`,
@@ -1789,10 +1799,12 @@ def console_from_file(filename: str) -> tcod.console.Console:
Use :any:`tcod.console_load_xp` to load REXPaint consoles.
Other formats are not actively supported.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- if not os.path.exists(filename):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),))
- return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(filename.encode("utf-8"))))
+ filename = Path(filename).resolve(strict=True)
+ return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(filename))))
@deprecate("Call the `Console.blit` method instead.")
@@ -1817,7 +1829,7 @@ def console_blit(
@deprecate("Pass the key color to `Console.blit` instead of calling this function.")
-def console_set_key_color(con: tcod.console.Console, col: Tuple[int, int, int]) -> None:
+def console_set_key_color(con: tcod.console.Console, col: tuple[int, int, int]) -> None:
"""Set a consoles blit transparent color.
.. deprecated:: 8.5
@@ -1882,7 +1894,8 @@ def console_fill_foreground(
You should assign to :any:`tcod.console.Console.fg` instead.
"""
if len(r) != len(g) or len(r) != len(b):
- raise TypeError("R, G and B must all have the same size.")
+ msg = "R, G and B must all have the same size."
+ raise TypeError(msg)
if isinstance(r, np.ndarray) and isinstance(g, np.ndarray) and isinstance(b, np.ndarray):
# numpy arrays, use numpy's ctypes functions
r_ = np.ascontiguousarray(r, dtype=np.intc)
@@ -1919,7 +1932,8 @@ def console_fill_background(
You should assign to :any:`tcod.console.Console.bg` instead.
"""
if len(r) != len(g) or len(r) != len(b):
- raise TypeError("R, G and B must all have the same size.")
+ msg = "R, G and B must all have the same size."
+ raise TypeError(msg)
if isinstance(r, np.ndarray) and isinstance(g, np.ndarray) and isinstance(b, np.ndarray):
# numpy arrays, use numpy's ctypes functions
r_ = np.ascontiguousarray(r, dtype=np.intc)
@@ -1959,78 +1973,106 @@ def console_fill_char(con: tcod.console.Console, arr: Sequence[int]) -> None:
@deprecate("This format is not actively supported")
-def console_load_asc(con: tcod.console.Console, filename: str) -> bool:
+def console_load_asc(con: tcod.console.Console, filename: str | PathLike[str]) -> bool:
"""Update a console from a non-delimited ASCII `.asc` file.
.. deprecated:: 12.7
This format is no longer supported.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- return bool(lib.TCOD_console_load_asc(_console(con), filename.encode("utf-8")))
+ filename = Path(filename).resolve(strict=True)
+ return bool(lib.TCOD_console_load_asc(_console(con), bytes(filename)))
@deprecate("This format is not actively supported")
-def console_save_asc(con: tcod.console.Console, filename: str) -> bool:
+def console_save_asc(con: tcod.console.Console, filename: str | PathLike[str]) -> bool:
"""Save a console to a non-delimited ASCII `.asc` file.
.. deprecated:: 12.7
This format is no longer supported.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- return bool(lib.TCOD_console_save_asc(_console(con), filename.encode("utf-8")))
+ return bool(lib.TCOD_console_save_asc(_console(con), bytes(Path(filename))))
@deprecate("This format is not actively supported")
-def console_load_apf(con: tcod.console.Console, filename: str) -> bool:
+def console_load_apf(con: tcod.console.Console, filename: str | PathLike[str]) -> bool:
"""Update a console from an ASCII Paint `.apf` file.
.. deprecated:: 12.7
This format is no longer supported.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- return bool(lib.TCOD_console_load_apf(_console(con), filename.encode("utf-8")))
+ filename = Path(filename).resolve(strict=True)
+ return bool(lib.TCOD_console_load_apf(_console(con), bytes(filename)))
@deprecate("This format is not actively supported")
-def console_save_apf(con: tcod.console.Console, filename: str) -> bool:
+def console_save_apf(con: tcod.console.Console, filename: str | PathLike[str]) -> bool:
"""Save a console to an ASCII Paint `.apf` file.
.. deprecated:: 12.7
This format is no longer supported.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- return bool(lib.TCOD_console_save_apf(_console(con), filename.encode("utf-8")))
+ return bool(lib.TCOD_console_save_apf(_console(con), bytes(Path(filename))))
@deprecate("Use tcod.console.load_xp to load this file.")
-def console_load_xp(con: tcod.console.Console, filename: str) -> bool:
+def console_load_xp(con: tcod.console.Console, filename: str | PathLike[str]) -> bool:
"""Update a console from a REXPaint `.xp` file.
.. deprecated:: 11.18
Functions modifying console objects in-place are deprecated.
Use :any:`tcod.console_from_xp` to load a Console from a file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- return bool(lib.TCOD_console_load_xp(_console(con), filename.encode("utf-8")))
+ filename = Path(filename).resolve(strict=True)
+ return bool(lib.TCOD_console_load_xp(_console(con), bytes(filename)))
@deprecate("Use tcod.console.save_xp to save this console.")
-def console_save_xp(con: tcod.console.Console, filename: str, compress_level: int = 9) -> bool:
- """Save a console to a REXPaint `.xp` file."""
- return bool(lib.TCOD_console_save_xp(_console(con), filename.encode("utf-8"), compress_level))
+def console_save_xp(con: tcod.console.Console, filename: str | PathLike[str], compress_level: int = 9) -> bool:
+ """Save a console to a REXPaint `.xp` file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
+ """
+ return bool(lib.TCOD_console_save_xp(_console(con), bytes(Path(filename)), compress_level))
@deprecate("Use tcod.console.load_xp to load this file.")
-def console_from_xp(filename: str) -> tcod.console.Console:
- """Return a single console from a REXPaint `.xp` file."""
- if not os.path.exists(filename):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),))
- return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(filename.encode("utf-8"))))
+def console_from_xp(filename: str | PathLike[str]) -> tcod.console.Console:
+ """Return a single console from a REXPaint `.xp` file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
+ """
+ filename = Path(filename).resolve(strict=True)
+ return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(filename))))
@deprecate("Use tcod.console.load_xp to load this file.")
def console_list_load_xp(
- filename: str,
-) -> Optional[List[tcod.console.Console]]:
- """Return a list of consoles from a REXPaint `.xp` file."""
- if not os.path.exists(filename):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),))
- tcod_list = lib.TCOD_console_list_from_xp(filename.encode("utf-8"))
+ filename: str | PathLike[str],
+) -> list[tcod.console.Console] | None:
+ """Return a list of consoles from a REXPaint `.xp` file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
+ """
+ filename = Path(filename).resolve(strict=True)
+ tcod_list = lib.TCOD_console_list_from_xp(bytes(filename))
if tcod_list == ffi.NULL:
return None
try:
@@ -2046,15 +2088,19 @@ def console_list_load_xp(
@deprecate("Use tcod.console.save_xp to save these consoles.")
def console_list_save_xp(
console_list: Sequence[tcod.console.Console],
- filename: str,
+ filename: str | PathLike[str],
compress_level: int = 9,
) -> bool:
- """Save a list of consoles to a REXPaint `.xp` file."""
+ """Save a list of consoles to a REXPaint `.xp` file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
+ """
tcod_list = lib.TCOD_list_new()
try:
for console in console_list:
lib.TCOD_list_push(tcod_list, _console(console))
- return bool(lib.TCOD_console_list_save_xp(tcod_list, filename.encode("utf-8"), compress_level))
+ return bool(lib.TCOD_console_list_save_xp(tcod_list, bytes(Path(filename)), compress_level))
finally:
lib.TCOD_list_delete(tcod_list)
@@ -2067,6 +2113,7 @@ def path_new_using_map(m: tcod.map.Map, dcost: float = 1.41) -> tcod.path.AStar:
m (Map): A Map instance.
dcost (float): The path-finding cost of diagonal movement.
Can be set to 0 to disable diagonal movement.
+
Returns:
AStar: A new AStar instance.
"""
@@ -2090,6 +2137,7 @@ def path_new_using_function(
userData (Any):
dcost (float): A multiplier for the cost of diagonal movement.
Can be set to 0 to disable diagonal movement.
+
Returns:
AStar: A new AStar instance.
"""
@@ -2106,6 +2154,7 @@ def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool
oy (int): Starting y position.
dx (int): Destination x position.
dy (int): Destination y position.
+
Returns:
bool: True if a valid path was found. Otherwise False.
"""
@@ -2113,13 +2162,14 @@ def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool
@pending_deprecate()
-def path_get_origin(p: tcod.path.AStar) -> Tuple[int, int]:
+def path_get_origin(p: tcod.path.AStar) -> tuple[int, int]:
"""Get the current origin position.
This point moves when :any:`path_walk` returns the next x,y step.
Args:
p (AStar): An AStar instance.
+
Returns:
Tuple[int, int]: An (x, y) point.
"""
@@ -2130,11 +2180,12 @@ def path_get_origin(p: tcod.path.AStar) -> Tuple[int, int]:
@pending_deprecate()
-def path_get_destination(p: tcod.path.AStar) -> Tuple[int, int]:
+def path_get_destination(p: tcod.path.AStar) -> tuple[int, int]:
"""Get the current destination position.
Args:
p (AStar): An AStar instance.
+
Returns:
Tuple[int, int]: An (x, y) point.
"""
@@ -2150,6 +2201,7 @@ def path_size(p: tcod.path.AStar) -> int:
Args:
p (AStar): An AStar instance.
+
Returns:
int: Length of the path.
"""
@@ -2169,7 +2221,7 @@ def path_reverse(p: tcod.path.AStar) -> None:
@pending_deprecate()
-def path_get(p: tcod.path.AStar, idx: int) -> Tuple[int, int]:
+def path_get(p: tcod.path.AStar, idx: int) -> tuple[int, int]:
"""Get a point on a path.
Args:
@@ -2188,6 +2240,7 @@ def path_is_empty(p: tcod.path.AStar) -> bool:
Args:
p (AStar): An AStar instance.
+
Returns:
bool: True if a path is empty. Otherwise False.
"""
@@ -2195,7 +2248,7 @@ def path_is_empty(p: tcod.path.AStar) -> bool:
@pending_deprecate()
-def path_walk(p: tcod.path.AStar, recompute: bool) -> Union[Tuple[int, int], Tuple[None, None]]:
+def path_walk(p: tcod.path.AStar, recompute: bool) -> tuple[int, int] | tuple[None, None]:
"""Return the next (x, y) point in a path, or (None, None) if it's empty.
When ``recompute`` is True and a previously valid path reaches a point
@@ -2204,6 +2257,7 @@ def path_walk(p: tcod.path.AStar, recompute: bool) -> Union[Tuple[int, int], Tup
Args:
p (AStar): An AStar instance.
recompute (bool): Recompute the path automatically.
+
Returns:
Union[Tuple[int, int], Tuple[None, None]]:
A single (x, y) point, or (None, None)
@@ -2265,7 +2319,7 @@ def dijkstra_reverse(p: tcod.path.Dijkstra) -> None:
@pending_deprecate()
-def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> Tuple[int, int]:
+def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> tuple[int, int]:
x = ffi.new("int *")
y = ffi.new("int *")
lib.TCOD_dijkstra_get(p._path_c, idx, x, y)
@@ -2280,7 +2334,7 @@ def dijkstra_is_empty(p: tcod.path.Dijkstra) -> bool:
@pending_deprecate()
def dijkstra_path_walk(
p: tcod.path.Dijkstra,
-) -> Union[Tuple[int, int], Tuple[None, None]]:
+) -> tuple[int, int] | tuple[None, None]:
x = ffi.new("int *")
y = ffi.new("int *")
if lib.TCOD_dijkstra_path_walk(p._path_c, x, y):
@@ -2304,8 +2358,9 @@ def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData:
if array.flags["F_CONTIGUOUS"]:
array = array.transpose()
if not array.flags["C_CONTIGUOUS"]:
- raise ValueError("array must be a contiguous segment.")
- if array.dtype != np.float32: # type: ignore
+ msg = "array must be a contiguous segment."
+ raise ValueError(msg)
+ if array.dtype != np.float32:
raise ValueError("array dtype must be float32, not %r" % array.dtype)
height, width = array.shape
pointer = ffi.from_buffer("float *", array)
@@ -2336,7 +2391,8 @@ def heightmap_new(w: int, h: int, order: str = "C") -> NDArray[np.float32]:
elif order == "F":
return np.zeros((w, h), np.float32, order="F")
else:
- raise ValueError("Invalid order parameter, should be 'C' or 'F'.")
+ msg = "Invalid order parameter, should be 'C' or 'F'."
+ raise ValueError(msg)
@deprecate("Assign to heightmaps as a NumPy array instead.")
@@ -2361,7 +2417,8 @@ def heightmap_set_value(hm: NDArray[np.float32], x: int, y: int, value: float) -
)
hm[x, y] = value
else:
- raise ValueError("This array is not contiguous.")
+ msg = "This array is not contiguous."
+ raise ValueError(msg)
@deprecate("Add a scalar to an array using `hm[:] += value`")
@@ -2407,7 +2464,7 @@ def heightmap_clear(hm: NDArray[np.float32]) -> None:
@deprecate("Clamp array values using `hm.clip(mi, ma)`")
def heightmap_clamp(hm: NDArray[np.float32], mi: float, ma: float) -> None:
- """Clamp all values on this heightmap between ``mi`` and ``ma``
+ """Clamp all values on this heightmap between ``mi`` and ``ma``.
Args:
hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions.
@@ -2452,8 +2509,7 @@ def heightmap_lerp_hm(
hm3: NDArray[np.float32],
coef: float,
) -> None:
- """Perform linear interpolation between two heightmaps storing the result
- in ``hm3``.
+ """Perform linear interpolation between two heightmaps storing the result in ``hm3``.
This is the same as doing ``hm3[:] = hm1[:] + (hm2[:] - hm1[:]) * coef``
@@ -2528,13 +2584,11 @@ def heightmap_add_hill(hm: NDArray[np.float32], x: float, y: float, radius: floa
@pending_deprecate()
def heightmap_dig_hill(hm: NDArray[np.float32], x: float, y: float, radius: float, height: float) -> None:
- """
+ """Dig a hill in a heightmap.
- This function takes the highest value (if height > 0) or the lowest
- (if height < 0) between the map and the hill.
+ This function takes the highest value (if height > 0) or the lowest (if height < 0) between the map and the hill.
- It's main goal is to carve things in maps (like rivers) by digging hills
- along a curve.
+ It's main goal is to carve things in maps (like rivers) by digging hills along a curve.
Args:
hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions.
@@ -2552,7 +2606,7 @@ def heightmap_rain_erosion(
nbDrops: int,
erosionCoef: float,
sedimentationCoef: float,
- rnd: Optional[tcod.random.Random] = None,
+ rnd: tcod.random.Random | None = None,
) -> None:
"""Simulate the effect of rain drops on the terrain, resulting in erosion.
@@ -2585,8 +2639,7 @@ def heightmap_kernel_transform(
minLevel: float,
maxLevel: float,
) -> None:
- """Apply a generic transformation on the map, so that each resulting cell
- value is the weighted sum of several neighbor cells.
+ """Apply a generic transformation on the map, so that each resulting cell value is the weighted sum of several neighbor cells.
This can be used to smooth/sharpen the map.
@@ -2638,7 +2691,7 @@ def heightmap_add_voronoi(
nbPoints: Any,
nbCoef: int,
coef: Sequence[float],
- rnd: Optional[tcod.random.Random] = None,
+ rnd: tcod.random.Random | None = None,
) -> None:
"""Add values from a Voronoi diagram to the heightmap.
@@ -2724,7 +2777,7 @@ def heightmap_scale_fbm(
delta: float,
scale: float,
) -> None:
- """Multiply the heighmap values with FBM noise.
+ """Multiply the heightmap values with FBM noise.
Args:
hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions.
@@ -2758,8 +2811,8 @@ def heightmap_scale_fbm(
@pending_deprecate()
def heightmap_dig_bezier(
hm: NDArray[np.float32],
- px: Tuple[int, int, int, int],
- py: Tuple[int, int, int, int],
+ px: tuple[int, int, int, int],
+ py: tuple[int, int, int, int],
startRadius: float,
startDepth: float,
endRadius: float,
@@ -2811,7 +2864,8 @@ def heightmap_get_value(hm: NDArray[np.float32], x: int, y: int) -> float:
)
return hm[x, y] # type: ignore
else:
- raise ValueError("This array is not contiguous.")
+ msg = "This array is not contiguous."
+ raise ValueError(msg)
@pending_deprecate()
@@ -2845,7 +2899,7 @@ def heightmap_get_slope(hm: NDArray[np.float32], x: int, y: int) -> float:
@pending_deprecate()
-def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> Tuple[float, float, float]:
+def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> tuple[float, float, float]:
"""Return the map normal at given coordinates.
Args:
@@ -2896,7 +2950,7 @@ def heightmap_has_land_on_border(hm: NDArray[np.float32], waterlevel: float) ->
@deprecate("Use `hm.min()` and `hm.max()` instead.")
-def heightmap_get_minmax(hm: NDArray[np.float32]) -> Tuple[float, float]:
+def heightmap_get_minmax(hm: NDArray[np.float32]) -> tuple[float, float]:
"""Return the min and max values of this heightmap.
Args:
@@ -2925,70 +2979,77 @@ def heightmap_delete(hm: Any) -> None:
"""
-@pending_deprecate()
+@deprecate("Use `tcod.image.Image(width, height)` instead.", FutureWarning)
def image_new(width: int, height: int) -> tcod.image.Image:
return tcod.image.Image(width, height)
-@pending_deprecate()
-def image_clear(image: tcod.image.Image, col: Tuple[int, int, int]) -> None:
+@deprecate("Use the `image.clear()` method instead.", FutureWarning)
+def image_clear(image: tcod.image.Image, col: tuple[int, int, int]) -> None:
image.clear(col)
-@pending_deprecate()
+@deprecate("Use the `image.invert()` method instead.", FutureWarning)
def image_invert(image: tcod.image.Image) -> None:
image.invert()
-@pending_deprecate()
+@deprecate("Use the `image.hflip()` method instead.", FutureWarning)
def image_hflip(image: tcod.image.Image) -> None:
image.hflip()
-@pending_deprecate()
+@deprecate("Use the `image.rotate90(n)` method instead.", FutureWarning)
def image_rotate90(image: tcod.image.Image, num: int = 1) -> None:
image.rotate90(num)
-@pending_deprecate()
+@deprecate("Use the `image.vflip()` method instead.", FutureWarning)
def image_vflip(image: tcod.image.Image) -> None:
image.vflip()
-@pending_deprecate()
+@deprecate("Use the `image.scale(new_width, new_height)` method instead.", FutureWarning)
def image_scale(image: tcod.image.Image, neww: int, newh: int) -> None:
image.scale(neww, newh)
-@pending_deprecate()
-def image_set_key_color(image: tcod.image.Image, col: Tuple[int, int, int]) -> None:
+@deprecate("Use the `image.image_set_key_color(rgb)` method instead.", FutureWarning)
+def image_set_key_color(image: tcod.image.Image, col: tuple[int, int, int]) -> None:
image.set_key_color(col)
-@pending_deprecate()
+@deprecate("Use `np.asarray(image)[y, x, 3]` instead.", FutureWarning)
def image_get_alpha(image: tcod.image.Image, x: int, y: int) -> int:
return image.get_alpha(x, y)
-@pending_deprecate()
+@deprecate("Use the Numpy array interface to check alpha or color keys.", FutureWarning)
def image_is_pixel_transparent(image: tcod.image.Image, x: int, y: int) -> bool:
return bool(lib.TCOD_image_is_pixel_transparent(image.image_c, x, y))
-@pending_deprecate(
- "This function may be removed in the future."
- " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio."
+@deprecate(
+ "Call the classmethod `tcod.image.Image.from_file` instead to load images."
+ "\nIt's recommended to load images with a more complete image library such as python-Pillow or python-imageio.",
+ FutureWarning,
)
-def image_load(filename: str) -> tcod.image.Image:
+def image_load(filename: str | PathLike[str]) -> tcod.image.Image:
"""Load an image file into an Image instance and return it.
Args:
- filename (AnyStr): Path to a .bmp or .png image file.
+ filename: Path to a .bmp or .png image file.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
+
+ .. deprecated:: 16.0
+ Use :any:`tcod.image.Image.from_file` instead.
"""
- return tcod.image.Image._from_cdata(ffi.gc(lib.TCOD_image_load(_bytes(filename)), lib.TCOD_image_delete))
+ return tcod.image.Image.from_file(filename)
-@pending_deprecate()
+@deprecate("Use `Tileset.render` instead of this function.", FutureWarning)
def image_from_console(console: tcod.console.Console) -> tcod.image.Image:
"""Return an Image with a Consoles pixel data.
@@ -2996,6 +3057,9 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image:
Args:
console (Console): Any Console instance.
+
+ .. deprecated:: 16.0
+ :any:`Tileset.render` is a better alternative.
"""
return tcod.image.Image._from_cdata(
ffi.gc(
@@ -3005,32 +3069,37 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image:
)
-@pending_deprecate()
+@deprecate("Use `Tileset.render` instead of this function.", FutureWarning)
def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console) -> None:
+ """Update an image made with :any:`image_from_console`.
+
+ .. deprecated:: 16.0
+ This function is unnecessary, use :any:`Tileset.render` instead.
+ """
image.refresh_console(console)
-@pending_deprecate()
-def image_get_size(image: tcod.image.Image) -> Tuple[int, int]:
+@deprecate("Access an images size with `image.width` or `image.height`.", FutureWarning)
+def image_get_size(image: tcod.image.Image) -> tuple[int, int]:
return image.width, image.height
-@pending_deprecate()
-def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> Tuple[int, int, int]:
+@deprecate("Use `np.asarray(image)[y, x, :3]` instead.", FutureWarning)
+def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> tuple[int, int, int]:
return image.get_pixel(x, y)
-@pending_deprecate()
-def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> Tuple[int, int, int]:
+@deprecate("Use the `image.get_mipmap_pixel(...)` method instead.", FutureWarning)
+def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> tuple[int, int, int]:
return image.get_mipmap_pixel(x0, y0, x1, y1)
-@pending_deprecate()
-def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: Tuple[int, int, int]) -> None:
+@deprecate("Use `np.asarray(image)[y, x, :3] = rgb` instead.", FutureWarning)
+def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: tuple[int, int, int]) -> None:
image.put_pixel(x, y, col)
-@pending_deprecate()
+@deprecate("Use the `image.blit(...)` method instead.", FutureWarning)
def image_blit(
image: tcod.image.Image,
console: tcod.console.Console,
@@ -3044,7 +3113,7 @@ def image_blit(
image.blit(console, x, y, bkgnd_flag, scalex, scaley, angle)
-@pending_deprecate()
+@deprecate("Use the `image.blit_rect(...)` method instead.", FutureWarning)
def image_blit_rect(
image: tcod.image.Image,
console: tcod.console.Console,
@@ -3057,7 +3126,7 @@ def image_blit_rect(
image.blit_rect(console, x, y, w, h, bkgnd_flag)
-@pending_deprecate()
+@deprecate("Use `Console.draw_semigraphics(image, ...)` instead.", FutureWarning)
def image_blit_2x(
image: tcod.image.Image,
console: tcod.console.Console,
@@ -3071,12 +3140,12 @@ def image_blit_2x(
image.blit_2x(console, dx, dy, sx, sy, w, h)
-@pending_deprecate()
-def image_save(image: tcod.image.Image, filename: str) -> None:
+@deprecate("Use the `image.save_as` method instead.", FutureWarning)
+def image_save(image: tcod.image.Image, filename: str | PathLike[str]) -> None:
image.save_as(filename)
-@deprecate("libtcod objects are deleted automatically.")
+@deprecate("libtcod objects are deleted automatically.", FutureWarning)
def image_delete(image: tcod.image.Image) -> None:
"""Does nothing. libtcod objects are managed by Python's garbage collector.
@@ -3084,7 +3153,7 @@ def image_delete(image: tcod.image.Image) -> None:
"""
-@deprecate("Use tcod.line_iter instead.")
+@deprecate("Use tcod.los.bresenham instead.", FutureWarning)
def line_init(xo: int, yo: int, xd: int, yd: int) -> None:
"""Initialize a line whose points will be returned by `line_step`.
@@ -3099,13 +3168,13 @@ def line_init(xo: int, yo: int, xd: int, yd: int) -> None:
yd (int): Y destination point.
.. deprecated:: 2.0
- Use `line_iter` instead.
+ This function was replaced by :any:`tcod.los.bresenham`.
"""
lib.TCOD_line_init(xo, yo, xd, yd)
-@deprecate("Use tcod.line_iter instead.")
-def line_step() -> Union[Tuple[int, int], Tuple[None, None]]:
+@deprecate("Use tcod.los.bresenham instead.", FutureWarning)
+def line_step() -> tuple[int, int] | tuple[None, None]:
"""After calling line_init returns (x, y) points of the line.
Once all points are exhausted this function will return (None, None)
@@ -3116,7 +3185,7 @@ def line_step() -> Union[Tuple[int, int], Tuple[None, None]]:
or (None, None) if there are no more points.
.. deprecated:: 2.0
- Use `line_iter` instead.
+ This function was replaced by :any:`tcod.los.bresenham`.
"""
x = ffi.new("int *")
y = ffi.new("int *")
@@ -3126,7 +3195,7 @@ def line_step() -> Union[Tuple[int, int], Tuple[None, None]]:
return None, None
-@deprecate("Use tcod.line_iter instead.")
+@deprecate("Use tcod.los.bresenham instead.", FutureWarning)
def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], bool]) -> bool:
"""Iterate over a line using a callback function.
@@ -3144,11 +3213,11 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b
A callback which takes x and y parameters and returns bool.
Returns:
- bool: False if the callback cancels the line interation by
+ bool: False if the callback cancels the line interaction by
returning False or None, otherwise True.
.. deprecated:: 2.0
- Use `line_iter` instead.
+ This function was replaced by :any:`tcod.los.bresenham`.
"""
for x, y in line_iter(xo, yo, xd, yd):
if not py_callback(x, y):
@@ -3158,9 +3227,9 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b
return False
-@deprecate("This function has been replaced by tcod.los.bresenham.")
-def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[Tuple[int, int]]:
- """returns an Iterable
+@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning)
+def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]:
+ """Returns an Iterable over a Bresenham line.
This Iterable does not include the origin point.
@@ -3185,25 +3254,12 @@ def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[Tuple[int, int]]:
yield (x[0], y[0])
-@deprecate("This function has been replaced by tcod.los.bresenham.")
-def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> Tuple[NDArray[np.intc], NDArray[np.intc]]:
+@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning)
+def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]:
"""Return a NumPy index array following a Bresenham line.
If `inclusive` is true then the start point is included in the result.
- Example:
- >>> where = tcod.line_where(1, 0, 3, 4)
- >>> where
- (array([1, 1, 2, 2, 3]...), array([0, 1, 2, 3, 4]...))
- >>> array = np.zeros((5, 5), dtype=np.int32)
- >>> array[where] = np.arange(len(where[0])) + 1
- >>> array
- array([[0, 0, 0, 0, 0],
- [1, 2, 0, 0, 0],
- [0, 0, 3, 4, 0],
- [0, 0, 0, 0, 5],
- [0, 0, 0, 0, 0]]...)
-
.. versionadded:: 4.6
.. deprecated:: 11.14
@@ -3216,7 +3272,7 @@ def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> Tu
return i, j
-@deprecate("Call tcod.map.Map(width, height) instead.")
+@deprecate("Call tcod.map.Map(width, height) instead.", FutureWarning)
def map_new(w: int, h: int) -> tcod.map.Map:
"""Return a :any:`tcod.map.Map` with a width and height.
@@ -3227,7 +3283,7 @@ def map_new(w: int, h: int) -> tcod.map.Map:
return tcod.map.Map(w, h)
-@deprecate("Use Python's standard copy module instead.")
+@deprecate("Use Python's standard copy module instead.", FutureWarning)
def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None:
"""Copy map data from `source` to `dest`.
@@ -3240,7 +3296,7 @@ def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None:
dest._Map__buffer[:] = source._Map__buffer[:] # type: ignore
-@deprecate("Set properties using the m.transparent and m.walkable arrays.")
+@deprecate("Set properties using the m.transparent and m.walkable arrays.", FutureWarning)
def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: bool) -> None:
"""Set the properties of a single cell.
@@ -3253,7 +3309,7 @@ def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: b
lib.TCOD_map_set_properties(m.map_c, x, y, isTrans, isWalk)
-@deprecate("Clear maps using NumPy broadcast rules instead.")
+@deprecate("Clear maps using NumPy broadcast rules instead.", FutureWarning)
def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False) -> None:
"""Change all map cells to a specific value.
@@ -3265,7 +3321,7 @@ def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False
m.walkable[:] = walkable
-@deprecate("Call the map.compute_fov method instead.")
+@deprecate("Call the map.compute_fov method instead.", FutureWarning)
def map_compute_fov(
m: tcod.map.Map,
x: int,
@@ -3282,10 +3338,9 @@ def map_compute_fov(
m.compute_fov(x, y, radius, light_walls, algo)
-@deprecate("Use map.fov to check for this property.")
+@deprecate("Use map.fov to check for this property.", FutureWarning)
def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool:
- """Return True if the cell at x,y is lit by the last field-of-view
- algorithm.
+ """Return True if the cell at x,y is lit by the last field-of-view algorithm.
.. note::
This function is slow.
@@ -3295,9 +3350,10 @@ def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool:
return bool(lib.TCOD_map_is_in_fov(m.map_c, x, y))
-@deprecate("Use map.transparent to check for this property.")
+@deprecate("Use map.transparent to check for this property.", FutureWarning)
def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool:
- """
+ """Return True is a map cell is transparent.
+
.. note::
This function is slow.
.. deprecated:: 4.5
@@ -3306,9 +3362,10 @@ def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool:
return bool(lib.TCOD_map_is_transparent(m.map_c, x, y))
-@deprecate("Use map.walkable to check for this property.")
+@deprecate("Use map.walkable to check for this property.", FutureWarning)
def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool:
- """
+ """Return True is a map cell is walkable.
+
.. note::
This function is slow.
.. deprecated:: 4.5
@@ -3317,7 +3374,7 @@ def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool:
return bool(lib.TCOD_map_is_walkable(m.map_c, x, y))
-@deprecate("libtcod objects are deleted automatically.")
+@deprecate("libtcod objects are deleted automatically.", FutureWarning)
def map_delete(m: tcod.map.Map) -> None:
"""Does nothing. libtcod objects are managed by Python's garbage collector.
@@ -3325,7 +3382,7 @@ def map_delete(m: tcod.map.Map) -> None:
"""
-@deprecate("Check the map.width attribute instead.")
+@deprecate("Check the map.width attribute instead.", FutureWarning)
def map_get_width(map: tcod.map.Map) -> int:
"""Return the width of a map.
@@ -3335,7 +3392,7 @@ def map_get_width(map: tcod.map.Map) -> int:
return map.width
-@deprecate("Check the map.height attribute instead.")
+@deprecate("Check the map.height attribute instead.", FutureWarning)
def map_get_height(map: tcod.map.Map) -> int:
"""Return the height of a map.
@@ -3345,31 +3402,39 @@ def map_get_height(map: tcod.map.Map) -> int:
return map.height
-@pending_deprecate()
+@deprecate("Use `tcod.sdl.mouse.show(visible)` instead.", FutureWarning)
def mouse_show_cursor(visible: bool) -> None:
- """Change the visibility of the mouse cursor."""
+ """Change the visibility of the mouse cursor.
+
+ .. deprecated:: 16.0
+ Use :any:`tcod.sdl.mouse.show` instead.
+ """
lib.TCOD_mouse_show_cursor(visible)
-@pending_deprecate()
+@deprecate("Use `is_visible = tcod.sdl.mouse.show()` instead.", FutureWarning)
def mouse_is_cursor_visible() -> bool:
- """Return True if the mouse cursor is visible."""
+ """Return True if the mouse cursor is visible.
+
+ .. deprecated:: 16.0
+ Use :any:`tcod.sdl.mouse.show` instead.
+ """
return bool(lib.TCOD_mouse_is_cursor_visible())
-@pending_deprecate()
+@deprecate("Use `tcod.sdl.mouse.warp_in_window` instead.", FutureWarning)
def mouse_move(x: int, y: int) -> None:
lib.TCOD_mouse_move(x, y)
-@deprecate("Use tcod.event.get_mouse_state() instead.")
+@deprecate("Use tcod.event.get_mouse_state() instead.", FutureWarning)
def mouse_get_status() -> Mouse:
return Mouse(lib.TCOD_mouse_get_status())
@pending_deprecate()
-def namegen_parse(filename: str, random: Optional[tcod.random.Random] = None) -> None:
- lib.TCOD_namegen_parse(_bytes(filename), random or ffi.NULL)
+def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None:
+ lib.TCOD_namegen_parse(bytes(Path(filename)), random or ffi.NULL)
@pending_deprecate()
@@ -3383,7 +3448,7 @@ def namegen_generate_custom(name: str, rule: str) -> str:
@pending_deprecate()
-def namegen_get_sets() -> List[str]:
+def namegen_get_sets() -> list[str]:
sets = lib.TCOD_namegen_get_sets()
try:
lst = []
@@ -3399,12 +3464,12 @@ def namegen_destroy() -> None:
lib.TCOD_namegen_destroy()
-@pending_deprecate()
+@deprecate("Use `tcod.noise.Noise(dimensions, hurst=, lacunarity=)` instead.", FutureWarning)
def noise_new(
dim: int,
h: float = NOISE_DEFAULT_HURST,
l: float = NOISE_DEFAULT_LACUNARITY, # noqa: E741
- random: Optional[tcod.random.Random] = None,
+ random: tcod.random.Random | None = None,
) -> tcod.noise.Noise:
"""Return a new Noise instance.
@@ -3420,7 +3485,7 @@ def noise_new(
return tcod.noise.Noise(dim, hurst=h, lacunarity=l, seed=random)
-@pending_deprecate()
+@deprecate("Use `noise.algorithm = x` instead.", FutureWarning)
def noise_set_type(n: tcod.noise.Noise, typ: int) -> None:
"""Set a Noise objects default noise algorithm.
@@ -3430,7 +3495,7 @@ def noise_set_type(n: tcod.noise.Noise, typ: int) -> None:
n.algorithm = typ
-@pending_deprecate()
+@deprecate("Use `value = noise[x]` instead.", FutureWarning)
def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT) -> float:
"""Return the noise value sampled from the ``f`` coordinate.
@@ -3450,7 +3515,7 @@ def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT)
return float(lib.TCOD_noise_get_ex(n.noise_c, ffi.new("float[4]", f), typ))
-@pending_deprecate()
+@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning)
def noise_get_fbm(
n: tcod.noise.Noise,
f: Sequence[float],
@@ -3471,7 +3536,7 @@ def noise_get_fbm(
return float(lib.TCOD_noise_get_fbm_ex(n.noise_c, ffi.new("float[4]", f), oc, typ))
-@pending_deprecate()
+@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning)
def noise_get_turbulence(
n: tcod.noise.Noise,
f: Sequence[float],
@@ -3492,7 +3557,7 @@ def noise_get_turbulence(
return float(lib.TCOD_noise_get_turbulence_ex(n.noise_c, ffi.new("float[4]", f), oc, typ))
-@deprecate("libtcod objects are deleted automatically.")
+@deprecate("libtcod objects are deleted automatically.", FutureWarning)
def noise_delete(n: tcod.noise.Noise) -> None:
# type (Any) -> None
"""Does nothing. libtcod objects are managed by Python's garbage collector.
@@ -3502,7 +3567,7 @@ def noise_delete(n: tcod.noise.Noise) -> None:
def _unpack_union(type_: int, union: Any) -> Any:
- """Unpack items from parser new_property (value_converter)"""
+ """Unpack items from parser new_property (value_converter)."""
if type_ == lib.TCOD_TYPE_BOOL:
return bool(union.b)
elif type_ == lib.TCOD_TYPE_CHAR:
@@ -3569,10 +3634,10 @@ def _pycall_parser_error(msg: Any) -> None:
@deprecate("Parser functions have been deprecated.")
-def parser_run(parser: Any, filename: str, listener: Any = None) -> None:
+def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None:
global _parser_listener
if not listener:
- lib.TCOD_parser_run(parser, _bytes(filename), ffi.NULL)
+ lib.TCOD_parser_run(parser, bytes(Path(filename)), ffi.NULL)
return
propagate_manager = _PropagateException()
@@ -3591,7 +3656,7 @@ def parser_run(parser: Any, filename: str, listener: Any = None) -> None:
with _parser_callback_lock:
_parser_listener = listener
with propagate_manager:
- lib.TCOD_parser_run(parser, _bytes(filename), c_listener)
+ lib.TCOD_parser_run(parser, bytes(Path(filename)), c_listener)
@deprecate("libtcod objects are deleted automatically.")
@@ -3663,7 +3728,7 @@ def random_get_instance() -> tcod.random.Random:
Returns:
Random: A Random instance using the default random number generator.
"""
- return tcod.random.Random._new_from_cdata(ffi.cast("mersenne_data_t*", lib.TCOD_random_get_instance()))
+ return tcod.random.Random._new_from_cdata(lib.TCOD_random_get_instance())
@pending_deprecate()
@@ -3695,7 +3760,7 @@ def random_new_from_seed(seed: Hashable, algo: int = RNG_CMWC) -> tcod.random.Ra
@pending_deprecate()
-def random_set_distribution(rnd: Optional[tcod.random.Random], dist: int) -> None:
+def random_set_distribution(rnd: tcod.random.Random | None, dist: int) -> None:
"""Change the distribution mode of a random number generator.
Args:
@@ -3706,7 +3771,7 @@ def random_set_distribution(rnd: Optional[tcod.random.Random], dist: int) -> Non
@pending_deprecate()
-def random_get_int(rnd: Optional[tcod.random.Random], mi: int, ma: int) -> int:
+def random_get_int(rnd: tcod.random.Random | None, mi: int, ma: int) -> int:
"""Return a random integer in the range: ``mi`` <= n <= ``ma``.
The result is affected by calls to :any:`random_set_distribution`.
@@ -3723,7 +3788,7 @@ def random_get_int(rnd: Optional[tcod.random.Random], mi: int, ma: int) -> int:
@pending_deprecate()
-def random_get_float(rnd: Optional[tcod.random.Random], mi: float, ma: float) -> float:
+def random_get_float(rnd: tcod.random.Random | None, mi: float, ma: float) -> float:
"""Return a random float in the range: ``mi`` <= n <= ``ma``.
The result is affected by calls to :any:`random_set_distribution`.
@@ -3741,7 +3806,7 @@ def random_get_float(rnd: Optional[tcod.random.Random], mi: float, ma: float) ->
@deprecate("Call tcod.random_get_float instead.")
-def random_get_double(rnd: Optional[tcod.random.Random], mi: float, ma: float) -> float:
+def random_get_double(rnd: tcod.random.Random | None, mi: float, ma: float) -> float:
"""Return a random float in the range: ``mi`` <= n <= ``ma``.
.. deprecated:: 2.0
@@ -3752,7 +3817,7 @@ def random_get_double(rnd: Optional[tcod.random.Random], mi: float, ma: float) -
@pending_deprecate()
-def random_get_int_mean(rnd: Optional[tcod.random.Random], mi: int, ma: int, mean: int) -> int:
+def random_get_int_mean(rnd: tcod.random.Random | None, mi: int, ma: int, mean: int) -> int:
"""Return a random weighted integer in the range: ``mi`` <= n <= ``ma``.
The result is affected by calls to :any:`random_set_distribution`.
@@ -3770,7 +3835,7 @@ def random_get_int_mean(rnd: Optional[tcod.random.Random], mi: int, ma: int, mea
@pending_deprecate()
-def random_get_float_mean(rnd: Optional[tcod.random.Random], mi: float, ma: float, mean: float) -> float:
+def random_get_float_mean(rnd: tcod.random.Random | None, mi: float, ma: float, mean: float) -> float:
"""Return a random weighted float in the range: ``mi`` <= n <= ``ma``.
The result is affected by calls to :any:`random_set_distribution`.
@@ -3789,7 +3854,7 @@ def random_get_float_mean(rnd: Optional[tcod.random.Random], mi: float, ma: floa
@deprecate("Call tcod.random_get_float_mean instead.")
-def random_get_double_mean(rnd: Optional[tcod.random.Random], mi: float, ma: float, mean: float) -> float:
+def random_get_double_mean(rnd: tcod.random.Random | None, mi: float, ma: float, mean: float) -> float:
"""Return a random weighted float in the range: ``mi`` <= n <= ``ma``.
.. deprecated:: 2.0
@@ -3800,7 +3865,7 @@ def random_get_double_mean(rnd: Optional[tcod.random.Random], mi: float, ma: flo
@deprecate("Use the standard library 'copy' module instead.")
-def random_save(rnd: Optional[tcod.random.Random]) -> tcod.random.Random:
+def random_save(rnd: tcod.random.Random | None) -> tcod.random.Random:
"""Return a copy of a random number generator.
.. deprecated:: 8.4
@@ -3809,17 +3874,14 @@ def random_save(rnd: Optional[tcod.random.Random]) -> tcod.random.Random:
"""
return tcod.random.Random._new_from_cdata(
ffi.gc(
- ffi.cast(
- "mersenne_data_t*",
- lib.TCOD_random_save(rnd.random_c if rnd else ffi.NULL),
- ),
+ lib.TCOD_random_save(rnd.random_c if rnd else ffi.NULL),
lib.TCOD_random_delete,
)
)
@deprecate("This function is deprecated.")
-def random_restore(rnd: Optional[tcod.random.Random], backup: tcod.random.Random) -> None:
+def random_restore(rnd: tcod.random.Random | None, backup: tcod.random.Random) -> None:
"""Restore a random number generator from a backed up copy.
Args:
@@ -3950,7 +4012,7 @@ def sys_elapsed_milli() -> int:
"""Get number of milliseconds since the start of the program.
Returns:
- int: Time since the progeam has started in milliseconds.
+ int: Time since the program has started in milliseconds.
.. deprecated:: 2.0
Use Python's :mod:`time` module instead.
@@ -3963,7 +4025,7 @@ def sys_elapsed_seconds() -> float:
"""Get number of seconds since the start of the program.
Returns:
- float: Time since the progeam has started in seconds.
+ float: Time since the program has started in seconds.
.. deprecated:: 2.0
Use Python's :mod:`time` module instead.
@@ -3996,7 +4058,7 @@ def sys_get_renderer() -> int:
# easy screenshots
@deprecate("This function is not supported if contexts are being used.")
-def sys_save_screenshot(name: Optional[str] = None) -> None:
+def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None:
"""Save a screenshot to a file.
By default this will automatically save screenshots in the working
@@ -4011,8 +4073,11 @@ def sys_save_screenshot(name: Optional[str] = None) -> None:
.. deprecated:: 11.13
This function is not supported by contexts.
Use :any:`Context.save_screenshot` instead.
+
+ .. versionchanged:: 16.0
+ Added PathLike support.
"""
- lib.TCOD_sys_save_screenshot(_bytes(name) if name is not None else ffi.NULL)
+ lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL)
# custom fullscreen resolution
@@ -4038,7 +4103,7 @@ def sys_force_fullscreen_resolution(width: int, height: int) -> None:
@deprecate("This function is deprecated, which monitor is detected is ambiguous.")
-def sys_get_current_resolution() -> Tuple[int, int]:
+def sys_get_current_resolution() -> tuple[int, int]:
"""Return a monitors pixel resolution as (width, height).
.. deprecated:: 11.13
@@ -4051,8 +4116,8 @@ def sys_get_current_resolution() -> Tuple[int, int]:
@deprecate("This function is not supported if contexts are being used.")
-def sys_get_char_size() -> Tuple[int, int]:
- """Return the current fonts character size as (width, height)
+def sys_get_char_size() -> tuple[int, int]:
+ """Return the current fonts character size as (width, height).
Returns:
Tuple[int,int]: The current font glyph size in (width, height)
@@ -4100,7 +4165,7 @@ def sys_update_char(
@deprecate("This function is not supported if contexts are being used.")
def sys_register_SDL_renderer(callback: Callable[[Any], None]) -> None:
- """Register a custom randering function with libtcod.
+ """Register a custom rendering function with libtcod.
Note:
This callback will only be called by the SDL renderer.
@@ -4127,7 +4192,7 @@ def _pycall_sdl_hook(sdl_surface: Any) -> None:
@deprecate("Use tcod.event.get to check for events.")
-def sys_check_for_event(mask: int, k: Optional[Key], m: Optional[Mouse]) -> int:
+def sys_check_for_event(mask: int, k: Key | None, m: Mouse | None) -> int:
"""Check for and return an event.
Args:
@@ -4144,7 +4209,7 @@ def sys_check_for_event(mask: int, k: Optional[Key], m: Optional[Mouse]) -> int:
@deprecate("Use tcod.event.wait to wait for events.")
-def sys_wait_for_event(mask: int, k: Optional[Key], m: Optional[Mouse], flush: bool) -> int:
+def sys_wait_for_event(mask: int, k: Key | None, m: Mouse | None, flush: bool) -> int:
"""Wait for an event then return.
If flush is True then the buffer will be cleared before waiting. Otherwise
@@ -4207,6 +4272,15 @@ def _atexit_verify() -> None:
lib.TCOD_console_delete(ffi.NULL)
+def __getattr__(name: str) -> Color:
+ """Mark access to color constants as deprecated."""
+ try:
+ return tcod.__getattr__(name, stacklevel=2) # type: ignore[call-arg]
+ except AttributeError:
+ msg = f"module {__name__!r} has no attribute {name!r}"
+ raise AttributeError(msg) from None
+
+
__all__ = [ # noqa: F405
"Color",
"Bsp",
@@ -4716,6 +4790,7 @@ def _atexit_verify() -> None:
"RENDERER_OPENGL2",
"RENDERER_SDL",
"RENDERER_SDL2",
+ "RENDERER_XTERM",
"RIGHT",
"RNG_CMWC",
"RNG_MT",
@@ -4760,202 +4835,5 @@ def _atexit_verify() -> None:
"TYPE_VALUELIST13",
"TYPE_VALUELIST14",
"TYPE_VALUELIST15",
- "amber",
- "azure",
- "black",
- "blue",
- "brass",
- "celadon",
- "chartreuse",
- "copper",
- "crimson",
- "cyan",
- "dark_amber",
- "dark_azure",
- "dark_blue",
- "dark_chartreuse",
- "dark_crimson",
- "dark_cyan",
- "dark_flame",
- "dark_fuchsia",
- "dark_gray",
- "dark_green",
- "dark_grey",
- "dark_han",
- "dark_lime",
- "dark_magenta",
- "dark_orange",
- "dark_pink",
- "dark_purple",
- "dark_red",
- "dark_sea",
- "dark_sepia",
- "dark_sky",
- "dark_turquoise",
- "dark_violet",
- "dark_yellow",
- "darker_amber",
- "darker_azure",
- "darker_blue",
- "darker_chartreuse",
- "darker_crimson",
- "darker_cyan",
- "darker_flame",
- "darker_fuchsia",
- "darker_gray",
- "darker_green",
- "darker_grey",
- "darker_han",
- "darker_lime",
- "darker_magenta",
- "darker_orange",
- "darker_pink",
- "darker_purple",
- "darker_red",
- "darker_sea",
- "darker_sepia",
- "darker_sky",
- "darker_turquoise",
- "darker_violet",
- "darker_yellow",
- "darkest_amber",
- "darkest_azure",
- "darkest_blue",
- "darkest_chartreuse",
- "darkest_crimson",
- "darkest_cyan",
- "darkest_flame",
- "darkest_fuchsia",
- "darkest_gray",
- "darkest_green",
- "darkest_grey",
- "darkest_han",
- "darkest_lime",
- "darkest_magenta",
- "darkest_orange",
- "darkest_pink",
- "darkest_purple",
- "darkest_red",
- "darkest_sea",
- "darkest_sepia",
- "darkest_sky",
- "darkest_turquoise",
- "darkest_violet",
- "darkest_yellow",
- "desaturated_amber",
- "desaturated_azure",
- "desaturated_blue",
- "desaturated_chartreuse",
- "desaturated_crimson",
- "desaturated_cyan",
- "desaturated_flame",
- "desaturated_fuchsia",
- "desaturated_green",
- "desaturated_han",
- "desaturated_lime",
- "desaturated_magenta",
- "desaturated_orange",
- "desaturated_pink",
- "desaturated_purple",
- "desaturated_red",
- "desaturated_sea",
- "desaturated_sky",
- "desaturated_turquoise",
- "desaturated_violet",
- "desaturated_yellow",
- "flame",
- "fuchsia",
- "gold",
- "gray",
- "green",
- "grey",
- "han",
- "light_amber",
- "light_azure",
- "light_blue",
- "light_chartreuse",
- "light_crimson",
- "light_cyan",
- "light_flame",
- "light_fuchsia",
- "light_gray",
- "light_green",
- "light_grey",
- "light_han",
- "light_lime",
- "light_magenta",
- "light_orange",
- "light_pink",
- "light_purple",
- "light_red",
- "light_sea",
- "light_sepia",
- "light_sky",
- "light_turquoise",
- "light_violet",
- "light_yellow",
- "lighter_amber",
- "lighter_azure",
- "lighter_blue",
- "lighter_chartreuse",
- "lighter_crimson",
- "lighter_cyan",
- "lighter_flame",
- "lighter_fuchsia",
- "lighter_gray",
- "lighter_green",
- "lighter_grey",
- "lighter_han",
- "lighter_lime",
- "lighter_magenta",
- "lighter_orange",
- "lighter_pink",
- "lighter_purple",
- "lighter_red",
- "lighter_sea",
- "lighter_sepia",
- "lighter_sky",
- "lighter_turquoise",
- "lighter_violet",
- "lighter_yellow",
- "lightest_amber",
- "lightest_azure",
- "lightest_blue",
- "lightest_chartreuse",
- "lightest_crimson",
- "lightest_cyan",
- "lightest_flame",
- "lightest_fuchsia",
- "lightest_gray",
- "lightest_green",
- "lightest_grey",
- "lightest_han",
- "lightest_lime",
- "lightest_magenta",
- "lightest_orange",
- "lightest_pink",
- "lightest_purple",
- "lightest_red",
- "lightest_sea",
- "lightest_sepia",
- "lightest_sky",
- "lightest_turquoise",
- "lightest_violet",
- "lightest_yellow",
- "lime",
- "magenta",
- "orange",
- "peach",
- "pink",
- "purple",
- "red",
- "sea",
- "sepia",
- "silver",
- "sky",
- "turquoise",
- "violet",
- "white",
- "yellow",
# --- End constants.py ---
]
diff --git a/tcod/loader.py b/tcod/loader.py
index 2469d8ce..36e6d17b 100644
--- a/tcod/loader.py
+++ b/tcod/loader.py
@@ -1,15 +1,16 @@
-"""This module handles loading of the libtcod cffi API.
-"""
+"""This module handles loading of the libtcod cffi API."""
from __future__ import annotations
+import logging
import os
import platform
import sys
-from typing import Any # noqa: F401
+from pathlib import Path
+from typing import Any
-import cffi # type: ignore
+import cffi
-from tcod import __path__ # type: ignore
+logger = logging.getLogger("tcod")
__sdl_version__ = ""
@@ -31,12 +32,13 @@
def verify_dependencies() -> None:
"""Try to make sure dependencies exist on this system."""
if sys.platform == "win32":
- lib_test = ffi_check.dlopen("SDL2.dll") # Make sure SDL2.dll is here.
- version = ffi_check.new("struct SDL_version*")
+ lib_test: Any = ffi_check.dlopen("SDL2.dll") # Make sure SDL2.dll is here.
+ version: Any = ffi_check.new("struct SDL_version*")
lib_test.SDL_GetVersion(version) # Need to check this version.
- version = version.major, version.minor, version.patch
- if version < (2, 0, 5):
- raise RuntimeError("Tried to load an old version of SDL %r" % (version,))
+ version_tuple = version.major, version.minor, version.patch
+ if version_tuple < (2, 0, 5):
+ msg = f"Tried to load an old version of SDL {version_tuple!r}"
+ raise RuntimeError(msg)
def get_architecture() -> str:
@@ -47,23 +49,15 @@ def get_architecture() -> str:
def get_sdl_version() -> str:
sdl_version = ffi.new("SDL_version*")
lib.SDL_GetVersion(sdl_version)
- return "%s.%s.%s" % (
- sdl_version.major,
- sdl_version.minor,
- sdl_version.patch,
- )
+ return f"{sdl_version.major}.{sdl_version.minor}.{sdl_version.patch}"
if sys.platform == "win32":
# add Windows dll's to PATH
- _bits, _linkage = platform.architecture()
- os.environ["PATH"] = "%s;%s" % (
- os.path.join(__path__[0], get_architecture()),
- os.environ["PATH"],
- )
+ os.environ["PATH"] = f"""{Path(__file__).parent / get_architecture()}{os.pathsep}{os.environ["PATH"]}"""
-class _Mock(object):
+class _Mock:
"""Mock object needed for ReadTheDocs."""
@staticmethod
@@ -73,7 +67,6 @@ def def_extern() -> Any:
def __getattr__(self, attr: str) -> None:
"""Return None on any attribute."""
- return None
def __bool__(self) -> bool:
"""Allow checking for this mock object at import time."""
@@ -89,8 +82,22 @@ def __bool__(self) -> bool:
lib = ffi = _Mock()
else:
verify_dependencies()
- from tcod._libtcod import ffi, lib # type: ignore # noqa: F401
+ from tcod._libtcod import ffi, lib # type: ignore
__sdl_version__ = get_sdl_version()
+
+@ffi.def_extern() # type: ignore
+def _libtcod_log_watcher(message: Any, userdata: None) -> None:
+ text = str(ffi.string(message.message), encoding="utf-8")
+ source = str(ffi.string(message.source), encoding="utf-8")
+ level = int(message.level)
+ lineno = int(message.lineno)
+ logger.log(level, "%s:%d:%s", source, lineno, text)
+
+
+if lib:
+ lib.TCOD_set_log_callback(lib._libtcod_log_watcher, ffi.NULL)
+ lib.TCOD_set_log_level(0)
+
__all__ = ["ffi", "lib"]
diff --git a/tcod/los.py b/tcod/los.py
index ce48d23b..d815504f 100644
--- a/tcod/los.py
+++ b/tcod/los.py
@@ -1,8 +1,7 @@
-"""This modules holds functions for NumPy-based line of sight algorithms.
-"""
+"""This modules holds functions for NumPy-based line of sight algorithms."""
from __future__ import annotations
-from typing import Any, Tuple
+from typing import Any
import numpy as np
from numpy.typing import NDArray
@@ -10,7 +9,7 @@
from tcod.loader import ffi, lib
-def bresenham(start: Tuple[int, int], end: Tuple[int, int]) -> NDArray[np.intc]:
+def bresenham(start: tuple[int, int], end: tuple[int, int]) -> NDArray[np.intc]:
"""Return a thin Bresenham line as a NumPy array of shape (length, 2).
`start` and `end` are the endpoints of the line.
diff --git a/tcod/map.py b/tcod/map.py
index 48ce9f9a..e33a0984 100644
--- a/tcod/map.py
+++ b/tcod/map.py
@@ -1,11 +1,8 @@
-"""libtcod map attributes and field-of-view functions.
-
-
-"""
+"""libtcod map attributes and field-of-view functions."""
from __future__ import annotations
import warnings
-from typing import Any, Tuple
+from typing import Any
import numpy as np
from numpy.typing import ArrayLike, NDArray
@@ -16,7 +13,7 @@
from tcod.loader import ffi, lib
-class Map(object):
+class Map:
"""A map containing libtcod attributes.
.. versionchanged:: 4.1
@@ -77,7 +74,7 @@ def __init__(
width: int,
height: int,
order: Literal["C", "F"] = "C",
- ):
+ ) -> None:
warnings.warn(
"This class may perform poorly and is no longer needed.",
DeprecationWarning,
@@ -87,7 +84,7 @@ def __init__(
self.height = height
self._order = tcod._internal.verify_order(order)
- self.__buffer = np.zeros((height, width, 3), dtype=np.bool_)
+ self.__buffer: NDArray[np.bool_] = np.zeros((height, width, 3), dtype=np.bool_)
self.map_c = self.__as_cdata()
def __as_cdata(self) -> Any:
@@ -136,19 +133,19 @@ def compute_fov(
algorithm (int): Defaults to tcod.FOV_RESTRICTIVE
If you already have transparency in a NumPy array then you could use
- :any:`tcod.map_compute_fov` instead.
+ :any:`tcod.map.compute_fov` instead.
"""
if not (0 <= x < self.width and 0 <= y < self.height):
warnings.warn(
- "Index (%r, %r) is outside of this maps shape (%r, %r)."
- "\nThis will raise an error in future versions." % (x, y, self.width, self.height),
+ "Index ({}, {}) is outside of this maps shape ({}, {})."
+ "\nThis will raise an error in future versions.".format(x, y, self.width, self.height),
RuntimeWarning,
stacklevel=2,
)
lib.TCOD_map_compute_fov(self.map_c, x, y, radius, light_walls, algorithm)
- def __setstate__(self, state: Any) -> None:
+ def __setstate__(self, state: dict[str, Any]) -> None:
if "_Map__buffer" not in state: # deprecated
# remove this check on major version update
self.__buffer = np.zeros((state["height"], state["width"], 3), dtype=np.bool_)
@@ -157,12 +154,10 @@ def __setstate__(self, state: Any) -> None:
self.__buffer[:, :, 2] = state["buffer"] & 0x04
del state["buffer"]
state["_order"] = "F"
- if "_order" not in state: # remove this check on major version update
- raise RuntimeError("This Map was saved with a bad version of tdl.")
self.__dict__.update(state)
self.map_c = self.__as_cdata()
- def __getstate__(self) -> Any:
+ def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
del state["map_c"]
return state
@@ -170,7 +165,7 @@ def __getstate__(self) -> Any:
def compute_fov(
transparency: ArrayLike,
- pov: Tuple[int, int],
+ pov: tuple[int, int],
radius: int = 0,
light_walls: bool = True,
algorithm: int = tcod.constants.FOV_RESTRICTIVE,
@@ -240,18 +235,16 @@ def compute_fov(
if len(transparency.shape) != 2:
raise TypeError("transparency must be an array of 2 dimensions" " (shape is %r)" % transparency.shape)
if isinstance(pov, int):
- raise TypeError(
- "The tcod.map.compute_fov function has changed. The `x` and `y`"
- " parameters should now be given as a single tuple."
- )
+ msg = "The tcod.map.compute_fov function has changed. The `x` and `y` parameters should now be given as a single tuple."
+ raise TypeError(msg)
if not (0 <= pov[0] < transparency.shape[0] and 0 <= pov[1] < transparency.shape[1]):
warnings.warn(
- "Given pov index %r is outside the array of shape %r."
- "\nThis will raise an error in future versions." % (pov, transparency.shape),
+ "Given pov index {!r} is outside the array of shape {!r}."
+ "\nThis will raise an error in future versions.".format(pov, transparency.shape),
RuntimeWarning,
stacklevel=2,
)
- map_buffer = np.empty(
+ map_buffer: NDArray[np.bool_] = np.empty(
transparency.shape,
dtype=[("transparent", bool), ("walkable", bool), ("fov", bool)],
)
diff --git a/tcod/noise.py b/tcod/noise.py
index 2ee91c96..843f60b9 100644
--- a/tcod/noise.py
+++ b/tcod/noise.py
@@ -12,33 +12,30 @@
... algorithm=tcod.noise.Algorithm.SIMPLEX,
... seed=42,
... )
- >>> samples = noise[tcod.noise.grid(shape=(5, 5), scale=0.25, origin=(0, 0))]
+ >>> samples = noise[tcod.noise.grid(shape=(5, 4), scale=0.25, origin=(0, 0))]
>>> samples # Samples are a grid of floats between -1.0 and 1.0
array([[ 0. , -0.55046356, -0.76072866, -0.7088647 , -0.68165785],
[-0.27523372, -0.7205134 , -0.74057037, -0.43919194, -0.29195625],
[-0.40398532, -0.57662135, -0.33160293, 0.12860827, 0.2864191 ],
- [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ],
- [-0.64945626, -0.12529983, 0.5346834 , 0.80402255, 0.52655405]],
+ [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ]],
dtype=float32)
>>> (samples + 1.0) * 0.5 # You can normalize samples to 0.0 - 1.0
array([[0.5 , 0.22476822, 0.11963567, 0.14556766, 0.15917107],
[0.36238313, 0.1397433 , 0.12971482, 0.28040403, 0.35402188],
[0.29800734, 0.21168932, 0.33419853, 0.5643041 , 0.6432096 ],
- [0.24613297, 0.3678193 , 0.6222316 , 0.8195127 , 0.79614234],
- [0.17527187, 0.4373501 , 0.76734173, 0.9020113 , 0.76327705]],
+ [0.24613297, 0.3678193 , 0.6222316 , 0.8195127 , 0.79614234]],
dtype=float32)
>>> ((samples + 1.0) * (256 / 2)).astype(np.uint8) # Or as 8-bit unsigned bytes.
array([[128, 57, 30, 37, 40],
[ 92, 35, 33, 71, 90],
[ 76, 54, 85, 144, 164],
- [ 63, 94, 159, 209, 203],
- [ 44, 111, 196, 230, 195]], dtype=uint8)
-""" # noqa: E501
+ [ 63, 94, 159, 209, 203]], dtype=uint8)
+"""
from __future__ import annotations
import enum
import warnings
-from typing import Any, Optional, Sequence, Tuple, Union
+from typing import Any, Sequence
import numpy as np
from numpy.typing import ArrayLike, NDArray
@@ -46,7 +43,6 @@
import tcod.constants
import tcod.random
-from tcod._internal import deprecate
from tcod.loader import ffi, lib
@@ -95,34 +91,32 @@ def __getattr__(name: str) -> Implementation:
if name in Implementation.__members__:
warnings.warn(
f"'tcod.noise.{name}' is deprecated," f" use 'tcod.noise.Implementation.{name}' instead.",
- DeprecationWarning,
+ FutureWarning,
stacklevel=2,
)
return Implementation[name]
- raise AttributeError(f"module {__name__} has no attribute {name}")
+ msg = f"module {__name__} has no attribute {name}"
+ raise AttributeError(msg)
-class Noise(object):
- """
+class Noise:
+ """A configurable noise sampler.
The ``hurst`` exponent describes the raggedness of the resultant noise,
with a higher value leading to a smoother noise.
Not used with tcod.noise.SIMPLE.
- ``lacunarity`` is a multiplier that determines how fast the noise
- frequency increases for each successive octave.
+ ``lacunarity`` is a multiplier that determines how fast the noise frequency increases for each successive octave.
Not used with tcod.noise.SIMPLE.
Args:
- dimensions (int): Must be from 1 to 4.
- algorithm (int): Defaults to :any:`tcod.noise.Algorithm.SIMPLEX`
- implementation (int):
- Defaults to :any:`tcod.noise.Implementation.SIMPLE`
- hurst (float): The hurst exponent. Should be in the 0.0-1.0 range.
- lacunarity (float): The noise lacunarity.
- octaves (float): The level of detail on fBm and turbulence
- implementations.
- seed (Optional[Random]): A Random instance, or None.
+ dimensions: Must be from 1 to 4.
+ algorithm: Defaults to :any:`tcod.noise.Algorithm.SIMPLEX`
+ implementation: Defaults to :any:`tcod.noise.Implementation.SIMPLE`
+ hurst: The hurst exponent. Should be in the 0.0-1.0 range.
+ lacunarity: The noise lacunarity.
+ octaves: The level of detail on fBm and turbulence implementations.
+ seed: A Random instance, or None.
Attributes:
noise_c (CData): A cffi pointer to a TCOD_noise_t object.
@@ -136,10 +130,11 @@ def __init__(
hurst: float = 0.5,
lacunarity: float = 2.0,
octaves: float = 4,
- seed: Optional[Union[int, tcod.random.Random]] = None,
- ):
+ seed: int | tcod.random.Random | None = None,
+ ) -> None:
if not 0 < dimensions <= 4:
- raise ValueError("dimensions must be in range 0 < n <= 4, got %r" % (dimensions,))
+ msg = f"dimensions must be in range 0 < n <= 4, got {dimensions}"
+ raise ValueError(msg)
self._seed = seed
self._random = self.__rng_from_seed(seed)
_random_c = self._random.random_c
@@ -155,7 +150,7 @@ def __init__(
self.implementation = implementation # sanity check
@staticmethod
- def __rng_from_seed(seed: Union[None, int, tcod.random.Random]) -> tcod.random.Random:
+ def __rng_from_seed(seed: None | int | tcod.random.Random) -> tcod.random.Random:
if seed is None or isinstance(seed, int):
return tcod.random.Random(seed=seed, algorithm=tcod.random.MERSENNE_TWISTER)
return seed
@@ -180,11 +175,6 @@ def __repr__(self) -> str:
def dimensions(self) -> int:
return int(self._tdl_noise_c.dimensions)
- @property # type: ignore
- @deprecate("This is a misspelling of 'dimensions'.")
- def dimentions(self) -> int:
- return self.dimensions
-
@property
def algorithm(self) -> int:
noise_type = self.noise_c.noise_type
@@ -201,7 +191,8 @@ def implementation(self) -> int:
@implementation.setter
def implementation(self, value: int) -> None:
if not 0 <= value < 3:
- raise ValueError("%r is not a valid implementation. " % (value,))
+ msg = f"{value!r} is not a valid implementation. "
+ raise ValueError(msg)
self._tdl_noise_c.implementation = value
@property
@@ -224,18 +215,17 @@ def get_point(self, x: float = 0, y: float = 0, z: float = 0, w: float = 0) -> f
"""Return the noise value at the (x, y, z, w) point.
Args:
- x (float): The position on the 1st axis.
- y (float): The position on the 2nd axis.
- z (float): The position on the 3rd axis.
- w (float): The position on the 4th axis.
+ x: The position on the 1st axis.
+ y: The position on the 2nd axis.
+ z: The position on the 3rd axis.
+ w: The position on the 4th axis.
"""
return float(lib.NoiseGetSample(self._tdl_noise_c, (x, y, z, w)))
def __getitem__(self, indexes: Any) -> NDArray[np.float32]:
"""Sample a noise map through NumPy indexing.
- This follows NumPy's advanced indexing rules, but allows for floating
- point values.
+ This follows NumPy's advanced indexing rules, but allows for floating point values.
.. versionadded:: 11.16
"""
@@ -245,15 +235,16 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]:
raise IndexError(
"This noise generator has %i dimensions, but was indexed with %i." % (self.dimensions, len(indexes))
)
- indexes = np.broadcast_arrays(*indexes) # type: ignore
+ indexes = np.broadcast_arrays(*indexes)
c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL]
for i, index in enumerate(indexes):
if index.dtype.type == np.object_:
- raise TypeError("Index arrays can not be of dtype np.object_.")
+ msg = "Index arrays can not be of dtype np.object_."
+ raise TypeError(msg)
indexes[i] = np.ascontiguousarray(index, dtype=np.float32)
c_input[i] = ffi.from_buffer("float*", indexes[i])
- out = np.empty(indexes[0].shape, dtype=np.float32)
+ out: NDArray[np.float32] = np.empty(indexes[0].shape, dtype=np.float32)
if self.implementation == Implementation.SIMPLE:
lib.TCOD_noise_get_vectorized(
self.noise_c,
@@ -292,23 +283,23 @@ def sample_mgrid(self, mgrid: ArrayLike) -> NDArray[np.float32]:
overhead when working with large mesh-grids.
Args:
- mgrid (numpy.ndarray): A mesh-grid array of points to sample.
+ mgrid: A mesh-grid array of points to sample.
A contiguous array of type `numpy.float32` is preferred.
Returns:
- numpy.ndarray: An array of sampled points.
+ An array of sampled points.
- This array has the shape: ``mgrid.shape[:-1]``.
- The ``dtype`` is `numpy.float32`.
+ This array has the shape: ``mgrid.shape[:-1]``.
+ The ``dtype`` is `numpy.float32`.
"""
mgrid = np.ascontiguousarray(mgrid, np.float32)
if mgrid.shape[0] != self.dimensions:
- raise ValueError(
- "mgrid.shape[0] must equal self.dimensions, " "%r[0] != %r" % (mgrid.shape, self.dimensions)
- )
+ msg = f"mgrid.shape[0] must equal self.dimensions, {mgrid.shape!r}[0] != {self.dimensions!r}"
+ raise ValueError(msg)
out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray(mgrid.shape[1:], np.float32)
if mgrid.shape[1:] != out.shape:
- raise ValueError("mgrid.shape[1:] must equal out.shape, " "%r[1:] != %r" % (mgrid.shape, out.shape))
+ msg = f"mgrid.shape[1:] must equal out.shape, {mgrid.shape!r}[1:] != {out.shape!r}"
+ raise ValueError(msg)
lib.NoiseSampleMeshGrid(
self._tdl_noise_c,
out.size,
@@ -320,19 +311,19 @@ def sample_mgrid(self, mgrid: ArrayLike) -> NDArray[np.float32]:
def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]:
"""Sample an open mesh-grid array and return the result.
- Args
- ogrid (Sequence[ArrayLike]): An open mesh-grid.
+ Args:
+ ogrid: An open mesh-grid.
Returns:
- numpy.ndarray: An array of sampled points.
+ An array of sampled points.
- The ``shape`` is based on the lengths of the open mesh-grid
- arrays.
- The ``dtype`` is `numpy.float32`.
+ The ``shape`` is based on the lengths of the open mesh-grid arrays.
+ The ``dtype`` is `numpy.float32`.
"""
if len(ogrid) != self.dimensions:
- raise ValueError("len(ogrid) must equal self.dimensions, " "%r != %r" % (len(ogrid), self.dimensions))
- ogrids = [np.ascontiguousarray(array, np.float32) for array in ogrid]
+ msg = f"len(ogrid) must equal self.dimensions, {len(ogrid)!r} != {self.dimensions!r}"
+ raise ValueError(msg)
+ ogrids: list[NDArray[np.float32]] = [np.ascontiguousarray(array, np.float32) for array in ogrid]
out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray([array.size for array in ogrids], np.float32)
lib.NoiseSampleOpenMeshGrid(
self._tdl_noise_c,
@@ -343,7 +334,7 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]:
)
return out
- def __getstate__(self) -> Any:
+ def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL:
# Trigger a side effect of wavelet, so that copies will be synced.
@@ -374,7 +365,7 @@ def __getstate__(self) -> Any:
}
return state
- def __setstate__(self, state: Any) -> None:
+ def __setstate__(self, state: dict[str, Any]) -> None:
if isinstance(state, tuple): # deprecated format
return self._setstate_old(state)
# unpack wavelet tile data if it exists
@@ -392,6 +383,7 @@ def __setstate__(self, state: Any) -> None:
state["_tdl_noise_c"]["noise"] = state["noise_c"]
state["_tdl_noise_c"] = ffi.new("TDLNoise*", state["_tdl_noise_c"])
self.__dict__.update(state)
+ return None
def _setstate_old(self, state: Any) -> None:
self._random = state[0]
@@ -411,54 +403,57 @@ def _setstate_old(self, state: Any) -> None:
def grid(
- shape: Tuple[int, ...],
- scale: Union[Tuple[float, ...], float],
- origin: Optional[Tuple[int, ...]] = None,
+ shape: tuple[int, ...],
+ scale: tuple[float, ...] | float,
+ origin: tuple[int, ...] | None = None,
indexing: Literal["ij", "xy"] = "xy",
-) -> Tuple[NDArray[Any], ...]:
- """A helper function for generating a grid of noise samples.
-
- `shape` is the shape of the returned mesh grid. This can be any number of
- dimensions, but :class:`Noise` classes only support up to 4.
-
- `scale` is the step size of indexes away from `origin`.
- This can be a single float, or it can be a tuple of floats with one float
- for each axis in `shape`. A lower scale gives smoother transitions
- between noise values.
+) -> tuple[NDArray[Any], ...]:
+ """Generate a mesh-grid of sample points to use with noise sampling.
- `origin` is the first sample of the grid.
- If `None` then the `origin` will be zero on each axis.
- `origin` is not scaled by the `scale` parameter.
-
- `indexing` is passed to :any:`numpy.meshgrid`.
+ Args:
+ shape: The shape of the grid.
+ This can be any number of dimensions, but :class:`Noise` classes only support up to 4.
+ scale: The step size between samples.
+ This can be a single float, or it can be a tuple of floats with one float for each axis in `shape`.
+ A lower scale gives smoother transitions between noise values.
+ origin: The position of the first sample.
+ If `None` then the `origin` will be zero on each axis.
+ `origin` is not scaled by the `scale` parameter.
+ indexing: Passed to :any:`numpy.meshgrid`.
+
+ Returns:
+ A sparse mesh-grid to be passed into a :class:`Noise` instance.
Example::
>>> noise = tcod.noise.Noise(dimensions=2, seed=42)
- >>> noise[tcod.noise.grid(shape=(5, 5), scale=0.25)]
- array([[ 0. , -0.55046356, -0.76072866, -0.7088647 , -0.68165785],
- [-0.27523372, -0.7205134 , -0.74057037, -0.43919194, -0.29195625],
- [-0.40398532, -0.57662135, -0.33160293, 0.12860827, 0.2864191 ],
- [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ],
- [-0.64945626, -0.12529983, 0.5346834 , 0.80402255, 0.52655405]],
+
+ # Common case for ij-indexed arrays.
+ >>> noise[tcod.noise.grid(shape=(3, 5), scale=0.25, indexing="ij")]
+ array([[ 0. , -0.27523372, -0.40398532, -0.50773406, -0.64945626],
+ [-0.55046356, -0.7205134 , -0.57662135, -0.2643614 , -0.12529983],
+ [-0.76072866, -0.74057037, -0.33160293, 0.24446318, 0.5346834 ]],
dtype=float32)
- >>> noise[tcod.noise.grid(shape=(5, 5), scale=(0.5, 0.25), origin=(1, 1))]
- array([[ 0.52655405, -0.5037453 , -0.81221616, -0.7057655 , 0.24630858],
- [ 0.25038874, -0.75348294, -0.6379566 , -0.5817767 , -0.02789652],
- [-0.03488023, -0.73630923, -0.12449139, -0.22774395, -0.22243626],
- [-0.18455243, -0.35063767, 0.4495706 , 0.02399864, -0.42226675],
- [-0.16333057, 0.18149695, 0.7547447 , -0.07006818, -0.6546707 ]],
+
+ # Transpose an xy-indexed array to get a standard order="F" result.
+ >>> noise[tcod.noise.grid(shape=(4, 5), scale=(0.5, 0.25), origin=(1.0, 1.0))].T
+ array([[ 0.52655405, 0.25038874, -0.03488023, -0.18455243, -0.16333057],
+ [-0.5037453 , -0.75348294, -0.73630923, -0.35063767, 0.18149695],
+ [-0.81221616, -0.6379566 , -0.12449139, 0.4495706 , 0.7547447 ],
+ [-0.7057655 , -0.5817767 , -0.22774395, 0.02399864, -0.07006818]],
dtype=float32)
.. versionadded:: 12.2
- """ # noqa: E501
+ """
if isinstance(scale, float):
scale = (scale,) * len(shape)
if origin is None:
origin = (0,) * len(shape)
if len(shape) != len(scale):
- raise TypeError("shape must have the same length as scale")
+ msg = "shape must have the same length as scale"
+ raise TypeError(msg)
if len(shape) != len(origin):
- raise TypeError("shape must have the same length as origin")
+ msg = "shape must have the same length as origin"
+ raise TypeError(msg)
indexes = (np.arange(i_shape) * i_scale + i_origin for i_shape, i_scale, i_origin in zip(shape, scale, origin))
- return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing)) # type: ignore
+ return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing))
diff --git a/tcod/path.c b/tcod/path.c
index dac89091..873e2dae 100644
--- a/tcod/path.c
+++ b/tcod/path.c
@@ -440,9 +440,9 @@ int update_frontier_heuristic(
for (int i = 0; i < frontier->heap.size; ++i) {
unsigned char* heap_ptr = (unsigned char*)frontier->heap.heap;
heap_ptr += frontier->heap.node_size * i;
- struct TCOD_HeapNode* heap_node = (void*)heap_ptr;
- struct FrontierNode* f_node = (struct FrontierNode*)heap_node->data;
- heap_node->priority = (f_node->distance + compute_heuristic(heuristic, frontier->ndim, f_node->index));
+ int* priority = (int*)heap_ptr;
+ struct FrontierNode* f_node = (struct FrontierNode*)(heap_ptr + frontier->heap.data_offset);
+ *priority = (f_node->distance + compute_heuristic(heuristic, frontier->ndim, f_node->index));
}
TCOD_minheap_heapify(&frontier->heap);
return 0;
@@ -478,8 +478,7 @@ int frontier_has_index(
for (int i = 0; i < frontier->heap.size; ++i) {
const unsigned char* heap_ptr = (const unsigned char*)frontier->heap.heap;
heap_ptr += frontier->heap.node_size * i;
- const struct TCOD_HeapNode* heap_node = (const void*)heap_ptr;
- const struct FrontierNode* f_node = (const void*)heap_node->data;
+ const struct FrontierNode* f_node = (const void*)(heap_ptr + frontier->heap.data_offset);
bool found = 1;
for (int j = 0; j < frontier->ndim; ++j) {
if (index[j] != f_node->index[j]) {
diff --git a/tcod/path.py b/tcod/path.py
index 59b9b301..9e2439c8 100644
--- a/tcod/path.py
+++ b/tcod/path.py
@@ -20,20 +20,19 @@
import functools
import itertools
import warnings
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+from typing import Any, Callable
import numpy as np
from numpy.typing import ArrayLike, NDArray
from typing_extensions import Literal
-import tcod.map # noqa: F401
from tcod._internal import _check
from tcod.loader import ffi, lib
@ffi.def_extern() # type: ignore
def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float:
- """libtcodpy style callback, needs to preserve the old userData issue."""
+ """Libtcodpy style callback, needs to preserve the old userData issue."""
func, userData = ffi.from_handle(handle)
return func(x1, y1, x2, y2, userData) # type: ignore
@@ -56,7 +55,7 @@ def _pycall_path_dest_only(x1: int, y1: int, x2: int, y2: int, handle: Any) -> f
return ffi.from_handle(handle)(x2, y2) # type: ignore
-def _get_pathcost_func(
+def _get_path_cost_func(
name: str,
) -> Callable[[int, int, int, int, Any], float]:
"""Return a properly cast PathCostArray callback."""
@@ -65,7 +64,7 @@ def _get_pathcost_func(
return ffi.cast("TCOD_path_func_t", ffi.addressof(lib, name)) # type: ignore
-class _EdgeCostFunc(object):
+class _EdgeCostFunc:
"""Generic edge-cost function factory.
`userdata` is the custom userdata to send to the C call.
@@ -75,16 +74,16 @@ class _EdgeCostFunc(object):
_CALLBACK_P = lib._pycall_path_old
- def __init__(self, userdata: Any, shape: Tuple[int, int]) -> None:
+ def __init__(self, userdata: Any, shape: tuple[int, int]) -> None:
self._userdata = userdata
self.shape = shape
- def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]:
- """Return (C callback, userdata handle, shape)"""
+ def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]:
+ """Return (C callback, userdata handle, shape)."""
return self._CALLBACK_P, ffi.new_handle(self._userdata), self.shape
def __repr__(self) -> str:
- return "%s(%r, shape=%r)" % (
+ return "{}({!r}, shape={!r})".format(
self.__class__.__name__,
self._userdata,
self.shape,
@@ -109,10 +108,10 @@ class EdgeCostCallback(_EdgeCostFunc):
def __init__(
self,
callback: Callable[[int, int, int, int], float],
- shape: Tuple[int, int],
- ):
+ shape: tuple[int, int],
+ ) -> None:
self.callback = callback
- super(EdgeCostCallback, self).__init__(callback, shape)
+ super().__init__(callback, shape)
class NodeCostArray(np.ndarray): # type: ignore
@@ -123,14 +122,14 @@ class NodeCostArray(np.ndarray): # type: ignore
"""
_C_ARRAY_CALLBACKS = {
- np.float32: ("float*", _get_pathcost_func("PathCostArrayFloat32")),
- np.bool_: ("int8_t*", _get_pathcost_func("PathCostArrayInt8")),
- np.int8: ("int8_t*", _get_pathcost_func("PathCostArrayInt8")),
- np.uint8: ("uint8_t*", _get_pathcost_func("PathCostArrayUInt8")),
- np.int16: ("int16_t*", _get_pathcost_func("PathCostArrayInt16")),
- np.uint16: ("uint16_t*", _get_pathcost_func("PathCostArrayUInt16")),
- np.int32: ("int32_t*", _get_pathcost_func("PathCostArrayInt32")),
- np.uint32: ("uint32_t*", _get_pathcost_func("PathCostArrayUInt32")),
+ np.float32: ("float*", _get_path_cost_func("PathCostArrayFloat32")),
+ np.bool_: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")),
+ np.int8: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")),
+ np.uint8: ("uint8_t*", _get_path_cost_func("PathCostArrayUInt8")),
+ np.int16: ("int16_t*", _get_path_cost_func("PathCostArrayInt16")),
+ np.uint16: ("uint16_t*", _get_path_cost_func("PathCostArrayUInt16")),
+ np.int32: ("int32_t*", _get_path_cost_func("PathCostArrayInt32")),
+ np.uint32: ("uint32_t*", _get_path_cost_func("PathCostArrayUInt32")),
}
def __new__(cls, array: ArrayLike) -> NodeCostArray:
@@ -139,16 +138,15 @@ def __new__(cls, array: ArrayLike) -> NodeCostArray:
return self
def __repr__(self) -> str:
- return "%s(%r)" % (
- self.__class__.__name__,
- repr(self.view(np.ndarray)),
- )
+ return f"{self.__class__.__name__}({repr(self.view(np.ndarray))!r})"
- def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]:
+ def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]:
if len(self.shape) != 2:
- raise ValueError("Array must have a 2d shape, shape is %r" % (self.shape,))
+ msg = f"Array must have a 2d shape, shape is {self.shape!r}"
+ raise ValueError(msg)
if self.dtype.type not in self._C_ARRAY_CALLBACKS:
- raise ValueError("dtype must be one of %r, dtype is %r" % (self._C_ARRAY_CALLBACKS.keys(), self.dtype.type))
+ msg = f"dtype must be one of {self._C_ARRAY_CALLBACKS.keys()!r}, dtype is {self.dtype.type!r}"
+ raise ValueError(msg)
array_type, callback = self._C_ARRAY_CALLBACKS[self.dtype.type]
userdata = ffi.new(
@@ -158,10 +156,10 @@ def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]:
return callback, userdata, (self.shape[0], self.shape[1])
-class _PathFinder(object):
+class _PathFinder:
"""A class sharing methods used by AStar and Dijkstra."""
- def __init__(self, cost: Any, diagonal: float = 1.41):
+ def __init__(self, cost: Any, diagonal: float = 1.41) -> None:
self.cost = cost
self.diagonal = diagonal
self._path_c: Any = None
@@ -198,11 +196,7 @@ def __init__(self, cost: Any, diagonal: float = 1.41):
)
def __repr__(self) -> str:
- return "%s(cost=%r, diagonal=%r)" % (
- self.__class__.__name__,
- self.cost,
- self.diagonal,
- )
+ return f"{self.__class__.__name__}(cost={self.cost!r}, diagonal={self.diagonal!r})"
def __getstate__(self) -> Any:
state = self.__dict__.copy()
@@ -222,14 +216,15 @@ def __setstate__(self, state: Any) -> None:
class AStar(_PathFinder):
- """
+ """The older libtcod A* pathfinder.
+
Args:
cost (Union[tcod.map.Map, numpy.ndarray, Any]):
diagonal (float): Multiplier for diagonal movement.
A value of 0 will disable diagonal movement entirely.
"""
- def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]:
+ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> list[tuple[int, int]]:
"""Return a list of (x, y) steps to reach the goal point, if possible.
Args:
@@ -237,6 +232,7 @@ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List
start_y (int): Starting Y position.
goal_x (int): Destination X position.
goal_y (int): Destination Y position.
+
Returns:
List[Tuple[int, int]]:
A list of points, or an empty list if there is no valid path.
@@ -251,7 +247,8 @@ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List
class Dijkstra(_PathFinder):
- """
+ """The older libtcod Dijkstra pathfinder.
+
Args:
cost (Union[tcod.map.Map, numpy.ndarray, Any]):
diagonal (float): Multiplier for diagonal movement.
@@ -266,7 +263,7 @@ def set_goal(self, x: int, y: int) -> None:
"""Set the goal point and recompute the Dijkstra path-finder."""
lib.TCOD_dijkstra_compute(self._path_c, x, y)
- def get_path(self, x: int, y: int) -> List[Tuple[int, int]]:
+ def get_path(self, x: int, y: int) -> list[tuple[int, int]]:
"""Return a list of (x, y) steps to reach the goal point, if possible."""
lib.TCOD_dijkstra_path_set(self._path_c, x, y)
path = []
@@ -292,7 +289,7 @@ def get_path(self, x: int, y: int) -> List[Tuple[int, int]]:
def maxarray(
- shape: Tuple[int, ...],
+ shape: tuple[int, ...],
dtype: Any = np.int32,
order: Literal["C", "F"] = "C",
) -> NDArray[Any]:
@@ -314,10 +311,11 @@ def maxarray(
return np.full(shape, np.iinfo(dtype).max, dtype, order)
-def _export_dict(array: NDArray[Any]) -> Dict[str, Any]:
+def _export_dict(array: NDArray[Any]) -> dict[str, Any]:
"""Convert a NumPy array into a format compatible with CFFI."""
if array.dtype.type not in _INT_TYPES:
- raise TypeError("dtype was %s, but must be one of %s." % (array.dtype.type, tuple(_INT_TYPES.keys())))
+ msg = f"dtype was {array.dtype.type}, but must be one of {tuple(_INT_TYPES.keys())}."
+ raise TypeError(msg)
return {
"type": _INT_TYPES[array.dtype.type],
"ndim": array.ndim,
@@ -332,7 +330,7 @@ def _export(array: NDArray[Any]) -> Any:
return ffi.new("struct NArray*", _export_dict(array))
-def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]:
+def _compile_cost_edges(edge_map: Any) -> tuple[Any, int]:
"""Return an edge_cost array using an integer map."""
edge_map = np.array(edge_map, copy=True)
if edge_map.ndim != 2:
@@ -344,7 +342,7 @@ def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]:
edge_array = np.transpose(edge_nz)
edge_array -= edge_center
c_edges = ffi.new("int[]", len(edge_array) * 3)
- edges = np.frombuffer(ffi.buffer(c_edges), dtype=np.intc).reshape(len(edge_array), 3) # type: ignore
+ edges = np.frombuffer(ffi.buffer(c_edges), dtype=np.intc).reshape(len(edge_array), 3)
edges[:, :2] = edge_array
edges[:, 2] = edge_map[edge_nz]
return c_edges, len(edge_array)
@@ -353,11 +351,11 @@ def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]:
def dijkstra2d(
distance: ArrayLike,
cost: ArrayLike,
- cardinal: Optional[int] = None,
- diagonal: Optional[int] = None,
+ cardinal: int | None = None,
+ diagonal: int | None = None,
*,
edge_map: Any = None,
- out: Optional[np.ndarray] = ..., # type: ignore
+ out: np.ndarray | None = ..., # type: ignore
) -> NDArray[Any]:
"""Return the computed distance of all nodes on a 2D Dijkstra grid.
@@ -473,8 +471,8 @@ def dijkstra2d(
.. versionchanged:: 12.1
Added `out` parameter. Now returns the output array.
"""
- dist = np.asarray(distance)
- if out is ...: # type: ignore
+ dist: NDArray[Any] = np.asarray(distance)
+ if out is ...:
out = dist
warnings.warn(
"No `out` parameter was given. "
@@ -491,14 +489,17 @@ def dijkstra2d(
out[...] = dist
if dist.shape != out.shape:
- raise TypeError("distance and output must have the same shape %r != %r" % (dist.shape, out.shape))
+ msg = f"distance and output must have the same shape {dist.shape!r} != {out.shape!r}"
+ raise TypeError(msg)
cost = np.asarray(cost)
if dist.shape != cost.shape:
- raise TypeError("output and cost must have the same shape %r != %r" % (out.shape, cost.shape))
+ msg = f"output and cost must have the same shape {out.shape!r} != {cost.shape!r}"
+ raise TypeError(msg)
c_dist = _export(out)
if edge_map is not None:
if cardinal is not None or diagonal is not None:
- raise TypeError("`edge_map` can not be set at the same time as" " `cardinal` or `diagonal`.")
+ msg = "`edge_map` can not be set at the same time as `cardinal` or `diagonal`."
+ raise TypeError(msg)
c_edges, n_edges = _compile_cost_edges(edge_map)
_check(lib.dijkstra2d(c_dist, _export(cost), n_edges, c_edges))
else:
@@ -510,7 +511,7 @@ def dijkstra2d(
return out
-def _compile_bool_edges(edge_map: ArrayLike) -> Tuple[Any, int]:
+def _compile_bool_edges(edge_map: ArrayLike) -> tuple[Any, int]:
"""Return an edge array using a boolean map."""
edge_map = np.array(edge_map, copy=True)
edge_center = edge_map.shape[0] // 2, edge_map.shape[1] // 2
@@ -522,9 +523,9 @@ def _compile_bool_edges(edge_map: ArrayLike) -> Tuple[Any, int]:
def hillclimb2d(
distance: ArrayLike,
- start: Tuple[int, int],
- cardinal: Optional[bool] = None,
- diagonal: Optional[bool] = None,
+ start: tuple[int, int],
+ cardinal: bool | None = None,
+ diagonal: bool | None = None,
*,
edge_map: Any = None,
) -> NDArray[Any]:
@@ -560,13 +561,15 @@ def hillclimb2d(
Added `edge_map` parameter.
"""
x, y = start
- dist = np.asarray(distance)
+ dist: NDArray[Any] = np.asarray(distance)
if not (0 <= x < dist.shape[0] and 0 <= y < dist.shape[1]):
- raise IndexError("Starting point %r not in shape %r" % (start, dist.shape))
+ msg = f"Starting point {start!r} not in shape {dist.shape!r}"
+ raise IndexError(msg)
c_dist = _export(dist)
if edge_map is not None:
if cardinal is not None or diagonal is not None:
- raise TypeError("`edge_map` can not be set at the same time as" " `cardinal` or `diagonal`.")
+ msg = "`edge_map` can not be set at the same time as `cardinal` or `diagonal`."
+ raise TypeError(msg)
c_edges, n_edges = _compile_bool_edges(edge_map)
func = functools.partial(lib.hillclimb2d, c_dist, x, y, n_edges, c_edges)
else:
@@ -578,11 +581,11 @@ def hillclimb2d(
return path
-def _world_array(shape: Tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]:
+def _world_array(shape: tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]:
"""Return an array where ``ij == arr[ij]``."""
return np.ascontiguousarray(
np.transpose(
- np.meshgrid( # type: ignore
+ np.meshgrid(
*(np.arange(i, dtype=dtype) for i in shape),
indexing="ij",
copy=False,
@@ -592,7 +595,7 @@ def _world_array(shape: Tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]:
)
-def _as_hashable(obj: Optional[np.ndarray[Any, Any]]) -> Optional[Any]:
+def _as_hashable(obj: np.ndarray[Any, Any] | None) -> Any | None:
"""Return NumPy arrays as a more hashable form."""
if obj is None:
return obj
@@ -661,36 +664,37 @@ class CustomGraph:
Added the `order` parameter.
"""
- def __init__(self, shape: Tuple[int, ...], *, order: str = "C"):
+ def __init__(self, shape: tuple[int, ...], *, order: str = "C") -> None:
self._shape = self._shape_c = tuple(shape)
self._ndim = len(self._shape)
self._order = order
if self._order == "F":
self._shape_c = self._shape_c[::-1]
if not 0 < self._ndim <= 4:
- raise TypeError("Graph dimensions must be 1 <= n <= 4.")
- self._graph: Dict[Tuple[Any, ...], Dict[str, Any]] = {}
- self._edge_rules_keep_alive: List[Any] = []
+ msg = "Graph dimensions must be 1 <= n <= 4."
+ raise TypeError(msg)
+ self._graph: dict[tuple[Any, ...], dict[str, Any]] = {}
+ self._edge_rules_keep_alive: list[Any] = []
self._edge_rules_p: Any = None
- self._heuristic: Optional[Tuple[int, int, int, int]] = None
+ self._heuristic: tuple[int, int, int, int] | None = None
@property
def ndim(self) -> int:
- """The number of dimensions."""
+ """Return the number of dimensions."""
return self._ndim
@property
- def shape(self) -> Tuple[int, ...]:
- """The shape of this graph."""
+ def shape(self) -> tuple[int, ...]:
+ """Return the shape of this graph."""
return self._shape
def add_edge(
self,
- edge_dir: Tuple[int, ...],
+ edge_dir: tuple[int, ...],
edge_cost: int = 1,
*,
cost: NDArray[Any],
- condition: Optional[ArrayLike] = None,
+ condition: ArrayLike | None = None,
) -> None:
"""Add a single edge rule.
@@ -742,20 +746,23 @@ def add_edge(
but bidirectional edges are not a requirement for the graph.
One directional edges such as pits can be added which will
only allow movement outwards from the root nodes of the pathfinder.
- """ # noqa: E501
+ """
self._edge_rules_p = None
edge_dir = tuple(edge_dir)
cost = np.asarray(cost)
if len(edge_dir) != self._ndim:
raise TypeError("edge_dir must have exactly %i items, got %r" % (self._ndim, edge_dir))
if edge_cost <= 0:
- raise ValueError("edge_cost must be greater than zero, got %r" % (edge_cost,))
+ msg = f"edge_cost must be greater than zero, got {edge_cost!r}"
+ raise ValueError(msg)
if cost.shape != self._shape:
- raise TypeError("cost array must be shape %r, got %r" % (self._shape, cost.shape))
+ msg = f"cost array must be shape {self._shape!r}, got {cost.shape!r}"
+ raise TypeError(msg)
if condition is not None:
condition = np.asarray(condition)
if condition.shape != self._shape:
- raise TypeError("condition array must be shape %r, got %r" % (self._shape, condition.shape))
+ msg = f"condition array must be shape {self._shape!r}, got {condition.shape!r}"
+ raise TypeError(msg)
if self._order == "F":
# Inputs need to be converted to C.
edge_dir = edge_dir[::-1]
@@ -772,7 +779,7 @@ def add_edge(
}
if condition is not None:
rule["condition"] = condition
- edge = edge_dir + (edge_cost,)
+ edge = (*edge_dir, edge_cost)
if edge not in rule["edge_list"]:
rule["edge_list"].append(edge)
@@ -781,7 +788,7 @@ def add_edges(
*,
edge_map: ArrayLike,
cost: NDArray[Any],
- condition: Optional[ArrayLike] = None,
+ condition: ArrayLike | None = None,
) -> None:
"""Add a rule with multiple edges.
@@ -895,7 +902,7 @@ def add_edges(
self.add_edge(edge, edge_cost, cost=cost, condition=condition)
def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w: int = 0) -> None:
- """Sets a pathfinder heuristic so that pathfinding can done with A*.
+ """Set a pathfinder heuristic so that pathfinding can done with A*.
`cardinal`, `diagonal`, `z, and `w` are the lower-bound cost of
movement in those directions. Values above the lower-bound can be
@@ -943,13 +950,15 @@ def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w:
that's because those nodes are only partially evaluated, but
pathfinding to those nodes will work correctly as long as the heuristic
isn't greedy.
- """ # noqa: E501
+ """
if 0 == cardinal == diagonal == z == w:
self._heuristic = None
if diagonal and cardinal > diagonal:
- raise ValueError("Diagonal parameter can not be lower than cardinal.")
+ msg = "Diagonal parameter can not be lower than cardinal."
+ raise ValueError(msg)
if cardinal < 0 or diagonal < 0 or z < 0 or w < 0:
- raise ValueError("Parameters can not be set to negative values..")
+ msg = "Parameters can not be set to negative values."
+ raise ValueError(msg)
self._heuristic = (cardinal, diagonal, z, w)
def _compile_rules(self) -> Any:
@@ -1022,12 +1031,14 @@ class SimpleGraph:
.. versionadded:: 11.15
"""
- def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1):
+ def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1) -> None:
cost = np.asarray(cost)
if cost.ndim != 2:
- raise TypeError("The cost array must e 2 dimensional, array of shape %r given." % (cost.shape,))
+ msg = f"The cost array must e 2 dimensional, array of shape {cost.shape!r} given."
+ raise TypeError(msg)
if greed <= 0:
- raise ValueError("Greed must be greater than zero, got %r" % (greed,))
+ msg = f"Greed must be greater than zero, got {greed}"
+ raise ValueError(msg)
edge_map = (
(diagonal, cardinal, diagonal),
(cardinal, 0, cardinal),
@@ -1046,11 +1057,11 @@ def ndim(self) -> int:
return 2
@property
- def shape(self) -> Tuple[int, int]:
+ def shape(self) -> tuple[int, int]:
return self._shape
@property
- def _heuristic(self) -> Optional[Tuple[int, int, int, int]]:
+ def _heuristic(self) -> tuple[int, int, int, int] | None:
return self._subgraph._heuristic
def set_heuristic(self, *, cardinal: int, diagonal: int) -> None:
@@ -1079,7 +1090,7 @@ class Pathfinder:
.. versionadded:: 11.13
"""
- def __init__(self, graph: Union[CustomGraph, SimpleGraph]):
+ def __init__(self, graph: CustomGraph | SimpleGraph) -> None:
self._graph = graph
self._order = graph._order
self._frontier_p = ffi.gc(lib.TCOD_frontier_new(self._graph._ndim), lib.TCOD_frontier_delete)
@@ -1087,12 +1098,12 @@ def __init__(self, graph: Union[CustomGraph, SimpleGraph]):
self._travel = _world_array(self._graph._shape_c)
self._distance_p = _export(self._distance)
self._travel_p = _export(self._travel)
- self._heuristic: Optional[Tuple[int, int, int, int, Tuple[int, ...]]] = None
+ self._heuristic: tuple[int, int, int, int, tuple[int, ...]] | None = None
self._heuristic_p: Any = ffi.NULL
@property
def distance(self) -> NDArray[Any]:
- """The distance values of the pathfinder.
+ """Distance values of the pathfinder.
The array returned from this property maintains the graphs `order`.
@@ -1112,7 +1123,7 @@ def distance(self) -> NDArray[Any]:
@property
def traversal(self) -> NDArray[Any]:
- """An array used to generate paths from any point to the nearest root.
+ """Array used to generate paths from any point to the nearest root.
The array returned from this property maintains the graphs `order`.
It has an extra dimension which includes the index of the next path.
@@ -1120,6 +1131,7 @@ def traversal(self) -> NDArray[Any]:
Example::
# This example demonstrates the purpose of the traversal array.
+ >>> import tcod.path
>>> graph = tcod.path.SimpleGraph(
... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3,
... )
@@ -1148,7 +1160,7 @@ def traversal(self) -> NDArray[Any]:
"""
if self._order == "F":
axes = range(self._travel.ndim)
- return self._travel.transpose((*axes[-2::-1], axes[-1]))[..., ::-1] # type: ignore
+ return self._travel.transpose((*axes[-2::-1], axes[-1]))[..., ::-1]
return self._travel
def clear(self) -> None:
@@ -1161,7 +1173,7 @@ def clear(self) -> None:
self._travel = _world_array(self._graph._shape_c)
lib.TCOD_frontier_clear(self._frontier_p)
- def add_root(self, index: Tuple[int, ...], value: int = 0) -> None:
+ def add_root(self, index: tuple[int, ...], value: int = 0) -> None:
"""Add a root node and insert it into the pathfinder frontier.
`index` is the root point to insert. The length of `index` must match
@@ -1179,7 +1191,7 @@ def add_root(self, index: Tuple[int, ...], value: int = 0) -> None:
self._update_heuristic(None)
lib.TCOD_frontier_push(self._frontier_p, index, value, value)
- def _update_heuristic(self, goal_ij: Optional[Tuple[int, ...]]) -> bool:
+ def _update_heuristic(self, goal_ij: tuple[int, ...] | None) -> bool:
"""Update the active heuristic. Return True if the heuristic changed."""
if goal_ij is None:
heuristic = None
@@ -1212,7 +1224,7 @@ def rebuild_frontier(self) -> None:
self._update_heuristic(None)
_check(lib.rebuild_frontier_from_distance(self._frontier_p, self._distance_p))
- def resolve(self, goal: Optional[Tuple[int, ...]] = None) -> None:
+ def resolve(self, goal: tuple[int, ...] | None = None) -> None:
"""Manually run the pathfinder algorithm.
The :any:`path_from` and :any:`path_to` methods will automatically
@@ -1229,6 +1241,7 @@ def resolve(self, goal: Optional[Tuple[int, ...]] = None) -> None:
Example::
+ >>> import tcod.path
>>> graph = tcod.path.SimpleGraph(
... cost=np.ones((4, 4), np.int8), cardinal=2, diagonal=3,
... )
@@ -1270,7 +1283,7 @@ def resolve(self, goal: Optional[Tuple[int, ...]] = None) -> None:
self._update_heuristic(goal)
self._graph._resolve(self)
- def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]:
+ def path_from(self, index: tuple[int, ...]) -> NDArray[Any]:
"""Return the shortest path from `index` to the nearest root.
The returned array is of shape `(length, ndim)` where `length` is the
@@ -1289,6 +1302,7 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]:
Example::
+ >>> import tcod.path
>>> cost = np.ones((5, 5), dtype=np.int8)
>>> cost[:, 3:] = 0
>>> graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3)
@@ -1303,7 +1317,7 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]:
>>> pf.path_from((4, 4))[1:].tolist() # Exclude the starting point so that a blocked path is an empty list.
[]
- """ # noqa: E501
+ """
index = tuple(index) # Check for bad input.
if len(index) != self._graph._ndim:
raise TypeError("Index must be %i items, got %r" % (self._distance.ndim, index))
@@ -1320,9 +1334,9 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]:
ffi.from_buffer("int*", path),
)
)
- return path[:, ::-1] if self._order == "F" else path # type: ignore
+ return path[:, ::-1] if self._order == "F" else path
- def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]:
+ def path_to(self, index: tuple[int, ...]) -> NDArray[Any]:
"""Return the shortest path from the nearest root to `index`.
See :any:`path_from`.
@@ -1333,6 +1347,7 @@ def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]:
Example::
+ >>> import tcod.path
>>> graph = tcod.path.SimpleGraph(
... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3,
... )
@@ -1346,5 +1361,5 @@ def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]:
[[1, 1], [2, 2], [3, 3]]
>>> pf.path_to((0, 0))[1:].tolist() # Exclude the starting point so that a blocked path is an empty list.
[]
- """ # noqa: E501
- return self.path_from(index)[::-1] # type: ignore
+ """
+ return self.path_from(index)[::-1]
diff --git a/tcod/random.py b/tcod/random.py
index eee4815c..12169141 100644
--- a/tcod/random.py
+++ b/tcod/random.py
@@ -11,7 +11,7 @@
import os
import random
import warnings
-from typing import Any, Hashable, Optional
+from typing import Any, Hashable
import tcod.constants
from tcod.loader import ffi, lib
@@ -21,7 +21,7 @@
MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC
-class Random(object):
+class Random:
"""The libtcod random number generator.
`algorithm` defaults to Mersenne Twister, it can be one of:
@@ -49,8 +49,8 @@ class Random(object):
def __init__(
self,
algorithm: int = MERSENNE_TWISTER,
- seed: Optional[Hashable] = None,
- ):
+ seed: Hashable | None = None,
+ ) -> None:
"""Create a new instance using this algorithm and seed."""
if seed is None:
seed = random.getrandbits(32)
@@ -75,10 +75,7 @@ def __init__(
seed = hash(seed)
self.random_c = ffi.gc(
- ffi.cast(
- "mersenne_data_t*",
- lib.TCOD_random_new_from_seed(algorithm, seed & 0xFFFFFFFF),
- ),
+ lib.TCOD_random_new_from_seed(algorithm, seed & 0xFFFFFFFF),
lib.TCOD_random_delete,
)
@@ -141,22 +138,24 @@ def __getstate__(self) -> Any:
"""Pack the self.random_c attribute into a portable state."""
state = self.__dict__.copy()
state["random_c"] = {
- "algo": self.random_c.algo,
- "distribution": self.random_c.distribution,
- "mt": list(self.random_c.mt),
- "cur_mt": self.random_c.cur_mt,
- "Q": list(self.random_c.Q),
- "c": self.random_c.c,
- "cur": self.random_c.cur,
+ "mt_cmwc": {
+ "algorithm": self.random_c.mt_cmwc.algorithm,
+ "distribution": self.random_c.mt_cmwc.distribution,
+ "mt": list(self.random_c.mt_cmwc.mt),
+ "cur_mt": self.random_c.mt_cmwc.cur_mt,
+ "Q": list(self.random_c.mt_cmwc.Q),
+ "c": self.random_c.mt_cmwc.c,
+ "cur": self.random_c.mt_cmwc.cur,
+ }
}
return state
def __setstate__(self, state: Any) -> None:
- """Create a new cdata object with the stored paramaters."""
- try:
- cdata = state["random_c"]
- except KeyError: # old/deprecated format
- cdata = state["cdata"]
- del state["cdata"]
- state["random_c"] = ffi.new("mersenne_data_t*", cdata)
+ """Create a new cdata object with the stored parameters."""
+ if "algo" in state["random_c"]:
+ # Handle old/deprecated format. Covert to libtcod's new union type.
+ state["random_c"]["algorithm"] = state["random_c"]["algo"]
+ del state["random_c"]["algo"]
+ state["random_c"] = {"mt_cmwc": state["random_c"]}
+ state["random_c"] = ffi.new("TCOD_Random*", state["random_c"])
self.__dict__.update(state)
diff --git a/tcod/render.py b/tcod/render.py
new file mode 100644
index 00000000..184d8abd
--- /dev/null
+++ b/tcod/render.py
@@ -0,0 +1,106 @@
+"""Handles the rendering of libtcod's tilesets.
+
+Using this module you can render a console to an SDL :any:`Texture` directly, letting you have full control over how
+consoles are displayed.
+This includes rendering multiple tilesets in a single frame and rendering consoles on top of each other.
+
+Example::
+
+ tileset = tcod.tileset.load_tilesheet("dejavu16x16_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD)
+ console = tcod.console.Console(20, 8)
+ console.print(0, 0, "Hello World")
+ sdl_window = tcod.sdl.video.new_window(
+ console.width * tileset.tile_width,
+ console.height * tileset.tile_height,
+ flags=tcod.lib.SDL_WINDOW_RESIZABLE,
+ )
+ sdl_renderer = tcod.sdl.render.new_renderer(sdl_window, target_textures=True)
+ atlas = tcod.render.SDLTilesetAtlas(sdl_renderer, tileset)
+ console_render = tcod.render.SDLConsoleRender(atlas)
+ while True:
+ sdl_renderer.copy(console_render.render(console))
+ sdl_renderer.present()
+ for event in tcod.event.wait():
+ if isinstance(event, tcod.event.Quit):
+ raise SystemExit()
+
+.. versionadded:: 13.4
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from typing_extensions import Final
+
+import tcod.console
+import tcod.sdl.render
+import tcod.tileset
+from tcod._internal import _check, _check_p
+from tcod.loader import ffi, lib
+
+
+class SDLTilesetAtlas:
+ """Prepares a tileset for rendering using SDL."""
+
+ def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Tileset) -> None:
+ self._renderer = renderer
+ self.tileset: Final[tcod.tileset.Tileset] = tileset
+ """The tileset used to create this SDLTilesetAtlas."""
+ self.p: Final = ffi.gc(
+ _check_p(lib.TCOD_sdl2_atlas_new(renderer.p, tileset._tileset_p)), lib.TCOD_sdl2_atlas_delete
+ )
+
+ @classmethod
+ def _from_ref(cls, renderer_p: Any, atlas_p: Any) -> SDLTilesetAtlas:
+ self = object.__new__(cls)
+ # Ignore Final reassignment type errors since this is an alternative constructor.
+ # This could be a sign that the current constructor was badly implemented.
+ self._renderer = tcod.sdl.render.Renderer(renderer_p)
+ self.tileset = tcod.tileset.Tileset._from_ref(atlas_p.tileset) # type: ignore[misc]
+ self.p = atlas_p # type: ignore[misc]
+ return self
+
+
+class SDLConsoleRender:
+ """Holds an internal cache console and texture which are used to optimized console rendering."""
+
+ def __init__(self, atlas: SDLTilesetAtlas) -> None:
+ self.atlas: Final[SDLTilesetAtlas] = atlas
+ """The SDLTilesetAtlas used to create this SDLConsoleRender.
+
+ .. versionadded:: 13.7
+ """
+ self._renderer = atlas._renderer
+ self._cache_console: tcod.console.Console | None = None
+ self._texture: tcod.sdl.render.Texture | None = None
+
+ def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture:
+ """Render a console to a cached Texture and then return the Texture.
+
+ You should not draw onto the returned Texture as only changed parts of it will be updated on the next call.
+
+ This function requires the SDL renderer to have target texture support.
+ It will also change the SDL target texture for the duration of the call.
+ """
+ if self._cache_console and (
+ self._cache_console.width != console.width or self._cache_console.height != console.height
+ ):
+ self._cache_console = None
+ self._texture = None
+ if self._cache_console is None or self._texture is None:
+ self._cache_console = tcod.console.Console(console.width, console.height)
+ self._texture = self._renderer.new_texture(
+ self.atlas.tileset.tile_width * console.width,
+ self.atlas.tileset.tile_height * console.height,
+ format=int(lib.SDL_PIXELFORMAT_RGBA32),
+ access=int(lib.SDL_TEXTUREACCESS_TARGET),
+ )
+
+ with self._renderer.set_render_target(self._texture):
+ _check(
+ lib.TCOD_sdl2_render_texture(
+ self.atlas.p, console.console_c, self._cache_console.console_c, self._texture.p
+ )
+ )
+ return self._texture
diff --git a/tcod/sdl.py b/tcod/sdl.py
deleted file mode 100644
index 8aa38ecb..00000000
--- a/tcod/sdl.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""SDL2 specific functionality.
-
-Add the line ``import tcod.sdl`` to include this module, as importing this
-module is not implied by ``import tcod``.
-"""
-from __future__ import annotations
-
-from typing import Any, Tuple
-
-import numpy as np
-from numpy.typing import ArrayLike
-
-from tcod.loader import ffi, lib
-
-__all__ = ("Window",)
-
-
-class _TempSurface:
- """Holds a temporary surface derived from a NumPy array."""
-
- def __init__(self, pixels: ArrayLike) -> None:
- self._array = np.ascontiguousarray(pixels, dtype=np.uint8)
- if len(self._array) != 3:
- raise TypeError("NumPy shape must be 3D [y, x, ch] (got %r)" % (self._array.shape,))
- if 3 <= self._array.shape[2] <= 4:
- raise TypeError("NumPy array must have RGB or RGBA channels. (got %r)" % (self._array.shape,))
- self.p = ffi.gc(
- lib.SDL_CreateRGBSurfaceFrom(
- ffi.from_buffer("void*", self._array),
- self._array.shape[1], # Width.
- self._array.shape[0], # Height.
- self._array.shape[2] * 8, # Bit depth.
- self._array.strides[1], # Pitch.
- 0x000000FF,
- 0x0000FF00,
- 0x00FF0000,
- 0xFF000000 if self._array.shape[2] == 4 else 0,
- ),
- lib.SDL_FreeSurface,
- )
-
-
-class Window:
- """An SDL2 Window object."""
-
- def __init__(self, sdl_window_p: Any) -> None:
- if ffi.typeof(sdl_window_p) is not ffi.typeof("struct SDL_Window*"):
- raise TypeError(
- "sdl_window_p must be %r type (was %r)." % (ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p))
- )
- if not sdl_window_p:
- raise ValueError("sdl_window_p can not be a null pointer.")
- self.p = sdl_window_p
-
- def __eq__(self, other: Any) -> bool:
- return bool(self.p == other.p)
-
- def set_icon(self, image: ArrayLike) -> None:
- """Set the window icon from an image.
-
- `image` is a C memory order RGB or RGBA NumPy array.
- """
- surface = _TempSurface(image)
- lib.SDL_SetWindowIcon(self.p, surface.p)
-
- @property
- def allow_screen_saver(self) -> bool:
- """If True the operating system is allowed to display a screen saver.
-
- You can set this attribute to enable or disable the screen saver.
- """
- return bool(lib.SDL_IsScreenSaverEnabled(self.p))
-
- @allow_screen_saver.setter
- def allow_screen_saver(self, value: bool) -> None:
- if value:
- lib.SDL_EnableScreenSaver(self.p)
- else:
- lib.SDL_DisableScreenSaver(self.p)
-
- @property
- def position(self) -> Tuple[int, int]:
- """Return the (x, y) position of the window.
-
- This attribute can be set the move the window.
- The constants tcod.lib.SDL_WINDOWPOS_CENTERED or
- tcod.lib.SDL_WINDOWPOS_UNDEFINED can be used.
- """
- xy = ffi.new("int[2]")
- lib.SDL_GetWindowPosition(self.p, xy, xy + 1)
- return xy[0], xy[1]
-
- @position.setter
- def position(self, xy: Tuple[int, int]) -> None:
- x, y = xy
- lib.SDL_SetWindowPosition(self.p, x, y)
-
- @property
- def size(self) -> Tuple[int, int]:
- """Return the pixel (width, height) of the window.
-
- This attribute can be set to change the size of the window but the
- given size must be greater than (1, 1) or else an exception will be
- raised.
- """
- xy = ffi.new("int[2]")
- lib.SDL_GetWindowSize(self.p, xy, xy + 1)
- return xy[0], xy[1]
-
- @size.setter
- def size(self, xy: Tuple[int, int]) -> None:
- if any(i <= 0 for i in xy):
- raise ValueError("Window size must be greater than zero, not %r" % (xy,))
- x, y = xy
- lib.SDL_SetWindowSize(self.p, x, y)
-
-
-def get_active_window() -> Window:
- """Return the SDL2 window current managed by libtcod.
-
- Will raise an error if libtcod does not currently have a window.
- """
- sdl_window = lib.TCOD_sys_get_window()
- if not sdl_window:
- raise RuntimeError("TCOD does not have an active window.")
- return Window(sdl_window)
diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py
new file mode 100644
index 00000000..8133948b
--- /dev/null
+++ b/tcod/sdl/__init__.py
@@ -0,0 +1,4 @@
+"""tcod.sdl package."""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py
new file mode 100644
index 00000000..79d59339
--- /dev/null
+++ b/tcod/sdl/_internal.py
@@ -0,0 +1,126 @@
+"""tcod.sdl private functions."""
+from __future__ import annotations
+
+import logging
+import sys as _sys
+from dataclasses import dataclass
+from types import TracebackType
+from typing import Any, Callable, TypeVar
+
+from tcod.loader import ffi, lib
+
+T = TypeVar("T")
+
+logger = logging.getLogger("tcod.sdl")
+
+_LOG_PRIORITY = {
+ 1: logging.DEBUG, # SDL_LOG_PRIORITY_VERBOSE
+ 2: logging.DEBUG, # SDL_LOG_PRIORITY_DEBUG
+ 3: logging.INFO, # SDL_LOG_PRIORITY_INFO
+ 4: logging.WARNING, # SDL_LOG_PRIORITY_WARN
+ 5: logging.ERROR, # SDL_LOG_PRIORITY_ERROR
+ 6: logging.CRITICAL, # SDL_LOG_PRIORITY_CRITICAL
+}
+
+_LOG_CATEGORY = {
+ int(lib.SDL_LOG_CATEGORY_APPLICATION): "APPLICATION",
+ int(lib.SDL_LOG_CATEGORY_ERROR): "ERROR",
+ int(lib.SDL_LOG_CATEGORY_ASSERT): "ASSERT",
+ int(lib.SDL_LOG_CATEGORY_SYSTEM): "SYSTEM",
+ int(lib.SDL_LOG_CATEGORY_AUDIO): "AUDIO",
+ int(lib.SDL_LOG_CATEGORY_VIDEO): "VIDEO",
+ int(lib.SDL_LOG_CATEGORY_RENDER): "RENDER",
+ int(lib.SDL_LOG_CATEGORY_INPUT): "INPUT",
+ int(lib.SDL_LOG_CATEGORY_TEST): "TEST",
+ int(lib.SDL_LOG_CATEGORY_CUSTOM): "",
+}
+
+
+@dataclass
+class _UnraisableHookArgs:
+ exc_type: type[BaseException]
+ exc_value: BaseException | None
+ exc_traceback: TracebackType | None
+ err_msg: str | None
+ object: object
+
+
+class _ProtectedContext:
+ def __init__(self, obj: object = None) -> None:
+ self.obj = obj
+
+ def __enter__(self) -> None:
+ pass
+
+ def __exit__(
+ self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
+ ) -> bool:
+ if exc_type is None:
+ return False
+ if _sys.version_info < (3, 8):
+ return False
+ _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type]
+ return True
+
+
+@ffi.def_extern() # type: ignore
+def _sdl_log_output_function(_userdata: None, category: int, priority: int, message_p: Any) -> None: # noqa: ANN401
+ """Pass logs sent by SDL to Python's logging system."""
+ message = str(ffi.string(message_p), encoding="utf-8")
+ logger.log(_LOG_PRIORITY.get(priority, 0), "%s:%s", _LOG_CATEGORY.get(category, ""), message)
+
+
+def _get_error() -> str:
+ """Return a message from SDL_GetError as a Unicode string."""
+ return str(ffi.string(lib.SDL_GetError()), encoding="utf-8")
+
+
+def _check(result: int) -> int:
+ """Check if an SDL function returned without errors, and raise an exception if it did."""
+ if result < 0:
+ raise RuntimeError(_get_error())
+ return result
+
+
+def _check_p(result: Any) -> Any:
+ """Check if an SDL function returned NULL, and raise an exception if it did."""
+ if not result:
+ raise RuntimeError(_get_error())
+ return result
+
+
+if lib._sdl_log_output_function:
+ lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL)
+ if __debug__:
+ lib.SDL_LogSetAllPriority(lib.SDL_LOG_PRIORITY_VERBOSE)
+
+
+def _compiled_version() -> tuple[int, int, int]:
+ return int(lib.SDL_MAJOR_VERSION), int(lib.SDL_MINOR_VERSION), int(lib.SDL_PATCHLEVEL)
+
+
+def _linked_version() -> tuple[int, int, int]:
+ sdl_version = ffi.new("SDL_version*")
+ lib.SDL_GetVersion(sdl_version)
+ return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch)
+
+
+def _version_at_least(required: tuple[int, int, int]) -> None:
+ """Raise an error if the compiled version is less than required. Used to guard recently defined SDL functions."""
+ if required <= _compiled_version():
+ return
+ msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}"
+ raise RuntimeError(msg)
+
+
+def _required_version(required: tuple[int, int, int]) -> Callable[[T], T]:
+ if not lib: # Read the docs mock object.
+ return lambda x: x
+ if required <= _compiled_version():
+ return lambda x: x
+
+ def replacement(*_args: Any, **_kwargs: Any) -> Any:
+ msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}"
+ raise RuntimeError(msg)
+
+ return lambda x: replacement # type: ignore[return-value]
diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py
new file mode 100644
index 00000000..f8261097
--- /dev/null
+++ b/tcod/sdl/audio.py
@@ -0,0 +1,689 @@
+"""SDL2 audio playback and recording tools.
+
+This module includes SDL's low-level audio API and a naive implementation of an SDL mixer.
+If you have experience with audio mixing then you might be better off writing your own mixer or
+modifying the existing one which was written using Python/Numpy.
+
+This module is designed to integrate with the wider Python ecosystem.
+It leaves the loading to sound samples to other libraries like
+`SoundFile `_.
+
+Example::
+
+ # Synchronous audio example using SDL's low-level API.
+ import time
+
+ import soundfile # pip install soundfile
+ import tcod.sdl.audio
+
+ device = tcod.sdl.audio.open() # Open the default output device.
+ sound, sample_rate = soundfile.read("example_sound.wav", dtype="float32") # Load an audio sample using SoundFile.
+ converted = device.convert(sound, sample_rate) # Convert this sample to the format expected by the device.
+ device.queue_audio(converted) # Play audio synchronously by appending it to the device buffer.
+
+ while device.queued_samples: # Wait until device is done playing.
+ time.sleep(0.001)
+
+Example::
+
+ # Asynchronous audio example using BasicMixer.
+ import time
+
+ import soundfile # pip install soundfile
+ import tcod.sdl.audio
+
+ mixer = tcod.sdl.audio.BasicMixer(tcod.sdl.audio.open()) # Setup BasicMixer with the default audio output.
+ sound, sample_rate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile.
+ sound = mixer.device.convert(sound, sample_rate) # Convert this sample to the format expected by the device.
+ channel = mixer.play(sound) # Start asynchronous playback, audio is mixed on a separate Python thread.
+ while channel.busy: # Wait until the sample is done playing.
+ time.sleep(0.001)
+
+.. versionadded:: 13.5
+"""
+from __future__ import annotations
+
+import enum
+import sys
+import threading
+import time
+from types import TracebackType
+from typing import Any, Callable, Hashable, Iterator
+
+import numpy as np
+from numpy.typing import ArrayLike, DTypeLike, NDArray
+from typing_extensions import Final, Literal, Self
+
+import tcod.sdl.sys
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _get_error, _ProtectedContext
+
+
+def _get_format(format: DTypeLike) -> int:
+ """Return a SDL_AudioFormat bit-field from a NumPy dtype."""
+ dt: Any = np.dtype(format)
+ assert dt.fields is None
+ bitsize = dt.itemsize * 8
+ assert 0 < bitsize <= lib.SDL_AUDIO_MASK_BITSIZE
+ if not dt.str[1] in "uif":
+ msg = f"Unexpected dtype: {dt}"
+ raise TypeError(msg)
+ is_signed = dt.str[1] != "u"
+ is_float = dt.str[1] == "f"
+ byteorder = dt.byteorder
+ if byteorder == "=":
+ byteorder = "<" if sys.byteorder == "little" else ">"
+
+ return int(
+ bitsize
+ | (lib.SDL_AUDIO_MASK_DATATYPE * is_float)
+ | (lib.SDL_AUDIO_MASK_ENDIAN * (byteorder == ">"))
+ | (lib.SDL_AUDIO_MASK_SIGNED * is_signed)
+ )
+
+
+def _dtype_from_format(format: int) -> np.dtype[Any]:
+ """Return a dtype from a SDL_AudioFormat.
+
+ >>> _dtype_from_format(tcod.lib.AUDIO_F32LSB)
+ dtype('float32')
+ >>> _dtype_from_format(tcod.lib.AUDIO_F32MSB)
+ dtype('>f4')
+ >>> _dtype_from_format(tcod.lib.AUDIO_S16LSB)
+ dtype('int16')
+ >>> _dtype_from_format(tcod.lib.AUDIO_S16MSB)
+ dtype('>i2')
+ >>> _dtype_from_format(tcod.lib.AUDIO_U16LSB)
+ dtype('uint16')
+ >>> _dtype_from_format(tcod.lib.AUDIO_U16MSB)
+ dtype('>u2')
+ """
+ bitsize = format & lib.SDL_AUDIO_MASK_BITSIZE
+ assert bitsize % 8 == 0
+ byte_size = bitsize // 8
+ byteorder = ">" if format & lib.SDL_AUDIO_MASK_ENDIAN else "<"
+ if format & lib.SDL_AUDIO_MASK_DATATYPE:
+ kind = "f"
+ elif format & lib.SDL_AUDIO_MASK_SIGNED:
+ kind = "i"
+ else:
+ kind = "u"
+ return np.dtype(f"{byteorder}{kind}{byte_size}")
+
+
+def convert_audio(
+ in_sound: ArrayLike, in_rate: int, *, out_rate: int, out_format: DTypeLike, out_channels: int
+) -> NDArray[Any]:
+ """Convert an audio sample into a format supported by this device.
+
+ Returns the converted array. This might be a reference to the input array if no conversion was needed.
+
+ Args:
+ in_sound: The input ArrayLike sound sample. Input format and channels are derived from the array.
+ in_rate: The sample-rate of the input array.
+ out_rate: The sample-rate of the output array.
+ out_format: The output format of the converted array.
+ out_channels: The number of audio channels of the output array.
+
+ .. versionadded:: 13.6
+
+ .. versionchanged:: 16.0
+ Now converts floating types to `np.float32` when SDL doesn't support the specific format.
+
+ .. seealso::
+ :any:`AudioDevice.convert`
+ """
+ in_array: NDArray[Any] = np.asarray(in_sound)
+ if len(in_array.shape) == 1:
+ in_array = in_array[:, np.newaxis]
+ if len(in_array.shape) != 2:
+ msg = f"Expected a 1 or 2 ndim input, got {in_array.shape} instead."
+ raise TypeError(msg)
+ cvt = ffi.new("SDL_AudioCVT*")
+ in_channels = in_array.shape[1]
+ in_format = _get_format(in_array.dtype)
+ out_sdl_format = _get_format(out_format)
+ try:
+ if (
+ _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate))
+ == 0
+ ):
+ return in_array # No conversion needed.
+ except RuntimeError as exc:
+ if ( # SDL now only supports float32, but later versions may add more support for more formats.
+ exc.args[0] == "Invalid source format"
+ and np.issubdtype(in_array.dtype, np.floating)
+ and in_array.dtype != np.float32
+ ):
+ return convert_audio( # Try again with float32
+ in_array.astype(np.float32),
+ in_rate,
+ out_rate=out_rate,
+ out_format=out_format,
+ out_channels=out_channels,
+ )
+ raise
+ # Upload to the SDL_AudioCVT buffer.
+ cvt.len = in_array.itemsize * in_array.size
+ out_buffer = cvt.buf = ffi.new("uint8_t[]", cvt.len * cvt.len_mult)
+ np.frombuffer(ffi.buffer(out_buffer[0 : cvt.len]), dtype=in_array.dtype).reshape(in_array.shape)[:] = in_array
+
+ _check(lib.SDL_ConvertAudio(cvt))
+ out_array: NDArray[Any] = (
+ np.frombuffer(ffi.buffer(out_buffer[0 : cvt.len_cvt]), dtype=out_format).reshape(-1, out_channels).copy()
+ )
+ return out_array
+
+
+class AudioDevice:
+ """An SDL audio device.
+
+ Open new audio devices using :any:`tcod.sdl.audio.open`.
+
+ When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously.
+ For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`.
+
+ .. versionchanged:: 16.0
+ Can now be used as a context which will close the device on exit.
+ """
+
+ def __init__(
+ self,
+ device_id: int,
+ capture: bool,
+ spec: Any, # SDL_AudioSpec*
+ ) -> None:
+ assert device_id >= 0
+ assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*")
+ assert spec
+ self.device_id: Final[int] = device_id
+ """The SDL device identifier used for SDL C functions."""
+ self.spec: Final[Any] = spec
+ """The SDL_AudioSpec as a CFFI object."""
+ self.frequency: Final[int] = spec.freq
+ """The audio device sound frequency."""
+ self.is_capture: Final[bool] = capture
+ """True if this is a recording device instead of an output device."""
+ self.format: Final[np.dtype[Any]] = _dtype_from_format(spec.format)
+ """The format used for audio samples with this device."""
+ self.channels: Final[int] = int(spec.channels)
+ """The number of audio channels for this device."""
+ self.silence: float = int(spec.silence)
+ """The value of silence, according to SDL."""
+ self.buffer_samples: Final[int] = int(spec.samples)
+ """The size of the audio buffer in samples."""
+ self.buffer_bytes: Final[int] = int(spec.size)
+ """The size of the audio buffer in bytes."""
+ self._handle: Any | None = None
+ self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback
+
+ def __repr__(self) -> str:
+ """Return a representation of this device."""
+ if self.stopped:
+ return f"<{self.__class__.__name__}() stopped=True>"
+ items = [
+ f"{self.__class__.__name__}(device_id={self.device_id})",
+ f"frequency={self.frequency}",
+ f"is_capture={self.is_capture}",
+ f"format={self.format}",
+ f"channels={self.channels}",
+ f"buffer_samples={self.buffer_samples}",
+ f"buffer_bytes={self.buffer_bytes}",
+ f"paused={self.paused}",
+ ]
+
+ if self.silence:
+ items.append(f"silence={self.silence}")
+ if self._handle is not None:
+ items.append(f"callback={self._callback}")
+ return f"""<{" ".join(items)}>"""
+
+ @property
+ def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]:
+ """If the device was opened with a callback enabled, then you may get or set the callback with this attribute."""
+ if self._handle is None:
+ msg = "This AudioDevice was opened without a callback."
+ raise TypeError(msg)
+ return self._callback
+
+ @callback.setter
+ def callback(self, new_callback: Callable[[AudioDevice, NDArray[Any]], None]) -> None:
+ if self._handle is None:
+ msg = "This AudioDevice was opened without a callback."
+ raise TypeError(msg)
+ self._callback = new_callback
+
+ @property
+ def _sample_size(self) -> int:
+ """The size of a sample in bytes."""
+ return self.format.itemsize * self.channels
+
+ @property
+ def stopped(self) -> bool:
+ """Is True if the device has failed or was closed."""
+ if not hasattr(self, "device_id"):
+ return True
+ return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) == lib.SDL_AUDIO_STOPPED)
+
+ @property
+ def paused(self) -> bool:
+ """Get or set the device paused state."""
+ return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_PLAYING)
+
+ @paused.setter
+ def paused(self, value: bool) -> None:
+ lib.SDL_PauseAudioDevice(self.device_id, value)
+
+ def _verify_array_format(self, samples: NDArray[Any]) -> NDArray[Any]:
+ if samples.dtype != self.format:
+ msg = f"Expected an array of dtype {self.format}, got {samples.dtype} instead."
+ raise TypeError(msg)
+ return samples
+
+ def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]:
+ if isinstance(samples_, np.ndarray):
+ samples_ = self._verify_array_format(samples_)
+ samples: NDArray[Any] = np.asarray(samples_, dtype=self.format)
+ if len(samples.shape) < 2:
+ samples = samples[:, np.newaxis]
+ return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format)
+
+ def convert(self, sound: ArrayLike, rate: int | None = None) -> NDArray[Any]:
+ """Convert an audio sample into a format supported by this device.
+
+ Returns the converted array. This might be a reference to the input array if no conversion was needed.
+
+ Args:
+ sound: An ArrayLike sound sample.
+ rate: The sample-rate of the input array.
+ If None is given then it's assumed to be the same as the device.
+
+ .. versionadded:: 13.6
+
+ .. seealso::
+ :any:`convert_audio`
+ """
+ in_array: NDArray[Any] = np.asarray(sound)
+ if len(in_array.shape) == 1:
+ in_array = in_array[:, np.newaxis]
+ return convert_audio(
+ in_sound=sound,
+ in_rate=rate if rate is not None else self.frequency,
+ out_channels=self.channels if in_array.shape[1] > 1 else 1,
+ out_format=self.format,
+ out_rate=self.frequency,
+ )
+
+ @property
+ def _queued_bytes(self) -> int:
+ """The current amount of bytes remaining in the audio queue."""
+ return int(lib.SDL_GetQueuedAudioSize(self.device_id))
+
+ @property
+ def queued_samples(self) -> int:
+ """The current amount of samples remaining in the audio queue."""
+ return self._queued_bytes // self._sample_size
+
+ def queue_audio(self, samples: ArrayLike) -> None:
+ """Append audio samples to the audio data queue."""
+ assert not self.is_capture
+ samples = self._convert_array(samples)
+ buffer = ffi.from_buffer(samples)
+ lib.SDL_QueueAudio(self.device_id, buffer, len(buffer))
+
+ def dequeue_audio(self) -> NDArray[Any]:
+ """Return the audio buffer from a capture stream."""
+ assert self.is_capture
+ out_samples = self._queued_bytes // self._sample_size
+ out = np.empty((out_samples, self.channels), self.format)
+ buffer = ffi.from_buffer(out)
+ bytes_returned = lib.SDL_DequeueAudio(self.device_id, buffer, len(buffer))
+ samples_returned = bytes_returned // self._sample_size
+ assert samples_returned == out_samples
+ return out
+
+ def __del__(self) -> None:
+ self.close()
+
+ def close(self) -> None:
+ """Close this audio device. Using this object after it has been closed is invalid."""
+ if not hasattr(self, "device_id"):
+ return
+ lib.SDL_CloseAudioDevice(self.device_id)
+ del self.device_id
+
+ def __enter__(self) -> Self:
+ """Return self and enter a managed context."""
+ return self
+
+ def __exit__(
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
+ ) -> None:
+ """Close the device when exiting the context."""
+ self.close()
+
+ @staticmethod
+ def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None:
+ stream[...] = device.silence
+
+
+class _LoopSoundFunc:
+ def __init__(self, sound: NDArray[Any], loops: int, on_end: Callable[[Channel], None] | None) -> None:
+ self.sound = sound
+ self.loops = loops
+ self.on_end = on_end
+
+ def __call__(self, channel: Channel) -> None:
+ if not self.loops:
+ if self.on_end is not None:
+ self.on_end(channel)
+ return
+ channel.play(self.sound, volume=channel.volume, on_end=self)
+ if self.loops > 0:
+ self.loops -= 1
+
+
+class Channel:
+ """An audio channel for :any:`BasicMixer`. Use :any:`BasicMixer.get_channel` to initialize this object.
+
+ .. versionadded:: 13.6
+ """
+
+ mixer: BasicMixer
+ """The :any:`BasicMixer` is channel belongs to."""
+
+ def __init__(self) -> None:
+ self._lock = threading.RLock()
+ self.volume: float | tuple[float, ...] = 1.0
+ self.sound_queue: list[NDArray[Any]] = []
+ self.on_end_callback: Callable[[Channel], None] | None = None
+
+ @property
+ def busy(self) -> bool:
+ """Is True when this channel is playing audio."""
+ return bool(self.sound_queue)
+
+ def play(
+ self,
+ sound: ArrayLike,
+ *,
+ volume: float | tuple[float, ...] = 1.0,
+ loops: int = 0,
+ on_end: Callable[[Channel], None] | None = None,
+ ) -> None:
+ """Play an audio sample, stopping any audio currently playing on this channel.
+
+ Parameters are the same as :any:`BasicMixer.play`.
+ """
+ sound = self._verify_audio_sample(sound)
+ with self._lock:
+ self.volume = volume
+ self.sound_queue[:] = [sound]
+ self.on_end_callback = on_end
+ if loops:
+ self.on_end_callback = _LoopSoundFunc(sound, loops, on_end)
+
+ def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]:
+ """Verify an audio sample is valid and return it as a Numpy array."""
+ array: NDArray[Any] = np.asarray(sample)
+ if array.dtype != self.mixer.device.format:
+ msg = f"Audio sample must be dtype={self.mixer.device.format}, input was dtype={array.dtype}"
+ raise TypeError(msg)
+ if len(array.shape) == 1:
+ array = array[:, np.newaxis]
+ return array
+
+ def _on_mix(self, stream: NDArray[Any]) -> None:
+ """Mix the next part of this channels audio into an active audio stream."""
+ with self._lock:
+ while self.sound_queue and stream.size:
+ buffer = self.sound_queue[0]
+ if buffer.shape[0] > stream.shape[0]:
+ # Mix part of the buffer into the stream.
+ stream[:] += buffer[: stream.shape[0]] * self.volume
+ self.sound_queue[0] = buffer[stream.shape[0] :]
+ break # Stream was filled.
+ # Remaining buffer fits the stream array.
+ stream[: buffer.shape[0]] += buffer * self.volume
+ stream = stream[buffer.shape[0] :]
+ self.sound_queue.pop(0)
+ if not self.sound_queue and self.on_end_callback is not None:
+ self.on_end_callback(self)
+
+ def fadeout(self, time: float) -> None:
+ """Fadeout this channel then stop playing."""
+ with self._lock:
+ if not self.sound_queue:
+ return
+ time_samples = round(time * self.mixer.device.frequency) + 1
+ buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32)
+ self._on_mix(buffer)
+ buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:, np.newaxis]
+ self.sound_queue[:] = [buffer]
+
+ def stop(self) -> None:
+ """Stop audio on this channel."""
+ self.fadeout(0.0005)
+
+
+class BasicMixer(threading.Thread):
+ """An SDL sound mixer implemented in Python and Numpy.
+
+ .. versionadded:: 13.6
+ """
+
+ def __init__(self, device: AudioDevice) -> None:
+ self.channels: dict[Hashable, Channel] = {}
+ assert device.format == np.float32
+ super().__init__(daemon=True)
+ self.device = device
+ """The :any:`AudioDevice`"""
+ self._lock = threading.RLock()
+ self._running = True
+ self.start()
+
+ def run(self) -> None:
+ buffer = np.full(
+ (self.device.buffer_samples, self.device.channels), self.device.silence, dtype=self.device.format
+ )
+ while self._running:
+ if self.device._queued_bytes > 0:
+ time.sleep(0.001)
+ continue
+ self._on_stream(buffer)
+ self.device.queue_audio(buffer)
+ buffer[:] = self.device.silence
+
+ def close(self) -> None:
+ """Shutdown this mixer, all playing audio will be abruptly stopped."""
+ self._running = False
+
+ def get_channel(self, key: Hashable) -> Channel:
+ """Return a channel tied to with the given key.
+
+ Channels are initialized as you access them with this function.
+ :any:`int` channels starting from zero are used internally.
+
+ This can be used to generate a ``"music"`` channel for example.
+ """
+ with self._lock:
+ if key not in self.channels:
+ self.channels[key] = Channel()
+ self.channels[key].mixer = self
+ return self.channels[key]
+
+ def _get_next_channel(self) -> Channel:
+ """Return the next available channel for the play method."""
+ with self._lock:
+ i = 0
+ while True:
+ if not self.get_channel(i).busy:
+ return self.channels[i]
+ i += 1
+
+ def play(
+ self,
+ sound: ArrayLike,
+ *,
+ volume: float | tuple[float, ...] = 1.0,
+ loops: int = 0,
+ on_end: Callable[[Channel], None] | None = None,
+ ) -> Channel:
+ """Play a sound, return the channel the sound is playing on.
+
+ Args:
+ sound: The sound to play. This a Numpy array matching the format of the loaded audio device.
+ volume: The volume to play the sound at.
+ You can also pass a tuple of floats to set the volume for each channel/speaker.
+ loops: How many times to play the sound, `-1` can be used to loop the sound forever.
+ on_end: A function to call when this sound has ended.
+ This is called with the :any:`Channel` which was playing the sound.
+ """
+ channel = self._get_next_channel()
+ channel.play(sound, volume=volume, loops=loops, on_end=on_end)
+ return channel
+
+ def stop(self) -> None:
+ """Stop playback on all channels from this mixer."""
+ with self._lock:
+ for channel in self.channels.values():
+ channel.stop()
+
+ def _on_stream(self, stream: NDArray[Any]) -> None:
+ """Called to fill the audio buffer."""
+ with self._lock:
+ for channel in list(self.channels.values()):
+ channel._on_mix(stream)
+
+
+class _AudioCallbackUserdata:
+ device: AudioDevice
+
+
+@ffi.def_extern() # type: ignore
+def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: # noqa: ANN401
+ """Handle audio device callbacks."""
+ data: _AudioCallbackUserdata = ffi.from_handle(userdata)
+ device = data.device
+ buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels)
+ with _ProtectedContext(device):
+ device._callback(device, buffer)
+
+
+def _get_devices(capture: bool) -> Iterator[str]:
+ """Get audio devices from SDL_GetAudioDeviceName."""
+ with tcod.sdl.sys._ScopeInit(tcod.sdl.sys.Subsystem.AUDIO):
+ device_count = lib.SDL_GetNumAudioDevices(capture)
+ for i in range(device_count):
+ yield str(ffi.string(lib.SDL_GetAudioDeviceName(i, capture)), encoding="utf-8")
+
+
+def get_devices() -> Iterator[str]:
+ """Iterate over the available audio output devices."""
+ yield from _get_devices(capture=False)
+
+
+def get_capture_devices() -> Iterator[str]:
+ """Iterate over the available audio capture devices."""
+ yield from _get_devices(capture=True)
+
+
+class AllowedChanges(enum.IntFlag):
+ """Which parameters are allowed to be changed when the values given are not supported."""
+
+ NONE = 0
+ """"""
+ FREQUENCY = 0x01
+ """"""
+ FORMAT = 0x02
+ """"""
+ CHANNELS = 0x04
+ """"""
+ SAMPLES = 0x08
+ """"""
+ ANY = FREQUENCY | FORMAT | CHANNELS | SAMPLES
+ """"""
+
+
+def open(
+ name: str | None = None,
+ capture: bool = False,
+ *,
+ frequency: int = 44100,
+ format: DTypeLike = np.float32,
+ channels: int = 2,
+ samples: int = 0,
+ allowed_changes: AllowedChanges = AllowedChanges.NONE,
+ paused: bool = False,
+ callback: None | Literal[True] | Callable[[AudioDevice, NDArray[Any]], None] = None,
+) -> AudioDevice:
+ """Open an audio device for playback or capture and return it.
+
+ Args:
+ name: The name of the device to open, or None for the most reasonable default.
+ capture: True if this is a recording device, or False if this is an output device.
+ frequency: The desired sample rate to open the device with.
+ format: The data format to use for samples as a NumPy dtype.
+ channels: The number of speakers for the device. 1, 2, 4, or 6 are typical options.
+ samples: The desired size of the audio buffer, must be a power of two.
+ allowed_changes:
+ By default if the hardware does not support the desired format than SDL will transparently convert between
+ formats for you.
+ Otherwise you can specify which parameters are allowed to be changed to fit the hardware better.
+ paused:
+ If True then the device will begin in a paused state.
+ It can then be unpaused by assigning False to :any:`AudioDevice.paused`.
+ callback:
+ If None then this device will be opened in push mode and you'll have to use :any:`AudioDevice.queue_audio`
+ to send audio data or :any:`AudioDevice.dequeue_audio` to receive it.
+ If a callback is given then you can change it later, but you can not enable or disable the callback on an
+ opened device.
+ If True then a default callback which plays silence will be used, this is useful if you need the audio
+ device before your callback is ready.
+
+ If a callback is given then it will be called with the `AudioDevice` and a Numpy buffer of the data stream.
+ This callback will be run on a separate thread.
+ Exceptions not handled by the callback become unraiseable and will be handled by :any:`sys.unraisablehook`.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_AudioSpec
+ https://wiki.libsdl.org/SDL_OpenAudioDevice
+
+ """
+ tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
+ desired = ffi.new(
+ "SDL_AudioSpec*",
+ {
+ "freq": frequency,
+ "format": _get_format(format),
+ "channels": channels,
+ "samples": samples,
+ "callback": ffi.NULL,
+ "userdata": ffi.NULL,
+ },
+ )
+ callback_data = _AudioCallbackUserdata()
+ if callback is not None:
+ handle = ffi.new_handle(callback_data)
+ desired.callback = lib._sdl_audio_callback
+ desired.userdata = handle
+ else:
+ handle = None
+
+ obtained = ffi.new("SDL_AudioSpec*")
+ device_id: int = lib.SDL_OpenAudioDevice(
+ ffi.NULL if name is None else name.encode("utf-8"),
+ capture,
+ desired,
+ obtained,
+ allowed_changes,
+ )
+ assert device_id >= 0, _get_error()
+ device = AudioDevice(device_id, capture, obtained)
+ if callback is not None:
+ callback_data.device = device
+ device._handle = handle
+ if callback is not True:
+ device._callback = callback
+ device.paused = paused
+ return device
diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py
new file mode 100644
index 00000000..2a00d0cc
--- /dev/null
+++ b/tcod/sdl/joystick.py
@@ -0,0 +1,414 @@
+"""SDL Joystick Support.
+
+.. versionadded:: 13.8
+"""
+from __future__ import annotations
+
+import enum
+from typing import Any, ClassVar
+from weakref import WeakValueDictionary
+
+from typing_extensions import Final, Literal
+
+import tcod.sdl.sys
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _check_p
+
+_HAT_DIRECTIONS: dict[int, tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = {
+ lib.SDL_HAT_CENTERED or 0: (0, 0),
+ lib.SDL_HAT_UP or 0: (0, -1),
+ lib.SDL_HAT_RIGHT or 0: (1, 0),
+ lib.SDL_HAT_DOWN or 0: (0, 1),
+ lib.SDL_HAT_LEFT or 0: (-1, 0),
+ lib.SDL_HAT_RIGHTUP or 0: (1, -1),
+ lib.SDL_HAT_RIGHTDOWN or 0: (1, 1),
+ lib.SDL_HAT_LEFTUP or 0: (-1, -1),
+ lib.SDL_HAT_LEFTDOWN or 0: (-1, 1),
+}
+
+
+class ControllerAxis(enum.IntEnum):
+ """The standard axes for a game controller."""
+
+ INVALID = lib.SDL_CONTROLLER_AXIS_INVALID or -1
+ LEFTX = lib.SDL_CONTROLLER_AXIS_LEFTX or 0
+ """"""
+ LEFTY = lib.SDL_CONTROLLER_AXIS_LEFTY or 1
+ """"""
+ RIGHTX = lib.SDL_CONTROLLER_AXIS_RIGHTX or 2
+ """"""
+ RIGHTY = lib.SDL_CONTROLLER_AXIS_RIGHTY or 3
+ """"""
+ TRIGGERLEFT = lib.SDL_CONTROLLER_AXIS_TRIGGERLEFT or 4
+ """"""
+ TRIGGERRIGHT = lib.SDL_CONTROLLER_AXIS_TRIGGERRIGHT or 5
+ """"""
+
+
+class ControllerButton(enum.IntEnum):
+ """The standard buttons for a game controller."""
+
+ INVALID = lib.SDL_CONTROLLER_BUTTON_INVALID or -1
+ A = lib.SDL_CONTROLLER_BUTTON_A or 0
+ """"""
+ B = lib.SDL_CONTROLLER_BUTTON_B or 1
+ """"""
+ X = lib.SDL_CONTROLLER_BUTTON_X or 2
+ """"""
+ Y = lib.SDL_CONTROLLER_BUTTON_Y or 3
+ """"""
+ BACK = lib.SDL_CONTROLLER_BUTTON_BACK or 4
+ """"""
+ GUIDE = lib.SDL_CONTROLLER_BUTTON_GUIDE or 5
+ """"""
+ START = lib.SDL_CONTROLLER_BUTTON_START or 6
+ """"""
+ LEFTSTICK = lib.SDL_CONTROLLER_BUTTON_LEFTSTICK or 7
+ """"""
+ RIGHTSTICK = lib.SDL_CONTROLLER_BUTTON_RIGHTSTICK or 8
+ """"""
+ LEFTSHOULDER = lib.SDL_CONTROLLER_BUTTON_LEFTSHOULDER or 9
+ """"""
+ RIGHTSHOULDER = lib.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER or 10
+ """"""
+ DPAD_UP = lib.SDL_CONTROLLER_BUTTON_DPAD_UP or 11
+ """"""
+ DPAD_DOWN = lib.SDL_CONTROLLER_BUTTON_DPAD_DOWN or 12
+ """"""
+ DPAD_LEFT = lib.SDL_CONTROLLER_BUTTON_DPAD_LEFT or 13
+ """"""
+ DPAD_RIGHT = lib.SDL_CONTROLLER_BUTTON_DPAD_RIGHT or 14
+ """"""
+ MISC1 = 15
+ """"""
+ PADDLE1 = 16
+ """"""
+ PADDLE2 = 17
+ """"""
+ PADDLE3 = 18
+ """"""
+ PADDLE4 = 19
+ """"""
+ TOUCHPAD = 20
+ """"""
+
+
+class Power(enum.IntEnum):
+ """The possible power states of a controller.
+
+ .. seealso::
+ :any:`Joystick.get_current_power`
+ """
+
+ UNKNOWN = lib.SDL_JOYSTICK_POWER_UNKNOWN or -1
+ """Power state is unknown."""
+ EMPTY = lib.SDL_JOYSTICK_POWER_EMPTY or 0
+ """<= 5% power."""
+ LOW = lib.SDL_JOYSTICK_POWER_LOW or 1
+ """<= 20% power."""
+ MEDIUM = lib.SDL_JOYSTICK_POWER_MEDIUM or 2
+ """<= 70% power."""
+ FULL = lib.SDL_JOYSTICK_POWER_FULL or 3
+ """<= 100% power."""
+ WIRED = lib.SDL_JOYSTICK_POWER_WIRED or 4
+ """"""
+ MAX = lib.SDL_JOYSTICK_POWER_MAX or 5
+ """"""
+
+
+class Joystick:
+ """A low-level SDL joystick.
+
+ .. seealso::
+ https://wiki.libsdl.org/CategoryJoystick
+ """
+
+ _by_instance_id: ClassVar[WeakValueDictionary[int, Joystick]] = WeakValueDictionary()
+ """Currently opened joysticks."""
+
+ def __init__(self, sdl_joystick_p: Any) -> None:
+ self.sdl_joystick_p: Final = sdl_joystick_p
+ """The CFFI pointer to an SDL_Joystick struct."""
+ self.axes: Final[int] = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p))
+ """The total number of axes."""
+ self.balls: Final[int] = _check(lib.SDL_JoystickNumBalls(self.sdl_joystick_p))
+ """The total number of trackballs."""
+ self.buttons: Final[int] = _check(lib.SDL_JoystickNumButtons(self.sdl_joystick_p))
+ """The total number of buttons."""
+ self.hats: Final[int] = _check(lib.SDL_JoystickNumHats(self.sdl_joystick_p))
+ """The total number of hats."""
+ self.name: Final[str] = str(ffi.string(lib.SDL_JoystickName(self.sdl_joystick_p)), encoding="utf-8")
+ """The name of this joystick."""
+ self.guid: Final[str] = self._get_guid()
+ """The GUID of this joystick."""
+ self.id: Final[int] = _check(lib.SDL_JoystickInstanceID(self.sdl_joystick_p))
+ """The instance ID of this joystick. This is not the same as the device ID."""
+ self._keep_alive: Any = None
+ """The owner of this objects memory if this object does not own itself."""
+
+ self._by_instance_id[self.id] = self
+
+ @classmethod
+ def _open(cls, device_index: int) -> Joystick:
+ tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK)
+ p = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose))
+ return cls(p)
+
+ @classmethod
+ def _from_instance_id(cls, instance_id: int) -> Joystick:
+ return cls._by_instance_id[instance_id]
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Joystick):
+ return self.id == other.id
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(self.id)
+
+ def _get_guid(self) -> str:
+ guid_str = ffi.new("char[33]")
+ lib.SDL_JoystickGetGUIDString(lib.SDL_JoystickGetGUID(self.sdl_joystick_p), guid_str, len(guid_str))
+ return str(ffi.string(guid_str), encoding="ascii")
+
+ def get_current_power(self) -> Power:
+ """Return the power level/state of this joystick. See :any:`Power`."""
+ return Power(lib.SDL_JoystickCurrentPowerLevel(self.sdl_joystick_p))
+
+ def get_axis(self, axis: int) -> int:
+ """Return the raw value of `axis` in the range -32768 to 32767."""
+ return int(lib.SDL_JoystickGetAxis(self.sdl_joystick_p, axis))
+
+ def get_ball(self, ball: int) -> tuple[int, int]:
+ """Return the values (delta_x, delta_y) of `ball` since the last poll."""
+ xy = ffi.new("int[2]")
+ _check(lib.SDL_JoystickGetBall(ball, xy, xy + 1))
+ return int(xy[0]), int(xy[1])
+
+ def get_button(self, button: int) -> bool:
+ """Return True if `button` is currently held."""
+ return bool(lib.SDL_JoystickGetButton(self.sdl_joystick_p, button))
+
+ def get_hat(self, hat: int) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]:
+ """Return the direction of `hat` as (x, y). With (-1, -1) being in the upper-left."""
+ return _HAT_DIRECTIONS[lib.SDL_JoystickGetHat(self.sdl_joystick_p, hat)]
+
+
+class GameController:
+ """A standard interface for an Xbox 360 style game controller."""
+
+ _by_instance_id: ClassVar[WeakValueDictionary[int, GameController]] = WeakValueDictionary()
+ """Currently opened controllers."""
+
+ def __init__(self, sdl_controller_p: Any) -> None:
+ self.sdl_controller_p: Final = sdl_controller_p
+ self.joystick: Final = Joystick(lib.SDL_GameControllerGetJoystick(self.sdl_controller_p))
+ """The :any:`Joystick` associated with this controller."""
+ self.joystick._keep_alive = self.sdl_controller_p # This objects real owner needs to be kept alive.
+ self._by_instance_id[self.joystick.id] = self
+
+ @classmethod
+ def _open(cls, joystick_index: int) -> GameController:
+ return cls(_check_p(ffi.gc(lib.SDL_GameControllerOpen(joystick_index), lib.SDL_GameControllerClose)))
+
+ @classmethod
+ def _from_instance_id(cls, instance_id: int) -> GameController:
+ return cls._by_instance_id[instance_id]
+
+ def get_button(self, button: ControllerButton) -> bool:
+ """Return True if `button` is currently held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, button))
+
+ def get_axis(self, axis: ControllerAxis) -> int:
+ """Return the state of the given `axis`.
+
+ The state is usually a value from -32768 to 32767, with positive values towards the lower-right direction.
+ Triggers have the range of 0 to 32767 instead.
+ """
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, axis))
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, GameController):
+ return self.joystick.id == other.joystick.id
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(self.joystick.id)
+
+ # These could exist as convenience functions, but the get_X functions are probably better.
+ @property
+ def _left_x(self) -> int:
+ """Return the position of this axis (-32768 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTX))
+
+ @property
+ def _left_y(self) -> int:
+ """Return the position of this axis (-32768 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTY))
+
+ @property
+ def _right_x(self) -> int:
+ """Return the position of this axis (-32768 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTX))
+
+ @property
+ def _right_y(self) -> int:
+ """Return the position of this axis (-32768 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTY))
+
+ @property
+ def _trigger_left(self) -> int:
+ """Return the position of this trigger (0 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERLEFT))
+
+ @property
+ def _trigger_right(self) -> int:
+ """Return the position of this trigger (0 to 32767)."""
+ return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERRIGHT))
+
+ @property
+ def _a(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_A))
+
+ @property
+ def _b(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_B))
+
+ @property
+ def _x(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_X))
+
+ @property
+ def _y(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_Y))
+
+ @property
+ def _back(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_BACK))
+
+ @property
+ def _guide(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_GUIDE))
+
+ @property
+ def _start(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_START))
+
+ @property
+ def _left_stick(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_LEFTSTICK))
+
+ @property
+ def _right_stick(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_RIGHTSTICK))
+
+ @property
+ def _left_shoulder(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_LEFTSHOULDER))
+
+ @property
+ def _right_shoulder(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER))
+
+ @property
+ def _dpad(self) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]:
+ return (
+ lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_RIGHT)
+ - lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_LEFT),
+ lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_DOWN)
+ - lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_UP),
+ )
+
+ @property
+ def _misc1(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_MISC1))
+
+ @property
+ def _paddle1(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE1))
+
+ @property
+ def _paddle2(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE2))
+
+ @property
+ def _paddle3(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE3))
+
+ @property
+ def _paddle4(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE4))
+
+ @property
+ def _touchpad(self) -> bool:
+ """Return True if this button is held."""
+ return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_TOUCHPAD))
+
+
+def init() -> None:
+ """Initialize SDL's joystick and game controller subsystems."""
+ tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER)
+
+
+def _get_number() -> int:
+ """Return the number of attached joysticks."""
+ init()
+ return _check(lib.SDL_NumJoysticks())
+
+
+def get_joysticks() -> list[Joystick]:
+ """Return a list of all connected joystick devices."""
+ return [Joystick._open(i) for i in range(_get_number())]
+
+
+def get_controllers() -> list[GameController]:
+ """Return a list of all connected game controllers.
+
+ This ignores joysticks without a game controller mapping.
+ """
+ return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGameController(i)]
+
+
+def _get_all() -> list[Joystick | GameController]:
+ """Return a list of all connected joystick or controller devices.
+
+ If the joystick has a controller mapping then it is returned as a :any:`GameController`.
+ Otherwise it is returned as a :any:`Joystick`.
+ """
+ return [GameController._open(i) if lib.SDL_IsGameController(i) else Joystick._open(i) for i in range(_get_number())]
+
+
+def joystick_event_state(new_state: bool | None = None) -> bool:
+ """Check or set joystick event polling.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_JoystickEventState
+ """
+ _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_IGNORE, True: lib.SDL_ENABLE}
+ return bool(_check(lib.SDL_JoystickEventState(_OPTIONS[new_state])))
+
+
+def controller_event_state(new_state: bool | None = None) -> bool:
+ """Check or set game controller event polling.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GameControllerEventState
+ """
+ _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_IGNORE, True: lib.SDL_ENABLE}
+ return bool(_check(lib.SDL_GameControllerEventState(_OPTIONS[new_state])))
diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py
new file mode 100644
index 00000000..5c7543a5
--- /dev/null
+++ b/tcod/sdl/mouse.py
@@ -0,0 +1,250 @@
+"""SDL mouse and cursor functions.
+
+You can use this module to move or capture the cursor.
+
+You can also set the cursor icon to an OS-defined or custom icon.
+
+.. versionadded:: 13.5
+"""
+from __future__ import annotations
+
+import enum
+from typing import Any
+
+import numpy as np
+from numpy.typing import ArrayLike, NDArray
+
+import tcod.event
+import tcod.sdl.video
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _check_p
+
+
+class Cursor:
+ """A cursor icon for use with :any:`set_cursor`."""
+
+ def __init__(self, sdl_cursor_p: Any) -> None:
+ if ffi.typeof(sdl_cursor_p) is not ffi.typeof("struct SDL_Cursor*"):
+ msg = f"Expected a {ffi.typeof('struct SDL_Cursor*')} type (was {ffi.typeof(sdl_cursor_p)})."
+ raise TypeError(msg)
+ if not sdl_cursor_p:
+ msg = "C pointer must not be null."
+ raise TypeError(msg)
+ self.p = sdl_cursor_p
+
+ def __eq__(self, other: Any) -> bool:
+ return bool(self.p == getattr(other, "p", None))
+
+ @classmethod
+ def _claim(cls, sdl_cursor_p: Any) -> Cursor:
+ """Verify and wrap this pointer in a garbage collector before returning a Cursor."""
+ return cls(ffi.gc(_check_p(sdl_cursor_p), lib.SDL_FreeCursor))
+
+
+class SystemCursor(enum.IntEnum):
+ """An enumerator of system cursor icons."""
+
+ ARROW = 0
+ """"""
+ IBEAM = enum.auto()
+ """"""
+ WAIT = enum.auto()
+ """"""
+ CROSSHAIR = enum.auto()
+ """"""
+ WAITARROW = enum.auto()
+ """"""
+ SIZENWSE = enum.auto()
+ """"""
+ SIZENESW = enum.auto()
+ """"""
+ SIZEWE = enum.auto()
+ """"""
+ SIZENS = enum.auto()
+ """"""
+ SIZEALL = enum.auto()
+ """"""
+ NO = enum.auto()
+ """"""
+ HAND = enum.auto()
+ """"""
+
+
+def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: tuple[int, int] = (0, 0)) -> Cursor:
+ """Return a new non-color Cursor from the provided parameters.
+
+ Args:
+ data: A row-major boolean array for the data parameters. See the SDL docs for more info.
+ mask: A row-major boolean array for the mask parameters. See the SDL docs for more info.
+ hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0).
+
+ .. seealso::
+ :any:`set_cursor`
+ https://wiki.libsdl.org/SDL_CreateCursor
+ """
+ if len(data.shape) != 2:
+ msg = "Data and mask arrays must be 2D."
+ raise TypeError(msg)
+ if data.shape != mask.shape:
+ msg = "Data and mask arrays must have the same shape."
+ raise TypeError(msg)
+ height, width = data.shape
+ data_packed = np.packbits(data, axis=0, bitorder="big")
+ mask_packed = np.packbits(mask, axis=0, bitorder="big")
+ return Cursor._claim(
+ lib.SDL_CreateCursor(
+ ffi.from_buffer("uint8_t*", data_packed), ffi.from_buffer("uint8_t*", mask_packed), width, height, *hot_xy
+ )
+ )
+
+
+def new_color_cursor(pixels: ArrayLike, hot_xy: tuple[int, int]) -> Cursor:
+ """Create a new color cursor.
+
+ Args:
+ pixels: A row-major array of RGB or RGBA pixels.
+ hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0).
+
+ .. seealso::
+ :any:`set_cursor`
+ """
+ surface = tcod.sdl.video._TempSurface(pixels)
+ return Cursor._claim(lib.SDL_CreateColorCursor(surface.p, *hot_xy))
+
+
+def new_system_cursor(cursor: SystemCursor) -> Cursor:
+ """Return a new Cursor from one of the system cursors labeled by SystemCursor.
+
+ .. seealso::
+ :any:`set_cursor`
+ """
+ return Cursor._claim(lib.SDL_CreateSystemCursor(cursor))
+
+
+def set_cursor(cursor: Cursor | SystemCursor | None) -> None:
+ """Change the active cursor to the one provided.
+
+ Args:
+ cursor: A cursor created from :any:`new_cursor`, :any:`new_color_cursor`, or :any:`new_system_cursor`.
+ Can also take values of :any:`SystemCursor` directly.
+ None will force the current cursor to be redrawn.
+ """
+ if isinstance(cursor, SystemCursor):
+ cursor = new_system_cursor(cursor)
+ lib.SDL_SetCursor(cursor.p if cursor is not None else ffi.NULL)
+
+
+def get_default_cursor() -> Cursor:
+ """Return the default cursor."""
+ return Cursor(_check_p(lib.SDL_GetDefaultCursor()))
+
+
+def get_cursor() -> Cursor | None:
+ """Return the active cursor, or None if these is no mouse."""
+ cursor_p = lib.SDL_GetCursor()
+ return Cursor(cursor_p) if cursor_p else None
+
+
+def capture(enable: bool) -> None:
+ """Enable or disable mouse capture to track the mouse outside of a window.
+
+ It is highly recommended to read the related remarks section in the SDL docs before using this.
+
+ Example::
+
+ # Make mouse button presses capture the mouse until all buttons are released.
+ # This means that dragging the mouse outside of the window will not cause an interruption in motion events.
+ for event in tcod.event.get():
+ match event:
+ case tcod.event.MouseButtonDown(button=button, pixel=pixel): # Clicking the window captures the mouse.
+ tcod.sdl.mouse.capture(True)
+ case tcod.event.MouseButtonUp(): # When all buttons are released then the mouse is released.
+ if tcod.event.mouse.get_global_state().state == 0:
+ tcod.sdl.mouse.capture(False)
+ case tcod.event.MouseMotion(pixel=pixel, pixel_motion=pixel_motion, state=state):
+ pass # While a button is held this event is still captured outside of the window.
+
+ .. seealso::
+ :any:`tcod.sdl.mouse.set_relative_mode`
+ https://wiki.libsdl.org/SDL_CaptureMouse
+ """
+ _check(lib.SDL_CaptureMouse(enable))
+
+
+def set_relative_mode(enable: bool) -> None:
+ """Enable or disable relative mouse mode which will lock and hide the mouse and only report mouse motion.
+
+ .. seealso::
+ :any:`tcod.sdl.mouse.capture`
+ https://wiki.libsdl.org/SDL_SetRelativeMouseMode
+ """
+ _check(lib.SDL_SetRelativeMouseMode(enable))
+
+
+def get_relative_mode() -> bool:
+ """Return True if relative mouse mode is enabled."""
+ return bool(lib.SDL_GetRelativeMouseMode())
+
+
+def get_global_state() -> tcod.event.MouseState:
+ """Return the mouse state relative to the desktop.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GetGlobalMouseState
+ """
+ xy = ffi.new("int[2]")
+ state = lib.SDL_GetGlobalMouseState(xy, xy + 1)
+ return tcod.event.MouseState((xy[0], xy[1]), state=state)
+
+
+def get_relative_state() -> tcod.event.MouseState:
+ """Return the mouse state, the coordinates are relative to the last time this function was called.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GetRelativeMouseState
+ """
+ xy = ffi.new("int[2]")
+ state = lib.SDL_GetRelativeMouseState(xy, xy + 1)
+ return tcod.event.MouseState((xy[0], xy[1]), state=state)
+
+
+def get_state() -> tcod.event.MouseState:
+ """Return the mouse state relative to the window with mouse focus.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GetMouseState
+ """
+ xy = ffi.new("int[2]")
+ state = lib.SDL_GetMouseState(xy, xy + 1)
+ return tcod.event.MouseState((xy[0], xy[1]), state=state)
+
+
+def get_focus() -> tcod.sdl.video.Window | None:
+ """Return the window which currently has mouse focus."""
+ window_p = lib.SDL_GetMouseFocus()
+ return tcod.sdl.video.Window(window_p) if window_p else None
+
+
+def warp_global(x: int, y: int) -> None:
+ """Move the mouse cursor to a position on the desktop."""
+ _check(lib.SDL_WarpMouseGlobal(x, y))
+
+
+def warp_in_window(window: tcod.sdl.video.Window, x: int, y: int) -> None:
+ """Move the mouse cursor to a position within a window."""
+ lib.SDL_WarpMouseInWindow(window.p, x, y)
+
+
+def show(visible: bool | None = None) -> bool:
+ """Optionally show or hide the mouse cursor then return the state of the cursor.
+
+ Args:
+ visible: If None then only return the current state. Otherwise set the mouse visibility.
+
+ Returns:
+ True if the cursor is visible.
+
+ .. versionadded:: 16.0
+ """
+ _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_DISABLE, True: lib.SDL_ENABLE}
+ return _check(lib.SDL_ShowCursor(_OPTIONS[visible])) == int(lib.SDL_ENABLE)
diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py
new file mode 100644
index 00000000..06962f93
--- /dev/null
+++ b/tcod/sdl/render.py
@@ -0,0 +1,733 @@
+"""SDL2 Rendering functionality.
+
+.. versionadded:: 13.4
+"""
+from __future__ import annotations
+
+import enum
+from typing import Any
+
+import numpy as np
+from numpy.typing import NDArray
+from typing_extensions import Final, Literal
+
+import tcod.sdl.video
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _check_p, _required_version
+
+
+class TextureAccess(enum.IntEnum):
+ """Determines how a texture is expected to be used."""
+
+ STATIC = 0
+ """Texture rarely changes."""
+ STREAMING = 1
+ """Texture frequently changes."""
+ TARGET = 2
+ """Texture will be used as a render target."""
+
+
+class RendererFlip(enum.IntFlag):
+ """Flip parameter for :any:`Renderer.copy`."""
+
+ NONE = 0
+ """Default value, no flip."""
+ HORIZONTAL = 1
+ """Flip the image horizontally."""
+ VERTICAL = 2
+ """Flip the image vertically."""
+
+
+class BlendFactor(enum.IntEnum):
+ """SDL blend factors.
+
+ .. seealso::
+ :any:`compose_blend_mode`
+ https://wiki.libsdl.org/SDL_BlendFactor
+
+ .. versionadded:: 13.5
+ """
+
+ ZERO = 0x1
+ """"""
+ ONE = 0x2
+ """"""
+ SRC_COLOR = 0x3
+ """"""
+ ONE_MINUS_SRC_COLOR = 0x4
+ """"""
+ SRC_ALPHA = 0x5
+ """"""
+ ONE_MINUS_SRC_ALPHA = 0x6
+ """"""
+ DST_COLOR = 0x7
+ """"""
+ ONE_MINUS_DST_COLOR = 0x8
+ """"""
+ DST_ALPHA = 0x9
+ """"""
+ ONE_MINUS_DST_ALPHA = 0xA
+ """"""
+
+
+class BlendOperation(enum.IntEnum):
+ """SDL blend operations.
+
+ .. seealso::
+ :any:`compose_blend_mode`
+ https://wiki.libsdl.org/SDL_BlendOperation
+
+ .. versionadded:: 13.5
+ """
+
+ ADD = 0x1
+ """dest + source"""
+ SUBTRACT = 0x2
+ """dest - source"""
+ REV_SUBTRACT = 0x3
+ """source - dest"""
+ MINIMUM = 0x4
+ """min(dest, source)"""
+ MAXIMUM = 0x5
+ """max(dest, source)"""
+
+
+class BlendMode(enum.IntEnum):
+ """SDL blend modes.
+
+ .. seealso::
+ :any:`Texture.blend_mode`
+ :any:`Renderer.draw_blend_mode`
+ :any:`compose_blend_mode`
+
+ .. versionadded:: 13.5
+ """
+
+ NONE = 0x00000000
+ """"""
+ BLEND = 0x00000001
+ """"""
+ ADD = 0x00000002
+ """"""
+ MOD = 0x00000004
+ """"""
+ INVALID = 0x7FFFFFFF
+ """"""
+
+
+def compose_blend_mode(
+ source_color_factor: BlendFactor,
+ dest_color_factor: BlendFactor,
+ color_operation: BlendOperation,
+ source_alpha_factor: BlendFactor,
+ dest_alpha_factor: BlendFactor,
+ alpha_operation: BlendOperation,
+) -> BlendMode:
+ """Return a custom blend mode composed of the given factors and operations.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_ComposeCustomBlendMode
+
+ .. versionadded:: 13.5
+ """
+ return BlendMode(
+ lib.SDL_ComposeCustomBlendMode(
+ source_color_factor,
+ dest_color_factor,
+ color_operation,
+ source_alpha_factor,
+ dest_alpha_factor,
+ alpha_operation,
+ )
+ )
+
+
+class Texture:
+ """SDL hardware textures.
+
+ Create a new texture using :any:`Renderer.new_texture` or :any:`Renderer.upload_texture`.
+ """
+
+ def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None:
+ self.p = sdl_texture_p
+ self._sdl_renderer_p = sdl_renderer_p # Keep alive.
+ query = self._query()
+ self.format: Final[int] = query[0]
+ """Texture format, read only."""
+ self.access: Final[TextureAccess] = TextureAccess(query[1])
+ """Texture access mode, read only.
+
+ .. versionchanged:: 13.5
+ Attribute is now a :any:`TextureAccess` value.
+ """
+ self.width: Final[int] = query[2]
+ """Texture pixel width, read only."""
+ self.height: Final[int] = query[3]
+ """Texture pixel height, read only."""
+
+ def __eq__(self, other: Any) -> bool:
+ return bool(self.p == getattr(other, "p", None))
+
+ def _query(self) -> tuple[int, int, int, int]:
+ """Return (format, access, width, height)."""
+ format = ffi.new("uint32_t*")
+ buffer = ffi.new("int[3]")
+ lib.SDL_QueryTexture(self.p, format, buffer, buffer + 1, buffer + 2)
+ return int(format[0]), int(buffer[0]), int(buffer[1]), int(buffer[2])
+
+ def update(self, pixels: NDArray[Any], rect: tuple[int, int, int, int] | None = None) -> None:
+ """Update the pixel data of this texture.
+
+ .. versionadded:: 13.5
+ """
+ if rect is None:
+ rect = (0, 0, self.width, self.height)
+ assert pixels.shape[:2] == (self.height, self.width)
+ if not pixels[0].flags.c_contiguous:
+ pixels = np.ascontiguousarray(pixels)
+ _check(lib.SDL_UpdateTexture(self.p, (rect,), ffi.cast("void*", pixels.ctypes.data), pixels.strides[0]))
+
+ @property
+ def alpha_mod(self) -> int:
+ """Texture alpha modulate value, can be set to 0 - 255."""
+ out = ffi.new("uint8_t*")
+ _check(lib.SDL_GetTextureAlphaMod(self.p, out))
+ return int(out[0])
+
+ @alpha_mod.setter
+ def alpha_mod(self, value: int) -> None:
+ _check(lib.SDL_SetTextureAlphaMod(self.p, value))
+
+ @property
+ def blend_mode(self) -> BlendMode:
+ """Texture blend mode, can be set.
+
+ .. versionchanged:: 13.5
+ Property now returns a BlendMode instance.
+ """
+ out = ffi.new("SDL_BlendMode*")
+ _check(lib.SDL_GetTextureBlendMode(self.p, out))
+ return BlendMode(out[0])
+
+ @blend_mode.setter
+ def blend_mode(self, value: int) -> None:
+ _check(lib.SDL_SetTextureBlendMode(self.p, value))
+
+ @property
+ def color_mod(self) -> tuple[int, int, int]:
+ """Texture RGB color modulate values, can be set."""
+ rgb = ffi.new("uint8_t[3]")
+ _check(lib.SDL_GetTextureColorMod(self.p, rgb, rgb + 1, rgb + 2))
+ return int(rgb[0]), int(rgb[1]), int(rgb[2])
+
+ @color_mod.setter
+ def color_mod(self, rgb: tuple[int, int, int]) -> None:
+ _check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2]))
+
+
+class _RestoreTargetContext:
+ """A context manager which tracks the current render target and restores it on exiting."""
+
+ def __init__(self, renderer: Renderer) -> None:
+ self.renderer = renderer
+ self.old_texture_p = lib.SDL_GetRenderTarget(renderer.p)
+
+ def __enter__(self) -> None:
+ pass
+
+ def __exit__(self, *_: Any) -> None:
+ _check(lib.SDL_SetRenderTarget(self.renderer.p, self.old_texture_p))
+
+
+class Renderer:
+ """SDL Renderer."""
+
+ def __init__(self, sdl_renderer_p: Any) -> None:
+ if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"):
+ msg = f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)})."
+ raise TypeError(msg)
+ if not sdl_renderer_p:
+ msg = "C pointer must not be null."
+ raise TypeError(msg)
+ self.p = sdl_renderer_p
+
+ def __eq__(self, other: Any) -> bool:
+ return bool(self.p == getattr(other, "p", None))
+
+ def copy(
+ self,
+ texture: Texture,
+ source: tuple[float, float, float, float] | None = None,
+ dest: tuple[float, float, float, float] | None = None,
+ angle: float = 0,
+ center: tuple[float, float] | None = None,
+ flip: RendererFlip = RendererFlip.NONE,
+ ) -> None:
+ """Copy a texture to the rendering target.
+
+ Args:
+ texture: The texture to copy onto the current texture target.
+ source: The (x, y, width, height) region of `texture` to copy. If None then the entire texture is copied.
+ dest: The (x, y, width, height) region of the target. If None then the entire target is drawn over.
+ angle: The angle in degrees to rotate the image clockwise.
+ center: The (x, y) point where rotation is applied. If None then the center of `dest` is used.
+ flip: Flips the `texture` when drawing it.
+
+ .. versionchanged:: 13.5
+ `source` and `dest` can now be float tuples.
+ Added the `angle`, `center`, and `flip` parameters.
+ """
+ _check(
+ lib.SDL_RenderCopyExF(
+ self.p,
+ texture.p,
+ (source,) if source is not None else ffi.NULL,
+ (dest,) if dest is not None else ffi.NULL,
+ angle,
+ (center,) if center is not None else ffi.NULL,
+ flip,
+ )
+ )
+
+ def present(self) -> None:
+ """Present the currently rendered image to the screen."""
+ lib.SDL_RenderPresent(self.p)
+
+ def set_render_target(self, texture: Texture) -> _RestoreTargetContext:
+ """Change the render target to `texture`, returns a context that will restore the original target when exited."""
+ restore = _RestoreTargetContext(self)
+ _check(lib.SDL_SetRenderTarget(self.p, texture.p))
+ return restore
+
+ def new_texture(self, width: int, height: int, *, format: int | None = None, access: int | None = None) -> Texture:
+ """Allocate and return a new Texture for this renderer.
+
+ Args:
+ width: The pixel width of the new texture.
+ height: The pixel height of the new texture.
+ format: The format the new texture.
+ access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`.
+ See :any:`TextureAccess` for more options.
+ """
+ if format is None:
+ format = 0
+ if access is None:
+ access = int(lib.SDL_TEXTUREACCESS_STATIC)
+ texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture)
+ return Texture(texture_p, self.p)
+
+ def upload_texture(self, pixels: NDArray[Any], *, format: int | None = None, access: int | None = None) -> Texture:
+ """Return a new Texture from an array of pixels.
+
+ Args:
+ pixels: An RGB or RGBA array of pixels in row-major order.
+ format: The format of `pixels` when it isn't a simple RGB or RGBA array.
+ access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`.
+ See :any:`TextureAccess` for more options.
+ """
+ if format is None:
+ assert len(pixels.shape) == 3
+ assert pixels.dtype == np.uint8
+ if pixels.shape[2] == 4:
+ format = int(lib.SDL_PIXELFORMAT_RGBA32)
+ elif pixels.shape[2] == 3:
+ format = int(lib.SDL_PIXELFORMAT_RGB24)
+ else:
+ msg = f"Can't determine the format required for an array of shape {pixels.shape}."
+ raise TypeError(msg)
+
+ texture = self.new_texture(pixels.shape[1], pixels.shape[0], format=format, access=access)
+ if not pixels[0].flags["C_CONTIGUOUS"]:
+ pixels = np.ascontiguousarray(pixels)
+ _check(
+ lib.SDL_UpdateTexture(texture.p, ffi.NULL, ffi.cast("const void*", pixels.ctypes.data), pixels.strides[0])
+ )
+ return texture
+
+ @property
+ def draw_color(self) -> tuple[int, int, int, int]:
+ """Get or set the active RGBA draw color for this renderer.
+
+ .. versionadded:: 13.5
+ """
+ rgba = ffi.new("uint8_t[4]")
+ _check(lib.SDL_GetRenderDrawColor(self.p, rgba, rgba + 1, rgba + 2, rgba + 3))
+ return tuple(rgba) # type: ignore[return-value]
+
+ @draw_color.setter
+ def draw_color(self, rgba: tuple[int, int, int, int]) -> None:
+ _check(lib.SDL_SetRenderDrawColor(self.p, *rgba))
+
+ @property
+ def draw_blend_mode(self) -> BlendMode:
+ """Get or set the active blend mode of this renderer.
+
+ .. versionadded:: 13.5
+ """
+ out = ffi.new("SDL_BlendMode*")
+ _check(lib.SDL_GetRenderDrawBlendMode(self.p, out))
+ return BlendMode(out[0])
+
+ @draw_blend_mode.setter
+ def draw_blend_mode(self, value: int) -> None:
+ _check(lib.SDL_SetRenderDrawBlendMode(self.p, value))
+
+ @property
+ def output_size(self) -> tuple[int, int]:
+ """Get the (width, height) pixel resolution of the rendering context.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GetRendererOutputSize
+
+ .. versionadded:: 13.5
+ """
+ out = ffi.new("int[2]")
+ _check(lib.SDL_GetRendererOutputSize(self.p, out, out + 1))
+ return out[0], out[1]
+
+ @property
+ def clip_rect(self) -> tuple[int, int, int, int] | None:
+ """Get or set the clipping rectangle of this renderer.
+
+ Set to None to disable clipping.
+
+ .. versionadded:: 13.5
+ """
+ if not lib.SDL_RenderIsClipEnabled(self.p):
+ return None
+ rect = ffi.new("SDL_Rect*")
+ lib.SDL_RenderGetClipRect(self.p, rect)
+ return rect.x, rect.y, rect.w, rect.h
+
+ @clip_rect.setter
+ def clip_rect(self, rect: tuple[int, int, int, int] | None) -> None:
+ rect_p = ffi.NULL if rect is None else ffi.new("SDL_Rect*", rect)
+ _check(lib.SDL_RenderSetClipRect(self.p, rect_p))
+
+ @property
+ def integer_scaling(self) -> bool:
+ """Get or set if this renderer enforces integer scaling.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_RenderSetIntegerScale
+
+ .. versionadded:: 13.5
+ """
+ return bool(lib.SDL_RenderGetIntegerScale(self.p))
+
+ @integer_scaling.setter
+ def integer_scaling(self, enable: bool) -> None:
+ _check(lib.SDL_RenderSetIntegerScale(self.p, enable))
+
+ @property
+ def logical_size(self) -> tuple[int, int]:
+ """Get or set a device independent (width, height) resolution.
+
+ Might be (0, 0) if a resolution was never assigned.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_RenderSetLogicalSize
+
+ .. versionadded:: 13.5
+ """
+ out = ffi.new("int[2]")
+ lib.SDL_RenderGetLogicalSize(self.p, out, out + 1)
+ return out[0], out[1]
+
+ @logical_size.setter
+ def logical_size(self, size: tuple[int, int]) -> None:
+ _check(lib.SDL_RenderSetLogicalSize(self.p, *size))
+
+ @property
+ def scale(self) -> tuple[float, float]:
+ """Get or set an (x_scale, y_scale) multiplier for drawing.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_RenderSetScale
+
+ .. versionadded:: 13.5
+ """
+ out = ffi.new("float[2]")
+ lib.SDL_RenderGetScale(self.p, out, out + 1)
+ return out[0], out[1]
+
+ @scale.setter
+ def scale(self, scale: tuple[float, float]) -> None:
+ _check(lib.SDL_RenderSetScale(self.p, *scale))
+
+ @property
+ def viewport(self) -> tuple[int, int, int, int] | None:
+ """Get or set the drawing area for the current rendering target.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_RenderSetViewport
+
+ .. versionadded:: 13.5
+ """
+ rect = ffi.new("SDL_Rect*")
+ lib.SDL_RenderGetViewport(self.p, rect)
+ return rect.x, rect.y, rect.w, rect.h
+
+ @viewport.setter
+ def viewport(self, rect: tuple[int, int, int, int] | None) -> None:
+ _check(lib.SDL_RenderSetViewport(self.p, (rect,)))
+
+ @_required_version((2, 0, 18))
+ def set_vsync(self, enable: bool) -> None:
+ """Enable or disable VSync for this renderer.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderSetVSync(self.p, enable))
+
+ def read_pixels(
+ self,
+ *,
+ rect: tuple[int, int, int, int] | None = None,
+ format: int | Literal["RGB", "RGBA"] = "RGBA",
+ out: NDArray[np.uint8] | None = None,
+ ) -> NDArray[np.uint8]:
+ """Fetch the pixel contents of the current rendering target to an array.
+
+ By default returns an RGBA pixel array of the full target in the shape: ``(height, width, rgba)``.
+ The target can be changed with :any:`set_render_target`
+
+ Args:
+ rect: The ``(left, top, width, height)`` region of the target to fetch, or None for the entire target.
+ format: The pixel format. Defaults to ``"RGBA"``.
+ out: The output array.
+ Can be None or must be an ``np.uint8`` array of shape: ``(height, width, channels)``.
+ Must be C contiguous along the ``(width, channels)`` axes.
+
+ This operation is slow due to coping from VRAM to RAM.
+ When reading the main rendering target this should be called after rendering and before :any:`present`.
+ See https://wiki.libsdl.org/SDL2/SDL_RenderReadPixels
+
+ Returns:
+ The output uint8 array of shape: ``(height, width, channels)`` with the fetched pixels.
+
+ .. versionadded:: 15.0
+ """
+ FORMATS: Final = {"RGB": lib.SDL_PIXELFORMAT_RGB24, "RGBA": lib.SDL_PIXELFORMAT_RGBA32}
+ sdl_format = FORMATS.get(format) if isinstance(format, str) else format
+ if rect is None:
+ texture_p = lib.SDL_GetRenderTarget(self.p)
+ if texture_p:
+ texture = Texture(texture_p)
+ rect = (0, 0, texture.width, texture.height)
+ else:
+ rect = (0, 0, *self.output_size)
+ width, height = rect[2:4]
+ if out is None:
+ if sdl_format == lib.SDL_PIXELFORMAT_RGBA32:
+ out = np.empty((height, width, 4), dtype=np.uint8)
+ elif sdl_format == lib.SDL_PIXELFORMAT_RGB24:
+ out = np.empty((height, width, 3), dtype=np.uint8)
+ else:
+ msg = f"Pixel format {format!r} not supported by tcod."
+ raise TypeError(msg)
+ if out.dtype != np.uint8:
+ msg = "`out` must be a uint8 array."
+ raise TypeError(msg)
+ expected_shape = (height, width, {lib.SDL_PIXELFORMAT_RGB24: 3, lib.SDL_PIXELFORMAT_RGBA32: 4}[sdl_format])
+ if out.shape != expected_shape:
+ msg = f"Expected `out` to be an array of shape {expected_shape}, got {out.shape} instead."
+ raise TypeError(msg)
+ if not out[0].flags.c_contiguous:
+ msg = "`out` array must be C contiguous."
+ _check(
+ lib.SDL_RenderReadPixels(
+ self.p,
+ (rect,),
+ sdl_format,
+ ffi.cast("void*", out.ctypes.data),
+ out.strides[0],
+ )
+ )
+ return out
+
+ def clear(self) -> None:
+ """Clear the current render target with :any:`draw_color`.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderClear(self.p))
+
+ def fill_rect(self, rect: tuple[float, float, float, float]) -> None:
+ """Fill a rectangle with :any:`draw_color`.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderFillRectF(self.p, (rect,)))
+
+ def draw_rect(self, rect: tuple[float, float, float, float]) -> None:
+ """Draw a rectangle outline.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderDrawRectF(self.p, (rect,)))
+
+ def draw_point(self, xy: tuple[float, float]) -> None:
+ """Draw a point.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderDrawPointF(self.p, (xy,)))
+
+ def draw_line(self, start: tuple[float, float], end: tuple[float, float]) -> None:
+ """Draw a single line.
+
+ .. versionadded:: 13.5
+ """
+ _check(lib.SDL_RenderDrawLineF(self.p, *start, *end))
+
+ def fill_rects(self, rects: NDArray[np.intc | np.float32]) -> None:
+ """Fill multiple rectangles from an array.
+
+ .. versionadded:: 13.5
+ """
+ assert len(rects.shape) == 2
+ assert rects.shape[1] == 4
+ rects = np.ascontiguousarray(rects)
+ if rects.dtype == np.intc:
+ _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0]))
+ elif rects.dtype == np.float32:
+ _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
+ else:
+ msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}."
+ raise TypeError(msg)
+
+ def draw_rects(self, rects: NDArray[np.intc | np.float32]) -> None:
+ """Draw multiple outlined rectangles from an array.
+
+ .. versionadded:: 13.5
+ """
+ assert len(rects.shape) == 2
+ assert rects.shape[1] == 4
+ rects = np.ascontiguousarray(rects)
+ if rects.dtype == np.intc:
+ _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0]))
+ elif rects.dtype == np.float32:
+ _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
+ else:
+ msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}."
+ raise TypeError(msg)
+
+ def draw_points(self, points: NDArray[np.intc | np.float32]) -> None:
+ """Draw an array of points.
+
+ .. versionadded:: 13.5
+ """
+ assert len(points.shape) == 2
+ assert points.shape[1] == 2
+ points = np.ascontiguousarray(points)
+ if points.dtype == np.intc:
+ _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0]))
+ elif points.dtype == np.float32:
+ _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0]))
+ else:
+ msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}."
+ raise TypeError(msg)
+
+ def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None:
+ """Draw a connected series of lines from an array.
+
+ .. versionadded:: 13.5
+ """
+ assert len(points.shape) == 2
+ assert points.shape[1] == 2
+ points = np.ascontiguousarray(points)
+ if points.dtype == np.intc:
+ _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1))
+ elif points.dtype == np.float32:
+ _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0] - 1))
+ else:
+ msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}."
+ raise TypeError(msg)
+
+ @_required_version((2, 0, 18))
+ def geometry(
+ self,
+ texture: Texture | None,
+ xy: NDArray[np.float32],
+ color: NDArray[np.uint8],
+ uv: NDArray[np.float32],
+ indices: NDArray[np.uint8 | np.uint16 | np.uint32] | None = None,
+ ) -> None:
+ """Render triangles from texture and vertex data.
+
+ .. versionadded:: 13.5
+ """
+ assert xy.dtype == np.float32
+ assert len(xy.shape) == 2
+ assert xy.shape[1] == 2
+ assert xy[0].flags.c_contiguous
+
+ assert color.dtype == np.uint8
+ assert len(color.shape) == 2
+ assert color.shape[1] == 4
+ assert color[0].flags.c_contiguous
+
+ assert uv.dtype == np.float32
+ assert len(uv.shape) == 2
+ assert uv.shape[1] == 2
+ assert uv[0].flags.c_contiguous
+ if indices is not None:
+ assert indices.dtype.type in (np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32)
+ indices = np.ascontiguousarray(indices)
+ assert len(indices.shape) == 1
+ assert xy.shape[0] == color.shape[0] == uv.shape[0]
+ _check(
+ lib.SDL_RenderGeometryRaw(
+ self.p,
+ texture.p if texture else ffi.NULL,
+ ffi.cast("float*", xy.ctypes.data),
+ xy.strides[0],
+ ffi.cast("uint8_t*", color.ctypes.data),
+ color.strides[0],
+ ffi.cast("float*", uv.ctypes.data),
+ uv.strides[0],
+ xy.shape[0], # Number of vertices.
+ ffi.cast("void*", indices.ctypes.data) if indices is not None else ffi.NULL,
+ indices.size if indices is not None else 0,
+ indices.itemsize if indices is not None else 0,
+ )
+ )
+
+
+def new_renderer(
+ window: tcod.sdl.video.Window,
+ *,
+ driver: int | None = None,
+ software: bool = False,
+ vsync: bool = True,
+ target_textures: bool = False,
+) -> Renderer:
+ """Initialize and return a new SDL Renderer.
+
+ Args:
+ window: The window that this renderer will be attached to.
+ driver: Force SDL to use a specific video driver.
+ software: If True then a software renderer will be forced. By default a hardware renderer is used.
+ vsync: If True then Vsync will be enabled.
+ target_textures: If True then target textures can be used by the renderer.
+
+ Example::
+
+ # Start by creating a window.
+ sdl_window = tcod.sdl.video.new_window(640, 480)
+ # Create a renderer with target texture support.
+ sdl_renderer = tcod.sdl.render.new_renderer(sdl_window, target_textures=True)
+
+ .. seealso::
+ :func:`tcod.sdl.video.new_window`
+ """
+ driver = driver if driver is not None else -1
+ flags = 0
+ if vsync:
+ flags |= int(lib.SDL_RENDERER_PRESENTVSYNC)
+ if target_textures:
+ flags |= int(lib.SDL_RENDERER_TARGETTEXTURE)
+ flags |= int(lib.SDL_RENDERER_SOFTWARE) if software else int(lib.SDL_RENDERER_ACCELERATED)
+ renderer_p = _check_p(ffi.gc(lib.SDL_CreateRenderer(window.p, driver, flags), lib.SDL_DestroyRenderer))
+ return Renderer(renderer_p)
diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py
new file mode 100644
index 00000000..b0cbd08b
--- /dev/null
+++ b/tcod/sdl/sys.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import enum
+import warnings
+from typing import Any
+
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _get_error
+
+
+class Subsystem(enum.IntFlag):
+ TIMER = 0x00000001
+ AUDIO = 0x00000010
+ VIDEO = 0x00000020
+ JOYSTICK = 0x00000200
+ HAPTIC = 0x00001000
+ GAMECONTROLLER = 0x00002000
+ EVENTS = 0x00004000
+ SENSOR = 0x00008000
+ EVERYTHING = lib.SDL_INIT_EVERYTHING or 0
+
+
+def init(flags: int = Subsystem.EVERYTHING) -> None:
+ _check(lib.SDL_InitSubSystem(flags))
+
+
+def quit(flags: int = Subsystem.EVERYTHING) -> None:
+ lib.SDL_QuitSubSystem(flags)
+
+
+class _ScopeInit:
+ def __init__(self, flags: int) -> None:
+ init(flags)
+ self.flags = flags
+
+ def close(self) -> None:
+ if self.flags:
+ quit(self.flags)
+ self.flags = 0
+
+ def __del__(self) -> None:
+ self.close()
+
+ def __enter__(self) -> _ScopeInit:
+ return self
+
+ def __exit__(self, *args: Any) -> None:
+ self.close()
+
+
+class _PowerState(enum.IntEnum):
+ UNKNOWN = 0
+ ON_BATTERY = enum.auto()
+ NO_BATTERY = enum.auto()
+ CHARGING = enum.auto()
+ CHARGED = enum.auto()
+
+
+def _get_power_info() -> tuple[_PowerState, int, int]:
+ buffer = ffi.new("int[2]")
+ power_state = _PowerState(lib.SDL_GetPowerInfo(buffer, buffer + 1))
+ seconds_of_power = buffer[0]
+ percentage = buffer[1]
+ return power_state, seconds_of_power, percentage
+
+
+def _get_clipboard() -> str:
+ """Return the text of the clipboard."""
+ text = str(ffi.string(lib.SDL_GetClipboardText()), encoding="utf-8")
+ if not text: # Show the reason for an empty return, this should probably be logged instead.
+ warnings.warn(f"Return string is empty because: {_get_error()}", RuntimeWarning, stacklevel=2)
+ return text
+
+
+def _set_clipboard(text: str) -> None:
+ """Replace the clipboard with text."""
+ _check(lib.SDL_SetClipboardText(text.encode("utf-8")))
diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py
new file mode 100644
index 00000000..21af4ecc
--- /dev/null
+++ b/tcod/sdl/video.py
@@ -0,0 +1,404 @@
+"""SDL2 Window and Display handling.
+
+There are two main ways to access the SDL window.
+Either you can use this module to open a window yourself bypassing libtcod's context,
+or you can use :any:`Context.sdl_window` to get the window being controlled by that context (if the context has one.)
+
+.. versionadded:: 13.4
+"""
+from __future__ import annotations
+
+import enum
+import sys
+from typing import Any
+
+import numpy as np
+from numpy.typing import ArrayLike, NDArray
+
+from tcod.loader import ffi, lib
+from tcod.sdl._internal import _check, _check_p, _required_version, _version_at_least
+
+__all__ = (
+ "WindowFlags",
+ "FlashOperation",
+ "Window",
+ "new_window",
+ "get_grabbed_window",
+ "screen_saver_allowed",
+)
+
+
+class WindowFlags(enum.IntFlag):
+ """Bit flags which make up a windows state.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_WindowFlags
+ """
+
+ FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN or 0
+ """"""
+ FULLSCREEN_DESKTOP = lib.SDL_WINDOW_FULLSCREEN_DESKTOP or 0
+ """"""
+ OPENGL = lib.SDL_WINDOW_OPENGL or 0
+ """"""
+ SHOWN = lib.SDL_WINDOW_SHOWN or 0
+ """"""
+ HIDDEN = lib.SDL_WINDOW_HIDDEN or 0
+ """"""
+ BORDERLESS = lib.SDL_WINDOW_BORDERLESS or 0
+ """"""
+ RESIZABLE = lib.SDL_WINDOW_RESIZABLE or 0
+ """"""
+ MINIMIZED = lib.SDL_WINDOW_MINIMIZED or 0
+ """"""
+ MAXIMIZED = lib.SDL_WINDOW_MAXIMIZED or 0
+ """"""
+ MOUSE_GRABBED = lib.SDL_WINDOW_INPUT_GRABBED or 0
+ """"""
+ INPUT_FOCUS = lib.SDL_WINDOW_INPUT_FOCUS or 0
+ """"""
+ MOUSE_FOCUS = lib.SDL_WINDOW_MOUSE_FOCUS or 0
+ """"""
+ FOREIGN = lib.SDL_WINDOW_FOREIGN or 0
+ """"""
+ ALLOW_HIGHDPI = lib.SDL_WINDOW_ALLOW_HIGHDPI or 0
+ """"""
+ MOUSE_CAPTURE = lib.SDL_WINDOW_MOUSE_CAPTURE or 0
+ """"""
+ ALWAYS_ON_TOP = lib.SDL_WINDOW_ALWAYS_ON_TOP or 0
+ """"""
+ SKIP_TASKBAR = lib.SDL_WINDOW_SKIP_TASKBAR or 0
+ """"""
+ UTILITY = lib.SDL_WINDOW_UTILITY or 0
+ """"""
+ TOOLTIP = lib.SDL_WINDOW_TOOLTIP or 0
+ """"""
+ POPUP_MENU = lib.SDL_WINDOW_POPUP_MENU or 0
+ """"""
+ VULKAN = lib.SDL_WINDOW_VULKAN or 0
+ """"""
+ METAL = getattr(lib, "SDL_WINDOW_METAL", None) or 0x20000000 # SDL >= 2.0.14
+ """"""
+
+
+class FlashOperation(enum.IntEnum):
+ """Values for :any:`Window.flash`."""
+
+ CANCEL = 0
+ """Stop flashing."""
+ BRIEFLY = 1
+ """Flash briefly."""
+ UNTIL_FOCUSED = 2
+ """Flash until focus is gained."""
+
+
+class _TempSurface:
+ """Holds a temporary surface derived from a NumPy array."""
+
+ def __init__(self, pixels: ArrayLike) -> None:
+ self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8)
+ if len(self._array.shape) != 3:
+ msg = f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})"
+ raise TypeError(msg)
+ if not (3 <= self._array.shape[2] <= 4):
+ msg = f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})"
+ raise TypeError(msg)
+ self.p = ffi.gc(
+ lib.SDL_CreateRGBSurfaceFrom(
+ ffi.from_buffer("void*", self._array),
+ self._array.shape[1], # Width.
+ self._array.shape[0], # Height.
+ self._array.shape[2] * 8, # Bit depth.
+ self._array.strides[1], # Pitch.
+ 0x000000FF,
+ 0x0000FF00,
+ 0x00FF0000,
+ 0xFF000000 if self._array.shape[2] == 4 else 0,
+ ),
+ lib.SDL_FreeSurface,
+ )
+
+
+class Window:
+ """An SDL2 Window object."""
+
+ def __init__(self, sdl_window_p: Any) -> None:
+ if ffi.typeof(sdl_window_p) is not ffi.typeof("struct SDL_Window*"):
+ msg = "sdl_window_p must be {!r} type (was {!r}).".format(
+ ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p)
+ )
+ raise TypeError(msg)
+ if not sdl_window_p:
+ msg = "sdl_window_p can not be a null pointer."
+ raise TypeError(msg)
+ self.p = sdl_window_p
+
+ def __eq__(self, other: Any) -> bool:
+ return bool(self.p == other.p)
+
+ def set_icon(self, pixels: ArrayLike) -> None:
+ """Set the window icon from an image.
+
+ Args:
+ pixels: A row-major array of RGB or RGBA pixel values.
+ """
+ surface = _TempSurface(pixels)
+ lib.SDL_SetWindowIcon(self.p, surface.p)
+
+ @property
+ def position(self) -> tuple[int, int]:
+ """Get or set the (x, y) position of the window.
+
+ This attribute can be set the move the window.
+ The constants tcod.lib.SDL_WINDOWPOS_CENTERED or tcod.lib.SDL_WINDOWPOS_UNDEFINED may be used.
+ """
+ xy = ffi.new("int[2]")
+ lib.SDL_GetWindowPosition(self.p, xy, xy + 1)
+ return xy[0], xy[1]
+
+ @position.setter
+ def position(self, xy: tuple[int, int]) -> None:
+ x, y = xy
+ lib.SDL_SetWindowPosition(self.p, x, y)
+
+ @property
+ def size(self) -> tuple[int, int]:
+ """Get or set the pixel (width, height) of the window client area.
+
+ This attribute can be set to change the size of the window but the given size must be greater than (1, 1) or
+ else ValueError will be raised.
+ """
+ xy = ffi.new("int[2]")
+ lib.SDL_GetWindowSize(self.p, xy, xy + 1)
+ return xy[0], xy[1]
+
+ @size.setter
+ def size(self, xy: tuple[int, int]) -> None:
+ if any(i <= 0 for i in xy):
+ msg = f"Window size must be greater than zero, not {xy}"
+ raise ValueError(msg)
+ x, y = xy
+ lib.SDL_SetWindowSize(self.p, x, y)
+
+ @property
+ def min_size(self) -> tuple[int, int]:
+ """Get or set this windows minimum client area."""
+ xy = ffi.new("int[2]")
+ lib.SDL_GetWindowMinimumSize(self.p, xy, xy + 1)
+ return xy[0], xy[1]
+
+ @min_size.setter
+ def min_size(self, xy: tuple[int, int]) -> None:
+ lib.SDL_SetWindowMinimumSize(self.p, xy[0], xy[1])
+
+ @property
+ def max_size(self) -> tuple[int, int]:
+ """Get or set this windows maximum client area."""
+ xy = ffi.new("int[2]")
+ lib.SDL_GetWindowMaximumSize(self.p, xy, xy + 1)
+ return xy[0], xy[1]
+
+ @max_size.setter
+ def max_size(self, xy: tuple[int, int]) -> None:
+ lib.SDL_SetWindowMaximumSize(self.p, xy[0], xy[1])
+
+ @property
+ def title(self) -> str:
+ """Get or set the title of the window."""
+ return str(ffi.string(lib.SDL_GetWindowTitle(self.p)), encoding="utf-8")
+
+ @title.setter
+ def title(self, value: str) -> None:
+ lib.SDL_SetWindowTitle(self.p, value.encode("utf-8"))
+
+ @property
+ def flags(self) -> WindowFlags:
+ """The current flags of this window, read-only."""
+ return WindowFlags(lib.SDL_GetWindowFlags(self.p))
+
+ @property
+ def fullscreen(self) -> int:
+ """Get or set the fullscreen status of this window.
+
+ Can be set to the :any:`WindowFlags.FULLSCREEN` or :any:`WindowFlags.FULLSCREEN_DESKTOP` flags.
+
+ Example::
+
+ # Toggle fullscreen.
+ window: tcod.sdl.video.Window
+ if window.fullscreen:
+ window.fullscreen = False # Set windowed mode.
+ else:
+ window.fullscreen = tcod.sdl.video.WindowFlags.FULLSCREEN_DESKTOP
+ """
+ return self.flags & (WindowFlags.FULLSCREEN | WindowFlags.FULLSCREEN_DESKTOP)
+
+ @fullscreen.setter
+ def fullscreen(self, value: int) -> None:
+ _check(lib.SDL_SetWindowFullscreen(self.p, value))
+
+ @property
+ def resizable(self) -> bool:
+ """Get or set if this window can be resized."""
+ return bool(self.flags & WindowFlags.RESIZABLE)
+
+ @resizable.setter
+ def resizable(self, value: bool) -> None:
+ lib.SDL_SetWindowResizable(self.p, value)
+
+ @property
+ def border_size(self) -> tuple[int, int, int, int]:
+ """Get the (top, left, bottom, right) size of the window decorations around the client area.
+
+ If this fails or the window doesn't have decorations yet then the value will be (0, 0, 0, 0).
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_GetWindowBordersSize
+ """
+ borders = ffi.new("int[4]")
+ # The return code is ignored.
+ _ = lib.SDL_GetWindowBordersSize(self.p, borders, borders + 1, borders + 2, borders + 3)
+ return borders[0], borders[1], borders[2], borders[3]
+
+ @property
+ def opacity(self) -> float:
+ """Get or set this windows opacity. 0.0 is fully transparent and 1.0 is fully opaque.
+
+ Will error if you try to set this and opacity isn't supported.
+ """
+ out = ffi.new("float*")
+ _check(lib.SDL_GetWindowOpacity(self.p, out))
+ return float(out[0])
+
+ @opacity.setter
+ def opacity(self, value: float) -> None:
+ _check(lib.SDL_SetWindowOpacity(self.p, value))
+
+ @property
+ def grab(self) -> bool:
+ """Get or set this windows input grab mode.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL_SetWindowGrab
+ """
+ return bool(lib.SDL_GetWindowGrab(self.p))
+
+ @grab.setter
+ def grab(self, value: bool) -> None:
+ lib.SDL_SetWindowGrab(self.p, value)
+
+ @property
+ def mouse_rect(self) -> tuple[int, int, int, int] | None:
+ """Get or set the mouse confinement area when the window has mouse focus.
+
+ Setting this will not automatically grab the cursor.
+
+ .. versionadded:: 13.5
+ """
+ _version_at_least((2, 0, 18))
+ rect = lib.SDL_GetWindowMouseRect(self.p)
+ return (rect.x, rect.y, rect.w, rect.h) if rect else None
+
+ @mouse_rect.setter
+ def mouse_rect(self, rect: tuple[int, int, int, int] | None) -> None:
+ _version_at_least((2, 0, 18))
+ _check(lib.SDL_SetWindowMouseRect(self.p, (rect,) if rect else ffi.NULL))
+
+ @_required_version((2, 0, 16))
+ def flash(self, operation: FlashOperation = FlashOperation.UNTIL_FOCUSED) -> None:
+ """Get the users attention."""
+ _check(lib.SDL_FlashWindow(self.p, operation))
+
+ def raise_window(self) -> None:
+ """Raise the window and set input focus."""
+ lib.SDL_RaiseWindow(self.p)
+
+ def restore(self) -> None:
+ """Restore a minimized or maximized window to its original size and position."""
+ lib.SDL_RestoreWindow(self.p)
+
+ def maximize(self) -> None:
+ """Make the window as big as possible."""
+ lib.SDL_MaximizeWindow(self.p)
+
+ def minimize(self) -> None:
+ """Minimize the window to an iconic state."""
+ lib.SDL_MinimizeWindow(self.p)
+
+ def show(self) -> None:
+ """Show this window."""
+ lib.SDL_ShowWindow(self.p)
+
+ def hide(self) -> None:
+ """Hide this window."""
+ lib.SDL_HideWindow(self.p)
+
+
+def new_window(
+ width: int,
+ height: int,
+ *,
+ x: int | None = None,
+ y: int | None = None,
+ title: str | None = None,
+ flags: int = 0,
+) -> Window:
+ """Initialize and return a new SDL Window.
+
+ Args:
+ width: The requested pixel width of the window.
+ height: The requested pixel height of the window.
+ x: The left-most position of the window.
+ y: The top-most position of the window.
+ title: The title text of the new window. If no option is given then `sys.arg[0]` will be used as the title.
+ flags: The SDL flags to use for this window, such as `tcod.sdl.video.WindowFlags.RESIZABLE`.
+ See :any:`WindowFlags` for more options.
+
+ Example::
+
+ import tcod.sdl.video
+ # Create a new resizable window with a custom title.
+ window = tcod.sdl.video.new_window(640, 480, title="Title bar text", flags=tcod.sdl.video.WindowFlags.RESIZABLE)
+
+ .. seealso::
+ :func:`tcod.sdl.render.new_renderer`
+ """
+ x = x if x is not None else int(lib.SDL_WINDOWPOS_UNDEFINED)
+ y = y if y is not None else int(lib.SDL_WINDOWPOS_UNDEFINED)
+ if title is None:
+ title = sys.argv[0]
+ window_p = ffi.gc(lib.SDL_CreateWindow(title.encode("utf-8"), x, y, width, height, flags), lib.SDL_DestroyWindow)
+ return Window(_check_p(window_p))
+
+
+def get_grabbed_window() -> Window | None:
+ """Return the window which has input grab enabled, if any."""
+ sdl_window_p = lib.SDL_GetGrabbedWindow()
+ return Window(sdl_window_p) if sdl_window_p else None
+
+
+def screen_saver_allowed(allow: bool | None = None) -> bool:
+ """Allow or prevent a screen saver from being displayed and return the current allowed status.
+
+ If `allow` is `None` then only the current state is returned.
+ Otherwise it will change the state before checking it.
+
+ SDL typically disables the screensaver by default.
+ If you're unsure, then don't touch this.
+
+ Example::
+
+ import tcod.sdl.video
+
+ print(f"Screen saver was allowed: {tcod.sdl.video.screen_saver_allowed()}")
+ # Allow the screen saver.
+ # Might be okay for some turn-based games which don't use a gamepad.
+ tcod.sdl.video.screen_saver_allowed(True)
+ """
+ if allow is None:
+ pass
+ elif allow:
+ lib.SDL_EnableScreenSaver()
+ else:
+ lib.SDL_DisableScreenSaver()
+ return bool(lib.SDL_IsScreenSaverEnabled())
diff --git a/tcod/tileset.py b/tcod/tileset.py
index 98ea4520..9f13f691 100644
--- a/tcod/tileset.py
+++ b/tcod/tileset.py
@@ -13,9 +13,9 @@
from __future__ import annotations
import itertools
-import os
+from os import PathLike
from pathlib import Path
-from typing import Any, Iterable, Optional, Tuple, Union
+from typing import Any, Iterable
import numpy as np
from numpy.typing import ArrayLike, NDArray
@@ -40,25 +40,32 @@ def __init__(self, tile_width: int, tile_height: int) -> None:
@classmethod
def _claim(cls, cdata: Any) -> Tileset:
"""Return a new Tileset that owns the provided TCOD_Tileset* object."""
- self: Tileset = object.__new__(cls)
+ self = object.__new__(cls)
if cdata == ffi.NULL:
- raise RuntimeError("Tileset initialized with nullptr.")
+ msg = "Tileset initialized with nullptr."
+ raise RuntimeError(msg)
self._tileset_p = ffi.gc(cdata, lib.TCOD_tileset_delete)
return self
+ @classmethod
+ def _from_ref(cls, tileset_p: Any) -> Tileset:
+ self = object.__new__(cls)
+ self._tileset_p = tileset_p
+ return self
+
@property
def tile_width(self) -> int:
- """The width of the tile in pixels."""
+ """Width of the tile in pixels."""
return int(lib.TCOD_tileset_get_tile_width_(self._tileset_p))
@property
def tile_height(self) -> int:
- """The height of the tile in pixels."""
+ """Height of the tile in pixels."""
return int(lib.TCOD_tileset_get_tile_height_(self._tileset_p))
@property
- def tile_shape(self) -> Tuple[int, int]:
- """The shape (height, width) of the tile in pixels."""
+ def tile_shape(self) -> tuple[int, int]:
+ """Shape (height, width) of the tile in pixels."""
return self.tile_height, self.tile_width
def __contains__(self, codepoint: int) -> bool:
@@ -74,7 +81,7 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]:
uint8. Note that most grey-scale tiles will only use the alpha
channel and will usually have a solid white color channel.
"""
- tile = np.zeros(self.tile_shape + (4,), dtype=np.uint8)
+ tile: NDArray[np.uint8] = np.zeros((*self.tile_shape, 4), dtype=np.uint8)
lib.TCOD_tileset_get_tile_(
self._tileset_p,
codepoint,
@@ -82,7 +89,7 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]:
)
return tile
- def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> None:
+ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
"""Upload a tile into this array.
Args:
@@ -133,11 +140,11 @@ def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) ->
"""
tile = np.ascontiguousarray(tile, dtype=np.uint8)
if tile.shape == self.tile_shape:
- full_tile = np.empty(self.tile_shape + (4,), dtype=np.uint8)
+ full_tile: NDArray[np.uint8] = np.empty((*self.tile_shape, 4), dtype=np.uint8)
full_tile[:, :, :3] = 255
full_tile[:, :, 3] = tile
return self.set_tile(codepoint, full_tile)
- required = self.tile_shape + (4,)
+ required = (*self.tile_shape, 4)
if tile.shape != required:
note = ""
if len(tile.shape) == 3 and tile.shape[2] == 3:
@@ -145,12 +152,14 @@ def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) ->
"\nNote: An RGB array is too ambiguous,"
" an alpha channel must be added to this array to divide the background/foreground areas."
)
- raise ValueError(f"Tile shape must be {required} or {self.tile_shape}, got {tile.shape}.{note}")
+ msg = f"Tile shape must be {required} or {self.tile_shape}, got {tile.shape}.{note}"
+ raise ValueError(msg)
lib.TCOD_tileset_set_tile_(
self._tileset_p,
codepoint,
ffi.from_buffer("struct TCOD_ColorRGBA*", tile),
)
+ return None
def render(self, console: tcod.console.Console) -> NDArray[np.uint8]:
"""Render an RGBA array, using console with this tileset.
@@ -164,10 +173,11 @@ def render(self, console: tcod.console.Console) -> NDArray[np.uint8]:
.. versionadded:: 11.9
"""
if not console:
- raise ValueError("'console' must not be the root console.")
+ msg = "'console' must not be the root console."
+ raise ValueError(msg)
width = console.width * self.tile_width
height = console.height * self.tile_height
- out = np.empty((height, width, 4), np.uint8)
+ out: NDArray[np.uint8] = np.empty((height, width, 4), np.uint8)
out[:] = 9
surface_p = ffi.gc(
lib.SDL_CreateRGBSurfaceWithFormatFrom(
@@ -180,16 +190,15 @@ def render(self, console: tcod.console.Console) -> NDArray[np.uint8]:
),
lib.SDL_FreeSurface,
)
- with surface_p:
- with ffi.new("SDL_Surface**", surface_p) as surface_p_p:
- _check(
- lib.TCOD_tileset_render_to_surface(
- self._tileset_p,
- _console(console),
- ffi.NULL,
- surface_p_p,
- )
+ with surface_p, ffi.new("SDL_Surface**", surface_p) as surface_p_p:
+ _check(
+ lib.TCOD_tileset_render_to_surface(
+ self._tileset_p,
+ _console(console),
+ ffi.NULL,
+ surface_p_p,
)
+ )
return out
def remap(self, codepoint: int, x: int, y: int = 0) -> None:
@@ -250,7 +259,7 @@ def set_default(tileset: Tileset) -> None:
lib.TCOD_set_default_tileset(tileset._tileset_p)
-def load_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int) -> Tileset:
+def load_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: int) -> Tileset:
"""Return a new Tileset from a `.ttf` or `.otf` file.
Same as :any:`set_truetype_font`, but returns a :any:`Tileset` instead.
@@ -258,16 +267,15 @@ def load_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int
This function is provisional. The API may change.
"""
- if not os.path.exists(path):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),))
- cdata = lib.TCOD_load_truetype_font_(str(path).encode(), tile_width, tile_height)
+ path = Path(path).resolve(strict=True)
+ cdata = lib.TCOD_load_truetype_font_(bytes(path), tile_width, tile_height)
if not cdata:
raise RuntimeError(ffi.string(lib.TCOD_get_error()))
return Tileset._claim(cdata)
@deprecate("Accessing the default tileset is deprecated.")
-def set_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int) -> None:
+def set_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: int) -> None:
"""Set the default tileset from a `.ttf` or `.otf` file.
`path` is the file path for the font file.
@@ -287,13 +295,12 @@ def set_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int)
This function does not support contexts.
Use :any:`load_truetype_font` instead.
"""
- if not os.path.exists(path):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),))
- if lib.TCOD_tileset_load_truetype_(str(path).encode(), tile_width, tile_height):
+ path = Path(path).resolve(strict=True)
+ if lib.TCOD_tileset_load_truetype_(bytes(path), tile_width, tile_height):
raise RuntimeError(ffi.string(lib.TCOD_get_error()))
-def load_bdf(path: Union[str, Path]) -> Tileset:
+def load_bdf(path: str | PathLike[str]) -> Tileset:
"""Return a new Tileset from a `.bdf` file.
For the best results the font should be monospace, cell-based, and
@@ -305,16 +312,15 @@ def load_bdf(path: Union[str, Path]) -> Tileset:
take effect when `tcod.console_init_root` is called.
.. versionadded:: 11.10
- """ # noqa: E501
- if not os.path.exists(path):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),))
- cdata = lib.TCOD_load_bdf(str(path).encode())
+ """
+ path = Path(path).resolve(strict=True)
+ cdata = lib.TCOD_load_bdf(bytes(path))
if not cdata:
raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode())
return Tileset._claim(cdata)
-def load_tilesheet(path: Union[str, Path], columns: int, rows: int, charmap: Optional[Iterable[int]]) -> Tileset:
+def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap: Iterable[int] | None) -> Tileset:
"""Return a new Tileset from a simple tilesheet image.
`path` is the file path to a PNG file with the tileset.
@@ -333,19 +339,18 @@ def load_tilesheet(path: Union[str, Path], columns: int, rows: int, charmap: Opt
.. versionadded:: 11.12
"""
- if not os.path.exists(path):
- raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),))
+ path = Path(path).resolve(strict=True)
mapping = []
if charmap is not None:
mapping = list(itertools.islice(charmap, columns * rows))
- cdata = lib.TCOD_tileset_load(str(path).encode(), columns, rows, len(mapping), mapping)
+ cdata = lib.TCOD_tileset_load(bytes(path), columns, rows, len(mapping), mapping)
if not cdata:
_raise_tcod_error()
return Tileset._claim(cdata)
def procedural_block_elements(*, tileset: Tileset) -> None:
- """Overwrites the block element codepoints in `tileset` with prodecually generated glyphs.
+ """Overwrite the block element codepoints in `tileset` with procedurally generated glyphs.
Args:
tileset (Tileset): A :any:`Tileset` with tiles of any shape.
@@ -415,7 +420,8 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
(0x259E, 0b0110), # "▞" Quadrant upper right and lower left.
(0x259F, 0b0111), # "▟" Quadrant upper right and lower left and lower right.
):
- alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) * 255
+ alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8)
+ alpha *= 255
tileset.set_tile(codepoint, alpha)
for codepoint, axis, fraction, negative in (
@@ -571,7 +577,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
0x007C,
0x007D,
0x007E,
- 0x007F,
+ 0x2302,
0x00C7,
0x00FC,
0x00E9,
@@ -706,6 +712,9 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
See :ref:`code-page-437` for more info and a table of glyphs.
.. versionadded:: 11.12
+
+.. versionchanged:: 14.0
+ Character at index ``0x7F`` was changed from value ``0x7F`` to the HOUSE ``⌂`` glyph ``0x2302``.
"""
CHARMAP_TCOD = [
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..38bb211b
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Test package."""
diff --git a/tests/conftest.py b/tests/conftest.py
index da22cd44..163d0918 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,18 +1,21 @@
+"""Test directory configuration."""
import random
import warnings
-from typing import Any, Callable, Iterator, Union
+from typing import Callable, Iterator, Union
import pytest
import tcod
+# ruff: noqa: D103
-def pytest_addoption(parser: Any) -> None:
+
+def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--no-window", action="store_true", help="Skip tests which need a rendering context.")
@pytest.fixture(scope="session", params=["SDL", "SDL2"])
-def session_console(request: Any) -> Iterator[tcod.console.Console]:
+def session_console(request: pytest.FixtureRequest) -> Iterator[tcod.console.Console]:
if request.config.getoption("--no-window"):
pytest.skip("This test needs a rendering context.")
FONT_FILE = "libtcod/terminal.png"
@@ -27,16 +30,16 @@ def session_console(request: Any) -> Iterator[tcod.console.Console]:
yield con
-@pytest.fixture(scope="function")
+@pytest.fixture()
def console(session_console: tcod.console.Console) -> tcod.console.Console:
console = session_console
tcod.console_flush()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
- console.default_fg = (255, 255, 255) # type: ignore
- console.default_bg = (0, 0, 0) # type: ignore
- console.default_bg_blend = tcod.BKGND_SET # type: ignore
- console.default_alignment = tcod.LEFT # type: ignore
+ console.default_fg = (255, 255, 255)
+ console.default_bg = (0, 0, 0)
+ console.default_bg_blend = tcod.BKGND_SET
+ console.default_alignment = tcod.LEFT
console.clear()
return console
@@ -81,6 +84,6 @@ def ch_latin1_str() -> str:
"latin1_str",
]
)
-def ch(request: Any) -> Callable[[], Union[int, str]]:
- """Test with multiple types of ascii/latin1 characters"""
+def ch(request: pytest.FixtureRequest) -> Callable[[], Union[int, str]]:
+ """Test with multiple types of ascii/latin1 characters."""
return globals()["ch_%s" % request.param]() # type: ignore
diff --git a/tests/data/README.md b/tests/data/README.md
new file mode 100644
index 00000000..8a7a10d7
--- /dev/null
+++ b/tests/data/README.md
@@ -0,0 +1,3 @@
+Data files for tests, such as old pickle streams.
+
+Remember to add new file types to `MANIFEST.in`.
diff --git a/tests/data/random_v13.pkl b/tests/data/random_v13.pkl
new file mode 100644
index 00000000..03fb7bf2
Binary files /dev/null and b/tests/data/random_v13.pkl differ
diff --git a/tests/test_console.py b/tests/test_console.py
index e4f15ef7..f8fa5d91 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -1,26 +1,29 @@
+"""Tests for tcod.console."""
import pickle
from pathlib import Path
import numpy as np
import pytest
-from numpy import array
import tcod
+import tcod.console
+
+# ruff: noqa: D103
-@pytest.mark.filterwarnings("ignore:Directly access a consoles")
-@pytest.mark.filterwarnings("ignore:This function may be deprecated in the fu")
def test_array_read_write() -> None:
console = tcod.console.Console(width=12, height=10)
FG = (255, 254, 253)
BG = (1, 2, 3)
CH = ord("&")
- tcod.console_put_char_ex(console, 0, 0, CH, FG, BG)
+ with pytest.warns():
+ tcod.console_put_char_ex(console, 0, 0, CH, FG, BG)
assert console.ch[0, 0] == CH
assert tuple(console.fg[0, 0]) == FG
assert tuple(console.bg[0, 0]) == BG
- tcod.console_put_char_ex(console, 1, 2, CH, FG, BG)
+ with pytest.warns():
+ tcod.console_put_char_ex(console, 1, 2, CH, FG, BG)
assert console.ch[2, 1] == CH
assert tuple(console.fg[2, 1]) == FG
assert tuple(console.bg[2, 1]) == BG
@@ -35,25 +38,28 @@ def test_array_read_write() -> None:
console.fg[1, ::2] = FG
console.bg[...] = BG
- assert tcod.console_get_char(console, 2, 1) == CH
- assert tuple(tcod.console_get_char_foreground(console, 2, 1)) == FG
- assert tuple(tcod.console_get_char_background(console, 2, 1)) == BG
+ with pytest.warns():
+ assert tcod.console_get_char(console, 2, 1) == CH
+ with pytest.warns():
+ assert tuple(tcod.console_get_char_foreground(console, 2, 1)) == FG
+ with pytest.warns():
+ assert tuple(tcod.console_get_char_background(console, 2, 1)) == BG
@pytest.mark.filterwarnings("ignore")
def test_console_defaults() -> None:
console = tcod.console.Console(width=12, height=10)
- console.default_bg = [2, 3, 4] # type: ignore
+ console.default_bg = [2, 3, 4] # type: ignore[assignment]
assert console.default_bg == (2, 3, 4)
- console.default_fg = (4, 5, 6) # type: ignore
+ console.default_fg = (4, 5, 6)
assert console.default_fg == (4, 5, 6)
- console.default_bg_blend = tcod.BKGND_ADD # type: ignore
+ console.default_bg_blend = tcod.BKGND_ADD
assert console.default_bg_blend == tcod.BKGND_ADD
- console.default_alignment = tcod.RIGHT # type: ignore
+ console.default_alignment = tcod.RIGHT
assert console.default_alignment == tcod.RIGHT
@@ -95,15 +101,16 @@ def test_console_pickle_fortran() -> None:
def test_console_repr() -> None:
- array # Needed for eval.
+ from numpy import array # noqa: F401 # Used for eval
+
eval(repr(tcod.console.Console(10, 2)))
-@pytest.mark.filterwarnings("ignore")
def test_console_str() -> None:
console = tcod.console.Console(10, 2)
console.ch[:] = ord(".")
- console.print_(0, 0, "Test")
+ with pytest.warns():
+ console.print_(0, 0, "Test")
assert str(console) == ("")
@@ -134,7 +141,7 @@ def test_console_semigraphics() -> None:
def test_rexpaint(tmp_path: Path) -> None:
xp_path = tmp_path / "test.xp"
- consoles = tcod.Console(80, 24, order="F"), tcod.Console(8, 8, order="F")
+ consoles = tcod.console.Console(80, 24, order="F"), tcod.console.Console(8, 8, order="F")
tcod.console.save_xp(xp_path, consoles, compress_level=0)
loaded = tcod.console.load_xp(xp_path, order="F")
assert len(consoles) == len(loaded)
@@ -146,7 +153,7 @@ def test_rexpaint(tmp_path: Path) -> None:
def test_draw_frame() -> None:
- console = tcod.Console(3, 3, order="C")
+ console = tcod.console.Console(3, 3, order="C")
with pytest.raises(TypeError):
console.draw_frame(0, 0, 3, 3, title="test", decoration="123456789")
with pytest.raises(TypeError):
@@ -154,4 +161,5 @@ def test_draw_frame() -> None:
console.draw_frame(0, 0, 3, 3, decoration=(49, 50, 51, 52, 53, 54, 55, 56, 57))
assert console.ch.tolist() == [[49, 50, 51], [52, 53, 54], [55, 56, 57]]
- console.draw_frame(0, 0, 3, 3, title="T")
+ with pytest.warns():
+ console.draw_frame(0, 0, 3, 3, title="T")
diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py
new file mode 100644
index 00000000..9d871ba7
--- /dev/null
+++ b/tests/test_deprecated.py
@@ -0,0 +1,38 @@
+"""Test deprecated features."""
+from __future__ import annotations
+
+import numpy as np
+import pytest
+
+import tcod
+import tcod.event
+import tcod.libtcodpy
+
+with pytest.warns():
+ import libtcodpy
+
+# ruff: noqa: D103
+
+
+@pytest.mark.filterwarnings("error")
+def test_deprecate_color() -> None:
+ with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"):
+ _ = tcod.black
+ with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"):
+ _ = tcod.libtcodpy.black
+ with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"):
+ _ = libtcodpy.black
+
+
+@pytest.mark.filterwarnings("error")
+def test_deprecate_key_constants() -> None:
+ with pytest.raises(FutureWarning, match=r".*KeySym.N1"):
+ _ = tcod.event.K_1
+ with pytest.raises(FutureWarning, match=r".*Scancode.N1"):
+ _ = tcod.event.SCANCODE_1
+
+
+def test_line_where() -> None:
+ with pytest.warns():
+ where = tcod.libtcodpy.line_where(1, 0, 3, 4)
+ np.testing.assert_array_equal(where, [[1, 1, 2, 2, 3], [0, 1, 2, 3, 4]])
diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py
index 5db89034..5803c625 100644
--- a/tests/test_libtcodpy.py
+++ b/tests/test_libtcodpy.py
@@ -1,14 +1,16 @@
-#!/usr/bin/env python
-
+"""Tests for the libtcodpy API."""
+from pathlib import Path
from typing import Any, Callable, Iterator, List, Optional, Tuple, Union
-import numpy
import numpy as np
import pytest
+from numpy.typing import NDArray
import tcod
import tcod as libtcodpy
+# ruff: noqa: D103
+
pytestmark = [
pytest.mark.filterwarnings("ignore::DeprecationWarning"),
pytest.mark.filterwarnings("ignore::PendingDeprecationWarning"),
@@ -30,7 +32,7 @@ def test_credits(console: tcod.console.Console) -> None:
libtcodpy.console_credits_reset()
-def assert_char(
+def assert_char( # noqa: PLR0913
console: tcod.console.Console,
x: int,
y: int,
@@ -144,20 +146,20 @@ def test_console_blit(console: tcod.console.Console, offscreen: tcod.console.Con
@pytest.mark.filterwarnings("ignore")
-def test_console_asc_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmpdir: Any) -> None:
+def test_console_asc_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmp_path: Path) -> None:
libtcodpy.console_print(console, 0, 0, "test")
- asc_file = tmpdir.join("test.asc").strpath
+ asc_file = tmp_path / "test.asc"
assert libtcodpy.console_save_asc(console, asc_file)
assert libtcodpy.console_load_asc(offscreen, asc_file)
assertConsolesEqual(console, offscreen)
@pytest.mark.filterwarnings("ignore")
-def test_console_apf_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmpdir: Any) -> None:
+def test_console_apf_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmp_path: Path) -> None:
libtcodpy.console_print(console, 0, 0, "test")
- apf_file = tmpdir.join("test.apf").strpath
+ apf_file = tmp_path / "test.apf"
assert libtcodpy.console_save_apf(console, apf_file)
assert libtcodpy.console_load_apf(offscreen, apf_file)
assertConsolesEqual(console, offscreen)
@@ -177,14 +179,14 @@ def test_console_rexpaint_load_test_file(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
def test_console_rexpaint_save_load(
console: tcod.console.Console,
- tmpdir: Any,
+ tmp_path: Path,
ch: int,
fg: Tuple[int, int, int],
bg: Tuple[int, int, int],
) -> None:
libtcodpy.console_print(console, 0, 0, "test")
libtcodpy.console_put_char_ex(console, 1, 1, ch, fg, bg)
- xp_file = tmpdir.join("test.xp").strpath
+ xp_file = tmp_path / "test.xp"
assert libtcodpy.console_save_xp(console, xp_file, 1)
xp_console = libtcodpy.console_from_xp(xp_file)
assert xp_console
@@ -194,12 +196,12 @@ def test_console_rexpaint_save_load(
@pytest.mark.filterwarnings("ignore")
-def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmpdir: Any) -> None:
+def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmp_path: Path) -> None:
con1 = libtcodpy.console_new(8, 2)
con2 = libtcodpy.console_new(8, 2)
libtcodpy.console_print(con1, 0, 0, "hello")
libtcodpy.console_print(con2, 0, 0, "world")
- xp_file = tmpdir.join("test.xp").strpath
+ xp_file = tmp_path / "test.xp"
assert libtcodpy.console_list_save_xp([con1, con2], xp_file, 1)
loaded_consoles = libtcodpy.console_list_load_xp(xp_file)
assert loaded_consoles
@@ -253,7 +255,7 @@ def test_console_fill(console: tcod.console.Console) -> None:
def test_console_fill_numpy(console: tcod.console.Console) -> None:
width = libtcodpy.console_get_width(console)
height = libtcodpy.console_get_height(console)
- fill = numpy.zeros((height, width), dtype=numpy.intc)
+ fill: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc)
for y in range(height):
fill[y, :] = y % 256
@@ -262,9 +264,9 @@ def test_console_fill_numpy(console: tcod.console.Console) -> None:
libtcodpy.console_fill_char(console, fill) # type: ignore
# verify fill
- bg = numpy.zeros((height, width), dtype=numpy.intc)
- fg = numpy.zeros((height, width), dtype=numpy.intc)
- ch = numpy.zeros((height, width), dtype=numpy.intc)
+ bg: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc)
+ fg: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc)
+ ch: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc)
for y in range(height):
for x in range(width):
bg[y, x] = libtcodpy.console_get_char_background(console, x, y)[0]
@@ -292,7 +294,7 @@ def test_console_buffer(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore:Console array attributes perform better")
def test_console_buffer_error(console: tcod.console.Console) -> None:
buffer = libtcodpy.ConsoleBuffer(0, 0)
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r".*Destination console has an incorrect size."):
buffer.blit(console)
@@ -323,8 +325,8 @@ def test_sys_time(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_sys_screenshot(console: tcod.console.Console, tmpdir: Any) -> None:
- libtcodpy.sys_save_screenshot(tmpdir.join("test.png").strpath)
+def test_sys_screenshot(console: tcod.console.Console, tmp_path: Path) -> None:
+ libtcodpy.sys_save_screenshot(tmp_path / "test.png")
@pytest.mark.filterwarnings("ignore")
@@ -334,7 +336,7 @@ def test_sys_custom_render(console: tcod.console.Console) -> None:
escape = []
- def sdl_callback(sdl_surface: Any) -> None:
+ def sdl_callback(sdl_surface: object) -> None:
escape.append(True)
libtcodpy.sys_register_SDL_renderer(sdl_callback)
@@ -343,7 +345,7 @@ def sdl_callback(sdl_surface: Any) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_image(console: tcod.console.Console, tmpdir: Any) -> None:
+def test_image(console: tcod.console.Console, tmp_path: Path) -> None:
img = libtcodpy.image_new(16, 16)
libtcodpy.image_clear(img, (0, 0, 0))
libtcodpy.image_invert(img)
@@ -361,7 +363,7 @@ def test_image(console: tcod.console.Console, tmpdir: Any) -> None:
libtcodpy.image_blit(img, console, 0, 0, libtcodpy.BKGND_SET, 1, 1, 0)
libtcodpy.image_blit_rect(img, console, 0, 0, 16, 16, libtcodpy.BKGND_SET)
libtcodpy.image_blit_2x(img, console, 0, 0)
- libtcodpy.image_save(img, tmpdir.join("test.png").strpath)
+ libtcodpy.image_save(img, tmp_path / "test.png")
libtcodpy.image_delete(img)
img = libtcodpy.image_from_console(console)
@@ -386,14 +388,12 @@ def test_clipboard(console: tcod.console.Console, sample: str) -> None:
# arguments to test with and the results expected from these arguments
LINE_ARGS = (-5, 0, 5, 10)
EXCLUSIVE_RESULTS = [(-4, 1), (-3, 2), (-2, 3), (-1, 4), (0, 5), (1, 6), (2, 7), (3, 8), (4, 9), (5, 10)]
-INCLUSIVE_RESULTS = [(-5, 0)] + EXCLUSIVE_RESULTS
+INCLUSIVE_RESULTS = [(-5, 0), *EXCLUSIVE_RESULTS]
@pytest.mark.filterwarnings("ignore")
def test_line_step() -> None:
- """
- libtcodpy.line_init and libtcodpy.line_step
- """
+ """libtcodpy.line_init and libtcodpy.line_step."""
libtcodpy.line_init(*LINE_ARGS)
for expected_xy in EXCLUSIVE_RESULTS:
assert libtcodpy.line_step() == expected_xy
@@ -402,9 +402,7 @@ def test_line_step() -> None:
@pytest.mark.filterwarnings("ignore")
def test_line() -> None:
- """
- tests normal use, lazy evaluation, and error propagation
- """
+ """Tests normal use, lazy evaluation, and error propagation."""
# test normal results
test_result: List[Tuple[int, int]] = []
@@ -428,17 +426,13 @@ def return_false(x: int, y: int) -> bool:
@pytest.mark.filterwarnings("ignore")
def test_line_iter() -> None:
- """
- libtcodpy.line_iter
- """
+ """libtcodpy.line_iter."""
assert list(libtcodpy.line_iter(*LINE_ARGS)) == INCLUSIVE_RESULTS
@pytest.mark.filterwarnings("ignore")
def test_bsp() -> None:
- """
- commented out statements work in libtcod-cffi
- """
+ """Commented out statements work in libtcod-cffi."""
bsp = libtcodpy.bsp_new_with_size(0, 0, 64, 64)
repr(bsp) # test __repr__ on leaf
libtcodpy.bsp_resize(bsp, 0, 0, 32, 32)
@@ -480,7 +474,7 @@ def test_bsp() -> None:
libtcodpy.bsp_split_recursive(bsp, None, 4, 2, 2, 1.0, 1.0)
# cover bsp_traverse
- def traverse(node: tcod.bsp.BSP, user_data: Any) -> None:
+ def traverse(node: tcod.bsp.BSP, user_data: object) -> None:
return None
libtcodpy.bsp_traverse_pre_order(bsp, traverse)
@@ -499,9 +493,10 @@ def traverse(node: tcod.bsp.BSP, user_data: Any) -> None:
@pytest.mark.filterwarnings("ignore")
def test_map() -> None:
- map = libtcodpy.map_new(16, 16)
- assert libtcodpy.map_get_width(map) == 16
- assert libtcodpy.map_get_height(map) == 16
+ WIDTH, HEIGHT = 13, 17
+ map = libtcodpy.map_new(WIDTH, HEIGHT)
+ assert libtcodpy.map_get_width(map) == WIDTH
+ assert libtcodpy.map_get_height(map) == HEIGHT
libtcodpy.map_copy(map, map)
libtcodpy.map_clear(map)
libtcodpy.map_set_properties(map, 0, 0, True, True)
@@ -639,7 +634,7 @@ def test_heightmap() -> None:
libtcodpy.heightmap_delete(h_map)
-MAP = np.array(
+MAP: NDArray[Any] = np.array(
[
list(line)
for line in (
diff --git a/tests/test_noise.py b/tests/test_noise.py
index ed77dce6..0f64d5fe 100644
--- a/tests/test_noise.py
+++ b/tests/test_noise.py
@@ -1,10 +1,13 @@
+"""Tests for the tcod.noise module."""
import copy
import pickle
import numpy as np
import pytest
-import tcod
+import tcod.noise
+
+# ruff: noqa: D103
@pytest.mark.parametrize("implementation", tcod.noise.Implementation)
@@ -28,7 +31,7 @@ def test_noise_class(
octaves=octaves,
)
# cover attributes
- assert noise.dimensions == 2
+ assert noise.dimensions == 2 # noqa: PLR2004
noise.algorithm = noise.algorithm
noise.implementation = noise.implementation
noise.octaves = noise.octaves
@@ -40,31 +43,31 @@ def test_noise_class(
noise.sample_mgrid(np.mgrid[:2, :3])
noise.sample_ogrid(np.ogrid[:2, :3])
- np.testing.assert_equal( # type: ignore
+ np.testing.assert_equal(
noise.sample_mgrid(np.mgrid[:2, :3]),
noise.sample_ogrid(np.ogrid[:2, :3]),
)
- np.testing.assert_equal(noise.sample_mgrid(np.mgrid[:2, :3]), noise[tuple(np.mgrid[:2, :3])]) # type: ignore
+ np.testing.assert_equal(noise.sample_mgrid(np.mgrid[:2, :3]), noise[tuple(np.mgrid[:2, :3])])
repr(noise)
def test_noise_samples() -> None:
noise = tcod.noise.Noise(2, tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.SIMPLE)
- np.testing.assert_equal( # type: ignore
+ np.testing.assert_equal(
noise.sample_mgrid(np.mgrid[:32, :24]),
noise.sample_ogrid(np.ogrid[:32, :24]),
)
def test_noise_errors() -> None:
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"dimensions must be in range"):
tcod.noise.Noise(0)
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"-1 is not a valid implementation"):
tcod.noise.Noise(1, implementation=-1)
noise = tcod.noise.Noise(2)
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"mgrid.shape\[0\] must equal self.dimensions"):
noise.sample_mgrid(np.mgrid[:2, :2, :2])
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"len\(ogrid\) must equal self.dimensions"):
noise.sample_ogrid(np.ogrid[:2, :2, :2])
with pytest.raises(IndexError):
noise[0, 0, 0, 0, 0]
@@ -77,7 +80,7 @@ def test_noise_pickle(implementation: tcod.noise.Implementation) -> None:
rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42)
noise = tcod.noise.Noise(2, implementation, seed=rand)
noise2 = copy.copy(noise)
- np.testing.assert_equal( # type: ignore
+ np.testing.assert_equal(
noise.sample_ogrid(np.ogrid[:3, :1]),
noise2.sample_ogrid(np.ogrid[:3, :1]),
)
@@ -87,7 +90,7 @@ def test_noise_copy() -> None:
rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42)
noise = tcod.noise.Noise(2, seed=rand)
noise2 = pickle.loads(pickle.dumps(noise))
- np.testing.assert_equal( # type: ignore
+ np.testing.assert_equal(
noise.sample_ogrid(np.ogrid[:3, :1]),
noise2.sample_ogrid(np.ogrid[:3, :1]),
)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 2e0ef5ed..ce200166 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,12 +1,13 @@
-#!/usr/bin/env python
-
-import os
+"""Test old libtcodpy parser."""
+from pathlib import Path
from typing import Any
import pytest
import tcod as libtcod
+# ruff: noqa: D103
+
@pytest.mark.filterwarnings("ignore")
def test_parser() -> None:
@@ -29,7 +30,7 @@ def test_parser() -> None:
# default listener
print("***** Default listener *****")
- libtcod.parser_run(parser, os.path.join("libtcod", "data", "cfg", "sample.cfg"))
+ libtcod.parser_run(parser, Path("libtcod/data/cfg/sample.cfg"))
print("bool_field : ", libtcod.parser_get_bool_property(parser, "myStruct.bool_field"))
print("char_field : ", libtcod.parser_get_char_property(parser, "myStruct.char_field"))
print("int_field : ", libtcod.parser_get_int_property(parser, "myStruct.int_field"))
@@ -48,7 +49,7 @@ def test_parser() -> None:
print("***** Custom listener *****")
class MyListener:
- def new_struct(self, struct: Any, name: str) -> bool:
+ def new_struct(self, struct: Any, name: str) -> bool: # noqa: ANN401
print("new structure type", libtcod.struct_get_name(struct), " named ", name)
return True
@@ -56,7 +57,7 @@ def new_flag(self, name: str) -> bool:
print("new flag named ", name)
return True
- def new_property(self, name: str, typ: int, value: Any) -> bool:
+ def new_property(self, name: str, typ: int, value: Any) -> bool: # noqa: ANN401
type_names = ["NONE", "BOOL", "CHAR", "INT", "FLOAT", "STRING", "COLOR", "DICE"]
type_name = type_names[typ & 0xFF]
if typ & libtcod.TYPE_LIST:
@@ -64,7 +65,7 @@ def new_property(self, name: str, typ: int, value: Any) -> bool:
print("new property named ", name, " type ", type_name, " value ", value)
return True
- def end_struct(self, struct: Any, name: str) -> bool:
+ def end_struct(self, struct: Any, name: str) -> bool: # noqa: ANN401
print("end structure type", libtcod.struct_get_name(struct), " named ", name)
return True
@@ -72,7 +73,7 @@ def error(self, msg: str) -> bool:
print("error : ", msg)
return True
- libtcod.parser_run(parser, os.path.join("libtcod", "data", "cfg", "sample.cfg"), MyListener())
+ libtcod.parser_run(parser, Path("libtcod/data/cfg/sample.cfg"), MyListener())
if __name__ == "__main__":
diff --git a/tests/test_random.py b/tests/test_random.py
index e0a33a3a..e6b64e1e 100644
--- a/tests/test_random.py
+++ b/tests/test_random.py
@@ -1,13 +1,19 @@
+"""Test random number generators."""
import copy
import pickle
+from pathlib import Path
-import tcod
+import tcod.random
+
+# ruff: noqa: D103
+
+SCRIPT_DIR = Path(__file__).parent
def test_tcod_random() -> None:
rand = tcod.random.Random(tcod.random.COMPLEMENTARY_MULTIPLY_WITH_CARRY)
- assert 0 <= rand.randint(0, 100) <= 100
- assert 0 <= rand.uniform(0, 100) <= 100
+ assert 0 <= rand.randint(0, 100) <= 100 # noqa: PLR2004
+ assert 0 <= rand.uniform(0, 100) <= 100 # noqa: PLR2004
rand.guass(0, 1)
rand.inverse_guass(0, 1)
@@ -26,3 +32,9 @@ def test_tcod_random_pickle() -> None:
assert rand.uniform(0, 1) == rand2.uniform(0, 1)
assert rand.uniform(0, 1) == rand2.uniform(0, 1)
assert rand.uniform(0, 1) == rand2.uniform(0, 1)
+
+
+def test_load_rng_v13_1() -> None:
+ rand: tcod.random.Random = pickle.loads((SCRIPT_DIR / "data/random_v13.pkl").read_bytes())
+ assert rand.randint(0, 0xFFFF) == 56422 # noqa: PLR2004
+ assert rand.randint(0, 0xFFFF) == 15795 # noqa: PLR2004
diff --git a/tests/test_sdl.py b/tests/test_sdl.py
new file mode 100644
index 00000000..fa2ac29a
--- /dev/null
+++ b/tests/test_sdl.py
@@ -0,0 +1,83 @@
+"""Test SDL specific features."""
+import sys
+
+import numpy as np
+import pytest
+
+import tcod.sdl.render
+import tcod.sdl.sys
+import tcod.sdl.video
+
+# ruff: noqa: D103
+
+
+def test_sdl_window() -> None:
+ assert tcod.sdl.video.get_grabbed_window() is None
+ window = tcod.sdl.video.new_window(1, 1)
+ window.raise_window()
+ window.maximize()
+ window.restore()
+ window.minimize()
+ window.hide()
+ window.show()
+ assert window.title == sys.argv[0]
+ window.title = "Title"
+ assert window.title == "Title"
+ assert window.opacity == 1.0 # noqa: PLR2004
+ window.position = window.position
+ window.fullscreen = window.fullscreen
+ window.resizable = window.resizable
+ window.size = window.size
+ window.min_size = window.min_size
+ window.max_size = window.max_size
+ window.border_size # noqa: B018
+ window.set_icon(np.zeros((32, 32, 3), dtype=np.uint8))
+ with pytest.raises(TypeError):
+ window.set_icon(np.zeros((32, 32, 5), dtype=np.uint8))
+ with pytest.raises(TypeError):
+ window.set_icon(np.zeros((32, 32), dtype=np.uint8))
+ window.opacity = window.opacity
+ window.grab = window.grab
+
+
+def test_sdl_window_bad_types() -> None:
+ with pytest.raises(TypeError):
+ tcod.sdl.video.Window(tcod.ffi.cast("SDL_Window*", tcod.ffi.NULL))
+ with pytest.raises(TypeError):
+ tcod.sdl.video.Window(tcod.ffi.new("SDL_Rect*"))
+
+
+def test_sdl_screen_saver() -> None:
+ tcod.sdl.sys.init()
+ assert tcod.sdl.video.screen_saver_allowed(False) is False
+ assert tcod.sdl.video.screen_saver_allowed(True) is True
+ assert tcod.sdl.video.screen_saver_allowed() is True
+
+
+def test_sdl_render() -> None:
+ window = tcod.sdl.video.new_window(1, 1)
+ render = tcod.sdl.render.new_renderer(window, software=True, vsync=False, target_textures=True)
+ render.present()
+ rgb = render.upload_texture(np.zeros((8, 8, 3), np.uint8))
+ assert (rgb.width, rgb.height) == (8, 8)
+ assert rgb.access == tcod.sdl.render.TextureAccess.STATIC
+ assert rgb.format == tcod.lib.SDL_PIXELFORMAT_RGB24
+ rgb.alpha_mod = rgb.alpha_mod
+ rgb.blend_mode = rgb.blend_mode
+ rgb.color_mod = rgb.color_mod
+ rgba = render.upload_texture(np.zeros((8, 8, 4), np.uint8), access=tcod.sdl.render.TextureAccess.TARGET)
+ with render.set_render_target(rgba):
+ render.copy(rgb)
+ with pytest.raises(TypeError):
+ render.upload_texture(np.zeros((8, 8, 5), np.uint8))
+
+ assert (render.read_pixels() == (0, 0, 0, 255)).all()
+ assert (render.read_pixels(format="RGB") == (0, 0, 0)).all()
+ assert render.read_pixels(rect=(1, 2, 3, 4)).shape == (4, 3, 4)
+
+
+def test_sdl_render_bad_types() -> None:
+ with pytest.raises(TypeError):
+ tcod.sdl.render.Renderer(tcod.ffi.cast("SDL_Renderer*", tcod.ffi.NULL))
+ with pytest.raises(TypeError):
+ tcod.sdl.render.Renderer(tcod.ffi.new("SDL_Rect*"))
diff --git a/tests/test_sdl_audio.py b/tests/test_sdl_audio.py
new file mode 100644
index 00000000..6441bda1
--- /dev/null
+++ b/tests/test_sdl_audio.py
@@ -0,0 +1,137 @@
+"""Test tcod.sdl.audio module."""
+import contextlib
+import sys
+import time
+from typing import Any
+
+import numpy as np
+import pytest
+from numpy.typing import NDArray
+
+import tcod.sdl.audio
+
+# ruff: noqa: D103
+
+
+needs_audio_device = pytest.mark.xfail(
+ not list(tcod.sdl.audio.get_devices()), reason="This test requires an audio device"
+)
+needs_audio_capture = pytest.mark.xfail(
+ not list(tcod.sdl.audio.get_capture_devices()), reason="This test requires an audio capture device"
+)
+
+
+def test_devices() -> None:
+ list(tcod.sdl.audio.get_devices())
+ list(tcod.sdl.audio.get_capture_devices())
+
+
+@needs_audio_device
+def test_audio_device() -> None:
+ with tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True) as device:
+ assert not device.stopped
+ assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 # noqa: PLR2004
+ assert device.convert(np.zeros((4, 4), dtype=np.float32)).shape == (4, 2)
+ assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 # noqa: PLR2004
+ assert device.paused is True
+ device.paused = False
+ assert device.paused is False
+ device.paused = True
+ assert device.queued_samples == 0
+ with pytest.raises(TypeError):
+ device.callback # noqa: B018
+ with pytest.raises(TypeError):
+ device.callback = lambda _device, _stream: None
+ with contextlib.closing(tcod.sdl.audio.BasicMixer(device)) as mixer:
+ assert mixer.daemon
+ assert mixer.play(np.zeros(4, np.float32)).busy
+ mixer.play(np.zeros(0, np.float32))
+ mixer.play(np.full(1, 0.01, np.float32), on_end=lambda _: None)
+ mixer.play(np.full(1, 0.01, np.float32), loops=2, on_end=lambda _: None)
+ mixer.play(np.full(4, 0.01, np.float32), loops=2).stop()
+ mixer.play(np.full(100000, 0.01, np.float32))
+ with pytest.raises(TypeError, match=r".*must be dtype=float32.*was dtype=int32"):
+ mixer.play(np.zeros(1, np.int32))
+ time.sleep(0.001)
+ mixer.stop()
+
+
+@needs_audio_capture
+def test_audio_capture() -> None:
+ with tcod.sdl.audio.open(capture=True) as device:
+ assert not device.stopped
+ assert isinstance(device.dequeue_audio(), np.ndarray)
+
+
+@needs_audio_device
+def test_audio_device_repr() -> None:
+ with tcod.sdl.audio.open(format=np.uint16, paused=True, callback=True) as device:
+ assert not device.stopped
+ assert "silence=" in repr(device)
+ assert "callback=" in repr(device)
+ assert "stopped=" in repr(device)
+
+
+def test_convert_bad_shape() -> None:
+ with pytest.raises(TypeError):
+ tcod.sdl.audio.convert_audio(
+ np.zeros((1, 1, 1), np.float32), 8000, out_rate=8000, out_format=np.float32, out_channels=1
+ )
+
+
+def test_convert_bad_type() -> None:
+ with pytest.raises(TypeError, match=r".*bool"):
+ tcod.sdl.audio.convert_audio(np.zeros(8, bool), 8000, out_rate=8000, out_format=np.float32, out_channels=1)
+ with pytest.raises(RuntimeError, match=r"Invalid source format"):
+ tcod.sdl.audio.convert_audio(np.zeros(8, np.int64), 8000, out_rate=8000, out_format=np.float32, out_channels=1)
+
+
+def test_convert_float64() -> None:
+ np.testing.assert_array_equal(
+ tcod.sdl.audio.convert_audio(
+ np.ones(8, np.float64), 8000, out_rate=8000, out_format=np.float32, out_channels=1
+ ),
+ np.ones((8, 1), np.float32),
+ )
+
+
+@needs_audio_device
+def test_audio_callback() -> None:
+ class CheckCalled:
+ was_called: bool = False
+
+ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None:
+ self.was_called = True
+ assert isinstance(device, tcod.sdl.audio.AudioDevice)
+ assert isinstance(stream, np.ndarray)
+ assert len(stream.shape) == 2 # noqa: PLR2004
+
+ check_called = CheckCalled()
+ with tcod.sdl.audio.open(callback=check_called, paused=False) as device:
+ assert not device.stopped
+ device.callback = device.callback
+ while not check_called.was_called:
+ time.sleep(0.001)
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason="Needs sys.unraisablehook support")
+@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
+@needs_audio_device
+def test_audio_callback_unraisable() -> None:
+ """Test unraisable error in audio callback.
+
+ This can't be checked with pytest very well, so at least make sure this doesn't crash.
+ """
+
+ class CheckCalled:
+ was_called: bool = False
+
+ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None:
+ self.was_called = True
+ raise Exception("Test unraisable error") # noqa
+
+ check_called = CheckCalled()
+ with tcod.sdl.audio.open(callback=check_called, paused=False) as device:
+ assert not device.stopped
+ while not check_called.was_called:
+ time.sleep(0.001)
diff --git a/tests/test_tcod.py b/tests/test_tcod.py
index bfb6a36e..33e50195 100644
--- a/tests/test_tcod.py
+++ b/tests/test_tcod.py
@@ -1,34 +1,32 @@
-#!/usr/bin/env python
-
+"""Tests for newer tcod API."""
import copy
import pickle
from typing import Any, NoReturn
import numpy as np
import pytest
-from numpy.typing import DTypeLike
+from numpy.typing import DTypeLike, NDArray
import tcod
+import tcod.console
+
+# ruff: noqa: D103
-def raise_Exception(*args: Any) -> NoReturn:
- raise RuntimeError("testing exception")
+def raise_Exception(*args: object) -> NoReturn:
+ raise RuntimeError("testing exception") # noqa: TRY003, EM101
def test_line_error() -> None:
- """
- test exception propagation
- """
- with pytest.raises(RuntimeError):
+ """Test exception propagation."""
+ with pytest.raises(RuntimeError), pytest.warns():
tcod.line(0, 0, 10, 10, py_callback=raise_Exception)
@pytest.mark.filterwarnings("ignore:Iterate over nodes using")
@pytest.mark.filterwarnings("ignore:Use pre_order method instead of walk.")
def test_tcod_bsp() -> None:
- """
- test tcod additions to BSP
- """
+ """Test tcod additions to BSP."""
bsp = tcod.bsp.BSP(0, 0, 32, 32)
assert bsp.level == 0
@@ -47,7 +45,7 @@ def test_tcod_bsp() -> None:
# test that operations on deep BSP nodes preserve depth
sub_bsp = bsp.children[0]
sub_bsp.split_recursive(3, 2, 2, 1, 1)
- assert sub_bsp.children[0].level == 2
+ assert sub_bsp.children[0].level == 2 # noqa: PLR2004
# cover find_node method
assert bsp.find_node(0, 0)
@@ -114,34 +112,34 @@ def test_color_class() -> None:
assert tcod.white * 1 == tcod.white
assert tcod.white * tcod.black == tcod.black
assert tcod.white - tcod.white == tcod.black
- assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1)
+ assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1) # noqa: RUF005
color = tcod.Color()
- color.r = 1 # type: ignore
- color.g = 2 # type: ignore
- color.b = 3 # type: ignore
+ color.r = 1
+ color.g = 2
+ color.b = 3
assert color == (1, 2, 3)
@pytest.mark.parametrize("dtype", [np.int8, np.int16, np.int32, np.uint8, np.uint16, np.uint32, np.float32])
def test_path_numpy(dtype: DTypeLike) -> None:
- map_np = np.ones((6, 6), dtype=dtype)
+ map_np: NDArray[Any] = np.ones((6, 6), dtype=dtype)
map_np[1:4, 1:4] = 0
astar = tcod.path.AStar(map_np, 0)
astar = pickle.loads(pickle.dumps(astar)) # test pickle
astar = tcod.path.AStar(astar.cost, 0) # use existing cost attribute
- assert len(astar.get_path(0, 0, 5, 5)) == 10
+ assert len(astar.get_path(0, 0, 5, 5)) == 10 # noqa: PLR2004
dijkstra = tcod.path.Dijkstra(map_np, 0)
dijkstra.set_goal(0, 0)
- assert len(dijkstra.get_path(5, 5)) == 10
+ assert len(dijkstra.get_path(5, 5)) == 10 # noqa: PLR2004
repr(dijkstra) # cover __repr__ methods
# cover errors
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"Array must have a 2d shape, shape is \(3, 3, 3\)"):
tcod.path.AStar(np.ones((3, 3, 3), dtype=dtype))
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"dtype must be one of dict_keys"):
tcod.path.AStar(np.ones((2, 2), dtype=np.float64))
@@ -160,7 +158,7 @@ def test_key_repr() -> None:
Key = tcod.Key
key = Key(vk=1, c=2, shift=True)
assert key.vk == 1
- assert key.c == 2
+ assert key.c == 2 # noqa: PLR2004
assert key.shift
key_copy = eval(repr(key))
assert key.vk == key_copy.vk
@@ -193,10 +191,12 @@ def test_context() -> None:
pass
WIDTH, HEIGHT = 16, 4
with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=tcod.RENDERER_SDL2) as context:
- console = tcod.Console(*context.recommended_console_size())
+ console = tcod.console.Console(*context.recommended_console_size())
context.present(console)
- context.sdl_window_p
- context.renderer_type
+ assert context.sdl_window_p is not None
+ assert context.renderer_type >= 0
context.change_tileset(tcod.tileset.Tileset(16, 16))
context.pixel_to_tile(0, 0)
context.pixel_to_subtile(0, 0)
+ with pytest.raises(RuntimeError, match=".*context has been closed"):
+ context.present(console)
diff --git a/tests/test_testing.py b/tests/test_testing.py
deleted file mode 100644
index 027da622..00000000
--- a/tests/test_testing.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python
-
-import os
-
-curdir = os.path.dirname(__file__)
-
-FONT_FILE = os.path.join(curdir, "data/fonts/consolas10x10_gs_tc.png")
-
-# def test_console():
-# libtcod.console_set_custom_font(FONT_FILE, libtcod.FONT_LAYOUT_TCOD)
-# libtcod.console_init_root(40, 30, 'test', False, libtcod.RENDERER_SDL)
-# libtcod.console_flush()
diff --git a/tests/test_tileset.py b/tests/test_tileset.py
index 36a3de58..aba88b98 100644
--- a/tests/test_tileset.py
+++ b/tests/test_tileset.py
@@ -1,4 +1,7 @@
-import tcod
+"""Test for tcod.tileset module."""
+import tcod.tileset
+
+# ruff: noqa: D103
def test_proc_block_elements() -> None: