Skip to content

Commit

Permalink
Merge b9c93ec into e17bcb3
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuablake committed Aug 18, 2016
2 parents e17bcb3 + b9c93ec commit f9f72f7
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 167 deletions.
33 changes: 16 additions & 17 deletions aimmo-game-worker/tests/simulation/test_world_map.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
from __future__ import absolute_import
from simulation.world_map import WorldMap
from simulation.location import Location
from simulation import location
from unittest import TestCase


class TestWorldMap(TestCase):
AVATAR = {'location': {'x': 0, 'y': 0}, 'health': True, 'score': 3, 'events': []}

def _generate_cells(self, columns=2, rows=2):
def _generate_cells(self, columns=3, rows=3):
cells = [{
'location': {'x': x, 'y': y},
'habitable': True,
'generates_score': False,
'avatar': None,
'pickup': None,
} for x in xrange(columns) for y in xrange(rows)]
} for x in xrange(-columns/2+1, 1+columns/2) for y in xrange(-rows/2+1, 1+rows/2)]
return cells

def assertGridSize(self, map, expected_rows, expected_columns=None):
Expand All @@ -36,21 +35,21 @@ def test_grid_size(self):
def test_all_cells(self):
map = WorldMap(self._generate_cells())
self.assertLocationsEqual(map.all_cells(),
[Location(x, y) for x in xrange(2) for y in xrange(2)])
[Location(x, y) for x in xrange(-1, 2) for y in xrange(-1, 2)])

def test_score_cells(self):
cells = self._generate_cells()
cells[0]['generates_score'] = True
cells[1]['generates_score'] = True
cells[5]['generates_score'] = True
map = WorldMap(cells)
self.assertLocationsEqual(map.score_cells(), (Location(0, 0), Location(0, 1)))
self.assertLocationsEqual(map.score_cells(), (Location(-1, -1), Location(0, 1)))

def test_pickup_cells(self):
cells = self._generate_cells()
cells[0]['pickup'] = {'health_restored': 5}
cells[3]['pickup'] = {'health_restored': 2}
cells[8]['pickup'] = {'health_restored': 2}
map = WorldMap(cells)
self.assertLocationsEqual(map.pickup_cells(), (Location(0, 0), Location(1, 1)))
self.assertLocationsEqual(map.pickup_cells(), (Location(-1, -1), Location(1, 1)))

def test_location_is_visible(self):
map = WorldMap(self._generate_cells())
Expand All @@ -61,13 +60,13 @@ def test_location_is_visible(self):
def test_x_off_map_is_not_visible(self):
map = WorldMap(self._generate_cells())
for y in (0, 1):
self.assertFalse(map.is_visible(Location(-1, y)))
self.assertFalse(map.is_visible(Location(-2, y)))
self.assertFalse(map.is_visible(Location(2, y)))

def test_y_off_map_is_not_visible(self):
map = WorldMap(self._generate_cells())
for x in (0, 1):
self.assertFalse(map.is_visible(Location(x, -1)))
self.assertFalse(map.is_visible(Location(x, -2)))
self.assertFalse(map.is_visible(Location(x, 2)))

def test_get_valid_cell(self):
Expand All @@ -81,15 +80,15 @@ def test_get_x_off_map(self):
map = WorldMap(self._generate_cells())
for y in (0, 1):
with self.assertRaises(KeyError):
map.get_cell(Location(-1, y))
map.get_cell(Location(-2, y))
with self.assertRaises(KeyError):
map.get_cell(Location(2, y))

def test_get_y_off_map(self):
map = WorldMap(self._generate_cells())
for x in (0, 1):
with self.assertRaises(KeyError):
map.get_cell(Location(x, -1))
map.get_cell(Location(x, -2))
with self.assertRaises(KeyError):
map.get_cell(Location(x, 2))

Expand All @@ -104,13 +103,13 @@ def test_cannot_move_to_cell_off_grid(self):
self.assertFalse(map.can_move_to(target))

def test_cannot_move_to_uninhabitable_cell(self):
cells = self._generate_cells(1, 1)
cells = self._generate_cells()
cells[0]['habitable'] = False
map = WorldMap(cells)
self.assertFalse(map.can_move_to(Location(0, 0)))
self.assertFalse(map.can_move_to(Location(-1, -1)))

def test_cannot_move_to_habited_cell(self):
cells = self._generate_cells(1, 1)
cells[0]['avatar'] = self.AVATAR
cells = self._generate_cells()
cells[1]['avatar'] = self.AVATAR
map = WorldMap(cells)
self.assertFalse(map.can_move_to(Location(0, 0)))
self.assertFalse(map.can_move_to(Location(-1, 0)))
35 changes: 19 additions & 16 deletions aimmo-game/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
from collections import defaultdict
import logging
import os
import sys
Expand All @@ -11,12 +12,9 @@
import flask
from flask_socketio import SocketIO, emit

from six.moves import range

from simulation.turn_manager import state_provider
from simulation import map_generator
from simulation.avatar.avatar_manager import AvatarManager
from simulation.location import Location
from simulation.game_state import GameState
from simulation.turn_manager import ConcurrentTurnManager
from simulation.turn_manager import SequentialTurnManager
Expand Down Expand Up @@ -58,21 +56,26 @@ def player_dict(avatar):
def get_world_state():
with state_provider as game_state:
world = game_state.world_map
num_cols = world.num_cols
num_rows = world.num_rows
grid = [[to_cell_type(world.get_cell(Location(x, y)))
for y in range(num_rows)]
for x in range(num_cols)]
player_data = {p.player_id: player_dict(p) for p in game_state.avatar_manager.avatars}
grid_dict = defaultdict(dict)
for cell in world.all_cells():
grid_dict[cell.location.x][cell.location.y] = to_cell_type(cell)
return {
'players': player_data,
'score_locations': [(cell.location.x, cell.location.y) for cell in world.score_cells()],
'pickup_locations': [(cell.location.x, cell.location.y) for cell in world.pickup_cells()],
'map_changed': True, # TODO: experiment with only sending deltas (not if not required)
'width': num_cols,
'height': num_rows,
'layout': grid,
}
'players': player_data,
'score_locations': [(cell.location.x, cell.location.y)
for cell in world.score_cells()],
'pickup_locations': [(cell.location.x, cell.location.y)
for cell in world.pickup_cells()],
# TODO: experiment with only sending deltas (not if not required)
'map_changed': True,
'width': world.num_cols,
'height': world.num_rows,
'minX': world.min_x(),
'minY': world.min_y(),
'maxX': world.max_x(),
'maxY': world.max_y(),
'layout': grid_dict,
}


@socketio.on('connect')
Expand Down
46 changes: 22 additions & 24 deletions aimmo-game/simulation/map_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@
from itertools import tee
from simulation.direction import ALL_DIRECTIONS
from simulation.location import Location
from simulation.world_map import Cell, WorldMap
from simulation.world_map import WorldMap
from six.moves import zip, range


def generate_map(height, width, obstacle_ratio=0.1):
grid = [[Cell(Location(x, y)) for y in range(height)] for x in range(width)]
world_map = WorldMap(grid)
world_map = WorldMap.generate_empty_map(height, width)

# We designate one non-corner edge cell as empty, to ensure that the map can be expanded
always_empty_edge_x, always_empty_edge_y = get_random_edge_index(height, width)
always_empty_edge_x, always_empty_edge_y = get_random_edge_index(world_map)
always_empty_location = Location(always_empty_edge_x, always_empty_edge_y)

for x, y in shuffled(_get_edge_coordinates(height, width)):
if (x, y) != (always_empty_edge_x, always_empty_edge_y) and random.random() < obstacle_ratio:
cell = grid[x][y]
for cell in shuffled(world_map.all_cells()):
if cell.location != always_empty_location and random.random() < obstacle_ratio:
cell.habitable = False
# So long as all habitable neighbours can still reach each other,
# then the map cannot get bisected
Expand Down Expand Up @@ -85,31 +84,30 @@ def manhattan_distance_to_destination_cell(this_branch):
return None


def get_random_edge_index(height, width, rng=random):
assert height >= 2 and width >= 2

num_row_cells = width - 2
num_col_cells = height - 2
def get_random_edge_index(map, rng=random):
num_row_cells = map.num_rows - 2
num_col_cells = map.num_cols - 2
num_edge_cells = 2*num_row_cells + 2*num_col_cells
random_cell = rng.randint(0, num_edge_cells-1)

if 0 <= random_cell < num_row_cells:
if 0 <= random_cell < num_col_cells:
# random non-corner cell on the first row
return random_cell+1, 0
elif num_row_cells <= random_cell < 2*num_row_cells:
# random non-corner cell on the last row
random_cell -= num_row_cells
return random_cell + 1, height - 1

random_cell -= 2*num_row_cells
return random_cell + 1 + map.min_x(), map.min_y()
random_cell -= num_col_cells

if 0 <= random_cell < num_col_cells:
# random non-corner cell on the last row
return random_cell + 1 + map.min_x(), map.max_y()
random_cell -= num_col_cells

if 0 <= random_cell < num_row_cells:
# random non-corner cell on the first column
return 0, random_cell + 1
assert num_col_cells <= random_cell < 2*num_col_cells
return map.min_x(), map.min_y() + random_cell + 1
random_cell -= num_row_cells

assert 0 <= random_cell < num_row_cells
# random non-corner cell on the last column
random_cell -= num_col_cells
return width - 1, random_cell + 1
return map.max_x(), map.min_y() + random_cell + 1


def get_adjacent_habitable_cells(cell, world_map):
Expand Down
85 changes: 65 additions & 20 deletions aimmo-game/simulation/world_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,28 @@ class WorldMap(object):
"""

def __init__(self, grid):
self.grid = grid
self._grid = grid

@classmethod
def _min_max_from_dimensions(cls, height, width):
max_x = int(math.floor(width / 2))
min_x = -(width - max_x - 1)
max_y = int(math.floor(height / 2))
min_y = -(height - max_y - 1)
return (min_x, max_x, min_y, max_y)

@classmethod
def generate_empty_map(cls, height, width):
(min_x, max_x, min_y, max_y) = WorldMap._min_max_from_dimensions(height, width)
grid = {}
for x in xrange(min_x, max_x+1):
for y in xrange(min_y, max_y+1):
location = Location(x, y)
grid[location] = Cell(location)
return cls(grid)

def all_cells(self):
return (cell for sublist in self.grid for cell in sublist)
return self._grid.itervalues()

def score_cells(self):
return (c for c in self.all_cells() if c.generates_score)
Expand All @@ -106,14 +124,33 @@ def pickup_cells(self):
return (c for c in self.all_cells() if c.pickup)

def is_on_map(self, location):
return (0 <= location.y < self.num_rows) and (0 <= location.x < self.num_cols)
try:
self._grid[location]
except KeyError:
return False
return True

def get_cell(self, location):
if not self.is_on_map(location):
try:
return self._grid[location]
except KeyError:
# For backwards-compatibility, this throws ValueError
raise ValueError('Location %s is not on the map' % location)
cell = self.grid[location.x][location.y]
assert cell.location == location, 'location lookup mismatch: arg={}, found={}'.format(location, cell.location)
return cell

def get_cell_by_coords(self, x, y):
return self.get_cell(Location(x, y))

def max_y(self):
return max(self._grid.keys(), key=lambda c: c.y).y

def min_y(self):
return min(self._grid.keys(), key=lambda c: c.y).y

def max_x(self):
return max(self._grid.keys(), key=lambda c: c.x).x

def min_x(self):
return min(self._grid.keys(), key=lambda c: c.x).x

def clear_cell_actions(self, location):
try:
Expand All @@ -124,11 +161,11 @@ def clear_cell_actions(self, location):

@property
def num_rows(self):
return len(self.grid[0])
return self.max_y() - self.min_y() + 1

@property
def num_cols(self):
return len(self.grid)
return self.max_x() - self.min_x() + 1

@property
def num_cells(self):
Expand All @@ -147,23 +184,27 @@ def reconstruct_interactive_state(self, 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 * 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_layer_to_vertical_edge()
self._add_layer_to_horizontal_edge()
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_layer_to_vertical_edge(self):
self.grid.append([Cell(Location(self.num_cols, y)) for y in range(self.num_rows)])
def _add_vertical_layer(self, x):
for y in xrange(self.min_y(), self.max_y()+1):
self._grid[Location(x, y)] = Cell(Location(x, y))

def _add_layer_to_horizontal_edge(self):
# Read rows once here, as we'll mutate it as part of the first iteration
rows = self.num_rows
for x in range(self.num_cols):
self.grid[x].append(Cell(Location(x, rows)))
def _add_horizontal_layer(self, y):
for x in xrange(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():
Expand Down Expand Up @@ -207,7 +248,6 @@ def get_random_spawn_location(self):
"""
return self._get_random_spawn_locations(1)[0].location

# TODO: cope with negative coords (here and possibly in other places)
def can_move_to(self, target_location):
if not self.is_on_map(target_location):
return False
Expand Down Expand Up @@ -241,4 +281,9 @@ def get_partial_fog_distance(self):
return PARTIAL_FOG_OF_WAR_DISTANCE

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

def __iter__(self):
return ((self.get_cell(Location(x, y))
for y in xrange(self.min_y(), self.max_y()+1))
for x in xrange(self.min_x(), self.max_x()+1))
9 changes: 5 additions & 4 deletions aimmo-game/tests/simulation/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ def get_cell(self, location):
def num_rows(self):
return float('inf')

def get_random_spawn_location(self):
return Location(10, 10)


class EmptyMap(object):
@property
def num_cols(self):
return float('inf')

class EmptyMap(WorldMap):
def __init__(self):
pass

def can_move_to(self, target_location):
return False

Expand Down
Loading

0 comments on commit f9f72f7

Please sign in to comment.