tcod-ecs 5.0.0__tar.gz → 5.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2023 Kyle Benesch
3
+ Copyright (c) 2023-2024 Kyle Benesch
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tcod-ecs
3
- Version: 5.0.0
3
+ Version: 5.2.0
4
4
  Summary: A type-hinted Entity Component System based on Python dictionaries and sets.
5
5
  Author-email: Kyle Benesch <4b796c65+github@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -45,7 +45,7 @@ The following features are currently implemented:
45
45
  - Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.
46
46
  - Entity relationships are supported, either as many-to-many or many-to-one relationships.
47
47
  - ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.
48
- - The ECS World object can be serialized with Python's pickle module for easy storage.
48
+ - The ECS Registry object can be serialized with Python's pickle module for easy storage.
49
49
 
50
50
  A lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/).
51
51
  `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.
@@ -53,6 +53,7 @@ A lightweight version which implements only the entity-component framework exist
53
53
  # Installation
54
54
 
55
55
  Use pip to install this library:
56
+
56
57
  ```
57
58
  pip install tcod-ecs
58
59
  ```
@@ -62,57 +63,59 @@ Remove or update `tcod` to fix this issue.
62
63
 
63
64
  # Examples
64
65
 
65
- ## World
66
+ ## Registry
67
+
68
+ The ECS Registry is used to create and store entities and their components.
66
69
 
67
70
  ```py
68
71
  >>> import tcod.ecs
69
- >>> world = tcod.ecs.World() # New empty world
72
+ >>> registry = tcod.ecs.Registry() # New empty registry
70
73
 
71
74
  ```
72
75
 
73
76
  ## Entity
74
77
 
75
- Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `world` it belongs.
76
- New unique entities can be created with `World.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
77
- An entity always knows about its assigned world, which can be access with the `Entity.world` property from any Entity instance.
78
- Worlds only know about their entities once the entity is assigned a name, component, tag, or relation.
78
+ Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs.
79
+ New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
80
+ An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance.
81
+ Registries only know about their entities once the entity is assigned a name, component, tag, or relation.
79
82
 
80
83
  ```py
81
- >>> entity = world.new_entity() # Creates a unique entity using `object()` as the uid
84
+ >>> entity = registry.new_entity() # Creates a unique entity using `object()` as the uid
82
85
  >>> entity
83
86
  <Entity(uid=object at ...)>
84
- >>> entity.world is world # Worlds can always be accessed from their entity
87
+ >>> entity.registry is registry # Registry can always be accessed from their entity
85
88
  True
86
- >>> world[entity.uid] is entity # Entities with the same world/uid are compared using `is`
89
+ >>> registry[entity.uid] is entity # Entities with the same registry/uid are compared using `is`
87
90
  True
88
91
 
89
92
  # Reference an entity with the given uid, can be any hashable object:
90
- >>> entity = world["MyEntity"]
93
+ >>> entity = registry["MyEntity"]
91
94
  >>> entity
92
95
  <Entity(uid='MyEntity')>
93
- >>> world["MyEntity"] is entity # Matching entities ALWAYS share a single identity
96
+ >>> registry["MyEntity"] is entity # Matching entities ALWAYS share a single identity
94
97
  True
95
98
 
96
99
  ```
97
100
 
98
- Use `World.new_entity` to create unique entities and use `World[x]` to reference a global entity or relation with an id.
99
- `World[None]` is recommend for use as a global entity when you want to store components in the world itself.
101
+ Use `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id.
102
+ `registry[None]` is recommend for use as a global entity when you want to store components in the registry itself.
100
103
 
101
- Do not save the `uid`'s of entities to be used later with `World[uid]`, this process is slower than holding onto the Entity instance.
104
+ Do not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance.
102
105
 
103
106
  ## Serialization
104
107
 
105
- Worlds are normal Python objects and can be pickled as long as all stored components are pickleable.
108
+ Registries are normal Python objects and can be pickled as long as all stored components are pickleable.
106
109
 
107
110
  ```py
