Aegidius
 Plüss   Aplulogo
     
 www.aplu.ch      Print Text 
© 2021, V10.4
 
  TcpJLib
 
 

 

6 The Class Player

To depose a mark in the game board the player left-clicks into a cell. The main purpose of the Player class is to send this cell position to the game server that sends it further to the teammate instance where the mark is also shown at the same board position. The game server stores all moves and checks if the winning rule is fulfilled or the board is filled up. If this is the case, the game is over and the result is transmitted to the players telling them who wins and who loses or if the game is a draw.

The players receives commands via the callback notification dataReceived(). As stated in the communication protocol the first element of the received integer array designs the command type. Therefore a switch construct is used to handle every command individually. Most of the actions are self-explanatory and contain a status information. As usual in callbacks, global flags (boolean instance variables) are set, to avoid long lasting code.

To show the guide mark when the mouse cursor moves over empty cells, mouse move, leave and enter events are activated by using the appropriate GGMouse mask in the addMouseListener() method. The events are treated in the mouseEvent() callback. To hide the guide mark when the mouse cursor is at an occupied cell or outside the board, it is moved to hideLoc that is located outside the game board.

// Player.java

import ch.aplu.tcp.*;
import ch.aplu.jgamegrid.*;

public class Player extends TcpAgent
  implements GGMouseListener
{
  private static final String VERSION = "1.3  ";
  private static final String nickname = "TicTacToePlayer";
  private volatile boolean isMyTurn = false;
  private volatile boolean isOver = false;
  private char mySign;
  private char teammateSign;
  private Guide guide = null;
  private final Board board = new Board();
  private final Location hideLoc = new Location(-1-1);

  public Player(String sessionID, String bridgeName)
  {
    super(sessionID, bridgeName);
    board.addMouseListener(this, GGMouse.lClick
      | GGMouse.move | GGMouse.leave | GGMouse.enter);
    board.setTitle(nickname + " V" + VERSION);
    addTcpAgentListener(new TcpAgentAdapter()
    {
      public void dataReceived(String source, int[] data)
      {
        switch (data[0])
        {
          case Command.IN_PROGRESS:
            board.setStatusText("Gameroom occupied.\nTerminating now...");
            TcpTools.delay(4000);
            System.exit(0);
            break;
          case Command.TEAMMATE_QUIT:
            board.setStatusText("Teammate left game room.\nWaiting for another teammate...");
            board.clear();
            isMyTurn = false;
            isOver = false;
            break;
          case Command.TEAMMATE_WAIT:
            board.setStatusText("Entered in game room.\nWaiting for teammate...");
            break;
          case Command.MARK_X:
            mySign = 'X';
            teammateSign = 'O';
            guide = new Guide('X');
            break;
          case Command.MARK_O:
            mySign = 'O';
            teammateSign = 'X';
            guide = new Guide('O');
            break;
          case Command.START_FIRST:
            board.clear();
            board.addActor(guide, hideLoc);
            board.setStatusText("Game started.\nClick to put your mark.");
            isOver = false;
            isMyTurn = true;
            break;
          case Command.START_SECOND:
            board.clear();
            board.addActor(guide, hideLoc);
            board.setStatusText("Game started.\nWait for partner's move.");
            isOver = false;
            isMyTurn = false;
            break;
          case Command.MOVE:
            Location loc = new Location(data[1], data[2]);
            board.addActor(new Mark(teammateSign), loc);
            board.setStatusText("It's your move.\nClick to put your mark.");
            isMyTurn = true;
            break;
          case Command.WON:
            isOver = true;
            board.setStatusText("Game over. You won.\nClick in board to restart.");
            break;
          case Command.LOST:
            isOver = true;
            board.setStatusText("Game over. You lost.\nClick in board to restart.");
            break;
          case Command.TIE:
            isOver = true;
            board.setStatusText("Game over. It is a draw.\nClick in board to restart.");
            break;
        }
        if (isOver)
        {
          isMyTurn = false;
          board.addActor(new Actor("sprites/gameover.gif")new Location(11));
        }
      }

      public void notifyBridgeConnection(boolean connected)
      {
        if (!connected)
        {
          board.setStatusText("Game server stopped.\nTerminating now...");
          TcpTools.delay(4000);
          System.exit(0);
        }
      }
    });
    connectToRelay(nickname);
    if (!isBridgeConnected())
    {
      board.setStatusText("Connected, but game server not found.\nTerminating now...");
      TcpTools.delay(4000);
      System.exit(0);
      return;
    }
  }

  public boolean mouseEvent(GGMouse mouse)
  {
    if (isOver && mouse.getEvent() == GGMouse.lClick)
    {
      isOver = false;
      board.clear();
      board.setStatusText("Restart requested.\nWait for partner's approval.");
      send("", Command.RESTART);
      return true;
    }

    if (isMyTurn)
    {
      Location loc = board.toLocation(mouse.getX(), mouse.getY());
      switch (mouse.getEvent())
      {
        case GGMouse.move:
        case GGMouse.enter:
          if (!guide.getLocation().equals(loc))
          {
            if  (board.getOneActorAt(loc, Mark.class) == null)
              guide.setLocation(loc);
            else
              guide.setLocation(hideLoc);
            board.refresh();
          }
          break;
        case GGMouse.leave:
          guide.setLocation(hideLoc);
          board.refresh();
          break;
        case GGMouse.lClick:
          if (board.getOneActorAt(loc, Mark.class) == null)
          {
            send("", Command.MOVE, loc.x, loc.y, mySign == 'O' ? 0 : 1);
            isMyTurn = false;
            board.setStatusText("Game in progress.\nWait for partner's move.");
            board.addActor(new Mark(mySign), loc);
          }
          break;
      }
    }
    return true;
  }
}

