12. Arrays and Array Processing

12.11. Case Study: An N-Player Computer Game

In this section we will make use of arrays to extend our game-playing library by developing a design that can support games that involve more than two players. We will use an array to store a variable number of play- ers. Following the object-oriented design principles described in Chap- ter 8, we will make use of inheritance and polymorphism to develop a design that is flexible and extensible, one that can be used to implement a wide variety of computer games. As in our TwoPlayer game example from Chapter 8, our design will allow both humans and computers to play the games. To help simplify the example, we will modify the WordGuess game that we developed in the Chapter 8. As you will see, it requires rela- tively few modifications to convert it from a subclass of TwoPlayerGame to a subclass of ComputerGame, the superclass for our N-Player game hierarchy.

The ComputerGame Hierarchy

Figure 9.29 provides a summary overview of the ComputerGame hierar- chy. This figure shows the relationships among the many classes and in- terfaces involved. The two classes whose symbols are bold, WordGuess and WordGuesser, are the classes that define the specific game we will be playing. The rest of the classes and interfaces are designed to be used with any N-player game.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 9.30: The ComputerGame

class.


At the root of this hierarchy is the abstract ComputerGame class. Note that it uses from 1 to N Players. These objects will be stored in a one- dimensional array in ComputerGame. Recall from Chapter 8 that an IPlayer was any class that implements the makeAMove() method. In this design, we have put the abstract makeAMove() method into the Player class, a class that defines a generic player of computer games. For the WordGuess game, the WordGuesser class extends Player. In order to play Word Guess, we will create a WordGuess instance, plus one or more instances of WordGuessers. This is similar to the OneRowNim example from the previous chapter,

Note where the TwoPlayerGame and OneRowNim classes occur in the hierarchy. TwoPlayerGame will now be an extension of ComputerGame. This is in keeping with the fact that a two-player game is a special kind of N-player computer game. As we will see when we look at the de- tails of these classes, TwoPlayerGame will override some of the methods inherited from ComputerGame.

Because it contains the abstract makeAMove() method, the Player class is an abstract class. Its purpose is to define and store certain data and methods that can be used by any computer games. For example, one important piece of information defined in Player is whether the player is a computer or a person. Player’s data and methods will be inherited by WordGuesser and by other classes that extend Player. Given its position in the hierarchy, we will be able to define polymorphic methods for WordGuessers that treat them as Players. As we will see, this will give our design great flexibility and extensibility.

 

The ComputerGame Class

Figure 9.30 shows the design details of the ComputerGame class. One of the key tasks of the ComputerGame class is to manage the one or more computer game players. Because this is a task that is common to all computer games, it makes sense to manage it here in the superclass. Toward this end, ComputerGame declares four instance variables and several methods. Three int variables define the total number of play- ers (nPlayers), the number of players that have been added to the game (addedPlayers), and the player whose turn it is (whoseTurn). An ar- ray named player stores the Players. In keeping with the zero indexing convention of arrays, we number the players from 0 to nPlayers-1. These variables are all declared protected, so that they can be referenced di- rectly by ComputerGame subclasses, but as protected variables, they remain hidden from all other classes.

The ComputerGame(int) constructor allows the number of players to be set when the game is constructed. The default constructor sets the number of players to one. The constructors create an array of length nPlayers:

,,

 

 

 

 

J

 

The setPlayer() and getPlayer() methods are the mutator and ac- cessor methods for the whoseTurn variable. This variable allows a user to determine and set whose turn it is, a useful feature for initializing a game. The changePlayer() method uses the default expression,

,,

 

J

for changing whose turn it is. Assuming that players are numbered from 0 to nPlayers-1, this code gives the turn to the next player, wrapping around to player 0, if necessary. Of course, a subclass of ComputerGame can override this method if the game requires some other order of play.

The addPlayer(Player) method is used to add a new Player to the game, including any subclass of Player. The method assumes that addedPlayers is initialized to 0. It increments this variable by 1 each time a new player is added to the array. For the game WordGuess, we would be adding Players of type WordGuesser to the game.

The complete source code for ComputerGame is shown in Figure 9.31. There are several points worth noting about this implementation. First,

note that just as in the case of the abstract TwoPlayerGame class from Chapter 8, the methods gameOver() and getWinner() are defined as abstract and the getRules() method is given a generic implementa- tion. The intent here is that the subclass will override getRules() and will provide game-specific implementations for the abstract methods.

Second, note how the addPlayer() method is coded. It uses the addedPlayers variable as the index into the player array, which al- ways has length nPlayers. An attempt to call this method when the array is already full will lead to the following exception being thrown by Java:

,,

 

 

 

J

