Phase 13: Inching into an ECSversion of the game by refactoring everything relevant to GameEngine into System.

master
Zed A. Shaw 11 months ago
parent a4dec0e952
commit 7133078a1f
  1. 62
      ecs.py
  2. 149
      phase_13/ecs.py
  3. 207
      phase_13/game.py
  4. 10
      phase_13/utils.py

@ -1,62 +0,0 @@
class ECS:
def __init__(self):
self.entities = {}
self.facts = {}
self.id_counter = 0
def entity(self):
self.id_counter += 1
return self.id_counter
def set(self, entity_id, obj):
name = obj.__class__.__qualname__
target = self.entities.get(name, {})
target[entity_id] = obj
self.entities[name] = target
def query(self, cls):
return self.entities[cls.__qualname__].items()
class Systems:
def __init__(self, ecs):
self.ecs = ecs
def play_sounds(self):
for eid, entity in ecs.query(Sound):
print("TALKING: ", entity.text)
def combat(self):
for eid, entity in ecs.query(Combat):
print("FIGHT: ", entity.hp)
def movement(self):
for eid, entity in ecs.query(Position):
print("MOVE: ", entity.x, entity.y)
class Combat:
def __init__(self, hp):
self.hp = hp
class Sound:
def __init__(self, text):
self.text = text
class Position:
def __init__(self, x, y):
self.x = x
self.y = y
ecs = ECS()
systems = Systems(ecs)
troll = ecs.entity()
ecs.set(troll, Combat(100))
ecs.set(troll, Sound("ROAR!"))
ecs.set(troll, Position(1, 2))
systems.play_sounds()
systems.combat()
systems.movement()

@ -0,0 +1,149 @@
import utils
import numpy as np
class Player:
def __init__(self, x, y):
self.x = x
self.y = y
self.symbol = '@'
self.name = 'You'
self.hp = 10
self.damage = 2
class Enemy:
def __init__(self, x, y, symbol):
self.x = x
self.y = y
self.symbol = symbol
self.name = 'Python'
self.hp = 5
self.damage = 1
self.hearing_distance = 5
class ECS:
def __init__(self):
self.entities = {}
self.facts = {}
self.id_counter = 0
def entity(self):
self.id_counter += 1
return self.id_counter
def set(self, entity_id, obj):
name = obj.__class__.__qualname__
target = self.entities.get(name, {})
target[entity_id] = obj
self.entities[name] = target
def query(self, cls):
return self.entities[cls.__qualname__].items()
class Systems:
def __init__(self, ecs, ui, the_map):
self.ecs = ecs
self.map = the_map
self.ui = ui
self.height = self.map.height
self.width = self.map.width
self.paths = np.full((self.height, self.width), utils.PATH_LIMIT, dtype=int)
def add_neighbors(self, neighbors, closed, near_y, near_x):
points = utils.compass(near_x, near_y)
for x,y in points:
if self.map.inbounds(x,y) and closed[y, x] == utils.SPACE:
closed[y, x] = utils.WALL
neighbors.append([x,y])
def path_enemies(self, in_grid):
self.paths.fill(utils.PATH_LIMIT)
closed = self.map.map.copy()
starting_pixels = []
open_pixels = []
counter = 0
while counter < self.height * self.width:
x = counter % self.width
y = counter // self.width
if in_grid[y, x] == 0:
self.paths[y, x] = 0
closed[y, x] = utils.WALL
starting_pixels.append([x,y])
counter += 1
for x, y in starting_pixels:
self.add_neighbors(open_pixels, closed, y, x)
counter = 1
while counter < utils.PATH_LIMIT and open_pixels:
next_open = []
for x,y in open_pixels:
self.paths[y, x] = counter
self.add_neighbors(next_open, closed, y, x)
open_pixels = next_open
counter += 1
for x, y in open_pixels:
self.paths[y, x] = counter
def move_enemies(self):
in_grid = np.full((self.map.height, self.map.width), 1, dtype=int)
in_grid[self.player.y, self.player.x] = 0
self.path_enemies(in_grid)
# for every enemy (actors[0] is player)
for enemy in self.actors[1:]:
nearby = utils.compass(enemy.x, enemy.y)
our_path = self.paths[enemy.y, enemy.x]
if our_path > enemy.hearing_distance: continue
for x, y in nearby:
if self.paths[y, x] <= our_path and not self.actor_collision(enemy, x, y):
enemy.x = x
enemy.y = y
break
def death(self, target):
self.actors.remove(target)
self.ui.post_status(f"Killed {target.name}")
def combat(self, actor, target):
target.hp -= actor.damage
if target.hp > 0:
self.ui.post_status(f"HIT {target.name} for {actor.damage}")
else:
self.death(target)
def actor_collision(self, actor, x, y):
for target in self.actors:
if target != actor and target.x == x and target.y == y:
return target
return None
def collision(self, actor, x, y):
if self.map.collision(x, y): return True
target = self.actor_collision(actor, x, y)
if target:
self.combat(actor, target)
return True
return False
def move_player(self, actor, x, y):
if not self.collision(actor, x, y):
actor.x = x
actor.y = y
def spawn_actors(self, enemy_count):
x, y = self.map.spawn()
self.player = Player(x, y)
self.actors = [self.player]
for i in range(0, enemy_count):
x, y = self.map.spawn()
enemy = Enemy(x, y, '{')
self.actors.append(enemy)

