“笨办法”学Python 3基础篇 - 搭建简易的网站

“笨办法”学Python 3基础篇系列文章

“笨办法”学Python 3基础篇 第一部分-打印与输入
“笨办法”学Python 3基础篇 第二部分-文件操作
“笨办法”学Python 3基础篇 第三部分-函数
“笨办法”学Python 3基础篇 第四部分-数据容器与程序结构
“笨办法”学Python 3基础篇 第五部分-面向对象的类
“笨办法”学Python 3基础篇 第六部分-项目骨架与自动测试
“笨办法”学Python 3基础篇 第七部分-搭建简易的网站


搭建简易的网站


前言

终于来到了本系列的最后一篇了,有点小兴奋。虽然中间间隔了很多时间,但每一次码字都让我懂得了坚持和总结带来的好处,都能够再一次地对原来储存在脑海中某个角落的知识进行重构,变成自己知识大厦中的地基,脑子也在每次总结中进行了一次精神地“按摩”。
在本文中,我将用面向对象的编程方法编写一个对话类的三体小游戏(章北海作为主角),并通过flask创建简单的游戏网站(目前只学到这)。

7.1 Web框架-flask安装和项目创建

要使用网站服务,需要安装一个“Web”框架,减少开发负担。在书中,主要介绍了flask框架的应用。在激活虚拟环境后,通过

pip install flask

实现安装,安装过程如下图:
“笨办法”学Python 3基础篇 - 搭建简易的网站
flask安装完成后,需要创建一个threebody项目,在PowerShell中输入如下命令:

cd projects
mkdir threebody
cd threebody
mkdir bin
mkdir threebody
mkdir tests
mkdir docs
mkdir templates
new-item -type file .\threebody\__init__.py
new-item -type file .\tests\__init__.py

与之前项目创建不同的是,新建了一个名为“templates”的目录,用于存放网站的html模板。

7.2 利用面向对象方法设计三体游戏

在threebody\threebody目录下新建一个game.py作为三体游戏的主程序。整个三体游戏的设计思路是通过场景切换进行的,游戏者通过做出选择进入不同剧情,类似《秋之回忆》之类的游戏。程序的核心为类Scene的设计,代码具体为:

class Scene:
    """基本场景类"""
    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.paths = {}
    
    def enter(self, direction):
        return self.paths.get(direction, None)

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

每个场景作为对象对类Scene进行初始化。初始化时渲染场景的名字和剧情描述,并初始化决定剧情走向的字典paths。通过enter(direction)函数返回direction指定的场景对象,通过add_paths(paths)函数来更新剧情走向字典paths,从而建立不同场景对象的剧情联系。关于剧情的走向脉络如下图所示:
“笨办法”学Python 3基础篇 - 搭建简易的网站
游戏剧情从自然选择号开始,到太空陵墓结束。中间每个场景如果选择错误,就会进入死亡场景(根据剧情不同,死亡场景还需要细分)。在死亡场景中可以通过选择play again重玩游戏。这一剧情流向通过Scene中的add_paths实现。具体代码如下:

natureSelectionFleet.add_paths({
    '1': death_1_1,
    '2': death_1_2,
    '3': death_1_3,
    '4': escape,
    '*': natureSelectionFleet 
})

escape.add_paths({
    '1': death_2,
    '2': death_2,
    '3': death_2,
    '4': control_terminal,
    '*': escape
})       
        
control_terminal.add_paths({
    '1': extrasolar,
    '2': death_3,
    '3': death_3,
    '4': death_3,
    '*': control_terminal
})

extrasolar.add_paths({
    '1': fleet_earth, 
    '2': death_2,
    '3': death_4,
    '*': extrasolar
})

fleet_earth.add_paths({
    '1': death_2,
    '2': ultimate_rule_fleet,
    '3': ultimate_rule_fleet,
    '*': fleet_earth
})

ultimate_rule_fleet.add_paths({
    '1': blue_space_fleet,
    '2': death_6,
    '3': death_5,
    '4': death_2,
    '*': ultimate_rule_fleet
})

blue_space_fleet.add_paths({
    '1': last_episode,
    '2': death_6,
    '*': blue_space_fleet
})

这里的*代表玩家的无效输入,无效输入时,剧情场景不切换,停留在原场景中。剩下的工作是设计不同的场景对象,具体的代码为:

#自然选择号死亡场景
death1 = """
        自然选择号在与三体文明'水滴'探测器的激战中被摧毁......
         """

death_1_1 = Scene("游戏失败",
f"""
原来你也接收了思想钢印。
警卫员!
马上逮捕章北海!

{death1}
""")
      
death_1_2 = Scene("游戏失败",
f"""
章北海,你这个懦夫!
你的内心充分暴露出来了,表面是个人类必胜论者,
内心确是一个失败论者。
警卫员!
马上逮捕章北海!

{death1}
""")

