鍍金池/ 教程/ Python/ exercise52.開始你的 web 游戲
附錄 A-練習(xí) 9:生成一個(gè)空文件(Touch, New-Item)
附錄 A-練習(xí) 10:復(fù)制文件 (cp)
exercise44.繼承 Vs.包含
附錄 A-練習(xí) 14:刪除文件 (rm)
附錄 A-練習(xí) 11:移動文件 (mv)
exercise46.項(xiàng)目骨架
附錄 A-練習(xí) 3:如果你迷路了
exercise37.復(fù)習(xí)符號
exercise47.自動化測試
exercise3.數(shù)字和數(shù)學(xué)計(jì)算
附錄 A-練習(xí) 1:安裝
exercise32.循環(huán)和列表
exercise31.做出決定
exercise42.對象、類、以及從屬關(guān)系
exercise48.更復(fù)雜的用戶輸入
下一步
簡介
附錄 A-練習(xí) 7:刪除路徑 (rmdir)
exercise49.寫代碼語句
exercise18.命名, 變量, 代碼, 函數(shù)
exercise12.提示別人
exercise14.提示和傳遞
exercise40.模塊, 類和對象
附錄 A-練習(xí) 12:查看文件 (less, MORE)
exercise9.打印, 打印, 打印
exercise13.參數(shù), 解包, 變量
exercise30. Else 和 If
exercise28. 布爾表達(dá)式
附錄 A-練習(xí) 4:創(chuàng)建一個(gè)路徑 (mkdir)
附錄 A-練習(xí) 15:退出命令行 (exit)
exercise25. 更多更多的練習(xí)
exercise6.字符串和文本
exercise2.注釋和井號“#”
exercise21. 函數(shù)的返回值
附錄 A-下一步
exercise1.第一個(gè)程序
exercise23. 閱讀代碼
附錄 A-練習(xí) 5:改變當(dāng)前路徑 (cd)
exercise17.更多文件操作
exercise24. 更多的練習(xí)
exercise19.函數(shù)和變量
exercise51.從瀏覽器獲取輸入
exercise22. 到目前為止你學(xué)到了什么?
exercise41.學(xué)會說面向?qū)ο?/span>
exercise52.開始你的 web 游戲
exercise20. 函數(shù)和文件
exercise15.讀文件
exercise45.你來制作一個(gè)游戲
exercise10.那是什么?
exercise8.打印, 打印
exercise35.分支和函數(shù)
exercise26. 恭喜你,可以進(jìn)行一次考試了
exercise33.while 循環(huán)
exercise29. IF 語句
exercise36.設(shè)計(jì)和調(diào)試
exercise0.安裝和準(zhǔn)備
exercise50.你的第一個(gè)網(wǎng)站
附錄 A-練習(xí) 2:路徑, 文件夾, 名錄 (pwd)
exercise38.列表操作
附錄 A-練習(xí) 6:列出當(dāng)前路徑 (ls)
exercise16.讀寫文件
exercise4.變量和命名
exercise34.訪問列表元素
exercise11.提問
exercise43.基本的面向?qū)ο蟮姆治龊驮O(shè)計(jì)
附錄 A-簡介
附錄 A-練習(xí) 8:目錄切換(pushd, popd)
來自老程序員的建議
exercise27. 記住邏輯
exercise5.更多的變量和打印
exercise7.更多的打印(輸出)
附錄 A-練習(xí) 13:輸出文件 (cat)
exercise39.字典,可愛的字典

exercise52.開始你的 web 游戲

這本書馬上就要結(jié)束了。本章的練習(xí)對你是一個(gè)真正的挑戰(zhàn)。當(dāng)你完成以后,你就可以算是一個(gè)能力不錯的 Python 初學(xué)者了。為了進(jìn)一步學(xué)習(xí),你還需要多讀一些書,多寫一些程序,不過你已經(jīng)具備進(jìn)一步學(xué)習(xí)的技能了。接下來的學(xué)習(xí)就只是時(shí)間、動力、以及資源的問題了。

