保龄球计分游戏单元测试异常:全中场景抛出ArgumentOutOfRangeException
保龄球计分游戏全中场景测试异常排查
我开发了一款计算保龄球击倒瓶数的计分游戏,针对各类场景编写了多组单元测试,但在**仅投出全中(目标12次投球、总分300分)**的场景下测试失败,抛出ArgumentOutOfRangeException异常。以下是Game类和Frame类的代码:
Game类代码
public class Game { private IOutputProvider _outputProvider; private List<Frame> frames = new List<Frame>(); private int currentRoll = 0; public Game(IOutputProvider outputProvider) { _outputProvider = outputProvider; for (int i = 0; i < 10; i++) { frames.Add(new Frame()); } } public void StarteSpiel() { while (!Over()) { _outputProvider.Write("Geben Sie die Anzahl der umgeworfenen Pins ein: "); string input = _outputProvider.Read(); int pins; while (!int.TryParse(input, out pins) || pins < 0 || pins > 10) { _outputProvider.Write("Ungültige Eingabe! Bitte geben Sie eine Zahl zwischen 0 und 10 ein: "); input = _outputProvider.Read(); } AddRoll(pins); } _outputProvider.Write("Spiel beendet! Endpunktzahl: " + TotalScore()); } private void AddRoll(int pins) { if (Over()) { throw new IndexOutOfRangeException("Das Spiel ist beendet!"); } int frameIndex = GetCurrentFrameIndex(); if (pins == 10 && currentRoll % 2 == 0 && frameIndex < 9) { _outputProvider.Write("Herzlichen Glückwunsch, Sie haben einen Strike geworfen!"); frames[frameIndex].PinsRolled[0] = pins; frames[frameIndex].IsComplete = true; currentRoll += 2; } else { if (frames[frameIndex].PinsRolled[0] + pins > 10 && frameIndex < 9) { _outputProvider.Write("Die Summe der beiden Würfe in einem Frame darf nicht mehr als 10 betragen. Bitte geben Sie eine gültige Anzahl Pins ein: "); return; } // Den Wurf zum aktuellen Frame hinzufügen if (frames[frameIndex].PinsRolled[0] == 0) { frames[frameIndex].PinsRolled[0] = pins; } else { frames[frameIndex].PinsRolled[1] = pins; frames[frameIndex].IsComplete = true; // Frame ist vollständig } currentRoll++; } CalculateScores(); _outputProvider.Write($"Aktuelle Punktzahl: {TotalScore()}"); } private int GetCurrentFrameIndex() { return currentRoll / 2; } private int TotalScore() { int totalScore = 0; foreach (var frame in frames) { totalScore += frame.Score; } return totalScore; } private bool Over() { // Stelle sicher, dass das Spiel 10 Frames hat if (frames.Count < 10) { return false; } var lastFrame = frames[9]; if (lastFrame.PinsRolled[0] == 10) { return lastFrame.PinsRolled.Length == 3 && currentRoll >= 12; } if (lastFrame.PinsRolled[0] + lastFrame.PinsRolled[1] == 10) { return currentRoll >= 11; } return currentRoll >= 20; } private void CalculateScores() { for (int i = 0; i < frames.Count; i++) { var frame = frames[i]; if (frame.IsComplete) { if (IsStrike(i)) { CalculateStrikeScore(frame, i); } else if (IsSpare(i)) { CalculateSpareScore(frame, i); } else { frame.Score = frame.PinsRolled[0] + frame.PinsRolled[1]; } } } } private void CalculateStrikeScore(Frame frame, int frameIndex) { frame.Score = 10 + StrikeBonus(frameIndex); } private void CalculateSpareScore(Frame frame, int frameIndex) { frame.Score = 10 + SpareBonus(frameIndex); } private bool IsStrike(int frameIndex) { return frames[frameIndex].PinsRolled[0] == 10; } private bool IsSpare(int frameIndex) { return frames[frameIndex].PinsRolled[0] + frames[frameIndex].PinsRolled[1] == 10; } private int StrikeBonus(int frameIndex) { if (frameIndex + 1 < frames.Count) { var nextFrame = frames[frameIndex + 1]; return nextFrame.PinsRolled[0] + (nextFrame.PinsRolled[1] != 0 ? nextFrame.PinsRolled[1] : (frameIndex + 2 < frames.Count ? frames[frameIndex + 2].PinsRolled[0] : 0)); } return 0; } private int SpareBonus(int frameIndex) { if (frameIndex + 1 < frames.Count) { return frames[frameIndex + 1].PinsRolled[0]; } return 0; } }
Frame类代码
public class Frame { public int[] PinsRolled { get; set; } public int Score { get; set; } public bool IsComplete { get; set; } public Frame() { PinsRolled = new int[2]; IsComplete = false; } }
问题根源分析
- 第10帧数组长度不足:保龄球规则中第10帧全中时允许额外2次投球(共3次),但默认Frame的
PinsRolled是长度为2的数组,存入第三次投球时会触发索引越界异常。 - 游戏结束判断逻辑错误:
Over方法中错误地检查lastFrame.PinsRolled.Length ==3,而Frame初始化时数组长度固定为2,该条件永远不成立,导致游戏不会终止,持续接受投球直到数组越界。 - 第10帧全中处理缺失:
AddRoll方法中仅处理前9帧的全中(frameIndex <9),第10帧的全中无法触发特殊逻辑,导致投球计数和帧状态更新错误。 - 全中奖励计算越界:
StrikeBonus方法中当计算第9帧的全中奖励时,可能尝试访问frames[11](超出数组索引范围0-9)。
修复方案
1. 调整Frame类支持第10帧的3次投球
修改Frame构造函数,根据是否为最后一帧初始化不同长度的数组:
public class Frame { public int[] PinsRolled { get; set; } public int Score { get; set; } public bool IsComplete { get; set; } public Frame(bool isLastFrame = false) { PinsRolled = isLastFrame ? new int[3] : new int[2]; IsComplete = false; } }
同时在Game构造函数中初始化第10帧时传入true:
public Game(IOutputProvider outputProvider) { _outputProvider = outputProvider; for (int i = 0; i < 10; i++) { frames.Add(new Frame(i == 9)); } }
2. 修正游戏结束判断逻辑
删除无效的数组长度检查,根据第10帧的状态判断所需投球次数:
private bool Over() { if (frames.Count < 10) { return false; } var lastFrame = frames[9]; if (IsStrike(9)) { // 第10帧全中需要3次投球,前9帧结束后currentRoll为18,3次投球后currentRoll达到21 return currentRoll >= 21; } else if (IsSpare(9)) { // 第10帧补中需要2次投球,currentRoll达到20时结束 return currentRoll >= 20; } else { // 第10帧未补中,2次投球后结束 return currentRoll >= 20; } }
3. 完善第10帧的全中处理逻辑
修改AddRoll方法中的全中判断条件,单独处理第10帧的情况:
private void AddRoll(int pins) { if (Over()) { throw new IndexOutOfRangeException("Das Spiel ist beendet!"); } int frameIndex = GetCurrentFrameIndex(); if (pins == 10 && currentRoll % 2 == 0) { _outputProvider.Write("Herzlichen Glückwunsch, Sie haben einen Strike geworfen!"); frames[frameIndex].PinsRolled[0] = pins; if (frameIndex < 9) { frames[frameIndex].IsComplete = true; currentRoll += 2; } else { // 第10帧全中,仅增加一次投球计数,继续接受额外投球 currentRoll++; } } else { if (frameIndex < 9 && frames[frameIndex].PinsRolled[0] + pins > 10) { _outputProvider.Write("Die Summe der beiden Würfe in einem Frame darf nicht mehr als 10 betragen. Bitte geben Sie eine gültige Anzahl Pins ein: "); return; } if (frameIndex == 9) { // 第10帧找到第一个空位置存入投球数 for (int i = 0; i < frames[frameIndex].PinsRolled.Length; i++) { if (frames[frameIndex].PinsRolled[i] == 0) { frames[frameIndex].PinsRolled[i] = pins; currentRoll++; // 判断第10帧是否完成 if ((IsStrike(9) && i == 2) || (!IsStrike(9) && !IsSpare(9) && i == 1) || (IsSpare(9) && i == 1)) { frames[frameIndex].IsComplete = true; } break; } } } else { // 前9帧原有逻辑不变 if (frames[frameIndex].PinsRolled[0] == 0) { frames[frameIndex].PinsRolled[0] = pins; } else { frames[frameIndex].PinsRolled[1] = pins; frames[frameIndex].IsComplete = true; } currentRoll++; } } CalculateScores(); _outputProvider.Write($"Aktuelle Punktzahl: {TotalScore()}"); }
4. 修正全中奖励计算的边界问题
修改StrikeBonus方法,避免访问超出索引范围的帧:
private int StrikeBonus(int frameIndex) { if (frameIndex >= 9) return 0; var nextFrame = frames[frameIndex + 1]; if (IsStrike(frameIndex + 1)) { // 下一帧也是全中,取下一帧的10分+下下帧的第一次投球(下下帧最多是第10帧) int bonus = 10; if (frameIndex + 2 <= 9) { bonus += frames[frameIndex + 2].PinsRolled[0]; } return bonus; } else { return nextFrame.PinsRolled[0] + nextFrame.PinsRolled[1]; } }
内容的提问来源于stack exchange,提问作者Linus Frenzer
相关产品推荐
相关产品推荐