death_1_3 = Scene("游戏失败",
f"""
你是云天明?
警卫员!
马上通知地球舰队!

{death1}
""")

death_2 = Scene("游戏失败", death1)

death_3 = Scene("游戏失败", 
f"""
飞船控制失败!

{death1}
"""
)

death_4 = Scene("游戏失败", 
f"""
东方延绪:
        章北海, 我代表星舰地球逮捕你。
        全员准备,返航迎击三体人的探测器。

        {death1}
""")

death_5 = Scene("游戏失败",
f"""
在更改航线后,遭遇了三体文明的舰队,被击毁...
""")

death_6 = Scene("游戏失败",
f"""
遭遇了黑暗森林法则,被星舰地球中的其它战舰次声波核弹攻击,
全员阵亡!
""")

#自然选择号
natureSelectionFleet = Scene("自然选择号", 
"""
东方延绪:
        章北海执行舰长,我是自然选择号的舰长东方延绪。欢迎您。
        现在我把自然选择号的指挥权交给您。飞船有四种前进模式,
        当进入前进4模式时,船员必须进入睡眠仓。现在,我需要
        再次确认您是否受到思想钢印的影响,请您回答我这个问题:
        '人类是否必胜?'
        [1] 必胜
        [2] 必败
        [3] 这要取决于我所见到的三体文明
        [4] 人的意志胜过一切
""")

#逃离太阳系
escape = Scene("自然选择号战舰控制中心",
"""
东方延绪:
        章北海舰长,现在我交出自然选择号的控制权。据地球舰队传来的最新消息:
        三体文明的探测器-水滴-已经抵达太阳系的边缘,15分钟后将与地球舰队接触。
        现在飞船等待您发布第一条命令:
        [1] 前进1 逃离太阳系
        [2] 前进2 逃离太阳系
        [3] 前进3 逃离太阳系
        [4] 前进4 逃离太阳系
""")

#飞控终端
control_terminal = Scene("自然选择号战舰飞控终端","""
警告!
警告!
警告!
飞船进入前进4必须通知所有船员进入深海睡眠模式。请输入飞控密码:
[1] Man Always Remember Love Because Of Romance Only
[2] If You Give Me A Pivot Point I Could Move The Earth
[3] The Sun Rather Than The Earth At The Center Of The Universe
[4] Two Heads Are Always Better Than One
""") 

# 太阳系外
extrasolar = Scene("自然选择号战舰","""
飞船已经飞出了太阳系,身后的蓝色空间号、深空号、终极规律号以及企业号正在追击。
下一步命令是:
[1] 延续人类文明,向前飞,向远飞
[2] 掉头击毁追击战舰
[3] 将舰队指挥权交给东方延续舰长
"""
)

# 星舰地球
fleet_earth = Scene("星舰地球","""
地球舰队已经被水滴摧毁.......
五艘战舰必须延续文明的责任,太空将是我们最后的归宿。星舰地球决定:
[1] 处决人类的叛徒章北海,回去与水滴战斗
[2] 处决人类的叛徒章北海,继续飞往十八光年外的天鹅座NH558J2
[3] 章北海是英雄,继续飞往十八光年外的天鹅座NH558J2
"""
)

#终极规律号
ultimate_rule_fleet = Scene("终极规律号","""
终极规律号战舰中出现了一种不安的气氛......
十四光年,飞船的资源是有限的,如何得到补给?
舰长,我们该怎么办?
[1] 向其它四艘战舰发射次声波氢弹
[2] 向其它四艘战舰求援,请求补给
[3] 要求星舰地球更改航程,找到补给点
[4] 回地球吧
"""
)

#蓝色空间号
blue_space_fleet = Scene("蓝色空间号","""
警告!
警告!
警告!
终极规律号的次声波氢弹冲击波即将到达....
防御成功,舰队损伤5%...
等待下一步命令:
[1] 发射高能伽马射线激光击毁终极规律号
[2] 逃离
""")

#最后的场景
last_episode = Scene("太空陵墓","""
蓝色空间号带走了所有的燃料和配件,并将自然选择号、企业号、深空号的
残骸切割多段,围城巨石阵,构建了一处太空陵墓,纪念人类第一次体会到
宇宙‘黑暗森林’的可怕。

恭喜你,顺利通过了剧情!
"""
) 

START = 'natureSelectionFleet'
# 返回场景名字对应的场景对象
def load_scene(name):
    return globals().get(name) 
# 返回场景对象对应的场景名字
def name_scene(scene):
    for key, value in globals().items():
        if value == scene:
            return key

7.3 编写game.py的自动测试脚本

接下来,将编写game.py的自动测试脚本。在tests\目录下新建game_tests.py,测试工具仍然为nosetests。分别对game.py中Scene的初始化函数、add_paths和enter函数进行测试。具体代码为:

from nose.tools import *
from threebody.game import *


