Browse Source

WIP: refactor + feat dashboard

pull/1/head
Michaël Costa 8 months ago
parent
commit
c9ac3f07cf
27 changed files with 312 additions and 1363 deletions
  1. +1
    -1
      .flake8
  2. +0
    -23
      ant/__init__.py
  3. +13
    -6
      ants.yml
  4. +30
    -19
      ants_sdl.py
  5. +1
    -0
      applicators/__init__.py
  6. +19
    -17
      applicators/collisionsystem.py
  7. +17
    -0
      applicators/dashboardsystem.py
  8. +33
    -35
      applicators/movementsystem.py
  9. +4
    -0
      objects/__init__.py
  10. +34
    -0
      objects/ant.py
  11. +38
    -0
      objects/baseobject.py
  12. +15
    -0
      objects/dashboard.py
  13. +107
    -0
      objects/utils.py
  14. +0
    -301
      old/ant.py
  15. +0
    -58
      old/ants.py
  16. +0
    -100
      old/config.py
  17. +0
    -238
      old/display.py
  18. +0
    -113
      old/farm.py
  19. +0
    -83
      old/mine.py
  20. +0
    -143
      old/playground.py
  21. +0
    -76
      old/utils.py
  22. +0
    -11
      renderer/__init__.py
  23. BIN
      resources/ants.bmp
  24. BIN
      resources/ants.png
  25. +0
    -105
      resources/ants.svg
  26. BIN
      screenshot.png
  27. +0
    -34
      utils/__init__.py

+ 1
- 1
.flake8 View File

@ -1,5 +1,5 @@
[flake8]
ignore = E111,I001,I004,I003,E265,E114,E501,I005
ignore = E111,I001,I004,I003,E265,E114,E501,I005,W503
exclude = .git,__pycache__
max-line-length = 160
use-flake8-tabs = true


+ 0
- 23
ant/__init__.py View File

@ -1,23 +0,0 @@
from uuid import uuid4
import sdl2
import sdl2.ext
from utils import Velocity, Direction
class Ant(sdl2.ext.Entity):
def __init__(self, world, sprite, hp=100, posx=0, posy=0, velocity=Velocity(0, 0)):
super(Ant, self).__init__()
self.ant = self
self.uuid = uuid4()
self.history = []
self.hp = hp
self.velocity = velocity
self.sprite = sprite
self.sprite.position = posx, posy
self.direction = Direction(Direction.random(), Direction.random())
def __str__(self):
return str(self.uuid)
def __repr__(self):
return str(self.uuid)

+ 13
- 6
ants.yml View File

@ -26,14 +26,21 @@ coloredlogs:
color: cyan
message:
color: white
playground:
width: 1280
height: 1024
sdl:
delay: 50
font:
filename: /usr/share/fonts/TTF/DejaVuSansMono.ttf
size: 32
delay: 125
dashboard:
width: 1024
height: 100
playground:
width: 1024
height: 668
ants:
max_ants: 5000
release_rate: 5000
max_ants: 4
max_memory: 20
hp: 1000
velocity:
vx: 1
vy: 1

+ 30
- 19
ants_sdl.py View File

@ -5,13 +5,14 @@ import sdl2.ext
import sdl2.sdlgfx
import logging
import ant
import log
import conf
import renderer
from objects import Ant, Dashboard
from applicators import MovementSystem
from applicators import DashboardSystem
from applicators import CollisionSystem
from utils import Velocity
def run(config):
@ -20,31 +21,41 @@ def run(config):
sdl2.ext.init()
window = sdl2.ext.Window("Ants: A Computer Assisted Bullshit for random based life form research",
size=(config["playground"]["width"], config["playground"]["height"]))
size=(config["sdl"]["playground"]["width"], config["sdl"]["playground"]["height"]))
window.show()
world = sdl2.ext.World()
movement = MovementSystem(0, 100, config["playground"]["width"], config["playground"]["height"])
# collision = CollisionSystem(0, 200, config["playground"]["width"], config["playground"]["height"] - 200)
movement_sys = MovementSystem(0, 100, config["sdl"]["playground"]["width"], config["sdl"]["playground"]["height"])
collision_sys = CollisionSystem(0, 100, config["sdl"]["playground"]["width"], config["sdl"]["playground"]["height"])
dashboard_sys = DashboardSystem(0, 0, config["sdl"]["playground"]["width"], config["sdl"]["dashboard"]["height"])
spriterenderer = renderer.SoftwareRenderer(window)
# world.add_system(collision)
world.add_system(movement)
dashboard = Dashboard(world,
config["sdl"]["dashboard"]["width"],
config["sdl"]["dashboard"]["height"],
config["sdl"]["font"]["filename"],
config["sdl"]["font"]["size"])
world.add_system(collision_sys)
world.add_system(movement_sys)
world.add_system(dashboard_sys)
world.add_system(spriterenderer)
factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
ants = []
# collision.ants = ants
running = True
max_ant_reached = False
while running:
if len(ants) < config["ants"]["max_ants"]:
ants.append(ant.Ant(world,
factory.from_color(renderer.colors["WHITE"], size=(3, 3)),
posx=round(config["playground"]["width"] / 2),
posy=round(config["playground"]["height"] / 2),
velocity=Velocity(config["ants"]["velocity"]["vx"], config["ants"]["velocity"]["vy"])))
logging.info("Total ants number: {}/{}".format(len(ants), config["ants"]["max_ants"]))
if len(collision_sys.ants) < config["ants"]["max_ants"] and not max_ant_reached:
new_ant = Ant(world, max_memory=config["ants"]["max_memory"], hp=config["ants"]["hp"])
new_ant_pos = (round(config["sdl"]["playground"]["width"] / 2), round(config["sdl"]["playground"]["height"] / 2))
new_ant.set_sprite("WHITE")
new_ant.set_position(new_ant_pos)
new_ant.set_velocity(config["ants"]["velocity"]["vx"], config["ants"]["velocity"]["vy"])
collision_sys.ants.append(new_ant)
else:
max_ant_reached = True
collision_sys.clean(world)
status_line = "Total ants number: {}/{}".format(len(collision_sys.ants), config["ants"]["max_ants"])
if len(collision_sys.ants) == 0:
status_line = "GAME OVER"
dashboard.set_sprite(status_line)
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:


