Tutorial 2: Connect 4 (alpha 0.2.3)
|
|
Last Updated:
3rd Oct, 2005, 14:18 GMT
|
|
Contents
|
|
|
|
|
Introduction
|
|
|
This tutorial shows how the turn-based game of
Connect 4
can be created using the JOGRE 0.2.3 API. This tutorial has been updated from the old
0.2.1 to the newer 0.2.3 release which adds more functionality on the server side
such as game state, server side game over detection etc.
Connect 4 is a game where two players take turns to drop coloured pieces down columns.
The aim of the game is for a player to create a 4 in a row pattern vertically, horizontally
or diagonally.
At the end of this tutorial you should have something which looks like the
following:
|
|
|
|
This tutorial illustrates each part of creating this tutorial using
source code listings and UML diagrams which show how each new class extends
from the JOGRE API. The key of the UML diagrams are as follows:
|
|
|
|
[ top ] |
|
|
Creating a New Project
|
|
|
Before you start this tutorial ensure you have the "Jogre Source, Alpha 0.2.3"
distribution extracted somewhere (e.g. c:\) which is available on the
download page and then built using
the ANT utility. To ensure we are following
the tutorial properly we have created a new folder under /jogre/games called
c4tutorial to avoid confusion from the existing /jogre/games/connect4.
The first step for creating a new project/game is to create a new directory underneath the
main "jogre/games" directory i.e. jogre/games/c4tutorial
Inside the jogre/games/c4tutorial directory create source and images folders i.e.
c4tutorial/src and c4tutorial/images. The c4tutorial/images folder
should contain the following 5 images.
|
|
jogre_icon.gif |
connect4_icon.gif |
no_piece.gif |
red_piece.gif |
yellow_piece.gif |
|
|
|
|
|
|
|
The source folders should be organised as follows:
|
|
Connect 4 src folders |
Description |
c4tutorial/src/org/jogre/c4tutorial/client |
Location of client specific files |
|
|
The next step is to create a build file for this project, which is stored
in the tutorial directory. The build program
ANT can be used to compile
and copy the properties files into a classes directory. Since applet support
in JOGRE 0.2.3 this build file has got much larger in size but is very similar and
straightforward for each game.
|
|
build.xml
<?xml version="1.0" encoding="utf-8"?>
<project name="Connect4 Tutorial" default="compile" basedir=".">
<!-- Source code directory -->
<property name="src.dir" value="${basedir}/src"/>
<!-- Class directory -->
<property name="class.dir" value="${basedir}/classes"/>
<!-- Image directory -->
<property name="image.dir" value="${basedir}/images"/>
<!-- JAR file -->
<property name="jar.file" value="c4tutorial.jar"/>
<!-- Temp JAR directory -->
<property name="temp.jar.dir" value="${basedir}/tempjardir"/>
<!-- Classpath -->
<path id="classpath">
<pathelement location="${basedir}/../../api/jogre.jar"/>
<pathelement location="${basedir}/../../server/server.jar"/>
</path>
<!-- Init. -->
<target name="init">
<!-- Create the build directory structure used by compile -->
<mkdir dir="${class.dir}"/>
</target>
<!-- Compile. -->
<target name="compile" depends="init"
description="compile the source">
<!-- Compile the java code from ${src} into ${classes} -->
<javac srcdir="${src.dir}" destdir="${class.dir}" >
<classpath refid="classpath"/>
</javac>
<copy todir="${class.dir}">
<fileset dir="${basedir}">
<include name="*.properties"/>
</fileset>
</copy>
<uptodate property="jar.notRequired" targetfile="${jar.file}" >
<srcfiles dir= "${class.dir}" includes="**/*.class"/>
<srcfiles file= "${classpath}" />
</uptodate>
</target>
<!-- All -->
<target name="all" depends="compile" unless="jar.notRequired"
description="compile and make jar">
<echo/>
<echo>--------- making a jar for connect4 applet ---------</echo>
<mkdir dir="${temp.jar.dir}"/>
<copy todir="${temp.jar.dir}">
<!-- Copy from API -->
<fileset dir="${basedir}/../../api/classes">
<include name="**/*.class"/>
</fileset>
<fileset dir="${basedir}/../../api">
<include name="**/*.gif"/>
<include name="*.properties"/>
</fileset>
<!-- Copy from Game -->
<fileset dir="${class.dir}">
<include name="**/*.class"/>
</fileset>
<fileset dir="${basedir}">
<include name="images/*.gif"/>
<include name="*.properties"/>
</fileset>
</copy>
<jar jarfile="${jar.file}" basedir="${temp.jar.dir}" compress="yes">
<exclude name="**/*.html"/>
</jar>
<delete dir="${temp.jar.dir}"/>
</target>
</project> |
|
|
The final step is to create two property files, game.properties (one exists for each game)
and game_labels.properties (multiple for each language). These are both stored in the c4tutorial directory.
|
|
game.properties
# Game ID (game)
game.id=c4tutorial
# Must be a "colour.1" to "colour.x" where x = max.num.players
player.colour.0=255,0,0
player.colour.1=255,255,0
# Declare background colour
background.colour=210,210,230
# Declare images
image.0=images/connect4_icon.gif, 1052
image.1=images/no_piece.gif, 1440
image.2=images/red_piece.gif, 2278
image.3=images/yellow_piece.gif, 2270 |
|
|
game_labels.properties
# Client
jogre.client.title=JOGRE Connect 4 Client
jogre.client.welcome.message=Welcome to JOGRE Connect 4!
jogre.table.title=Jogre Connect 4 Table
# Must be a "player.label.0" to "player.label.x"
player.label.0=Red
player.label.1=Yellow |
|
|
The game.property file contains game properties including
the important game.id property which sets the game key. It also
includes the colours of players (connect 4 is red and yellow) and background
colours using 0-255 RGB values. Finally, since applet support
in Alpha 0.2.3 the images must be defined in the property files along with the
image sizes (in bytes). This is necessary as the images are read from an applet jar file.
The game_labels.property files contains five labels which the API makes use of.
Most of the properties are self-explanatory and simply
set the name of the client and the welcome message that a client recieves. The
player.label.x properties are the names
of the player when they sit down at a table. For Connect 4 these are "yellow"
and "red". Additional labels can be inserted here if labels are required in the
game.
|
|
[ top ] |
|
|
Creating a Server
|
|
|
In JOGRE Alpha 0.2.3 a single server exists which can handle multiple games.
To ensure that the server will support a game the game key
(from game.properties) must be inserted in the
/jogre/server/server.xml file. It also defines the minimum and maximum
number of players for a game. For connect 4 these are both 2.
|
|
server.xml
<!-- List of supported games. -->
<supported_games>
<game id="battleships" minPlayers="2" maxPlayers="2" />
<game id="checkers" minPlayers="2" maxPlayers="2" />
<game id="c4tutorial" minPlayers="2" maxPlayers="2" />
<game id="chess" minPlayers="2" maxPlayers="2" />
<game id="dots" minPlayers="2" maxPlayers="2" />
...
</supported_games> |
|
|
[ top ] |
|
|
Running a Server
|
|
|
As mentioned earlier there is only one server which can be run through the
/jogre/server/server.bat batch file. Release alpha 0.2.1
also has a GUI wrapper for the server which is very useful
as it shows messages sent / received from clients and the various users / tables
and players in a nice GUI tree. To run the GUI wrapper
execute the /jogre/server/servergui.bat batch file.
If the server runs OK you will see the following screenshot and notice
the c4tutorial key under the Games being served.
|
|
|
|
Note:The c4tutorial gamekey will not have a [controller] string to its left.
If you are running the GUI wrapper ensure that you copy the connect4_icon.gif
image from /jogre/games/connect4/images/ to /jogre/server/images
and rename it to c4tutorial.gif (game key with extension .gif). You should then
see something like the following:
|
|
|
|
[ top ] |
|
|
Creating a Client Frame/Table
|
|
|
Once a server has been successfully created the next step is to create a client
frame which can connect to the server and interact with other users.
To create a client frame two classes must now be created (alpha 0.1 required four).
These Connect4ClientFrame and Connect4TableFrame
which extend extend JogreClientFrame and JogreTableFrame
from the JOGRE API. These classes should be stored in the
src/org/jogre/c4tutorial/client folder under /jogre/games/c4tutorial.
|
|
Location |
Class/Interface Name |
API Extends |
Synopsis |
org.jogre.c4tutorial.client |
Connect4ClientFrame |
JogreClientFrame |
Connect 4 client frame for creating tables. |
Connect4TableFrame |
JogreTableFrame |
Connect 4 table frame for playing a game. |
|
|
The following clickable image shows these classes as a UML diagram.
|
|
|
|
The UML diagram illustrates both the Connect4ClientFrame and Connect4TableFrame
inheriting the JogreClientFrame and JogreTableFrame
classes from the API which in turn extend the Swing class JFrame
The architecture of alpha 0.2 has changed a great deal with regards to client / server communication.
Clients don't need to create their own connection threads
anymore but communicate to the server through the ClientConnectionThread and
TableConnectionThread classes. Communication is sent from the server back to the
ClientConnectionThread which automatically delegates it to
the correct client frame through the IClient and ITable interfaces depending of
the layer (game or table) of the communication message. No manual
delegation of communication is required as of alpha 0.2.
These two classes will now be explained in more detail using their source code. Any items on the
source which are in bold are of some interest and will be discussed further.
|
|
Connect4ClientFrame.java
package org.jogre.c4tutorial.client;
import org.jogre.client.TableConnectionThread;
import org.jogre.client.awt.JogreClientFrame;
import org.jogre.client.awt.JogreTableFrame;
// Tic-tac-toe client frame.
public class Connect4ClientFrame extends JogreClientFrame {
// Constructor
public Connect4ClientFrame (String [] args) {
super (args);
}
// Return the correct table frame
public JogreTableFrame getJogreTableFrame (TableConnectionThread conn) {
return new Connect4TableFrame (conn);
}
// Main method where this class gets run
public static void main (String [] args) {
Connect4ClientFrame frame = new Connect4ClientFrame (args);
}
} |
|
|
The Connect4ClientFrame is the client class which is executed as
it contains a main method. This class extends JogreClientFrame and must
implement one abstract method (alpha 0.1 required two methods to be filled).
This method is getJogreTableFrame which is filled in to return the correct
Connect4TableFrame where each of the games of connect 4 are played.
|
|
Connect4TableFrame.java
package org.jogre.c4tutorial.client;
import javax.swing.*;
import org.jogre.client.awt.JogreTableFrame;
import org.jogre.client.awt.GameImages;
import org.jogre.client.TableConnectionThread;
// Jogre table frame
public class Connect4TableFrame extends JogreTableFrame {
// Constructor which passes a table connection thread.
public Connect4TableFrame (TableConnectionThread conn) {
super (conn);
// Set icon
this.setIconImage (GameImages.getImage (0));
// Fill in rest later with instances of MVC classes
}
} |
|
|
The Connect4TableFrame is where each game is going of connect 4 is to be played.
The class must consist of a constructor which matches that of the
getJogreTableFrame (TableConnectionThread conn) method of
Connect4ClientFrame. This class also keeps instances of the
MVC (model/view/controller) classes. These will be filled in later in
the tutorial.
|
|
[ top ] |
|
|
Running a client
|
|
|
The next step is to compile these files by running the ANT script or using an IDE like
Eclipse and manually copying the properties files to the classes folder. If you are using
Eclipse (or any IDE) ensure that the build path has a link to the API (either the jogre/api/jogre.jar file
or a link to a Jogre API project). Once the classes are compiled OK
run the Jogre Server. The connect 4 client can now execute and connect to the
server. This is achieved by going to the jogre/games/connect4 folder
and typing the following java command:
java -classpath .;classes;..\..\api\jogre.jar org.jogre.c4tutorial.client.Connect4ClientFrame
If the command is sucessful then the client will appear with a logon screen.
Ensure you have a username in the the users.xml file (located in the
jogre/server/data/xml folder). You should see something like the following:
|
|
|
|
As you can see the JogreTableFrame is blank and does not contain the
actual game. This is created in the next section.
|
|
[ top ] |
|
|
The Connect 4 MVC (Model/View/Controller)
|
|
|
The next step is to create the actual game of "connect 4" using the MVC
design pattern (see the tic-tac-toe
tutorial for more information on MVC).
Of all the changes from alpha 0.1 to the alpha 0.2 API, the
MVC part of the JOGRE API has changed the least (although new convenience functionality has been added).
This section is quite similar to the same section in the "tic-tac-toe" tutorial.
To create the game of connect 4 another three classes are going to be created
and initialised in the ConnectTableFrame class. These classes are as
follows:
|
|
Location |
Class/Interface Name |
API Extends |
Synopsis |
org/jogre/c4tutorial/client |
Connect4Model |
JogreModel |
Model which holds the data for a game of Connect4. |
Connect4Controller |
JogreController |
Controller which updates the model and listens to user input on the visual board. |
Connect4BoardComponent |
AbstractBoardComponent (-> JogreComponent) |
Connect4 visual board component (view of the model). |
|
|
The following clickable image shows these three MVC classes as a clickable UML diagram.
|
|
|
|
Once again, all these classes extend classes from the JOGRE API. These
three classes will now be explained in more detail using their source code. Any items on the
source which are in bold are of some interest and will be discussed further.
|
|
Connect4Model.java
package org.jogre.c4tutorial.client;
import org.jogre.common.JogreModel;
// Model which holds the data for a game of Connect4
public class Connect4Model extends JogreModel {
// Declare constants to define model
public static final int COLS = 7;
public static final int ROWS = 6;
public static final int BLANK = -1;
public static final int RED = 0;
public static final int YELLOW = 1;
// model (2 dimensional int array)
private int [][] data = new int [COLS][ROWS];
// Constructor
public Connect4Model() {
super();
reset ();
}
// reset the model back to zeros
public void reset () {
for (int x = 0; x < COLS; x++) {
for (int y = 0; y < ROWS; y++) {
data [x][y] = BLANK;
}
}
refreshObservers();
}
// return data at a particular point
public int getData (int x, int y) {
return data [x][y];
}
// set data at a point
public void setData (int x, int value) {
// Find the next available empty space
for (int y = ROWS - 1; y >= 0; y--) {
if (getData(x, y) == BLANK) {
data [x][y] = value;
refreshObservers(); // update any views on this model
return;
}
}
}
} |
|
|
The model class Connect4Model is similar to the TicTacToe
model in the previous tutorial. It's data is stored in a 2 dimensional integer
array of 7 x 6 (7 columns by 6 rows). The data on each of these points can
be either -1, 0 or 1 (empty, red or yellow). The class also contains a start () method
which calls the reset () method which wipes all the data. There is also getter
and setter methods. The set method takes a value (red or yellow) and a column. The method
starts at the bottom of the board and loops until it finds an empty place and gives
it a value. The refreshObservers () method (from JogreModel) is then called
which will refresh any component registered with this data i.e. Connect4BoardComponent.
The next class discussed is the view part of the MVC architecture which is the
Connect4BoardComponent.java.
|
|
Connect4BoardComponent.java
package org.jogre.c4tutorial.client;
import java.awt.*;
import javax.swing.ImageIcon;
import org.jogre.client.awt.AbstractBoardComponent;
import org.jogre.client.awt.GameImages;
// Connect4 visual board component (view of the model)
public class Connect4BoardComponent extends AbstractBoardComponent {
// Declare constants which define what the board looks like
private static final int NUM_OF_COLS = 7;
private static final int NUM_OF_ROWS = 6;
private static final int CELL_SIZE = 50;
private static final int CELL_SPACING = 0;
private static final int BORDER_WIDTH = 0;
// Link to the model
protected Connect4Model model;
protected int curMousePoint = -1;
// Constructor which creates the board
public Connect4BoardComponent (Connect4Model model) {
// Call constructor in AbstractBoardComponent
super (NUM_OF_ROWS, NUM_OF_COLS, CELL_SIZE, CELL_SPACING,
BORDER_WIDTH, 0, false, false, false);
this.model = model; // link to model
// set colours
Color bgColor = new Color (34, 34, 102);
setColours (bgColor, bgColor, Color.black, Color.white);
}
// Update the graphics depending on the model
public void paintComponent (Graphics g) {
// Draw the board (AbstractBoardComponent)
super.paintComponent (g);
// draw each Connect 4 piece on the board (loop through the model)
for (int x = 0; x < Connect4Model.COLS; x++) {
for (int y = 0; y < Connect4Model.ROWS; y++) {
// get piece from model
int piece = model.getData(x, y);
// Now get screen co-ordinates (AbstractBoardComponent)
Point screen = getScreenCoords (x, y);
// Now get screen co-ordinates (AbstractBoardComponent)
Image image = GameImages.getImage(piece + 2);
g.drawImage (image, screen.x, screen.y, null);
}
}
// Draw the box around the board
if (curMousePoint != -1) {
g.setColor (new Color (200, 200, 200));
Point screen = getScreenCoords (curMousePoint, 0);
g.drawRect (screen.x, 0, CELL_SIZE - 1, (CELL_SIZE * NUM_OF_ROWS) - 1);
}
}
// Return the current mouse point.
public int getCurMousePoint() {
return curMousePoint;
}
// Set the current mouse point to another point.
public void setCurMousePoint (int newPoint) {
curMousePoint = newPoint;
}
}
|
|
|
The Connect4BoardComponent class extends an AbstractBoardComponent which is
a helper class used for creating game boards (e.g. chess, checkers, etc). This
class has a link to the connect 4 model which it renders. Its constructor class
the super constructor in AbstractBoardComponent which sets the number of
rows, columns, cellspacing etc.
The most important method for any class which extends JogreComponent is the
paintComponent method as it is responsible for actually rendering the
graphics onto the screen. This method can be called from a model using the
refreshObservers() method call. The paintComponent method
starts with a call to super.paintComponent (g)
which renders board from AbstractBoardComponent. It then loops through the
rows / columns of the Connect4Model and gets the screen co-ordinate of each
position. It then draws a graphic onto the screen at this point depending on its
value in the model (no piece, red piece or yellow). Finally, if it is a players go
it draws a box around the position of the mouse to show which column the piece will drop down.
Getter and setter methods for setting this mouse point are included and these are called
from the controller (next).
|
|
Connect4Controller.java
package org.jogre.c4tutorial.client;
import java.awt.Point;
import java.awt.event.MouseEvent;
import org.jogre.client.JogreController;
import org.jogre.common.comm.CommGameOver;
// Controller for the
public class Connect4Controller extends JogreController {
// links to game data and the board component
protected Connect4Model model;
protected Connect4BoardComponent boardComponent;
// Constructor
public Connect4Controller (Connect4Model model, Connect4BoardComponent boardComponent) {
super(model, boardComponent);
this.model = model; // set fields
this.boardComponent = boardComponent;
}
// Start method which calls reset method (resets the model).
public void start() {
model.reset ();
}
// Overwrite the mousePressed method
public void mousePressed (MouseEvent e) {
if (isGamePlaying() && isThisPlayersTurn ()) { // ensure game has started
// get board point from screen position of mouse click
Point board = boardComponent.getBoardCoords (e.getX(), e.getY());
// ensure board point is in range (0 to 2)
if (board.x >= 0 && board.x < Connect4Model.COLS &&
board.y >= 0 && board.y < Connect4Model.ROWS) {
// check model at this point is BLANK and it is this persons go
if (model.getData(board.x, 0) == Connect4Model.BLANK) {
int value = getCurrentPlayerSeatNum();
move (board.x, value); // move
checkGameOver (); // check to see if the games over
}
}
}
}
// Show the column that the mouse is on.
public void mouseMoved (MouseEvent e) {
if (isGamePlaying () && isThisPlayersTurn ()) {
Point point = boardComponent.getBoardCoords (e.getX(), e.getY());
// If user has moved to a new column
if (point.x != boardComponent.getCurMousePoint()) {
// Check to see if there is room left
if (model.getData(point.x, 0) != Connect4Model.BLANK)
boardComponent.setCurMousePoint (-1);
else
boardComponent.setCurMousePoint (point.x);
// update the board
boardComponent.repaint();
}
}
}
// Update the model
public void move (int x, int value) {
boardComponent.setCurMousePoint (-1);
// update model and change player turn
model.setData (x, value);
// next players turn
nextPlayer ();
}
// Check to see if the game is over or not.
private void checkGameOver () {
// Do later
}
} |
|
|
The Connect4Controller controller class updates the model and
listens to user input on the Connect4BoardComponent. This class
extends the JogreController class which is an adapter class which
implements all the methods from MouseMotionListener,
MouseListener and KeyListener.
The controller class has links to both the model
and the view (see the UML diagram
previous) which are both set in the constructor. The mousePressed and
mouseMoved methods are overwritten to response to user input. Both methods
start with the line:
if (isGamePlaying() && isThisPlayersTurn ()) { ...
This ensures that the method will not execute any further unless the game
has started and is this particular players turn to go. Both methods then convert
the large pixel co-ordinate into smaller board co-ordinate (0 .. 6) using the helper method
getBoardCoords() from AbstractBoardComponent. If the Connect4BoardComponent
didn't extend from AbstractBoardComponent then this method would have be
created manually (e.g. check out Propinqity game which creates a hexagonal board).
In the mousePressed method the model is check to ensure there is space
for a piece to be dropped. If this is the case then the move is made.
The mouseMoved method simply sets the curMousePoint (this was
mentioned earlier in Connect4BoardComponent) which draws a box to show
where the user has the mouse on the component.
Finally, the Connect4TableFrame must be filled in to create instances
of the model/view/controller classes.
|
|
Connect4TableFrame.java
package org.jogre.c4tutorial.client;
import org.jogre.client.TableConnectionThread;
import org.jogre.client.awt.JogrePanel;
import org.jogre.client.awt.JogreTableFrame;
import org.jogre.client.awt.GameImages;
// Jogre table frame
public class Connect4TableFrame extends JogreTableFrame {
private Connect4Model model;
private Connect4BoardComponent boardComponent;
private Connect4Controller controller;
// Constructor which passes a client connection thread and a table
public Connect4TableFrame (TableConnectionThread conn) {
super (conn);
// Set icon
this.setIconImage (GameImages.getImage (0));
// Create model, view and register view to model
model = new Connect4Model (); // create model
boardComponent = new Connect4BoardComponent (model); // board
model.addObserver(boardComponent); // board observes model
// Create controller which updates model and controlls the view
controller = new Connect4Controller (model, boardComponent);
controller.setConnection (conn); // connection
boardComponent.setController(controller);
// Add view to a panel and set on table frame
JogrePanel panel = new JogrePanel (); // create a panel
panel.add (boardComponent); // add board to panel
setGamePanel (panel); // add panel to table frame
// Set up MVC classes in super class
setupMVC (model, boardComponent, controller);
}
} |
|
|
The way this is done for Connect4 is identical as the
TicTacToe tutorial. The steps are summarised as follows:
- Create model.
- Create view and pass in the model.
- Ensure that the view observes the model.
- Create a controller and pass in the model and view.
- Register the controller on the view.
- Add view to a panel and set this as the main game panel using the setGamePanel method.
- Finally, call the important setupMVC method.
To test these changes, recompile the classes again and run the JogreServer
server and two clients. Get one of the clients to create a table which
the other client should join. Both players should then sit down and start
a game. If everything was successful then you should see the clients update
themselves but not each other.
|
|
|
|
For the clients to keep in sync requires communication to the server. This
is discussed in the next chapter.
|
|
[ top ] |
|
|
Server communication
|
|
|
As mentioned previously, the MVC part of JOGRE has changed very little so the
previous section was quite similar to the Tic-Tac-Toe tutorial. However,
with regards to communication, alpha 0.2 has changed considerably.
The old server per game
model has disappeared and so has the need for classes to
extend ClientConnectionThread and ServerConnectionThread (e.g. TicTacToe
tutorial required a TicTacToeClientConnectionThread).
All communication is now transmitted as XML (much more powerful than using
flat Strings in alpha 0.1).
In JOGRE Alpha 0.2.3, communication is either a base message or a table message. A table
message always contains a table attribute and is delegated to the specified
table frame automatically. Click here
for the JOGRE communication protocol.
The JogreClientFrame now has a link to the ClientConnectionThread
and the JogreTableFrame has a link to the new TableConnectionThread
class. These classes automatically add table numbers
automatically to communication objects which makes the programmers life easier.
Also, using layers, the messy manual delegation between the ClientConnectionThread, the
JogreClientFrame and the JogreTableFrame is now unnecessary as the
(e.g. the method ticTacToeMove (CommTicTacToeMove move) from the TicTacToeClientFrame
class in tutorial 1).
These are the main differences between 0.1 and 0.2 communication. The following
UML diagram shows a class diagram of the JOGRE communication model.
|
|
|
|
Communication is sent:
-
client -> server - communication is sent using the send ()
methods from ClientConnectionThread and TableConnecionThread
(depending if it is a game message or a table message)
.
- server -> client - communication is received from the server
into the receiveMessages () method on the JogreClientFrame and
JogreTableFrame respectively.
New convience methods for communication also exist in
the JogreController class for
sending / recieving properties (and XML objects). A very easy way to send a property
to all the other players in a Connect 4 game is to use the sendProperty. To do this
update the move method in Connect4Controller and insert a new
receiveProperty method as follows.
|
|
Connect4Controller.java
// Update the model
public void move (int x, int value) {
boardComponent.setCurMousePoint (-1);
// update model and change player turn
model.setData (x, value);
// next player turn.
nextPlayer ();
// send move to other user
sendProperty ("move", x, value);
}
// recieve
public void receiveProperty (String key, int x, int value) {
model.setData (x, value);
}
|
|
|
As you can see, it is extremely easy to communicate now in alpha 0.2. The
sendProperty method sends the String "move", the x board position and the
piece value (yellow / red) to the server. The server then sends this to all
the other clients through the recieveProperty method. This method simply
updates the connect 4 model so the 2 clients are in synch.
To test, restart the server, run 2 clients and create a new game. This time
you can see that each client look the same. However, you notice in the following
screenshot that both "Dave" and "Bob" have 4-in-a-row, but neither of them
have won the game. This is discussed later in the tutorial.
|
|
|
|
[ top ] |
|
|
Model State
|
|
|
Since Alpha 0.2.3 game state is now kept on the server side using server controllers.
This means that other users can now join a game in progress. The server controllers
keep a copy of the model for each table on each game. When a user joins a table where
the game is on progress they must be sent the state of the game as an XML
message. Two methods must now be implemented for the Connect4Model class.
These are XMLElement flatten () (turns state of model into an XML message)
and void setState (XMLElement message) (sets state of model from an XML
message).
The following is the flatten method in connect 4 model.
|
|
Connect4Model.java
// New imports at top of file
import nanoxml.XMLElement;
import org.jogre.common.util.JogreUtils;
// Flatten the state of the model into an XML object.
public XMLElement flatten () {
// Retrieve empty state message from super class
XMLElement state = super.flatten();
// Flatten 2d data to a 1d array
int [] data1D = JogreUtils.convertTo1DArray (data);
// Flatten 1d array into a space delimited string and set attribute
state.setAttribute ("pieces", JogreUtils.valueOf (data1D));
return state;
} |
|
|
From earlier we noticed how the state of a game of connect 4 was stored as a 2-dimensional
integer array. How do we store this as an attribute of an XML message? The first
thing we do is retrieve an empty <model> message from the JogreModel super class.
We then convert the data field (2D integer array) into a 1D integer array using the JogreUtils class.
We then convert this 1D int array into a spaced delimited string and set a "pieces"
attribute in the <model> message. If for example Bob and Dave are
playing a game connect 4 and another user shazza joins the table, the server will
sent shazza the following message (once the connect 4 server controller exists):
<join_table table="1" username="shazza">
<player_list owner="dave" curPlayer="dave">
<player name="shazza" state="viewing" seat="-1"/>
<player name="bob" state="started" seat="1"/>
<player name="dave" state="started" seat="0"/>
</player_list>
<model pieces="-1 -1 -1 -1 -1 0 -1 -1 -1 -1 -1 0 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 1 1" />
</join_table>
The following is the setState which then takes the <model> message
and sets the state of the model accordingly.
|
|
Connect4Model.java
// New import at top of file
import org.jogre.common.TransmissionException;
// Set the state of the model
public void setState (XMLElement message) throws TransmissionException {
// Retrieve "pieces" attribute into String
String pieces = message.getStringAttribute ("pieces");
// Convert space delimited string into an integer array
int [] data1D = JogreUtils.convertToIntArray (pieces);
// Convert from 1D to 2D and set data field
this.data = JogreUtils.convertTo2DArray (data1D, COLS, ROWS);
// If everything is read sucessfully then refresh observers
refreshObservers();
} |
|
|
The setState method does the reverse of the flatten method. It retrieves
the value of the "pieces" attribute into a String, converts this String to
an 1d integer array and finally from a 1d integer array into the data field
of Connect4Model (which is a 2d integer array). The last step is to call
the refreshObservers method which will refresh the Connect4BoardComponent.
However the server must have a copy of the model otherwise none of this would work. This
is discussed in the next section.
|
|
[ top ] |
|
|
Server Controllers
|
|
|
On the client side we use controllers (i.e. Connect4Controller) to controll user input
and to send messages to the server to keep clients in sync. On the server side all standard messages
are parsed using the default server table and game controllers. However a custom server
controller can be installed which will receive all messages send from a particular game.
To create a controller it must be created in the following location:
/jogre/games/<game key>/src/org/jogre/<game key>/server
i.e.
/jogre/games/c4tutorial/src/org/jogre/c4tutorial/server
The class name must also end in ServerController. When the Jogre Server loads up it will scan
the <game key>/classes/org/jogre/<game key>/server folder of each game
that is included in the server.xml file. If it detects a class ending in ServerController
it will try to create an instance of it using reflection. Thus, the Jogre Server loads server controllers
using a plug-in style architecture.
The next step is to create a new class called C4TutorialServerController (we cannot name it
Connect4ServerController as this already exists).
|
|
C4tutorialServerController.java
package org.jogre.c4tutorial.server;
import java.awt.Point;
import org.jogre.common.IGameOver;
import org.jogre.common.JogreModel;
import org.jogre.c4tutorial.client.Connect4Model;
import org.jogre.server.ServerConnectionThread;
import org.jogre.server.ServerController;
// Create chess server controller.
public class C4tutorialServerController extends ServerController {
// Constructor
public C4tutorialServerController (String gameKey) {
super (gameKey);
}
// Override startGame method to create a new model when a new game starts.
public void startGame (int tableNum) {
setModel (tableNum, new Connect4Model ());
}
// Receive property from client and update model
public void receiveProperty (JogreModel model, String key, int x, int y) {
((Connect4Model)model).setData (x, y);
}
// Game over
public void gameOver (ServerConnectionThread conn, int tableNum, int resultType) {
// Fill in later.
}
} |
|
|
NOTE: This will compile OK if you run the ANT script as its classpath points to the server.jar
file which contains server specific classes including ServerController and ServerConnectionThread.
If you are developing using Eclipse you will have to add the server.jar (or a link to the Jogre Server project if
one is created) to the project classpath. Otherwise this file will not compile in Eclipse.
Once the file is compiled, restart Jogre Server again and if everything is OK it should pick up the
controller as follows:
|
|
|
|
Each server controller must implement a one parameter constructor and 2 abstract methods.
The constructor simply takes note of the gameKey. The startGame method is called when
a new game starts at a particular table. In this case a new connect 4 model is initilised on the server.
The gameOver method is called when a client requests that a game is over. This is verified on the
server side to prevent client side hacking. This method is filled in the next section.
The abstract class ServerController also contains a number of adapter methods
which can be overwritten if required. These adapater methods include userResigns, usersAgreeDraw
and a number of receiveProperty and receiveObject methods. On the client side, other clients
were kept in synch via the sendProperty method call and overriding the receiveProperty
adapter method to receive properties. The receiveProperty is similiar on the server side with the additional
JogreModel parameter as the server has to handle a number of different models for each table. When a client
makes a move (and sends a property to other clients) the server controller receives it in the receiveProperty
method. The model is then casted to a Connect4Model and kept in synch with the various clients models
using the setData (x, y) method call.
To ensure that everything is working properly run a Jogre Server, 2 clients (e.g. dave, bob) and start a game.
After a few moves run another user (e.g sharon) and join the game as a viewer. If everything is working the user sharon
should see the state of the game. This is because the server will send sharon the state of the game
in a <join_table> message. The state is created from the flatten() method in the previous section
and the model is updated on the server controller using the setState() method also from the previous section.
The final piece of the puzzle is determining when a game is over which is discussed in the next section.
|
|
[ top ] |
|
|
Game Over
|
|
|
In Connect 4 a game can end with a player winning (getting 4 of his
pieces in row) or end in a draw (no spaces left on the board with neither player winning).
It is quite similar to tic-tac-toe.
|
|
To implement game over checking, two methods were added to
Connect4Model and the checkGameOver
method in the Connect4Controller is implemented.
|
|
Connect4Model.java
// return true if the game is won by this player
public boolean isGameWon (int player) {
for (int x = 0; x < COLS; x++) {
for (int y = 0; y < ROWS; y++) {
// check horizontally, vertically a diagonally
int c1 = 0, c2 = 0, c3 = 0, c4 = 0;
for (int i = 0; i < 4; i++) {
if (x + i < COLS) {
if (data[x+i][y] == player)
c1 ++;
}
if (y + i < ROWS) {
if (data[x][y+i] == player)
c2 ++;
}
if (x + i < COLS && y + i < ROWS) {
if (data[x+i][y+i] == player)
c3 ++;
}
if (x - i >=0 && y + i < ROWS) {
if (data[x-i][y+i] == player)
c4 ++;
}
}
// Has any of these been 4 in a row
if (c1 == 4 || c2 == 4 || c3 == 4 || c4 == 4)
return true;
}
}
return false;
}
// return true if all spaces are taken
public boolean isNoCellsLeft () {
int count = 0;
// check each cell
for (int x = 0; x < COLS; x++) {
for (int y = 0; y < ROWS; y++) {
if (data[x][y] != BLANK)
count++;
}
}
return (count == ROWS * COLS); // if all 9 spaces filled return true
}
|
|
|
The isGameWon method checks the int array in the Connect4Model
horizontally, vertically and diagonally. It returns true if a 4 in a row is
detected. The isNoCellsLeft method does a count of all the
non blank cells in the int array. If its equal to nine it returns true (no empty cells).
|
|
Connect4Controller.java
// Check to see if the game is over or not.
private void checkGameOver () {
// Status is either -1, DRAW or WIN
int status = -1;
if (model.isGameWon (getCurrentPlayerSeatNum()))
status = CommGameOver.WIN;
else if (model.isNoCellsLeft ())
status = CommGameOver.DRAW;
// Create game over object if a win or draw
if (status != -1 && conn != null) {
CommGameOver gameOver = new CommGameOver (status);
conn.send (gameOver);
}
} |
|
|
The checkGameOver method in the Connect4Controller
is updated to check the Connect4Model for a win or a draw.
A status integer is then set and a CommGameOver
communications object is then created and sent to the server.
Previous to alpha 0.2.3 sending a <game_over> message to the
server resulting in a game ending. This would create problems in a real-life environment
as hackers would start sending fake <game_over> messages and
increase their score. Since alpha 0.2.3 when the server receives this message
it runs the abstract method gameOver in the correct server controller class to verify that
the game is actually over (and thus can update scores).
The implementation of this method is as follows:
|
|
C4TutorialServerController.java
// Game over on server controller
public void gameOver (ServerConnectionThread conn, int tableNum, int resultType) {
Connect4Model model = ((Connect4Model)getModel(tableNum));
int player = getSeatNum(conn.getUsername(), tableNum);
// Status is either -1, DRAW or WIN
int status = -1;
if (model.isGameWon(player))
status = IGameOver.WIN;
else if (model.isNoCellsLeft ())
status = IGameOver.DRAW;
// Create game over object if a win or draw
if (status != -1) {
// Update the server data
gameOver (conn, tableNum, conn.getUsername(), status);
}
} |
|
|
When a client informs the server that a game is over the abstract gameOver method is called on the
server controller. The model is retrieved for the specified table (getModel(int table)) and casted to a Connect4Model.
The seat number of the current player is also noted. A status variable is created and a gameover check
is performed. If the game is over a gameOver method is called which takes four parameters
(client connection, client username, status and table number). This method updates the
scores on the server persistent data. Currently this is stored as an XML file in the
/jogre/server/data/xml/users.xml file. The server finally sends a message to each client with the updated
scores.
Note: later versions of JOGRE will store persistent data in a database. Switching to a database
will require changing the server_data attribute in the <configuration>
element in the server.xml file from "xml" to "database".
|
|
|
|
[ top ] |
|
|
Applet Support
|
|
|
Applet support is available in the alpha 0.2.3 release. To run the connect 4 tutorial
as an applet we must create the following file.
|
|
Connect4Applet.java
package org.jogre.c4tutorial.client;
import org.jogre.client.TableConnectionThread;
import org.jogre.client.awt.JogreTableFrame;
import org.jogre.client.awt.JogreClientApplet;
// Connect 4 applet
public class Connect4Applet extends JogreClientApplet {
// Constructor
public Connect4Applet () {
super ();
}
// Return the correct table frame
public JogreTableFrame getJogreTableFrame (TableConnectionThread conn) {
return new Connect4TableFrame (conn);
}
} |
|
|
The Connect4Applet file is very similar to the Connect4ClientFrame we
created earlier. To run the applet we must create a c4tutorial.jar file. This involves running the
ANT script we created at the top of the tutorial (delete the classes folder and run ant all at the command line).
This should create a jogre/games/c4tutorial/c4tutorial.jar file which is about 200KB in size.
To test the applet we can modify jogre/games/applet_test.html applet testing file. We need to
add a link to the c4tutorial.jar to the <select name="game"> html combobox (line 45).
Add the following option to the combobox:
<option value="c4tutorial/c4tutorial.jar org.jogre.c4tutorial.client.Connect4Applet.class">C4Tutorial</option>
To test the applet run a server and open the applet_test.html file using your favourite browser
(e.g. Firefox 1.5 / IE 6.0). Select the C4Tutorial option from the "Game" combobox,
enter a username and click the "Connect" button. You should see something like the following:
|
|
|
|
This brings the Connect4 tutorial to an end. Its aim is to illustrate the
implementation of game using the JOGRE API alpha 0.2.3 using screenshots, UML diagrams
and full source code listings. It should also illustrate the differences
between JOGRE alpha 0.1 and alpha 0.2.3. If you have any comments on this tutorial,
then feel free to contact its author
Bob Marks.
|
|
[ top ] |
|
|