在本章習(xí)題中,我們不會去創(chuàng)建一個(gè)完整的游戲,取而代之的是我們會為《習(xí)題 47》中的游戲創(chuàng)建一個(gè)“引擎(engine)”,讓這個(gè)游戲能夠在瀏覽器中運(yùn)行起來。這會涉及到將《習(xí)題 43》中的游戲“重構(gòu)(refactor)”,將《習(xí)題 47》中的架構(gòu)混合進(jìn)來,添加自動測試代碼,最后創(chuàng)建一個(gè)可以運(yùn)行游戲的 web 引擎。

這是一節(jié)很龐大的習(xí)題。我預(yù)測你要花一周到一個(gè)月才能完成它。最好的方法是一點(diǎn)點(diǎn)來,每天晚上完成一點(diǎn),在進(jìn)行下一步之前確認(rèn)上一步有正確完成。

重構(gòu)習(xí)題 43 的游戲

你已經(jīng)在兩個(gè)練習(xí)中修改了 gothonweb 項(xiàng)目,這節(jié)習(xí)題中你會再修改一次。這種修改的技術(shù)叫做“重構(gòu)(refactoring)”,或者用我喜歡的講法來說,叫“修修補(bǔ)補(bǔ)(fixing stuff)”。重構(gòu)是一個(gè)編程術(shù)語,它指的是清理舊代碼或者為舊代碼添加新功能的過程。你其實(shí)已經(jīng)做過這樣的事情了,只不過不知道這個(gè)術(shù)語而已。這是寫軟件過程的第二個(gè)自然屬性。

你在本節(jié)中要做的,是將《習(xí)題 47》中的可以測試的房間地圖,以及《習(xí)題 43》中的游戲這兩樣?xùn)|西歸并到一起,創(chuàng)建一個(gè)新的游戲架構(gòu)。游戲的內(nèi)容不會變化,只不過我們會通過“重構(gòu)”讓它有一個(gè)更好的架構(gòu)而已。

第一步是將 ex47/game.py 的內(nèi)容復(fù)制到 gothonweb/map.py 中,然后將 tests/ex47_tests.py 的內(nèi)容復(fù)制到 tests/map_tests.py 中,然后再次運(yùn)行 nosetests,確認(rèn)他們還能正常工作。

NOTE: 從現(xiàn)在開始我不會再向你展示運(yùn)行測試的輸出了,我就假設(shè)你回去運(yùn)行這些測試,而且知道怎樣的輸出是正確的。

將《習(xí)題 47》的代碼拷貝完畢后,你就該開始重構(gòu)它,讓它包含《習(xí)題 43》中的地圖。我一開始會把基本架構(gòu)為你準(zhǔn)備好,然后你需要去完成 map.py 和 map_tests.py 里邊的內(nèi)容。

首先要做的是使用 Room 類來構(gòu)建基本的地圖架構(gòu):

class Room(object):

    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.paths = {}

    def go(self, direction):
        return self.paths.get(direction, None)

    def add_paths(self, paths):
        self.paths.update(paths)

central_corridor = Room("Central Corridor",
"""
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew.  You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory,
put it in the bridge, and blow the ship up after getting into an 
escape pod.

You're running down the central corridor to the Weapons Armory when
a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume
flowing around his hate filled body.  He's blocking the door to the
Armory and about to pull a weapon to blast you.
""")

laser_weapon_armory = Room("Laser Weapon Armory",
"""
Lucky for you they made you learn Gothon insults in the academy.
You tell the one Gothon joke you know:
Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr.
The Gothon stops, tries not to laugh, then busts out laughing and can't move.
While he's laughing you run up and shoot him square in the head
putting him down, then jump through the Weapon Armory door.

You do a dive roll into the Weapon Armory, crouch and scan the room
for more Gothons that might be hiding.  It's dead quiet, too quiet.
You stand up and run to the far side of the room and find the
neutron bomb in its container.  There's a keypad lock on the box
and you need the code to get the bomb out.  If you get the code
wrong 10 times then the lock closes forever and you can't
get the bomb.  The code is 3 digits.
""")

