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

 

8 A Multi-Room Game Servers

In a more general game topology there is not a game server for each game. Because by definition a game room only hosts one type of game, in this general scenario the server must manage different game rooms in parallel. An universal game server may even handle different games in different rooms. This last generalization blows up the code of the game server considerably, but a simple approach would be to split the code into individual parts for each game type. To avoid code duplication, classes should be defined that can be used for game of related types. In the following example we construct a game server that is capable to manage only one type of game, namely Tic-Tac-Toe, in several game rooms in parallel. (This scenario is also explained in TcpJLib Example 3.)

In the previous example a game room corresponds to a TcpRelay session. While the game is in progress, three TcpNodes are in that session: The game server (TcpBridge) and two players (TcpAgents). In the following scenario the game server must manage several rooms, each of them with two players. To enable the server to communicate with all players, the game server and all players must be part of the same relay session. Therefore the session ID is fixed and has not to be asked when the game server application starts. When a player application starts, the name of the game room is still requested and sent to the game server as login request. It's now up to the game server to manage the game rooms indentified by this room name. All game specific information must be preserved in a state variable separated from other rooms.

To group game specific variables together, we wrap them into a class RoomState and allow an easy access by declaring them protected.

// RoomState.java

import java.util.ArrayList;

class RoomState
{
  ArrayList<String> players = new ArrayList<String>();
  char[][] gameState = new char[3][3];
  String[] startingGrid = new String[2];
  int restartRequest = 0;
}

The ArrayList players stores the names of the players that entered the room (the player's nickname is asked when the player's application starts). The game state is stored by the game server in a character array gameState. startingGrid stores the start order of the current game because we want to interchange the order if the game is a draw. Finally restartRequest is used to check if both players want to restart the game after it is over. We put the RoomStates of all game rooms in a dynamic structure, a Hashtable<String>, <RoomState> that uses the room name as key and room state as value. This table may be considered like a database table to be used by the game establishment to manage its game rooms.

 

  tictactoe8
 

 

The game establishment with game rooms and a game server
(in reality the players are located anywhere in the world and interlinked via TCP/IP)

 

The game server's pipeRequest() callback must now handle requests from all rooms in parallel. The code executed for a specific room should never block and return quickly to avoid starvation of other rooms. Because the same session ID is used for all players, new players log-in by sending a command REQUEST_GAME_ENTRY and specifying the name of the room. The game server handles this request in the pipeRequest() callback. Thus the code for handling new players is part of the pipeRequest() method (and not of notifyAgentConnection() as in the example before). The code to handle the three cases (room empty, room full, one player waiting) is as follows:

public void pipeRequest(String source, String destination, int[] data)
{
  switch (data[0])
  {
    case Command.REQUEST_GAME_ENTRY:
      String roomName = TcpTools.intAryToString(data, 1);
      c.println("Player " + source + " requests entry in room " + roomName);

      
if (rooms.get(roomName) == null)  // No player in room
      {
        c.println("First player " + source + " entered in room " + roomName);
        roomState = new RoomState();
        rooms.put(roomName, roomState);
        roomState.players.add(source);
        send(bridgeName, source, Command.TEAMMATE_WAIT);
        return;
      }

      roomState = rooms.get(roomName);
      players = roomState.players;

      
if (players.size() == 2) // Room occupied
      {
        c.println("Room: " + roomName + 
                  " -- Too many players. Game in process.");
        send(bridgeName, source, Command.IN_PROGRESS);
        return;
      }

      
if (players.size() == 1) // One player waiting, game starts
      {
        c.println("Room: " + roomName + " -- Second player " + source + 
                  " entered.");
        players.add(source);

        // We transmit names of players
        String player0 = players.get(0);
        String player1 = players.get(1);

        String text =
          TcpTools.stripNickname(player0)
          + ">"
          + TcpTools.stripNickname(player1);
        sendCommand(bridgeName, player0, Command.PLAYER_NAMES,
          TcpTools.stringToIntAry(text));

        text = TcpTools.stripNickname(player1)
          + ">"
          + TcpTools.stripNickname(player0);
        sendCommand(bridgeName, player1, Command.PLAYER_NAMES,
          TcpTools.stringToIntAry(text));

        roomState.startingGrid[0] = player0;
        roomState.startingGrid[1] = player1;
        initGame(roomName, player0, player1);
      }
      break;
      
      ...

When a move is reported, the nickname of the player is received in the source parameter of the pipeRequest() callback. The game server can find out the corresponding room name by examine the hash table:

private String getRoomName(String player)
// Returns the room name, empty string if player is not found in any room
{
  Enumeration e = rooms.keys();
  while (e.hasMoreElements())
  {
    String roomName = (String)e.nextElement();
    ArrayList<String> players = rooms.get(roomName).players;
    for (int i = 0; i < players.size(); i++)
    {
      if (players.get(i).equals(player))
        return roomName;
    }
  }
  return "";
}

The server then retrieves the corresponding room state with the hashtable's get() method. In the same way the teammate of a given player can be determined. (There are no problems with name clashes because the TcpRelay makes the attributed nickname unique by appending (n) trailers). When a player quits the room by terminating the application, he is removed from the players list in the roomState entry. Any teammate is notified by the TEAMMATE_QUIT command. If the waiting player quits, the room is removed too.

private void removePlayer(String agentName)
{
  Enumeration e = rooms.keys();
  while (e.hasMoreElements())
  {
    String roomName = (String)e.nextElement();
    ArrayList<String> players = rooms.get(roomName).players;
    int nb = players.size();

    if (nb == 1)  // One waiting player
    {
      rooms.remove(roomName);
      c.println("Room: " + roomName + " -- Player " + agentName 
        + " and room removed.");
      return;
    }

    if (nb == 2)  // Game in progress
    {
      for (int i = 0; i < nb; i++)
      {
        if (players.get(i).equals(agentName))
        {
          send(bridgeName, players.get((+ 1) % 2), Command.TEAMMATE_QUIT);
          players.remove(agentName);
          c.println("Room: " + roomName + " -- Player " + agentName 
            + " removed.");
          return;
        }
      }
    }
  }
}

The player class is pretty simple because all "intelligence" is ported to the game server. The player just receives commands from the game server in the dataReceived() callback and executes them accordingly. To display a faint guide mark when the cursor passes over the cells, the mouse move, leave and enter events are activated. The action takes place in the mouseEvent() callback. Don't forget to refresh the game grid, because the simulation cycling in not turned on.

public boolean mouseEvent(GGMouse mouse)
{
  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;
}

This concludes the explications of the program code. The complete source code is marginally more complicated and can be downloaded from here. Don't forget to define a session ID that is unique for you.

Execute the player program on the same or different computers with any number of player pairs.
The game server is provided by us.