Introducing OOP in a Python game

以Python游戏介绍面向对象程序设计

In 2015 a 9-year-old boy with limited English said he’d written down for me some questions in Chinese about computer programming. I got as far as reading the first two and they meant something like “why do we have to write def __init__ after class” and “why do we need import”. It turned out he was trying to work through a pygame example in one of those “how-to” books that gives you code without really explaining what it does (and it didn’t help that he’d loaded Python 3 while the book was using Python 2). Below is my attempt to explain in Chinese and simple English. Before we go anywhere near pygame, I’d like to show what classes and modules actually *do*, using a text adventure game as an example and trying not to “waffle” too much in between. I assume Python 3, and I recommend Thonny 3 if you don’t already have a Python setup. Beware Thonny 4 contains a pro-Ukraine message which might get you in trouble in some countries.

House Adventure 1

想想一个冒险游戏,你站在一栋房子而可以向北、南、东或西走。我们用英文字母n=北、s=南、e=东、w=西。

房子冒险游戏一

Think of a game where you are in a house and can go north, south, east or west (we’ll write n, s, e and w for short).

Look at this program:

请看看以下个程序

# Start on the landing 开始是楼梯平台
room = "landing"
# Repeat until we're shut down 再做直到关机
while True:
    # Describe the player's current room.
    # 描述玩家的当前房间
    if room == "bathroom": # 浴室
        print ("You are in a bathroom.")
        print ("You can go: n.")
    if room == "landing": # 楼梯平台
        print ("You are on a landing.")
        print ("You can go: s or e or w.")
    if room == "bedroom 1": # 卧室 1
        print ("You are in a bedroom.")
        print ("You can go: w.")
    if room == "bedroom 2": # 卧室 2
        print ("You are in a bedroom.")
        print ("You can go: e.")
    # Find out what the player wants to do
    # 问一问玩家的决定
    what = input("What now? ")
    # Depending on what the player typed,
    # go to a new room.
    # 按照玩家的决定,去新的房间
    if room=="bathroom" and what=="n":
        room = "landing"
    elif room=="landing" and what=="s":
        room = "bathroom"
    elif room=="landing" and what=="e":
        room = "bedroom 1"
    elif room=="landing" and what=="w":
        room = "bedroom 2"
    elif room=="bedroom 1" and what=="w":
        room = "landing"
    elif room=="bedroom 2" and what=="e":
        room = "landing"
    else: print ("You can't do that now.")

Questions:

1. 在楼梯平台的北面,加第三个卧室。

Add a 3rd bedroom to the north of the landing.

问题

How many more lines did you have to write?

应该写多少附加的程序线?

In how many places did you need to change the program?

应该在多少地方改变程序?

Did you check it’s possible to go into *and back out from* the new bedroom?

你是不是检查玩家的确能够进的去新卧室,而且也能够从新卧室出来?

2. In this program, “bedroom 1” is mentioned 3 times. What will happen if, on the second time, you accidentally type “badroom 1” instead of “bedroom 1”?

这个程序三次说“bedroom 1”。假设第二次你打错“badroom 1”(坏房间)而不是“bedroom 1”(卧室),这个错在游戏里有什么后果?

3. What will happen if you accidentally typed “v” instead of “s” when putting in the bathroom?

假设你安装浴室时把南的s打错为v,这个错在游戏里有什么后果?

4. Imagine your friend is helping you make this game, and he wants to put a cupboard to the south of the landing. He doesn’t notice there’s already a bathroom there. He adds his cupboard anyway. What will happen?

假如一个朋友帮助你编写这个程序,但他没留意楼梯平台的南边已经有浴室。他试试在楼梯平台的南边加个橱柜房间。什么会发生?

5. Think about how hard it would be to have more than 1 player in the game.

游戏目前只有一位玩家,假设你想加其他玩家,想一想那样做是不是很难。

6. Think about how hard it would be to put things in the rooms that you can pick up and carry.

假设你想加可带的物体,想一想那是不是更难。

House Adventure 2

以下是同一个游戏,但程序好一点。试试了解。

Here is the same game, but programmed a bit better. Try to understand it.

房子冒险游戏二

