Metadata-Version: 2.4
Name: gamekit
Version: 0.1.0
Summary: A tiny, dependency-free, headless toolkit for building games: ECS, grids and A* pathfinding, a fixed-timestep loop, timers, and input mapping.
Author: nehz
License-Expression: MIT
Keywords: game,gamedev,ecs,pathfinding,game-loop,roguelike
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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), typed `query()` over component combinations, ordered systems, and
  deferred `destroy()` 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 interpolation `alpha`,
  headless `run(ticks)` for tests, and `run_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:

```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -e .
```

## Quickstart

```python
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`](examples/chase.py):

```bash
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

```bash
python3 -m unittest discover -s tests -t .    # or: python3 -m pytest
```

## License

MIT