the_bridge = Room("The Bridge",
"""
The container clicks open and the seal breaks, letting gas out.
You grab the neutron bomb and run as fast as you can to the
bridge where you must place it in the right spot.

You burst onto the Bridge with the netron destruct bomb
under your arm and surprise 5 Gothons who are trying to
take control of the ship.  Each of them has an even uglier
clown costume than the last.  They haven't pulled their
weapons out yet, as they see the active bomb under your
arm and don't want to set it off.
""")

escape_pod = Room("Escape Pod",
"""
You point your blaster at the bomb under your arm
and the Gothons put their hands up and start to sweat.
You inch backward to the door, open it, and then carefully
place the bomb on the floor, pointing your blaster at it.
You then jump back through the door, punch the close button
and blast the lock so the Gothons can't get out.
Now that the bomb is placed you run to the escape pod to
get off this tin can.

You rush through the ship desperately trying to make it to
the escape pod before the whole ship explodes.  It seems like
hardly any Gothons are on the ship, so your run is clear of
interference.  You get to the chamber with the escape pods, and
now need to pick one to take.  Some of them could be damaged
but you don't have time to look.  There's 5 pods, which one
do you take?
""")

the_end_winner = Room("The End",
"""
You jump into pod 2 and hit the eject button.
The pod easily slides out into space heading to
the planet below.  As it flies to the planet, you look
back and see your ship implode then explode like a
bright star, taking out the Gothon ship at the same
time.  You won!
""")

the_end_loser = Room("The End",
"""
You jump into a random pod and hit the eject button.
The pod escapes out into the void of space, then
implodes as the hull ruptures, crushing your body
into jam jelly.
"""
)

escape_pod.add_paths({
    '2': the_end_winner,
    '*': the_end_loser
})

generic_death = Room("death", "You died.")

the_bridge.add_paths({
    'throw the bomb': generic_death,
    'slowly place the bomb': escape_pod
})

laser_weapon_armory.add_paths({
    '0132': the_bridge,
    '*': generic_death
})

central_corridor.add_paths({
    'shoot!': generic_death,
    'dodge!': generic_death,
    'tell a joke': laser_weapon_armory
})

START = central_corridor

你會發(fā)現(xiàn)我們的 Room 類和地圖有一些問題:

1.在進(jìn)入一個(gè)房間以前會打印出一段文字作為房間的描述,我們需要將這些描述和每個(gè)房間關(guān)聯(lián)起來,這樣房間的次序就不會被打亂了,這對我們的游戲是一件好事。這些描述本來是在 if-else 結(jié)構(gòu)中的,這是我們后面要修改的東西。 2.原版游戲中我們使用了專門的代碼來生成一些內(nèi)容,例如炸彈的激活鍵碼,艦艙的選擇等,這次我們做游戲時(shí)就先使用默認(rèn)值好了,不過后面的附加題里,我會要求你把這些功能再加到游戲中。 3.我為所有的游戲中的失敗結(jié)尾寫了一個(gè) generic_death,你需要去補(bǔ)全這個(gè)函數(shù)。你需要把原版游戲中所有的失敗結(jié)尾都加進(jìn)去,并確保代碼能正確運(yùn)行。 4.我添加了一種新的轉(zhuǎn)換模式,以"*"為標(biāo)記,用來在游戲引擎中實(shí)現(xiàn)“catch-all”動作。

等你把上面的代碼基本寫好以后,接下來就是引導(dǎo)你繼續(xù)寫下去的自動測試的內(nèi)容 tests/map_test.py 了:

from nose.tools import *
from gothonweb.map import *

def test_room():
    gold = Room("GoldRoom",
                """This room has gold in it you can grab. There's a
                door to the north.""")
    assert_equal(gold.name, "GoldRoom")
    assert_equal(gold.paths, {})

def test_room_paths():
    center = Room("Center", "Test room in the center.")
    north = Room("North", "Test room in the north.")
    south = Room("South", "Test room in the south.")

    center.add_paths({'north': north, 'south': south})
    assert_equal(center.go('north'), north)
    assert_equal(center.go('south'), south)

