punyecs 0.1.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.
punyecs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.3
2
+ Name: punyecs
3
+ Version: 0.1.0
4
+ Summary: A simple attribute-based approach to ECS.
5
+ Author-email: csumnicht@berkeley.edu
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+
9
+ # punyecs
10
+
11
+ `punyecs` is a tiny Entity Component System (ECS) inspired by [tiny-ecs](https://github.com/bakpakin/tiny-ecs) for Python. `punyecs` operates directly on class attributes as opposed to creating components along with querying mechanisms for fine grain control over which objects are operated on by systems similar to how tiny-ecs works on Lua tables.
12
+
13
+ # What is it?
14
+
15
+ Instead of requiring inheritance, one can specify which attributes to operate on and any object (regardless of class) that has those attributes is operated on. That is, if a `Player` has an `x` and `y` attribute and an (unrelated) `Enemy` class has an `x` and `y` attribute you can have them both influenced by a `World` object. This avoids complicated inheritance hierarchies.
16
+
17
+ Here is a small example to illustrate the above:
18
+
19
+ ```py
20
+ from dataclasses import dataclass
21
+ from punyecs import World, requirements
22
+
23
+ w = World()
24
+
25
+ @dataclass
26
+ class Player:
27
+ x: float
28
+ y: float
29
+
30
+ @dataclass
31
+ class Enemy:
32
+ x: float
33
+ y: float
34
+
35
+ @requirements(w, {"x", "y"})
36
+ def move(e, dt):
37
+ e.x += 0.1
38
+ e.y += 0.1
39
+
40
+ player = Player(0.0, 0.0)
41
+ enemy = Enemy(1.0, 1.0)
42
+ w.add(player)
43
+ w.add(enemy)
44
+
45
+ w.update(1)
46
+ print(player.x)
47
+ # Prints 0.1
48
+ print(player.y)
49
+ # Prints 0.1
50
+
51
+ print(enemy.x)
52
+ # Prints 1.1
53
+ print(enemy.y)
54
+ # Prints 1.1
55
+ ```
56
+
57
+ # A Bit More Sophistication
58
+
59
+ We may also do exclusions for fine grain control. Returning to the example above, we may want various enemies to move like above but instead want to allow controller input for the `player` object. We can avoid influencing the `player` object by putting it in the excluded objects list. The function `f` becomes:
60
+
61
+ ```py
62
+ @requirements(w, {"x", "y"}, exclude_objs=[player])
63
+ def move(e, dt):
64
+ e.x += 0.1
65
+ e.y += 0.1
66
+ ```
67
+
68
+ Then after every `world.update(1)`, the `player` object *will still remain at* `x=0.0`, `y=0.0`.
69
+
70
+ # Even More Sophistication!
71
+
72
+ It might be inconvenient to exclude *individual* objects if a large number of objects need to be excluded. `punyecs` provides a couple more filtering options. One way around this is to specify which attributes an object should *not* have.
73
+
74
+ For instance, we may have many different kinds of creatures. Most can follow the usual movement update function, but some creatures have a `wiggle` attribute. `wiggle` could be a Boolean, or even something more sophisticated like a function that describes how the creature wiggles.
75
+
76
+ To illustrate this consider:
77
+
78
+ ```py
79
+ from dataclasses import dataclass
80
+ from punyecs import World, requirements
81
+
82
+ w = World()
83
+
84
+ @dataclass
85
+ class Player:
86
+ x: float
87
+ y: float
88
+
89
+ @dataclass
90
+ class WalkingEnemy:
91
+ x: float
92
+ y: float
93
+
94
+ @dataclass
95
+ class Wiggler:
96
+ x: float
97
+ y: float
98
+ wiggle: lambda x: x + 2
99
+
100
+ @requirements(w, {"x", "y"}, exclude={"wiggle"})
101
+ def move(e, dt):
102
+ e.x += 0.1
103
+ e.y += 0.1
104
+
105
+ @requirements(w, {"wiggle", "x", "y"})
106
+ def wiggle(e, dt):
107
+ e.x = wiggle(e.x)
108
+ e.y = wiggle(e.y)
109
+
110
+
111
+ player = Player(0.0, 0.0)
112
+ enemy = Enemy(1.0, 1.0)
113
+ wiggler = Wiggle(3.0, 3.0)
114
+ w.add(player)
115
+ w.add(enemy)
116
+ w.add(wiggler)
117
+
118
+ w.update(1)
119
+ print(player.x)
120
+ # Prints 0.1
121
+ print(player.y)
122
+ # Prints 0.1
123
+
124
+ print(enemy.x)
125
+ # Prints 1.1
126
+ print(enemy.y)
127
+ # Prints 1.1
128
+
129
+ print(wiggler.x)
130
+ # Prints 5.0
131
+ print(wiggler.y)
132
+ # Prints 5.0
133
+ ```
134
+
135
+ Thus, `move` does not operate on `wiggler` but `wiggle` does.
136
+
137
+ # Documentation
138
+
139
+ Even more filtering options are available. To learn more, see the [readthedocs.](https://punyecs.readthedocs.io/en/latest/api.html)
@@ -0,0 +1,131 @@
1
+ # punyecs
2
+
3
+ `punyecs` is a tiny Entity Component System (ECS) inspired by [tiny-ecs](https://github.com/bakpakin/tiny-ecs) for Python. `punyecs` operates directly on class attributes as opposed to creating components along with querying mechanisms for fine grain control over which objects are operated on by systems similar to how tiny-ecs works on Lua tables.
4
+
5
+ # What is it?
6
+
7
+ Instead of requiring inheritance, one can specify which attributes to operate on and any object (regardless of class) that has those attributes is operated on. That is, if a `Player` has an `x` and `y` attribute and an (unrelated) `Enemy` class has an `x` and `y` attribute you can have them both influenced by a `World` object. This avoids complicated inheritance hierarchies.
8
+
9
+ Here is a small example to illustrate the above:
10
+
11
+ ```py
12
+ from dataclasses import dataclass
13
+ from punyecs import World, requirements
14
+
15
+ w = World()
16
+
17
+ @dataclass
18
+ class Player:
19
+ x: float
20
+ y: float
21
+
22
+ @dataclass
23
+ class Enemy:
24
+ x: float
25
+ y: float
26
+
27
+ @requirements(w, {"x", "y"})
28
+ def move(e, dt):
29
+ e.x += 0.1
30
+ e.y += 0.1
31
+
32
+ player = Player(0.0, 0.0)
33
+ enemy = Enemy(1.0, 1.0)
34
+ w.add(player)
35
+ w.add(enemy)
36
+
37
+ w.update(1)
38
+ print(player.x)
39
+ # Prints 0.1
40
+ print(player.y)
41
+ # Prints 0.1
42
+
43
+ print(enemy.x)
44
+ # Prints 1.1
45
+ print(enemy.y)
46
+ # Prints 1.1
47
+ ```
48
+
49
+ # A Bit More Sophistication
50
+
51
+ We may also do exclusions for fine grain control. Returning to the example above, we may want various enemies to move like above but instead want to allow controller input for the `player` object. We can avoid influencing the `player` object by putting it in the excluded objects list. The function `f` becomes:
52
+
53
+ ```py
54
+ @requirements(w, {"x", "y"}, exclude_objs=[player])
55
+ def move(e, dt):
56
+ e.x += 0.1
57
+ e.y += 0.1
58
+ ```
59
+
60
+ Then after every `world.update(1)`, the `player` object *will still remain at* `x=0.0`, `y=0.0`.
61
+
62
+ # Even More Sophistication!
63
+
64
+ It might be inconvenient to exclude *individual* objects if a large number of objects need to be excluded. `punyecs` provides a couple more filtering options. One way around this is to specify which attributes an object should *not* have.
65
+
66
+ For instance, we may have many different kinds of creatures. Most can follow the usual movement update function, but some creatures have a `wiggle` attribute. `wiggle` could be a Boolean, or even something more sophisticated like a function that describes how the creature wiggles.
67
+
68
+ To illustrate this consider:
69
+
70
+ ```py
71
+ from dataclasses import dataclass
72
+ from punyecs import World, requirements
73
+
74
+ w = World()
75
+
76
+ @dataclass
77
+ class Player:
78
+ x: float
79
+ y: float
80
+
81
+ @dataclass
82
+ class WalkingEnemy:
83
+ x: float
84
+ y: float
85
+
86
+ @dataclass
87
+ class Wiggler:
88
+ x: float
89
+ y: float
90
+ wiggle: lambda x: x + 2
91
+
92
+ @requirements(w, {"x", "y"}, exclude={"wiggle"})
93
+ def move(e, dt):
94
+ e.x += 0.1
95
+ e.y += 0.1
96
+
97
+ @requirements(w, {"wiggle", "x", "y"})
98
+ def wiggle(e, dt):
99
+ e.x = wiggle(e.x)
100
+ e.y = wiggle(e.y)
101
+
102
+
103
+ player = Player(0.0, 0.0)
104
+ enemy = Enemy(1.0, 1.0)
105
+ wiggler = Wiggle(3.0, 3.0)
106
+ w.add(player)
107
+ w.add(enemy)
108
+ w.add(wiggler)
109
+
110
+ w.update(1)
111
+ print(player.x)
112
+ # Prints 0.1
113
+ print(player.y)
114
+ # Prints 0.1
115
+
116
+ print(enemy.x)
117
+ # Prints 1.1
118
+ print(enemy.y)
119
+ # Prints 1.1
120
+
121
+ print(wiggler.x)
122
+ # Prints 5.0
123
+ print(wiggler.y)
124
+ # Prints 5.0
125
+ ```
126
+
127
+ Thus, `move` does not operate on `wiggler` but `wiggle` does.
128
+
129
+ # Documentation
130
+
131
+ Even more filtering options are available. To learn more, see the [readthedocs.](https://punyecs.readthedocs.io/en/latest/api.html)
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "punyecs"
3
+ version = "0.1.0"
4
+ description = "A simple attribute-based approach to ECS."
5
+ readme = "README.md"
6
+ authors = [
7
+ { email = "csumnicht@berkeley.edu" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = []
11
+
12
+ [tool.pyrefly]
13
+ project-includes = [
14
+ "**/*.py*",
15
+ "**/*.ipynb",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.5,<0.10.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pytest>=9.0.3",
25
+ ]
26
+ docs = [
27
+ "sphinx>=9.0.4",
28
+ "sphinx-automodapi>=0.22.0",
29
+ ]
@@ -0,0 +1,111 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Callable
3
+
4
+
5
+ @dataclass
6
+ class Query:
7
+ """A class that represnets which attributes and objects should be allowed
8
+ (or disallowed) in a group."""
9
+ and_attr: set[str] = field(default_factory=set)
10
+ exclude_attr: set[str] = field(default_factory=set)
11
+ exclude_objs: list[Any] = field(default_factory=list)
12
+ exclude_attr_vals: dict[str, Any] = field(default_factory=dict)
13
+
14
+
15
+ @dataclass
16
+ class World:
17
+ groups: list[tuple[Query, list, list[Callable[[Any, float], None]]]] = \
18
+ field(default_factory=list)
19
+
20
+ def entity_satisfies_query(self, entity, query) -> bool:
21
+ """Check if an entity should (or should not) be added to a particular
22
+ group by analyzing the query structure.
23
+
24
+ :param entity: The entity to query.
25
+ :param query: The query to check if the entity can belong to it.
26
+ """
27
+ for e in query.exclude_objs:
28
+ if entity == e:
29
+ return False
30
+ for attr in query.and_attr:
31
+ if not hasattr(entity, attr):
32
+ return False
33
+ for attr in query.exclude_attr:
34
+ if hasattr(entity, attr):
35
+ return False
36
+ for attr, val in query.exclude_attr_vals.items():
37
+ if hasattr(entity, attr) and getattr(entity, attr) == val:
38
+ return False
39
+ return True
40
+
41
+ def push_group(self, query: Query):
42
+ """Add the group and return that group.
43
+
44
+ :param query: The query to associate with the group.
45
+ """
46
+ new_group = (query, [], [])
47
+ self.groups.append(new_group)
48
+ return new_group
49
+
50
+ def add(self, entity: Any):
51
+ """Add an entity to the world. Under the hood, determines what groups
52
+ the entity should belong to.
53
+
54
+ :param entity: The entity to add to the world.
55
+ """
56
+ for query, group, funcs in self.groups:
57
+ if self.entity_satisfies_query(entity, query):
58
+ group.append(entity)
59
+
60
+ def update(self, dt: float):
61
+ """Update the world (and all the corresponding groups/entities).
62
+
63
+ :param dt: The amount of time that elapsed. (Common for videogame
64
+ applications)
65
+ """
66
+ for _, group, funcs in self.groups:
67
+ for func in funcs:
68
+ for entity in group:
69
+ func(entity, dt)
70
+
71
+
72
+ def requirements(world: World,
73
+ require: set[str],
74
+ exclude: set[str] | None=None,
75
+ exclude_objs: list[Any] | None=None,
76
+ exclude_attr_vals: dict[str, Any] | None = None):
77
+ """Use as a decorator, runs the decorated function on each entity that
78
+ has the required components and none of the excluded components (or
79
+ excluded objects).
80
+
81
+ :param require: Required attribute for an entity to be ran.
82
+ :param exclude: Entity must *not* have the following attributes.
83
+ :param exclude_objs: Exculde individual objects from being ran.
84
+ """
85
+ exclude = exclude or set()
86
+ exclude_objs = exclude_objs or []
87
+ exclude_attr_vals = exclude_attr_vals or {}
88
+ def req_dec(func):
89
+ query = Query(require, exclude, exclude_objs, exclude_attr_vals)
90
+ group = world.push_group(query)
91
+ def inner(e, dt):
92
+ return func(e, dt)
93
+ group[2].append(inner)
94
+ return inner
95
+ return req_dec
96
+
97
+ def query(world: World, query: Query):
98
+ """Use as a decorator, runs the decorated function on each entity that
99
+ satisfy the query object (similar to ``requirements`` but takes in a
100
+ Query object directly. ``requirements`` builds a query object.
101
+
102
+ :param world: World to query over.
103
+ :param query: Query to execute against.
104
+ """
105
+ def query_dec(func):
106
+ group = world.push_group(query)
107
+ def inner(e, dt):
108
+ return func(e, dt)
109
+ group[2].append(inner)
110
+ return inner
111
+ return query_dec
File without changes