Ralph Morelli, Ralph Walde, Beryl Hoffman, David G. Cooper
Section2.5CASE 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".
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:
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.
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.
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.
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).
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.
publicclassClassName{// Instance variablesVariableDeclaration1VariableDeclaration2...// Instance methodsMethodDefinition1MethodDefinition2...}// End of class
Figure2.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.
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.
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.
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.
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.
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.
Principle2.5.6.EFFECTIVE DESIGN: Public vs. Private Methods.
If a method is used to communicate with an object, or if it passes information to or from an object, it should be declared public. If a method is intended to be used solely for internal operations within the object, it should be declared private. Private methods are sometimes called utility methods or helper methods.
publicclassOneRowNim{privateint nSticks =7;// Start with 7 sticks.privateint player =1;// Player 1 plays first.publicvoidtakeOne(){}// Method bodies still needpublicvoidtakeTwo(){}// to be defined.publicvoidtakeThree(){}publicvoidreport(){}}//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.
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:
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:
publicvoidtakeOne(){
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:
publicclassOneRowNim{privateint nSticks =7;// Start with 7 sticks.privateint player =1;//Player 1 plays first.publicvoidtakeOne(){ nSticks = nSticks -1;
player =3- player;}// takeOne()publicvoidtakeTwo(){ nSticks = nSticks -2;
player =3- player;}// takeTwo()publicvoidtakeThree(){ nSticks = nSticks -3;
player =3- player;}// takeThree()publicvoidreport(){System.out.println("Number of sticks left: "+ nSticks);System.out.println("Next turn by player "+ player);}// report()}// OneRowNim class
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.
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.
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:
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.
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".
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?
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?
Subsection2.5.4Flow 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.
Figure2.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.
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.
publicclassOneRowNim2{privateint nSticks =7;// Start with 7 sticks.3privateint player =1;//Player 1 plays first.publicvoidtakeOne()20{ nSticks = nSticks -1;21 player =3- player;}// takeOne()publicvoidtakeTwo(){ nSticks = nSticks -2;
player =3- player;}// takeTwo()publicvoidtakeThree()8,14{ nSticks = nSticks -3;9,15 player =3- player;}// takeThree()publicvoidreport()5,11,17,23{System.out.println("Number of sticks left: "+ nSticks);6,12,18,24System.out.println("Next turn by player "+ player);}// report()publicstaticvoidmain(String args[])1{OneRowNim game =newOneRowNim();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
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.
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.
Executing statement 8 causes to be subtracted from nSticks, leaving the value of . Executing statement 9 assigns the value 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.
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.
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.
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.