rewrite. add behavior and lifespan
authorBen Doumenc <bdoumenc@gmail.com>
Sat, 31 May 2014 06:20:29 +0000 (08:20 +0200)
committerBen Doumenc <bdoumenc@gmail.com>
Sat, 31 May 2014 06:20:29 +0000 (08:20 +0200)
evolution/entities.py
evolution/main.py
evolution/systems.py
evolution/utils.py [new file with mode: 0644]

index 16c1fdf..fdd284a 100644 (file)
 
 import copy
+import random
+import math
+
+from utils import SceneLogger
 
 class Scene:
-    def __init__(self):
-        self.all = []
+    class Directions:
+        NONE = -1
+        UP = 0
+        DOWN = 1
+        LEFT = 2
+        RIGHT = 3
+        ALL = [NONE, UP, DOWN, LEFT, RIGHT]
+
+    class Types:
+        DRAWABLE = "Drawable"
+        PHYSICAL = "Physical"
+        ALIVE    = "Alive"
+        GOODS    = "Goods"
+
+    class GoodTypes:
+        FOOD = "Food"
+
+    def __init__(self, name, clock):
+        self.name = name
+        self.clock = clock
+        self.all = set()
         self.byType = {}
         self.byEntity = {}
+        self.logger = SceneLogger(self)
 
-    def add(self, cpt):
-        self.all.append(cpt)
-        self.byType.setdefault(cpt.TYPE, []).append(cpt)
-        self.byEntity.setdefault(cpt.entity, {}).setdefault(cpt.TYPE, []).append(cpt)
+    def add(self, entity):
+        self.logger.debug("Scene: add %s", entity)
+        self.all.add(entity)
+        for type in entity.TYPES:
+            self.byType.setdefault(type, []).append(entity)
 
-    def remove(self, cpt):
-        self.all.remove(cpt)
-        cpts = self.byType.get(cpt.TYPE, [])
-        if cpt in cpts: cpts.remove(cpt)
-        cpts = self.byEntity.get(cpt.entity, {}).get(cpt.TYPE, [])
-        if cpt in cpts: cpts.remove(cpt)
+    def remove(self, entity):
+        self.logger.debug("Scene: remove %s", entity)
+        self.all.remove(entity)
+        for type in entity.TYPES:
+            entities = self.byType.get(type, [])
+            if entity in entities: entities.remove(entity)
 
     def getByType(self, type):
         return self.byType.get(type, [])
 
-    def entitiesByType(self, type):
-        return list(set([c.entity for c in self.getByType(type)]))
+    def getDirection(self, entity, target):
+        dx = target.x - entity.x
+        dy = target.y - entity.y
+        if abs(dx) >= abs(dy):
+            if dx < 0:
+                return Scene.Directions.LEFT
+            if dx >= 0:
+                return Scene.Directions.RIGHT
+        else:
+            if dy < 0:
+                return Scene.Directions.UP
+            if dy >= 0:
+                return Scene.Directions.DOWN
+        return Scene.Directions.NONE
+
+    def getAt(self, entity, type):
+        return [e for e in self.getByType(type) if (e.x == entity.x and e.y == entity.y)]
+
+    def getAround(self, entity, distance, type):
+        def isAround(e):
+            return math.sqrt((e.x - entity.x)**2 + (e.y - entity.y)**2) < distance
+        return [e for e in self.getByType(type) if isAround(e)]
 
-    def getByEntity(self, entity, type):
-        return self.byEntity.get(entity, {}).get(type, None)
-
-    def entityHasComponentType(self, entity, type):
-        cpts = entity.scene.getByEntity(entity, type)
-        return cpts and len(cpts) > 0
 
 _entityCounter = 0
 
 class Entity(object):
-    def __init__(self, scene):
+    TYPES = []
+    def __init__(self):
         global _entityCounter
-        self.scene = scene
         self.id = _entityCounter
         _entityCounter += 1
 
-    def add(self, cpt):
-        cpt.add(self)
-        self.scene.add(cpt)
-        return self
-
-    def remove(self, cpt):
-        cpt.remove(self)
-        self.scene.remove(cpt)
-        return self
-
     def __repr__(self):