108
111
  >>> import pickle
109
- >>> pickled_data: bytes = pickle.dumps(world)
110
- >>> world = pickle.loads(pickled_data)
112
+ >>> pickled_data: bytes = pickle.dumps(registry)
113
+ >>> registry = pickle.loads(pickled_data)
111
114
 
112
115
  ```
113
116
 
114
117
  Stability is a priority but changes may still break older saves.
115
- Backwards compatibility is not a priority, pickled worlds should not be unpickled with an older version of the library.
118
+ Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.
116
119
  This 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.
117
120
  Check the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks.
118
121
  There should always be a transition period before a format break, so keeping up with the latest version is a good idea.
@@ -126,7 +129,7 @@ The types used can be custom classes or standard Python types.
126
129
 
127
130
  ```py
128
131
  >>> import attrs
129
- >>> entity = world.new_entity()
132
+ >>> entity = registry.new_entity()
130
133
  >>> entity.components[int] = 42
131
134
  >>> entity.components[int]
132
135
  42
@@ -152,20 +155,20 @@ Vector2(x=1, y=2)
152
155
  >>> entity.components[Vector2]
153
156
  Vector2(x=0, y=0)
154
157
 
155
- # Queries can be made on all entities of a world with matching components
156
- >>> for e in world.Q.all_of(components=[Vector2]):
158
+ # Queries can be made on all entities of a registry with matching components
159
+ >>> for e in registry.Q.all_of(components=[Vector2]):
157
160
  ... e.components[Vector2].x += 10
158
161
  >>> entity.components[Vector2]
159
162
  Vector2(x=10, y=0)
160
163
 
161
164
  # You can match components and iterate over them at the same time. This can be combined with the above
162
- >>> for pos, i in world.Q[Vector2, int]:
165
+ >>> for pos, i in registry.Q[Vector2, int]:
163
166
  ... print((pos, i))
164
167
  (Vector2(x=10, y=0), 11)
165
168
 
166
169
  # You can include `Entity` to iterate over entities with their components
167
170
  # This always iterates over the entity itself instead of an Entity component
168
- >>> for e, pos, i in world.Q[tcod.ecs.Entity, Vector2, int]:
171
+ >>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:
169
172
  ... print((e, pos, i))
170
173
  (<Entity...>, Vector2(x=10, y=0), 11)
171
174
 
@@ -180,7 +183,7 @@ The syntax `[type]` and `[(name, type)]` can be used interchangeably in all plac
180
183
  Queries on components access named components with the same syntax and must use names explicitly.
181
184
 
182
185
  ```py
183
- >>> entity = world.new_entity()
186
+ >>> entity = registry.new_entity()
184
187
  >>> entity.components[Vector2] = Vector2(0, 0)
185
188
  >>> entity.components[("velocity", Vector2)] = Vector2(1, 1)
186
189
  >>> entity.components[("velocity", Vector2)]
@@ -203,11 +206,11 @@ Vector2(x=1, y=1)
203
206
  'empty'
204
207
 
205
208
  # Queries can be made on all named components with the same syntax as normal ones
206
- >>> for e in world.Q.all_of(components=[("hp", int), ("max_hp", int)]):
209
+ >>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]):
207
210
  ... e.components[("hp", int)] = e.components[("max_hp", int)]
208
211
  >>> entity.components[("hp", int)]
209
212
  12
210
- >>> for e, pos, delta in world.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
213
+ >>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
211
214
  ... e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)
212
215
  >>> entity.components[Vector2]
213
216
  Vector2(x=1, y=1)
@@ -220,67 +223,85 @@ Tags are hashable objects stored in the set-like `Entity.tags`.
220
223
  These are useful as flags or to group entities together.
221
224
 
222
225
  ```py