def test_map():
    start = Room("Start", "You can go west and down a hole.")
    west = Room("Trees", "There are trees here, you can go east.")
    down = Room("Dungeon", "It's dark down here, you can go up.")

    start.add_paths({'west': west, 'down': down})
    west.add_paths({'east': start})
    down.add_paths({'up': start})

    assert_equal(start.go('west'), west)
    assert_equal(start.go('west').go('east'), start)
    assert_equal(start.go('down').go('up'), start)

def test_gothon_game_map():
    assert_equal(START.go('shoot!'), generic_death)
    assert_equal(START.go('dodge!'), generic_death)

    room = START.go('tell a joke')
    assert_equal(room, laser_weapon_armory)

你在這部分練習(xí)中的任務(wù)是完成地圖,并且讓自動測試可以完整地檢查過整個(gè)地圖。這包括將所有的 generic_death 對象修正為游戲中實(shí)際的失敗結(jié)尾。讓你的代碼成功運(yùn)行起來,并讓你的測試越全面越好。后面我們會對地圖做一些修改,到時(shí)候這些測試將保證修改后的代碼還可以正常工作。

會話(session)和用戶跟蹤

在你的 web 程序運(yùn)行的某個(gè)位置,你需要追蹤一些信息,并將這些信息和用戶的瀏覽器關(guān)聯(lián)起來。在 HTTP 協(xié)議的框架中,web 環(huán)境是“無狀態(tài)(stateless)”的,這意味著你的每一次請求都是獨(dú)立于其他請求的。如果你請求了頁面 A,輸入了一些數(shù)據(jù),然后點(diǎn)了一個(gè)頁面 B 的鏈接,那你在頁面 A 輸入的數(shù)據(jù)就全部消失了。

解決這個(gè)問題的方法是為 web 程序建立一個(gè)很小的數(shù)據(jù)存儲功能,給每個(gè)瀏覽器進(jìn)程賦予一個(gè)獨(dú)一無二的數(shù)字,用來跟蹤瀏覽器所作的事情。這個(gè)存儲通常用數(shù)據(jù)庫或者存儲在磁盤上的文件來實(shí)現(xiàn)。這就是所謂的“會話跟蹤”和在瀏覽器中使用 Cookies 以保持用戶狀態(tài)。在 lpthw.web 這個(gè)小框架中實(shí)現(xiàn)這樣的功能是很容易的,以下就是一個(gè)這樣的例子:

import web

web.config.debug = False

urls = (
    "/count", "count",
    "/reset", "reset"
)
app = web.application(urls, locals())
store = web.session.DiskStore('sessions')
session = web.session.Session(app, store, initializer={'count': 0})

class count:
    def GET(self):
        session.count += 1
        return str(session.count)

class reset:
    def GET(self):
        session.kill()
        return ""

if __name__ == "__main__":
    app.run()

為了實(shí)現(xiàn)這個(gè)功能,你需要創(chuàng)建一個(gè) sessions/文件夾作為程序的會話存儲位置,創(chuàng)建好以后運(yùn)行這個(gè)程序,然后檢查/count 頁面,刷新一下這個(gè)頁面,看計(jì)數(shù)會不會累加上去。關(guān)掉瀏覽器后,程序就會“忘掉”之前的位置,這也是我們的游戲所需的功能。有一種方法可以讓瀏覽器永遠(yuǎn)記住一些信息,不過這會讓測試和開發(fā)變得更難。如果你回到/reset/頁面,然后再訪問/count 頁面,你可以看到你的計(jì)數(shù)器被重置了,因?yàn)槟阋呀?jīng)把會話殺掉了。

你需要花點(diǎn)時(shí)間弄懂這段代碼,注意會話開始時(shí) count 的值是如何設(shè)為 0 的。另外再看看 sessions/下面的文件,看你能不能把它們打開。下面是我把一個(gè) Python 會話打開并且解碼的過程:

>>> import pickle
>>> import base64
>>> base64.b64decode(open("sessions/XXXXX").read())
"(dp1\nS'count'\np2\nI1\nsS'ip'\np3\nV127.0.0.1\np4\nsS'session_id'\np5\nS'XXXX'\np6\ns."
>>>
>>> x = base64.b64decode(open("sessions/XXXXX").read())
>>>
>>> pickle.loads(x)
{'count': 1, 'ip': u'127.0.0.1', 'session_id': 'XXXXX'}

