When we had to make the NextAction method static I ended up completely rewriting the class - maybe because I misunderstood the task at hand. It took a bit of work to get there but ended up working quite nicely.
I value readability over conciseness so it’s not perhaps as minimal as Ben’s solution (same can be said of my ScoreMaster) but it works. Use it if you like:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class ActionMaster {
public enum Action {
Reset,
Tidy,
EndTurn,
EndGame,
Invalid
}
public static Action NextAction (List<int> referenceBowls) {
// Take a copy of the list of bowls, otherwise we mutate it.
List<int> bowls = new List<int> (referenceBowls);
// Initial invalid state.
Action nextAction = Action.Invalid;
// Check that every bowl has a valid count, and throw an exception if not.
CheckValidPinStrikes (bowls);
// Keep track of frames so we can use different logic in the last frame.
int currentFrame = 1;
// Iterate until we have no bowls left to analyse
while (bowls.Count > 0) {
// Optimistically take the first two bowls (if possible),
// assuming they are a whole frame.
int take = (bowls.Count >= 2) ? 2 : 1;
List<int> currentBowls = bowls.GetRange (0, take);
bowls.RemoveRange (0, take);
// Last frame special handling. No further play is possible so we return
// the action from this method.
if (currentFrame == 10) {
// Check for a third ball in the original list
if (bowls.Count > 0) {
currentBowls.Add (bowls [0]);
bowls.Clear ();
}
return HandleFinalFrame (currentBowls);
}
// Handling of frames 1-9
nextAction = HandleRegularFrame (currentBowls);
// Return second bowl to the master list if the first was a strike
if (currentBowls.Count == 1) {
bowls.Insert (0, currentBowls [0]);
}
// Advance to the next frame
currentFrame++;
}
return nextAction;
}
private static void CheckValidPinStrikes (List<int> bowls) {
for (int i = 0; i < bowls.Count; i++) {
int pinsStruck = bowls [i];
if (pinsStruck < 0 || pinsStruck > 10) {
throw new UnityException ("invalid number of pins struck at index " + i + ": " + pinsStruck);
}
}
}
// Regular frame that might have one or two balls bowled
private static Action HandleRegularFrame (List<int> bowls) {
Action nextAction = HandleFirstBall (bowls [0]);
// End early conditions:
// - first ball strike or
// - only one ball bowled in this frame
// Remove the handled ball and exit with the first ball action.
if (nextAction == Action.EndTurn || bowls.Count == 1) {
bowls.RemoveAt (0);
return nextAction;
}
// Regardless of what is bowled, we end the turn.
bowls.Clear ();
return Action.EndTurn;
}
private static Action HandleFirstBall (int pinsStruck) {
if (pinsStruck == 10) {
return Action.EndTurn;
}
return Action.Tidy;
}
// Handle last three balls in their own specific ways.
private static Action HandleFinalFrame (List<int> bowls) {
if (bowls.Count == 1)
return HandleLastFrameFirstBall (bowls [0]);
if (bowls.Count == 2)
return HandleLastFrameSecondBall (bowls [0], bowls [1]);
// Regardless of the last pin score, it's the end of the game.
return Action.EndGame;
}
private static Action HandleLastFrameFirstBall (int pinsStruck) {
if (pinsStruck == 10)
return Action.Reset;
return Action.Tidy;
}
private static Action HandleLastFrameSecondBall (int firstBall, int secondBall) {
// First ball was a strike
if (firstBall == 10) {
// Second ball strike, reset to allow for third strike attempt.
if (secondBall == 10)
return Action.Reset;
// otherwise tidy for an attempt at a spare
return Action.Tidy;
}
// Picking up a spare with the second ball allows for a subsequent strike attempt.
if (firstBall + secondBall == 10)
return Action.Reset;
// No spare, no chance at a 21st ball in the game.
return Action.EndGame;
}
}