+ 1
- 0
applicators/__init__.py View File

@ -1,2 +1,3 @@
from applicators.movementsystem import MovementSystem
from applicators.collisionsystem import CollisionSystem
from applicators.dashboardsystem import DashboardSystem

+ 19
- 17
applicators/collisionsystem.py View File

@ -1,32 +1,34 @@
import sdl2
import sdl2.ext
import sdl2.sdlgfx
from utils import Velocity
from objects import Ant
class CollisionSystem(sdl2.ext.Applicator):
def __init__(self, minx, miny, maxx, maxy):
super(CollisionSystem, self).__init__()
self.componenttypes = Velocity, sdl2.ext.Sprite
self.ants = None
self.componenttypes = Ant, sdl2.ext.Sprite
self.ants = []
self.minx = minx
self.miny = miny
self.maxx = maxx
self.maxy = maxy
def _overlap(self, item):
pos, sprite = item
ant_sprites = [ant.sprite for ants in self.ants]
if sprite == self.ball.sprite:
return False
left, top, right, bottom = sprite.area
bleft, btop, bright, bbottom = self.ball.sprite.area
def clean(self, world):
dead_ants = [ant for ant in self.ants if ant.life.hp <= 0]
self.ants = [ant for ant in self.ants if ant.life.hp > 0]
for dead_ant in dead_ants:
world.delete(dead_ant)
return (bleft < right and bright > left and
btop < bottom and bbottom > top)
def _overlap(self, item):
item_ant, item_sprite = item
colliders = [a for a in self.ants if a != item_ant and a.collide(item_sprite)]
sprite_pos = item_ant.get_position()
sprite_color = "WHITE"
if len(colliders) > 0:
sprite_color = "RED"
item_ant.set_sprite(sprite_color)
item_ant.set_position(sprite_pos)
return colliders
def process(self, world, componentsets):
collitems = [comp for comp in componentsets if self._overlap(comp)]
if collitems:
self.ball.velocity.vx = -self.ball.velocity.vx
colliders = [comp for comp in componentsets if self._overlap(comp)]

+ 17
- 0
applicators/dashboardsystem.py View File

@ -0,0 +1,17 @@
import sdl2
import sdl2.ext
from objects import Dashboard
class DashboardSystem(sdl2.ext.Applicator):
def __init__(self, minx, miny, maxx, maxy):
super(DashboardSystem, self).__init__()
self.componenttypes = Dashboard, sdl2.ext.Sprite
self.minx = minx
self.miny = miny
self.maxx = maxx
self.maxy = maxy
def process(self, world, componentsets):
for dashboard, sprite in componentsets:
dashboard.set_position((0, 0))

+ 33
- 35
applicators/movementsystem.py View File

@ -1,44 +1,42 @@
import random
import sdl2
import sdl2.ext
import sdl2.sdlgfx
import logging
from ant import Ant
from utils import Velocity, Direction
from objects import Ant
from objects import Direction
class MovementSystem(sdl2.ext.Applicator):
def __init__(self, minx, miny, maxx, maxy):
super(MovementSystem, self).__init__()
self.componenttypes = Velocity, Direction, Ant, sdl2.ext.Sprite
self.minx = minx
self.miny = miny
self.maxx = maxx
self.maxy = maxy
def __init__(self, minx, miny, maxx, maxy):
super(MovementSystem, self).__init__()
self.componenttypes = Ant, sdl2.ext.Sprite
self.minx = minx
self.miny = miny
self.maxx = maxx
self.maxy = maxy
def process(self, world, componentsets):
for velocity, direction, ant, sprite in componentsets:
ndirection = direction
swidth, sheight = sprite.size
if not int(random.random() * 10) % 3:
if int(random.random() * 2) % 2:
ndirection = Direction(Direction.random(), direction.dy)
else:
ndirection = Direction(direction.dx, Direction.random())
def process(self, world, componentsets):
for ant, sprite in componentsets:
ndirection = ant.direction
swidth, sheight = ant.sprite.size
if not int(random.random() * 10) % 3:
if int(random.random() * 2) % 2:
ndirection = Direction(Direction.random(), ant.direction.dy)
else:
ndirection = Direction(ant.direction.dx, Direction.random())
nx = (ndirection.dx + direction.dx) * velocity.vx
ny = (ndirection.dy + direction.dy) * velocity.vy
sprite.x += nx
sprite.y += ny
ox, oy = ant.get_position()
nx = (ndirection.dx + ant.direction.dx) * ant.velocity.vx
ny = (ndirection.dy + ant.direction.dy) * ant.velocity.vy
nx += ox
ny += oy
nx = max(self.minx, nx)
ny = max(self.miny, ny)
sprite.x = max(self.minx, sprite.x)
sprite.y = max(self.miny, sprite.y)
pmaxx = sprite.x + swidth
pmaxy = sprite.y + sheight
if pmaxx > self.maxx:
sprite.x = self.maxx - swidth
if pmaxy > self.maxy:
sprite.y = self.maxy - sheight
ant.direction = ndirection
logging.debug("[{}]: direction: {} / ndirection: {}".format(ant, direction, ant.direction))
pmaxx = nx + swidth
pmaxy = ny + sheight
if pmaxx > self.maxx:
nx = self.maxx - swidth
if pmaxy > self.maxy:
ny = self.maxy - sheight
ant.set_position((nx, ny))
ant.direction = ndirection

+ 4
- 0
objects/__init__.py View File

@ -0,0 +1,4 @@
from objects.utils import Direction, History, Life, Velocity, Size, Food, Font
from objects.baseobject import BaseObject, spritefactory, COLORS
from objects.ant import Ant
from objects.dashboard import Dashboard

