I was given a simple coding exercise as part of a job interview process, for which I received a negative review.
This is the question:
DiUS is starting a bowling club. To help with the club, we have engaged you to program a scoring system.
The features on the system are:
- One player only
- In each frame, the bowler has 2 tries to knock down all the pins
- If in 2 tries, the bowler fails to knock down all the pins, their score is the sum of the number of pins they've knocked down in the 2 attempts
- E.g, if a bowler rolls,
4,4
, then their score is 8.If in 2 tries, the bowler knocks down all the pins, it is a spare. The scoring of a spare is the sum of the number of pins knocked down plus the number of pins knocked down in the next bowl.
- E.g, if a bowler rolls,
4,6
|5,0
, then their score is 20 = (4 + 6 + 5) + (5 + 0).If in one try, the bowler knocks down all the pins, it is a strike. The scoring of a strike is the sum of the number of pins knocked down plus the number of pins knocked down in the next two bowls.
- E.g, if a bowler rolls,
10
|5, 4
, then their score is 28 = (10 + 5 + 4) + (5 + 4).- There are 10 pins in a frame
- There are 10 frames in a match
- Don't worry about validating the number of rolls in a frame
The interface should look like this (in Java);
bowlingGame.roll(noOfPins); bowlingGame.score();
Here is my solution:
MatchFactory.java
public final class MatchFactory {
private MatchFactory(){};
public static BowlingGame createMatch() {
return new BowlingGameScoreBoard();
}
}
BowlingGame.java
public interface BowlingGame {
/**
* Keeps track of pins knocked over
* @param noOfPins knocked over per frame
* @exception {@link au.com.dius.BowlingGameScoreBoard.BowlingException}
*/
public void roll(int noOfPins);
/**
*
* @return score of current frame only
*/
public int score();
}
BowlingGameScoreBoard.java
public final class BowlingGameScoreBoard implements BowlingGame {
private final List<Frame> frames;
private static final int MAX_FRAMES = 10;
private static final int MAX_PINS = 10;
private static final int MAX_ATTEMPTS_PER_FRAME = 2;
private int frameCounter = 0;
private int strikeCounter = 0;
private static final int ALL_STRIKE_SCORE = 300;
/**
* setup the game, ie all {@link BowlingGameScoreBoard#MAX_FRAMES} frames
*/
public BowlingGameScoreBoard() {
frames = new ArrayList<Frame>(MAX_FRAMES);
for (int i = 0; i < MAX_FRAMES; i++) {
frames.add(new Frame());
}
}
@Override
public void roll(int noOfPins) {
if (noOfPins > MAX_PINS) {
throw new BowlingException("illegal argument " + noOfPins);
}
Frame frame = getFrame();
if (frame == null) {
throw new BowlingException("all attempts exhausted - start new game");
}
frame.setScore(noOfPins);
if (isBonusFrame()) {
Frame prev = getPreviousFrame();
// restrict to one attempt, when last frame was spare
if (prev.isSpare()) {
frame.limitToOneAttempt();
}
}
}
/**
* returns a frame and moves to next frame if current has used up attempts
* @return {@link au.com.dius.BowlingGameScoreBoard.Frame}
*/
private Frame getFrame(){
Frame frame = getCurrentFrame();
if (frame.isDone()) {
// new bonus frame
if(isLastFrame() && (frame.isSpare() || frame.isStrike())) {
Frame bonus = new Frame();
frames.add(bonus);
frameCounter++;
return bonus;
}
frameCounter++;
if (frameCounter == MAX_FRAMES || isBonusFrame()) {
return null;
}
frame = getCurrentFrame();
}
return frame;
}
@Override
public int score() {
int score;
// first frame
if (frameCounter == 0) {
Frame curr = getCurrentFrame();
return curr.score();
} else {
// score 300, strikes for all frames
if (isLastFrame() && isAllStrikes()) {
return ALL_STRIKE_SCORE;
}
Frame curr = getCurrentFrame();
Frame prev = getPreviousFrame();
// only add previous last frame to current score
if (isBonusFrame()) {
return prev.score() + curr.score();
}
score = curr.score();
if(prev.isSpare()) {
score += (prev.score() + curr.getFirstScore());
}
if(prev.isStrike()) {
score += (prev.score() + curr.getFirstScore() + curr.getSecondScore());
}
}
return score;
}
private Frame getPreviousFrame() {
return frames.get(frameCounter-1);
}
private Frame getCurrentFrame() {
return frames.get(frameCounter);
}
private boolean isAllStrikes() {
return strikeCounter == MAX_FRAMES ;
}
private boolean isBonusFrame() {
return frames.size() > MAX_FRAMES;
}
private boolean isLastFrame() {
return frameCounter == MAX_FRAMES - 1;
}
/**
* This nested class encapsulates the concept of a frame
* and manages score and attempts allowed for each frame
*/
private class Frame {
private int[] scores = new int[MAX_ATTEMPTS_PER_FRAME];
private int noOfPins = 10;
private int noAttempts = 0;
private boolean isStrike = false;
private boolean isSpare() {
return noOfPins == 0 && noAttempts == MAX_ATTEMPTS_PER_FRAME && !isStrike;
}
private boolean isStrike() {
return noOfPins == 0 && noAttempts == MAX_ATTEMPTS_PER_FRAME && isStrike;
}
private boolean isDone () {
return noAttempts == MAX_ATTEMPTS_PER_FRAME;
}
private void setScore(int score) {
scores[noAttempts++] = score;
noOfPins -= score; // keep track of remaining pins/frame
if (score == MAX_PINS) {
isStrike = true;
strikeCounter++;
}
}
private void limitToOneAttempt(){
scores[1] = 0;
noAttempts++;
}
private int score() { return scores[0] + scores[1];}
private int getFirstScore() {
return scores[0];
}
private int getSecondScore() {
return scores[1];
}
}
/**
* Represents an exception for the bowling game
*/
public class BowlingException extends RuntimeException {
BowlingException(String message) {
super(message);
}
}
}
BowlingGameTest.java
@RunWith(JUnit4.class)
public class BowlingGameTest {
private BowlingGame game;
private static final int MAX_ATTEMPTS = 20;
@Before
public void setUp() {
game = MatchFactory.createMatch();
}
@Test
public void testScoreNoSpareOrStrike() {
game.roll(4);
game.roll(4);
int score = game.score();
Assert.assertEquals(8, score);
}
@Test
public void testSpare() {
game.roll(4);
game.roll(6);
int score = game.score();
Assert.assertEquals(10, score);
game.roll(5);
game.roll(0);
score = game.score();
Assert.assertEquals(20, score);
}
@Test
public void testStrikeOnSecondAttempt() {
game.roll(0);
game.roll(10);
int score = game.score();
Assert.assertEquals(10, score);
game.roll(5);
game.roll(4);
score = game.score();
Assert.assertEquals(28, score);
}
@Test
public void testStrikeOnFirstAttempt() {
game.roll(10);
game.roll(0);
int score = game.score();
Assert.assertEquals(10, score);
game.roll(5);
game.roll(5);
score = game.score();
Assert.assertEquals(30, score);
}
@Test
public void testStrikeEveryRoll() {
for (int i = 0; i < 10 ; i++) {
game.roll(10);
game.roll(0);
}
int score = game.score();
Assert.assertEquals(300, score);
}
@Test
public void testLastFrameSpare() {
for (int i = 0; i < 10 ; i++) {
game.roll(5);
game.roll(5);
}
game.roll(5);
int score = game.score();
Assert.assertEquals(15, score);
}
@Test
public void testLastFrameStrike() {
for (int i = 0; i < 10 ; i++) {
game.roll(10);
game.roll(0);
}
game.roll(3);
game.roll(4);
int score = game.score();
Assert.assertEquals(17, score);
}
@Test(expected = BowlingGameScoreBoard.BowlingException.class)
public void testLastFrameNoStrike() {
for (int i = 0; i < 10 ; i++) {
game.roll(3);
game.roll(5);
}
// this wont happen as last frame wasnt strike/spare
game.roll(3);
game.roll(4);
}
/**
* Exception is generated if try and go beyond 10 frames / match
*/
@Test(expected = BowlingGameScoreBoard.BowlingException.class)
public void testPlayMoreThanAllFrames() {
for (int i = 0; i <= MAX_ATTEMPTS ; i++) {
game.roll(i/10);
}
}
/**
* This tests an illegal argument , ie cant pass more than 10 pins to
* knock down
*
* I'am using custom exception here instead of Java's {@link java.lang.IllegalArgumentException}
*/
@Test(expected = BowlingGameScoreBoard.BowlingException.class)
public void testIllegalBowlException() {
game.roll(200);
}
}
The feedback I got was:
Pros
- Has test
- Broken up the solution into 2 classes (Bowling Game/Frame)
Cons
- Doesn't roll again if it is a strike
MatchFactory
seems unnecessary- Scoring method is a bit complicated
- Doesn't handle multiple strike scenario properly
- Not sure why the
Frame
is a private class
I'd like a review and agreements/disagreements with the feedback given.