playscii - ascii art and game creation tool (v9.18)

desktop GUI app using SDL2 + OpenGL. NOT a web app, no server component.
originally from https://heptapod.host/jp-lebreton/playscii (mercurial).
we maintain our own git repo.


running
-------
  just run          launch the app (uv run python -m playscii)
  just check        lint gate (ruff check + format)
  just lint         ruff check --fix + ruff format
  just typecheck    ty check (601 errors, legacy code, not in check gate)
  just test         pytest (no tests yet, not in check gate)


project structure
-----------------
  python source organized as playscii/ package (~50 .py files)
  data directories (charsets/, palettes/, etc.) live at repo root
  formats/ and games/ also at root (loaded dynamically via importlib)

  playscii/
    __init__.py     package marker
    __main__.py     entry point for python -m playscii
    app.py          main application, SDL2/OpenGL setup (was playscii.py)
    art.py          art/canvas management, the core data model
    ui*.py          ~20 UI modules (dialogs, panels, menus, toolbar, etc.)
    game_*.py       game engine (objects, rooms, world, HUD)
    renderable*.py  rendering pipeline (sprites, lines, framebuffer)

  formats/          import/export handlers (ANS, ATA, BMP, EDSCII, PNG, GIF, TXT)
  games/            bundled example games (crawler, fireplace, flood, maze, shmup, etc.)
  charsets/         30+ classic computer character sets (.char + .png)
  palettes/         color palette definitions
  shaders/          GLSL shader files
  artscripts/       art animation scripts (.arsc files)
  art/              sample ASCII art (.psci files)
  docs/             documentation and design notes
  ui/               UI image resources


dependencies
------------
  runtime: appdirs, numpy, Pillow, PyOpenGL, PySDL2, packaging
  system:  libSDL2, libSDL2_mixer (must be installed on the OS)
  dev:     ruff, pytest


gotchas
-------
  package structure:
    all python source now lives in playscii/ package. files use relative
    imports (from .art import ...). formats/ and games/ use absolute imports
    (from playscii.art import ...). data directories stay at root since the
    app uses CWD-relative paths to load charsets, shaders, etc.

  art scripts and exec():
    art.py imports random at module level with a noqa comment. art scripts
    (.arsc files) are loaded via exec() in art.py's scope. they use random
    without importing it themselves. do NOT remove that import even though
    ruff flags it as unused. same pattern may apply to other "unused" imports
    in art.py — check before removing anything.

  pdoc detection:
    app.py has a contextlib.suppress block that tries to import pdoc.
    the import MUST stay inside the suppress block. if it gets moved or
    removed, pdoc_available becomes unconditionally True and the help menu
    breaks when pdoc isn't installed.

  SDL2 init ordering:
    many modules import from app.py's namespace or expect SDL2 to be
    initialized before they run. import order in app.py matters — E402
    is disabled for this reason.

  mutable default arguments:
    B006 is disabled. many functions use mutable defaults (lists, dicts).
    changing these could alter behavior since some code may rely on the
    shared mutable state. do not "fix" these without testing.

  variable naming:
    E741 is disabled. single-letter vars (l, x, y, z, i, r, g, b) are
    everywhere in the math/rendering/game code. this is intentional.


ruff config
-----------
  rules: E, F, I, UP, B, SIM
  13 rules are explicitly ignored (see pyproject.toml for rationale)
  line-length: 88, formatter handles what it can

  when adding new code, write it clean. the ignored rules are concessions
  to legacy code, not a style guide for new work.
