Created
October 10, 2025 09:45
-
-
Save timcowlishaw/a7a193fba4b28df7da9db3891915c415 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # We're going to make an adventure game! | |
| # In the game, you navigate through a maze of rooms, by typing in "north", "south", "east", or "west". | |
| # In each room you enter, the computer will describe your surroundings, and then ask you what direction | |
| # you want to go in next. | |
| # If you haven't already done so, run this program in the PYTHON INTERPRETER to see how the gameplay works. | |
| # So, how to put all this together? | |
| # First, we need some descriptions for our rooms. | |
| # Let's define them, using VARIABLES to give them each a name, and making each room a DICTIONARY, containing the description. | |
| # Using a dictionary here might seem a bit superfluous, given it's only got one entry (the description) - we could | |
| # just not bother with the dictionary, and make each variable just contain the string describing the room. | |
| # However, wrapping this in a dictionary is a useful move for when we want to extend our game - If we wanted | |
| # to keep some other information about each room here (Such as a list of characters or objects that live there), | |
| # we can just extend the dictionary to contain it. | |
| chapel = { | |
| "description": "a dusty chapel, with broken windows through which the wind howls and the rain spits.", | |
| } | |
| passageway = { | |
| "description": "a dingy, dimly lit passageway. you can hear the sound of dripping water faintly in the distance", | |
| } | |
| fork = { | |
| "description": "a fork in the passageway, un-signposted.", | |
| } | |
| knight = { | |
| "description": "a small candlelit chamber, containing a suit of armour", | |
| } | |
| dragon = { | |
| "description": "A large, cavernous chamber from which heat radiates - its walls illuminated with the yellow glow of a fire, against which, in silhouette, you see a sleeping dragon" | |
| } | |
| # Once we've done that, we need to describe the layout of the maze. This is like a two dimensional | |
| #grid (from north to south, and from east to west). | |
| # We can represent that with a list of lists (the outer list represents the steps from north to south, | |
| #and the inner lists represent the steps from west to east) | |
| # Here, you can use the variables we defined earlier, in order to place each room in the right place in the grid. | |
| # Note, you can repeat rooms if you want - like our "passageway" example which appears multiple times | |
| # connecting different rooms. "None", which you see here is a special value in python - it represents an absence, | |
| # nothing. When we include None here, it represents a part of the map that the player can't access. | |
| map = [ | |
| [None, None, dragon, passageway, fork, knight ], | |
| [None, None, None, None, passageway, None ], | |
| [None, None, None, passageway, passageway, None ], | |
| [None, None, None, passageway, None, None ], | |
| [None, None, None, chapel, None, None ] | |
| ] | |
| # We also need to define in which room the player starts their adventure. This is a coordinate - two | |
| # numbers, one representing which ROW of the map they're in (their position from north to south), | |
| # and the other representing which COLUMN (their position from west to east). | |
| # A coordinate like this we can represent as a TUPLE. | |
| # Tuples are very much like lists, except we normally use them for collections that have a fixed, known | |
| # length, ahead of time. For isnstance, a coordinate in our game will ALWAYS be two numbers, not one or three, or seventeen, | |
| # or whatever, so it makes sense to use a tuple rather than a list. | |
| # In python, when we count positions in a list or tuple, we count from 0, instead of from 1, so | |
| # the player will start in the 4th column of the 5th row - the chapel. | |
| # Once again, we're going to wrap this position in a dictionary, representing the state of our player, | |
| # for the same reason as before. If later on we wanted to allow the player to pick up and keep objects they find, | |
| # for instance, or keep an accumulated score, or number of lives, we'd need a place to store it, and this allows | |
| # us to do that. | |
| player = { | |
| "position": (4, 3) | |
| } | |
| # We've now set up everything we need to describe our maze! Each room has a description, the map | |
| # describes what room is at what position, and we know which room the player starts in. | |
| # We could change everything above this point and get a completely different maze, and a completely | |
| # different story! Everything below here can remain the same. | |
| # So, your first challenge, is to make your own game, based on this structure. Edit the variables above, | |
| # the map and the starting position, to tell a different story, and describe a different world. | |
| # When you're done, we can read on, but we're going to do it slightly out of order, for reasons that | |
| # will become obvious shortly. Once you've got to this point, scroll down the file until you find a | |
| # comment which starts with the double arrow "repeat" emoji: ๐, | |
| # ********************************** STOP READING HERE AND FOLLOW THE EMOJI! ************************ | |
| # ๐ ๏ธ THIS IS WHERE WE DEFINE OUR FUNCTIONS! | |
| # Ok, welcome back, sorry about that diversion. Maybe now you're getting an idea about why it was | |
| # necessary - we wanted to read our program starting "from the outside, in", working out what our MAIN | |
| # loop does, and then getting into the details later. | |
| # However, our main loop takes advantage of a bunch of FUNCTIONS, much like len, input, and print, that we've | |
| # already met, but which seem to be kinda specific to the game we're writing! It seems unlikely that python comes | |
| # with a built in room_at function, for instance, for folks wanting to build a text adventure game. So, where | |
| # do these functions come from? The answer is, we define them ourselves, here in the file! | |
| # We're gonna talk more about this next week, but for now, I just want you to note that we can use the special | |
| # 'def' keyword to define functions to use later on in our program. | |
| # This is the reaason I had to send you to the bottom of the file first, in order to read the main loop of the program: | |
| # Our main loop has to come *after* our function definitions, as python interprets the file from top to | |
| # bottom, and our functions have to be defined BEFORE they gets used! | |
| # | |
| # This is an example of where our intution of a program being "a bunch of statements that are executed in order, | |
| # from top to bottom" falls down a bit, so we need to be alert. | |
| # Anyway, on with the game logic. | |
| # There were three places in the main loop where we called a function to help us with some specific part of the | |
| # game logic. | |
| # | |
| # - We called 'room_at', to be able to tell what room a player is in, based on his position, so we can print out | |
| # the right description. | |
| # | |
| # - we called 'move', to update the player's position when they move - giving them new coordinates in response to they | |
| # direction that they specify.' | |
| # | |
| # - we called 'neighbours', to check the player's position, tell them which directions they are allowed to move in. | |
| # - we're also going to define a function which tells us whether or not the player has reached the edge | |
| # of the map, for reasons that will become clear extremely soon. | |
| # | |
| # Each of these are simple, self-contained things, so we're going to define them as FUNCTIONS | |
| # that we can use later on. | |
| # First, a function which tells us which room is at a given point on the map. | |
| # It takes a coordinate as an ARGUMENT, and RETURNS the room, or None, if there's | |
| # nothing on the map at that point. | |
| def room_at(coords): | |
| return map[coords[0]][coords[1]] | |
| # Now, a function which checks whether we're about to go off the edge of the map or not. | |
| # It takes the current coordinates of the player, and the direction that they are travelling in, | |
| # and returns a BOOLEAN - True if it's safe for them to move in that direction, and False if not. | |
| def check_edge(coords, direction): | |
| if direction == "north": | |
| return coords[0] > 0 | |
| if direction == "south": | |
| return coords[0] < len(map) - 1 | |
| if direction == "west": | |
| return coords[1] > 0 | |
| if direction == "east": | |
| return coords[1] < len(map[0]) - 1 | |
| # Next, Another function which updates the player's position as they move. Like the previous one, | |
| # it takes the current position of the player, and the direction they're travelling in as an argument, | |
| # then returns their NEW position after they've moved. | |
| def move(position, direction): | |
| if direction == "north": | |
| return (position[0] - 1, position[1]) | |
| if direction == "south": | |
| return (position[0] + 1, position[1]) | |
| if direction == "west": | |
| return (position[0], position[1] - 1) | |
| if direction == "east": | |
| return (position[0], position[1] + 1) | |
| # Finally we need a function which tells us in which direction a player is allowed to move, based on their current | |
| # position. This takes their current position and checks the square of the map in every direction, to see if there | |
| # is something there or not, then returns a list of the allowed directions. Note that this USES the three functions | |
| # we defined previously: check_edge, room_at, and move. This is an extremely common strategy in programming - you | |
| # break down a tricky problem into smaller simpler, parts, then combine them together to make it easier to solve. | |
| def neighbours(coord): | |
| directions = [] | |
| for direction in ["north", "south", "east", "west"]: | |
| if check_edge(coord, direction): | |
| room = room_at(move(coord, direction)) | |
| if room is not None: | |
| directions.append(direction) | |
| return directions | |
| # ๐ THIS IS THE MAIN PROGRAM LOOP! | |
| # This is the main loop of the program, it's what gets run when we execute this file with python. | |
| # Why do we call it a "main loop"? Because it loops around forever, until the player quits. | |
| # This is because we've used the "while" keyword below, and passed it "True" as the condition. | |
| while True: | |
| # First, we store the position of the player in a variable, as we'll use it a lot below, | |
| # this makes everything that follows much more readable | |
| position = player["position"] | |
| # Then we get the room that they are in currently | |
| room = room_at(position) | |
| # We print out the description of the room they are in | |
| print("You are in " + room["description"]) | |
| # Then we get the list of possible directions they can travel in. | |
| directions = neighbours(position) | |
| # We print out the list of directions: | |
| print("You can go: " + ", ".join(directions)) | |
| # Then we ask them which way they want to go. | |
| direction = input("Which way?> ") | |
| # We check that the direction they gave us is a possible direction they can move in: | |
| if direction in directions: | |
| # If it is, we update the player with the new position | |
| player["position"] = move(position, direction) | |
| else: | |
| # If not, we print a warning, and we don't update their position | |
| print("You can't go " + direction) | |
| # Finally we print a divider and some space to help make it easier for them to read | |
| # through the story | |
| print("\n---\n") | |
| # At the end of the loop, we go back to the start and do it agin. | |
| # If you're reading through this for the first time you might be thinking - "hang on, you're cheating | |
| # a bit here - you haven't explained anything about how these room_at, neighbours, and move functions | |
| # actually work, and where they come from!", and you'd be entirely right. this isn't some magic functionality | |
| # for making text games that comes included with python - we need to define all these things themselves! | |
| # However, it didn't really make sense to start getting into the detail of how each of these things work, | |
| # until we'd seen, in broad strokes, how the game was going to behave (which we've just done, reading through | |
| # the main loop). Now we've done that, we're ready to dig into the detail - so return above to the comment that | |
| # says "๐ ๏ธ THIS IS WHERE WE DEFINE OUR FUNCTIONS", and continue from there. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment