From 2fe31caf455648f3bb8603d804a4a873244acec6 Mon Sep 17 00:00:00 2001 From: Peter Ward Date: Sun, 6 Apr 2014 21:41:27 +1000 Subject: Ripping out functionality to meet new spec. --- robots/constants.py | 45 +++++++++++++ robots/cursesviewer.py | 28 +++------ robots/game.py | 167 +++++++++++++++---------------------------------- robots/state.py | 105 +++++++++++++++++++++++++++++++ robots/utils.py | 7 +++ simple.py | 23 +++---- 6 files changed, 227 insertions(+), 148 deletions(-) create mode 100644 robots/constants.py create mode 100644 robots/state.py diff --git a/robots/constants.py b/robots/constants.py new file mode 100644 index 0000000..4c36cce --- /dev/null +++ b/robots/constants.py @@ -0,0 +1,45 @@ +DIRECTIONS = { + 'U': (0, -1), + 'D': (0, 1), + 'L': (-1, 0), + 'R': (1, 0), +} + +class City: + NORMAL = '.' + GHOST = 'X' + FACTORY = '+' + + # Cities which can pledge allegiance. + # Allegiable is totally a real word. + # No, really, it is. + # Why doesn't anyone believe me?!! + # /sobs + allegiable = (NORMAL, FACTORY) + + # Cities which robots can move into. + traversable = (NORMAL, FACTORY) + +class Action: + UP = 'U' + DOWN = 'D' + LEFT = 'L' + RIGHT = 'R' + PROMOTE = 'P' + NOTHING = '-' + + all = (UP, DOWN, LEFT, RIGHT, PROMOTE, NOTHING) + +class Energy: + INITIAL = 6 + MAXIMUM = 9 + GAIN = 2 + +ENERGY_BY_ACTION = { + Action.UP: 3, + Action.DOWN: 3, + Action.LEFT: 3, + Action.RIGHT: 3, + Action.PROMOTE: 4, + Action.NOTHING: 0, +} diff --git a/robots/cursesviewer.py b/robots/cursesviewer.py index 0131894..434df47 100644 --- a/robots/cursesviewer.py +++ b/robots/cursesviewer.py @@ -20,31 +20,27 @@ class CursesViewer: colours = cycle('red blue green yellow magenta cyan'.split()) self.player_colours = dict(zip( - game.players.keys(), + game.state.players, colours, )) self.player_colours['*'] = 'white' - def draw_board(self): + def draw_board(self, state): print(self.terminal.clear, end='') - robots = {} - for player, info in self.game.players.items(): - for x, y in info['robots']: - robots[x, y] = player - - width = len(self.game.board[0]) * 2 + width = len(state.board[0]) * 2 print(Box.TL + Box.H * width + Box.TR) - for y, row in enumerate(self.game.board): + for y, row in enumerate(state.board): output = [] for x, cell in enumerate(row): value = ' ' - background = self.player_colours.get(cell) + background = self.player_colours.get(cell['allegiance']) + + if cell['robots']: + (robot_owner, energy), = cell['robots'] - robot_owner = robots.get((x, y)) - if robot_owner: foreground = self.player_colours[robot_owner] if foreground == background: foreground = 'white' @@ -64,13 +60,9 @@ class CursesViewer: def run(self): limiter = rate_limit(10) - while not self.game.finished: + for state in self.game: next(limiter) - - self.draw_board() - self.game.next() - - self.draw_board() + self.draw_board(state) winners = self.game.finished if winners is True: diff --git a/robots/game.py b/robots/game.py index 82ee5ce..ce343e1 100644 --- a/robots/game.py +++ b/robots/game.py @@ -1,30 +1,11 @@ -from collections import defaultdict, OrderedDict +from collections import defaultdict, deque from contextlib import contextmanager from copy import deepcopy +from robots.constants import Action, City, DIRECTIONS, Energy, ENERGY_BY_ACTION +from robots.state import GameState from robots.utils import iter_board -class Squares: - EMPTY = '.' - WALL = '*' - -class Actions: - UP = 'U' - DOWN = 'D' - LEFT = 'L' - RIGHT = 'R' - PAINT = 'P' - NOTHING = '-' - - all = (UP, DOWN, LEFT, RIGHT, PAINT, NOTHING) - -DIRECTIONS = { - 'U': (0, -1), - 'D': (0, 1), - 'L': (-1, 0), - 'R': (1, 0), -} - @contextmanager def protected(value, name): value_copy = deepcopy(value) @@ -34,7 +15,7 @@ def protected(value, name): if value_copy != value: raise RuntimeError('Protected value %s was modified.' % name) -def _extract_spawn_points(map_): +def extract_spawn_points(map_): points = [] for x, y, cell in iter_board(map_): @@ -44,7 +25,7 @@ def _extract_spawn_points(map_): pass else: points.append((n, (x, y))) - map_[y][x] = Squares.EMPTY + map_[y][x] = City.NORMAL points.sort() return [point for n, point in points] @@ -52,17 +33,12 @@ def _extract_spawn_points(map_): class Game: def __init__(self, map_): self.bots = {} - self.players = OrderedDict() - - self.board = deepcopy(map_) - self._spawns = _extract_spawn_points(self.board) + self.time = 0 - self._painted_tiles = { - '*': float('inf'), # not used - '.': self.available_tiles, - } + map_ = deepcopy(map_) + self.spawn_points = deque(extract_spawn_points(map_)) - self.time = 0 + self.state = GameState(map_) def add_bot(self, bot, name=None): if name is None: @@ -74,46 +50,30 @@ class Game: (name,) ) - spawn = self._spawns.pop() - self.bots[name] = bot - self.players[name] = { - 'robots': [spawn], - 'spawn': spawn, - 'last_spawn': 0, - } - self._painted_tiles[name] = 0 - @property - def width(self): - return len(self.board[0]) - - @property - def height(self): - return len(self.board) - - @property - def available_tiles(self): - n = 0 - for x, y, cell in iter_board(self.board): - if cell != Squares.WALL: - n += 1 - return n + # Place the initial robot for this player. + x, y = self.spawn_points.popleft() + self.state.robots_by_player[name] = [ + (x, y, Energy.INITIAL), + ] @property def finished(self): - target = int(round(self.available_tiles * 0.5)) + target = self.state.n_cities_to_win winners = [] roboteers = [] - for whoami, info in self.players.items(): - n_tiles = self._painted_tiles[whoami] - if n_tiles > target: - winners.append(whoami) + n_allegiances_by_player = self.state.n_allegiances_by_player - if info['robots']: - roboteers.append(whoami) + for name, robots in self.state.robots_by_player.items(): + n_cities = n_allegiances_by_player[name] + if n_cities >= target: + winners.append(name) + + if robots: + roboteers.append(name) won = bool(winners) if len(roboteers) <= 1: @@ -125,22 +85,21 @@ class Game: def call_bots(self): for whoami, bot in self.bots.items(): try: - with protected(self.players, 'players') as players, \ - protected(self.board, 'board') as board: - result = bot(whoami, players, board) + with protected(self.state, 'state') as state: + result = bot(whoami, state) assert isinstance(result, (str, list, tuple)), \ 'Bot did not return a str/list/tuple, got %r' % (result,) result = str(''.join(result)) - my_robots = self.players[whoami]['robots'] + my_robots = self.state.robots_by_player[whoami] assert len(result) == len(my_robots), ( 'Bot did not return an action for all robots ' '(%d actions != %d robots)' % (len(result), len(my_robots)) ) for action in result: - assert action in Actions.all, ( + assert action in Action.all, ( 'Got unexpected action %r' % (action,) ) @@ -153,47 +112,31 @@ class Game: yield whoami, bot, action def apply_actions(self, actions): - new_robots = defaultdict(list) - bot_locations = defaultdict(int) + robots_by_player = defaultdict(list) for whoami, bot, action in actions: - x, y = bot - - if action == Actions.PAINT: - previous = self.board[y][x] - self._painted_tiles[previous] -= 1 + x, y, energy = bot - self.board[y][x] = whoami - self._painted_tiles[whoami] += 1 + if action == Action.PROMOTE: + self.state.allegiances[x, y] = whoami - elif action != Actions.NOTHING: + elif action != Action.NOTHING: dx, dy = DIRECTIONS[action] - nx = (x + dx) % self.width - ny = (y + dy) % self.height + nx = (x + dx) % self.state.width + ny = (y + dy) % self.state.height - if self.board[ny][nx] != Squares.WALL: + if self.state.cities[ny][nx] in City.traversable: x = nx y = ny + else: + action = Action.NOTHING - new_robots[whoami].append((x, y)) - bot_locations[x, y] += 1 + energy -= ENERGY_BY_ACTION[action] - for whoami, (x, y) in self.spawn_bots(): - new_robots[whoami].append((x, y)) - bot_locations[x, y] += 1 + robots_by_player[whoami].append((x, y, energy)) - to_remove = set( - (x, y) - for (x, y), count in bot_locations.items() - if count > 1 - ) - - for whoami, info in self.players.items(): - info['robots'][:] = [ - (x, y) - for x, y in new_robots[whoami] - if (x, y) not in to_remove - ] + for name, robots in self.state.robots_by_player.items(): + robots[:] = robots_by_player[name] def get_spawn_time(self, whoami): n_robots = len(self.players[whoami]['robots']) @@ -205,27 +148,19 @@ class Game: ) return int(10 * fraction_unowned) + n_robots - def spawn_bots(self): - for whoami, info in self.players.items(): - spawn_time = self.get_spawn_time(whoami) -# print(whoami, spawn_time) - time_since_last_spawn = self.time - info['last_spawn'] - n_tiles = self._painted_tiles[whoami] - if time_since_last_spawn >= spawn_time: - yield whoami, info['spawn'] - info['last_spawn'] = self.time + def __iter__(self): + while not self.finished: + yield self.state + self.next() + yield self.state def next(self): + # 1. You give commands you your robots. actions = self.call_bots() + # 2. They perform the requested commands. self.apply_actions(actions) - - import sys - print( - *(self.get_spawn_time(whoami) for whoami in self.players), -# *(len(info['robots']) for info in self.players.values()), - sep='\t', - file=sys.stderr - ) - sys.stderr.flush() + # 3. Factories produce new robots. + # 4. Combats are resolved. + # 5. Each robot gains energy, up to the maximum. self.time += 1 diff --git a/robots/state.py b/robots/state.py new file mode 100644 index 0000000..1fe9c26 --- /dev/null +++ b/robots/state.py @@ -0,0 +1,105 @@ +from collections import defaultdict, Counter + +from robots.constants import City +from robots.utils import ceil_div + +class GameState: + """The state of a game at a point in time. + + Instances can be serialized for inter-process communication. + """ + + def __init__(self, board): + # _[y][x] = City + self.cities = board + # _[x, y] = str (player id) + self.allegiances = {} + # _[player_id] = [(x, y, energy), ...] + self.robots_by_player = {} + + def __hash__(self): + return hash(self._authorative_state) + + def __eq__(self, other): + if isinstance(other, GameState): + return self._authorative_state == other._authorative_state + + @property + def _authorative_state(self): + return (self.cities, self.allegiances, self.robots_by_player) + + @property + def players(self): + return self.robots_by_player.keys() + + @property + def robots(self): + result = defaultdict(list) + for player, robots in self.robots_by_player.items(): + for x, y, energy in robots: + result[x, y].append((player, energy)) + return result + + @property + def allegiances_by_player(self): + result = defaultdict(list) + for (x, y), player in self.allegiances.items(): + result[player].append((x, y)) + return result + + @property + def board(self): + # TODO: remove this once I've figured out caching. + self_robots = self.robots + + result = [] + for y, row in enumerate(self.cities): + result_row = [] + for x, city in enumerate(row): + allegiance = self.allegiances.get((x, y)) + robots = self_robots[x, y] + + result_row.append({ + 'city': city, + 'allegiance': allegiance, + 'robots': robots, + }) + result.append(result_row) + return result + + @property + def width(self): + return len(self.cities[0]) + + @property + def height(self): + return len(self.cities) + + @property + def n_alive_players(self): + """How many players are still alive.""" + return sum( + 1 + for player, robots in self.robots_by_player.items() + if robots + ) + + @property + def n_allegiable_cities(self): + """How many cities are capable of pledging allegiance.""" + return sum( + 1 + for row in self.cities + for city in row + if city in City.allegiable + ) + + @property + def n_allegiances_by_player(self): + """How many cities have pledged allegiance to each player.""" + return Counter(self.allegiances.values()) + + @property + def n_cities_to_win(self): + """How many cities you need to pledge allegiance to you to win.""" + return ceil_div(self.n_allegiable_cities, self.n_alive_players) diff --git a/robots/utils.py b/robots/utils.py index 544bec7..dff2a3e 100644 --- a/robots/utils.py +++ b/robots/utils.py @@ -1,6 +1,13 @@ from random import sample import time +def ceil_div(a, b): + """Divide a by b, rounding towards infinity.""" + return -(-a // b) + +def ilen(items): + return sum(1 for _ in items) + def add_spawns(map_, n_spawns): available = [] for x, y, cell in iter_board(map_): diff --git a/simple.py b/simple.py index 932cc09..10dba02 100644 --- a/simple.py +++ b/simple.py @@ -2,9 +2,7 @@ from collections import defaultdict import random import robots - -iter_board = robots.utils.iter_board -DIRECTIONS = robots.game.DIRECTIONS +from robots.constants import DIRECTIONS def shuffled(items): items = list(items) @@ -103,8 +101,8 @@ def never_paint(whoami, players, board): for _ in range(len(my_robots)) ) -def bot(whoami, players, board): - my_robots = players[whoami]['robots'] +def bot(whoami, state): + my_robots = state.robots_by_player[whoami] return ''.join( random.choice('ULDRP-') for _ in range(len(my_robots)) @@ -114,18 +112,15 @@ if __name__ == '__main__': # random.seed(42) map_ = robots.border_map(30, 10, 0) for y in range(8): - map_[y][10] = '*' + map_[y][10] = 'X' for y in range(11, 2, -1): - map_[y][20] = '*' + map_[y][20] = 'X' map_[5][5] = '1' - map_[5][15] = '2' - map_[5][25] = '3' + map_[5][15] = '3' + map_[5][25] = '2' game = robots.Game(map_) -# game.add_bot(attacker, 'Alice') -# game.add_bot(attacker, 'Adam') - game.add_bot(attacker, 'Barry') - game.add_bot(attacker, 'Bob') - game.add_bot(never_paint, 'Baldrick') + game.add_bot(bot, 'Alice') + game.add_bot(bot, 'Bob') viewer = robots.CursesViewer(game) viewer.run() -- cgit v1.2.3