diff options
Diffstat (limited to 'docs')
-rw-r--r-- | docs/Makefile | 27 | ||||
-rw-r--r-- | docs/closest_apple.py | 60 | ||||
-rw-r--r-- | docs/closest_apple.tex | 13 | ||||
-rw-r--r-- | docs/firstbot.py | 2 | ||||
-rw-r--r-- | docs/firstbot.tex | 81 | ||||
-rw-r--r-- | docs/introduction.tex | 61 | ||||
-rwxr-xr-x | docs/jinja2 | 51 | ||||
-rw-r--r-- | docs/look_ahead.tex | 9 | ||||
-rw-r--r-- | docs/macros.tex | 19 | ||||
-rw-r--r-- | docs/print_bot.py | 3 | ||||
-rw-r--r-- | docs/random_avoid.py | 26 | ||||
-rw-r--r-- | docs/random_avoid.tex | 167 | ||||
-rw-r--r-- | docs/random_simple.py | 4 | ||||
-rw-r--r-- | docs/random_simple.tex | 65 | ||||
-rw-r--r-- | docs/tutorial.tex | 61 |
15 files changed, 649 insertions, 0 deletions
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} |