練習(xí) 52. 創(chuàng)建你的 web 游戲
這本書馬上就要結(jié)束了助琐。這節(jié)練習(xí)對你來說是個真正的挑戰(zhàn)肥惭。當(dāng)你完成以后,你就可以算是一個能力不錯的 Python 初學(xué)者了忍饰。為了進一步學(xué)習(xí),你還需要多讀一些書寺庄,多寫一些程序艾蓝,不過你已經(jīng)具備進一步學(xué)習(xí)的能力了。接下來的學(xué)習(xí)就只是時間斗塘、動力赢织、以及資源的問題了。
在本節(jié)練習(xí)中馍盟,我們不會去創(chuàng)建一個完整的游戲于置,而是要為《練習(xí) 47》中的游戲創(chuàng)建一個“引擎(engine)”,讓這個游戲能夠在瀏覽器中運行起來贞岭。這會涉及到將《習(xí)題 43》中的游戲“重構(gòu)(refactor)”八毯,將《習(xí)題 47》中的架構(gòu)混合進來,添加自動測試代碼瞄桨,最后創(chuàng)建一個可以運行游戲的 web 引擎话速。
這個練習(xí)會非常龐大。我預(yù)測你要花一周到一個月時間才能完成它芯侥。你最好一點一點來泊交,每天晚上完成一點,在進行下一步之前確保上一步已經(jīng)正確完成。
重構(gòu)《練習(xí) 43》的游戲
你已經(jīng)在兩個練習(xí)中修改了 gothonweb 項目活合,這節(jié)習(xí)題中你會再修改一次。這種修改的技術(shù)叫做“重構(gòu)(refactoring)”物赶,或者用我喜歡的講法來說白指,叫“修修補補(fixing stuff)”。重構(gòu)是一個編程術(shù)語酵紫,它指的是清理舊代碼或者為舊代碼添加新功能的過程告嘲。你其實已經(jīng)做過這樣的事情了,只不過不知道這個術(shù)語而已奖地。這是寫軟件過程的第二個自然屬性橄唬。
你在本節(jié)中要做的,是將《習(xí)題 47》中的可以測試的房間地圖参歹,以及《習(xí)題 43》中的游戲這兩樣?xùn)|西歸并到一起仰楚,創(chuàng)建一個新的游戲架構(gòu)。游戲的內(nèi)容不會發(fā)生變化犬庇,只不過我們會通過“重構(gòu)”讓它有一個更好的架構(gòu)而已僧界。
第一步是將 ex47/game.py
的內(nèi)容復(fù)制到 gothonweb/planisphere.py
中,然后將 tests/ex47_tests.py
的內(nèi)容復(fù)制到 tests/planisphere_tests.py
中臭挽,然后再次運行 nosetests捂襟,確保他們還能正常工作』斗澹“planisphere”這個詞是地圖的同義詞葬荷,用這個名字是為了避免 Python 內(nèi)置的 map 函數(shù)。同義詞典(Thesaurus)是個好東西纽帖,要善于利用它宠漩。
警告! |
---|
從現(xiàn)在開始抛计,我不會再向你展示我運行測試的輸出結(jié)果了哄孤。我假設(shè)你會自己去做測試,所以測試是個前提吹截,除非你遇到了錯誤瘦陈。 |
當(dāng)你把《練習(xí) 47》的代碼復(fù)制好之后,你就該開始重構(gòu)它了波俄,讓它包含《習(xí)題 43》中的地圖晨逝。我一開始會把基本架構(gòu)為你準(zhǔn)備好,然后你需要去完成 planisphere.py
和 planisphere_tests.py
這兩個文件里邊的內(nèi)容懦铺。
首先要做的是使用 Room 類來構(gòu)建基本的地圖架構(gòu):
planisphere.py
1 class Room(object):
2
3 def __init__(self, name, description):
4 self.name = name
5 self.description = description
6 self.paths = {}
7
8 def go(self, direction):
9 return self.paths.get(direction, None)
10
11 def add_paths(self, paths):
12 self.paths.update(paths)
13
14
15 central_corridor = Room("Central Corridor",
16 """
17 The Gothons of Planet Percal #25 have invaded your ship and destroyed
18 your entire crew. You are the last surviving member and your last
19 mission is to get the neutron destruct bomb from the Weapons Armory, put
20 it in the bridge, and blow the ship up after getting into an escape pod.
21
22 You're running down the central corridor to the Weapons Armory when a
23 Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown
24 costume flowing around his hate filled body. He's blocking the door to
25 the Armory and about to pull a weapon to blast you.
26 """)
27
28
29 laser_weapon_armory = Room("Laser Weapon Armory",
30 """
31 Lucky for you they made you learn Gothon insults in the academy. You
32 tell the one Gothon joke you know: Lbhe zbgure vf fb sng, jura fur fvgf
33 nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr. The Gothon stops, tries
34 not to laugh, then busts out laughing and can't move. While he's
35 laughing you run up and shoot him square in the head putting him down,
36 then jump through the Weapon Armory door.
37
38 You do a dive roll into the Weapon Armory, crouch and scan the room for
39 more Gothons that might be hiding. It's dead quiet, too quiet. You
40 stand up and run to the far side of the room and find the neutron bomb
41 in its container. There's a keypad lock on the box and you need the
42 code to get the bomb out. If you get the code wrong 10 times then the
43 lock closes forever and you can't get the bomb. The code is 3 digits.
44 """)
45
46
47 the_bridge = Room("The Bridge",
48 """
49 The container clicks open and the seal breaks, letting gas out. You
50 grab the neutron bomb and run as fast as you can to the bridge where you
51 must place it in the right spot.
52
53 You burst onto the Bridge with the netron destruct bomb under your arm
54 and surprise 5 Gothons who are trying to take control of the ship. Each
55 of them has an even uglier clown costume than the last. They haven't
56 pulled their weapons out yet, as they see the active bomb under your arm
57 and don't want to set it off.
58 """)
59
60
61 escape_pod = Room("Escape Pod",
62 """
63 You point your blaster at the bomb under your arm and the Gothons put
64 their hands up and start to sweat. You inch backward to the door, open
65 it, and then carefully place the bomb on the floor, pointing your
66 blaster at it. You then jump back through the door, punch the close
67 button and blast the lock so the Gothons can't get out. Now that the
68 bomb is placed you run to the escape pod to get off this tin can.
69
70 You rush through the ship desperately trying to make it to the escape
71 pod before the whole ship explodes. It seems like hardly any Gothons
72 are on the ship, so your run is clear of interference. You get to the
73 chamber with the escape pods, and now need to pick one to take. Some of
74 them could be damaged but you don't have time to look. There's 5 pods,
75 which one do you take?
76 """)
77
78
79 the_end_winner = Room("The End",
80 """
81 You jump into pod 2 and hit the eject button. The pod easily slides out
82 into space heading to the planet below. As it flies to the planet, you
83 look back and see your ship implode then explode like a bright star,
84 taking out the Gothon ship at the same time. You won!
85 """)
86
87
88 the_end_loser = Room("The End",
89 """
90 You jump into a random pod and hit the eject button. The pod escapes
91 out into the void of space, then implodes as the hull ruptures, crushing
92 your body into jam jelly.
93 """
94 )
95
96 escape_pod.add_paths({
97 '2': the_end_winner,
98 '*': the_end_loser
99 })
100
101 generic_death = Room("death", "You died.")
102
103 the_bridge.add_paths({
104 'throw the bomb': generic_death,
105 'slowly place the bomb': escape_pod
106 })
107
108 laser_weapon_armory.add_paths({
109 '0132': the_bridge,
110 '*': generic_death
111 })
112
113 central_corridor.add_paths({
114 'shoot!': generic_death,
115 'dodge!': generic_death,
116 'tell a joke': laser_weapon_armory
117 })
118
119 START = 'central_corridor'
120
121 def load_room(name):
122 """
123 There is a potential security problem here.
124 Who gets to set name? Can that expose a variable?
125 """
126 return globals().get(name)
127
128 def name_room(room):
129 """
130 Same possible security problem. Can you trust room?
131 What's a better solution than this globals lookup?
132 """
133 for key, value in globals().items():
134 if value == room:
135 return key
你會發(fā)現(xiàn)我們的 Room 類和地圖有一些問題:
我們必須把放在 if-else 語句中的文本在進入一個房間之前打印出來捉貌,作為每個房間的一部分。這就意味著你不能把 planisphere 打亂,這很好趁窃。你要在這個練習(xí)中慢慢修復(fù)它牧挣。
原版游戲中我們使用了專門的代碼來生成一些內(nèi)容,例如炸彈的激活鍵碼醒陆,艦艙的選擇等瀑构,這次我們做游戲時就先使用默認(rèn)值好了,不過后面的附加練習(xí)里刨摩,我會要求你把這些功能再加到游戲中寺晌。
我為游戲中的所有失敗結(jié)尾寫了一個
generic_death
,你需要去補全這個函數(shù)澡刹。你需要把原版游戲中所有的失敗結(jié)尾都加進去呻征,并確保代碼能正確運行。我添加了一種新的轉(zhuǎn)換模式罢浇,以"*"為標(biāo)記陆赋,用來在游戲引擎中實現(xiàn)“catch-all”動作。
等你把上面的代碼基本寫好以后己莺,接下來就是引導(dǎo)你繼續(xù)寫下去的自動測試的內(nèi)容 tests/planisphere_test.py
:
planisphere_tests.py
1 from nose.tools import *
2 from gothonweb.planisphere import *
3
4 def test_room():
5 gold = Room("GoldRoom",
6 """This room has gold in it you can grab. There's a
7 door to the north.""")
8 assert_equal(gold.name, "GoldRoom")
9 assert_equal(gold.paths, {})
10
11 def test_room_paths():
12 center = Room("Center", "Test room in the center.")
13 north = Room("North", "Test room in the north.")
14 south = Room("South", "Test room in the south.")
15
16 center.add_paths({'north': north, 'south': south})
17 assert_equal(center.go('north'), north)
18 assert_equal(center.go('south'), south)
19
20 def test_map():
21 start = Room("Start", "You can go west and down a hole."
22 west = Room("Trees", "There are trees here, you can go east.")
23 down = Room("Dungeon", "It's dark down here, you can go up.")
24
25 start.add_paths({'west': west, 'down': down})
26 west.add_paths({'east': start})
27 down.add_paths({'up': start})
28
29 assert_equal(start.go('west'), west)
30 assert_equal(start.go('west').go('east'), start)
31 assert_equal(start.go('down').go('up'), start)
32
33 def test_gothon_game_map():
34 start_room = load_room(START)
35 assert_equal(start_room.go('shoot!'), generic_death)
36 assert_equal(start_room.go('dodge!'), generic_death)
37
38 room = start_room.go('tell a joke')
39 assert_equal(room, laser_weapon_armory)
你在這部分練習(xí)中的任務(wù)是完成這個地圖奏甫,并且讓自動測試可以完整地檢查過整個地圖。這包括將所有的 generic_death
對象修正為游戲中實際的失敗結(jié)尾凌受。讓你的代碼成功運行起來阵子,并讓你的測試越全面越好。后面我們會對地圖做一些修改胜蛉,到時候這些測試將保證修改后的代碼還可以正常工作挠进。
創(chuàng)建一個引擎
你應(yīng)該讓你的游戲地圖正常運行,并對它進行良好的單元測試誊册。我現(xiàn)在想讓你做一個簡單的小游戲引擎领突,它將運行房間、收集來自玩家的輸入案怯,并跟蹤玩家在游戲中的位置君旦。我們將使用你剛剛學(xué)會的會話來創(chuàng)建一個簡單的游戲引擎,這個引擎會做這些事情:
為新用戶開啟一個新游戲嘲碱。
為用戶展示房間金砍。
從用戶獲取輸入。
通過游戲運行用戶的輸入麦锯。
呈現(xiàn)結(jié)果卜录,并繼續(xù)運行轰绵,直至用戶掛掉。
要做到這些惧蛹,你需要使用你一直在寫的可靠的 app.py稽屏,來創(chuàng)建一個運行良好的、基于會話的游戲引擎。問題是,我需要做一個非常簡單的基本 HTML 文件澎羞,它將由你來完成它。這是基礎(chǔ)引擎:
app.py
1 from flask import Flask, session, redirect, url_for, escape, request
2 from flask import render_template
3 from gothonweb import planisphere
4
5 app = Flask(__name__)
6
7 @app.route("/")
8 def index():
9 # this is used to "setup" the session with starting value
10 session['room_name'] = planisphere.START
11 return redirect(url_for("game"))
12
13 @app.route("/game", methods=['GET', 'POST'])
14 def game():
15 room_name = session.get('room_name')
16
17 if request.method == "GET":
18 if room_name:
19 room = planisphere.load_room(room_name)
20 return render_template("show_room.html", room=room)
21 else:
22 # why is there here? do you need it?'
23 return render_template("you_died.html")
24 else:
25 action = request.form.get('action')
26
27 if room_name and action:
28 room = planisphere.load_room(room_name)
29 next_room = room.go(action)
30
31 if not next_room:
32 session['room_name'] = planisphere.name_room
33 else:
34 session['room_name'] = planisphere.name_room
35
36 return redirect(url_for("game"))
37
38
39 # YOU SHOULD CHANGE THIS IF YOU PUT ON THE INTERNET
40 app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'
41
42 if __name__ == "__main__":
43 app.run()
這個腳本中有更多的新東西敛苇,但神奇的是煤痕,這個小文件是一個完全基于 web 的游戲引擎。在運行 app.py
之前接谨,需要更改 PYTHONPATH 環(huán)境變量。不知道那是什么塘匣?我知道這有點枯燥脓豪,但你必須學(xué)習(xí)這是什么來運行基本的 Python 程序,沒辦法忌卤,用 Python 的人就喜歡這樣扫夜。
在你的終端輸入:
export PYTHONPATH=$PYTHONPATH:.
在 Windows 的 PowerShell 中輸入:
$env:PYTHONPATH = "$env:PYTHONPATH;."
你只要針對每一個命令行會話界面輸入一次就可以了,不過如果你運行 Python 代碼時看到了 import error驰徊,或者你輸入錯誤笤闯,那就需要再去執(zhí)行一下上面的命令。
接下來你需要刪掉 templates/hello_form.html
和 templates/index.html
棍厂,并創(chuàng)建兩個前面代碼中提到的模板颗味。這是一個非常簡單的 templates/show_room.html
:
show_room.html
{% extends "layout.html" %}
{% block content %}
<h1> {{ room.name }} </h1>
<pre>
{{ room.description }}
</pre>
{% if room.name in ["death", "The End"] %}
<p><a href="/">Play Again?</a></p>
{% else %}
<p>
<form action="/game" method="POST">
- <input type="text" name="action"> <input type="SUBMIT">
</form>
</p>
{% endif %}
{% endblock %}
這是在游戲中顯示房間的模板。接下來你需要一個模板來告訴用戶他們已經(jīng)死了牺弹,以防他們意外地去到地圖的結(jié)尾浦马,也就是 templates/you_die .html:
you_died.html
<h1>You Died!</h1>
<p>Looks like you bit the dust.</p>
<p><a href="/">Play Again</a></p>
這些都弄好了之后,你可以這樣做:
- 讓
tests/app_tests.py
再次運行來測試這個游戲张漂。因為有會話晶默,所以你只需要在游戲里點幾下就行。不過航攒,你應(yīng)該能做一些基本操作磺陡。 - 運行
python3.6 app.py
腳本來玩一下這個游戲。
你需要和往常一樣刷新和修正你的游戲漠畜,慢慢修改游戲的 HTML 文件和引擎币他,直到你實現(xiàn)游戲需要的所有功能為止。
你的期末考試
你有沒有覺著我一下子給了你超多的信息呢盆驹?那就對了圆丹,我想要你在學(xué)習(xí)技能的同時可以有一些可以用來鼓搗的東西。為了完成這節(jié)習(xí)題躯喇,我會給你最后一套需要你自己完成的練習(xí)辫封。你應(yīng)該注意到硝枉,到目前為止你寫的游戲并不是很好,這只是你的第一版代碼而已倦微。你現(xiàn)在的任務(wù)是讓游戲更加完善妻味,實現(xiàn)下面的這些功能:
- 修正代碼中所有我提到和沒提到的 bug,如果你發(fā)現(xiàn)了新的 bug欣福,可以告訴我责球。
- 改進所有的自動測試,讓你可以測試更多的內(nèi)容拓劝,直到你可以不用瀏覽器就能測到所有的內(nèi)容為止雏逾。
- 讓 HTML 頁面看上去更美觀一些。
- 研究一下網(wǎng)頁登錄系統(tǒng)郑临,為這個程序創(chuàng)建一個登錄界面栖博,這樣人們就可以登錄這個游戲,并且可以保存游戲高分厢洞。
- 完成游戲地圖仇让,盡可能地把游戲做大,功能做全躺翻。
- 給用戶一個“幫助系統(tǒng)”丧叽,讓他們可以查詢每個房間里可以執(zhí)行哪些命令。
- 為你的游戲添加任何你能想到的新功能公你。
- 創(chuàng)建多個地圖踊淳,讓用戶可以選擇他們想要玩的一張來進行游戲。你的
app.py
應(yīng)該可以運行提供給它的任意的地圖陕靠,這樣你的引擎就可以支持多個不同的游戲嚣崭。 - 最后,使用你在練習(xí) 48 和 49 中學(xué)到的東西來創(chuàng)建一個更好的輸入處理器懦傍。你手頭已經(jīng)有了大部分必要的代碼雹舀,你只需要改進語法,讓它和你的輸入表單以及游戲引擎掛鉤即可粗俱。
祝你好運说榆!
常見問題
我在游戲中用了 session,但不能用 nosetests 測試寸认。 閱讀 Flask 測試文檔(Flask Testing Documentation)中的“其他測試技巧”(Other Testing Tricks)签财,了解關(guān)于在游戲中創(chuàng)建“假會話”(fake sessions)的信息。
我收到了一個 ImportError
偏塞。 可能是以下情況中的一種或幾種: 錯誤的目錄唱蒸,錯誤的 Python 版本,沒有設(shè)置 PYTHON-PATH灸叼,沒有 init.py 文件神汹,以及(或者)import 中存在拼寫錯誤庆捺。