From 342bdfdc64e0b7e1874e6e9280e1a2475cc6b8bb Mon Sep 17 00:00:00 2001 From: Peter Ward Date: Sat, 22 Feb 2014 20:22:46 +1100 Subject: initial version --- .hgignore | 3 + robots/__init__.py | 3 + robots/cursesviewer.py | 72 ++++++++++++++++++++++++ robots/game.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ robots/utils.py | 39 +++++++++++++ simple.py | 18 ++++++ 6 files changed, 285 insertions(+) create mode 100644 .hgignore create mode 100644 robots/__init__.py create mode 100644 robots/cursesviewer.py create mode 100644 robots/game.py create mode 100644 robots/utils.py create mode 100644 simple.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..e21f17c --- /dev/null +++ b/.hgignore @@ -0,0 +1,3 @@ +syntax: glob +*~ +*.py[co] diff --git a/robots/__init__.py b/robots/__init__.py new file mode 100644 index 0000000..089cb44 --- /dev/null +++ b/robots/__init__.py @@ -0,0 +1,3 @@ +from robots.cursesviewer import CursesViewer +from robots.game import Game +from robots.utils import empty_map diff --git a/robots/cursesviewer.py b/robots/cursesviewer.py new file mode 100644 index 0000000..4dbe198 --- /dev/null +++ b/robots/cursesviewer.py @@ -0,0 +1,72 @@ +from itertools import cycle + +from blessings import Terminal + +from robots.utils import rate_limit + +class Box: + V = '│' + H = '─' + TL = '┌' + TR = '┐' + BL = '└' + BR = '┘' + +class CursesViewer: + def __init__(self, game): + self.game = game + + self.terminal = Terminal() + + colours = cycle('red blue green yellow magenta cyan'.split()) + self.player_colours = dict(zip( + game.players.keys(), + colours, + )) + self.player_colours['*'] = 'white' + + def draw_board(self): + print(self.terminal.clear) + + 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 + print(Box.TL + Box.H * width + Box.TR) + + for y, row in enumerate(self.game.board): + output = [] + for x, cell in enumerate(row): + value = ' ' + + background = self.player_colours.get(cell) + + robot_owner = robots.get((x, y)) + if robot_owner: + foreground = self.player_colours[robot_owner] + if foreground == background: + foreground = 'white' + + func = getattr(self.terminal, foreground) + value = func('<>') + + if background: + func = getattr(self.terminal, 'on_' + background) + value = func(value) + + output.append(value) + print(Box.V + ''.join(output) + Box.V) + + print(Box.BL + Box.H * width + Box.BR) + + def run(self): + limiter = rate_limit(10) + + while not self.game.finished: + next(limiter) + + self.draw_board() + self.game.next() + diff --git a/robots/game.py b/robots/game.py new file mode 100644 index 0000000..143f391 --- /dev/null +++ b/robots/game.py @@ -0,0 +1,150 @@ +from copy import deepcopy +from contextlib import contextmanager + +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) + try: + yield value_copy + finally: + if value_copy != value: + raise RuntimeError('Protected value %s was modified.' % name) + +def _extract_spawn_points(map_): + points = [] + + for x, y, cell in iter_board(map_): + try: + n = int(cell) + except ValueError: + pass + else: + points.append((n, (x, y))) + map_[y][x] = Squares.EMPTY + + points.sort() + return [point for n, point in points] + +class Game: + def __init__(self, map_): + self.bots = {} + self.players = {} + + self.board = deepcopy(map_) + self._spawns = _extract_spawn_points(self.board) + + def add_bot(self, bot, name=None): + if name is None: + name = bot.__name__ + + if name in self.bots: + raise KeyError( + 'There is already a bot named %r in this game!' % + (name,) + ) + + spawn = self._spawns.pop() + + self.bots[name] = bot + self.players[name] = { + 'robots': [spawn], + 'spawn': spawn, + } + + @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 + + @property + def finished(self): + return False + + def next(self): + all_actions = [] + + 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) + + 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'] + 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, ( + 'Got unexpected action %r' % (action,) + ) + + except Exception as e: + # TODO: log this exception + print(e) + + else: + for bot, action in zip(my_robots, result): + all_actions.append((whoami, bot, action)) + + new_robots = {} + + for whoami, bot, action in all_actions: + x, y = bot + if action == Actions.PAINT: + self.board[y][x] = whoami + elif action == Actions.NOTHING: + pass + else: + dx, dy = DIRECTIONS[action] + x += dx + y += dy + + x %= self.width + y %= self.height + + if whoami not in new_robots: + new_robots[whoami] = [] + new_robots[whoami].append((x, y)) + + for whoami, info in self.players.items(): + info['robots'][:] = new_robots[whoami] diff --git a/robots/utils.py b/robots/utils.py new file mode 100644 index 0000000..4473a6b --- /dev/null +++ b/robots/utils.py @@ -0,0 +1,39 @@ +from random import sample +import time + +def empty_map(width, height, n_spawns): + board = [['.'] * width for y in range(height)] + + all_positions = [ + (x, y) + for x in range(width) + for y in range(height) + ] + spawns = sample(all_positions, n_spawns) + + for i, (x, y) in enumerate(spawns): + board[y][x] = str(i) + return board + +def rate_limit(fps): + delay_ms = 1. / fps + + while True: + # time one iteration of the loop + start = time.time() + yield + end = time.time() + yield_time = end - start + + # delay to fill up the rest of the time + wait_time = delay_ms - yield_time + if wait_time < 0: + # TODO: log failure +# log.debug('not meeting rate (%.2f ms)' % (wait_time * 1000)) + continue + time.sleep(wait_time) + +def iter_board(board): + for y, row in enumerate(board): + for x, cell in enumerate(row): + yield x, y, cell diff --git a/simple.py b/simple.py new file mode 100644 index 0000000..faeb366 --- /dev/null +++ b/simple.py @@ -0,0 +1,18 @@ +import random +import robots + +def bot(whoami, players, board): + my_robots = players[whoami]['robots'] + action = random.choice('UDLRP-') + return action * len(my_robots) + +if __name__ == '__main__': + random.seed(64) + map_ = robots.empty_map(20, 20, 4) + game = robots.Game(map_) + game.add_bot(bot, 'Alice') + game.add_bot(bot, 'Bob') + game.add_bot(bot, 'Charlie') + game.add_bot(bot, 'Doug') + viewer = robots.CursesViewer(game) + viewer.run() -- cgit v1.2.3