Metadata-Version: 2.4
Name: ecs-entity
Version: 1.0.0
Summary: Minimal entity type from Entity Component System
Keywords: ecs,entity,component,context,dependency-injection
Author: DSLstandard
Author-email: DSLstandard <qgeneral35@gmail.com>
License-Expression: CC0-1.0
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# ecs-entity

**License:** [CC0 1.0 Universal](LICENSE)

A minimal `Entity` type for the [entity-component-system (ECS)](https://en.wikipedia.org/wiki/Entity_component_system)
pattern. It is common in game development, but useful wherever a value is a bag of typed parts.

Unity users will recognize `GameObject`/`MonoBehavior`: `entity.find(Transform)` is `GetComponent<Transform>()`, `entity.has(Transform)` is `TryGetComponent`, and `entity.get(Transform)`.

I think it is helpful to reimagine the `Entity` type in more generalized settings too:

- An `Entity` is a poor man's Haskell [`Data.Dependent.Map`](https://hackage-content.haskell.org/package/dependent-map/docs/Data-Dependent-Map.html).
- An `Entity` can be used as a service locator, like [`python-injector`](https://github.com/alecthomas/injector) but without the automatic dependency injection part.
- An `Entity` is a dictionary of components with type-safe access.

The library is minimal. There is no built-in entity ID logic, no entity manager, no game-world engine, or anything like it. You can build your own game architecture and use this library's `Entity` as a supporting type.

## Install

```sh
uv add ecs-entity
# or
pip install ecs-entity
```

You may also directly copy [`src/ecs/entity.py`](src/ecs/entity.py) into your
project.

## Library Usage

Read the code documentation in [`src/ecs/entity.py`](src/ecs/entity.py). It is under 200 lines.

Here is a Minecraft example:

```python
from dataclasses import dataclass

from ecs import Entity

Vec3 = tuple[float, float, float]
Quat = tuple[float, float, float, float]

MinecraftEntity = Entity  # a player, a cow, a dropped item: all just component bags


@dataclass
class Transform:
  """Where the entity is, and which way it faces."""

  position: Vec3
  rotation: Quat


@dataclass
class Physics:
  """How the entity moves next tick."""

  velocity: Vec3
  acceleration: Vec3


@dataclass
class InventoryData:
  items: tuple[str, ...]


@dataclass
class LivingBase:
  """A health bar."""

  health: float
  max_health: float


@dataclass
class PlayerBase:
  """Only players carry these."""

  client_ip: str
  gamemode: str
  is_admin: bool


steve = MinecraftEntity.of(
  Transform(position=(0.5, 64.0, 0.5), rotation=(0.0, 0.0, 0.0, 1.0)),
  Physics(velocity=(0.0, 0.0, 0.0), acceleration=(0.0, -0.08, 0.0)),
  LivingBase(health=20.0, max_health=20.0),
  InventoryData(items=("diamond_sword", "bread")),
  PlayerBase(client_ip="203.0.113.7", gamemode="survival", is_admin=False),
)

cow = MinecraftEntity.of(
  Transform(position=(3.0, 64.0, 1.5), rotation=(0.0, 0.0, 0.0, 1.0)),
  Physics(velocity=(0.0, 0.0, 0.0), acceleration=(0.0, -0.08, 0.0)),
  LivingBase(health=10.0, max_health=10.0),
)

def is_player(entity: MinecraftEntity) -> bool:
  """A player is an entity that carries PlayerBase."""
  return entity.has(PlayerBase)


assert is_player(steve)  # true because steve carries PlayerBase
assert not is_player(cow)
assert steve.get(Transform).position == (0.5, 64.0, 0.5)
assert cow.find(InventoryData) is None


def tick(entity: MinecraftEntity) -> None:
  """One physics step: acceleration moves velocity, velocity moves position."""
  physics = entity.find(Physics)
  transform = entity.find(Transform)
  if physics is None or transform is None:
    raise RuntimeError("Input entity does not support physics!")

  vx, vy, vz = physics.velocity
  ax, ay, az = physics.acceleration
  px, py, pz = transform.position

  velocity = (vx + ax, vy + ay, vz + az)
  position = (px + velocity[0], py + velocity[1], pz + velocity[2])

  physics.velocity = velocity
  transform.position = position


tick(cow)
assert cow.get(Physics).velocity == (0.0, -0.08, 0.0)
assert cow.get(Transform).position == (3.0, 63.92, 1.5)


def damage(entity: MinecraftEntity, amount: float) -> None:
  """Anything with a health bar can be hurt; this never asks what kind of entity it is."""
  bar = entity.find(LivingBase)
  if bar is None:
    raise RuntimeError("Input entity does not support damage!")

  bar.health = max(bar.health - amount, 0.0)


damage(cow, 4.0)
assert cow.get(LivingBase).health == 6.0
assert cow.get(LivingBase).max_health == 10.0
```

For the rest of the API, read [`src/ecs/entity.py`](src/ecs/entity.py).