223
- >>> entity = world.new_entity()
226
+ >>> entity = registry.new_entity()
224
227
  >>> entity.tags.add("player") # Works well for groups
225
228
  >>> "player" in entity.tags
226
229
  True
227
230
  >>> entity.tags.add(("eats", "fruit"))
228
231
  >>> entity.tags.add(("eats", "meat"))
229
- >>> set(world.Q.all_of(tags=["player"])) == {entity}
232
+ >>> set(registry.Q.all_of(tags=["player"])) == {entity}
230
233
  True
231
234
 
232
235
  ```
233
236
 
234
237
  ## Relations
235
238
 
236
- Use `Entity.relation_components[component_key][target] = component` to associate a target entity with a component.
237
- Use `Entity.relation_tag[tag] = target` to associate a tag exclusively with a target entity.
238
- Use `Entity.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
239
+ Entity relations are unidirectional from an origin entity to possibly multiple target entities.
240
+
241
+ - Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
242
+ This uses standard assignment and is useful for tags which would not make sense with multiple targets.
243
+ Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
244
+ - Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
245
+ This supports `set`-like syntax such as adding or removing multiple targets at once.
246
+ This allows for many-to-many relations.
247
+ - Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
248
+ This allows storing data along with a relation.
249
+ This supports `dict`-like syntax.
250
+ The `component_key` can be queried like a normal tag.
251
+
252
+ ### Relation queries
253
+
254
+ Relations are queried with `registry.Q.all_of(relations=[...])`.
255
+ This expects 2-item or 3-item tuples following these rules:
239
256
 
240
- Relation queries are a little more complex than other queries.
241
- Relation tags and relation components share the same space then queried, so 'normal' tags should not be in the format of a component key.
242
- Relations are unidirectional, but you can query either end of a relation.
257
+ - Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
258
+ - If `tag` is a component key then component relations are also matched.
259
+ This means you should be careful with tags which look like component keys.
260
+ - `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
261
+ - `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
262
+ - `target` can be `...` which means an entity with a relation to any entity is matched.
263
+ - To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
264
+
265
+ Relations using sub-queries may be chained together.
266
+ See [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.
267
+
268
+ You can use the following table to help with constructing relation queries:
269
+
270
+ | Matches | Syntax |
271
+ | ------------------------------------------------------------------- | :--------------------------------------: |
272
+ | Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
273
+ | Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
274
+ | Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
275
+ | Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
276
+ | Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
277
+ | Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
243
278
 
244
279
  ```py
245
280
  >>> @attrs.define
246
281
  ... class OrbitOf: # OrbitOf component
247
282
  ... dist: int
248
283
  >>> LandedOn = "LandedOn" # LandedOn tag
249
- >>> star = world.new_entity()
250
- >>> planet = world.new_entity()
251
- >>> moon = world.new_entity()
252
- >>> ship = world.new_entity()
253
- >>> player = world.new_entity()
254
- >>> moon_rock = world.new_entity()
284
+ >>> star = registry.new_entity()
285
+ >>> planet = registry.new_entity()
286
+ >>> moon = registry.new_entity()
287
+ >>> ship = registry.new_entity()
288
+ >>> player = registry.new_entity()
289
+ >>> moon_rock = registry.new_entity()
255
290
  >>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)
256
291
  >>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)
257
292
  >>> ship.relation_tag[LandedOn] = moon
258
293
  >>> moon_rock.relation_tag[LandedOn] = moon
259
294
  >>> player.relation_tag[LandedOn] = moon_rock
260
- >>> set(world.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
295
+ >>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
261
296
  True
262
- >>> set(world.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
297
+ >>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
263
298
  True
264
- >>> set(world.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
299
+ >>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
265
300
  True
266
- >>> set(world.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
301
+ >>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
267
302
  True
268
- >>> set(world.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
303
+ >>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
269
304
  True
270
305
 
271
306
  ```
272
307
 
273
- ### Relation queries
274
-
275
- You can use the following table to help with constructing relation queries.
276
- `tag` is a component key if you are querying for a component relation.
277
-
278
- | Includes | Syntax |
279
- | --------------------------------------------------------------------- | :----: |
280
- | Entities with a relation tag to the given target | `(tag, target_entity)` |
281
- | Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
282
- | Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` |
283
- | The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
284
- | The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
285
- | The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` |
286
-
@@ -16,7 +16,7 @@ The following features are currently implemented:
16
16
  - Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.
17
17
  - Entity relationships are supported, either as many-to-many or many-to-one relationships.
18
18
  - ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.
19
- - The ECS World object can be serialized with Python's pickle module for easy storage.
19
+ - The ECS Registry object can be serialized with Python's pickle module for easy storage.
20
20
 
21
21
  A lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/).
