You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

保龄球计分游戏单元测试异常:全中场景抛出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;
    }
}

问题根源分析

  1. 第10帧数组长度不足:保龄球规则中第10帧全中时允许额外2次投球(共3次),但默认Frame的PinsRolled是长度为2的数组,存入第三次投球时会触发索引越界异常。
  2. 游戏结束判断逻辑错误:Over方法中错误地检查lastFrame.PinsRolled.Length ==3,而Frame初始化时数组长度固定为2,该条件永远不成立,导致游戏不会终止,持续接受投球直到数组越界。
  3. 第10帧全中处理缺失:AddRoll方法中仅处理前9帧的全中(frameIndex <9),第10帧的全中无法触发特殊逻辑,导致投球计数和帧状态更新错误。
  4. 全中奖励计算越界: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 12:10:54