From a2130079ca771104d87a919f0b4d88583e66d566 Mon Sep 17 00:00:00 2001 From: Peter Ward Date: Thu, 19 Jul 2012 23:05:30 +1000 Subject: Lots of refactoring. --- README | 141 +++--- setup.py | 6 +- snakegame/__init__.py | 10 + snakegame/bots/__init__.py | 36 ++ snakegame/common.py | 55 ++- snakegame/engines/__init__.py | 29 ++ snakegame/engines/base.py | 159 ++++++ snakegame/engines/pyglet.py | 12 +- snakegame/pngcanvas.py | 291 ----------- snakegame/pngchart.py | 53 -- snakegame/pygooglechart.py | 1066 ----------------------------------------- snakegame/snake.py | 6 +- snakegame/stats.py | 61 --- 13 files changed, 354 insertions(+), 1571 deletions(-) create mode 100644 snakegame/bots/__init__.py create mode 100644 snakegame/engines/base.py delete mode 100644 snakegame/pngcanvas.py delete mode 100644 snakegame/pngchart.py delete mode 100644 snakegame/pygooglechart.py delete mode 100644 snakegame/stats.py diff --git a/README b/README index 97e7b5b..8e6140f 100644 --- a/README +++ b/README @@ -1,86 +1,57 @@ -How to write a bot -================== - -guarantee code indicates something you can run which will execute without errors -if "input" refers to a valid board. - -input = { - # the object you control - - # guarantee code: - # me = input["whoami"] - # my = input["objects"][me] - # assert my["type"] == "snake" - # (see below for more guarantees) - - "whoami": "a", - - # the board is by a torus by default (wraps around horiz & vert), - # but maps will have walls on all the edges to disable this. - - # guarantee code: - # for row in input["board"]: - # for item in row: - # assert item in input["objects"] - - "board": [ - ["W", "W", "W", "W", "W", "W"], - ["W", " ", "*", " ", " ", "W"], - ["W", "a", " ", "b", "b", "W"], - ["W", " ", " ", " ", " ", "W"], - ["W", "W", "W", "W", "W", "W"] - ], - - # each object refers to a type of "thing" which can be in the board. - - # guarantee code: - # for key, thing in input["objects"].items(): - # if thing["type"] == "snake": - # assert "valid_moves" in thing - # x, y = thing["head"] - # assert input["board"][y][x] == key - # elif thing["type"] == "special": - # for effect in thing["effects"]: - # assert effect[0] in ("die", "grow") - # else: - # raise TypeError("invalid thing type") - - "objects": { - "a": { - "type": "snake", - "head": [1, 2], - "valid_moves": { - "L": [0, 2], - "R": [2, 2], - "U": [1, 1], - "D": [1, 3] - } - }, - - "b": { - "type": "snake", - "head": [4, 2], - "valid_moves": { - "L": [3, 2], - "R": [5, 2], - "U": [4, 1], - "D": [4, 3] - } - }, - - "W": { - "type": "special", - "effects": [ - ["die"] - ] - }, - - "*": { # represents an apple - "type": "special", - "effects": [ - ["grow", 1] - ] - } - } -} += Getting started = + +$ cat > simple.py +def bot(board, position): + return 'L' +^D + +$ snakegame -e pyglet simple + += Writing a bot = + +A bot is simply a Python function which takes two arguments, the current state +of the board, and the current position of the head of your snake. The function +must return one of the strings 'L', 'U', 'R' or 'D', indicating which direction +the snake should next move (left, up, right or down, respectively). + +== The Board == + +The board is a list containing each row of the board. +Each row is a list containing the cells of that row. + +The board is actually a torus (that is, the top edge wraps to the bottom, and +the left edge to the right, and vice versa). +Map designers can easily turn this into a normal grid simply by placing walls on +the edges. + +Each cell is a single character string: + +* period (.) indicates an empty cell +* asterisk (*) indicates an apple +* plus (+) indicates an ice cream +* minus (-) indicates a shrinking potion +* octothorpe (#) indicates a wall +* uppercase letters (A-Z) indicate the head of a snake. +* lowercase letters (a-z) indicate the body of a snake. + +All other characters are reserved for future use. + +Every snake will have exactly one head. +Snakes may have no body. +Snakes may have a body which is not contiguous on the board. + +== Usual boilerplate == + +The typical boilerplate for writing a bot looks like this, which gets the +character of your snake’s head, and the size of the board. + +def bot(board, position): + x, y = position + me = board[y][x] + + height = len(board) + width = len(board[0]) + + # ... + return 'L' diff --git a/setup.py b/setup.py index 9c07911..559bf7b 100644 --- a/setup.py +++ b/setup.py @@ -7,5 +7,9 @@ setup( author='Peter Ward', author_email='peteraward@gmail.com', packages=['snakegame'], - scripts=[], + entry_points={ + 'console_scripts': [ + 'snakegame = snakegame:main', + ] + }, ) diff --git a/snakegame/__init__.py b/snakegame/__init__.py index e69de29..7fbff4d 100644 --- a/snakegame/__init__.py +++ b/snakegame/__init__.py @@ -0,0 +1,10 @@ +import argparse + +from snakegame.engines import BUILTIN_ENGINES +from snakegame.bots import BUILTIN_BOTS + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('-e', '--engine') + args = parser.parse_args(argv) + print args diff --git a/snakegame/bots/__init__.py b/snakegame/bots/__init__.py new file mode 100644 index 0000000..05e3a78 --- /dev/null +++ b/snakegame/bots/__init__.py @@ -0,0 +1,36 @@ +from random import choice + +from snakegame import common + +def make_direction_bot(direction, human): + def bot(board, position): + return direction + bot.__doc__ = 'This bot always moves %s.' % human + return bot + +up_bot = make_direction_bot('U', 'up') +down_bot = make_direction_bot('D', 'down') +left_bot = make_direction_bot('L', 'left') +right_bot = make_direction_bot('R', 'right') + +def random_bot(board, position): + "This bot just chooses a random direction to move." + return choice('UDLR') + +def random_avoid_bot(board, position): + """ + This bot chooses a random direction to move, but will not move into a + square which will kill it immediately (unless it has no choice). + """ + x, y = position + + available = [] + for direction, (dx, dy) in common.directions.items(): + cell = common.get_cell(board, x + dx, y + dy) + if common.is_vacant(cell): + available.append(direction) + + if not available: + return 'U' + return choice(available) + diff --git a/snakegame/common.py b/snakegame/common.py index 398f810..a44219b 100644 --- a/snakegame/common.py +++ b/snakegame/common.py @@ -1,4 +1,5 @@ -import os +from string import ascii_lowercase as lowercase, ascii_uppercase as uppercase +alphabet = lowercase + uppercase directions = { 'U': (0, -1), @@ -7,7 +8,53 @@ directions = { 'R': (1, 0), } -class Squares(object): - EMPTY = '.' - APPLE = '*' +EMPTY = '.' +APPLE = '*' +WALL = '#' +is_empty = EMPTY.__eq__ +is_apple = APPLE.__eq__ +is_wall = WALL.__eq__ + +def is_vacant(cell): + return cell in (EMPTY, APPLE) + +def is_blocking(cell): + return cell not in (EMPTY, APPLE) + +def is_snake(cell): + return cell in alphabet + +def is_snake_head(cell): + return cell in uppercase + +def is_snake_body(cell): + return cell in lowercase + +def is_enemy_snake(cell, me): + assert me.isupper() + return is_snake(cell) and cell.upper() != me + +def is_my_snake(cell, me): + assert me.isupper() + return cell.upper() == me + +def get_size(board): + height = len(board) + width = len(board[0]) + return width, height + +def in_bounds(x, y, width, height): + return ( + x >= 0 and x < width and + y >= 0 and y < height + ) + +def get_cell(board, x, y, wrap=True): + width, height = get_size(board) + if wrap: + x %= width + y %= height + elif not in_bounds(x, y, width, height): + return None + return board[y][x] diff --git a/snakegame/engines/__init__.py b/snakegame/engines/__init__.py index e69de29..197aeec 100644 --- a/snakegame/engines/__init__.py +++ b/snakegame/engines/__init__.py @@ -0,0 +1,29 @@ +try: + from collections import OrderedDict as MaybeOrderedDict +except ImportError: + MaybeOrderedDict = dict + +from snakegame.engines.base import Engine + +BUILTIN_ENGINES = MaybeOrderedDict() + +try: + from snakegame.engines.pyglet import PygletEngine +except ImportError: + pass +else: + BUILTIN_ENGINES['pyglet'] = PygletEngine + +try: + from snakegame.engines.pygame import PygameEngine +except ImportError: + pass +else: + BUILTIN_ENGINES['pygame'] = PygameEngine + +try: + from snakegame.engines.curses import CursesEngine +except ImportError: + pass +else: + BUILTIN_ENGINES['curses'] = CursesEngine diff --git a/snakegame/engines/base.py b/snakegame/engines/base.py new file mode 100644 index 0000000..539498f --- /dev/null +++ b/snakegame/engines/base.py @@ -0,0 +1,159 @@ +from __future__ import division + +import sys +import time +import string +import random +from colour import hash_colour +from random import randint +from collections import deque +from copy import deepcopy +import traceback + +from common import * + +class Engine(object): + def __init__(self, rows, columns, n_apples, wrap=False, results=False, + *args, **kwargs): + super(Engine, self).__init__(*args, **kwargs) + + 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 = randint(0, self.columns - 1) + y = randint(0, self.rows - 1) + 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 = random.randint(0, sys.maxint) + + self.letters = list(string.lowercase) + self.letters.reverse() + + self.rows = rows + self.columns = columns + + # make board + self.board = [[Squares.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] = Squares.APPLE + + def add_bot(self, bot): + """ + 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') + """ + letter = self.letters.pop() + + name = bot.__name__ + colour = hash_colour(name) + + position = self.replace_random(Squares.EMPTY, letter.upper()) + if position is None: + raise KeyError, "Could not insert snake into the board." + + self.bots[letter] = [bot, colour, deque([position])] + 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] = Squares.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, directions_id=id(directions)): + assert id(directions) == directions_id, \ + "The common.directions dictionary has been modified since startup..." + + self.game_ticks += 1 + + for letter, (bot, colour, path) in self.bots.items(): + board = deepcopy(self.board) + try: + x, y = path[-1] + d = bot(board, (x, y)) + + # Sanity checking... + assert isinstance(d, basestring), \ + "Return value should be a string." + d = d.upper() + assert d in directions, "Return value should be 'U', 'D', 'L' or 'R'." + + # Get new position. + dx, dy = 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 oldcell in (Squares.EMPTY, Squares.APPLE): + # 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 == Squares.APPLE: + # Add in an apple to compensate. + self.replace_random(Squares.EMPTY, Squares.APPLE) + else: + # Remove last part of snake. + ox, oy = path.popleft() + self.board[oy][ox] = Squares.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) + diff --git a/snakegame/engines/pyglet.py b/snakegame/engines/pyglet.py index 5a17ac0..f5b5d0e 100644 --- a/snakegame/engines/pyglet.py +++ b/snakegame/engines/pyglet.py @@ -9,7 +9,7 @@ pyglet.resource.reindex() from pyglet import gl import common -from snake import SnakeEngine +from snakegame.engine import Engine def scale_aspect((source_width, source_height), (target_width, target_height)): source_aspect = source_width / source_height @@ -24,12 +24,12 @@ def scale_aspect((source_width, source_height), (target_width, target_height)): width = height * source_aspect return (width, height) -class PygletSnakeEngine(SnakeEngine, pyglet.window.Window): +class PygletEngine(Engine, pyglet.window.Window): EDGE_COLOR = (255, 255, 255, 255) EDGE_WIDTH = 2 def __init__(self, rows, columns, n_apples, *args, **kwargs): - super(PygletSnakeEngine, self).__init__(rows, columns, n_apples, *args, **kwargs) + super(PygletEngine, self).__init__(rows, columns, n_apples, *args, **kwargs) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) @@ -37,7 +37,7 @@ class PygletSnakeEngine(SnakeEngine, pyglet.window.Window): pyglet.clock.schedule_interval(lambda t: self.update_snakes(), 0.025) def new_game(self, rows, columns, n_apples): - super(PygletSnakeEngine, self).new_game(rows, columns, n_apples) + super(PygletEngine, self).new_game(rows, columns, n_apples) # make board surface self.board_width, self.board_height = scale_aspect( @@ -100,7 +100,7 @@ class PygletSnakeEngine(SnakeEngine, pyglet.window.Window): def update_snakes(self, *args): if not self.bots: pyglet.app.exit() - super(PygletSnakeEngine, self).update_snakes(*args) + super(PygletEngine, self).update_snakes(*args) def run(self): pyglet.app.run() @@ -110,7 +110,7 @@ if __name__ == '__main__': from processbot import BotWrapper rows, columns, apples = map(int, sys.argv[1:4]) - game = PygletSnakeEngine(rows, columns, apples) + game = PygletEngine(rows, columns, apples) for filename in sys.argv[4:]: bot = BotWrapper(filename) game.add_bot(bot) diff --git a/snakegame/pngcanvas.py b/snakegame/pngcanvas.py deleted file mode 100644 index 394ff4f..0000000 --- a/snakegame/pngcanvas.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python - -"""Simple PNG Canvas for Python""" -__version__ = "0.8" -__author__ = "Rui Carmo (http://the.taoofmac.com)" -__copyright__ = "CC Attribution-NonCommercial-NoDerivs 2.0 Rui Carmo" -__contributors__ = ["http://collaboa.weed.rbse.com/repository/file/branches/pgsql/lib/spark_pr.rb"], ["Eli Bendersky"] - -import zlib, struct - -signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10) - -# alpha blends two colors, using the alpha given by c2 -def blend(c1, c2): - return [c1[i]*(0xFF-c2[3]) + c2[i]*c2[3] >> 8 for i in range(3)] - -# calculate a new alpha given a 0-0xFF intensity -def intensity(c,i): - return [c[0],c[1],c[2],(c[3]*i) >> 8] - -# calculate perceptive grayscale value -def grayscale(c): - return int(c[0]*0.3 + c[1]*0.59 + c[2]*0.11) - -# calculate gradient colors -def gradientList(start,end,steps): - delta = [end[i] - start[i] for i in range(4)] - grad = [] - for i in range(steps+1): - grad.append([start[j] + (delta[j]*i)/steps for j in range(4)]) - return grad - -class PNGCanvas: - def __init__(self, width, height,bgcolor=[0xff,0xff,0xff,0xff],color=[0,0,0,0xff]): - self.canvas = [] - self.width = width - self.height = height - self.color = color #rgba - bgcolor = bgcolor[0:3] # we don't need alpha for background - for i in range(height): - self.canvas.append([bgcolor] * width) - - def point(self,x,y,color=None): - if x<0 or y<0 or x>self.width-1 or y>self.height-1: return - if color == None: color = self.color - self.canvas[y][x] = blend(self.canvas[y][x],color) - - def _rectHelper(self,x0,y0,x1,y1): - x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) - if x0 > x1: x0, x1 = x1, x0 - if y0 > y1: y0, y1 = y1, y0 - return [x0,y0,x1,y1] - - def verticalGradient(self,x0,y0,x1,y1,start,end): - x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) - grad = gradientList(start,end,y1-y0) - for x in range(x0, x1+1): - for y in range(y0, y1+1): - self.point(x,y,grad[y-y0]) - - def rectangle(self,x0,y0,x1,y1): - x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) - self.polyline([[x0,y0],[x1,y0],[x1,y1],[x0,y1],[x0,y0]]) - - def filledRectangle(self,x0,y0,x1,y1): - x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) - for x in range(x0, x1+1): - for y in range(y0, y1+1): - self.point(x,y,self.color) - - def copyRect(self,x0,y0,x1,y1,dx,dy,destination): - x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) - for x in range(x0, x1+1): - for y in range(y0, y1+1): - destination.canvas[dy+y-y0][dx+x-x0] = self.canvas[y][x] - - def blendRect(self,x0,y0,x1,y1,dx,dy,destination,alpha=0xff): - x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) - for x in range(x0, x1+1): - for y in range(y0, y1+1): - rgba = self.canvas[y][x] + [alpha] - destination.point(dx+x-x0,dy+y-y0,rgba) - - # draw a line using Xiaolin Wu's antialiasing technique - def line(self,x0, y0, x1, y1): - # clean params - x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) - if y0>y1: - y0, y1, x0, x1 = y1, y0, x1, x0 - dx = x1-x0 - if dx < 0: - sx = -1 - else: - sx = 1 - dx *= sx - dy = y1-y0 - - # 'easy' cases - if dy == 0: - for x in range(x0,x1,sx): - self.point(x, y0) - return - if dx == 0: - for y in range(y0,y1): - self.point(x0, y) - self.point(x1, y1) - return - if dx == dy: - for x in range(x0,x1,sx): - self.point(x, y0) - y0 = y0 + 1 - return - - # main loop - self.point(x0, y0) - e_acc = 0 - if dy > dx: # vertical displacement - e = (dx << 16) / dy - for i in range(y0,y1-1): - e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF - if (e_acc <= e_acc_temp): - x0 = x0 + sx - w = 0xFF-(e_acc >> 8) - self.point(x0, y0, intensity(self.color,(w))) - y0 = y0 + 1 - self.point(x0 + sx, y0, intensity(self.color,(0xFF-w))) - self.point(x1, y1) - return - - # horizontal displacement - e = (dy << 16) / dx - for i in range(x0,x1-sx,sx): - e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF - if (e_acc <= e_acc_temp): - y0 = y0 + 1 - w = 0xFF-(e_acc >> 8) - self.point(x0, y0, intensity(self.color,(w))) - x0 = x0 + sx - self.point(x0, y0 + 1, intensity(self.color,(0xFF-w))) - self.point(x1, y1) - - def polyline(self,arr): - for i in range(0,len(arr)-1): - self.line(arr[i][0],arr[i][1],arr[i+1][0], arr[i+1][1]) - - def dump(self): - raw_list = [] - for y in range(self.height): - raw_list.append(chr(0)) # filter type 0 (None) - for x in range(self.width): - raw_list.append(struct.pack("!3B",*self.canvas[y][x])) - raw_data = ''.join(raw_list) - - # 8-bit image represented as RGB tuples - # simple transparency, alpha is pure white - return signature + \ - self.pack_chunk('IHDR', struct.pack("!2I5B",self.width,self.height,8,2,0,0,0)) + \ - self.pack_chunk('tRNS', struct.pack("!6B",0xFF,0xFF,0xFF,0xFF,0xFF,0xFF)) + \ - self.pack_chunk('IDAT', zlib.compress(raw_data,9)) + \ - self.pack_chunk('IEND', '') - - def pack_chunk(self,tag,data): - to_check = tag + data - return struct.pack("!I",len(data)) + to_check + struct.pack("!I", zlib.crc32(to_check) & 0xFFFFFFFF) - - def load(self,f): - assert f.read(8) == signature - self.canvas=[] - for tag, data in self.chunks(f): - if tag == "IHDR": - ( width, - height, - bitdepth, - colortype, - compression, filter, interlace ) = struct.unpack("!2I5B",data) - self.width = width - self.height = height - if (bitdepth,colortype,compression, filter, interlace) != (8,2,0,0,0): - raise TypeError('Unsupported PNG format') - # we ignore tRNS because we use pure white as alpha anyway - elif tag == 'IDAT': - raw_data = zlib.decompress(data) - rows = [] - i = 0 - for y in range(height): - filtertype = ord(raw_data[i]) - i = i + 1 - cur = [ord(x) for x in raw_data[i:i+width*3]] - if y == 0: - rgb = self.defilter(cur,None,filtertype) - else: - rgb = self.defilter(cur,prev,filtertype) - prev = cur - i = i+width*3 - row = [] - j = 0 - for x in range(width): - pixel = rgb[j:j+3] - row.append(pixel) - j = j + 3 - self.canvas.append(row) - - def defilter(self,cur,prev,filtertype,bpp=3): - if filtertype == 0: # No filter - return cur - elif filtertype == 1: # Sub - xp = 0 - for xc in range(bpp,len(cur)): - cur[xc] = (cur[xc] + cur[xp]) % 256 - xp = xp + 1 - elif filtertype == 2: # Up - for xc in range(len(cur)): - cur[xc] = (cur[xc] + prev[xc]) % 256 - elif filtertype == 3: # Average - xp = 0 - for xc in range(len(cur)): - cur[xc] = (cur[xc] + (cur[xp] + prev[xc])/2) % 256 - xp = xp + 1 - elif filtertype == 4: # Paeth - xp = 0 - for i in range(bpp): - cur[i] = (cur[i] + prev[i]) % 256 - for xc in range(bpp,len(cur)): - a = cur[xp] - b = prev[xc] - c = prev[xp] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - value = a - elif pb <= pc: - value = b - else: - value = c - cur[xc] = (cur[xc] + value) % 256 - xp = xp + 1 - else: - raise TypeError('Unrecognized scanline filter type') - return cur - - def chunks(self,f): - while 1: - try: - length = struct.unpack("!I",f.read(4))[0] - tag = f.read(4) - data = f.read(length) - crc = struct.unpack("!i",f.read(4))[0] - except: - return - if zlib.crc32(tag + data) != crc: - raise IOError - yield [tag,data] - -if __name__ == '__main__': - width = 128 - height = 64 - print "Creating Canvas..." - c = PNGCanvas(width,height) - c.color = [0xff,0,0,0xff] - c.rectangle(0,0,width-1,height-1) - print "Generating Gradient..." - c.verticalGradient(1,1,width-2, height-2,[0xff,0,0,0xff],[0x20,0,0xff,0x80]) - print "Drawing Lines..." - c.color = [0,0,0,0xff] - c.line(0,0,width-1,height-1) - c.line(0,0,width/2,height-1) - c.line(0,0,width-1,height/2) - # Copy Rect to Self - print "Copy Rect" - c.copyRect(1,1,width/2-1,height/2-1,0,height/2,c) - # Blend Rect to Self - print "Blend Rect" - c.blendRect(1,1,width/2-1,height/2-1,width/2,0,c) - # Write test - print "Writing to file..." - f = open("test.png", "wb") - f.write(c.dump()) - f.close() - # Read test - print "Reading from file..." - f = open("test.png", "rb") - c.load(f) - f.close() - # Write back - print "Writing to new file..." - f = open("recycle.png","wb") - f.write(c.dump()) - f.close() - diff --git a/snakegame/pngchart.py b/snakegame/pngchart.py deleted file mode 100644 index 5428718..0000000 --- a/snakegame/pngchart.py +++ /dev/null @@ -1,53 +0,0 @@ -from pngcanvas import PNGCanvas - -try: - from itertools import izip as zip -except ImportError: - pass - -class SimpleLineChart(object): - def __init__(self, width, height, colours=None, legend=None): - self.canvas = PNGCanvas(width, height) - - self.width = width - self.height = height - - self.colours = colours - self.legend = legend - - self.series = [] - - def add_data(self, series): - self.series.append(series) - - def render(self): - max_width = max(map(len, self.series)) - max_height = max(map(max, self.series)) - x_scale = float(self.width) / max_width - y_scale = float(self.height) / max_height - - data = zip(self.series, self.colours or [], self.legend or []) - for series, colour, legend in data: - colour = int(colour, 16) - self.canvas.color = ( - colour>>16 & 0xff, - colour>>8 & 0xff, - colour & 0xff, - 0xff, - ) - last = None - for x, y in enumerate(series): - if y is not None: - y = self.height - y * y_scale - if last is not None: - x *= x_scale - self.canvas.line(x - x_scale, last, x, y) - last = y - - def download(self, filename): - self.render() - - f = open(filename, 'wb') - f.write(self.canvas.dump()) - f.close() - diff --git a/snakegame/pygooglechart.py b/snakegame/pygooglechart.py deleted file mode 100644 index 0c17973..0000000 --- a/snakegame/pygooglechart.py +++ /dev/null @@ -1,1066 +0,0 @@ -""" -pygooglechart - A complete Python wrapper for the Google Chart API - -http://pygooglechart.slowchop.com/ - -Copyright 2007-2008 Gerald Kaszuba - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -""" - -import os -import urllib -import urllib2 -import math -import random -import re -import warnings -import copy - -# Helper variables and functions -# ----------------------------------------------------------------------------- - -__version__ = '0.2.1' -__author__ = 'Gerald Kaszuba' - -reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$') - -def _check_colour(colour): - if not reo_colour.match(colour): - raise InvalidParametersException('Colours need to be in ' \ - 'RRGGBB or RRGGBBAA format. One of your colours has %s' % \ - colour) - - -def _reset_warnings(): - """Helper function to reset all warnings. Used by the unit tests.""" - globals()['__warningregistry__'] = None - - -# Exception Classes -# ----------------------------------------------------------------------------- - - -class PyGoogleChartException(Exception): - pass - - -class DataOutOfRangeException(PyGoogleChartException): - pass - - -class UnknownDataTypeException(PyGoogleChartException): - pass - - -class NoDataGivenException(PyGoogleChartException): - pass - - -class InvalidParametersException(PyGoogleChartException): - pass - - -class BadContentTypeException(PyGoogleChartException): - pass - - -class AbstractClassException(PyGoogleChartException): - pass - - -class UnknownChartType(PyGoogleChartException): - pass - - -# Data Classes -# ----------------------------------------------------------------------------- - - -class Data(object): - - def __init__(self, data): - if type(self) == Data: - raise AbstractClassException('This is an abstract class') - self.data = data - - @classmethod - def float_scale_value(cls, value, range): - lower, upper = range - assert(upper > lower) - scaled = (value - lower) * (float(cls.max_value) / (upper - lower)) - return scaled - - @classmethod - def clip_value(cls, value): - return max(0, min(value, cls.max_value)) - - @classmethod - def int_scale_value(cls, value, range): - return int(round(cls.float_scale_value(value, range))) - - @classmethod - def scale_value(cls, value, range): - scaled = cls.int_scale_value(value, range) - clipped = cls.clip_value(scaled) - Data.check_clip(scaled, clipped) - return clipped - - @staticmethod - def check_clip(scaled, clipped): - if clipped != scaled: - warnings.warn('One or more of of your data points has been ' - 'clipped because it is out of range.') - - -class SimpleData(Data): - - max_value = 61 - enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - - def __repr__(self): - encoded_data = [] - for data in self.data: - sub_data = [] - for value in data: - if value is None: - sub_data.append('_') - elif value >= 0 and value <= self.max_value: - sub_data.append(SimpleData.enc_map[value]) - else: - raise DataOutOfRangeException('cannot encode value: %d' - % value) - encoded_data.append(''.join(sub_data)) - return 'chd=s:' + ','.join(encoded_data) - - -class TextData(Data): - - max_value = 100 - - def __repr__(self): - encoded_data = [] - for data in self.data: - sub_data = [] - for value in data: - if value is None: - sub_data.append(-1) - elif value >= 0 and value <= self.max_value: - sub_data.append("%.1f" % float(value)) - else: - raise DataOutOfRangeException() - encoded_data.append(','.join(sub_data)) - return 'chd=t:' + '|'.join(encoded_data) - - @classmethod - def scale_value(cls, value, range): - # use float values instead of integers because we don't need an encode - # map index - scaled = cls.float_scale_value(value, range) - clipped = cls.clip_value(scaled) - Data.check_clip(scaled, clipped) - return clipped - - -class ExtendedData(Data): - - max_value = 4095 - enc_map = \ - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.' - - def __repr__(self): - encoded_data = [] - enc_size = len(ExtendedData.enc_map) - for data in self.data: - sub_data = [] - for value in data: - if value is None: - sub_data.append('__') - elif value >= 0 and value <= self.max_value: - first, second = divmod(int(value), enc_size) - sub_data.append('%s%s' % ( - ExtendedData.enc_map[first], - ExtendedData.enc_map[second])) - else: - raise DataOutOfRangeException( \ - 'Item #%i "%s" is out of range' % (data.index(value), \ - value)) - encoded_data.append(''.join(sub_data)) - return 'chd=e:' + ','.join(encoded_data) - - -# Axis Classes -# ----------------------------------------------------------------------------- - - -class Axis(object): - - BOTTOM = 'x' - TOP = 't' - LEFT = 'y' - RIGHT = 'r' - TYPES = (BOTTOM, TOP, LEFT, RIGHT) - - def __init__(self, axis_index, axis_type, **kw): - assert(axis_type in Axis.TYPES) - self.has_style = False - self.axis_index = axis_index - self.axis_type = axis_type - self.positions = None - - def set_index(self, axis_index): - self.axis_index = axis_index - - def set_positions(self, positions): - self.positions = positions - - def set_style(self, colour, font_size=None, alignment=None): - _check_colour(colour) - self.colour = colour - self.font_size = font_size - self.alignment = alignment - self.has_style = True - - def style_to_url(self): - bits = [] - bits.append(str(self.axis_index)) - bits.append(self.colour) - if self.font_size is not None: - bits.append(str(self.font_size)) - if self.alignment is not None: - bits.append(str(self.alignment)) - return ','.join(bits) - - def positions_to_url(self): - bits = [] - bits.append(str(self.axis_index)) - bits += [str(a) for a in self.positions] - return ','.join(bits) - - -class LabelAxis(Axis): - - def __init__(self, axis_index, axis_type, values, **kwargs): - Axis.__init__(self, axis_index, axis_type, **kwargs) - self.values = [str(a) for a in values] - - def __repr__(self): - return '%i:|%s' % (self.axis_index, '|'.join(self.values)) - - -class RangeAxis(Axis): - - def __init__(self, axis_index, axis_type, low, high, **kwargs): - Axis.__init__(self, axis_index, axis_type, **kwargs) - self.low = low - self.high = high - - def __repr__(self): - return '%i,%s,%s' % (self.axis_index, self.low, self.high) - -# Chart Classes -# ----------------------------------------------------------------------------- - - -class Chart(object): - """Abstract class for all chart types. - - width are height specify the dimensions of the image. title sets the title - of the chart. legend requires a list that corresponds to datasets. - """ - - BASE_URL = 'http://chart.apis.google.com/chart?' - BACKGROUND = 'bg' - CHART = 'c' - ALPHA = 'a' - VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA) - SOLID = 's' - LINEAR_GRADIENT = 'lg' - LINEAR_STRIPES = 'ls' - - def __init__(self, width, height, title=None, legend=None, colours=None, - auto_scale=True, x_range=None, y_range=None, - colours_within_series=None): - if type(self) == Chart: - raise AbstractClassException('This is an abstract class') - assert(isinstance(width, int)) - assert(isinstance(height, int)) - self.width = width - self.height = height - self.data = [] - self.set_title(title) - self.set_legend(legend) - self.set_legend_position(None) - self.set_colours(colours) - self.set_colours_within_series(colours_within_series) - - # Data for scaling. - self.auto_scale = auto_scale # Whether to automatically scale data - self.x_range = x_range # (min, max) x-axis range for scaling - self.y_range = y_range # (min, max) y-axis range for scaling - self.scaled_data_class = None - self.scaled_x_range = None - self.scaled_y_range = None - - self.fill_types = { - Chart.BACKGROUND: None, - Chart.CHART: None, - Chart.ALPHA: None, - } - self.fill_area = { - Chart.BACKGROUND: None, - Chart.CHART: None, - Chart.ALPHA: None, - } - self.axis = [] - self.markers = [] - self.line_styles = {} - self.grid = None - - # URL generation - # ------------------------------------------------------------------------- - - def get_url(self, data_class=None): - url_bits = self.get_url_bits(data_class=data_class) - return self.BASE_URL + '&'.join(url_bits) - - def get_url_bits(self, data_class=None): - url_bits = [] - # required arguments - url_bits.append(self.type_to_url()) - url_bits.append('chs=%ix%i' % (self.width, self.height)) - url_bits.append(self.data_to_url(data_class=data_class)) - # optional arguments - if self.title: - url_bits.append('chtt=%s' % self.title) - if self.legend: - url_bits.append('chdl=%s' % '|'.join(self.legend)) - if self.legend_position: - url_bits.append('chdlp=%s' % (self.legend_position)) - if self.colours: - url_bits.append('chco=%s' % ','.join(self.colours)) - if self.colours_within_series: - url_bits.append('chco=%s' % '|'.join(self.colours_within_series)) - ret = self.fill_to_url() - if ret: - url_bits.append(ret) - ret = self.axis_to_url() - if ret: - url_bits.append(ret) - if self.markers: - url_bits.append(self.markers_to_url()) - if self.line_styles: - style = [] - for index in xrange(max(self.line_styles) + 1): - if index in self.line_styles: - values = self.line_styles[index] - else: - values = ('1', ) - style.append(','.join(values)) - url_bits.append('chls=%s' % '|'.join(style)) - if self.grid: - url_bits.append('chg=%s' % self.grid) - return url_bits - - # Downloading - # ------------------------------------------------------------------------- - - def download(self, file_name): - opener = urllib2.urlopen(self.get_url()) - - if opener.headers['content-type'] != 'image/png': - raise BadContentTypeException('Server responded with a ' \ - 'content-type of %s' % opener.headers['content-type']) - - open(file_name, 'wb').write(opener.read()) - - # Simple settings - # ------------------------------------------------------------------------- - - def set_title(self, title): - if title: - self.title = urllib.quote(title) - else: - self.title = None - - def set_legend(self, legend): - """legend needs to be a list, tuple or None""" - assert(isinstance(legend, list) or isinstance(legend, tuple) or - legend is None) - if legend: - self.legend = [urllib.quote(a) for a in legend] - else: - self.legend = None - - def set_legend_position(self, legend_position): - if legend_position: - self.legend_position = urllib.quote(legend_position) - else: - self.legend_position = None - - # Chart colours - # ------------------------------------------------------------------------- - - def set_colours(self, colours): - # colours needs to be a list, tuple or None - assert(isinstance(colours, list) or isinstance(colours, tuple) or - colours is None) - # make sure the colours are in the right format - if colours: - for col in colours: - _check_colour(col) - self.colours = colours - - def set_colours_within_series(self, colours): - # colours needs to be a list, tuple or None - assert(isinstance(colours, list) or isinstance(colours, tuple) or - colours is None) - # make sure the colours are in the right format - if colours: - for col in colours: - _check_colour(col) - self.colours_within_series = colours - - # Background/Chart colours - # ------------------------------------------------------------------------- - - def fill_solid(self, area, colour): - assert(area in Chart.VALID_SOLID_FILL_TYPES) - _check_colour(colour) - self.fill_area[area] = colour - self.fill_types[area] = Chart.SOLID - - def _check_fill_linear(self, angle, *args): - assert(isinstance(args, list) or isinstance(args, tuple)) - assert(angle >= 0 and angle <= 90) - assert(len(args) % 2 == 0) - args = list(args) # args is probably a tuple and we need to mutate - for a in xrange(len(args) / 2): - col = args[a * 2] - offset = args[a * 2 + 1] - _check_colour(col) - assert(offset >= 0 and offset <= 1) - args[a * 2 + 1] = str(args[a * 2 + 1]) - return args - - def fill_linear_gradient(self, area, angle, *args): - assert(area in Chart.VALID_SOLID_FILL_TYPES) - args = self._check_fill_linear(angle, *args) - self.fill_types[area] = Chart.LINEAR_GRADIENT - self.fill_area[area] = ','.join([str(angle)] + args) - - def fill_linear_stripes(self, area, angle, *args): - assert(area in Chart.VALID_SOLID_FILL_TYPES) - args = self._check_fill_linear(angle, *args) - self.fill_types[area] = Chart.LINEAR_STRIPES - self.fill_area[area] = ','.join([str(angle)] + args) - - def fill_to_url(self): - areas = [] - for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA): - if self.fill_types[area]: - areas.append('%s,%s,%s' % (area, self.fill_types[area], \ - self.fill_area[area])) - if areas: - return 'chf=' + '|'.join(areas) - - # Data - # ------------------------------------------------------------------------- - - def data_class_detection(self, data): - """Determines the appropriate data encoding type to give satisfactory - resolution (http://code.google.com/apis/chart/#chart_data). - """ - assert(isinstance(data, list) or isinstance(data, tuple)) - if not isinstance(self, (LineChart, BarChart, ScatterChart)): - # From the link above: - # Simple encoding is suitable for all other types of chart - # regardless of size. - return SimpleData - elif self.height < 100: - # The link above indicates that line and bar charts less - # than 300px in size can be suitably represented with the - # simple encoding. I've found that this isn't sufficient, - # e.g. examples/line-xy-circle.png. Let's try 100px. - return SimpleData - else: - return ExtendedData - - def _filter_none(self, data): - return [r for r in data if r is not None] - - def data_x_range(self): - """Return a 2-tuple giving the minimum and maximum x-axis - data range. - """ - try: - lower = min([min(self._filter_none(s)) - for type, s in self.annotated_data() - if type == 'x']) - upper = max([max(self._filter_none(s)) - for type, s in self.annotated_data() - if type == 'x']) - return (lower, upper) - except ValueError: - return None # no x-axis datasets - - def data_y_range(self): - """Return a 2-tuple giving the minimum and maximum y-axis - data range. - """ - try: - lower = min([min(self._filter_none(s)) - for type, s in self.annotated_data() - if type == 'y']) - upper = max([max(self._filter_none(s)) + 1 - for type, s in self.annotated_data() - if type == 'y']) - return (lower, upper) - except ValueError: - return None # no y-axis datasets - - def scaled_data(self, data_class, x_range=None, y_range=None): - """Scale `self.data` as appropriate for the given data encoding - (data_class) and return it. - - An optional `y_range` -- a 2-tuple (lower, upper) -- can be - given to specify the y-axis bounds. If not given, the range is - inferred from the data: (0, ) presuming no negative - values, or (, ) if there are negative - values. `self.scaled_y_range` is set to the actual lower and - upper scaling range. - - Ditto for `x_range`. Note that some chart types don't have x-axis - data. - """ - self.scaled_data_class = data_class - - # Determine the x-axis range for scaling. - if x_range is None: - x_range = self.data_x_range() - if x_range and x_range[0] > 0: - x_range = (x_range[0], x_range[1]) - self.scaled_x_range = x_range - - # Determine the y-axis range for scaling. - if y_range is None: - y_range = self.data_y_range() - if y_range and y_range[0] > 0: - y_range = (y_range[0], y_range[1]) - self.scaled_y_range = y_range - - scaled_data = [] - for type, dataset in self.annotated_data(): - if type == 'x': - scale_range = x_range - elif type == 'y': - scale_range = y_range - elif type == 'marker-size': - scale_range = (0, max(dataset)) - scaled_dataset = [] - for v in dataset: - if v is None: - scaled_dataset.append(None) - else: - scaled_dataset.append( - data_class.scale_value(v, scale_range)) - scaled_data.append(scaled_dataset) - return scaled_data - - def add_data(self, data): - self.data.append(data) - return len(self.data) - 1 # return the "index" of the data set - - def data_to_url(self, data_class=None): - if not data_class: - data_class = self.data_class_detection(self.data) - if not issubclass(data_class, Data): - raise UnknownDataTypeException() - if self.auto_scale: - data = self.scaled_data(data_class, self.x_range, self.y_range) - else: - data = self.data - return repr(data_class(data)) - - def annotated_data(self): - for dataset in self.data: - yield ('x', dataset) - - # Axis Labels - # ------------------------------------------------------------------------- - - def set_axis_labels(self, axis_type, values): - assert(axis_type in Axis.TYPES) - values = [urllib.quote(str(a)) for a in values] - axis_index = len(self.axis) - axis = LabelAxis(axis_index, axis_type, values) - self.axis.append(axis) - return axis_index - - def set_axis_range(self, axis_type, low, high): - assert(axis_type in Axis.TYPES) - axis_index = len(self.axis) - axis = RangeAxis(axis_index, axis_type, low, high) - self.axis.append(axis) - return axis_index - - def set_axis_positions(self, axis_index, positions): - try: - self.axis[axis_index].set_positions(positions) - except IndexError: - raise InvalidParametersException('Axis index %i has not been ' \ - 'created' % axis) - - def set_axis_style(self, axis_index, colour, font_size=None, \ - alignment=None): - try: - self.axis[axis_index].set_style(colour, font_size, alignment) - except IndexError: - raise InvalidParametersException('Axis index %i has not been ' \ - 'created' % axis) - - def axis_to_url(self): - available_axis = [] - label_axis = [] - range_axis = [] - positions = [] - styles = [] - index = -1 - for axis in self.axis: - available_axis.append(axis.axis_type) - if isinstance(axis, RangeAxis): - range_axis.append(repr(axis)) - if isinstance(axis, LabelAxis): - label_axis.append(repr(axis)) - if axis.positions: - positions.append(axis.positions_to_url()) - if axis.has_style: - styles.append(axis.style_to_url()) - if not available_axis: - return - url_bits = [] - url_bits.append('chxt=%s' % ','.join(available_axis)) - if label_axis: - url_bits.append('chxl=%s' % '|'.join(label_axis)) - if range_axis: - url_bits.append('chxr=%s' % '|'.join(range_axis)) - if positions: - url_bits.append('chxp=%s' % '|'.join(positions)) - if styles: - url_bits.append('chxs=%s' % '|'.join(styles)) - return '&'.join(url_bits) - - # Markers, Ranges and Fill area (chm) - # ------------------------------------------------------------------------- - - def markers_to_url(self): - return 'chm=%s' % '|'.join([','.join(a) for a in self.markers]) - - def add_marker(self, index, point, marker_type, colour, size, priority=0): - self.markers.append((marker_type, colour, str(index), str(point), \ - str(size), str(priority))) - - def add_horizontal_range(self, colour, start, stop): - self.markers.append(('r', colour, '0', str(start), str(stop))) - - def add_data_line(self, colour, data_set, size, priority=0): - self.markers.append(('D', colour, str(data_set), '0', str(size), str(priority))) - - def add_marker_text(self, string, colour, data_set, data_point, size, priority=0): - self.markers.append((str(string), colour, str(data_set), str(data_point), str(size), str(priority))) - - def add_vertical_range(self, colour, start, stop): - self.markers.append(('R', colour, '0', str(start), str(stop))) - - def add_fill_range(self, colour, index_start, index_end): - self.markers.append(('b', colour, str(index_start), str(index_end), \ - '1')) - - def add_fill_simple(self, colour): - self.markers.append(('B', colour, '1', '1', '1')) - - # Line styles - # ------------------------------------------------------------------------- - - def set_line_style(self, index, thickness=1, line_segment=None, \ - blank_segment=None): - value = [] - value.append(str(thickness)) - if line_segment: - value.append(str(line_segment)) - value.append(str(blank_segment)) - self.line_styles[index] = value - - # Grid - # ------------------------------------------------------------------------- - - def set_grid(self, x_step, y_step, line_segment=1, \ - blank_segment=0): - self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \ - blank_segment) - - -class ScatterChart(Chart): - - def type_to_url(self): - return 'cht=s' - - def annotated_data(self): - yield ('x', self.data[0]) - yield ('y', self.data[1]) - if len(self.data) > 2: - # The optional third dataset is relative sizing for point - # markers. - yield ('marker-size', self.data[2]) - - -class LineChart(Chart): - - def __init__(self, *args, **kwargs): - if type(self) == LineChart: - raise AbstractClassException('This is an abstract class') - Chart.__init__(self, *args, **kwargs) - - -class SimpleLineChart(LineChart): - - def type_to_url(self): - return 'cht=lc' - - def annotated_data(self): - # All datasets are y-axis data. - for dataset in self.data: - yield ('y', dataset) - - -class SparkLineChart(SimpleLineChart): - - def type_to_url(self): - return 'cht=ls' - - -class XYLineChart(LineChart): - - def type_to_url(self): - return 'cht=lxy' - - def annotated_data(self): - # Datasets alternate between x-axis, y-axis. - for i, dataset in enumerate(self.data): - if i % 2 == 0: - yield ('x', dataset) - else: - yield ('y', dataset) - - -class BarChart(Chart): - - def __init__(self, *args, **kwargs): - if type(self) == BarChart: - raise AbstractClassException('This is an abstract class') - Chart.__init__(self, *args, **kwargs) - self.bar_width = None - self.zero_lines = {} - - def set_bar_width(self, bar_width): - self.bar_width = bar_width - - def set_zero_line(self, index, zero_line): - self.zero_lines[index] = zero_line - - def get_url_bits(self, data_class=None, skip_chbh=False): - url_bits = Chart.get_url_bits(self, data_class=data_class) - if not skip_chbh and self.bar_width is not None: - url_bits.append('chbh=%i' % self.bar_width) - zero_line = [] - if self.zero_lines: - for index in xrange(max(self.zero_lines) + 1): - if index in self.zero_lines: - zero_line.append(str(self.zero_lines[index])) - else: - zero_line.append('0') - url_bits.append('chp=%s' % ','.join(zero_line)) - return url_bits - - -class StackedHorizontalBarChart(BarChart): - - def type_to_url(self): - return 'cht=bhs' - - -class StackedVerticalBarChart(BarChart): - - def type_to_url(self): - return 'cht=bvs' - - def annotated_data(self): - for dataset in self.data: - yield ('y', dataset) - - -class GroupedBarChart(BarChart): - - def __init__(self, *args, **kwargs): - if type(self) == GroupedBarChart: - raise AbstractClassException('This is an abstract class') - BarChart.__init__(self, *args, **kwargs) - self.bar_spacing = None - self.group_spacing = None - - def set_bar_spacing(self, spacing): - """Set spacing between bars in a group.""" - self.bar_spacing = spacing - - def set_group_spacing(self, spacing): - """Set spacing between groups of bars.""" - self.group_spacing = spacing - - def get_url_bits(self, data_class=None): - # Skip 'BarChart.get_url_bits' and call Chart directly so the parent - # doesn't add "chbh" before we do. - url_bits = BarChart.get_url_bits(self, data_class=data_class, - skip_chbh=True) - if self.group_spacing is not None: - if self.bar_spacing is None: - raise InvalidParametersException('Bar spacing is required ' \ - 'to be set when setting group spacing') - if self.bar_width is None: - raise InvalidParametersException('Bar width is required to ' \ - 'be set when setting bar spacing') - url_bits.append('chbh=%i,%i,%i' - % (self.bar_width, self.bar_spacing, self.group_spacing)) - elif self.bar_spacing is not None: - if self.bar_width is None: - raise InvalidParametersException('Bar width is required to ' \ - 'be set when setting bar spacing') - url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing)) - elif self.bar_width: - url_bits.append('chbh=%i' % self.bar_width) - return url_bits - - -class GroupedHorizontalBarChart(GroupedBarChart): - - def type_to_url(self): - return 'cht=bhg' - - -class GroupedVerticalBarChart(GroupedBarChart): - - def type_to_url(self): - return 'cht=bvg' - - def annotated_data(self): - for dataset in self.data: - yield ('y', dataset) - - -class PieChart(Chart): - - def __init__(self, *args, **kwargs): - if type(self) == PieChart: - raise AbstractClassException('This is an abstract class') - Chart.__init__(self, *args, **kwargs) - self.pie_labels = [] - if self.y_range: - warnings.warn('y_range is not used with %s.' % \ - (self.__class__.__name__)) - - def set_pie_labels(self, labels): - self.pie_labels = [urllib.quote(a) for a in labels] - - def get_url_bits(self, data_class=None): - url_bits = Chart.get_url_bits(self, data_class=data_class) - if self.pie_labels: - url_bits.append('chl=%s' % '|'.join(self.pie_labels)) - return url_bits - - def annotated_data(self): - # Datasets are all y-axis data. However, there should only be - # one dataset for pie charts. - for dataset in self.data: - yield ('x', dataset) - - def scaled_data(self, data_class, x_range=None, y_range=None): - if not x_range: - x_range = [0, sum(self.data[0])] - return Chart.scaled_data(self, data_class, x_range, self.y_range) - - -class PieChart2D(PieChart): - - def type_to_url(self): - return 'cht=p' - - -class PieChart3D(PieChart): - - def type_to_url(self): - return 'cht=p3' - - -class VennChart(Chart): - - def type_to_url(self): - return 'cht=v' - - def annotated_data(self): - for dataset in self.data: - yield ('y', dataset) - - -class RadarChart(Chart): - - def type_to_url(self): - return 'cht=r' - - -class SplineRadarChart(RadarChart): - - def type_to_url(self): - return 'cht=rs' - - -class MapChart(Chart): - - def __init__(self, *args, **kwargs): - Chart.__init__(self, *args, **kwargs) - self.geo_area = 'world' - self.codes = [] - - def type_to_url(self): - return 'cht=t' - - def set_codes(self, codes): - self.codes = codes - - def get_url_bits(self, data_class=None): - url_bits = Chart.get_url_bits(self, data_class=data_class) - url_bits.append('chtm=%s' % self.geo_area) - if self.codes: - url_bits.append('chld=%s' % ''.join(self.codes)) - return url_bits - - -class GoogleOMeterChart(PieChart): - """Inheriting from PieChart because of similar labeling""" - - def __init__(self, *args, **kwargs): - PieChart.__init__(self, *args, **kwargs) - if self.auto_scale and not self.x_range: - warnings.warn('Please specify an x_range with GoogleOMeterChart, ' - 'otherwise one arrow will always be at the max.') - - def type_to_url(self): - return 'cht=gom' - - -class QRChart(Chart): - - def __init__(self, *args, **kwargs): - Chart.__init__(self, *args, **kwargs) - self.encoding = None - self.ec_level = None - self.margin = None - - def type_to_url(self): - return 'cht=qr' - - def data_to_url(self, data_class=None): - if not self.data: - raise NoDataGivenException() - return 'chl=%s' % urllib.quote(self.data[0]) - - def get_url_bits(self, data_class=None): - url_bits = Chart.get_url_bits(self, data_class=data_class) - if self.encoding: - url_bits.append('choe=%s' % self.encoding) - if self.ec_level: - url_bits.append('chld=%s|%s' % (self.ec_level, self.margin)) - return url_bits - - def set_encoding(self, encoding): - self.encoding = encoding - - def set_ec(self, level, margin): - self.ec_level = level - self.margin = margin - - -class ChartGrammar(object): - - def __init__(self): - self.grammar = None - self.chart = None - - def parse(self, grammar): - self.grammar = grammar - self.chart = self.create_chart_instance() - - for attr in self.grammar: - if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'): - continue # These are already parsed in create_chart_instance - attr_func = 'parse_' + attr - if not hasattr(self, attr_func): - warnings.warn('No parser for grammar attribute "%s"' % (attr)) - continue - getattr(self, attr_func)(grammar[attr]) - - return self.chart - - def parse_data(self, data): - self.chart.data = data - - @staticmethod - def get_possible_chart_types(): - possible_charts = [] - for cls_name in globals().keys(): - if not cls_name.endswith('Chart'): - continue - cls = globals()[cls_name] - # Check if it is an abstract class - try: - a = cls(1, 1, auto_scale=False) - del a - except AbstractClassException: - continue - # Strip off "Class" - possible_charts.append(cls_name[:-5]) - return possible_charts - - def create_chart_instance(self, grammar=None): - if not grammar: - grammar = self.grammar - assert(isinstance(grammar, dict)) # grammar must be a dict - assert('w' in grammar) # width is required - assert('h' in grammar) # height is required - assert('type' in grammar) # type is required - chart_type = grammar['type'] - w = grammar['w'] - h = grammar['h'] - auto_scale = grammar.get('auto_scale', None) - x_range = grammar.get('x_range', None) - y_range = grammar.get('y_range', None) - types = ChartGrammar.get_possible_chart_types() - if chart_type not in types: - raise UnknownChartType('%s is an unknown chart type. Possible ' - 'chart types are %s' % (chart_type, ','.join(types))) - return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale, - x_range=x_range, y_range=y_range) - - def download(self): - pass - diff --git a/snakegame/snake.py b/snakegame/snake.py index 89e97da..539498f 100644 --- a/snakegame/snake.py +++ b/snakegame/snake.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from __future__ import division import sys @@ -14,10 +12,10 @@ import traceback from common import * -class SnakeEngine(object): +class Engine(object): def __init__(self, rows, columns, n_apples, wrap=False, results=False, *args, **kwargs): - super(SnakeEngine, self).__init__(*args, **kwargs) + super(Engine, self).__init__(*args, **kwargs) self.wrap = wrap self.bots = {} diff --git a/snakegame/stats.py b/snakegame/stats.py deleted file mode 100644 index aaacfcf..0000000 --- a/snakegame/stats.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -import sys -from collections import defaultdict -from pngchart import SimpleLineChart -#from pygooglechart import SimpleLineChart -from colour import hash_colour - -WIDTH = 800 -HEIGHT = 300 -RESULTS_FILE = 'results.csv' - -def main(): - data = {} - order = [] - snakes = [] - for line in open(RESULTS_FILE): - game_id, name, length, life = line[:-1].split(',') - game_id = int(game_id) - length = int(length) - life = float(life) - - if name not in data: - snakes.append(name) - data[name] = {} - - if game_id not in order: - order.append(game_id) - - data[name][game_id] = (length, life) - - length_data = [] - time_data = [] - colours = [] - for name in snakes: - time_series = [] - length_series = [] - - for game_id in order: - length, time = data[name].get(game_id, (None, None)) - time_series.append(time) - length_series.append(length) - - colours.append('%02X%02X%02X' % hash_colour(name)) - - time_data.append(time_series) - length_data.append(length_series) - - for filename, data in (('length_chart.png', length_data), - ('time_chart.png', time_data)): - chart = SimpleLineChart(WIDTH, HEIGHT, colours=colours, legend=snakes) - for series in data: - chart.add_data(series) - print 'Updating', filename, '... ', - sys.stdout.flush() - chart.download(filename) - print 'done!' - -if __name__ == '__main__': - main() - -- cgit v1.2.3