zoukankan      html  css  js  c++  java
  • 在线捉鬼游戏开发之三

    -----------回顾分割线-----------

    此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式、重构的理解。

    索引目录

    0. 索引(持续更新中)

    1. 游戏流程介绍与技术选用

    2. 设计业务对象与对象职责划分(1)(图解旧版本)

    3. 设计业务对象与对象职责划分(2)(旧版本代码剖析)

    4. 设计业务对象与对象职责划分(3)(新版本业务对象设计)

    5. 业务对象核心代码编写与单元测试(游戏开始前:玩家入座与退出)

    6. 业务对象核心代码编写与单元测试(游戏开始:抽题、分角色、开启鬼讨论模式)

    7. 代码与测试(鬼讨论、鬼投票)

    8. 代码与测试(玩家发言)

    -----------回顾结束分割线-----------

    先放上源代码,svn地址:https://115.29.246.25/svn/Catghost/

    账号:guest 密码:guest(支持源代码下载,已设只读权限,待我基本做出初始版本后再放到git)

    -----------本篇开始分割线----------

    依旧是按照顺序图来完成(越发感觉到类图、顺序图给代码带来的指导性意义)

     
    Player Speak Diagram

    1. 玩家发言

    顺序图中的1-3步:CurrentSpeaker发言后,SpeakManager记录并显示出来,同时设置下一个允许发言的玩家。

    在SpeakManager中其实已经写好了大部分PlayerSpeak()的内容,只需加一行SetNextSpeaker()即可。

    public void PlayerSpeak(Player player, string str)
    {
        if (IsGhostDiscussing())
        {
            CheckGhostSpeaker(player);
        }
        else
        {
            CheckCurrentSpeaker(player);
        }
        AddToRecord(FormatSpeak(player.NickName, str));
        SetNextSpeaker(player);
    }
    
    /// <summary>
    /// 设置下一位发言人
    /// </summary>
    /// <param name="currentPlayer">当前发言人</param>
    private void SetNextSpeaker(Player currentPlayer)
    {
        Player[] players = GetPlayerManager().GetAllPlayerArray();
        Player nextPlayer = null;
        for (int i = 0; i < players.Length; i++)
        {
            if (players[i].Equals(currentPlayer))
            {
                if (i == players.Count() - 1)
                {
                    nextPlayer = players[0];
                    break;
                }
                nextPlayer = players[i + 1];
                break;
            }
        }
        SetSpeaker(nextPlayer);
    }

     测试代码如下:用for来测试是否满足循环发言时的SetNextSpeaker

    [TestMethod]
    public void PlayerSpeakUnitTest()
    {
        JoinGame();
    
        // ghost discussing...
    
        Player electPlayer = GetPlayerManager().GetAllPlayerArray()[5]; // elect player
        GhostVoting(electPlayer);
    
        for (int i = 0; i < 2; i++)
        {
            PlayerSpeaking(electPlayer, i);
        }
    
        ShowPlayerListen();
    }
    
    // private method
    
    private void PlayerSpeaking(Player starter, int times)
    {
        bool canSpeak = false;
        if (times > 0) canSpeak = true;
        foreach (Player p in GetPlayerManager().GetAllPlayerArray())
        {
            if (p.Equals(starter))
            {
                canSpeak = true;
            }
            if (canSpeak)
            {
                p.Speak("i'm " + p.NickName);
            }
        }
    }

    测试结果如期所至:所有玩家都能看到,且从第6个玩家(kimi)开始发言,两轮后结束。因为没加入LoopManager进行监控,所以要到vivian才结束。加入LoopManager后应该在coco发言完就结束了。

    再看代码度量值,貌似还有进步的空间:判断太多导致圈复杂度上升,类耦合可适当减少。

    先看当前的SetNextSpeaker()代码

    /// <summary>
    /// 设置下一位发言人
    /// </summary>
    /// <param name="currentPlayer">当前发言人</param>
    private void SetNextSpeaker(Player currentPlayer)
    {
        Player[] players = GetPlayerManager().GetAllPlayerArray();
        Player nextPlayer = null;
        for (int i = 0; i < players.Length; i++)
        {
            if (players[i].Equals(currentPlayer))
            {
                if (i == players.Count() - 1)
                {
                    nextPlayer = players[0];
                    break;
                }
                nextPlayer = players[i + 1];
                break;
            }
        }
        SetSpeaker(nextPlayer);
    }

    感觉最内层的if(已表粗体)只是为了简单判断如果循环到数组末尾,则从第一个开始。坏味道出现了——这是SpeakManager该做的事情吗?整个SetNextSpeaker()的主要任务是设置下一个玩家允许发言,你就别给我整当前玩家是谁,你直接给我来下一个玩家是谁不就得了?所以,整个SetNextSpeaker()都跨越了自己的职责,占用了谁的职责呢?谁最清楚下一个玩家是谁呢?Player自己知道吗——不知道,只有玩家管理者PlayerManager知道,因为他就是干这个的——维护玩家列表!

    题外提一句,这里很容易想到状态模式——Player说完自动换下一位Player,即CurrentSpeaker标记的状态在改变——但很遗憾,这里并不合适:首先,Player自己不应该知道自己下一位是谁,而是PlayerManager才知道,且如果Player知道自己的下一位,那么他就有权决定下一位是谁(状态模式是为了易于轻松增/改传递的下一个状态),这就与游戏规则不符了。故,还是需要一个统领全局的局外人PlayerManager来操作(有点儿建造者模式中Builder的味道)。

    首先在PlayerManager中增加GetNextPlayer()方法:

    /// <summary>
    /// 返回下一位玩家
    /// </summary>
    /// <param name="currentPlayer">当前玩家</param>
    /// <returns>下一位玩家</returns>
    public Player GetNextPlayer(Player currentPlayer)
    {
        CheckPlayer(currentPlayer);
        Player result = null;
        for (int i = 0; i < GetAllPlayerArray().Length; i++)
        {
            if (GetAllPlayerArray()[i].Equals(currentPlayer))
            {
                if (i == GetAllPlayerArray().Length - 1)
                {
                    result = GetAllPlayerArray()[0];
                }
    else
    { result
    = GetAllPlayerArray()[i + 1];
    } } }
    return result; }

    对应SpeakManager中的SetNextSpeaker()将非常简单:

    /// <summary>
    /// 设置下一位发言人
    /// </summary>
    /// <param name="currentPlayer">当前发言人</param>
    private void SetNextSpeaker(Player currentPlayer)
    {
        Player nextPlayer = GetPlayerManager().GetNextPlayer(currentPlayer);
        SetSpeaker(nextPlayer);
    }

     

    代码度量值方面:减少了类耦合(将不属于的职责分离出去了),但圈复杂度传给了PlayerManager。需要继续优化:

    我们可以观察到寻找下一位玩家的关键就在于座位号,无论是对当前玩家的判断,还是下一位玩家的筛选,都要通过座位号,所以考虑提取出GetSeatOrder()方法:

    public Player GetNextPlayer(Player currentPlayer)
    {
        Player[] players = GetAllPlayerArray();
        int currentSeatOrder = GetSeatOrder(currentPlayer);
        return currentSeatOrder == players.Length - 1 ? players[0] : players[currentSeatOrder + 1];
    }
    
    /// <summary>
    /// 返回玩家座位号
    /// </summary>
    /// <param name="player">玩家</param>
    /// <returns>座位号</returns>
    private int GetSeatOrder(Player player)
    {
        CheckPlayer(player);
        for (int i = 0; i < GetAllPlayerArray().Length; i++)
        {
            if (GetAllPlayerArray()[i].Equals(player))
            {
                return i;
            }
        }
        return -1;
    }

    测试之,没问题。再看代码度量值:很好,算是完成了玩家发言的部分。

    2. 循环管理

    顺序图中的4-6步:设置下一允许发言的玩家后,LoopManager负责检查是否此循环结束(首轮发言有两个循环),若没结束,则不做操作;若已结束,则SpeakManager发出系统指令告诉大家开始投票,投票环节不允许发言,故要对CurrentSpeaker做一些处理。

    首先在SpeakManager.SetNextSpeaker()的时候增加CheckIsEnd()检查:如果发言完了,则设置当前允许发言的人为空,且系统发出提示。

    /// <summary>
    /// 设置下一位发言人
    /// </summary>
    /// <param name="currentPlayer">当前发言人</param>
    private void SetNextSpeaker(Player currentPlayer)
    {
        Player nextPlayer = GetPlayerManager().GetNextPlayer(currentPlayer);
        SetSpeaker(nextPlayer);
        ChechIsLoopEnd(nextPlayer);
    }
    
    /// <summary>
    /// 检查是否循环结束
    /// </summary>
    /// <param name="currentPlayer">当前玩家</param>
    private void ChechIsLoopEnd(Player currentPlayer)
    {
        if (GetLoopManager().IsLoopEnd(currentPlayer))
        {
            SetSpeaker(null);
            SystemSpeak(GetSetting().GetAppSettingValue("VoteTip"));
        }
    }

    接着在LoopManager中填充IsLoopEnd()方法:其注意第一轮是发言两圈。

    public bool IsLoopEnd(Player currentPlayer)
    {
        if (currentPlayer.Equals(this._loopStarter))
        {
            if (_isFirstLoop)
            {
                _isFirstLoop = false;
                return false;
            }
            return true;
        }
        return false;
    }

    测试结果需要做一些小调整:把SpeakManager.CheckCurrentSpeaker()的“不许场外”的异常先禁用,否则会报错看不到输出。

    可以看到,首轮每人发言了两次,且第二论(及以后)每人只能发言一次,最后框外的kimi-vivian的发言是因为异常未阻止导致的,符合预期。测试通过。代码度量值也是ok,就不贴图了。

    到此,循环管理也算完成。

    也许朋友们会问:那异常的处理在最后要怎么办?是返回string,还是终止程序?还是不予处理?——这些都在考虑ui的时候在考虑,此环节仅作核心Models代码编写。千万不能一时混淆太多考虑——饭要一口一口吃,代码要一处一处写

  • 相关阅读:
    linux配置显示git分支名
    tensorrt int8量化原理几点问题记录
    cuda Global Memory Access
    cuda shared memory bank conflict
    一种简单的死锁检测算法
    n = 5x+2y+z,程序优化:unroll loop
    c++ detect && solve integer overflow
    Tensorpack.MultiProcessPrefetchData改进,实现高效的数据流水线
    tensorflow layout optimizer && conv autotune
    python 产生token及token验证
  • 原文地址:https://www.cnblogs.com/lzhlyle/p/Catghost-Models4.html
Copyright © 2011-2022 走看看