Because the graphics and mouse handling are almost cost-free when using the JGameGrid library we have to focus our main programming effort to the game server.

 

7 The Class TicTacToeServer

One of the main duty of the game server is to manage the game room, where players may try to enter (starting their application) or leave (terminating their application) any time. Fortunately with TcpJLib every change of TcpAgents is asynchronously notified via the callback method notifyAgentConnection(). We use an ArrayList players to store the current player's nickname. Because all TcpAgents use the same nickname, we are grateful that the TcpRelay handles name clashes and appends (n) trailers automatically if necessary.

After the two players settled down in the game room, the first move is given to the player that first entered the room and the game starts. Any other player that tries to enter the room is rejected. Data sent from the agent are received in the server's pipeRequest() callback. The source parameter is the nickname of the agent who sent the data. It's either TicTacToePlayer or TicTacToePlayer(1), so it's easy to determine the teammate to whom the server must send the move command. The game server maintains the current state of the game in an array variable gameState that contains 'X', 'O' or space characters according to the board cells. The method getPlayerState() checks if the winning rule is fulfilled. To avoid checking lines, columns and diagonals separately, the game state is converted into a comma separated string pattern. A winning situation is obtained when this string contains a XXX or OOO sequence. As long as this is not the case, null is returned. If the board is full before a winning situation is obtained, the game is a draw.

When the game is over, the players are informed about the situation and each player can request a new game with the same teammate by clicking into the game board. If the game was a draw, the starting order is interchanged, otherwise the loser makes the first move in the next game.

For demonstration and debugging purposes some actions of the game server are logged in a scrolling console window. If you prefer a log file instead, just replace Console.init() by Console.init(path), where path is the relative or fully qualified file name of the log file. To avoid conflicts with other applications handled by the TcpRelay, a unique session ID should be used. Therefore the room name is appended to a randomized constant string, making the session ID (hopefully) unique for this particular game.

// TicTacToeServer.java

import ch.aplu.tcp.*
import java.util.ArrayList;
import ch.aplu.util.Console;
import javax.swing.JOptionPane;

public class TicTacToeServer extends TcpBridge
{
  private static String VERSION = "1.3";
  private static String sessionID = "TicTacToe_67%&%2134";
  private static String id;
  private static final String bridgeName = "TicTacToeServer";
  // players.get(0) has mark 'X', players.get(1) has mark 'O'
  private ArrayList<String> players = new ArrayList<String>();
  private Console c;
  private char[][] gameState = new char[3][3];
  private int startRequest;
  private String[] startingGrid = new String[2];

  static
  {
    id = requestEntry("Game Room ID (>2 chars):");
    sessionID += id;
  }

  public TicTacToeServer()
  {
    super(sessionID, bridgeName);
    c = Console.init();
    c.setTitle("TicTacToeServer Version " + VERSION
      + " Using Game Room ID: " + id);
    addTcpBridgeListener(new TcpBridgeAdapter()
    {
      public void pipeRequest(String source, String destination, int[] data)
      {
        showPipeRequest(source, data);
        String teammate = source.contains("(1)")
          ? TcpTools.stripNickname(source, false) : (source + "(1)");

        switch (data[0])
        {
          case Command.MOVE:
            char value = data[3] == 0 ? 'O' : 'X';
            gameState[data[1]][data[2]] = value;
            send("", teammate, data);
            String[] playerState = getPlayerState();
            if (playerState == null)  // still in progress
            {
              if (isBoardFull())
              {
                c.println("Game over. It's a draw");
                send("", players.get(0), Command.TIE);
                send("", players.get(1), Command.TIE);
                // Exchange starting grid
                String temp = startingGrid[1];
                startingGrid[1] = startingGrid[0];
                startingGrid[0] = temp;
              }
            }
            else
            {
              c.println("Game over. Winner: " + playerState[0] + ". Loser: "
                + playerState[1]);
              send("", playerState[0], Command.WON);
              send("", playerState[1], Command.LOST);
              startingGrid[0] = playerState[1];  // Loser makes first move
              startingGrid[1] = playerState[0];
            }
            break;

          case Command.RESTART:
            startRequest++;
            if (startRequest == 2)
              initGame();
            break;
        }
      }

      public void notifyAgentConnection(String agentName, boolean connected)
      {
        c.println(agentName + (connected ? " connected" : " disconnected"));
        if (connected)
        {
          if (players.size() >= 2)
          {
            send("", agentName, Command.IN_PROGRESS);
            return;
          }

          players.add(agentName);

          if (players.size() == 1)
          {
            send("", players.get(0), Command.TEAMMATE_WAIT);
            c.println("Wait for teammate.");
          }

          if (players.size() == 2)
          {
            // First player logged in will start
            startingGrid[0] = players.get(0);
            startingGrid[1] = players.get(1);
            initGame();  
            c.println("Game started.");
          }
        }
        else  // disconnected
        {
          players.remove(agentName);
          if (players.size() == 1)
          {
            send("", players.get(0), Command.TEAMMATE_QUIT);
            c.println("Teammate abandoned.");
          }
        }
      }
    });

    c.print("Connecting to relay...");
    if (connectToRelay().size() > 1)
    {
      c.println("Connected.\nBut game room ID in use.\nTerminating now...");
      TcpTools.delay(4000);
      System.exit(0);
    }

    c.println("done.\nWaiting for players...");
  }

