|
Java
has grabbed the undivided attention of Web developers and shows
no sign of disappearing. What does all this Java hype mean for game
developers? It means the potential for creating cross-platform Internet
games that can be played within the context of the familiar Web
page format. In other words, you develop one set of source code
and publish one executable on the Web.
Because
of its power and ease of use, standard networking support in Java
is probably the most compelling reason for game developers to give
Java a closer look. This article explains networking support in
Java and how it can be used to build multiplayer Web games. You'll
see how Java makes it surprisingly easy to tackle many of the network
challenges faced with other languages and development environments.
Is
Java Ready for Games?
If
you shudder thinking about the fact that Java executables are interpreted,
you are probably a game developer who understands the critical issue
of performance in games. Performance is so important in game programming
that languages and development tools are regularly chosen solely
based on their performance. This stands in stark contrast to other
areas of software development, where maintainability and ease of
use generally take precedence over raw performance.
How
does Java fare as a game programming language and run-time environment?
In its traditional interpreted form, Java is pretty weak. In fact,
few commercial games relying heavily on high-performance graphics
would run tolerably in a Java environment. However, a recent technology
has greatly improved this scenario. I'm referring to just-in-time
Java compilers, which eliminate the costly interpretation of generic
Java bytecodes. A just-in-time (JIT) compiler is a back-end compiler
that converts generic Java bytecode executables into native executables.
Because JIT compilers generate native code, Java performance starts
approaching that of natively compiled languages like C and C++.
Even
though JIT compilers greatly improve the performance of Java programs,
it's still hard to pit Java head-to-head with a platform-dependent
program written in C or C++. This is mainly due to the API layering
involved in making the Java API cross-platform. If you've ever been
involved in a media-intensive cross-platform project, you probably
understand how messy it can be. Java, on the other hand, provides
one API and requires you only develop a single set of source code
that can be executed on any Java supported platform. So, when assessing
the performance limitations of Java, you must acknowledge this benefit.
Java
in its current state is still a little on the slow side, which rules
it out as a possibility for high-performance, graphically intensive
games. However, Java is certainly capable of being used for other
types of games, such as multiplayer strategy games. As this article
culminates with the development of a complete Web-based multiplayer
NetConnect4 Java game, you can judge for yourself where Java stands
as a game development environment.
Internet
Fundamentals
Before
we look into the network support Java provides, it's important you
understand some fundamentals about the structure of the Internet
as a network. The only way to allow a wide range of computer systems
to coexist and communicate with each other effectively is to hammer
out some standards. Fortunately, plenty of standards abound for
the Internet, and they share wide support across many different
computer systems. Let's take a look at a few of them.
Addresses.
One of the first areas of standardization on the Internet was in
establishing a means to uniquely identify each connected computer.
The solution that was implemented was IP addresses, which come in
the form of a 32-bit number that looks like this: 243.37.126.82.
You're probably more familiar with the symbolic form of IP addresses,
which looks like this: gdmag.com. Without an addressing scheme,
there would be no way to distinguish among different computers.
Protocols.
Many different types of communication can take place on the Internet,
so there must be an equal number of mechanisms for facilitating
them. Protocols are sets of rules and standards that define certain
types of Internet communication. More specifically, a protocol specifies
the format of data being sent over the Internet, along with how
and when it is sent. On the other end of the communication, the
protocol also defines how the data is received along with its structure
and what it means.
Without
a doubt, the Internet protocol getting the most attention these
days is the hyper-text transfer protocol (HTTP), which is used to
transfer HTML documents on the web. The file transfer protocol (FTP)
is a more general protocol used to transfer files over the Internet.
These two protocols both have their own unique set of rules and
standards defining how information is transferred, and Java provides
support for both of them.
Ports.
Internet protocols make sense only in the context of a service.
For example, the HTTP protocol comes into play when you are providing
Web content (HTML pages) through an HTTP service. Each computer
on the Internet has the capability to provide a variety of services
through the various protocols supported. There is a problem, however--the
type of service must be known before information can be transferred.
This is where ports come in. A port is a software abstraction that
provides a means to differentiate between different services. More
specifically, a port is a 16-bit number identifying the different
services offered by a network server.
Each
computer on the Internet has a bunch of ports that can be assigned
different services. To use a particular service and therefore establish
a line of communication via a particular protocol, you must connect
to the correct port. Ports are numbered, and some of the numbers
are specifically associated with a type of service. Ports with specific
service assignments are known as standard ports, meaning that you
can always count on a particular port corresponding to a certain
service.
For
example, the FTP service is located on port 21, so any other computer
wanting to perform an FTP file transfer would connect to port 21
of the host computer. Likewise, the HTTP service is located on port
80, so any time you access a Web site, you are really connecting
to port 80 of the host using the HTTP protocol behind the scenes.
Figure 1 illustrates the relationship between
ports and protocols.
Figure
1. Services are assigned to specific ports
Protocols
and Ports
All
standard service assignments are given port values below 1024. Ports
above 1024 are considered available for custom communications, such
as those required by a network game implementing its own protocol.
Keep in mind, however, that other types of custom communication
also take place above port 1024, so you might have to try a few
different ports to find an unused one.
One
of Java's major strong suits as a programming language is its wide
range of network support. Java has this advantage because it was
developed with the Internet in mind. The result is that you have
lots of options in regard to network programming in Java. Even though
there are many network options, Java network game programming uses
a particular type of network communication known as sockets. A socket
is a software abstraction for an input or output communication medium.
Java
performs all its low-level network communication through sockets.
Logically, sockets are one step lower than ports; you use sockets
to communicate through a particular port. So a socket is a communication
channel enabling you to transfer data through a certain port. Check
out Figure 2, which shows communication taking
place through multiple sockets on a port.
Figure
2. Socket communication
Socket
Communication
This
figure brings up an interesting point about sockets: data can be
transferred through multiple sockets for a single port. Java provides
socket classes to make programming with sockets much easier. Java
sockets are broken down into two types: stream sockets and datagram
sockets.
A
stream socket, or connected socket, is a socket over which data
can be transmitted continuously. By continuously, I don't necessarily
mean that data is being sent all the time, but that the socket itself
is active and ready for communication all the time. Think of a stream
socket as a dedicated network connection in which a communication
medium is always available for use. The benefit of using a stream
socket is that information can be sent with less worry about when
it will arrive at its destination. Because the communication link
is always "live," data is generally transmitted immediately after
you send it.
Java
supports stream socket programming primarily through two classes:
Socket and ServerSocket. Socket provides the necessary overhead
to facilitate a stream socket client, and ServerSocket provides
the core functionality for a server. Most of the actual code facilitating
communication via sockets is handled through input and output streams
connected to a socket. In this way, the communication itself is
handled independently of the network socket connection. This might
not seem like a big deal at first, but it is crucial in the design
of the socket classes; after you've created a socket, you connect
an input or output stream to it and forget about the socket.
The
other type of socket supported by Java is the datagram socket. Unlike
stream sockets, in which the communication is akin to a live network,
a datagram socket is more akin to a dial-up network, in which the
communication link isn't continuously active. A datagram socket
is a socket over which data is bundled into packets and sent without
requiring a live connection to the destination computer.
Because
of the nature of the communication medium involved, datagram sockets
aren't guaranteed to transmit information at a particular time or
even in any particular order. The reason datagram sockets perform
this way is that they don't require an actual connection to another
computer; the address of the target computer is just bundled with
the information being sent. This bundle is then sent out over the
Internet, leaving the sender to hope for the best.
On
the receiving end, the bundles of information can be received in
any order and at any time. For this reason, datagrams also include
a sequence number that specifies which piece of the puzzle each
bundle corresponds to. The receiver waits to receive the entire
sequence, then puts them back together to form a complete information
transfer. As you might think, datagram sockets are less than ideal
for network game programming because of the implied time delays,
heightened reliability issues, and sequencing complexities.
A
Reusable Socket Class
You've
now learned enough about network theory and the Java networking
support to write some code. Before you can think in terms of writing
network game code, however, you need to develop some code that helps
facilitate the core communications necessary for a game. In doing
so, you'll have reliable, reusable code that can easily be applied
to provide functionality specific to a particular game communication
protocol.
The
first layer of code necessary to facilitate network communications
comes in the form of a socket helper class that handles the details
of initializing a socket and managing the associated data streams.
The SocketAction class was developed by Greg Turner to help ease
the pain in establishing a communication channel using Java sockets.
The SocketAction class is derived from the standard Thread class,
so it has its own thread of execution. Let's start by looking at
the member variables of SocketAction, which is shown in Listing
1.
The
first two members, inStream and outStream, are the input and output
streams used to receive and send data through the socket. The third
member variable, socket, is the socket object. The constructor for
SocketAction takes a Socket object as its only parameter, as shown
in Listing 2.
The
constructor creates the buffered data streams and initializes the
socket member variable with the Socket object passed in. If there
was a problem in initializing the streams, the constructor detects
it by using the catch clause. If there is an error in creating the
streams, something is seriously wrong, which explains why the entire
program exits.
The
send and receive methods are possibly the most useful methods in
SocketAction, even though they contain very little code, as shown
in Listing 3. The send method simply sends
a string out over the socket connection by using the output data
stream. Similarly, the receive method receives a string by using
the input data stream.
The
closeConnections method simply closes the socket, as shown in Listing
4. The isConnected method verifies that the input and output
streams as well as the socket object are valid, as shown in Listing
5. Finally, the finalize method closes the socket as an added
safety precaution, as shown in Listing 6.
And
that's all there is to the SocketAction class, which is pretty elementary.
Nevertheless, its simple function of providing a clean management
class for sockets and their associated streams will make life much
easier when building network Java games.
The
core of Java network game programming revolves around a client/server
communication strategy. In fact, the design of the NetConnect4 game
can be divided cleanly into the client side and the server side.
These two components are logically separate, communicating entirely
through a game protocol defined between them. Let's take a look
at what each of these pieces is responsible for.
The
NetConnect4 Server
In
any Java game, the server side of the game acts almost like a referee,
managing the different players and helping them communicate effectively.
More specifically, a game server takes on the role of handling the
network connection for each player, along with querying for and
responding to events for the players. The role of a generic game
server can be broken down into the following actions:
- Initialize
the server socket
- Wait
for a client to connect
- Accept
the client connection
- Create
a daemon thread to support the client
- Go
back to step 2.
The
most crucial aspect of this series is Step 4, when the server creates
a daemon thread to support the client. You're probably wondering
what I mean by "support." In a generic sense, I don't know what
I mean. The reason is that the daemon thread is where the applet-specific
code goes. So a generic server only knows that it needs to create
a daemon thread; it doesn't know or care about what that thread
actually does. You'll learn more about daemon threads a little later
when you actually get into the code for NetConnect4.
You
now have an idea about the role a generic game server plays in the
context of a network game. The question is what role does such a
server play in the context of a specific game, namely NetConnect4?
The role of the NetConnect4 server ends up being not much different
from that of the generic server, but it is important that you understand
exactly what it needs to do differently.
Because
NetConnect4 is a two-player game, the first job of the server is
to pair up players (clients) as they connect to the game. A more
limited approach would be to permit only the first two players who
connect to play the game. But you're more thorough than that and
demand a more powerful game server. Your game server enables multiple
games to be played at once, simply by pairing additional client
players together for each game. In this way, a typical NetConnect4
server session might have six or eight players playing at once.
Of course, the players know only about the other player in their
immediate game. To keep things a little simpler, don't worry about
players choosing who they play against, just pair players on a first-come
first-served basis.
After
the server has detected two players and paired them up for a game,
it becomes the responsibility of the server's daemon thread to dictate
the flow of the game between the players. The daemon accomplishes
this by informing each player of the state of the game, while also
modifying the state according to each player's turn. The responsibilities
of the NetConnect4 server and daemon can be summarized as follows:
- Accept
client player connections
- Pair
up players to form separate games
- Manage
the flow of the game
- Communicate
each player's move to the other player
- Notify
the players of the state of the game
Figure
3. Starting up NetConnect4 in a wait state.
The
Client
The
other side of a Java network game is the client. The client portion
of a network game corresponds to the applet being run by each player.
Because game players interact with the client, the client program
is usually much fancier than the server in regard to how information
is displayed. As a matter of fact, game servers typically don't
even have user interfaces; they crank away entirely behind-the-scenes
doing all the dirty work while the client applets dazzle the users.
The
basic responsibility of a game client is to connect to the server
and communicate the user's actions, along with receiving game state
information from the server and updating itself accordingly. Of
course, along with this comes the responsibility of displaying the
game graphics and managing the entire game interface for the user.
You can probably already see that game clients tend to require the
most work.
Since
the graphics, user interface, and game playing strategy aspects
of NetConnect4 don't really impact the network code, I'll leave
it for you to study on your own. You can check out the complete
source code for the game by downloading it.
Following
is a summary of what network functionality the NetConnect4 client
needs to provide:
- Connect
to the server
- Notify
the player of the connection and game state
- Communicate
the player's move to the server
- Receive
the other player's move from the server
- Update
the game with the state received from the server
Figure
4. Pairing up players and starting the game.
Pairing
the Client and Server
You
might be wondering how this whole client/server game scenario works
in regard to a Web server because it's apparent that the game server
must be running at all times. For a network game to work, you must
have the game server always running in the background. It must somehow
be launched by the Web server or by some type of system startup
feature making it available to connect clients who come along wanting
to play.
When
a Web client shows up to play a game, the game server accepts the
client's connection and takes on the role of hooking the client
up with another client to play a game. The game server is entirely
responsible for detecting when clients arrive as well as when they
leave, creating and canceling game sessions along the way. Because
the game server is being run in the background all the time, it
must be extremely robust. Also, since the game server is responsible
for detecting and pairing clients, it is imperative that the server
be running at all times. Without the server, you have no knowledge
of or communication between clients.
Figure
5. Winning and losing scenarios
Running
NetConnect4
The
NetConnect4 sample applet demonstrates all the details of using
Internet network communication to develop a multiplayer Java game.
Even though the focus here is on the actual programming involved
in making NetConnect4 a reality, you'll probably find the code a
little easier to follow if you run the finished product first. Knowing
that, let's put NetConnect4 through its paces and see how to use
it.
As
you already know, the NetConnect4 game is composed of two parts:
a client and a server. The NetConnect4 server is the core of the
game and must be running for the clients to work. To get the game
running, you must first run the server by using the Java interpreter
(java); you do this from a command line:
java NetConnect4Server
The
other half of NetConnect4 is the client, which is an applet that
runs from within a Java-enabled browser, like Netscape Navigator
3.0 or Internet Explorer 3.0. After you have the server up and running,
fire up a browser and load the HTML document containing the NetConnect4
client applet. After running the NetConnect4 client applet, you
should see something similar to what's shown in Figure
3.
At
this point, you have the server up and running with a single client
attached to it. Because two players are required to start a game,
the client is in a wait state until another player comes along.
Now, load a second instance of the Web browser with the same HTML
document; this is your second player. When the server detects this
player, it pairs the two players and starts the game. Figure
4 shows this scenario.
By
switching between the Web browsers, you can simulate a network game
between the two players. Go ahead and outwit yourself so that you
can see what happens when one player wins. This situation is shown
in Figure 5.
For
another game to start between the same two players, each player
just clicks once in the applet window. You can see now how two players
interact together in a game of NetConnect4. Now, if you really want
to put the server to the test, try loading two more instances of
the Web browser and starting another game between two new players.
In this scenario, you have a total of four players involved in two
separate games, all running off the same server. The game server
supports an unlimited number of players and games, although at some
point it might be wise to impose a limit so that performance doesn't
start dragging. A couple of hundred players banging away at your
game server might tend to slow things down!
The
client/server nature of NetConnect4 doesn't just apply at the conceptual
level, it also plays a role in how the code is laid out for the
game. Because the client and server components function as separate
programs, it makes sense to develop the code for them as two different
efforts. With that in mind, let's tackle each part separately.
The
Server
The
NetConnect4 server is composed of four classes:
- Connect4Server
- Connect4Daemon
- Connect4Player
- Game
The
Connect4Server class
serves as a stub program to get the server started. The source code
for it is shown in Listing 7. As you can see,
the Connect4Server
class contains only one method, main, which prints a message and
creates a Connect4Daemon
object. The Connect4Daemon
class is where the server is actually created and initialized. The
Connect4Daemon class
is responsible for creating the server socket and handling client
connections. The run method in Connect4Daemon,
where the details of connecting clients are handled, is shown in
Listing 8.
The
run method first
retrieves the socket for a connecting client via a call to the ServerSocket
class's accept method. The accept method waits until a client connects
and then returns a socket for the client. After a client connects,
a Connect4Player
object is created using the client socket.
The
waitForGame method
is where players are paired up with each other. Listing 9 contains
the source code for the waitForGame
method.
The
waitForGame method
is called from within the Connect4Player
class, which you'll learn about in a moment. waitForGame
is passed a Connect4Player
object as its only parameter. If no player is waiting to play, this
player is flagged as a waiting player, and a loop is entered that
waits until another player connects. A null Game
object is then returned to indicate that only one player is present.
When another player connects and waitForGame
is called, things happen a little differently. Because a player
is now waiting, a Game
object is created using the two players. This Game
object is then returned to indicate that the game is ready to begin.
The
Connect4Daemon class
makes a few references to the Connect4Player
class, which logically represents a player in the game. Listing
10 contains the source code for the Connect4Player
class.
The
Connect4Player class
represents a player from the server's perspective. Connect4Player
is derived from SocketAction,
which is the generic socket class you developed earlier; I told
you it would come in handy! The only member variable defined in
Connect4Player is
daemon, which holds the Connect4Daemon
object associated with the player.
The
run method for Connect4Player
calls back to the daemon's waitForGame
method to get a Game
object for the player. The playGame
method is then called on the Game
object to get the game underway. The closeConnections
method closes the client connection and is typically used to end
the game.
The
last class the NetConnect4 server contains is the Game
class, which handles the details associated with managing the game
logic and the communication between players. The Game
class takes on the bulk of the work involved in maintaining the
state of the game as well as communicating that state between the
players. The Game class contains a group of member constants that
define the different states in the game:
public
static final int ERROR = -1;
public static final int IWON = -2;
public static final int IQUIT = -3;
public static final int ITIED = -4;
public static final int YOURTURN = -5;
public static final int SENTSTRING = -6;
Along
with the constants, the Game
class has member variables representing each player, along with
an event queue for each player and a string used to send messages
to the other player. An event queue is a list of events that take
place within a particular context. In the case of NetConnect4, an
event consists of player moves and related game states, in which
case the event queue is used to keep up with the latest player moves
and game states.
The
workhorse method in the Game class is playGame,
which essentially manages the game flow and logic for each player.
Listing 11 contains the source code for the
playGame method.
The
logic used in playGame
is fairly simple in that it models the way a game of NetConnect4
takes place; basically, each player waits while the other takes
his or her turn. The only potentially confusing aspect of playGame
is the mechanism it uses to communicate between the players. Each
player has an event queue, which contains game information sent
by the other player. The players communicate with each other in
an indirect fashion by using the event queue. The state of the game
is encoded into event messages using the state constants, along
with strings. The playGame
method interprets this information for each player.
The
getStatus method
gets the status of the game for the player passed in the me parameter.
Listing 12 contains the source code for the
getStatus method.
The getStatus method
waits until the player's event queue contains status information,
grabs the information, and returns it. The sendStatus
method is the complement of getStatus;
it's used to update a player's event queue with status information,
as shown in Listing 13.
That
sums up the code for the server. At this point, you have half a
game. Too bad you can't do much with it yet; you still need a client.
Knowing that, let's take a look at the code involved in making the
client side work.
The
Client
The
client side of NetConnect4 consists of four classes, two of which
are unrelated to networking:
- Connect4State
- Connect4Engine
- Connect4
- Connect4ClientConnection
The
first two classes, Connect4State
and Connect4Engine,
handle the underlying Connect4 game logic and are totally unrelated
to the networking aspect of the game. More specifically, they establish
the rules of the game and determine whether the game has been won,
lost, or tied. The Connect4
applet class primarily handles the graphics, sound, and user interface
for the game. However, it also handles establishing a server connection
for new clients.
The
Connect4ClientConnection
class is in charge of managing the client socket and ensuring that
information is sent back and forth to the server correctly. Connect4ClientConnection
is derived from SocketAction,
which is another good example of code reuse. The constructor for
Connect4ClientConnection takes an Applet object as its only parameter,
as shown in Listing 14.
The
Connect4ClientConnection
constructor creates a socket connection based on the applet parameter
and a port number. This port number must match the port number used
by the server. If the port numbers for the client and server don't
match, none of the socket communication will take place, and the
game won't run.
The
getTheirMove method
in Connect4ClientConnection
is used to get the other player's move, so the client game can be
updated. Listing 15 contains the source code
for the getTheirMove
method.
The
getTheirMove method
basically just receives a string from the server and resolves it
down to an integer, which is then returned. The integer it receives
is a game state constant as defined in Connect4ClientConnection.
Following are the game state constants defined in Connect4ClientConnection:
static
final int ERROR = -1;
static final int PLSWAIT = -2;
static final int YOURTURN = -3;
static final int THEIRTURN = -4;
static final int THEYWON = -5;
static final int THEYQUIT = -6;
static final int THEYTIED = -7;
static final int GAMEOVER = -8;
Although
these game state constants are similar in function to the ones defined
on the server side in the Game
class, keep in mind that they are client-specific and make sense
only in the context of the client. The constants are all negative,
which is based on the fact that the integer state constant is also
used to convey the location of a player's move; all moves are in
the range 0 through 6, which corresponds to the column a piece is
being dropped in.
The
getStatus method
resolves a string status message into an integer game state constant.
Listing 16 contains the source code for getStatus.
The getStatus method
is used by getTheirMove to convert incoming text messages to their
integer equivalent. The sendMove method is pretty straightforward;
it simply sends the player's move to the server:
public void sendMove(int col) {
s = (new Integer(col)).toString();
send(s);
}
Likewise,
the sendIQUIT, sendIWON,
and sendITIED methods
are used to send the corresponding messages IQUIT,
IWON, and ITIED
to the server:
public void sendIQUIT() {
send("IQUIT");
}
public void sendIWON() {
send("IWON");
}
public void sendITIED() {
send("ITIED");
}
Wrapping
It Up
That
wraps up the client side of NetConnect4 and ultimately the entire
NetConnect4 game. If you're still a little dizzy from all the code,
feel free to go through it again and study the details until you
feel comfortable with everything. Even though there is a fair amount
of code, keep in mind that you just developed a multiplayer game
that can be played over the Web on any platform that supports Java.
In
other words, someone using a Sun workstation running the Solaris
operating system can be playing a game of NetConnect4 with someone
on the other side of the world using a Macintosh, and all through
the familiar interface of the Web. That's a scenario hard to match
with any other programming environment!
Code
Listings
|
Listing
1. Member Variables of SocketAction
|
private DataInputStream inStream = null;
protected PrintStream outStream = null;
private Socket socket = null;
|
|
Listing
2. The Constructor for SocketAction
|
public SocketAction(Socket sock) {
super("SocketAction");
try {
inStream = new DataInputStream(new
BufferedInputStream(sock.getInputStream(), 1024));
outStream = new PrintStream(new
BufferedOutputStream(sock.getOutputStream(), 1024), true);
socket = sock;
}
catch (IOException e) {
System.out.println("Couldnt initialize SocketAction:" + e);
System.exit(1);
}
}
|
|
Listing
3. Send and Recieve Methods
|
public void send(String s) {
outStream.println(s);
}
public String receive() throws IOException {
return inStream.readLine();
}
|
|
Listing
4. The closeConnections Method
|
public void closeConnections() {
try {
socket.close();
socket = null;
}
catch (IOException e) {
System.out.println("Couldnt close socket:" + e);
}
}
|
|
Listing
5. The isConnected Method
|
public boolean isConnected() {
return ((inStream != null) && (outStream != null) && (socket != null));
}
|
|
Listing
6. The finalize Method
|
protected void finalize () {
if (socket != null) {
try {
socket.close();
}
catch (IOException e) {
System.out.println("Couldnt close socket:" + e);
}
socket = null;
}
}
|
|
Listing
7. The Connect4Server Class
|
class Connect4Server {
public static void main(String args[ ]) {
System.out.println("NetConnect4 server up and running...");
new Connect4Daemon().start();
}
}
|
|
Listing
8. The run Method in Connect4Daemon
|
public void run() {
Socket clientSocket;
while (true) {
if (port == null) {
System.out.println("Sorry, the port disappeared.");
System.exit(1);
}
try {
clientSocket = port.accept();
new Connect4Player(this, clientSocket).start();
}
catch (IOException e) {
System.out.println("Couldnt connect player:" + e);
System.exit(1);
}
}
}
|
|
Listing
9. The Connect4Daemon Classs waitForGame Method
|
public synchronized Game waitForGame(Connect4Player p) {
Game retval = null;
if (playerWaiting == null) {
playerWaiting = p;
thisGame = null; // just in case!
p.send("PLSWAIT");
while (playerWaiting != null) {
try {
wait();
}
catch (InterruptedException e) {
System.out.println("Error:" + e);
}
}
return thisGame;
}
else {
thisGame = new Game(playerWaiting, p);
retval = thisGame;
playerWaiting = null;
notify();
return retval;
}
}
|
|
Listing
10. The Connect4Player Class
|
class Connect4Player extends SocketAction {
private Connect4Daemon daemon = null;
public Connect4Player(Connect4Daemon server, Socket sock) {
super(sock);
daemon = server;
}
public void run() {
daemon.waitForGame(this).playGame(this);
}
public void closeConnections() {
super.closeConnections();
if (outStream != null) {
send("GAMEOVER");
}
}
}
|
|
Listing
11. The Game Classs playGame Method
|
public void playGame(Connect4Player me) {
String instr;
boolean playgame = true;
boolean theirturn = false;
try {
if (me == player2) {
theirturn = true;
}
else if (me != player1) {
System.out.println("Illegal call to playGame!");
return;
}
while (playgame) {
if (!theirturn) {
me.send("YOURTURN");
instr = me.receive();
instr = instr.toUpperCase();
instr = instr.trim();
if (instr.startsWith("IQUIT")) {
sendStatus(me, IQUIT);
playgame = false;
}
else if (instr.startsWith("IWON")) {
sentString = me.receive();
sentString = sentString.toUpperCase();
sentString = sentString.trim();
sendStatus(me, IWON);
sendStatus(me, SENTSTRING);
playgame = false;
}
else if (instr.startsWith("ITIED")) {
sentString = me.receive();
sentString = sentString.toUpperCase();
sentString = sentString.trim();
sendStatus(me, ITIED);
sendStatus(me, SENTSTRING);
}
else {
sentString = instr;
sendStatus(me, SENTSTRING);
}
}
else {
theirturn = false;
}
if (playgame) {
me.send("THEIRTURN");
int stat = getStatus(me);
if (stat == IWON) {
me.send("THEYWON");
if (getStatus(me) != SENTSTRING) {
System.out.println("Received Bad Status");
me.closeConnections();
}
me.send(sentString);
playgame = false;
}
else if (stat == ITIED) {
me.send("THEYTIED");
if (getStatus(me) != SENTSTRING) {
System.out.println("Received Bad Status");
me.closeConnections();
}
me.send(sentString);
playgame = false;
}
else if (stat == IQUIT) {
me.send("THEYQUIT");
playgame = false;
}
else if (stat == SENTSTRING) {
me.send(sentString);
}
else if (stat == ERROR) {
me.send("ERROR");
me.closeConnections();
playgame = false;
}
else {
System.out.println("Received Bad Status");
sendStatus(me,ERROR);
me.closeConnections();
playgame = false;
}
}
}
me.closeConnections();
return;
}
catch (IOException e) {
System.out.println("I/O Error:" + e);
System.exit(1);
}
}
|
|
Listing
12. The Game Classs getStatus Method
|
private synchronized int getStatus(Connect4Player me) {
Vector ourVector = ((me == player1) ? p1Queue : p2Queue);
while (ourVector.isEmpty()) {
try {
wait();
}
catch (InterruptedException e) {
System.out.println("Error:" + e);
}
}
try {
Integer retval = (Integer)(ourVector.firstElement());
try {
ourVector.removeElementAt(0);
}
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out of bounds:" + e);
System.exit(1);
}
return retval.intValue();
}
catch (NoSuchElementException e) {
System.out.println("Couldnt get first element:" + e);
System.exit(1);
return 0; // never reached, just there to appease compiler
}
}
|
|
Listing
13. The sendStatus Method
|
private synchronized void sendStatus(Connect4Player me, int message) {
Vector theirVector = ((me == player1) ? p2Queue : p1Queue);
theirVector.addElement(new Integer(message));
notify();
}
|
|
Listing
14. The Constructor for Connect4ClientConnection
|
Connect4ClientConnection(Applet a) throws IOException {
super(new Socket(a.getCodeBase().getHost(), PORTNUM));
}
|
|
Listing
15. The Connect4ClientConnection Classs getTheirMove
Method
|
public int getTheirMove() {
// Make sure were still connected
if (!isConnected())
throw new NullPointerException("Attempted to read closed socket!");
try {
String s = receive();
System.out.println("Received:" + s);
if (s == null)
return GAMEOVER;
s = s.trim();
try {
return (new Integer(s)).intValue();
}
catch (NumberFormatException e) {
// It was probably a status report error
return getStatus(s);
}
}
catch (IOException e) {
System.out.println("I/O Error:" + e);
System.exit(1);
return 0;
}
}
|
|
Listing
16. The Connect4ClientConnection Classs getStatus Method
|
private int getStatus(String s) {
s = s.trim();
if (s.startsWith("PLSWAIT"))
return PLSWAIT;
if (s.startsWith("THEIRTURN"))
return THEIRTURN;
if (s.startsWith("YOURTURN"))
return YOURTURN;
if (s.startsWith("THEYWON"))
return THEYWON;
if (s.startsWith("THEYQUIT"))
return THEYQUIT;
if (s.startsWith("THEYTIED"))
return THEYTIED;
if (s.startsWith("GAMEOVER"))
return GAMEOVER;
// Something has gone horribly wrong!
System.out.println("received invalid status from server:" + s);
return ERROR;
}
|
Michael
Morrison is the author of Teach Yourself Internet Game Programming
with Java in 21 Days. You can reach Michael via his Web site
at www.thetribe.com.
|