Skip to content

Commit 42b7daa

Browse files
committed
Implement entity examples
1 parent 812f09d commit 42b7daa

11 files changed

Lines changed: 158 additions & 21 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
},
1515
"cSpell.words": [
1616
"isort",
17+
"PAGEDOWN",
18+
"PAGEUP",
1719
"tcod",
1820
"tileset",
1921
"tilesheet"

g.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import tcod.console
55
import tcod.context
6+
import tcod.ecs
67

78
import game.state
89

@@ -14,3 +15,6 @@
1415

1516
states: list[game.state.State] = []
1617
"""A stack of states with the last item being the active state."""
18+
19+
world: tcod.ecs.Registry
20+
"""The active ECS registry and current session."""

game/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Game namespace package."""

game/components.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Collection of common components."""
2+
from __future__ import annotations
3+
4+
from typing import Final, Self
5+
6+
import attrs
7+
import tcod.ecs.callbacks
8+
from tcod.ecs import Entity
9+
10+
11+
@attrs.define(frozen=True)
12+
class Position:
13+
"""An entities position."""
14+
15+
x: int
16+
y: int
17+
18+
def __add__(self, direction: tuple[int, int]) -> Self:
19+
"""Add a vector to this position."""
20+
x, y = direction
21+
return self.__class__(self.x + x, self.y + y)
22+
23+
24+
@tcod.ecs.callbacks.register_component_changed(component=Position)
25+
def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None:
26+
"""Mirror position components as a tag."""
27+
if old == new:
28+
return
29+
if old is not None:
30+
entity.tags.discard(old)
31+
if new is not None:
32+
entity.tags.add(new)
33+
34+
35+
@attrs.define(frozen=True)
36+
class Graphic:
37+
"""An entities icon and color."""
38+
39+
ch: int = ord("!")
40+
fg: tuple[int, int, int] = (255, 255, 255)
41+
42+
43+
Gold: Final = ("Gold", int)
44+
"""Amount of gold."""

game/state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
class State(Protocol):
1111
"""An abstract game state."""
1212

13+
__slots__ = ()
14+
1315
def on_event(self, event: tcod.event.Event) -> None:
1416
"""Called on events."""
1517

game/state_tools.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""State handling functions."""
2+
from __future__ import annotations
3+
24
import tcod.console
35

46
import g
@@ -18,6 +20,5 @@ def main_loop() -> None:
1820
while g.states:
1921
main_draw()
2022
for event in tcod.event.wait():
21-
print(event)
2223
if g.states:
2324
g.states[-1].on_event(event)

game/states.py

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,72 @@
11
"""A collection of game states."""
22
from __future__ import annotations
33

4+
from typing import Final
5+
46
import attrs
57
import tcod.console
68
import tcod.event
79
from tcod.event import KeySym
810

11+
import g
12+
from game.components import Gold, Graphic, Position
13+
from game.tags import IsItem, IsPlayer
14+
15+
DIRECTION_KEYS: Final = {
16+
# Arrow keys
17+
KeySym.LEFT: (-1, 0),
18+
KeySym.RIGHT: (1, 0),
19+
KeySym.UP: (0, -1),
20+
KeySym.DOWN: (0, 1),
21+
# Arrow key diagonals
22+
KeySym.HOME: (-1, -1),
23+
KeySym.END: (-1, 1),
24+
KeySym.PAGEUP: (1, -1),
25+
KeySym.PAGEDOWN: (1, 1),
26+
# Keypad
27+
KeySym.KP_4: (-1, 0),
28+
KeySym.KP_6: (1, 0),
29+
KeySym.KP_8: (0, -1),
30+
KeySym.KP_2: (0, 1),
31+
KeySym.KP_7: (-1, -1),
32+
KeySym.KP_1: (-1, 1),
33+
KeySym.KP_9: (1, -1),
34+
KeySym.KP_3: (1, 1),
35+
# VI keys
36+
KeySym.h: (-1, 0),
37+
KeySym.l: (1, 0),
38+
KeySym.k: (0, -1),
39+
KeySym.j: (0, 1),
40+
KeySym.y: (-1, -1),
41+
KeySym.b: (-1, 1),
42+
KeySym.u: (1, -1),
43+
KeySym.n: (1, 1),
44+
}
945

10-
@attrs.define(eq=False)
11-
class ExampleState:
12-
"""Example state with a hard-coded player position."""
1346

14-
player_x: int
15-
"""Player X position, left-most position is zero."""
16-
player_y: int
17-
"""Player Y position, top-most position is zero."""
47+
@attrs.define()
48+
class InGame:
49+
"""Primary in-game state."""
1850

1951
def on_event(self, event: tcod.event.Event) -> None:
20-
"""Move the player on events and handle exiting. Movement is hard-coded."""
52+
"""Handle events for the in-game state."""
53+
(player,) = g.world.Q.all_of(tags=[IsPlayer])
2154
match event:
2255
case tcod.event.Quit():
2356
raise SystemExit()
24-
case tcod.event.KeyDown(sym=KeySym.LEFT):
25-
self.player_x -= 1
26-
case tcod.event.KeyDown(sym=KeySym.RIGHT):
27-
self.player_x += 1
28-
case tcod.event.KeyDown(sym=KeySym.UP):
29-
self.player_y -= 1
30-
case tcod.event.KeyDown(sym=KeySym.DOWN):
31-
self.player_y += 1
57+
case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS:
58+
player.components[Position] += DIRECTION_KEYS[sym]
59+
# Auto pickup gold
60+
for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]):
61+
player.components[Gold] += gold.components[Gold]
62+
print(f"Picked up {gold.components[Gold]}g, total: {player.components[Gold]}g")
63+
gold.clear()
3264

3365
def on_draw(self, console: tcod.console.Console) -> None:
34-
"""Draw the player with print. Bounds do not need to be checked with this function."""
35-
console.print(self.player_x, self.player_y, "@")
66+
"""Draw the standard screen."""
67+
for entity in g.world.Q.all_of(components=[Position, Graphic]):
68+
pos = entity.components[Position]
69+
if not (0 <= pos.x < console.width and 0 <= pos.y < console.height):
70+
continue
71+
graphic = entity.components[Graphic]
72+
console.rgb[["ch", "fg"]][pos.y, pos.x] = graphic.ch, graphic.fg

game/tags.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Collection of common tags."""
2+
from __future__ import annotations
3+
4+
from typing import Final
5+
6+
IsPlayer: Final = "IsPlayer"
7+
"""Entity is the player."""
8+
9+
IsActor: Final = "IsActor"
10+
"""Entity is an actor."""
11+
12+
IsItem: Final = "IsItem"
13+
"""Entity is an item."""

game/world_tools.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Functions for working with worlds."""
2+
from __future__ import annotations
3+
4+
from random import Random
5+
6+
from tcod.ecs import Registry
7+
8+
from game.components import Gold, Graphic, Position
9+
from game.tags import IsActor, IsItem, IsPlayer
10+
11+
12+
def new_world() -> Registry:
13+
"""Return a freshly generated world."""
14+
world = Registry()
15+
16+
rng = world[None].components[Random] = Random()
17+
18+
player = world[object()]
19+
player.components[Position] = Position(5, 5)
20+
player.components[Graphic] = Graphic(ord("@"))
21+
player.components[Gold] = 0
22+
player.tags |= {IsPlayer, IsActor}
23+
24+
for _ in range(10):
25+
gold = world[object()]
26+
gold.components[Position] = Position(rng.randint(0, 20), rng.randint(0, 20))
27+
gold.components[Graphic] = Graphic(ord("$"), fg=(255, 255, 0))
28+
gold.components[Gold] = rng.randint(1, 10)
29+
gold.tags |= {IsItem}
30+
31+
return world

main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
import tcod.console
66
import tcod.context
7-
import tcod.event
87
import tcod.tileset
98

109
import g
1110
import game.state_tools
1211
import game.states
12+
import game.world_tools
1313

1414

1515
def main() -> None:
@@ -19,7 +19,8 @@ def main() -> None:
1919
)
2020
tcod.tileset.procedural_block_elements(tileset=tileset)
2121
g.console = tcod.console.Console(80, 50)
22-
g.states = [game.states.ExampleState(player_x=g.console.width // 2, player_y=g.console.height // 2)]
22+
g.states = [game.states.InGame()]
23+
g.world = game.world_tools.new_world()
2324
with tcod.context.new(console=g.console, tileset=tileset) as g.context:
2425
game.state_tools.main_loop()
2526

0 commit comments

Comments
 (0)