w5-故事接龙游戏wheniwas18

这周的课程任务本来是放公网上,没看进度,用了新兴的docker运营商daocloud的方案,由于docker方案尚在推广阶段,优惠力度也不错,docker方案做版本管理也方便,而且相对传统的PAAS平台架构上可以更加灵活些,所以直接放在一个container里面,w4就等于是提前完成了,成品如下:

http://liangchaob-wheniwas18.daoapp.io

想法

正如上周的想法,我打算在这个基础上继续调整,但是对我来说,感觉只做一个公网同步笔记的工具实在是不过瘾,我打算做成一个小游戏!

曾经大学时玩桌游有个挺有意思的桌游叫《很久很久以前》,大概玩法如下:

很久很久以前是一个使用神话传说的常见的游戏元素来讲故事的游戏。玩家将要成为故事的讲述者,使用以她抽到的结局结尾。同时,其他参与游戏的玩家,会利用手中的各种卡片来打断她的讲述。成功打断的玩家将成为新的讲述者,接着上一名玩家的故事继续讲述。游戏的目的是为了享受游戏过程,创造出动听的故事。第一个成功地将手中的要素卡打出,并且以结局卡结尾的玩家将获得胜利。 简要过程: 选择一名玩家开始讲述一个故事。当她提到她手中某一个故事要素时,打出这张要素卡。如果她提到的某个要素,别的玩家有这张要素卡并且打出来了,那么这名玩家就成为新的故事讲述者,接着上面的故事,继续讲述。胜利的条件是打完所有的要素卡,并且以自己的结局卡结尾。 比如手上的要素卡有国王,宝剑,继承,牧羊女,聪明等,就可以这么讲述:很久很久以前,有一个年轻的国王(这时打出国王牌),他在很小的时候就从老国王那里继承(打出继承)了王位,有一次去森林里,他寻找到了一把宝剑(这里其他人手上有森林牌,打出了,于是其他人就开始讲故事)

当时在桌游吧本来我的一个朋友很喜欢玩,但苦于脑洞需求太大,判定方式复杂,所以响应者寥寥,结果我也没有玩成。但我感觉规则还是挺有意思的,感觉这和故事接龙很像,有的一玩,但故事接龙玩一会儿就会腻,我猜想主要原因可能是缺少限定条件,难以激发大家的挑战乐趣,所以我打算尝试把这两个规则合一下,做一个有限制条件,难度,带奖励反馈的小文字游戏。

目前来看,前几周做的这个公网的笔记工具,完全可以做成一个聊天室,既然如此,这个游戏的基本输入输出方式也就具备了,我打算在这个基础上做做看。

基础

我打算以w4的代码为蓝本开始实现 https://github.com/liangchaob/OMOOC2py/tree/master/_src/om2py4w/4wex0

# -*- coding: UTF-8 -*-
# 引入os模块
import os
# 引入datatime模块
import time
# 引入sys模块,并将默认字符格式转为utf-8
import sys
sys.path.append("./")
reload(sys)
sys.setdefaultencoding( "utf-8" )
# 引入flask模块
from flask import Flask, render_template, request
# 导入sqllite模块
import sqlite3
# 创建一个Flask对象
app = Flask(__name__)
# 创建一个数据库对象
DATABASE = 'linenote.db'
# 指定路由
@app.route('/', methods=['POST', 'GET'])
# 定义响应函数
def index():
    # 如果响应为post
    if request.method == 'POST':
        # 内容
        content=request.form['lineInput']
        # 获取当前系统时间
        timeNow = str(time.strftime("%Y-%m-%d %H:%M:%S"))
        # 获取客户端ip
        addr = str(request.remote_addr)
        # 将信息写入数据库
        conn=sqlite3.connect(DATABASE)
        # 执行输入语句
        conn.execute('insert into linecontent (ip, time, content) values (?, ?, ?)',[addr, timeNow, content])
        # 提交输入请求
        conn.commit()
        # 关闭数据库连接
        conn.close()
    else:
        pass
    # 设置读取内容
    conn=sqlite3.connect(DATABASE)
    # 设置查询语句
    cursor = conn.execute('select ip, time, content from linecontent order by id desc')
    # 设置字典格式
    entries = [dict(addr=row[0], time=row[1], content=row[2]) for row in cursor.fetchall()]
    # 关掉数据库连接
    conn.close()
    # 用获取到的字典渲染页面
    return render_template('linenote.html', entries=entries)
# 运行主函数
if __name__ == '__main__':
    # 对外开放8080端口
    app.run(host='0.0.0.0',port=8080)

屏幕快照 2015-11-13 22.02.41.png

项目分析

之前看TED演讲玩游戏,创造美好世界,消除了对游戏的偏见,然后就读了《游戏改变世界》,然后还有知乎的一个关于游戏化的回答 http://www.zhihu.com/question/28389624 理解了构建一个游戏的四个决定性特征:

  1. 目标(goal),指的是玩家努力达成的具体结果。它吸引了玩家的注意力,不断调整他们的参与度。目标为玩家提供了“目的性”(sense of purpose)。
    1. 规则(rules),为玩家如何实现目标做出限制。它消除或限制了达成目标最明显的方式,推动玩家去探索此前未知的可能空间。规则可以释放玩家的创造力,培养玩家的策略性思维。
    2. 反馈系统(feedback system),告诉玩家距离实现目标还有多远。它通过点数、级别、得分、进度条等形式来反映。反馈系统最基本也最简单的形式,就是让玩家认识到—个客观结果:等……的时候,游戏就结束了。对玩家而言,实时反馈是—种承诺:目标绝对是可以达到的,它给了人们继续玩下去的动力。
    3. 自愿参与(voluntary participation),要求所有玩游戏的人都了解并愿意接受目标,规则和反馈。了解是建立多人游戏的共同基础。任意参与和离去的自由,则是为了保证玩家把游戏中蓄意设计的高压挑战工作视为安全且愉快的活动。

对于要做一个这样的小游戏,原来的代码已经具备了基本的故事接龙的能力,我认为目前还需要实现的是制造限定规则,并建立反馈系统。

我准备利用《很久很久以前》的卡牌建立限定规则,不过这回毕竟不是桌游,没有必要非得是卡牌,我准备用普通的有意义的关键词进行替代。

比如规则可以这样设定: 随便输入一句话,但是句中必须含有指定的关键词,且满足字数限制,否则无法输入。

然后反馈系统,我打算引用知乎的『赞』,或者三国杀的『成就』系统,这两种方案备选:

  1. 赞——前一个故事者的故事如果能被后一个人成功续接,则该故事人赞+1,这样所有的人就能倾向于把故事说的留有空间,有续接可能,有利于游戏继续进行。
  2. 成就——可以对指定承接的故事条目选择评价,获取的评价会在讲故事人的id下显示称号,比如『故事大王』『脑洞大开』『发人深省』『感天动地』『忍俊不禁』等等

然后这个项目需要个有文化的名字,毕竟和《很久很久以前》规则不一样,生搬的话虽然可能会吸引到桌游老玩家,但这个量也不大,未必有效,叫『故事接龙』的话倒是简单粗暴,所见即所得,但总是觉得太土,不够吸引人

我想了个与《很久以前》异曲同工的名字『那年18』,梗来自于我最喜欢的姜文电影《让子弹飞》,在姜文说完自己的故事后,葛大爷应景做深情状"而我的故事是这样的,那年我17岁,她也17岁..."

但17这个岁数总是感觉不太顺口与显眼,我改成了18,一个对我们80后90后都十分敏感的一个年纪,一个所有人在那一年都有故事的年纪。

技术分析

对于限定规则,出现的关键词应该是随机的,所以我需要建立一个随机数列来承载关键词,这个关键词生成我打算放在前端用js来随机生成,也用js来进行输入验证,这样可以避免给后端造成压力。

对于反馈系统,它强烈依赖于账号系统,目前的用户识别的是ip地址,这种方式并不好,而且由于我的docker运营商由于对客户端源地址也做了nat,所以也无法清楚的识别客户ip来源,根本无法区分。

对于构建一个账号系统,我有两种方案:

  1. 自建账号系统,需要建立独立的注册,验证与登录流程,这样最牢靠;但是我没有做这个的经验,这一套做下来,估计坑会很多,而且我也不认为我有足够强力的web安全能力,足矣保障用户密码不泄露。
  2. 直接使用第三方的账号认证系统,这个看上去比较好,可以省去繁琐的用户注册流程,但是这个也没用过,不知坑多深,也不确定用哪家账号系统会比较合适,微信?QQ?微博?github?

