Skip to content

Commit

Permalink
Move logic outside world map (#544)
Browse files Browse the repository at this point in the history
* Move spawn location logic outside of the world_map

* Add spawn location finder back to world map (for now)

* Fix bugs

* Fix tests

* Simplify imports

* move score updating and pickup logic outside of world_map

* Move expanding map logic out of world_map

also put Cell into a new file to avoid import errors

* Merge branch 'master' into move_logic_outside_world_map

* Remove comments
  • Loading branch information
mrniket authored and OlafSzmidt committed Apr 25, 2018
1 parent 531b462 commit 733b323
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 144 deletions.
56 changes: 56 additions & 0 deletions aimmo-game/simulation/cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from simulation.action import MoveAction


class Cell(object):
"""
Any position on the world grid.
"""

def __init__(self, location, habitable=True, generates_score=False,
partially_fogged=False):
self.location = location
self.habitable = habitable
self.generates_score = generates_score
self.avatar = None
self.pickup = None
self.partially_fogged = partially_fogged
self.actions = []

def __repr__(self):
return 'Cell({} h={} s={} a={} p={} f{})'.format(
self.location, self.habitable, self.generates_score, self.avatar, self.pickup,
self.partially_fogged)

def __eq__(self, other):
return self.location == other.location

def __ne__(self, other):
return not self == other

def __hash__(self):
return hash(self.location)

@property
def moves(self):
return [move for move in self.actions if isinstance(move, MoveAction)]

@property
def is_occupied(self):
return self.avatar is not None

def serialise(self):
if self.partially_fogged:
return {
'generates_score': self.generates_score,
'location': self.location.serialise(),
'partially_fogged': self.partially_fogged
}
else:
return {
'avatar': self.avatar.serialise() if self.avatar else None,
'generates_score': self.generates_score,
'habitable': self.habitable,
'location': self.location.serialise(),
'pickup': self.pickup.serialise() if self.pickup else None,
'partially_fogged': self.partially_fogged
}
10 changes: 10 additions & 0 deletions aimmo-game/simulation/game_logic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .spawn_location_finder import SpawnLocationFinder
from map_updaters import ScoreLocationUpdater, MapContext, PickupUpdater, MapExpander

__all__ = [
'SpawnLocationFinder',
'ScoreLocationUpdater',
'MapContext',
'PickupUpdater',
'MapExpander'
]
77 changes: 77 additions & 0 deletions aimmo-game/simulation/game_logic/map_updaters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from abc import ABCMeta, abstractmethod
from collections import namedtuple
import random
import math
from logging import getLogger
from simulation.pickups import ALL_PICKUPS
from simulation.cell import Cell
from simulation.location import Location

LOGGER = getLogger(__name__)

MapContext = namedtuple('MapContext', 'num_avatars')


class _MapUpdater:
__metaclass__ = ABCMeta

@abstractmethod
def update(self, world_map, context):
raise NotImplementedError


class ScoreLocationUpdater(_MapUpdater):
def update(self, world_map, context):
for cell in world_map.score_cells():
if random.random() < world_map.settings['SCORE_DESPAWN_CHANCE']:
cell.generates_score = False

new_num_score_locations = len(list(world_map.score_cells()))
target_num_score_locations = int(math.ceil(
context.num_avatars * world_map.settings['TARGET_NUM_SCORE_LOCATIONS_PER_AVATAR']
))
num_score_locations_to_add = target_num_score_locations - new_num_score_locations
locations = world_map._spawn_location_finder.get_random_spawn_locations(num_score_locations_to_add)
for cell in locations:
cell.generates_score = True


class PickupUpdater(_MapUpdater):
def update(self, world_map, context):
target_num_pickups = int(math.ceil(
context.num_avatars * world_map.settings['TARGET_NUM_PICKUPS_PER_AVATAR']
))
LOGGER.debug('Aiming for %s new pickups', target_num_pickups)
max_num_pickups_to_add = target_num_pickups - len(list(world_map.pickup_cells()))
locations = world_map._spawn_location_finder.get_random_spawn_locations(max_num_pickups_to_add)
for cell in locations:
if random.random() < world_map.settings['PICKUP_SPAWN_CHANCE']:
LOGGER.info('Adding new pickup at %s', cell)
cell.pickup = random.choice(ALL_PICKUPS)(cell)


class MapExpander(_MapUpdater):
def update(self, world_map, context):
LOGGER.info('Expanding map')
start_size = world_map.num_cells
target_num_cells = int(math.ceil(
context.num_avatars * world_map.settings['TARGET_NUM_CELLS_PER_AVATAR']
))
num_cells_to_add = target_num_cells - world_map.num_cells
if num_cells_to_add > 0:
self._add_outer_layer(world_map)
assert world_map.num_cells > start_size

def _add_outer_layer(self, world_map):
self._add_vertical_layer(world_map, world_map.min_x() - 1)
self._add_vertical_layer(world_map, world_map.max_x() + 1)
self._add_horizontal_layer(world_map, world_map.min_y() - 1)
self._add_horizontal_layer(world_map, world_map.max_y() + 1)

def _add_vertical_layer(self, world_map, x):
for y in range(world_map.min_y(), world_map.max_y() + 1):
world_map.grid[Location(x, y)] = Cell(Location(x, y))

def _add_horizontal_layer(self, world_map, y):
for x in range(world_map.min_x(), world_map.max_x() + 1):
world_map.grid[Location(x, y)] = Cell(Location(x, y))
38 changes: 38 additions & 0 deletions aimmo-game/simulation/game_logic/spawn_location_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import random
from logging import getLogger

LOGGER = getLogger(__name__)


class SpawnLocationFinder:

def __init__(self, world_map):
self._world_map = world_map

def potential_spawn_locations(self):
"""
Used to make sure that the cell is free before spawning.
"""
return (c for c in self._world_map.all_cells()
if c.habitable
and not c.generates_score
and not c.avatar
and not c.pickup)

def get_random_spawn_locations(self, max_locations):
if max_locations <= 0:
return []
potential_locations = list(self.potential_spawn_locations())
try:
return random.sample(potential_locations, max_locations)
except ValueError:
LOGGER.debug('Not enough potential locations')
return potential_locations

def get_random_spawn_location(self):
"""Return a single random spawn location.
Throws:
IndexError: if there are no possible locations.
"""
return self.get_random_spawn_locations(1)[0].location
1 change: 1 addition & 0 deletions aimmo-game/simulation/game_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class GameState(object):
"""
Encapsulates the entire game state, including avatars, their code, and the world.
"""

def __init__(self, world_map, avatar_manager, completion_check_callback=lambda: None):
self.world_map = world_map
self.avatar_manager = avatar_manager
Expand Down
1 change: 1 addition & 0 deletions aimmo-game/simulation/turn_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import time
from threading import RLock
from threading import Thread
import requests

from simulation.action import PRIORITIES

Expand Down
152 changes: 11 additions & 141 deletions aimmo-game/simulation/world_map.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,14 @@
import math
import random
from logging import getLogger

from simulation.level_settings import DEFAULT_LEVEL_SETTINGS
from pickups import ALL_PICKUPS
from simulation.action import MoveAction
from simulation.location import Location
from simulation.game_logic import SpawnLocationFinder, ScoreLocationUpdater, MapContext, PickupUpdater, MapExpander
from simulation.cell import Cell

LOGGER = getLogger(__name__)


class Cell(object):
"""
Any position on the world grid.
"""

def __init__(self, location, habitable=True, generates_score=False,
partially_fogged=False):
self.location = location
self.habitable = habitable
self.generates_score = generates_score
self.avatar = None
self.pickup = None
self.partially_fogged = partially_fogged
self.actions = []

def __repr__(self):
return 'Cell({} h={} s={} a={} p={} f{})'.format(
self.location, self.habitable, self.generates_score, self.avatar, self.pickup,
self.partially_fogged)

def __eq__(self, other):
return self.location == other.location

def __ne__(self, other):
return not self == other

def __hash__(self):
return hash(self.location)

@property
def moves(self):
return [move for move in self.actions if isinstance(move, MoveAction)]

@property
def is_occupied(self):
return self.avatar is not None

def serialise(self):
if self.partially_fogged:
return {
'generates_score': self.generates_score,
'location': self.location.serialise(),
'partially_fogged': self.partially_fogged
}
else:
return {
'avatar': self.avatar.serialise() if self.avatar else None,
'generates_score': self.generates_score,
'habitable': self.habitable,
'location': self.location.serialise(),
'pickup': self.pickup.serialise() if self.pickup else None,
'partially_fogged': self.partially_fogged
}


class WorldMap(object):
"""
The non-player world state.
Expand All @@ -77,6 +21,7 @@ def __init__(self, grid, settings):
"""
self.grid = grid
self.settings = settings
self._spawn_location_finder = SpawnLocationFinder(self)

@classmethod
def _min_max_from_dimensions(cls, height, width):
Expand Down Expand Up @@ -109,16 +54,6 @@ def all_cells(self):
def score_cells(self):
return (c for c in self.all_cells() if c.generates_score)

def potential_spawn_locations(self):
"""
Used to make sure that the cell is free before spawning.
"""
return (c for c in self.all_cells()
if c.habitable
and not c.generates_score
and not c.avatar
and not c.pickup)

def pickup_cells(self):
return (c for c in self.all_cells() if c.pickup)

Expand Down Expand Up @@ -191,78 +126,10 @@ def _apply_score(self):
pass

def _update_map(self, num_avatars):
self._expand(num_avatars)
self._reset_score_locations(num_avatars)
self._add_pickups(num_avatars)

def _expand(self, num_avatars):
LOGGER.info('Expanding map')
start_size = self.num_cells
target_num_cells = int(math.ceil(
num_avatars * self.settings['TARGET_NUM_CELLS_PER_AVATAR']
))
num_cells_to_add = target_num_cells - self.num_cells
if num_cells_to_add > 0:
self._add_outer_layer()
assert self.num_cells > start_size

def _add_outer_layer(self):
self._add_vertical_layer(self.min_x() - 1)
self._add_vertical_layer(self.max_x() + 1)
self._add_horizontal_layer(self.min_y() - 1)
self._add_horizontal_layer(self.max_y() + 1)

def _add_vertical_layer(self, x):
for y in range(self.min_y(), self.max_y() + 1):
self.grid[Location(x, y)] = Cell(Location(x, y))

def _add_horizontal_layer(self, y):
for x in range(self.min_x(), self.max_x() + 1):
self.grid[Location(x, y)] = Cell(Location(x, y))

def _reset_score_locations(self, num_avatars):
for cell in self.score_cells():
if random.random() < self.settings['SCORE_DESPAWN_CHANCE']:
cell.generates_score = False

new_num_score_locations = len(list(self.score_cells()))
target_num_score_locations = int(math.ceil(
num_avatars * self.settings['TARGET_NUM_SCORE_LOCATIONS_PER_AVATAR']
))
num_score_locations_to_add = target_num_score_locations - new_num_score_locations
locations = self._get_random_spawn_locations(num_score_locations_to_add)
for cell in locations:
cell.generates_score = True

def _add_pickups(self, num_avatars):
target_num_pickups = int(math.ceil(
num_avatars * self.settings['TARGET_NUM_PICKUPS_PER_AVATAR']
))
LOGGER.debug('Aiming for %s new pickups', target_num_pickups)
max_num_pickups_to_add = target_num_pickups - len(list(self.pickup_cells()))
locations = self._get_random_spawn_locations(max_num_pickups_to_add)
for cell in locations:
if random.random() < self.settings['PICKUP_SPAWN_CHANCE']:
LOGGER.info('Adding new pickup at %s', cell)
cell.pickup = random.choice(ALL_PICKUPS)(cell)

def _get_random_spawn_locations(self, max_locations):
if max_locations <= 0:
return []
potential_locations = list(self.potential_spawn_locations())
try:
return random.sample(potential_locations, max_locations)
except ValueError:
LOGGER.debug('Not enough potential locations')
return potential_locations

def get_random_spawn_location(self):
"""Return a single random spawn location.
Throws:
IndexError: if there are no possible locations.
"""
return self._get_random_spawn_locations(1)[0].location
context = MapContext(num_avatars=num_avatars)
MapExpander().update(self, context=context)
ScoreLocationUpdater().update(self, context=context)
PickupUpdater().update(self, context=context)

def can_move_to(self, target_location):
if not self.is_on_map(target_location):
Expand Down Expand Up @@ -297,6 +164,9 @@ def get_no_fog_distance(self):
def get_partial_fog_distance(self):
return self.settings['PARTIAL_FOG_OF_WAR_DISTANCE']

def get_random_spawn_location(self):
return self._spawn_location_finder.get_random_spawn_location()

def __repr__(self):
return repr(self.grid)

Expand Down Expand Up @@ -402,5 +272,5 @@ def _generate_obstacle_list():


def WorldMapStaticSpawnDecorator(world_map, spawn_location):
world_map.get_random_spawn_location = lambda: spawn_location
world_map._spawn_location_finder.get_random_spawn_location = lambda: spawn_location
return world_map
Loading

0 comments on commit 733b323

Please sign in to comment.