-        return "Entity(" + str(self.id) + ")"
-        
+        return "%s(%d)" % (self.__class__.__name__, self.id)
 
-_componentCounter = 0
 
-class Component(object):
-    TYPE = None
-    def __init__(self):
-        global _componentCounter
-        self.id = _componentCounter
-        _componentCounter += 1
-        self.entity = None
-
-    def add(self, entity):
-        self.entity = entity
-        return self
-
-    def remove(self, entity):
-        self.entity = None
-        return self
-
-    def __repr__(self):
-        return "Component(%d, (%s)" % (self.id, self.TYPE)
-        
-
-class Position(Component):
-    TYPE = "POSITION"
+class Physical(Entity):
+    TYPES = Entity.TYPES + [Scene.Types.DRAWABLE, Scene.Types.PHYSICAL]
     def __init__(self, x, y, walkable=True):
-        super(Position, self).__init__()
+        super(Physical, self).__init__()
+        self.scene = None
         self.x = x
         self.y = y
         self.walkable = walkable
 
+    def addToScene(self, scene):
+        self.scene = scene
+        self.scene.add(self)
+        return self
+
+    def removeFromScene(self):
+        self.scene.remove(self)
+        self.scene = None
+        return self
+
     def move(self, dx=0, dy=0):
         self.x += dx
         self.y += dy
+        return self
 
-    def set(self, x=0, y=0):
+    def at(self, x=0, y=0):
         self.x = x
         self.y = y
+        return self
 
-    def __repr__(self):
-        return "Position(%d, (%d, %d, %s))" % (self.id, self.x, self.y, self.walkable)
-
-class Movable(Component):
-    TYPE = "MOVABLE"
-    NONE = -1
-    UP = 0
-    DOWN = 1
-    LEFT = 2
-    RIGHT = 3
-
-    def __init__(self):
-        super(Movable, self).__init__()
-        self.direction = Movable.NONE
-
-    def __repr__(self):
-        return "Movable(%d, (%d, %d))" % (self.id, self.x, self.y)
 
-class CursesRendering(Component):
-    TYPE = "RENDERING"
-    def __init__(self, glyph):
-        super(CursesRendering, self).__init__()
-        self.glyph = glyph
+class Good(Physical):
+    TYPES = Physical.TYPES + [Scene.Types.GOODS]
+    def __init__(self, x, y):
+        super(Good, self).__init__(x, y, False)
+        self.type = Scene.GoodTypes.FOOD
+        self.value = 5
 
     def getGlyph(self):
-        return self.glyph
-
+        return "#"
+
+    def use(self, e):
+        e.stats["life"] += self.value
+        self.removeFromScene()
+
+
+class Bot(Physical):
+    TYPES = Physical.TYPES + [Scene.Types.ALIVE]
+    def __init__(self, x, y):
+        super(Bot, self).__init__(x, y, True)
+        self.direction = random.choice(Scene.Directions.ALL)
+        self.genes = []
+        self.stats = {"life": 10, "speed": 1}
+        self.stamina = 10
+        self.behavior = None
+        self.behaviorData = {}
+
+    def addGene(self, gene):
+        self.genes.append(gene)
+        gene.apply(self)
+        return self
 
-class Being(Component):
-    TYPE = "BEING"
-    def __init__(self):
-        super(Being, self).__init__()
-        self.originalStats = {
-            "life": 1,
-            # "speed": 0,
-            # "charisma": 0,
-            # "strength": 0,
-            # "resistance": 0,
-        }
-        self.stats = copy.deepcopy(self.originalStats)
+    def getGenes(self):
+        return self.genes
 
-    def __repr__(self):
-        return "Being(%d, (%s))" % (self.id, self.stats)
+    def getGlyph(self):
+        return "@"
 
 
-class Gene(Component):
-    TYPE = "GENE"
+class Gene(object):
     def __init__(self):
-        super(Gene, self).__init__()
         self.name = self.__class__.__name__
         self.attribute = ""
         self.modifier = 0
 