+ 34
- 0
objects/ant.py View File

@ -0,0 +1,34 @@
from objects import BaseObject, Velocity, Direction, History, Life, Size, Food
SIZE = Size(3, 3)
class Ant(BaseObject):
def __init__(self, world, max_memory=20, hp=100):
super(Ant, self).__init__(world)
self.ant = self
self.size = SIZE
self.history = History(max_memory)
self.Life = Life(hp)
self.velocity = Velocity(0, 0)
self.direction = Direction(Direction.random(), Direction.random())
self.stock = Food(0)
def set_position(self, position):
self.history.add(self.sprite.position)
self.sprite.position = position
self.life.decrement()
def set_velocity(self, vx, vy):
self.velocity = Velocity(vx, vy)
def collide(self, sprite):
area_left, area_top, area_right, area_bottom = self.sprite.area
s_area_left, s_area_top, s_area_right, s_area_bottom = sprite.area
return (s_area_left < area_right
and s_area_right > area_left
and s_area_top < area_bottom
and s_area_bottom > area_top)
def __str__(self):
return "Ant({}: {}, {}, {}, {}, Position: {})".format(str(self.uuid), self.life, self.food, self.direction, self.velocity, self.get_position())

+ 38
- 0
objects/baseobject.py View File

@ -0,0 +1,38 @@
from uuid import uuid4
from objects import Size
import sdl2
import sdl2.ext
SIZE = Size(1, 1)
COLORS = {"WHITE": sdl2.ext.Color(255, 255, 255),
"GREEN": sdl2.ext.Color(0, 255, 0),
"RED": sdl2.ext.Color(255, 0, 0),
"BLUE": sdl2.ext.Color(0, 0, 255),
"YELLOW": sdl2.ext.Color(255, 255, 0)}
spritefactory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
class BaseObject(sdl2.ext.Entity):
def __init__(self, world):
super(BaseObject, self).__init__()
self.uuid = uuid4()
self.size = SIZE
self.sprite = None
def set_sprite(self, color):
self.sprite = spritefactory.from_color(COLORS[color], size=(self.size.width, self.size.height))
def get_position(self):
return self.sprite.position
def set_position(self, position):
self.sprite.position = position
def collide(self, other):
area_left, area_top, area_right, area_bottom = self.sprite.area
s_area_left, s_area_top, s_area_right, s_area_bottom = other.sprite.area
return (s_area_left < area_right
and s_area_right > area_left
and s_area_top < area_bottom
and s_area_bottom > area_top)

+ 15
- 0
objects/dashboard.py View File

@ -0,0 +1,15 @@
from objects import BaseObject, Size, Font
class Dashboard(BaseObject):
def __init__(self, world, width, height, font_filename, font_size):
super(Dashboard, self).__init__(world)
self.dashboard = self
self.font = Font(font_filename, font_size)
self.size = Size(width, height)
def set_sprite(self, message):
self.sprite = self.font.render_text(message)
def set_position(self, position):
self.sprite.position = position

+ 107
- 0
objects/utils.py View File

@ -0,0 +1,107 @@
import random
import sdl2.ext
class History:
def __init__(self, max_len=20):
self.path = []
self.max_len = max_len
def add(self, position):
self.path.append(position)
self.path = self.path[-self.max_len:]
def __str__(self):
return "Path({})".format(self.path)
def __repr__(self):
return "Path({})".format(self.path)
class Life:
def __init__(self, hp=0):
self.hp = hp
def decrement(self, hp=1):
self.hp -= hp
def increment(self, hp=1):
self.hp += hp
def __str__(self):
return "Life({})".format(self.hp)
def __repr__(self):
return "Life({})".format(self.hp)
class Size:
def __init__(self, width, height):
super(Size, self).__init__()
self.width = width
self.height = height
def __str__(self):
return "Size({}, {})".format(self.width, self.height)
def __repr__(self):
return "Size({}, {})".format(self.width, self.height)
class Velocity:
def __init__(self, vx=0, vy=0):
super(Velocity, self).__init__()
self.vx = vx
self.vy = vy
def __str__(self):
return "Velocity({}, {})".format(self.vx, self.vy)
def __repr__(self):
return "Velocity({}, {})".format(self.vx, self.vy)
class Direction:
def __init__(self, dx=0, dy=0):
super(Direction, self).__init__()
self.dx = dx
self.dy = dy
def random():
""" Randomly returns one of the direction values.
Directions values are: -1, 0, 1
"""
directions = [-1, 0, 1]
return directions[int(random.random() * len(directions))]
def __str__(self):
return "Direction({}, {})".format(self.dx, self.dy)
def __repr__(self):
return "Direction({}, {})".format(self.dx, self.dy)
class Food:
def __init__(self, stock=0):
self.stock = stock
def decrement(self, stock=1):
self.stock -= stock
def increment(self, stock=1):
self.stock += stock
def __str__(self):
return "Food({})".format(self.stock)
def __repr__(self):
return "Food({})".format(self.stock)
class Font:
def __init__(self, filename, size):
self.manager = sdl2.ext.FontManager(filename, size=size)
def render_text(self, text):
surface = self.manager.render(text)
return sdl2.ext.SoftwareSprite(surface, free=True)

+ 0
- 301
old/ant.py View File

