您的位置:首页 > 编程语言

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

2015-09-03 14:48 441 查看
-----------回顾分割线-----------

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

索引目录

0. 索引(持续更新中)

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

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

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

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

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

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

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

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

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

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

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

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

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



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代码编写。千万不能一时混淆太多考虑——饭要一口一口吃,代码要一处一处写
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: