寻宝游戏
一.实验目的
1.练习 MongoDB 操作,学习如何设计数据库
2.练习 Python 的 Flask 框架
3.学会用 pytest 测试
4.学会用定时任务执行函数
二.实验要求
考虑以下游戏场景:
- 每个游戏玩家都有一定数量的金币、宝物。有一个市场供玩家们买卖宝物。玩家可以将宝物放到市场上挂牌,自己确定价格。其他玩家支付足够的金币,可购买宝物。
- 宝物分为两类:一类为工具,它决定持有玩家的工作能力;一类为配饰,它决定持有玩家的运气。
- 每位玩家每天可以通过寻宝获得一件宝物,宝物的价值由玩家的运气决定。每位玩家每天可以通过劳动赚取金币,赚得多少由玩家的工作能力决定。(游戏中的一天可以是现实中的 1 分钟、5 分钟、10 分钟。自主设定。)
- 每个宝物都有一个自己的名字(尽量不重复)。每位玩家能够佩戴的宝物是有限的(比如一个玩家只能佩戴一个工具和两个配饰)。多余的宝物被放在存储箱中,不起作用,但可以拿到市场出售。
- 在市场上挂牌的宝物必须在存储箱中并仍然在存储箱中,直到宝物被卖出。挂牌的宝物可以被收回,并以新的价格重新挂牌。当存储箱装不下时,运气或工作能力值最低的宝物将被系统自动回收。
- 假设游戏永不停止而玩家的最终目的是获得最好的宝物。
请根据以上场景构建一个假想的 Web 游戏,可供多人在线上玩耍。界面尽可能简单(简单文字和链接即可,不需要 style)。后台的数据库使用 MongoDB。对游戏玩家提供以下几种操作:寻宝(可以自动每天一次)、赚钱(可以自动每天一次)、佩戴宝物、浏览市场、买宝物、挂牌宝物、收回宝物。
提交:程序 + 文档
要求:
- 文档主要用于解释你的数据库设计,即需要构建哪些 collection,每个 collection 的文档结构是什么,需要构建哪些索引,应用如何访问数据库(具体的 CRUD 命令);
- 为玩家的操作设计 JSON HTTP 协议的接口,自定义接口格式(request 和 response 的 JSON);为每个接口编写测试用例和测试代码;
- 不限制编程语言及 Web 框架。
三.代码执行顺序及使用方法
整个项目代码位于 flaskProject 文件夹中。
1.执行 init_db.py,初始化 treasures 宝物库。
2.执行 app.py,后台自动运行寻宝和赚钱进程,每 20s 结果会显示在命令行上;登录 localhost:5000/ 即可进入登录界面,登录界面对应 templates/index.html
3.用浏览器在 localhost:5000/中执行注册/登录操作进入用户页面,用户界面对应 templates/game.html
4.在用户界面网页中允许用户使用 post form 提交表单来执行操作,也可以直接遵循 app.py 中的路由规则用 url 的 get 执行相关操作
(如果用 postman 测试,还支持 post JSON 的输入)
5.如果要用 pytest,在命令行中输入 pytest 即可,pytest 的配置文件为_init_.py 和 conftest.py,pytest 会按顺序运行 test_user.py 中的函数。(注意运行时要先把 MongoDB 中 markets 和 players 两个 collection 更改为 collection markets.json 和 collection players 两个 JSON 文件的内容,删除当前数据然后用 MongoDB compass 直接导入即可,否则有些测试代码如买卖商品会无法执行。)
6.为了重现本项目测试的过程,我将用到的四个 collection 中的数据存了下来,分别为 collection markets.json,collection players.json,collection treasures.json 和 picurl.json,后两个是在 init_db.py 时自动生成的不用手动去建立,前两个是用户使用过程中产生的,可以直接往名为 webgame 的数据库中的 markets 和 players collection 中导入。
四.实验过程
1.数据库设计
分为四个 collection:分别是 treasures,players,markets 和 picurl,其中前三个是必须的
连接的数据库是 MongoDB
E-R 图如下:
1.1 treasures
treasures 是一个宝物库,是静态的,在 ini_db.py 中提前执行一遍即可建立,需要修改宝物库时再重新运行 ini_db.py 即可。
treasures 的结构类似以下:
python
{"name": "10级工具", "property": "T", "level": 10}
name 表示物品名字,property 表示用途(T=tool 工具,A=accessory 饰品),level 表示宝物等级
建立关于 name 的索引:
collection 内容如下:
1.2 players
players 存储用户信息,在新用户注册时会添加文档,对应函数为 app.py 中的 register(username, password)函数。
players 的结构类似以下:
python
{"name": username,
"money": 1000,
"password": password,
"treasure": {"T": "1级工具", "A": "1级饰品"},
"box": []}
name 表示用户名字,money 表示用户金币数,password 是用户登录密码,treasures 是一个字典存当前装备的工具和饰品名,box 中的列表装用户多余的物品(最大值为 10 个)
建立关于 name 的索引:
collection 内容如下:
1.3 markets
markets 储存市场信息,当有人出售或购买物品时会往该 collection 添加或删除文档。
markets 的结构类似以下:
python
{"name": treasure, "price": price, "owner": username}
name 表示商品名字,price 表示商品价格,owner 表示商品出售者。
collection 内容如下:
1.4 picurl
这个 collection 不是必须的,之前三个 collection 已经建好了,我还想把宝物和图片联系起来就加了这个数据库,是为了实现自己附加的查看工具图片模样的功能,它存储每样工具图片的路径。
markets 的结构类似以下:
{"name": "10级工具.jpg"}
name 表示工具的名字路径
图片被放在 static 文件夹下:
collection 内容如下:
2.基本功能函数实现(登录,cwur 等)
为了满足 pytest 的要求,除了登录注册,返回的都是 JSON 类型的数据。
2.1 登录/注册
index.html 的登录页的表单数据会被传到 login 函数中,login 函数会判断用户是否注册,未注册会跳转到 register 完成注册,已注册直接进入用户界面 game.html,密码错误会提示
```python
以下是不同表单的处理函数,跳转到对应的后端函数中
@app.route('/process', methods=['POST', 'GET']) def process(): if request.method == 'POST': username = request.form.get("Name") password = request.form.get("Password") return login(username, password) ```
```python
登录,转到用户主页
def login(username, password): players.create_index([("name", pymongo.ASCENDING)], unique=True) if players.find_one({"name": username}) is None: return register(username, password) else: if players.find_one({"name": username})['password'] != password: return "
玩家 %s 密码错误请重新输入
" % username user_dict = str(show_dict(players.find_one({"name": username}))) return render_template('game.html', Name=username, Userdict=user_dict)注册
def register(username, password): var = players.insert_one({"name": username, "money": 1000, "password": password, "treasure": {"T": "1级工具", "A": "1级饰品"}, "box": []}).inserted_id return "
玩家 %s 注册成功,请返回登录页面
" % username + "" ```
密码错误会无法进入游戏页面
登录注册的界面见后面前端展示,也可以 py app.py 后访问 localhost:5000 直接查看。
2.2 查看用户箱子
查看某用户的箱子,用法例如 localhost:5000/qk/box
```python def look_box(username): answer = show_dict(players.find_one({"name": username}))
answer["answer"] = "这是%s的box返回结果:" % username
return jsonify(answer)
```
2.3 浏览市场
查看市场,用法例如 localhost:5000/qk/market
```python
浏览市场
def look_market(username): # 没找到用户 if players.find_one({"name": username}) is None: return "
请先注册用户
" # 显示market res = { "answer": "玩家%s查看市场" % username } for treasure in markets.find(): res["%s" % treasure["_id"]] = show_dict(treasure) return jsonify(res) ```
2.4 佩戴宝物
配戴宝物,用法例如 localhost:5000/qk/wear/10 级工具,如果箱子没有或者宝物库没有则配戴失败。
```python
佩戴宝物
def wear(username, treasure): # box中没有该宝物 if treasures.find_one({"name": treasure}) is None: return jsonify({"error": "该宝物名不存在"}) ''' return "
宝物库中没有 %s 宝物
" % treasure + "" + \ str(show_dict(players.find_one({"name": username}))) '''
# 要佩戴的宝物类型
t_class = treasures.find_one({"name": treasure})['property']
# 要替换的当前佩戴在身上的该类型宝物
original = players.find_one({"name": username})["treasure"][t_class]
# 用flag判断宝箱中有没有该宝物
flag = 0
box = players.find_one({'name': username})['box']
player_treasure = players.find_one({"name": username})["treasure"]
for t in box:
if t == treasure:
box.remove(t)
box.append(original)
player_treasure[t_class] = treasure
# 更新宝箱和佩戴的宝物
players.update_one({"name": username}, {"$set": {"box": box}})
players.update_one({"name": username}, {"$set": {"treasure": player_treasure}})
flag = 1
answer = show_dict(players.find_one({"name": username}))
answer["answer"] = "玩家%s穿戴成功" % username
return jsonify(answer)
if flag == 0:
return jsonify({"error": "存储箱没有该宝物"})
```
2.5 购买宝物
购买宝物,用法例如 localhost:5000/qk/buy/9 级工具,如果市场没有或者钱不够则购买失败。
```python
购买宝物
def buy(username, treasure): # 市场没有该宝物 if markets.find_one({"name": treasure}) is None: return jsonify({"error": "市场无此宝物"}) ''' return "
市场暂无 %s 宝物
" % treasure + "" \ + str(show_dict(players.find_one({"name": username}))) ''' player = players.find_one({"name": username}) box1 = player['box'] if len(box1) >= 10: recovery_treasure(username) box = player['box'] box.append(treasure) players.update_one({"name": username}, {"$set": {"box": box}}) treasure_money = sys.maxsize id_ = markets.find_one({"name": treasure})[' id'] # 用id进行记录,因为市场重复 for thing in markets.find({"name": treasure}): if int(thing['price']) < treasure_money: treasure_money = int(thing['price']) id = thing[' id'] money1 = player['money'] - treasure_money # 买不起 if money1 < 0: return jsonify({"error": "余额不足"}) players.update_one({"name": username}, {"$set": {"money": money1}}) owner = markets.find_one({"_id": id })['owner'] money2 = players.find_one({"name": owner})['money'] + treasure_money players.update_one({"name": owner}, {"$set": {"money": money2}}) # 市场删除该宝物 markets.delete_one({"name": treasure}) return jsonify({"answer": "购买完成,请查看背包"}) ```
2.6 撤回宝物
撤回宝物,用法例如 localhost:5000/qk/withdraw/10 级饰品,如果市场没有则撤回失败。
```python
收回挂牌宝物
def withdraw(username, treasure): # 市场没有该宝物 if markets.find_one({"name": treasure, "owner": username}) is None: return jsonify({"error": "市场无此宝物"}) ''' return "
市场没有 %s 宝物
" % treasure + "" \ + str(show_dict(players.find_one({"name": username}))) ''' # 市场删除宝物 markets.delete_one({"name": treasure, "owner": username}) # 玩家收回宝物 box = players.find_one({"name": username})['box'] if len(box) >= 10: recovery_treasure(username) box = players.find_one({"name": username})['box'] box.append(treasure) players.update_one({"name": username}, {"$set": {"box": box}}) return jsonify({"answer": "收回成功,请查看背包"}) ```
2.7 出售宝物
出售宝物,用法例如 localhost:5000/qk/sell/10 级饰品/1000,如果背包没有则出售失败。
```python
出卖宝物
def sell(username, treasure, price): box = players.find_one({'name': username})['box'] if treasure not in box: return jsonify({"error": "存储箱没有该宝物"}) price = int(price) player = players.find_one({"name": username}) # 卖家宝物到位 box = player['box'] for t in box: if t == treasure: box.remove(t) break players.update_one({"name": username}, {"$set": {"box": box}}) # 市场宝物到位 markets.insert_one({"name": treasure, "price": price, "owner": username}) return jsonify({"answer": "挂牌成功,请查看市场"}) ```
3.定时任务(寻宝 + 赚钱)函数实现
用到了 apscheduler 这个库,作用是定时执行程序
from flask_apscheduler import APScheduler
再实现需要自动执行的函数,这里是 find_treasure 和 find_money 函数
```python
自动寻宝
def find_treasure(): # 遍历每个玩家 for player in players.find(): name = player["name"] # 宝箱已满 if len(player['box']) >= 10: print("存储箱已满将回收一件低端宝物") recovery_treasure(name) # 得到的宝物和饰品的级别有关 box = players.find_one({"name": name})['box'] wear_treasure_name = player['treasure']['A'] wear_treasure_level = treasures.find_one({"name": wear_treasure_name})['level'] ls = [] for col in treasures.find({"level": {"$lte": wear_treasure_level + 2, "$gte": wear_treasure_level - 2}}): ls.append(col) # 随机寻宝 x = random.randint(0, len(ls) - 1) box.append(ls[x]['name']) # 更新宝物 players.update_one({"name": name}, {"$set": {"box": box}}) print("玩家 %-6s 获得宝物 %s" % (name, ls[x]['name']))
自动赚钱
def find_money(): # 遍历每个玩家 for player in players.find(): wear_treasure_name = player['treasure']['T'] wear_treasure_level = treasures.find_one({"name": wear_treasure_name})['level'] # 得到的金钱和工具的级别有关 money_get = random.randint((wear_treasure_level - 1) * 100, (wear_treasure_level + 1) * 100) # 打入账户 money = player['money'] + money_get name = player["name"] # 更新账户 players.update_one({"name": name}, {"$set": {"money": money}}) print("玩家 %-6s 金币到账 %d" % (name, money_get)) ```
写一个配置自动任务的类,将 find_treasure 和 find_money 设定为每 20 seconds 执行一次
```python
配置自动任务的类
class Config(object): JOBS = [ { 'id': 'job1', 'func': ' main :find_treasure', 'trigger': 'interval', 'seconds': 20,
},
{
'id': 'job2',
'func': '__main__:find_money',
'trigger': 'interval',
'seconds': 20,
}
]
```
最后在主函数中执行
python
if __name__ == "__main__":
app.config.from_object(Config()) # 配置自动执行任务,后台寻宝赚钱
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
app.run() # app开始运行
可以在终端看到结果
4.附加功能函数实现
为了游戏的丰富性、完整性和个性化,我添加了 pic_find,merge 和 finish 函数,功能如下图所示
注:finish 的通关指的是同时获得 10 级工具和 10 级饰品,缺一不可,merge 的融合需要付 100 金币
函数名 | 参数 | 功能 |
---|---|---|
pic_find | pic_url 表示图片路径 | 查看某样宝物的图片 |
merge | username 表示用户名, treasure 表示第一个宝物, treasure2 表示第二个宝物 | 融合两件宝物成随机一件宝物 |
finish | username 表示用户名 | 检查该用户是否通关 |
python
def pic_find(pic_url):
if pictures.find_one({"name": pic_url}) is None:
return "<h1>找不到该图片信息</h1>"
return render_template('picture.html', Name=pic_url)
```python
融合宝物
def merge(username, treasure, treasure2): player = players.find_one({"name": username}) box = player['box'] if player['money'] < 1000: return "
操作失败,当前金币小于1000无法寻宝
" + "" \ + str(show_dict(players.find_one({"name": username})))
if treasure not in box:
return "<h1>操作失败,存储箱没有 %s 宝物</h1>" % treasure + "<br><br>" \
+ str(show_dict(players.find_one({"name": username})))
if treasure2 not in box:
return "<h1>操作失败,存储箱没有 %s 宝物</h1>" % treasure2 + "<br><br>" \
+ str(show_dict(players.find_one({"name": username})))
if treasure == treasure2:
num = 0
for t in box:
if t == treasure:
num += 1
if num < 2:
return "<h1>操作失败,存储箱没有两件 %s 宝物</h1>" % treasure2 + "<br><br>" \
+ str(show_dict(players.find_one({"name": username})))
for t in box:
if t == treasure:
box.remove(t)
break
for t in box:
if t == treasure2:
box.remove(t)
break
ls = []
for col in treasures.find():
ls.append(col)
# 随机寻宝
x = random.randint(0, len(ls) - 1)
new_treasure_name = ls[x]['name']
box.append(ls[x]['name'])
players.update_one({"name": username}, {"$set": {"box": box}})
money1 = player['money'] - 100
players.update_one({"name": username}, {"$set": {"money": money1}})
return "<h1>融合成功,得到 %s ,请查看背包,100元已经扣除</h1>" % new_treasure_name + "<br><br>" + \
str(show_dict(players.find_one({"name": username})))
```
python
def finish(username):
player = players.find_one({"name": username})
box = player['box']
player_treasure = player["treasure"]
flag_t = 0
flag_a = 0
for t in box:
if t == "10级工具":
flag_t = 1
break
for t in box:
if t == "10级饰品":
flag_a = 1
break
if player_treasure["T"] == "10级工具":
flag_t = 1
if player_treasure["A"] == "10级饰品":
flag_a = 1
if flag_a == 1 and flag_t == 1:
return "<h1>通关成功,谢谢游玩</h1>" + "<br><br>" \
+ str(show_dict(players.find_one({"name": username})))
else:
return "<h1>不好意思,您还未集齐10级工具和10级饰品,请继续游玩</h1>" + "<br><br>" \
+ str(show_dict(players.find_one({"name": username})))
演示如下:
5.pytest 测试
pytest 主要用到 get 路由的方法,不用访问前端也可完成。
app.py 中通过 route 来执行 get 方法,写了个 find_method 作为函数的中转站
pytest 的配置在 conftest.py 中,pytest 中的测试函数 test_user.py 中包含 13 个函数测试 crud,用 get 路由去访问
```python
根据不同参数设置不同路由,用于url的访问
@app.route("/
一个中转站根据路由进行重定向
def find_method(username, operation, treasure='test', treasure2='test', price=0): if operation == 'login': return login(username, "123456") elif operation == 'box': return look_box(username) elif operation == 'market': return look_market(username) elif operation == 'wear': return wear(username, treasure) elif operation == 'buy': return buy(username, treasure) elif operation == 'withdraw': return withdraw(username, treasure) elif operation == 'sell': return sell(username, treasure, price) elif operation == 'merge': return merge(username, treasure, treasure2) elif operation == 'finish': return finish(username) else: return "
输入或操作错误
" ```test_user.py 的函数都类似以下,一共 13 个,具体见 test_user.py
python
def test_market(client: FlaskClient):
response = client.get("/qk/market")
json = response.get_json()
print(json)
assert json["answer"] == "玩家qk查看市场"
测试结果如下:
注意测试前要将 MongoDB 数据库中 markets 和 players 设置成 collection markets.json 和 collection players.json(在 flaskProject 文件夹中,建议删除原 collection 然后用 MongoDB compass 直接导入)
直接 pytest
用 coverage 测试
coverage 的测试结果
6.前端展示
前端代码位于 templates 文件夹中,可以满足用表单的 post 请求来对数据库执行操作,需要 import request
python
from flask import Flask, render_template, request
app.py 中以 process 开头的函数都是处理表单的函数,从 form 中得到参数,调用相应函数即可
前端页面展示如下:
登录注册页面
用户开始游戏页面:
点击按钮的结果(以查看背包和查看市场为例子,返回 JSON,结果和直接用 get 方法是一样的但是这样用 post 可以保护个人信息):
查看背包
查看市场
五.注意事项
- 一开始所有函数返回的都是 HTML 文本,导致难以转变为 JSON 无法进行 pytest 测试,后来全部统一改成字典 jsonify 成 JSON 数据得以解决。
- JSON 输出会出现十六进制乱码,解决方法如下,app.py 的 JSON 配置中加以下两行
python
# 为了防止json中文乱码请加入这两行
app.config['JSON_AS_ASCII'] = False
app.config['JSONIFY_MIMETYPE'] = "application/json;charset=utf-8"
3. 当函数中往往需要输出 players.find_one({"name": username})),但是直接输出甚至会暴露用户的密码,所以要用一个函数去把字典中密码的部分去掉,用到以下函数
python
# 处理每个操作返回的结果,
def show_dict(dictionary):
dict_ = {}
for key in dictionary.keys():
if key != '_id' and key != "password":
dict_[key] = dictionary[key]
return dict_
4. 用户的箱子只能存十个,超过十个时要回收等级最低的宝物,所以需要如下函数
python
# 如果宝箱充满系统回收等级最低宝物
def recovery_treasure(name):
box = players.find_one({"name": name})['box']
treasure_name = box[0]
level = treasures.find_one({"name": box[0]})['level']
# 找到等级最低宝物
for treasure in box[1:]:
temp = treasures.find_one({"name": treasure})['level']
if temp < level:
level = temp
treasure_name = treasure
# 删除该宝物
for treasure in box:
if treasure == treasure_name:
box.remove(treasure)
break
# 更新宝箱
players.update_one({'name': name}, {"$set": {"box": box}})
print("玩家 %-6s 被系统回收宝物 %-6s" % (name, treasure_name))
参考文献
- 基于asp.net的在线软件项目交易系统的设计与实现(电子科技大学·顾杰)
- 分布式在线旅游搜索爬虫系统设计与实现(北京邮电大学·徐显炼)
- 主题搜索引擎搜索策略的研究及算法设计(兰州大学·高庆芳)
- 博客搜索引擎与排名技术研究(江南大学·严磊)
- 基于.NET自定义控件的社区网站系统研究与实现(武汉理工大学·刘亚)
- 网络游戏虚拟物品交易系统设计与实现(吉林大学·李云峰)
- 面向特定网页的Web爬虫的设计与实现(吉林大学·马慧)
- 面向游戏垂类的搜索系统的设计与实现(北京交通大学·马振领)
- 分布式在线旅游搜索爬虫系统设计与实现(北京邮电大学·徐显炼)
- 网络游戏虚拟物品交易系统设计与实现(吉林大学·李云峰)
- 基于网络爬虫的搜索引擎的设计与实现(湖北工业大学·冯丹)
- 基于Lucene的商品垂直搜索引擎研究与实现(东华大学·潘磊宁)
- 基于.NET平台的游戏门户系统设计与实现(电子科技大学·余胜鹏)
- 搜索引擎中网络爬虫技术研究(西安电子科技大学·郭海燕)
- 面向游戏垂类的搜索系统的设计与实现(北京交通大学·马振领)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:代码客栈 ,原文地址:https://bishedaima.com/yuanma/36002.html