22
22
  `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.
@@ -24,6 +24,7 @@ A lightweight version which implements only the entity-component framework exist
24
24
  # Installation
25
25
 
26
26
  Use pip to install this library:
27
+
27
28
  ```
28
29
  pip install tcod-ecs
29
30
  ```
@@ -33,57 +34,59 @@ Remove or update `tcod` to fix this issue.
33
34
 
34
35
  # Examples
35
36
 
36
- ## World
37
+ ## Registry
38
+
39
+ The ECS Registry is used to create and store entities and their components.
37
40
 
38
41
  ```py
39
42
  >>> import tcod.ecs
40
- >>> world = tcod.ecs.World() # New empty world
43
+ >>> registry = tcod.ecs.Registry() # New empty registry
41
44
 
42
45
  ```
43
46
 
44
47
  ## Entity
45
48
 
46
- Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `world` it belongs.
47
- New unique entities can be created with `World.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
48
- An entity always knows about its assigned world, which can be access with the `Entity.world` property from any Entity instance.
49
- Worlds only know about their entities once the entity is assigned a name, component, tag, or relation.
49
+ Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs.
50
+ New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
51
+ An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance.
52
+ Registries only know about their entities once the entity is assigned a name, component, tag, or relation.
50
53
 
51
54
  ```py
52
- >>> entity = world.new_entity() # Creates a unique entity using `object()` as the uid
55
+ >>> entity = registry.new_entity() # Creates a unique entity using `object()` as the uid
53
56
  >>> entity
54
57
  <Entity(uid=object at ...)>
55
- >>> entity.world is world # Worlds can always be accessed from their entity
58
+ >>> entity.registry is registry # Registry can always be accessed from their entity
56
59
  True
57
- >>> world[entity.uid] is entity # Entities with the same world/uid are compared using `is`
60
+ >>> registry[entity.uid] is entity # Entities with the same registry/uid are compared using `is`
58
61
  True
59
62
 
60
63
  # Reference an entity with the given uid, can be any hashable object:
61
- >>> entity = world["MyEntity"]
64
+ >>> entity = registry["MyEntity"]
62
65
  >>> entity
63
66
  <Entity(uid='MyEntity')>
64
- >>> world["MyEntity"] is entity # Matching entities ALWAYS share a single identity
67
+ >>> registry["MyEntity"] is entity # Matching entities ALWAYS share a single identity
65
68
  True
66
69
 
67
70
  ```
68
71
 
69
- Use `World.new_entity` to create unique entities and use `World[x]` to reference a global entity or relation with an id.
70
- `World[None]` is recommend for use as a global entity when you want to store components in the world itself.
72
+ Use `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id.
73
+ `registry[None]` is recommend for use as a global entity when you want to store components in the registry itself.
71
74
 
72
- Do not save the `uid`'s of entities to be used later with `World[uid]`, this process is slower than holding onto the Entity instance.
75
+ Do not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance.
73
76
 
74
77
  ## Serialization
75
78
 
76
- Worlds are normal Python objects and can be pickled as long as all stored components are pickleable.
79
+ Registries are normal Python objects and can be pickled as long as all stored components are pickleable.
77
80
 
78
81
  ```py
