Here are our goals for today:
1. Finish the game!
This means we need a list of features we want to implement, and a list of bugs we need to fix. Then, we 'freeze' these lists and only work on adding the named features and fixing the named bugs.
There is some problem where the robots move on top of a player. Also, robots should turn to stone if they hit each other. For this let's re-work the logic of moveRobot().
def moveRobot(self, x, y): # The robot wants to move towards the player. dx = self.px - x dy = self.py - y if dx > 0: dx = 1 if dx < 0: dx = -1 if dy > 0: dy = 1 if dy < 0: dy = -1 if dx != 0 and dy != 0: xory = random.randint(1,2) if xory == 1: dx = 0 else: dy = 0 at = self.gameMap[y+dy][x+dx] if at == ' ': at = 'R' elif at == 'R' or 'r': at = '*' elif at == '@': self.killPlayer() self.gameMap[y][x] = ' ' self.gameMap[y+dy][x+dx] = at
Not shown is sn elif which says if a robot moves on to a * it becomes a *.
This logic is a bit cleaner and allows for more expansion later.
When leveling, the robots would all instantly die. We traced this to a bug where in the initialization of the map, a '' (nothing) was being added instead of a ' ' (space). We could fix this in the moveRobots logic, but, it really should be fixed in the initialization. The map area must 'exist', and rules must be applied and relied on. It's not the function of moveRobot() to check map sanity.
This means that when we were searching the map, we were doing it in the wrong place. Here is the new code:
def moveRobots(self): # 1. Find and move every robot. for x in range(self.gameW): for y in range(self.gameH): if self.gameMap[y][x] == 'r': self.moveRobot(x,y) # 2. repair the map for x in range(self.gameW): for y in range(self.gameH): if self.gameMap[y][x] == 'R': self.gameMap[y][x] = 'r' if self.countRobots() == 0: self.level = self.level + 1 self.makeLevel(self.level) def countRobots(self): robots = 0 for x in range(self.gameW): for y in range(self.gameH): if self.gameMap[y][x] == 'r': robots = robots + 1 return robots
As you can see, moveRobots() has been updated to be a bit more logical, and an explicit check for robots has been designed. This could be made more efficient but it would require mixing the logic of countRobots into something unrelated, which would make the code more difficult to understand. It's not a good practice so we will avoid refactoring this code prematurely.
Before we deal with the teleport bug, let's discuss the new code for leveling and then the teleport command itself. If the bug is not obvious by the end of this, we'll explain it then.
class Game: def __init__(self, window): self.window = window self.screen = window.screen self.logo = window.logo self.font = window.font # Clear the screen. self.screen.fill((0, 0, 0)) # Set up game variables self.running = True # Set up level 1. self.level = 1 self.makeLevel(self.level) def makeLevel(self, level): # create map self.gameW = 60 self.gameH = 21 self.gameMap = [[' ' for _ in range(self.gameW)] for _ in range(self.gameH)] # add walls for x in range(self.gameW): self.gameMap[0][x] = '#' self.gameMap[self.gameH-1][x] = '#' for y in range(self.gameH): self.gameMap[y][0] = '#' self.gameMap[y][self.gameW-1] = '#' # put player in a random place. self.px = int(self.gameW / 2) # random.randint(1, self.gameW-2) self.py = int(self.gameH / 2) # random.randint(1, self.gameH-2) # Add rocks. numRocks = int(level / 2) + 1 for x in range(numRocks): rx = random.randint(1, self.gameW-2) ry = random.randint(1, self.gameH-2) self.gameMap[ry][rx] = '*' # Add Robots numRobots = level for x in range(numRobots): rx = random.randint(1, self.gameW-2) ry = random.randint(1, self.gameH-2) self.gameMap[ry][rx] = 'r'
So, essentially, all of the “board setup” and map initialization features are moved into a newLevel() function. Simple concept. This is called after the check in moveRobots().
What's the teleport bug? The teleport bug might have shown up earlier as a rock appearing in the middle of a wall. Then, a -2 would be added ex. (self.gameW-2) as a bound for randomly determining the rock's position. But the teleport bug is especially onerous as it could cause the player to teleport directly onto a stone or a robot. Thus we must make a new function which finds a free spot on the board, and use this instead of blindly picking a random map space.
First add this to the keyboard handler checkEvents():
elif event.key == pygame.K_t: self.teleportPlayer() elif event.key == pygame.K_q: print("Game quit on level " + str(self.level)) quit()
We threw in a quit command for free. Now we'll add the teleport function as an example of what to do.
def teleportPlayer(self): (self.px, self.py) = self.findFreeSpace()
Actually, as you can see, the code is more compact and probably a bit more orderly or easy to read than getting two random numbers. In every case where we must add something randomly to the map, we must use a call to findFreeSpace() or there is a chance it will be randomly placed on a pre-existing addition.
Let's look at findFreeSpace() now:
# Note that this function can lock if there are no free spaces! # We don't check for this because long before this could happen # a situation will be created where the player will always die after # his first move. def findFreeSpace(self): ok = False while ok == False: rx = random.randint(1, self.gameW-2) # 1 and -2 are wall bounds ry = random.randint(1, self.gameH-2) # so we dont land on a wall if self.gameMap[ry][rx] == ' ': ok = True return (rx, ry)
At this point the game is essentially finished. There is no point in a save or load game function, athough one could theoretically be added. This will be left as an exercise for the reader, if interested. In such a case one must decide between only saving the level or saving the entire map state as well.