summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore11
-rw-r--r--README57
-rw-r--r--bots.py52
-rw-r--r--colour.py8
-rw-r--r--common.py13
-rw-r--r--docs/Makefile27
-rw-r--r--docs/closest_apple.py60
-rw-r--r--docs/closest_apple.tex13
-rw-r--r--docs/firstbot.py2
-rw-r--r--docs/firstbot.tex81
-rw-r--r--docs/introduction.tex61
-rwxr-xr-xdocs/jinja251
-rw-r--r--docs/look_ahead.tex9
-rw-r--r--docs/macros.tex19
-rw-r--r--docs/print_bot.py3
-rw-r--r--docs/random_avoid.py26
-rw-r--r--docs/random_avoid.tex167
-rw-r--r--docs/random_simple.py4
-rw-r--r--docs/random_simple.tex65
-rw-r--r--docs/tutorial.tex61
-rw-r--r--oldbot.py30
-rwxr-xr-xoldbots/peter.py83
-rwxr-xr-xoldbots/peter_smart.py131
-rwxr-xr-xoldbots/peter_smart2.py135
-rw-r--r--pngcanvas.py291
-rw-r--r--pngchart.py53
-rwxr-xr-xpygame_snake.py160
-rwxr-xr-xpyglet_snake.py123
-rw-r--r--pygooglechart.py1066
-rw-r--r--setup.py22
-rw-r--r--snake.py155
-rw-r--r--snakegame/__init__.py55
-rw-r--r--snakegame/bots/__init__.py44
-rw-r--r--snakegame/colour.py7
-rw-r--r--snakegame/common.py87
-rw-r--r--snakegame/engine.py205
-rw-r--r--snakegame/engines/__init__.py1
-rw-r--r--snakegame/images/apple.png (renamed from images/apple.png)bin10421 -> 10421 bytes
-rw-r--r--snakegame/images/eyes.png (renamed from images/eyes.png)bin4037 -> 4037 bytes
-rw-r--r--snakegame/images/icecream.pngbin0 -> 6346 bytes
-rw-r--r--snakegame/images/shrinkpotion.pngbin0 -> 42082 bytes
-rw-r--r--snakegame/images/wall.pngbin0 -> 1025 bytes
-rw-r--r--snakegame/utils.py20
-rw-r--r--snakegame/viewers/__init__.py10
-rw-r--r--[-rwxr-xr-x]snakegame/viewers/curses.py (renamed from console_snake.py)44
-rw-r--r--snakegame/viewers/pygame.py138
-rw-r--r--snakegame/viewers/pyglet.py127
-rw-r--r--snakegame/zmq.py35
-rw-r--r--stats.py61
-rw-r--r--template.py15
50 files changed, 1478 insertions, 2410 deletions
diff --git a/.hgignore b/.hgignore
index 85576b0..a23a37a 100644
--- a/.hgignore
+++ b/.hgignore
@@ -1,2 +1,9 @@
-glob:*.pyc
-glob:*.orig
+syntax: glob
+*~
+*.py[co]
+
+docs/build
+docs/tutorial.pdf
+
+pyglet
+*_bot.py
diff --git a/README b/README
new file mode 100644
index 0000000..8e6140f
--- /dev/null
+++ b/README
@@ -0,0 +1,57 @@
+= Getting started =
+
+$ cat > simple.py
+def bot(board, position):
+ return 'L'
+^D
+
+$ snakegame -e pyglet simple
+
+= Writing a bot =
+
+A bot is simply a Python function which takes two arguments, the current state
+of the board, and the current position of the head of your snake. The function
+must return one of the strings 'L', 'U', 'R' or 'D', indicating which direction
+the snake should next move (left, up, right or down, respectively).
+
+== The Board ==
+
+The board is a list containing each row of the board.
+Each row is a list containing the cells of that row.
+
+The board is actually a torus (that is, the top edge wraps to the bottom, and
+the left edge to the right, and vice versa).
+Map designers can easily turn this into a normal grid simply by placing walls on
+the edges.
+
+Each cell is a single character string:
+
+* period (.) indicates an empty cell
+* asterisk (*) indicates an apple
+* plus (+) indicates an ice cream
+* minus (-) indicates a shrinking potion
+* octothorpe (#) indicates a wall
+* uppercase letters (A-Z) indicate the head of a snake.
+* lowercase letters (a-z) indicate the body of a snake.
+
+All other characters are reserved for future use.
+
+Every snake will have exactly one head.
+Snakes may have no body.
+Snakes may have a body which is not contiguous on the board.
+
+== Usual boilerplate ==
+
+The typical boilerplate for writing a bot looks like this, which gets the
+character of your snake’s head, and the size of the board.
+
+def bot(board, position):
+ x, y = position
+ me = board[y][x]
+
+ height = len(board)
+ width = len(board[0])
+
+ # ...
+ return 'L'
+
diff --git a/bots.py b/bots.py
deleted file mode 100644
index f3e7cee..0000000
--- a/bots.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import random
-
-from common import *
-
-def right_bot(board, (x, y)):
- return 'R'
-
-def random_bot(board, (x, y)):
- return random.choice('UDLR')
-
-def random_bounds_bot(board, (x, y)):
- height = len(board)
- width = len(board[0])
- moves = []
- if x > 0:
- moves.append('L')
- if x < width - 1:
- moves.append('R')
- if y > 0:
- moves.append('U')
- if y < height - 1:
- moves.append('D')
-
- move = 'U'
- while moves and move not in moves:
- move = random_bot(board, (x, y))
- return move
-
-def random_square_bot(board, (x, y)):
- def in_bounds(x, y, w, h):
- return x >= 0 and y >= 0 and x < w and y < h
-
- h = len(board)
- w = len(board[0])
-
- todo = directions.keys()
-
- move = random_bot(board, (x, y))
- dx, dy = directions[move]
- nx = x + dx
- ny = y + dy
-
- while todo and in_bounds(nx, ny, w, h) and \
- board[ny][nx] not in (Squares.EMPTY, Squares.APPLE):
- if move in todo:
- todo.remove(move)
- move = random_bot(board, (x, y))
- dx, dy = directions[move]
- nx = x + dx
- ny = y + dy
- return move
-
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/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..5cd4a7e
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,27 @@
+BUILD_DIR = build
+
+FILES = $(wildcard *.tex *.py)
+BUILD_FILES = $(patsubst %,${BUILD_DIR}/%,${FILES})
+
+LATEX=xelatex
+LATEX_FLAGS=-shell-escape -interaction=nonstopmode
+
+.PHONY: all
+
+all: tutorial.pdf
+
+${BUILD_DIR}:
+ mkdir -p ${BUILD_DIR}
+
+${BUILD_DIR}/%.tex: %.tex
+ ./jinja2 --latex < $< > $@
+
+${BUILD_DIR}/%.py: %.py
+ ln $< $@
+
+tutorial.pdf: ${BUILD_DIR}/tutorial.tex ${BUILD_FILES}
+ cd "${BUILD_DIR}" && \
+ ${LATEX} ${LATEX_FLAGS} tutorial && \
+ ${LATEX} ${LATEX_FLAGS} tutorial && \
+ ${LATEX} ${LATEX_FLAGS} tutorial
+ mv -f "${BUILD_DIR}/tutorial.pdf" tutorial.pdf
diff --git a/docs/closest_apple.py b/docs/closest_apple.py
new file mode 100644
index 0000000..3696bfa
--- /dev/null
+++ b/docs/closest_apple.py
@@ -0,0 +1,60 @@
+DIRECTIONS = {
+ 'L': (-1, 0),
+ 'U': (0, -1),
+ 'R': (1, 0),
+ 'D': (0, 1),
+}
+
+def closest_apple_bot(board, position):
+ x, y = position
+ height = len(board)
+ width = len(board[0])
+
+ # todo contains the squares we need to explore
+ todo = []
+ # done contains the squares we've already explored
+ done = set()
+
+ # for each initial direction
+ for direction in DIRECTIONS:
+ dx, dy = DIRECTIONS[direction]
+ # find the new position
+ nx = (x + dx) % width
+ ny = (y + dy) % height
+ # add to todo and done
+ todo.append((nx, ny, direction))
+ done.add((nx, ny))
+
+ while todo:
+ # take the first item in todo
+ x, y, direction = todo.pop(0)
+
+ cell = board[y][x]
+
+ # if we've reached an apple, we've found the shortest path
+ # and direction is the right way to go
+ if cell == '*':
+ return direction
+
+ # if we can't move into this cell, go to the next square to explore
+ if cell != '.':
+ continue
+
+ # at this square, we can go any direction,
+ # as long as it's not in our done set
+ for dx, dy in DIRECTIONS.values():
+ nx = (x + dx) % width
+ ny = (y + dy) % height
+
+ if (nx, ny) not in done:
+ # we haven't visited this square before,
+ # add it to our list of squares to visit
+ # note that the third item here is the direction we initially
+ # took to get to this square
+ todo.append((nx, ny, direction))
+ done.add((nx, ny))
+
+ # if we get here, there are no apples on the board,
+ # so we'll just move up.
+ return 'U'
+
diff --git a/docs/closest_apple.tex b/docs/closest_apple.tex
new file mode 100644
index 0000000..36b7382
--- /dev/null
+++ b/docs/closest_apple.tex
@@ -0,0 +1,13 @@
+\section{Closest apple}
+
+One interesting bot we can write is one which always moves towards the closest
+apple. Both the idea and the coding are a little tricky, but see if you can
+handle it. In order to find the closest apple, we actually want to know
+the \emph{shortest path} to the apple, and when we know that, the first step in
+that path is the direction we need to move in.
+
+To find the shortest path, we need to use an algorithm called
+\emph{breadth first search} (BFS).
+% TODO: description of the algorithm
+
+\pythonfile{closest_apple.py}
diff --git a/docs/firstbot.py b/docs/firstbot.py
new file mode 100644
index 0000000..6eb8484
--- /dev/null
+++ b/docs/firstbot.py
@@ -0,0 +1,2 @@
+def up_bot(board, position):
+ return 'U'
diff --git a/docs/firstbot.tex b/docs/firstbot.tex
new file mode 100644
index 0000000..7e95b85
--- /dev/null
+++ b/docs/firstbot.tex
@@ -0,0 +1,81 @@
+\section{Your First Bot}
+\label{sec:firstbot}
+\fasttrack{Always move up.}
+
+Alright, let’s get started.
+If you think back to when you started programming, chances are the first program
+you ever wrote was one which printed out the immortal phrase “Hello World”.
+Well we can’t print stuff here, but our first bot is going to be almost as
+useless as that: our bot is just going to continually move up.
+
+Let’s have a look at the code:
+\pythonfile{firstbot.py}
+
+Pretty simple, huh?
+It’s a function takes as input two parameters, and returns a string.
+We’ll have a look at \texttt{board} and \texttt{position} later,
+but the important thing here is that the returned value says which direction the
+snake should move in. Each time the snake is allowed to make a move, the
+function is called, it returns one of \py|'U', 'D', 'L', 'R'|
+(indicating up, down, left and right, respectively), and the snake is moved in
+that direction.
+
+\subsection{Running the code}
+
+Depending on how you have installed SnakeGame, there are a few different ways to
+run the code. If you’re in some kind of programming class, ask your instructor
+which method to use.
+
+\subsubsection{Method A: CLI interface}
+
+If you installed from the repository (using \texttt{pip}), this is the method
+you should use.
+Assuming you’ve put the \texttt{up\_bot} function in a file called
+\texttt{mybot.py}, you can run this command:
+
+\begin{shell}
+$ snakegame mybot:up_bot
+\end{shell}
+
+To use different viewers, you can supply the \texttt{-v VIEWER} argument:
+\begin{shell}
+$ snakegame -v pyglet mybot:up_bot
+$ snakegame -v pygame mybot:up_bot
+$ snakegame -v curses mybot:up_bot
+\end{shell}
+
+You can specify multiple bots, and also control the width, height and number of
+apples on the board:
+\begin{shell}
+$ snakegame -w 4 -h 20 -a 30 mybot:up_bot mybot:up_bot mybot:up_bot
+\end{shell}
+
+\subsubsection{Method B: Pyglet / Pygame}
+
+You can also add some code to the file containing your bot so that you can run
+that file as a normal Python program, which will run the game.
+At the end of the file, add this:
+\begin{pythoncode}
+if __name__ == '__main__':
+ from snakegame.engine import Engine
+ from snakegame.viewers.pyglet import Viewer
+ engine = Engine(10, 10, 25)
+ engine.add_bot(up_bot)
+ viewer = Viewer(engine)
+ viewer.run()
+\end{pythoncode}
+
+If you want to use pygame instead, change \texttt{snakegame.viewers.pyglet} to
+\texttt{snakegame.viewers.pygame}.
+
+If neither of these work, there is also a console viewer, which works if you’re
+in a terminal (it will not work in IDLE!):
+use \texttt{snakegame.viewers.curses}.
+
+\subsection{Got it running?}
+
+Great, you should see a nice big board with some apples scattered over it,
+and a snake continually moving upwards.
+
+Once you’re ready, we’ll move on to something a little more interesting.
+
diff --git a/docs/introduction.tex b/docs/introduction.tex
new file mode 100644
index 0000000..5a149b7
--- /dev/null
+++ b/docs/introduction.tex
@@ -0,0 +1,61 @@
+\section{Introduction}
+
+Before starting this tutorial, you should \emph{already} know the basics of
+Python. Specifically, you should know these bits of Python:
+\begin{itemize}
+ \item How to \py|print| things
+ \item \py|if|, \py|elif| and \py|else|
+ \item \py|for| and \py|while| loops
+ \item \py|list|s and \py|dict|ionaries
+ \item functions (\py|def|)
+\end{itemize}
+
+\subsection{Help! I don’t know what these are…}
+
+If you have no idea what any of those things are, \emph{don’t panic}.
+All that means is that you’re not quite ready to follow this tutorial yet, and
+you need to learn the basics of Python first.
+There are many excellent \emph{free} resources for doing this:
+\begin{itemize}
+ \item How to Think Like a Computer Scientist \\
+ (\url{http://openbookproject.net/thinkcs/python/english2e/})
+ \item Learn Python the Hard Way \\
+ \url{http://learnpythonthehardway.org/}
+ \item The official tutorial in the Python documentation \\
+ \url{http://docs.python.org/tutorial/}
+\end{itemize}
+
+The most important resource to learn programming, however, are the people you
+know who are learning Python with you, already know Python, or some other
+programming language.
+You can spend hours trying to understand something in books and not get it,
+but ask another person to explain it, and it will all suddenly ‘click’ and make
+sense.
+
+\subsection{Yeah, I know what those are.}
+
+Excellent! Let’s get started then.
+
+If you’re doing this in some kind of programming class, your instructor may have
+provided you with a zip file (or similar) containing SnakeGame and pyglet.
+If so, follow their instructions for setting it up, and head straight on to
+Your First Bot.
+
+Otherwise, you’ll need to first install the code. The latest version of
+SnakeGame is available in a Mercurial repository at
+\url{http://hg.flowblok.id.au/snakegame}.
+You can install it using pip:
+\begin{shell}
+$ pip install hg+http://hg.flowblok.id.au/snakegame#egg=SnakeGame
+\end{shell}
+
+If you wish to have a pretty graphical viewer for watching the game being
+played, you will also need to install pyglet\footnoteurl{http://pyglet.org/}
+and/or pygame\footnoteurl{http://pygame.org}.
+
+\subsection{Skipping ahead}
+
+If you already know Python, you will probably want to skip some sections of this
+tutorial. To make this easier, there is a \emph{Fast track} note at the start of
+each section: if you can write a bot which does what it says, you can safely
+skip that section.
diff --git a/docs/jinja2 b/docs/jinja2
new file mode 100755
index 0000000..a1c89e2
--- /dev/null
+++ b/docs/jinja2
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+import argparse
+import json
+from os import path
+import sys
+
+from jinja2 import Environment, FileSystemLoader
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--latex', action='store_true')
+parser.add_argument('template', nargs='?')
+parser.add_argument('data', nargs='?')
+
+args = parser.parse_args()
+
+if args.latex:
+ env = Environment(
+ block_start_string='%%',
+ block_end_string='%%',
+ variable_start_string='<',
+ variable_end_string='>',
+ comment_start_string='###',
+ comment_end_string='###',
+ )
+else:
+ env = Environment()
+
+if args.template:
+ dirname, basename = path.split(args.template)
+
+ env.loader = FileSystemLoader(dirname)
+ template = env.get_template(basename)
+
+ if args.data:
+ with open(args.data, 'rb') as f:
+ data = json.load(f)
+
+ else:
+ data = json.load(sys.stdin)
+
+else:
+ source = sys.stdin.read()
+ template = env.from_string(source.decode('utf-8'))
+ data = {}
+
+ env.loader = FileSystemLoader('.')
+
+output = template.render(data)
+
+sys.stdout.write(output.encode('utf-8'))
diff --git a/docs/look_ahead.tex b/docs/look_ahead.tex
new file mode 100644
index 0000000..d5c37c6
--- /dev/null
+++ b/docs/look_ahead.tex
@@ -0,0 +1,9 @@
+\section{Look ahead}
+
+By now you’re probably itching to try out some of your own ideas about how to
+make a good bot. Now’s a great time to take a break from following my detailed
+instructions and just play around with some ideas.
+Here’s an idea to get you started: our \texttt{random\_avoid\_bot} only looked
+one square ahead, try making a bot which looks two squares ahead and chooses the
+direction with the most free space.
+
diff --git a/docs/macros.tex b/docs/macros.tex
new file mode 100644
index 0000000..2f16900
--- /dev/null
+++ b/docs/macros.tex
@@ -0,0 +1,19 @@
+%%- macro make_board(board) %%
+\begin{verbatim}
+%% for row in board %%
+%%- if loop.first %%
+<- ' ' >
+%%- for n in range(row |length) %%
+<- ' ' ~ n >
+%%- endfor %%
+< ' +' ~ '-+' * (row | length) >
+%%- endif %%
+< loop.index0 ~ '|' >
+%%- for cell in row %%
+<- cell >|
+%%- endfor %%
+< ' +' ~ '-+' * (row | length) >
+%%- endfor %%
+\end{verbatim}
+%%- endmacro %%
+
diff --git a/docs/print_bot.py b/docs/print_bot.py
new file mode 100644
index 0000000..5adee1f
--- /dev/null
+++ b/docs/print_bot.py
@@ -0,0 +1,3 @@
+def print_bot(board, position):
+ print position
+ print board
diff --git a/docs/random_avoid.py b/docs/random_avoid.py
new file mode 100644
index 0000000..bf08eef
--- /dev/null
+++ b/docs/random_avoid.py
@@ -0,0 +1,26 @@
+from random import choice
+
+def random_avoid_bot(board, position):
+ x, y = position
+ height = len(board)
+ width = len(board[0])
+
+ valid_moves = []
+
+ left = board[y][(x - 1) % width]
+ if left == '.' or left == '*':
+ valid_moves.append('L')
+
+ right = board[y][(x + 1) % width]
+ if right == '.' or right == '*':
+ valid_moves.append('R')
+
+ up = board[(y - 1) % height][x]
+ if up == '.' or up == '*':
+ valid_moves.append('U')
+
+ down = board[(y + 1) % height][x]
+ if down == '.' or down == '*':
+ valid_moves.append('D')
+
+ return choice(valid_moves)
diff --git a/docs/random_avoid.tex b/docs/random_avoid.tex
new file mode 100644
index 0000000..90ac183
--- /dev/null
+++ b/docs/random_avoid.tex
@@ -0,0 +1,167 @@
+%%- from "macros.tex" import make_board -%%
+
+\section{Random Avoid Bot}
+\fasttrack{Choose a direction at random, but not one which will lead to immediate death.}
+
+The last bot we wrote had a big problem, it ran into its own tail.
+We don’t want our next bot to be that stupid, so we need to teach it how to not
+do that!
+
+But before we can do that, we need to know few more things about our bots.
+You might have noticed that our functions have two parameters,
+\texttt{board} and \texttt{position}.
+We haven’t had to use them so far, but we will now, so we need to know what they
+are.
+But rather than me just telling you what they are,
+why not have a look yourself?
+
+\pythonfile{print_bot.py}
+
+You should see something like this (on a 4x3 board):
+\begin{minted}{pytb}
+(1, 2)
+[['.', '.', '*', '.'], ['.', '.', '*', '.'], ['.', 'A', '.', '.']]
+Exception in bot A (<'<'>function print_bot at 0x7f61165f2e60<'>'>):
+------------------------------------------------------------
+Traceback (most recent call last):
+ File "…/snakegame/engine.py", line 132, in update_snakes
+ "Return value should be a string."
+AssertionError: Return value should be a string.
+------------------------------------------------------------
+\end{minted}
+
+Ignore all the Exception stuff, that’s just because we didn’t return one of
+\py|'L'|, \py|'U'|, \py|'D'| or \py|'R'|.
+The first line is our position: it’s a \py|tuple| of the x and y
+coordinates of our snake’s head.
+The second line is the board: it’s a list of each row in the board,
+and each row is a list of the cells in that row.
+
+Notice that if we index the board first by the y coordinate and then by the x
+coordinate, we can get the character in the board where our snake is:
+\py|board[y][x] == board[2][1] == 'A'|.
+The head of our snake is always an uppercase character in the board,
+and the rest of our body (the tail) are always lowercase characters.
+
+This is all very well, but how do we stop our bot from eating its tail?
+Well, the answer is that we need to look at each of the squares surrounding our
+snake’s head, to see if we’ll die if we move into them or not.
+
+Let’s have a look at the square to the right of our snake’s head.
+First, we need to know its coordinates: looking at
+Board~\ref{brd:right-square:normal},
+we see that if our snake is at position $(x, y)$,
+then the square on the right will be at position $(x + 1, y)$.
+
+But this isn’t the whole story: Board~\ref{brd:right-square:wrapping}
+shows that if the snake is on the rightmost column, the square on the right
+is going to wrap around to be on the leftmost column.
+
+\begin{board}
+\begin{subfigure}{.45\linewidth}
+ \begin{tabular}{l|l|l|l|l}
+ … & $x-1$ & $x$ & $x + 1$ & … \\\hline
+ $y-1$ & & & & \\\hline
+ $y$ & & $\mathbf{(x,y)}$ & $\mathbf{(x+1,y)}$ & \\\hline
+ $y+1$ & & & & \\\hline
+ … & & & & … \\
+ \end{tabular}
+\caption{Coordinate of the square to the right (ignoring wrapping).}
+\label{brd:right-square:normal}
+\end{subfigure}
+\hfill
+%
+\begin{subfigure}{.45\linewidth}
+< make_board([
+ '.....',
+ '*...A',
+ '.....',
+]) >
+\caption{%
+ The board wraps around,
+ so the square to the right of our snake $(4, 1)$
+ is the apple $(0, 1)$.
+}
+\label{brd:right-square:wrapping}
+\end{subfigure}
+
+\caption{Finding the square to the right.}
+\label{brd:right-square}
+\end{board}
+
+Fortunately for us, there’s an easy way of ‘wrapping’ around in Python,
+which is the modulo operator (\%). The modulo operator returns the
+\emph{remainder} when you divide two numbers.
+\begin{minted}{pycon}
+>>> 3 % 8
+3
+>>> 7 % 8
+7
+>>> 8 % 8
+0
+>>> 9 % 8
+1
+>>> for i in range(20):
+... print i % 8,
+...
+0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3
+\end{minted}
+
+\newcommand\mod{\,\%\,}
+
+% TODO: how do we get the width and height of the board?
+
+Looking back at Board~\ref{brd:right-square:wrapping}, we need to wrap the x
+coordinate back to $0$ when $x + 1 = width$,
+so we need $(x + 1) \mod width$.
+Taking this to a more general level, imagine we need to get the cell where
+the x coordinate is shifted by $dx$
+and the y coordinate is shifted by $dy$.
+For example, we might want to get the cell diagonally adjacent on the bottom
+left: it’s one square to the left, $dx = -1$ and one square down $dy = 1$.
+Don’t forget that moving right or down means adding
+and moving left or upwards means subtracting!
+Back to our general case, our new cell is going to be at the position
+$((x + dx) \mod width, (y + dy) \mod height)$.
+
+Don’t worry if you didn’t follow the general case there, you just need to
+remember that the cell to the right is at $((x + 1) \mod width, y)$.
+We then need to look \emph{in the board} at that position to see what’s in that
+cell.
+Remember that our board is a list of rows (stacked vertically),
+and each row is a list of cells (stacked horizontally).
+So we need to first find the right row, which we will do by using the y
+coordinate: \py|board[y]|.
+Then we need to find the right cell in the row, using the x coordinate:
+\py|board[y][(x + 1) % width]|.
+
+We’re almost at the end: all we need to do is build up a list of each cell we
+can move into. We know that we can move into cells which are
+empty (represented by a full stop)
+or have an apple (represented by an asterisk) in them,
+so we’ll test for that.
+Take a moment to write out the code we’ve managed to build so far, hopefully
+you’ll end up with something very close to what I’ve got below.
+Then you just need to add the other directions (left, up and down), and you’re
+done.
+
+\begin{pythoncode}
+from random import choice
+
+def bot(board, position):
+ x, y = position
+ height = len(board)
+ width = len(board[0])
+
+ valid_moves = []
+
+ right = board[y][(x + 1) % width]
+ if right == '.' or right == '*':
+ valid_moves.append('R')
+
+ return choice(valid_moves)
+\end{pythoncode}
+
+If you’re really stuck, or want to check your solution, here’s my solution:
+\pythonfile{random_avoid.py}
+
diff --git a/docs/random_simple.py b/docs/random_simple.py
new file mode 100644
index 0000000..f7ca03c
--- /dev/null
+++ b/docs/random_simple.py
@@ -0,0 +1,4 @@
+from random import choice
+
+def random_bot(board, position):
+ return choice('UDLR')
diff --git a/docs/random_simple.tex b/docs/random_simple.tex
new file mode 100644
index 0000000..a1ff557
--- /dev/null
+++ b/docs/random_simple.tex
@@ -0,0 +1,65 @@
+%%- from "macros.tex" import make_board -%%
+
+\section{Random Bot}
+\fasttrack{Choose a direction at random.}
+
+The next bot we’ll write is one which instead of moving in just one direction,
+chooses a direction at random to move in.
+Go on, try writing it yourself! I’ll wait here until you’re ready.
+
+Hint: check out the \texttt{random} module.
+
+Got it working? Good work!
+But you’ve probably noticed that there’s a problem:
+it doesn’t take long for our random bot to die.
+But why does it die?
+The answer is that once it eats an apple, it then has a tail, and since it
+doesn’t know any better, it will happily move into the square where its tail is.
+
+\begin{board}
+\hfill
+%
+\begin{subfigure}{.3\linewidth}
+< make_board([' *', ' A* ', ' * '])>
+\caption{Our intrepid snake heads towards an apple. Next move: \textbf{R}}
+\label{brd:random-death:1}
+\end{subfigure}
+\hfill
+%
+\begin{subfigure}{.3\linewidth}
+< make_board(['* *', ' aA ', ' * ']) >
+\caption{It has eaten the apple, and now has a tail. Next move: \textbf{L}}
+\label{brd:random-death:2}
+\end{subfigure}
+\hfill
+%
+\begin{subfigure}{.3\linewidth}
+< make_board(['* *', ' ', ' * ']) >
+\caption{It decided to move left, and ran into itself, oh no!}
+\label{brd:random-death:3}
+\end{subfigure}
+%
+\hfill
+
+\caption{The last moves of Random Bot before death.}
+\label{brd:random-death}
+\end{board}
+
+\pagebreak
+
+By the way, how long was your solution?
+If you’re still learning Python, you might like to have a peek at my solution to
+this bot, it’s only three lines long.
+Hopefully you didn’t write too much more than that!
+
+\pythonfile{random_simple.py}
+
+There are two key things that make my solution work.
+The first is the \texttt{random.choice} function,
+which returns a random item chosen from a sequence you give it.
+The second thing is that a string is a sequence:
+it is made up of the characters in it.
+So if you write \mint{python}|choice('UDLR')|,
+that’s the same as if you had written
+\mint{python}|choice(['U', 'D', 'L', 'R'])|.
+
diff --git a/docs/tutorial.tex b/docs/tutorial.tex
new file mode 100644
index 0000000..907052d
--- /dev/null
+++ b/docs/tutorial.tex
@@ -0,0 +1,61 @@
+\documentclass[12pt]{article}
+
+\usepackage{fontspec}
+
+\usepackage[pdfborder={0 0 0}]{hyperref}
+\usepackage[margin=20mm]{geometry}
+
+\usepackage{float}
+\usepackage{subcaption}
+
+\floatstyle{ruled}
+\newfloat{board}{bh}{brd}
+\floatname{board}{Board}
+\DeclareCaptionSubType{board}
+
+\usepackage{minted}
+\usemintedstyle{tango}
+
+\newmintinline[py]{python}{}
+\newminted{python}{}
+\newmintedfile{python}{}
+
+\newminted[shell]{sh}{}
+
+\setmainfont{Linux Libertine O}
+\setmonofont[AutoFakeBold]{Inconsolata}
+
+\setlength\parskip{2ex}
+\setlength\parindent{0mm}
+
+%\widowpenalty=1000
+%\clubpenalty=1000
+\newcommand\fasttrack[1]{\vspace{-2ex}\hfill\emph{Fast track: #1}\nopagebreak}
+
+\newcommand\footnoteurl[1]{\footnote{\url{#1}}}
+
+\begin{document}
+
+\title{Writing SnakeGame bots}
+\author{Peter Ward}
+\date{July 29, 2012}
+\maketitle
+
+\input{introduction.tex}
+
+\input{firstbot.tex}
+
+\input{random_simple.tex}
+
+% this section is rather long,
+% perhaps take an intermission to explain the board
+% and getting width, height
+% and modulo properly?
+\input{random_avoid.tex}
+
+\input{look_ahead.tex}
+
+\break
+\input{closest_apple.tex}
+
+\end{document}
diff --git a/oldbot.py b/oldbot.py
deleted file mode 100644
index 3bd3038..0000000
--- a/oldbot.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import subprocess
-
-class BotWrapper(object):
- def __init__(self, process):
- self.process = process
- self.__name__ = process
-
- def __call__(self, board, (x, y)):
- height = len(board)
- width = len(board[0])
-
- letter = board[y][x].lower()
-
- proc = subprocess.Popen(
- [self.process],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- )
-
- board = '\n'.join([''.join(row) for row in board])
-
- print>>proc.stdin, width, height, letter
- print>>proc.stdin, board
- proc.stdin.close()
- proc.wait()
-
- assert proc.returncode == 0, 'Snake died.'
- output = proc.stdout.read()
- return output.strip()
-
diff --git a/oldbots/peter.py b/oldbots/peter.py
deleted file mode 100755
index 72bbc95..0000000
--- a/oldbots/peter.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env python
-"""New and improved bot, OPTIMISED!!"""
-
-import random
-import sys
-
-EMPTY_TILE = '.'
-APPLE_TILE = '*'
-
-WIDTH, HEIGHT, SNAKE_BODY = raw_input().split()
-WIDTH = int(HEIGHT)
-HEIGHT = int(HEIGHT)
-
-SNAKE_BODY = SNAKE_BODY.lower()
-SNAKE_HEAD = SNAKE_BODY.upper()
-
-HEADX = None
-HEADY = None
-
-def get_cell(board, x, y):
- if x < 0 or x >= WIDTH or y < 0 or y >= HEIGHT:
- raise KeyError, 'out of range.'
- return board[y][x]
-
-BOARD = []
-for y in xrange(HEIGHT):
- row = raw_input()
- for x, char in enumerate(row):
- if char == SNAKE_HEAD:
- HEADX = x
- HEADY = y
- BOARD.append(row)
-
-md_two = {
- (-1, 0, 'l'): ((-2, 0), (-1, 1), (-1, -1)),
- (0, -1, 'u'): ((-1, -1), (1, -1), (0, -2)),
- (1, 0, 'r'): ((2, 0), (1, 1), (1, -1)),
- (0, 1, 'd'): (((0, 2), (-1, 1), (1, 1))),
-}
-
-max_score = 0
-max_moves = []
-
-for (dx, dy, move), adj in md_two.items():
- score = 0
-
- try:
- square = get_cell(BOARD, HEADX + dx, HEADY + dy)
- except KeyError:
- continue
-
- if square == APPLE_TILE:
- score += 2
- elif square != EMPTY_TILE:
- continue # Definitely cannot move here.
-
- for ddx, ddy in adj:
- try:
- square = get_cell(BOARD, HEADX + ddx, HEADY + ddy)
- except KeyError:
- score -= 1
- continue
-
- if square == APPLE_TILE:
- score += 2
- elif square == EMPTY_TILE:
- score += 1
- elif square == SNAKE_BODY:
- score -= 1
- elif square.isupper():
- score += 3
-
- if score == max_score:
- max_moves.append(move)
- elif score > max_score:
- max_score = score
- max_moves = [move]
-
-if max_moves:
- print random.choice(max_moves)
-else:
- print 'U' # Suicide!
-
diff --git a/oldbots/peter_smart.py b/oldbots/peter_smart.py
deleted file mode 100755
index b099cdd..0000000
--- a/oldbots/peter_smart.py
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env python
-"""New and improved bot, OPTIMISED!!"""
-
-import random
-import sys
-
-DEBUG = False
-
-# Show tracebacks, then pause for debugging.
-if DEBUG:
- sys_excepthook = sys.excepthook
- def excepthook(*args, **kwargs):
- sys_excepthook(*args, **kwargs)
- import time
- time.sleep(10)
- sys.excepthook = excepthook
-
-EMPTY_TILE = '.'
-APPLE_TILE = '*'
-
-WIDTH, HEIGHT, SNAKE_BODY = raw_input().split()
-WIDTH = int(WIDTH)
-HEIGHT = int(HEIGHT)
-
-SNAKE_BODY = SNAKE_BODY.lower()
-SNAKE_HEAD = SNAKE_BODY.upper()
-
-HEADX = None
-HEADY = None
-
-SNAKE_LENGTH = 0
-
-def get_cell(board, x, y):
- if x < 0 or x >= WIDTH or y < 0 or y >= HEIGHT:
- raise KeyError, 'out of range.'
- return board[y][x]
-
-BOARD = []
-for y in xrange(HEIGHT):
- row = raw_input()
- for x, char in enumerate(row):
- if char == SNAKE_HEAD:
- HEADX = x
- HEADY = y
- elif char == SNAKE_BODY:
- SNAKE_LENGTH += 1
- BOARD.append(row)
-
-MOVES = (
- (-1, 0, 'l'),
- (1, 0, 'r'),
- (0, -1, 'u'),
- (0, 1, 'd')
-)
-
-def get_score(x, y, n, done=None):
- if done is None:
- done = set()
-
- done.add((x, y))
-
- score = 0
- explore = False
-
- # See if the cell exists.
- try:
- square = get_cell(BOARD, x, y)
- except KeyError:
- return 0
-
- # Give some extra points for getting an apple.
- if square == APPLE_TILE:
- explore = True
- score += 50
-
- # Yay - it's empty!
- elif square == EMPTY_TILE:
- explore = True
- score += 10
-
- elif square.islower():
- score -= 1
-
- if explore and n > 0:
- # Explore n-1 cells further.
- for dx, dy, move in MOVES:
- nx = x + dx
- ny = y + dy
-
- if (nx, ny) in done:
- continue
-
- subscore = get_score(nx, ny, n - 1, done)
- score += subscore / 10
-
- return score * n
-
-max_score = None
-max_moves = []
-
-for dx, dy, move in MOVES:
- score = 0
-
- x = HEADX + dx
- y = HEADY + dy
-
- n = (SNAKE_LENGTH + 4) / 2
- score = get_score(x, y, n)
-
-# print 'Score for', move, '=', score
-
- # Suicide protection squad!
- try:
- square = get_cell(BOARD, x, y)
- except KeyError:
- continue
- else:
- if square not in (APPLE_TILE, EMPTY_TILE):
- continue
-
- if score == max_score:
- max_moves.append(move)
- elif max_score is None or score > max_score:
- max_score = score
- max_moves = [move]
-
-if max_moves:
- print random.choice(max_moves)
-else:
- raise Exception, "No suitable moves found!"
-
diff --git a/oldbots/peter_smart2.py b/oldbots/peter_smart2.py
deleted file mode 100755
index 8f431ca..0000000
--- a/oldbots/peter_smart2.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/usr/bin/env python
-"""New and improved bot, OPTIMISED!!"""
-
-import random
-import sys
-
-DEBUG = False
-
-# Show tracebacks, then pause for debugging.
-if DEBUG:
- sys_excepthook = sys.excepthook
- def excepthook(*args, **kwargs):
- sys_excepthook(*args, **kwargs)
- import time
- time.sleep(10)
- sys.excepthook = excepthook
-
-EMPTY_TILE = '.'
-APPLE_TILE = '*'
-
-WIDTH, HEIGHT, SNAKE_BODY = raw_input().split()
-WIDTH = int(WIDTH)
-HEIGHT = int(HEIGHT)
-
-SNAKE_BODY = SNAKE_BODY.lower()
-SNAKE_HEAD = SNAKE_BODY.upper()
-
-HEADX = None
-HEADY = None
-
-SNAKE_LENGTH = 0
-
-def get_cell(board, x, y):
- if x < 0 or x >= WIDTH or y < 0 or y >= HEIGHT:
- raise KeyError, 'out of range.'
- return board[y][x]
-
-BOARD = []
-for y in xrange(HEIGHT):
- row = raw_input()
- for x, char in enumerate(row):
- if char == SNAKE_HEAD:
- HEADX = x
- HEADY = y
- elif char == SNAKE_BODY:
- SNAKE_LENGTH += 1
- BOARD.append(row)
-
-MOVES = (
- (-1, 0, 'l'),
- (1, 0, 'r'),
- (0, -1, 'u'),
- (0, 1, 'd')
-)
-
-def get_score(x, y, n, done=None):
- if done is None:
- done = set()
-
- done.add((x, y))
-
- score = 0
- explore = False
-
- # See if the cell exists.
- try:
- square = get_cell(BOARD, x, y)
- except KeyError:
- return 0
-
- # Give some extra points for getting an apple.
- if square == APPLE_TILE:
- explore = True
- score += 100
-
- # Yay - it's empty!
- elif square == EMPTY_TILE:
- explore = True
- score += 50
-
- elif square.islower():
- score += 2
-
- elif square.isupper():
- score += 1
-
- if explore and n > 0:
- # Explore n-1 cells further.
- for dx, dy, move in MOVES:
- nx = x + dx
- ny = y + dy
-
- if (nx, ny) in done:
- continue
-
- subscore = get_score(nx, ny, n - 1, done)
- score += subscore / 10
-
- return score * n
-
-max_score = None
-max_moves = []
-
-for dx, dy, move in MOVES:
- score = 0
-
- x = HEADX + dx
- y = HEADY + dy
-
- n = (SNAKE_LENGTH + 4) / 2
- n = min([n, 10])
- score = get_score(x, y, n)
-
-# print 'Score for', move, '=', score
-
- # Suicide protection squad!
- try:
- square = get_cell(BOARD, x, y)
- except KeyError:
- continue
- else:
- if square not in (APPLE_TILE, EMPTY_TILE):
- continue
-
- if score == max_score:
- max_moves.append(move)
- elif max_score is None or score > max_score:
- max_score = score
- max_moves = [move]
-
-if max_moves:
- print random.choice(max_moves)
-else:
- raise Exception, "No suitable moves found!"
-
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 64faf9b..0000000
--- a/pygame_snake.py
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import division
-
-import time
-
-import pygame
-pygame.init()
-from pygame.locals 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__':
- from bots import *
- from oldbot import BotWrapper
-
- ROWS = 25
- COLUMNS = 25
- APPLES = 50
- game = PygameSnakeEngine(ROWS, COLUMNS, APPLES, results=True)
-
- while True:
- game.add_bot(right_bot)
- game.add_bot(random_bot)
- game.add_bot(random_bounds_bot)
- game.add_bot(random_square_bot)
- game.add_bot(BotWrapper('oldbots/peter.py'))
- game.run()
- game.new_game(ROWS, COLUMNS, APPLES)
-
- # Early window close, late process cleanup.
- pygame.display.quit()
-
-
diff --git a/pyglet_snake.py b/pyglet_snake.py
deleted file mode 100755
index 79ae047..0000000
--- a/pyglet_snake.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python
-
-from __future__ import division
-
-import time
-
-import pyglet
-pyglet.resource.path = ['images']
-pyglet.resource.reindex()
-
-from pyglet.gl import *
-
-from common import *
-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)
-
- glEnable(GL_BLEND)
- glBlendFunc(GL_SRC_ALPHA, 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.
- glLineWidth(self.EDGE_WIDTH)
- pyglet.graphics.draw(4, GL_LINE_LOOP,
- ('v2f', r),
- ('c4B', self.EDGE_COLOR * 4))
-
- # Draw the things on the square.
- if cell == 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,)
- glPolygonMode(GL_FRONT, GL_FILL)
- pyglet.graphics.draw(4, 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__':
- from bots import random_bounds_bot, random_square_bot
- from oldbot import BotWrapper
- from peter_bot import peter_bot
-
- game = PygletSnakeEngine(25, 25, 50, results=True)
-# game.add_bot(random_bounds_bot)
-# game.add_bot(random_square_bot)
- for i in xrange(0):
- game.add_bot(BotWrapper('oldbots/peter.py'))
- for i in xrange(1):
- game.add_bot(peter_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 <http://www.gnu.org/licenses/>.
-
-"""
-
-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, <max-value>) presuming no negative
- values, or (<min-value>, <max-value>) 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/setup.py b/setup.py
new file mode 100644
index 0000000..6357f28
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,22 @@
+from setuptools import setup
+
+setup(
+ name='SnakeGame',
+ version='1.0',
+ description='The game of Snake, for beginner AI bot writers.',
+ author='Peter Ward',
+ author_email='peteraward@gmail.com',
+ packages=['snakegame'],
+ zip_safe=False,
+ install_requires=[
+ 'six',
+ ],
+ package_data={
+ 'snakegame': 'images/*.png',
+ },
+ entry_points={
+ 'console_scripts': [
+ 'snakegame = snakegame:main',
+ ]
+ },
+)
diff --git a/snake.py b/snake.py
deleted file mode 100644
index 0429040..0000000
--- a/snake.py
+++ /dev/null
@@ -1,155 +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, results=False):
- super(SnakeEngine, self).__init__()
-
- 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 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/__init__.py b/snakegame/__init__.py
new file mode 100644
index 0000000..be1f7a7
--- /dev/null
+++ b/snakegame/__init__.py
@@ -0,0 +1,55 @@
+from snakegame.engine import Engine
+from snakegame.viewers import BUILTIN_VIEWERS
+
+def first(d):
+ for item in d:
+ return item
+
+def rsplit_get(s, sep, default):
+ if sep not in s:
+ return (s, default)
+ return s.rsplit(sep, 1)
+
+def import_thing(name, default_obj):
+ pkg, obj = rsplit_get(name, ':', default_obj)
+ mod = __import__(pkg, fromlist=[obj])
+ return getattr(mod, obj)
+
+def main(argv=None):
+ import argparse
+
+ parser = argparse.ArgumentParser(conflict_handler='resolve')
+ parser.add_argument(
+ '-v', '--viewer',
+ default=first(BUILTIN_VIEWERS),
+ )
+ parser.add_argument(
+ '-w', '--width',
+ default=30,
+ type=int,
+ )
+ parser.add_argument(
+ '-h', '--height',
+ default=20,
+ type=int,
+ )
+ parser.add_argument(
+ '-a', '--apples',
+ default=40,
+ type=int,
+ )
+ parser.add_argument('bot', nargs='+')
+ args = parser.parse_args(argv)
+
+ viewer_name = BUILTIN_VIEWERS.get(args.viewer, args.viewer)
+ viewer_class = import_thing(viewer_name, 'Viewer')
+
+ game = Engine(args.height, args.width, args.apples)
+
+ for name in args.bot:
+ bot = import_thing(name, 'bot')
+ game.add_bot(bot)
+
+ viewer = viewer_class(game)
+ viewer.run()
+
diff --git a/snakegame/bots/__init__.py b/snakegame/bots/__init__.py
new file mode 100644
index 0000000..c404485
--- /dev/null
+++ b/snakegame/bots/__init__.py
@@ -0,0 +1,44 @@
+from random import choice
+
+from snakegame import common
+
+def make_direction_bot(direction, human):
+ def bot(board, position):
+ return direction
+ bot.__doc__ = 'This bot always moves %s.' % human
+ return bot
+
+up_bot = make_direction_bot('U', 'up')
+down_bot = make_direction_bot('D', 'down')
+left_bot = make_direction_bot('L', 'left')
+right_bot = make_direction_bot('R', 'right')
+
+def random_bot(board, position):
+ "This bot just chooses a random direction to move."
+ return choice('UDLR')
+
+def random_avoid_bot(board, position):
+ """
+ This bot chooses a random direction to move, but will not move into a
+ square which will kill it immediately (unless it has no choice).
+ """
+ x, y = position
+
+ available = []
+ for direction, (dx, dy) in common.directions.items():
+ cell = common.get_cell(board, x + dx, y + dy)
+ if common.is_vacant(cell):
+ available.append(direction)
+
+ if not available:
+ return 'U'
+ return choice(available)
+
+BUILTIN_BOTS = {
+ 'up_bot': up_bot,
+ 'down_bot': down_bot,
+ 'left_bot': left_bot,
+ 'right_bot': right_bot,
+ 'random_bot': random_bot,
+ 'random_avoid_bot': random_avoid_bot,
+}
diff --git a/snakegame/colour.py b/snakegame/colour.py
new file mode 100644
index 0000000..f8e4aec
--- /dev/null
+++ b/snakegame/colour.py
@@ -0,0 +1,7 @@
+import hashlib
+from random import Random
+
+def hash_colour(data):
+ n = int(hashlib.md5(data.encode('utf-8')).hexdigest(), 16)
+ r = Random(n)
+ return r.randrange(256), r.randrange(256), r.randrange(256)
diff --git a/snakegame/common.py b/snakegame/common.py
new file mode 100644
index 0000000..9feb395
--- /dev/null
+++ b/snakegame/common.py
@@ -0,0 +1,87 @@
+import random
+from string import ascii_lowercase as lowercase, ascii_uppercase as uppercase
+alphabet = lowercase + uppercase
+
+directions = {
+ 'U': (0, -1),
+ 'D': (0, 1),
+ 'L': (-1, 0),
+ 'R': (1, 0),
+}
+
+EMPTY = '.'
+APPLE = '*'
+WALL = '#'
+ICE_CREAM = '+'
+SHRINK_POTION = '-'
+TELEPORTER = '?'
+
+is_empty = EMPTY.__eq__
+is_apple = APPLE.__eq__
+is_wall = WALL.__eq__
+
+def is_vacant(cell):
+ return cell in (EMPTY, APPLE, ICE_CREAM, SHRINK_POTION, TELEPORTER)
+
+def is_blocking(cell):
+ return not is_vacant(cell)
+
+def is_snake(cell):
+ return cell in alphabet
+
+def is_snake_head(cell):
+ return cell in uppercase
+
+def is_snake_body(cell):
+ return cell in lowercase
+
+def is_enemy_snake(cell, me):
+ assert me.isupper()
+ return is_snake(cell) and cell.upper() != me
+
+def is_my_snake(cell, me):
+ assert me.isupper()
+ return cell.upper() == me
+
+def get_size(board):
+ height = len(board)
+ width = len(board[0])
+ return width, height
+
+def in_bounds(x, y, width, height):
+ return (
+ x >= 0 and x < width and
+ y >= 0 and y < height
+ )
+
+def get_cell(board, x, y, wrap=True):
+ width, height = get_size(board)
+ if wrap:
+ x %= width
+ y %= height
+ elif not in_bounds(x, y, width, height):
+ return None
+ return board[y][x]
+
+def get_neighbours(x, y, width, height):
+ for d, (dx, dy) in directions.iteritems():
+ nx = (x + dx) % width
+ ny = (y + dy) % height
+ yield d, nx, ny
+
+def max_items(items, alpha=1.0):
+ """
+ >>> max_items([(1, 'a'), (2, 'b'), (2, 'c'), (0, 'd')])
+ [(2, 'b'), (2, 'c')]
+ """
+ max_score, _ = max(items)
+ return [
+ (score, item)
+ for score, item in items
+ if score >= max_score * alpha
+ ]
+
+def choose_move(choices, default='U', random=random):
+ if not choices:
+ return default
+ return random.choice(choices)
diff --git a/snakegame/engine.py b/snakegame/engine.py
new file mode 100644
index 0000000..155e9bf
--- /dev/null
+++ b/snakegame/engine.py
@@ -0,0 +1,205 @@
+from collections import defaultdict, deque
+from copy import deepcopy
+from random import Random
+from string import ascii_lowercase as lowercase
+import time
+import traceback
+
+import six
+from six.moves import xrange
+
+from snakegame.colour import hash_colour
+from snakegame import common
+
+SOFT_TIME_LIMIT = 0.5
+HARD_TIME_LIMIT = 1.0
+
+class Engine(object):
+ def __init__(
+ self,
+ rows, columns, n_apples,
+ n_ice_creams=0, n_shrink_potions=0, n_walls=0,
+ wrap=True,
+ random=None,
+ *args, **kwargs
+ ):
+ super(Engine, self).__init__(*args, **kwargs)
+
+ if random is None:
+ random = Random()
+ self.random = random
+
+ self.wrap = wrap
+ self.bots = {}
+
+ self.new_game(
+ rows, columns, n_apples,
+ n_ice_creams, n_shrink_potions, n_walls,
+ )
+
+ def get_random_position(self):
+ x = self.random.randrange(0, self.columns)
+ y = self.random.randrange(0, self.rows)
+ return (x, y)
+
+ def replace_random(self, old, new):
+ for i in xrange(self.rows * self.columns):
+ x, y = self.get_random_position()
+ if self.board[y][x] == old:
+ self.board[y][x] = new
+ return x, y
+
+ def add_items(self, item, amount):
+ for i in xrange(amount):
+ x, y = self.get_random_position()
+ self.board[y][x] = item
+
+ def shrink(self, path):
+ if len(path) > 1:
+ x, y = path.popleft()
+ self.board[y][x] = common.EMPTY
+
+ def new_game(
+ self,
+ rows, columns, n_apples,
+ n_ice_creams, n_shrink_potions, n_walls,
+ ):
+ self.game_ticks = 0
+
+ self.letters = list(lowercase)
+ self.letters.reverse()
+
+ self.rows = rows
+ self.columns = columns
+
+ self.messages_by_team = defaultdict(dict)
+
+ # make board
+ self.board = [[common.EMPTY for x in xrange(columns)] for y in xrange(rows)]
+ self.add_items(common.APPLE, n_apples)
+ self.add_items(common.ICE_CREAM, n_ice_creams)
+ self.add_items(common.SHRINK_POTION, n_shrink_potions)
+ self.add_items(common.WALL, n_walls)
+
+ def add_bot(self, bot, team=None, colour=None):
+ """
+ A bot is a callable object, with this method signature:
+ def bot_callable(
+ board=[[cell for cell in row] for row in board],
+ position=(snake_x, snake_y)
+ ):
+ return random.choice('RULD')
+
+ If team is not None, this means you will get a third parameter,
+ containing messages from the other bots on your team.
+ """
+ letter = self.letters.pop()
+
+ name = bot.__name__
+ if colour is None:
+ colour = hash_colour(name)
+
+ position = self.replace_random(common.EMPTY, letter.upper())
+ if position is None:
+ raise KeyError("Could not insert snake into the board.")
+
+ self.bots[letter] = [bot, colour, deque([position]), team]
+ return letter
+
+ def remove_bot(self, letter):
+ letter = letter.lower()
+
+ for row in self.board:
+ for x, cell in enumerate(row):
+ if cell.lower() == letter:
+ row[x] = common.EMPTY
+
+ del self.bots[letter]
+
+ def update_snakes(self):
+ self.game_ticks += 1
+
+ for letter, (bot, colour, path, team) in list(self.bots.items()):
+ board = deepcopy(self.board)
+ try:
+ x, y = path[-1]
+
+ start = time.time()
+
+ if team is None:
+ d = bot(board, (x, y))
+ else:
+ messages = self.messages_by_team[team]
+ d, message = bot(board, (x, y), messages)
+
+ assert isinstance(message, str), \
+ "Message should be a byte string, not %s (%r)." % (
+ type(message),
+ message,
+ )
+ messages[letter] = message
+
+ end = time.time()
+ delta = end - start
+ assert delta < HARD_TIME_LIMIT, 'Exceeded hard time limit.'
+ if delta >= SOFT_TIME_LIMIT:
+ print('Bot %s (%r) exceeded soft time limit.' % (letter.upper(), bot))
+
+ # Sanity checking...
+ assert isinstance(d, six.string_types), \
+ "Return value should be a string."
+ d = d.upper()
+ assert d in common.directions, "Return value should be 'U', 'D', 'L' or 'R'."
+
+ # Get new position.
+ dx, dy = common.directions[d]
+ nx = x + dx
+ ny = y + dy
+
+ if self.wrap:
+ ny %= self.rows
+ nx %= self.columns
+ else:
+ if ny < 0 or ny >= self.rows or nx < 0 or nx >= self.columns:
+ self.remove_bot(letter)
+ continue
+
+ oldcell = self.board[ny][nx]
+ if common.is_vacant(oldcell):
+ # Move snake forward.
+ self.board[ny][nx] = letter.upper()
+ path.append((nx, ny))
+ tail = path[0]
+
+ # Make old head into body.
+ self.board[y][x] = letter.lower()
+
+ if oldcell == common.APPLE:
+ path.appendleft(tail)
+ self.replace_random(common.EMPTY, common.APPLE)
+ elif oldcell == common.ICE_CREAM:
+ for i in xrange(3):
+ path.appendleft(tail)
+ self.replace_random(common.EMPTY, common.ICE_CREAM)
+ elif oldcell == common.SHRINK_POTION:
+ self.shrink(path)
+ self.replace_random(common.EMPTY, common.SHRINK_POTION)
+
+ self.shrink(path)
+
+ else:
+ self.remove_bot(letter)
+
+ except:
+ print("Exception in bot %s (%s):" % (letter.upper(), bot))
+ print('-'*60)
+ traceback.print_exc()
+ print('-'*60)
+ self.remove_bot(letter)
+
+ def __iter__(self):
+ yield self.board
+ while self.bots:
+ self.update_snakes()
+ yield self.board
+
diff --git a/snakegame/engines/__init__.py b/snakegame/engines/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/snakegame/engines/__init__.py
@@ -0,0 +1 @@
+
diff --git a/images/apple.png b/snakegame/images/apple.png
index 69a00ea..69a00ea 100644
--- a/images/apple.png
+++ b/snakegame/images/apple.png
Binary files differ
diff --git a/images/eyes.png b/snakegame/images/eyes.png
index 643158c..643158c 100644
--- a/images/eyes.png
+++ b/snakegame/images/eyes.png
Binary files differ
diff --git a/snakegame/images/icecream.png b/snakegame/images/icecream.png
new file mode 100644
index 0000000..7313223
--- /dev/null
+++ b/snakegame/images/icecream.png
Binary files differ
diff --git a/snakegame/images/shrinkpotion.png b/snakegame/images/shrinkpotion.png
new file mode 100644
index 0000000..376a994
--- /dev/null
+++ b/snakegame/images/shrinkpotion.png
Binary files differ
diff --git a/snakegame/images/wall.png b/snakegame/images/wall.png
new file mode 100644
index 0000000..c211d78
--- /dev/null
+++ b/snakegame/images/wall.png
Binary files differ
diff --git a/snakegame/utils.py b/snakegame/utils.py
new file mode 100644
index 0000000..0339518
--- /dev/null
+++ b/snakegame/utils.py
@@ -0,0 +1,20 @@
+try:
+ from collections import OrderedDict as MaybeOrderedDict
+except ImportError:
+ MaybeOrderedDict = dict
+
+def scale_aspect(source_size, target_size):
+ source_width, source_height = source_size
+ target_width, target_height = target_size
+ source_aspect = float(source_width) / source_height
+ target_aspect = float(target_width) / target_height
+ if source_aspect > target_aspect:
+ # restrict width
+ width = target_width
+ height = float(width) / source_aspect
+ else:
+ # restrict height
+ height = target_height
+ width = height * source_aspect
+ return (width, height)
+
diff --git a/snakegame/viewers/__init__.py b/snakegame/viewers/__init__.py
new file mode 100644
index 0000000..7864e39
--- /dev/null
+++ b/snakegame/viewers/__init__.py
@@ -0,0 +1,10 @@
+from snakegame.utils import MaybeOrderedDict
+
+BUILTIN_VIEWERS = MaybeOrderedDict()
+
+def add_viewer(name):
+ BUILTIN_VIEWERS[name] = 'snakegame.viewers.%s:Viewer' % name
+
+add_viewer('pyglet')
+add_viewer('pygame')
+add_viewer('curses')
diff --git a/console_snake.py b/snakegame/viewers/curses.py
index 8c327e5..f8a9602 100755..100644
--- a/console_snake.py
+++ b/snakegame/viewers/curses.py
@@ -1,17 +1,15 @@
-#!/usr/bin/env python
-
-from __future__ import division
+from __future__ import absolute_import
+import curses
import time
-import curses
+from snakegame import common
-from common import *
-from snake import SnakeEngine
+class Viewer(object):
+ def __init__(self, engine, *args, **kwargs):
+ super(Viewer, self).__init__(*args, **kwargs)
-class ConsoleSnakeEngine(SnakeEngine):
- def new_game(self, *args):
- super(ConsoleSnakeEngine, self).new_game(*args)
+ self.engine = engine
self.window = curses.initscr()
curses.start_color()
@@ -23,15 +21,15 @@ class ConsoleSnakeEngine(SnakeEngine):
self.APPLE_COLOUR = curses.color_pair(1)
self.SNAKE_COLOUR = curses.color_pair(4)
- def draw_board(self):
+ def draw_board(self, board):
# Draw grid.
- for y, row in enumerate(self.board):
+ for y, row in enumerate(board):
for x, cell in enumerate(row):
char = '.'
colour = self.EMPTY_COLOUR
# Draw the things on the square.
- if cell == Squares.APPLE:
+ if cell == common.APPLE:
char = '@'
colour = self.APPLE_COLOUR
@@ -43,32 +41,14 @@ class ConsoleSnakeEngine(SnakeEngine):
self.window.addstr(y, x, char, colour)
def run(self):
- while self.bots:
+ for board in self.engine:
# Clear the screen.
self.window.erase()
# Draw the board.
- self.draw_board()
+ self.draw_board(board)
# Update the display.
self.window.refresh()
time.sleep(0.025)
- # Let the snakes move!
- self.update_snakes()
-
-def main(*args):
- from bots import *
- from oldbot import BotWrapper
-
- game = ConsoleSnakeEngine(25, 25, 50)
- game.add_bot(right_bot)
- game.add_bot(random_bot)
- game.add_bot(random_bounds_bot)
- game.add_bot(random_square_bot)
- game.add_bot(BotWrapper('oldbots/peter.py'))
- game.run()
-
-if __name__ == '__main__':
- curses.wrapper(main)
-
diff --git a/snakegame/viewers/pygame.py b/snakegame/viewers/pygame.py
new file mode 100644
index 0000000..2a2f603
--- /dev/null
+++ b/snakegame/viewers/pygame.py
@@ -0,0 +1,138 @@
+from __future__ import absolute_import
+
+import time
+
+import pkg_resources
+
+import pygame
+from pygame.image import load
+pygame.init()
+
+from snakegame import common
+from snakegame.utils import scale_aspect
+
+sprite_cache = {}
+
+def load_sprite(filename):
+ if filename in sprite_cache:
+ return sprite_cache[filename]
+
+ f = pkg_resources.resource_stream('snakegame', filename)
+ image = load(f).convert_alpha()
+
+ sprite_cache[filename] = image
+ return image
+
+def load_image(filename, xscale, yscale):
+ image = load_sprite(filename)
+ w, h = scale_aspect(image.get_size(), (xscale, yscale))
+ return pygame.transform.smoothscale(image, (int(w), int(h)))
+
+class Viewer(object):
+ EDGE_COLOR = (255, 255, 255)
+ EDGE_WIDTH = 1
+
+ def __init__(self, engine, width=800, height=600, fullscreen=False, **kwargs):
+ super(Viewer, self).__init__(**kwargs)
+
+ self.engine = engine
+
+ flags = 0
+ if fullscreen:
+ flags |= pygame.FULLSCREEN
+ self.screen = pygame.display.set_mode((width, height), flags)
+
+ self.width = width
+ self.height = height
+
+ self.columns = None
+ self.rows = None
+
+ def on_resize(self):
+ # make board surface
+ self.board_width, self.board_height = scale_aspect(
+ (self.columns, self.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
+
+ self.items = {
+ common.APPLE : 'images/apple.png',
+ common.ICE_CREAM : 'images/icecream.png',
+ common.SHRINK_POTION : 'images/shrinkpotion.png',
+ common.WALL : 'images/wall.png',
+ }
+ for item in self.items:
+ self.items[item] = load_image(self.items[item], xscale, yscale)
+ self.eyes = load_image('images/eyes.png', xscale, yscale)
+
+ def draw_board(self, board):
+ xscale = self.board_width / self.columns
+ yscale = self.board_height / self.rows
+
+ # Draw grid.
+ for y, row in enumerate(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 = pygame.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 in self.items:
+ self.surface.blit(self.items[cell], r.topleft)
+
+ elif common.is_snake(cell):
+ bot = self.engine.bots[cell.lower()]
+ colour = bot[1]
+ self.surface.fill(colour, r)
+
+ if common.is_snake_head(cell):
+ self.surface.blit(self.eyes, r.topleft)
+
+ def run(self):
+ clock = pygame.time.Clock()
+
+ running = True
+
+ for board in self.engine:
+ columns, rows = common.get_size(board)
+ if columns != self.columns or rows != self.rows:
+ self.columns = columns
+ self.rows = rows
+ self.on_resize()
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT or \
+ (event.type == pygame.KEYDOWN and event.key == pygame.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(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)
+
+ if running:
+ time.sleep(2)
+
diff --git a/snakegame/viewers/pyglet.py b/snakegame/viewers/pyglet.py
new file mode 100644
index 0000000..c5b7e0f
--- /dev/null
+++ b/snakegame/viewers/pyglet.py
@@ -0,0 +1,127 @@
+from __future__ import absolute_import
+
+import pyglet.resource
+pyglet.resource.path.append('@snakegame')
+pyglet.resource.reindex()
+
+from pyglet import gl
+
+from snakegame import common
+from snakegame.utils import scale_aspect
+
+class Viewer(pyglet.window.Window):
+ EDGE_COLOR = (255, 255, 255, 255)
+ EDGE_WIDTH = 2
+
+ def __init__(self, engine, caption='SnakeGame Window', resizable=True, **kwargs):
+ super(Viewer, self).__init__(
+ caption=caption,
+ resizable=resizable,
+ **kwargs
+ )
+
+ self.engine = engine
+ self.engine_iter = iter(engine)
+
+ 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(), 1/30.0)
+
+ self.board = None
+ self.columns = None
+ self.rows = None
+
+ def update_snakes(self, *args):
+ self.board = next(self.engine_iter, None)
+ if self.board is None:
+ pyglet.app.exit()
+ return
+
+ columns, rows = common.get_size(self.board)
+ if columns != self.columns or rows != self.rows:
+ self.columns = columns
+ self.rows = rows
+ self.on_resize(self.width, self.height)
+
+ def on_resize(self, width, height):
+ super(Viewer, self).on_resize(width, height)
+
+ if self.board is None:
+ return
+
+ # make board surface
+ self.board_width, self.board_height = scale_aspect(
+ (self.columns, self.rows), (self.width, self.height)
+ )
+
+ # load sprites
+ xscale = float(self.board_width) / self.columns
+ yscale = float(self.board_height) / self.rows
+
+ self.images = {
+ common.APPLE : 'images/apple.png',
+ common.ICE_CREAM : 'images/icecream.png',
+ common.SHRINK_POTION : 'images/shrinkpotion.png',
+ common.WALL : 'images/wall.png',
+ }
+
+ for item, location in self.images.items():
+ image = pyglet.resource.image(location)
+ image.size = scale_aspect(
+ (image.width, image.height),
+ (xscale, yscale)
+ )
+ self.images[item] = image
+
+ self.eyes = pyglet.resource.image('images/eyes.png')
+ self.eyes.size = scale_aspect(
+ (self.eyes.width, self.eyes.height),
+ (xscale, yscale)
+ )
+
+ def on_draw(self):
+ self.clear()
+
+ if self.board is None:
+ return
+
+ xscale = float(self.board_width) / self.columns
+ yscale = float(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 in self.images:
+ image = self.images[cell]
+ w, h = image.size
+ image.blit(left + (xscale - w) / 2.0, top - h, width=w, height=h)
+
+ elif common.is_snake(cell):
+ bot = self.engine.bots[cell.lower()]
+ colour = bot[1] + (255,)
+ gl.glPolygonMode(gl.GL_FRONT, gl.GL_FILL)
+ pyglet.graphics.draw(4, gl.GL_POLYGON,
+ ('v2f', r),
+ ('c4B', colour * 4),
+ )
+
+ if common.is_snake_head(cell):
+ w, h = self.eyes.size
+ self.eyes.blit(left, top - h, width=w, height=h)
+
+ def run(self):
+ pyglet.app.run()
diff --git a/snakegame/zmq.py b/snakegame/zmq.py
new file mode 100644
index 0000000..c38c680
--- /dev/null
+++ b/snakegame/zmq.py
@@ -0,0 +1,35 @@
+from __future__ import absolute_import
+
+import pickle
+
+class Viewer(object):
+ def __init__(self, engine, sock):
+ self.sock = sock
+ self.engine = engine
+
+ def run(self):
+ for board in self.engine:
+ bots = {
+ letter: (None, colour, None, team)
+ for letter, (_, colour, _, team) in self.engine.bots.iteritems()
+ }
+
+ msg = pickle.dumps({
+ 'bots': bots,
+ 'board': board,
+ }, protocol=2)
+
+ self.sock.send(msg)
+
+class Engine(object):
+ def __init__(self, sock):
+ self.sock = sock
+
+ def __iter__(self):
+ while True:
+ if not self.sock.poll(timeout=2000):
+ break
+ msg = self.sock.recv()
+ obj = pickle.loads(msg)
+ self.bots = obj['bots']
+ yield obj['board']
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()
-