2.5. Case Study: Simulating a Two-Person GameIn this section, we will design and write the definition for a class that keeps track of the details of a well-known two-person game. We will focus on the details of designing the definition of a class in the Java language. Our objective is to understand what the program is doing and how it works, without necessarily understanding why it works the way it does. We will get to "why" later in the book. 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 a player must remove one, two, or three sticks from the row. The player who removes the last stick from the row loses. The game can be played with any number of sticks, but starting with 21 sticks is quite common. This game is sometimes called "Nim," but there is a similar game involving multiple rows of sticks that is more frequently given that name. Thus we will refer to the game as "One-Row Nim." 2.5.1. Designing a OneRowNim ClassProblem SpecificationLet's design a class named OneRowNim that simulates the game of One-Row Nim with a row of sticks. An object constructed with this class should manage data that corresponds to having some specified number of sticks when the game begins. It should keep track of whose turn it is, and it should allow a player to diminish the number of sticks remaining by one, two, or three. Finally, a OneRowNim object should be able to decide when the game is over and which player has won. Problem DecompositionLet's design OneRowNim so that it can be used with different kinds of user interfaces. One user interface could manage a game played by two persons who alternately designate their moves to the computer. Another user interface could let a human player play against moves made by the computer. In either of these cases we could have a human player designate a move by typing from the keyboard after being prompted in a console window or, alternatively, by inputting a number into a text field or selecting a radio button on a window. In this chapter, we will be concerned only with designing an object for managing the game. We will design user interfaces for the game in subsequent chapters. Class Design: OneRowNimAs we saw in the Riddle example, class definitions can usually be broken down into two parts: (1) the information or attributes that the object needs, which must be stored in variables, and (2) the behavior or actions the object can take, which are defined in methods. In this chapter, we will focus on choosing appropriate instance variables and on designing methods as blocks of reusable code. Recall that a parameter is a variable that temporarily stores data values that are being passed to a method when the method is called. In this chapter, we will restrict our design to methods that do not have parameters and do not return values. We will return to the problem of designing changes to this class in the next chapter after an in-depth discussion of method parameters and return values. The OneRowNim object should manage two pieces of information that vary as the game is played. One is the number of sticks remaining in the row, and the other is which player has the next turn. Clearly, the number of sticks remaining corresponds to a positive integer that can be stored in a variable of type int. One suitable name for such a variable is nSticks. For this chapter, let us assume that the game starts with seven sticks rather than 21, to simplify discussion of the program. What data do we need? Data designating which player takes the next turn could be stored in different ways. One way to do this is to think of the players as player 1 and player 2 and store a 1 or 2 in an int variable. Let's use player as the name for such a variable and assume that player 1 has the first turn. The values of these two variables for a particular OneRowNim object at a particular time describe the object's state. An object's state at the beginning of a game is a 7 stored in nSticks and a 1 stored in player. After player 1 removes, say, two sticks on the first turn, the values 5 and 2 will be stored in the two variables. Method DecompositionNow that we have decided what information the OneRowNim object should manage, we need to decide what actions it should be able to perform. We should think of methods that would be needed to communicate with a user interface that is both prompting the human players and receiving moves from them. Clearly, methods are needed for taking a turn in the game. If a message to a OneRowNim object has no argument to indicate the number of sticks taken, there will need to be three methods corresponding to taking one, two, or three sticks. The method names takeOne(), takeTwo(), and takeThree() are descriptive of these actions. Each of these methods will be responsible for reducing the value of nSticks as well as for changing the value of player. What methods do we need? We should also have a method that gives the information a user needs when considering a move. Reporting the number of sticks remaining and whose turn it is to the console window would be an appropriate action. We can use report() as a name for this action. Figure 2.16 is a UML class diagram that summarizes this design of the OneRowNim class. Note that the methods are declared public (+) and will thereby form the interface for a OneRowNim object. These will be the methods that other objects will use to interact with it. Similarly, we have followed the convention of designating that an object's instance variablesOneRowNim's instance variablesare to be kept hidden from other objects, and so we have designated them as private (-). Figure 2.16. A UML class diagram for OneRowNim.2.5.2. Defining the OneRowNim ClassGiven our design of the OneRowNim class as described in Figure 2.16, the next step in building our simulation is to begin writing the Java class definition. The Class HeaderWe need a class header, which will give the class a name and will specify its relationship to other classes. Like all classes designed to create objects that could be used by other objects or classes, the class OneRowNim should be preceded by the public modifier. Because the class OneRowNim has not been described as having a relationship to any other Java class, its header can omit the extends clause, and therefore it will be a direct subclass of Object (Figure 2.17). Thus, the class header for OneRowNim will look like: public class OneRowNim // Class header Figure 2.17. By default, OneRowNim is a subclass of Object.The Class's Instance VariablesThe body of a class definition consists of two parts: the class-level variables and the method definitions. A class-level variable is a variable whose definition applies to the entire class in which it is defined. Instance variables, which were introduced in Chapter 1, are one kind of class-level variable. Variables and methods In general, a class definition will take the form shown in Figure 2.18. Although Java does not impose any particular order on variable and method declarations, in this book we will define the class's class-level variables at the beginning of the class definition, followed by method definitions. Class-level variables are distinguished from local variables. A local variable is a variable that is defined within a method. Examples would be the variables q and a defined in the Riddle(String q, String a) constructor (Fig. 2.12). As we will see better in Chapter 3, Java handles each type of variable differently. Figure 2.18. A template for constructing a Java class definition.(This item is displayed on page 80 in the print version)
Class-level vs. local variables A declaration for a variable at class level must follow the rules for declaring variables that were described in Section 1.4.8 with the added restriction that they should be modified by an access modifier, either public, private, or protected. The rules associated with these access modifiers are:
When a class, instance variable, or method is defined, you can declare it public, protected, or private. Or you can leave its access unspecified, in which case Java's default accessibility will apply. Java determines accessibility in a top-down manner. Instance variables and methods are contained in classes, which are contained in packages. To determine whether an 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 Table 2.2.
Recall the distinction we made in Chapter 0 between class variables and instance variables. A class variable is associated with the class itself, whereas an instance variable is associated with each of the class's instances. In other words, each object contains its own copy of the class's instance variables, but only the class itself contains the single copy of a class variable. To designate a variable as a class variable it must be declared static. The Riddle class that we considered earlier has the following two examples of valid declarations of instance variables: private String question; Class-Level Variables for OneRowNimLet's now consider how to declare the class-level variables for the OneRowNim class. The UML class diagram for OneRowNim in Figure 2.16 contains all the information we need. The variables nSticks and player will store data for playing one game of One-Row Nim, so they should clearly be private instance variables. They both will store integer values, so they should be declared as variables of type int. Because we wish to start a game of One-Row Nim using seven sticks, with player 1 making the first move, we will assign 7 as the initial value for nSticks and 1 as the initial value for player. If we add the declarations for our instance variable declarations to the class header for the OneRowNim class, we get the following: public class OneRowNim To summarize, despite its apparent simplicity, a class-level variable declaration actually accomplishes five tasks:
OneRowNim's MethodsDesigning 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 will 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 The Method HeaderThe 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. Table 2.3 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. Effective Design: Public versus Private Methods
Recall from Chapter 0 the distinction between instance methods and class methods. Methods declared at the class level are assumed to be instance methods unless they are also declared static. The static modifier is used to declare that a class method or variable is associated with the class itself rather than with its instances. Just like static variables, methods that are declared static are associated with the class and are therefore called class methods. As its name implies, an instance method can only be used in association with an object (or instance) of a class. Most of the class-level methods we declare will be instance methods. Class methods are used only rarely in Java and mainly in situations where it is necessary to perform a calculation of some kind before objects of the class are created. We will see examples of class methods when we discuss the Math class, which has such methods as sqrt(N) to calculate the square root of N. Java Programming Tip: Class versus Instance Methods
All four of the methods in the OneRowNim class are instance methods (Fig. 2.19). They all perform actions associated with a particular instance of OneRowNim. That is, they are all used to manage a particular One-Row Nim game. Moreover, all four methods should be declared public, because they are designed for communicating with other objects rather than for performing internal calculations. Three of the methods are described as changing the values of the instance variables nSticks and player, and the fourth, report(), writes information to the console. 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. Given these design decisions, we now can add method headers to our class definition of OneRowNim, in Figure 2.19. The figure displays the class header, instance variable declarations, and method headers. Figure 2.19. The Instance variables and method headers for the OneRowNim class.
The Method BodyThe 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 description of the action required of the takeOne() method is typical of many methods that change the state of an object. The body of the takeOne() method should use a series of assignment statements to reduce the value stored in nSticks by 1 and change the value in player from 2 to 1 or from 1 to 2. The first change is accomplished in a straightforward way by the assignment: nSticks = nSticks - 1; Designing a method is an application of the encapsulation principle. 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. Note, however, that in both cases the current value plus the desired new value is equal to 3. Therefore, the new value of player are equal to 3 minus its current value. Writing this as an assignment we have: player = 3 - player; One can easily verify that this clever assignment assigns 2 to player if its current value is 1 and assigns 1 to it if its current value is 2. In effect, this assignment will toggle the value off player between 1 and 2 each time it is executed. In the next chapter we will introduce the if-else control structure, which will allow us to accomplish the same toggling action in a more straightforward manner. The complete definition of takeOne() method becomes: public void takeOne() The takeTwo() and takeThree() methods are completely analogous to the takeOne() method, with the only difference being the amount subtracted from nSticks. The body of the report() method must merely print the current values of the instance variables to the console window with System.out.println(). To be understandable to someone using a OneRowNim object, the values should be clearly labeled. Thus the body of report() could contain: System.out.println("Number of sticks left: " + nSticks); This completes the method bodies of the OneRowNim class. The completed class definition is shown in Figure 2.20. 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 by indicating certain moves to make. Figure 2.20. The OneRowNim class definition.
2.5.3. Testing the OneRowNim ClassRecall our define, create, and use mantra from Section 2.4.5. Now that we have defined the OneRowNim class, we can test whether it works correctly by creating OneRowNim objects and using them to perform the actions associated with the game. At this point, 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 an object for it to refer to. The variable can have any name, but a name like game would be consistent with the function of recording moves in a single game. To test the OneRowNim class, we should make a typical series of moves. For example, three moves taking three, three, and one sticks respectively would be one way that the seven sticks could be removed. 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:
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: public class OneRowNimTester When it is run, OneRowNimTester produces the following output: Number of sticks left: 7 This output indicates that player 1 removed the final stick and so player 2 is the winner of the game. Self-Study Exercises
2.5.4. Flow of Control: Method Call and ReturnA 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 is 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.21 shows the method call and return structure. Figure 2.21. The method call and return control structure. It is 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. Java Language Rule: Return Statement
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. Default returns 2.5.5. Tracing the OneRowNim ProgramTo help us understand the flow of control in OneRowNim, we will perform a trace of its execution. Figure 2.22 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. But keep in mind that the code in the main() method could just as well appear in the OneRowNimTester class. The listing in Figure 2.22 also adds line numbers to the program to show the order in which its statements are executed. Figure 2.22. 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, which in this case is a default 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 the initial state shown in Figure 2.23. 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. Figure 2.23. The initial state of game, a OneRowNim object.Number of sticks left: 7 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 the int value stored in the instance variable nSticks of game, leaving the value of 4. Executing statement 9 subtracts the value stored in the player variable, which is 1, from 3 and assigns the result (the value 2) back to player. The state of the object game, at this point, is shown in Figure 2.24. Tracing the remainder of the program follows in a similar manner. Note 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.24. The state of game after line 9 is executed.2.5.6. Object-Oriented Design: Basic PrinciplesWe 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.
The OneRowNim class has some obvious shortcomings that are a result of our decision to limit the design to methods without parameters or return values. These shortcomings include:
As we study other features of Java in the next two chapters, we will modify the OneRowNim class to address these identified shortcomings. |
Wednesday, November 25, 2009
Section 2.5. Case Study: Simulating a Two-Person Game
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment