答题小程序模式架构

最近公司接到一个项目,仿头脑王者模式开发一个小程序出来。
我心想,如此大的一个项目如何才能够去完美的架构开发出来。
一开始是毫无头绪的,但是把需求和玩法整理一遍,似乎也就那么回事,该篇文章只介绍答题模式的架构,其他的小功能不在这篇文章展示了。
下面是以PHP的代码进行架构与分析

架构流程

  • 需求分析
    • 原始需求
      • 排位赛
      • 计时赛
      • 难度赛
      • 互选赛
    • 流程图和思维导图(略)
  • 代码架构
    • 技术选型
    • 问题分析与解决方案(部分代码)
      • 控制流程代码(工厂代码)
      • 玩家游戏状态控制
      • 对战基础文件
      • 答题者代码
      • 排位赛
    • 遇到的问题
    • 小结

需求分析

原始需求

排位赛

1)匹配机制

每局比赛两人参与,系统随机匹配段位相同的在线两个人,五道题,答对一道题最多可得 200 分,答得越慢分越少,答错不得分。有十秒时间限制,如果在第一秒答对,得 200 分,如果用了一秒(即还剩 9 秒),则得 180 分,

剩8 秒则得 160 分,以此类推。最后一道题得分双倍,即 400 分、360 分、320 分……这样最高可得 1200 分,胜利者,获得(10)积分和对应金币,失败者和平局均不获得金币,失败者扣除(5)积分,当匹配同段位如果匹配不到用户,将逐级匹配低等级玩家(2 级范围内)(机器人:超过 10S 匹配不到自动匹配机器人)

2)晋升机制

当积分达到升级标准时,等级进行晋升 LV.x,同时晋升时会弹出一个小窗口提示3)降级机制

当积分降低到下级标准时,等级进行下降 LV.x,同时降级时会弹出一个小窗口提示4)胜机制

当该用户连胜达到 3 场及以上时,用户完成答题后界面进行提示,并且可继续挑战(系统重新匹配段位相同在线的玩家),同时所匹配对手也能看到该用户连胜状态,如果连胜被终结,该状态取消。同时也可点击炫耀成绩,发送给微信群或微信好友,好友点击开始切磋按钮,可查看分享着答题记录情况

计时赛

有1 分钟计时赛 X 题和 2 分钟计时赛 X 题可供选择。对手由系统随机匹配,也可以发送链接,邀请在线好友进入战局,计时比赛开始,答对题数多者胜出,1 分钟计时赛胜者获得系统 20 金币奖励,败者扣除账上 20 金币,平局双方没有得失金币.2 分钟计时赛胜者获得系统 50 金币奖励,败者扣除账上 50 金币,平局双方没有得失金币.两种模式,玩家有一方账户金币不足均无法开始游戏

难度赛

有1 星、2 星、3 星、4 星、5 星共 5 个难度可供选择。难度星级分别对应后台难度星级题库的随机 5 道题。每道题都有 10 秒答题时间倒计时。对手可由系统随机匹配,也可以发送链接,邀请在线好友进入战局,计时比赛开始,答对题数多者胜出,1 星难度赛胜者获得系统 10 金币奖励,败者扣除账上 10 金币,平局双方没有得失金币.2 星难度赛胜者获得系统 20 金币奖励,败者扣除账上 20 金币,平局双方没有得失金币.以此类推到 5 星3)金币储蓄赛

互选赛

对手可由系统随机匹配,也可以发送链接,邀请在线好友进入战局,互选比赛开始之前,对手互相为对方从题库挑选题目类别(题目类别由后台指定)。互选比赛开始,答对题数多者胜出,胜者获得系统 50 金币奖励,败者扣除账上 50 金币,平局双方没有得失金币.

流程图和思维导图(略)

代码架构

