{"id":16696012,"url":"https://github.com/hexdecimal/python-tcod-ecs","last_synced_at":"2026-01-16T13:47:31.579Z","repository":{"id":148709462,"uuid":"618648612","full_name":"HexDecimal/python-tcod-ecs","owner":"HexDecimal","description":"Python sparse-set ECS with strong type-hinting.  Supports entitiy relations.","archived":false,"fork":false,"pushed_at":"2025-04-21T23:45:22.000Z","size":375,"stargazers_count":20,"open_issues_count":7,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-25T09:48:40.429Z","etag":null,"topics":["design-patterns","ecs","entity-component-system","python","python-library","python3","sparse-set","sparse-set-ecs"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HexDecimal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-03-25T00:15:13.000Z","updated_at":"2025-04-18T05:34:17.000Z","dependencies_parsed_at":null,"dependency_job_id":"692f2fe7-f18a-4166-85af-7ed2f4abbed1","html_url":"https://github.com/HexDecimal/python-tcod-ecs","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"purl":"pkg:github/HexDecimal/python-tcod-ecs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HexDecimal%2Fpython-tcod-ecs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HexDecimal%2Fpython-tcod-ecs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HexDecimal%2Fpython-tcod-ecs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HexDecimal%2Fpython-tcod-ecs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HexDecimal","download_url":"https://codeload.github.com/HexDecimal/python-tcod-ecs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HexDecimal%2Fpython-tcod-ecs/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259960635,"owners_count":22938099,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["design-patterns","ecs","entity-component-system","python","python-library","python3","sparse-set","sparse-set-ecs"],"created_at":"2024-10-12T17:25:23.914Z","updated_at":"2026-01-16T13:47:26.552Z","avatar_url":"https://github.com/HexDecimal.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# About\n\n[![PyPI](https://img.shields.io/pypi/v/tcod-ecs)](https://pypi.org/project/tcod-ecs/)\n[![PyPI - License](https://img.shields.io/pypi/l/tcod-ecs)](https://github.com/HexDecimal/python-tcod-ecs/blob/main/LICENSE)\n[![Documentation Status](https://readthedocs.org/projects/python-tcod-ecs/badge/?version=latest)](https://python-tcod-ecs.readthedocs.io)\n[![codecov](https://codecov.io/gh/HexDecimal/python-tcod-ecs/branch/main/graph/badge.svg?token=4Ak5QpTLZB)](https://codecov.io/gh/HexDecimal/python-tcod-ecs)\n[![CommitsSinceLastRelease](https://img.shields.io/github/commits-since/HexDecimal/python-tcod-ecs/latest)](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md)\n\n`tcod-ecs` is a [Sparse-set](https://skypjack.github.io/2020-08-02-ecs-baf-part-9/) [Entity-component-system](https://en.wikipedia.org/wiki/Entity_component_system) implemented using Python's `dict` and `set` types.\nSee the [ECS FAQ](https://github.com/SanderMertens/ecs-faq) for more info.\n\nThis implementation focuses on type-hinting, organization, and is designed to work well with Python.\nThe following features are currently implemented:\n\n- Entities can store components which are instances of any Python object. Components are looked up by their type.\n- Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.\n- Entity relationships are supported, either as many-to-many or many-to-one relationships.\n- ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.\n- The ECS Registry object can be serialized with Python's pickle module for easy storage.\n\nA lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/).\n`tcod-ec` was geared towards a dynamic-typed-dict style of syntax and is missing a lot of important features such as queries and named components.\n\n# Installation\n\nUse pip to install this library:\n\n```\npip install tcod-ecs\n```\n\nIf `tcod` is installed and the version is less than `14.0.0` then `import tcod.ecs` will fail.\nRemove or update `tcod` to fix this issue.\n\n# Examples\n\n## Registry\n\nThe ECS Registry is used to create and store entities and their components.\n\n```py\n\u003e\u003e\u003e import tcod.ecs\n\u003e\u003e\u003e registry = tcod.ecs.Registry()  # New empty registry\n\n```\n\n## Entity\n\nEach Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs.\nNew unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desirable.\nAn entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance.\nRegistries only know about their entities once the entity is assigned a name, component, tag, or relation.\n\n```py\n\u003e\u003e\u003e entity = registry.new_entity()  # Creates a unique entity using `object()` as the uid\n\u003e\u003e\u003e entity\n\u003cEntity(uid=object at ...)\u003e\n\u003e\u003e\u003e entity.registry is registry  # Registry can always be accessed from their entity\nTrue\n\u003e\u003e\u003e registry[entity.uid] is entity  # Entities with the same registry/uid are compared using `is`\nTrue\n\n# Reference an entity with the given uid, can be any hashable object:\n\u003e\u003e\u003e entity = registry[\"MyEntity\"]\n\u003e\u003e\u003e entity\n\u003cEntity(uid='MyEntity')\u003e\n\u003e\u003e\u003e registry[\"MyEntity\"] is entity  # Matching entities ALWAYS share a single identity\nTrue\n\n```\n\nUse `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id.\n`registry[None]` is recommend for use as a global entity when you want to store components in the registry itself.\n\nDo not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance.\n\n## Serialization\n\nRegistries are normal Python objects and can be pickled as long as all stored components are pickleable.\n\n```py\n\u003e\u003e\u003e import pickle\n\u003e\u003e\u003e pickled_data: bytes = pickle.dumps(registry)\n\u003e\u003e\u003e registry = pickle.loads(pickled_data)\n\n```\n\nStability is a priority but changes may still break older saves.\nBackwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.\nThis project follows [Semantic Versioning](https://semver.org/), major version increments will break the API, the save format or both, minor version increments may break backwards compatibility.\nCheck the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks.\nThere should always be a transition period before a format break, so keeping up with the latest version is a good idea.\n\n## Components\n\nComponents are instances of any Python type.\nThese can be accessed, assigned, or removed from entities via the dict-like `Entity.components` attribute.\nThe type is used as the key to access the component.\nThe types used can be custom classes or standard Python types.\n\n```py\n\u003e\u003e\u003e import attrs\n\u003e\u003e\u003e entity = registry.new_entity()\n\u003e\u003e\u003e entity.components[int] = 42\n\u003e\u003e\u003e entity.components[int]\n42\n\u003e\u003e\u003e int in entity.components\nTrue\n\u003e\u003e\u003e del entity.components[int]\n\u003e\u003e\u003e entity.components[int]  # Missing keys raise KeyError\nTraceback (most recent call last):\n  ...\nKeyError: \u003cclass 'int'\u003e\n\u003e\u003e\u003e entity.components.get(int, \"default\")  # Test keys with `.get()` like a dictionary\n'default'\n\u003e\u003e\u003e @attrs.define\n... class Vector2:\n...     x: int = 0\n...     y: int = 0\n\u003e\u003e\u003e entity.components[Vector2] = Vector2(1, 2)\n\u003e\u003e\u003e entity.components[Vector2]\nVector2(x=1, y=2)\n\u003e\u003e\u003e entity.components |= {int: 11, Vector2: Vector2(0, 0)}  # Multiple values can be assigned like a dict\n\u003e\u003e\u003e entity.components[int]\n11\n\u003e\u003e\u003e entity.components[Vector2]\nVector2(x=0, y=0)\n\n# Queries can be made on all entities of a registry with matching components\n\u003e\u003e\u003e for e in registry.Q.all_of(components=[Vector2]):\n...     e.components[Vector2].x += 10\n\u003e\u003e\u003e entity.components[Vector2]\nVector2(x=10, y=0)\n\n# You can match components and iterate over them at the same time.  This can be combined with the above\n\u003e\u003e\u003e for pos, i in registry.Q[Vector2, int]:\n...     print((pos, i))\n(Vector2(x=10, y=0), 11)\n\n# You can include `Entity` to iterate over entities with their components\n# This always iterates over the entity itself instead of an Entity component\n\u003e\u003e\u003e for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:\n...     print((e, pos, i))\n(\u003cEntity...\u003e, Vector2(x=10, y=0), 11)\n\n```\n\n## Named Components\n\nOnly one component can be assigned unless that component is given a unique name.\nYou can name components with the key syntax `(name, type)` when assigning components.\nNames are not limited to strings, they are a tag equivalent and can be any hashable or frozen object.\nThe syntax `[type]` and `[(name, type)]` can be used interchangeably in all places accepting a component key.\nQueries on components access named components with the same syntax and must use names explicitly.\n\n```py\n\u003e\u003e\u003e entity = registry.new_entity()\n\u003e\u003e\u003e entity.components[Vector2] = Vector2(0, 0)\n\u003e\u003e\u003e entity.components[(\"velocity\", Vector2)] = Vector2(1, 1)\n\u003e\u003e\u003e entity.components[(\"velocity\", Vector2)]\nVector2(x=1, y=1)\n\u003e\u003e\u003e @attrs.define(frozen=True)\n... class Slot:\n...     index: int\n\u003e\u003e\u003e entity.components |= {  # Like a dict Entity.components can use |= to update items in-place\n...     (\"hp\", int): 10,\n...     (\"max_hp\", int): 12,\n...     (\"atk\", int): 1,\n...     str: \"foo\",\n...     (Slot(1), str): \"empty\",\n... }\n\u003e\u003e\u003e entity.components[(\"hp\", int)]\n10\n\u003e\u003e\u003e entity.components[str]\n'foo'\n\u003e\u003e\u003e entity.components[(Slot(1), str)]\n'empty'\n\n# Queries can be made on all named components with the same syntax as normal ones\n\u003e\u003e\u003e for e in registry.Q.all_of(components=[(\"hp\", int), (\"max_hp\", int)]):\n...     e.components[(\"hp\", int)] = e.components[(\"max_hp\", int)]\n\u003e\u003e\u003e entity.components[(\"hp\", int)]\n12\n\u003e\u003e\u003e for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, (\"velocity\", Vector2)]:\n...     e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)\n\u003e\u003e\u003e entity.components[Vector2]\nVector2(x=1, y=1)\n\n```\n\n## Tags\n\nTags are hashable objects stored in the set-like `Entity.tags`.\nThese are useful as flags or to group entities together.\n\n```py\n\u003e\u003e\u003e entity = registry.new_entity()\n\u003e\u003e\u003e entity.tags.add(\"player\")  # Works well for groups\n\u003e\u003e\u003e \"player\" in entity.tags\nTrue\n\u003e\u003e\u003e entity.tags.add((\"eats\", \"fruit\"))\n\u003e\u003e\u003e entity.tags.add((\"eats\", \"meat\"))\n\u003e\u003e\u003e set(registry.Q.all_of(tags=[\"player\"])) == {entity}\nTrue\n\n```\n\n## Relations\n\nEntity relations are unidirectional from an origin entity to possibly multiple target entities.\n\n- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.\n  This uses standard assignment and is useful for tags which would not make sense with multiple targets.\n  Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.\n- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.\n  This supports `set`-like syntax such as adding or removing multiple targets at once.\n  This allows for many-to-many relations.\n- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.\n  This allows storing data along with a relation.\n  This supports `dict`-like syntax.\n  The `component_key` can be queried like a normal tag.\n\n### Relation queries\n\nRelations are queried with `registry.Q.all_of(relations=[...])`.\nThis expects 2-item or 3-item tuples following these rules:\n\n- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.\n- If `tag` is a component key then component relations are also matched.\n  This means you should be careful with tags which look like component keys.\n- `target` can be a specific entity. This means only entities relating to that specific entity will be matched.\n- `target` can be query itself. This means only entities relating to a match from the sub-query are matched.\n- `target` can be `...` which means an entity with a relation to any entity is matched.\n- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.\n\nRelations using sub-queries may be chained together.\nSee [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this.\n\nYou can use the following table to help with constructing relation queries:\n\n| Matches                                                             |                  Syntax                  |\n| ------------------------------------------------------------------- | :--------------------------------------: |\n| Origins with a relation `tag` to `target_entity`                    |          `(tag, target_entity)`          |\n| Origins with a relation `tag` to any target entity                  |    `(tag, ...)` (Literal dot-dot-dot)    |\n| Origins with a relation `tag` to any targets matching a sub-query   |     `(tag, registry.Q.all_of(...))`      |\n| Targets of the relation `tag` from `origin_entity`                  |       `(origin_entity, tag, None)`       |\n| Targets of the relation `tag` from any origin entity                | `(..., tag, None)` (Literal dot-dot-dot) |\n| Targets of the relation `tag` from any origins matching a sub-query |  `(registry.Q.all_of(...), tag, None)`   |\n\n```py\n\u003e\u003e\u003e @attrs.define\n... class OrbitOf:  # OrbitOf component\n...     dist: int\n\u003e\u003e\u003e LandedOn = \"LandedOn\"  # LandedOn tag\n\u003e\u003e\u003e star = registry.new_entity()\n\u003e\u003e\u003e planet = registry.new_entity()\n\u003e\u003e\u003e moon = registry.new_entity()\n\u003e\u003e\u003e ship = registry.new_entity()\n\u003e\u003e\u003e player = registry.new_entity()\n\u003e\u003e\u003e moon_rock = registry.new_entity()\n\u003e\u003e\u003e planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)\n\u003e\u003e\u003e moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)\n\u003e\u003e\u003e ship.relation_tag[LandedOn] = moon\n\u003e\u003e\u003e moon_rock.relation_tag[LandedOn] = moon\n\u003e\u003e\u003e player.relation_tag[LandedOn] = moon_rock\n\u003e\u003e\u003e set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}\nTrue\n\u003e\u003e\u003e set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon}  # Get objects in an orbit\nTrue\n\u003e\u003e\u003e set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet}  # Get objects being orbited\nTrue\n\u003e\u003e\u003e set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}\nTrue\n\u003e\u003e\u003e set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}\nTrue\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexdecimal%2Fpython-tcod-ecs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhexdecimal%2Fpython-tcod-ecs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexdecimal%2Fpython-tcod-ecs/lists"}