Home

A Simple Python Game

I was helping out Quazipseudo with Geodesic Chess, and he's been learning to program. Very wisely, he said: "Since I learn from doing, trial and error, much more quickly than from reading, I'd like you to walk me through writing a simple script to move a piece around a square 3 x 3 board." While this isn't exactly what he asked for, here's a walk-through on how to build a small game in python.

Before We Begin

I'll write this tutorial such that when I'm about to write code, I'll tell you what the code will do and then you should try and write it yourself. While I have included my implementation, ideally you should try to write it BEFORE looking at my code. BUT, as with all languages (spoken and computer) reading is easier than writing - so if you get stuck there is no shame in looking at the answers. In fact, it may be worth having a look at the complete code (at the end of this email), or skim reading this whole post before starting.

Specifications

If you're up for a challenge, program the whole thing just after looking at these specifications. I'll walk you through it, but this is a summary of what task you have and what you need to do.

Good Coding Notes

There are two programs, one is called 'pycodestyle' and the other is 'pylint.' These help you two write code that is consistent. They have rules like: "All variable names should be in snake_case and longer than three letters" and "indents should be four spaces." I very strongly suggest that you use these programs. I have mine set up the text editor 'Geany' such that I press F8 and they tell me what is wrong with my code. Treat this like a spellcheck. You wouldn't send a formal message or write a report without running a spellchecker on it, so with code, you should use a style checker.

One of the rules they enforce is called 'docsctrings.' Pretty much it's to try and get you to comment your code. Commenting your code is a bit of an art form. Doing something like: a += 3 # Adding 3 to variable a is not useful to anyone with eyes. Instead you want comments that tell you why: a += 3 # There is a three character offset around an equals sign, # eg a = 5 has three characters between the a and the 5 Obviously that is very contrived, so perhaps a better example is some from one of my projects. Have a look at some of the comments in thisexcessively documented. While I don't expect you to document that much, you'll quickly find that good comments are invaluable when trying to understand someone's code. Things to particularly comment on are:

In theory, comment before or while your write the code. Keeping it neat from the beginning is much easier than tidying at the end.

Overall Design

A game runs in steps, called an 'event loop.' In most games this is something quite simple, being the process of: and thus, we arrive at the basic outline for our game: while(??): ???? = get_user_input() ???? = move_player(????) display_board(????) Obviously we need a little more than that. We need to figure out what information goes where, and of what form it is For example what is the 'player input' that is returned by get_user_input. Is it the string 'n', 's', 'e', 'w' or is it a list of x and y: , , , ? Does the player input need to include a 'q' for quit? What we are figuring out here is the program's data flow. How about this: done = False while(not done): done, movement = get_user_input() player_position = move_player(board, movement, player_position) display_board(board, player_position, exit_position) if player_position == exit_position: done = True Now we need to figure out what a board is and what a position is and what's in each function.

Starting to Program

==Positions How do you represent a position in a game? Our board will be a 2D grid, and hence we have an X co-ordinate and a Y co-ordinate. Let's put them in a list: position = [x, y] At some point we're going to want to move the player, so we need to be able to add something to a position. Let's just say that a velocity (a change in position) is the same as a position, and figure out a way to add positions together.

What we want: new_position = add_positions(old_position, velocity) print(add_positions([0,0], [1,0]) == [1,0]) print(add_positions([1,1], [1,0]) == [2,1]) print(add_positions([1,0], [-1,0]) == [0,0]) Have a go at implementing the add_positions function. Make sure that your code does not edit the old position or the velocity that get passed in. Otherwise this would fail: print(add_positions((0,0), (1,0)) == [1,0]) And some other things would be confusing. So make sure you try the above test case as well.

Sample Implementation: def add_positions(old_position, velocity): '''Adds a velocity to a position and returns the new position of the object.''' x = old_position[0] + velocity[0] y = old_position[1] + velocity[1] return [x,y]

The Board

Let's go on and implement the board. It's a 2D grid. So let's keep it really simple: A list of lists where 0 is free space and 1 is an obstacle board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] Why did I pick this? Because we can check what is at a given location in the map simply by asking: board[5][3] Which will give us the value of the piece five from the top, three across. Is this how you'd expect? I'd expect it to be five across, three up. (Like a graph, X axis along the bottom, Y axis vertically on the left, (X, Y). Rather than make it confusing, let's wrap some things up to give us this behaviour.

