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!
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
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
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;
} else {
if (index >= BTHelpers.FIRST_THROW_INDEX &&
index <= BTHelpers.LAST_THROW_INDEX) return this._throwList[index];
else return null;
}
}
}
...
...
}
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(...)
public void ThrowBall(int iPins) {
if (!this.IsLastFrame) {
if (this._throwList.Count == 0) {
BallThrow throwBall = new BallThrow(iPins);
if (iPins == BTHelpers.MAX_PINS)
throwBall.MarkBall(true, false);
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 {
BallThrow prevThrow = this._throwList[BTHelpers.FIRST_THROW_INDEX];
if (prevThrow.PinsKnocked + iPins == BTHelpers.MAX_PINS) {
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:
BallThrow throwFirstBall = new BallThrow(iPins);
if (iPins == BTHelpers.MAX_PINS) throwFirstBall.MarkBall(true, false);
this._throwList.Add(throwFirstBall);
break;
case 1:
prevThrow = this._throwList[BTHelpers.FIRST_THROW_INDEX];
if (prevThrow.StrikeBall) {
this._throwList.Add(throwLast);
return;
} else {
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;
this.OnEndOfFrame(new FrameScoreEventArgs
(TotalPinsKnocked, this._iFrameNo));
this.OnAllFramesDone();
return;
} else {
if (prevThrow.PinsKnocked + iPins == BTHelpers.MAX_PINS) {
throwLast.MarkBall(false, true);
this._throwList.Add(throwLast);
int TotalPinsKnocked = BTHelpers.MAX_PINS;
this._iFrameScore = TotalPinsKnocked;
}
}
}
break;
case 2:
default:
prevThrow = this._throwList
[BTHelpers.SECOND_THROW_INDEX];
if (prevThrow.StrikeBall) {
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;
this.OnEndOfFrame(new FrameScoreEventArgs(TotalPinsKnocked,
this._iFrameNo));
this.OnAllFramesDone();
return;
}
if (prevThrow.SpareBall) {
if (prevThrow.PinsKnocked +
iPins < BTHelpers.MAX_PINS) {
this._throwList.Add(throwLast);
int TotalPinsKnocked = BTHelpers.MAX_PINS +
this._throwList[BTHelpers.LAST_THROW_INDEX].PinsKnocked;
this._iFrameScore = TotalPinsKnocked;
this._iTotalPinsKnocked = TotalPinsKnocked;
this.OnEndOfFrame(new FrameScoreEventArgs
(TotalPinsKnocked, this._iFrameNo));
} else {
if (prevThrow.PinsKnocked + iPins ==
BTHelpers.MAX_PINS) {
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;
this.OnEndOfFrame(new FrameScoreEventArgs
(TotalPinsKnocked, this._iFrameNo));
} else {
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;
this.OnEndOfFrame(new FrameScoreEventArgs
(TotalPinsKnocked, this._iFrameNo));
}
}
this.OnAllFramesDone();
} else {
this._throwList.Add(throwLast);
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;
this.OnEndOfFrame(new FrameScoreEventArgs
(TotalPinsKnocked, this._iFrameNo));
this.OnAllFramesDone();
}
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:
FrameHandler
Now we're almost done, this class is next up for discussion.
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));
}
}
....
....
private int CalcScore(int iFrameNo) {
int iScore = 0;
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;
}
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...
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
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
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:
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...
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.
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;
}
}
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)) {
this.InvalidThrow(txtBox);
return false;
}
} else {
if (txtBox.Text.Equals(STRIKE)) {
this._frameHandler[iFrameNo].ThrowBall(true, false);
return true;
}
}
int iPins = int.Parse(txtBox.Text);
this._frameHandler[iFrameNo].ThrowBall(iPins);
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.
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) {
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) {
txtBox.Text = SPARE;
goto Throw2ndSpare;
}
if (iFirstThrow + iSecondThrow < BowlTracker.BTHelpers.MAX_PINS) {
this._frameHandler[iFrameNo].ThrowBall(iSecondThrow);
return true;
}
this.InvalidThrow(txtBox);
return false;
} else {
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)) {
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...
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;
}
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!
Happy bowling and may you get the "Perfect game" next time! ;)
References
Sources used...
History
- 7th August, 2009 - First release