在线捉鬼游戏开发之三 - 代码与测试(玩家发言)

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

此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的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代码编写。千万不能一时混淆太多考虑——饭要一口一口吃,代码要一处一处写

posted @ 2015-09-03 14:48  lzhlyle  阅读(942)  评论(0编辑  收藏  举报