class Room: # 房间类
    def __init__(new_room, description): # 新的房间, 描述
        # This is the set-up code for a new Room.
        # 这是新房间物体的开办程序
        new_room.description = description
        new_room.doors = {} # let's start with no doors
    def connect(this_room, direction, other_room):
        # This is code to add a door between two rooms.
        # 这是连同两个房间的程序
        assert not direction in this_room.doors
        assert not opposite(direction) in other_room.doors
        this_room.doors[direction] = other_room
        other_room.doors[opposite(direction)] = this_room
    def __str__(this_room):
        # This is code to describe a room.  We use the
        # special name __str__ so we can just 'print' the room.
        return this_room.description + "\n" + \
            "You can go: " + \
            " or ".join(this_room.doors)
def opposite(direction): # 计算方向的对面
    return {"n":"s", "s":"n",
            "e":"w", "w":"e"} [direction]
def init():
    # Some set-up code for the house map:
    # 房子地图的开办程序
    bathroom = Room("You are in a bathroom.")
    bedroom1 = Room("You are in a bedroom.")
    bedroom2 = Room("You are in a bedroom.")
    landing = Room("You are on a landing.")
    landing.connect('s', bathroom)
    landing.connect('e', bedroom1)
    landing.connect('w', bedroom2)
    return landing # (see start_room below)
class Player: # 玩家类
    def __init__(new_player, first_room):
        # This is the set-up code for a new Player.
        # 这是新家类物体的开办程序
        new_player.in_room = first_room
    def have_a_turn(this_player): # 轮流
        print (this_player.in_room) # 说目前的房间
        what = input("What now? ")
        if what in this_player.in_room.doors:
            this_player.in_room = this_player.in_room.doors[what]
        else: print ("You can't do that now.")
start_room = init()
player1 = Player (start_room)
while True:
    player1.have_a_turn()

Questions:

1. 在楼梯平台的北面,再次加第三个卧室。

Add a 3rd bedroom to the north of the landing again.

问题

How many more lines did you have to write this time? Was it more or fewer than last time?

这次应该写多少附加的程序线? 比上次多或少?

In how many places did you need to change the program? Was it more or fewer?

这次应该在多少地方改变程序? 比上次多或少?

If you’re making a very big house with 100 rooms, would you prefer to use House Adventure 1 or House Adventure 2? Why?

要是你做一百房间大的公馆,你比较喜欢采用《房子冒险游戏1》或《房子冒险游戏2》的程序? 为什么?

2. What happens now if you type badroom1 instead of bedroom1 on *one* of the two lines that say it?

假设有一次你打错”badroom 1”而不是”bedroom 1”,这个错这次有什么后果?

Why are you now more likely to notice the mistake?

你为什么现在更可能马上留意这个错误?

3. What happens now if you accidentally type “v” instead of “s” when connecting the bathroom?

假设你连接浴室时把南的s打错为v,这个错这次有什么后果?

Why are you now more likely to notice the mistake?

你为什么现在更可能马上留意这个错误?

4. What happens now if your friend connects a cupboard to the south of the landing?

假如一个朋友试试在楼梯平台的南边连接个橱柜房间,这次有什么后果?

5. Why is it good if the computer can help us to notice our mistakes more quickly?

电脑帮我们更快的留意我们哪里错了为什么是好事?

6. Change the program so there are 2 players. Don’t forget to print whose turn it is (player 1 or player 2). If you like, you can ask for their names at the start.

修改程序所以有两个玩家。别忘记显示该谁做轮流。可以说player 1(玩家一)或player 2(玩家二),或者可以提前求他们输入自己的名字而使用这个。

How many extra lines did you have to write? Do you think it’s easier than it would have been last time?

应该写多少附加的程序线? 觉得比上次容易吗?

7. (harder) Change the program so we can put ‘things’ in the rooms that can be moved around.

Hint: you might want to break up the task:

(比较难) 修改程序所以房间有可带的物体。

7.1. where it says new_room.doors = {}, try adding new_room.things = [] as well.

7.2. How does __str__ need to change so it can tell you what things are in the room?

7.3. 现在能在浴室加一块毛巾?

Can you now put a towel in the bathroom?

7.4. 修改have_a_turn的程序,让玩家打"get " + 物体的名字,就会带。可能你得修改玩家类的__init__加个存货清单,开始是空白的。会不会查考怎样使用append(附加)和remove(排除)?

