gamekit
The game-logic half of a game engine, with no window required.
gamekit is a small, pure-Python toolkit for the parts of a game that aren't graphics: an entity-component-system, tile grids with A* pathfinding, a fixed-timestep game loop with timers, and action-based input mapping. It has zero dependencies and never opens a display, so your game logic runs the same in a terminal, a test suite, a server, or under whatever renderer you bolt on later (curses, pygame, a web front end...).
Features
- ECS (
World): integer entity ids, components are plain objects (dataclasses work well), typedquery()over component combinations, ordered systems, and deferreddestroy()so it's safe to remove entities while iterating. - Grids & pathfinding (
Grid): build maps from ASCII art, 4-way neighbours, flood-fill reachability, A* shortest paths, and text rendering with overlays. - Fixed-timestep loop (
GameLoop): frame-rate-independent updates, an accumulator with a "spiral of death" cap, render interpolationalpha, headlessrun(ticks)for tests, andrun_realtime()with an injectable clock. - Timers (
Timer,GameLoop.every/after): one-shot and repeating timers in simulated time, with exact catch-up when a big step overshoots. - Input mapping (
InputMap): bind any hashable key to named actions; query held / just-pressed / just-released and two-action axes. Backend-agnostic. Vec2: immutable, hashable 2D vector usable as a dict key.- Fully type-hinted (
py.typed), standard library only, Python 3.10+.
Install
From a checkout:
python3 -m venv .venv && . .venv/bin/activate
pip install -e .
Quickstart
from dataclasses import dataclass
from gamekit import GameLoop, Grid, Vec2, World
@dataclass
class Pos:
at: Vec2
@dataclass
class Seeker:
goal: Vec2
grid = Grid.from_ascii("""
#######
#.....#
#.###.#
#.....#
#######
""")
world = World()
world.spawn(Pos(Vec2(1, 1)), Seeker(goal=Vec2(5, 3)))
@world.add_system
def seek(world: World, dt: float) -> None:
for eid, pos, seeker in world.query(Pos, Seeker):
path = grid.find_path(pos.at, seeker.goal)
if path and len(path) > 1:
pos.at = path[1] # one step per tick
else:
world.destroy(eid) # arrived (removed at end of step)
loop = GameLoop(world.step, tick_rate=10)
loop.every(0.5, lambda: print(f"t={loop.time:.1f}s entities={len(world)}"))
loop.run(10)
A complete example, a tiny terminal chase game that uses every module, lives in
examples/chase.py:
python3 examples/chase.py # coin-collecting player escapes the ghost
python3 examples/chase.py --idle # player stands still; the ghost catches it
python3 examples/chase.py --every 1 --delay 0.1 # watch it frame by frame
API overview
Everything below is importable from the top-level gamekit package.
Vec2(x=0, y=0) (frozen dataclass)
| Member | Description |
|---|---|
+, -, * k, k *, unary - |
Vector arithmetic |
iter(v) |
Unpack: x, y = v |
length() -> float |
Euclidean length |
manhattan(other) |
Taxicab distance |
normalized() -> Vec2 |
Unit vector (zero stays zero) |
CARDINALS: tuple of the four unit directions (1,0), (-1,0), (0,1), (0,-1); y grows downward.
World(), the ECS (Entity is an alias for int)
| Method | Description |
|---|---|
spawn(*components) -> Entity |
Create an entity |
destroy(entity) |
Mark for removal; applied at the end of step() or by flush() |
flush() |
Apply pending destroys now |
alive(entity) -> bool |
Exists and is not pending destruction |
len(world) |
Number of live entities |
add(entity, component) |
Attach/replace a component (keyed by its type) |
remove(entity, ctype) |
Detach a component type (no-op if absent) |
get(entity, ctype) |
Component, or KeyError |
try_get(entity, ctype) |
Component or None |
has(entity, *ctypes) -> bool |
Has every listed type |
query(*ctypes) |
Yields (entity, comp1, comp2, ...) in creation order |
add_system(fn) |
Register fn(world, dt); usable as a decorator |
step(dt) |
Run all systems in order, then flush destroys |
Grid(width, height, walls=())
| Method | Description |
|---|---|
Grid.from_ascii(text, wall="#") |
Build from ASCII art |
width, height |
Dimensions |
in_bounds(p), is_wall(p), passable(p) |
Cell tests (out of bounds counts as wall) |
set_wall(p, wall=True) |
Add/remove a wall (IndexError if out of bounds) |
cells() |
All cells, row-major |
neighbors(p) -> list[Vec2] |
Passable 4-connected neighbours |
reachable(start) -> set[Vec2] |
Flood fill |
find_path(start, goal) -> list[Vec2] | None |
A* shortest path including both endpoints |
render(marks=None, wall="#", floor=".") -> str |
Text rendering with per-cell overrides |
GameLoop(update, tick_rate=60.0, max_steps_per_advance=8)
| Member | Description |
|---|---|
update(dt) |
Your callback, called once per fixed step |
dt, ticks, time, running |
Step size, steps run, simulated seconds, run state |
alpha |
Leftover fraction of a step, for render interpolation |
step() |
Run exactly one step: update, advance ticks/time, then timers |
advance(elapsed) -> int |
Feed real elapsed seconds; returns steps run |
run(ticks) |
Run N steps immediately (headless) |
run_realtime(duration=None, clock=time.perf_counter, sleep=time.sleep) |
Real-time loop |
stop() |
Exit run/run_realtime after the current step |
every(seconds, cb) -> Timer / after(seconds, cb) -> Timer |
Schedule in simulated time |
Timer(duration, callback=None, repeat=False)
tick(dt) -> int (times fired), reset(), progress (0..1), remaining, finished.
InputMap(bindings=None)
bindings is {"action": [key, ...]}; keys are any hashable.
| Method | Description |
|---|---|
bind(action, key) / unbind(action, key=None) |
Edit bindings |
keys_for(action) -> frozenset |
Keys bound to an action |
press(key) / release(key) |
Feed raw events from your backend |
is_held(action), just_pressed(action), just_released(action) |
Action state |
axis(negative, positive) -> int |
-1, 0 or +1 |
end_frame() |
Clear edge state; call once per frame |
Development
python3 -m unittest discover -s tests -t . # or: python3 -m pytest
License
MIT
Metadata
Release files for gamekit 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| gamekit-0.1.0.tar.gz | 17.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gamekit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.6 kB
Release files / gamekit-0.1.0.tar.gz
| Download URL | gamekit-0.1.0.tar.gz |
|---|---|
| Size | 17.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8bea5e307fd9acc852d6baacf5b1fc5631b9d4c28f9b12da090de948ff8e2a43
|
|
BLAKE2b-256 checksum How to use checksums |
10f0ec7f1ab7412fd8361aed83bc499ff32ff267ae1452b2a73c68cb42344e8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / gamekit-0.1.0-py3-none-any.whl
| Download URL | gamekit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f05cc3c8cc6e79beb8acf9fb7dfe24247b24ccca41dcbca2d8a3ccad9a949d14
|
|
BLAKE2b-256 checksum How to use checksums |
13948a920cca68bbee557cd7070cfc346d0f1049f035859baddfc2150aa331bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Security for gamekit 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.