diff --git a/.gitattributes b/.gitattributes
index 740e182d..2dc9f280 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -19,6 +19,3 @@
# Custom for this project
*.bat eol=crlf
*.txt eol=crlf
-
-# Fix an issue with Python 2 on Linux
-tcod/libtcod_cdef.h eol=lf
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..2b2eb8c4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# Keep GitHub Actions up to date with GitHub's Dependabot...
+# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ groups:
+ github-actions:
+ patterns:
+ - "*" # Group all Actions updates into a single larger pull request
+ schedule:
+ interval: weekly
+ cooldown:
+ default-days: 7
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 28ef5efc..7896754a 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -4,9 +4,13 @@
name: Package
on:
+ workflow_dispatch:
push:
pull_request:
- types: [opened, reopened]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
defaults:
run:
@@ -19,8 +23,11 @@ env:
jobs:
ruff:
runs-on: ubuntu-latest
+ timeout-minutes: 5
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
- name: Install Ruff
run: pip install ruff
- name: Ruff Check
@@ -30,8 +37,11 @@ jobs:
mypy:
runs-on: ubuntu-latest
+ timeout-minutes: 5
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- name: Install typing dependencies
@@ -44,14 +54,16 @@ jobs:
sdist:
runs-on: ubuntu-latest
+ timeout-minutes: 5
steps:
- - uses: libsdl-org/setup-sdl@6574e20ac65ce362cd12f9c26b3a5e4d3cd31dee
+ - uses: libsdl-org/setup-sdl@1894460666e4fe0c6603ea0cce5733f1cca56ba1
with:
install-linux-dependencies: true
build-type: "Debug"
version: ${{ env.sdl-version }}
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
@@ -59,7 +71,7 @@ jobs:
run: pip install build
- name: Build source distribution
run: python -m build --sdist
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v7
with:
name: sdist
path: dist/tcod-*.tar.gz
@@ -70,18 +82,20 @@ jobs:
parse_sdl:
needs: [ruff, mypy]
runs-on: ${{ matrix.os }}
+ timeout-minutes: 5
strategy:
matrix:
os: ["windows-latest", "macos-latest"]
sdl-version: ["3.2.16"]
fail-fast: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install build dependencies
@@ -94,29 +108,36 @@ jobs:
build:
needs: [ruff, mypy, sdist]
runs-on: ${{ matrix.os }}
+ timeout-minutes: 15
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
- python-version: ["3.10", "pypy-3.10"]
+ python-version: ["3.10", "3.14t", "pypy-3.10"]
architecture: ["x64"]
include:
- os: "windows-latest"
python-version: "3.10"
architecture: "x86"
- os: "windows-latest"
- python-version: "pypy-3.10"
+ python-version: "3.14t"
architecture: "x86"
+ - os: "windows-11-arm"
+ python-version: "3.11"
+ architecture: "arm64"
+ - os: "windows-11-arm"
+ python-version: "3.14t"
+ architecture: "arm64"
fail-fast: false
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
- run: |
- git submodule update --init --recursive --depth 1
+ run: git submodule update --init --recursive --depth 1
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
@@ -125,7 +146,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install xvfb
- - uses: libsdl-org/setup-sdl@6574e20ac65ce362cd12f9c26b3a5e4d3cd31dee
+ - uses: libsdl-org/setup-sdl@1894460666e4fe0c6603ea0cce5733f1cca56ba1
if: runner.os == 'Linux'
with:
install-linux-dependencies: true
@@ -137,26 +158,20 @@ jobs:
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: |
- pip install -e . # Install the package in-place.
+ run: pip install -e . # Install the package in-place.
- name: Build package
- run: |
- python -m build
+ run: python -m build
- name: Test with pytest
if: runner.os == 'Windows'
- run: |
- pytest --cov-report=xml --timeout=300
+ run: 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 --timeout=300
+ run: 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@v4
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- - uses: actions/upload-artifact@v4
+ - uses: codecov/codecov-action@v6
+ - uses: actions/upload-artifact@v7
if: runner.os == 'Windows'
with:
name: wheels-windows-${{ matrix.architecture }}-${{ matrix.python-version }}
@@ -167,15 +182,17 @@ jobs:
test-docs:
needs: [ruff, mypy, sdist]
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- - uses: libsdl-org/setup-sdl@6574e20ac65ce362cd12f9c26b3a5e4d3cd31dee
+ - uses: libsdl-org/setup-sdl@1894460666e4fe0c6603ea0cce5733f1cca56ba1
if: runner.os == 'Linux'
with:
install-linux-dependencies: true
build-type: "Debug"
version: ${{ env.sdl-version }}
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
@@ -194,23 +211,25 @@ jobs:
tox:
needs: [ruff, sdist]
runs-on: ${{ matrix.os }}
+ timeout-minutes: 15
strategy:
matrix:
os: ["ubuntu-latest"] # "windows-latest" disabled due to free-threaded build issues
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --depth 1
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip tox
- - uses: libsdl-org/setup-sdl@6574e20ac65ce362cd12f9c26b3a5e4d3cd31dee
+ - uses: libsdl-org/setup-sdl@1894460666e4fe0c6603ea0cce5733f1cca56ba1
if: runner.os == 'Linux'
with:
install-linux-dependencies: true
@@ -223,33 +242,22 @@ jobs:
linux-wheels:
needs: [ruff, mypy, sdist]
runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || 'ubuntu-latest'}}
+ timeout-minutes: 15
strategy:
matrix:
arch: ["x86_64", "aarch64"]
- build: ["cp310-manylinux*", "pp310-manylinux*"]
+ build: ["cp310-manylinux*", "pp310-manylinux*", "cp314t-manylinux*"]
env:
BUILD_DESC: ""
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- - name: Set up QEMU
- if: ${{ matrix.arch == 'aarch64' }}
- uses: docker/setup-qemu-action@v3
- name: Checkout submodules
- run: |
- git submodule update --init --recursive --depth 1
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.x"
- - name: Install Python dependencies
- run: |
- python -m pip install --upgrade pip
- pip install cibuildwheel==2.23.3
+ run: git submodule update --init --recursive --depth 1
- name: Build wheels
- run: |
- python -m cibuildwheel --output-dir wheelhouse
+ uses: pypa/cibuildwheel@v3.1.4
env:
CIBW_BUILD: ${{ matrix.build }}
CIBW_ARCHS_LINUX: ${{ matrix.arch }}
@@ -276,12 +284,13 @@ jobs:
# Skip test on emulated architectures
CIBW_TEST_SKIP: "*_aarch64"
- name: Remove asterisk from label
+ env:
+ BUILD_DESC: ${{ matrix.build }}
run: |
- BUILD_DESC=${{ matrix.build }}
BUILD_DESC=${BUILD_DESC//\*}
echo BUILD_DESC=${BUILD_DESC} >> $GITHUB_ENV
- name: Archive wheel
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: wheels-linux-${{ matrix.arch }}-${{ env.BUILD_DESC }}
path: wheelhouse/*.whl
@@ -291,19 +300,21 @@ jobs:
build-macos:
needs: [ruff, mypy, sdist]
runs-on: "macos-14"
+ timeout-minutes: 15
strategy:
fail-fast: true
matrix:
- python: ["cp310-*_universal2", "pp310-*"]
+ python: ["cp310-*_universal2", "cp314t-*_universal2", "pp310-*"]
env:
PYTHON_DESC: ""
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
+ persist-credentials: false
fetch-depth: ${{ env.git-depth }}
- name: Checkout submodules
run: git submodule update --init --recursive --depth 1
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install Python dependencies
@@ -312,7 +323,7 @@ jobs:
# Downloads SDL for the later step.
run: python build_sdl.py
- name: Build wheels
- uses: pypa/cibuildwheel@v2.23.3
+ uses: pypa/cibuildwheel@v3.1.4
env:
CIBW_BUILD: ${{ matrix.python }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
@@ -322,33 +333,63 @@ jobs:
CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64"
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Remove asterisk from label
+ env:
+ PYTHON_DESC: ${{ matrix.python }}
run: |
- PYTHON_DESC=${{ matrix.python }}
PYTHON_DESC=${PYTHON_DESC//\*/X}
echo PYTHON_DESC=${PYTHON_DESC} >> $GITHUB_ENV
- name: Archive wheel
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: wheels-macos-${{ env.PYTHON_DESC }}
path: wheelhouse/*.whl
retention-days: 7
compression-level: 0
+ pyodide:
+ needs: [ruff, mypy, sdist]
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ fetch-depth: ${{ env.git-depth }}
+ - name: Checkout submodules
+ run: git submodule update --init --recursive --depth 1
+ - uses: libsdl-org/setup-sdl@1894460666e4fe0c6603ea0cce5733f1cca56ba1
+ with:
+ install-linux-dependencies: true
+ build-type: "Debug"
+ version: "3.2.4" # Should be equal or less than the version used by Emscripten
+ - uses: pypa/cibuildwheel@v3.1.4
+ env:
+ CIBW_BUILD: cp313-pyodide_wasm32
+ CIBW_PLATFORM: pyodide
+ - name: Archive wheel
+ uses: actions/upload-artifact@v7
+ with:
+ name: pyodide
+ path: wheelhouse/*.whl
+ retention-days: 30
+ compression-level: 0
+
publish:
- needs: [sdist, build, build-macos, linux-wheels]
+ needs: [sdist, build, build-macos, linux-wheels, pyodide]
runs-on: ubuntu-latest
+ timeout-minutes: 5
if: github.ref_type == 'tag'
environment:
name: pypi
url: https://pypi.org/project/tcod/${{ github.ref_name }}
permissions:
- id-token: write
+ id-token: write # Attestation
steps:
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v8
with:
name: sdist
path: dist/
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v8
with:
pattern: wheels-*
path: dist/
diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml
index 2171ddbf..d2e52c2f 100644
--- a/.github/workflows/release-on-tag.yml
+++ b/.github/workflows/release-on-tag.yml
@@ -5,21 +5,24 @@ on:
name: Create Release
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
+ timeout-minutes: 5
permissions:
- contents: write
+ contents: write # Publish GitHub Releases
steps:
- - name: Checkout code
- uses: actions/checkout@v4
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
- name: Generate body
- run: |
- scripts/get_release_description.py | tee release_body.md
+ run: scripts/get_release_description.py | tee release_body.md
- name: Create Release
id: create_release
- uses: ncipollo/release-action@v1
- with:
- name: ""
- bodyFile: release_body.md
+ # https://cli.github.com/manual/gh_release_create
+ run: gh release create "${GITHUB_REF_NAME}" --verify-tag --notes-file release_body.md
diff --git a/.github/zizmor.yaml b/.github/zizmor.yaml
new file mode 100644
index 00000000..b247c2d3
--- /dev/null
+++ b/.github/zizmor.yaml
@@ -0,0 +1,7 @@
+rules:
+ anonymous-definition:
+ disable: true
+ excessive-permissions:
+ disable: true
+ unpinned-uses:
+ disable: true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 43080c04..6baa9d9b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ ci:
autoupdate_schedule: quarterly
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -17,8 +17,12 @@ repos:
- id: fix-byte-order-marker
- id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.13
+ rev: v0.15.9
hooks:
- id: ruff-check
args: [--fix-only, --exit-non-zero-on-fix]
- id: ruff-format
+ - repo: https://github.com/zizmorcore/zizmor-pre-commit
+ rev: v1.23.1
+ hooks:
+ - id: zizmor
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 8d733f5e..098d76db 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -9,7 +9,17 @@ build:
tools:
python: "3.11"
apt_packages:
- - libsdl3-dev
+ - build-essential
+ - make
+ - pkg-config
+ - cmake
+ - ninja-build
+ jobs:
+ pre_install:
+ - git clone --depth 1 --branch release-3.2.16 https://github.com/libsdl-org/SDL.git sdl_repo
+ - cmake -S sdl_repo -B sdl_build -D CMAKE_INSTALL_PREFIX=~/.local
+ - cmake --build sdl_build --config Debug
+ - cmake --install sdl_build
submodules:
include: all
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 446a6363..10b92720 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -13,19 +13,23 @@
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"files.associations": {
- "*.spec": "python",
+ "*.spec": "python"
},
"mypy-type-checker.importStrategy": "fromEnvironment",
"cSpell.words": [
"aarch",
"ADDA",
"ADDALPHA",
+ "ADDEVENT",
+ "addoption",
+ "addopts",
"addressof",
"addsub",
"addx",
"addy",
"algo",
"ALPH",
+ "alsa",
"ALTERASE",
"arange",
"ARCHS",
@@ -44,6 +48,7 @@
"AUDIOREWIND",
"AUDIOSTOP",
"autoclass",
+ "AUTOCORRECT",
"autofunction",
"autogenerated",
"automodule",
@@ -69,11 +74,8 @@
"bysource",
"caeldera",
"CAPSLOCK",
- "caxis",
- "cbutton",
"ccoef",
"cdef",
- "cdevice",
"cffi",
"cflags",
"CFLAGS",
@@ -103,8 +105,10 @@
"CONTROLLERDEVICEADDED",
"CONTROLLERDEVICEREMAPPED",
"CONTROLLERDEVICEREMOVED",
+ "cooldown",
"cplusplus",
"CPLUSPLUS",
+ "cpython",
"CRSEL",
"ctypes",
"CURRENCYSUBUNIT",
@@ -115,6 +119,7 @@
"DBLAMPERSAND",
"DBLAPOSTROPHE",
"DBLVERTICALBAR",
+ "dbus",
"dcost",
"DCROSS",
"DECIMALSEPARATOR",
@@ -127,8 +132,10 @@
"devel",
"DHLINE",
"DISPLAYSWITCH",
+ "distutils",
"dlopen",
"docstrings",
+ "doctest",
"documentclass",
"Doryen",
"DPAD",
@@ -141,6 +148,8 @@
"dunder",
"DVLINE",
"elif",
+ "Emscripten",
+ "EMSDK",
"ENDCALL",
"endianness",
"epel",
@@ -149,22 +158,29 @@
"errorvf",
"EXCLAM",
"EXSEL",
+ "faulthandler",
"favicon",
"ffade",
"fgcolor",
"fheight",
+ "filterwarnings",
"Flecs",
"flto",
"fmean",
"fontx",
"fonty",
+ "freethreading",
"freetype",
"frombuffer",
"fullscreen",
"furo",
"fwidth",
"GAMECONTROLLER",
+ "gamedev",
"gamepad",
+ "gaxis",
+ "gbutton",
+ "gdevice",
"genindex",
"getbbox",
"GFORCE",
@@ -189,13 +205,17 @@
"htmlhelp",
"htmlzip",
"IBEAM",
+ "ibus",
+ "ICCPROF",
"ifdef",
"ifndef",
"iinfo",
"IJKL",
"imageio",
"imread",
+ "includepath",
"INCOL",
+ "INPUTTYPE",
"INROW",
"interactable",
"intersphinx",
@@ -212,6 +232,7 @@
"jhat",
"jice",
"jieba",
+ "jmccardle",
"JOYAXISMOTION",
"JOYBALLMOTION",
"JOYBUTTONDOWN",
@@ -254,9 +275,13 @@
"letterpaper",
"LGUI",
"LHYPER",
+ "libdrm",
+ "libgbm",
"libsdl",
"libtcod",
"libtcodpy",
+ "libusb",
+ "libxkbcommon",
"linspace",
"liskin",
"LMASK",
@@ -284,11 +309,13 @@
"mgrid",
"milli",
"minmax",
+ "minversion",
"mipmap",
"mipmaps",
"MMASK",
"modindex",
"moduleauthor",
+ "modversion",
"MOUSEBUTTONDOWN",
"MOUSEBUTTONUP",
"MOUSEMOTION",
@@ -309,6 +336,7 @@
"neww",
"noarchive",
"NODISCARD",
+ "NOLONGLONG",
"NOMESSAGE",
"Nonrepresentable",
"NONUSBACKSLASH",
@@ -331,6 +359,7 @@
"pagerefs",
"PAGEUP",
"papersize",
+ "passthru",
"PATCHLEVEL",
"pathfinding",
"pathlib",
@@ -338,23 +367,29 @@
"PERLIN",
"PILCROW",
"pilmode",
+ "PIXELART",
"PIXELFORMAT",
"PLUSMINUS",
"pointsize",
+ "popleft",
"PRESENTVSYNC",
"PRINTF",
"printn",
"PRINTSCREEN",
"propname",
+ "pulseaudio",
"pushdown",
"pycall",
"pycparser",
+ "pydocstyle",
"pyinstaller",
+ "pyodide",
"pypa",
"PYPI",
"pypiwin",
"pypy",
"pytest",
+ "pytestmark",
"PYTHONHASHSEED",
"PYTHONOPTIMIZE",
"Pyup",
@@ -370,6 +405,7 @@
"Redistributable",
"redistributables",
"repr",
+ "rexpaint",
"rgba",
"RGUI",
"RHYPER",
@@ -384,17 +420,20 @@
"RMASK",
"rmeta",
"roguelike",
+ "roguelikedev",
"rpath",
"RRGGBB",
"rtype",
"RWIN",
"RWOPS",
+ "SCALEMODE",
"scalex",
"scaley",
"Scancode",
"scancodes",
"scipy",
"scoef",
+ "Scrn",
"SCROLLLOCK",
"sdist",
"SDL's",
@@ -421,12 +460,16 @@
"sphinxstrong",
"sphinxtitleref",
"staticmethod",
+ "stdarg",
+ "stddef",
"stdeb",
"struct",
"structs",
+ "subclassing",
"SUBP",
"SYSREQ",
"tablefmt",
+ "Tamzen",
"TARGETTEXTURE",
"tcod",
"tcoddoc",
@@ -434,6 +477,8 @@
"TCODLIB",
"TEEE",
"TEEW",
+ "termbox",
+ "testpaths",
"TEXTUREACCESS",
"thirdparty",
"THOUSANDSSEPARATOR",
@@ -451,6 +496,7 @@
"TRIGGERRIGHT",
"tris",
"truetype",
+ "tryddle",
"typestr",
"undoc",
"Unifont",
@@ -468,6 +514,7 @@
"VERTICALBAR",
"vflip",
"viewcode",
+ "VITAFILE",
"vline",
"VOLUMEDOWN",
"VOLUMEUP",
@@ -478,6 +525,7 @@
"WAITARROW",
"WASD",
"waterlevel",
+ "WINAPI",
"windowclose",
"windowenter",
"WINDOWEVENT",
@@ -496,11 +544,17 @@
"windowshown",
"windowsizechanged",
"windowtakefocus",
+ "xcframework",
+ "Xcursor",
"xdst",
+ "Xext",
+ "Xfixes",
+ "Xrandr",
"xrel",
"xvfb",
"ydst",
- "yrel"
+ "yrel",
+ "zizmor"
],
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
@@ -508,7 +562,7 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
- "cSpell.enableFiletypes": [
- "github-actions-workflow"
- ]
+ "[html]": {
+ "editor.formatOnSave": false
+ }
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cbd924b..bc5f79f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,204 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
## [Unreleased]
+## [21.2.0] - 2026-04-04
+
+### Added
+
+- `tcod.event.MouseWheel` now has `which`, `window_id`, `position` and `integer_position` attributes.
+
+## Fixed
+
+- `tcod.event.convert_coordinates_from_window` was not converting all types of mouse events.
+
+## [21.1.0] - 2026-04-04
+
+### Added
+
+- `MouseButtonEvent.integer_position` property.
+
+## Fixed
+
+- `integer_position` was missing from mouse button events.
+
+## [21.0.0] - 2026-03-13
+
+### Added
+
+- `tcod.sdl.video.Window` now accepts an SDL WindowID.
+- `tcod.event`:
+ - `MouseState.integer_position` and `MouseMotion.integer_motion` to handle cases where integer values are preferred.
+ - `ClipboardUpdate` event.
+ - `Drop` event.
+ - `KeyboardEvent.pressed`, `KeyboardEvent.which`, `KeyboardEvent.window_id` attributes.
+ - `WindowEvent.data` and `WindowEvent.window_id` attributes and added missing SDL3 window events.
+ - `which` and `window_id` attributes for mouse events.
+ - Events now have `Event.timestamp` and `Event.timestamp_ns` which use SDL's timer at `tcod.event.time` and `tcod.event.time_ns`.
+
+### Changed
+
+- Event classes are now more strict with attribute types
+- Event class initializers are keyword-only and no longer take a type parameter, with exceptions.
+ Generally event class initialization is an internal process.
+- `MouseButtonEvent` no longer a subclass of `MouseState`.
+- `tcod.event.Point` is now a generic type containing `int` or `float` values depending on the context.
+- When converting mouse events to tiles:
+ `MouseState.position` and `MouseMotion.motion` refers to sub-tile coordinates.
+ `MouseState.integer_position` and `MouseMotion.integer_motion` refers to integer tile coordinates.
+
+### Deprecated
+
+- `Event.type` is deprecated except for special cases such as `ControllerDevice`, `WindowEvent`, etc.
+- `MouseButtonEvent.state` is deprecated, replaced by the existing `.button` attribute.
+
+### Fixed
+
+- Fixed incorrect C FFI types inside `tcod.event.get_mouse_state`.
+- Fixed regression in mouse event tile coordinates being `float` instead of `int`.
+ `convert_coordinates_from_window` can be used if sub-tile coordinates were desired.
+- Fixed regression in `libtcodpy.bsp_split_recursive` not accepting `0`.
+
+## [20.1.0] - 2026-02-25
+
+### Added
+
+- `Tileset` now supports `MutableMapping` semantics.
+ Can get, set, or iterate over tiles as if it were a dictionary containing tile glyph arrays.
+ Also supports `+`, `|`, `+=`, and `|=` with other tilesets or mappings to merge them into a single Tileset.
+- `tcod.tileset.procedural_block_elements` can take a tile shape and return a tileset.
+
+### Deprecated
+
+- `Tileset.set_tile(codepoint, tile)` was replaced with `tileset[codepoint] = tile` syntax.
+- `Tileset.get_tile(codepoint)` was soft replaced with `tileset[codepoint]` syntax.
+- `tcod.tileset.procedural_block_elements` should be used with dictionary semantics instead of passing in a tileset.
+
+## [20.0.0] - 2026-02-06
+
+### Added
+
+- Now supports free-threaded Python, deploys with `cp314t` wheels.
+- Added methods: `Renderer.coordinates_from_window` and `Renderer.coordinates_to_window`
+- Added `tcod.event.convert_coordinates_from_window`.
+
+### Changed
+
+- `Renderer.logical_size` now returns `None` instead of `(0, 0)` when logical size is unset.
+
+## [19.6.3] - 2026-01-12
+
+Fix missing deployment
+
+## [19.6.2] - 2026-01-12
+
+### Changed
+
+- Update to libtcod 2.2.2
+
+### Fixed
+
+- Mouse coordinate to tile conversions now support SDL renderer logical size and scaling.
+
+## [19.6.1] - 2025-12-15
+
+### Fixed
+
+- `tcod.event.add_watch` was crashing due to a cdef type mismatch.
+
+## [19.6.0] - 2025-10-20
+
+### Added
+
+- Alternative syntax for number symbols with `KeySym`, can now specify `KeySym["3"]`, etc.
+ Only available on Python 3.13 or later.
+
+### Fixed
+
+- Fixed regression with lowercase key symbols with `tcod.event.K_*` and `KeySym.*` constants, these are still deprecated.
+ Event constants are only fixed for `tcod.event.K_*`, not the undocumented `tcod.event_constants` module.
+ Lowercase `KeySym.*` constants are only available on Python 3.13 or later.
+- `BSP.split_recursive` did not accept a `Random` class as the seed. #168
+
+## [19.5.0] - 2025-09-13
+
+### Changed
+
+- Update to libtcod 2.2.1.
+- Scaling defaults to nearest, set `os.environ["SDL_RENDER_SCALE_QUALITY"] = "linear"` if linear scaling was preferred.
+
+### Fixed
+
+- `SDL_RENDER_SCALE_QUALITY` is now respected again since the change to SDL3.
+- Fixed crash on controller events.
+
+## [19.4.1] - 2025-08-27
+
+### Fixed
+
+- Fixed dangling pointer in `Pathfinder.clear` method.
+- Fixed hang in `Pathfinder.rebuild_frontier` method.
+
+## [19.4.0] - 2025-08-06
+
+### Changed
+
+- Checking "WindowSizeChanged" was not valid since SDL 3 and was also not valid in previous examples.
+ You must no longer check the type of the `WindowResized` event.
+
+### Fixed
+
+- Corrected some inconsistent angle brackets in the `__str__` of Event subclasses. #165
+- Fix regression with window events causing them to be `Unknown` and uncheckable.
+
+## [19.3.1] - 2025-08-02
+
+Solved a deprecation warning which was internal to tcod and no doubt annoyed many devs.
+Thanks to jmccardle for forcing me to resolve this.
+
+### Fixed
+
+- Silenced internal deprecation warnings within `Context.convert_event`.
+
+## [19.3.0] - 2025-07-26
+
+Thanks to cr0ne for pointing out missing texture scaling options.
+These options did not exist in SDL2.
+
+### Added
+
+- `tcod.sdl.render`: Added `ScaleMode` enum and `Texture.scale_mode` attribute.
+
+## [19.2.0] - 2025-07-20
+
+Thanks to tryddle for demonstrating how bad the current API was with chunked world generation.
+
+### Added
+
+- `tcod.noise.grid` now has the `offset` parameter for easier sampling of noise chunks.
+
+## [19.1.0] - 2025-07-12
+
+### Added
+
+- Added text input support to `tcod.sdl.video.Window` which was missing since the SDL3 update.
+ After creating a context use `assert context.sdl_window` or `if context.sdl_window:` to verify that an SDL window exists then use `context.sdl_window.start_text_input` to enable text input events.
+ Keep in mind that this can open an on-screen keyboard.
+
+## [19.0.2] - 2025-07-11
+
+Resolve wheel deployment issue.
+
+## [19.0.1] - 2025-07-11
+
+### Fixed
+
+- `Console.print` methods using `string` keyword were marked as invalid instead of deprecated.
+
+## [19.0.0] - 2025-06-13
+
+Finished port to SDL3, this has caused several breaking changes from SDL such as lowercase key constants now being uppercase and mouse events returning `float` instead of `int`.
+Be sure to run [Mypy](https://mypy.readthedocs.io/en/stable/getting_started.html) on your projects to catch any issues from this update.
+
### Changed
- Updated libtcod to 2.1.1
@@ -15,7 +213,6 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
- `tcod.event.KeySym` single letter symbols are now all uppercase.
- Relative mouse mode is set via `tcod.sdl.video.Window.relative_mouse_mode` instead of `tcod.sdl.mouse.set_relative_mode`.
- `tcod.sdl.render.new_renderer`: Removed `software` and `target_textures` parameters, `vsync` takes `int`, `driver` takes `str` instead of `int`.
-- SDL renderer logical
- `tcod.sdl.render.Renderer`: `integer_scaling` and `logical_size` are now set with `set_logical_presentation` method.
- `tcod.sdl.render.Renderer.geometry` now takes float values for `color` instead of 8-bit integers.
- `tcod.event.Point` and other mouse/tile coordinate types now use `float` instead of `int`.
@@ -28,6 +225,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
- Sound queueing methods were moved from `AudioDevice` to a new `AudioStream` class.
- `BasicMixer` may require manually specifying `frequency` and `channels` to replicate old behavior.
- `get_devices` and `get_capture_devices` now return `dict[str, AudioDevice]`.
+- `TextInput` events are no longer enabled by default.
### Deprecated
@@ -42,6 +240,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
- `WindowFlags.FULLSCREEN_DESKTOP` is now just `WindowFlags.FULLSCREEN`
- `tcod.sdl.render.Renderer.integer_scaling` removed.
- Removed `callback`, `spec`, `queued_samples`, `queue_audio`, and `dequeue_audio` attributes from `tcod.sdl.audio.AudioDevice`.
+- `tcod.event.WindowResized`: `type="WindowSizeChanged"` removed and must no longer be checked for.
+ `EventDispatch.ev_windowsizechanged` is no longer called.
### Fixed
diff --git a/LICENSE.txt b/LICENSE.txt
index ed980457..8af4c736 100755
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
BSD 2-Clause License
-Copyright (c) 2009-2025, Kyle Benesch and the python-tcod contributors.
+Copyright (c) 2009-2026, Kyle Benesch and the python-tcod contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/build_libtcod.py b/build_libtcod.py
index f9a5e3dd..14a46710 100755
--- a/build_libtcod.py
+++ b/build_libtcod.py
@@ -11,9 +11,8 @@
import re
import subprocess
import sys
-from collections.abc import Iterable, Iterator
from pathlib import Path
-from typing import Any, ClassVar
+from typing import TYPE_CHECKING, Any, ClassVar, Final
import attrs
import pycparser # type: ignore[import-untyped]
@@ -27,7 +26,10 @@
import build_sdl
-Py_LIMITED_API = 0x03100000
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator
+
+Py_LIMITED_API: None | int = 0x03100000
HEADER_PARSE_PATHS = ("tcod/", "libtcod/src/libtcod/")
HEADER_PARSE_EXCLUDES = ("gl2_ext_.h", "renderer_gl_internal.h", "event.h")
@@ -43,6 +45,7 @@
r"TCODLIB_C?API|TCOD_PUBLIC|TCOD_NODISCARD|TCOD_DEPRECATED_NOMESSAGE|TCOD_DEPRECATED_ENUM"
r"|(TCOD_DEPRECATED\(\".*?\"\))"
r"|(TCOD_DEPRECATED|TCODLIB_FORMAT)\([^)]*\)|__restrict"
+ r"|TCODLIB_(BEGIN|END)_IGNORE_DEPRECATIONS"
)
RE_VAFUNC = re.compile(r"^[^;]*\([^;]*va_list.*\);", re.MULTILINE)
RE_INLINE = re.compile(r"(^.*?inline.*?\(.*?\))\s*\{.*?\}$", re.DOTALL | re.MULTILINE)
@@ -162,7 +165,17 @@ def walk_sources(directory: str) -> Iterator[str]:
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)]
+define_macros: list[tuple[str, Any]] = []
+
+if "free-threading build" in sys.version:
+ Py_LIMITED_API = None
+if "PYODIDE" in os.environ:
+ # Unable to apply Py_LIMITED_API to Pyodide in cffi<=1.17.1
+ # https://github.com/python-cffi/cffi/issues/179
+ Py_LIMITED_API = None
+
+if Py_LIMITED_API:
+ define_macros.append(("Py_LIMITED_API", Py_LIMITED_API))
sources += walk_sources("tcod/")
sources += walk_sources("libtcod/src/libtcod/")
@@ -180,8 +193,8 @@ def walk_sources(directory: str) -> Iterator[str]:
include_dirs.append("libtcod/src/zlib/")
-if sys.platform == "darwin":
- # Fix "implicit declaration of function 'close'" in zlib.
+if sys.platform != "win32":
+ # Fix implicit declaration of multiple functions in zlib.
define_macros.append(("HAVE_UNISTD_H", 1))
@@ -263,7 +276,7 @@ def find_sdl_attrs(prefix: str) -> Iterator[tuple[str, int | str | Any]]:
`prefix` is used to filter out which names to copy.
"""
- from tcod._libtcod import lib
+ from tcod._libtcod import lib # noqa: PLC0415
if prefix.startswith("SDL_"):
name_starts_at = 4
@@ -314,12 +327,14 @@ def parse_sdl_attrs(prefix: str, all_names: list[str] | None) -> tuple[str, str]
]
+RE_CONSTANTS_ALL: Final = re.compile(
+ r"(.*# --- From constants.py ---).*(# --- End constants.py ---.*)",
+ re.DOTALL,
+)
+
+
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,
- )
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()
@@ -340,8 +355,8 @@ def generate_enums(prefix: str) -> Iterator[str]:
def write_library_constants() -> None:
"""Write libtcod constants into the tcod.constants module."""
- import tcod.color
- from tcod._libtcod import ffi, lib
+ import tcod.color # noqa: PLC0415
+ from tcod._libtcod import ffi, lib # noqa: PLC0415
with Path("tcod/constants.py").open("w", encoding="utf-8") as f:
all_names = []
@@ -435,6 +450,8 @@ def _fix_reserved_name(name: str) -> str:
@attrs.define(frozen=True)
class ConvertedParam:
+ """Converted type parameter from C types into Python type-hints."""
+
name: str = attrs.field(converter=_fix_reserved_name)
hint: str
original: str
@@ -454,7 +471,8 @@ def _type_from_names(names: list[str]) -> str:
return "Any"
-def _param_as_hint(node: pycparser.c_ast.Node, default_name: str) -> ConvertedParam:
+def _param_as_hint(node: pycparser.c_ast.Node, default_name: str) -> ConvertedParam: # noqa: PLR0911
+ """Return a Python type-hint from a C AST node."""
original = pycparser.c_generator.CGenerator().visit(node)
name: str
names: list[str]
diff --git a/build_sdl.py b/build_sdl.py
index 1f7d05ec..a1b8f6a2 100755
--- a/build_sdl.py
+++ b/build_sdl.py
@@ -3,10 +3,10 @@
from __future__ import annotations
+import functools
import io
import logging
import os
-import platform
import re
import shutil
import subprocess
@@ -20,7 +20,7 @@
import requests
# This script calls a lot of programs.
-# ruff: noqa: S603, S607, T201
+# ruff: noqa: S603, S607
# Ignore f-strings in logging, these will eventually be replaced with t-strings.
# ruff: noqa: G004
@@ -28,7 +28,20 @@
logger = logging.getLogger(__name__)
-BIT_SIZE, LINKAGE = platform.architecture()
+RE_MACHINE = re.compile(r".*\((.+)\)\]", re.DOTALL)
+
+
+@functools.cache
+def python_machine() -> str:
+ """Return the Python machine architecture (e.g. 'i386', 'AMD64', 'ARM64')."""
+ # Only needs to function correctly for Windows platforms.
+ match = RE_MACHINE.match(sys.version)
+ assert match, repr(sys.version)
+ (machine,) = match.groups()
+ machine = {"Intel": "i386"}.get(machine, machine)
+ logger.info(f"python_machine: {machine}")
+ return machine
+
# Reject versions of SDL older than this, update the requirements in the readme if you change this.
SDL_MIN_VERSION = (3, 2, 0)
@@ -134,12 +147,19 @@ def check_sdl_version() -> None:
sdl_version_str = subprocess.check_output(
["pkg-config", "sdl3", "--modversion"], universal_newlines=True
).strip()
- except FileNotFoundError as exc:
- msg = (
- "libsdl3-dev or equivalent must be installed on your system and must be at least version"
- f" {needed_version}.\nsdl3-config must be on PATH."
- )
- raise RuntimeError(msg) from exc
+ except FileNotFoundError:
+ try:
+ sdl_version_str = subprocess.check_output(["sdl3-config", "--version"], universal_newlines=True).strip()
+ except FileNotFoundError as exc:
+ msg = (
+ f"libsdl3-dev or equivalent must be installed on your system and must be at least version {needed_version}.\n"
+ "sdl3-config must be on PATH."
+ )
+ raise RuntimeError(msg) from exc
+ except subprocess.CalledProcessError as exc:
+ if sys.version_info >= (3, 11):
+ exc.add_note(f"Note: {os.environ.get('PKG_CONFIG_PATH')=}")
+ raise
logger.info(f"Found SDL {sdl_version_str}.")
sdl_version = tuple(int(s) for s in sdl_version_str.split("."))
if sdl_version < SDL_MIN_VERSION:
@@ -205,13 +225,19 @@ def get_output(self) -> str:
buffer.write(f"#define {name} ...\n")
return buffer.getvalue()
+ def on_file_open(self, is_system_include: bool, includepath: str) -> Any: # noqa: ANN401, FBT001
+ """Ignore includes other than SDL headers."""
+ if not Path(includepath).parent.name == "SDL3":
+ raise FileNotFoundError
+ return super().on_file_open(is_system_include, includepath)
+
def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curdir: str, includepath: str) -> None: # noqa: ARG002, FBT001
"""Remove bad includes such as stddef.h and stdarg.h."""
assert "SDL3/SDL" not in includepath, (includepath, curdir)
raise pcpp.OutputDirective(pcpp.Action.IgnoreAndRemove)
- def _should_track_define(self, tokens: list[Any]) -> bool:
- if len(tokens) < 3:
+ def _should_track_define(self, tokens: list[Any]) -> bool: # noqa: PLR0911
+ if len(tokens) < 3: # noqa: PLR2004
return False
if tokens[0].value in IGNORE_DEFINES:
return False
@@ -223,7 +249,7 @@ def _should_track_define(self, tokens: list[Any]) -> bool:
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":
+ if len(tokens) >= 4 and tokens[2].type == "CPP_INTEGER" and tokens[3].type == "CPP_DOT": # noqa: PLR2004
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.
@@ -249,23 +275,35 @@ def on_directive_handle(
return super().on_directive_handle(directive, tokens, if_passthru, preceding_tokens)
+def get_emscripten_include_dir() -> Path:
+ """Find and return the Emscripten include dir."""
+ # None of the EMSDK environment variables exist! Search PATH for Emscripten as a workaround
+ for path in os.environ["PATH"].split(os.pathsep)[::-1]:
+ if Path(path).match("upstream/emscripten"):
+ return Path(path, "system/include").resolve(strict=True)
+ raise AssertionError(os.environ["PATH"])
+
+
check_sdl_version()
-if sys.platform == "win32" or sys.platform == "darwin":
+SDL_PARSE_PATH: Path | None = None
+SDL_BUNDLE_PATH: Path | None = None
+if (sys.platform == "win32" or sys.platform == "darwin") and "PYODIDE" not in os.environ:
SDL_PARSE_PATH = unpack_sdl(SDL_PARSE_VERSION)
SDL_BUNDLE_PATH = unpack_sdl(SDL_BUNDLE_VERSION)
SDL_INCLUDE: Path
-if sys.platform == "win32":
+if sys.platform == "win32" and SDL_PARSE_PATH is not None:
SDL_INCLUDE = SDL_PARSE_PATH / "include"
-elif sys.platform == "darwin":
+elif sys.platform == "darwin" and SDL_PARSE_PATH is not None:
SDL_INCLUDE = SDL_PARSE_PATH / "Versions/A/Headers"
else: # Unix
matches = re.findall(
r"-I(\S+)",
subprocess.check_output(["pkg-config", "sdl3", "--cflags"], universal_newlines=True),
)
- assert matches
+ if not matches:
+ matches = ["/usr/include"]
for match in matches:
if Path(match, "SDL3/SDL.h").is_file():
@@ -275,6 +313,7 @@ def on_directive_handle(
raise AssertionError(matches)
assert SDL_INCLUDE
+logger.info(f"{SDL_INCLUDE=}")
EXTRA_CDEF = """
#define SDLK_SCANCODE_MASK ...
@@ -285,7 +324,7 @@ def on_directive_handle(
// 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);
+bool _sdl_event_watcher(void* userdata, SDL_Event* event);
}
"""
@@ -350,19 +389,20 @@ def get_cdef() -> tuple[str, dict[str, str]]:
libraries: list[str] = []
library_dirs: list[str] = []
-
-if sys.platform == "darwin":
+if "PYODIDE" in os.environ:
+ pass
+elif sys.platform == "darwin":
extra_link_args += ["-framework", "SDL3"]
else:
libraries += ["SDL3"]
# Bundle the Windows SDL DLL.
-if sys.platform == "win32":
+if sys.platform == "win32" and SDL_BUNDLE_PATH is not None:
include_dirs.append(str(SDL_INCLUDE))
- ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"}
- SDL_LIB_DIR = Path(SDL_BUNDLE_PATH, "lib/", ARCH_MAPPING[BIT_SIZE])
+ ARCH_MAPPING = {"i386": "x86", "AMD64": "x64", "ARM64": "arm64"}
+ SDL_LIB_DIR = Path(SDL_BUNDLE_PATH, "lib/", ARCH_MAPPING[python_machine()])
library_dirs.append(str(SDL_LIB_DIR))
- SDL_LIB_DEST = Path("tcod", ARCH_MAPPING[BIT_SIZE])
+ SDL_LIB_DEST = Path("tcod", ARCH_MAPPING[python_machine()])
SDL_LIB_DEST.mkdir(exist_ok=True)
SDL_LIB_DEST_FILE = SDL_LIB_DEST / "SDL3.dll"
SDL_LIB_FILE = SDL_LIB_DIR / "SDL3.dll"
@@ -371,14 +411,16 @@ def get_cdef() -> tuple[str, dict[str, str]]:
# Link to the SDL framework on MacOS.
# Delocate will bundle the binaries in a later step.
-if sys.platform == "darwin":
+if sys.platform == "darwin" and SDL_BUNDLE_PATH is not None:
include_dirs.append(SDL_INCLUDE)
extra_link_args += [f"-F{SDL_BUNDLE_PATH}/.."]
extra_link_args += ["-rpath", f"{SDL_BUNDLE_PATH}/.."]
extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"]
-# Use sdl-config to link to SDL on Linux.
-if sys.platform not in ["win32", "darwin"]:
+if "PYODIDE" in os.environ:
+ extra_compile_args += ["--use-port=sdl3"]
+elif sys.platform not in ["win32", "darwin"]:
+ # Use sdl-config to link to SDL on Linux.
extra_compile_args += (
subprocess.check_output(["pkg-config", "sdl3", "--cflags"], universal_newlines=True).strip().split()
)
diff --git a/docs/_templates/page.html b/docs/_templates/page.html
new file mode 100644
index 00000000..213c761f
--- /dev/null
+++ b/docs/_templates/page.html
@@ -0,0 +1,21 @@
+{% extends "!page.html" %}
+{% block content %}
+ {{ super() }}
+
+
+{% endblock %}
diff --git a/docs/conf.py b/docs/conf.py
index 0fda406a..70be25bf 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,5 @@
-"""Sphinx config file."""
-# tdl documentation build configuration file, created by
+"""Sphinx config file.""" # noqa: INP001
+# python-tcod documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 25 12:49:46 2016.
#
# This file is execfile()d with the current directory set to its
@@ -65,7 +65,7 @@
# General information about the project.
project = "python-tcod"
-copyright = "2009-2025, Kyle Benesch"
+copyright = "2009-2026, Kyle Benesch" # noqa: A001
author = "Kyle Benesch"
# The version info for the project you're documenting, acts as replacement for
@@ -73,13 +73,12 @@
# built documents.
#
# The full version, including alpha/beta/rc tags.
-git_describe = subprocess.run(
+release = subprocess.run(
["git", "describe", "--abbrev=0"], # noqa: S607
stdout=subprocess.PIPE,
text=True,
check=True,
-)
-release = git_describe.stdout.strip()
+).stdout.strip()
assert release
print(f"release version: {release!r}")
@@ -87,6 +86,7 @@
match_version = re.match(r"([0-9]+\.[0-9]+).*?", release)
assert match_version
version = match_version.group()
+print(f"short version: {version!r}")
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -167,9 +167,9 @@
# html_theme_path = []
# The name for this set of Sphinx documents.
-# " v documentation" by default.
+# " documentation" by default.
#
-# html_title = u'tdl v1'
+html_title = f"{project} {release} documentation"
# A shorter title for the navigation bar. Default is the same as html_title.
#
diff --git a/docs/tcod/event.rst b/docs/tcod/event.rst
index 7f2f059e..1fd5c7bb 100644
--- a/docs/tcod/event.rst
+++ b/docs/tcod/event.rst
@@ -3,6 +3,7 @@ SDL Event Handling ``tcod.event``
.. automodule:: tcod.event
:members:
+ :inherited-members: object, int, str, tuple
:member-order: bysource
:exclude-members:
KeySym, Scancode, Modifier, get, wait
diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst
index add5d9f0..aabb4f7f 100644
--- a/docs/tcod/getting-started.rst
+++ b/docs/tcod/getting-started.rst
@@ -114,7 +114,7 @@ clearing the console every frame and replacing it only on resizing the window.
match event:
case tcod.event.Quit():
raise SystemExit
- case tcod.event.WindowResized(type="WindowSizeChanged"):
+ case tcod.event.WindowResized(width=width, height=height): # Size in pixels
pass # The next call to context.new_console may return a different size.
diff --git a/examples/cavegen.py b/examples/cavegen.py
index 8b95d3fc..ace6bb98 100755
--- a/examples/cavegen.py
+++ b/examples/cavegen.py
@@ -10,7 +10,7 @@
from typing import Any
import numpy as np
-import scipy.signal # type: ignore
+import scipy.signal # type: ignore[import-untyped]
from numpy.typing import NDArray
diff --git a/examples/distribution/PyInstaller/requirements.txt b/examples/distribution/PyInstaller/requirements.txt
index 477fbbdc..24379896 100644
--- a/examples/distribution/PyInstaller/requirements.txt
+++ b/examples/distribution/PyInstaller/requirements.txt
@@ -1,3 +1,3 @@
tcod==16.2.3
-pyinstaller==6.9.0
+pyinstaller==6.14.2
pypiwin32; sys_platform=="win32"
diff --git a/examples/eventget.py b/examples/eventget.py
index dad8649d..9dc82ca3 100755
--- a/examples/eventget.py
+++ b/examples/eventget.py
@@ -8,12 +8,11 @@
import tcod.context
import tcod.event
import tcod.sdl.joystick
-import tcod.sdl.sys
-WIDTH, HEIGHT = 720, 480
+WIDTH, HEIGHT = 1280, 720
-def main() -> None:
+def main() -> None: # noqa: C901, PLR0912
"""Example program for tcod.event."""
event_log: list[str] = []
motion_desc = ""
@@ -22,6 +21,8 @@ def main() -> None:
joysticks: set[tcod.sdl.joystick.Joystick] = set()
with tcod.context.new(width=WIDTH, height=HEIGHT) as context:
+ if context.sdl_window:
+ context.sdl_window.start_text_input()
console = context.new_console()
while True:
# Display all event items.
@@ -38,24 +39,23 @@ def main() -> None:
for event in tcod.event.wait():
context.convert_event(event) # Set tile coordinates for event.
print(repr(event))
- if isinstance(event, tcod.event.Quit):
- raise SystemExit
- if isinstance(event, tcod.event.WindowResized) and event.type == "WindowSizeChanged":
- 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: # Log all events other than MouseMotion.
- event_log.append(str(event))
+ match event:
+ case tcod.event.Quit():
+ raise SystemExit
+ case tcod.event.WindowResized(type="WindowResized"):
+ console = context.new_console()
+ case tcod.event.ControllerDevice(type="CONTROLLERDEVICEADDED", controller=controller):
+ controllers.add(controller)
+ case tcod.event.ControllerDevice(type="CONTROLLERDEVICEREMOVED", controller=controller):
+ controllers.remove(controller)
+ case tcod.event.JoystickDevice(type="JOYDEVICEADDED", joystick=joystick):
+ joysticks.add(joystick)
+ case tcod.event.JoystickDevice(type="JOYDEVICEREMOVED", joystick=joystick):
+ joysticks.remove(joystick)
+ case tcod.event.MouseMotion():
+ motion_desc = str(event)
+ if not isinstance(event, tcod.event.MouseMotion): # Log all events other than MouseMotion
+ event_log.append(repr(event))
if __name__ == "__main__":
diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py
index 9e0cb41d..71780d97 100755
--- a/examples/samples_libtcodpy.py
+++ b/examples/samples_libtcodpy.py
@@ -12,14 +12,7 @@
import sys
import warnings
-import tcod as libtcod
-
-try: # Import Psyco if available
- import psyco
-
- psyco.full()
-except ImportError:
- pass
+from tcod import tcod as libtcod
if not sys.warnoptions:
warnings.simplefilter("ignore") # Prevent flood of deprecation warnings.
diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py
index 7c08906a..bae9acd6 100755
--- a/examples/samples_tcod.py
+++ b/examples/samples_tcod.py
@@ -13,6 +13,7 @@
import sys
import time
import warnings
+from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -21,6 +22,7 @@
import tcod.bsp
import tcod.cffi
import tcod.console
+import tcod.constants
import tcod.context
import tcod.event
import tcod.image
@@ -33,14 +35,11 @@
import tcod.sdl.render
import tcod.tileset
from tcod import libtcodpy
+from tcod.event import KeySym
if TYPE_CHECKING:
from numpy.typing import NDArray
-# 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."""
@@ -64,8 +63,8 @@
tileset: tcod.tileset.Tileset
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")
+root_console = tcod.console.Console(80, 50)
+sample_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
cur_sample = 0 # Current selected sample.
frame_times = [time.perf_counter()]
frame_length = [0.0]
@@ -77,112 +76,125 @@ def _get_elapsed_time() -> float:
return time.perf_counter() - START_TIME
-class Sample(tcod.event.EventDispatch[None]):
- def __init__(self, name: str = "") -> None:
- self.name = name
+class Sample:
+ """Samples base class."""
+
+ name: str = "???"
def on_enter(self) -> None:
- pass
+ """Called when entering a sample."""
def on_draw(self) -> None:
- pass
+ """Called every frame."""
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
+ def on_event(self, event: tcod.event.Event) -> None:
+ """Called for each event."""
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.KeySym.UP:
- cur_sample = (cur_sample - 1) % len(SAMPLES)
- SAMPLES[cur_sample].on_enter()
- draw_samples_menu()
- elif event.sym == tcod.event.KeySym.RETURN and event.mod & tcod.event.Modifier.ALT:
- sdl_window = context.sdl_window
- if sdl_window:
- sdl_window.fullscreen = not sdl_window.fullscreen
- elif event.sym in (tcod.event.KeySym.PRINTSCREEN, tcod.event.KeySym.P):
- print("screenshot")
- if event.mod & tcod.event.Modifier.ALT:
- libtcodpy.console_save_apf(root_console, "samples.apf")
- print("apf")
- else:
- libtcodpy.sys_save_screenshot()
- print("png")
- elif event.sym in RENDERER_KEYS:
- # Swap the active context for one with a different renderer.
- init_context(RENDERER_KEYS[event.sym])
+ match event:
+ case tcod.event.Quit() | tcod.event.KeyDown(sym=KeySym.ESCAPE):
+ raise SystemExit
+ case tcod.event.KeyDown(sym=KeySym.DOWN):
+ cur_sample = (cur_sample + 1) % len(SAMPLES)
+ SAMPLES[cur_sample].on_enter()
+ draw_samples_menu()
+ case tcod.event.KeyDown(sym=KeySym.UP):
+ cur_sample = (cur_sample - 1) % len(SAMPLES)
+ SAMPLES[cur_sample].on_enter()
+ draw_samples_menu()
+ case tcod.event.KeyDown(sym=KeySym.RETURN, mod=mod) if mod & tcod.event.Modifier.ALT:
+ sdl_window = context.sdl_window
+ if sdl_window:
+ sdl_window.fullscreen = not sdl_window.fullscreen
+ case tcod.event.KeyDown(sym=tcod.event.KeySym.PRINTSCREEN | tcod.event.KeySym.P):
+ print("screenshot")
+ if event.mod & tcod.event.Modifier.ALT:
+ libtcodpy.console_save_apf(root_console, "samples.apf")
+ print("apf")
+ else:
+ libtcodpy.sys_save_screenshot()
+ print("png")
+ case tcod.event.KeyDown(sym=sym) if sym in RENDERER_KEYS:
+ init_context(RENDERER_KEYS[sym]) # Swap the active context for one with a different renderer
class TrueColorSample(Sample):
+ """Simple performance benchmark."""
+
+ name = "True colors"
+
def __init__(self) -> None:
- self.name = "True colors"
- # corner colors
- 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: NDArray[np.int16] = np.array([[1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, 1, -1]], dtype=np.int16)
- # corner indexes
- self.corners: NDArray[np.int16] = np.array([0, 1, 2, 3], dtype=np.int16)
+ """Initialize random generators."""
+ self.noise = tcod.noise.Noise(2, tcod.noise.Algorithm.SIMPLEX)
+ """Noise for generating color."""
+
+ self.generator = np.random.default_rng()
+ """Numpy generator for random text."""
def on_draw(self) -> None:
- self.slide_corner_colors()
+ """Draw this sample."""
self.interpolate_corner_colors()
self.darken_background_characters()
self.randomize_sample_console()
- self.print_banner()
-
- def slide_corner_colors(self) -> None:
- # pick random RGB channels for each corner
- rand_channels = np.random.randint(low=0, high=3, size=4)
-
- # shift picked color channels in the direction of slide_dir
- self.colors[self.corners, rand_channels] += self.slide_dir[self.corners, rand_channels] * 5
+ sample_console.print(
+ x=1,
+ y=5,
+ width=sample_console.width - 2,
+ height=sample_console.height - 1,
+ text="The Doryen library uses 24 bits colors, for both background and foreground.",
+ fg=WHITE,
+ bg=GREY,
+ bg_blend=libtcodpy.BKGND_MULTIPLY,
+ alignment=libtcodpy.CENTER,
+ )
- # reverse slide_dir values when limits are reached
- self.slide_dir[self.colors[:] == 255] = -1
- self.slide_dir[self.colors[:] == 0] = 1
+ def get_corner_colors(self) -> NDArray[np.uint8]:
+ """Return 4 random 8-bit colors, smoothed over time."""
+ noise_samples_ij = (
+ [ # i coordinates are per color channel per color
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ [9, 10, 11],
+ ],
+ time.perf_counter(), # j coordinate is time broadcast to all samples
+ )
+ colors = self.noise[noise_samples_ij]
+ colors = ((colors + 1.0) * (0.5 * 255.0)).clip(min=0, max=255) # Convert -1..1 to 0..255
+ return colors.astype(np.uint8)
def interpolate_corner_colors(self) -> None:
- # interpolate corner colors across the sample console
- left = np.linspace(self.colors[0], self.colors[2], SAMPLE_SCREEN_HEIGHT)
- right = np.linspace(self.colors[1], self.colors[3], SAMPLE_SCREEN_HEIGHT)
- sample_console.bg[:] = np.linspace(left, right, SAMPLE_SCREEN_WIDTH)
+ """Interpolate corner colors across the sample console."""
+ colors = self.get_corner_colors()
+ top = np.linspace(colors[0], colors[1], SAMPLE_SCREEN_WIDTH)
+ bottom = np.linspace(colors[2], colors[3], SAMPLE_SCREEN_WIDTH)
+ sample_console.bg[:] = np.linspace(top, bottom, SAMPLE_SCREEN_HEIGHT)
def darken_background_characters(self) -> None:
- # darken background characters
+ """Darken background characters."""
sample_console.fg[:] = sample_console.bg[:]
sample_console.fg[:] //= 2
def randomize_sample_console(self) -> None:
- # randomize sample console characters
- sample_console.ch[:] = np.random.randint(
+ """Randomize sample console characters."""
+ sample_console.ch[:] = self.generator.integers(
low=ord("a"),
- high=ord("z") + 1,
+ high=ord("z"),
+ endpoint=True,
size=sample_console.ch.size,
dtype=np.intc,
).reshape(sample_console.ch.shape)
- def print_banner(self) -> None:
- # print text on top of samples
- sample_console.print_box(
- x=1,
- y=5,
- width=sample_console.width - 2,
- height=sample_console.height - 1,
- string="The Doryen library uses 24 bits colors, for both background and foreground.",
- fg=WHITE,
- bg=GREY,
- bg_blend=libtcodpy.BKGND_MULTIPLY,
- alignment=libtcodpy.CENTER,
- )
-
class OffscreenConsoleSample(Sample):
+ """Console blit example."""
+
+ name = "Offscreen console"
+
+ CONSOLE_MOVE_RATE = 1 / 2
+ CONSOLE_MOVE_MARGIN = 5
+
def __init__(self) -> None:
- self.name = "Offscreen console"
+ """Initialize the offscreen console."""
self.secondary = tcod.console.Console(sample_console.width // 2, sample_console.height // 2)
self.screenshot = tcod.console.Console(sample_console.width, sample_console.height)
self.counter = 0.0
@@ -191,61 +203,57 @@ def __init__(self) -> None:
self.x_dir = 1
self.y_dir = 1
- self.secondary.draw_frame(
+ self.secondary.draw_frame(0, 0, self.secondary.width, self.secondary.height, clear=False, fg=WHITE, bg=BLACK)
+ self.secondary.print(
0,
0,
- sample_console.width // 2,
- sample_console.height // 2,
- "Offscreen console",
- clear=False,
- fg=WHITE,
- bg=BLACK,
+ width=self.secondary.width,
+ height=self.secondary.height,
+ text=" Offscreen console ",
+ fg=BLACK,
+ bg=WHITE,
+ alignment=tcod.constants.CENTER,
)
- self.secondary.print_box(
- 1,
- 2,
- sample_console.width // 2 - 2,
- sample_console.height // 2,
- "You can render to an offscreen console and blit in on another one, simulating alpha transparency.",
+ self.secondary.print(
+ x=1,
+ y=2,
+ width=sample_console.width // 2 - 2,
+ height=sample_console.height // 2,
+ text="You can render to an offscreen console and blit in on another one, simulating alpha transparency.",
fg=WHITE,
bg=None,
alignment=libtcodpy.CENTER,
)
def on_enter(self) -> None:
+ """Capture the previous sample screen as this samples background."""
self.counter = _get_elapsed_time()
- # get a "screenshot" of the current sample screen
- sample_console.blit(dest=self.screenshot)
+ sample_console.blit(dest=self.screenshot) # get a "screenshot" of the current sample screen
def on_draw(self) -> None:
- if _get_elapsed_time() - self.counter >= 1:
+ """Draw and animate the offscreen console."""
+ if _get_elapsed_time() - self.counter >= self.CONSOLE_MOVE_RATE:
self.counter = _get_elapsed_time()
self.x += self.x_dir
self.y += self.y_dir
- if self.x == sample_console.width / 2 + 5:
+ if self.x == sample_console.width / 2 + self.CONSOLE_MOVE_MARGIN:
self.x_dir = -1
- elif self.x == -5:
+ elif self.x == -self.CONSOLE_MOVE_MARGIN:
self.x_dir = 1
- if self.y == sample_console.height / 2 + 5:
+ if self.y == sample_console.height / 2 + self.CONSOLE_MOVE_MARGIN:
self.y_dir = -1
- elif self.y == -5:
+ elif self.y == -self.CONSOLE_MOVE_MARGIN:
self.y_dir = 1
self.screenshot.blit(sample_console)
self.secondary.blit(
- sample_console,
- self.x,
- self.y,
- 0,
- 0,
- sample_console.width // 2,
- sample_console.height // 2,
- 1.0,
- 0.75,
+ sample_console, self.x, self.y, 0, 0, sample_console.width // 2, sample_console.height // 2, 1.0, 0.75
)
class LineDrawingSample(Sample):
+ name = "Line drawing"
+
FLAG_NAMES = (
"BKGND_NONE",
"BKGND_SET",
@@ -263,24 +271,24 @@ class LineDrawingSample(Sample):
)
def __init__(self) -> None:
- self.name = "Line drawing"
self.mk_flag = libtcodpy.BKGND_SET
self.bk_flag = libtcodpy.BKGND_SET
- self.bk = tcod.console.Console(sample_console.width, sample_console.height, order="F")
+ self.background = tcod.console.Console(sample_console.width, sample_console.height)
# initialize the colored background
- self.bk.bg[:, :, 0] = np.linspace(0, 255, self.bk.width)[:, np.newaxis]
- self.bk.bg[:, :, 2] = np.linspace(0, 255, self.bk.height)
- self.bk.bg[:, :, 1] = (self.bk.bg[:, :, 0].astype(int) + self.bk.bg[:, :, 2]) / 2
- self.bk.ch[:] = ord(" ")
-
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym in (tcod.event.KeySym.RETURN, tcod.event.KeySym.KP_ENTER):
- self.bk_flag += 1
- if (self.bk_flag & 0xFF) > libtcodpy.BKGND_ALPH:
- self.bk_flag = libtcodpy.BKGND_NONE
- else:
- super().ev_keydown(event)
+ self.background.bg[:, :, 0] = np.linspace(0, 255, self.background.width)
+ self.background.bg[:, :, 2] = np.linspace(0, 255, self.background.height)[:, np.newaxis]
+ self.background.bg[:, :, 1] = (self.background.bg[:, :, 0].astype(int) + self.background.bg[:, :, 2]) / 2
+ self.background.ch[:] = ord(" ")
+
+ def on_event(self, event: tcod.event.Event) -> None:
+ match event:
+ case tcod.event.KeyDown(sym=KeySym.RETURN | KeySym.KP_ENTER):
+ self.bk_flag += 1
+ if (self.bk_flag & 0xFF) > libtcodpy.BKGND_ALPH:
+ self.bk_flag = libtcodpy.BKGND_NONE
+ case _:
+ super().on_event(event)
def on_draw(self) -> None:
alpha = 0.0
@@ -293,14 +301,12 @@ def on_draw(self) -> None:
alpha = (1.0 + math.cos(time.time() * 2)) / 2.0
self.bk_flag = libtcodpy.BKGND_ADDALPHA(int(alpha))
- self.bk.blit(sample_console)
+ self.background.blit(sample_console)
rect_y = int((sample_console.height - 2) * ((1.0 + math.cos(time.time())) / 2.0))
for x in range(sample_console.width):
value = x * 255 // sample_console.width
col = (value, value, value)
- libtcodpy.console_set_char_background(sample_console, x, rect_y, col, self.bk_flag)
- libtcodpy.console_set_char_background(sample_console, x, rect_y + 1, col, self.bk_flag)
- libtcodpy.console_set_char_background(sample_console, x, rect_y + 2, col, self.bk_flag)
+ sample_console.draw_rect(x=x, y=rect_y, width=1, height=3, ch=0, fg=None, bg=col, bg_blend=self.bk_flag)
angle = time.time() * 2.0
cos_angle = math.cos(angle)
sin_angle = math.sin(angle)
@@ -312,17 +318,13 @@ def on_draw(self) -> None:
# in python the easiest way is to use the line iterator
for x, y in tcod.los.bresenham((xo, yo), (xd, yd)).tolist():
if 0 <= x < sample_console.width and 0 <= y < sample_console.height:
- libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_BLUE, self.bk_flag)
- sample_console.print(
- 2,
- 2,
- f"{self.FLAG_NAMES[self.bk_flag & 0xFF]} (ENTER to change)",
- fg=WHITE,
- bg=None,
- )
+ sample_console.draw_rect(x, y, width=1, height=1, ch=0, fg=None, bg=LIGHT_BLUE, bg_blend=self.bk_flag)
+ sample_console.print(2, 2, f"{self.FLAG_NAMES[self.bk_flag & 0xFF]} (ENTER to change)", fg=WHITE, bg=None)
class NoiseSample(Sample):
+ name = "Noise"
+
NOISE_OPTIONS = ( # (name, algorithm, implementation)
(
"perlin noise",
@@ -372,7 +374,6 @@ class NoiseSample(Sample):
)
def __init__(self) -> None:
- self.name = "Noise"
self.func = 0
self.dx = 0.0
self.dy = 0.0
@@ -430,8 +431,8 @@ def on_draw(self) -> None:
bg=GREY,
bg_blend=libtcodpy.BKGND_MULTIPLY,
)
- sample_console.fg[2 : 2 + rect_w, 2 : 2 + rect_h] = (
- sample_console.fg[2 : 2 + rect_w, 2 : 2 + rect_h] * GREY / 255
+ sample_console.fg[2 : 2 + rect_h, 2 : 2 + rect_w] = (
+ sample_console.fg[2 : 2 + rect_h, 2 : 2 + rect_w] * GREY / 255
)
for cur_func in range(len(self.NOISE_OPTIONS)):
@@ -464,34 +465,35 @@ def on_draw(self) -> None:
bg=None,
)
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- 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 == tcod.event.KeySym.E:
- self.hurst += 0.1
- self.noise = self.get_noise()
- elif event.sym == tcod.event.KeySym.D:
- self.hurst -= 0.1
- self.noise = self.get_noise()
- elif event.sym == tcod.event.KeySym.R:
- self.lacunarity += 0.5
- self.noise = self.get_noise()
- elif event.sym == tcod.event.KeySym.F:
- self.lacunarity -= 0.5
- self.noise = self.get_noise()
- elif event.sym == tcod.event.KeySym.T:
- self.octaves += 0.5
- self.noise.octaves = self.octaves
- elif event.sym == tcod.event.KeySym.G:
- self.octaves -= 0.5
- self.noise.octaves = self.octaves
- elif event.sym == tcod.event.KeySym.Y:
- self.zoom += 0.2
- elif event.sym == tcod.event.KeySym.H:
- self.zoom -= 0.2
- else:
- super().ev_keydown(event)
+ def on_event(self, event: tcod.event.Event) -> None:
+ match event:
+ case tcod.event.KeyDown(sym=sym) if KeySym.N9 >= sym >= KeySym.N1:
+ self.func = sym - tcod.event.KeySym.N1
+ self.noise = self.get_noise()
+ case tcod.event.KeyDown(sym=KeySym.E):
+ self.hurst += 0.1
+ self.noise = self.get_noise()
+ case tcod.event.KeyDown(sym=KeySym.D):
+ self.hurst -= 0.1
+ self.noise = self.get_noise()
+ case tcod.event.KeyDown(sym=KeySym.R):
+ self.lacunarity += 0.5
+ self.noise = self.get_noise()
+ case tcod.event.KeyDown(sym=KeySym.F):
+ self.lacunarity -= 0.5
+ self.noise = self.get_noise()
+ case tcod.event.KeyDown(sym=KeySym.T):
+ self.octaves += 0.5
+ self.noise.octaves = self.octaves
+ case tcod.event.KeyDown(sym=KeySym.G):
+ self.octaves -= 0.5
+ self.noise.octaves = self.octaves
+ case tcod.event.KeyDown(sym=KeySym.Y):
+ self.zoom += 0.2
+ case tcod.event.KeyDown(sym=KeySym.H):
+ self.zoom -= 0.2
+ case _:
+ super().on_event(event)
#############################################
@@ -525,7 +527,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
"##############################################",
)
-SAMPLE_MAP: NDArray[Any] = np.array([[ord(c) for c in line] for line in SAMPLE_MAP_]).transpose()
+SAMPLE_MAP: NDArray[Any] = np.array([[ord(c) for c in line] for line in SAMPLE_MAP_])
FOV_ALGO_NAMES = (
"BASIC ",
@@ -549,9 +551,9 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
class FOVSample(Sample):
- def __init__(self) -> None:
- self.name = "Field of view"
+ name = "Field of view"
+ def __init__(self) -> None:
self.player_x = 20
self.player_y = 10
self.torch = False
@@ -559,12 +561,12 @@ def __init__(self) -> None:
self.algo_num = libtcodpy.FOV_SYMMETRIC_SHADOWCAST
self.noise = tcod.noise.Noise(1) # 1D noise for the torch flickering.
- map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
+ map_shape = (SAMPLE_SCREEN_HEIGHT, SAMPLE_SCREEN_WIDTH)
- self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
+ self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool)
self.walkable[:] = SAMPLE_MAP[:] == ord(" ")
- self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
+ self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool)
self.transparent[:] = self.walkable[:] | (SAMPLE_MAP[:] == ord("="))
# Lit background colors for the map.
@@ -599,7 +601,7 @@ def on_draw(self) -> None:
# Get a 2D boolean array of visible cells.
fov = tcod.map.compute_fov(
transparency=self.transparent,
- pov=(self.player_x, self.player_y),
+ pov=(self.player_y, self.player_x),
radius=TORCH_RADIUS if self.torch else 0,
light_walls=self.light_walls,
algorithm=self.algo_num,
@@ -615,7 +617,7 @@ def on_draw(self) -> None:
brightness = 0.2 * self.noise.get_point(torch_t + 17)
# Get the squared distance using a mesh grid.
- x, y = np.mgrid[:SAMPLE_SCREEN_WIDTH, :SAMPLE_SCREEN_HEIGHT]
+ y, x = np.mgrid[:SAMPLE_SCREEN_HEIGHT, :SAMPLE_SCREEN_WIDTH]
# Center the mesh grid on the torch position.
x = x.astype(np.float32) - torch_x
y = y.astype(np.float32) - torch_y
@@ -645,7 +647,7 @@ def on_draw(self) -> None:
default=self.dark_map_bg,
)
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
+ def on_event(self, event: tcod.event.Event) -> None:
MOVE_KEYS = { # noqa: N806
tcod.event.KeySym.I: (0, -1),
tcod.event.KeySym.J: (-1, 0),
@@ -658,34 +660,35 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
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 == tcod.event.KeySym.T:
- self.torch = not self.torch
- 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]
- self.algo_num %= len(FOV_ALGO_NAMES)
- else:
- super().ev_keydown(event)
+ match event:
+ case tcod.event.KeyDown(sym=sym) if sym in MOVE_KEYS:
+ x, y = MOVE_KEYS[sym]
+ if self.walkable[self.player_y + y, self.player_x + x]:
+ self.player_x += x
+ self.player_y += y
+ case tcod.event.KeyDown(sym=KeySym.T):
+ self.torch = not self.torch
+ case tcod.event.KeyDown(sym=KeySym.W):
+ self.light_walls = not self.light_walls
+ case tcod.event.KeyDown(sym=sym) if sym in FOV_SELECT_KEYS:
+ self.algo_num += FOV_SELECT_KEYS[sym]
+ self.algo_num %= len(FOV_ALGO_NAMES)
+ case _:
+ super().on_event(event)
class PathfindingSample(Sample):
+ name = "Path finding"
+
def __init__(self) -> None:
"""Initialize this sample."""
- self.name = "Path finding"
-
self.player_x = 20
self.player_y = 10
self.dest_x = 24
self.dest_y = 1
self.using_astar = True
self.busy = 0.0
- self.cost = SAMPLE_MAP.T[:] == ord(" ")
+ self.cost = SAMPLE_MAP[:] == ord(" ")
self.graph = tcod.path.SimpleGraph(cost=self.cost, cardinal=70, diagonal=99)
self.pathfinder = tcod.path.Pathfinder(graph=self.graph)
@@ -694,8 +697,8 @@ def __init__(self) -> None:
# draw the dungeon
self.background_console.rgb["fg"] = BLACK
self.background_console.rgb["bg"] = DARK_GROUND
- self.background_console.rgb["bg"][SAMPLE_MAP.T[:] == ord("#")] = DARK_WALL
- self.background_console.rgb["ch"][SAMPLE_MAP.T[:] == ord("=")] = ord("═")
+ self.background_console.rgb["bg"][SAMPLE_MAP[:] == ord("#")] = DARK_WALL
+ self.background_console.rgb["ch"][SAMPLE_MAP[:] == ord("=")] = ord("═")
def on_enter(self) -> None:
"""Do nothing."""
@@ -723,10 +726,10 @@ def on_draw(self) -> None:
np.array(self.pathfinder.distance, copy=True, dtype=np.float32)
interpolate = self.pathfinder.distance[reachable] * 0.9 / dijkstra_max_dist
color_delta = (np.array(DARK_GROUND) - np.array(LIGHT_GROUND)).astype(np.float32)
- sample_console.rgb.T["bg"][reachable] = np.array(LIGHT_GROUND) + interpolate[:, np.newaxis] * color_delta
+ sample_console.rgb["bg"][reachable] = np.array(LIGHT_GROUND) + interpolate[:, np.newaxis] * color_delta
# draw the path
- path = self.pathfinder.path_to((self.dest_y, self.dest_x))[1:, ::-1]
+ path = self.pathfinder.path_to((self.dest_y, self.dest_x))[1:]
sample_console.rgb["bg"][tuple(path.T)] = LIGHT_GROUND
# move the creature
@@ -734,90 +737,90 @@ def on_draw(self) -> None:
if self.busy <= 0.0:
self.busy = 0.2
if len(path):
- self.player_x = int(path.item(0, 0))
- self.player_y = int(path.item(0, 1))
+ self.player_y = int(path.item(0, 0))
+ self.player_x = int(path.item(0, 1))
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
+ def on_event(self, event: tcod.event.Event) -> None:
"""Handle movement and UI."""
- if event.sym == tcod.event.KeySym.I and self.dest_y > 0: # destination move north
- self.dest_y -= 1
- elif event.sym == tcod.event.KeySym.K and self.dest_y < SAMPLE_SCREEN_HEIGHT - 1: # destination move south
- self.dest_y += 1
- elif event.sym == tcod.event.KeySym.J and self.dest_x > 0: # destination move west
- self.dest_x -= 1
- elif event.sym == tcod.event.KeySym.L and self.dest_x < SAMPLE_SCREEN_WIDTH - 1: # destination move east
- self.dest_x += 1
- elif event.sym == tcod.event.KeySym.TAB:
- self.using_astar = not self.using_astar
- else:
- super().ev_keydown(event)
-
- def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
- """Move destination via mouseover."""
- mx = event.tile.x - SAMPLE_SCREEN_X
- my = event.tile.y - SAMPLE_SCREEN_Y
- if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT:
- self.dest_x = int(mx)
- self.dest_y = int(my)
+ match event:
+ case tcod.event.KeyDown(sym=KeySym.I) if self.dest_y > 0: # destination move north
+ self.dest_y -= 1
+ case tcod.event.KeyDown(sym=KeySym.K) if self.dest_y < SAMPLE_SCREEN_HEIGHT - 1: # destination move south
+ self.dest_y += 1
+ case tcod.event.KeyDown(sym=KeySym.J) if self.dest_x > 0: # destination move west
+ self.dest_x -= 1
+ case tcod.event.KeyDown(sym=KeySym.L) if self.dest_x < SAMPLE_SCREEN_WIDTH - 1: # destination move east
+ self.dest_x += 1
+ case tcod.event.KeyDown(sym=KeySym.TAB):
+ self.using_astar = not self.using_astar
+ case tcod.event.MouseMotion(): # Move destination via mouseover
+ mx = event.tile.x - SAMPLE_SCREEN_X
+ my = event.tile.y - SAMPLE_SCREEN_Y
+ if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT:
+ self.dest_x = int(mx)
+ self.dest_y = int(my)
+ case _:
+ super().on_event(event)
#############################################
# bsp sample
#############################################
-bsp_depth = 8
-bsp_min_room_size = 4
-# a room fills a random part of the node or the maximum available space ?
-bsp_random_room = False
-# if true, there is always a wall on north & west side of a room
-bsp_room_walls = True
# draw a vertical line
-def vline(m: NDArray[np.bool_], x: int, y1: int, y2: int) -> None:
+def vline(map_: NDArray[np.bool_], x: int, y1: int, y2: int) -> None:
if y1 > y2:
y1, y2 = y2, y1
for y in range(y1, y2 + 1):
- m[x, y] = True
+ map_[y, x] = True
# draw a vertical line up until we reach an empty space
-def vline_up(m: NDArray[np.bool_], x: int, y: int) -> None:
- while y >= 0 and not m[x, y]:
- m[x, y] = True
+def vline_up(map_: NDArray[np.bool_], x: int, y: int) -> None:
+ while y >= 0 and not map_[y, x]:
+ map_[y, x] = True
y -= 1
# draw a vertical line down until we reach an empty space
-def vline_down(m: NDArray[np.bool_], x: int, y: int) -> None:
- while y < SAMPLE_SCREEN_HEIGHT and not m[x, y]:
- m[x, y] = True
+def vline_down(map_: NDArray[np.bool_], x: int, y: int) -> None:
+ while y < SAMPLE_SCREEN_HEIGHT and not map_[y, x]:
+ map_[y, x] = True
y += 1
# draw a horizontal line
-def hline(m: NDArray[np.bool_], x1: int, y: int, x2: int) -> None:
+def hline(map_: NDArray[np.bool_], x1: int, y: int, x2: int) -> None:
if x1 > x2:
x1, x2 = x2, x1
for x in range(x1, x2 + 1):
- m[x, y] = True
+ map_[y, x] = True
# draw a horizontal line left until we reach an empty space
-def hline_left(m: NDArray[np.bool_], x: int, y: int) -> None:
- while x >= 0 and not m[x, y]:
- m[x, y] = True
+def hline_left(map_: NDArray[np.bool_], x: int, y: int) -> None:
+ while x >= 0 and not map_[y, x]:
+ map_[y, x] = True
x -= 1
# draw a horizontal line right until we reach an empty space
-def hline_right(m: NDArray[np.bool_], x: int, y: int) -> None:
- while x < SAMPLE_SCREEN_WIDTH and not m[x, y]:
- m[x, y] = True
+def hline_right(map_: NDArray[np.bool_], x: int, y: int) -> None:
+ while x < SAMPLE_SCREEN_WIDTH and not map_[y, x]:
+ map_[y, x] = True
x += 1
# the class building the dungeon from the bsp nodes
-def traverse_node(bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP) -> None:
+def traverse_node(
+ bsp_map: NDArray[np.bool_],
+ node: tcod.bsp.BSP,
+ *,
+ bsp_min_room_size: int,
+ bsp_random_room: bool,
+ bsp_room_walls: bool,
+) -> None:
if not node.children:
# calculate the room size
if bsp_room_walls:
@@ -830,7 +833,7 @@ def traverse_node(bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP) -> None:
node.y += random.randint(0, node.height - new_height)
node.width, node.height = new_width, new_height
# dig the room
- bsp_map[node.x : node.x + node.width, node.y : node.y + node.height] = True
+ bsp_map[node.y : node.y + node.height, node.x : node.x + node.width] = True
else:
# resize the node to fit its sons
left, right = node.children
@@ -874,92 +877,105 @@ def traverse_node(bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP) -> None:
class BSPSample(Sample):
+ name = "Bsp toolkit"
+
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: NDArray[np.bool_] = np.zeros((SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT), dtype=bool, order="F")
+ self.bsp_map: NDArray[np.bool_] = np.zeros((SAMPLE_SCREEN_HEIGHT, SAMPLE_SCREEN_WIDTH), dtype=bool)
+
+ self.bsp_depth = 8
+ self.bsp_min_room_size = 4
+ self.bsp_random_room = False # a room fills a random part of the node or the maximum available space ?
+ self.bsp_room_walls = True # if true, there is always a wall on north & west side of a room
+
self.bsp_generate()
def bsp_generate(self) -> None:
self.bsp.children = ()
- if bsp_room_walls:
+ if self.bsp_room_walls:
self.bsp.split_recursive(
- bsp_depth,
- bsp_min_room_size + 1,
- bsp_min_room_size + 1,
+ self.bsp_depth,
+ self.bsp_min_room_size + 1,
+ self.bsp_min_room_size + 1,
1.5,
1.5,
)
else:
- self.bsp.split_recursive(bsp_depth, bsp_min_room_size, bsp_min_room_size, 1.5, 1.5)
+ self.bsp.split_recursive(self.bsp_depth, self.bsp_min_room_size, self.bsp_min_room_size, 1.5, 1.5)
self.bsp_refresh()
def bsp_refresh(self) -> None:
self.bsp_map[...] = False
for node in copy.deepcopy(self.bsp).inverted_level_order():
- traverse_node(self.bsp_map, node)
+ traverse_node(
+ self.bsp_map,
+ node,
+ bsp_min_room_size=self.bsp_min_room_size,
+ bsp_random_room=self.bsp_random_room,
+ bsp_room_walls=self.bsp_room_walls,
+ )
def on_draw(self) -> None:
sample_console.clear()
rooms = "OFF"
- if bsp_random_room:
+ if self.bsp_random_room:
rooms = "ON"
sample_console.print(
1,
1,
"ENTER : rebuild bsp\n"
"SPACE : rebuild dungeon\n"
- f"+-: bsp depth {bsp_depth}\n"
- f"*/: room size {bsp_min_room_size}\n"
+ f"+-: bsp depth {self.bsp_depth}\n"
+ f"*/: room size {self.bsp_min_room_size}\n"
f"1 : random room size {rooms}",
fg=WHITE,
bg=None,
)
- if bsp_random_room:
+ if self.bsp_random_room:
walls = "OFF"
- if bsp_room_walls:
+ if self.bsp_room_walls:
walls = "ON"
sample_console.print(1, 6, f"2 : room walls {walls}", fg=WHITE, bg=None)
# render the level
for y in range(SAMPLE_SCREEN_HEIGHT):
for x in range(SAMPLE_SCREEN_WIDTH):
- color = DARK_GROUND if self.bsp_map[x][y] else DARK_WALL
+ color = DARK_GROUND if self.bsp_map[y, x] else DARK_WALL
libtcodpy.console_set_char_background(sample_console, x, y, color, libtcodpy.BKGND_SET)
- 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.KeySym.RETURN, tcod.event.KeySym.KP_ENTER):
- self.bsp_generate()
- elif event.sym == tcod.event.KeySym.SPACE:
- self.bsp_refresh()
- 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.KeySym.MINUS, tcod.event.KeySym.KP_MINUS):
- bsp_depth = max(1, bsp_depth - 1)
- self.bsp_generate()
- 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.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.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.KeySym.N2, tcod.event.KeySym.KP_2):
- bsp_room_walls = not bsp_room_walls
- self.bsp_refresh()
- else:
- super().ev_keydown(event)
+ def on_event(self, event: tcod.event.Event) -> None:
+ match event:
+ case tcod.event.KeyDown(sym=KeySym.RETURN | KeySym.KP_ENTER):
+ self.bsp_generate()
+ case tcod.event.KeyDown(sym=KeySym.SPACE):
+ self.bsp_refresh()
+ case tcod.event.KeyDown(sym=KeySym.EQUALS | KeySym.KP_PLUS):
+ self.bsp_depth += 1
+ self.bsp_generate()
+ case tcod.event.KeyDown(sym=KeySym.MINUS | KeySym.KP_MINUS):
+ self.bsp_depth = max(1, self.bsp_depth - 1)
+ self.bsp_generate()
+ case tcod.event.KeyDown(sym=KeySym.N8 | KeySym.KP_MULTIPLY):
+ self.bsp_min_room_size += 1
+ self.bsp_generate()
+ case tcod.event.KeyDown(sym=KeySym.SLASH | KeySym.KP_DIVIDE):
+ self.bsp_min_room_size = max(2, self.bsp_min_room_size - 1)
+ self.bsp_generate()
+ case tcod.event.KeyDown(sym=KeySym.N1 | KeySym.KP_1):
+ self.bsp_random_room = not self.bsp_random_room
+ if not self.bsp_random_room:
+ self.bsp_room_walls = True
+ self.bsp_refresh()
+ case tcod.event.KeyDown(sym=KeySym.N2 | KeySym.KP_2):
+ self.bsp_room_walls = not self.bsp_room_walls
+ self.bsp_refresh()
+ case _:
+ super().on_event(event)
class ImageSample(Sample):
- def __init__(self) -> None:
- self.name = "Image toolkit"
+ name = "Image toolkit"
+ def __init__(self) -> None:
self.img = tcod.image.Image.from_file(DATA_DIR / "img/skull.png")
self.img.set_key_color(BLACK)
self.circle = tcod.image.Image.from_file(DATA_DIR / "img/circle.png")
@@ -991,11 +1007,10 @@ def on_draw(self) -> None:
class MouseSample(Sample):
- def __init__(self) -> None:
- self.name = "Mouse support"
+ name = "Mouse support"
+ def __init__(self) -> None:
self.motion = tcod.event.MouseMotion()
- self.mouse_left = self.mouse_middle = self.mouse_right = 0
self.log: list[str] = []
def on_enter(self) -> None:
@@ -1004,36 +1019,20 @@ def on_enter(self) -> None:
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
-
- def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> None:
- if event.button == tcod.event.BUTTON_LEFT:
- self.mouse_left = True
- elif event.button == tcod.event.BUTTON_MIDDLE:
- self.mouse_middle = True
- elif event.button == tcod.event.BUTTON_RIGHT:
- self.mouse_right = True
-
- def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> None:
- if event.button == tcod.event.BUTTON_LEFT:
- self.mouse_left = False
- elif event.button == tcod.event.BUTTON_MIDDLE:
- self.mouse_middle = False
- elif event.button == tcod.event.BUTTON_RIGHT:
- self.mouse_right = False
-
def on_draw(self) -> None:
sample_console.clear(bg=GREY)
+ mouse_state = tcod.event.get_mouse_state()
sample_console.print(
1,
1,
- f"Pixel position : {self.motion.position.x:4.0f}x{self.motion.position.y:4.0f}\n"
- f"Tile position : {self.motion.tile.x:4.0f}x{self.motion.tile.y:4.0f}\n"
- f"Tile movement : {self.motion.tile_motion.x:4.0f}x{self.motion.tile_motion.y:4.0f}\n"
- f"Left button : {'ON' if self.mouse_left else 'OFF'}\n"
- f"Right button : {'ON' if self.mouse_right else 'OFF'}\n"
- f"Middle button : {'ON' if self.mouse_middle else 'OFF'}\n",
+ f"Pixel position : {mouse_state.position.x:4.0f}x{mouse_state.position.y:4.0f}\n"
+ f"Tile position : {self.motion.tile.x:4d}x{self.motion.tile.y:4d}\n"
+ f"Tile movement : {self.motion.tile_motion.x:4d}x{self.motion.tile_motion.y:4d}\n"
+ f"Left button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.LEFT else 'OFF'}\n"
+ f"Middle button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.MIDDLE else 'OFF'}\n"
+ f"Right button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.RIGHT else 'OFF'}\n"
+ f"X1 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X1 else 'OFF'}\n"
+ f"X2 button : {'ON' if mouse_state.state & tcod.event.MouseButtonMask.X2 else 'OFF'}\n",
fg=LIGHT_YELLOW,
bg=None,
)
@@ -1045,19 +1044,22 @@ def on_draw(self) -> None:
bg=None,
)
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- 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)
+ def on_event(self, event: tcod.event.Event) -> None:
+ match event:
+ case tcod.event.MouseMotion():
+ self.motion = event
+ case tcod.event.KeyDown(sym=KeySym.N1):
+ tcod.sdl.mouse.show(visible=False)
+ case tcod.event.KeyDown(sym=KeySym.N2):
+ tcod.sdl.mouse.show(visible=True)
+ case _:
+ super().on_event(event)
class NameGeneratorSample(Sample):
- def __init__(self) -> None:
- self.name = "Name generator"
+ name = "Name generator"
+ def __init__(self) -> None:
self.current_set = 0
self.delay = 0.0
self.names: list[str] = []
@@ -1096,24 +1098,22 @@ def on_draw(self) -> None:
self.delay -= 0.5
self.names.append(libtcodpy.namegen_generate(self.sets[self.current_set]))
- def ev_keydown(self, event: tcod.event.KeyDown) -> None:
- if event.sym == tcod.event.KeySym.EQUALS:
- self.current_set += 1
- self.names.append("======")
- elif event.sym == tcod.event.KeySym.MINUS:
- self.current_set -= 1
- self.names.append("======")
- else:
- super().ev_keydown(event)
+ def on_event(self, event: tcod.event.Event) -> None:
+ match event:
+ case tcod.event.KeyDown(sym=KeySym.EQUALS):
+ self.current_set += 1
+ self.names.append("======")
+ case tcod.event.KeyDown(sym=KeySym.MINUS):
+ self.current_set -= 1
+ self.names.append("======")
+ case _:
+ super().on_event(event)
self.current_set %= len(self.sets)
#############################################
# python fast render sample
#############################################
-numpy_available = True
-
-use_numpy = numpy_available # default option
SCREEN_W = SAMPLE_SCREEN_WIDTH
SCREEN_H = SAMPLE_SCREEN_HEIGHT
HALF_W = SCREEN_W // 2
@@ -1133,36 +1133,31 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
# example: (4x3 pixels screen)
# xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] # noqa: ERA001
# yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] # noqa: ERA001
-if numpy_available:
- (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.Noise(2, hurst=0.5, lacunarity=2.0)
-if numpy_available: # the texture starts empty
- texture = np.zeros((RES_U, RES_V))
+(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
+@dataclass(frozen=False, slots=True)
class Light:
- def __init__(
- self,
- x: float,
- y: float,
- z: float,
- r: int,
- g: int,
- b: int,
- strength: float,
- ) -> None:
- self.x, self.y, self.z = x, y, z # pos.
- self.r, self.g, self.b = r, g, b # color
- self.strength = strength # between 0 and 1, defines brightness
+ """Lighting effect entity."""
+
+ x: float # pos
+ y: float
+ z: float
+ r: int # color
+ g: int
+ b: int
+ strength: float # between 0 and 1, defines brightness
class FastRenderSample(Sample):
+ name = "Python fast render"
+
def __init__(self) -> None:
- self.name = "Python fast render"
+ self.texture = np.zeros((RES_U, RES_V))
+ self.noise2d = tcod.noise.Noise(2, hurst=0.5, lacunarity=2.0)
def on_enter(self) -> None:
sample_console.clear() # render status message
@@ -1179,8 +1174,6 @@ def on_enter(self) -> None:
self.tex_b = 0.0
def on_draw(self) -> None:
- global texture
-
time_delta = frame_length[-1] * SPEED # advance time
self.frac_t += time_delta # increase fractional (always < 1.0) time
self.abs_t += time_delta # increase absolute elapsed time
@@ -1208,14 +1201,14 @@ def on_draw(self) -> None:
# new pixels are based on absolute elapsed time
int_abs_t = int(self.abs_t)
- texture = np.roll(texture, -int_t, 1)
+ self.texture = np.roll(self.texture, -int_t, 1)
# replace new stretch of texture with new values
for v in range(RES_V - int_t, RES_V):
for u in range(RES_U):
tex_v = (v + int_abs_t) / float(RES_V)
- texture[u, v] = libtcodpy.noise_get_fbm(
- noise2d, [u / float(RES_U), tex_v], 32.0
- ) + libtcodpy.noise_get_fbm(noise2d, [1 - u / float(RES_U), tex_v], 32.0)
+ self.texture[u, v] = libtcodpy.noise_get_fbm(
+ self.noise2d, [u / float(RES_U), tex_v], 32.0
+ ) + libtcodpy.noise_get_fbm(self.noise2d, [1 - u / float(RES_U), tex_v], 32.0)
# squared distance from center,
# clipped to sensible minimum and maximum values
@@ -1230,7 +1223,7 @@ def on_draw(self) -> None:
uu = np.mod(RES_U * (np.arctan2(yc, xc) / (2 * np.pi) + 0.5), RES_U)
# retrieve corresponding pixels from texture
- brightness = texture[uu.astype(int), vv.astype(int)] / 4.0 + 0.5
+ brightness = self.texture[uu.astype(int), vv.astype(int)] / 4.0 + 0.5
# use the brightness map to compose the final color of the tunnel
rr = brightness * self.tex_r
@@ -1254,7 +1247,7 @@ def on_draw(self) -> None:
for light in self.lights: # render lights
# move light's Z coordinate with time, then project its XYZ
# coordinates to screen-space
- light.z -= float(time_delta) / TEX_STRETCH
+ light.z -= time_delta / TEX_STRETCH
xl = light.x / light.z * SCREEN_H
yl = light.y / light.z * SCREEN_H
@@ -1275,7 +1268,7 @@ def on_draw(self) -> None:
bb = bb.clip(0, 255)
# fill the screen with these background colors
- sample_console.bg.transpose(2, 1, 0)[...] = (rr, gg, bb)
+ sample_console.bg.transpose(2, 0, 1)[...] = (rr, gg, bb)
#############################################
@@ -1349,43 +1342,14 @@ def init_context(renderer: int) -> None:
def main() -> None:
- global context, tileset
+ global tileset
tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD)
init_context(libtcodpy.RENDERER_SDL2)
try:
SAMPLES[cur_sample].on_enter()
while True:
- root_console.clear()
- draw_samples_menu()
- draw_renderer_menu()
-
- # render the sample
- SAMPLES[cur_sample].on_draw()
- sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y)
- draw_stats()
- if context.sdl_renderer:
- # Clear the screen to ensure no garbage data outside of the logical area is displayed
- context.sdl_renderer.draw_color = (0, 0, 0, 255)
- context.sdl_renderer.clear()
- # 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)
-
+ redraw_display()
handle_time()
handle_events()
finally:
@@ -1395,6 +1359,41 @@ def main() -> None:
context.close()
+def redraw_display() -> None:
+ """Full clear-draw-present of the screen."""
+ root_console.clear()
+ draw_samples_menu()
+ draw_renderer_menu()
+
+ # render the sample
+ SAMPLES[cur_sample].on_draw()
+ sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y)
+ draw_stats()
+ if 0 <= mouse_tile_xy[0] < root_console.width and 0 <= mouse_tile_xy[1] < root_console.height:
+ root_console.rgb[["fg", "bg"]].T[mouse_tile_xy] = (0, 0, 0), (255, 255, 255) # Highlight mouse tile
+ if context.sdl_renderer:
+ # Clear the screen to ensure no garbage data outside of the logical area is displayed
+ context.sdl_renderer.draw_color = (0, 0, 0, 255)
+ context.sdl_renderer.clear()
+ # SDL renderer support, upload the sample console background to a minimap texture.
+ sample_minimap.update(sample_console.rgb["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)
+
+
def handle_time() -> None:
if len(frame_times) > 100:
frame_times.pop(0)
@@ -1403,28 +1402,20 @@ def handle_time() -> None:
frame_length.append(frame_times[-1] - frame_times[-2])
+mouse_tile_xy = (-1, -1)
+"""Last known mouse tile position."""
+
+
def handle_events() -> None:
+ global mouse_tile_xy
for event in tcod.event.get():
- 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
- if isinstance(event, tcod.event.KeyDown) and event.sym == tcod.event.KeySym.ESCAPE:
- raise SystemExit
+ tile_event = tcod.event.convert_coordinates_from_window(event, context, root_console)
+ SAMPLES[cur_sample].on_event(tile_event)
+ match tile_event:
+ case tcod.event.MouseMotion(integer_position=(x, y)):
+ mouse_tile_xy = x, y
+ case tcod.event.WindowEvent(type="WindowLeave"):
+ mouse_tile_xy = -1, -1
def draw_samples_menu() -> None:
@@ -1485,4 +1476,15 @@ def draw_renderer_menu() -> None:
if __name__ == "__main__":
+ if not sys.warnoptions:
+ warnings.simplefilter("default") # Show all warnings.
+
+ @tcod.event.add_watch
+ def _handle_events(event: tcod.event.Event) -> None:
+ """Keep window responsive during resize events."""
+ match event:
+ case tcod.event.WindowEvent(type="WindowExposed"):
+ redraw_display()
+ handle_time()
+
main()
diff --git a/examples/termbox/README.md b/examples/termbox/README.md
deleted file mode 100644
index 36f13c20..00000000
--- a/examples/termbox/README.md
+++ /dev/null
@@ -1,57 +0,0 @@
-API of `termbox` Python module implemented in `tld`.
-
-The code here are modified files from
-[termbox repository](https://github.com/nsf/termbox/), so please consult
-it for the license and other info.
-
-The code consists of two part - `termbox.py` module with API, translation
-of official binding form the description below into `tld`:
-
-https://github.com/nsf/termbox/blob/b20c0a11/src/python/termboxmodule.pyx
-
-And the example `termboxtest.py` which is copied verbatim from:
-
-https://github.com/nsf/termbox/blob/b20c0a11/test_termboxmodule.py
-
-### API Mapping Notes
-
-Notes taken while mapping the Termbox class:
-
- tb_init() // initialization console = tdl.init(132, 60)
- tb_shutdown() // shutdown
-
- tb_width() // width of the terminal screen console.width
- tb_height() // height of the terminal screen console.height
-
- tb_clear() // clear buffer console.clear()
- tb_present() // sync internal buffer with terminal tdl.flush()
-
- tb_put_cell()
- tb_change_cell() console.draw_char(x, y, ch, fg, bg)
- tb_blit() // drawing functions
-
- tb_select_input_mode() // change input mode
- tb_peek_event() // peek a keyboard event
- tb_poll_event() // wait for a keyboard event * tdl.event.get()
-
-
- * - means the translation is not direct
-
-
-
- init...
- tdl doesn't allow to resize window (or rather libtcod)
- tb works in existing terminal window and queries it rather than making own
-
- colors...
- tdl uses RGB values
- tb uses it own constants
-
- event...
- tb returns event one by one
- tdl return an event iterator
-
-
- tb Event tdl Event
- .type .type
- EVENT_KEY KEYDOWN
diff --git a/examples/termbox/termbox.py b/examples/termbox/termbox.py
deleted file mode 100755
index 4634c990..00000000
--- a/examples/termbox/termbox.py
+++ /dev/null
@@ -1,296 +0,0 @@
-"""Implementation of Termbox Python API in tdl.
-
-See README.md for details.
-"""
-
-import tdl
-
-"""
-Implementation status:
- [ ] tdl.init() needs a window, made 132x60
- [ ] Termbox.close() is not implemented, does nothing
- [ ] poll_event needs review, because it does not
- completely follows the original logic
- [ ] peek is stubbed, but not implemented
- [ ] not all keys/events are mapped
-"""
-
-# ruff: noqa
-
-
-class TermboxException(Exception):
- def __init__(self, msg) -> None:
- self.msg = msg
-
- def __str__(self) -> str:
- return self.msg
-
-
-_instance = None
-
-# keys ----------------------------------
-KEY_F1 = 0xFFFF - 0
-KEY_F2 = 0xFFFF - 1
-KEY_F3 = 0xFFFF - 2
-KEY_F4 = 0xFFFF - 3
-KEY_F5 = 0xFFFF - 4
-KEY_F6 = 0xFFFF - 5
-KEY_F7 = 0xFFFF - 6
-KEY_F8 = 0xFFFF - 7
-KEY_F9 = 0xFFFF - 8
-KEY_F10 = 0xFFFF - 9
-KEY_F11 = 0xFFFF - 10
-KEY_F12 = 0xFFFF - 11
-KEY_INSERT = 0xFFFF - 12
-KEY_DELETE = 0xFFFF - 13
-
-KEY_PGUP = 0xFFFF - 16
-KEY_PGDN = 0xFFFF - 17
-
-KEY_MOUSE_LEFT = 0xFFFF - 22
-KEY_MOUSE_RIGHT = 0xFFFF - 23
-KEY_MOUSE_MIDDLE = 0xFFFF - 24
-KEY_MOUSE_RELEASE = 0xFFFF - 25
-KEY_MOUSE_WHEEL_UP = 0xFFFF - 26
-KEY_MOUSE_WHEEL_DOWN = 0xFFFF - 27
-
-KEY_CTRL_TILDE = 0x00
-KEY_CTRL_2 = 0x00
-KEY_CTRL_A = 0x01
-KEY_CTRL_B = 0x02
-KEY_CTRL_C = 0x03
-KEY_CTRL_D = 0x04
-KEY_CTRL_E = 0x05
-KEY_CTRL_F = 0x06
-KEY_CTRL_G = 0x07
-KEY_BACKSPACE = 0x08
-KEY_CTRL_H = 0x08
-KEY_TAB = 0x09
-KEY_CTRL_I = 0x09
-KEY_CTRL_J = 0x0A
-KEY_CTRL_K = 0x0B
-KEY_CTRL_L = 0x0C
-KEY_ENTER = 0x0D
-KEY_CTRL_M = 0x0D
-KEY_CTRL_N = 0x0E
-KEY_CTRL_O = 0x0F
-KEY_CTRL_P = 0x10
-KEY_CTRL_Q = 0x11
-KEY_CTRL_R = 0x12
-KEY_CTRL_S = 0x13
-KEY_CTRL_T = 0x14
-KEY_CTRL_U = 0x15
-KEY_CTRL_V = 0x16
-KEY_CTRL_W = 0x17
-KEY_CTRL_X = 0x18
-KEY_CTRL_Y = 0x19
-KEY_CTRL_Z = 0x1A
-
-
-# -- mapped to tdl
-KEY_HOME = "HOME"
-KEY_END = "END"
-KEY_ARROW_UP = "UP"
-KEY_ARROW_DOWN = "DOWN"
-KEY_ARROW_LEFT = "LEFT"
-KEY_ARROW_RIGHT = "RIGHT"
-KEY_ESC = "ESCAPE"
-# /--
-
-
-KEY_CTRL_LSQ_BRACKET = 0x1B
-KEY_CTRL_3 = 0x1B
-KEY_CTRL_4 = 0x1C
-KEY_CTRL_BACKSLASH = 0x1C
-KEY_CTRL_5 = 0x1D
-KEY_CTRL_RSQ_BRACKET = 0x1D
-KEY_CTRL_6 = 0x1E
-KEY_CTRL_7 = 0x1F
-KEY_CTRL_SLASH = 0x1F
-KEY_CTRL_UNDERSCORE = 0x1F
-KEY_SPACE = 0x20
-KEY_BACKSPACE2 = 0x7F
-KEY_CTRL_8 = 0x7F
-
-MOD_ALT = 0x01
-
-# attributes ----------------------
-
-# -- mapped to tdl
-DEFAULT = Ellipsis
-
-BLACK = 0x000000
-RED = 0xFF0000
-GREEN = 0x00FF00
-YELLOW = 0xFFFF00
-BLUE = 0x0000FF
-MAGENTA = 0xFF00FF
-CYAN = 0x00FFFF
-WHITE = 0xFFFFFF
-# /--
-
-BOLD = 0x10
-UNDERLINE = 0x20
-REVERSE = 0x40
-
-# misc ----------------------------
-
-HIDE_CURSOR = -1
-INPUT_CURRENT = 0
-INPUT_ESC = 1
-INPUT_ALT = 2
-OUTPUT_CURRENT = 0
-OUTPUT_NORMAL = 1
-OUTPUT_256 = 2
-OUTPUT_216 = 3
-OUTPUT_GRAYSCALE = 4
-
-
-# -- mapped to tdl
-EVENT_KEY = "KEYDOWN"
-# /--
-EVENT_RESIZE = 2
-EVENT_MOUSE = 3
-
-
-class Event:
- """Aggregate for Termbox Event structure."""
-
- type = None
- ch = None
- key = None
- mod = None
- width = None
- height = None
- mousex = None
- mousey = None
-
- def gettuple(self):
- return (self.type, self.ch, self.key, self.mod, self.width, self.height, self.mousex, self.mousey)
-
-
-class Termbox:
- def __init__(self, width=132, height=60) -> None:
- global _instance
- if _instance:
- msg = "It is possible to create only one instance of Termbox"
- raise TermboxException(msg)
-
- try:
- self.console = tdl.init(width, height)
- except tdl.TDLException as e:
- raise TermboxException(e)
-
- self.e = Event() # cache for event data
-
- _instance = self
-
- def __del__(self) -> None:
- self.close()
-
- def __exit__(self, *args): # t, value, traceback):
- self.close()
-
- def __enter__(self):
- return self
-
- def close(self):
- global _instance
- # tb_shutdown()
- _instance = None
- # TBD, does nothing
-
- def present(self):
- """Sync state of the internal cell buffer with the terminal."""
- tdl.flush()
-
- def change_cell(self, x, y, ch, fg, bg):
- """Change cell in position (x;y)."""
- self.console.draw_char(x, y, ch, fg, bg)
-
- def width(self):
- """Returns width of the terminal screen."""
- return self.console.width
-
- def height(self):
- """Return height of the terminal screen."""
- return self.console.height
-
- def clear(self):
- """Clear the internal cell buffer."""
- self.console.clear()
-
- def set_cursor(self, x, y):
- """Set cursor position to (x;y).
-
- Set both arguments to HIDE_CURSOR or use 'hide_cursor' function to
- hide it.
- """
- tb_set_cursor(x, y)
-
- def hide_cursor(self):
- """Hide cursor."""
- tb_set_cursor(-1, -1)
-
- def select_input_mode(self, mode):
- """Select preferred input mode: INPUT_ESC or INPUT_ALT.
-
- INPUT_CURRENT returns the selected mode without changing anything.
- """
- return int(tb_select_input_mode(mode))
-
- def select_output_mode(self, mode):
- """Select preferred output mode: one of OUTPUT_* constants.
-
- OUTPUT_CURRENT returns the selected mode without changing anything.
- """
- return int(tb_select_output_mode(mode))
-
- def peek_event(self, timeout=0):
- """Wait for an event up to 'timeout' milliseconds and return it.
-
- Returns None if there was no event and timeout is expired.
- Returns a tuple otherwise: (type, unicode character, key, mod,
- width, height, mousex, mousey).
- """
- """
- cdef tb_event e
- with self._poll_lock:
- with nogil:
- result = tb_peek_event(&e, timeout)
- assert(result >= 0)
- if result == 0:
- return None
- if e.ch:
- uch = unichr(e.ch)
- else:
- uch = None
- """
- # return (e.type, uch, e.key, e.mod, e.w, e.h, e.x, e.y)
-
- def poll_event(self):
- """Wait for an event and return it.
-
- Returns a tuple: (type, unicode character, key, mod, width, height,
- mousex, mousey).
- """
- """
- cdef tb_event e
- with self._poll_lock:
- with nogil:
- result = tb_poll_event(&e)
- assert(result >= 0)
- if e.ch:
- uch = unichr(e.ch)
- else:
- uch = None
- """
- for e in tdl.event.get():
- # [ ] not all events are passed thru
- self.e.type = e.type
- 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
deleted file mode 100755
index 696be1ce..00000000
--- a/examples/termbox/termboxtest.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env python
-
-import termbox
-
-# ruff: noqa
-
-spaceord = ord(" ")
-
-
-def print_line(t, msg, y, fg, bg):
- w = t.width()
- l = len(msg)
- x = 0
- for i in range(w):
- c = spaceord
- if i < l:
- c = ord(msg[i])
- t.change_cell(x + i, y, c, fg, bg)
-
-
-class SelectBox:
- def __init__(self, tb, choices, active=-1) -> None:
- self.tb = tb
- self.active = active
- self.choices = choices
- self.color_active = (termbox.BLACK, termbox.CYAN)
- self.color_normal = (termbox.WHITE, termbox.BLACK)
-
- def draw(self):
- for i, c in enumerate(self.choices):
- color = self.color_normal
- if i == self.active:
- color = self.color_active
- print_line(self.tb, c, i, *color)
-
- def validate_active(self):
- if self.active < 0:
- self.active = 0
- if self.active >= len(self.choices):
- self.active = len(self.choices) - 1
-
- def set_active(self, i):
- self.active = i
- self.validate_active()
-
- def move_up(self):
- self.active -= 1
- self.validate_active()
-
- def move_down(self):
- self.active += 1
- self.validate_active()
-
-
-choices = [
- "This instructs Psyco",
- "to compile and run as",
- "much of your application",
- "code as possible. This is the",
- "simplest interface to Psyco.",
- "In good cases you can just add",
- "these two lines and enjoy the speed-up.",
- "If your application does a lot",
- "of initialization stuff before",
- "the real work begins, you can put",
- "the above two lines after this",
- "initialization - e.g. after importing",
- "modules, creating constant global objects, etc.",
- "This instructs Psyco",
- "to compile and run as",
- "much of your application",
- "code as possible. This is the",
- "simplest interface to Psyco.",
- "In good cases you can just add",
- "these two lines and enjoy the speed-up.",
- "If your application does a lot",
- "of initialization stuff before",
- "the real work begins, you can put",
- "the above two lines after this",
- "initialization - e.g. after importing",
- "modules, creating constant global objects, etc.",
-]
-
-
-def draw_bottom_line(t, i):
- i = i % 8
- w = t.width()
- h = t.height()
- c = i
- palette = [
- termbox.DEFAULT,
- termbox.BLACK,
- termbox.RED,
- termbox.GREEN,
- termbox.YELLOW,
- termbox.BLUE,
- termbox.MAGENTA,
- termbox.CYAN,
- termbox.WHITE,
- ]
- for x in range(w):
- t.change_cell(x, h - 1, ord(" "), termbox.BLACK, palette[c])
- t.change_cell(x, h - 2, ord(" "), termbox.BLACK, palette[c])
- c += 1
- if c > 7:
- c = 0
-
-
-with termbox.Termbox() as t:
- sb = SelectBox(t, choices, 0)
- t.clear()
- sb.draw()
- t.present()
- i = 0
- run_app = True
- while run_app:
- event_here = t.poll_event()
- while event_here:
- (type, ch, key, mod, w, h, x, y) = event_here
- if type == termbox.EVENT_KEY and key == termbox.KEY_ESC:
- run_app = False
- if type == termbox.EVENT_KEY:
- if key == termbox.KEY_ARROW_DOWN:
- sb.move_down()
- elif key == termbox.KEY_ARROW_UP:
- sb.move_up()
- elif key == termbox.KEY_HOME:
- sb.set_active(-1)
- elif key == termbox.KEY_END:
- sb.set_active(999)
- event_here = t.peek_event()
-
- t.clear()
- sb.draw()
- draw_bottom_line(t, i)
- t.present()
- i += 1
diff --git a/examples/ttf.py b/examples/ttf.py
index fb1e35f6..4a6041a8 100755
--- a/examples/ttf.py
+++ b/examples/ttf.py
@@ -84,7 +84,7 @@ def main() -> None:
for event in tcod.event.wait():
if isinstance(event, tcod.event.Quit):
raise SystemExit
- if isinstance(event, tcod.event.WindowResized) and event.type == "WindowSizeChanged":
+ if isinstance(event, tcod.event.WindowResized):
# Resize the Tileset to match the new screen size.
context.change_tileset(
load_ttf(
diff --git a/libtcod b/libtcod
index ffa44720..27c2dbc9 160000
--- a/libtcod
+++ b/libtcod
@@ -1 +1 @@
-Subproject commit ffa447202e9b354691386e91f1288fd69dc1eaba
+Subproject commit 27c2dbc9d97bacb18b9fd43a5c7f070dc34339ed
diff --git a/pyproject.toml b/pyproject.toml
index 27a3dc16..b8c7cc34 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,19 +28,22 @@ license-files = [
"libtcod/LIBTCOD-CREDITS.txt",
]
dependencies = [
+ "attrs>=25.2.0",
"cffi>=1.15",
'numpy>=1.21.4; implementation_name != "pypy"',
"typing_extensions>=4.12.2",
]
keywords = [
"roguelike",
- "cffi",
+ "roguelikedev",
+ "gamedev",
"Unicode",
"libtcod",
+ "libtcodpy",
"field-of-view",
"pathfinding",
]
-classifiers = [
+classifiers = [ # https://pypi.org/classifiers/
"Development Status :: 5 - Production/Stable",
"Environment :: Win32 (MS Windows)",
"Environment :: MacOS X",
@@ -50,11 +53,9 @@ classifiers = [
"Operating System :: POSIX",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
+ "Programming Language :: C",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: Free Threading :: 2 - Beta",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Games/Entertainment",
@@ -74,9 +75,6 @@ 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 = "cp310"
-
[tool.setuptools_scm]
write_to = "tcod/version.py"
@@ -99,6 +97,13 @@ filterwarnings = [
"ignore:'import tcod as libtcodpy' is preferred.",
]
+[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html
+exclude_lines = ['^\s*\.\.\.', "if TYPE_CHECKING:", "# pragma: no cover"]
+omit = ["tcod/__pyinstaller/*"]
+
+[tool.cibuildwheel] # https://cibuildwheel.pypa.io/en/stable/options/
+enable = ["pypy", "pyodide-prerelease"]
+
[tool.mypy]
files = ["."]
python_version = "3.10"
@@ -149,20 +154,12 @@ line-length = 120
select = ["ALL"]
ignore = [
"COM", # flake8-commas
- "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
- "D206", # indent-with-spaces
"E501", # line-too-long
"PYI064", # redundant-final-literal
"S101", # assert
"S301", # suspicious-pickle-usage
"S311", # suspicious-non-cryptographic-random-usage
"SLF001", # private-member-access
- "W191", # tab-indentation
]
[tool.ruff.lint.per-file-ignores]
"**/{tests}/*" = [
diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py
index 2a7814b7..bc7efb19 100755
--- a/scripts/generate_charmap_table.py
+++ b/scripts/generate_charmap_table.py
@@ -8,7 +8,7 @@
import argparse
import unicodedata
-from collections.abc import Iterable, Iterator
+from collections.abc import Iterable, Iterator # noqa: TC003
from tabulate import tabulate
diff --git a/scripts/tag_release.py b/scripts/tag_release.py
index 066eaeda..5f39e4b2 100755
--- a/scripts/tag_release.py
+++ b/scripts/tag_release.py
@@ -45,7 +45,7 @@ def parse_changelog(args: argparse.Namespace) -> tuple[str, str]:
return f"{header}{tagged}{tail}", changes
-def replace_unreleased_tags(tag: str, dry_run: bool) -> None:
+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
@@ -77,7 +77,7 @@ def main() -> None:
print("--- New changelog:")
print(new_changelog)
- replace_unreleased_tags(args.tag, args.dry_run)
+ replace_unreleased_tags(args.tag, dry_run=args.dry_run)
if not args.dry_run:
(PROJECT_DIR / "CHANGELOG.md").write_text(new_changelog, encoding="utf-8")
diff --git a/setup.py b/setup.py
index 6a835e5b..3ff98114 100755
--- a/setup.py
+++ b/setup.py
@@ -3,8 +3,6 @@
from __future__ import annotations
-import platform
-import subprocess
import sys
from pathlib import Path
@@ -19,55 +17,36 @@
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",
"lib/LIBTCOD-LICENSE.txt",
"lib/README-SDL.txt",
]
- if "win32" in sys.platform:
- if bit_size == "32bit":
- files += ["x86/SDL3.dll"]
- else:
+ if sys.platform == "win32":
+ if "ARM64" in sys.version:
+ files += ["arm64/SDL3.dll"]
+ elif "AMD64" in sys.version:
files += ["x64/SDL3.dll"]
+ else:
+ files += ["x86/SDL3.dll"]
if sys.platform == "darwin":
files += ["SDL3.framework/Versions/A/SDL3"]
return files
-def check_sdl_version() -> None:
- """Check the local SDL version on Linux distributions."""
- if not sys.platform.startswith("linux"):
- return
- needed_version = "{}.{}.{}".format(*SDL_VERSION_NEEDED)
- try:
- sdl_version_str = subprocess.check_output(
- ["pkg-config", "sdl3", "--modversion"], # noqa: S607
- universal_newlines=True,
- ).strip()
- except FileNotFoundError:
- try:
- sdl_version_str = subprocess.check_output(["sdl3-config", "--version"], universal_newlines=True).strip() # noqa: S603, S607
- except FileNotFoundError as exc:
- msg = (
- f"libsdl3-dev or equivalent must be installed on your system and must be at least version {needed_version}."
- "\nsdl3-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_VERSION_NEEDED:
- msg = f"SDL version must be at least {needed_version}, (found {sdl_version_str})"
- raise RuntimeError(msg)
-
-
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()
+options = {
+ "bdist_wheel": {
+ "py_limited_api": "cp310",
+ }
+}
+if "free-threading build" in sys.version:
+ del options["bdist_wheel"]["py_limited_api"]
setup(
py_modules=["libtcodpy"],
@@ -75,4 +54,5 @@ def check_sdl_version() -> None:
package_data={"tcod": get_package_data()},
cffi_modules=["build_libtcod.py:ffi"],
platforms=["Windows", "MacOS", "Linux"],
+ options=options,
)
diff --git a/tcod/_internal.py b/tcod/_internal.py
index 3391a602..b67242b8 100644
--- a/tcod/_internal.py
+++ b/tcod/_internal.py
@@ -8,7 +8,7 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, AnyStr, Literal, NoReturn, SupportsInt, TypeVar
-from typing_extensions import LiteralString, deprecated
+from typing_extensions import deprecated
from tcod.cffi import ffi, lib
@@ -40,16 +40,6 @@ def decorator(func: F) -> F:
deprecate = deprecated if __debug__ or TYPE_CHECKING else _deprecate_passthrough
-def pending_deprecate(
- message: LiteralString = "This function may be deprecated in the future."
- " Consider raising an issue on GitHub if you need this feature.",
- 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."""
- return deprecate(message, category=category, stacklevel=stacklevel)
-
-
def verify_order(order: Literal["C", "F"]) -> Literal["C", "F"]:
"""Verify and return a Numpy order string."""
order = order.upper() # type: ignore[assignment]
diff --git a/tcod/_libtcod.pyi b/tcod/_libtcod.pyi
index 4b8ed0fe..04fba29b 100644
--- a/tcod/_libtcod.pyi
+++ b/tcod/_libtcod.pyi
@@ -7528,142 +7528,6 @@ class _lib:
def TCOD_zip_skip_bytes(zip: Any, nbBytes: Any, /) -> None:
"""void TCOD_zip_skip_bytes(TCOD_zip_t zip, uint32_t nbBytes)"""
- @staticmethod
- def TDL_color_HSV(h: float, s: float, v: float, /) -> int:
- """int TDL_color_HSV(float h, float s, float v)"""
-
- @staticmethod
- def TDL_color_RGB(r: int, g: int, b: int, /) -> int:
- """int TDL_color_RGB(int r, int g, int b)"""
-
- @staticmethod
- def TDL_color_add(c1: int, c2: int, /) -> int:
- """int TDL_color_add(int c1, int c2)"""
-
- @staticmethod
- def TDL_color_equals(c1: int, c2: int, /) -> bool:
- """bool TDL_color_equals(int c1, int c2)"""
-
- @staticmethod
- def TDL_color_from_int(color: int, /) -> Any:
- """TCOD_color_t TDL_color_from_int(int color)"""
-
- @staticmethod
- def TDL_color_get_hue(color: int, /) -> float:
- """float TDL_color_get_hue(int color)"""
-
- @staticmethod
- def TDL_color_get_saturation(color: int, /) -> float:
- """float TDL_color_get_saturation(int color)"""
-
- @staticmethod
- def TDL_color_get_value(color: int, /) -> float:
- """float TDL_color_get_value(int color)"""
-
- @staticmethod
- def TDL_color_int_to_array(color: int, /) -> Any:
- """int *TDL_color_int_to_array(int color)"""
-
- @staticmethod
- def TDL_color_lerp(c1: int, c2: int, coef: float, /) -> int:
- """int TDL_color_lerp(int c1, int c2, float coef)"""
-
- @staticmethod
- def TDL_color_multiply(c1: int, c2: int, /) -> int:
- """int TDL_color_multiply(int c1, int c2)"""
-
- @staticmethod
- def TDL_color_multiply_scalar(c: int, value: float, /) -> int:
- """int TDL_color_multiply_scalar(int c, float value)"""
-
- @staticmethod
- def TDL_color_scale_HSV(color: int, scoef: float, vcoef: float, /) -> int:
- """int TDL_color_scale_HSV(int color, float scoef, float vcoef)"""
-
- @staticmethod
- def TDL_color_set_hue(color: int, h: float, /) -> int:
- """int TDL_color_set_hue(int color, float h)"""
-
- @staticmethod
- def TDL_color_set_saturation(color: int, h: float, /) -> int:
- """int TDL_color_set_saturation(int color, float h)"""
-
- @staticmethod
- def TDL_color_set_value(color: int, h: float, /) -> int:
- """int TDL_color_set_value(int color, float h)"""
-
- @staticmethod
- def TDL_color_shift_hue(color: int, hue_shift: float, /) -> int:
- """int TDL_color_shift_hue(int color, float hue_shift)"""
-
- @staticmethod
- def TDL_color_subtract(c1: int, c2: int, /) -> int:
- """int TDL_color_subtract(int c1, int c2)"""
-
- @staticmethod
- def TDL_color_to_int(color: Any, /) -> int:
- """int TDL_color_to_int(TCOD_color_t *color)"""
-
- @staticmethod
- def TDL_console_get_bg(console: Any, x: int, y: int, /) -> int:
- """int TDL_console_get_bg(TCOD_console_t console, int x, int y)"""
-
- @staticmethod
- def TDL_console_get_fg(console: Any, x: int, y: int, /) -> int:
- """int TDL_console_get_fg(TCOD_console_t console, int x, int y)"""
-
- @staticmethod
- def TDL_console_put_char_ex(console: Any, x: int, y: int, ch: int, fg: int, bg: int, flag: Any, /) -> int:
- """int TDL_console_put_char_ex(TCOD_console_t console, int x, int y, int ch, int fg, int bg, TCOD_bkgnd_flag_t flag)"""
-
- @staticmethod
- def TDL_console_set_bg(console: Any, x: int, y: int, color: int, flag: Any, /) -> None:
- """void TDL_console_set_bg(TCOD_console_t console, int x, int y, int color, TCOD_bkgnd_flag_t flag)"""
-
- @staticmethod
- def TDL_console_set_fg(console: Any, x: int, y: int, color: int, /) -> None:
- """void TDL_console_set_fg(TCOD_console_t console, int x, int y, int color)"""
-
- @staticmethod
- def TDL_list_get_bool(l: Any, idx: int, /) -> bool:
- """bool TDL_list_get_bool(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_char(l: Any, idx: int, /) -> Any:
- """char TDL_list_get_char(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_color(l: Any, idx: int, /) -> Any:
- """TCOD_color_t TDL_list_get_color(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_dice(l: Any, idx: int, /) -> Any:
- """TCOD_dice_t TDL_list_get_dice(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_float(l: Any, idx: int, /) -> float:
- """float TDL_list_get_float(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_int(l: Any, idx: int, /) -> int:
- """int TDL_list_get_int(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_string(l: Any, idx: int, /) -> Any:
- """char *TDL_list_get_string(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_list_get_union(l: Any, idx: int, /) -> Any:
- """TCOD_value_t TDL_list_get_union(TCOD_list_t l, int idx)"""
-
- @staticmethod
- def TDL_map_data_from_buffer(map: Any, buffer: Any, /) -> None:
- """void TDL_map_data_from_buffer(TCOD_map_t map, uint8_t *buffer)"""
-
- @staticmethod
- def TDL_map_fov_to_buffer(map: Any, buffer: Any, cumulative: bool, /) -> None:
- """void TDL_map_fov_to_buffer(TCOD_map_t map, uint8_t *buffer, bool cumulative)"""
-
@staticmethod
def _libtcod_log_watcher(message: Any, userdata: Any, /) -> None:
"""void _libtcod_log_watcher(const TCOD_LogMessage *message, void *userdata)"""
@@ -9648,7 +9512,7 @@ class _lib:
TCOD_LOG_INFO: Final[Literal[20]] = 20
TCOD_LOG_WARNING: Final[Literal[30]] = 30
TCOD_MAJOR_VERSION: Final[Literal[2]] = 2
- TCOD_MINOR_VERSION: Final[Literal[1]] = 1
+ TCOD_MINOR_VERSION: Final[Literal[2]] = 2
TCOD_NB_RENDERERS: Final[int]
TCOD_NOISE_DEFAULT: Final[Literal[0]] = 0
TCOD_NOISE_MAX_DIMENSIONS: Final[Literal[4]] = 4
diff --git a/tcod/bsp.py b/tcod/bsp.py
index f4d7204d..21aa093d 100644
--- a/tcod/bsp.py
+++ b/tcod/bsp.py
@@ -27,7 +27,6 @@
from __future__ import annotations
-from collections.abc import Iterator
from typing import TYPE_CHECKING, Any
from typing_extensions import deprecated
@@ -35,6 +34,8 @@
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Iterator
+
import tcod.random
@@ -125,7 +126,11 @@ def _unpack_bsp_tree(self, cdata: Any) -> None: # noqa: ANN401
self.children[1].parent = self
self.children[1]._unpack_bsp_tree(lib.TCOD_bsp_right(cdata))
- def split_once(self, horizontal: bool, position: int) -> None:
+ def split_once(
+ self,
+ horizontal: bool, # noqa: FBT001
+ position: int,
+ ) -> None:
"""Split this partition into 2 sub-partitions.
Args:
@@ -148,20 +153,17 @@ def split_recursive( # noqa: PLR0913
"""Divide this partition recursively.
Args:
- depth (int): The maximum depth to divide this object recursively.
- min_width (int): The minimum width of any individual partition.
- min_height (int): The minimum height of any individual partition.
- max_horizontal_ratio (float):
- Prevent creating a horizontal ratio more extreme than this.
- max_vertical_ratio (float):
- Prevent creating a vertical ratio more extreme than this.
- seed (Optional[tcod.random.Random]):
- The random number generator to use.
+ depth: The maximum depth to divide this object recursively.
+ min_width: The minimum width of any individual partition.
+ min_height: The minimum height of any individual partition.
+ max_horizontal_ratio: Prevent creating a horizontal ratio more extreme than this.
+ max_vertical_ratio: Prevent creating a vertical ratio more extreme than this.
+ seed: The random number generator to use.
"""
cdata = self._as_cdata()
lib.TCOD_bsp_split_recursive(
cdata,
- seed or ffi.NULL,
+ seed.random_c if seed is not None else ffi.NULL,
depth,
min_width,
min_height,
@@ -214,13 +216,13 @@ def level_order(self) -> Iterator[BSP]:
.. versionadded:: 8.3
"""
- next = [self]
- while next:
- level = next
- next = []
+ next_ = [self]
+ while next_:
+ level = next_
+ next_ = []
yield from level
for node in level:
- next.extend(node.children)
+ next_.extend(node.children)
def inverted_level_order(self) -> Iterator[BSP]:
"""Iterate over this BSP's hierarchy in inverse level order.
diff --git a/tcod/cffi.h b/tcod/cffi.h
index fc373dac..5f8fa494 100644
--- a/tcod/cffi.h
+++ b/tcod/cffi.h
@@ -9,4 +9,3 @@
#include "path.h"
#include "random.h"
#include "tcod.h"
-#include "tdl.h"
diff --git a/tcod/cffi.py b/tcod/cffi.py
index b0590d0e..57f4039b 100644
--- a/tcod/cffi.py
+++ b/tcod/cffi.py
@@ -7,7 +7,7 @@
import platform
import sys
from pathlib import Path
-from typing import Any
+from typing import Any, Literal
import cffi
@@ -39,8 +39,10 @@ def verify_dependencies() -> None:
raise RuntimeError(msg)
-def get_architecture() -> str:
- """Return the Windows architecture, one of "x86" or "x64"."""
+def get_architecture() -> Literal["x86", "x64", "arm64"]:
+ """Return the Windows architecture."""
+ if "(ARM64)" in sys.version:
+ return "arm64"
return "x86" if platform.architecture()[0] == "32bit" else "x64"
@@ -60,7 +62,7 @@ def get_sdl_version() -> str:
__sdl_version__ = get_sdl_version()
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _libtcod_log_watcher(message: Any, _userdata: None) -> None: # noqa: ANN401
text = str(ffi.string(message.message), encoding="utf-8")
source = str(ffi.string(message.source), encoding="utf-8")
diff --git a/tcod/color.py b/tcod/color.py
index 9fc6f260..bf9dabb3 100644
--- a/tcod/color.py
+++ b/tcod/color.py
@@ -89,6 +89,8 @@ def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401, D105
else:
super().__setitem__(index, value)
+ __hash__ = None
+
def __eq__(self, other: object) -> bool:
"""Compare equality between colors.
diff --git a/tcod/console.py b/tcod/console.py
index c4b24281..bfcbce00 100644
--- a/tcod/console.py
+++ b/tcod/console.py
@@ -8,7 +8,6 @@
from __future__ import annotations
import warnings
-from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, overload
@@ -22,6 +21,7 @@
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Iterable
from os import PathLike
from numpy.typing import ArrayLike, NDArray
@@ -1038,6 +1038,24 @@ def print(
string: str = "",
) -> int: ...
+ @overload
+ @deprecated(
+ "Replace text, fg, bg, bg_blend, and alignment with keyword arguments."
+ "\n'string' keyword should be renamed to `text`"
+ )
+ def print(
+ self,
+ x: int,
+ y: int,
+ text: str = "",
+ 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,
+ *,
+ string: str,
+ ) -> int: ...
+
def print( # noqa: PLR0913
self,
x: int,
@@ -1164,7 +1182,9 @@ def print( # noqa: PLR0913
)
)
- @deprecated("Switch to using keywords and then replace with 'console.print(...)'")
+ @deprecated(
+ "Switch parameters to keywords, then replace method with 'console.print(...)', then replace 'string=' with 'text='"
+ )
def print_box( # noqa: PLR0913
self,
x: int,
@@ -1206,6 +1226,9 @@ def print_box( # noqa: PLR0913
.. versionchanged:: 13.0
`x` and `y` are now always used as an absolute position for negative values.
+
+ .. deprecated:: 18.0
+ This method was replaced by more functional :any:`Console.print` method.
"""
string_ = string.encode("utf-8")
return int(
diff --git a/tcod/context.py b/tcod/context.py
index 769f84d2..01ed61f7 100644
--- a/tcod/context.py
+++ b/tcod/context.py
@@ -29,9 +29,9 @@
import pickle
import sys
import warnings
-from collections.abc import Iterable
+from math import floor
from pathlib import Path
-from typing import Any, Literal, NoReturn, TypeVar
+from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeVar
from typing_extensions import Self, deprecated
@@ -44,6 +44,9 @@
from tcod._internal import _check, _check_warn
from tcod.cffi import ffi, lib
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+
__all__ = (
"RENDERER_OPENGL",
"RENDERER_OPENGL2",
@@ -65,7 +68,7 @@
"new_window",
)
-_Event = TypeVar("_Event", bound=tcod.event.Event)
+_Event = TypeVar("_Event", bound="tcod.event.Event")
SDL_WINDOW_FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN
"""Fullscreen mode."""
@@ -123,7 +126,7 @@
def _handle_tileset(tileset: tcod.tileset.Tileset | None) -> Any: # noqa: ANN401
"""Get the TCOD_Tileset pointer from a Tileset or return a NULL pointer."""
- return tileset._tileset_p if tileset else ffi.NULL
+ return tileset._tileset_p if tileset is not None else ffi.NULL
def _handle_title(title: str | None) -> Any: # noqa: ANN401
@@ -252,9 +255,11 @@ def convert_event(self, event: _Event) -> _Event:
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)):
- 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._MouseEventWithPosition):
+ assert isinstance(event_copy, tcod.event._MouseEventWithPosition)
+ event_copy.position = tcod.event.Point(*self.pixel_to_tile(event.position[0], event.position[1]))
+ if isinstance(event, tcod.event._MouseEventWithTile):
+ event._tile = tcod.event.Point(floor(event_copy.position[0]), floor(event_copy.position[1]))
if isinstance(event, tcod.event.MouseMotion):
assert isinstance(event_copy, tcod.event.MouseMotion)
assert event._tile is not None
@@ -262,8 +267,13 @@ def convert_event(self, event: _Event) -> _Event:
event.position[0] - event.motion[0],
event.position[1] - event.motion[1],
)
- event_copy.motion = event._tile_motion = tcod.event.Point(
- int(event._tile[0]) - int(prev_tile[0]), int(event._tile[1]) - int(prev_tile[1])
+ event_copy.motion = tcod.event.Point(
+ event_copy.position[0] - prev_tile[0],
+ event_copy.position[1] - prev_tile[1],
+ )
+ event._tile_motion = tcod.event.Point(
+ event._tile[0] - floor(prev_tile[0]),
+ event._tile[1] - floor(prev_tile[1]),
)
return event_copy
@@ -444,7 +454,7 @@ def __reduce__(self) -> NoReturn:
raise pickle.PicklingError(msg)
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_cli_output(catch_reference: Any, output: Any) -> None: # noqa: ANN401
"""Callback for the libtcod context CLI.
diff --git a/tcod/event.py b/tcod/event.py
index b780ffdf..6b9453cf 100644
--- a/tcod/event.py
+++ b/tcod/event.py
@@ -27,21 +27,19 @@
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.
+ for pixel_event in tcod.event.wait():
+ event = context.convert_event(pixel_event) # Convert mouse pixel coordinates to tile coordinates
+ print(event) # Print all events, for learning and debugging
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.
+ print(f"{event.sym=}, {event.scancode=}") # Show Scancode and KeySym enum names
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.
+ print(f"{event.button=}, {event.integer_position=}") # Show mouse button and tile
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.
+ print(f"{event.integer_position=}, {event.integer_motion=}") # Current mouse tile and tile motion
Python 3.10 introduced `match statements `_
which can be used to dispatch events more gracefully:
@@ -61,21 +59,23 @@
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.
+ for pixel_event in tcod.event.wait():
+ event = context.convert_event(pixel_event) # Converts mouse pixel coordinates to tile coordinates.
match event:
case tcod.event.Quit():
raise SystemExit()
- case tcod.event.KeyDown(sym) if sym in KEY_COMMANDS:
+ case tcod.event.KeyDown(sym=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.MouseButtonDown(button=button, integer_position=tile):
+ print(f"MouseButtonDown: {button=}, {tile=}")
+ case tcod.event.MouseMotion(integer_position=tile, integer_motion=tile_motion):
+ assert isinstance(pixel_event, tcod.event.MouseMotion)
+ pixel_motion = pixel_event.motion
+ print(f"MouseMotion: {pixel_motion=}, {tile=}, {tile_motion=}")
case tcod.event.Event() as event:
- print(event) # Show any unhandled events.
+ print(event) # Print unhandled events
.. versionadded:: 8.4
"""
@@ -83,16 +83,35 @@
from __future__ import annotations
import enum
+import functools
+import sys
import warnings
from collections.abc import Callable, Iterator, Mapping
-from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NamedTuple, TypeVar
+from math import floor
+from pathlib import Path
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Final,
+ Generic,
+ Literal,
+ NamedTuple,
+ Protocol,
+ TypeAlias,
+ TypedDict,
+ TypeVar,
+ overload,
+ runtime_checkable,
+)
+import attrs
import numpy as np
-from typing_extensions import deprecated
+from typing_extensions import Self, deprecated
-import tcod.event
+import tcod.context
import tcod.event_constants
import tcod.sdl.joystick
+import tcod.sdl.render
import tcod.sdl.sys
from tcod.cffi import ffi, lib
from tcod.event_constants import * # noqa: F403
@@ -102,6 +121,13 @@
from numpy.typing import NDArray
T = TypeVar("T")
+_EventType = TypeVar("_EventType", bound="Event")
+
+_C_SDL_Event: TypeAlias = Any
+"""A CFFI pointer to an SDL_Event union.
+
+See SDL docs: https://wiki.libsdl.org/SDL3/SDL_Event
+"""
class _ConstantsWithPrefix(Mapping[int, str]):
@@ -138,29 +164,39 @@ def _describe_bitmask(bits: int, table: Mapping[int, str], default: str = "0") -
return "|".join(result)
-def _pixel_to_tile(x: float, y: float) -> tuple[float, float] | None:
+def _pixel_to_tile(xy: tuple[float, float], /) -> Point[float] | None:
"""Convert pixel coordinates to tile coordinates."""
if not lib.TCOD_ctx.engine:
return None
- xy = ffi.new("double[2]", (x, y))
- lib.TCOD_sys_pixel_to_tile(xy, xy + 1)
- return xy[0], xy[1]
+ xy_out = ffi.new("double[2]", xy)
+ lib.TCOD_sys_pixel_to_tile(xy_out, xy_out + 1)
+ return Point(float(xy_out[0]), float(xy_out[1]))
-class Point(NamedTuple):
- """A 2D position used for events with mouse coordinates.
+if sys.version_info >= (3, 11) or TYPE_CHECKING:
- .. seealso::
- :any:`MouseMotion` :any:`MouseButtonDown` :any:`MouseButtonUp`
- """
+ class Point(NamedTuple, Generic[T]):
+ """A 2D position used for events with mouse coordinates.
+
+ .. seealso::
+ :any:`MouseMotion` :any:`MouseButtonDown` :any:`MouseButtonUp`
- x: float
- """A pixel or tile coordinate starting with zero as the left-most position."""
- y: float
- """A pixel or tile coordinate starting with zero as the top-most position."""
+ .. versionchanged:: 19.0
+ Now uses floating point coordinates due to the port to SDL3.
+ """
+
+ x: T
+ """A pixel or tile coordinate starting with zero as the left-most position."""
+ y: T
+ """A pixel or tile coordinate starting with zero as the top-most position."""
+else:
+
+ class Point(NamedTuple): # noqa: D101
+ x: Any
+ y: Any
-def _verify_tile_coordinates(xy: Point | None) -> Point:
+def _verify_tile_coordinates(xy: Point[int] | None) -> Point[int]:
"""Check if an events tile coordinate is initialized and warn if not.
Always returns a valid Point object for backwards compatibility.
@@ -194,6 +230,7 @@ class Modifier(enum.IntFlag):
Example::
+ >>> import tcod.event
>>> mod = tcod.event.Modifier(4098)
>>> mod & tcod.event.Modifier.SHIFT # Check if any shift key is held.
@@ -258,6 +295,7 @@ class MouseButton(enum.IntEnum):
"""Forward mouse button."""
def __repr__(self) -> str:
+ """Return the enum name, excluding the value."""
return f"{self.__class__.__name__}.{self.name}"
@@ -279,566 +317,669 @@ class MouseButtonMask(enum.IntFlag):
"""Forward mouse button is held."""
def __repr__(self) -> str:
+ """Return the bitwise OR flag combination of this value."""
if self.value == 0:
return f"{self.__class__.__name__}(0)"
return "|".join(f"{self.__class__.__name__}.{self.__class__(bit).name}" for bit in self.__class__ if bit & self)
+class _CommonSDLEventAttributes(TypedDict):
+ """Common keywords for Event subclasses."""
+
+ sdl_event: _C_SDL_Event
+ timestamp_ns: int
+
+
+def _unpack_sdl_event(sdl_event: _C_SDL_Event) -> _CommonSDLEventAttributes:
+ """Unpack an SDL_Event union into common attributes, such as timestamp."""
+ return {
+ "sdl_event": sdl_event,
+ "timestamp_ns": sdl_event.common.timestamp,
+ }
+
+
+@attrs.define(slots=True, kw_only=True)
class Event:
- """The base event class.
+ """The base event class."""
- Attributes:
- type (str): This events type.
- sdl_event: When available, this holds a python-cffi 'SDL_Event*'
- pointer. All sub-classes have this attribute.
+ sdl_event: _C_SDL_Event = attrs.field(default=None, eq=False, repr=False)
+ """Holds a python-cffi ``SDL_Event*`` pointer for this event when available."""
+
+ timestamp_ns: int = attrs.field(default=0, eq=False)
+ """The time of this event in nanoseconds since SDL has been initialized.
+
+ .. seealso::
+ :any:`tcod.event.time_ns`
+
+ .. versionadded:: 21.0
"""
- 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
+ @property
+ def timestamp(self) -> float:
+ """The time of this event in seconds since SDL has been initialized.
+
+ .. seealso::
+ :any:`tcod.event.time`
+
+ .. versionadded:: 21.0
+ """
+ return self.timestamp_ns / 1_000_000_000
+
+ @property
+ @deprecated("The Event.type attribute is deprecated, use isinstance instead.")
+ def type(self) -> str:
+ """This events type.
+
+ .. deprecated:: 21.0
+ Using this attribute is now actively discouraged. Use :func:`isinstance` or :ref:`match`.
+
+ :meta private:
+ """
+ type_override: str | None = getattr(self, "_type", None)
+ if type_override is not None:
+ return type_override
+ return self.__class__.__name__.upper()
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Event:
- """Return a class instance from a python-cffi 'SDL_Event*' pointer."""
- raise NotImplementedError
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Event:
+ """Return a class instance from a python-cffi 'SDL_Event*' pointer.
- def __str__(self) -> str:
- return f""
+ .. versionchanged:: 21.0
+ This method was unsuitable for the public API and is now private.
+ """
+ raise NotImplementedError
+@attrs.define(slots=True, kw_only=True)
class Quit(Event):
"""An application quit request event.
For more info on when this event is triggered see:
https://wiki.libsdl.org/SDL_EventType#SDL_QUIT
-
- Attributes:
- type (str): Always "QUIT".
"""
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Quit:
- self = cls()
- self.sdl_event = sdl_event
- return self
-
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}()"
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(**_unpack_sdl_event(sdl_event))
+@attrs.define(slots=True, kw_only=True)
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
- of the key on the keyboard rather than the keys symbol.
- sym (KeySym): The keyboard symbol.
- mod (Modifier): A bitmask of the currently held modifier keys.
+ .. versionchanged:: 12.5
+ `scancode`, `sym`, and `mod` now use their respective enums.
+ """
- For example, if shift is held then
- ``event.mod & tcod.event.Modifier.SHIFT`` will evaluate to a true
- value.
+ scancode: Scancode
+ """The keyboard scan-code, this is the physical location
+ of the key on the keyboard rather than the keys symbol."""
+ sym: KeySym
+ """The keyboard symbol."""
+ mod: Modifier
+ """A bitmask of the currently held modifier keys.
+
+ For example, if shift is held then
+ ``event.mod & tcod.event.Modifier.SHIFT`` will evaluate to a true
+ value.
+ """
+ repeat: bool = False
+ """True if this event exists because of key repeat."""
+ which: int = 0
+ """The SDL keyboard instance ID. Zero if unknown or virtual.
- repeat (bool): True if this event exists because of key repeat.
+ .. versionadded:: 21.0
+ """
+ window_id: int = 0
+ """The SDL window ID with keyboard focus.
- .. versionchanged:: 12.5
- `scancode`, `sym`, and `mod` now use their respective enums.
+ .. versionadded:: 21.0
"""
+ pressed: bool = False
+ """True if the key was pressed, False if the key was released.
- def __init__(self, scancode: int, sym: int, mod: int, repeat: bool = False) -> None:
- super().__init__()
- self.scancode = Scancode(scancode)
- self.sym = KeySym(sym)
- self.mod = Modifier(mod)
- self.repeat = repeat
+ .. versionadded:: 21.0
+ """
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Any:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
keysym = sdl_event.key
- self = cls(keysym.scancode, keysym.key, keysym.mod, bool(sdl_event.key.repeat))
- self.sdl_event = sdl_event
- return self
-
- def __repr__(self) -> str:
- return "tcod.event.{}(scancode={!r}, sym={!r}, mod={!r}{})".format(
- self.__class__.__name__,
- self.scancode,
- self.sym,
- self.mod,
- ", repeat=True" if self.repeat else "",
+ return cls(
+ scancode=Scancode(keysym.scancode),
+ sym=KeySym(keysym.key),
+ mod=Modifier(keysym.mod),
+ repeat=bool(keysym.repeat),
+ pressed=bool(keysym.down),
+ which=int(keysym.which),
+ window_id=int(keysym.windowID),
+ **_unpack_sdl_event(sdl_event),
)
- def __str__(self) -> str:
- return self.__repr__().replace("tcod.event.", "")
-
+@attrs.define(slots=True, kw_only=True)
class KeyDown(KeyboardEvent):
- pass
+ """A :any:`KeyboardEvent` where the key was pressed."""
+@attrs.define(slots=True, kw_only=True)
class KeyUp(KeyboardEvent):
- pass
+ """A :any:`KeyboardEvent` where the key was released."""
+@attrs.define(slots=True, kw_only=True)
class MouseState(Event):
"""Mouse state.
- Attributes:
- type (str): Always "MOUSESTATE".
- 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.
-
- Will be a combination of the following names:
-
- * tcod.event.BUTTON_LMASK
- * tcod.event.BUTTON_MMASK
- * tcod.event.BUTTON_RMASK
- * tcod.event.BUTTON_X1MASK
- * tcod.event.BUTTON_X2MASK
-
.. versionadded:: 9.3
.. versionchanged:: 15.0
Renamed `pixel` attribute to `position`.
"""
- def __init__(
- self,
- position: tuple[float, float] = (0, 0),
- tile: tuple[float, float] | None = (0, 0),
- state: int = 0,
- ) -> None:
- super().__init__()
- self.position = Point(*position)
- self._tile = Point(*tile) if tile is not None else None
- self.state = state
+ position: Point[float] = attrs.field(default=Point(0.0, 0.0))
+ """The position coordinates of the mouse."""
+ _tile: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile")
+
+ state: MouseButtonMask = attrs.field(default=MouseButtonMask(0))
+ """A bitmask of which mouse buttons are currently held."""
+
+ which: int = 0
+ """The mouse device ID for this event.
+
+ .. versionadded:: 21.0
+ """
+
+ window_id: int = 0
+ """The window ID with mouse focus.
+
+ .. versionadded:: 21.0
+ """
+
+ @property
+ def integer_position(self) -> Point[int]:
+ """Integer coordinates of this event.
+
+ .. versionadded:: 21.0
+ """
+ x, y = self.position
+ return Point(floor(x), floor(y))
@property
- def pixel(self) -> Point:
- warnings.warn(
- "The mouse.pixel attribute is deprecated. Use mouse.position instead.",
- DeprecationWarning,
- stacklevel=2,
- )
+ @deprecated("The mouse.pixel attribute is deprecated. Use mouse.position instead.")
+ def pixel(self) -> Point[float]: # noqa: D102 # Skip docstring for deprecated attribute
return self.position
@pixel.setter
- def pixel(self, value: Point) -> None:
+ def pixel(self, value: Point[float]) -> None:
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,
- )
+ @deprecated(
+ "The mouse.tile attribute is deprecated."
+ " Use mouse.integer_position of the event returned by context.convert_event instead."
+ )
+ def tile(self) -> Point[int]:
+ """The integer tile coordinates of the mouse on the screen.
+
+ .. deprecated:: 21.0
+ Use :any:`integer_position` of the event returned by :any:`Context.convert_event` instead.
+ """
return _verify_tile_coordinates(self._tile)
@tile.setter
- def tile(self, xy: tuple[float, float]) -> None:
+ @deprecated(
+ "The mouse.tile attribute is deprecated."
+ " Use mouse.integer_position of the event returned by context.convert_event instead."
+ )
+ def tile(self, xy: tuple[int, int]) -> None:
self._tile = Point(*xy)
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, tile={tuple(self.tile)!r}, state={MouseButtonMask(self.state)})"
-
- def __str__(self) -> str:
- return ("<%s, position=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % (
- super().__str__().strip("<>"),
- *self.position,
- *self.tile,
- MouseButtonMask(self.state),
- )
-
+@attrs.define(slots=True, kw_only=True)
class MouseMotion(MouseState):
"""Mouse motion event.
- Attributes:
- type (str): Always "MOUSEMOTION".
- 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.
-
- Will be a combination of the following names:
-
- * tcod.event.BUTTON_LMASK
- * tcod.event.BUTTON_MMASK
- * 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`.
+
+ .. versionchanged:: 19.0
+ `position` and `motion` now use floating point coordinates.
"""
- def __init__(
- self,
- position: tuple[float, float] = (0, 0),
- motion: tuple[float, float] = (0, 0),
- tile: tuple[float, float] | None = (0, 0),
- tile_motion: tuple[float, float] | None = (0, 0),
- state: int = 0,
- ) -> None:
- super().__init__(position, tile, state)
- self.motion = Point(*motion)
- self._tile_motion = Point(*tile_motion) if tile_motion is not None else None
+ motion: Point[float] = attrs.field(default=Point(0.0, 0.0))
+ """The pixel delta."""
+ _tile_motion: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile_motion")
@property
- def pixel_motion(self) -> Point:
- warnings.warn(
- "The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.",
- DeprecationWarning,
- stacklevel=2,
- )
+ def integer_motion(self) -> Point[int]:
+ """Integer motion of this event.
+
+ .. versionadded:: 21.0
+ """
+ x, y = self.position
+ dx, dy = self.motion
+ prev_x, prev_y = x - dx, y - dy
+ return Point(floor(x) - floor(prev_x), floor(y) - floor(prev_y))
+
+ @property
+ @deprecated("The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.")
+ def pixel_motion(self) -> Point[float]: # noqa: D102 # Skip docstring for deprecated attribute
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,
- )
+ @deprecated("The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.")
+ def pixel_motion(self, value: Point[float]) -> None:
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,
- )
+ @deprecated(
+ "The mouse.tile_motion attribute is deprecated."
+ " Use mouse.integer_motion of the event returned by context.convert_event instead."
+ )
+ def tile_motion(self) -> Point[int]:
+ """The tile delta.
+
+ .. deprecated:: 21.0
+ Use :any:`integer_motion` of the event returned by :any:`Context.convert_event` instead.
+ """
return _verify_tile_coordinates(self._tile_motion)
@tile_motion.setter
- def tile_motion(self, xy: tuple[float, float]) -> 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,
- )
+ @deprecated(
+ "The mouse.tile_motion attribute is deprecated."
+ " Use mouse.integer_motion of the event returned by context.convert_event instead."
+ )
+ def tile_motion(self, xy: tuple[int, int]) -> None:
self._tile_motion = Point(*xy)
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> MouseMotion:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
motion = sdl_event.motion
+ common = {"which": int(motion.which), "window_id": int(motion.windowID)}
+ state = MouseButtonMask(motion.state)
- pixel = motion.x, motion.y
- pixel_motion = motion.xrel, motion.yrel
- subtile = _pixel_to_tile(*pixel)
+ pixel = Point(float(motion.x), float(motion.y))
+ pixel_motion = Point(float(motion.xrel), float(motion.yrel))
+ subtile = _pixel_to_tile(pixel)
if subtile is None:
- self = cls(pixel, pixel_motion, None, None, motion.state)
+ self = cls(
+ position=pixel,
+ motion=pixel_motion,
+ tile=None,
+ tile_motion=None,
+ state=state,
+ **common,
+ **_unpack_sdl_event(sdl_event),
+ )
else:
- tile = int(subtile[0]), int(subtile[1])
- prev_pixel = pixel[0] - pixel_motion[0], pixel[1] - pixel_motion[1]
- prev_subtile = _pixel_to_tile(*prev_pixel) or (0, 0)
- prev_tile = int(prev_subtile[0]), int(prev_subtile[1])
- tile_motion = tile[0] - prev_tile[0], tile[1] - prev_tile[1]
- self = cls(pixel, pixel_motion, tile, tile_motion, motion.state)
+ tile = Point(floor(subtile[0]), floor(subtile[1]))
+ prev_pixel = (pixel[0] - pixel_motion[0], pixel[1] - pixel_motion[1])
+ prev_subtile = _pixel_to_tile(prev_pixel) or (0, 0)
+ prev_tile = floor(prev_subtile[0]), floor(prev_subtile[1])
+ tile_motion = Point(tile[0] - prev_tile[0], tile[1] - prev_tile[1])
+ self = cls(
+ position=pixel,
+ motion=pixel_motion,
+ tile=tile,
+ tile_motion=tile_motion,
+ state=state,
+ **common,
+ **_unpack_sdl_event(sdl_event),
+ )
self.sdl_event = sdl_event
return self
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, motion={tuple(self.motion)!r}, tile={tuple(self.tile)!r}, tile_motion={tuple(self.tile_motion)!r}, state={MouseButtonMask(self.state)!r})"
- def __str__(self) -> str:
- return ("<%s, motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % (
- super().__str__().strip("<>"),
- *self.motion,
- *self.tile_motion,
- )
+@attrs.define(slots=True, kw_only=True)
+class MouseButtonEvent(Event):
+ """Mouse button event.
+ .. versionchanged:: 19.0
+ `position` and `tile` now use floating point coordinates.
-class MouseButtonEvent(MouseState):
- """Mouse button event.
+ .. versionchanged:: 21.0
+ No longer a subclass of :any:`MouseState`.
+ """
- Attributes:
- type (str): Will be "MOUSEBUTTONDOWN" or "MOUSEBUTTONUP",
- depending on the event.
- 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.
+ position: Point[float] = attrs.field(default=Point(0.0, 0.0))
+ """The coordinates of the mouse."""
+ _tile: Point[int] | None = attrs.field(default=Point(0, 0), alias="tile")
+ """The tile integer coordinates of the mouse on the screen. Deprecated."""
+ button: MouseButton
+ """Which mouse button index was pressed or released in this event.
- This will be one of the following names:
+ .. versionchanged:: 21.0
+ Is now strictly a :any:`MouseButton` type.
+ """
- * tcod.event.BUTTON_LEFT
- * tcod.event.BUTTON_MIDDLE
- * tcod.event.BUTTON_RIGHT
- * tcod.event.BUTTON_X1
- * tcod.event.BUTTON_X2
+ which: int = 0
+ """The mouse device ID for this event.
+ .. versionadded:: 21.0
"""
- def __init__(
- self,
- pixel: tuple[float, float] = (0, 0),
- tile: tuple[float, float] | None = (0, 0),
- button: int = 0,
- ) -> None:
- super().__init__(pixel, tile, button)
+ window_id: int = 0
+ """The window ID with mouse focus.
+
+ .. versionadded:: 21.0
+ """
@property
- def button(self) -> int:
- return self.state
+ def integer_position(self) -> Point[int]:
+ """Integer coordinates of this event.
- @button.setter
- def button(self, value: int) -> None:
- self.state = value
+ .. versionadded:: 21.1
+ """
+ x, y = self.position
+ return Point(floor(x), floor(y))
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Any:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
button = sdl_event.button
- pixel = button.x, button.y
- subtile = _pixel_to_tile(*pixel)
+ pixel = Point(float(button.x), float(button.y))
+ subtile = _pixel_to_tile(pixel)
if subtile is None:
- tile: tuple[float, float] | None = None
+ tile: Point[int] | None = None
else:
- tile = float(subtile[0]), float(subtile[1])
- self = cls(pixel, tile, button.button)
+ tile = Point(floor(subtile[0]), floor(subtile[1]))
+ self = cls(
+ position=pixel,
+ tile=tile,
+ button=MouseButton(button.button),
+ which=int(button.which),
+ window_id=int(button.windowID),
+ **_unpack_sdl_event(sdl_event),
+ )
self.sdl_event = sdl_event
return self
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(position={tuple(self.position)!r}, tile={tuple(self.tile)!r}, button={MouseButton(self.button)!r})"
-
- def __str__(self) -> str:
- return " int: # noqa: D102 # Skip docstring for deprecated property
+ return int(self.button)
+@attrs.define(slots=True, kw_only=True)
class MouseButtonDown(MouseButtonEvent):
- """Same as MouseButtonEvent but with ``type="MouseButtonDown"``."""
+ """Mouse button has been pressed."""
+@attrs.define(slots=True, kw_only=True)
class MouseButtonUp(MouseButtonEvent):
- """Same as MouseButtonEvent but with ``type="MouseButtonUp"``."""
+ """Mouse button has been released."""
+@attrs.define(slots=True, kw_only=True)
class MouseWheel(Event):
- """Mouse wheel event.
-
- Attributes:
- type (str): Always "MOUSEWHEEL".
- x (int): Horizontal scrolling. A positive value means scrolling right.
- y (int): Vertical scrolling. A positive value means scrolling away from
- the user.
- flipped (bool): If True then the values of `x` and `y` are the opposite
- of their usual values. This depends on the settings of
- the Operating System.
+ """Mouse wheel event."""
+
+ x: int
+ """Horizontal scrolling. A positive value means scrolling right."""
+ y: int
+ """Vertical scrolling. A positive value means scrolling away from the user."""
+ flipped: bool
+ """If True then the values of `x` and `y` are the opposite of their usual values.
+ This depends on the operating system settings.
+ """
+
+ position: Point[float] = attrs.field(default=Point(0.0, 0.0))
+ """Coordinates of the mouse for this event.
+
+ .. versionadded:: 21.2
+ """
+
+ which: int = 0
+ """Mouse device ID for this event.
+
+ .. versionadded:: 21.2
"""
- def __init__(self, x: int, y: int, flipped: bool = False) -> None:
- super().__init__()
- self.x = x
- self.y = y
- self.flipped = flipped
+ window_id: int = 0
+ """Window ID with mouse focus.
+
+ .. versionadded:: 21.2
+ """
+
+ @property
+ def integer_position(self) -> Point[int]:
+ """Integer coordinates of this event.
+
+ .. versionadded:: 21.2
+ """
+ x, y = self.position
+ return Point(floor(x), floor(y))
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> MouseWheel:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
wheel = sdl_event.wheel
- self = cls(wheel.x, wheel.y, bool(wheel.direction))
- self.sdl_event = sdl_event
- return self
-
- def __repr__(self) -> str:
- return "tcod.event.%s(x=%i, y=%i%s)" % (
- self.__class__.__name__,
- self.x,
- self.y,
- ", flipped=True" if self.flipped else "",
+ return cls(
+ x=int(wheel.integer_x),
+ y=int(wheel.integer_y),
+ flipped=bool(wheel.direction),
+ position=Point(float(wheel.mouse_x), float(wheel.mouse_y)),
+ which=int(wheel.which),
+ window_id=int(wheel.windowID),
+ **_unpack_sdl_event(sdl_event),
)
- def __str__(self) -> str:
- return "<%s, x=%i, y=%i, flipped=%r)" % (
- super().__str__().strip("<>"),
- self.x,
- self.y,
- self.flipped,
- )
+
+@runtime_checkable
+class _MouseEventWithPosition(Protocol):
+ """Mouse event with position. Used internally to handle conversions."""
+
+ position: Point[float]
+
+
+@runtime_checkable
+class _MouseEventWithTile(Protocol):
+ """Mouse event with position and deprecated tile attribute. Used internally to handle conversions."""
+
+ position: Point[float]
+ _tile: Point[int] | None
+@attrs.define(slots=True, kw_only=True)
class TextInput(Event):
"""SDL text input event.
- Attributes:
- type (str): Always "TEXTINPUT".
- text (str): A Unicode string with the input.
+ .. warning::
+ These events are not enabled by default since `19.0`.
+
+ Use :any:`Window.start_text_input` to enable this event.
"""
- def __init__(self, text: str) -> None:
- super().__init__()
- self.text = text
+ text: str
+ """A Unicode string with the input."""
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> TextInput:
- self = cls(ffi.string(sdl_event.text.text, 32).decode("utf8"))
- self.sdl_event = sdl_event
- return self
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(text=str(ffi.string(sdl_event.text.text, 32), encoding="utf8"), **_unpack_sdl_event(sdl_event))
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(text={self.text!r})"
- def __str__(self) -> str:
- return "<{}, text={!r})".format(super().__str__().strip("<>"), self.text)
+_WindowTypes = Literal[
+ "WindowShown",
+ "WindowHidden",
+ "WindowExposed",
+ "WindowMoved",
+ "WindowResized",
+ "PixelSizeChanged",
+ "MetalViewResized",
+ "WindowMinimized",
+ "WindowMaximized",
+ "WindowRestored",
+ "WindowEnter",
+ "WindowLeave",
+ "WindowFocusGained",
+ "WindowFocusLost",
+ "WindowClose",
+ "WindowTakeFocus",
+ "WindowHitTest",
+ "ICCProfileChanged",
+ "DisplayChanged",
+ "DisplayScaleChanged",
+ "SafeAreaChanged",
+ "Occluded",
+ "EnterFullscreen",
+ "LeaveFullscreen",
+ "Destroyed",
+ "HDRStateChanged",
+]
+@attrs.define(slots=True, kw_only=True)
class WindowEvent(Event):
- """A window event."""
-
- type: Final[ # type: ignore[misc] # Narrowing final type.
- Literal[
- "WindowShown",
- "WindowHidden",
- "WindowExposed",
- "WindowMoved",
- "WindowResized",
- "WindowSizeChanged",
- "WindowMinimized",
- "WindowMaximized",
- "WindowRestored",
- "WindowEnter",
- "WindowLeave",
- "WindowFocusGained",
- "WindowFocusLost",
- "WindowClose",
- "WindowTakeFocus",
- "WindowHitTest",
- ]
- ]
+ """A window event.
+
+ Example::
+
+ match event:
+ case tcod.event.WindowEvent(type="WindowShown", window_id=window_id):
+ print(f"Window {window_id} was shown")
+ case tcod.event.WindowEvent(type="WindowHidden", window_id=window_id):
+ print(f"Window {window_id} was hidden")
+ case tcod.event.WindowEvent(type="WindowExposed", window_id=window_id):
+ print(f"Window {window_id} was exposed and needs to be redrawn")
+ case tcod.event.WindowEvent(type="WindowMoved", data=(x, y), window_id=window_id):
+ print(f"Window {window_id} was moved to {x=},{y=}")
+ case tcod.event.WindowEvent(type="WindowResized", data=(width, height), window_id=window_id):
+ print(f"Window {window_id} was resized to {width=},{height=}")
+ case tcod.event.WindowEvent(type="WindowMinimized", window_id=window_id):
+ print(f"Window {window_id} was minimized")
+ case tcod.event.WindowEvent(type="WindowMaximized", window_id=window_id):
+ print(f"Window {window_id} was maximized")
+ case tcod.event.WindowEvent(type="WindowRestored", window_id=window_id):
+ print(f"Window {window_id} was restored")
+ case tcod.event.WindowEvent(type="WindowEnter", window_id=window_id):
+ print(f"Mouse cursor has entered window {window_id}")
+ case tcod.event.WindowEvent(type="WindowLeave", window_id=window_id):
+ print(f"Mouse cursor has left window {window_id}")
+ case tcod.event.WindowEvent(type="WindowFocusGained", window_id=window_id):
+ print(f"Window {window_id} has gained keyboard focus")
+ case tcod.event.WindowEvent(type="WindowFocusLost", window_id=window_id):
+ print(f"Window {window_id} has lost keyboard focus")
+ case tcod.event.WindowEvent(type="WindowClose", window_id=window_id):
+ print(f"Window {window_id} has been closed")
+ case tcod.event.WindowEvent(type="DisplayChanged", data=(display_id, _), window_id=window_id):
+ print(f"Window {window_id} has been moved to display {display_id}")
+ case tcod.event.WindowEvent(type=subtype, data=data, window_id=window_id):
+ print(f"Other window event {subtype} on window {window_id} with {data=}")
+
+ .. versionchanged:: 21.0
+ Added `data` and `window_id` attributes and added missing SDL3 window events.
+ """
+
+ type: Final[_WindowTypes]
"""The current window event. This can be one of various options."""
- @classmethod
- 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]
- self: WindowEvent
- if sdl_event.window.event == lib.SDL_EVENT_WINDOW_MOVED:
- self = WindowMoved(sdl_event.window.data1, sdl_event.window.data2)
- elif sdl_event.window.event in (
- lib.SDL_EVENT_WINDOW_RESIZED,
- lib.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED,
- ):
- self = WindowResized(event_type, sdl_event.window.data1, sdl_event.window.data2)
- else:
- self = cls(event_type)
- self.sdl_event = sdl_event
- return self
+ window_id: int
+ """The SDL window ID associated with this event."""
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(type={self.type!r})"
-
- __WINDOW_TYPES: Final = {
- lib.SDL_EVENT_WINDOW_SHOWN: "WindowShown",
- lib.SDL_EVENT_WINDOW_HIDDEN: "WindowHidden",
- lib.SDL_EVENT_WINDOW_EXPOSED: "WindowExposed",
- lib.SDL_EVENT_WINDOW_MOVED: "WindowMoved",
- lib.SDL_EVENT_WINDOW_RESIZED: "WindowResized",
- lib.SDL_EVENT_WINDOW_MINIMIZED: "WindowMinimized",
- lib.SDL_EVENT_WINDOW_MAXIMIZED: "WindowMaximized",
- lib.SDL_EVENT_WINDOW_RESTORED: "WindowRestored",
- lib.SDL_EVENT_WINDOW_MOUSE_ENTER: "WindowEnter",
- lib.SDL_EVENT_WINDOW_MOUSE_LEAVE: "WindowLeave",
- lib.SDL_EVENT_WINDOW_FOCUS_GAINED: "WindowFocusGained",
- lib.SDL_EVENT_WINDOW_FOCUS_LOST: "WindowFocusLost",
- lib.SDL_EVENT_WINDOW_CLOSE_REQUESTED: "WindowClose",
- lib.SDL_EVENT_WINDOW_HIT_TEST: "WindowHitTest",
- }
+ data: tuple[int, int]
+ """The SDL data associated with this event. What these values are for depends on the event sub-type."""
+ @classmethod
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> WindowEvent | Undefined:
+ if sdl_event.type not in _WINDOW_TYPES_FROM_ENUM:
+ return Undefined._from_sdl_event(sdl_event)
+ event_type: Final = _WINDOW_TYPES_FROM_ENUM[sdl_event.type]
+ new_cls = cls
+ if sdl_event.type == lib.SDL_EVENT_WINDOW_MOVED:
+ new_cls = WindowMoved
+ elif sdl_event.type == lib.SDL_EVENT_WINDOW_RESIZED:
+ new_cls = WindowResized
+ return new_cls(
+ type=event_type,
+ window_id=int(sdl_event.window.windowID),
+ data=(int(sdl_event.window.data1), int(sdl_event.window.data2)),
+ **_unpack_sdl_event(sdl_event),
+ )
-class WindowMoved(WindowEvent):
- """Window moved event.
- Attributes:
- x (int): Movement on the x-axis.
- y (int): Movement on the y-axis.
- """
+_WINDOW_TYPES_FROM_ENUM: Final[dict[int, _WindowTypes]] = {
+ lib.SDL_EVENT_WINDOW_SHOWN: "WindowShown",
+ lib.SDL_EVENT_WINDOW_HIDDEN: "WindowHidden",
+ lib.SDL_EVENT_WINDOW_EXPOSED: "WindowExposed",
+ lib.SDL_EVENT_WINDOW_MOVED: "WindowMoved",
+ lib.SDL_EVENT_WINDOW_RESIZED: "WindowResized",
+ lib.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: "PixelSizeChanged",
+ lib.SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: "MetalViewResized",
+ lib.SDL_EVENT_WINDOW_MINIMIZED: "WindowMinimized",
+ lib.SDL_EVENT_WINDOW_MAXIMIZED: "WindowMaximized",
+ lib.SDL_EVENT_WINDOW_RESTORED: "WindowRestored",
+ lib.SDL_EVENT_WINDOW_MOUSE_ENTER: "WindowEnter",
+ lib.SDL_EVENT_WINDOW_MOUSE_LEAVE: "WindowLeave",
+ lib.SDL_EVENT_WINDOW_FOCUS_GAINED: "WindowFocusGained",
+ lib.SDL_EVENT_WINDOW_FOCUS_LOST: "WindowFocusLost",
+ lib.SDL_EVENT_WINDOW_CLOSE_REQUESTED: "WindowClose",
+ lib.SDL_EVENT_WINDOW_HIT_TEST: "WindowHitTest",
+ lib.SDL_EVENT_WINDOW_ICCPROF_CHANGED: "ICCProfileChanged",
+ lib.SDL_EVENT_WINDOW_DISPLAY_CHANGED: "DisplayChanged",
+ lib.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: "DisplayScaleChanged",
+ lib.SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: "SafeAreaChanged",
+ lib.SDL_EVENT_WINDOW_OCCLUDED: "Occluded",
+ lib.SDL_EVENT_WINDOW_ENTER_FULLSCREEN: "EnterFullscreen",
+ lib.SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: "LeaveFullscreen",
+ lib.SDL_EVENT_WINDOW_DESTROYED: "Destroyed",
+ lib.SDL_EVENT_WINDOW_HDR_STATE_CHANGED: "HDRStateChanged",
+}
- type: Final[Literal["WINDOWMOVED"]] # type: ignore[assignment,misc]
- """Always "WINDOWMOVED"."""
- def __init__(self, x: int, y: int) -> None:
- super().__init__(None)
- self.x = x
- self.y = y
+@attrs.define(slots=True, kw_only=True)
+class WindowMoved(WindowEvent):
+ """Window moved event."""
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, x={self.x!r}, y={self.y!r})"
+ @property
+ def x(self) -> int:
+ """Movement on the x-axis."""
+ return self.data[0]
- def __str__(self) -> str:
- return "<{}, x={!r}, y={!r})".format(
- super().__str__().strip("<>"),
- self.x,
- self.y,
- )
+ @property
+ def y(self) -> int:
+ """Movement on the y-axis."""
+ return self.data[1]
+@attrs.define(slots=True, kw_only=True)
class WindowResized(WindowEvent):
"""Window resized event.
- Attributes:
- width (int): The current width of the window.
- height (int): The current height of the window.
+ .. versionchanged:: 19.4
+ Removed "WindowSizeChanged" type.
"""
- type: Final[Literal["WindowResized", "WindowSizeChanged"]] # type: ignore[misc]
- """WindowResized" or "WindowSizeChanged"""
-
- def __init__(self, type: str, width: int, height: int) -> None:
- super().__init__(type)
- self.width = width
- self.height = height
-
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, width={self.width!r}, height={self.height!r})"
+ @property
+ def width(self) -> int:
+ """The current width of the window."""
+ return self.data[0]
- def __str__(self) -> str:
- return "<{}, width={!r}, height={!r})".format(
- super().__str__().strip("<>"),
- self.width,
- self.height,
- )
+ @property
+ def height(self) -> int:
+ """The current height of the window."""
+ return self.data[1]
+@attrs.define(slots=True, kw_only=True)
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."""
+ which: int
+ """The ID of the joystick this event is for."""
@property
def joystick(self) -> tcod.sdl.joystick.Joystick:
- if self.type == "JOYDEVICEADDED":
+ """The :any:`Joystick` for this event."""
+ if isinstance(self, JoystickDevice) and 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__}(type={self.type!r}, which={self.which})"
-
- def __str__(self) -> str:
- prefix = super().__str__().strip("<>")
- return f"<{prefix}, which={self.which}>"
-
+@attrs.define(slots=True, kw_only=True)
class JoystickAxis(JoystickEvent):
"""When a joystick axis changes in value.
@@ -848,31 +989,24 @@ class JoystickAxis(JoystickEvent):
:any:`tcod.sdl.joystick`
"""
- which: int
- """The ID of the joystick this event is for."""
+ _type: Final[Literal["JOYAXISMOTION"]] = "JOYAXISMOTION"
- 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."""
+ axis: int
+ """The index of the changed axis."""
+ value: int
+ """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 _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(
+ which=int(sdl_event.jaxis.which),
+ axis=int(sdl_event.jaxis.axis),
+ value=int(sdl_event.jaxis.value),
+ **_unpack_sdl_event(sdl_event),
)
- def __str__(self) -> str:
- prefix = super().__str__().strip("<>")
- return f"<{prefix}, axis={self.axis}, value={self.value}>"
-
+@attrs.define(slots=True, kw_only=True)
class JoystickBall(JoystickEvent):
"""When a joystick ball is moved.
@@ -882,35 +1016,27 @@ class JoystickBall(JoystickEvent):
:any:`tcod.sdl.joystick`
"""
- which: int
- """The ID of the joystick this event is for."""
+ _type: Final[Literal["JOYBALLMOTION"]] = "JOYBALLMOTION"
- 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."""
+ ball: int
+ """The index of the moved ball."""
+ dx: int
+ """The X motion of the ball."""
+ dy: int
+ """The Y motion of the ball."""
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> JoystickBall:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
return cls(
- "JOYBALLMOTION", sdl_event.jball.which, sdl_event.jball.ball, sdl_event.jball.xrel, sdl_event.jball.yrel
+ which=int(sdl_event.jball.which),
+ ball=int(sdl_event.jball.ball),
+ dx=int(sdl_event.jball.xrel),
+ dy=int(sdl_event.jball.yrel),
+ **_unpack_sdl_event(sdl_event),
)
- 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}>"
-
+@attrs.define(slots=True, kw_only=True)
class JoystickHat(JoystickEvent):
"""When a joystick hat changes direction.
@@ -920,28 +1046,20 @@ class JoystickHat(JoystickEvent):
:any:`tcod.sdl.joystick`
"""
- which: int
- """The ID of the joystick this event is for."""
+ _type: Final[Literal["JOYHATMOTION"]] = "JOYHATMOTION"
- 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."""
+ x: Literal[-1, 0, 1]
+ """The new X direction of the hat."""
+ y: Literal[-1, 0, 1]
+ """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__}(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}>"
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ x, y = _HAT_DIRECTIONS[sdl_event.jhat.hat]
+ return cls(which=int(sdl_event.jhat.which), x=x, y=y, **_unpack_sdl_event(sdl_event))
+@attrs.define(slots=True, kw_only=True)
class JoystickButton(JoystickEvent):
"""When a joystick button is pressed or released.
@@ -957,35 +1075,32 @@ class JoystickButton(JoystickEvent):
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."""
+ button: int
+ """The index of the button this event is for."""
+ pressed: bool
+ """True if the button was pressed, False if the button was released."""
@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_EVENT_JOYSTICK_BUTTON_DOWN: "JOYBUTTONDOWN",
- lib.SDL_EVENT_JOYSTICK_BUTTON_UP: "JOYBUTTONUP",
- }[sdl_event.type]
- return cls(type, sdl_event.jbutton.which, sdl_event.jbutton.button)
+ @deprecated("Check 'JoystickButton.pressed' instead of '.type'.")
+ def type(self) -> Literal["JOYBUTTONUP", "JOYBUTTONDOWN"]:
+ """Button state as a string.
- def __repr__(self) -> str:
- return f"tcod.event.{self.__class__.__name__}(type={self.type!r}, which={self.which}, button={self.button})"
+ .. deprecated:: 21.0
+ Use :any:`pressed` instead.
+ """
+ return ("JOYBUTTONUP", "JOYBUTTONDOWN")[self.pressed]
- def __str__(self) -> str:
- prefix = super().__str__().strip("<>")
- return f"<{prefix}, button={self.button}>"
+ @classmethod
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(
+ which=int(sdl_event.jbutton.which),
+ button=int(sdl_event.jbutton.button),
+ pressed=bool(sdl_event.jbutton.down),
+ **_unpack_sdl_event(sdl_event),
+ )
+@attrs.define(slots=True, kw_only=True)
class JoystickDevice(JoystickEvent):
"""An event for when a joystick is added or removed.
@@ -1002,7 +1117,7 @@ class JoystickDevice(JoystickEvent):
joysticks.remove(joystick)
"""
- type: Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]] # type: ignore[misc]
+ type: Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]]
which: int
"""When type="JOYDEVICEADDED" this is the device ID.
@@ -1010,150 +1125,212 @@ class JoystickDevice(JoystickEvent):
"""
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> JoystickDevice:
- type = {
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ types: Final[dict[int, Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]]] = {
lib.SDL_EVENT_JOYSTICK_ADDED: "JOYDEVICEADDED",
lib.SDL_EVENT_JOYSTICK_REMOVED: "JOYDEVICEREMOVED",
- }[sdl_event.type]
- return cls(type, sdl_event.jdevice.which)
+ }
+ return cls(type=types[sdl_event.type], which=int(sdl_event.jdevice.which), **_unpack_sdl_event(sdl_event))
+@attrs.define(slots=True, kw_only=True)
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."""
+ which: int
+ """The ID of the controller this event is for."""
@property
def controller(self) -> tcod.sdl.joystick.GameController:
"""The :any:`GameController` for this event."""
- if self.type == "CONTROLLERDEVICEADDED":
+ if isinstance(self, ControllerDevice) and 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__}(type={self.type!r}, which={self.which})"
-
- def __str__(self) -> str:
- prefix = super().__str__().strip("<>")
- return f"<{prefix}, which={self.which}>"
-
+@attrs.define(slots=True, kw_only=True)
class ControllerAxis(ControllerEvent):
"""When a controller axis is moved.
.. versionadded:: 13.8
"""
- type: Final[Literal["CONTROLLERAXISMOTION"]] # type: ignore[misc]
+ _type: Final[Literal["CONTROLLERAXISMOTION"]] = "CONTROLLERAXISMOTION"
- 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.
+ axis: int
+ """Which axis is being moved. One of :any:`ControllerAxis`."""
+ value: int
+ """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."""
+ 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:
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
return cls(
- "CONTROLLERAXISMOTION",
- sdl_event.caxis.which,
- tcod.sdl.joystick.ControllerAxis(sdl_event.caxis.axis),
- sdl_event.caxis.value,
+ which=int(sdl_event.gaxis.which),
+ axis=tcod.sdl.joystick.ControllerAxis(sdl_event.gaxis.axis),
+ value=int(sdl_event.gaxis.value),
+ **_unpack_sdl_event(sdl_event),
)
- 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}>"
-
+@attrs.define(slots=True, kw_only=True)
class ControllerButton(ControllerEvent):
"""When a controller button is pressed or released.
.. versionadded:: 13.8
"""
- type: Final[Literal["CONTROLLERBUTTONDOWN", "CONTROLLERBUTTONUP"]] # type: ignore[misc]
+ button: tcod.sdl.joystick.ControllerButton
+ """The button for this event. One of :any:`ControllerButton`."""
+ pressed: bool
+ """True if the button was pressed, False if it was released."""
- 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."""
+ @property
+ @deprecated("Check 'ControllerButton.pressed' instead of '.type'.")
+ def type(self) -> Literal["CONTROLLERBUTTONUP", "CONTROLLERBUTTONDOWN"]:
+ """Button state as a string.
+
+ .. deprecated:: 21.0
+ Use :any:`pressed` instead.
+ """
+ return ("CONTROLLERBUTTONUP", "CONTROLLERBUTTONDOWN")[self.pressed]
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> ControllerButton:
- type = {
- lib.SDL_EVENT_GAMEPAD_BUTTON_DOWN: "CONTROLLERBUTTONDOWN",
- lib.SDL_EVENT_GAMEPAD_BUTTON_UP: "CONTROLLERBUTTONUP",
- }[sdl_event.type]
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
return cls(
- type,
- sdl_event.cbutton.which,
- tcod.sdl.joystick.ControllerButton(sdl_event.cbutton.button),
- bool(sdl_event.cbutton.down),
- )
-
- 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})"
+ which=int(sdl_event.gbutton.which),
+ button=tcod.sdl.joystick.ControllerButton(sdl_event.gbutton.button),
+ pressed=bool(sdl_event.gbutton.down),
+ **_unpack_sdl_event(sdl_event),
)
- def __str__(self) -> str:
- prefix = super().__str__().strip("<>")
- return f"<{prefix}, button={self.button}, pressed={self.pressed}>"
-
+@attrs.define(slots=True, kw_only=True)
class ControllerDevice(ControllerEvent):
"""When a controller is added, removed, or remapped.
.. versionadded:: 13.8
"""
- type: Final[Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]] # type: ignore[misc]
+ type: Final[Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]]
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> ControllerDevice:
- type = {
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ types: dict[int, Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]] = {
lib.SDL_EVENT_GAMEPAD_ADDED: "CONTROLLERDEVICEADDED",
lib.SDL_EVENT_GAMEPAD_REMOVED: "CONTROLLERDEVICEREMOVED",
lib.SDL_EVENT_GAMEPAD_REMAPPED: "CONTROLLERDEVICEREMAPPED",
- }[sdl_event.type]
- return cls(type, sdl_event.cdevice.which)
+ }
+ return cls(type=types[sdl_event.type], which=int(sdl_event.gdevice.which), **_unpack_sdl_event(sdl_event))
+
+
+@attrs.define(slots=True, kw_only=True)
+class ClipboardUpdate(Event):
+ """Announces changed contents of the clipboard.
+
+ .. versionadded:: 21.0
+ """
+
+ mime_types: tuple[str, ...]
+ """The MIME types of the clipboard."""
+
+ @classmethod
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(
+ mime_types=tuple(
+ str(ffi.string(sdl_event.clipboard.mime_types[i]), encoding="utf8")
+ for i in range(sdl_event.clipboard.num_mime_types)
+ ),
+ **_unpack_sdl_event(sdl_event),
+ )
+
+
+@attrs.define(slots=True, kw_only=True)
+class Drop(Event):
+ """Handle dropping text or files on the window.
+
+ Example::
+
+ match event:
+ case tcod.event.Drop(type="BEGIN"):
+ print("Object dragged over the window")
+ case tcod.event.Drop(type="POSITION", position=position):
+ pass
+ case tcod.event.Drop(type="TEXT", position=position, text=text):
+ print(f"Dropped {text=} at {position=}")
+ case tcod.event.Drop(type="FILE", position=position, path=path):
+ print(f"Dropped {path=} at {position=}")
+ case tcod.event.Drop(type="COMPLETE"):
+ print("Drop handling finished")
+
+ .. versionadded:: 21.0
+ """
+
+ type: Literal["BEGIN", "FILE", "TEXT", "COMPLETE", "POSITION"]
+ """The subtype of this event."""
+ window_id: int
+ """The active window ID for this event."""
+ position: Point[float]
+ """Mouse position relative to the window. Available in all subtypes except for ``type="BEGIN"``."""
+ source: str
+ """The source app for this event, or an empty string if unavailable."""
+ text: str
+ """The dropped data of a ``Drop(type="TEXT")`` or ``Drop(type="FILE")`` event.
+
+ - If ``Drop(type="TEXT")`` then `text` is the dropped string.
+ - If ``Drop(type="FILE")`` then `text` is the str path of the dropped file.
+ Alternatively :any:`path` can be used.
+ - Otherwise `text` is an empty string.
+ """
+
+ @property
+ def path(self) -> Path:
+ """Return the current `text` as a :any:`Path`."""
+ return Path(self.text)
+
+ @classmethod
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ types: dict[int, Literal["BEGIN", "FILE", "TEXT", "COMPLETE", "POSITION"]] = {
+ lib.SDL_EVENT_DROP_BEGIN: "BEGIN",
+ lib.SDL_EVENT_DROP_FILE: "FILE",
+ lib.SDL_EVENT_DROP_TEXT: "TEXT",
+ lib.SDL_EVENT_DROP_COMPLETE: "COMPLETE",
+ lib.SDL_EVENT_DROP_POSITION: "POSITION",
+ }
+ return cls(
+ type=types[sdl_event.drop.type],
+ window_id=int(sdl_event.drop.windowID),
+ position=Point(float(sdl_event.drop.x), float(sdl_event.drop.y)),
+ source=str(ffi.string(sdl_event.drop.source), encoding="utf8") if sdl_event.drop.source else "",
+ text=str(ffi.string(sdl_event.drop.data), encoding="utf8") if sdl_event.drop.data else "",
+ **_unpack_sdl_event(sdl_event),
+ )
+@functools.cache
+def _find_event_name(index: int, /) -> str:
+ """Return the SDL event name for this index."""
+ for attr in dir(lib):
+ if attr.startswith("SDL_EVENT_") and getattr(lib, attr) == index:
+ return attr
+ return "???"
+
+
+@attrs.define(slots=True, kw_only=True)
class Undefined(Event):
"""This class is a place holder for SDL events without their own tcod.event class."""
- def __init__(self) -> None:
- super().__init__("")
-
@classmethod
- def from_sdl_event(cls, sdl_event: Any) -> Undefined:
- self = cls()
- self.sdl_event = sdl_event
- return self
+ def _from_sdl_event(cls, sdl_event: _C_SDL_Event) -> Self:
+ return cls(**_unpack_sdl_event(sdl_event))
- def __str__(self) -> str:
- if self.sdl_event:
- return "" % self.sdl_event.type
- return ""
+ def __repr__(self) -> str:
+ """Return debug info for this undefined event, including the SDL event name."""
+ return f""
_SDL_TO_CLASS_TABLE: dict[int, type[Event]] = {
@@ -1165,7 +1342,6 @@ def __str__(self) -> str:
lib.SDL_EVENT_MOUSE_BUTTON_UP: MouseButtonUp,
lib.SDL_EVENT_MOUSE_WHEEL: MouseWheel,
lib.SDL_EVENT_TEXT_INPUT: TextInput,
- # lib.SDL_EVENT_WINDOW_EVENT: WindowEvent,
lib.SDL_EVENT_JOYSTICK_AXIS_MOTION: JoystickAxis,
lib.SDL_EVENT_JOYSTICK_BALL_MOTION: JoystickBall,
lib.SDL_EVENT_JOYSTICK_HAT_MOTION: JoystickHat,
@@ -1179,17 +1355,25 @@ def __str__(self) -> str:
lib.SDL_EVENT_GAMEPAD_ADDED: ControllerDevice,
lib.SDL_EVENT_GAMEPAD_REMOVED: ControllerDevice,
lib.SDL_EVENT_GAMEPAD_REMAPPED: ControllerDevice,
+ lib.SDL_EVENT_CLIPBOARD_UPDATE: ClipboardUpdate,
+ lib.SDL_EVENT_DROP_BEGIN: Drop,
+ lib.SDL_EVENT_DROP_FILE: Drop,
+ lib.SDL_EVENT_DROP_TEXT: Drop,
+ lib.SDL_EVENT_DROP_COMPLETE: Drop,
+ lib.SDL_EVENT_DROP_POSITION: Drop,
}
-def _parse_event(sdl_event: Any) -> Event:
+def _parse_event(sdl_event: _C_SDL_Event) -> 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)
+ if sdl_event.type in _SDL_TO_CLASS_TABLE:
+ return _SDL_TO_CLASS_TABLE[sdl_event.type]._from_sdl_event(sdl_event)
+ if sdl_event.type in _WINDOW_TYPES_FROM_ENUM:
+ return WindowEvent._from_sdl_event(sdl_event)
+ return Undefined._from_sdl_event(sdl_event)
-def get() -> Iterator[Any]:
+def get() -> Iterator[Event]:
"""Return an iterator for all pending events.
Events are processed as the iterator is consumed.
@@ -1206,13 +1390,10 @@ def get() -> Iterator[Any]:
return
sdl_event = ffi.new("SDL_Event*")
while lib.SDL_PollEvent(sdl_event):
- if sdl_event.type in _SDL_TO_CLASS_TABLE:
- yield _SDL_TO_CLASS_TABLE[sdl_event.type].from_sdl_event(sdl_event)
- else:
- yield Undefined.from_sdl_event(sdl_event)
+ yield _parse_event(sdl_event)
-def wait(timeout: float | None = None) -> Iterator[Any]:
+def wait(timeout: float | None = None) -> Iterator[Event]:
"""Block until events exist, then return an event iterator.
`timeout` is the maximum number of seconds to wait as a floating point
@@ -1244,7 +1425,8 @@ def wait(timeout: float | None = None) -> Iterator[Any]:
@deprecated(
- "Event dispatch should be handled via a single custom method in a Protocol instead of this class.",
+ """EventDispatch is no longer maintained.
+Event dispatching should be handled via a single custom method in a Protocol instead of this class.""",
category=DeprecationWarning,
)
class EventDispatch(Generic[T]):
@@ -1258,8 +1440,8 @@ class EventDispatch(Generic[T]):
The type hints at the return value of :any:`dispatch` and the `ev_*` methods.
.. deprecated:: 18.0
- Event dispatch should be handled via a single custom method in a Protocol instead of this class.
- Note that events can and should be handled using Python's `match` statement.
+ Event dispatch should be handled via a single custom method in a :class:`~typing.Protocol` instead of this class.
+ Note that events can and should be handled using :ref:`match`.
Example::
@@ -1357,7 +1539,7 @@ def cmd_quit(self) -> None:
__slots__ = ()
- def dispatch(self, event: Any) -> T | None:
+ def dispatch(self, event: Any) -> T | None: # noqa: ANN401
"""Send an event to an `ev_*` method.
`*` will be the `event.type` attribute converted to lower-case.
@@ -1383,11 +1565,11 @@ def dispatch(self, event: Any) -> T | None:
return None
return func(event)
- def event_get(self) -> None:
+ def event_get(self) -> None: # noqa: D102
for event in get():
self.dispatch(event)
- def event_wait(self, timeout: float | None) -> None:
+ def event_wait(self, timeout: float | None) -> None: # noqa: D102
wait(timeout)
self.event_get()
@@ -1433,9 +1615,6 @@ def ev_windowmoved(self, event: tcod.event.WindowMoved, /) -> T | None:
def ev_windowresized(self, event: tcod.event.WindowResized, /) -> T | None:
"""Called when the window is resized."""
- 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, /) -> T | None:
"""Called when the window is minimized."""
@@ -1460,10 +1639,10 @@ def ev_windowfocuslost(self, event: tcod.event.WindowEvent, /) -> T | None:
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, /) -> T | None:
+ def ev_windowtakefocus(self, event: tcod.event.WindowEvent, /) -> T | None: # noqa: D102
pass
- def ev_windowhittest(self, event: tcod.event.WindowEvent, /) -> T | None:
+ def ev_windowhittest(self, event: tcod.event.WindowEvent, /) -> T | None: # noqa: D102
pass
def ev_joyaxismotion(self, event: tcod.event.JoystickAxis, /) -> T | None:
@@ -1544,7 +1723,7 @@ def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice, /) ->
.. versionadded:: 13.8
"""
- def ev_(self, event: Any, /) -> T | None:
+ def ev_(self, event: Any, /) -> T | None: # noqa: ANN401, D102
pass
@@ -1553,16 +1732,88 @@ def get_mouse_state() -> MouseState:
.. versionadded:: 9.3
"""
- xy = ffi.new("int[2]")
+ xy = ffi.new("float[2]")
buttons = lib.SDL_GetMouseState(xy, xy + 1)
- tile = _pixel_to_tile(*xy)
+ tile = _pixel_to_tile(tuple(xy))
if tile is None:
- return MouseState((xy[0], xy[1]), None, buttons)
- return MouseState((xy[0], xy[1]), (int(tile[0]), int(tile[1])), buttons)
+ return MouseState(position=Point(xy[0], xy[1]), tile=None, state=buttons)
+ return MouseState(position=Point(xy[0], xy[1]), tile=Point(floor(tile[0]), floor(tile[1])), state=buttons)
+
+
+@overload
+def convert_coordinates_from_window(
+ event: _EventType,
+ /,
+ context: tcod.context.Context | tcod.sdl.render.Renderer,
+ console: tcod.console.Console | tuple[int, int],
+ dest_rect: tuple[int, int, int, int] | None = None,
+) -> _EventType: ...
+@overload
+def convert_coordinates_from_window(
+ xy: tuple[float, float],
+ /,
+ context: tcod.context.Context | tcod.sdl.render.Renderer,
+ console: tcod.console.Console | tuple[int, int],
+ dest_rect: tuple[int, int, int, int] | None = None,
+) -> tuple[float, float]: ...
+def convert_coordinates_from_window(
+ event: _EventType | tuple[float, float],
+ /,
+ context: tcod.context.Context | tcod.sdl.render.Renderer,
+ console: tcod.console.Console | tuple[int, int],
+ dest_rect: tuple[int, int, int, int] | None = None,
+) -> _EventType | tuple[float, float]:
+ """Return an event or position with window mouse coordinates converted into console tile coordinates.
+
+ Args:
+ event: :any:`Event` to convert, or the `(x, y)` coordinates to convert.
+ context: Context or Renderer to fetch the SDL renderer from for reference with conversions.
+ console: A console used as a size reference.
+ Otherwise the `(columns, rows)` can be given directly as a tuple.
+ dest_rect: The consoles rendering destination as `(x, y, width, height)`.
+ If None is given then the whole rendering target is assumed.
+
+ .. versionadded:: 20.0
+ """
+ if isinstance(context, tcod.context.Context):
+ maybe_renderer: Final = context.sdl_renderer
+ if maybe_renderer is None:
+ return event
+ context = maybe_renderer
+
+ if isinstance(console, tcod.console.Console):
+ console = console.width, console.height
+
+ if dest_rect is None:
+ dest_rect = (0, 0, *(context.logical_size or context.output_size))
+
+ x_scale: Final = console[0] / dest_rect[2]
+ y_scale: Final = console[1] / dest_rect[3]
+ x_offset: Final = dest_rect[0]
+ y_offset: Final = dest_rect[1]
+
+ if not isinstance(event, Event):
+ x, y = context.coordinates_from_window(event)
+ return (x - x_offset) * x_scale, (y - y_offset) * y_scale
+
+ if isinstance(event, MouseMotion):
+ previous_position = convert_coordinates_from_window(
+ ((event.position[0] - event.motion[0]), (event.position[1] - event.motion[1])), context, console, dest_rect
+ )
+ position = convert_coordinates_from_window(event.position, context, console, dest_rect)
+ event.motion = Point(position[0] - previous_position[0], position[1] - previous_position[1])
+ event._tile_motion = Point(
+ floor(position[0]) - floor(previous_position[0]), floor(position[1]) - floor(previous_position[1])
+ )
+ elif isinstance(event, _MouseEventWithPosition):
+ event.position = Point(*convert_coordinates_from_window(event.position, context, console, dest_rect))
+ if isinstance(event, _MouseEventWithTile):
+ event._tile = Point(floor(event.position[0]), floor(event.position[1]))
+ return event
-@ffi.def_extern() # type: ignore[misc]
-def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int:
+@ffi.def_extern() # type: ignore[untyped-decorator]
+def _sdl_event_watcher(userdata: Any, sdl_event: _C_SDL_Event) -> int: # noqa: ANN401
callback: Callable[[Event], None] = ffi.from_handle(userdata)
callback(_parse_event(sdl_event))
return 0
@@ -2211,14 +2462,18 @@ def _missing_(cls, value: object) -> Scancode | None:
return result
def __eq__(self, other: object) -> bool:
+ """Compare with another Scancode value.
+
+ Comparison between :any:`KeySym` and :any:`Scancode` is not allowed and will raise :any:`TypeError`.
+ """
if isinstance(other, KeySym):
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:
- # __eq__ was defined, so __hash__ must be defined.
- return super().__hash__()
+ """Return the hash for this value."""
+ return super().__hash__() # __eq__ was defined, so __hash__ must be defined
def __repr__(self) -> str:
"""Return the fully qualified name of this enum."""
@@ -2228,11 +2483,19 @@ def __repr__(self) -> str:
class KeySym(enum.IntEnum):
"""Keyboard constants based on their symbol.
- These names are derived from SDL except for the numbers which are prefixed
- with ``N`` (since raw numbers can not be a Python name.)
+ These names are derived from SDL except for numbers which are prefixed with ``N`` (since raw numbers can not be a Python name).
+ Alternatively ``KeySym["9"]`` can be used to represent numbers (since Python 3.13).
.. versionadded:: 12.3
+ .. versionchanged:: 19.0
+ SDL backend was updated to 3.x, which means some enums have been renamed.
+ Single letters are now uppercase.
+
+ .. versionchanged:: 19.6
+ Number symbols can now be fetched with ``KeySym["9"]``, etc.
+ With Python 3.13 or later.
+
================== ==========
UNKNOWN 0
BACKSPACE 8
@@ -2278,32 +2541,32 @@ class KeySym(enum.IntEnum):
CARET 94
UNDERSCORE 95
BACKQUOTE 96
- a 97
- b 98
- c 99
- d 100
- e 101
- f 102
- g 103
- h 104
- i 105
- j 106
- k 107
- l 108
- m 109
- n 110
- o 111
- p 112
- q 113
- r 114
- s 115
- t 116
- u 117
- v 118
- w 119
- x 120
- y 121
- z 122
+ A 97
+ B 98
+ C 99
+ D 100
+ E 101
+ F 102
+ G 103
+ H 104
+ I 105
+ J 106
+ K 107
+ L 108
+ M 109
+ N 110
+ O 111
+ P 112
+ Q 113
+ R 114
+ S 115
+ T 116
+ U 117
+ V 118
+ W 119
+ X 120
+ Y 121
+ Z 122
DELETE 127
SCANCODE_MASK 1073741824
CAPSLOCK 1073741881
@@ -2746,6 +3009,7 @@ def label(self) -> str:
Example::
+ >>> import tcod.event
>>> tcod.event.KeySym.F1.label
'F1'
>>> tcod.event.KeySym.BACKSPACE.label
@@ -2782,20 +3046,66 @@ def _missing_(cls, value: object) -> KeySym | None:
return result
def __eq__(self, other: object) -> bool:
+ """Compare with another KeySym value.
+
+ Comparison between :any:`KeySym` and :any:`Scancode` is not allowed and will raise :any:`TypeError`.
+ """
if isinstance(other, Scancode):
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:
- # __eq__ was defined, so __hash__ must be defined.
- return super().__hash__()
+ """Return the hash for this value."""
+ return super().__hash__() # __eq__ was defined, so __hash__ must be defined
def __repr__(self) -> str:
"""Return the fully qualified name of this enum."""
return f"tcod.event.{self.__class__.__name__}.{self.name}"
+if sys.version_info >= (3, 13):
+ # Alias for lower case letters removed from SDL3
+ KeySym.A._add_alias_("a")
+ KeySym.B._add_alias_("b")
+ KeySym.C._add_alias_("c")
+ KeySym.D._add_alias_("d")
+ KeySym.E._add_alias_("e")
+ KeySym.F._add_alias_("f")
+ KeySym.G._add_alias_("g")
+ KeySym.H._add_alias_("h")
+ KeySym.I._add_alias_("i")
+ KeySym.J._add_alias_("j")
+ KeySym.K._add_alias_("k")
+ KeySym.L._add_alias_("l")
+ KeySym.M._add_alias_("m")
+ KeySym.N._add_alias_("n")
+ KeySym.O._add_alias_("o")
+ KeySym.P._add_alias_("p")
+ KeySym.Q._add_alias_("q")
+ KeySym.R._add_alias_("r")
+ KeySym.S._add_alias_("s")
+ KeySym.T._add_alias_("t")
+ KeySym.U._add_alias_("u")
+ KeySym.V._add_alias_("v")
+ KeySym.W._add_alias_("w")
+ KeySym.X._add_alias_("x")
+ KeySym.Y._add_alias_("y")
+ KeySym.Z._add_alias_("z")
+
+ # Alias for numbers, since Python enum names can not be number literals
+ KeySym.N0._add_alias_("0")
+ KeySym.N1._add_alias_("1")
+ KeySym.N2._add_alias_("2")
+ KeySym.N3._add_alias_("3")
+ KeySym.N4._add_alias_("4")
+ KeySym.N5._add_alias_("5")
+ KeySym.N6._add_alias_("6")
+ KeySym.N7._add_alias_("7")
+ KeySym.N8._add_alias_("8")
+ KeySym.N9._add_alias_("9")
+
+
def __getattr__(name: str) -> int:
"""Migrate deprecated access of event constants."""
if name.startswith("BUTTON_"):
@@ -2819,6 +3129,9 @@ def __getattr__(name: str) -> int:
)
return replacement
+ if name.startswith("K_") and len(name) == 3: # noqa: PLR2004
+ name = name.upper() # Silently fix single letter key symbols removed from SDL3, these are still deprecated
+
value: int | None = getattr(tcod.event_constants, name, None)
if not value:
msg = f"module {__name__!r} has no attribute {name!r}"
@@ -2854,24 +3167,33 @@ def __getattr__(name: str) -> int:
return value
-__all__ = [ # noqa: F405 RUF022
- "Modifier",
+def time_ns() -> int:
+ """Return the nanoseconds elapsed since SDL was initialized.
+
+ .. versionadded:: 21.0
+ """
+ return int(lib.SDL_GetTicksNS())
+
+
+def time() -> float:
+ """Return the seconds elapsed since SDL was initialized.
+
+ .. versionadded:: 21.0
+ """
+ return time_ns() / 1_000_000_000
+
+
+__all__ = ( # noqa: F405 RUF022
"Point",
- "BUTTON_LEFT",
- "BUTTON_MIDDLE",
- "BUTTON_RIGHT",
- "BUTTON_X1",
- "BUTTON_X2",
- "BUTTON_LMASK",
- "BUTTON_MMASK",
- "BUTTON_RMASK",
- "BUTTON_X1MASK",
- "BUTTON_X2MASK",
+ "Modifier",
+ "MouseButton",
+ "MouseButtonMask",
"Event",
"Quit",
"KeyboardEvent",
"KeyDown",
"KeyUp",
+ "MouseState",
"MouseMotion",
"MouseButtonEvent",
"MouseButtonDown",
@@ -2902,8 +3224,9 @@ def __getattr__(name: str) -> int:
"get_modifier_state",
"Scancode",
"KeySym",
+ "time_ns",
+ "time",
# --- From event_constants.py ---
"MOUSEWHEEL_NORMAL",
"MOUSEWHEEL_FLIPPED",
- "MOUSEWHEEL",
-]
+)
diff --git a/tcod/image.py b/tcod/image.py
index 6b4a1083..0f154450 100644
--- a/tcod/image.py
+++ b/tcod/image.py
@@ -71,7 +71,7 @@ def from_array(cls, array: ArrayLike) -> Image:
.. versionadded:: 11.4
"""
array = np.asarray(array, dtype=np.uint8)
- height, width, depth = array.shape
+ height, width, _depth = array.shape
image = cls(width, height)
image_array: NDArray[np.uint8] = np.asarray(image)
image_array[...] = array
diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py
index 2bff5397..bae62a1f 100644
--- a/tcod/libtcodpy.py
+++ b/tcod/libtcodpy.py
@@ -6,9 +6,8 @@
import sys
import threading
import warnings
-from collections.abc import Callable, Hashable, Iterable, Iterator, Sequence
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, Final, Literal
import numpy as np
from typing_extensions import deprecated
@@ -36,7 +35,6 @@
_unicode,
_unpack_char_p,
deprecate,
- pending_deprecate,
)
from tcod.cffi import ffi, lib
from tcod.color import Color
@@ -54,6 +52,7 @@
)
if TYPE_CHECKING:
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Sequence
from os import PathLike
from numpy.typing import NDArray
@@ -69,18 +68,23 @@
NOISE_DEFAULT_LACUNARITY = 2.0
-def FOV_PERMISSIVE(p: int) -> int:
+def FOV_PERMISSIVE(p: int) -> int: # noqa: N802
return FOV_PERMISSIVE_0 + p
-def BKGND_ALPHA(a: int) -> int:
+def BKGND_ALPHA(a: int) -> int: # noqa: N802
return BKGND_ALPH | (int(a * 255) << 8)
-def BKGND_ADDALPHA(a: int) -> int:
+def BKGND_ADDALPHA(a: int) -> int: # noqa: N802
return BKGND_ADDA | (int(a * 255) << 8)
+_PENDING_DEPRECATE_MSG: Final = (
+ "This function may be deprecated in the future. Consider raising an issue on GitHub if you need this feature."
+)
+
+
@deprecated("Console array attributes perform better than this class.")
class ConsoleBuffer:
"""Simple console that allows direct (fast) access to cells. Simplifies use of the "fill" functions.
@@ -364,14 +368,14 @@ def __init__(
vk: int = 0,
c: int = 0,
text: str = "",
- pressed: bool = False,
- lalt: bool = False,
- lctrl: bool = False,
- lmeta: bool = False,
- ralt: bool = False,
- rctrl: bool = False,
- rmeta: bool = False,
- shift: bool = False,
+ pressed: bool = False, # noqa: FBT001, FBT002
+ lalt: bool = False, # noqa: FBT001, FBT002
+ lctrl: bool = False, # noqa: FBT001, FBT002
+ lmeta: bool = False, # noqa: FBT001, FBT002
+ ralt: bool = False, # noqa: FBT001, FBT002
+ rctrl: bool = False, # noqa: FBT001, FBT002
+ rmeta: bool = False, # noqa: FBT001, FBT002
+ shift: bool = False, # noqa: FBT001, FBT002
) -> None:
if isinstance(vk, ffi.CData):
self.cdata = vk
@@ -424,7 +428,7 @@ def __repr__(self) -> str:
"rmeta",
]:
if getattr(self, attr):
- params.append(f"{attr}={getattr(self, attr)!r}")
+ params.append(f"{attr}={getattr(self, attr)!r}") # noqa: PERF401
return "libtcodpy.Key({})".format(", ".join(params))
@property
@@ -502,7 +506,7 @@ def __repr__(self) -> str:
"wheel_down",
]:
if getattr(self, attr):
- params.append(f"{attr}={getattr(self, attr)!r}")
+ params.append(f"{attr}={getattr(self, attr)!r}") # noqa: PERF401
return "libtcodpy.Mouse({})".format(", ".join(params))
@property
@@ -530,7 +534,7 @@ def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP:
@deprecate("Call node.split_once instead.", category=FutureWarning)
-def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None:
+def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None: # noqa: FBT001
"""Deprecated function.
.. deprecated:: 2.0
@@ -542,18 +546,20 @@ def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None:
@deprecate("Call node.split_recursive instead.", category=FutureWarning)
def bsp_split_recursive(
node: tcod.bsp.BSP,
- randomizer: tcod.random.Random | None,
+ randomizer: Literal[0] | tcod.random.Random | None,
nb: int,
- minHSize: int,
- minVSize: int,
- maxHRatio: float,
- maxVRatio: float,
+ minHSize: int, # noqa: N803
+ minVSize: int, # noqa: N803
+ maxHRatio: float, # noqa: N803
+ maxVRatio: float, # noqa: N803
) -> None:
"""Deprecated function.
.. deprecated:: 2.0
Use :any:`BSP.split_recursive` instead.
"""
+ if randomizer == 0:
+ randomizer = None
node.split_recursive(nb, minHSize, minVSize, maxHRatio, maxVRatio, randomizer)
@@ -633,7 +639,7 @@ def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> tcod.bsp.BSP | None:
def _bsp_traverse(
node_iter: Iterable[tcod.bsp.BSP],
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any,
+ userData: Any, # noqa: N803
) -> None:
"""Pack callback into a handle for use with the callback _pycall_bsp_callback."""
for node in node_iter:
@@ -644,7 +650,7 @@ def _bsp_traverse(
def bsp_traverse_pre_order(
node: tcod.bsp.BSP,
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
) -> None:
"""Traverse this nodes hierarchy with a callback.
@@ -658,7 +664,7 @@ def bsp_traverse_pre_order(
def bsp_traverse_in_order(
node: tcod.bsp.BSP,
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
) -> None:
"""Traverse this nodes hierarchy with a callback.
@@ -672,7 +678,7 @@ def bsp_traverse_in_order(
def bsp_traverse_post_order(
node: tcod.bsp.BSP,
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
) -> None:
"""Traverse this nodes hierarchy with a callback.
@@ -686,7 +692,7 @@ def bsp_traverse_post_order(
def bsp_traverse_level_order(
node: tcod.bsp.BSP,
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
) -> None:
"""Traverse this nodes hierarchy with a callback.
@@ -700,7 +706,7 @@ def bsp_traverse_level_order(
def bsp_traverse_inverted_level_order(
node: tcod.bsp.BSP,
callback: Callable[[tcod.bsp.BSP, Any], None],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
) -> None:
"""Traverse this nodes hierarchy with a callback.
@@ -737,7 +743,7 @@ def bsp_delete(node: tcod.bsp.BSP) -> None:
"""
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def color_lerp(c1: tuple[int, int, int], c2: tuple[int, int, int], a: float) -> Color:
"""Return the linear interpolation between two colors.
@@ -757,7 +763,7 @@ def color_lerp(c1: tuple[int, int, int], c2: tuple[int, int, int], a: float) ->
return Color._new_from_cdata(lib.TCOD_color_lerp(c1, c2, a))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def color_set_hsv(c: Color, h: float, s: float, v: float) -> None:
"""Set a color using: hue, saturation, and value parameters.
@@ -774,7 +780,7 @@ def color_set_hsv(c: Color, h: float, s: float, v: float) -> None:
c[:] = new_color.r, new_color.g, new_color.b
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def color_get_hsv(c: tuple[int, int, int]) -> tuple[float, float, float]:
"""Return the (hue, saturation, value) of a color.
@@ -791,8 +797,8 @@ def color_get_hsv(c: tuple[int, int, int]) -> tuple[float, float, float]:
return hsv[0], hsv[1], hsv[2]
-@pending_deprecate()
-def color_scale_HSV(c: Color, scoef: float, vcoef: float) -> None:
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
+def color_scale_HSV(c: Color, scoef: float, vcoef: float) -> None: # noqa: N802
"""Scale a color's saturation and value.
Does not return a new Color. ``c`` is modified in-place.
@@ -810,7 +816,7 @@ def color_scale_HSV(c: Color, scoef: float, vcoef: float) -> None:
c[:] = color_p.r, color_p.g, color_p.b
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def color_gen_map(colors: Iterable[tuple[int, int, int]], indexes: Iterable[int]) -> list[Color]:
"""Return a smoothly defined scale of colors.
@@ -848,10 +854,10 @@ def console_init_root(
w: int,
h: int,
title: str | None = None,
- fullscreen: bool = False,
+ fullscreen: bool = False, # noqa: FBT001, FBT002
renderer: int | None = None,
order: Literal["C", "F"] = "C",
- vsync: bool | None = None,
+ vsync: bool | None = None, # noqa: FBT001
) -> tcod.console.Console:
"""Set up the primary display and return the root console.
@@ -947,7 +953,7 @@ def console_init_root(
https://python-tcod.readthedocs.io/en/latest/tcod/getting-started.html"""
)
def console_set_custom_font(
- fontFile: str | PathLike[str],
+ fontFile: str | PathLike[str], # noqa: N803
flags: int = FONT_LAYOUT_ASCII_INCOL,
nb_char_horiz: int = 0,
nb_char_vertic: int = 0,
@@ -980,7 +986,7 @@ def console_set_custom_font(
.. versionchanged:: 16.0
Added PathLike support. `fontFile` no longer takes bytes.
"""
- fontFile = Path(fontFile).resolve(strict=True)
+ fontFile = Path(fontFile).resolve(strict=True) # noqa: N806
_check(lib.TCOD_console_set_custom_font(_path_encode(fontFile), flags, nb_char_horiz, nb_char_vertic))
@@ -1017,7 +1023,7 @@ def console_get_height(con: tcod.console.Console) -> int:
@deprecate("Setup fonts using the tcod.tileset module.")
-def console_map_ascii_code_to_font(asciiCode: int, fontCharX: int, fontCharY: int) -> None:
+def console_map_ascii_code_to_font(asciiCode: int, fontCharX: int, fontCharY: int) -> None: # noqa: N803
"""Set a character code to new coordinates on the tile-set.
`asciiCode` should be any Unicode codepoint.
@@ -1037,7 +1043,7 @@ def console_map_ascii_code_to_font(asciiCode: int, fontCharX: int, fontCharY: in
@deprecate("Setup fonts using the tcod.tileset module.")
-def console_map_ascii_codes_to_font(firstAsciiCode: int, nbCodes: int, fontCharX: int, fontCharY: int) -> None:
+def console_map_ascii_codes_to_font(firstAsciiCode: int, nbCodes: int, fontCharX: int, fontCharY: int) -> None: # noqa: N803
"""Remap a contiguous set of codes to a contiguous set of tiles.
Both the tile-set and character codes must be contiguous to use this
@@ -1061,7 +1067,7 @@ 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:
+def console_map_string_to_font(s: str, fontCharX: int, fontCharY: int) -> None: # noqa: N803
r"""Remap a string of codes to a contiguous set of tiles.
Args:
@@ -1093,7 +1099,7 @@ def console_is_fullscreen() -> bool:
@deprecate("This function is not supported if contexts are being used.")
-def console_set_fullscreen(fullscreen: bool) -> None:
+def console_set_fullscreen(fullscreen: bool) -> None: # noqa: FBT001
"""Change the display to be fullscreen or windowed.
Args:
@@ -1149,7 +1155,7 @@ def console_set_window_title(title: str) -> None:
lib.TCOD_console_set_window_title(_bytes(title))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def console_credits() -> None:
lib.TCOD_console_credits()
@@ -1158,7 +1164,7 @@ def console_credits_reset() -> None:
lib.TCOD_console_credits_reset()
-def console_credits_render(x: int, y: int, alpha: bool) -> bool:
+def console_credits_render(x: int, y: int, alpha: bool) -> bool: # noqa: FBT001
return bool(lib.TCOD_console_credits_render(x, y, alpha))
@@ -1277,7 +1283,7 @@ def console_clear(con: tcod.console.Console) -> None:
lib.TCOD_console_clear(_console(con))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def console_put_char(
con: tcod.console.Console,
x: int,
@@ -1297,7 +1303,7 @@ def console_put_char(
lib.TCOD_console_put_char(_console(con), x, y, _int(c), flag)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def console_put_char_ex(
con: tcod.console.Console,
x: int,
@@ -1321,7 +1327,7 @@ def console_put_char_ex(
lib.TCOD_console_put_char_ex(_console(con), x, y, _int(c), fore, back)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def console_set_char_background(
con: tcod.console.Console,
x: int,
@@ -1537,7 +1543,7 @@ def console_rect(
y: int,
w: int,
h: int,
- clr: bool,
+ clr: bool, # noqa: FBT001
flag: int = BKGND_DEFAULT,
) -> None:
"""Draw a the background color on a rect optionally clearing the text.
@@ -1593,7 +1599,7 @@ def console_print_frame(
y: int,
w: int,
h: int,
- clear: bool = True,
+ clear: bool = True, # noqa: FBT001, FBT002
flag: int = BKGND_DEFAULT,
fmt: str = "",
) -> None:
@@ -1615,7 +1621,7 @@ def console_print_frame(
_check(lib.TCOD_console_printf_frame(_console(con), x, y, w, h, clear, flag, fmt_))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def console_set_color_control(con: int, fore: tuple[int, int, int], back: tuple[int, int, int]) -> None:
"""Configure :term:`color controls`.
@@ -1683,7 +1689,7 @@ def console_get_char(con: tcod.console.Console, x: int, y: int) -> int:
@deprecate("This function is not supported if contexts are being used.", category=FutureWarning)
-def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None:
+def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None: # noqa: N803
"""Deprecated function.
.. deprecated:: 11.13
@@ -1714,7 +1720,7 @@ def console_get_fading_color() -> Color:
# handling keyboard input
@deprecate("Use the tcod.event.wait function to wait for events.")
-def console_wait_for_keypress(flush: bool) -> Key:
+def console_wait_for_keypress(flush: bool) -> Key: # noqa: FBT001
"""Block until the user presses a key, then returns a new Key.
Args:
@@ -2099,7 +2105,7 @@ def console_list_save_xp(
lib.TCOD_list_delete(tcod_list)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_new_using_map(m: tcod.map.Map, dcost: float = 1.41) -> tcod.path.AStar:
"""Return a new AStar using the given Map.
@@ -2114,12 +2120,12 @@ def path_new_using_map(m: tcod.map.Map, dcost: float = 1.41) -> tcod.path.AStar:
return tcod.path.AStar(m, dcost)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_new_using_function(
w: int,
h: int,
func: Callable[[int, int, int, int, Any], float],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
dcost: float = 1.41,
) -> tcod.path.AStar:
"""Return a new AStar using the given callable function.
@@ -2138,7 +2144,7 @@ def path_new_using_function(
return tcod.path.AStar(tcod.path._EdgeCostFunc((func, userData), (w, h)), dcost)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool:
"""Find a path from (ox, oy) to (dx, dy). Return True if path is found.
@@ -2155,7 +2161,7 @@ def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool
return bool(lib.TCOD_path_compute(p._path_c, ox, oy, dx, dy))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_get_origin(p: tcod.path.AStar) -> tuple[int, int]:
"""Get the current origin position.
@@ -2173,7 +2179,7 @@ def path_get_origin(p: tcod.path.AStar) -> tuple[int, int]:
return x[0], y[0]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_get_destination(p: tcod.path.AStar) -> tuple[int, int]:
"""Get the current destination position.
@@ -2189,7 +2195,7 @@ def path_get_destination(p: tcod.path.AStar) -> tuple[int, int]:
return x[0], y[0]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_size(p: tcod.path.AStar) -> int:
"""Return the current length of the computed path.
@@ -2202,7 +2208,7 @@ def path_size(p: tcod.path.AStar) -> int:
return int(lib.TCOD_path_size(p._path_c))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_reverse(p: tcod.path.AStar) -> None:
"""Reverse the direction of a path.
@@ -2214,7 +2220,7 @@ def path_reverse(p: tcod.path.AStar) -> None:
lib.TCOD_path_reverse(p._path_c)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_get(p: tcod.path.AStar, idx: int) -> tuple[int, int]:
"""Get a point on a path.
@@ -2228,7 +2234,7 @@ def path_get(p: tcod.path.AStar, idx: int) -> tuple[int, int]:
return x[0], y[0]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def path_is_empty(p: tcod.path.AStar) -> bool:
"""Return True if a path is empty.
@@ -2241,8 +2247,8 @@ def path_is_empty(p: tcod.path.AStar) -> bool:
return bool(lib.TCOD_path_is_empty(p._path_c))
-@pending_deprecate()
-def path_walk(p: tcod.path.AStar, recompute: bool) -> tuple[int, int] | tuple[None, None]:
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
+def path_walk(p: tcod.path.AStar, recompute: bool) -> tuple[int, int] | tuple[None, None]: # noqa: FBT001
"""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
@@ -2271,48 +2277,48 @@ def path_delete(p: tcod.path.AStar) -> None:
"""
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_new(m: tcod.map.Map, dcost: float = 1.41) -> tcod.path.Dijkstra:
return tcod.path.Dijkstra(m, dcost)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_new_using_function(
w: int,
h: int,
func: Callable[[int, int, int, int, Any], float],
- userData: Any = 0,
+ userData: Any = 0, # noqa: N803
dcost: float = 1.41,
) -> tcod.path.Dijkstra:
return tcod.path.Dijkstra(tcod.path._EdgeCostFunc((func, userData), (w, h)), dcost)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_compute(p: tcod.path.Dijkstra, ox: int, oy: int) -> None:
lib.TCOD_dijkstra_compute(p._path_c, ox, oy)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_path_set(p: tcod.path.Dijkstra, x: int, y: int) -> bool:
return bool(lib.TCOD_dijkstra_path_set(p._path_c, x, y))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_get_distance(p: tcod.path.Dijkstra, x: int, y: int) -> int:
return int(lib.TCOD_dijkstra_get_distance(p._path_c, x, y))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_size(p: tcod.path.Dijkstra) -> int:
return int(lib.TCOD_dijkstra_size(p._path_c))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_reverse(p: tcod.path.Dijkstra) -> None:
lib.TCOD_dijkstra_reverse(p._path_c)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> tuple[int, int]:
x = ffi.new("int *")
y = ffi.new("int *")
@@ -2320,12 +2326,12 @@ def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> tuple[int, int]:
return x[0], y[0]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_is_empty(p: tcod.path.Dijkstra) -> bool:
return bool(lib.TCOD_dijkstra_is_empty(p._path_c))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def dijkstra_path_walk(
p: tcod.path.Dijkstra,
) -> tuple[int, int] | tuple[None, None]:
@@ -2344,7 +2350,7 @@ def dijkstra_delete(p: tcod.path.Dijkstra) -> None:
"""
-def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData:
+def _heightmap_cdata(array: NDArray[np.float32]) -> Any:
"""Return a new TCOD_heightmap_t instance using an array.
Formatting is verified during this function.
@@ -2362,7 +2368,7 @@ def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData:
return ffi.new("TCOD_heightmap_t *", (width, height, pointer))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_new(w: int, h: int, order: str = "C") -> NDArray[np.float32]:
"""Return a new numpy.ndarray formatted for use with heightmap functions.
@@ -2486,7 +2492,7 @@ def heightmap_copy(hm1: NDArray[np.float32], hm2: NDArray[np.float32]) -> None:
hm2[:] = hm1[:]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_normalize(hm: NDArray[np.float32], mi: float = 0.0, ma: float = 1.0) -> None:
"""Normalize heightmap values between ``mi`` and ``ma``.
@@ -2498,7 +2504,7 @@ def heightmap_normalize(hm: NDArray[np.float32], mi: float = 0.0, ma: float = 1.
lib.TCOD_heightmap_normalize(_heightmap_cdata(hm), mi, ma)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_lerp_hm(
hm1: NDArray[np.float32],
hm2: NDArray[np.float32],
@@ -2562,7 +2568,7 @@ def heightmap_multiply_hm(
hm3[:] = hm1[:] * hm2[:]
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_add_hill(hm: NDArray[np.float32], x: float, y: float, radius: float, height: float) -> None:
"""Add a hill (a half spheroid) at given position.
@@ -2578,7 +2584,7 @@ def heightmap_add_hill(hm: NDArray[np.float32], x: float, y: float, radius: floa
lib.TCOD_heightmap_add_hill(_heightmap_cdata(hm), x, y, radius, height)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_dig_hill(hm: NDArray[np.float32], x: float, y: float, radius: float, height: float) -> None:
"""Dig a hill in a heightmap.
@@ -2596,12 +2602,12 @@ def heightmap_dig_hill(hm: NDArray[np.float32], x: float, y: float, radius: floa
lib.TCOD_heightmap_dig_hill(_heightmap_cdata(hm), x, y, radius, height)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_rain_erosion(
hm: NDArray[np.float32],
- nbDrops: int,
- erosionCoef: float,
- sedimentationCoef: float,
+ nbDrops: int, # noqa: N803
+ erosionCoef: float, # noqa: N803
+ sedimentationCoef: float, # noqa: N803
rnd: tcod.random.Random | None = None,
) -> None:
"""Simulate the effect of rain drops on the terrain, resulting in erosion.
@@ -2625,15 +2631,15 @@ def heightmap_rain_erosion(
)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_kernel_transform(
hm: NDArray[np.float32],
kernelsize: int,
dx: Sequence[int],
dy: Sequence[int],
weight: Sequence[float],
- minLevel: float,
- maxLevel: float,
+ minLevel: float, # noqa: N803
+ maxLevel: float, # noqa: N803
) -> None:
"""Apply a generic transformation on the map, so that each resulting cell value is the weighted sum of several neighbor cells.
@@ -2681,11 +2687,11 @@ def heightmap_kernel_transform(
lib.TCOD_heightmap_kernel_transform(_heightmap_cdata(hm), kernelsize, c_dx, c_dy, c_weight, minLevel, maxLevel)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_add_voronoi(
hm: NDArray[np.float32],
- nbPoints: Any,
- nbCoef: int,
+ nbPoints: Any, # noqa: N803
+ nbCoef: int, # noqa: N803
coef: Sequence[float],
rnd: tcod.random.Random | None = None,
) -> None:
@@ -2702,7 +2708,7 @@ def heightmap_add_voronoi(
second closest site : coef[1], ...
rnd (Optional[Random]): A Random instance, or None.
"""
- nbPoints = len(coef)
+ nbPoints = len(coef) # noqa: N806
ccoef = ffi.new("float[]", coef)
lib.TCOD_heightmap_add_voronoi(
_heightmap_cdata(hm),
@@ -2804,15 +2810,15 @@ def heightmap_scale_fbm(
)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_dig_bezier(
hm: NDArray[np.float32],
px: tuple[int, int, int, int],
py: tuple[int, int, int, int],
- startRadius: float,
- startDepth: float,
- endRadius: float,
- endDepth: float,
+ startRadius: float, # noqa: N803
+ startDepth: float, # noqa: N803
+ endRadius: float, # noqa: N803
+ endDepth: float, # noqa: N803
) -> None:
"""Carve a path along a cubic Bezier curve.
@@ -2851,19 +2857,19 @@ def heightmap_get_value(hm: NDArray[np.float32], x: int, y: int) -> float:
DeprecationWarning,
stacklevel=2,
)
- return hm[y, x] # type: ignore
+ return hm.item(y, x)
if hm.flags["F_CONTIGUOUS"]:
warnings.warn(
"Get a value from this heightmap with hm[x,y]",
DeprecationWarning,
stacklevel=2,
)
- return hm[x, y] # type: ignore
+ return hm.item(x, y)
msg = "This array is not contiguous."
raise ValueError(msg)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_get_interpolated_value(hm: NDArray[np.float32], x: float, y: float) -> float:
"""Return the interpolated height at non integer coordinates.
@@ -2878,7 +2884,7 @@ def heightmap_get_interpolated_value(hm: NDArray[np.float32], x: float, y: float
return float(lib.TCOD_heightmap_get_interpolated_value(_heightmap_cdata(hm), x, y))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_get_slope(hm: NDArray[np.float32], x: int, y: int) -> float:
"""Return the slope between 0 and (pi / 2) at given coordinates.
@@ -2893,8 +2899,8 @@ def heightmap_get_slope(hm: NDArray[np.float32], x: int, y: int) -> float:
return float(lib.TCOD_heightmap_get_slope(_heightmap_cdata(hm), x, y))
-@pending_deprecate()
-def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> tuple[float, float, float]:
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
+def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> tuple[float, float, float]: # noqa: N803
"""Return the map normal at given coordinates.
Args:
@@ -2930,7 +2936,7 @@ def heightmap_count_cells(hm: NDArray[np.float32], mi: float, ma: float) -> int:
return int(lib.TCOD_heightmap_count_cells(_heightmap_cdata(hm), mi, ma))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def heightmap_has_land_on_border(hm: NDArray[np.float32], waterlevel: float) -> bool:
"""Returns True if the map edges are below ``waterlevel``, otherwise False.
@@ -3250,7 +3256,7 @@ def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]:
@deprecate("This function has been replaced by tcod.los.bresenham.", category=FutureWarning)
-def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]:
+def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]: # noqa: FBT001, FBT002
"""Return a NumPy index array following a Bresenham line.
If `inclusive` is true then the start point is included in the result.
@@ -3287,12 +3293,12 @@ def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None:
array attributes manually.
"""
if source.width != dest.width or source.height != dest.height:
- dest.__init__(source.width, source.height, source._order) # type: ignore
- dest._Map__buffer[:] = source._Map__buffer[:] # type: ignore
+ tcod.map.Map.__init__(dest, source.width, source.height, source._order)
+ dest._buffer[:] = source._buffer[:]
@deprecate("Set properties using the m.transparent and m.walkable arrays.", category=FutureWarning)
-def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: bool) -> None:
+def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: bool) -> None: # noqa: FBT001, N803
"""Set the properties of a single cell.
.. note::
@@ -3305,7 +3311,7 @@ def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: b
@deprecate("Clear maps using NumPy broadcast rules instead.", category=FutureWarning)
-def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False) -> None:
+def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False) -> None: # noqa: FBT001, FBT002
"""Change all map cells to a specific value.
.. deprecated:: 4.5
@@ -3322,7 +3328,7 @@ def map_compute_fov(
x: int,
y: int,
radius: int = 0,
- light_walls: bool = True,
+ light_walls: bool = True, # noqa: FBT001, FBT002
algo: int = FOV_RESTRICTIVE,
) -> None:
"""Compute the field-of-view for a map instance.
@@ -3378,7 +3384,7 @@ def map_delete(m: tcod.map.Map) -> None:
@deprecate("Check the map.width attribute instead.", category=FutureWarning)
-def map_get_width(map: tcod.map.Map) -> int:
+def map_get_width(map: tcod.map.Map) -> int: # noqa: A002
"""Return the width of a map.
.. deprecated:: 4.5
@@ -3388,7 +3394,7 @@ def map_get_width(map: tcod.map.Map) -> int:
@deprecate("Check the map.height attribute instead.", category=FutureWarning)
-def map_get_height(map: tcod.map.Map) -> int:
+def map_get_height(map: tcod.map.Map) -> int: # noqa: A002
"""Return the height of a map.
.. deprecated:: 4.5
@@ -3398,7 +3404,7 @@ def map_get_height(map: tcod.map.Map) -> int:
@deprecate("Use `tcod.sdl.mouse.show(visible)` instead.", category=FutureWarning)
-def mouse_show_cursor(visible: bool) -> None:
+def mouse_show_cursor(visible: bool) -> None: # noqa: FBT001
"""Change the visibility of the mouse cursor.
.. deprecated:: 16.0
@@ -3427,22 +3433,22 @@ def mouse_get_status() -> Mouse:
return Mouse(lib.TCOD_mouse_get_status())
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None:
lib.TCOD_namegen_parse(_path_encode(Path(filename).resolve(strict=True)), random or ffi.NULL)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def namegen_generate(name: str) -> str:
- return _unpack_char_p(lib.TCOD_namegen_generate(_bytes(name), False))
+ return _unpack_char_p(lib.TCOD_namegen_generate(_bytes(name), False)) # noqa: FBT003
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def namegen_generate_custom(name: str, rule: str) -> str:
- return _unpack_char_p(lib.TCOD_namegen_generate_custom(_bytes(name), _bytes(rule), False))
+ return _unpack_char_p(lib.TCOD_namegen_generate_custom(_bytes(name), _bytes(rule), False)) # noqa: FBT003
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def namegen_get_sets() -> list[str]:
sets = lib.TCOD_namegen_get_sets()
try:
@@ -3454,7 +3460,7 @@ def namegen_get_sets() -> list[str]:
return lst
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def namegen_destroy() -> None:
lib.TCOD_namegen_destroy()
@@ -3584,8 +3590,11 @@ def _unpack_union(type_: int, union: Any) -> Any: # noqa: PLR0911
raise RuntimeError(msg)
-def _convert_TCODList(c_list: Any, type_: int) -> Any:
- return [_unpack_union(type_, lib.TDL_list_get_union(c_list, i)) for i in range(lib.TCOD_list_size(c_list))]
+def _convert_TCODList(c_list: Any, type_: int) -> Any: # noqa: N802
+ with ffi.new("TCOD_value_t[]", lib.TCOD_list_size(c_list)) as unions:
+ for i, union in enumerate(unions):
+ union.custom = lib.TCOD_list_get(c_list, i)
+ return [_unpack_union(type_, union) for union in unions]
@deprecate("Parser functions have been deprecated.")
@@ -3604,27 +3613,27 @@ def parser_new_struct(parser: Any, name: str) -> Any:
_parser_listener: Any = None
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_parser_new_struct(struct: Any, name: str) -> Any:
return _parser_listener.new_struct(struct, _unpack_char_p(name))
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_parser_new_flag(name: str) -> Any:
return _parser_listener.new_flag(_unpack_char_p(name))
-@ffi.def_extern() # type: ignore[misc]
-def _pycall_parser_new_property(propname: Any, type: Any, value: Any) -> Any:
+@ffi.def_extern() # type: ignore[untyped-decorator]
+def _pycall_parser_new_property(propname: Any, type: Any, value: Any) -> Any: # noqa: A002
return _parser_listener.new_property(_unpack_char_p(propname), type, _unpack_union(type, value))
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_parser_end_struct(struct: Any, name: Any) -> Any:
return _parser_listener.end_struct(struct, _unpack_char_p(name))
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_parser_error(msg: Any) -> None:
_parser_listener.error(_unpack_char_p(msg))
@@ -3703,7 +3712,7 @@ def parser_get_dice_property(parser: Any, name: str) -> Dice:
@deprecate("Parser functions have been deprecated.")
-def parser_get_list_property(parser: Any, name: str, type: Any) -> Any:
+def parser_get_list_property(parser: Any, name: str, type: Any) -> Any: # noqa: A002
c_list = lib.TCOD_parser_get_list_property(parser, _bytes(name), type)
return _convert_TCODList(c_list, type)
@@ -3718,7 +3727,7 @@ def parser_get_list_property(parser: Any, name: str, type: Any) -> Any:
DISTRIBUTION_GAUSSIAN_RANGE_INVERSE = 4
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_get_instance() -> tcod.random.Random:
"""Return the default Random instance.
@@ -3728,7 +3737,7 @@ def random_get_instance() -> tcod.random.Random:
return tcod.random.Random._new_from_cdata(lib.TCOD_random_get_instance())
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_new(algo: int = RNG_CMWC) -> tcod.random.Random:
"""Return a new Random instance. Using ``algo``.
@@ -3741,7 +3750,7 @@ def random_new(algo: int = RNG_CMWC) -> tcod.random.Random:
return tcod.random.Random(algo)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_new_from_seed(seed: Hashable, algo: int = RNG_CMWC) -> tcod.random.Random:
"""Return a new Random instance. Using the given ``seed`` and ``algo``.
@@ -3756,7 +3765,7 @@ def random_new_from_seed(seed: Hashable, algo: int = RNG_CMWC) -> tcod.random.Ra
return tcod.random.Random(algo, seed)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_set_distribution(rnd: tcod.random.Random | None, dist: int) -> None:
"""Change the distribution mode of a random number generator.
@@ -3767,7 +3776,7 @@ def random_set_distribution(rnd: tcod.random.Random | None, dist: int) -> None:
lib.TCOD_random_set_distribution(rnd.random_c if rnd else ffi.NULL, dist)
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_get_int(rnd: tcod.random.Random | None, mi: int, ma: int) -> int:
"""Return a random integer in the range: ``mi`` <= n <= ``ma``.
@@ -3784,7 +3793,7 @@ def random_get_int(rnd: tcod.random.Random | None, mi: int, ma: int) -> int:
return int(lib.TCOD_random_get_int(rnd.random_c if rnd else ffi.NULL, mi, ma))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
def random_get_float(rnd: tcod.random.Random | None, mi: float, ma: float) -> float:
"""Return a random float in the range: ``mi`` <= n <= ``ma``.
@@ -3813,7 +3822,7 @@ def random_get_double(rnd: tcod.random.Random | None, mi: float, ma: float) -> f
return float(lib.TCOD_random_get_double(rnd.random_c if rnd else ffi.NULL, mi, ma))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
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``.
@@ -3831,7 +3840,7 @@ def random_get_int_mean(rnd: tcod.random.Random | None, mi: int, ma: int, mean:
return int(lib.TCOD_random_get_int_mean(rnd.random_c if rnd else ffi.NULL, mi, ma, mean))
-@pending_deprecate()
+@deprecate(_PENDING_DEPRECATE_MSG, category=PendingDeprecationWarning)
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``.
@@ -3906,19 +3915,19 @@ def struct_add_flag(struct: Any, name: str) -> None:
@deprecate("This function is deprecated.")
-def struct_add_property(struct: Any, name: str, typ: int, mandatory: bool) -> None:
+def struct_add_property(struct: Any, name: str, typ: int, mandatory: bool) -> None: # noqa: FBT001
lib.TCOD_struct_add_property(struct, _bytes(name), typ, mandatory)
@deprecate("This function is deprecated.")
-def struct_add_value_list(struct: Any, name: str, value_list: Iterable[str], mandatory: bool) -> None:
+def struct_add_value_list(struct: Any, name: str, value_list: Iterable[str], mandatory: bool) -> None: # noqa: FBT001
c_strings = [ffi.new("char[]", value.encode("utf-8")) for value in value_list]
c_value_list = ffi.new("char*[]", c_strings)
lib.TCOD_struct_add_value_list(struct, name, c_value_list, mandatory)
@deprecate("This function is deprecated.")
-def struct_add_list_property(struct: Any, name: str, typ: int, mandatory: bool) -> None:
+def struct_add_list_property(struct: Any, name: str, typ: int, mandatory: bool) -> None: # noqa: FBT001
lib.TCOD_struct_add_list_property(struct, _bytes(name), typ, mandatory)
@@ -4131,7 +4140,7 @@ def sys_get_char_size() -> tuple[int, int]:
# update font bitmap
@deprecate("This function is not supported if contexts are being used.")
def sys_update_char(
- asciiCode: int,
+ asciiCode: int, # noqa: N803
fontx: int,
fonty: int,
img: tcod.image.Image,
@@ -4161,7 +4170,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:
+def sys_register_SDL_renderer(callback: Callable[[Any], None]) -> None: # noqa: N802
"""Register a custom rendering function with libtcod.
Note:
@@ -4180,7 +4189,7 @@ def sys_register_SDL_renderer(callback: Callable[[Any], None]) -> None:
"""
with _PropagateException() as propagate:
- @ffi.def_extern(onerror=propagate) # type: ignore
+ @ffi.def_extern(onerror=propagate) # type: ignore[untyped-decorator]
def _pycall_sdl_hook(sdl_surface: Any) -> None:
callback(sdl_surface)
@@ -4205,7 +4214,7 @@ def sys_check_for_event(mask: int, k: Key | None, m: Mouse | None) -> int:
@deprecate("Use tcod.event.wait to wait for events.")
-def sys_wait_for_event(mask: int, k: Key | None, m: Mouse | None, flush: bool) -> int:
+def sys_wait_for_event(mask: int, k: Key | None, m: Mouse | None, flush: bool) -> int: # noqa: FBT001
"""Wait for an event then return.
If flush is True then the buffer will be cleared before waiting. Otherwise
diff --git a/tcod/map.py b/tcod/map.py
index 45b4561a..e1f4403b 100644
--- a/tcod/map.py
+++ b/tcod/map.py
@@ -3,9 +3,10 @@
from __future__ import annotations
import warnings
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, Final, Literal
import numpy as np
+from typing_extensions import deprecated
import tcod._internal
import tcod.constants
@@ -15,6 +16,7 @@
from numpy.typing import ArrayLike, NDArray
+@deprecated("This class may perform poorly and is no longer needed.")
class Map:
"""A map containing libtcod attributes.
@@ -29,13 +31,6 @@ class Map:
height (int): Height of the new Map.
order (str): Which numpy memory order to use.
- Attributes:
- width (int): Read only width of this Map.
- height (int): Read only height of this Map.
- transparent: A boolean array of transparent cells.
- walkable: A boolean array of walkable cells.
- fov: A boolean array of the cells lit by :any:'compute_fov'.
-
Example::
>>> import tcod
@@ -78,16 +73,13 @@ def __init__(
order: Literal["C", "F"] = "C",
) -> None:
"""Initialize the map."""
- warnings.warn(
- "This class may perform poorly and is no longer needed.",
- DeprecationWarning,
- stacklevel=2,
- )
- self.width = width
- self.height = height
- self._order = tcod._internal.verify_order(order)
+ self.width: Final = width
+ """Read only width of this Map."""
+ self.height: Final = height
+ """Read only height of this Map."""
+ self._order: Literal["C", "F"] = tcod._internal.verify_order(order)
- self.__buffer: NDArray[np.bool_] = 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: # noqa: ANN401
@@ -97,23 +89,26 @@ def __as_cdata(self) -> Any: # noqa: ANN401
self.width,
self.height,
self.width * self.height,
- ffi.from_buffer("struct TCOD_MapCell*", self.__buffer),
+ ffi.from_buffer("struct TCOD_MapCell*", self._buffer),
),
)
@property
def transparent(self) -> NDArray[np.bool_]:
- buffer: np.ndarray[Any, np.dtype[np.bool_]] = self.__buffer[:, :, 0]
+ """A boolean array of transparent cells."""
+ buffer: np.ndarray[Any, np.dtype[np.bool_]] = self._buffer[:, :, 0]
return buffer.T if self._order == "F" else buffer
@property
def walkable(self) -> NDArray[np.bool_]:
- buffer: np.ndarray[Any, np.dtype[np.bool_]] = self.__buffer[:, :, 1]
+ """A boolean array of walkable cells."""
+ buffer: np.ndarray[Any, np.dtype[np.bool_]] = self._buffer[:, :, 1]
return buffer.T if self._order == "F" else buffer
@property
def fov(self) -> NDArray[np.bool_]:
- buffer: np.ndarray[Any, np.dtype[np.bool_]] = self.__buffer[:, :, 2]
+ """A boolean array of the cells lit by :any:'compute_fov'."""
+ buffer: np.ndarray[Any, np.dtype[np.bool_]] = self._buffer[:, :, 2]
return buffer.T if self._order == "F" else buffer
def compute_fov(
@@ -121,7 +116,7 @@ def compute_fov(
x: int,
y: int,
radius: int = 0,
- light_walls: bool = True,
+ light_walls: bool = True, # noqa: FBT001, FBT002
algorithm: int = tcod.constants.FOV_RESTRICTIVE,
) -> None:
"""Compute a field-of-view on the current instance.
@@ -149,18 +144,21 @@ def compute_fov(
lib.TCOD_map_compute_fov(self.map_c, x, y, radius, light_walls, algorithm)
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_)
- self.__buffer[:, :, 0] = state["buffer"] & 0x01
- self.__buffer[:, :, 1] = state["buffer"] & 0x02
- self.__buffer[:, :, 2] = state["buffer"] & 0x04
+ """Unpickle this instance."""
+ if "_Map__buffer" in state: # Deprecated since 19.6
+ state["_buffer"] = state.pop("_Map__buffer")
+ if "buffer" in state: # Deprecated
+ self._buffer = np.zeros((state["height"], state["width"], 3), dtype=np.bool_)
+ self._buffer[:, :, 0] = state["buffer"] & 0x01
+ self._buffer[:, :, 1] = state["buffer"] & 0x02
+ self._buffer[:, :, 2] = state["buffer"] & 0x04
del state["buffer"]
state["_order"] = "F"
self.__dict__.update(state)
self.map_c = self.__as_cdata()
def __getstate__(self) -> dict[str, Any]:
+ """Pickle this instance."""
state = self.__dict__.copy()
del state["map_c"]
return state
@@ -170,7 +168,7 @@ def compute_fov(
transparency: ArrayLike,
pov: tuple[int, int],
radius: int = 0,
- light_walls: bool = True,
+ light_walls: bool = True, # noqa: FBT001, FBT002
algorithm: int = tcod.constants.FOV_RESTRICTIVE,
) -> NDArray[np.bool_]:
"""Return a boolean mask of the area covered by a field-of-view.
diff --git a/tcod/noise.py b/tcod/noise.py
index b4631455..161dfc26 100644
--- a/tcod/noise.py
+++ b/tcod/noise.py
@@ -12,7 +12,7 @@
... algorithm=tcod.noise.Algorithm.SIMPLEX,
... seed=42,
... )
- >>> samples = noise[tcod.noise.grid(shape=(5, 4), scale=0.25, origin=(0, 0))]
+ >>> samples = noise[tcod.noise.grid(shape=(5, 4), scale=0.25, offset=(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],
@@ -36,7 +36,6 @@
import enum
import warnings
-from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Literal
import numpy as np
@@ -46,6 +45,8 @@
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Sequence
+
from numpy.typing import ArrayLike, NDArray
@@ -65,6 +66,7 @@ class Algorithm(enum.IntEnum):
"""Wavelet noise."""
def __repr__(self) -> str:
+ """Return the string representation for this algorithm."""
return f"tcod.noise.Algorithm.{self.name}"
@@ -87,6 +89,7 @@ class Implementation(enum.IntEnum):
"""Turbulence noise implementation."""
def __repr__(self) -> str:
+ """Return the string representation for this implementation."""
return f"tcod.noise.Implementation.{self.name}"
@@ -160,16 +163,17 @@ def __rng_from_seed(seed: None | int | tcod.random.Random) -> tcod.random.Random
return seed
def __repr__(self) -> str:
+ """Return the string representation of this noise instance."""
parameters = [
f"dimensions={self.dimensions}",
f"algorithm={self.algorithm!r}",
f"implementation={Implementation(self.implementation)!r}",
]
- if self.hurst != 0.5:
+ if self.hurst != 0.5: # noqa: PLR2004 # Default value
parameters.append(f"hurst={self.hurst}")
- if self.lacunarity != 2:
+ if self.lacunarity != 2: # noqa: PLR2004 # Default value
parameters.append(f"lacunarity={self.lacunarity}")
- if self.octaves != 4:
+ if self.octaves != 4: # noqa: PLR2004 # Default value
parameters.append(f"octaves={self.octaves}")
if self._seed is not None:
parameters.append(f"seed={self._seed}")
@@ -177,10 +181,12 @@ def __repr__(self) -> str:
@property
def dimensions(self) -> int:
+ """Number of dimensions supported by this noise generator."""
return int(self._tdl_noise_c.dimensions)
@property
def algorithm(self) -> int:
+ """Current selected algorithm. Can be changed."""
noise_type = self.noise_c.noise_type
return Algorithm(noise_type) if noise_type else Algorithm.SIMPLEX
@@ -190,6 +196,7 @@ def algorithm(self, value: int) -> None:
@property
def implementation(self) -> int:
+ """Current selected implementation. Can be changed."""
return Implementation(self._tdl_noise_c.implementation)
@implementation.setter
@@ -201,14 +208,17 @@ def implementation(self, value: int) -> None:
@property
def hurst(self) -> float:
+ """Noise hurst exponent. Can be changed."""
return float(self.noise_c.H)
@property
def lacunarity(self) -> float:
+ """Noise lacunarity. Can be changed."""
return float(self.noise_c.lacunarity)
@property
def octaves(self) -> float:
+ """Level of detail on fBm and turbulence implementations. Can be changed."""
return float(self._tdl_noise_c.octaves)
@octaves.setter
@@ -342,6 +352,7 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]:
return out
def __getstate__(self) -> dict[str, Any]:
+ """Support picking this instance."""
state = self.__dict__.copy()
if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # noqa: PLR2004
# Trigger a side effect of wavelet, so that copies will be synced.
@@ -350,9 +361,9 @@ def __getstate__(self) -> dict[str, Any]:
self.get_point()
self.algorithm = saved_algo
- waveletTileData = None
+ waveletTileData = None # noqa: N806
if self.noise_c.waveletTileData != ffi.NULL:
- waveletTileData = list(self.noise_c.waveletTileData[0 : 32 * 32 * 32])
+ waveletTileData = list(self.noise_c.waveletTileData[0 : 32 * 32 * 32]) # noqa: N806
state["_waveletTileData"] = waveletTileData
state["noise_c"] = {
@@ -373,6 +384,7 @@ def __getstate__(self) -> dict[str, Any]:
return state
def __setstate__(self, state: dict[str, Any]) -> None:
+ """Unpickle this instance."""
if isinstance(state, tuple): # deprecated format
return self._setstate_old(state)
# unpack wavelet tile data if it exists
@@ -412,8 +424,10 @@ def _setstate_old(self, state: tuple[Any, ...]) -> None:
def grid(
shape: tuple[int, ...],
scale: tuple[float, ...] | float,
- origin: tuple[int, ...] | None = None,
+ origin: tuple[float, ...] | None = None,
indexing: Literal["ij", "xy"] = "xy",
+ *,
+ offset: tuple[float, ...] | None = None,
) -> tuple[NDArray[np.number], ...]:
"""Generate a mesh-grid of sample points to use with noise sampling.
@@ -427,6 +441,11 @@ def grid(
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`.
+ offset: The offset into the shape to generate.
+ Similar to `origin` but is scaled by the `scale` parameter.
+ Can be multiples of `shape` to index noise samples by chunk.
+
+ .. versionadded:: 19.2
Returns:
A sparse mesh-grid to be passed into a :class:`Noise` instance.
@@ -435,14 +454,14 @@ def grid(
>>> noise = tcod.noise.Noise(dimensions=2, seed=42)
- # Common case for ij-indexed arrays.
+ # 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)
- # Transpose an xy-indexed array to get a standard order="F" result.
+ # 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],
@@ -450,6 +469,23 @@ def grid(
[-0.7057655 , -0.5817767 , -0.22774395, 0.02399864, -0.07006818]],
dtype=float32)
+ # Can sample noise by chunk using the offset keyword
+ >>> noise[tcod.noise.grid(shape=(3, 5), scale=0.25, indexing="ij", offset=(0, 0))]
+ 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=(3, 5), scale=0.25, indexing="ij", offset=(3, 0))]
+ array([[-0.7088647 , -0.43919194, 0.12860827, 0.6390255 , 0.80402255],
+ [-0.68165785, -0.29195625, 0.2864191 , 0.5922846 , 0.52655405],
+ [-0.7841389 , -0.46131462, 0.0159424 , 0.17141782, -0.04198273]],
+ dtype=float32)
+ >>> noise[tcod.noise.grid(shape=(3, 5), scale=0.25, indexing="ij", offset=(6, 0))]
+ array([[-0.779634 , -0.60696834, -0.27446985, -0.23233278, -0.5037453 ],
+ [-0.5474089 , -0.54476213, -0.42235228, -0.49519652, -0.7101793 ],
+ [-0.28291094, -0.4326369 , -0.5227732 , -0.69655263, -0.81221616]],
+ dtype=float32)
+
.. versionadded:: 12.2
"""
if isinstance(scale, (int, float)):
@@ -462,6 +498,13 @@ def grid(
if len(shape) != len(origin):
msg = "shape must have the same length as origin"
raise TypeError(msg)
+ if offset is not None:
+ if len(shape) != len(offset):
+ msg = "shape must have the same length as offset"
+ raise TypeError(msg)
+ origin = tuple(
+ i_origin + i_scale * i_offset for i_scale, i_offset, i_origin in zip(scale, offset, origin, strict=True)
+ )
indexes = (
np.arange(i_shape) * i_scale + i_origin for i_shape, i_scale, i_origin in zip(shape, scale, origin, strict=True)
)
diff --git a/tcod/path.c b/tcod/path.c
index 61a09260..8f4f2fc6 100644
--- a/tcod/path.c
+++ b/tcod/path.c
@@ -454,7 +454,7 @@ static int update_frontier_from_distance_iterator(
int dist = get_array_int(dist_map, dimension, index);
return TCOD_frontier_push(frontier, index, dist, dist);
}
- for (int i = 0; i < dist_map->shape[dimension];) {
+ for (int i = 0; i < dist_map->shape[dimension]; ++i) {
index[dimension] = i;
int err = update_frontier_from_distance_iterator(frontier, dist_map, dimension + 1, index);
if (err) { return err; }
diff --git a/tcod/path.py b/tcod/path.py
index f16c0210..ecdb6b46 100644
--- a/tcod/path.py
+++ b/tcod/path.py
@@ -21,7 +21,6 @@
import functools
import itertools
import warnings
-from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Any, Final, Literal
import numpy as np
@@ -32,29 +31,31 @@
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Callable, Sequence
+
from numpy.typing import ArrayLike, DTypeLike, NDArray
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401
"""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[no-any-return]
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_path_simple(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401
"""Does less and should run faster, just calls the handle function."""
return ffi.from_handle(handle)(x1, y1, x2, y2) # type: ignore[no-any-return]
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_path_swap_src_dest(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401
"""A TDL function dest comes first to match up with a dest only call."""
return ffi.from_handle(handle)(x2, y2, x1, y1) # type: ignore[no-any-return]
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _pycall_path_dest_only(_x1: int, _y1: int, x2: int, y2: int, handle: Any) -> float: # noqa: ANN401
"""A TDL function which samples the dest coordinate only."""
return ffi.from_handle(handle)(x2, y2) # type: ignore[no-any-return]
@@ -111,6 +112,7 @@ def __init__(
callback: Callable[[int, int, int, int], float],
shape: tuple[int, int],
) -> None:
+ """Initialize this callback."""
self.callback = callback
super().__init__(callback, shape)
@@ -138,6 +140,7 @@ def __new__(cls, array: ArrayLike) -> Self:
return np.asarray(array).view(cls)
def __repr__(self) -> str:
+ """Return the string representation of this object."""
return f"{self.__class__.__name__}({repr(self.view(np.ndarray))!r})"
def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]:
@@ -148,7 +151,7 @@ def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]:
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]
+ _array_type, callback = self._C_ARRAY_CALLBACKS[self.dtype.type]
userdata = ffi.new(
"struct PathCostArray*",
(ffi.cast("char*", self.ctypes.data), self.strides),
@@ -473,7 +476,7 @@ def dijkstra2d( # noqa: PLR0913
Added `out` parameter. Now returns the output array.
"""
dist: NDArray[Any] = np.asarray(distance)
- if out is ...:
+ if out is ...: # type: ignore[comparison-overlap]
out = dist
warnings.warn(
"No `out` parameter was given. "
@@ -525,8 +528,8 @@ def _compile_bool_edges(edge_map: ArrayLike) -> tuple[Any, int]:
def hillclimb2d(
distance: ArrayLike,
start: tuple[int, int],
- cardinal: bool | None = None,
- diagonal: bool | None = None,
+ cardinal: bool | None = None, # noqa: FBT001
+ diagonal: bool | None = None, # noqa: FBT001
*,
edge_map: ArrayLike | None = None,
) -> NDArray[np.intc]:
@@ -998,12 +1001,12 @@ def _compile_rules(self) -> Any: # noqa: ANN401
def _resolve(self, pathfinder: Pathfinder) -> None:
"""Run the pathfinding algorithm for this graph."""
- rules, keep_alive = self._compile_rules()
+ rules, _keep_alive = self._compile_rules()
_check(
lib.path_compute(
pathfinder._frontier_p,
- pathfinder._distance_p,
- pathfinder._travel_p,
+ _export(pathfinder._distance),
+ _export(pathfinder._travel),
len(rules),
rules,
pathfinder._heuristic_p,
@@ -1067,10 +1070,12 @@ def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int
@property
def ndim(self) -> int:
+ """Number of dimensions."""
return 2
@property
def shape(self) -> tuple[int, int]:
+ """Shape of this graph."""
return self._shape
@property
@@ -1110,8 +1115,6 @@ def __init__(self, graph: CustomGraph | SimpleGraph) -> None:
self._frontier_p = ffi.gc(lib.TCOD_frontier_new(self._graph._ndim), lib.TCOD_frontier_delete)
self._distance = maxarray(self._graph._shape_c)
self._travel = _world_array(self._graph._shape_c)
- self._distance_p = _export(self._distance)
- self._travel_p = _export(self._travel)
self._heuristic: tuple[int, int, int, int, tuple[int, ...]] | None = None
self._heuristic_p: Any = ffi.NULL
@@ -1180,8 +1183,22 @@ def traversal(self) -> NDArray[Any]:
def clear(self) -> None:
"""Reset the pathfinder to its initial state.
- This sets all values on the :any:`distance` array to their maximum
- value.
+ This sets all values on the :any:`distance` array to their maximum value.
+
+ Example::
+
+ >>> import tcod.path
+ >>> graph = tcod.path.SimpleGraph(
+ ... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3,
+ ... )
+ >>> pf = tcod.path.Pathfinder(graph)
+ >>> pf.add_root((0, 0))
+ >>> pf.path_to((2, 2)).tolist()
+ [[0, 0], [1, 1], [2, 2]]
+ >>> pf.clear() # Reset Pathfinder to its initial state
+ >>> pf.add_root((0, 2))
+ >>> pf.path_to((2, 2)).tolist()
+ [[0, 2], [1, 2], [2, 2]]
"""
self._distance[...] = np.iinfo(self._distance.dtype).max
self._travel = _world_array(self._graph._shape_c)
@@ -1234,10 +1251,24 @@ def rebuild_frontier(self) -> None:
After you are finished editing :any:`distance` you must call this
function before calling :any:`resolve` or any function which calls
:any:`resolve` implicitly such as :any:`path_from` or :any:`path_to`.
+
+ Example::
+
+ >>> import tcod.path
+ >>> graph = tcod.path.SimpleGraph(
+ ... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3,
+ ... )
+ >>> pf = tcod.path.Pathfinder(graph)
+ >>> pf.distance[:, 0] = 0 # Set roots along entire left edge
+ >>> pf.rebuild_frontier()
+ >>> pf.path_to((0, 2)).tolist() # Finds best path from [:, 0]
+ [[0, 0], [0, 1], [0, 2]]
+ >>> pf.path_to((4, 2)).tolist()
+ [[4, 0], [4, 1], [4, 2]]
"""
lib.TCOD_frontier_clear(self._frontier_p)
self._update_heuristic(None)
- _check(lib.rebuild_frontier_from_distance(self._frontier_p, self._distance_p))
+ _check(lib.rebuild_frontier_from_distance(self._frontier_p, _export(self._distance)))
def resolve(self, goal: tuple[int, ...] | None = None) -> None:
"""Manually run the pathfinder algorithm.
@@ -1341,12 +1372,13 @@ def path_from(self, index: tuple[int, ...]) -> NDArray[np.intc]:
self.resolve(index)
if self._order == "F": # Convert to ij indexing order.
index = index[::-1]
- length = _check(lib.get_travel_path(self._graph._ndim, self._travel_p, index, ffi.NULL))
+ _travel_p = _export(self._travel)
+ length = _check(lib.get_travel_path(self._graph._ndim, _travel_p, index, ffi.NULL))
path: np.ndarray[Any, np.dtype[np.intc]] = np.ndarray((length, self._graph._ndim), dtype=np.intc)
_check(
lib.get_travel_path(
self._graph._ndim,
- self._travel_p,
+ _travel_p,
index,
ffi.from_buffer("int*", path),
)
diff --git a/tcod/random.py b/tcod/random.py
index 6d332b86..1b7b16c7 100644
--- a/tcod/random.py
+++ b/tcod/random.py
@@ -12,14 +12,16 @@
import os
import random
import warnings
-from collections.abc import Hashable
-from typing import Any
+from typing import TYPE_CHECKING, Any
from typing_extensions import deprecated
import tcod.constants
from tcod.cffi import ffi, lib
+if TYPE_CHECKING:
+ from collections.abc import Hashable
+
MERSENNE_TWISTER = tcod.constants.RNG_MT
COMPLEMENTARY_MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC
MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC
diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py
index a671331e..db67fa2e 100644
--- a/tcod/sdl/_internal.py
+++ b/tcod/sdl/_internal.py
@@ -4,7 +4,6 @@
import logging
import sys
-from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, NoReturn, Protocol, TypeVar, overload, runtime_checkable
@@ -13,6 +12,7 @@
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Callable
from types import TracebackType
T = TypeVar("T")
@@ -149,7 +149,7 @@ def __setitem__(self, key: tuple[str, type[T]], value: T, /) -> None:
lib.SDL_SetPointerProperty(self.p, name, value._as_property_pointer())
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
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")
@@ -161,7 +161,7 @@ def _get_error() -> str:
return str(ffi.string(lib.SDL_GetError()), encoding="utf-8")
-def _check(result: bool, /) -> bool:
+def _check(result: bool, /) -> bool: # noqa: FBT001
"""Check if an SDL function returned without errors, and raise an exception if it did."""
if not result:
raise RuntimeError(_get_error())
diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py
index d15abb32..0cb7a9e9 100644
--- a/tcod/sdl/audio.py
+++ b/tcod/sdl/audio.py
@@ -1,14 +1,15 @@
-"""SDL2 audio playback and recording tools.
+"""SDL 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 `_.
+It leaves the loading to sound samples to other libraries such as
+`soundfile `_.
+
+Example::
-Example:
# Synchronous audio example
import time
@@ -53,6 +54,7 @@
from tcod.sdl._internal import _check, _check_float, _check_int, _check_p
if TYPE_CHECKING:
+ import builtins
from collections.abc import Callable, Hashable, Iterable, Iterator
from types import TracebackType
@@ -211,13 +213,14 @@ def convert_audio(
class AudioDevice:
"""An SDL audio device.
- Example:
+ Example::
+
device = tcod.sdl.audio.get_default_playback().open() # Open a common audio device
.. versionchanged:: 16.0
Can now be used as a context which will close the device on exit.
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Removed `spec` and `callback` attribute.
`queued_samples`, `queue_audio`, and `dequeue_audio` moved to :any:`AudioStream` class.
@@ -269,7 +272,7 @@ def __init__(
self.is_physical: Final[bool] = bool(lib.SDL_IsAudioDevicePhysical(device_id))
"""True of this is a physical device, or False if this is a logical device.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
def __repr__(self) -> str:
@@ -294,7 +297,7 @@ def __repr__(self) -> str:
def name(self) -> str:
"""Name of the device.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
return str(ffi.string(_check_p(lib.SDL_GetAudioDeviceName(self.device_id))), encoding="utf-8")
@@ -304,7 +307,7 @@ def gain(self) -> float:
Default is 1.0 but can be set higher or zero.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
return _check_float(lib.SDL_GetAudioDeviceGain(self.device_id), failure=-1.0)
@@ -320,7 +323,7 @@ def open(
) -> Self:
"""Open a new logical audio device for this device.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
.. seealso::
https://wiki.libsdl.org/SDL3/SDL_OpenAudioDevice
@@ -349,7 +352,7 @@ def _sample_size(self) -> int:
def stopped(self) -> bool:
"""Is True if the device has failed or was closed.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
No longer used by the SDL3 API.
"""
return bool(not hasattr(self, "device_id"))
@@ -417,16 +420,17 @@ def close(self) -> None:
def __enter__(self) -> Self:
"""Return self and enter a managed context.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
Use :func:`contextlib.closing` if you want to close this device after a context.
"""
return self
def __exit__(
self,
- type: type[BaseException] | None, # noqa: A002
- value: BaseException | None,
- traceback: TracebackType | None,
+ _type: builtins.type[BaseException] | None, # Explicit builtins prefix to disambiguate Sphinx cross-reference
+ _value: BaseException | None,
+ _traceback: TracebackType | None,
+ /,
) -> None:
"""Close the device when exiting the context."""
self.close()
@@ -443,7 +447,7 @@ def new_stream(
) -> AudioStream:
"""Create, bind, and return a new :any:`AudioStream` for this device.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
new_stream = AudioStream.new(format=format, channels=channels, frequency=frequency)
self.bind((new_stream,))
@@ -463,7 +467,7 @@ def bind(self, streams: Iterable[AudioStream], /) -> None:
class AudioStreamCallbackData:
"""Data provided to AudioStream callbacks.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
additional_bytes: int
@@ -487,7 +491,7 @@ class AudioStream:
This class is commonly created with :any:`AudioDevice.new_stream` which creates a new stream bound to the device.
- ..versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
__slots__ = ("__weakref__", "_stream_p")
@@ -819,10 +823,10 @@ class BasicMixer:
.. versionadded:: 13.6
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Added `frequency` and `channels` parameters.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
Changes in the SDL3 API have made this classes usefulness questionable.
This class should be replaced with custom streams.
"""
@@ -904,7 +908,7 @@ def _on_stream(self, audio_stream: AudioStream, data: AudioStreamCallbackData) -
audio_stream.queue_audio(stream)
-@ffi.def_extern() # type: ignore[misc]
+@ffi.def_extern() # type: ignore[untyped-decorator]
def _sdl_audio_stream_callback(userdata: Any, stream_p: Any, additional_amount: int, total_amount: int, /) -> None: # noqa: ANN401
"""Handle audio device callbacks."""
stream = AudioStream(stream_p)
@@ -927,7 +931,7 @@ def _sdl_audio_stream_callback(userdata: Any, stream_p: Any, additional_amount:
def get_devices() -> dict[str, AudioDevice]:
"""Iterate over the available audio output devices.
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Now returns a dictionary of :any:`AudioDevice`.
"""
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
@@ -942,7 +946,7 @@ def get_devices() -> dict[str, AudioDevice]:
def get_capture_devices() -> dict[str, AudioDevice]:
"""Iterate over the available audio capture devices.
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Now returns a dictionary of :any:`AudioDevice`.
"""
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
@@ -957,10 +961,11 @@ def get_capture_devices() -> dict[str, AudioDevice]:
def get_default_playback() -> AudioDevice:
"""Return the default playback device.
- Example:
+ Example::
+
playback_device = tcod.sdl.audio.get_default_playback().open()
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
return AudioDevice(ffi.cast("SDL_AudioDeviceID", lib.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK))
@@ -969,10 +974,11 @@ def get_default_playback() -> AudioDevice:
def get_default_recording() -> AudioDevice:
"""Return the default recording device.
- Example:
+ Example::
+
recording_device = tcod.sdl.audio.get_default_recording().open()
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO)
return AudioDevice(ffi.cast("SDL_AudioDeviceID", lib.SDL_AUDIO_DEVICE_DEFAULT_RECORDING))
@@ -982,7 +988,7 @@ def get_default_recording() -> AudioDevice:
class AllowedChanges(enum.IntFlag):
"""Which parameters are allowed to be changed when the values given are not supported.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
This is no longer used.
"""
@@ -1033,12 +1039,12 @@ def open( # noqa: A001, PLR0913
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.
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
SDL3 returns audio devices differently, exact formatting is set with :any:`AudioDevice.new_stream` instead.
`samples` and `allowed_changes` are ignored.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
This is an outdated method.
Use :any:`AudioDevice.open` instead, for example:
``tcod.sdl.audio.get_default_playback().open()``
diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py
index b658c510..e6f2de84 100644
--- a/tcod/sdl/joystick.py
+++ b/tcod/sdl/joystick.py
@@ -103,6 +103,7 @@ class Joystick:
"""Currently opened joysticks."""
def __init__(self, sdl_joystick_p: Any) -> None: # noqa: ANN401
+ """Wrap an SDL joystick C pointer."""
self.sdl_joystick_p: Final = sdl_joystick_p
"""The CFFI pointer to an SDL_Joystick struct."""
self.axes: Final[int] = _check_int(lib.SDL_GetNumJoystickAxes(self.sdl_joystick_p), failure=-1)
@@ -135,11 +136,13 @@ def _from_instance_id(cls, instance_id: int) -> Joystick:
return cls._by_instance_id[instance_id]
def __eq__(self, other: object) -> bool:
+ """Return True if `self` and `other` refer to the same joystick."""
if isinstance(other, Joystick):
return self.id == other.id
return NotImplemented
def __hash__(self) -> int:
+ """Return the joystick id as a hash."""
return hash(self.id)
def _get_guid(self) -> str:
@@ -173,6 +176,7 @@ class GameController:
"""Currently opened controllers."""
def __init__(self, sdl_controller_p: Any) -> None: # noqa: ANN401
+ """Wrap an SDL controller C pointer."""
self.sdl_controller_p: Final = sdl_controller_p
self.joystick: Final = Joystick(lib.SDL_GetGamepadJoystick(self.sdl_controller_p))
"""The :any:`Joystick` associated with this controller."""
@@ -200,11 +204,13 @@ def get_axis(self, axis: ControllerAxis) -> int:
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, axis))
def __eq__(self, other: object) -> bool:
+ """Return True if `self` and `other` are both controllers referring to the same joystick."""
if isinstance(other, GameController):
return self.joystick.id == other.joystick.id
return NotImplemented
def __hash__(self) -> int:
+ """Return the joystick id as a hash."""
return hash(self.joystick.id)
# These could exist as convenience functions, but the get_X functions are probably better.
@@ -335,10 +341,10 @@ def _touchpad(self) -> bool:
def init() -> None:
"""Initialize SDL's joystick and game controller subsystems."""
- CONTROLLER_SYSTEMS = tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER
- if tcod.sdl.sys.Subsystem(lib.SDL_WasInit(CONTROLLER_SYSTEMS)) == CONTROLLER_SYSTEMS:
+ controller_systems = tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER
+ if tcod.sdl.sys.Subsystem(lib.SDL_WasInit(controller_systems)) == controller_systems:
return # Already initialized
- tcod.sdl.sys.init(CONTROLLER_SYSTEMS)
+ tcod.sdl.sys.init(controller_systems)
def _get_number() -> int:
@@ -371,7 +377,7 @@ def _get_all() -> list[Joystick | GameController]:
return [GameController._open(i) if lib.SDL_IsGamepad(i) else Joystick._open(i) for i in range(_get_number())]
-def joystick_event_state(new_state: bool | None = None) -> bool:
+def joystick_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001
"""Check or set joystick event polling.
.. seealso::
@@ -384,7 +390,7 @@ def joystick_event_state(new_state: bool | None = None) -> bool:
return lib.SDL_JoystickEventsEnabled()
-def controller_event_state(new_state: bool | None = None) -> bool:
+def controller_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001
"""Check or set game controller event polling.
.. seealso::
diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py
index f4c16ed5..c1dbff7f 100644
--- a/tcod/sdl/mouse.py
+++ b/tcod/sdl/mouse.py
@@ -28,6 +28,7 @@ class Cursor:
"""A cursor icon for use with :any:`set_cursor`."""
def __init__(self, sdl_cursor_p: Any) -> None: # noqa: ANN401
+ """Wrap an SDL cursor C pointer."""
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)
@@ -37,10 +38,17 @@ def __init__(self, sdl_cursor_p: Any) -> None: # noqa: ANN401
self.p = sdl_cursor_p
def __eq__(self, other: object) -> bool:
- return bool(self.p == getattr(other, "p", None))
+ """Return True if `self` is the same cursor as `other`."""
+ if isinstance(other, Cursor):
+ return bool(self.p == getattr(other, "p", None))
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ """Returns the hash of this objects C pointer."""
+ return hash(self.p)
@classmethod
- def _claim(cls, sdl_cursor_p: Any) -> Cursor:
+ def _claim(cls, sdl_cursor_p: Any) -> Cursor: # noqa: ANN401
"""Verify and wrap this pointer in a garbage collector before returning a Cursor."""
return cls(ffi.gc(_check_p(sdl_cursor_p), lib.SDL_DestroyCursor))
@@ -149,7 +157,7 @@ def get_cursor() -> Cursor | None:
return Cursor(cursor_p) if cursor_p else None
-def capture(enable: bool) -> None:
+def capture(enable: bool) -> None: # noqa: FBT001
"""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.
@@ -176,14 +184,14 @@ def capture(enable: bool) -> None:
@deprecated("Set 'Window.relative_mouse_mode = value' instead.")
-def set_relative_mode(enable: bool) -> None:
+def set_relative_mode(enable: bool) -> None: # noqa: FBT001
"""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_SetWindowRelativeMouseMode
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
Replaced with :any:`tcod.sdl.video.Window.relative_mouse_mode`
"""
_check(lib.SDL_SetWindowRelativeMouseMode(lib.SDL_GetMouseFocus(), enable))
@@ -193,7 +201,7 @@ def set_relative_mode(enable: bool) -> None:
def get_relative_mode() -> bool:
"""Return True if relative mouse mode is enabled.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
Replaced with :any:`tcod.sdl.video.Window.relative_mouse_mode`
"""
return bool(lib.SDL_GetWindowRelativeMouseMode(lib.SDL_GetMouseFocus()))
@@ -207,7 +215,7 @@ def get_global_state() -> tcod.event.MouseState:
"""
xy = ffi.new("int[2]")
state = lib.SDL_GetGlobalMouseState(xy, xy + 1)
- return tcod.event.MouseState((xy[0], xy[1]), state=state)
+ return tcod.event.MouseState(position=tcod.event.Point(xy[0], xy[1]), state=state)
def get_relative_state() -> tcod.event.MouseState:
@@ -218,7 +226,7 @@ def get_relative_state() -> tcod.event.MouseState:
"""
xy = ffi.new("int[2]")
state = lib.SDL_GetRelativeMouseState(xy, xy + 1)
- return tcod.event.MouseState((xy[0], xy[1]), state=state)
+ return tcod.event.MouseState(position=tcod.event.Point(xy[0], xy[1]), state=state)
def get_state() -> tcod.event.MouseState:
@@ -229,7 +237,7 @@ def get_state() -> tcod.event.MouseState:
"""
xy = ffi.new("int[2]")
state = lib.SDL_GetMouseState(xy, xy + 1)
- return tcod.event.MouseState((xy[0], xy[1]), state=state)
+ return tcod.event.MouseState(position=tcod.event.Point(xy[0], xy[1]), state=state)
def get_focus() -> tcod.sdl.video.Window | None:
@@ -248,7 +256,7 @@ def warp_in_window(window: tcod.sdl.video.Window, x: int, y: int) -> None:
lib.SDL_WarpMouseInWindow(window.p, x, y)
-def show(visible: bool | None = None) -> bool:
+def show(visible: bool | None = None) -> bool: # noqa: FBT001
"""Optionally show or hide the mouse cursor then return the state of the cursor.
Args:
diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py
index 0a1052e1..05147825 100644
--- a/tcod/sdl/render.py
+++ b/tcod/sdl/render.py
@@ -1,4 +1,4 @@
-"""SDL2 Rendering functionality.
+"""SDL Rendering functionality.
.. versionadded:: 13.4
"""
@@ -6,7 +6,6 @@
from __future__ import annotations
import enum
-from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Final, Literal
import numpy as np
@@ -18,6 +17,8 @@
from tcod.sdl._internal import Properties, _check, _check_p
if TYPE_CHECKING:
+ from collections.abc import Sequence
+
from numpy.typing import NDArray
@@ -48,7 +49,7 @@ class LogicalPresentation(enum.IntEnum):
See https://wiki.libsdl.org/SDL3/SDL_RendererLogicalPresentation
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
DISABLED = 0
@@ -140,6 +141,19 @@ class BlendMode(enum.IntEnum):
""""""
+class ScaleMode(enum.IntEnum):
+ """Texture scaling modes.
+
+ .. versionadded:: 19.3
+ """
+
+ NEAREST = lib.SDL_SCALEMODE_NEAREST
+ """Nearing neighbor."""
+ LINEAR = lib.SDL_SCALEMODE_LINEAR
+ """Linier filtering."""
+ # PIXELART = lib.SDL_SCALEMODE_PIXELART # Needs SDL 3.4 # noqa: ERA001
+
+
def compose_blend_mode( # noqa: PLR0913
source_color_factor: BlendFactor,
dest_color_factor: BlendFactor,
@@ -173,7 +187,7 @@ class Texture:
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:
+ def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: # noqa: ANN401
"""Encapsulate an SDL_Texture pointer. This function is private."""
self.p = sdl_texture_p
self._sdl_renderer_p = sdl_renderer_p # Keep alive.
@@ -200,6 +214,10 @@ def __eq__(self, other: object) -> bool:
return bool(self.p == other.p)
return NotImplemented
+ def __hash__(self) -> int:
+ """Return hash for the owned pointer."""
+ return hash(self.p)
+
def update(self, pixels: NDArray[Any], rect: tuple[int, int, int, int] | None = None) -> None:
"""Update the pixel data of this texture.
@@ -249,6 +267,20 @@ def color_mod(self) -> tuple[int, int, int]:
def color_mod(self, rgb: tuple[int, int, int]) -> None:
_check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2]))
+ @property
+ def scale_mode(self) -> ScaleMode:
+ """Get or set this textures :any:`ScaleMode`.
+
+ .. versionadded:: 19.3
+ """
+ mode = ffi.new("SDL_ScaleMode*")
+ _check(lib.SDL_GetTextureScaleMode(self.p, mode))
+ return ScaleMode(mode[0])
+
+ @scale_mode.setter
+ def scale_mode(self, value: ScaleMode, /) -> None:
+ _check(lib.SDL_SetTextureScaleMode(self.p, value))
+
class _RestoreTargetContext:
"""A context manager which tracks the current render target and restores it on exiting."""
@@ -267,7 +299,7 @@ def __exit__(self, *_: object) -> None:
class Renderer:
"""SDL Renderer."""
- def __init__(self, sdl_renderer_p: Any) -> None:
+ def __init__(self, sdl_renderer_p: Any) -> None: # noqa: ANN401
"""Encapsulate an SDL_Renderer pointer. This function is private."""
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)})."
@@ -283,6 +315,10 @@ def __eq__(self, other: object) -> bool:
return bool(self.p == other.p)
return NotImplemented
+ def __hash__(self) -> int:
+ """Return hash for the owned pointer."""
+ return hash(self.p)
+
def copy( # noqa: PLR0913
self,
texture: Texture,
@@ -328,7 +364,7 @@ def set_render_target(self, texture: Texture) -> _RestoreTargetContext:
_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:
+ def new_texture(self, width: int, height: int, *, format: int | None = None, access: int | None = None) -> Texture: # noqa: A002
"""Allocate and return a new Texture for this renderer.
Args:
@@ -339,13 +375,13 @@ def new_texture(self, width: int, height: int, *, format: int | None = None, acc
See :any:`TextureAccess` for more options.
"""
if format is None:
- format = 0
+ format = 0 # noqa: A001
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:
+ def upload_texture(self, pixels: NDArray[Any], *, format: int | None = None, access: int | None = None) -> Texture: # noqa: A002
"""Return a new Texture from an array of pixels.
Args:
@@ -358,9 +394,9 @@ def upload_texture(self, pixels: NDArray[Any], *, format: int | None = None, acc
assert len(pixels.shape) == 3 # noqa: PLR2004
assert pixels.dtype == np.uint8
if pixels.shape[2] == 4: # noqa: PLR2004
- format = int(lib.SDL_PIXELFORMAT_RGBA32)
+ format = int(lib.SDL_PIXELFORMAT_RGBA32) # noqa: A001
elif pixels.shape[2] == 3: # noqa: PLR2004
- format = int(lib.SDL_PIXELFORMAT_RGB24)
+ format = int(lib.SDL_PIXELFORMAT_RGB24) # noqa: A001
else:
msg = f"Can't determine the format required for an array of shape {pixels.shape}."
raise TypeError(msg)
@@ -403,16 +439,16 @@ def draw_blend_mode(self, value: int) -> None:
@property
def output_size(self) -> tuple[int, int]:
- """Get the (width, height) pixel resolution of the rendering context.
+ """Get the (width, height) pixel resolution of the current rendering context.
.. seealso::
- https://wiki.libsdl.org/SDL_GetRendererOutputSize
+ https://wiki.libsdl.org/SDL3/SDL_GetCurrentRenderOutputSize
.. versionadded:: 13.5
"""
out = ffi.new("int[2]")
_check(lib.SDL_GetCurrentRenderOutputSize(self.p, out, out + 1))
- return out[0], out[1]
+ return tuple(out)
@property
def clip_rect(self) -> tuple[int, int, int, int] | None:
@@ -439,28 +475,30 @@ def set_logical_presentation(self, resolution: tuple[int, int], mode: LogicalPre
.. seealso::
https://wiki.libsdl.org/SDL3/SDL_SetRenderLogicalPresentation
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
width, height = resolution
_check(lib.SDL_SetRenderLogicalPresentation(self.p, width, height, mode))
@property
- def logical_size(self) -> tuple[int, int]:
- """Get current independent (width, height) resolution.
-
- Might be (0, 0) if a resolution was never assigned.
+ def logical_size(self) -> tuple[int, int] | None:
+ """Get current independent (width, height) resolution, or None if logical size is unset.
.. seealso::
https://wiki.libsdl.org/SDL3/SDL_GetRenderLogicalPresentation
.. versionadded:: 13.5
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Setter is deprecated, use :any:`set_logical_presentation` instead.
+
+ .. versionchanged:: 20.0
+ Return ``None`` instead of ``(0, 0)`` when logical size is disabled.
"""
out = ffi.new("int[2]")
lib.SDL_GetRenderLogicalPresentation(self.p, out, out + 1, ffi.NULL)
- return out[0], out[1]
+ out_tuple = tuple(out)
+ return None if out_tuple == (0, 0) else out_tuple
@logical_size.setter
@deprecated("Use set_logical_presentation method to correctly setup logical size.")
@@ -473,7 +511,7 @@ def scale(self) -> tuple[float, float]:
"""Get or set an (x_scale, y_scale) multiplier for drawing.
.. seealso::
- https://wiki.libsdl.org/SDL_RenderSetScale
+ https://wiki.libsdl.org/SDL3/SDL_SetRenderScale
.. versionadded:: 13.5
"""
@@ -490,7 +528,7 @@ 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
+ https://wiki.libsdl.org/SDL3/SDL_SetRenderViewport
.. versionadded:: 13.5
"""
@@ -502,7 +540,7 @@ def viewport(self) -> tuple[int, int, int, int] | None:
def viewport(self, rect: tuple[int, int, int, int] | None) -> None:
_check(lib.SDL_SetRenderViewport(self.p, (rect,)))
- def set_vsync(self, enable: bool) -> None:
+ def set_vsync(self, enable: bool) -> None: # noqa: FBT001
"""Enable or disable VSync for this renderer.
.. versionadded:: 13.5
@@ -533,11 +571,11 @@ def read_pixels(
See https://wiki.libsdl.org/SDL3/SDL_RenderReadPixels
Returns:
- The output uint8 array of shape: ``(height, width, channels)`` with the fetched pixels.
+ The output uint8 array of shape ``(height, width, channels)`` with the fetched pixels.
.. versionadded:: 15.0
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
`format` no longer accepts `int` values.
"""
surface = _check_p(
@@ -625,7 +663,7 @@ def fill_rects(self, rects: NDArray[np.number] | Sequence[tuple[float, float, fl
.. versionadded:: 13.5
"""
rects = self._convert_array(rects, item_length=4)
- _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
+ _check(lib.SDL_RenderFillRects(self.p, ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
def draw_rects(self, rects: NDArray[np.number] | Sequence[tuple[float, float, float, float]]) -> None:
"""Draw multiple outlined rectangles from an array.
@@ -638,7 +676,7 @@ def draw_rects(self, rects: NDArray[np.number] | Sequence[tuple[float, float, fl
rects = self._convert_array(rects, item_length=4)
assert len(rects.shape) == 2 # noqa: PLR2004
assert rects.shape[1] == 4 # noqa: PLR2004
- _check(lib.SDL_RenderRects(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
+ _check(lib.SDL_RenderRects(self.p, ffi.from_buffer("SDL_FRect*", rects), rects.shape[0]))
def draw_points(self, points: NDArray[np.number] | Sequence[tuple[float, float]]) -> None:
"""Draw an array of points.
@@ -649,7 +687,7 @@ def draw_points(self, points: NDArray[np.number] | Sequence[tuple[float, float]]
.. versionadded:: 13.5
"""
points = self._convert_array(points, item_length=2)
- _check(lib.SDL_RenderPoints(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0]))
+ _check(lib.SDL_RenderPoints(self.p, ffi.from_buffer("SDL_FPoint*", points), points.shape[0]))
def draw_lines(self, points: NDArray[np.number] | Sequence[tuple[float, float]]) -> None:
"""Draw a connected series of lines from an array.
@@ -660,7 +698,7 @@ def draw_lines(self, points: NDArray[np.number] | Sequence[tuple[float, float]])
.. versionadded:: 13.5
"""
points = self._convert_array(points, item_length=2)
- _check(lib.SDL_RenderLines(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0]))
+ _check(lib.SDL_RenderLines(self.p, ffi.from_buffer("SDL_FPoint*", points), points.shape[0]))
def geometry(
self,
@@ -681,7 +719,7 @@ def geometry(
.. versionadded:: 13.5
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
`color` now takes float values instead of 8-bit integers.
"""
xy = np.ascontiguousarray(xy, np.float32)
@@ -717,6 +755,32 @@ def geometry(
)
)
+ def coordinates_from_window(self, xy: tuple[float, float], /) -> tuple[float, float]:
+ """Return the renderer coordinates from the given windows coordinates.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesFromWindow
+
+ .. versionadded:: 20.0
+ """
+ x, y = xy
+ out_xy = ffi.new("float[2]")
+ _check(lib.SDL_RenderCoordinatesFromWindow(self.p, x, y, out_xy, out_xy + 1))
+ return tuple(out_xy)
+
+ def coordinates_to_window(self, xy: tuple[float, float], /) -> tuple[float, float]:
+ """Return the window coordinates from the given render coordinates.
+
+ .. seealso::
+ https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesToWindow
+
+ .. versionadded:: 20.0
+ """
+ x, y = xy
+ out_xy = ffi.new("float[2]")
+ _check(lib.SDL_RenderCoordinatesToWindow(self.p, x, y, out_xy, out_xy + 1))
+ return tuple(out_xy)
+
def new_renderer(
window: tcod.sdl.video.Window,
@@ -741,7 +805,7 @@ def new_renderer(
.. seealso::
:func:`tcod.sdl.video.new_window`
- .. versionchanged:: Unreleased
+ .. versionchanged:: 19.0
Removed `software` and `target_textures` parameters.
`vsync` now takes an integer.
`driver` now take a string.
diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py
index 869d6448..c4056c69 100644
--- a/tcod/sdl/sys.py
+++ b/tcod/sdl/sys.py
@@ -24,7 +24,7 @@ def init(flags: int) -> None:
_check(lib.SDL_InitSubSystem(flags))
-def quit(flags: int | None = None) -> None:
+def quit(flags: int | None = None) -> None: # noqa: A001
if flags is None:
lib.SDL_Quit()
return
diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py
index cee02bf5..e9565f88 100644
--- a/tcod/sdl/video.py
+++ b/tcod/sdl/video.py
@@ -1,4 +1,4 @@
-"""SDL2 Window and Display handling.
+"""SDL 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,
@@ -24,7 +24,9 @@
from numpy.typing import ArrayLike, NDArray
__all__ = (
+ "Capitalization",
"FlashOperation",
+ "TextInputType",
"Window",
"WindowFlags",
"get_grabbed_window",
@@ -89,6 +91,56 @@ class FlashOperation(enum.IntEnum):
"""Flash until focus is gained."""
+class TextInputType(enum.IntEnum):
+ """SDL input types for text input.
+
+ .. seealso::
+ :any:`Window.start_text_input`
+ https://wiki.libsdl.org/SDL3/SDL_TextInputType
+
+ .. versionadded:: 19.1
+ """
+
+ TEXT = lib.SDL_TEXTINPUT_TYPE_TEXT
+ """The input is text."""
+ TEXT_NAME = lib.SDL_TEXTINPUT_TYPE_TEXT_NAME
+ """The input is a person's name."""
+ TEXT_EMAIL = lib.SDL_TEXTINPUT_TYPE_TEXT_EMAIL
+ """The input is an e-mail address."""
+ TEXT_USERNAME = lib.SDL_TEXTINPUT_TYPE_TEXT_USERNAME
+ """The input is a username."""
+ TEXT_PASSWORD_HIDDEN = lib.SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN
+ """The input is a secure password that is hidden."""
+ TEXT_PASSWORD_VISIBLE = lib.SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_VISIBLE
+ """The input is a secure password that is visible."""
+ NUMBER = lib.SDL_TEXTINPUT_TYPE_NUMBER
+ """The input is a number."""
+ NUMBER_PASSWORD_HIDDEN = lib.SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN
+ """The input is a secure PIN that is hidden."""
+ NUMBER_PASSWORD_VISIBLE = lib.SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_VISIBLE
+ """The input is a secure PIN that is visible."""
+
+
+class Capitalization(enum.IntEnum):
+ """Text capitalization for text input.
+
+ .. seealso::
+ :any:`Window.start_text_input`
+ https://wiki.libsdl.org/SDL3/SDL_Capitalization
+
+ .. versionadded:: 19.1
+ """
+
+ NONE = lib.SDL_CAPITALIZE_NONE
+ """No auto-capitalization will be done."""
+ SENTENCES = lib.SDL_CAPITALIZE_SENTENCES
+ """The first letter of sentences will be capitalized."""
+ WORDS = lib.SDL_CAPITALIZE_WORDS
+ """The first letter of words will be capitalized."""
+ LETTERS = lib.SDL_CAPITALIZE_LETTERS
+ """All letters will be capitalized."""
+
+
class _TempSurface:
"""Holds a temporary surface derived from a NumPy array."""
@@ -105,7 +157,7 @@ def __init__(self, pixels: ArrayLike) -> None:
lib.SDL_CreateSurfaceFrom(
self._array.shape[1],
self._array.shape[0],
- lib.SDL_PIXELFORMAT_RGBA32 if self._array.shape[2] == 4 else lib.SDL_PIXELFORMAT_RGB24,
+ lib.SDL_PIXELFORMAT_RGBA32 if self._array.shape[2] == 4 else lib.SDL_PIXELFORMAT_RGB24, # noqa: PLR2004
ffi.from_buffer("void*", self._array),
self._array.strides[0],
)
@@ -115,9 +167,21 @@ def __init__(self, pixels: ArrayLike) -> None:
class Window:
- """An SDL2 Window object."""
+ """An SDL Window object.
+
+ Created from :any:`tcod.sdl.video.new_window` when working with SDL directly.
+
+ When using the libtcod :any:`Context` you can access its `Window` via :any:`Context.sdl_window`.
+ """
+
+ def __init__(self, sdl_window_p: Any | int) -> None: # noqa: ANN401
+ """Wrap a SDL_Window pointer or SDL WindowID.
- def __init__(self, sdl_window_p: Any) -> None: # noqa: ANN401
+ .. versionchanged:: 21.0
+ Now accepts `int` types as an SDL WindowID.
+ """
+ if isinstance(sdl_window_p, int):
+ sdl_window_p = _check_p(lib.SDL_GetWindowFromID(sdl_window_p))
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)
@@ -129,10 +193,15 @@ def __init__(self, sdl_window_p: Any) -> None: # noqa: ANN401
self.p = sdl_window_p
def __eq__(self, other: object) -> bool:
+ """Return True if `self` and `other` wrap the same window."""
if not isinstance(other, Window):
return NotImplemented
return bool(self.p == other.p)
+ def __hash__(self) -> int:
+ """Return the hash of this instances SDL window pointer."""
+ return hash(self.p)
+
def _as_property_pointer(self) -> Any: # noqa: ANN401
return self.p
@@ -276,7 +345,7 @@ def opacity(self, value: float) -> None:
def grab(self) -> bool:
"""Get or set this windows input grab mode.
- .. deprecated:: Unreleased
+ .. deprecated:: 19.0
This attribute as been split into :any:`mouse_grab` and :any:`keyboard_grab`.
"""
return self.mouse_grab
@@ -289,7 +358,7 @@ def grab(self, value: bool) -> None:
def mouse_grab(self) -> bool:
"""Get or set this windows mouse input grab mode.
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
return bool(lib.SDL_GetWindowMouseGrab(self.p))
@@ -303,7 +372,7 @@ def keyboard_grab(self) -> bool:
https://wiki.libsdl.org/SDL3/SDL_SetWindowKeyboardGrab
- .. versionadded:: Unreleased
+ .. versionadded:: 19.0
"""
return bool(lib.SDL_GetWindowKeyboardGrab(self.p))
@@ -369,6 +438,81 @@ def relative_mouse_mode(self) -> bool:
def relative_mouse_mode(self, enable: bool, /) -> None:
_check(lib.SDL_SetWindowRelativeMouseMode(self.p, enable))
+ def start_text_input(
+ self,
+ *,
+ type: TextInputType = TextInputType.TEXT, # noqa: A002
+ capitalization: Capitalization | None = None,
+ autocorrect: bool = True,
+ multiline: bool | None = None,
+ android_type: int | None = None,
+ ) -> None:
+ """Start receiving text input events supporting Unicode. This may open an on-screen keyboard.
+
+ This method is meant to be paired with :any:`set_text_input_area`.
+
+ Args:
+ type: Type of text being inputted, see :any:`TextInputType`
+ capitalization: Capitalization hint, default is based on `type` given, see :any:`Capitalization`.
+ autocorrect: Enable auto completion and auto correction.
+ multiline: Allow multiple lines of text.
+ android_type: Input type for Android, see SDL docs.
+
+ Example::
+
+ context: tcod.context.Context # Assuming tcod context is used
+
+ if context.sdl_window:
+ context.sdl_window.start_text_input()
+
+ ... # Handle Unicode input using TextInput events
+
+ context.sdl_window.stop_text_input() # Close on-screen keyboard when done
+
+ .. seealso::
+ :any:`stop_text_input`
+ :any:`set_text_input_area`
+ :any:`TextInput`
+ https://wiki.libsdl.org/SDL3/SDL_StartTextInputWithProperties
+
+ .. versionadded:: 19.1
+ """
+ props = Properties()
+ props[("SDL_PROP_TEXTINPUT_TYPE_NUMBER", int)] = int(type)
+ if capitalization is not None:
+ props[("SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER", int)] = int(capitalization)
+ props[("SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN", bool)] = autocorrect
+ if multiline is not None:
+ props[("SDL_PROP_TEXTINPUT_MULTILINE_BOOLEAN", bool)] = multiline
+ if android_type is not None:
+ props[("SDL_PROP_TEXTINPUT_ANDROID_INPUTTYPE_NUMBER", int)] = int(android_type)
+ _check(lib.SDL_StartTextInputWithProperties(self.p, props.p))
+
+ def set_text_input_area(self, rect: tuple[int, int, int, int], cursor: int) -> None:
+ """Assign the area used for entering Unicode text input.
+
+ Args:
+ rect: `(x, y, width, height)` rectangle used for text input
+ cursor: Cursor X position, relative to `rect[0]`
+
+ .. seealso::
+ :any:`start_text_input`
+ https://wiki.libsdl.org/SDL3/SDL_SetTextInputArea
+
+ .. versionadded:: 19.1
+ """
+ _check(lib.SDL_SetTextInputArea(self.p, (rect,), cursor))
+
+ def stop_text_input(self) -> None:
+ """Stop receiving text events for this window and close relevant on-screen keyboards.
+
+ .. seealso::
+ :any:`start_text_input`
+
+ .. versionadded:: 19.1
+ """
+ _check(lib.SDL_StopTextInput(self.p))
+
def new_window( # noqa: PLR0913
width: int,
@@ -420,7 +564,7 @@ def get_grabbed_window() -> Window | None:
return Window(sdl_window_p) if sdl_window_p else None
-def screen_saver_allowed(allow: bool | None = None) -> bool:
+def screen_saver_allowed(allow: bool | None = None) -> bool: # noqa: FBT001
"""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.
diff --git a/tcod/tdl.c b/tcod/tdl.c
deleted file mode 100644
index c293428f..00000000
--- a/tcod/tdl.c
+++ /dev/null
@@ -1,201 +0,0 @@
-/* extra functions provided for the python-tdl library */
-#include "tdl.h"
-
-#include "../libtcod/src/libtcod/wrappers.h"
-
-void SDL_main(void){};
-
-TCOD_value_t TDL_list_get_union(TCOD_list_t l, int idx) {
- TCOD_value_t item;
- item.custom = TCOD_list_get(l, idx);
- return item;
-}
-
-bool TDL_list_get_bool(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).b; }
-
-char TDL_list_get_char(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).c; }
-
-int TDL_list_get_int(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).i; }
-
-float TDL_list_get_float(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).f; }
-
-char* TDL_list_get_string(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).s; }
-
-TCOD_color_t TDL_list_get_color(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).col; }
-
-TCOD_dice_t TDL_list_get_dice(TCOD_list_t l, int idx) { return TDL_list_get_union(l, idx).dice; }
-
-/* get a TCOD color type from a 0xRRGGBB formatted integer */
-TCOD_color_t TDL_color_from_int(int color) {
- TCOD_color_t tcod_color = {(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff};
- return tcod_color;
-}
-
-int TDL_color_to_int(TCOD_color_t* color) { return (color->r << 16) | (color->g << 8) | color->b; }
-
-int* TDL_color_int_to_array(int color) {
- static int array[3];
- array[0] = (color >> 16) & 0xff;
- array[1] = (color >> 8) & 0xff;
- array[2] = color & 0xff;
- return array;
-}
-
-int TDL_color_RGB(int r, int g, int b) { return ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff); }
-
-int TDL_color_HSV(float h, float s, float v) {
- TCOD_color_t tcod_color = TCOD_color_HSV(h, s, v);
- return TDL_color_to_int(&tcod_color);
-}
-
-bool TDL_color_equals(int c1, int c2) { return (c1 == c2); }
-
-int TDL_color_add(int c1, int c2) {
- TCOD_color_t tc1 = TDL_color_from_int(c1);
- TCOD_color_t tc2 = TDL_color_from_int(c2);
- tc1 = TCOD_color_add(tc1, tc2);
- return TDL_color_to_int(&tc1);
-}
-
-int TDL_color_subtract(int c1, int c2) {
- TCOD_color_t tc1 = TDL_color_from_int(c1);
- TCOD_color_t tc2 = TDL_color_from_int(c2);
- tc1 = TCOD_color_subtract(tc1, tc2);
- return TDL_color_to_int(&tc1);
-}
-
-int TDL_color_multiply(int c1, int c2) {
- TCOD_color_t tc1 = TDL_color_from_int(c1);
- TCOD_color_t tc2 = TDL_color_from_int(c2);
- tc1 = TCOD_color_multiply(tc1, tc2);
- return TDL_color_to_int(&tc1);
-}
-
-int TDL_color_multiply_scalar(int c, float value) {
- TCOD_color_t tc = TDL_color_from_int(c);
- tc = TCOD_color_multiply_scalar(tc, value);
- return TDL_color_to_int(&tc);
-}
-
-int TDL_color_lerp(int c1, int c2, float coef) {
- TCOD_color_t tc1 = TDL_color_from_int(c1);
- TCOD_color_t tc2 = TDL_color_from_int(c2);
- tc1 = TCOD_color_lerp(tc1, tc2, coef);
- return TDL_color_to_int(&tc1);
-}
-
-float TDL_color_get_hue(int color) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- return TCOD_color_get_hue(tcod_color);
-}
-float TDL_color_get_saturation(int color) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- return TCOD_color_get_saturation(tcod_color);
-}
-float TDL_color_get_value(int color) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- return TCOD_color_get_value(tcod_color);
-}
-int TDL_color_set_hue(int color, float h) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- TCOD_color_set_hue(&tcod_color, h);
- return TDL_color_to_int(&tcod_color);
-}
-int TDL_color_set_saturation(int color, float h) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- TCOD_color_set_saturation(&tcod_color, h);
- return TDL_color_to_int(&tcod_color);
-}
-int TDL_color_set_value(int color, float h) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- TCOD_color_set_value(&tcod_color, h);
- return TDL_color_to_int(&tcod_color);
-}
-int TDL_color_shift_hue(int color, float hue_shift) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- TCOD_color_shift_hue(&tcod_color, hue_shift);
- return TDL_color_to_int(&tcod_color);
-}
-int TDL_color_scale_HSV(int color, float scoef, float vcoef) {
- TCOD_color_t tcod_color = TDL_color_from_int(color);
- TCOD_color_scale_HSV(&tcod_color, scoef, vcoef);
- return TDL_color_to_int(&tcod_color);
-}
-
-#define TRANSPARENT_BIT 1
-#define WALKABLE_BIT 2
-#define FOV_BIT 4
-
-/* set map transparent and walkable flags from a buffer */
-void TDL_map_data_from_buffer(TCOD_map_t map, uint8_t* buffer) {
- int width = TCOD_map_get_width(map);
- int height = TCOD_map_get_height(map);
- int x;
- int y;
- for (y = 0; y < height; y++) {
- for (x = 0; x < width; x++) {
- int i = y * width + x;
- TCOD_map_set_properties(map, x, y, (buffer[i] & TRANSPARENT_BIT) != 0, (buffer[i] & WALKABLE_BIT) != 0);
- }
- }
-}
-
-/* get fov from tcod map */
-void TDL_map_fov_to_buffer(TCOD_map_t map, uint8_t* buffer, bool cumulative) {
- int width = TCOD_map_get_width(map);
- int height = TCOD_map_get_height(map);
- int x;
- int y;
- for (y = 0; y < height; y++) {
- for (x = 0; x < width; x++) {
- int i = y * width + x;
- if (!cumulative) { buffer[i] &= ~FOV_BIT; }
- if (TCOD_map_is_in_fov(map, x, y)) { buffer[i] |= FOV_BIT; }
- }
- }
-}
-
-/* set functions are called conditionally for ch/fg/bg (-1 is ignored)
- colors are converted to TCOD_color_t types in C and is much faster than in
- Python.
- Also Python indexing is used, negative x/y will index to (width-x, etc.) */
-int TDL_console_put_char_ex(TCOD_console_t console, int x, int y, int ch, int fg, int bg, TCOD_bkgnd_flag_t blend) {
- int width = TCOD_console_get_width(console);
- int height = TCOD_console_get_height(console);
- TCOD_color_t color;
-
- if (x < -width || x >= width || y < -height || y >= height) { return -1; /* outside of console */ }
-
- /* normalize x, y */
- if (x < 0) { x += width; };
- if (y < 0) { y += height; };
-
- if (ch != -1) { TCOD_console_set_char(console, x, y, ch); }
- if (fg != -1) {
- color = TDL_color_from_int(fg);
- TCOD_console_set_char_foreground(console, x, y, color);
- }
- if (bg != -1) {
- color = TDL_color_from_int(bg);
- TCOD_console_set_char_background(console, x, y, color, blend);
- }
- return 0;
-}
-
-int TDL_console_get_bg(TCOD_console_t console, int x, int y) {
- TCOD_color_t tcod_color = TCOD_console_get_char_background(console, x, y);
- return TDL_color_to_int(&tcod_color);
-}
-
-int TDL_console_get_fg(TCOD_console_t console, int x, int y) {
- TCOD_color_t tcod_color = TCOD_console_get_char_foreground(console, x, y);
- return TDL_color_to_int(&tcod_color);
-}
-
-void TDL_console_set_bg(TCOD_console_t console, int x, int y, int color, TCOD_bkgnd_flag_t flag) {
- TCOD_console_set_char_background(console, x, y, TDL_color_from_int(color), flag);
-}
-
-void TDL_console_set_fg(TCOD_console_t console, int x, int y, int color) {
- TCOD_console_set_char_foreground(console, x, y, TDL_color_from_int(color));
-}
diff --git a/tcod/tdl.h b/tcod/tdl.h
deleted file mode 100644
index 71a7492e..00000000
--- a/tcod/tdl.h
+++ /dev/null
@@ -1,54 +0,0 @@
-#ifndef PYTHON_TCOD_TDL_H_
-#define PYTHON_TCOD_TDL_H_
-
-#include "../libtcod/src/libtcod/libtcod.h"
-
-/* TDL FUNCTIONS ---------------------------------------------------------- */
-
-TCOD_value_t TDL_list_get_union(TCOD_list_t l, int idx);
-bool TDL_list_get_bool(TCOD_list_t l, int idx);
-char TDL_list_get_char(TCOD_list_t l, int idx);
-int TDL_list_get_int(TCOD_list_t l, int idx);
-float TDL_list_get_float(TCOD_list_t l, int idx);
-char* TDL_list_get_string(TCOD_list_t l, int idx);
-TCOD_color_t TDL_list_get_color(TCOD_list_t l, int idx);
-TCOD_dice_t TDL_list_get_dice(TCOD_list_t l, int idx);
-/*bool (*TDL_parser_new_property_func)(const char *propname, TCOD_value_type_t
- * type, TCOD_value_t *value);*/
-
-/* color functions modified to use integers instead of structs */
-TCOD_color_t TDL_color_from_int(int color);
-int TDL_color_to_int(TCOD_color_t* color);
-int* TDL_color_int_to_array(int color);
-int TDL_color_RGB(int r, int g, int b);
-int TDL_color_HSV(float h, float s, float v);
-bool TDL_color_equals(int c1, int c2);
-int TDL_color_add(int c1, int c2);
-int TDL_color_subtract(int c1, int c2);
-int TDL_color_multiply(int c1, int c2);
-int TDL_color_multiply_scalar(int c, float value);
-int TDL_color_lerp(int c1, int c2, float coef);
-float TDL_color_get_hue(int color);
-float TDL_color_get_saturation(int color);
-float TDL_color_get_value(int color);
-int TDL_color_set_hue(int color, float h);
-int TDL_color_set_saturation(int color, float h);
-int TDL_color_set_value(int color, float h);
-int TDL_color_shift_hue(int color, float hue_shift);
-int TDL_color_scale_HSV(int color, float scoef, float vcoef);
-
-/* map data functions using a bitmap of:
- * 1 = is_transparant
- * 2 = is_walkable
- * 4 = in_fov
- */
-void TDL_map_data_from_buffer(TCOD_map_t map, uint8_t* buffer);
-void TDL_map_fov_to_buffer(TCOD_map_t map, uint8_t* buffer, bool cumulative);
-
-int TDL_console_put_char_ex(TCOD_console_t console, int x, int y, int ch, int fg, int bg, TCOD_bkgnd_flag_t flag);
-int TDL_console_get_bg(TCOD_console_t console, int x, int y);
-int TDL_console_get_fg(TCOD_console_t console, int x, int y);
-void TDL_console_set_bg(TCOD_console_t console, int x, int y, int color, TCOD_bkgnd_flag_t flag);
-void TDL_console_set_fg(TCOD_console_t console, int x, int y, int color);
-
-#endif /* PYTHON_TCOD_TDL_H_ */
diff --git a/tcod/tileset.py b/tcod/tileset.py
index 6362c93d..58ddf058 100644
--- a/tcod/tileset.py
+++ b/tcod/tileset.py
@@ -2,31 +2,31 @@
If you want to load a tileset from a common tileset image then you only need :any:`tcod.tileset.load_tilesheet`.
-Tilesets can be loaded as a whole from tile-sheets or True-Type fonts, or they
-can be put together from multiple tile images by loading them separately
-using :any:`Tileset.set_tile`.
+Tilesets can be loaded as a whole from tile-sheets or True-Type fonts,
+or they can be put together from multiple tile images by loading them separately using :any:`Tileset.__setitem__`.
-A major restriction with libtcod is that all tiles must be the same size and
-tiles can't overlap when rendered. For sprite-based rendering it can be
-useful to use `an alternative library for graphics rendering
+A major restriction with libtcod is that all tiles must be the same size and tiles can't overlap when rendered.
+For sprite-based rendering it can be useful to use `an alternative library for graphics rendering
`_ while continuing to use
python-tcod's pathfinding and field-of-view algorithms.
"""
from __future__ import annotations
+import copy
import itertools
-from collections.abc import Iterable
+from collections.abc import Iterator, Mapping, MutableMapping
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, SupportsInt, overload
import numpy as np
-from typing_extensions import deprecated
+from typing_extensions import Self, deprecated
from tcod._internal import _check, _check_p, _console, _path_encode, _raise_tcod_error
from tcod.cffi import ffi, lib
if TYPE_CHECKING:
+ from collections.abc import Iterable
from os import PathLike
from numpy.typing import ArrayLike, NDArray
@@ -34,10 +34,11 @@
import tcod.console
-class Tileset:
+class Tileset(MutableMapping[int, "NDArray[np.uint8]"]):
"""A collection of graphical tiles.
- This class is provisional, the API may change in the future.
+ .. versionchanged:: 20.1
+ Is now a :class:`collections.abc.MutableMapping` type.
"""
def __init__(self, tile_width: int, tile_height: int) -> None:
@@ -80,28 +81,105 @@ 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:
+ def __copy__(self) -> Self:
+ """Return a clone of this tileset.
+
+ This is not an exact copy. :any:`remap` will not work on the clone.
+
+ .. versionadded:: 20.1
+ """
+ clone = self.__class__(self.tile_width, self.tile_height)
+ for codepoint, tile in self.items():
+ clone[codepoint] = tile
+ return clone
+
+ @staticmethod
+ def _iter_items(
+ tiles: Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]] | Mapping[int, ArrayLike | NDArray[np.uint8]], /
+ ) -> Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]]:
+ """Convert a potential mapping to an iterator."""
+ if isinstance(tiles, Mapping):
+ return tiles.items() # pyright: ignore[reportReturnType]
+ return tiles
+
+ def __iadd__(
+ self,
+ other: Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]] | Mapping[int, ArrayLike | NDArray[np.uint8]],
+ /,
+ ) -> Self:
+ """Add tiles to this tileset inplace, prefers replacing tiles.
+
+ .. versionadded:: 20.1
+ """
+ for codepoint, tile in self._iter_items(other):
+ self[codepoint] = tile
+ return self
+
+ def __add__(
+ self,
+ other: Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]] | Mapping[int, ArrayLike | NDArray[np.uint8]],
+ /,
+ ) -> Self:
+ """Combine tiles with this tileset, prefers replacing tiles.
+
+ .. versionadded:: 20.1
+ """
+ clone = copy.copy(self)
+ clone += other
+ return clone
+
+ def __ior__(
+ self,
+ other: Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]] | Mapping[int, ArrayLike | NDArray[np.uint8]],
+ /,
+ ) -> Self:
+ """Add tiles to this tileset inplace, keeps existing tiles instead of replacing them.
+
+ .. versionadded:: 20.1
+ """
+ for codepoint, tile in self._iter_items(other):
+ if codepoint not in self:
+ self[codepoint] = tile
+ return self
+
+ def __or__(
+ self,
+ other: Iterable[tuple[int, ArrayLike | NDArray[np.uint8]]] | Mapping[int, ArrayLike | NDArray[np.uint8]],
+ /,
+ ) -> Self:
+ """Combine tiles with this tileset, prefers keeping existing tiles instead of replacing them.
+
+ .. versionadded:: 20.1
+ """
+ clone = copy.copy(self)
+ clone |= other
+ return clone
+
+ def __contains__(self, codepoint: object, /) -> bool:
"""Test if a tileset has a codepoint with ``n in tileset``."""
- return bool(lib.TCOD_tileset_get_tile_(self._tileset_p, codepoint, ffi.NULL) == 0)
+ if not isinstance(codepoint, SupportsInt):
+ return False
+ codepoint = int(codepoint)
+ if not 0 <= codepoint < self._tileset_p.character_map_length:
+ return False
+ return bool(self._get_character_map()[int(codepoint)] > 0)
- def get_tile(self, codepoint: int) -> NDArray[np.uint8]:
- """Return a copy of a tile for the given codepoint.
+ def __getitem__(self, codepoint: int, /) -> NDArray[np.uint8]:
+ """Return the RGBA tile data for the given codepoint.
- If the tile does not exist yet then a blank array will be returned.
+ The tile will have a shape of (height, width, rgba) and a dtype of uint8.
+ Note that most grey-scale tilesets will only use the alpha channel with a solid white color channel.
- The tile will have a shape of (height, width, rgba) and a dtype of
- uint8. Note that most grey-scale tiles will only use the alpha
- channel and will usually have a solid white color channel.
+ .. versionadded:: 20.1
"""
- tile: NDArray[np.uint8] = np.zeros((*self.tile_shape, 4), dtype=np.uint8)
- lib.TCOD_tileset_get_tile_(
- self._tileset_p,
- codepoint,
- ffi.from_buffer("struct TCOD_ColorRGBA*", tile),
+ if codepoint not in self:
+ raise KeyError(codepoint)
+ tile_p = lib.TCOD_tileset_get_tile(self._tileset_p, codepoint)
+ return np.frombuffer(ffi.buffer(tile_p[0 : self.tile_shape[0] * self.tile_shape[1]]), dtype=np.uint8).reshape(
+ *self.tile_shape, 4, copy=True
)
- return tile
- def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
+ def __setitem__(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8], /) -> None:
"""Upload a tile into this array.
Args:
@@ -127,14 +205,14 @@ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
# Normal usage when a tile already has its own alpha channel.
# The loaded tile must be the correct shape for the tileset you assign it to.
# The tile is assigned to a private use area and will not conflict with any exiting codepoint.
- tileset.set_tile(0x100000, imageio.imread("rgba_tile.png"))
+ tileset[0x100000] = imageio.imread("rgba_tile.png")
# Load a greyscale tile.
- tileset.set_tile(0x100001, imageio.imread("greyscale_tile.png"), mode="L")
+ tileset[0x100001] = imageio.imread("greyscale_tile.png", mode="L")
# If you are stuck with an RGB array then you can use the red channel as the input: `rgb[:, :, 0]`
# Loads an RGB sprite without a background.
- tileset.set_tile(0x100002, imageio.imread("rgb_no_background.png", mode="RGBA"))
+ tileset[0x100002] = imageio.imread("rgb_no_background.png", mode="RGBA")
# If you're stuck with an RGB array then you can pad the channel axis with an alpha of 255:
# rgba = np.pad(rgb, pad_width=((0, 0), (0, 0), (0, 1)), constant_values=255)
@@ -147,8 +225,9 @@ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
sprite_alpha = sprite_mask.astype(np.uint8) * 255
# Combine the RGB and alpha arrays into an RGBA array.
sprite_rgba = np.append(sprite_rgb, sprite_alpha, axis=2)
- tileset.set_tile(0x100003, sprite_rgba)
+ tileset[0x100003] = sprite_rgba
+ .. versionadded:: 20.1
"""
tile = np.ascontiguousarray(tile, dtype=np.uint8)
if tile.shape == self.tile_shape:
@@ -173,11 +252,71 @@ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
)
return None
+ def _get_character_map(self) -> NDArray[np.intc]:
+ """Return the internal character mapping as an array.
+
+ This reference will break if the tileset is modified.
+ """
+ return np.frombuffer(
+ ffi.buffer(self._tileset_p.character_map[0 : self._tileset_p.character_map_length]), dtype=np.intc
+ )
+
+ def __delitem__(self, codepoint: int, /) -> None:
+ """Unmap a `codepoint` from this tileset.
+
+ Tilesets are optimized for set-and-forget, deleting a tile may not free up memory.
+
+ .. versionadded:: 20.1
+ """
+ if codepoint not in self:
+ raise KeyError(codepoint)
+ self._get_character_map()[codepoint] = 0
+
+ def __len__(self) -> int:
+ """Return the total count of codepoints assigned in this tileset.
+
+ .. versionadded:: 20.1
+ """
+ return int((self._get_character_map() > 0).sum())
+
+ def __iter__(self) -> Iterator[int]:
+ """Iterate over the assigned codepoints of this tileset.
+
+ .. versionadded:: 20.1
+ """
+ # tolist makes a copy, otherwise the reference to character_map can dangle during iteration
+ for i, v in enumerate(self._get_character_map().tolist()):
+ if v:
+ yield i
+
+ def get_tile(self, codepoint: int) -> NDArray[np.uint8]:
+ """Return a copy of a tile for the given codepoint.
+
+ If the tile does not exist then a blank zero array will be returned.
+
+ The tile will have a shape of (height, width, rgba) and a dtype of
+ uint8. Note that most grey-scale tiles will only use the alpha
+ channel and will usually have a solid white color channel.
+ """
+ try:
+ return self[codepoint]
+ except KeyError:
+ return np.zeros((*self.tile_shape, 4), dtype=np.uint8)
+
+ @deprecated("Assign to a tile using 'tileset[codepoint] = tile' instead.")
+ def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None:
+ """Upload a tile into this array.
+
+ .. deprecated:: 20.1
+ Was replaced with :any:`Tileset.__setitem__`.
+ Use ``tileset[codepoint] = tile`` syntax instead of this method.
+ """
+ self[codepoint] = tile
+
def render(self, console: tcod.console.Console) -> NDArray[np.uint8]:
"""Render an RGBA array, using console with this tileset.
- `console` is the Console object to render, this can not be the root
- console.
+ `console` is the Console object to render, this can not be the root console.
The output array will be a np.uint8 array with the shape of:
``(con_height * tile_height, con_width * tile_width, 4)``.
@@ -222,8 +361,8 @@ def remap(self, codepoint: int, x: int, y: int = 0) -> None:
Large values of `x` will wrap to the next row, so using `x` by itself
is equivalent to `Tile Index` in the :any:`charmap-reference`.
- This is normally used on loaded tilesheets. Other methods of Tileset
- creation won't have reliable tile indexes.
+ This is typically used on tilesets loaded with :any:`load_tilesheet`.
+ Other methods of Tileset creation will not have reliable tile indexes.
.. versionadded:: 11.12
"""
@@ -379,11 +518,23 @@ def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap:
return Tileset._claim(cdata)
-def procedural_block_elements(*, tileset: Tileset) -> None:
- """Overwrite the block element codepoints in `tileset` with procedurally generated glyphs.
+@overload
+@deprecated(
+ "Prefer assigning tiles using dictionary semantics:\n"
+ "'tileset += tcod.tileset.procedural_block_elements(shape=tileset.tile_shape)'"
+)
+def procedural_block_elements(*, tileset: Tileset) -> Tileset: ...
+@overload
+def procedural_block_elements(*, shape: tuple[int, int]) -> Tileset: ...
+
+
+def procedural_block_elements(*, tileset: Tileset | None = None, shape: tuple[int, int] | None = None) -> Tileset:
+ """Generate and return a :any:`Tileset` with procedurally generated block elements.
Args:
- tileset (Tileset): A :any:`Tileset` with tiles of any shape.
+ tileset: A :any:`Tileset` with tiles of any shape. The codepoints of this tileset will be overwritten.
+ This parameter is deprecated and only shape `should` be used.
+ shape: The ``(height, width)`` tile size to generate.
This will overwrite all of the codepoints `listed here `_
except for the shade glyphs.
@@ -393,11 +544,15 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
.. versionadded:: 13.1
+ .. versionchanged:: 20.1
+ Added `shape` parameter, now returns a `Tileset`.
+ `tileset` parameter is deprecated.
+
Example::
>>> import tcod.tileset
>>> tileset = tcod.tileset.Tileset(8, 8)
- >>> tcod.tileset.procedural_block_elements(tileset=tileset)
+ >>> tileset += tcod.tileset.procedural_block_elements(shape=tileset.tile_shape)
>>> tileset.get_tile(0x259E)[:, :, 3] # "▞" Quadrant upper right and lower left.
array([[ 0, 0, 0, 0, 255, 255, 255, 255],
[ 0, 0, 0, 0, 255, 255, 255, 255],
@@ -426,6 +581,9 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
[255, 255, 255, 0, 0, 0, 0, 0],
[255, 255, 255, 0, 0, 0, 0, 0]], dtype=uint8)
"""
+ if tileset is None:
+ assert shape is not None
+ tileset = Tileset(shape[1], shape[0])
quadrants: NDArray[np.uint8] = np.zeros(tileset.tile_shape, dtype=np.uint8)
half_height = tileset.tile_height // 2
half_width = tileset.tile_width // 2
@@ -453,7 +611,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
):
alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8)
alpha *= 255
- tileset.set_tile(codepoint, alpha)
+ tileset[codepoint] = alpha
for codepoint, axis, fraction, negative in (
(0x2581, 0, 7, True), # "▁" Lower one eighth block.
@@ -477,7 +635,8 @@ def procedural_block_elements(*, tileset: Tileset) -> None:
indexes[axis] = slice(divide, None) if negative else slice(None, divide)
alpha = np.zeros(tileset.tile_shape, dtype=np.uint8)
alpha[tuple(indexes)] = 255
- tileset.set_tile(codepoint, alpha)
+ tileset[codepoint] = alpha
+ return tileset
CHARMAP_CP437 = [
diff --git a/tests/conftest.py b/tests/conftest.py
index 182cb6d6..580f16bc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,14 +4,15 @@
import random
import warnings
-from collections.abc import Callable, Iterator
+from typing import TYPE_CHECKING
import pytest
import tcod
from tcod import libtcodpy
-# ruff: noqa: D103
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
def pytest_addoption(parser: pytest.Parser) -> None:
@@ -31,15 +32,15 @@ def uses_window(request: pytest.FixtureRequest) -> Iterator[None]:
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"
- WIDTH = 12
- HEIGHT = 10
- TITLE = "libtcod-cffi tests"
- FULLSCREEN = False
- RENDERER = getattr(libtcodpy, "RENDERER_" + request.param)
-
- libtcodpy.console_set_custom_font(FONT_FILE)
- with libtcodpy.console_init_root(WIDTH, HEIGHT, TITLE, FULLSCREEN, RENDERER, vsync=False) as con:
+ font_file = "libtcod/terminal.png"
+ width = 12
+ height = 10
+ title = "libtcod-cffi tests"
+ fullscreen = False
+ renderer = getattr(libtcodpy, "RENDERER_" + request.param)
+
+ libtcodpy.console_set_custom_font(font_file)
+ with libtcodpy.console_init_root(width, height, title, fullscreen, renderer, vsync=False) as con:
yield con
diff --git a/tests/test_console.py b/tests/test_console.py
index 4b8f8435..873a7691 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -9,14 +9,12 @@
import tcod
import tcod.console
-# ruff: noqa: D103
-
def test_array_read_write() -> None:
console = tcod.console.Console(width=12, height=10)
- FG = (255, 254, 253)
- BG = (1, 2, 3)
- CH = ord("&")
+ FG = (255, 254, 253) # noqa: N806
+ BG = (1, 2, 3) # noqa: N806
+ CH = ord("&") # noqa: N806
with pytest.warns():
tcod.console_put_char_ex(console, 0, 0, CH, FG, BG)
assert console.ch[0, 0] == CH
@@ -76,7 +74,7 @@ def test_console_methods() -> None:
console.print_rect(0, 0, 2, 8, "a b c d e f")
console.get_height_rect(0, 0, 2, 8, "a b c d e f")
with pytest.deprecated_call():
- console.rect(0, 0, 2, 2, True)
+ console.rect(0, 0, 2, 2, True) # noqa: FBT003
with pytest.deprecated_call():
console.hline(0, 1, 10)
with pytest.deprecated_call():
@@ -109,7 +107,7 @@ def test_console_pickle_fortran() -> None:
def test_console_repr() -> None:
- from numpy import array # noqa: F401 # Used for eval
+ from numpy import array # Used for eval # noqa: F401, PLC0415
eval(repr(tcod.console.Console(10, 2))) # noqa: S307
diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py
index 82001d47..4a960bba 100644
--- a/tests/test_deprecated.py
+++ b/tests/test_deprecated.py
@@ -14,8 +14,6 @@
with pytest.warns():
import libtcodpy
-# ruff: noqa: D103
-
def test_deprecate_color() -> None:
with pytest.warns(FutureWarning, match=r"\(0, 0, 0\)"):
diff --git a/tests/test_event.py b/tests/test_event.py
new file mode 100644
index 00000000..9cc28243
--- /dev/null
+++ b/tests/test_event.py
@@ -0,0 +1,73 @@
+"""Tests for event parsing and handling."""
+
+from typing import Any, Final
+
+import pytest
+
+import tcod.event
+import tcod.sdl.sys
+from tcod._internal import _check
+from tcod.cffi import ffi, lib
+from tcod.event import KeySym, Modifier, Scancode
+
+EXPECTED_EVENTS: Final = (
+ tcod.event.Quit(),
+ tcod.event.KeyDown(scancode=Scancode.A, sym=KeySym.A, mod=Modifier(0), pressed=True),
+ tcod.event.KeyUp(scancode=Scancode.A, sym=KeySym.A, mod=Modifier(0), pressed=False),
+)
+"""Events to compare with after passing though the SDL event queue."""
+
+
+def as_sdl_event(event: tcod.event.Event) -> dict[str, dict[str, Any]]:
+ """Convert events into SDL_Event unions using cffi's union format."""
+ match event:
+ case tcod.event.Quit():
+ return {"quit": {"type": lib.SDL_EVENT_QUIT}}
+ case tcod.event.KeyboardEvent():
+ return {
+ "key": {
+ "type": (lib.SDL_EVENT_KEY_UP, lib.SDL_EVENT_KEY_DOWN)[event.pressed],
+ "scancode": event.scancode,
+ "key": event.sym,
+ "mod": event.mod,
+ "down": event.pressed,
+ "repeat": event.repeat,
+ }
+ }
+ raise AssertionError
+
+
+EVENT_PACK: Final = ffi.new("SDL_Event[]", [as_sdl_event(_e) for _e in EXPECTED_EVENTS])
+"""A custom C array of SDL_Event unions based on EXPECTED_EVENTS."""
+
+
+def push_events() -> None:
+ """Reset the SDL event queue to an expected list of events."""
+ tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.EVENTS) # Ensure SDL event queue is enabled
+
+ lib.SDL_PumpEvents() # Clear everything from the queue
+ lib.SDL_FlushEvents(lib.SDL_EVENT_FIRST, lib.SDL_EVENT_LAST)
+
+ assert _check( # Fill the queue with EVENT_PACK
+ lib.SDL_PeepEvents(EVENT_PACK, len(EVENT_PACK), lib.SDL_ADDEVENT, lib.SDL_EVENT_FIRST, lib.SDL_EVENT_LAST)
+ ) == len(EVENT_PACK)
+
+
+def test_get_events() -> None:
+ push_events()
+ assert tuple(tcod.event.get()) == EXPECTED_EVENTS
+
+ assert tuple(tcod.event.get()) == ()
+ assert tuple(tcod.event.wait(timeout=0)) == ()
+
+ push_events()
+ assert tuple(tcod.event.wait()) == EXPECTED_EVENTS
+
+
+def test_event_dispatch() -> None:
+ push_events()
+ with pytest.deprecated_call():
+ tcod.event.EventDispatch().event_wait(timeout=0)
+ push_events()
+ with pytest.deprecated_call():
+ tcod.event.EventDispatch().event_get()
diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py
index 7ace644a..ca1ab1a2 100644
--- a/tests/test_libtcodpy.py
+++ b/tests/test_libtcodpy.py
@@ -11,8 +11,6 @@
import tcod
from tcod import libtcodpy
-# ruff: noqa: D103
-
pytestmark = [
pytest.mark.filterwarnings("ignore::DeprecationWarning"),
pytest.mark.filterwarnings("ignore::PendingDeprecationWarning"),
@@ -25,12 +23,12 @@ def test_console_behavior(console: tcod.console.Console) -> None:
@pytest.mark.skip("takes too long")
@pytest.mark.filterwarnings("ignore")
-def test_credits_long(console: tcod.console.Console) -> None:
+def test_credits_long(console: tcod.console.Console) -> None: # noqa: ARG001
libtcodpy.console_credits()
-def test_credits(console: tcod.console.Console) -> None:
- libtcodpy.console_credits_render(0, 0, True)
+def test_credits(console: tcod.console.Console) -> None: # noqa: ARG001
+ libtcodpy.console_credits_render(0, 0, True) # noqa: FBT003
libtcodpy.console_credits_reset()
@@ -114,7 +112,7 @@ def test_console_printing(console: tcod.console.Console, fg: tuple[int, int, int
@pytest.mark.filterwarnings("ignore")
def test_console_rect(console: tcod.console.Console) -> None:
- libtcodpy.console_rect(console, 0, 0, 4, 4, False, libtcodpy.BKGND_SET)
+ libtcodpy.console_rect(console, 0, 0, 4, 4, False, libtcodpy.BKGND_SET) # noqa: FBT003
@pytest.mark.filterwarnings("ignore")
@@ -129,13 +127,13 @@ def test_console_print_frame(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_console_fade(console: tcod.console.Console) -> None:
+def test_console_fade(console: tcod.console.Console) -> None: # noqa: ARG001
libtcodpy.console_set_fade(0, (0, 0, 0))
libtcodpy.console_get_fade()
libtcodpy.console_get_fading_color()
-def assertConsolesEqual(a: tcod.console.Console, b: tcod.console.Console) -> bool:
+def assert_consoles_equal(a: tcod.console.Console, b: tcod.console.Console, /) -> bool:
return bool((a.fg[:] == b.fg[:]).all() and (a.bg[:] == b.bg[:]).all() and (a.ch[:] == b.ch[:]).all())
@@ -143,7 +141,7 @@ def assertConsolesEqual(a: tcod.console.Console, b: tcod.console.Console) -> boo
def test_console_blit(console: tcod.console.Console, offscreen: tcod.console.Console) -> None:
libtcodpy.console_print(offscreen, 0, 0, "test")
libtcodpy.console_blit(offscreen, 0, 0, 0, 0, console, 0, 0, 1, 1)
- assertConsolesEqual(console, offscreen)
+ assert_consoles_equal(console, offscreen)
libtcodpy.console_set_key_color(offscreen, (0, 0, 0))
@@ -154,7 +152,7 @@ def test_console_asc_read_write(console: tcod.console.Console, offscreen: tcod.c
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)
+ assert_consoles_equal(console, offscreen)
@pytest.mark.filterwarnings("ignore")
@@ -164,11 +162,11 @@ def test_console_apf_read_write(console: tcod.console.Console, offscreen: tcod.c
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)
+ assert_consoles_equal(console, offscreen)
@pytest.mark.filterwarnings("ignore")
-def test_console_rexpaint_load_test_file(console: tcod.console.Console) -> None:
+def test_console_rexpaint_load_test_file(console: tcod.console.Console) -> None: # noqa: ARG001
xp_console = libtcodpy.console_from_xp("libtcod/data/rexpaint/test.xp")
assert xp_console
assert libtcodpy.console_get_char(xp_console, 0, 0) == ord("T")
@@ -192,13 +190,13 @@ def test_console_rexpaint_save_load(
assert libtcodpy.console_save_xp(console, xp_file, 1)
xp_console = libtcodpy.console_from_xp(xp_file)
assert xp_console
- assertConsolesEqual(console, xp_console)
- assert libtcodpy.console_load_xp(None, xp_file) # type: ignore
- assertConsolesEqual(console, xp_console)
+ assert_consoles_equal(console, xp_console)
+ assert libtcodpy.console_load_xp(None, xp_file) # type: ignore[arg-type]
+ assert_consoles_equal(console, xp_console)
@pytest.mark.filterwarnings("ignore")
-def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmp_path: Path) -> None:
+def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmp_path: Path) -> None: # noqa: ARG001
con1 = libtcodpy.console_new(8, 2)
con2 = libtcodpy.console_new(8, 2)
libtcodpy.console_print(con1, 0, 0, "hello")
@@ -208,18 +206,18 @@ def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmp_path
loaded_consoles = libtcodpy.console_list_load_xp(xp_file)
assert loaded_consoles
for a, b in zip([con1, con2], loaded_consoles, strict=True):
- assertConsolesEqual(a, b)
+ assert_consoles_equal(a, b)
libtcodpy.console_delete(a)
libtcodpy.console_delete(b)
@pytest.mark.filterwarnings("ignore")
-def test_console_fullscreen(console: tcod.console.Console) -> None:
- libtcodpy.console_set_fullscreen(False)
+def test_console_fullscreen(console: tcod.console.Console) -> None: # noqa: ARG001
+ libtcodpy.console_set_fullscreen(False) # noqa: FBT003
@pytest.mark.filterwarnings("ignore")
-def test_console_key_input(console: tcod.console.Console) -> None:
+def test_console_key_input(console: tcod.console.Console) -> None: # noqa: ARG001
libtcodpy.console_check_for_keypress()
libtcodpy.console_is_key_pressed(libtcodpy.KEY_ENTER)
@@ -301,15 +299,15 @@ def test_console_buffer_error(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_console_font_mapping(console: tcod.console.Console) -> None:
+def test_console_font_mapping(console: tcod.console.Console) -> None: # noqa: ARG001
libtcodpy.console_map_ascii_code_to_font(ord("@"), 1, 1)
libtcodpy.console_map_ascii_codes_to_font(ord("@"), 1, 0, 0)
libtcodpy.console_map_string_to_font("@", 0, 0)
@pytest.mark.filterwarnings("ignore")
-def test_mouse(console: tcod.console.Console) -> None:
- libtcodpy.mouse_show_cursor(True)
+def test_mouse(console: tcod.console.Console) -> None: # noqa: ARG001
+ libtcodpy.mouse_show_cursor(True) # noqa: FBT003
libtcodpy.mouse_is_cursor_visible()
mouse = libtcodpy.mouse_get_status()
repr(mouse)
@@ -317,7 +315,7 @@ def test_mouse(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_sys_time(console: tcod.console.Console) -> None:
+def test_sys_time(console: tcod.console.Console) -> None: # noqa: ARG001
libtcodpy.sys_set_fps(0)
libtcodpy.sys_get_fps()
libtcodpy.sys_get_last_frame_length()
@@ -327,18 +325,18 @@ def test_sys_time(console: tcod.console.Console) -> None:
@pytest.mark.filterwarnings("ignore")
-def test_sys_screenshot(console: tcod.console.Console, tmp_path: Path) -> None:
+def test_sys_screenshot(console: tcod.console.Console, tmp_path: Path) -> None: # noqa: ARG001
libtcodpy.sys_save_screenshot(tmp_path / "test.png")
@pytest.mark.filterwarnings("ignore")
-def test_sys_custom_render(console: tcod.console.Console) -> None:
+def test_sys_custom_render(console: tcod.console.Console) -> None: # noqa: ARG001
if libtcodpy.sys_get_renderer() != libtcodpy.RENDERER_SDL:
pytest.xfail(reason="Only supports SDL")
escape = []
- def sdl_callback(sdl_surface: object) -> None:
+ def sdl_callback(_sdl_surface: object) -> None:
escape.append(True)
libtcodpy.sys_register_SDL_renderer(sdl_callback)
@@ -378,7 +376,7 @@ def test_image(console: tcod.console.Console, tmp_path: Path) -> None:
@pytest.mark.parametrize("sample", ["@", "\u2603"]) # Unicode snowman
@pytest.mark.xfail(reason="Unreliable")
@pytest.mark.filterwarnings("ignore")
-def test_clipboard(console: tcod.console.Console, sample: str) -> None:
+def test_clipboard(console: tcod.console.Console, sample: str) -> None: # noqa: ARG001
saved = libtcodpy.sys_clipboard_get()
try:
libtcodpy.sys_clipboard_set(sample)
@@ -459,9 +457,9 @@ def test_bsp() -> None:
assert libtcodpy.bsp_find_node(bsp, 1, 1) == bsp
assert not libtcodpy.bsp_find_node(bsp, -1, -1)
- libtcodpy.bsp_split_once(bsp, False, 4)
+ libtcodpy.bsp_split_once(bsp, False, 4) # noqa: FBT003
repr(bsp) # test __repr__ with parent
- libtcodpy.bsp_split_once(bsp, True, 4)
+ libtcodpy.bsp_split_once(bsp, True, 4) # noqa: FBT003
repr(bsp)
# cover functions on parent
@@ -475,7 +473,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: object) -> None:
+ def traverse(_node: tcod.bsp.BSP, _user_data: object) -> None:
return None
libtcodpy.bsp_traverse_pre_order(bsp, traverse)
@@ -494,13 +492,13 @@ def traverse(node: tcod.bsp.BSP, user_data: object) -> None:
@pytest.mark.filterwarnings("ignore")
def test_map() -> None:
- WIDTH, HEIGHT = 13, 17
- map = libtcodpy.map_new(WIDTH, HEIGHT)
+ WIDTH, HEIGHT = 13, 17 # noqa: N806
+ map = libtcodpy.map_new(WIDTH, HEIGHT) # noqa: A001
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)
+ libtcodpy.map_set_properties(map, 0, 0, True, True) # noqa: FBT003
assert libtcodpy.map_is_transparent(map, 0, 0)
assert libtcodpy.map_is_walkable(map, 0, 0)
libtcodpy.map_is_in_fov(map, 0, 0)
@@ -524,14 +522,14 @@ def test_color() -> None:
color_b = libtcodpy.Color(255, 255, 255)
assert color_a != color_b
- color = libtcodpy.color_lerp(color_a, color_b, 0.5) # type: ignore
+ color = libtcodpy.color_lerp(color_a, color_b, 0.5) # type: ignore[arg-type]
libtcodpy.color_set_hsv(color, 0, 0, 0)
- libtcodpy.color_get_hsv(color) # type: ignore
+ libtcodpy.color_get_hsv(color) # type: ignore[arg-type]
libtcodpy.color_scale_HSV(color, 0, 0)
def test_color_repr() -> None:
- Color = libtcodpy.Color
+ Color = libtcodpy.Color # noqa: N806
col = Color(0, 1, 2)
assert eval(repr(col)) == col # noqa: S307
@@ -670,7 +668,7 @@ def map_() -> Iterator[tcod.map.Map]:
@pytest.fixture
def path_callback(map_: tcod.map.Map) -> Callable[[int, int, int, int, None], bool]:
- def callback(ox: int, oy: int, dx: int, dy: int, user_data: None) -> bool:
+ def callback(_ox: int, _oy: int, dx: int, dy: int, _user_data: None) -> bool:
return bool(map_.walkable[dy, dx])
return callback
@@ -705,7 +703,7 @@ def test_astar(map_: tcod.map.Map) -> None:
x, y = libtcodpy.path_get(astar, i)
while (x, y) != (None, None):
- x, y = libtcodpy.path_walk(astar, False)
+ x, y = libtcodpy.path_walk(astar, False) # noqa: FBT003
libtcodpy.path_delete(astar)
diff --git a/tests/test_noise.py b/tests/test_noise.py
index 80023f5f..28825328 100644
--- a/tests/test_noise.py
+++ b/tests/test_noise.py
@@ -9,8 +9,6 @@
import tcod.noise
import tcod.random
-# ruff: noqa: D103
-
@pytest.mark.parametrize("implementation", tcod.noise.Implementation)
@pytest.mark.parametrize("algorithm", tcod.noise.Algorithm)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 5494b786..67cd10e3 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -7,27 +7,25 @@
import tcod as libtcod
-# ruff: noqa: D103
-
@pytest.mark.filterwarnings("ignore")
def test_parser() -> None:
print("***** File Parser test *****")
parser = libtcod.parser_new()
struct = libtcod.parser_new_struct(parser, "myStruct")
- libtcod.struct_add_property(struct, "bool_field", libtcod.TYPE_BOOL, True)
- libtcod.struct_add_property(struct, "char_field", libtcod.TYPE_CHAR, True)
- libtcod.struct_add_property(struct, "int_field", libtcod.TYPE_INT, True)
- libtcod.struct_add_property(struct, "float_field", libtcod.TYPE_FLOAT, True)
- libtcod.struct_add_property(struct, "color_field", libtcod.TYPE_COLOR, True)
- libtcod.struct_add_property(struct, "dice_field", libtcod.TYPE_DICE, True)
- libtcod.struct_add_property(struct, "string_field", libtcod.TYPE_STRING, True)
- libtcod.struct_add_list_property(struct, "bool_list", libtcod.TYPE_BOOL, True)
- libtcod.struct_add_list_property(struct, "char_list", libtcod.TYPE_CHAR, True)
- libtcod.struct_add_list_property(struct, "integer_list", libtcod.TYPE_INT, True)
- libtcod.struct_add_list_property(struct, "float_list", libtcod.TYPE_FLOAT, True)
- libtcod.struct_add_list_property(struct, "string_list", libtcod.TYPE_STRING, True)
- libtcod.struct_add_list_property(struct, "color_list", libtcod.TYPE_COLOR, True)
+ libtcod.struct_add_property(struct, "bool_field", libtcod.TYPE_BOOL, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "char_field", libtcod.TYPE_CHAR, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "int_field", libtcod.TYPE_INT, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "float_field", libtcod.TYPE_FLOAT, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "color_field", libtcod.TYPE_COLOR, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "dice_field", libtcod.TYPE_DICE, True) # noqa: FBT003
+ libtcod.struct_add_property(struct, "string_field", libtcod.TYPE_STRING, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "bool_list", libtcod.TYPE_BOOL, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "char_list", libtcod.TYPE_CHAR, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "integer_list", libtcod.TYPE_INT, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "float_list", libtcod.TYPE_FLOAT, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "string_list", libtcod.TYPE_STRING, True) # noqa: FBT003
+ libtcod.struct_add_list_property(struct, "color_list", libtcod.TYPE_COLOR, True) # noqa: FBT003
# default listener
print("***** Default listener *****")
diff --git a/tests/test_random.py b/tests/test_random.py
index d20045cc..764ae988 100644
--- a/tests/test_random.py
+++ b/tests/test_random.py
@@ -8,8 +8,6 @@
import tcod.random
-# ruff: noqa: D103
-
SCRIPT_DIR = Path(__file__).parent
diff --git a/tests/test_sdl.py b/tests/test_sdl.py
index f33f4738..42433510 100644
--- a/tests/test_sdl.py
+++ b/tests/test_sdl.py
@@ -9,10 +9,8 @@
import tcod.sdl.sys
import tcod.sdl.video
-# ruff: noqa: D103
-
-def test_sdl_window(uses_window: None) -> None:
+def test_sdl_window(uses_window: None) -> None: # noqa: ARG001
assert tcod.sdl.video.get_grabbed_window() is None
window = tcod.sdl.video.new_window(1, 1)
window.raise_window()
@@ -40,6 +38,10 @@ def test_sdl_window(uses_window: None) -> None:
window.opacity = window.opacity
window.grab = window.grab
+ window.start_text_input(capitalization=tcod.sdl.video.Capitalization.NONE, multiline=False)
+ window.set_text_input_area((0, 0, 8, 8), 0)
+ window.stop_text_input()
+
def test_sdl_window_bad_types() -> None:
with pytest.raises(TypeError):
@@ -48,14 +50,14 @@ def test_sdl_window_bad_types() -> None:
tcod.sdl.video.Window(tcod.ffi.new("SDL_Rect*"))
-def test_sdl_screen_saver(uses_window: None) -> None:
+def test_sdl_screen_saver(uses_window: None) -> None: # noqa: ARG001
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.VIDEO)
- 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(False) is False # noqa: FBT003
+ assert tcod.sdl.video.screen_saver_allowed(True) is True # noqa: FBT003
assert tcod.sdl.video.screen_saver_allowed() is True
-def test_sdl_render(uses_window: None) -> None:
+def test_sdl_render(uses_window: None) -> None: # noqa: ARG001
window = tcod.sdl.video.new_window(4, 4)
render = tcod.sdl.render.new_renderer(window, driver="software", vsync=False)
render.clear()
@@ -68,6 +70,7 @@ def test_sdl_render(uses_window: None) -> None:
rgb.alpha_mod = rgb.alpha_mod
rgb.blend_mode = rgb.blend_mode
rgb.color_mod = rgb.color_mod
+ rgb.scale_mode = rgb.scale_mode
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)
diff --git a/tests/test_sdl_audio.py b/tests/test_sdl_audio.py
index f8da6155..94bd151e 100644
--- a/tests/test_sdl_audio.py
+++ b/tests/test_sdl_audio.py
@@ -12,8 +12,6 @@
import tcod.sdl.audio
-# ruff: noqa: D103
-
def device_works(device: Callable[[], tcod.sdl.audio.AudioDevice]) -> bool:
try:
@@ -116,6 +114,7 @@ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) ->
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Needs sys.unraisablehook support")
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
+@pytest.mark.skip(reason="Unsupported, causes too many issues")
@needs_audio_device
def test_audio_callback_unraisable() -> None:
"""Test unraisable error in audio callback.
@@ -126,9 +125,9 @@ def test_audio_callback_unraisable() -> None:
class CheckCalled:
was_called: bool = False
- def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None:
+ def __call__(self, _device: tcod.sdl.audio.AudioDevice, _stream: NDArray[Any]) -> None:
self.was_called = True
- raise Exception("Test unraisable error") # noqa
+ raise Exception("Test unraisable error") # noqa: EM101, TRY002, TRY003
check_called = CheckCalled()
with tcod.sdl.audio.open(callback=check_called, paused=False) as device:
diff --git a/tests/test_tcod.py b/tests/test_tcod.py
index 25f60d0e..3f8c1d9b 100644
--- a/tests/test_tcod.py
+++ b/tests/test_tcod.py
@@ -9,20 +9,23 @@
from numpy.typing import DTypeLike, NDArray
import tcod
+import tcod.bsp
import tcod.console
+import tcod.context
+import tcod.map
+import tcod.path
+import tcod.random
from tcod import libtcodpy
-# ruff: noqa: D103
-
-def raise_Exception(*_args: object) -> NoReturn:
+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), pytest.warns():
- tcod.line(0, 0, 10, 10, py_callback=raise_Exception)
+ libtcodpy.line(0, 0, 10, 10, py_callback=raise_exception)
@pytest.mark.filterwarnings("ignore:Iterate over nodes using")
@@ -36,7 +39,7 @@ def test_tcod_bsp() -> None:
assert not bsp.children
with pytest.raises(RuntimeError):
- libtcodpy.bsp_traverse_pre_order(bsp, raise_Exception)
+ libtcodpy.bsp_traverse_pre_order(bsp, raise_exception)
bsp.split_recursive(3, 4, 4, 1, 1)
for node in bsp.walk():
@@ -46,7 +49,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)
+ sub_bsp.split_recursive(3, 2, 2, 1, 1, seed=tcod.random.Random(seed=42))
assert sub_bsp.children[0].level == 2 # noqa: PLR2004
# cover find_node method
@@ -99,7 +102,7 @@ def test_tcod_map_pickle() -> None:
def test_tcod_map_pickle_fortran() -> None:
map_ = tcod.map.Map(2, 3, order="F")
map2: tcod.map.Map = pickle.loads(pickle.dumps(copy.copy(map_)))
- assert map_._Map__buffer.strides == map2._Map__buffer.strides # type: ignore
+ assert map_._buffer.strides == map2._buffer.strides
assert map_.transparent.strides == map2.transparent.strides
assert map_.walkable.strides == map2.walkable.strides
assert map_.fov.strides == map2.fov.strides
@@ -145,7 +148,7 @@ def test_path_numpy(dtype: DTypeLike) -> None:
tcod.path.AStar(np.ones((2, 2), dtype=np.float64))
-def path_cost(this_x: int, this_y: int, dest_x: int, dest_y: int) -> bool:
+def path_cost(_this_x: int, _this_y: int, _dest_x: int, _dest_y: int) -> bool:
return True
@@ -157,7 +160,7 @@ def test_path_callback() -> None:
def test_key_repr() -> None:
- Key = libtcodpy.Key
+ Key = libtcodpy.Key # noqa: N806
key = Key(vk=1, c=2, shift=True)
assert key.vk == 1
assert key.c == 2 # noqa: PLR2004
@@ -169,7 +172,7 @@ def test_key_repr() -> None:
def test_mouse_repr() -> None:
- Mouse = libtcodpy.Mouse
+ Mouse = libtcodpy.Mouse # noqa: N806
mouse = Mouse(x=1, lbutton=True)
mouse_copy = eval(repr(mouse)) # noqa: S307
assert mouse.x == mouse_copy.x
@@ -182,16 +185,16 @@ def test_cffi_structs() -> None:
@pytest.mark.filterwarnings("ignore")
-def test_recommended_size(console: tcod.console.Console) -> None:
+def test_recommended_size(console: tcod.console.Console) -> None: # noqa: ARG001
tcod.console.recommended_size()
@pytest.mark.filterwarnings("ignore")
-def test_context(uses_window: None) -> None:
+def test_context(uses_window: None) -> None: # noqa: ARG001
with tcod.context.new_window(32, 32, renderer=libtcodpy.RENDERER_SDL2):
pass
- WIDTH, HEIGHT = 16, 4
- with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=libtcodpy.RENDERER_SDL2) as context:
+ width, height = 16, 4
+ with tcod.context.new_terminal(columns=width, rows=height, renderer=libtcodpy.RENDERER_SDL2) as context:
console = tcod.console.Console(*context.recommended_console_size())
context.present(console)
assert context.sdl_window_p is not None
@@ -199,5 +202,18 @@ def test_context(uses_window: None) -> None:
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"):
+ with pytest.raises(RuntimeError, match=r".*context has been closed"):
context.present(console)
+
+
+def test_event_watch() -> None:
+ def handle_events(_event: tcod.event.Event) -> None:
+ pass
+
+ tcod.event.add_watch(handle_events)
+ with pytest.warns(RuntimeWarning, match=r"nothing was added"):
+ tcod.event.add_watch(handle_events)
+
+ tcod.event.remove_watch(handle_events)
+ with pytest.warns(RuntimeWarning, match=r"nothing was removed"):
+ tcod.event.remove_watch(handle_events)
diff --git a/tests/test_tileset.py b/tests/test_tileset.py
index dd272fe7..78b458dd 100644
--- a/tests/test_tileset.py
+++ b/tests/test_tileset.py
@@ -1,12 +1,103 @@
"""Test for tcod.tileset module."""
+from pathlib import Path
+
+import pytest
+
+import tcod.console
import tcod.tileset
-# ruff: noqa: D103
+PROJECT_DIR = Path(__file__).parent / ".."
+
+TERMINAL_FONT = PROJECT_DIR / "fonts/libtcod/terminal8x8_aa_ro.png"
+BDF_FONT = PROJECT_DIR / "libtcod/data/fonts/Tamzen5x9r.bdf"
+
+BAD_FILE = PROJECT_DIR / "CHANGELOG.md" # Any existing non-font file
def test_proc_block_elements() -> None:
- tileset = tcod.tileset.Tileset(8, 8)
- tcod.tileset.procedural_block_elements(tileset=tileset)
tileset = tcod.tileset.Tileset(0, 0)
- tcod.tileset.procedural_block_elements(tileset=tileset)
+ with pytest.deprecated_call():
+ tcod.tileset.procedural_block_elements(tileset=tileset)
+ tileset += tcod.tileset.procedural_block_elements(shape=tileset.tile_shape)
+
+ tileset = tcod.tileset.Tileset(8, 8)
+ with pytest.deprecated_call():
+ tcod.tileset.procedural_block_elements(tileset=tileset)
+ tileset += tcod.tileset.procedural_block_elements(shape=tileset.tile_shape)
+
+
+def test_tileset_mix() -> None:
+ tileset1 = tcod.tileset.Tileset(1, 1)
+ tileset1[ord("a")] = [[0]]
+
+ tileset2 = tcod.tileset.Tileset(1, 1)
+ tileset2[ord("a")] = [[1]]
+ tileset2[ord("b")] = [[1]]
+
+ assert (tileset1 + tileset2)[ord("a")].tolist() == [[[255, 255, 255, 1]]] # Replaces tile
+ assert (tileset1 | tileset2)[ord("a")].tolist() == [[[255, 255, 255, 0]]] # Skips existing tile
+
+
+def test_tileset_contains() -> None:
+ tileset = tcod.tileset.Tileset(1, 1)
+
+ # Missing keys
+ assert None not in tileset
+ assert ord("x") not in tileset
+ assert -1 not in tileset
+ with pytest.raises(KeyError, match=rf"{ord('x')}"):
+ tileset[ord("x")]
+ with pytest.raises(KeyError, match=rf"{ord('x')}"):
+ del tileset[ord("x")]
+ assert len(tileset) == 0
+
+ # Assigned tile is found
+ tileset[ord("x")] = [[255]]
+ assert ord("x") in tileset
+ assert len(tileset) == 1
+
+ # Can be deleted and reassigned
+ del tileset[ord("x")]
+ assert ord("x") not in tileset
+ assert len(tileset) == 0
+ tileset[ord("x")] = [[255]]
+ assert ord("x") in tileset
+ assert len(tileset) == 1
+
+
+def test_tileset_assignment() -> None:
+ tileset = tcod.tileset.Tileset(1, 2)
+ tileset[ord("a")] = [[1], [1]]
+ tileset[ord("b")] = [[[255, 255, 255, 2]], [[255, 255, 255, 2]]]
+
+ with pytest.raises(ValueError, match=r".*must be \(2, 1, 4\) or \(2, 1\), got \(2, 1, 3\)"):
+ tileset[ord("c")] = [[[255, 255, 255]], [[255, 255, 255]]]
+
+ assert tileset.get_tile(ord("d")).shape == (2, 1, 4)
+
+
+def test_tileset_render() -> None:
+ tileset = tcod.tileset.Tileset(1, 2)
+ tileset[ord("x")] = [[255], [0]]
+ console = tcod.console.Console(3, 2)
+ console.rgb[0, 0] = (ord("x"), (255, 0, 0), (0, 255, 0))
+ output = tileset.render(console)
+ assert output.shape == (4, 3, 4)
+ assert output[0:2, 0].tolist() == [[255, 0, 0, 255], [0, 255, 0, 255]]
+
+
+def test_tileset_tilesheet() -> None:
+ tileset = tcod.tileset.load_tilesheet(TERMINAL_FONT, 16, 16, tcod.tileset.CHARMAP_CP437)
+ assert tileset.tile_shape == (8, 8)
+
+ with pytest.raises(RuntimeError):
+ tcod.tileset.load_tilesheet(BAD_FILE, 16, 16, tcod.tileset.CHARMAP_CP437)
+
+
+def test_tileset_bdf() -> None:
+ tileset = tcod.tileset.load_bdf(BDF_FONT)
+ assert tileset.tile_shape == (9, 5)
+
+ with pytest.raises(RuntimeError):
+ tileset = tcod.tileset.load_bdf(BAD_FILE)