for
Game time!
Allow the user to navigate a maze by entering directions to move (n
, s
, w
, or e
). They can also enter q
to quit.
The maze should look like this, with .
representing a space the player can move into, and #
representing a wall. An @
symbol indicates the player’s current position.
Every move, the map should be displayed:
#####################
#...#...............#
#...#..........#....#
#...#..........#....#
#...#..........#....#
#...#..@.......#....#
#..............#....#
#..............#....#
#####################
Enter a move (n,s,w,e,q):
Keep this project in mind as you read through the chapter.
Problem-solving step: Understanding the Problem.
Remember how regular variables hold one thing? Well, lists are variables that can hold a lot of things.
Fun Lists Fact: Most other languages have a different name for lists: they call them “arrays”. Same thing.
But wait—if a list can hold a lot of things, how do we differentiate? How do I tell Python that I want the second thing in the list? Or the fifth thing?
Luckily it’s easy enough; we just have to specify the index into the list that holds the thing we want.
Think of it as a row of postboxes, numbered starting from 0
, then 1
, then 2
, and going on up to however many postboxes we have. Each postbox can hold a thing, and you can refer to it by giving the postbox number.
Let’s take a look at a simple example:
= [10, 3, 7, 9] x
There’s a list. We know it’s a list because of the square brackets around it. It’s a list of four integers.
Let’s print out the zeroth element in the list. We do this by using square brackets after the list variable name and giving the index inside those brackets. Does this look familiar? It’s the same syntax we used to get individual characters out of strings!
print(x[0]) # prints 10
and the element at index 2:
print(x[2]) # prints 7
Do you remember that strings had a cool trick where you could use a negative index to refer to characters from the end of the string? We can do the same thing with lists!
print(x[-1]) # prints 9, the element at the end of the list
Remember that indexes start at zero, again, just like with strings.
Keeping with the “things you can also do with strings” theme, you can also slice an array, just like a string.
= [1, 2, 3, 4, 5]
a
= a[1:-1] # Slice all but the first and last elements
b
print(b) # [2, 3, 4]
You can also set individual elements, leaving the rest of the list unchanged:
1] = 99
x[
print(x[1]) # now prints 99
This brings us to a stark difference between lists and strings: lists are mutable. You can change individual elements inside the list without creating a new list! Remember with strings, you couldn’t change them—you could only make new ones.
It’s such a key difference, we’re going to talk about it in detail now, and then again later. This is a big source of confusion among new developers.
Do you remember way back in the variables chapter when I was talking about how variable assignment worked? Revisit it if you have to.
And you were wondering what that had to do with anything?
Well, we’re going to check it out again, but this time in the context of lists.
We just mentioned that lists are different than integers in that lists are mutable. We can change them. This has some interesting repercussions.
Let’s look at strings versus lists.
= "Hello"
x = x y
In that example, as we learned earlier, there is one string "Hello"
in memory, and both x
and y
refer to it.
Strings are immutable. So you can’t do something like this:
= "Hello"
x = x
y
# Change character at index 2 to 'Z'
2] = "Z" # ERROR! Strings are immutable--you can't change them x[
But lists are mutable. We can change something that’s in a list. Let’s do an analogous example:
= ["A", "B", "C", "D"]
x = x
y
# Change string at index 2 to 'Z'
2] = "Z" # Works
x[
print(x[2]) # "Z"
print(y[2]) # "Z" also!!!
Wait—what happened there? We changed the value in the second index of x
, but it also changed it in y
! How did that happen?
Remember: it’s because when we did:
= ["A", "B", "C", "D"]
x = x y
both x
and y
came to point to the same list. There is only one list. Both x
and y
refer to it. So if you change that one list, you see that change reflected in both x
and y
.
(If you could change a string, it would work the same way. But you can’t because it’s immutable.)
Mutable types include the following (some of which we haven’t talked about yet):
In some languages, types that appear to get copied on assignment (like strings and integers) are called value types. Whereas types that can be referred to by multiple variables through assignment (like lists) are called reference types. Python doesn’t make this distinction, although you might hear this phraseology used in the wild.
What if you want a copy of a list, and not just a copy of the reference? You can force a list copy a number of ways, but these are three common ones:
= a.copy() # Copy with .copy() method
b = list(a) # Copy with the list() function
b = a[:] # Copy by slicing the entire list b
Even if you don’t have it quite down yet, don’t worry. We’ll hit this topic a few more times as we progress.
for
and Lists—Powerful StuffProblem-solving step: Understanding the Problem.
Remember our good friend the for
loop? We used it with range
to loop a number of times, and we used it with strings to loop over each character in the string.
We can also use it with lists55 to do things with each list element in order!
Here’s a simple example that prints all the elements in a list:
= [11, 55, 33, 99]
x
for i in x:
print(f"element is: {i}")
and this will output:
element is: 11
element is: 55
element is: 33 element is: 99
But wait, there’s more!
Recall from above that you can get the element out of a list if you know its index, for example, x[2]
.
Let’s put those together in another way to use for
and lists. This example does the same thing as the one above, just in a different way:
= [11, 55, 33, 99]
x
for i in range(4):
print(f"element is: {x[i]}")
Although that’s not idiomatic Python56 (the first example is better), it demonstrates how to use a variable as the index. We refer to x[i]
inside the loop, and then have i
change to loop over every element’s index.
It’s irking me that we have that hard-coded 4
in the range()
. It only works for lists of length 4
. Let’s see if we can fix it.
Sneak preview: you can get the number of elements in a list with len()
.
Let’s make the range()
go up to “the length of the list” instead of to 4
:
= [11, 55, 33, 99]
x
for i in range(len(x)): # <---
print(f"element is: {x[i]}")
And now that works for lists of any length—much better!
for
and enumerate()
Problem-solving step: Understanding the Problem.
In the examples above, we used for
with the elements in the list themselves, and also with range()
over the indexes of the elements in the list.
What if you want to do both at the same time? That is, you want the elements and you want the indexes?
A function that’s worthy of mention is enumerate()
. It will iterate through each element in the list, returning the element and its index. You can get them both at the same time!
= [11, 55, 33, 99]
x
for i, v in enumerate(x):
print(f"The element at index {i} has value {v}")
This results in:
The element at index 0 has value 11
The element at index 1 has value 55
The element at index 2 has value 33 The element at index 3 has value 99
Problem-solving step: Understanding the Problem.
Let’s write some code that takes a list and goes through all the elements in that list. If an element is even, we should multiply the value by 2
. If it’s odd, we should do nothing with it.
For example, if the input list is:
1, 2, 3, 4, 5, 6] [
after processing and doubling all the even values, it will be:
1, 4, 3, 8, 5, 12] [
Problem-solving step: Devising a Plan.
We know we need to iterate over the list, so that sounds like a job for a for
-loop. And we need to test if a number is even or odd, which sounds like a job for a if
statement.
How can we tell if a number is odd?
There’s a very common way to do this. Divide by
2
and take the remainder. If the remainder is1
, it’s odd. If it’s0
, it’s even.Do you remember how to take the remainder in Python? With the modulo operator:
%
.= 12 x if x % 2 == 0: print("x is even!") else: print("x is odd!")
So our plan is shaping up like this:
for each element in the list:
if that element is even: double the value and store it at the same place in the list
Then maybe print it out at the end, just for fun.
Problem-solving step: Carrying out the Plan.
Here’s each line of the plan’s pseudocode converted to Python57:
x = [1, 2, 3, 4, 5, 6]
# for each element in the list
for i, v in enumerate(x):
# if that element is even
if v % 2 == 0: # check if v is even
# double the value and store it at the same place in the list
x[i] = v * 2
print(x) # Print it out, just for fun
And the output:
[1, 4, 3, 8, 5, 12]
Voila! There it is!
Problem-solving step: Looking Back.
Anything you could make better about this?
Instead of using enumerate()
, you could have used for
-range()
-len()
like we did in an earlier example. Would you have felt that produced cleaner code?
(Remember: coding is creative. There are a lot of solutions. It’s up to you as a dev to make informed decisions about which methods you like more than others!)
One interesting thing to note is that on line 14, we just printed the entire list in one go. You can do that! print()
prints out the string version of whatever you pass in. More on that in the future.
There are several useful built-in functions and methods58 that you can use with lists. Some we’ve already seen.
In the following table, the variable a
represents a list.
Function | Description |
---|---|
len(a) |
Return the number of elements in the list |
enumerate(a) |
Iterate over index/value pairs in the list |
a.append(x) |
Append variable x to the end of the list |
a.clear() |
Clear all elements from the list |
a.copy() |
Make a copy of the list |
a.count(v) |
Count the number of occurrences of v in the list |
a.extend(b) |
Add elements of list b to end of list a |
a.index(v) |
Return the first index of v in list a |
a.insert(i,v) |
Insert v in list a before index i |
a.pop() |
Remove and return the last element in a |
a.pop(i) |
Remove and return the element at index i in a |
a.reverse() |
Reverse the elements in the list |
a.sort() |
Sort the list |
Let’s just fire up the editor and start messing around with these to see how they work.
Here’s a program called listops.py
59 that does just that. You should also experiment with variations of these to get a feel for them:
a = [5, 2, 8, 4, 7, 4, 0, 9]
print(len(a)) # 8, the number of elements in the list
a.append(100)
print(a) # [5, 2, 8, 4, 7, 4, 0, 9, 100]
print(a.count(4)) # 2, the number of 4s in the list
print(a.index(4)) # 3, the index of the first 4 in the list
v = a.pop() # Remove the 100 from the end of the list
print(v) # 100
print(a) # [5, 2, 8, 4, 7, 4, 0, 9]
a.reverse() # Reverse the list
print(a) # [9, 0, 4, 7, 4, 8, 2, 5]
a.insert(2, 999) # insert 999 before index 2
print(a) # [9, 0, 999, 4, 7, 4, 8, 2, 5]
b = [1, 2, 3]
a.extend(b) # Add contents of b to end of a
print(a) # [9, 0, 999, 4, 7, 4, 8, 2, 5, 1, 2, 3]
a.sort() # Sort all elements
print(a) # [0, 1, 2, 2, 3, 4, 4, 5, 7, 8, 9, 999]
a.clear() # Remove all elements
print(a) # [], an empty list of length 0
In addition to those functions, the +
operator will take two lists and concatenate them together into a third list:
= [1, 2, 3]
a = [4, 5, 6]
b
= a + b # c refers to a new list [1, 2, 3, 4, 5, 6]
c
# lists a and b are unchanged
Look at the amount of control we have over lists now! Not only can you read and write values at specific list indexes, but you can add to the end, insert stuff in the middle, remove from the end, or from anywhere within the list.
You are All Powerful!
Okay, maybe not, but at least you can do a thing or two with lists.
“So I can become some kind of list ninja. What good does that do me?”
When it comes to anything in programming, it’s often helpful to try to think of physical, actual real-life examples. What are some of the lists you have in real life? You can use Python lists to store those.
Shopping lists, bowling scores, favorite rocks, employee names, numbers, etc., etc.
= [
shopping_list "spam",
"eggs",
"bacon",
"sausage",
"spam",
"spam"
]
Sometimes we know those lists upfront, and other times we compute them as we go.
Problem-solving step: Understanding the Problem.
More math! [Groan!] Let’s compute the Fibonacci Sequence! [Yay!]
The Fibonacci Sequence is a famous mathematical sequence (a progression of related numbers) that runs like this:
\(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...\)
Before I give it away, study it a bit—can you see the pattern?
Lots of times, the job of a dev is to find patterns in data so that you can write code that generates them.
Spoiler alert!
Each number is the sum of the previous two numbers.
\(0+1=1\), \(1+1=2\), \(1+2=3\), etc.
But what about the first two numbers in the sequence
Those are given to be \(0\) and \(1\), no questions asked. This way you always have at least two previous numbers to get the next one.
What we want to do is write a program that builds and prints a list containing the first 100 Fibonacci numbers. We don’t want to do any of the addition ourselves—we want the code to compute it for us.
Problem-solving step: Make A Plan.
We’re going to need a list to hold all the numbers.
We have the first two numbers (\(0\) and \(1\)), so we can put those in the list.
Then we have to look at the previous two numbers from the end, add them together, and then append the sum to the end of the list.
And we have to do that 98 more times to get 100 numbers.
Doing something 98 times seems like a for
-range()
loop to me.
We can append with the .append()
method.
We can get the last and previous-to-last elements in the list with negative list indexes.
initialize the list with [0, 1]
for 98 times:
compute the sum of the previous two numbers
append sum to the list
print the list
Time to code it up!
Problem-solving step: Carrying out the Plan.
# initialize the list with [0, 1]
fib = [0, 1]
# for 98 times
for _ in range(98):
# compute the sum of the previous two numbers
s = fib[-1] + fib[-2]
# append sum to the list
fib.append(s)
# print the list
print(fib)
And you’ll get some output that looks vaguely like this (I’ve rewrapped the output here—yours might not be so pretty):
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597,
2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418,
317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465,
14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296,
433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976,
7778742049, 12586269025, 20365011074, 32951280099, 53316291173,
86267571272, 139583862445, 225851433717, 365435296162, 591286729879,
956722026041, 1548008755920, 2504730781961, 4052739537881,
6557470319842, 10610209857723, 17167680177565, 27777890035288,
44945570212853, 72723460248141, 117669030460994, 190392490709135,
308061521170129, 498454011879264, 806515533049393, 1304969544928657,
2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464,
14472334024676221, 23416728348467685, 37889062373143906,
61305790721611591, 99194853094755497, 160500643816367088,
259695496911122585, 420196140727489673, 679891637638612258,
1100087778366101931, 1779979416004714189, 2880067194370816120,
4660046610375530309, 7540113804746346429, 12200160415121876738,
19740274219868223167, 31940434634990099905, 51680708854858323072, 83621143489848422977, 135301852344706746049, 218922995834555169026]
Problem-solving step: Looking Back.
Notice how the list grows bigger and bigger with each step of the loop. If we were to print out the list for every iteration of the loop, we’d see something like this:
[0, 1]
[0, 1, 1]
[0, 1, 1, 2]
[0, 1, 1, 2, 3]
[0, 1, 1, 2, 3, 5]
[0, 1, 1, 2, 3, 5, 8] [0, 1, 1, 2, 3, 5, 8, 13]
and so on. We’re constructing the list as we go, building it from the previous elements61.
Another thing I did in the for
loop was use _
as the looping variable name. That’s a perfectly legitimate variable name62.
By convention, _
is used as a name when you don’t intend to use it elsewhere.
You must have a variable named in your for
loop—no option not to. But using _
for that name indicates to programmers that you are just using the loop to count, and you don’t actually care what the count is.
Problem-solving step: Understanding the Problem.
We’ve already seen how to initialize a list with a few elements in it:
= [11, 55, 33, 99] a
You can multiply a list by a constant value to get a new list repeated that many times.
What?
Easier demonstrated:
= [11, 99]
a = a * 3
b
print(b) # [11, 99, 11, 99, 11, 99]
It just repeats the list that many times into a new list.
A very common use of this is to create a new list of a certain number of elements, initialize to zero:
= [0] * 10
a print(a) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
While we’re on about esoteric list declarations, you can declare a list of no elements like so”
= [] a
presumably to compute values for later.
Problem-solving step: Understanding the Problem.
This is a really neat language feature of Python. It allows you to construct a new list from an old one, modifying and filtering elements as you go.
Before going into the syntax, we’ll write something using what we know so far with for
and if
and see how that looks. Then we’ll compare it to a list comprehension version.
I’m going to write some code that takes a list and produces a new list from it, not including all the odd numbers, and replacing the even numbers with the even number times 4.
= [1, 2, 3, 4, 5, 6]
a = []
new_list
for v in a:
if v % 2 == 0: # if v is even
* 4)
new_list.append(v
print(new_list) # [8, 16, 24]
Study that until you’re sure you have it down.
Now, here’s that same program as a list comprehension:
= [1, 2, 3, 4, 5, 6]
a = [v * 4 for v in a if v % 2 == 0]
new_list
print(new_list) # [8, 16, 24]
Well, it’s certainly more concise!
But it’s also, I’m suspecting you’re finding, a lot harder to read. Yeah.
It’s always like this when you try to pick up a new piece of weird syntax you’ve never seen before. At first, it’s all weird, but the more you study it, the more used to it you get.
What I don’t want is for you to look at it and think, “It’s unreadable! Forget it!”
All you have to do is take it a step at a time.
What do we have in that line?
# result loop filter
# |----| |--------| |-----------|
= [v * 4 for v in a if v % 2 == 0] new_list
It’s split into three parts.
The “result”: this is what values will be included in the output list. The variable named here is the one in the “loop” clause.
The “loop”: assigns elements from the list into a variable, repeatedly.
The “filter” (optional): only elements for which the condition is true will be included in the output.
If we wanted to include only the odd numbers times 4, we could have done this:
# result loop filter
# |----| |--------| |-----------|
= [v * 4 for v in a if v % 2 == 1] new_list
Or the odd numbers divided by 3:
# result loop filter
# |----| |--------| |-----------|
= [v / 3 for v in a if v % 2 == 1] new_list
Leaving the filter off makes the list unconditionally. For example, all the numbers in the list times 4:
# result loop
# |----| |--------|
= [v * 4 for v in a] new_list
When should I use them?
Any time you’re making a new list from an existing one and you want to optionally change or filter elements from the original list, list comprehensions are a great tool to us.
Problem-solving step: Understanding the Problem.
You can have lists of just about anything in Python. Lists of numbers, lists of strings, lists of lists…
That’s right, folks. Lists containing other lists. How does that work?
Here’s one example where we’ll make a bunch of lists, and then put them in another list.
= [1, 2, 3]
x = [4, 5, 6]
y = [x, y] z
It’s important to note that when we assign into
z
, we’re not copying listsx
andy
. What we’re doing is making it so that the values in listz
refer to the same lists asx
andy
do.In other words, there’s only one list in memory with the values
[1,2,3]
. And is referred to by bothx
andz[0]
. Both variables reference the same list.
In that example, how could we access elements of the lists-in-list?
We’re going to use square bracket notation again, but even more so.
= [1, 2, 3]
x = [4, 5, 6]
y = [x, y]
z
print(z[0]) # z[0] refers to x, so this prints [1, 2, 3]
print(z[1]) # z[1] refers to y, so this prints [4. 5. 6]
So far so good?
Here’s the thing to notice: since z[0]
and z[1]
are lists, you can access the elements within those lists by using square bracket notation again.
Let’s try to get the number 6
out of that second list.
print(z[1][2]) # prints 6
Take apart that line of code. What’s happening there?
First Python evaluates the first square brackets it comes to. It knows z
is a list, and so it evaluates z[1]
to get the list [4, 5, 6]
. Then it takes that list and evaluates it with [2]
, getting the 6
out of it.
Another way to think of it would be to imagine using parentheses:
1])[2] # first evaluate z[1], then evaluate [2] on the result
(z[
1][2] # this is equivalent to the previous line z[
(While Python doesn’t mind if you use parentheses like this, programmers don’t do it since it makes the code look messy.)
But what does this buy us? Lists of lists are exciting and all. (Right?) What are they useful for?
Having a list of lists literally adds a second dimension to the data you can represent. With a single list, you can represent one “row” of data. With a list of lists, you can represent multiple rows or a grid of data.
What are some places in computing a grid or multiple rows of data are used?
Spreadsheets! What else? See if any other ideas come to mind.
For declaring lists of lists, it’s really common to just declare them all at once, and not use an intermediate variable to represent the sublists.
For example, the previous list we were using, above, could be declared more simply like so:
= [
z 1, 2, 3],
[4, 5, 6]
[ ]
or, if it looks better and your style guide allows it, you can put it on one line:
= [ [1, 2, 3], [4, 5, 6] ]
z
print(z[0][1]) # Prints 2
Now let’s see if we can put it all together for this chapter’s project!
This is the big one. This project is going to draw on multiple things we’ve learned so far and put them all together into a working solution.
That makes this project more difficult than the previous ones. We’re going to break down the problem and decide what tools we know that we can bring to bear to solve it.
And this is what being a software developer is all about.
I’m not expecting the answer to be obvious. You’ll rarely see a problem that has an obvious solution, even as a seasoned developer. But we do have our problem-solving framework to break down the problem into workable parts. So let’s do it!
Problem-solving step: Understanding the Problem.
We want to do several things with this project:
What’s missing from the spec that we need to know?
Remember your compass directions?
N
|
W --+-- E
| S
Now’s the time to get the answers to questions clarified. Much easier to do it now than after you’ve coded up the wrong thing!
What if the user provides invalid input? What happens then is missing from the spec. For that, let’s print an error message:
Unknown command: {x}
Where x
is whatever the user entered.
What if the player tries to move through a wall? Let’s use this error message:
You can't go that way.
Anything else missing from the spec?
Problem-solving step: Make A Plan.
We’re going to make use of two important techniques as we make this plan: simplify the problem and breaking down subproblems.
Simplifying the problem means taking our eventual goal and remove requirements to make it easier to code. Sure, eventually we’ll have to add those requirements back in, but simplifying the problem makes the initial coding easier.
What are examples of things we can simplify about this project? Here are some ideas:
@
on the map where the player isq
Again, we’ll have to add this eventually, but removing them temporarily makes it much easier to reach an initial version.
The other technique, breaking down subproblems, is a variant of what we’ve been doing already.
Let’s start with high-level pseudocode, and then break it down where required.
while not quit:
print map and player indicator
get input
make sure input is valid make sure we're not moving through walls
How do we know that breaking down subproblems will be useful with this pseudocode? The first clue is that some of the steps are substantial, e.g. “print map and player indicator” immediately brings to mind the question, “How the heck can we do that?”
If any steps are too complex or are unclear, it means you have to break them down further. Let’s do that for all the unclear sections:
while not quit:
print map and player indicator
for each row of the map:
for each column of the map:
if this is where the player is:
print @
else:
print the map character
get input
make sure input is valid
if input invalid:
print error message
elif input is "q":
quit
else:
figure out the new row and column of the player
if the map at the new player position is "#":
print "You can't go that way."
else: set the current position to the new position
That’s significantly better. It’ll be a lot easier to translate to Python.
Now… how are we going to store the data we need for this project? And what data do we need, anyway?
How are we going to store the map? In the spec, it’s displayed as text, like so:
#####################
#...#...............#
#...#..........#....#
#...#..........#....#
#...#..........#....#
#...#..........#....#
#..............#....#
#..............#....# #####################
where #
is a wall and .
is an empty floor (that we can move through).
We could store all that as a single string… but that might make our lives a little more difficult since we have to put an @
in where the player is located.
And, because of that, we’re planning to print the map out a character at at a time so we can decide if we’re going to draw an @
or the map character.
What would be a more sensible way to store the map rather than a single big string?
Ponder that for a second.
Spoiler alert!
How about a list of strings? One string would be one row of the map. Then we could go through the single row a character at a time and decide what to print. (Remember we can use array bracket notation on a string to get single characters out!)
Look at all the techniques we’re using!
for
for
for
loopsif
conditions to decide what to do with user inputif
conditions to determine if we can move that directionwhile
loop to run the game until the user quitsHoly moly! That’s a lot of stuff. But that’s what we do as software developers: we take all we know and figure out how to put it together into the solution.
And it’s rarely an obvious one. We all have to work hard to come up with the answers.
Problem-solving step: Carrying out the Plan.
Before we start this phase, I want you to notice how much time we’ve spent on the Understand and Plan phases without writing any code at all. It’s very tempting, especially for junior devs, to want to jump into the code without spending sufficient time on Understanding and Planning. Unfortunately, this practice causes one to waste productivity unnecessarily.
You’re not done understanding the problem until you have no more questions about.
You’re not done making a plan until you know how to convert every step of the plan to code.
If you spend enough time understanding and planning, coding almost becomes an afterthought.
And here we are. Let’s take our pseudocode and convert it into Python.
Coming back to simplify the problem, let’s start by just storing and printing the map. No player, no input, no loop. Let’s just get that working.
First of all, we need to store the map data, so let’s do that:
# The map
map_data = [
"#####################",
"#...#...............#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#..............#....#",
"#..............#....#",
"#####################"
]
We’ve split the map list into multiple lines to make it easier to read.
Now we need to print it out. In our pseudocode, we used a nested for
loop with if
conditions.
# The map
map_data = [
"#####################",
"#...#...............#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#..............#....#",
"#..............#....#",
"#####################"
]
# Print map and player indicator
for row in map_data: # for each row
for map_character in row: # for each col
print(map_character, end="")
print()
There are a couple of things to note there, so make sure to digest the code. We’re going through each row, and for each row, we’re going through each column and printing the character.
We want the characters to all print on the same line for a given row, so we use the end=""
trick to keep Python from going to the next line.
And at the end of each row, we have an empty print()
to get the cursor down to the next line for starting to print the next row.
And when we run that, we get the map printed out!
But we don’t have the player position stored anywhere, and we’re not showing it on the screen. Let’s add that next.
# The map
map_data = [
"#####################",
"#...#...............#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#..............#....#",
"#..............#....#",
"#####################"
]
# Player position
player_row = 4 # <-- add player position
player_column = 9
# Print map and player indicator
# Use enumerate() to get the row and column indexes for the if:
for row_index, row in enumerate(map_data): # for each row
for col_index, map_character in enumerate(row): # for each col
if row_index == player_row and col_index == player_column:
print("@", end="") # end="" no newline
else:
print(map_character, end="")
print()
So there we’ve added a couple of variables to store where the player is, and then in the map printing loop, we check to see if this location is where the player is. If it is, print an @
, otherwise print the map character.
For the next small thing to add, let’s get user input and quit if the user enters “q
”. Otherwise, we’ll print the map again in a loop.
# The map
map_data = [
"#####################",
"#...#...............#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#..............#....#",
"#..............#....#",
"#####################"
]
# Player position
player_row = 4
player_column = 9
quit = False
while not quit:
# Print map and player indicator
for row_index, row in enumerate(map_data): # for each row
for col_index, map_character in enumerate(row): # for each col
if row_index == player_row and col_index == player_column:
print("@", end="") # end="" no newline
else:
print(map_character, end="")
print()
# Get input
command = input("Enter a move (n,s,w,e,q): ")
if command == "q":
quit = True
continue # jump right back to the top of the while
else:
print(f'Unknown command {command}')
Getting there!
Something new to note! There’s a continue
statement on line 40. This causes program execution to jump back to the top of the while
loop, ignoring the rest of the loop body. It means, “Don’t do anything else in this block—just short circuit back to the while
condition. (Which tests to false immediately and exits the loop.)
So now we have the player position being printed, and we have the user inputting a command. However, we still need to handle the directional commands and actually move the player around.
We plan to compute the new position for the player based on the current position and the user input. For example, if the user goes north (up) on the screen, the player’s column stays the same, but the row number decreases by 1.
# The map
map_data = [
"#####################",
"#...#...............#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#...#..........#....#",
"#..............#....#",
"#..............#....#",
"#####################"
]
# Player position
player_row = 4
player_column = 9
quit = False
while not quit:
# Print map and player indicator
for row_index, row in enumerate(map_data): # for each row
for col_index, map_character in enumerate(row): # for each col
if row_index == player_row and col_index == player_column:
print("@", end="") # end="" no newline
else:
print(map_character, end="")
print()
# Get input
command = input("Enter a move (n,s,w,e,q): ")
# Figure out the new row and column of the player
# Make sure input is valid
if command == "n":
new_row = player_row - 1
new_column = player_column
elif command == "s":
new_row = player_row + 1
new_column = player_column
elif command == "w":
new_row = player_row
new_column = player_column - 1
elif command == "e":
new_row = player_row
new_column = player_column + 1
elif command == "q":
quit = True
continue # jump right back to the top of the while
else:
print(f'Unknown command {command}')
# Set the current position to the new position
player_row = new_row
player_column = new_column
That’s working great, but we can still walk through the walls. Let’s change those last few lines of the program to verify that the new position is an empty room before we move the player in there. (Note the line numbers!)
if map_data[new_row][new_column] != ".":
print("You can't move that way!")
else:
# Set the current position to the new position
player_row = new_row
player_column = new_column
Woo! You’ve written your very own Roguelike63 game!
Problem-solving step: Looking Back.
For the next steps, consider adding some of the following:
Just back to “Understand the Problem” and implement some of those things.
Also, the game looks neater if you clear the screen before printing the map, but unfortunately there’s no easy way to do this in a cross-platform manner64. But there is a hacky thing we can do.
If your terminal obeys ANSI escape codes65, which is likely, we can send special sequences of characters to it to clear the screen then home the cursor (move it to the top left).
The magical incantation looks like this:
print("\x1b[2J\x1b[H", end="") # Clear the screen
Go ahead and tuck that up above where you start printing the map and you’ll see the effect. If your terminal doesn’t support ANSI sequences, you’ll just see some weird characters. Bogus66.
If you really want to get into character graphics, there’s a library you should try: curses67. It allows you to clear the screen, position the cursor, get input without echoing it to the screen or waiting for RETURN
, output in color, and more.
Although we have enough knowledge to add monsters and treasure and so on, it will be easier to do so once we learn about dictionaries and objects in future chapters. We have more tools at our disposal!
Remember: to get your value out of this book, you have to do these exercises. After 20 minutes of being stuck on a problem, you’re allowed to look at the solution.
Use any knowledge you have to solve these, not only what you learned in this chapter.
Always use the four problem-solving steps to solve these problems: understand the problem, devise a plan, carry it out, look back to see what you could have done better.
The following code prints out 99
:
How can we change only line 2 so that b
is a copy of a
, causing the program to print out 1
instead?
Write a loop that prints out the total sum of the following list:
14, 31, 44, 46, 54, 59, 45, 55, 21, 11, 8, 34, 66, 41] [
The sum is 529.
Take the following list:
11, 22, 33] [
and write one line of Python that changes the list to:
11, 22, 33, 99] [
Then write another line that changes that list to:
11, 33, 99] [
Then, finally, write another line that changes the list to:
11, 33, 88, 99] [
This exercise should manipulate the same list, not create new lists.
Create the following list in under 20 characters of Python code:
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3] [
Using a list comprehension, make a new list from the following that only includes numbers that are multiples of 5:
14, 31, 44, 46, 54, 59, 45, 55, 21, 11, 8, 34, 66, 41] [
Using a list comprehension, make a new list from the following that only includes all-uppercase versions of all words that begin with a consonant.
"alice", "beej", "chris", "dave", "eve", "frank"] [
Sample output:
['BEEJ', 'CHRIS', 'DAVE', 'FRANK']
Write a program that generates a list of lists (2D list) containing a multiplication table up to \(12\times12\).
You should be able to print the result of, say, \(7\times5\) like so:
print(multtable[7][5]) # prints 35
Look at all the stuff we’ve covered in this chapter!
Lists are a powerful tool to add to our arsenal. We’re going to make heavy use of them as our programs get more complex.