*随便写点:当时一看到这么多种模式的答题小程序,一下子就懵逼了。在我脑海中一时半会也想不出怎么去架构,如何去架构才能比较完美的匹配这几种模式的赛季。既然没有思路,那我就先梳理一下思维 。
先随便画画,打打草稿(字丑,都是个人的一些思维草稿,打出的草稿应该是我脑海里面的片段和自己的技术养成习惯吧!也许是这样。)

  • 第一天,先随便画画答题小程序模式架构_第1张图片
  • 第二天,突然灵感来了,再写一下
    答题小程序模式架构_第2张图片
  • 第三天,咦,架构好像呼之欲出了。
    答题小程序模式架构_第3张图片

好吧,上面的不重要接下来我们开始分析一下用程序去分析一下该如何去实现吧。下面架构出来的也许不是很完美,但是都是自我成长的一种记录过程。请勿喷。

技术选型

  • PHP
  • redis
  • workerman

问题分析与解决方案(部分代码)

代码结构
答题小程序模式架构_第4张图片
如何做到用一套代码并且关联4种模式?
我们知道答题小程序不外乎就是开始、答题、结束这几个流程。那么只要控制好这三个流程我们就能够在去控制子模式了。所以这里选择使用工厂模式作为控制流程。

控制流程代码(工厂代码)


/**
 * Created by PhpStorm.
 * User: Xgh
 * Date: 2020/5/11
 * Time: 14:39
 * 答题模式对战工厂
 *
 */

class BattleFactory{
    /**
     * BattleFactory constructor.
     * @param $mode 模式
     * @param $user_id 用户id
     */
    protected $mode;
    protected $playerObj; //玩家用户实例
    public function __construct($mode,$user_id)
    {
        //检测模型是否存在
        //实例模式处理
        //实例玩家的信息情况

    }

    //开始游戏
    public function start(){
        //检测用户是否正在游戏或者匹配中
        if($this->playerObj->isBattleIng()){}
        if($this->playerObj->isPending()){}
        $this->mode->start($this->playerObj);
    }
    //答题
    public function answer($answer_id){
        //分数处理-记得做锁
        try{
            //分数处理
            $asObj = new Answer($this->mode->now_answer_user_key);
            $asObj->select($answer_id); //选择答案
            $asObj->account($this->mode);
            $this->mode->answerAfter();
            //场次结束之后
            if($this->mode->game_finally == true){
                $this->end();
            }
        }catch(Exception $e){
        }


    }
    //游戏结束
    public function end(){
        //检测用户是否正在游戏或者匹配中
        if(!$this->playerObj->isBattleIng()){}
        $this->mode->end();
    }

    //机器人启动游戏
    public function robotStart(){

    }

    //重连机制

}

接下来,游戏开始,我们需要控制一下玩家的状态,不然很容易出现一账号出现多匹配,那么这里就是属于系统大BUG了。

玩家游戏状态控制


/**
 * Created by PhpStorm.
 * User: Xgh
 * Date: 2020/5/11
 * Time: 15:01
 * 玩家状态
 * 触发点:用户一旦连上socket立即触发用户状态,保存redis hex
 */

class Player{
    protected $redis_name='hex_user_'; //玩家登陆信息前缀
    protected $data ; //玩家信息
    public function __construct($user_id)
    {
    }

    /**
     * 玩家登陆
     * redis数据保存信息
     * battleKey=> 对战场次key
     * battleStatus=>用户对战状态 IDLE 空闲 PENDING匹配中 BATTLE游戏中
     */
    public function login(){
        //获取登陆用户登录信息,若无,初始化用户当前状态
        //若对战中弹出需要进入对战的按钮
    }

    //初始用户信息
    public function initInfo(){
        //判断key值是否存在hexists
        //不存在则初始hset($redis_name.$user_id,array('battleKey'=>'','battleStatus'=>'IDLE','user_id'=>0))
    }

    //更新用户对战信息
    public function updateData($arr){}

    //是否正在对战中
    public function isBattleIng(){
        //hvals
        if($this->data['battleStatus'] == 'BATTLE')
            return true;
        return false;
    }
    //是否正在匹配中
    public function isPending(){
        if($this->data['battleStatus'] == 'PENDING')
            return true;
        return false;
    }

    /**
     * @return mixed
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * @param mixed $data
     */
    public function setData($data)
    {
        $this->data = $data;
    }

    public function __get($name)
    {
        // TODO: Implement __get() method.
        return $this->data[$name];
    }


}

接下来,我会考虑一下去如何架构对战的最基本的东西,处理玩家答题的情况,对战数据设计?
答题情况我会做关联并且会自动生成一个场次,玩家答题redis_key作为保存用户每一题的情况。相当于发一张试卷,每个同学做题的时间,每一套题开始的时间,每一题的成绩是多少等这样的数据初始化。每一场对战都会新建初始这些数据。

对战基础文件


/**
 * Created by PhpStorm.
 * User: Xgh
 * Date: 2020/5/11
 * Time: 14:55
 * 答题对战的基础文件
 * 作为答题对战的基础数据架构
 */

class BattleBase {
    use Common;

    public $game_finally = false; //游戏是否结束
    protected $payers_answer ; //玩家答题情况,json集合[user_id=>redisKey]
    public $battle_key ; //场次key
    protected $answer_key ;  //目前正在答题的用户key
    protected $subject_num = 5 ; //题目数量
    protected $data ; //数据
//    protected $every_answer_time = 10; //每一题答题时间
    //protected $every_subject_open_time = []; //每一题出题时间
    //protected $subject_progress ; //当前题目进行量
//    protected $subject_list ; //题目集合


    /**
     * BattleBase constructor.
     * @param $gameKey 游戏场次的key
     */
    protected function __construct()
    {

    }
    public function setGameKey(){

    }

    /**
     * 创建游戏
     */
    protected function init($user_lists = [],$extends_config = []){
        $users_key = implode('_',$user_lists);
        $key = $this->battle_key . $users_key .$this->get_rand_str(6,0,1);
        //先自动创建用户答题情况
        foreach($user_lists as $k => $v){
            $asObj = new Answer();
            $anser_key = $asObj->init($this->subject_num);
            $this->payers_answer[$v] = $anser_key;
            $player = new Player($v);
            $player->updateData(['battleKey'=>$anser_key,'battleStatus'=>'BATTLE']);
            unset($asObj);
            unset($player);
        }
        $base_config = ['game_finally'=>false,'payers_answer'=>json_encode($this->payers_answer)];
        $value = array_merge($extends_config,$base_config);
        //使用hmset保存

        //自动创建一个房间
        //$this->battle_key
    }

    protected function updateData($data){
        $this->data = array_merge($this->data,$data);
        //hmset $key
    }

    /**
     * 游戏结束需要销毁创建游戏的redis数据集
     */
    public function end(){
        //若有机器人,需要考虑机器人的销毁
        //处理场次
        foreach($this->payers_answer as $user_id => $r_user_answer_key){
            //玩家全局状态
            $player = new Player($user_id);
            $player->updateData(['battleKey'=>'','battleStatus'=>'IDLE']);
            //销毁玩家答题情况
            Redis::del($r_user_answer_key);
        }
        //销毁场次
        Redis::del($this->battle_key);

    }
    //发送题目
    public function sendSubject(){}

    //解析hex
    public function parePayersAnswerData(){
    }


}

答题者代码



/**
 * Created by PhpStorm.
 * User: Xgh
 * Date: 2020/5/12
 * Time: 9:06
 * 选手答题类
 */
class Answer
{
    use Common;

    protected $data; //用户回答的数据
    protected $payerAnswerKey; // rediskey
    protected $now_answer;//当前回答的题目数据
    public function __construct($payerAnswerKey)
    {
        //除了init其他都需要payerAnswerKey

    }

    //初始化每道题目的答题情况
    public function init($subjuct_count)
    {
        if($subjuct_count <= 0)
            return false;

        $dataStruct=['now_subject_index'=>0,'every_subject_data'=>[]];
        for($i=0;$i<$subjuct_count;$i++){
            $dataStruct['every_subject_data'][] = ['subject_open_time'=>0,'answer'=>null,'score'=>0,'use_time'=>0,'answer_time'=>0] ;
        }
        $this->payerAnswerKey = $this->get_rand_str(16,1,1);
        //Redis::set($this->payerAnswerKey,json_encode($dataStruct))
        return $this->payerAnswerKey;
    }