@ -1,301 +0,0 @@
"""Provides the Ant class for the ants module.
"""
import config
import uuid
import threading
import random
import time
import logging
import utils
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Ant
# -- -------------------------------------------------------------------------------------------------------------------
class Ant(threading.Thread):
""" The Ant object is a self living virtual creature that tries to simulate
some ant's basic behaviour.
The Ant object runs its own thread.
"""
def __init__(self, playground, display_q, position, farm_id):
"""
- playground: The Playground in which the Ant lives
- display_q: The FIFO queue in wich data are sent to the Display object
- position: A tuple seen as initial position (x, y)
- farm_id: The Farm ID to which the Ant belongs
"""
threading.Thread.__init__(self)
self.color = config.ANT_HUNTING_COLOR
self.outline_color = config.ANT_NEUTRAL_OUTLINE_COLOR
self.ID = str(uuid.uuid4())
self.playground = playground
self.position = position
self.new_position = position
self.direction = None
self.display_q = display_q
self.farm_id = farm_id
self.life = config.ANT_MAX_LIFE
self.waiting = False
self.history = []
self.togo = []
self.food = 0
self.last_hail = None
self.data = {'type': 'Ant %s' % (self.ID)}
self.around = {'ants': [], 'mines': [], 'farms': []}
def __str__(self):
""" Returns a string representation of the Ant. """
return str(self.to_dict())
def to_dict(self):
""" Returns a dict representation of the Ant object. This representation is
used by the Display workers to display the Ant.
"""
return {'type': 'ant',
'ID': self.ID,
'old_position': self.position,
'new_position': self.new_position,
'radius': config.ANT_RADIUS,
'color': self.color,
'outline': self.outline_color}
def display(self, new=False):
""" Send Ant dict representation to Display """
self.display_q.put(self.to_dict())
def set_colors(self):
""" Adjust the Ant colors depending on the Ant status.
"""
if self.food > 0:
self.color = config.ANT_HOMING_COLOR
self.outline_color = config.ANT_LOST_OUTLINE_COLOR
if self.is_busy():
self.outline_color = config.ANT_BUSY_OUTLINE_COLOR
else:
self.color = config.ANT_HUNTING_COLOR
self.outline_color = config.ANT_NEUTRAL_OUTLINE_COLOR
if self.is_busy():
self.outline_color = config.ANT_BUSY_OUTLINE_COLOR
def walk(self):
""" Set a new position.
If Ant already has a path to go, then the next position is picked up from this path.
Otherwise, a new position is randomly picked up.
"""
if self.is_busy():
self.new_position = self.pop_new_position()
else:
self.new_position = self.set_new_position()
self.display()
self.position = self.new_position
def set_new_position(self):
""" Set a new position from the actual position.
A new position is randomly generated by adding -1, 0 or 1 to one of
the actual position componant (x or y).
If the new position is out of the playground then the position is
adjusted. Thus an ant can not walk out of the play ground which is finite.
"""
if not self.direction:
self.direction = (utils.random_dir(), utils.random_dir())
else:
if not int(random.random() * 10) % 4:
if int(random.random() * 2) % 2:
self.direction = (utils.random_dir(), self.direction[1])
else:
self.direction = (self.direction[0], utils.random_dir())
new_pos_x = self.position[0] + self.direction[0]
new_pos_y = self.position[1] + self.direction[1]
if new_pos_x > self.playground.width:
new_pos_x = self.playground.width
if new_pos_x < 0:
new_pos_x = 0
if new_pos_y > self.playground.height:
new_pos_y = self.playground.height
if new_pos_y < 0:
new_pos_y = 0
return (new_pos_x, new_pos_y)
def pop_new_position(self):
""" Pick the next position in the path to go.
"""
return self.togo.pop(0)
def record(self):
""" Record the actual position in the Ant history.
For each new record, the history is recreated in a way that the shortest path
from the oldest position to the current position is retained.
Still, the new shortest path will only use position wihin the history.
The shortest path from within the history is rarely (who said never ?) the shortest path on the play ground.
"""
# FIXME: utils.get_shortest_path()
# Any better optimization algorithm out here ?
self.history = utils.get_shortest_path(self.ID, self.history, self.position, config.ANT_RADIUS)
def clean_history(self):
""" Delete oldest position in history when history length is larger than <max_history>.
See config.ANT_MAX_HISTORY
"""
if len(self.history) > config.ANT_MAX_HISTORY:
self.history.pop(0)
def check_around(self):
""" Process actions according to what is around the present position.
Object around the present position are given bay the Playground.scan(position) method.
"""
self.around = self.playground.scan(self.position)
ants = self.around['ants']
farms = self.around['farms']
mines = self.around['mines']
if len(farms) > 0:
for farm in farms:
self.store(farm)
if len(mines) > 0:
for mine in mines:
self.mine(mine)
if len(ants) > 0:
for ant in ants:
if ant.ID != self.ID:
self.hail(ant)
def hail(self, ant):
""" Hails the given Ant.
Whenever an Ant encounter an another, it hails it.
Depending on the status of each the concerned Ants, some data are exchanged.
An Ant can not hail a dead Ant.
"""
todo = "Hail: {}".format(ant.ID)
ant.wait()
# Fucking Hack: Compare a string representation of multiple boolean conditions
history_table = ['00110', '00011', '00001']
togo_table = ['10111', '10010', '00111', '00010']
truth = '%d%d%d%d%d' % (self.near_mine(), self.is_busy(), self.has_food(), ant.is_busy(), ant.has_food())
if truth in history_table:
todo += "Updated: Togo"
self.togo = [ant.position] + list(reversed(ant.history))
self.pause(config.ANT_PAUSE_DELAY)
elif truth in togo_table:
todo += "Updated: History"
self.togo = [ant.position] + ant.togo[:]
self.pause(config.ANT_PAUSE_DELAY)
# /Fucking hack
ant.restart()
logging.warning(todo, extra=self.data)
def store(self, farm):
""" Stores the carried food to the given farm. """
if farm.ID == self.farm_id and self.food > 0:
farm.store(self.food, self.ID)
self.food = 0
self.swap_histories()
def near_mine(self):
""" Returns wether or not the Ant is near a mine """
return len(self.around['mines']) > 0
def mine(self, mine):
""" Pick the maximum food amount from the given mine."""
if self.food < config.ANT_MAX_FOOD:
food = 0
if mine.stock > 0:
food = mine.stock
self.swap_histories()
if mine.stock >= config.ANT_MAX_FOOD - self.food:
food = config.ANT_MAX_FOOD - self.food
mine.pick(food, self.ID)
self.food = food
def swap_histories(self):
""" Swap history and path to go.
When an Ant find food, it needs to go back home.
The partial or complete path to home is in its history so the history is reverted then given has a path to go.
Once the path to go is set, the actual history is wiped out.
Same process occurs when an Ant comes back to home with food, so it can go back to the Mine.
"""
self.togo = list(reversed(self.history))
self.history = [self.position]
def in_range(self, position):
""" Returns if wheter or not the given position is aside the Ant.
- Position: A tuple seen as coordinates (x, y)"""
pos_x, pos_y = position
dx = abs(pos_x - self.position[0])
dy = abs(pos_y - self.position[1])
result = False
if dx + dy <= config.ANT_RADIUS:
result = True
if dx > config.ANT_RADIUS:
result = False
if dy > config.ANT_RADIUS:
result = False
if pow(dx, 2) + pow(dy, 2) <= pow(config.ANT_RADIUS, 2):
result = True
else:
result = False
return result
def is_busy(self):
""" Returns wheter or not the Ant is busy.
An Ant is busy when the length of its path to go is > 0.
"""
return len(self.togo) > 0
def is_alive(self):
""" Returns wether or not Ant <life> is > 0"""
return self.life > 0
def has_food(self):
""" Returns whether or not the Ant carries food """
return self.food > 0
def wait(self):
""" Wait until restart() is called."""
logging.warning("Status: Waiting", extra=self.data)
self.waiting = True
def restart(self):
""" Restart after wait() """
logging.warning("Status: Restarted", extra=self.data)
self.waiting = False
def pause(self, delay):
""" Wait time seconds """
time.sleep(delay)
def stop(self):
""" Kill the Ant.
this will exit the Ant thread. """
self.wait()
self.life = 0
def run(self):
""" The Ant thread main loop """
while self.is_alive():
if not self.waiting:
self.check_around()
self.set_colors()
self.walk()
self.record()
self.clean_history()
self.life -= 1
if not self.is_alive() and self.food > 0:
self.food -= 1
self.life += int(config.ANT_MAX_LIFE / 3)
logging.warning("Position: {}, Life: {}, Food: {}, Busy: {}, History: {}".format(self.position, self.life, self.food, self.is_busy(), len(self.history)), extra=self.data)
else:
time.sleep(config.ANT_HAIL_WAIT_DELAY)
time.sleep(config.ANT_TURN_SLEEP_DELAY)
self.outline_color = config.ANT_DEAD_OUTLINE_COLOR
self.display_q.put(self.to_dict())
logging.warning("Died at: {}, History: {}".format(self.position, len(self.history)), extra=self.data)

