{"id":31386940,"url":"https://github.com/avidrucker/pycats","last_synced_at":"2026-07-19T16:31:56.269Z","repository":{"id":301002086,"uuid":"1006295599","full_name":"avidrucker/pycats","owner":"avidrucker","description":null,"archived":false,"fork":false,"pushed_at":"2026-07-16T05:40:49.000Z","size":2852,"stargazers_count":1,"open_issues_count":94,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T07:12:26.158Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/avidrucker.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-06-21T23:52:30.000Z","updated_at":"2026-07-16T05:40:53.000Z","dependencies_parsed_at":"2025-06-24T17:41:20.919Z","dependency_job_id":"28f20289-bf2b-4c9d-879c-75b4021f1d0d","html_url":"https://github.com/avidrucker/pycats","commit_stats":null,"previous_names":["avidrucker/pycats"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/avidrucker/pycats","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avidrucker%2Fpycats","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avidrucker%2Fpycats/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avidrucker%2Fpycats/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avidrucker%2Fpycats/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/avidrucker","download_url":"https://codeload.github.com/avidrucker/pycats/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avidrucker%2Fpycats/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35659409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-19T02:00:06.923Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-28T20:59:16.001Z","updated_at":"2026-07-19T16:31:56.261Z","avatar_url":"https://github.com/avidrucker.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PyCats\n\nA local 2-player cat fighter for the keyboard, written in Python with Pygame and\ninspired by **Project M** (the *Super Smash Bros. Brawl* mod). Two cats knock each\nother off a stage; damage builds knockback, and a launched fighter that flies past\nthe blast line loses a stock.\n\n## What it is (and isn't)\n\npycats is a **personal learning project**, so its goals are shaped by that:\n\n- **Project-M-at-heart, not a clone.** The *feel* — movement, knockback, shield,\n  dodges, ledges — follows Project M; where pycats deliberately diverges is written\n  down in [docs/project-m-parity.md](./docs/project-m-parity.md).\n- **Deterministic and headless-first.** The simulation is frame-counted, RNG-free at\n  its core, and runs without a display, so recorded \"golden\" snapshots reproduce a\n  fight exactly and act as the regression oracle. This constraint is load-bearing —\n  see the determinism/headless contract in [CONTEXT.md](./CONTEXT.md).\n- **Local, keyboard, two players.** Two humans on one keyboard, or a human against a\n  computer-controlled cat (see [Play against the computer](#play-against-the-computer)).\n\n**Not** goals of this project: online / netplay, and a large character roster.\nPlay is keyboard-first; a game-controller backend is only exploratory, not part of\nthe current game.\n\n## Quickstart\n\nRequires Python 3.10+. These steps use a project virtualenv (`.venv`), which works\neverywhere — including Debian/Ubuntu/Mint, where a bare `pip install` is refused with\n`externally-managed-environment` (PEP 668).\n\n```bash\ngit clone https://github.com/avidrucker/pycats.git\ncd pycats\npython3 -m venv .venv\n.venv/bin/python -m pip install -r requirements.txt   # pygame-ce + the statechart engine\nmake run                                               # play (see `make help` for all dev commands)\n```\n\n`requirements.txt` pulls both runtime dependencies: **pygame-ce**, and **statecharts**\n— the statechart engine that drives every fighter and screen (ADR-0002). The engine\nisn't on PyPI, so it's pinned there to a git release tag; nothing else to clone.\n\n(If your `python` already points at a writable environment, you can skip the `.venv`\nand drop the `.venv/bin/` prefixes.)\n\n## Controls\n\nTwo players share the keyboard. Every key below is **rebindable in the in-game Options\nscreen** (the defaults are what \"reset to defaults\" restores).\n\n| Action  | Player 1 | Player 2 |\n| ------- | -------- | -------- |\n| Move    | `W` `A` `S` `D` | Arrow keys |\n| Attack  | `V` | `/` |\n| Special | `C` | `.` |\n| Shield  | `X` | `,` |\n| Smash   | `B` | `'` |\n\nJump is *up* (`W` / `Up`); hold *down* to crouch and to drop through / off ledges.\nAttack is the standard A-button; **Special** is the B-button (e.g. Nalio's fireball);\n**Smash** is a dedicated strong-attack input.\n\n## Play against the computer\n\nA cat can be driven by the computer along **two independent axes** — don't confuse them:\n\n**CPU difficulty — `--p1-level` / `--p2-level`, a level from 1 to 9.** This is the\nSmash-style **CPU** opponent: higher levels react faster, attack and shield more, and\nat the top levels throw specials (fireballs). Turning on a level makes that player a\nCPU.\n\n```bash\n# Two Nalios: a level-5 CPU (P1) versus a level-9 CPU (P2), reproducible with a seed.\n.venv/bin/python watch.py --p1-char nalio --p1-level 5 --p2-char nalio --p2-level 9 --seed 42\n```\n\n**Behavior variants — `--vs {idle,chase,idler,follower}`.** A *separate* axis: these\nare scripted-controller personalities for P2 (idle = no controller, chase = pursue,\nidler = baseline, follower = shadow P1), used for demos and testing. They are **not**\nCPU difficulty levels — a \"follower\" is not \"harder\" than an \"idler\", just different.\n\n```bash\n.venv/bin/python watch.py --vs chase --seed 42   # P1 attacker vs a P2 that chases\n```\n\nOther `watch.py` modes: `--match` plays a full match to a KO, `--demo showcase` plays\nthe captioned feature demo, and `--video out.mp4` records instead of showing a live\nwindow (recording needs `imageio` + `imageio-ffmpeg`). Run `watch.py --help` for the\nfull flag list. Omit `--seed` for a clocktime seed so a computer match varies\nrun-to-run; pass an int for a reproducible one.\n\n---\n\n## For contributors\n\nCommon dev commands run through the root `Makefile` — the command SSOT (#724). `make help`\nlists them, and each resolves the project `.venv` automatically (including from a `git`\nworktree, which has no local `.venv`).\n\n### Running the tests\n\nThe suite runs headless (no display) and is the regression oracle:\n\n```bash\nmake test                          # full suite, headless\nmake test ARGS=\"-m 'not slow'\"     # skip the benchmark tests\nmake test ARGS=\"-k dodge\"          # run a subset (any pytest args pass through)\n```\n\nA green run with some skips is expected. Golden snapshots live in `tests/golden/`;\nregenerate them intentionally with `PYCATS_UPDATE_GOLDENS=1`.\n\n### Development setup\n\nThe tests, linter, benchmark, and video recording need a few more packages, in\n`requirements-dev.txt` (pytest, ruff, pre-commit, imageio) — install alongside the\nruntime deps (see [ADR-0006](./docs/adr/) for the lint setup):\n\n```bash\n.venv/bin/python -m pip install -r requirements.txt -r requirements-dev.txt\n```\n\n**Hacking on the engine?** To develop `statecharts-py` alongside pycats, clone it as a\nsibling and install it editable *instead of* the pinned line in `requirements.txt`\n(see [ADR-0002](./docs/adr/) for why it is the sole state engine):\n\n```bash\ngit clone https://github.com/avidrucker/statecharts-py.git   # ../statecharts-py\n.venv/bin/python -m pip install -e ../statecharts-py          # overrides the pinned tag\n```\n\nLint + format (ruff; config in `ruff.toml` and `.pre-commit-config.yaml`):\n\n```bash\nmake lint                          # ruff format --check + ruff check on pycats/ (the close-gate)\nmake format                        # apply ruff formatting (write-twin of lint's --check)\n.venv/bin/pre-commit install       # one-time: run both on each commit\n```\n\nThe lint hook is ruff-only so it stays fast; `pytest` remains the on-demand source of\ntruth (there is no CI gate). Legacy debug scripts that once masqueraded as tests now\nlive in `scripts/`, so a bare `pytest` collects only real assert-based tests.\n\n### Benchmark\n\n```bash\nmake bench                                                       # quick run\nmake bench ARGS=\"--frames 20000 --json bench_results/run.json\"   # longer, with JSON output\n```\n\n### Project layout\n\nThe `pycats/` package is split into a **display-free rules core** and a separate\n**present layer** (rendering, input polling, `game.py`). The core sub-packages:\n\n| Package | Holds |\n| ------- | ----- |\n| `pycats/core/` | input frames, rebindable keymaps, low-level physics |\n| `pycats/entities/` | `Fighter` (pure state/rules), the `Player` sprite adapter, platforms, ledges |\n| `pycats/combat/` | the data-driven attack system — hitboxes, knockback, shield, move selection |\n| `pycats/characters/` | per-archetype fighter data + skins (Nalio, Birky, Narz, the default cat) |\n| `pycats/charts/` + `pycats/systems/` | the statechart definitions and state engines |\n| `pycats/sim/` | the headless battle runner, AI controllers, demos, and the captioned showcase |\n\nEntry points are modules, **not** a `main.py`: `python -m pycats.game` (the game),\n`watch.py` (replays / demos / CPU battles), and `bench.py` (benchmark). The\narchitecture layer map lives in [CONTEXT.md](./CONTEXT.md).\n\n## Project docs\n\nNew here (human or agent)? Start with these:\n\n- [CONTEXT.md](./CONTEXT.md) — domain vocabulary + the determinism/headless contract.\n- [docs/glossary.md](./docs/glossary.md) — one-line definitions of every PM/Smash mechanic + project term, linked to the authoritative doc.\n- [docs/adr/](./docs/adr/) — architecture decision records (the *why* behind design calls).\n- [docs/project-m-parity.md](./docs/project-m-parity.md) — where pycats deliberately diverges from Project M.\n- [docs/pygame-fonts.md](./docs/pygame-fonts.md) — working with the font/text stack: sizes, scaling, mixed text, per-frame `SysFont` and test-isolation gotchas.\n- [RULES.md](./RULES.md) — project conventions (labels, filing, closing work).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favidrucker%2Fpycats","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Favidrucker%2Fpycats","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favidrucker%2Fpycats/lists"}