    //计算数据总分数
    public function sum()
    {
        return array_sum(array_column($this->data['every_subject_data'], 'score'));
    }

    //
    /**
     * 计算分数-单题
     * @param $obj 游戏模式实例对象
     */
    public function account($obj)
    {
        //判断实例对象中是否存在计算分数的方法
        $score = 0 ;
        if(method_exists($obj,'getScore')){
            $score = $obj->getScore();
        }
        //兼容计时赛分数处理
        $time = time();
        //$one_subject['subject_progress'] = $this->data;
        $index = $this->data['now_subject_index'];
        $one_subject['score'] = $score;
        $use_time = $time - $this->data['every_subject_data'][$index]['subject_open_time'];

        $this->updateData(['score'=>$score,'use_time'=>$use_time,'answer_time'=>$time]);

    }

    private function updateData($arr,$next=false){
        $index = $this->data['now_subject_index'];
        $data = $this->data['every_subject_data'][$index];
        $this->data['every_subject_data'][$index] = array_merge($data,$arr);
        if($next){
            $this->data['subject_progress'] += 1;
        }
        //Redis::set($this->payerAnswerKey,json_encode($this->data))
    }


}

好吧,上面大致的东西都已经做出来了。那么我们在细化到某一个模式,我现在把排位赛的部分代码贴上来,就能完成这个架构了。可以实现其他模式的兼容,并且能够做到代码不冗余,易读易开发。

排位赛


/**
 * Created by PhpStorm.
 * User: Xgh
 * Date: 2020/5/12
 * Time: 16:19
 * 排位赛
 */

class Ranking extends BattleBase{
    protected $ranking_room_list_key = 'ranking_room_list_'; //排位房间队列
    protected $battle_key = 'ranking_string_';
    protected $answer_key = 'ranking_answer_';
    protected $subject_num = 5;
    //开始游戏做匹配处理
    final public function start(Player $playerObj){
        //当前玩家的段位-如果可以,使用redis的事务
        $level = 1;
        //监听当前段位的房间队列-可做长连接10S或者做定时器
        $pk_user_id = Redis::lpop($this->ranking_room_list_key . $level);

        //若无用户存在,则插入该房间的队列
        //若有用户存在,则初始化数据
        $ex_config['every_answer_time'] = 10; //每道题答题时间
        $ex_config['subject_progress'] = 0 ; //当前双方进行答题数
        //获取相对应的题目--现在未处理,需要处理该处
        $all_subject = Subject::get($this->subject_num);
        $players = [$playerObj->user_id,$pk_user_id];

        //双方的题目是这个
        foreach($players as $v){
            $ex_config['subjects'][$v] = $all_subject;
        }
        $this->init($players,$ex_config);
    }
    //计算分数
    public function getScore(){}
    //回答之后处理
    public function answerAfter(){}

    //游戏结束
    public function end()
    {

        parent::end(); // TODO: Change the autogenerated stub
    }

    //机器人开始游戏
    public function robotStart(){
        //模拟用户-随机获取一个机器人
    }

}

遇到的问题

  1. 如何避免并发匹配同一个用户?
    每一次用户需要进入匹配,首先先进入队列进行等待弹出(list),即可避免一个用户匹配不同的玩家,保证原子性。
  2. 如何避免玩家断线之后重连回到当前的这个场次的状态?
    若是玩家是处于自身网络不稳定状态,导致重新连接的情况下,系统中玩家答题状态,答题场次,答题情况,进行中的游戏时间,都会保存下来,只要重新调用即可保证当前答题场次的状态。
  3. 如何保证用户答题并发的问题
    使用redis锁即可

小结

上面的代码只是部分代码,也是作为一个学习记录。

你可能感兴趣的:(答题小程序,设计模式,php)