79
82
  >>> import pickle
80
- >>> pickled_data: bytes = pickle.dumps(world)
81
- >>> world = pickle.loads(pickled_data)
83
+ >>> pickled_data: bytes = pickle.dumps(registry)
84
+ >>> registry = pickle.loads(pickled_data)
82
85
 
83
86
  ```
84
87
 
85
88
  Stability is a priority but changes may still break older saves.
86
- Backwards compatibility is not a priority, pickled worlds should not be unpickled with an older version of the library.
89
+ Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.
87
90
  This 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.
88
91
  Check the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks.
89
92
  There should always be a transition period before a format break, so keeping up with the latest version is a good idea.
@@ -97,7 +100,7 @@ The types used can be custom classes or standard Python types.
97
100
 
98
101
  ```py
99
102
  >>> import attrs
100
- >>> entity = world.new_entity()
103
+ >>> entity = registry.new_entity()
101
104
  >>> entity.components[int] = 42
102
105
  >>> entity.components[int]
103
106
  42
@@ -123,20 +126,20 @@ Vector2(x=1, y=2)
123
126
  >>> entity.components[Vector2]
124
127
  Vector2(x=0, y=0)
125
128
 
126
- # Queries can be made on all entities of a world with matching components
127
- >>> for e in world.Q.all_of(components=[Vector2]):
129
+ # Queries can be made on all entities of a registry with matching components
130
+ >>> for e in registry.Q.all_of(components=[Vector2]):
128
131
  ... e.components[Vector2].x += 10
129
132
  >>> entity.components[Vector2]
130
133
  Vector2(x=10, y=0)
131
134
 
132
135
  # You can match components and iterate over them at the same time. This can be combined with the above
133
- >>> for pos, i in world.Q[Vector2, int]:
136
+ >>> for pos, i in registry.Q[Vector2, int]:
134
137
  ... print((pos, i))
135
138
  (Vector2(x=10, y=0), 11)
136
139
 
137
140
  # You can include `Entity` to iterate over entities with their components
138
141
  # This always iterates over the entity itself instead of an Entity component
139
- >>> for e, pos, i in world.Q[tcod.ecs.Entity, Vector2, int]:
142
+ >>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:
140
143
  ... print((e, pos, i))
141
144
  (<Entity...>, Vector2(x=10, y=0), 11)
142
145
 
@@ -151,7 +154,7 @@ The syntax `[type]` and `[(name, type)]` can be used interchangeably in all plac
151
154
  Queries on components access named components with the same syntax and must use names explicitly.
152
155
 
153
156
  ```py
154
- >>> entity = world.new_entity()
157
+ >>> entity = registry.new_entity()
155
158
  >>> entity.components[Vector2] = Vector2(0, 0)
156
159
  >>> entity.components[("velocity", Vector2)] = Vector2(1, 1)
157
160
  >>> entity.components[("velocity", Vector2)]
@@ -174,11 +177,11 @@ Vector2(x=1, y=1)
174
177
  'empty'
175
178
 
176
179
  # Queries can be made on all named components with the same syntax as normal ones
177
- >>> for e in world.Q.all_of(components=[("hp", int), ("max_hp", int)]):
180
+ >>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]):
178
181
  ... e.components[("hp", int)] = e.components[("max_hp", int)]
179
182
  >>> entity.components[("hp", int)]
180
183
  12
181
- >>> for e, pos, delta in world.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
184
+ >>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
182
185
  ... e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)
183
186
  >>> entity.components[Vector2]
184
187
  Vector2(x=1, y=1)
@@ -191,66 +194,84 @@ Tags are hashable objects stored in the set-like `Entity.tags`.
191
194
  These are useful as flags or to group entities together.
192
195
 
193
196
  ```py
194
- >>> entity = world.new_entity()
197
+ >>> entity = registry.new_entity()
195
198
  >>> entity.tags.add("player") # Works well for groups
196
199
  >>> "player" in entity.tags
197
200
  True
198
201
  >>> entity.tags.add(("eats", "fruit"))
199
202
  >>> entity.tags.add(("eats", "meat"))
200
- >>> set(world.Q.all_of(tags=["player"])) == {entity}
203
+ >>> set(registry.Q.all_of(tags=["player"])) == {entity}
201
204
  True
202
205
 
203
206
  ```
204
207
 
205
208
  ## Relations
206
209
 
207
- Use `Entity.relation_components[component_key][target] = component` to associate a target entity with a component.
208
- Use `Entity.relation_tag[tag] = target` to associate a tag exclusively with a target entity.
209
- Use `Entity.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
210
+ Entity relations are unidirectional from an origin entity to possibly multiple target entities.
211
+
212
+ - Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
213
+ This uses standard assignment and is useful for tags which would not make sense with multiple targets.
214
+ Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
215
+ - Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
216
+ This supports `set`-like syntax such as adding or removing multiple targets at once.
217
+ This allows for many-to-many relations.
218
+ - Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
219
+ This allows storing data along with a relation.
220
+ This supports `dict`-like syntax.
221
+ The `component_key` can be queried like a normal tag.
222
+
223
+ ### Relation queries
224
+
225
+ Relations are queried with `registry.Q.all_of(relations=[...])`.
226
+ This expects 2-item or 3-item tuples following these rules:
210
227
 