我打算用第三方的,刚查阅了文档发现oauth2.0貌似是专门干这个的,而且很成熟,也找到了专门py的oauth实现的视频 http://www.jikexueyuan.com/course/695.html ,应该可行

得蒙上次大妈的启发,开始对于并发与压力做考虑,首先对于web层面而言,由于使用的docker主机直接跑的flask,前面并没放apache或者ngnix,所以对于web层面的高并发应该支持很差,所以我考虑在docker里面装个ngnix看看效果。

如果时间实在有限的话,我也可以先不管这个,由于我的运营商自带集群功能,也可以通过多扩几台docker机器组成集群来负载均衡也能对并发有一定的承载效果。

又或者我把框架换成对高并发支持更好地tornado,之前没用过需要现学不说,更需要伤筋动骨的重构了,这就看到时的情况了,属下下之选

而对于数据库层,显然跑在docker里面的那个单机的sqlite3是不行了,由于docker特性,无法做数据持久化,万一这个container有了故障一旦重启所有的数据就都没了,而且由于可能会做集群,因此我需要使用公共的数据库服务。

我的服务商daocloud提供的数据库服务有4个:mongodb,mysql,redis,influxdb。

其中mongodb属于kv库,mysql属于传统库,redis听说是高大上而且快速的内存数据库,而最后一个我不认识╮(╯_╰)╭

对于这个应用而言,mysql和mongodb都ok,使用mysql的话,好处在于之前的sql语句一点都不用改(虽然只有两句),而mongodb的优势也很明显,mongo显然就是专门干这种存文字与故事这种非结构数据的的首选,但貌似要先跳json这个没跳过的坑。

算了,跳就跳罢,我选mongodb,从没用过kv库,尝尝鲜也好,况且下一个应用可能也用得上。

我的项目名既然叫『那年18』,翻译成英文就叫做"wheniwas18",我立刻上万网注册这个域名wheniwas18.com

前端设计

分析罢了,开始干活,首先还是mockups一个大致的web模样看下效果

demo_2.png

其实变动不大,但是手机端看右上角的个人属性估计会很不好处理,干脆就另起一页放个人档案好了

demo_3.png

大致做好了效果就是这样了:

屏幕快照 2015-11-15 00.56.25.png

屏幕快照 2015-11-15 00.56.37.png

到时候把人头换成个人头像,然后个人属性通过点击头像进行链接应该就差不多了。

js交互

首先我需要一个方法生成关键词,我准备把这个任务交给前端来完成,期望每次刷新的时候会随机生成一个类似验证码的关键词,然后对这个关键词做检查,输入语句必须包含它。

生成的关键词,我准备做一个数列,然后每次get刷新就会随机抽取一个生成,以此往复,这在python中有random可以轻易实现,现在需要想办法在js中的实现方法

百度了下『js随机数』我找到了 http://www.studyofnet.com/news/181.html 提到了Math.random()的函数,专门生成0~1之间随机数的一个函数;和Math.round(n)用来四舍五入取整的函数,有了它就好办了,我到chrome的console里试了一下

var list=['q','w','e','r','t','y','u','i','o','p']
undefined
list[4]
"t"
Math.random()
0.5417060917243361
Math.random()*10
2.528261838015169
Math.round(Math.random()*10)
4
n=Math.round(Math.random()*10)
9
list[n]
"p"

看来Math.round(Math.random()*10)是个可用的函数,我现在需要找10个词试一下,『隔壁老王』这个词可能太不严肃了,作为实验,又叫『18岁』,我试着以『校园』为主体抽10个关键词出来看下:

校园-关键词.png

我暂时选了些大家耳熟能详的名词,这样参与者都能有话可说,但是感觉还是有点太普通了,于是增加了些稍微能带动情绪能挑起些许波澜的词

校园-关键词2.png

这样就有了20个词,把它做成数列试一下

var keywords=['物理','小卖部','张老师','篮球','值日','小纸条','女厕所','家长会','罚站','同桌','课间操','校长','操场','粉笔','盒饭','抄100遍','没收','表白','迟到','小窗户']
undefined
n=Math.round(Math.random()*19)
6
keyword=keywords[n]
"女厕所"

证明这是可行的,但把生成随机数写在前端也有个坏处,就是很容易被hack过去,但是想想也无所谓,只要续上故事就行,是否一定是关键词没这么重要。

我把这组js加到body尾部,尝试刷新随机生成成功

<script type="text/javascript">
$(document).ready(function(){
    var keywords=['物理','小卖部','张老师','篮球','值日','小纸条','女厕所','家长会','罚站','同桌','课间操','校长','操场','粉笔','盒饭','抄100遍','没收','表白','迟到','小窗户']
    n=Math.round(Math.random()*19)
    var keyword=keywords[n]
    $("#keyword").find('code').text(keyword);
});
</script>

我尝试了一下,故事是完全可以说的通的,也就放心了

屏幕快照 2015-11-15 14.27.32.png

此时的前端我还差一套输入验证系统,对于输入框我需要做检查,避免不合理的输入造成问题,预期的输入应该包含以下规则:

  1. 输入的语句必须包含关键词,但并不能只包含关键词
  2. 输入的语句不能过短也不能太长,最好控制在10~50字。

验证是否包含关键词,我需要找js中验证包含字符串的方法,我找到了indexOf()

var tempStr = "tempText" ;
var bool = tempStr.indexOf("Texxt");
//返回大于等于0的整数值,若不包含"Text"则返回"-1

我测试了下

var keyword='校长'
undefined
var input='我转身跑向教室,千不该万不该这个时候迎面碰见了校长,手里拿着我的篮球。。。'
undefined
var bool = input.indexOf(keyword);
undefined
bool
23
var input='哈哈哈哈'
undefined
var bool = input.indexOf(keyword);
undefined
bool
-1

这是可行的,然后等于关键词直接用相等来判定即可,另外还需要判定输入的长度,直接使用.length即可,然后如果输入合法,则把提交按钮禁用状态解除。

为实现这些验证效果,我需要对表单进行验证,以判断按钮状态,但是搜索很久发现传统的表单验证似乎都是以input失去焦点状态函数blur作为事件触发的,但是我的输入框只有一行,当失去焦点状态时候就该提交了,此时按钮还是禁用状态显然不行,所以我需要个实时的输入检查工具,通过搜索找到了jquery validate,但看了教程后感觉有点迷糊,而且也没有公网的cdnjs,如果用下载的方式还需要在flask中额外的设置路径很麻烦,于是我又找到了js的原装函数addEventListener()和attachEvent()好像也很复杂,最后,找到了一个onkeyup事件属性貌似是时刻监视键盘行为的,可以达到实时的效果,好像很有意思测试了下,确实可用,最后js实现如下:

// 设置键盘抬起后触发的验证函数
function validateEvent(){
    var strInput=storyForm.lineInput.value;
    var keyword=$('#keyword code')[0].innerText;
    var bool = strInput.indexOf(keyword);
    // 如果包含关键词,字符长度满足要求,则解禁提交按钮
    if (bool>=0 && strInput.length>10 && strInput.length<50){
        // 去掉禁止提交属性
        $('#submitButton').removeAttr('disabled');
    }else{
        // 添加禁止提交属性
        $('#submitButton').attr('disabled',"disabled");
    }
}

而html中:

        <form action="/" method="POST" name='storyForm'>
          <div class="form-group">
            <label id='keyword' form="lineInput"><a href="#"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></a>
            &nbsp;keywords: <code></code></label>
            <input id="lineInput" type="text" class="form-control" name="lineInput" placeholder="必须包含关键词,且字数在10~50" onkeyup="validateEvent()">
          </div>
          <button id='submitButton' type="submit" class="btn btn-default" disabled="disabled">提交</button>
        </form>

这样前端js已经基本可用了,但是测试后发现由于中英文字符占位不同,所以字符统计其实是有问题的,但这个应该好解决,而且不是关键问题,先往后放。

在刷新的时候我遇到了上周没解决的一个小问题,我在填完表格提交后,此时如果手动刷新网页,就会出弹出重新提交的请求,如果点『是』,则原表格内部数据会重新发送一遍,造成同一份地数据重复提交了两次,我百度了下,找到两个结果:

http://www.oschina.net/question/1049491_147683

http://stackoverflow.com/questions/21668481/difference-between-render-template-and-redirect

提示可能是我最后的函数是render_template而不是redirect造成的,但是改成redirect后发现它不能跟渲染参数,而且要写路由路径,所以我把它放在了前一个提交操作分支的末尾

    # 提交输入请求
    conn.commit()
    # 关闭数据库连接
    conn.close()
    # 重定向到根页面
    return redirect('/')