-    def add(self, entity):
-        super(Gene, self).add(entity)
-        being = entity.scene.getByEntity(entity, Being.TYPE)
-        if being and len(being) > 0:
-            stat = being[0].stats.setdefault(self.attribute, 0)
-            if stat + self.modifier >= 0:
-                being[0].stats[self.attribute] = stat + self.modifier
-        return self
-
-    def remove(self, entity):
-        super(Gene, self).remove(entity)
-        being = entity.scene.getByEntity(entity, self.TYPE)
-        if being and len(being) > 0:
-            stat = being[0].stats.setdefault(self.attribute, 0)
-            if stat - self.modifier >= 0:
-                being[0].stats[self.attribute] = stat - self.modifier
+    def apply(self, entity):
+        stat = entity.stats.setdefault(self.attribute, 0)
+        if stat + self.modifier >= 0:
+            entity.stats[self.attribute] = stat + self.modifier
         return self
 
-
     def __repr__(self):
-        return "%s(%d, (%s, %d))" % (self.name, self.id, self.attribute, self.modifier)
+        return "%s" % (self.name)
 
 
 class LongLegs(Gene):
@@ -175,12 +174,6 @@ class LongLegs(Gene):
         self.attribute = "speed"
         self.modifier = 1
 
-class LongLegs(Gene):
-    def __init__(self):
-        super(LongLegs, self).__init__()
-        self.attribute = "speed"
-        self.modifier = 1
-
 class ShortLegs(Gene):
     def __init__(self):
         super(ShortLegs, self).__init__()
@@ -199,6 +192,12 @@ class ExoticBeauty(Gene):
         self.attribute = "charisma"
         self.modifier = 2
 
+class Eyes(Gene):
+    def __init__(self):
+        super(Eyes, self).__init__()
+        self.attribute = "perception"
+        self.modifier = 2
+
 
 import inspect, sys
 GENES = dict(inspect.getmembers(sys.modules[__name__], lambda member: inspect.isclass(member) and member.__module__ == __name__ and issubclass(member, Gene)))
index b9ddd52..5d87b41 100644 (file)
@@ -2,57 +2,47 @@
 import copy
 import random
 
-from entities import *
-from systems import CursesRenderer, Movement
-
-
-def pretty(d, indent=0):
-   for key, value in d.iteritems():
-      print '\t' * indent + str(key)
-      if isinstance(value, dict):
-         pretty(value, indent+1)
-      else:
-         print '\t' * (indent+1) + str(value)
+import logging
 
+from entities import *
+from systems import CursesRenderer, Movement, Behavior
+from utils import Clock
 
 def generateRandomGenes(entity):
     for g in random.sample(GENES.values(), 3):
-        entity.add(g())
+        entity.addGene(g())
     
 
 if __name__ == "__main__":
+    logging.basicConfig(format='%(module)s:%(levelname)s:%(message)s', filename='output.log',level=logging.DEBUG)
+
     random.seed(1)
-    scene = Scene()
+    clock = Clock(0.1)
+    scene = Scene("mainScene", clock)
+    size = (140,40)
     renderer = CursesRenderer()
-    movement = Movement(20, 20)
-    beings = []
-    for _ in xrange(0, 4):
-        b = Entity(scene).add(Being()).add(Position(random.randint(0, 20), random.randint(0, 20))).add(CursesRendering("*")).add(Movable())
+    movement = Movement(size[0], size[1])
+    behavior = Behavior()
+    population = 4
+    food = 15
+    for _ in xrange(0, population):
+        b = Bot(random.randint(0, size[0] - 1), random.randint(0, size[1] - 1)).addToScene(scene)
         generateRandomGenes(b)
-        o = Entity(scene).add(Position(random.randint(0, 20), random.randint(0, 20), False)).add(CursesRendering("o"))
-
-    player_move = Movable()
-    player = Entity(scene).add(Being()).add(Position(10, 10)).add(CursesRendering("@")).add(player_move)
 
+    for _ in xrange(0, food):
+        o = Good(random.randint(0, size[0] - 1), random.randint(0, size[1] - 1)).addToScene(scene)
 
-    # pretty(scene.byEntity)
     renderer.update(scene)
     while True:
-       event = renderer.screen.getch()
-       if event == ord("q"): break 
-       elif event == renderer.curses.KEY_RIGHT:
-           player_move.direction = Movable.RIGHT
-       elif event == renderer.curses.KEY_LEFT:
-           player_move.direction = Movable.LEFT
-       elif event == renderer.curses.KEY_UP:
-           player_move.direction = Movable.UP
-       elif event == renderer.curses.KEY_DOWN:
-           player_move.direction = Movable.DOWN
-
-       movement.update(scene)
-       renderer.update(scene)
-       player_move.direction = Movable.NONE
+        event = renderer.screen.getch()
+        if event == ord("q"): break 
+        clock.tick()
+
+        behavior.update(scene)
+        movement.update(scene)
+        renderer.update(scene)
 
     renderer.close()
     movement.close()
+    behavior.close()
 
index e2e9b9c..5f3a293 100644 (file)
@@ -1,5 +1,7 @@
 
-from entities import CursesRendering, Position, Movable
+import random
+
+from entities import Scene
 
 class CursesRenderer:
     def __init__(self):
@@ -9,14 +11,16 @@ class CursesRenderer:
         self.curses.noecho()
         self.curses.curs_set(0)
         self.screen.keypad(1)
+        self.screen.nodelay(1)
 
     def update(self, scene):
         self.screen.clear() 
-        toRender = scene.getByType(CursesRendering.TYPE)
-        for render in toRender:
-            cpts = scene.getByEntity(render.entity, Position.TYPE)
-            for cpt in cpts:
-                self.screen.addstr(cpt.y, cpt.x, render.getGlyph())
+        toRender = scene.getByType(Scene.Types.DRAWABLE)
+        for e in toRender:
+            try:
+                self.screen.addstr(e.y, e.x, e.getGlyph())
+            except self.curses.error:
+                pass
 
     def close(self):
         self.curses.endwin()
@@ -28,38 +32,111 @@ class Movement:
         self.h = h
 
     def update(self, scene):
-        # build map
-        grid = [[True for x in xrange(self.w)] for x in xrange(self.h)] 
-        allPositions = scene.getByType(Position.TYPE)
-        for p in allPositions:
-            grid[p.x][p.y] = p.walkable
-
-        def isValid(p, dx, dy):
-            newX = p.x + dx
-            newY = p.y + dy
+        def isValid(e, dx, dy):
+            newX = e.x + dx
+            newY = e.y + dy
             return (newX < self.w and newX >= 0 and
-                    newY < self.h and newY >= 0 and
-                    grid[newX][newY])
-
-        movables = scene.entitiesByType(Movable.TYPE)
-        for e in movables:
-            positions = scene.getByEntity(e, Position.TYPE)
-            for m in scene.getByEntity(e, Movable.TYPE):
-                dx = 0; dy = 0
-                if m.direction == Movable.UP:
-                    dx = 0; dy = -1
-                elif m.direction == Movable.DOWN:
-                    dx = 0; dy = 1
-                elif m.direction == Movable.LEFT:
-                    dx = -1; dy = 0
-                elif m.direction == Movable.RIGHT:
-                    dx = 1; dy = 0
-                if dx != 0 or dy != 0:
-                    for p in positions:
-                        if isValid(p, dx, dy):
-                            p.move(dx, dy)
+                    newY < self.h and newY >= 0)
+
+        entities = scene.getByType(Scene.Types.ALIVE)
+        for e in entities:
+            dx = 0; dy = 0
+            if e.direction == Scene.Directions.UP:
+                dx = 0; dy = -1
+            elif e.direction == Scene.Directions.DOWN:
+                dx = 0; dy = 1
+            elif e.direction == Scene.Directions.LEFT:
+                dx = -1; dy = 0
+            elif e.direction == Scene.Directions.RIGHT:
+                dx = 1; dy = 0
+            if dx != 0 or dy != 0:
+                if isValid(e, dx, dy):
+                    # speed = e.stats.get("speed", 1)
+                    speed = 1
+                    e.move(dx * speed, dy * speed)
 
     def close(self):
         pass
 