In other words, it is an error to try to add more players than will fit in the player array. In Chapter 11, we will learn how to design our code to guard against such problems.

Finally, note the implementation of the listPlayers() method (Fig. 9.31). Here is a good example of polymorphism at work. The elements of the player array have a declared type of Player. Their dynamic type is WordGuesser. So when the expression player[k].toString() is invoked, dynamic binding is used to bind this method call to the implementation of toString() defined in the WordGuesser class. Thus, by allowing toString() to be bound at run time, we are able to define a method here that doesn’t know the exact types of the objects it will be listing.

The power of polymorphism is the flexibility and extensibility it lendspolymorphism

to our class hierarchy. Without this feature, we would not be able to define

 

listPlayers() here in the superclass, and would instead have to define it in each subclass.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 9.32:The WordGuess

class.


The
WordGuess and WordGuesser Classes

We will assume here that you are familiar with the WordGuess example from Chapter 8. If not, you will need to review that section before proceed- ing. Word Guess is a game in which players take turns trying to guess a secret word by guessing its letters. Players keep guessing as long as they correctly guess a letter in the word. If they guess wrong, it becomes the next player’s turn. The winner of the game is the person who guesses the last letter secret letter, thereby completely identifying the word.

Figure 9.32 provides an overview of the WordGuess class. If you com- pare it with the design we used in Chapter 8, the only change in the in- stance methods and instance variables is the addition of a new constructor, WordGuess(int), and an init() method. This constructor takes an in- teger parameter representing the number of players. The default construc- tor assumes that there is one player. Of course, this version of WordGuess extends the ComputerGame class, rather than the TwoPlayerGame class. Both constructors call the init() method to initialize the game:

,,

 

 

 

 

 

 

 

J

The only other change required to convert WordGuess to an N-player game, is to rewrite its play() method. Because the new play() method makes use of functionality inherited from the ComputerGame() class,

 

it is actually much simpler than the play() method in the Chapter 8 version:

,,

 

 

 

 

 

 

 

 

 

 

 

J

The method begins by displaying the game’s rules and listing its players. The listPlayers() method is inherited from the ComputerGame class. After displaying the game’s current state, the method enters the play loop. On each iteration of the loop, a player is selected from the array:

,,

 

J

The use of the WordGuesser variable, p, just makes the code some- what more readable. Note that we have to use a cast operator, (WordGuesser), to convert the array element, a Player, into a WordGuesser. Because p is a WordGuesser, we can refer directly to its isComputer() method.

If the player is a computer, we prompt it to make a move, submit the move to the submitUserMove() method, and then report the result. This is all done in a single statement:

,,

 

J

If the player is a human, we prompt the player and use the KeyboardReader’s getUserInput() method to read the user’s move. We then submit the move to the submitUserMove() method and report the result. At the end of the loop, we report the game’s updated state.

 

The following code segment illustrates a small portion of the interaction generated by this play() method:

,,

 

 

 

 

 

 

 

J

In this example, players 0 and 1 are computers and player 2 is a human.

In our new design, the WordGuesser class is a subclass of Player (Figure 9.33). The WordGuesser class itself requires no changes other than its declaration:

,,

 

 

 

 

 

 

 

 

 

 

Figure 9.33: The WordGuesser

class extends Player.


J

As we saw when we were discussing the WordGuess class, the play() method invokes WordGuesser’s isComputer() method. But this method is inherited from the Player class. The only other method used by play() is the makeAMove() method. This method is coded exactly the same as it was in the previous version of WordGuesser.

Figure 9.34 shows the implementation of the Player class. Most of its code is very simple. Note that the default value for the kind vari- able is HUMAN and the default id is -1, indicating the lack of an assigned identification.

What gives Player its utility is the fact that it encapsulates those at- tributes and actions that are common to all computer game players. Defin- ing these elements, in the superclass, allows them to be used throughout the Player hierarchy. It also makes it possible to establish an association between a Player and a ComputerGame.

Given the ComputerGame and Player hierarchies and the many in- terfaces they contain, the task of designing and implementing a new N-

player game is made much simpler. This too is due to the power of object- oriented programming. By learning to use a library of classes, such as

 

these, even inexperienced programmers can create relatively sophisticated and complex computer games.

Finally, the following main() method instantiates and runs an instance of the WordGuess game in a command-line user interface (CLUI):

,,

 

 

 

 

 

 

 

 

J

In this example, we create a three player game in which two of the players are computers. Note how we create a new WordGuesser, passing it a reference to the game itself, as well as its individual identification num- ber, and its type (HUMAN or COMPUTER). To run the game, we simply invoke its play() method. You know enough now about object-oriented design principles to recognize that the use of play() in this context is an example of polymorphism.