+ 0
- 58
old/ants.py View File

@ -1,58 +0,0 @@
#!/usr/bin/env python
# -*- coding:Utf-8 -*-
"""The ants module entry point.
"""
import config
import farm
import mine
import playground
import display
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- App
# -- -------------------------------------------------------------------------------------------------------------------
if __name__ == '__main__':
# The display
disp = display.Display(config.PLAYGROUND_SIZE, config.FARM_INITIAL_FOOD_STOCK)
# The FIFO queue to communicate with display
display_q = disp.get_display_queue()
# The queue needed to have response back from display to playground
response_q = disp.get_response_queue()
# Create play ground
ground = playground.Playground(config.PLAYGROUND_SIZE, response_q)
# Create a food mine
mine_position = (int(config.FARM_POS[0] + 50), int(config.FARM_POS[1] + 50))
mine = mine.Mine(mine_position, config.MINE_RADIUS)
# Add the mine to the playground
ground.add_mine(mine)
# Display the mine
display_q.put(mine.to_dict())
# Create the farm
farm = farm.Farm(ground,
display_q,
config.FARM_POS,
config.FARM_GROWTH_RATE,
config.FARM_INITIAL_FOOD_STOCK,
config.FARM_SURVIVAL_TIMEOUT,
config.FARM_RADIUS)
# Add farm to the playground
ground.add_farm(farm)
# Display farm
display_q.put(farm.to_dict())
# Start the farm own process
farm.start()
# Start the display own process
disp.start()

+ 0
- 100
old/config.py View File