所以會話其實(shí)就是使用 pickle 和 base64 這些庫寫到磁盤上的字典。存儲和管理會話的方法很多,大概和 Python 的 web 框架那么多,所以了解它們的工作原理并不重要。當(dāng)然如果你需要調(diào)試或者清空會話時(shí),知道點(diǎn)原理還是有用的。

創(chuàng)建引擎

你應(yīng)該已經(jīng)寫好了游戲地圖和它的單元測試代碼。現(xiàn)在我要求你制作一個(gè)簡單的游戲引擎,用來讓游戲中的各個(gè)房間運(yùn)轉(zhuǎn)起來,從玩家收集輸入,并且記住玩家到了那一幕。我們將用到你剛學(xué)過的會話來制作一個(gè)簡單的引擎,讓它可以:

1.為新用戶啟動新的游戲。 2.將房間展示給用戶。 3.接受用戶的輸入。 4.在游戲中處理用戶的輸入。 5.顯示游戲的結(jié)果,繼續(xù)游戲的下一幕,知道玩家角色死亡為止.

為了創(chuàng)建這個(gè)引擎,你需要將我們久經(jīng)考驗(yàn)的 bin/app.py 搬過來,創(chuàng)建一個(gè)功能完備的、基于會話的游戲引擎。這里的難點(diǎn)是我會先使用基本的 HTML 文件創(chuàng)建一個(gè)非常簡單的版本,接下來將由你完成它,基本的引擎是這個(gè)樣子的:

import web
from gothonweb import map

urls = (
  '/game', 'GameEngine',
  '/', 'Index',
)

app = web.application(urls, globals())

# little hack so that debug mode works with sessions
if web.config.get('_session') is None:
    store = web.session.DiskStore('sessions')
    session = web.session.Session(app, store,
                                  initializer={'room': None})
    web.config._session = session
else:
    session = web.config._session

render = web.template.render('templates/', base="layout")

class Index(object):
    def GET(self):
        # this is used to "setup" the session with starting values
        session.room = map.START
        web.seeother("/game")

class GameEngine(object):

    def GET(self):
        if session.room:
            return render.show_room(room=session.room)
        else:
            # why is there here? do you need it?
            return render.you_died()

    def POST(self):
        form = web.input(action=None)

        # there is a bug here, can you fix it?
        if session.room and form.action:
            session.room = session.room.go(form.action)

        web.seeother("/game")

if __name__ == "__main__":
    app.run()

這個(gè)腳本里你可以看到更多的新東西,不過了不起的事情是,整個(gè)基于網(wǎng)頁的游戲引擎只要一個(gè)小文件就可以做到了。這段腳本里最有技術(shù)含量的事情就是將會話帶回來的那幾行,這對于調(diào)試模式下的代碼重載是必須的,否則每次你刷新網(wǎng)頁,會話就會消失,游戲也不會再繼續(xù)了。

在你運(yùn)行 bin/app.py 之前,你需要修改 PYTHONPATH 環(huán)境變量。不知道什么是環(huán)境變量?為了運(yùn)行一個(gè)最基本的 Python 程序,你就得學(xué)會環(huán)境變量,Python 的這一點(diǎn)確實(shí)有點(diǎn)挫。不過沒辦法,用 Python 的人就喜歡這樣: 在你的命令行終端,輸入:

export PYTHONPATH=$PYTHONPATH:.

如果你用的是 Windows,那就輸入:

$env:PYTHONPATH = "$env:PYTHONPATH;."

你只要針對每一個(gè)命令行會話界面輸入一次就可以了,不過如果你運(yùn)行 Python 代碼時(shí)看到了 import 錯誤,那你就需要去執(zhí)行一下上面的命令,或者也許是因?yàn)槟闵洗螆?zhí)行的 有錯才導(dǎo)致 import 錯誤的。

接下來你需要刪掉 templates/hello_form.html 和 templates/index.html,然后重新創(chuàng)建上面代碼中提到的兩個(gè)模板。這里是一個(gè)非常簡單的 templates/show_room.html 供你參考:

$def with (room)

<h1> $room.name </h1>

<pre>
$room.description
</pre>

$if room.name == "death":
    <p><a href="/">Play Again?</a></p>
$else:
    <p>
    <form action="/game" method="POST">
        - <input type="text" name="action"> <input type="SUBMIT">
    </form>
    </p>

這就用來顯示游戲中的房間的模板。接下來,你需要在用戶跑到地圖的邊界時(shí),用一個(gè) 模板告訴用戶他的角色的死亡信息,也就是 templates/you_died.html 這個(gè)模板:

<h1>You Died!</h1>

<p>Looks like you bit the dust.</p>
<p><a href="/">Play Again</a></p>

準(zhǔn)備好了這些文件,你現(xiàn)在可以做下面的事情了:

1.讓測試代碼 tests/app_tests.py 再次運(yùn)行起來,這樣你就可以去測試這個(gè)游戲。由于會話的存在,你可能頂多只能實(shí)現(xiàn)幾次點(diǎn)擊,不過你應(yīng)該可以做出一些基本的測試來。 2.刪除 sessions/*下的文件,再重新運(yùn)行一遍游戲,確認(rèn)游戲是從一開始運(yùn)行起來的。 3.執(zhí)行 python bin/app.py 腳本,試玩一下你的游戲。

你需要和往常一樣刷新和修正你的游戲,慢慢修改游戲的 HTML 文件和引擎,直到你實(shí)現(xiàn)游戲需要的所有功能為止。

你的期末考試

你有沒有覺著我一下子給了你超多的信息呢?那就對了,我想要你在學(xué)習(xí)技能的同時(shí)可以有一些可以用來鼓搗的東西。為了完成這節(jié)習(xí)題,我將給你最后一套需要你自己完成的練習(xí)。你將注意到,到目前為止你寫的游戲并不是很好,這只是你的第一版代碼而已。你現(xiàn)在的任務(wù)是通過做這些事讓游戲更加完善:

1.修正代碼中所有我提到和沒提到的 bug,如果你發(fā)現(xiàn)了新的 bug,你可以告訴我。 2.改進(jìn)所有的自動測試,讓你可以測試更多的內(nèi)容,直到你可以不用瀏覽器就能測到所有的內(nèi)容為止。 3.讓 HTML 頁面看上去更美觀一些。 4.研究一下網(wǎng)頁登錄系統(tǒng),為這個(gè)程序創(chuàng)建一個(gè)登錄界面,這樣人們就可以登錄這個(gè)游戲,并且可以保存游戲高分。 5.完成游戲地圖,盡可能地把游戲做大,功能做全。 6.給用戶一個(gè)“幫助系統(tǒng)”,讓他們可以查詢每個(gè)房間里可以執(zhí)行哪些命令。 7.為你的游戲添加新功能,想到什么功能就添加什么功能。 8.創(chuàng)建多個(gè)地圖,讓用戶可以選擇他們想要玩的一張來進(jìn)行游戲。你的 bin/app.py 應(yīng)該可以運(yùn)行提供給它的任意的地圖,這樣你的引擎就可以支持多個(gè)不同的游戲。 9.最后,使用你在習(xí)題 48 和 49 中學(xué)到的東西來創(chuàng)建一個(gè)更好的輸入處理器。你手頭已經(jīng)有了大部分必要的代碼,你只需要改進(jìn)語法,讓它和你的輸入表單以及游戲引擎掛鉤即可。

祝你好運(yùn)!

常見問題

Q: 我在游戲中使用了 sessions,但是我沒辦法利用 nosetests 來測試它

你需要了解 sessions 的機(jī)制。http://webpy.org/cookbook/session_with_reloader。

Q: 我遇到報(bào)錯 ImportError

錯誤的目錄。 錯誤的 Python 版本。 PYTHONPATH 沒有設(shè)置,沒有init.py 文件,檢查 import 中的拼寫錯誤。