Well, I already had a static ActionMaster

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;
	}
}

Needless to say, I despise nested if/else blocks and rather have many small methods (which can make more detailed unit testing easier, as well as making the code easier to understand).

Privacy & Terms