@ -1,100 +0,0 @@
"""Provides basic configuration for the Ants application.
"""
import logging
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Logging
# -- -------------------------------------------------------------------------------------------------------------------
# Logging string format
FORMAT = '%(asctime)-15s: \033[95m%(type)-10s:\033[0m %(message)s'
logging.basicConfig(format=FORMAT)
# -- -------------------------------------------------------------------------------------------------------------------
# -- GUI
# -- -------------------------------------------------------------------------------------------------------------------
WINDOW_TITLE = 'Ants: A Computer Assisted Bullshit for random based life form research'
# -- -------------------------------------------------------------------------------------------------------------------
# -- Ant
# -- -------------------------------------------------------------------------------------------------------------------
# Ant radius (Don't touch this)
ANT_RADIUS = 1
# Max number of moves for an ant
ANT_MAX_LIFE = 1000
# How many food unit an ant can carry
ANT_MAX_FOOD = 5
# How many moves an ant can remmeber
ANT_MAX_HISTORY = 100
# Ant body color when looking for food
ANT_HUNTING_COLOR = '#FF0000'
# Ant body color when looking for home
ANT_HOMING_COLOR = '#00FF00'
# Ant neutral outline color
ANT_NEUTRAL_OUTLINE_COLOR = '#FFFFFF'
# Ant outline color when lost
ANT_LOST_OUTLINE_COLOR = '#FFFF00'
# Ant outline color when busy
ANT_BUSY_OUTLINE_COLOR = '#0000FF'
# Ant outline color when dead
ANT_DEAD_OUTLINE_COLOR = '#FF0000'
# Pause delay after hailing (in seconds)
ANT_PAUSE_DELAY = 3
# Delay in seconds between each turn
ANT_TURN_SLEEP_DELAY = 0.2
# Delay in seconds between each turn when in wait mode
# DON'T TOUCH THIS
ANT_HAIL_WAIT_DELAY = 0.5
# -- -------------------------------------------------------------------------------------------------------------------
# -- Playground
# -- -------------------------------------------------------------------------------------------------------------------
# Playground dimension
PLAYGROUND_SIZE = (200, 200)
# -- -------------------------------------------------------------------------------------------------------------------
# -- Display
# -- -------------------------------------------------------------------------------------------------------------------
DISPLAY_ANT_BY_WORKERS = 4
# -- -------------------------------------------------------------------------------------------------------------------
# -- Farm
# -- -------------------------------------------------------------------------------------------------------------------
# Farm poistion
FARM_POS = (PLAYGROUND_SIZE[0] / 2, PLAYGROUND_SIZE[0] / 2)
# How many ants by minute a farm can pop an ant
# MUST be <= 60
FARM_GROWTH_RATE = 60
# How many time in minutes a farm can survive without food stock
FARM_SURVIVAL_TIMEOUT = 10
# Radius of a farm
FARM_RADIUS = 10
# Food stock of newly created farm
FARM_INITIAL_FOOD_STOCK = 50
# Farm body color
FARM_COLOR = '#4B1BB4'
# Farm outline color
FARM_OUTLINE_COLOR = '#FFFFFF'
# -- -------------------------------------------------------------------------------------------------------------------
# -- Mine
# -- -------------------------------------------------------------------------------------------------------------------
# Radius of a mine
MINE_RADIUS = 10
# Mine body color
MINE_COLOR = '#EF26AD'
# Mine outline color
MINE_OUTLINE_COLOR = '#FFFFFF'
# Mine initial amount of food
MINE_INITIAL_STOCK = 100

+ 0
- 238
old/display.py View File

@ -1,238 +0,0 @@
"""Provides Worker and Display classes.
NumberList holds a sequence of numbers, and defines several statistical
operations (mean, stdev, etc.) FrequencyDistribution holds a mapping from
items (not necessarily numbers) to counts, and defines operations such as
Shannon entropy and frequency normalization.
"""
import sys
import queue
import config
from tkinter import *
import threading
import logging
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Display
# -- -------------------------------------------------------------------------------------------------------------------
class Worker(threading.Thread):
""" The Worker object provides a way to process data found in a queue.
When data is found, it is parsed and then displayed accordingly.
The Worker object runs in its own thread.
"""
def __init__(self, display, ID, queue):
"""
display: The display on wich data are displayed
ID: The Worker instance ID
queue: The FIFO queue where the data have to be collected
"""
threading.Thread.__init__(self)
self.ID = ID
self.display = display
self.queue = queue
self.active = True
self.data = {'type': 'Display worker %d' % self.ID}
def run(self):
"""
The Worker thread main loop:
- Get data from queue and give it to the process method
"""
logging.warning("Report status: Active {}".format(self.active), extra=self.data)
while self.active:
try:
item = self.queue.get(False)
self.process(item)
except queue.Empty:
pass
logging.warning("Report status: Active {}".format(self.active), extra=self.data)
def process(self, item):
"""
Displays received item.
Item must be a dict (JSON like hash)
Valid data can be one of the 3 following type:
- ant
- farm
- mine
"""
if item['type'] == 'ant':
self.display.draw_ant(item['ID'], item['old_position'], item['new_position'], item['radius'], item['color'], item['outline'])
elif item['type'] == 'farm':
self.display.draw_farm(item['ID'], item['position'], item['radius'], item['color'], item['outline'])
elif item['type'] == 'mine':
self.display.draw_mine(item['ID'], item['position'], item['radius'], item['color'], item['outline'])
def stop(self):
"""
Exit from main loop
"""
self.active = False
class Display(object):
"""
The Display object is the main interface to TKInter display.
Basically it is a simple Canvas with a frame inside a window.
Data processed by workers are displayed inside the canvas.
"""
def __init__(self, size, max_ants):
"""
size: The display size. Should be equal to config.PLAYGROUND_SIZE
max_ants: The estimated maximum number of ants
Since the number of living ants can not be guessed or guaranted, the max_ants
is an estimation.
It's a good idea to make it equal to config.FARM_INITIAL_FOOD_STOCK.
The max_ants value will determine the number of workers created by the Display object.
"""
# The display queue. Workers will write to it.
self.data = {'type': 'Display'}
self.display_q = queue.Queue()
self.response_q = queue.Queue()
self.width, self.height = size
# The main Tk window
self.master = Tk()
self.master.title(config.WINDOW_TITLE)
self.workers = {}
self.num_ants = 0
self.sv_num_ants = StringVar()
# Register the stop() method to the WM_DELETE_WINDOW event
self.master.protocol("WM_DELETE_WINDOW", self.stop)
# Create all needed workers
self.workers[len(self.workers)] = Worker(self, len(self.workers), self.display_q)
self.workers[len(self.workers) - 1].start()
# Build the GUI
self.build_gui()
# Update the display
self.update_gui()
def get_display_queue(self):
""" Returns the display queue"""
return self.display_q
def get_response_queue(self):
""" Returns the response queue"""
return self.response_q
def build_gui(self):
""" Build the GUI with widgets from Tkinter module """
self.canvas_frame = LabelFrame(self.master, width=self.width + 10, height=self.height + 10, text='Ants')
self.canvas = Canvas(self.canvas_frame, width=self.width, height=self.height, background='black', relief=SUNKEN)
self.num_ants_entry = Entry(self.canvas_frame, justify=RIGHT, width=10, textvariable=self.sv_num_ants)
self.num_ants_label = Label(self.canvas_frame, justify=LEFT, text='Number of ants')
self.num_ants_label.grid()
self.num_ants_entry.grid()
self.canvas.grid()
self.canvas_frame.grid(row=0, column=0)
def draw_ant(self, ID, old_pos, new_pos, radius, color, outline):
"""
Draw an non existent ant according to data received.
If ant already exists, the ant is moved to new_pos coordinates.
ID: The ant ID
old_pos: The previous position of this ant
new_pos: The new position where this ant must be displayed
radius: The ant radius (Don't touch this: see config.ANT_RADIUS)
color: The ant body color
outline: The ant outline color
"""
try:
if len(self.canvas.find_withtag(ID)) > 0:
# Ant already exists so we compute the new_pos/old_pos offset
x_offset = new_pos[0] - old_pos[0]
y_offset = new_pos[1] - old_pos[1]
self.canvas.itemconfig(ID, fill=color, outline=outline)
self.canvas.move(ID, x_offset, y_offset)
else:
# Ant is new
self.num_ants += 1
self.sv_num_ants.set(str(self.num_ants))
self.canvas.create_oval(new_pos[0] - radius,
new_pos[1] - radius,
new_pos[0] + radius,
new_pos[1] + radius,
tags=ID,
fill=color,
outline=outline)
# Put the ant up in the higher layer
self.canvas.tag_raise(ID)
# create a new worker if needed
if self.num_ants / len(self.workers) > config.DISPLAY_ANT_BY_WORKERS:
self.workers[len(self.workers)] = Worker(self, len(self.workers), self.display_q)
self.workers[len(self.workers) - 1].start()
self.update_gui()
except Exception as e:
logging.error("Error: [{}] {}".format(e.__class__.__name__, str(e)), extra=self.data)
exit(0)
def draw_mine(self, ID, pos, radius, color, outline):
"""
Draw an non existent mine according to data received.
ID: The mine ID
pos: The position of this mine
radius: The mine radius (see config.MINE_RADIUS)
color: The mine body color (see config.MINE_COLOR)
outline: The mine outline color (see config.MINE_OUTLINE_COLOR)
"""
ux = pos[0] - radius
uy = pos[1] - radius
dx = pos[0] + radius
dy = pos[1] + radius
logging.warning("Draw mine: {}, mux: {}, muy: {}, mdx: {}, mdy: {}".format(ID, ux, uy, dx, dy), extra=self.data)
self.canvas.create_oval(ux, uy, dx, dy, tags=ID, fill=color, outline=outline)
# Put the mine down in the lower layer
self.canvas.tag_lower(ID)
self.update_gui()
def draw_farm(self, ID, pos, radius, color, outline):
"""
Draw an non existent farm according to data received.
ID: The farm ID
pos: The position of this farm
radius: The farm radius (see config.FARM_RADIUS)
color: The farm body color (see config.FARM_COLOR)
outline: The mine outline color (see config.FARM_OUTLINE_COLOR)
"""
ux = pos[0] - radius
uy = pos[1] - radius
dx = pos[0] + radius
dy = pos[1] + radius
logging.warning("Draw farm: {}, fux: {}, fuy: {}, fdx: {}, fdy: {}".format(ID, ux, uy, dx, dy), extra=self.data)
self.canvas.create_oval(ux, uy, dx, dy, tags=ID, fill=color, outline=outline)
# Put the farm down in the lower layer
self.canvas.tag_lower(ID)
self.update_gui()
def update_gui(self):
""" Update the canvas """
self.canvas.update()
def start(self):
""" Start the window main loop of events """
self.master.mainloop()
def stop(self):
""" Send stop signal to all objects (ants, farms, mines, workers)"""
logging.warning("Sending kill signal", extra=self.data)
self.response_q.put({'type': 'kill'})
for worker in self.workers:
self.workers[worker].stop()
self.master.destroy()
logging.warning("Master window destroyed", extra=self.data)
logging.warning("Exiting", extra=self.data)
sys.exit(0)

