diff --git a/run.py b/run.py index acf3973..ce7f4df 100644 --- a/run.py +++ b/run.py @@ -1,20 +1,53 @@ +import random +import numpy as np +from typing import Optional, List + import arcade from arcade.gui import UIManager from arcade.gui.widgets import UITextArea +from src.being_sprite import BeingSprite from src.world import World -SCREEN_WIDTH = 800 -SCREEN_HEIGHT = 500 +WORLD_SIZE = 800 +SIDEBAR_WIDTH = 200 + +SCREEN_WIDTH = WORLD_SIZE + SIDEBAR_WIDTH +SCREEN_HEIGHT = WORLD_SIZE SCREEN_TITLE = "Evolving Beings" DEBUG_PADDING = 10 +FPS_LIMIT = 120 +INITIAL_POPULATION = 1000 + +FOOD_SCALE = 0.25 +TILE_SCALE = 1 + +TILE_SIZE = 64 class EvolvingBeings(arcade.Window): def __init__(self): - super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) + super().__init__( + width=SCREEN_WIDTH, + height=SCREEN_HEIGHT, + title=SCREEN_TITLE, + update_rate=1/FPS_LIMIT, + ) + + self.background_color = arcade.color.DARK_BROWN - self.world = World(250, 250) + self.manager: Optional[UIManager] = None + self.debug_text: Optional[UITextArea] = None + self.world: Optional[World] = None + self.grid_sprite_list: Optional[arcade.SpriteList] = None + self.food_sprite_list: Optional[arcade.SpriteList] = None + self.bg_sprite_list: Optional[arcade.SpriteList] = None + self.fps: List[float] = [] + self.available_sprites: List[int] = [] + + def setup(self): + """Sets up the world for the current simulation""" + self.world = World(WORLD_SIZE, WORLD_SIZE) self.manager = UIManager() self.manager.enable() @@ -24,36 +57,34 @@ def __init__(self): y=-DEBUG_PADDING, width=SCREEN_WIDTH - SCREEN_HEIGHT - DEBUG_PADDING, height=SCREEN_HEIGHT - DEBUG_PADDING, - text='Testing this', + text='', text_color=(255, 255, 255, 255), ) - self.manager.add(self.debug_text) - self.background_color = arcade.color.BLACK - # cell rendering self.grid_sprite_list = arcade.SpriteList() + self.food_sprite_list = arcade.SpriteList() - def setup(self): - """Sets up the world for the current simulation""" - self.world.spawn(10) - - def on_key_press(self, key: int, modifiers: int): - """Processes key presses + # create beings + for _ in range(INITIAL_POPULATION): + idx = len(self.grid_sprite_list) + location, _ = self.world.spawn(idx) - Arguments: - key {int} -- Which key was pressed - modifiers {int} -- Which modifiers were down at the time - """ + being_sprite = BeingSprite() + being_sprite.center_x = location[0] + SIDEBAR_WIDTH + being_sprite.center_y = location[1] - def on_key_release(self, key: int, modifiers: int): - """Processes key releases + self.grid_sprite_list.append(being_sprite) - Arguments: - key {int} -- Which key was released - modifiers {int} -- Which modifiers were down at the time - """ + self.bg_sprite_list = arcade.SpriteList(use_spatial_hash=True) + for x in range(SIDEBAR_WIDTH, SCREEN_WIDTH, TILE_SIZE): + for y in range(0, SCREEN_HEIGHT, TILE_SIZE): + bg_tile_idx = random.randint(1, 2) + bg = arcade.Sprite(f":resources:images/topdown_tanks/tileSand{bg_tile_idx}.png", TILE_SCALE) + bg.center_x = x + TILE_SIZE / 2 + bg.center_y = y + TILE_SIZE / 2 + self.bg_sprite_list.append(bg) def on_update(self, delta_time: float): """Updates the position of all game objects @@ -61,16 +92,41 @@ def on_update(self, delta_time: float): Arguments: delta_time {float} -- How much time since the last call """ - self.debug_text.text = f'FPS: {int(1 / delta_time)}\n' \ - f'Beings alive: {self.world.beings()}\n' \ + self.fps.append(1 / delta_time) + if len(self.fps) > 100: + self.fps.pop(0) + self.debug_text.text = f'FPS: {round(np.mean(self.fps))}/{FPS_LIMIT}\n' \ + f'Beings alive: {self.world.alive}\n' \ + f'Sprite buffer: {len(self.available_sprites)}\n' \ + + idx = 0 + for location, being in self.world.locations.items(): + self.grid_sprite_list[idx].angle = being.angle + if being.speed > 0: + self.grid_sprite_list[idx].center_x = location[0] + SIDEBAR_WIDTH + self.grid_sprite_list[idx].center_y = location[1] + + idx += 1 + + dead_sprites = self.world.step() + for sprite_index in dead_sprites: + self.grid_sprite_list[sprite_index].visible = False + self.available_sprites.append(sprite_index) def on_draw(self): self.clear() self.manager.draw() + self.bg_sprite_list.draw() + # self.food_sprite_list.draw() + self.grid_sprite_list.draw() + if __name__ == "__main__": + print('Init Evolving Beings') + window = EvolvingBeings() window.setup() + arcade.run() diff --git a/src/being.py b/src/being.py index 9dbb3af..966a6c6 100644 --- a/src/being.py +++ b/src/being.py @@ -3,8 +3,8 @@ import random # Hyper-parameters -ENERGY_LOSS_GENERAL = 0.1 -ENERGY_LOSS_ACTIONS = 0.2 +ENERGY_LOSS_GENERAL = 0.001 +ENERGY_LOSS_ACTIONS = 0.005 FOOD_TO_ENERGY = 0.1 WATER_TO_ENERGY = 0.1 @@ -21,28 +21,31 @@ class Being: Energy goes down when performing actions, low energy leads to lower happiness. """ - # Subjective states (things the "brain" feels) - happiness = 1 - hunger = 0 - thirst = 0 + def __init__(self, sprite_index): + # Subjective states (things the "brain" feels) + self.happiness = 1 + self.hunger = 0 + self.thirst = 0 - # Objective states (hidden from the "brain") - food = 1 - water = 1 - energy = 1 + # Objective states (hidden from the "brain") + self.food = 1 + self.water = 1 + self.energy = 1 - direction = [1, 0] # vector from (0,0) (the being) to the direction its facing - action_space = ['NOOP', 'TURN_LEFT', 'TURN_RIGHT', 'MOVE', 'EAT', 'DRINK'] + self.angle = 0 + self.direction = [1, 0] # vector from (0,0) (the being) to the direction its facing + self.speed = 0 + self.action_space = ['NOOP', 'TURN_LEFT', 'TURN_RIGHT', 'MOVE', 'STOP', 'EAT', 'DRINK'] + + self.sprite_index = sprite_index def choose_action(self): action = random.choice(self.action_space) - if action != 'NOOP': - self.energy -= ENERGY_LOSS_ACTIONS - if action == 'TURN_LEFT' or action == 'TURN_RIGHT': rot = rot_left if action == 'TURN_LEFT' else rot_right self.direction = np.round(np.dot(rot, self.direction), 0).astype(int) + self.angle += theta if action == 'TURN_LEFT' else -theta return action @@ -57,5 +60,8 @@ def step(self): self.water -= WATER_TO_ENERGY self.energy += WATER_TO_ENERGY - def color(self): - return 155 + self.energy * 100 \ No newline at end of file + if self.speed > 0: + self.energy -= ENERGY_LOSS_ACTIONS + + def is_alive(self): + return self.energy > 0 \ No newline at end of file diff --git a/src/being_sprite.py b/src/being_sprite.py new file mode 100644 index 0000000..783a7c2 --- /dev/null +++ b/src/being_sprite.py @@ -0,0 +1,11 @@ +import arcade + +BEING_SCALE = 0.1 + + +class BeingSprite(arcade.Sprite): + def __init__(self): + super().__init__() + + self.scale = BEING_SCALE + self.texture = arcade.load_texture(":resources:images/enemies/slimeBlue.png") diff --git a/src/cell.py b/src/cell.py deleted file mode 100644 index 0446671..0000000 --- a/src/cell.py +++ /dev/null @@ -1,17 +0,0 @@ -class Cell: - def __init__(self, x, y): - self.x = x - self.y = y - self.type = 'NONE' - self.content = None - - def update(self, type, content=None): - self.type = type - self.content = content - - def color(self): - if self.type == 'NONE': - return 0 - - if self.type == 'BEING': - return self.content.color() \ No newline at end of file diff --git a/src/world.py b/src/world.py index 15a429b..b410b60 100644 --- a/src/world.py +++ b/src/world.py @@ -1,70 +1,65 @@ -import numpy as np import random +import operator from src.being import Being -from src.cell import Cell class World: def __init__(self, w=128, h=128): self.w = w self.h = h - self.state = np.empty((w, h), dtype=object) - for i in range(w): - for j in range(h): - self.state[i, j] = Cell(i, j) + self.alive = 0 + + self.locations = dict() def step(self): - state = np.copy(self.state) + self.alive = 0 + new_locations = dict() + dead_sprites = [] + for location, being in self.locations.items(): + if not being.is_alive(): + dead_sprites.append(being.sprite_index) + continue + + self.alive += 1 + + being.step() + action = being.choose_action() + + if action == 'STOP': + being.speed = 0 - for i, row in enumerate(self.state): - for j, cell in enumerate(row): - if cell.type != 'BEING': + if action == 'MOVE': + being.speed = 1 + + if being.speed > 0: + new_location = ( + max(0, min(self.w, location[0] + being.direction[0])), + max(0, min(self.h, location[1] + being.direction[1])), + ) + + if new_location not in self.locations and new_location not in new_locations: + new_locations[new_location] = being continue - cell.content.step() - action = cell.content.choose_action() + new_locations[location] = being - if action == 'MOVE': - # lets see if the desired cell is empty - direction = cell.content.direction - next_loc = [ - max(0, min(self.w - 1, cell.x + direction[0])), - max(0, min(self.h - 1, cell.y + direction[1])) - ] + self.locations = new_locations - if state[next_loc[0], next_loc[1]].type == 'NONE': - # cell is empty, lets move! - state[next_loc[0], next_loc[1]].update('BEING', cell.content) - state[i, j].update('NONE') + return dead_sprites - self.state = state + def spawn(self, sprite_index): + x = random.randint(0, self.w - 1) + y = random.randint(0, self.h - 1) + location = (x, y) - def spawn(self, number): - for _ in range(number): + while location in self.locations: x = random.randint(0, self.w - 1) y = random.randint(0, self.h - 1) + location = (x, y) + + self.alive += 1 + being = Being(sprite_index) + self.locations[location] = being - while self.state[x, y].type != 'NONE': - x = random.randint(0, self.w - 1) - y = random.randint(0, self.h - 1) - # TODO: this could be infinite - - being = Being() - self.state[x, y].update('BEING', being) - - def beings(self): - num = 0 - for row in self.state: - for cell in row: - if cell.type is 'BEING' and cell.content.energy > 0: - num += 1 - return num - - def render(self): - state = np.zeros((self.w, self.h)) - for i, row in enumerate(self.state): - for j, cell in enumerate(row): - state[i, j] = cell.color() - - return state + return location, being