How to design a bowling scoreboard

I just saw this post: 

http://www.iteye.com/topic/1112383 

titled as OO 还是 procedural 小程序的设计.

It's kind of interesting. First, I love bowling (though just average player), secondly, it's a good design question for interviews.

Here is a picture from google for the score board.

For each round, there is a score section called frame. In each round we have a chance to play at most twice. If we get a strike, meaning we hit all 10 pins, then this round is over. Otherwise, we can roll twice.

Check wiki pages for more detail.

扫描二维码关注公众号,回复: 1256914 查看本文章

The first class we create is the frame:

public class BowlingFrame 
{
	private static final int STRIKE = 2;
	private static final int SPARE = 1;
	private static final int OPEN = 0;
	
	private int[] rolls = new int[3];
	private int bonus = OPEN;
	public int score = 0;
	
	public BowlingFrame(int firstRoll)
	{
		rolls[0] = firstRoll;
		score = firstRoll;
		if (firstRoll == 10)
			bonus = STRIKE;		
	}
	
	public BowlingFrame(int firstRoll, int secondRoll)
	{
		rolls[0] = firstRoll;
		rolls[1] = secondRoll;
		score = firstRoll + secondRoll;
		if (firstRoll == 10)
			bonus = STRIKE;
		if (firstRoll + secondRoll == 10)
			bonus = SPARE;		
	}
	
	// This is for last frame, so we leave bonus as OPEN
	public BowlingFrame(int firstRoll, int secondRoll, int thirdRoll)
	{
		rolls[0] = firstRoll;
		rolls[1] = secondRoll;
		rolls[2] = thirdRoll;
		score = firstRoll + secondRoll + thirdRoll;
	}
	
	public int get1stScore() 
	{
		return rolls[0];
	}
	
	public int get2ndScore()
	{
		return rolls[1];
	}
	
	public boolean isStrike()
	{
		return bonus == STRIKE;
	}
	
	public boolean isSpare()
	{
		return bonus == SPARE;
	}
}
 

The behaviors (methods) are defined by another class, the score board class. There are 3 constructors corresponding to cases like strike, spare/open, and last round. It should be self explained.

The actual board class is like this:

public class BowlingScoreBoard 
{
	private BowlingFrame[] frames = new BowlingFrame[10];
	private int index = 0;
	
	public void addFrame(BowlingFrame bowlingFrame)
	{
		if (index >= 10)
			throw new RuntimeException("can't go beyond 10 frames");
		
		frames[index] = bowlingFrame;
		
		// try to set bonus for two frames back in case of a strike there.
		int p2 = index - 2;
		if (p2 >= 0)
		{
			BowlingFrame bf = frames[p2];
			if (bf.isStrike())
			{
				BowlingFrame previous = frames[index - 1];
				if (previous.isStrike())
				{
					bf.score += previous.get1stScore() + bowlingFrame.get1stScore();
					// previous.get1stScore() is just 10 since it's a strike
				}
			}
		}
		
		// try to set bonus for previous frame in case of a spare there.
		int p1 = index - 1;
		if (p1 >= 0)
		{
			BowlingFrame bf = frames[p1];
			if (bf.isSpare())
			{
				bf.score += bowlingFrame.get1stScore();
			}
			else if (bf.isStrike() && !bowlingFrame.isStrike())
			{
				bf.score += bowlingFrame.get1stScore() +  bowlingFrame.get2ndScore();
			}
		}
		
		index++;
	}
	
	public int totalScore()
	{
		int ret = 0;
		for (int i=0; i<index; i++)
		{
			ret += frames[i].score;			
		}
		
		return ret;
	}
}
 

As a game goes, we keep adding new frames. As we add new frames, we need to figure out the scores of previous 1 or 2 frames, depend on the bonus status, strike/spare/open. So at the end of the game, scores in each frame should be calculated correctly (This is also helpful for debugging).

The testcase is like this:

import junit.framework.TestCase;

public class BowlingScoreBoardTest extends TestCase
{
	private static final BowlingFrame EMPTY_FRAME = new BowlingFrame(0);
	
	public void testStrike()
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		bsb.addFrame(new BowlingFrame(10)); // strike
		bsb.addFrame(new BowlingFrame(3, 6)); // open
		
		System.out.println(bsb.totalScore());
		assertTrue(bsb.totalScore() == 28); // 1st frame: 10 + (3 + 6), 2nd frame: 3 + 6
	}
	
	public void testDouble() // two strikes in a row
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		bsb.addFrame(new BowlingFrame(10)); // strike
		bsb.addFrame(new BowlingFrame(10)); // strike
		bsb.addFrame(new BowlingFrame(3, 6)); // open
		
		System.out.println(bsb.totalScore());
		assertTrue(bsb.totalScore() == 51); 
		// 1st: 10 + 10 + 3; 2nd: 10 + 3 + 6; 3rd: 3 + 6.
	}
	
	public void testTurkey() // 3 consecutive strikes
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		bsb.addFrame(new BowlingFrame(10)); // strike
		bsb.addFrame(new BowlingFrame(10)); // strike
		bsb.addFrame(new BowlingFrame(10)); // strike
		
		System.out.println(bsb.totalScore());
		// first frame should have score 30 = 10 + (2nd frame) 10 + (3rd frame) 10
		// second frame bonus is not set yet since 3rd frame is a strike and thus
		// it needs the forth frame, similar to the 3rd frame, just 10.
		assertTrue(bsb.totalScore() == 50);
	}
	
	public void testTailTurkey()
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		bsb.addFrame(EMPTY_FRAME); // we dont care the first few frames, so just empty
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(EMPTY_FRAME);
		bsb.addFrame(new BowlingFrame(10));
		bsb.addFrame(new BowlingFrame(10));
		bsb.addFrame(new BowlingFrame(10, 10, 10));
		
		System.out.println(bsb.totalScore());
		assertTrue(bsb.totalScore() == 90); // 30 for each in the last 3 frames.
	}
	
	public void testSpare()
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		bsb.addFrame(new BowlingFrame(4, 6));
		bsb.addFrame(new BowlingFrame(3, 7));
		
		System.out.println(bsb.totalScore());
		assertTrue(bsb.totalScore() == 23); 
		// 1st: 4+6 + 3(bonus from 2nd frame); 2nd: 3 + 7 (it's also a spare, but no 3rd frame yet).
	}
	
	public void testPerfectScore()
	{
		BowlingScoreBoard bsb = new BowlingScoreBoard();
		for (int i=0; i<9; i++)
			bsb.addFrame(new BowlingFrame(10));
		bsb.addFrame(new BowlingFrame(10, 10, 10));
		
		System.out.println(bsb.totalScore());
		assertTrue(bsb.totalScore() == 300); // 30 for each frame.
	}
}
 

These cases should cover most of the code.

Several notes:

1. The frame class just maintains the current states. The bonus logic should be somewhere else, in the score board class, where we handle the logic between frames.

2. The requirement is to design a score calculator, so we shouldn't get the game playing logic here because it's a separate concern.

3. I played a lot bowling so I know fairly well about the game. If you don't, you may start coding the logic is one big static method and then refactor out the frame portion. The logic is explained well in the wiki page: http://en.wikipedia.org/wiki/Ten-pin_bowling.

猜你喜欢

转载自jellyfish.iteye.com/blog/1131077