What we want: value = get_point(board, position) board = [ [1, 2], [3, 4] ] print(get_point([[0]], [0, 0]) == 0) print(get_point(board, [0, 0]) == 3) print(get_point(board, [0, 1]) == 1) print(get_point(board, [0, -5]) is None) # I'd rather it returned None than an error Sample Implementation: def get_point(board, position): '''Returns the value of a point in the board, indexed like a graph''' y = (len(board) - 1) - position[1] # Index from bottom x = position[0] # Already indexed from left # Check that the Y value is within the size of the board and get # the row if y < 0 or y >= len(board): return None row = board[y] # Check that the X value is within the size of the board and get # the value within the row if x < 0 or x >= len(row): return None return row[x]

Displaying Things:

Let's write the function display_board. I'll step you through this one a bit more than the previous ones as it's probably the hardest in this whole mini-project. display_board(board, player_position, exit_position). Where do we start? Well, obviously we'll have to make use of our get_point function and iterate through all the points in the map: board = [ [1, 2], [3, 4] ] for x in range(len(board)): for y in range(len(board[x])): print(get_point(board, [x,y])) seems like a good start. But what did it give us? It gave me: 3 1 4 2 That's not quite what we're after is it? First off it's in the wrong order, and second they're all on separate lines! The reason for the first issue is similar as to why we had to write a get_point function. When we print, we always print from the top left, but point 0,0 is the bottom left. So we need to 'undo' what we did in get_point. We also need to switch the x and y order because we need to go across the row before we go on to the next one. The second point we can solve by using print("string", end=""), which tells print not to add a newline. So our new code looks like: for inv_y in range(len(board)): y = (len(board) - 1) - inv_y # To index from bottom rather than top for x in range(len(board[y])): print(get_point(board, [x,y]), end='') print("") Which prints, as we want: 12 34 I'll leave you to figure out how to convert that into the symbols that the specifications asked for.

Test Cases: board = [ [1, 1, 1], [0, 0, 0], [1, 1, 1], ] display_board(board, [2,1], [0,1]) [][][] >< :) [][][] display_board(board, [2,2], [0,0]) [][]:) ><[][] display_board(board, None, None) # So we don't always have to display them [][][] [][][] Sample Implementation: def display_board(board, player_position, exit_position): '''Displays a board and two special positions: the player and the exit''' for inv_y in range(len(board)): y = (len(board) - 1) - inv_y # To index from bottom rather than top for x in range(len(board[y])): # Before we display the map, let's check to see if any of the # special positions are there. If they are, then we should display # the over the top of the map if [x,y] == player_position: print(":)", end="") elif [x,y] == exit_position: print("><", end="") else: # Grab the map and turn the 0 or 1 into spaces or brackets map_point = get_point(board, [x,y]) if map_point == 0: print(" ", end="") elif map_point == 1: print("[]", end ="") print("") # Go on to the next line

Milestone 1:

Have you got all three of those functions working? Let's chain them together. Try running this: import time import random board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1, 0, 1], [1, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] # Starting position and movement direction pos = [1,3] direction = [1,0] while(1): # Check for collisions. If there are none, continue moving if get_point(board, add_positions(pos, direction)) == 0: pos = add_positions(pos, direction) else: # If you hit something, pick a new direction direction = random.choice([[1,0], [-1,0], [0,1], [0,-1]]) print("\n"*50) # Cheapo way of clearing the screen: print lots of lines! display_board(board, pos, None) time.sleep(0.1) # Restrict the framerate so you can actually see things

Getting Player Input:

Let's focus on some of the other functions now - namely the player input. Python has a built in function 'input()' which get's a typed string from the user's input - you will need to press the enter key though. Have a play with the input function now. (If you haven't as part of your other courses)

We want to build a bit of abstraction around it to turn it into a 'direction' (ie one of , , , ). If you remember from our overall plan at the top, we had: exit, movement = get_user_input() Whereby exit being True would stop the game.

We want to convert 'n' into [0,1], 's' into [0,-1], 'e' into [1,0], 'w' into [-1,0] and 'q' should have a direction of [0,0] but set exit to True. Make it so that invalid input neither exits nor moves a player (False, [0,0]) Try it yourself now.

