by Marko Riedel
The idea behind this recipe is very simple: implement an application that allows two human players to play “connect four” over the network. The program displays the board to each player, who may drop chips of his own color in any one of the seven columns. The player who is the first to obtain four chips along a horizontal, vertical, or diagonal line, wins the game. The application detects a win and uses an alert panel to inform the players that the game is over. It also detects ties. The application has three states: “my turn”, “opponent’s turn” and “game over.”
This game provides an opportunity to illustrate the use of distributed objects (DO) in an easy-to-understand application. There are two classes: a controller that interacts with the application object through delegation and a subclass of NSView that implements the board and the “connect four” functionality. There is one instance of the board, which the controller vends to the network so that the opponent may invoke methods to restart or quit the game, and record a move, all of it through a proxy. These methods are grouped in the protocol “connect four.” The protocol works both ways: each player has a proxy of his opponent’s board and uses it to communicate moves and restart or quit events to the process of his opponent. The connection is bi-directional rather than client-server. The name chosen by the controller to vend the board object has the form “connectfour-user-color”, where “user” is the name of the user playing the game and “color” is the color the user chose (red or yellow, red begins). Start the game from the command line like this:
openapp DOConnectFour.app/ red 172.16.1.150 riedel.
Start by including all the necessary headers and define the width and height of a single square on the board. Define an enumerated datatype that represents the contents of the board: a square is either empty or it contains a red or a yellow chip.
The next set of constants defines the number of rows and columns, the total number of squares on the board and the dimensions of the board in pixels.
The game has three states: “my turn”, “opponent’s turn” and “game over.”
The protocol ConnectFour is of key importance. It defines the methods that the application may invoke on the proxy object representing the opponent’s board. There are three methods: the first communicates to the opponent process to restart the game, the second, to quit the game and the third, to update the board and the state of the opponent’s game to reflect a move by the local player. The qualifier oneway void indicates that the application needn’t wait for the invoked method to return.
We may now define the class that represents the board. Note the first line – it says that it implements the protocol ConnectFour. The board stores the state of the game in the array data, which is BOARD_WIDTH columns wide and BOARD_HEIGHT rows high. The board records the number of moves and the state of the game, as well as the color of the local player (red or yellow). Finally there is an instance variable that holds the proxy object representing the opponent’s board, which also implements the protocol ConnectFour (obviously, since it is an instance of the same class).
The class Board only contains a few simple methods. There is an initializer and a method to restart the game. There is an accessor to set the proxy object. The method winner determines whether the local player has a winning configuration. The most important methods are drawRect:, which draws the board and mouseDown:, which responds to a click on a column, advances the state of the game and notifies the opponent of the change.
Now start the implementation. The initializer is very simple. It computes the frame rectangle and invokes NSView’s method initWithFrame:. It records the color of the local player and restarts the game.
The method newGame is straightforward. It iterates over the squares of the board and resets every square to be empty.
It also resets the move counter to zero and determines the initial state based on the convention that red begins. The last step is to mark the view as needing redisplay.
The setter method setRemote: stores the proxy for the remote board in the corresponding instance variable. Retain and release for this object are handled by the controller.
The method that detects winning configurations iterates over every square of the board. It checks whether the current square could be the first of a horizontal, vertical, ascending diagonal or descending diagonal segment of length four, and if it is, the segment’s contents (chips) are checked. We have a winner if all four are the color of the local player.
Check horizontal segments first.
Do vertical segments next.
Now try ascending diagonals.
Check descending diagonals last and return NO if no segment of length four was found.
The method drawRect: is important, yet straightforward. Start by allocating an array of colors for indexing with the FIELD_STATE data type.
Set the line width to 1.0 and paint the background of the view blue.
The grid that delinates the squares is next. It is drawn in black. Left, right, upper and lower margins are not drawn. There are two loops: the first draws the vertical lines of the grid and the second one draws the horizontal ones.
The last step is to draw the chips, or a black filled circle for empty squares. We iterate over the data array, compute the center of the filled arc and use the data value as an index into the array of colors. We set the color and fill the arc.
The way the board responds to clicks by the user is determined by the method mouseDown:. If the state of the game is not equal to “my turn” i.e. STATE_LOCAL_MOVE, then the click is not valid, and the user hears a beep.
We need to know where the user has clicked and retrieve the corrdinates where the mouse-down occurred. We convert these coordinates to view coordinates. We have a case where the view is the content view of the window, so the conversion is not strictly necessary, but it is the right thing to do, since a view may be anywhere in a window.
Next we compute the column where the user clicked. We use a loop to start at the top of the column and look for the highest square that is not empty. We break out of the loop if we find one.
If the highest square that is not empty is the top square of the column, then we know that the user clicked on a column that is full, and the application beeps to inform the user of this fact.
The remaining case is the case of a valid move. We record the move in the data array and display the view.
It remains to check whether the move resulted in a winning configuration. We check and record the result in win, and communicate the state change to the other player.
The game goes into the “game over” state in the case of a winning move. We pop up an alert panel to congratulate the user.
The game also goes into the “game over” state if there are no more free squares available, in which case we have a tie, and alert the user.