summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore3
-rw-r--r--robots/__init__.py3
-rw-r--r--robots/cursesviewer.py72
-rw-r--r--robots/game.py150
-rw-r--r--robots/utils.py39
-rw-r--r--simple.py18
6 files changed, 285 insertions, 0 deletions
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()