How does have_a_turn need to change so you can type "get " + the name of a thing to pick it up? You might want to change Player’s __init__ so there’s a list of things the player is carrying. You might also want to look up how to use append and remove in lists.

7.5. 也能修改have_a_turn的程序让玩家打"drop " + 物体名字,就放回房间吗?

Can you also change have_a_turn so you can type "drop " + the name of a thing you’re carrying to put it back into the room?

房子冒险游戏三 House Adventure 3

冒险游戏往往有玩家先得站在某个地方而带某个物体才能使用的那个特别词语。比如,可以有锁住的门,一般来说进不去,但如果玩家有钥匙,可以打unlock door(开锁+门)然后能进去。打unlock door需要先带钥匙而站在锁住门的房间。

Usually, in games like this, there are special words you can say only in certain places and only when you have certain things. For example, there might be a door that is locked. Normally you can’t go through it. But if you have a key, you can say unlock door, and after that you can go through it. You can say unlock door only if you have the key and you are in the room with the locked door.

为了告诉玩家某个房间目前有什么可带的物体,__str__该有什么改变?

In House Adventure 2, you could write something into have_a_turn like:

在《房子冒险游戏二》的have_a_turn里也许写这样的程序:

if what=="unlock door" \
    and "key" in player.things \
    and player.in_room == landing \
    and not 'n' in landing.doors: # not already unlocked
        landing.connect('n', secret_room)
        print ("The door is now unlocked.")

but why don’t I like this? (Hint: think about how hard it was to change things in House Adventure 1. If I make many special rules about many special rooms, and put them all into have_a_turn, could I end up with a program that’s nearly as bad as House Adventure 1?)

可是,我为什么不喜欢这样写? (线索: 想一想修改《房子冒险游戏一》的程序很难。如果我为许多特别房间写许多特别规则,而把全都放在have_a_turn的程序,可不可以有类似难应付的复杂程序?)

Now, watch this:

那么,看看一下

class Room:
    def __init__(new_room, description):
        # same as before 跟上次一样
        new_room.description = description
        new_room.doors = {}
        new_room.things = []
    def connect(this_room, direction, other_room):
        # same as before 跟上次一样
        assert not direction in this_room.doors
        assert not opposite(direction) in other_room.doors
        this_room.doors[direction] = other_room
        other_room.doors[opposite(direction)] = this_room
    def __str__(this_room):
        # same as before; I've done the "things" question
        # 跟上次一样; 我已经做了那个可带物体的问题
        things = " and ".join(this_room.things)
        if things: things = "\nThings here: " + things
        return this_room.description + "\n" + \
            "You can go: " + \
            " or ".join(this_room.doors) + things
    def special_action(this_room, player, action):
        # This is a new part of the program!
        # 这是新的。但一般房间类的程序只说“结果是‘不’”而已。
        return False
class Landing(Room):
    # This is new!  Landing is now a special type of Room.
    # 这也是新的。楼梯平台现在属于“平台类”,是个房间类的子范畴。
    def __init__(new_landing):
        Room.__init__(
            new_landing,
            "You are on a landing. The north door has a lock.")
    # Everything else is the same as for normal Rooms,
    # so we don't have to repeat it here.  But our new part:
    # 其他的都跟一般的房间类一样,不需要在子范畴重写。
    # 但以下是子范畴的新功能:
    def special_action(this_landing, player, action):
        if action=="unlock door":
            if 'n' in this_landing.doors: # 已经开锁了
                print ("The door is already unlocked.")
            elif not "key" in player.things: # 没带钥匙
                print ("You do not have the key.")
            else: # 否则我们可以开锁
                this_landing.connect('n', secret_room)
                print ("The door is now unlocked.")
                return True # 结果是‘是’
def opposite(direction):
    # same as before 跟上次一样
    return {"n":"s", "s":"n",
            "e":"w", "w":"e"} [direction]
def init():
    bathroom = Room("You are in a bathroom.")
    bathroom.things.append("towel")
    bedroom1 = Room("You are in a bedroom.")
    bedroom1.things.append("key")
    landing = Landing()
    landing.connect('s', bathroom)
    landing.connect('e', bedroom1)
    landing.connect('w', Room("You are in a bedroom."))
    global secret_room # so it can be used later 之后采用
    secret_room = Room("You are in the secret room.")
    return landing # as before 跟上次一样
class Player:
    def __init__(new_player, first_room):
        new_player.in_room = first_room
        new_player.things = []
    def have_a_turn(this_player):
        print (this_player.in_room)
        what = input("What now? ") ; print()
        if this_player.in_room.special_action(this_player, what):
            return
        # Again I've done the "things" question.  There are
        # several ways.  Here's one of them.
        # 我再次已经做了那个可带物体的问题。有不同的方法,这是一个:
        what = what.split(None,1)
        # Now what[0] is the first word, what[1] is the rest.
        # (Look up 'split' to see why.)
        if not what: print("You didn't say anything!")
        elif what[0]=="get":
            if len(what)==1:
                print ("You have to tell me what to get.")
            elif what[1] in this_player.in_room.things:
                this_player.in_room.things.remove(what[1])
                this_player.things.append(what[1])
                print ("Got it.")
            elif what[1] in this_player.things:
                print ("You already had that.")
            else: print (what[1]+" is not here.")
        elif what[0]=="drop":
            if len(what)==1:
                print ("You have to tell me what to drop.")
            elif what[1] in this_player.things:
                this_player.things.remove(what[1])
                this_player.in_room.things.append(what[1])
                print ("Dropped it.")
            else: print ("You are not carrying "+what[1]+".")
        elif what[0] in this_player.in_room.doors:
            this_player.in_room = this_player.in_room.doors[what[0]]
        else: print ("You can't do that now.")
start_room = init()
player1 = Player (start_room) # same as before 跟上次一样
while True:
    player1.have_a_turn()

Questions:

1. 我为什么写return False和return True? (线索: 仔细留意have_a_turn程序怎样使用special_action的结果。)

Why did I say return False and return True? (Hint: look carefully at how special_action is used in have_a_turn.)

2. 要是你没带钥匙而试试开锁,游戏显示:

If you try to unlock the door when you don’t have the key, it says:

问题

You do not have the key.
You can't do that now.

Change the program so it *doesn’t* say “You can’t do that now” after it has already said “You do not have the key”. (Hint: only two more words need to be added to the program, in the right place! Think carefully about question 1.)

修改程序所以说“你没有钥匙”之后不也说“你不会现在这样做”。(线索: 你只需要加两个词语而已,只不过得选择正确的地方加那些两词! 仔细想一想以上的问题。)

3. Change the program so that, after the door is unlocked, you can lock it again by saying lock door. You can’t lock the door unless you’re standing there, you have the key, and the door is already unlocked. (Hint: you might want to make a disconnect that is like connect but does del this_room.doors[direction] and del other_room.doors[opposite(direction)]. How do the asserts change in disconnect?)

修改程序所以开锁后玩家能打lock door(把门锁上)。把门锁上需要玩家带钥匙、站在平台、而门已经开锁。 线索: 在房间类,试试写个disconnect(使分离)程序,这跟connect的程序类似但做del this_room.doors[direction]和del other_room.doors[opposite(direction)](del是Python的delete短写,是删除的意思)。disconnect程序的assert(坚持)省察怎样从connect的版本改变?

4. What happened to bedroom2 in this version? Can you guess why?

《房子冒险游戏一》与《房子冒险游戏二》的程序都有个bedroom2,但《房子冒险游戏三》的程序看起来没说bedroom2但仍然有这个卧室在游戏里,怎么会? 能猜我为什么这样写程序呢?

5. Add some more rooms, and use special_action to put interesting puzzles into one or two of them.

加几个其他房间,并使用special_action(特别行动)的程序把一两个有意思的谜语放在一两个房间里。

Modules

我们的游戏程序越来越大。让我们分割成两个文件免得同时编写太多程序。

Our game is getting quite big now. Let’s try splitting it into two files, so we don’t have so much to see at once.

模块

1. Make a file called play.py, and put the whole of class Player into this new file.

2. 在旧的文件,删除Player类的程序,反而写:

In the old file, delete the whole of class Player, and instead write:

打开一个名叫play.py的空白文件,而把Player类的程序全都在这个新文件。

from play import Player

Now do you see what import does?

现在会看见Python的import(进口)关键词有什么成效呢?

If you had lots of things in play.py, you can import all of them at once by writing:

假如你的play.py含有许多不同的功能(不只是玩家类的功能),能够写以下进口全都功能:

from play import *