  private boolean isBoardFull()
  {
    int nb = 0;
    for (int i = 0; i < 3; i++)
      for (int k = 0; k < 3; k++)
        if (gameState[i][k] != ' ')
          nb++;
    return nb == 9;
  }

  private String[] getPlayerState()
  {
    // Convert gameState into string pattern
    StringBuffer pattern = new StringBuffer();
    // Horizontal
    for (int i = 0; i < 3; i++)
    {
      for (int k = 0; k < 3; k++)
      {
        pattern.append(gameState[i][k]);
      }
      pattern.append(',');  // Separator
    }
    // Vertical
    for (int k = 0; k < 3; k++)
    {
      for (int i = 0; i < 3; i++)
      {
        pattern.append(gameState[i][k]);
      }
      pattern.append(',');
    }
    // Diagonal
    for (int i = 0; i < 3; i++)
      pattern.append(gameState[i][i]);
    pattern.append(',');
    for (int i = 0; i < 3; i++)
      pattern.append(gameState[i][2 - i]);

    c.println("Pattern: " + pattern);
    String[] result = new String[2];
    if (pattern.toString().contains("XXX"))
    {
      result[0] = players.get(0);
      result[1] = players.get(1);
      return result;
    }
    if (pattern.toString().contains("OOO"))
    {
      result[0] = players.get(1);
      result[1] = players.get(0);
      return result;
    }
    return null;
  }

  private void initGame()
  {
    for (int i = 0; i < 3; i++)
      for (int k = 0; k < 3; k++)
        gameState[i][k] = ' ';
    startRequest = 0;
    send("", players.get(0), Command.MARK_X);
    send("", players.get(1), Command.MARK_O);
    // Send first to 'second' to make him ready to receive move requests
    send("", startingGrid[1], Command.START_SECOND);
    TcpTools.delay(2000);
    send("", startingGrid[0], Command.START_FIRST)
  }

  private void showPipeRequest(String source, int[] data)
  {
    c.print("pipeRequest from: " + source);
    c.print(". Data: ");
    for (Integer k : data)
      c.print(+ " ");
    c.println();
  }

  private static String requestEntry(String prompt)
  {
    String entry = "";
    while (entry.length() < 3)
    {
      entry = JOptionPane.showInputDialog(null, prompt, "");
      if (entry == null)
        System.exit(0);
    }
    return entry.trim();
  }

  public static void main(String[] args)
  {
    new TicTacToeServer();
  }
}

 

7 The Application Driver Class, Starting the Game

In a clean OO design it is usual (but not absolutely necessary) to separate the application class containing the main method from other classes, because its somewhat weird to consider a player as an application. At the same time some application specific setups can be made in the driver class. Here we ask the user for the name of the game room.

// TicTacToe.java

import javax.swing.JOptionPane;

public class TicTacToe
{
  private static String sessionID = "TicTacToe_67%&%2134";
  private static final String bridgeName = "TicTacToeServer";

  public static void main(String[] args)
  {
    String id = requestEntry("Game Room ID (>2 chars):");
    sessionID += id;
    new Player(sessionID, bridgeName);
  }

  private static String requestEntry(String prompt)
  {
    String entry = "";
    while (entry.length() < 3)
    {
      entry = JOptionPane.showInputDialog(null, prompt, "");
      if (entry == null)
        System.exit(0);
    }
    return entry.trim();
  }
}


Now the development is completed and it's time to play a game.

First start the Game Server (using WebStart).
Then start the player application for each player (using WebStart).

All three applications may run on the same or different computers located anywhere in the world.

If you want to recompile the source, download it from here. You need the following libraries:
JGameGrid
TcpJLib

Aplu Utility Package

(continued in Tutorial 4)