+ 0
- 113
old/farm.py View File

@ -1,113 +0,0 @@
"""Provides the Farm class for the ants module.
The Farm object runs its own thread and continuously create ants according to initial parameters.
"""
import config
import uuid
import threading
import time
import logging
import ant
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Farm
# -- -------------------------------------------------------------------------------------------------------------------
class Farm(threading.Thread):
""" The Farm object continuously create ants untill it runs out of food.
"""
def __init__(self, playground, display_q, position, growthrate, food, life, radius):
"""
- playground: The Playground in which ants will be created
- display_q: The FIFO queue in wich data are sent to the Display object
- position: The Farm position as a tuple (x, y)
- growthrate: The growthrate in ants/minute
- food: The initial amount of food. One unit of food is needed for each ant creation.
when the Farm runs out of food it will stay alive for <life> minutes. If ants store food
in the Farm before the Farm runs out of <life>, then the ant creation process restart.
- life: The number of minutes the Farm stay alive when all food has been consumed.
- radius: The radius of the Farm. This is the display radius and the area where an ant is considered
at home.
"""
threading.Thread.__init__(self)
self.ID = str(uuid.uuid4())
self.playground = playground
self.position = position
self.growthrate = growthrate
self.food = food
self.life = life
self.radius = radius
self.display_q = display_q
self.color = config.FARM_COLOR
self.outline_color = config.FARM_OUTLINE_COLOR
self.data = {'type': 'Farm %s' % (self.ID)}
def __str__(self):
""" Returns a string representation of the farm """
return str(self.to_dict())
def to_dict(self):
""" Returns a dict representation of the Farm object. This representation is
used by the Display workers to display the Farm.
"""
return {'type': 'farm',
'ID': self.ID,
'position': self.position,
'food': self.food,
'life': self.life,
'radius': self.radius,
'color': self.color,
'outline': self.outline_color}
def store(self, food, ant_ID):
self.food += food
logging.warning("Food received from {}: {}".format(ant_ID, food), extra=self.data)
def run(self):
""" The Farm object main thread loop """
while self.life > 0:
if self.food > 0:
new_ant = ant.Ant(self.playground, self.display_q, self.position, self.ID)
self.playground.add_ant(new_ant)
new_ant.start()
self.food -= 1
logging.warning("Status: Food: {}".format(self.food), extra=self.data)
else:
self.life -= 1
logging.warning("Dying: Life: {}".format(self.life), extra=self.data)
time.sleep(60 / self.growthrate)
logging.warning("Dead: Life: {}".format(self.life), extra=self.data)
def in_range(self, position):
""" Returns if wheter or not the given position is aside the Farm.
- Position: A tuple seen as coordinates (x, y)"""
pos_x, pos_y = position
dx = abs(pos_x - self.position[0])
dy = abs(pos_y - self.position[1])
result = False
if dx + dy <= self.radius:
result = True
if dx > self.radius:
result = False
if dy > self.radius:
result = False
if pow(dx, 2) + pow(dy, 2) <= pow(self.radius, 2):
result = True
else:
result = False
return result
def stop(self):
""" Stops the Farm main loop. This will exit the Farm thread. """
self.life = 0

