diff options
50 files changed, 1478 insertions, 2410 deletions
@@ -1,2 +1,9 @@ -glob:*.pyc -glob:*.orig +syntax: glob +*~ +*.py[co] + +docs/build +docs/tutorial.pdf + +pyglet +*_bot.py @@ -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 Binary files differindex 69a00ea..69a00ea 100644 --- a/images/apple.png +++ b/snakegame/images/apple.png diff --git a/images/eyes.png b/snakegame/images/eyes.png Binary files differindex 643158c..643158c 100644 --- a/images/eyes.png +++ b/snakegame/images/eyes.png diff --git a/snakegame/images/icecream.png b/snakegame/images/icecream.png Binary files differnew file mode 100644 index 0000000..7313223 --- /dev/null +++ b/snakegame/images/icecream.png diff --git a/snakegame/images/shrinkpotion.png b/snakegame/images/shrinkpotion.png Binary files differnew file mode 100644 index 0000000..376a994 --- /dev/null +++ b/snakegame/images/shrinkpotion.png diff --git a/snakegame/images/wall.png b/snakegame/images/wall.png Binary files differnew file mode 100644 index 0000000..c211d78 --- /dev/null +++ b/snakegame/images/wall.png 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() - |