But I like to keep things tidy, so I usually write:

可是我比较喜欢整洁的程序,所以一般来说我写:

import play

and then I have to say play.Player instead of just Player, which is more to type, but it’s easier to see where things came from later.

然后我得打play.Player而不只是Player, 但虽然那得打更多,但是后来很容易看那个功能是从哪里来的。

The best thing about import is, other people can let you use parts of *their* programs to help your program, and you don’t need to Copy/Paste: you just import the right part (as long as that part is on your computer and all set up). For example, someone wrote a module called random which is already included in Python, and it can do this:

进口的最好使用是其他人能容许你使用他们程序的某些功能。那能够多多帮助你的程序。不需费事地“拷贝/粘贴”正确部分,只写“进口”而已才能开始写你使用他们所给你的功能的那个程序! (有时候能进口之前需要下载什么东西而正确的安装在电脑上。) 比如,某人写了random(随机)的模块,几乎所有的Python版本已经有,可以这样使用:

import random
print ("I will go "+random.choice(["n","s","e","w"]))

(Make sure to spell choice correctly!)

(我要去北/南/东/西,随机的选择。请留意英文choice(选择)的拼写不是choise。)

Exercises:

1. 在你所修改的《房子冒险游戏二》或《房子冒险游戏三》版本,多修改所以第二个玩家是被电脑控制的而随机走路。(也许你可以写个Player类的子范畴,只个子范畴可以有不同的have_a_turn程序。请别忘记显示什么东西所以我们可以看那电脑所控制的玩家到底做什么。)

In your 2-player version of House Adventure 2 (or House Adventure 3), make it so that Player 2 is controlled by the computer and just walks about at random. (You might want to make a special version of Player with a different have_a_turn. Remember to put something on the screen so we can see what the computer player is doing.)

2. 试试加一个每玩都不一样的迷宫。连接迷宫的房间时得使用random.choice。每一个迷宫房间的描述都可以说“You are in a maze”(你在迷宫里)而已。会不会只写少数程序线但创造许多许多的迷宫房间?

Try to put a maze into the house that is different every time. Use random.choice when connecting up the rooms of the maze. Their descriptions can all say “You are in a maze” and nothing else. Can you make it so only a few lines makes many many rooms?

pygame

现在希望你会了解import pygame的真正意思。对于pygame.init(), 那是Pygame程序的开始功能,是Pygame作家希望你先使用。是跟我们所写的init()开办我们的房子地图类似。如果你喜欢,你可以看看Pygame的程序,看看他们的init到底做什么,但你不需这样做才能使用他们所给你的功能。

You should now be able to understand what import pygame *means*. As for pygame.init(), that is a part of the Pygame program that the people who wrote Pygame want you to call before you use anything else, a bit like *we* made an init() to set up our house map. If you like, you can look at the Pygame program and see what their init actually *does*, but you don’t have to do this just to use it.

练习

You should also have a better idea of why most classes have __init__. The first thing that goes into the class’s programming is usually called self in the books, because it points out the thing (like the room or the player) that the program is now working on. (If you didn’t know this, how would you cope when there’s more than one thing of the same class, like more than one room or more than one player?) We called it new_room, this_room, new_player and this_player so you can see what’s happening more easily. When you understand this well, you can start writing self because that’s what people *usually* do in Python, but you don’t have to. (In other programming languages it’s called other things. For example, in C++ it’s called this, but you don’t always have to write it—the computer can usually figure out where to put it in *for* you. Other parts of C++ can be hard though.)

希望你也更明白为什么大多数的类有__init__的程序。而且,书籍经常使用self(自己)指出类的哪一个物体(比如哪一个房间或哪一个玩家)是正在运转的(要是你不知道哪一个正在运转,有两多房间或两多玩家怎么办?) 我们称它为new_room、this_room、new_player、this_player让你看出怎么运转,但你了解这样之后能够开始写self因为别人习惯在Python的编程语言这样写(但这是大家的习惯而不是百分之百必须的)。其他编程语言有其他名字,比如C++成它为this(这个)而电脑一般能够计算哪里应该加所以你不必一直写。但C++的其他方面有时候一点难。

Legal

All material © Silas S. Brown unless otherwise stated. Python is a trademark of the Python Software Foundation. Any other trademarks I mentioned without realising are trademarks of their respective holders.