Example #1
0
class GameInstance(CoordParseMixin):
    """Container for world and actors in world.

    GameInstance is responsible for tracking the world and the actors
    within it. It has functions to simplify interacting with the world
    and actors in it. It also provides functions that actors can call
    for information about the world, in order to aid in their decision
    making.

    Most functions that have a signature of the form fn(x,y) can
    accept a number of different arguments. The goal is to have as open
    an api as possible, given
    """
    def __init__(self, model=None):
        if not model:
            # For testing.
            self.actors = {}
            self.current_turn = 0
            self.world = WorldState(current_turn=self.current_turn)
        else:
            self.uuid = str(model.uuid)
            self.current_turn = model.current_turn
            self.actors = {}
            if model.seed:
                seed = json.loads(model.seed)
                if model.cells:
                    seed['cells'] = json.loads(model.cells)
                else:
                    seed['cells'] = {}
                self.world = WorldState(json_dump=seed, current_turn=self.current_turn)
            else:
                self.world = WorldState(current_turn=self.current_turn)

            for a in model.actors.all():
                self.add_actor(Actor(model=a))

        self.smell_matrix = defaultdict(list)

    def __getitem__(self, key):
        return self.world[key]

    def __setitem__(self, key, item):
        self.world[key] = item

    @property
    def is_night(self):
        return (self.current_turn / 12) % 2 == 0

    def set_turn(self, num):
        self.current_turn = num
        self.world.current_turn = num

    def add_actor(self, a, xy=None):
        """Add an Actor to the GameInstance.

        :param a: Actor object to be added.
        :param xy: x,y coord OR a WorldInhabitant (such as a Cell)
        """
        atest = self.actors.get(a.uuid)
        if atest:
            tmp = "{} already in GameInstance."
            raise ValueError(tmp.format(atest))

        if xy:
            x, y = self.coord_parse(xy)
        else:
            x, y = self.coord_parse(a)

        # Insert actor at nearby location if spot is full.
        atest = self.get_actor((x, y))
        if atest:
            for x1, y1 in self.circle_at((x, y), 4):
                atest = self.get_actor((x1, y1))
                if not atest:
                    self.actors[a.uuid] = a
                    a._coords = (x1, y1)
                    a.gameInstance = self
                    self.world.add_inhabitant(a)
                    warn = "Warning, actor already at ({},{}): '{}' inserted at ({},{})."
                    print(warn.format(x, y, a.name, x1, y1))
                    break
        else:
            self.actors[a.uuid] = a
            a._coords = (x, y)
            a.gameInstance = self
            self.world.add_inhabitant(a)

    def remove_actor(self, act_uuid):
        """Remove an actor from the GameInstance. Fail silently.

        :param xy_or_WI: x,y coord OR a WorldInhabitant object.
        """
        actr = self.get_actor(act_uuid)
        if not actr:
            return

        del self.actors[actr.uuid]
        self.world.remove_inhabitant(actr)
        actr._coords = (-1, -1)
        return

    def get_actor(self, xy_or_UUID):
        """Get actor by _coords, UUID, or WorldInhabitant.

        :param xy_or_UUID: x,y coord OR UUID of actor, or WorldInhabitant
        :return: Actor that fits description, or None
        """
        try:
            uuid.UUID(xy_or_UUID)
            is_uuid = True
        except Exception as e:
            is_uuid = False

        if is_uuid:
            return self.actors.get(xy_or_UUID)
        else:
            print(xy_or_UUID)
            x, y = self.coord_parse(xy_or_UUID)

        content = self[x][y]
        if len(content) > 1:
            for z in content:
                if isinstance(z, Actor):
                    return z
        return None

    def check_actor(self, xy):
        """ Check to see if actor is currently in location specified by param.
        :param xy: x,y coord of gameInstance
        :return Boolean if Actor is at xy location or False if not
        """
        x, y = self.coord_parse(xy)
        content = self[x][y]
        if len(content) > 1:
            for z in content:
                if isinstance(z, Actor):
                    return True
        return False

    def move_actor(self, actor_or_UUID_or_coords, xy_or_WI):
        """Move an actor to a location.

        :param actor_or_UUID_or_coords: Exactly what it says.
        :param xy_or_WI: Coords or a WorldInhabitant (cell)
        :return: None.
        """
        if isinstance(actor_or_UUID_or_coords, Actor):
            actor = actor_or_UUID_or_coords
        else:
            actor = self.get_actor(actor_or_UUID_or_coords)

        x,y = self.coord_parse(xy_or_WI)

        self.world.remove_inhabitant(actor)
        actor._coords = (x,y)
        self.world.add_inhabitant(actor)

    def has_attr(self, world_inhab, attr):
        """Determine if the wold_inhab has an attribute.

        :param world_inhab: A WorldInhabitant.
        :param attr: A property defined in globals.ATTRIBUTES
        :return: bool
        """
        if attr == "FOOD":
            return world_inhab.is_food
        if attr == "DEADLY":
            return world_inhab.is_deadly
        if attr == "ACTOR":
            return world_inhab.is_actor
        if attr == "WATER":
            return world_inhab.is_water
        if attr == "GRASS":
            return world_inhab.is_grass
        if attr == "ROCK":
            return world_inhab.is_rock
        if attr == "PLANT":
            return world_inhab.is_plant
        else:
            return False

    def find_nearest(self, xy_or_WI, attr):
        """Find and return nearest coords of world where something with attr is.

        :param xy_or_UUID: x,y tuple OR a WorldInhabitant
        :param attr: A property defined in globals.ATTRIBUTES
        :return: Coordinate tuple of nearest cell with something having that attribute.
                Returns original coords if not found.
        """

        vision_radius = 2 if self.is_night else 4
        scan_area = self.circle_at(xy_or_WI, vision_radius)

        for x, y in scan_area:
            coord_contents = self.world[x][y]
            for content in coord_contents:
                if self.has_attr(content, attr):
                    return x, y
        return self.coord_parse(xy_or_WI)

    def actor_turn_effects(self, actor_turn):
        """ Receive a turn and determine if it has any reprocussions.

        :param actor_turn: the delta that the actor wants to execute
        :return: delta of any direct changes that have occured.
        """

        effects = []
        if not actor_turn:
            return []
        if not isinstance(actor_turn, list):
            actor_turn = [actor_turn]

        # Calculate side effects of the actor's turn.
        for delta in actor_turn:
            actor = self.get_actor(delta['actorID'])

            if delta['varTarget'] == "_coords":
                new_x = delta["to"][0]
                new_y = delta["to"][1]
                coord_contents = self.world.get_cell((new_x, new_y))
                if self.has_attr(coord_contents, "WATER"):
                    effects.append({
                        "type": "actorDelta",
                        "coords": {'x': new_x, 'y': new_y},
                        "actorID": actor.uuid,
                        "varTarget": "health",
                        "from": actor.health,
                        "to": 0,
                        "message": actor.name + " has drowned!"
                    })
                if self.has_attr(coord_contents, "DEADLY"):
                     effects.append({
                        "type": "actorDelta",
                        "coords": {'x': new_x, 'y': new_y},
                        "actorID": actor.uuid,
                        "varTarget": "health",
                        "from": actor.health,
                        "to": actor.health-50
                    })

            # Check if actor is alive
            if delta['varTarget'] == 'health':
                if delta['to'] <= 0:
                    effects.append({
                        "type": "actorDelta",
                        "coords": {'x': delta["coords"]['x'], 'y': delta["coords"]['y']},
                        "actorID": actor.uuid,
                        "varTarget": "is_alive",
                        "from": True,
                        "to": False,
                        "message:": actor.name + " has died!"
                    })
            if delta['varTarget'] == 'hunger':
                if delta['to'] == False:
                    effects.append({
                        "type": "actorDelta",
                        "coords": {'x': self.x, 'y': self.y},
                        "actorID": self.uuid,
                        "varTarget": "has_food",
                        "from": True,
                        "to": False
                    })
            
        # Calculate any side effects of the side effects.
        old_effects = effects
        while old_effects:
            new_effects = self.actor_turn_effects(old_effects)
            old_effects = new_effects
            effects.extend(old_effects)

        return effects

    def global_turn_effects(self):
        """Calculate the effects of this turn not necessarily related to actor action."""
        effects = []

        for u, actr in self.actors.items():
            if not actr.is_alive:
                continue
            if actr.sleep <= 1 and not actr.is_sleeping:
                sleep_action = actr.sleep_action()
                sleep_action['message'] = actr.name + " is exhauseted and fell asleep!"
                effects.append(sleep_action)

            if actr.sleep >= 100 and actr.is_sleeping:
                wake_action = actr.wake_action()
                wake_action['message'] = actr.name + " is fully rested and has woken up!"
                effects.append(wake_action)

            if actr.hunger <= 1:
                effects.append({
                    "type": "actorDelta",
                    "coords": {'x': actr.x, 'y': actr.y},
                    "actorID": actr.uuid,
                    "varTarget": "health",
                    "from": actr.health,
                    "to": actr.health-5,
                    "message": actr.name + " is starving!"
                })
        return effects

    def do_turn(self, up_to=0):
        """High level function for returning a list of turns in this game."""
        all_turns = []
        while self.current_turn < up_to:
            this_turn = {'number': self.current_turn, 'deltas': [], }
            self.current_turn += 1
            this_turn['diff'] = self.world.apply_updates() #Returns diff each call. They should be stored though.
            self.compute_smells()

            for uuid, actor in self.actors.items():
                turn_res = []

                aturn = actor.do_turn()
                if aturn:
                    turn_res.append(aturn)

                effects = self.actor_turn_effects(turn_res)
                if effects:
                    turn_res.extend(effects)

                if turn_res:
                    self.apply_deltas(turn_res)
                    this_turn['deltas'].extend(turn_res)

            global_effects = self.global_turn_effects()
            self.apply_deltas(global_effects)
            this_turn['deltas'].extend(global_effects)
            all_turns.append(this_turn)

        return all_turns

    def apply_deltas(self, delta_list, reverse=False):
        """Apply the deltas produced during turns."""
        for delta in delta_list:
            if reverse:
                val = delta['from']
            else:
                val = delta['to']
            actr = self.get_actor(delta['actorID'])
            if delta['varTarget'] == '_coords':
                self.move_actor(actr, val)
            if delta['varTarget'] == 'health':
                actr.health = val
            if delta['varTarget'] == 'is_sleeping':
                actr.is_sleeping = val
            if delta['varTarget'] == 'has_rock':
                if (delta['to'] == True):
                    actr.has_rock = val
                elif (delta['to'] == False):
                    actr.has_rock = val
            if delta['varTarget'] == 'has_food':
                if (delta['to'] == False):
                    actr.has_food = val
                if (delta['to'] == True):
                    actr.has_food = val

        if reverse:
            for act in self.actors.values():
                act._turn_stat_change(reverse=True)

    def to_dict(self, withseed=True):
        d = self.world.to_dict(withseed=withseed)
        d['current_turn'] = self.current_turn
        return d

    def _coord_neighbors(self, xy):
        x, y = xy
        return (x-1, y), (x+1, y), (x,y-1), (x, y+1)

    def compute_smells(self):
        self.smell_matrix = defaultdict(list)
        for act in self.actors.values():
            self._bfs_smell_spread(act)

    def _bfs_smell_spread(self, world_inhabitant):
        smell_code = world_inhabitant.smell_code
        x, y = world_inhabitant._coords
        z = self.world.get_cell((x, y)).elevation

        q = deque()
        visited = set()

        neighbors = world_inhabitant.neighbors
        q.extendleft(neighbors)
        visited.add(world_inhabitant._coords)
        visited = visited.union(neighbors)

        #  BFS to populate smell matrix for the turn.
        while q:
            # get first coord
            x1, y1 = q.pop()
            z1 = self.world.get_cell((x1,y1)).elevation

            x2,y2,z2 = x1-x, y1-y, z1-z
            intensity = math.exp(-(x2**2 + y2**2 + z2**2)/SMELL_SPREAD)

            if intensity > .3:
                # get its unvisited neighbors and put them in their place.
                neighbors = set(self._coord_neighbors((x1, y1)))
                neighbors = neighbors.difference(visited)
                q.extendleft(neighbors)
                visited = visited.union(neighbors)

                self.smell_matrix[(x1, y1)].append((smell_code, intensity))