测试后发现问题消失了,但是走了一遍逻辑,发现这样的话其实是每次提交后渲染页面操作实际执行了两次,所以我把后续的所有原来不走分支的语句,转到get分支下,就ok了。

在手机测试中遇到了头疼的问题,由于手机提交表单的时候使用软键盘输入后有右下角会有个『开始』的回车按钮,通常大家在输入的时候最后会按这个,但是在我的表单中这个键一旦被按下,就会触发表单的提交,而且是直接跳过我的禁用按钮提交,造成我的判定规则失效,我需要解决这个问题,想办法把这个软键盘提交给禁用掉:

后来又搜到了这个 http://www.jb51.net/article/47586.htmhttp://outofmemory.cn/code-snippet/4463/jQuery-jinyong-form-huiche-jian-tijiao 我发现了其中这个13是个神秘的数字,然后我找到了一个对应表发现13对应的貌似是回车键,然后js代码的逻辑就应该是把这个回车键截取掉让它返回空值,

// 将回车键提交表单禁用掉
function delReturn(){
    if(event.keyCode==13){
        //把event事件给截取掉,表单就获取不到event.keyCode==13了
        event.returnValue=false;
    }
}

搞定!

用户认证系统

本来打算先做数据库的,但是发现如果做用户的信息统计离不开用户账号,所以先花些时间搞清第三方认证系统尤其是微信账号,用author是怎么做的,官网说的太复杂,我去找一篇容易理解的

我发现阮一峰还真是个多面手,前几天查cfa查司法考试他居然也都有研究,连oauth也不例外 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

大致了解了概念后,我准备使用授权码模式 bg2014051204.png

我搜索了下微信的oauthor方式,找到了腾讯开放平台 注册了下,要求传带身份证的照片,ps合并后传上去,显示注册成功!

但是选择创建应用种类的时候犯了难,如果把这个归类为『游戏』,则按腾讯的要求必须放在腾讯云上,但是这样的话,腾讯云我没用过也不知道水多深收费和怎么样,所以我在这里选择了『社交』类,这样也不算错,然后子分类选择『好友互动』,然后还有问是否是html5应用,我觉得可能是腾讯对html5应用会做前端的静态优化,我的应用反正是html,无所谓5不5,先选择5罢

注册完成后,平台给了我一对appid,appkey,然而,全部注册完后却并没找到合适的oauthor相关的链接。于是我又找到了这个

http://mp.weixin.qq.com/wiki/home/index.html

这个里面倒是提到了oauthor,但好像是要我去注册微信公众平台,好罢,我去注册。。

https://mp.weixin.qq.com 于是稀里糊涂的注册了一个微信公众平台,但是我重新想了一轮,我要的认证不过只是微信的登录验证而已,期望可以通过二维码或者链接能够访问跳转获取用户头像与id而已,需要公众号吗?我觉得可能走偏了,但还没想到办法绕回来,折腾太久了,还是去找issue问问大妈罢。

大妈建议我换个服务商试下,我打算找找qq和微博的,由于我已经注册了腾讯开放平台,我打算找qq的试一下。 搜来搜去,我找到了这个 http://qzonestyle.gtimg.cn/qzone/openapi/js-sdk-demo.html

尝试了之后,已经可以成功插入按钮,cookie也能找到登录过的qq号码,但是回调解决却返回了『获取用户信息失败!』

貌似是需要在开放平台里做些调整,但是目前看来在这里花费的时间有些太长了,我转身开始先搞定数据库

数据库

我决定使用mongodb,首先我现在自己的本地虚拟机里起一个container,再在mac上装上一个robomongo客户端,试试水

这里又遇到了坑,启动docker后,本地客户端死活连不上,开始以为是docker的端口映射问题,结果换了好几轮,问题依旧,最后进到docker里面直接连本地也提示有问题,这才发现原来是本地的container也要手动启mongod服务。

之前用mysql和apache镜像的时候,服务都是连带着container启动后自动起来的,如今的公共镜像库还真是亟待标准化!

在flask中使用mongo需要安装pymongo,使用sudo pip install pymongo安装,而关于mongo的语法我找到了一个很有意思的文章,对照sql的话理解起来很快 Mongo db 与mysql 语法比较

这周提前开课,先做到这里打算先给大妈看下,直接到daocloud里面上线。

上线后输入了两行觉得还不错,但是发现有个问题,时区不对,我参考文档通过控制台调整了下时区。