Skip to main content
Logo image

Java, Java, Java: Object-Oriented Problem Solving, 2024E

Section 3.7 Testing an Improved OneRowNim

Subsection 3.7.1 Using If/Else Structures

Let’s use the control structures that we have discussed to improve the definition of the takeSticks() method of OneRowNim. We noted earlier that our current definition allows \(4\) or more sticks to be removed from nSticks even though the rules of One Row Nim indicate that a player must take one, two, or three sticks on a turn. We can use if-else statements to make certain that no more than \(3\) sticks get removed.
What should happen if the method takeSticks() is called with an argument that does not represent a legal number of sticks to remove? In this case, it would probably make sense to remove no sticks at all and to keep the value of player the same so that the player whose turn it is does not change. In addition, it would be nice if the method could signal that an illegal move has been attempted. This can be accomplished if we redefine takeSticks() to return a boolean value. Let’s have a return value of true represent the case that a valid number of sticks have been removed and the player to play next has been changed. A return of false will indicate that an illegal move has been attempted. Making these changes to the takeSticks() method will yield a method definition that looks like:
public boolean takeSticks(int num)
{   if (num < 1) {
       return false; // Error
    } else if ( num > 3) {
       return false; // Error
    } else {
       nSticks = nSticks - num;
       player = 3 - player;
       return true;
    } //else
 } //takeSticks
Notice that the new definition of the takeSticks() method has a boolean return type. Also notice that the if/else multiway structure is used to handle the three cases of the parameter num being less than one, more than three, or a valid number. Try the code below to see it running.

Activity 3.7.1.

Run the code below. Try taking 1, 2, and -1 sticks by changing the code in main.
Let us add one more method to the OneRowNim class. Let’s define a method called getWinner() that will return the number of the winning player if the game is over. Recall that the player who takes the last stick loses, so after that last play, the player whose turn it is to play next is the winner. However, we should be concerned about what value to return if the game is not over when the method is called. A common strategy is to have a method return a special value to indicate that it is in a state in which it cannot return the value requested. Returning a \(0\) value is a good way to indicate that the game is not over so a winner cannot be identified. With this information, the if/else statement can be used in the definition of getWinner().
public int getWinner()
{   if (nSticks < 1)
        return player;
    else
        return 0;
} // getWinner()
We now have the final version (for this chapter) of the OneRowNim class whose implementation is given in Figure 3.7.1. We have turned a very simple class into one that contains quite a few elements. Compared to our first version (in Chapter 1), this Chapter’s version of OneRowNim presents an interface (to other objects) that is easy and convenient to use. The constructor methods with parameters provide an easy way to create a OneRowNim instance with any number of sticks. The use of private instance variables and a single, carefully designed mutator method, takeSticks(), prevents other objects from tampering with the state of a OneRowNim object’s state. The other methods provide a flexible way to find out the state of a OneRowNim object. The complete implementation of this OneRowNim is shown in Figure 3.7.1.
public class OneRowNim
{   private int nSticks = 7;
    private int player = 1;
    public OneRowNim()
    {
    } // OneRowNim() constructor
    public OneRowNim(int sticks)
    {   nSticks = sticks;
    }  // OneRowNim() constructor2
    public OneRowNim(int sticks, int starter)
    {   nSticks = sticks;
        player = starter;
    }  // OneRowNim() constructor3
    public boolean takeSticks(int num)
    {   if (num < 1) return false;       // Error
        else if ( num > 3) return false; // Error
        else              // this is a valid move
        {   nSticks = nSticks - num;
            player = 3 - player;
            return true;
        } // else
    } // takeSticks()
    public int getSticks()
    {   return nSticks;
    } // getSticks()
    public int getPlayer()
    {   return player;
    } // getPlayer()
    public boolean gameOver()
    {   return (nSticks <= 0);
    } // gameOver()
    public int getWinner()
    {   if (nSticks < 1) return getPlayer();
        else return 0;  //game is not over
    } // getWinner()
    public void report()
    {   System.out.println("Number of sticks left: " +
                                             getSticks());
        System.out.println("Next turn by player " +
                                             getPlayer());
    }   // report()
  } // OneRowNim class
Listing 3.7.1. The OneRowNim class with improved methods.

Subsection 3.7.2 Using a While Loop

Let’s use a while statement to test the new methods of the class. A pseudocode description of how a game is played might look like:
Here is a version with a main method that chooses a random number of sticks each time through the loop.

Activity 3.7.2.

Run the code below a couple times. Use the Show CodeLens button to step through it to understand how the loop in main works. What would happen if you changed the random number to be * 4 instead of * 3? Try it and see.

Activity 3.7.3.

This version (Listing 3.7.3) uses the Scanner class introduced in the previous chapter to allow the game to be played with keyboard input for each player.
import java.util.Scanner;
public class TestOneRowNim {
  public static void main(String argv[]) { 
    Scanner sc = new Scanner(System.in);
    OneRowNim game = new OneRowNim(11);
    while(game.gameOver() == false)  {   
      game.report();  // Prompt the user
      System.out.print("Input 1, 2, or 3: ");
      int sticks = sc.nextInt(); // Get move
      game.takeSticks(sticks);   // Do move
      System.out.println();
    } // while
    game.report();  // The game is now over
    System.out.print("Game won by player ");
    System.out.println(game.getWinner());
 } // main()
} // TestOneRowNim
Listing 3.7.3. The TestOneRowNim class with keyboard input.
Run the code multiple times and enter 1, 2, or 3. Can you figure out a winning strategy?
Note that the return value of the takeSticks() method is ignored in this test program. We will make use of the return value in test programs in the next chapter when better user interfaces are developed for OneRowNim. Note, however, that taken together, the public methods for OneRowNim provide other objects with an interface that they can use to communicate with individual OneRowNim objects.
To reiterate a point made at the outset, object-oriented programming is a process of constructing objects that will interact with each other. Object-oriented programs must ensure that the objects themselves are well designed in terms of their ability to carry out their designated functions. Good design in this sense requires careful selection of instance variables and careful design of methods to ensure that the object can carry out its assigned tasks. However, equal care must be taken to ensure that the interactions that take place among objects are constrained in ways that make sense for that particular program. This aspect of designing objects comes into play in designing the methods—constructor, accessor, and mutator—that make up the object’s interface.
You have attempted of activities on this page.