def test_scene():
    gold = Scene("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_add_enter_paths():
    center = Scene("Center", "Test room in the center.")
    north = Scene("North", "Test room in the north.")
    south = Scene("South", "Test room in the south.")

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


def test_game_map():
    start_room = load_scene(START)
    assert_equal(start_room.enter('1'), death_1_1)
    assert_equal(start_room.enter('2'), death_1_2)
    assert_equal(start_room.enter('3'), death_1_3)
    assert_equal(start_room.enter('4'), escape)
    assert_equal(start_room.enter('*'), natureSelectionFleet)

需要注意的是,在引入特性时,需要从threebody\game.py中引入所有提到的类和全局变量。PowerShell中的测试结果为:
“笨办法”学Python 3基础篇 - 搭建简易的网站

7.4 编写HTML模板

在三体游戏中,我们采用了布局模板和POST表单。在templates\目录下创建show_room.html和layout.html两个文件。布局模板代码为:

<html>
    <head>
        <title>三体游戏</title>
    </head>
    <body>
        {% block content %}

        {% endblock %}
    </body>
</html>

主要包含标题“三体游戏”。POST表单代码为:

{% extends "layout.html" %}

{% block content %}

<h1>{{ room.name }}</h1>

<pre>
    {{ room.description }}
</pre>

{% if room.name == "游戏失败"%}
    <p><a href="/">Play Again?</a></p>
{% elif room.name == "太空陵墓" %}
    <p><a href="/">退出游戏</a></p>
{% else %}
    <p>
        <form action='/play' method='POST'>
            - <input type='text' name='action'><input type='SUBMIT'>
        </form>
    </p>    
{% endif %}

{% endblock %}

在这个html文件中,首先加载布局模板,其次将场景对象的名字和剧情描述显示在网页上。随后启用 if-else 条件判断语句,当检测到场景名为“游戏失败”的字符串时,在剧情描述底部显示 “play again?”;当检测到场景名为“太空陵墓”的字符串时,在剧情描述底部显示“退出游戏”;否则就显示文本框,等待用户输入信息推动剧情发展。

7.5 创建三体web游戏引擎

游戏引擎的python文件应创建在项目主目录下,即第一个threebody目录下。我创建了一个名为app.py的web引擎文件。主要引用了flask网络框架中的FLASK, session, redirect,url_for, request,render_template特性。当然,为了运行游戏,还需要引入game.py。具体代码如下:

from flask import Flask, session, redirect, url_for, request
from flask import render_template
from threebody import game

app = Flask(__name__)

@app.route("/")
def index():
    #this is used to "setup" session with starting values
    session['scene_name'] = game.START #利用会话临时存储场景名字
    return redirect(url_for("play"))


@app.route("/play", methods=['POST', 'GET'])
def play():
    scene_name = session.get('scene_name') #获得当前场景的名字

    if request.method == 'GET':
        room = game.load_scene(scene_name) #通过场景的名字加载场景对象
        return render_template("show_room.html", room=room)    
 
    else:
        action = request.form.get('action')
        
        if scene_name and action:
            room = game.load_scene(scene_name)
            next_scene = room.enter(action) #获取下一个场景的对象
            if not next_scene:
                next_scene = room.enter('*')
                #将下一个场景的名字存在临时会话中,供web服务器使用
                session['scene_name'] = game.name_scene(next_scene)
            else:
                session['scene_name'] = game.name_scene(next_scene)
        return redirect(url_for("play"))

# YOU SHOULD CHANGE THIS IF YOU PUT ON THE INTERNET
app.secret_key = 'sdjffiweriwuer923riew3shfs'

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

程序的第一个主要函数为index(), 代表了主目录“/”,使用了session临时会话来存储场景名字,以工下次调用时使用。返回“/play"目录。程序第二个主要函数为play()。在"/play"目录下有两种模式POST或GET,当未获得用户输入时,默认为GET,此时引擎加载show_room.html文件。当用户输入有效信息后,进入post模式,此时引擎根据信息得到下一个剧情场景,并存储在session会话中,返回play目录进行下一场景加载。最后,通过app.run()语句来自动运行web游戏引擎。

7.6 游戏运行效果

在Poweshell中,运行

python app.py

结果为:
“笨办法”学Python 3基础篇 - 搭建简易的网站
此时debug模式是关闭的,如果要打开,则将app.run()修改为app.run(debug = True)。在本地网页端运行时,根据提示,在浏览器上输入localhost:5000/即可。效果如下图:
“笨办法”学Python 3基础篇 - 搭建简易的网站

结语

这是CSDN上自己写的第一个系列的文章,内容虽然没有什么新意,但坚持写作,把自己的思路理的非常清晰,也知道了什么地方还没有掌握。接下来,要沉静一段时间,继续学习,坚持写作。

上一篇:ThreadLocal的进化——TransmittableThreadLocal


下一篇:生产者消费者问题--lock