+ 0
- 83
old/mine.py View File

@ -1,83 +0,0 @@
"""Provides the Mine class for the ants module.
"""
import config
import uuid
import logging
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Mine
# -- -------------------------------------------------------------------------------------------------------------------
class Mine(object):
""" The Mine object is the food source for ants."""
def __init__(self, position, radius):
"""
- position: The Mine position as a tuple (x, y)
- radius: The mine radius. This is the display radius and the area where an ant is considered
in range to pick up some food.
"""
self.position = position
self.radius = radius
self.ID = str(uuid.uuid4())
self.stock = config.MINE_INITIAL_STOCK
self.color = config.MINE_COLOR
self.outline_color = config.MINE_OUTLINE_COLOR
self.data = {'type': 'Mine %s' % (self.ID)}
def __str__(self):
""" Returns the string representation of the Mine object"""
return str(self.to_dict())
def to_dict(self):
""" Returns a dict representation of the Mine object. This representation is
used by the Display workers to display the Mine.
"""
return {'type': 'mine',
'ID': self.ID,
'position': self.position,
'stock': self.stock,
'color': self.color,
'radius': self.radius,
'outline': self.outline_color}
def in_range(self, position):
""" Returns if wheter or not the given position is aside the Mine.
- Position: A tuple seen as coordinates (x, y)"""
pos_x, pos_y = position
dx = abs(pos_x - self.position[0])
dy = abs(pos_y - self.position[1])
result = False
if dx + dy <= self.radius:
result = True
if dx > self.radius:
result = False
if dy > self.radius:
result = False
if pow(dx, 2) + pow(dy, 2) <= pow(self.radius, 2):
result = True
else:
result = False
return result
def pick(self, q, ant_id):
""" Remove <q> food unit from Mine stock
- q: An int value"""
self.stock -= q
logging.warning("Ant: {} mined: {}, left: {}".format(ant_id, q, self.stock), extra=self.data)
if self.stock == 0:
logging.warning("Stock: {}".format(self.stock), extra=self.data)
def stop(self):
""" Actually do nothing"""
logging.warning("Killed", extra=self.data)

+ 0
- 143
old/playground.py View File

@ -1,143 +0,0 @@
"""Provides the Playground class which is the sandbox where
ants are living.
"""
import threading
import logging
__author__ = "Michaël Costa"
__copyright__ = "Copyright 2017, Michaël Costa"
__credits__ = ["Michaël Costa"]
__license__ = "WTFPL"
__version__ = "2.0.0"
__maintainer__ = "Michaël Costa"
__email__ = "michael.costa@mcos.nc"
__status__ = "Testing"
# -- -------------------------------------------------------------------------------------------------------------------
# -- Playground
# -- -------------------------------------------------------------------------------------------------------------------
class Worker(threading.Thread):
""" The Worker object provides a way to process data found in a queue.
When data is found, the corresponding object is killed.
The Worker object runs in its own thread.
"""
def __init__(self, ants, farms, mines, queue):
"""
ants: The ant collection
farms: The farm collection
mines: The mine collection
queue: The FIFO queue where the data have to be collected
"""
threading.Thread.__init__(self)
self.ants = ants
self.mines = mines
self.farms = farms
self.queue = queue
self.active = True
self.data = {'type': 'Playground worker'}
def run(self):
"""
The Worker thread main loop:
- Get data from queue and give it to the process method
"""
logging.warning("Report status: Active: {}".format(self.active), extra=self.data)
while self.active:
item = self.queue.get()
self.process(item)
logging.warning("Report status: Active: {}".format(self.active), extra=self.data)
def process(self, item):
"""
Displays received item.
Item must be a dict (JSON like hash)
Valid data can be one of the 3 following type:
- ant
- farm
- mine
"""
if item['type'] == 'kill':
logging.warning("Kill signal received", extra=self.data)
for farm in self.farms:
self.farms[farm].stop()
for mine in self.mines:
self.mines[mine].stop()
for ant in self.ants:
self.ants[ant].stop()
self.stop()
def stop(self):
"""
Exit from main loop
"""
self.active = False
class Playground(object):
""" The Playground class provides the eco-system within ants are living.
- size is a tuple seen as a dimension (width, height).
"""
def __init__(self, size, response_q):
self.width, self.height = size
self.response_q = response_q
self.ants = {}
self.farms = {}
self.mines = {}
self.worker = Worker(self.ants, self.farms, self.mines, self.response_q)
self.worker.start()
self.data = {'type': 'Playground'}
def scan(self, position):
""" Returns all objects (ants, farms, mines) nearby the given position.
- position is a tupe seen as coordinates (x, y)
"""
try:
ants = [self.ants[ant] for ant in self.ants if self.ants[ant].in_range(position) and self.ants[ant].is_alive()]
farms = [self.farms[farm] for farm in self.farms if self.farms[farm].in_range(position)]
mines = [self.mines[mine] for mine in self.mines if self.mines[mine].in_range(position)]
except RuntimeError:
return self.scan(position)