9.11. CASE STUDY: An N-Player Computer GameIn 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 players. Following the object-oriented design principles described in Chapter 8, we will make use of inheritance and polymorphism to develop a flexible and extensible design 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 Chapter 8. As you will see, few modifications are needed to convert it from a subclass of TwoPlayerGame to a subclass of ComputerGame, the superclass for our N-Player game hierarchy. 9.11.1. The ComputerGame HierarchyFigure 9.29 provides a summary overview of the ComputerGame hierarchy. This figure shows the relationships among the many classes and interfaces 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.29. Overview of the ComputerGame class hierarchy. |
public abstract class ComputerGame { protected int nPlayers; protected int addedPlayers = 0; protected int whoseTurn; protected Player player[]; // An array of players public ComputerGame() { nPlayers = 1; // Default: 1 player game player = new Player[1]; } public ComputerGame(int n) { nPlayers = n; player = new Player[n]; // N-Player game } public void setPlayer(int starter) { whoseTurn = starter; } public int getPlayer() { return whoseTurn; } public void addPlayer(Player p) { player[addedPlayers] = p; ++addedPlayers; } public void changePlayer() { whoseTurn = (whoseTurn + 1) % nPlayers; } public String getRules() { return "The rules of this game are: "; } public String listPlayers() { StringBuffer result = new StringBuffer("\nThe players are:\n"); for (int k = 0; k < nPlayers; k++) result.append("Player" + k + " " + player[k].toString() + "\n"); result.append("\n"); return result.toString(); } public abstract boolean gameOver(); // Abstract public abstract String getWinner(); // methods } // ComputerGame class |
Second, note how the addPlayer() method is coded. It uses the addedPlayers variable as the index into the player array, which always 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:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2 at ComputerGame.addPlayer(ComputerGame.java:22) at TwentyOne.main(TwentyOne.java:121)
In other words, it is an error to try to add more players than will fit in the player array. In Chapter 10, 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 runtime, we are able to define a method here that does not know the exact types of the objects it will be listing.
The power of polymorphism is the flexibility and extensibility it lends 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.
Polymorphism
Effective Design: Extensibility
Polymorphic methods allow us to implement methods that can be applied to yet-to-be-defined subclasses. |
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 proceeding. 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 secret letter, thereby completely identifying the word.
Figure 9.32 provides an overview of the WordGuess class. If you compare it with the design we used in Chapter 8, the only change in the instance methods and instance variables is the addition of a new constructor, WordGuess(int), and an init() method. This constructor takes an integer parameter representing the number of players. The default constructor 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:
public WordGuess() { super(1); init(); } public WordGuess(int m) { super(m); init(); } public void init() { secretWord = getSecretWord(); currentWord = new StringBuffer(secretWord); previousGuesses = new StringBuffer(); for (int k = 0; k < secretWord.length(); k++) currentWord.setCharAt(k,'?'); unguessedLetters = secretWord.length(); }
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:
public void play(UserInterface ui) { ui.report(getRules()); ui.report(listPlayers()); ui.report(reportGameState()); while(!gameOver()) { WordGuesser p = (WordGuesser)player[whoseTurn]; if (p.isComputer()) ui.report(submitUserMove(p.makeAMove(getGamePrompt()))); else { ui.prompt(getGamePrompt()); ui.report(submitUserMove(ui.getUserInput())); } ui.report(reportGameState()); } // while } // play()
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:
WordGuesser p = (WordGuesser)player[whoseTurn];
The use of the WordGuesser variable, p, just makes the code somewhat 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:
ui.report(submitUserMove(p.makeAMove(getGamePrompt())));
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:
Current word ???????? Previous guesses GLE Player 0 guesses next.Sorry, Y is NOT a new letter in the secret word Current word ???????? Previous guesses GLEY Player 1 guesses next.Sorry, H is NOT a new letter in the secret word Current word ???????? Previous guesses GLEYH Player 2 guesses next. Guess a letter that you think is in the secret word: a Yes, the letter A is in the secret word
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:
public class WordGuesser extends Player
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 variable is HUMAN and the default id is -1, indicating the lack of an assigned identification.
public abstract class Player { public static final int COMPUTER=0; public static final int HUMAN=1; protected int id = -1; // Stores a valve between 0 and nPlayers-1 protected int kind = HUMAN; // Default is HUMAN public Player() { } public Player(int id, int kind) { this.id = id; this.kind = kind; } public void setID(int k) { id = k; } public int getID() { return id; } public void setKind(int k) { kind = k; } public int getKind() { return kind; } public boolean isComputer() { return kind == COMPUTER; } public abstract String makeAMove(String prompt); } // Player class |
What gives Player its utility is the fact that it encapsulates attributes and actions that are common to all computer game players. Defining 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 interfaces 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.
Effective Design: Code Reuse
Reusing library code by extending classes and implementing interfaces makes it much simpler to create sophisticated applications. |
Finally, the following main() method instantiates and runs an instance of the WordGuess game in a command-line user interface (CLUI):
public static void main(String args[]) { KeyboardReader kb = new KeyboardReader(); ComputerGame game = new WordGuess(3); game.addPlayer(new WordGuesser((WordGuess)game, 0, Player.HUMAN)); game.addPlayer(new WordGuesser((WordGuess)game, 1, Player.COMPUTER); game.addPlayer(new WordGuesser((WordGuess)game, 2, Player.COMPUTER); ((CLUIPlayableGame)game).play(kb); } // main()
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 number 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.