Sample Implementations: What you probably did was: def get_user_input(): '''Returns the desired direction of the player, and whether to quit or not''' raw = input("(nsewq):") if raw == 'n': return False, [0,1] if raw == 's': return False, [0,-1] if raw == 'e': return False, [1,0] if raw == 'w': return False, [-1,0] if raw == '1': return True, [0,0] return False, [0,0] Which is an adequate solution. However, a slightly 'neater' one is to use a dictionary: def get_user_input(): '''Returns the desired direction of the player, and whether to quit or not''' raw = input("(nsewq):") OUTPUTS = { 'n': [False, [0,1]], 's': [False, [0,-1]], 'e': [False, [1,0]], 'w': [False, [-1,0]], 'q': [True, [0,0]], } return OUTPUTS.get(raw, [False, [0,0]])

Milestone 2:

We're getting there! Now we should be able to assemble a way of moving around a map: board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1, 0, 1], [1, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] done = False player_position = [1,1] while not done: done, movement = get_user_input() player_position = add_positions(player_position, movement) print("\n"*50) display_board(board, player_position, None) However, we can still go through things. So.....

Moving the Player

The final function we have to write is move_player(board, movement, player_position). It needs to check if the player can move into a certain space before moving them there. This should hopefully be quite easy. To test this, use your previous functions and the framework: board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1, 0, 1], [1, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] done = False player_position = [1,1] exit_position = [8,8] while(not done): done, movement = get_user_input() player_position = move_player(board, movement, player_position) print("\n"*50) display_board(board, player_position, exit_position) if player_position == exit_position: print("You Made It") done = True Sample Implementation def move_player(board, movement, player_position): '''Moves a player, not allowing motion if it will collide with the board''' next_step = add_positions(player_position, movement) if get_point(board, next_step) == 0: return next_step return player_position

Complete Code

With luck, you managed to make a complete game! If you didn't, let me know where you got stuck. Here's the complete code that I wrote: def add_positions(old_position, velocity): '''Adds a velocity to a position and returns the new position of the object.''' x = old_position[0] + velocity[0] y = old_position[1] + velocity[1] return [x, y] def get_point(board, position): '''Returns the value of a point in the board, indexed like a graph''' y = (len(board) - 1) - position[1] # Index from bottom x = position[0] # Already indexed from left # Check that the Y value is within the size of the board and get # the row if y < 0 or y >= len(board): return None row = board[y] # Check that the X value is within the size of the board and get # the value within the row if x < 0 or x >= len(row): return None return row[x] def display_board(board, player_position, exit_position): '''Displays a board and two special positions: the player and the exit''' for inv_y in range(len(board)): y = (len(board) - 1) - inv_y # To index from bottom rather than top for x in range(len(board[y])): # Before we display the map, let's check to see if any of the # special positions are there. If they are, then we should display # the over the top of the map if [x, y] == player_position: print(":)", end="") elif [x, y] == exit_position: print("><", end="") else: # Grab the map and turn the 0 or 1 into spaces or brackets map_point = get_point(board, [x, y]) if map_point == 0: print(" ", end="") elif map_point == 1: print("[]", end="") print("") # Go on to the next line def get_user_input(): '''Returns the desired direction of the player, and wether to quit or not''' raw = input("(nsewq):") outputs = { 'n': [False, [0, 1]], 's': [False, [0, -1]], 'e': [False, [1, 0]], 'w': [False, [-1, 0]], 'q': [True, [0, 0]], } return OUTPUTS.get(outputs, [False, [0, 0]]) def move_player(board, movement, player_position): '''Moves a player, not allowing motion if it will collide with the board''' next_step = add_positions(player_position, movement) if get_point(board, next_step) == 0: return next_step return player_position board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1, 0, 1], [1, 1, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] done = False player_position = [1, 1] exit_position = [8, 8] while not done: done, movement = get_user_input() player_position = move_player(board, movement, player_position) print("\n"*50) display_board(board, player_position, exit_position) if player_position == exit_position: print("You Made It") done = True

Extensions:

Even if you couldn't complete the project, grab the final code and have a go at some of these. Reading and modifing existing code is often easier than writing from scratch, and is how I learned python.