Click here to Skip to main content
15,880,469 members
Articles / Programming Languages / C#

Ten Pin Bowling Calculator

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
8 Aug 2009CPOL20 min read 63.7K   1.3K   14   4
Simple ten-pin bowling calculator to calculate the scores of the game
WinBowling.png

Introduction

Recently, I was tasked to carry out this assignment, and have not come across many articles on explaining this, let alone how to compute the scores. I have come across two open-source projects (see the references below) that do it but my needs were more simpler than that, hence this article aims to fill the gap and also it is about how to calculate the scores used in Ten Pin Bowling, using an OOP approach that is intuitive and easy to use. The scoring system can be confusing but will be dispelled once you read this. I used the open-source projects to compare the results of the scores.

Background

Ten Pin Bowling is a competitive game with a simple objective - to knock down as many pins per throw on a narrow alley with the pins situated at the end of the alley. The game composes of frames, at each frame all ten pins are set up standing, the player gets two throws per frame to knock the pins down. There are ten frames per game. A throw is where the player throws a ball down the alley to attempt knocking down pin(s).

In each frame, there are two throws, with the exception of the last frame (will be discussed shortly).

  • If all pins are knocked down on the first throw, that is called a Strike.
  • If the player fails to knock down ALL pins on the first throw but succeeds on the second throw, that is called a Spare.
  • x pins knocked on the first throw, and the remainder of y pins knocked on the second throw, such that, x + y is less than 10.
    Forgive the usage of mathematical notations - everything tis clear as mud aye!

The scoring works like this:

  • Strike - Add ten plus the number of pins knocked down by the next two balls to the score of the previous frame.
  • Spare - Add ten plus the number of pins knocked down by the next ball to the score of the previous frame.
  • Normal - Add the total of pins knocked down by the two throws to the previous frame. A gutter ball, is when the ball ends up in the gutter and does not knock any pins, that is worth zero points! :)

There, that's it, sounds tricky...wait...

On the last frame, if a Strike is thrown, the player has two throws to complete the frame. Likewise, if a Spare is thrown, the player has one throw to complete the frame. Hence the last frame will have three throws! Hmmm... how to compute THAT!

Note: Discussion of tactics/techniques are not discussed here. If you're looking to get a perfect score of maximum 300 points, you've come to the wrong place, but feel free to read it, and no, it will not help you anyway! :)

Using the Code

I will discuss the classes used and then the GUI, but first let's head on into classes here. Here's an overview of the classes in UML diagram (created with Dia).

UML Classes

Click on the image below to see the full UML in its glory!

Click to enlarge image

A look at the classes and what they mean:

  • BallThrow - This class is responsible for holding the number of pins knocked, and can tell if that throw is:
    • A Gutter ball
    • A Strike
    • A Spare

    There are only two methods (both overloads) in this class called MarkBall.

  • Frame - This class holds an internal generic list of type BallThrow, and for whatever frame the state of play is at, it will fire an event EndOfFrame, likewise when the game is finished, i.e. all ten frames are completed, it fires an event AllFramesDone. The knocking of pins is handled here by invoking ThrowBall, which in turn invokes the method overloads MarkBall of the BallThrow class.
  • FrameHandler - This is the main class which holds an internal generic list of type Frame, this also happens to be a consumer of the events fired from the Frame class. Speaking of events, this class fires off three events to the external consumer, in this case of the application, the GUI, consumes it and acts accordingly.
    • Score - This is how the GUI updates the score for each frame.
    • PerfectGame - This will fire if the maximum points have reached, i.e. 300 (You'd have to be a good bowler or the 'King of Pins' to achieve this.)
    • EndOfGame - This will fire when the game ends and all ten frames are completed.

    It is also used to handle the state of play and keep track of the scoring by referring to the above two classes. When playing a frame, the UI extracts the appropriate frame from this class (Frame) by simply indexing using the actual number that denotes the frame and sets it accordingly by invoking the method overloads for ThrowBall depending on whether it was a strike/spare or a number used (how the GUI marks it as a strike/spare will be discussed shortly).

  • FrameScoreEventArgs - A class inherited from System.EventArgs to hold the Frame number (FrameNumber) and the score for that frame (FrameScore). The events that use this class are found in Frame.EndOfFrame and in FrameHandler.Score.

Now that the overview of the classes are done, this should make it easier to understand the code as we'll go through below. All XML comments are stripped for brevity here.

BallThrow

C#
public class BallThrow{
    ....
    //
    public BallThrow() {}
    //
    public BallThrow(int iPinsKnocked) {
        this._iPinsKnocked = iPinsKnocked;
    }
    //
    ....
    //
    public void MarkBall(int iPinsKnocked){
        this._iPinsKnocked = iPinsKnocked;
    }
    //
    public void MarkBall(bool bStrike, bool bSpare) {
        this._bThrowIsStrike = bStrike;
        this._bThrowIsSpare = bSpare;
    }
}
Figure 1.

This class in Figure 1, above, only has a number of properties which uses privately declared variables to determine the state of this throw.

  • PinsKnocked - The number of pins knocked down, this uses _iPinsKnocked.
  • IsAGutter - Not really used here in this application but useful nonetheless! If no pins are knocked down, then it checks for comparison with _iPinsKnocked to zero.
  • SpareBall - Was this throw a spare? It uses _bThrowIsSpare to determine the status of throw.
  • StrikeBall - Was this throw a strike? This uses _bThrowIsStrike to determine if this throw resulted in a strike.

Astute readers will notice that there are two constructors, the reason is, further on higher up in the class hierarchy, if a throw was a spare or strike, a need to instantiate this class with an empty constructor is required and the second overload of MarkBall is invoked to mark this throw as spare or a strike. Otherwise, if this was a normal throw, the number of pins knocked down is passed into the constructor.

  • MarkBall(int iPinsKnocked) - Sets up the number of pins knocked down by assigning _iPinsKnocked to the desired quantity.
  • MarkBall(bool bStrike, bool bSpare) - Sets the appropriate flag to mark this throw as a strike or a spare.

Frame

C#
public class Frame {
    ....
    //
    public Frame(int iFrameNo) {
        this._iFrameNo = iFrameNo;
        this._throwList = new List<BallThrow>();
    }
    //
    public BallThrow this[int index] {
        get {
            if (this._throwList.Count == 0) return null;
            if (!this.IsLastFrame) {
                if (index >= BTHelpers.FIRST_THROW_INDEX && 
		index <= BTHelpers.SECOND_THROW_INDEX) return this._throwList[index];
                else return null; // Out of bounds....
            } else {
                // Last Frame
                if (index >= BTHelpers.FIRST_THROW_INDEX && 
		index <= BTHelpers.LAST_THROW_INDEX) return this._throwList[index];
                else return null; // Out of bounds....
            }
        }
    }
    //
    ...
    ...
}
Figure 2.

I deliberately left out the MarkBall overloads as this will be discussed shortly. This class in Figure 2, above, only has a number of properties about the frame:

  • this - This returns back the appropriate BallThrow object, there's a validation to check on the index to the List<BallThrow> collection.
  • Number - This simply tells what frame we're at.
  • IsLastFrame - Are we on the last frame? (Remember the last frame could have three throws)
  • IsFirstFrame - Are we on the first frame?
  • IsStrike - Was this frame a strike?
  • IsSpare - Was this frame a spare?
  • TotalPinsKnocked - The total number of pins knocked down in this frame. In the case of either a strike or a spare, this returns 10 either way.
  • IsNormal - Does this frame have no strike or spare, i.e. the sum of pins knocked down which is less than 10.
  • CountOfThrows - The number of throws in this frame.
  • FirstBall - Returns back the number of pins knocked down on the first throw, otherwise it will have 10 for a strike.
  • SecondBall - Returns back the number of pins knocked down on the second throw. If this frame was a spare, then FirstBall + this shall equal to 10.
  • LastBall - This is for if we're the last frame and to return back the number of pins knocked down.

Note that the property TotalPinsKnocked is taken care of in the method overloads MarkBall and takes into account the last frame especially!

Frame.MarkBall(...)

C#
public void ThrowBall(int iPins) {
    if (!this.IsLastFrame) {
        if (this._throwList.Count == 0) { 
            // First throw...
            BallThrow throwBall = new BallThrow(iPins);
            //
            if (iPins == BTHelpers.MAX_PINS) 
		throwBall.MarkBall(true, false); // A Strike was used!
            this._throwList.AddRange(new BallThrow[] { throwBall, new BallThrow() });
            if (throwBall.StrikeBall) {
                throwBall.MarkBall(BTHelpers.MAX_PINS);
                int TotalPinsKnocked = BTHelpers.MAX_SCORE;
                this._iFrameScore = TotalPinsKnocked;
                this._iTotalPinsKnocked = BTHelpers.MAX_SCORE;
                this.OnEndOfFrame(new FrameScoreEventArgs
			(TotalPinsKnocked, this._iFrameNo));
                return;
            }
        } else {
            // Check for the first throw
            BallThrow prevThrow = this._throwList[BTHelpers.FIRST_THROW_INDEX];
            if (prevThrow.PinsKnocked + iPins == BTHelpers.MAX_PINS) {
                // Spare!
                this._throwList[BTHelpers.SECOND_THROW_INDEX].MarkBall(false, true);
            }
            this._throwList[BTHelpers.SECOND_THROW_INDEX].MarkBall(iPins);
            int TotalPinsKnocked = this._throwList
				[BTHelpers.FIRST_THROW_INDEX].PinsKnocked +
                                    this._throwList
				[BTHelpers.SECOND_THROW_INDEX].PinsKnocked;
            //
            this._iFrameScore = TotalPinsKnocked;
            if (this._throwList.Count == BTHelpers.FRAME_STD_THROWS) {
                this._iTotalPinsKnocked = TotalPinsKnocked;
                this.OnEndOfFrame(new FrameScoreEventArgs
				(TotalPinsKnocked, this._iFrameNo));
            }
        }
    } else {
        BallThrow prevThrow = null;
        BallThrow throwLast = new BallThrow(iPins);
        switch (this._throwList.Count) {
            case 0:
                // Simple enough case...we're on the first throw of the last frame.
                // This could be either n pins knocked or a strike
                BallThrow throwFirstBall = new BallThrow(iPins);
                if (iPins == BTHelpers.MAX_PINS) throwFirstBall.MarkBall(true, false);
                this._throwList.Add(throwFirstBall);
                break;
            case 1:
                // Second throw of the last frame
                // Check the previous
                prevThrow = this._throwList[BTHelpers.FIRST_THROW_INDEX]; 
                if (prevThrow.StrikeBall) {
                    // this... CANNOT BE A SPARE - IMPOSSIBLE! 
		  // 1st Throw a Strike, IMPOSSIBLE TO HAVE A SPARE in the second
                    this._throwList.Add(throwLast);
                    return;
                } else {
                    // Both ordinary no's...i.e. the first throw was n Pins, 
		  // the second throw was (MAX_PINS - n) pins knocked down.
                    if (prevThrow.PinsKnocked + iPins < BTHelpers.MAX_PINS) {
                        this._throwList.Add(throwLast);
                        int TotalPinsKnocked = this._throwList
				[BTHelpers.FIRST_THROW_INDEX].PinsKnocked +
                                   	this._throwList
				[BTHelpers.SECOND_THROW_INDEX].PinsKnocked;
                        //
                        this._iFrameScore = TotalPinsKnocked;
                        this._iTotalPinsKnocked = TotalPinsKnocked;
                        // Signal end of frame...no more throws at this point
                        this.OnEndOfFrame(new FrameScoreEventArgs
				(TotalPinsKnocked, this._iFrameNo));
                        // Signal game complete!
                        this.OnAllFramesDone();
                        return;
                    } else {
                        // Both ordinary no's...i.e. the first throw was n Pins, 
		      // the second throw was spare!
                        // So another throw would be used i.e. 3rd throw.
                        if (prevThrow.PinsKnocked + iPins == BTHelpers.MAX_PINS) {
                            // This is a spare
                            throwLast.MarkBall(false, true);
                            this._throwList.Add(throwLast);
                            int TotalPinsKnocked = BTHelpers.MAX_PINS;
                            this._iFrameScore = TotalPinsKnocked;
                        }
                    }
                }
                break;
            case 2:
            default:
                // We're on the last throw
                prevThrow = this._throwList
		[BTHelpers.SECOND_THROW_INDEX]; // Check the second throw
                if (prevThrow.StrikeBall) {
                    // this... CANNOT BE A SPARE - IMPOSSIBLE!
                    this._throwList.Add(throwLast);
                    int TotalPinsKnocked = 
			this._throwList[BTHelpers.SECOND_THROW_INDEX].PinsKnocked +
                        	this._throwList[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
                    //
                    this._iFrameScore = TotalPinsKnocked;
                    this._iTotalPinsKnocked = TotalPinsKnocked;
                    // Signal end of frame...no more throws at this point
                    this.OnEndOfFrame(new FrameScoreEventArgs(TotalPinsKnocked, 
				this._iFrameNo));
                    this.OnAllFramesDone();
                    return;
                }
                if (prevThrow.SpareBall) {
                    // Oh! the 2nd throw was a spare...
                    if (prevThrow.PinsKnocked + 
			iPins < BTHelpers.MAX_PINS) { // Both ordinary no's...
                        this._throwList.Add(throwLast);
                        int TotalPinsKnocked = BTHelpers.MAX_PINS + 
			this._throwList[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
                        this._iFrameScore = TotalPinsKnocked;
                        this._iTotalPinsKnocked = TotalPinsKnocked;
                        // Signal end of frame...no more throws at this point
                        this.OnEndOfFrame(new FrameScoreEventArgs
				(TotalPinsKnocked, this._iFrameNo));
                    } else {
                        if (prevThrow.PinsKnocked + iPins == 
				BTHelpers.MAX_PINS) { // this throw + 
				// prev = spare.. Will have another shot!...
                            throwLast.MarkBall(false, true);
                            this._throwList.Add(throwLast);
                            int TotalPinsKnocked = BTHelpers.MAX_PINS + 
                               this._throwList[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
                            //
                            this._iFrameScore = TotalPinsKnocked;
                            this._iTotalPinsKnocked = TotalPinsKnocked;
                            // Signal end of frame...no more throws at this point
                            this.OnEndOfFrame(new FrameScoreEventArgs
				(TotalPinsKnocked, this._iFrameNo));
                        } else {
                            // 2nd throw was a spare
                            // 3rd throw was a normal one with n pins knocked.
                            throwLast.MarkBall(false, true);
                            this._throwList.Add(throwLast);
                            int TotalPinsKnocked = BTHelpers.MAX_PINS + 
			   this._throwList[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
                            this._iFrameScore = TotalPinsKnocked;
                            this._iTotalPinsKnocked = TotalPinsKnocked;
                            // Signal end of frame...no more throws at this point
                            this.OnEndOfFrame(new FrameScoreEventArgs
					(TotalPinsKnocked, this._iFrameNo));
                        }
                    }
                    // Signal game complete!
                    this.OnAllFramesDone();
                } else {
                    // 2nd Throw was just an ordinary pins knocked
                    // this last throw just happened
                    this._throwList.Add(throwLast);
                    // Normal digits...
                    int TotalPinsKnocked = this._throwList
				[BTHelpers.FIRST_THROW_INDEX].PinsKnocked + 
                                     this._throwList
				[BTHelpers.SECOND_THROW_INDEX].PinsKnocked + 
                                     this._throwList
				[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
                    //
                    this._iFrameScore = TotalPinsKnocked;
                    this._iTotalPinsKnocked = TotalPinsKnocked;
                    // Signal End of Frame
                    this.OnEndOfFrame(new FrameScoreEventArgs
				(TotalPinsKnocked, this._iFrameNo));
                    // Signal game complete!
                    this.OnAllFramesDone();
                }
                // Allow it to fall through!
                break;
        }
    }
}
Figure 3.

Now, what a mouthful of code...fear not, for I shall lead you down the bowling alley and the light will shine! :) Ok, seriously, the logic is similar to the second overload of the above method, frames 1 to 9 are simple enough, it's the last frame that's where we need to watch out for, because there are a number of possibilities combining strike(s) and spare(s). It should be noted:

  • For frames 1-9 inclusive, if a number of pins was thrown and is the first throw, then this is what happens:
    1. An empty BallThrow class is instantiated - throwBall.
    2. throwBall.MarkBall (2nd overload) is invoked to set the flags then... (This is optional unless a strike is thrown.)
    3. throwBall.MarkBall (1st overload) is invoked to set the pins knocked down to the desired number of pins (This will be 10 if this was a strike, and hence, this frame gets marked as a strike).
    4. The newly instantiated class is added to the collection (List<BallThrow>) using the AddRange method of the generic list collection adding another empty BallThrow to the collection, to make this Frame consist of two throws.
    5. The event OnEndOfFrame gets fired, in which the FrameHandler class can then advance on to the next Frame.

    Otherwise, if this was a second throw, then this is what happens:

    1. A check is made by obtaining the first throw in the <BallThrow> collection, prevThrow (type of BallThrow class), and checking if the sum of pins knocked down on the previous throw equals 10, then this throw gets marked as a spare.
    2. The event OnEndOfFrame gets fired, in which the FrameHandler class can then advance on to the next Frame.
  • For the last frame, a check is made to see what throw we are on by checking the List<BallThrow>'s property Count and using the switch statement to determine the throw we're on..we also create a new BallThrow object which gets added to the collection in one of the cases below...
    1. If it is 0, then we're on the first throw of this last frame, again, we check if a strike was thrown by comparing to 10. Add it to the collection. No events are fired at this point.
    2. If it is 1, then we're on the second throw, again, we check the _prevThrow which is a BallThrow object accessed from the generic collection...i.e. first throw...
      • We then determine if the previous throw was a strike.
      • If the _prevThrow's pins plus this throw's pins is less than 10, then we add this throw to the generic collection, fire the event EndOfFrame and also fire another event AllFramesDone.
      • If the _prevThrow's pins plus this throw's pins is equal to 10, then we add this throw to the generic collection, mark this throw as a spare. No events are fired at this point since a third throw will be warranted.
    3. If it is 2, then we're on the third throw, again, we check the _prevThrow which is a BallThrow object accessed from the generic collection...i.e. second throw...
      • We then determine if the previous throw was a strike, add it to the generic collection and fire two events EndOfFrame and AllFramesDone.
      • If the _prevThrow was a spare, then we add to the generic collection and again, fire two events EndOfFrame and AllFramesDone.
      • If the _prevThrow's pins plus this throw's pins is less than 10, then we add this throw to the generic collection, fire the event EndOfFrame and also fire another event AllFramesDone.
      • If the _prevThrow's pins plus this throw's pins is equal to 10, then we add this throw to the generic collection, mark this throw as a spare, fire the event EndOfFrame and also fire another event AllFramesDone.

FrameHandler

Now we're almost done, this class is next up for discussion.

C#
public class FrameHandler : IEnumerable<frame>, IEnumerator<frame> {
    ....
    //
    public FrameHandler() {
        for (int nLoopCnt = 0; nLoopCnt < BTHelpers.MAX_FRAMES; nLoopCnt++) {
            Frame f = new Frame(nLoopCnt + 1);
            f.EndOfFrame += new EventHandler<FrameScoreEventArgs>(f_EndOfFrame);
            this._listFrames.Add(f);
        }
        Frame fLastFrame = this[BTHelpers.LAST_FRAME_INDEX];
        fLastFrame.AllFramesDone += 
		new EventHandler<EventArgs>(fLastFrame_AllFramesDone);
    }
    //
    ....
    //
    void fLastFrame_AllFramesDone(object sender, EventArgs e) {
        System.Diagnostics.Debug.WriteLine("**** THIS GAME IS COMPLETE ****");
        Frame fPrev = this[BTHelpers.LAST_FRAME_INDEX - 1];
        Frame fCurr = this[BTHelpers.LAST_FRAME_INDEX];
        fCurr.Score = fCurr.TotalPinsKnocked + fPrev.Score;
        this.OnScore(new FrameScoreEventArgs(fCurr.Score, fCurr.Number));
        this.OnEndOfGame();
        if (fCurr.Score == BTHelpers.MAX_FRAMES_SCORE) this.OnPerfectGame();
    }
    //
    void f_EndOfFrame(object sender, FrameScoreEventArgs e) {
        this.CalcScore(e.FrameNumber);
        this.Reset();
        foreach (Frame f in this) {
            if (!f.IsLastFrame) this.OnScore
		(new FrameScoreEventArgs(f.Score, f.Number));
        }
    }

    // IEnumerable<frame> Members
    ....
    //
    ....
    //
    private int CalcScore(int iFrameNo) {
        int iScore = 0;
        //System.Diagnostics.Debug.WriteLine(string.Format("[DEBUG] - 
        //CalcScore() - FRAME #{0} **********", iFrameNo));
        for (int iCurrentFrame = BTHelpers.FIRST_FRAME_INDEX; 
			iCurrentFrame < iFrameNo; iCurrentFrame++) {
            Frame f = this[iCurrentFrame];
            Frame fNext = this[iCurrentFrame + 1];
            Frame fNextNext = this[iCurrentFrame + 2];
            int iNextTwoBalls = 0;
            if (fNext != null){
                iNextTwoBalls += fNext.FirstBall;
                if (fNext.SecondBall == 0) {
                    if (fNextNext != null) {
                        iNextTwoBalls += fNextNext.FirstBall;
                    }
                } else {
                    iNextTwoBalls += fNext.SecondBall;
                }
            }
            if (f.IsStrike)  iScore += BTHelpers.MAX_SCORE + iNextTwoBalls;
            else if (f.IsSpare) iScore += BTHelpers.MAX_SCORE + fNext.FirstBall;
            else iScore += f.TotalPinsKnocked;
            f.Score = iScore;
            //System.Diagnostics.Debug.WriteLine
	   //(string.Format("[DEBUG] - CalcScore() - Score: {0}", iScore));
        }
        return iScore;
    }
}
Figure 4.

This class is the meat of it all and handles the consuming of the Frame's events EndOfFrame and AllFramesDone. When this class gets instantiated, it initializes the generic list collection, List<Frame>. Notice how we looped from Frame 1 to 9 inclusive and assign the event handler for EndOfFrame.
Then we specifically add a new Frame that denotes the tenth frame and assign the event handler to AllFramesDone.
It also implements the IEnumerable<Frame> and IEnumerator<Frame> to make it easy to iterate through the generic collection List<Frame>. Notice how I did not allow the Frame's events to get propagated to the outside, instead it gets handled here, and this class simply forwards it on to the outside, EndOfGame and PerfectGame.

From this so far, due to the way this was designed, perhaps a weakness maybe, that you cannot go back to a frame and stick in a value as it's already set up and would lead to incorrect results as the throws for a frame that you went back onto, was already done.
There is only one property for this class accessible to the outside:

  • this[index] - This returns back the appropriate frame.

The exposable method to the outside is ClearAllThrows which simply iterates through the collection and resets the Frame's collection of BallThrow.

  • Upon receiving the Frame's event EndOfFrame, the class invokes CalcScore and iterates through the collection, repeatedly firing off Score event to update the score as it goes through each frame.
  • After the Frame's event AllFramesDone gets trapped, we then add on the sum of Frame 9 to Frame 10, to give the final score for Frame 10 and fire off the event EndOfGame. We also check to see if the score has reached maximum points of 300 and fire the event PerfectGame.

In referring to the private method CalcScore, the code to handle the scoring logic is this in pseudocode...

int score = 0;
for each frame
    if frame is strike then score += 10 + next_two_balls();
    else if frame is spare then score += 10 + next_ball();
    else score += frame's score.

next_two_balls()
    return firstball + secondball;
next_ball()
    return firstball;
Figure 5.

The trick lies in the usage of the next_two_balls in the above Figure 5, since when a frame has a strike, it has two BallThrow's in that Frame's generic collection, the first element of the zero-th index would be 10, the next element of the first index would be 0, this would obviously not make sense i.e. 10 + 0 = 10... (I fell for this when I discovered the code didn't calculate properly - duh!), so I used the next frame's first ball! That would explain why I had two variables fNext and fNextNext and check on them to compute iNextTwoBalls as shown in Figure 4 above. The results computed then get assigned to that frame. Ok, everyone got that - aye tis clear as mud, wrong? right!

Now that's the meat of the bowling calculator out of the way, time to move on to the UI. By now, you can imagine, when the UI receives the events from the FrameHandler's class, namely EndOfGame, PerfectGame and Score, you can deduce it notifies the user of that - and updates the UI to reflect that. So far, so good...

The User Interface

The UI code is split up into two files Form1.cs and frmBowlClass.cs. Let's talk about the controls for frames used which makes up the interface...

  • TextBoxes - For frames 1-9 inclusive, there's two textboxes (representing a throw each), for the last frame, there's three, inputs for each of the frame allowed are:
    • Numeric - 0..9 only
    • Spare - / only
    • Strike - X only

    It should be noted that the input length is 1 only, any other inputs are discarded (or assigned to /dev/null for that matter) and also the inputs advance on to the next frame automatically.

    • Only the first throw can have an 'X' (a strike). The second input box becomes disabled.
    • Only the second throw can have an '/' (a spare).
    • The above two is for frames 1-9 inclusive!
    • Numbers 0-9 on both throws are accepted.
  • Labels - This is purely to show the score for each of the frames.

Each of the above are placed into the group box (or child controls if you prefer). The {FrameNo} and {ThrowNo} are substituted with the actual numbers indicating the frame and throw respectively. The names I gave to these controls are:

  • Group boxes - grpFrame{FrameNo}, and in each group box, there's two text boxes and a label..
  • Text boxes - txtBoxF{FrameNo}Throw{ThrowNo}...
  • Labels - lblFrame{FrameNo}.

Right on, let's move on from here...

Form1.cs

C#
private const string KEY_TAB = "{TAB}";
//
private const string LABELNAME = "lblFrame{0}";
private const string FIRSTTHROW_ANY_FRAME = "txtBoxF{0}Throw1";
private const string SECONDTHROW_ANY_FRAME = "txtBoxF{0}Throw2";
private const string THIRDTHROW_LAST_FRAME = "txtBoxF10Throw3";
//
private const string SPARE = "/";
private const string STRIKE = "X";
//
private const string REGEXP_FRAMENO = "frameNo";
private const string REGEXP_THROWNO = "throwNo";
//
private readonly string VALID_KEYS_THROW = "/0123456789X";
//
private System.Text.RegularExpressions.Regex _reTextInputName = new Regex(@
    "^txtBoxF(?<frameno>\d+)Throw(?<throwno />\d+)$", 
    System.Text.RegularExpressions.RegexOptions.Compiled | 
	System.Text.RegularExpressions.RegexOptions.IgnoreCase);
//
private System.Text.RegularExpressions.Regex _reLblName = new Regex(@
    "^lblFrame(?<frameno>\d+)", 
    System.Text.RegularExpressions.RegexOptions.Compiled | 
	System.Text.RegularExpressions.RegexOptions.IgnoreCase);
//
private System.Text.RegularExpressions.Regex _reGrpName = new Regex(@"
    ^grpFrame(?<frameno>\d+)", 
    System.Text.RegularExpressions.RegexOptions.Compiled | 
	System.Text.RegularExpressions.RegexOptions.IgnoreCase);
//
private BowlTracker.FrameHandler _frameHandler = null;
//
private System.Collections.Generic.Dictionary<int, /> _hashCtls = new Dictionary<int, />();
//
public frmBowlCalc() {
    InitializeComponent();
    this.HashGroups();
    this._frameHandler = new FrameHandler();
    this._frameHandler.Score += 
	new EventHandler<framescoreeventargs>(_frameHandler_Score);
    this._frameHandler.EndOfGame += new EventHandler<eventargs />(_frameHandler_EndOfGame);
    this._frameHandler.PerfectGame += new EventHandler<eventargs />(_frameHandler_PerfectGame);
}
Figure 6.

This explains why you're seeing the regex's and the constants...Looks crappy but...(I am my own worst enemy). Let's take a look at this constructor, it calls HashGroups and initializes a new instance of FrameHandler, sets up the event handlers for Score, EndOfGame and PerfectGame. Now, but wtf is HashGroups... hmmm... is there some hocus pocus going on here?.... Let's take a dive into it... HashGroups is situated in frmBowlClass.cs so look in there.

HashGroups

C#
private void HashGroups() {
    foreach (Control c in this.Controls) {
        if (c is GroupBox) {
            GroupBox grp = (GroupBox)c as GroupBox;
            if (grp != null) {
                Match m = this._reGrpName.Match(grp.Name);
                if (m.Success) {
                    int iFrameNo = int.Parse(m.Groups[REGEXP_FRAMENO].Value);
                    this._hashCtls.Add(iFrameNo, grp);
                }
            }
        }
    }
}
Figure 7.

The above method, shown in the above Figure 7, is a generic collection Dictionary<int, GroupBox> which simply stores the GroupBox into the dictionary with the key being as the Frame Number. Yup, you've guessed it, each control in each group has a frame number from 1 to 10 inclusive! This makes it easier to pull in the label, find out the contents entered into the appropriate text box and perform some magic on it.

From looking at Figure 7, you can see how the controls on the UI are iterated and checked if the control is a group box, extract the frame number from the group box's Name property by performing a regexp matching to extract the desired frame number and add it to the dictionary.
This makes things easier to "get at" the appropriate label to update the score...see Figure 8 below for the two methods used:

C#
private TextBox LookUpTextBox(int iFrameNo, string sTxtBoxTargetName) {
    GroupBox grpBox = this._hashCtls[iFrameNo];
    if (grpBox != null) {
        foreach (Control t in grpBox.Controls) {
            if (t is TextBox) {
                TextBox txtBox = (TextBox)t;
                if (txtBox.Name.Equals(sTxtBoxTargetName)) return txtBox;
            }
        }
    }
    return null;
}

private Label LookUpLabel(int iFrameNo, string sLblBoxTargetName) {
    GroupBox grpBox = this._hashCtls[iFrameNo];
    if (grpBox != null) {
        foreach (Control t in grpBox.Controls) {
            if (t is Label) {
                Label lblBox = (Label)t;
                if (lblBox.Name.Equals(sLblBoxTargetName)) return lblBox;
            }
        }
    }
    return null;
}
Figure 8.

Those two Figures, 7 and 8 respectively are in frmBowlClass.cs...Was that code just being smart and clever?.... Now it is obvious how the score gets shoved into the label on the UI, LookUpLabel... likewise the similar method for LookUpTextBox...

Let's take a look at how the score "gets at" the appropriate label for a frame, this is in Form1.cs by the way...

C#
void _frameHandler_Score(object sender, FrameScoreEventArgs e) {
    if (e.FrameScore > 0) {
        Label lblFrame = this.LookUpLabel
		(e.FrameNumber, string.Format(LABELNAME, e.FrameNumber));
        lblFrame.Text = e.FrameScore.ToString();
    }
}
Figure 9.

There... wasn't so difficult, was it... notice how the LookUpLabel was used to get the specific label for a specific frame number as shown in Figure 9.

The common code that is shared between all text box inputs to filter out fluff is situated in txtBoxThrow_KeyPress, see Figure 10 below. The code to handle each throw is in txtBoxThrow(One|Two|Three)_KeyUp event handlers. ...let's take a look at the first throw input box...again, as shown below in Figure 10.

C#
// in Form1.cs!!
private void txtBoxThrowOne_KeyUp(object sender, KeyEventArgs e) {
    TextBox txtBox = (TextBox)sender as TextBox;
    if (txtBox != null && txtBox.Text.Length > 0) {
        if (this.HandleFirstThrow(txtBox)) {
            e.Handled = true;
            SendKeys.Send(KEY_TAB);
        } else e.Handled = false;
    }
}

private void txtBoxThrowOne_KeyUp(object sender, KeyEventArgs e) {
    TextBox txtBox = (TextBox)sender as TextBox;
    if (txtBox != null && txtBox.Text.Length > 0) {
        if (this.HandleFirstThrow(txtBox)) {
            e.Handled = true;
            SendKeys.Send(KEY_TAB);
        } else e.Handled = false;
    }
}

// in frmBowlClass.cs
private bool HandleFirstThrow(TextBox txtBox) {
    string sName = txtBox.Name;
    Match m = this._reTextInputName.Match(sName);
    if (m.Success) {
        int iFrameNo = int.Parse(m.Groups[REGEXP_FRAMENO].Value);
        int iThrowNo = int.Parse(m.Groups[REGEXP_THROWNO].Value);
        if (iFrameNo < BowlTracker.BTHelpers.MAX_FRAMES) {
            if (txtBox.Text.Equals(STRIKE)) {
                this.DisableSecondThrow(iFrameNo);
                this._frameHandler[iFrameNo].ThrowBall(true, false);
                return true;
            }
            if (txtBox.Text.Equals(SPARE)) { 	// Cannot have a spare on the 
					// first throw! - illogical!
                this.InvalidThrow(txtBox);
                return false;
            }
        } else {
            // Last Frame...
            if (txtBox.Text.Equals(STRIKE)) {
                this._frameHandler[iFrameNo].ThrowBall(true, false);
                return true;
            }
        }
        // Anything else here would be numeric...
        int iPins = int.Parse(txtBox.Text);
        this._frameHandler[iFrameNo].ThrowBall(iPins);
        // Go back to previous frame and check if a spare was used
        return true;
    }
    return false;
}
Figure 10.

Looking at the txtBoxThrowXXX_KeyUp event handler, it is obvious how if the input is deemed valid then it advances on to the next frame using the Send method of SendKeys class.

In the function HandleFirstThrow returns a boolean which will determine if it's ok to advance onwards to the next input.

The parameter TextBox's Name property gets passed through regexp to determine which throw and which frame is this textbox under, we now also can reference the appropriate frame by using the indexer to the FrameHandler class to manipulate the frame. A check is made here to see which frame are we on.

  • We then check if a strike was entered, if it was, the method DisableSecondThrow is called which prevents any input in the textbox for the second throw. And the method ThrowBall gets called marking the selected frame as a strike.
  • Otherwise, we perform a simple check if the Spare was entered, if it was, then a method InvalidThrow gets called which simply clears the textbox thus forcing the user to enter a proper input.
  • Anything else entered, i.e. numeric input, gets passed into the method ThrowBall of the appropriate frame's class.

Let's look at the handler for handling second throws below in Figure 11.

C#
private bool HandleSecondThrow(TextBox txtBox) {
    string sName = txtBox.Name;
    Match m = this._reTextInputName.Match(sName);
    if (m.Success) {
        int iFrameNo = int.Parse(m.Groups[REGEXP_FRAMENO].Value);
        int iThrowNo = int.Parse(m.Groups[REGEXP_THROWNO].Value);
        if (iFrameNo < BowlTracker.BTHelpers.MAX_FRAMES) {
            // We're on the other frame(s)...
        Throw2ndSpare:
            if (txtBox.Text.Equals(SPARE)) {
                this._frameHandler[iFrameNo].ThrowBall(false, true);
                return true;
            }
            if (txtBox.Text.Equals(STRIKE)) {
                this.InvalidThrow(txtBox);
                return false;
            }
            TextBox prevThrow = this.LookUpTextBox
		(iFrameNo, string.Format(FIRSTTHROW_ANY_FRAME, iFrameNo));
            int iFirstThrow = 0;
            if (prevThrow != null) iFirstThrow = int.Parse(prevThrow.Text);
            int iSecondThrow = int.Parse(txtBox.Text);
            if (iFirstThrow + iSecondThrow == BowlTracker.BTHelpers.MAX_PINS) {
                // Set it to spare
                txtBox.Text = SPARE;
                goto Throw2ndSpare;
            }
            if (iFirstThrow + iSecondThrow < BowlTracker.BTHelpers.MAX_PINS) {
                this._frameHandler[iFrameNo].ThrowBall(iSecondThrow);
                return true;
            }
            // Any thing else here is an error!
            this.InvalidThrow(txtBox);
            return false;
        } else {
            // We're on the last Frame...
            if (txtBox.Text.Equals(SPARE) || txtBox.Text.Equals(STRIKE)) {
                if (txtBox.Text.Equals(STRIKE)) 
		this._frameHandler[iFrameNo].ThrowBall(true, false);
                if (txtBox.Text.Equals(SPARE)) 
		this._frameHandler[iFrameNo].ThrowBall(false, true);
                return true;
            } else {
                TextBox txtBoxPrevThrow = this.LookUpTextBox
			(BowlTracker.BTHelpers.MAX_FRAMES, 
                            string.Format(FIRSTTHROW_ANY_FRAME, 
			BowlTracker.BTHelpers.MAX_FRAMES));
                //
                if (!txtBoxPrevThrow.Text.Equals(STRIKE)) {
                    // Disable the third
                    TextBox txtBoxLastFrameLastThrow = this.LookUpTextBox
			(BowlTracker.BTHelpers.MAX_FRAMES, THIRDTHROW_LAST_FRAME);
                    if (txtBoxLastFrameLastThrow != null) 
			txtBoxLastFrameLastThrow.Enabled = false;
                }
            }
            int iSecondThrow = int.Parse(txtBox.Text);
            this._frameHandler[iFrameNo].ThrowBall(iSecondThrow);
            return true;
        }
    }
    return false;
}
Figure 11.

Again, this function in Figure 11, above, has a similar signature as shown in Figure 10....

In the function HandleSecondThrow returns a boolean which will determine if it's ok to advance onwards to the next input.
The parameter TextBox's Name property gets passed through regexp to determine which throw and which frame is this textbox under, we now also can reference the appropriate frame by using the indexer to the FrameHandler class to manipulate the frame. A check is made here to see which frame we are on.

  • Frames 1-9 inclusive:
    • If a spare was used, then it marks the frame's appropriate throw - ...Gasp! Oh dear! Did I just see a label Throw2ndSpare....OMG!...read on...relax...
    • Since this is the second throw, it is impossible to have a strike here and hence calls InvalidThrow.
    • We check the previous throw and determine if the pins knocked on this throw plus the previous throw is equal to ten then we... GASP! ARGC! ARGV! Did I just see the unspeakable code there goto...??? well, there we are, we need to jump back up to the label Throw2ndSpare to replace the text box with a / to indicate a spare. Hence the usage of the unspeakably sloppy goto! :) Otherwise we just, merely set the number of pins for this throw.
  • Last Frame - We check if a spare or a strike was entered and set the frame accordingly, otherwise, we check the previous throw, and if that is not a strike, then this frame will have two throws so we disable the third input box.

I can promise you that the last part of this before we wrap up will be shorter...

C#
private bool HandleLastThrow(TextBox txtBox) {
    string sName = txtBox.Name;
    Match m = this._reTextInputName.Match(sName);
    if (m.Success) {
        int iFrameNo = int.Parse(m.Groups[REGEXP_FRAMENO].Value);
        int iThrowNo = int.Parse(m.Groups[REGEXP_THROWNO].Value);
        if (iThrowNo == 3) {
            if (txtBox.Text.Equals(STRIKE)) {
                this._frameHandler[iFrameNo].ThrowBall(true, false);
                return true;
            }
            if (txtBox.Text.Equals(SPARE)) {
                this._frameHandler[iFrameNo].ThrowBall(false, true);
                return true;
            }
            // Anything else here is number by default
            int iVal = int.Parse(txtBox.Text);
            this._frameHandler[iFrameNo].ThrowBall(iVal);
            return true;
        }
    }
    return false;
}
Figure 12.

HandleLastThrow is far more simpler and again, similar signature to the previous two highlighted above. And the checking is simpler, since if we're executing in this function, then it must accept either a number or a spare or a strike and return back true to the event handler txtBoxThrowThree_KeyUp.

That wasn't so bad, was it...Now I hope you have a better understanding of the game of Ten pin Bowling and how the scoring works. The real scoring machines that you see in a real bowling alley are more complex than this. As, it would have, sensors and electronical-what-nots all over the place to be able to calculate as the game-play advances along and can tell instantly the score. Now, you know why you cannot go backwards i.e. playing on frame 6, then go back to a frame previously and shove in something into the input, but realistically speaking, that should be impossible since game play has advanced...but then again, this code can be easily modified to cater for that as I'm sure that there are sophisticated ways of achieving that in a real bowling alley to "resolve" scoring "conflicts"...

Points of Interest

Heh! Taking cue from this template, I never understood the scoring in bowling. And also, when I realized the scoring wasn't coming out correctly - I spent a day trying to figure out where or what or how it happened, as I pointed out above earlier on when talking about the pseudo-code in Figure 5 above.

All in all, it was good fun - Simple as that! :) Now, where did I leave my bowling ball & shoes...

Since I am very keen on Mono, I thought I'd include the screenshot of the calculator for your pleasure!

Ten Pin Bowling Calculator under Mono - Click to enlarge image

Happy bowling and may you get the "Perfect game" next time! ;)

References

Sources used...

History

  • 7th August, 2009 - First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Ireland Ireland
B.Sc. in Information Systems.
Languages: C, Assembly 80x86, VB6, Databases, .NET, Linux, Win32 API.

Short Note:
Having worked with IT systems spanning over 14 years, he still can remember writing a TSR to trap the three-finger salute in the old days of DOS with Turbo C. Smile | :) Having worked or hacked with AS/400 system while in his college days, graduating to working with Cobol on IBM MVS/360, to RS/6000 AIX/C. He can remember obtaining OS/2 version 2 and installing it on an antique 80386. Boy it ran but crawled! Smile | :) Keen to dabble in new technologies. A self-taught programmer, he is keen to relinquish and acquire new bits and bytes, but craves for the dinosaur days when programmers were ultimately in control over the humble DOS and hacking it!! Smile | :)

Comments and Discussions

 
Questionthis bowling system can add database??? Pin
Member 1067557126-Apr-14 13:29
Member 1067557126-Apr-14 13:29 
Questiona little bug or feature Pin
gharutyu11-Oct-12 22:48
gharutyu11-Oct-12 22:48 
Generalnot bad... but.... Pin
Blubbo10-Aug-09 9:33
Blubbo10-Aug-09 9:33 
hey! where's the one for CandlePin bowling!??!!? it used 3 balls...

Yeah I know its not popular around the nation but its still used in New England & Canada.
GeneralComments [modified] Pin
Tomas Brennan8-Aug-09 13:24
Tomas Brennan8-Aug-09 13:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.