Skip to main content
Logo image

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

Section 2.5 CASE STUDY: Simulating a Two-Person Game

In this section, we will design a class definition that keeps track of the details of a well known, two-person game. Our objective is to understand what the program is doing and how it works.
The game we will consider is played by two persons with a row of sticks or coins or other objects. The players alternate turns. On each turn, the player must remove one, two, or three sticks from the row. Whoever removes the last stick loses. The game can be played with any number of sticks but starting with twenty one sticks is quite common.
This game is sometimes referred to as "Nim", but there is a similar game involving multiple rows of sticks that is more frequently given that name. Thus we will refer to our game as "One Row Nim".

Subsection 2.5.1 Designing a OneRowNim class

Project 2.5.1.

Specification: Design a class named OneRowNim that simulates the game of One Row Nim with a row of sticks. A OneRowNim object should keep track of whose turn it is and allow players to “take away” one, two, or three sticks. It should be able to decide when the game is over and which player has won.

Subsubsection 2.5.1.1 Problem Decomposition

Let’s design OneRowNim so that it can be used in with different kinds of user interfaces:
  • Person vs. person.
  • Person vs. computer.
  • Keyboard and console interface.
  • Graphical interface.
In this chapter, we will work on the OneRowNim class. We will design the user interfaces in a subsequent chapter.

Subsubsection 2.5.1.2 Class Design: OneRowNim

As we saw in the Riddle example, class definitions are broken down into two parts:
  1. The information or attributes that the object needs (variables).
  2. The behavior or actions the object can take (methods).
The OneRowNim object should manage two pieces of information that vary as the game is played. One is the number of sticks remaining and the other is which player has the next turn. We can represent both of these as whole numbers, which correspond to int in Java:
Variable Name Type Values
nSticks int 0 through 21
player int 1 or 2
As this table suggests, we will represent our two players as the numbers 1 or 2.
During the playing of the game, the values of these two variables will represent the state of the game: number of sticks remaining, and whose turn it is.

Subsubsection 2.5.1.3 Method Decomposition

One action needed for our game is the taking away of 1-3 sticks. The easiest way to do this is to have three methods corresponding to taking one, two, or three sticks: takeOne(), takeTwo(), and takeThree(). Each method will be responsible for reducing the value of nSticks as well as switching to the other player.
(Once we learn how to use parameters and return values (next chapter), we’ll be able to replace these three methods with a single method that can take away 1, 2 or 3 sticks.)
We also need a method that gives the information that a user needs when considering a move. The report() method will report the number of sticks remaining and whose turn it is.
Figure 2.5.1 is a UML class diagram that summarizes our design decisions. Note that the instance variables are specified as private (\(-\)) to hide them from other objects, and the methods are specified as public (\(+\)) and will thereby form the OneRowNim interface. These will be the methods that other objects will use to interact with it.
Figure 2.5.1. The OneRowNim class.

Subsection 2.5.2 Defining the OneRowNim Class

Given our design of the OneRowNim class as described in Figure 2.5.1, the next step in building our simulation is to begin writing the Java class definition.

Subsubsection 2.5.2.1 The Class Header

The purpoe of the class header is give the class a name and specify its relationship to other classes. It takes the following form:
ClassModifiers\(_{opt}\) class ClassName Pedigree\(_{opt}\)
Because our OneRowNim object will interact with other objects, we will designate its access as public. And because OneRowNim is not a subclass of any other type of object, we will omit the extends clause, thereby making it a direct subclass of Object by default ((Figure 2.5.2).
Figure 2.5.2. By default, OneRowNim is a subclass of Object.
This gives us the following class header for OneRowNim:
public class OneRowNim  // Class header
{                       // Beginning of class body
}                       // End of class body

Subsubsection 2.5.2.2 Instance Variables

The body of a class definition consists of two parts: variables and method definitions. An instance variable is a variable that can be used throughout the class, which makes it a class-level variable.
Although Java does not impose any particular order on variable and method declarations, in this book a class definition will take the form shown in Figure 2.5.3. First we’ll define the class’s variables followed by its method definitions.
public class ClassName
{  // Instance variables
     VariableDeclaration1
     VariableDeclaration2
     ...
    // Instance methods
     MethodDefinition1
     MethodDefinition2
     ...
} // End of class
Figure 2.5.3. A template for constructing a Java class definition.
Instance variables are distinguished from local variables, which are variables defined within a method. Examples would be the variables q and a that were defined in the Riddle(String q, String a) constructor (Figure 2.4.1). As we will see better in the next chapter, Java handles each type of variable differently.
A declaration for an instance variable must follow the rules for naming variables that were described in Section 1.5.9. Also, instance variables are usually declared private, to protect them from other objects.
Java provides three different access modifiers, which observe the following rules:
  • A private element cannot be accessed outside the class in which it is declared.
  • A public element is accessible to any other object.
  • A protected element is accessible only within subclasses of its class and by other classes that belong to the same package.
Whenever a class, instance variable, or method is defined, you can explicitly declare it public, protected, or private. Or you can leave its access unspecified, in which case Java’s default accessibility rules will apply.
Subsubsection Default Accessibility
Java determines default accessibility in a top-down manner. Instance variables and methods are contained in classes, which are contained in packages. To determine whether a instance variable or method is accessible, Java starts by determining whether its containing package is accessible, and then whether its containing class is accessible. Access to classes, instance variables, and methods is defined according to the rules shown in the table below.
Table 2.5.4. Java’s accessibility rules.
Element Modifier Rule
Class public Accessible if its package is accessible.
by default Accessible only within its package.
Instance variable public Accessible to all other objects.
or method protected Accessible to its subclasses and to other classes in its package
private Accessible only within the class.
by default Accessible only within the package.
Given our design for the OneRowNim class as described in Figure 2.5.1, we get the following declarations for our instance variables:
public class OneRowNim
{
  private int nSticks = 7;
  private int player = 1;

  //Method definitions go here
} // OneRowNim
Our initializations of the variables indicate that the game will start with 7 sticka and player 1 will go first.

Subsubsection 2.5.2.3 OneRowNim’s Methods

Designing and defining methods is a form of abstraction. By defining a certain sequence of actions as a method, you encapsulate those actions under a single name that can be invoked whenever needed. Instead of having to list the entire sequence again each time you want it performed, you simply call it by name. As you recall from Chapter 1, a method definition consists of two parts, the method header and the method body. The method header declares the name of the method and other general information about the method. The method body contains the executable statements that the method performs.
public void methodName()  // Method header
{                  // Beginning of method body
}                  // End of method body

Subsubsection 2.5.2.4 The Method Header

The method header follows a general format that consists of one or more MethodModifiers, the method’s ResultType, the MethodName, and the method’s FormalParameterList, which is enclosed in parentheses. The following table illustrates the method header form, and includes several examples of method headers that we have already encountered. The method body follows the method header.
Table 2.5.5.
MethodModifiers\(_{opt}\) ResultType MethodName FormalParameterList
publicstatic void main (String argv[])
public void paint (Graphics g)
public Riddle (String q, String a)
public String getQuestion ()
public String getAnswer ()
The rules on method access are the same as the rules on instance variable access: private methods are accessible only within the class itself, protected methods are accessible only to subclasses of the class in which the method is defined and to other classes in the same package, and public methods are accessible to all other classes.
public class OneRowNim
{ 
  private int nSticks = 7; // Start with 7 sticks.
  private int player = 1;  // Player 1 plays first.

  public void takeOne(){ } // Method bodies still need
  public void takeTwo(){ } //  to be defined.
  public void takeThree(){ }
  public void report(){ }
} //OneRowNim class
Listing 2.5.7. The Instance variables and method headers for the OneRowNim class.
Because all four of the OneRowNim instance methods are meant to communicate with other objects, they should all be declared public. All four methods will receive no data when being called and will not return any values. Thus they should all have void as a return type and should all have empty parameter lists. Listing 2.5.7> shows the code with these method headers added.

Subsubsection 2.5.2.5 The Method Body

The body of a method definition is a block of Java statements enclosed by braces, which are executed in sequence when the method is called. The takeOne() method, which represents the act of taking away one stick, should reduce the value stored in nSticks by one and change the value in player from 2 to 1 or from 1 to 2. The first of these changes is accomplished by the following assignment:
nSticks = nSticks - 1;
This statement says subtract 1 from the value stored in nSticks and assign the new value back to nSticks.
Deciding how to change the value in player is more difficult because we do not know whether its current value is 1 or 2. If its current value is 1, its new value should be 2; if its current value is 2, its new value should be 1. Notice, however, that in both cases the current value plus the desired new value are equal to 3. Therefore, the new value of player will always be equal to 3 minus its current value. Writing this as an assignment we have:
player = 3 - player;
The following table can be used to verify that this assignment will switch players from 1 to 2 and vice versa:
Current player 3 - player New player
-------------- ---------- ----------
1 3-1=2 2
2 3-2=1 1
The complete definition of takeOne() method becomes:
public void takeOne()
{
   nSticks = nSticks - 1;  // Take one stick
   player = 3 - player;    // Change to other player
}
The takeTwo() and takeThree() methods are completely analogous to the takeOne() method with the only difference being the amount subtracted from nSticks.
The report() method should print the current values of the instance variables using System.out.println(), with some additional text to clarufy the meaning of the values. Thus the body of report() could contain:
System.out.println("Number of sticks left: " + nSticks);
System.out.println("Next turn by player " + player);
This completes the method bodies of the OneRowNim class. The completed class definition is shown in Listing 2.5.8.
public class OneRowNim
{ private int nSticks = 7; // Start with 7 sticks.
  private int player = 1;  //Player 1 plays first.

  public void takeOne()
  { nSticks = nSticks - 1;
    player = 3 - player;
  } // takeOne()

  public void takeTwo()
  { nSticks = nSticks - 2;
    player = 3 - player;
  } // takeTwo()

  public void takeThree()
  { nSticks = nSticks - 3;
    player = 3 - player;
  }  // takeThree()

  public void report()
  { System.out.println("Number of sticks left: " + nSticks);
    System.out.println("Next turn by player " + player);
  }   // report()
} // OneRowNim class
Listing 2.5.8. The OneRowNim class definition.
We will discuss alternative methods for this class in the next chapter. In Chapter 4, we will develop several One Row Nim user interface classes that will facilitate a user indicating certain moves to make.

Subsection 2.5.3 Testing the OneRowNim Class

We can test OneRowNim by defining a main() method. Following the design we used in the riddle example, we will locate the main() method in a separate, user-interface class, named OneRowNimTester.
The body of main() should declare a variable of type OneRowNim and create a OneRowNimobject. Let’s name the variable game. To test the OneRowNim class, we should code a series of moves. For example, three moves taking 3, 3, and 1 sticks respectively would be one way to remove 7 sticks and end the game. Also, executing the report() method before the first move and after each move should display the current state of the game in the console window so that we can determine whether it is working correctly.
The following pseudocode outlines an appropriate sequence of statements in a main() method:
Declare a variable of type OneRowNim named game.
Instantiate a OneRowNim object to which game refers.
Tell game to report.
Tell game to remove three sticks.
Tell game to report.
Tell game to remove three sticks.
Tell game to report.
Tell game to remove one stick.
Tell game to report.
It is now an easy task to convert the steps in the pseudocode outline into Java statements. The resulting main() method is shown with the complete definition of the OneRowNimTester class:

Activity 2.5.2.

Run the following program to see the game in action. Change the code in main to call takeOne() instead of takeThree().
When it is run, OneRowNimTester produces the following output:
Number of sticks left: 7
Next turn by player 1
Number of sticks left: 4
Next turn by player 2
Number of sticks left: 1
Next turn by player 1
Number of sticks left: 0
Next turn by player 2
This output indicates that player 1 removed the final stick and so player 2 is the winner of this game.

Exercises Self-Study Exercises

1. Add Hint to Riddle Class.
  1. Add a new declaration to the Riddle class for a private String instance variable named hint. Assign the variable an initial value of "This riddle is too easy for a hint".
  2. Write a header for a new method definition for Riddle named getHint(). Assume that this method requires no parameters and that it simply returns the String value stored in the hint instance variable. Should this method be declared public or private?
  3. Write a header for the definition of a new public method for Riddle named setHint() which sets the value of the hint instance variable to whatever String value it receives as a parameter. What should the result type be for this method?
2. Student Class.
Create a definition of a Student class.
  1. Create instance variables for the first name, last name, and an integer student identification number.
  2. Write the headers for three methods. One method uses three parameters to set values for the three instance variables.
  3. One method returns the student identification number.
  4. The last method returns a String containing the student’s first name and last name.

Subsection 2.5.4 Flow of Control: Method Call and Return

A program’s flow of control is the order in which its statements are executed. In an object-oriented program, control passes from one object to another during the program’s execution. It’s important to have a clear understanding of this process.
In order to understand a Java program, it is necessary to understand the method call and return mechanism. We will encounter it repeatedly. A method call causes a program to transfer control to a statement located in another method. Figure 2.5.9 shows the method call and return structure.
Figure 2.5.9. The method call and return control structure. It’s important to realize that method1() and method2() may be contained in different classes.
In this example, we have two methods. We make no assumptions about where these methods are in relation to each other. They could be defined in the same class or in different classes. The method1() method executes sequentially until it calls method2(). This transfers control to the first statement in method2(). Execution continues sequentially through the statements in method2() until the return statement is executed.
Recall that if a void method does not contain a return statement, then control will automatically return to the calling statement after the invoked method executes its last statement.

Subsection 2.5.5 Tracing the OneRowNim Program

To help us understand the flow of control in OneRowNim, we will perform a trace of its execution. Figure 2.5.11 shows all of the Java code involved in the program. In order to simplify our trace, we have moved the main() method from OneRowNimTester to the OneRowNim class. This does not affect the program’s order of execution in any way. The listing in Listing 2.5.11 adds line numbers to the program to show the order in which its statements are executed.
public class OneRowNim
2 {  private int nSticks = 7; // Start with 7 sticks.
3    private int player = 1;  //Player 1 plays first.
      public void takeOne()
20    {  nSticks = nSticks - 1;
21       player = 3 - player;
      } // takeOne()
      public void takeTwo()
      {  nSticks = nSticks - 2;
         player = 3 - player;
      } // takeTwo()
      public void takeThree()
8,14  {  nSticks = nSticks - 3;
9,15     player = 3 - player;
      }  // takeThree()
      public void report()
5,11,17,23 { System.out.println("Number of sticks left: " + nSticks);
6,12,18,24   System.out.println("Next turn by player " + player);
      }   // report()
      public static void main(String args[])
1     {  OneRowNim game = new OneRowNim();
4        game.report();
7        game.takeThree();
10       game.report();
13       game.takeThree();
16       game.report();
19       game.takeOne();
22       game.report();
23     } //main()
   } //OneRowNim class
Listing 2.5.11. A trace of the OneRowNim program.
Execution of the OneRowNim program begins with the first statement in the main() method, labeled with line number 1. This statement declares a variable of type OneRowNim named game and calls a constructor OneRowNim() to create and initialize it. The constructor causes control to shift to the declaration of the instance variables nSticks and player in statements 2 and 3, and assigns them initial values of 7 and 1 respectively. Control then shifts back to the second statement in main(), which has the label 4.
Figure 2.5.12. The initial state of game.
At this point, game refers to an instance of the OneRowNim class with an initial state shown in Figure 2.5.12. Executing statement 4 causes control to shift to the report() method where statements 5 and 6 use System.out.println() to write the following statements to the console.
Number of sticks left: 7
Next turn by player 1
Control shifts back to statement 7 in the main() method, which calls the takeThree() method, sending control to the first statement of that method.
Executing statement 8 causes \(3\) to be subtracted from nSticks, leaving the value of \(4\text{.}\) Executing statement 9 assigns the value \(2\) to player, leading to the state shown in Figure 2.5.13.
Tracing the remainder of the program follows in a similar manner. Notice that the main() method calls game.report() four different times so that the two statements in the report() method are both executed on four different occasions. Note also that there is no call of game.takeTwo() in main(). As a result, the two statements in that method are never executed.
Figure 2.5.13. The initial state of game after line 9 is executed.

Subsection 2.5.6 Object-Oriented Design: Basic Principles

We complete our discussion of the design and this first implementation of the OneRowNim class with a brief review of some of the object-oriented design principles that were employed in this example.
  • Encapsulation. The OneRowNim class was designed to simulate playing the One Row Nim game. As such it encapsulate a certain state and a certain set of actions. In addition, its methods were designed to encapsulate the actions that make up their particular tasks.
  • Information Hiding. OneRowNim’s instance variables, nSticks and player are declared private and can only be changed through the class’s public methods.
  • Clearly Designed Interface. OneRowNim’s interface is defined in terms of its public methods, which constrain the way users can interact with OneRowNim objects. This ensures that OneRowNim instances remain in a valid state.
The OneRowNim class has some obvious shortcomings that are a result of our decision to limit methods to those without parameters or return values. These shortcomings include:
  • A OneRowNim has no way communicate how many sticks remain or whose turn it is, other than by writing a report to the console.
  • The takeOne(), takeTwo() and takeThree() methods all have similar definitions. It would be a better design a single method that could take away a specified number of sticks.
  • There is no way to start OneRowNim with a different number of sticks. It would be nice to have a way of playing that starts with any number of sticks.
  • In order to for a user to play a OneRowNim game, a user interface class would need to be developed that would allow the user to receive information about the state of the game and to input moves to make.
As we study other features of Java in the next two chapters, we will modify the OneRowNim class to address these shortcomings.
You have attempted of activities on this page.