Skip to content

Commit

Permalink
feat: setup basic rendering engine
Browse files Browse the repository at this point in the history
  • Loading branch information
aurbano committed May 15, 2022
1 parent 3f99448 commit 92d9894
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 109 deletions.
108 changes: 82 additions & 26 deletions run.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -24,53 +57,76 @@ 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
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()
40 changes: 23 additions & 17 deletions src/being.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
if self.speed > 0:
self.energy -= ENERGY_LOSS_ACTIONS

def is_alive(self):
return self.energy > 0
11 changes: 11 additions & 0 deletions src/being_sprite.py
Original file line number Diff line number Diff line change
@@ -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")
17 changes: 0 additions & 17 deletions src/cell.py

This file was deleted.

93 changes: 44 additions & 49 deletions src/world.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 92d9894

Please sign in to comment.