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.
If we didn’t win and there is room on the board, then it must be the opponent’s turn, and we make the appropriate state change.
Note that we recorded the state change before we displayed the alert panel in the two cases where an alert was necessary. This is because the alert panel runs a modal loop during which our board could receive events from the opponent. The way the code is written we will not overwrite any state changes that could occur while the alert panel is on screen.
The last section of the board class implements the protocol ConnectFour, which we saw earlier, and which makes communication between the two players possible. The first method (remoteNewGame) is invoked when the opponent decides to restart the game. We do the same, thus keeping the two boards in sync.
The second method is invoked when the opponent quits the game. We are observing the notification “connection died”, which we stop observing when the opponent quits. We terminate the application once this is done.
The method remoteMoveAtRow:Col:Win is the most important one of the protocol and it keeps the two boards in sync. It starts by incrementing the move counter, records the move in the data array and displays the board.
If the opponent reached a winning position, then we lost the game, change state to “game over” and run an appropriate alert.
We also go into the state “game over” if there is no more room on the board. This is exactly the same as in the method mouseDown:, and we also run an alert panel.
The last case occurs if the opponent’s move wasn’t a winning one and there is room on the board. In that case it’s our turn; by the way, the observation about not changing state after the panel appears applies here as well.
The last part of this recipe is the controller. It responds to two notifications: “application finished launching” and “connection died.” It stores the window and the board in instance variables, as well as two strings that describe the two players and a proxy object that represents the board object of the opponent.
The first two methods respond two the two notifications described above. The remaining two methods are action methods that respond to clicks on the main menu. They restart the game and terminate the application, respectively.
It is the job of applicationDidFinishLaunching: to build the two components of the GUI: the main menu and the window that displays the board. It must also read command line parameters that determine the color of the local player and the host and username of the opponent. It first retrieves the array of arguments and checks that we have the right number and raises an exception otherwise.
The first argument gives the color of the local player. We retrieve it from the argument array and compare it against the strings “red” and “yellow” and store the result in the variable lcolor. We raise an exception if the user entered an unknown color.
The three parameters remoteHost, remoteUser and remoteColor describe the opponent. They are initialized from the array of arguments.
The next step is to allocate and initialize the board and the window whose content view it will become. The window will not be resizable.
The title of the window shows the color of the local player and the name and host of the opponent. We make the board the content view of the window and set the window delegate.
The variable localName holds the name under which we will register the local connection and make the local board available to the opponent.
We retrieve the default connection, set its root object and attempt to register it under the local name. We raise an exception if we couldn’t register under the local name.
The next step is to constuct the name of the remote connection. It has the same form as the local name. (This is important.) It contains the remote user and color and a prefix that identifies the application.
We now try to establish a connection. We try this sixty times, with a pause of three seconds between each attempt, for a total of three minutes. We try to obtain the proxy object under the remote name at the remote host. We sleep for three seconds if we fail and then we try again. We log each attempt so that the user sees what the application is doing.
If the variable remote is nil after the loop has finished, then we failed to obtain a connection and raise an exception. Otherwise we retain the proxy object and set the corresponding instance variable of the local board object.
We add ourselves to the default notification center as an observer of the notification “connection died.” We want to be notified so that we may terminate the application.
We may order the window to the front once we have registered under the local name and obtained the proxy object of our opponent. The window is centered on screen.
The last step is to construct the main menu, which has two entries, one to restart the game and another one to quit the application. Now the application is ready to go.
The next method responds to the notification that the connection died. We don’t do anything fancy here; we release the proxy object and raise an exception.
The two action methods are last. The method newGame: is invoked from the menu. It communicates the state change to the opponent and resets the local state and board.
The last method in this recipe is the method terminate:, which is also invoked from the menu. We do not want to receive the notification “connection died” when the opponent’s application quits in response to our message. Hence we remove ourselves from the notification center. The next step is to tell the other side to quit. We terminate the application once this is done.
This is the end of the recipe, which is based on the DO tutorials by Nicola Pero and Adam Fedor. We do not show the function main, since it is identical to what we have e.g. in the n-queens recipe.