diff options
author | Peter Ward <peteraward@gmail.com> | 2012-07-21 12:14:16 +1000 |
---|---|---|
committer | Peter Ward <peteraward@gmail.com> | 2012-07-21 12:14:16 +1000 |
commit | b99240781b813dd030e18750bb9d2ac62711c249 (patch) | |
tree | 62c76efdd9d4115756d92f7eadff6b912f1aee76 /snakegame/engine.py | |
parent | 38ad46d7ce7cb965726d2af5dee8f90261b4a44a (diff) |
Refactoring into a single engine and viewers.
Diffstat (limited to 'snakegame/engine.py')
-rw-r--r-- | snakegame/engine.py | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/snakegame/engine.py b/snakegame/engine.py new file mode 100644 index 0000000..7ba9e0b --- /dev/null +++ b/snakegame/engine.py @@ -0,0 +1,196 @@ +from collections import defaultdict, deque +from copy import deepcopy +from random import Random +from string import ascii_lowercase as lowercase +import sys +import time +import traceback + +from snakegame.colour import hash_colour +from snakegame import common + +SOFT_TIME_LIMIT = 0.5 +HARD_TIME_LIMIT = 1.0 + +class Engine(object): + def __init__( + self, + rows, columns, n_apples, + wrap=True, results=False, + random=None, + *args, **kwargs + ): + super(Engine, self).__init__(*args, **kwargs) + + if random is None: + random = Random() + self.random = random + + self.wrap = wrap + self.bots = {} + self.results = None + if results: + self.results = open('results.csv', 'a+') + + self.new_game(rows, columns, n_apples) + + def get_random_position(self): + x = self.random.randrange(0, self.columns) + y = self.random.randrange(0, self.rows) + return (x, y) + + def replace_random(self, old, new): + for i in xrange(self.rows * self.columns): + x, y = self.get_random_position() + if self.board[y][x] == old: + self.board[y][x] = new + return x, y + + def new_game(self, rows, columns, n_apples): + self.game_ticks = 0 + self.game_id = self.random.randint(0, sys.maxint) + + self.letters = list(lowercase) + self.letters.reverse() + + self.rows = rows + self.columns = columns + + self.messages_by_team = defaultdict(dict) + + # make board + self.board = [[common.EMPTY for x in xrange(columns)] for y in xrange(rows)] + for i in xrange(n_apples): + x, y = self.get_random_position() + self.board[y][x] = common.APPLE + + def add_bot(self, bot, team=None): + """ + A bot is a callable object, with this method signature: + def bot_callable( + board=[[cell for cell in row] for row in board], + position=(snake_x, snake_y) + ): + return random.choice('RULD') + + If team is not None, this means you will get a third parameter, + containing messages from the other bots on your team. + """ + letter = self.letters.pop() + + name = bot.__name__ + colour = hash_colour(name) + + position = self.replace_random(common.EMPTY, letter.upper()) + if position is None: + raise KeyError, "Could not insert snake into the board." + + self.bots[letter] = [bot, colour, deque([position]), team] + return letter + + def remove_bot(self, letter): + letter = letter.lower() + + time_score = self.game_ticks + + for row in self.board: + for x, cell in enumerate(row): + if cell.lower() == letter: + row[x] = common.EMPTY + + bot = self.bots[letter] + del self.bots[letter] + + if not self.results: + return + + try: + name = bot[0].__name__ + except AttributeError: + pass + else: + apple_score = len(bot[2]) + self.results.write('%s,%s,%s,%s\n' % \ + (self.game_id, name, apple_score, time_score)) + self.results.flush() + + def update_snakes(self): + self.game_ticks += 1 + + for letter, (bot, colour, path, team) in self.bots.items(): + board = deepcopy(self.board) + try: + x, y = path[-1] + + start = time.time() + + if team is None: + d = bot(board, (x, y)) + else: + messages = self.messages_by_team[team] + d, message = bot(board, (x, y), messages) + + assert isinstance(message, str), \ + "Message should be a byte string, not %s (%r)." % ( + type(message), + message, + ) + messages[letter] = message + + end = time.time() + delta = end - start + assert delta < HARD_TIME_LIMIT, 'Exceeded hard time limit.' + if delta >= SOFT_TIME_LIMIT: + print 'Bot %s (%r) exceeded soft time limit.' % (letter.upper(), bot) + + # Sanity checking... + assert isinstance(d, basestring), \ + "Return value should be a string." + d = d.upper() + assert d in common.directions, "Return value should be 'U', 'D', 'L' or 'R'." + + # Get new position. + dx, dy = common.directions[d] + nx = x + dx + ny = y + dy + + if self.wrap: + ny %= self.rows + nx %= self.columns + else: + if ny < 0 or ny >= self.rows or nx < 0 or nx >= self.columns: + self.remove_bot(letter) + continue + + oldcell = self.board[ny][nx] + if common.is_vacant(oldcell): + # Move snake forward. + self.board[ny][nx] = letter.upper() + path.append((nx, ny)) + + # Make old head into body. + self.board[y][x] = letter.lower() + + if oldcell == common.APPLE: + # Add in an apple to compensate. + self.replace_random(common.EMPTY, common.APPLE) + else: + # Remove last part of snake. + ox, oy = path.popleft() + self.board[oy][ox] = common.EMPTY + else: + self.remove_bot(letter) + + except: + print "Exception in bot %s (%s):" % (letter.upper(), bot) + print '-'*60 + traceback.print_exc() + print '-'*60 + self.remove_bot(letter) + + def __iter__(self): + yield self.board + while self.bots: + self.update_snakes() + yield self.board + |