From 93ba106c94b7a2d2109842432fed3dbe920c3558 Mon Sep 17 00:00:00 2001 From: Peter Ward Date: Thu, 19 Jul 2012 20:08:23 +1000 Subject: Move everything into a package. --- colour.py | 8 - common.py | 13 - console_snake.py | 73 --- images/apple.png | Bin 10421 -> 0 bytes images/eyes.png | Bin 4037 -> 0 bytes pngcanvas.py | 291 ------------ pngchart.py | 53 --- pygame_snake.py | 155 ------- pyglet_snake.py | 118 ----- pygooglechart.py | 1066 -------------------------------------------- snake.py | 161 ------- snakegame/colour.py | 8 + snakegame/common.py | 13 + snakegame/console_snake.py | 73 +++ snakegame/images/apple.png | Bin 0 -> 10421 bytes snakegame/images/eyes.png | Bin 0 -> 4037 bytes snakegame/pngcanvas.py | 291 ++++++++++++ snakegame/pngchart.py | 53 +++ snakegame/pygame_snake.py | 155 +++++++ snakegame/pyglet_snake.py | 118 +++++ snakegame/pygooglechart.py | 1066 ++++++++++++++++++++++++++++++++++++++++++++ snakegame/snake.py | 161 +++++++ snakegame/stats.py | 61 +++ snakegame/template.py | 15 + stats.py | 61 --- template.py | 15 - 26 files changed, 2014 insertions(+), 2014 deletions(-) delete mode 100644 colour.py delete mode 100644 common.py delete mode 100755 console_snake.py delete mode 100644 images/apple.png delete mode 100644 images/eyes.png delete mode 100644 pngcanvas.py delete mode 100644 pngchart.py delete mode 100755 pygame_snake.py delete mode 100755 pyglet_snake.py delete mode 100644 pygooglechart.py delete mode 100644 snake.py create mode 100644 snakegame/colour.py create mode 100644 snakegame/common.py create mode 100755 snakegame/console_snake.py create mode 100644 snakegame/images/apple.png create mode 100644 snakegame/images/eyes.png create mode 100644 snakegame/pngcanvas.py create mode 100644 snakegame/pngchart.py create mode 100755 snakegame/pygame_snake.py create mode 100755 snakegame/pyglet_snake.py create mode 100644 snakegame/pygooglechart.py create mode 100644 snakegame/snake.py create mode 100644 snakegame/stats.py create mode 100644 snakegame/template.py delete mode 100644 stats.py delete mode 100644 template.py diff --git a/colour.py b/colour.py deleted file mode 100644 index fa24f2b..0000000 --- a/colour.py +++ /dev/null @@ -1,8 +0,0 @@ -import hashlib - -def hash_colour(data): - data = map(ord, hashlib.md5(data).digest()) - colour = data[::3], data[1::3], data[2::3] - colour = map(sum, colour) - return (colour[0] % 255, colour[1] % 255, colour[2] % 255) - diff --git a/common.py b/common.py deleted file mode 100644 index 398f810..0000000 --- a/common.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - -directions = { - 'U': (0, -1), - 'D': (0, 1), - 'L': (-1, 0), - 'R': (1, 0), -} - -class Squares(object): - EMPTY = '.' - APPLE = '*' - diff --git a/console_snake.py b/console_snake.py deleted file mode 100755 index 1db5e38..0000000 --- a/console_snake.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python - -from __future__ import division - -import time - -import curses - -from common import * -from snake import SnakeEngine - -class ConsoleSnakeEngine(SnakeEngine): - def new_game(self, *args): - super(ConsoleSnakeEngine, self).new_game(*args) - - self.window = curses.initscr() - curses.start_color() - - curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) - curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) - - self.EMPTY_COLOUR = curses.color_pair(0) - self.APPLE_COLOUR = curses.color_pair(1) - self.SNAKE_COLOUR = curses.color_pair(4) - - def draw_board(self): - # Draw grid. - for y, row in enumerate(self.board): - for x, cell in enumerate(row): - char = '.' - colour = self.EMPTY_COLOUR - - # Draw the things on the square. - if cell == Squares.APPLE: - char = '@' - colour = self.APPLE_COLOUR - - elif cell.isalpha(): # Snake... -# colour = self.bots[cell.lower()][1] - char = cell - colour = self.SNAKE_COLOUR - - self.window.addstr(y, x, char, colour) - - def run(self): - while self.bots: - # Clear the screen. - self.window.erase() - - # Draw the board. - self.draw_board() - - # Update the display. - self.window.refresh() - time.sleep(0.025) - - # Let the snakes move! - self.update_snakes() - -def main(*args): - import sys - from processbot import BotWrapper - - rows, columns, apples = map(int, sys.argv[1:4]) - game = ConsoleSnakeEngine(rows, columns, apples) - for filename in sys.argv[4:]: - bot = BotWrapper(filename) - game.add_bot(bot) - game.run() - -if __name__ == '__main__': - curses.wrapper(main) - diff --git a/images/apple.png b/images/apple.png deleted file mode 100644 index 69a00ea..0000000 Binary files a/images/apple.png and /dev/null differ diff --git a/images/eyes.png b/images/eyes.png deleted file mode 100644 index 643158c..0000000 Binary files a/images/eyes.png and /dev/null differ diff --git a/pngcanvas.py b/pngcanvas.py deleted file mode 100644 index 394ff4f..0000000 --- a/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/pngchart.py b/pngchart.py deleted file mode 100644 index 5428718..0000000 --- a/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/pygame_snake.py b/pygame_snake.py deleted file mode 100755 index cf07297..0000000 --- a/pygame_snake.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python - -from __future__ import division - -import os -import time - -import pygame -pygame.init() -from pygame.locals import * - -from common import * -from snake import SnakeEngine - -class Sprites(object): - PREFIX = 'images' - def __getattribute__(self, name): - try: - return object.__getattribute__(self, name.upper()) - except AttributeError: - from pygame.image import load - filename = os.path.join(self.PREFIX, name.lower() + ".png") - image = load(filename).convert_alpha() - setattr(self, name, image) - return image -Sprites = Sprites() - -def scale_aspect((source_width, source_height), (target_width, target_height)): - source_aspect = source_width / source_height - target_aspect = target_width / target_height - if source_aspect > target_aspect: - # restrict width - width = target_width - height = width / source_aspect - else: - # restrict height - height = target_height - width = height * source_aspect - return (width, height) - -class PygameSnakeEngine(SnakeEngine): - EDGE_COLOR = (255, 255, 255) - EDGE_WIDTH = 1 - - def __init__(self, rows, columns, n_apples, - width=800, height=600, fullscreen=False, - **kwargs): - flags = 0 - if fullscreen: - flags |= pygame.FULLSCREEN - self.screen = pygame.display.set_mode((width, height), flags) - - self.width = width - self.height = height - - super(PygameSnakeEngine, self).__init__(rows, columns, n_apples, - **kwargs) - - def new_game(self, rows, columns, n_apples): - super(PygameSnakeEngine, self).new_game(rows, columns, n_apples) - - # make board surface - self.board_width, self.board_height = scale_aspect( - (columns, rows), (self.width, self.height) - ) - self.surface = pygame.Surface((self.board_width, self.board_height)) - - # load sprites - xscale = self.board_width / self.columns - yscale = self.board_height / self.rows - - def load_image(image): - new_size = scale_aspect(image.get_size(), (xscale, yscale)) - return pygame.transform.smoothscale(image, new_size) - - self.apple = load_image(Sprites.APPLE) - self.eyes = load_image(Sprites.EYES) - - def draw_board(self): - xscale = self.board_width / self.columns - yscale = self.board_height / self.rows - - # Draw grid. - for y, row in enumerate(self.board): - for x, cell in enumerate(row): - left = int(x * xscale) - top = int(y * yscale) - w = int((x + 1) * xscale) - left - h = int((y + 1) * yscale) - top - r = Rect(left, top, w, h) - - # Draw a square. - pygame.draw.rect(self.surface, self.EDGE_COLOR, r, - self.EDGE_WIDTH) - - # Draw the things on the square. - if cell == Squares.APPLE: - self.surface.blit(self.apple, r.topleft) - - elif cell.isalpha(): # Snake... - colour = self.bots[cell.lower()][1] - self.surface.fill(colour, r) - - if cell.isupper(): # Snake head - self.surface.blit(self.eyes, r.topleft) - - def run(self): - clock = pygame.time.Clock() - - running = True - while running and self.bots: - for event in pygame.event.get(): - if event.type == pygame.QUIT or \ - (event.type == pygame.KEYDOWN and event.key == K_ESCAPE): - running = False - break - if not running: break - - # Clear the screen. - self.screen.fill((0, 0, 0)) - self.surface.fill((0, 0, 0)) - - # Draw the board. - self.draw_board() - - # Center the board. - x = (self.width - self.board_width) / 2 - y = (self.height - self.board_height) / 2 - self.screen.blit(self.surface, (x, y)) - - # Update the display. - pygame.display.flip() - clock.tick(20) - - # Let the snakes move! - self.update_snakes() - - if running: - time.sleep(2) - -if __name__ == '__main__': - import sys - from processbot import BotWrapper - - rows, columns, apples = map(int, sys.argv[1:4]) - game = PygameSnakeEngine(rows, columns, apples) - for filename in sys.argv[4:]: - bot = BotWrapper(filename) - game.add_bot(bot) - game.run() - - # Early window close, late process cleanup. - pygame.display.quit() - - diff --git a/pyglet_snake.py b/pyglet_snake.py deleted file mode 100755 index 5a17ac0..0000000 --- a/pyglet_snake.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python - -from __future__ import division - -import pyglet -pyglet.resource.path = ['images'] -pyglet.resource.reindex() - -from pyglet import gl - -import common -from snake import SnakeEngine - -def scale_aspect((source_width, source_height), (target_width, target_height)): - source_aspect = source_width / source_height - target_aspect = target_width / target_height - if source_aspect > target_aspect: - # restrict width - width = target_width - height = width / source_aspect - else: - # restrict height - height = target_height - width = height * source_aspect - return (width, height) - -class PygletSnakeEngine(SnakeEngine, 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) - - gl.glEnable(gl.GL_BLEND) - gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - - 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) - - # make board surface - self.board_width, self.board_height = scale_aspect( - (columns, rows), (self.width, self.height) - ) - - # load sprites - xscale = self.board_width / self.columns - yscale = self.board_height / self.rows - - self.apple = pyglet.resource.image('apple.png') - self.apple.size = scale_aspect( - (self.apple.width, self.apple.height), - (xscale, yscale) - ) - self.eyes = pyglet.resource.image('eyes.png') - self.eyes.size = scale_aspect( - (self.eyes.width, self.eyes.height), - (xscale, yscale) - ) - - def on_draw(self): - self.clear() - - xscale = self.board_width / self.columns - yscale = self.board_height / self.rows - - # Draw grid. - for y, row in enumerate(self.board): - for x, cell in enumerate(row): - left = int(x * xscale) - top = self.height - int(y * yscale) - right = int((x + 1) * xscale) - bottom = self.height - int((y + 1) * yscale) - r = (left, top, right, top, right, bottom, left, bottom) - - # Draw a square. - gl.glLineWidth(self.EDGE_WIDTH) - pyglet.graphics.draw(4, gl.GL_LINE_LOOP, - ('v2f', r), - ('c4B', self.EDGE_COLOR * 4)) - - # Draw the things on the square. - if cell == common.Squares.APPLE: - w, h = self.apple.size - self.apple.blit(left + (xscale - w) / 2.0, top - h, width=w, height=h) - - elif cell.isalpha(): # Snake... - colour = self.bots[cell.lower()][1] + (255,) - gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL) - pyglet.graphics.draw(4, gl.GL_POLYGON, - ('v2f', r), - ('c4B', colour * 4), - ) - - if cell.isupper(): # Snake head - w, h = self.eyes.size - self.eyes.blit(left, top - h, width=w, height=h) - - def update_snakes(self, *args): - if not self.bots: - pyglet.app.exit() - super(PygletSnakeEngine, self).update_snakes(*args) - - def run(self): - pyglet.app.run() - -if __name__ == '__main__': - import sys - from processbot import BotWrapper - - rows, columns, apples = map(int, sys.argv[1:4]) - game = PygletSnakeEngine(rows, columns, apples) - for filename in sys.argv[4:]: - bot = BotWrapper(filename) - game.add_bot(bot) - game.run() - diff --git a/pygooglechart.py b/pygooglechart.py deleted file mode 100644 index 0c17973..0000000 --- a/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/snake.py b/snake.py deleted file mode 100644 index 89e97da..0000000 --- a/snake.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python - -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 SnakeEngine(object): - def __init__(self, rows, columns, n_apples, wrap=False, results=False, - *args, **kwargs): - super(SnakeEngine, 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/colour.py b/snakegame/colour.py new file mode 100644 index 0000000..fa24f2b --- /dev/null +++ b/snakegame/colour.py @@ -0,0 +1,8 @@ +import hashlib + +def hash_colour(data): + data = map(ord, hashlib.md5(data).digest()) + colour = data[::3], data[1::3], data[2::3] + colour = map(sum, colour) + return (colour[0] % 255, colour[1] % 255, colour[2] % 255) + diff --git a/snakegame/common.py b/snakegame/common.py new file mode 100644 index 0000000..398f810 --- /dev/null +++ b/snakegame/common.py @@ -0,0 +1,13 @@ +import os + +directions = { + 'U': (0, -1), + 'D': (0, 1), + 'L': (-1, 0), + 'R': (1, 0), +} + +class Squares(object): + EMPTY = '.' + APPLE = '*' + diff --git a/snakegame/console_snake.py b/snakegame/console_snake.py new file mode 100755 index 0000000..1db5e38 --- /dev/null +++ b/snakegame/console_snake.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +from __future__ import division + +import time + +import curses + +from common import * +from snake import SnakeEngine + +class ConsoleSnakeEngine(SnakeEngine): + def new_game(self, *args): + super(ConsoleSnakeEngine, self).new_game(*args) + + self.window = curses.initscr() + curses.start_color() + + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) + + self.EMPTY_COLOUR = curses.color_pair(0) + self.APPLE_COLOUR = curses.color_pair(1) + self.SNAKE_COLOUR = curses.color_pair(4) + + def draw_board(self): + # Draw grid. + for y, row in enumerate(self.board): + for x, cell in enumerate(row): + char = '.' + colour = self.EMPTY_COLOUR + + # Draw the things on the square. + if cell == Squares.APPLE: + char = '@' + colour = self.APPLE_COLOUR + + elif cell.isalpha(): # Snake... +# colour = self.bots[cell.lower()][1] + char = cell + colour = self.SNAKE_COLOUR + + self.window.addstr(y, x, char, colour) + + def run(self): + while self.bots: + # Clear the screen. + self.window.erase() + + # Draw the board. + self.draw_board() + + # Update the display. + self.window.refresh() + time.sleep(0.025) + + # Let the snakes move! + self.update_snakes() + +def main(*args): + import sys + from processbot import BotWrapper + + rows, columns, apples = map(int, sys.argv[1:4]) + game = ConsoleSnakeEngine(rows, columns, apples) + for filename in sys.argv[4:]: + bot = BotWrapper(filename) + game.add_bot(bot) + game.run() + +if __name__ == '__main__': + curses.wrapper(main) + diff --git a/snakegame/images/apple.png b/snakegame/images/apple.png new file mode 100644 index 0000000..69a00ea Binary files /dev/null and b/snakegame/images/apple.png differ diff --git a/snakegame/images/eyes.png b/snakegame/images/eyes.png new file mode 100644 index 0000000..643158c Binary files /dev/null and b/snakegame/images/eyes.png differ diff --git a/snakegame/pngcanvas.py b/snakegame/pngcanvas.py new file mode 100644 index 0000000..394ff4f --- /dev/null +++ b/snakegame/pngcanvas.py @@ -0,0 +1,291 @@ +#!/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 new file mode 100644 index 0000000..5428718 --- /dev/null +++ b/snakegame/pngchart.py @@ -0,0 +1,53 @@ +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/pygame_snake.py b/snakegame/pygame_snake.py new file mode 100755 index 0000000..cf07297 --- /dev/null +++ b/snakegame/pygame_snake.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +from __future__ import division + +import os +import time + +import pygame +pygame.init() +from pygame.locals import * + +from common import * +from snake import SnakeEngine + +class Sprites(object): + PREFIX = 'images' + def __getattribute__(self, name): + try: + return object.__getattribute__(self, name.upper()) + except AttributeError: + from pygame.image import load + filename = os.path.join(self.PREFIX, name.lower() + ".png") + image = load(filename).convert_alpha() + setattr(self, name, image) + return image +Sprites = Sprites() + +def scale_aspect((source_width, source_height), (target_width, target_height)): + source_aspect = source_width / source_height + target_aspect = target_width / target_height + if source_aspect > target_aspect: + # restrict width + width = target_width + height = width / source_aspect + else: + # restrict height + height = target_height + width = height * source_aspect + return (width, height) + +class PygameSnakeEngine(SnakeEngine): + EDGE_COLOR = (255, 255, 255) + EDGE_WIDTH = 1 + + def __init__(self, rows, columns, n_apples, + width=800, height=600, fullscreen=False, + **kwargs): + flags = 0 + if fullscreen: + flags |= pygame.FULLSCREEN + self.screen = pygame.display.set_mode((width, height), flags) + + self.width = width + self.height = height + + super(PygameSnakeEngine, self).__init__(rows, columns, n_apples, + **kwargs) + + def new_game(self, rows, columns, n_apples): + super(PygameSnakeEngine, self).new_game(rows, columns, n_apples) + + # make board surface + self.board_width, self.board_height = scale_aspect( + (columns, rows), (self.width, self.height) + ) + self.surface = pygame.Surface((self.board_width, self.board_height)) + + # load sprites + xscale = self.board_width / self.columns + yscale = self.board_height / self.rows + + def load_image(image): + new_size = scale_aspect(image.get_size(), (xscale, yscale)) + return pygame.transform.smoothscale(image, new_size) + + self.apple = load_image(Sprites.APPLE) + self.eyes = load_image(Sprites.EYES) + + def draw_board(self): + xscale = self.board_width / self.columns + yscale = self.board_height / self.rows + + # Draw grid. + for y, row in enumerate(self.board): + for x, cell in enumerate(row): + left = int(x * xscale) + top = int(y * yscale) + w = int((x + 1) * xscale) - left + h = int((y + 1) * yscale) - top + r = Rect(left, top, w, h) + + # Draw a square. + pygame.draw.rect(self.surface, self.EDGE_COLOR, r, + self.EDGE_WIDTH) + + # Draw the things on the square. + if cell == Squares.APPLE: + self.surface.blit(self.apple, r.topleft) + + elif cell.isalpha(): # Snake... + colour = self.bots[cell.lower()][1] + self.surface.fill(colour, r) + + if cell.isupper(): # Snake head + self.surface.blit(self.eyes, r.topleft) + + def run(self): + clock = pygame.time.Clock() + + running = True + while running and self.bots: + for event in pygame.event.get(): + if event.type == pygame.QUIT or \ + (event.type == pygame.KEYDOWN and event.key == K_ESCAPE): + running = False + break + if not running: break + + # Clear the screen. + self.screen.fill((0, 0, 0)) + self.surface.fill((0, 0, 0)) + + # Draw the board. + self.draw_board() + + # Center the board. + x = (self.width - self.board_width) / 2 + y = (self.height - self.board_height) / 2 + self.screen.blit(self.surface, (x, y)) + + # Update the display. + pygame.display.flip() + clock.tick(20) + + # Let the snakes move! + self.update_snakes() + + if running: + time.sleep(2) + +if __name__ == '__main__': + import sys + from processbot import BotWrapper + + rows, columns, apples = map(int, sys.argv[1:4]) + game = PygameSnakeEngine(rows, columns, apples) + for filename in sys.argv[4:]: + bot = BotWrapper(filename) + game.add_bot(bot) + game.run() + + # Early window close, late process cleanup. + pygame.display.quit() + + diff --git a/snakegame/pyglet_snake.py b/snakegame/pyglet_snake.py new file mode 100755 index 0000000..5a17ac0 --- /dev/null +++ b/snakegame/pyglet_snake.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +from __future__ import division + +import pyglet +pyglet.resource.path = ['images'] +pyglet.resource.reindex() + +from pyglet import gl + +import common +from snake import SnakeEngine + +def scale_aspect((source_width, source_height), (target_width, target_height)): + source_aspect = source_width / source_height + target_aspect = target_width / target_height + if source_aspect > target_aspect: + # restrict width + width = target_width + height = width / source_aspect + else: + # restrict height + height = target_height + width = height * source_aspect + return (width, height) + +class PygletSnakeEngine(SnakeEngine, 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) + + gl.glEnable(gl.GL_BLEND) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + + 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) + + # make board surface + self.board_width, self.board_height = scale_aspect( + (columns, rows), (self.width, self.height) + ) + + # load sprites + xscale = self.board_width / self.columns + yscale = self.board_height / self.rows + + self.apple = pyglet.resource.image('apple.png') + self.apple.size = scale_aspect( + (self.apple.width, self.apple.height), + (xscale, yscale) + ) + self.eyes = pyglet.resource.image('eyes.png') + self.eyes.size = scale_aspect( + (self.eyes.width, self.eyes.height), + (xscale, yscale) + ) + + def on_draw(self): + self.clear() + + xscale = self.board_width / self.columns + yscale = self.board_height / self.rows + + # Draw grid. + for y, row in enumerate(self.board): + for x, cell in enumerate(row): + left = int(x * xscale) + top = self.height - int(y * yscale) + right = int((x + 1) * xscale) + bottom = self.height - int((y + 1) * yscale) + r = (left, top, right, top, right, bottom, left, bottom) + + # Draw a square. + gl.glLineWidth(self.EDGE_WIDTH) + pyglet.graphics.draw(4, gl.GL_LINE_LOOP, + ('v2f', r), + ('c4B', self.EDGE_COLOR * 4)) + + # Draw the things on the square. + if cell == common.Squares.APPLE: + w, h = self.apple.size + self.apple.blit(left + (xscale - w) / 2.0, top - h, width=w, height=h) + + elif cell.isalpha(): # Snake... + colour = self.bots[cell.lower()][1] + (255,) + gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL) + pyglet.graphics.draw(4, gl.GL_POLYGON, + ('v2f', r), + ('c4B', colour * 4), + ) + + if cell.isupper(): # Snake head + w, h = self.eyes.size + self.eyes.blit(left, top - h, width=w, height=h) + + def update_snakes(self, *args): + if not self.bots: + pyglet.app.exit() + super(PygletSnakeEngine, self).update_snakes(*args) + + def run(self): + pyglet.app.run() + +if __name__ == '__main__': + import sys + from processbot import BotWrapper + + rows, columns, apples = map(int, sys.argv[1:4]) + game = PygletSnakeEngine(rows, columns, apples) + for filename in sys.argv[4:]: + bot = BotWrapper(filename) + game.add_bot(bot) + game.run() + diff --git a/snakegame/pygooglechart.py b/snakegame/pygooglechart.py new file mode 100644 index 0000000..0c17973 --- /dev/null +++ b/snakegame/pygooglechart.py @@ -0,0 +1,1066 @@ +""" +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 new file mode 100644 index 0000000..89e97da --- /dev/null +++ b/snakegame/snake.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python + +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 SnakeEngine(object): + def __init__(self, rows, columns, n_apples, wrap=False, results=False, + *args, **kwargs): + super(SnakeEngine, 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/stats.py b/snakegame/stats.py new file mode 100644 index 0000000..aaacfcf --- /dev/null +++ b/snakegame/stats.py @@ -0,0 +1,61 @@ +#!/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() + diff --git a/snakegame/template.py b/snakegame/template.py new file mode 100644 index 0000000..194b1a5 --- /dev/null +++ b/snakegame/template.py @@ -0,0 +1,15 @@ +from pyglet_snake import PygletSnakeEngine + +def your_name_here_bot(board, position): + x, y = position + mychar = board[y][x] + return 'U' + +# Test code to run the snake game. +# Leave the if statement as is, otherwise I won't be able to run your bot with +# the other bots. +if __name__ == '__main__': + p = PygletSnakeEngine(25, 25, 10) + p.add_bot(your_name_here_bot) + p.run() + diff --git a/stats.py b/stats.py deleted file mode 100644 index aaacfcf..0000000 --- a/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() - diff --git a/template.py b/template.py deleted file mode 100644 index 194b1a5..0000000 --- a/template.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyglet_snake import PygletSnakeEngine - -def your_name_here_bot(board, position): - x, y = position - mychar = board[y][x] - return 'U' - -# Test code to run the snake game. -# Leave the if statement as is, otherwise I won't be able to run your bot with -# the other bots. -if __name__ == '__main__': - p = PygletSnakeEngine(25, 25, 10) - p.add_bot(your_name_here_bot) - p.run() - -- cgit v1.2.3