+class Behavior:
+    EXHAUSTED = "Exhausted"
+    WANDER = "Wander"
+    SEEK   = "Seek"
+
+    def __init__(self): 
+        self.behaviors = {
+            Behavior.WANDER: self.wander,
+            Behavior.EXHAUSTED: self.exhausted,
+            Behavior.SEEK: self.seek,
+        }
+
+    def setState(self, e, state):
+        e.scene.logger.debug("setState: [%s] %s -> %s", e, e.behavior, state)
+        e.behaviorData = {}
+        e.behavior = state
+
+    def update(self, scene):
+        entities = scene.getByType(Scene.Types.ALIVE)
+        for e in entities:
+            if e.behavior == None:
+                self.setState(e, Behavior.WANDER)
+            self.behaviors.get(e.behavior)(scene, e)
+
+    def seek(self, scene, entity):
+        # Check for in-cell food
+        entities = scene.getAt(entity, Scene.Types.GOODS)
+        for e in entities:
+            if e.type == Scene.GoodTypes.FOOD:
+                e.use(entity)
+                self.setState(entity, Behavior.WANDER)
+                return
+        # Check for surrounding food
+        perceptionDistance = entity.stats.get("perception", 1) * 4
+        if not entity.behaviorData.get("target", None):
+            scene.logger.debug("Seek: [%s] Looking for food", entity)
+            entities = scene.getAround(entity, perceptionDistance, Scene.Types.GOODS)
+            for e in entities:
+                if e.type == Scene.GoodTypes.FOOD:
+                    scene.logger.debug("Seek: [%s] Found target %s", entity, e)
+                    entity.behaviorData["target"] = e
+                    break
+
+        if (entity.behaviorData.has_key("target")):
+            # And move
+            entity.direction = scene.getDirection(entity, entity.behaviorData["target"])
+        else:
+            self.setState(entity, Behavior.WANDER)
+
+    def exhausted(self, scene, entity):
+        entity.behaviorData["count"] = entity.behaviorData.get("count", 5) - 1
+        if entity.behaviorData["count"] == 0:
+            entity.stamina = 10
+            entity.stats["life"] = entity.stats["life"] - 1
+
+        if entity.stats["life"] == 0:
+            scene.logger.debug("Exhausted: [%s] died. Was %s", entity, entity.getGenes())
+            entity.removeFromScene()
+            return
+        if entity.stamina > 0:
+            self.setState(entity, Behavior.WANDER)
+            
+    def wander(self, scene, e):
+        def reset():
+            e.behaviorData["count"] = 5
+            directions = Scene.Directions.ALL[:]
+            directions.remove(e.direction)
+            e.direction = random.choice(directions)
+
+        if not e.behaviorData.has_key("count"): reset()
+        e.behaviorData["count"] = e.behaviorData.get("count", 5) - 1
+        e.stamina -= 1
+        if e.stamina == 0:
+            self.setState(e, Behavior.EXHAUSTED)
+        elif e.behaviorData["count"] == 0:
+            self.setState(e, Behavior.SEEK)
+        
+
+    def close(self):
+        pass
 
+        
diff --git a/evolution/utils.py b/evolution/utils.py
new file mode 100644 (file)
index 0000000..cf3d91c
--- /dev/null
@@ -0,0 +1,26 @@
+
+
+import logging
+import time
+
+class Clock:
+    def __init__(self, delay):
+        self.delay = delay
+        self._tick = 0
+
+    def tick(self):
+        time.sleep(self.delay)
+        self._tick += 1
+
+    def getTick(self):
+        return self._tick
+
+
+class SceneLogger(logging.LoggerAdapter):
+    def __init__(self, scene):
+        super(SceneLogger, self).__init__(logging.getLogger(scene.name), {})
+        self.scene = scene
+
+    def process(self, msg, kwargs):
+        return '[%8d] %s' % (self.scene.clock.getTick(), msg), kwargs
+