@ -0,0 +1,207 @@
import curses
import sys
import random
import numpy as np
import ecs
import utils
class Map:
def __init__(self, width, height):
self.width = width
self.height = height
grid = self.make_grid()
dead_ends = self.hunt_and_kill(grid)
grid = self.sample_rooms(grid, dead_ends, 4, int(len(dead_ends) * 0.6))
self.hunt_and_kill(grid)
self.render_map(grid)
def spawn(self):
while True:
y = random.randrange(0, self.height)
x = random.randrange(0, self.width)
if self.map[y][x] == utils.SPACE:
return x, y
def sample_rooms(self, grid, dead_ends, size, count):
grid = self.make_grid()
for x, y in random.sample(dead_ends, count):
if x < self.width - size and y < self.height - size:
self.make_room(grid, x, y, size)
return grid
def make_grid(self):
return np.full((self.height, self.width), utils.WALL, dtype=str)
def make_room(self, grid, x, y, size):
for row in range(y, y+size):
for col in range(x, x+size):
grid[row, col] = utils.SPACE
def find_coord(self, grid):
for y in range(1, self.height, 2):
for x in range(1, self.width, 2):
if grid[y, x] != utils.WALL: continue
found = self.neighbors(grid, x, y)
for found_x, found_y in found:
if grid[found_y, found_x] == utils.SPACE:
return [[x,y],[found_x, found_y]]
return None
def inbounds(self, x, y):
return x >= 0 and x < self.width and y >= 0 and y < self.height
def neighbors(self, grid, x, y):
points = utils.compass(x, y, 2)
result = []
for x,y in points:
if self.inbounds(x, y):
result.append([x,y])
return result
def neighbor_walls(self, grid, x, y):
neighbors = self.neighbors(grid, x, y)
result = []
for x,y in neighbors:
if grid[y, x] == utils.WALL:
result.append([x,y])
return result
def hunt_and_kill(self, grid):
on_x = 1
on_y = 1
dead_ends = []
while True:
n = self.neighbor_walls(grid, on_x, on_y)
if len(n) == 0:
dead_ends.append([on_x, on_y])
t = self.find_coord(grid)
if t == None: break
on_x, on_y = t[0]
found_x, found_y = t[1]
grid[on_y, on_x] = utils.SPACE
row = (on_y + found_y) // 2
col = (on_x + found_x) // 2
grid[row, col] = utils.SPACE
else:
nb_x, nb_y = random.choice(n)
grid[nb_y, nb_x] = utils.SPACE
row = (nb_y + on_y) // 2
col = (nb_x + on_x) // 2
grid[row, col] = utils.SPACE
on_x, on_y = nb_x, nb_y
return dead_ends
def render_map(self, grid):
self.map = np.full((self.height, self.width), '#', dtype=str)
for y, y_line in enumerate(grid):
for x, char in enumerate(y_line):
self.map[y, x] = char
def collision(self, target_x, target_y):
# remember this is True==COLLIDE WITH WALL, False=CAN MOVE THERE
return self.map[target_y][target_x] == utils.WALL
def draw(self, win):
for y, row in enumerate(self.map):
win.addstr(y, 0, "".join(row))
class UI:
def __init__(self, stdscr, height, width, status_height):
curses.curs_set(0)
stdscr.clear()
begin_x = 0
begin_y = 0
win = curses.newwin(height, width, begin_y, begin_x)
win.keypad(True)
status = win.subwin(status_height, width, height-status_height, begin_x)
# keep these for later by assigning to self
self.begin_x = 0
self.begin_y = 0
self.map = None
self.height = height
self.width = width
self.win = win
self.status = status
self.status_msg = "HAVE FUN!"
self.status_height = status_height
def set_map(self, the_map):
self.map = the_map
def update(self, actors):
assert self.map, "You forgot to call set_map()"
self.win.clear()
self.status.box()
self.map.draw(self.win)
# this assumes actors[0] is the player
self.draw_status(actors)
for actor in actors:
self.draw_actor(actor)
self.win.refresh()
def post_status(self, msg):
self.status_msg = msg
def draw_status(self, actors):
self.status.addstr(1, 1, self.status_msg)
def draw_actor(self, actor):
assert self.map.map[actor.y][actor.x] != utils.WALL, f"WHAT? actor at {actor.x},{actor.y} but that's a wall!"
# actor has to be moved in by 1 for the border
self.win.addstr(actor.y, actor.x, actor.symbol, curses.A_BOLD)
def handle_input(self, x, y):
ch = self.win.getch()
if ch == ord('q'):
sys.exit(0)
elif ch == curses.KEY_UP:
y = (y - 1) % self.height
elif ch == curses.KEY_DOWN:
y = (y + 1) % self.height
elif ch == curses.KEY_RIGHT:
x = (x + 1) % self.width
elif ch == curses.KEY_LEFT:
x = (x - 1) % self.width
return x, y
class GameEngine:
def __init__(self, ui):
self.ui = ui
self.map = Map(ui.width, ui.height - ui.status_height)
self.ui = ui
ui.set_map(self.map)
self.ecs = ecs.ECS()
self.systems = ecs.Systems(self.ecs, self.ui, self.map)
def run(self):
self.systems.spawn_actors(5)
self.systems.move_enemies()
while True:
# remember, first one has to be the player
self.ui.update(self.systems.actors)
new_x, new_y = self.ui.handle_input(self.systems.player.x, self.systems.player.y)
self.systems.move_player(self.systems.player, new_x, new_y)
self.systems.move_enemies()
def main(stdscr):
width=27
height=16
ui = UI(stdscr, height, width, 5)
game = GameEngine(ui)
game.run()
curses.wrapper(main)

@ -0,0 +1,10 @@
WALL = '#'
SPACE = '.'
PATH_LIMIT = 1000
def compass(x, y, offset=1):
return [[x, y - offset], # North
[x, y + offset], # South
[x + offset, y], # East
[x - offset, y]] # West
Loading…
Cancel
Save