211
- Relation queries are a little more complex than other queries.
212
- Relation tags and relation components share the same space then queried, so 'normal' tags should not be in the format of a component key.
213
- Relations are unidirectional, but you can query either end of a relation.
228
+ - Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
229
+ - If `tag` is a component key then component relations are also matched.
230
+ This means you should be careful with tags which look like component keys.
231
+ - `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
232
+ - `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
233
+ - `target` can be `...` which means an entity with a relation to any entity is matched.
234
+ - To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
235
+
236
+ Relations using sub-queries may be chained together.
237
+ See [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.
238
+
239
+ You can use the following table to help with constructing relation queries:
240
+
241
+ | Matches | Syntax |
242
+ | ------------------------------------------------------------------- | :--------------------------------------: |
243
+ | Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
244
+ | Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
245
+ | Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
246
+ | Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
247
+ | Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
248
+ | Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
214
249
 
215
250
  ```py
216
251
  >>> @attrs.define
217
252
  ... class OrbitOf: # OrbitOf component
218
253
  ... dist: int
219
254
  >>> LandedOn = "LandedOn" # LandedOn tag
220
- >>> star = world.new_entity()
221
- >>> planet = world.new_entity()
222
- >>> moon = world.new_entity()
223
- >>> ship = world.new_entity()
224
- >>> player = world.new_entity()
225
- >>> moon_rock = world.new_entity()
255
+ >>> star = registry.new_entity()
256
+ >>> planet = registry.new_entity()
257
+ >>> moon = registry.new_entity()
258
+ >>> ship = registry.new_entity()
259
+ >>> player = registry.new_entity()
260
+ >>> moon_rock = registry.new_entity()
226
261
  >>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)
227
262
  >>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)
228
263
  >>> ship.relation_tag[LandedOn] = moon
229
264
  >>> moon_rock.relation_tag[LandedOn] = moon
230
265
  >>> player.relation_tag[LandedOn] = moon_rock
231
- >>> set(world.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
266
+ >>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
232
267
  True
233
- >>> set(world.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
268
+ >>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
234
269
  True
235
- >>> set(world.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
270
+ >>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
236
271
  True
237
- >>> set(world.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
272
+ >>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
238
273
  True
239
- >>> set(world.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
274
+ >>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
240
275
  True
241
276
 
242
277
  ```
243
-
244
- ### Relation queries
245
-
246
- You can use the following table to help with constructing relation queries.
247
- `tag` is a component key if you are querying for a component relation.
248
-
249
- | Includes | Syntax |
250
- | --------------------------------------------------------------------- | :----: |
251
- | Entities with a relation tag to the given target | `(tag, target_entity)` |
252
- | Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
253
- | Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` |
254
- | The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
255
- | The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
256
- | The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` |
@@ -46,16 +46,6 @@ Changelog = "https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.m
46
46
  Documentation = "https://python-tcod-ecs.readthedocs.io"
47
47
  Source = "https://github.com/HexDecimal/python-tcod-ecs"
48
48
 
49
- [tool.black] # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file
50
- target-version = ["py38"]
51
- line-length = 120
52
-
53
- [tool.isort] # https://pycqa.github.io/isort/docs/configuration/options.html
54
- py_version = "38"
55
- line_length = 120
56
- profile = "black"
57
- skip_gitignore = true
58
-
59
49
  [tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html
60
50
  files = "."
61
51
  exclude = ['^build/', '^\.']
@@ -86,42 +76,23 @@ testpaths = ["."]
86
76
  exclude_lines = ['^\s*\.\.\.', "if TYPE_CHECKING:", "# pragma: no cover"]
87
77
 
88
78
  [tool.ruff]
89
- # https://beta.ruff.rs/docs/rules/
90
- select = [
91
- "C90", # mccabe
92
- "E", # pycodestyle
93
- "W", # pycodestyle
94
- "F", # Pyflakes
95
- "I", # isort
96
- "UP", # pyupgrade
97
- "YTT", # flake8-2020
98
- "ANN", # flake8-annotations
99
- "S", # flake8-bandit
100
- "B", # flake8-bugbear
101
- "C4", # flake8-comprehensions
102
- "DTZ", # flake8-datetimez
103
- "EM", # flake8-errmsg
104
- "EXE", # flake8-executable
105
- "RET", # flake8-return
106
- "ICN", # flake8-import-conventions
107
- "PIE", # flake8-pie
108
- "PT", # flake8-pytest-style
109
- "SIM", # flake8-simplify
110
- "PTH", # flake8-use-pathlib
111
- "PL", # Pylint
112
- "TRY", # tryceratops
113
- "RUF", # NumPy-specific rules
114
- "G", # flake8-logging-format
115
- "D", # pydocstyle
116
- ]
79
+ line-length = 120
80
+
81
+ [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/
82
+ select = ["ALL"]
117
83
  ignore = [
118
- "E501", # line-too-long
119
- "S101", # assert
120
84
  "ANN101", # missing-type-self
121
85
  "ANN102", # missing-type-cls
86
+ "COM", # flake8-commas, handled by formatter
87
+ "E501", # line-too-long
88
+ "S101", # assert
89
+ "SLF001", # private-member-access
90
+ "T10", # flake8-debugger
91
+ "T20", # flake8-print
122
92
  ]
123
- line-length = 120
124
93
 
125
- [tool.ruff.pydocstyle]
126
- # Use Google-style docstrings.
127
- convention = "google"
94
+ [tool.ruff.lint.isort]
95
+ required-imports = ["from __future__ import annotations"]
96
+
97
+ [tool.ruff.lint.pydocstyle]
98
+ convention = "google" # Use Google-style docstrings.
@@ -1,4 +1,5 @@
1
1
  """A type-hinted Entity Component System based on Python dictionaries and sets."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import importlib.metadata
@@ -7,12 +8,14 @@ from typing import TypeVar
7
8
 
8
9
  from tcod.ecs.constants import IsA
9
10
  from tcod.ecs.entity import Entity
10
- from tcod.ecs.world import World
11
+ from tcod.ecs.registry import Registry
12
+ from tcod.ecs.registry import Registry as World
11
13
 
12
14
  __all__ = (
13
15
  "__version__",
14
16
  "Entity",
15
17
  "IsA",
18
+ "Registry",
16
19
  "World",
17
20
  )
18
21