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

 

9 An Embedded Game Server

9.1 The connecting phase

To avoid the complexity of a game server that manages multiple game rooms, the game server may be part of the player application and connects as TcpBridge to the TcpRelay when the player application is launched using the same session id as the player (the "room name"). After connection the bridge detects as first task if another instance is already present with the same session id and disconnects immediately if this is the case. Then the client application starts a TcpAgent as game client.

In the following example we demonstrate the connecting phase. We construct two classes, the player class Buddy and the embedded server class BuddyServer. When the Buddy instance initializes, it request a room and a player name (set to defaults if debug = true). A server console windows is shown using the convenient ch.aplu.util.Console instance that redirects System.out. A TcpBridge instance is created and the server connects to the relay. The returned connection list is checked to see if another server instance with the same session id is already connected. In this case the new instance is disconnected immediately. The notifyAgentConnection() callback is used to add/remove the player names in a player list. As long as the number of players is not reached, the server only registers the new players. When the game room is full, the server sends the player names to all players and the game may start. (The console window close button only hides the console window, but does not terminate the application).

Here comes the code for the server class:

import ch.aplu.tcp.*;
import java.util.*;
import ch.aplu.util.*;
import java.awt.*;

public class BuddyServer extends TcpBridge implements TcpBridgeListener
{
  private ArrayList<String> playerList = new ArrayList<String>();
  private String serverName;

  public BuddyServer(String sessionID, String gameRoom, String serverName)
  {
    super(sessionID, serverName);
    this.serverName = serverName;
    addTcpBridgeListener(this);
    ArrayList<String> connectList = connectToRelay(6000);
    if (connectList.isEmpty())
    {
      ch.aplu.util.Console console = new ch.aplu.util.Console();
      System.out.println("Connection to relay failed");
      return;
    }
    if (!connectList.get(0).equals(serverName))
    {
      System.out.println("A server instance is already running.");
      disconnect();
    }
    else
    {
      final ch.aplu.util.Console console =
        new ch.aplu.util.Console(new Position(00),
        new Size(300400)new Font("Courier.PLAIN"1010));
      console.addExitListener(new ExitListener()
      {
        public void notifyExit()
        {
          console.end();
        }
      });
      System.out.println("Server in game room '" + gameRoom + "' running.");
    }
  }

  public void notifyRelayConnection(boolean connected)
  {
  }

  public void notifyAgentConnection(String agentName, boolean connected)
  {
    if (!connected && playerList.contains(agentName))
    {
      System.out.println("Player: " + agentName + " disconnected");
      if (playerList.contains(agentName))
        sendCommandToGroup(Buddy.Command.DISCONNECT,
          TcpTools.stringToIntAry(agentName));
      playerList.remove(agentName);
    }
    else
    {
      if (playerList.size() >= Buddy.NB_PLAYERS)
        return;
      
      System.out.println("Player: " + agentName + " connected");
      playerList.add(agentName);
      if (playerList.size() == Buddy.NB_PLAYERS)
      {
        String names = "";
        for (int i = 0; i < playerList.size(); i++)
        {
          names += playerList.get(i);
          if (< playerList.size() - 1)
            names += ',';
        }
        int[] data = TcpTools.stringToIntAry(names);
        sendCommandToGroup(Buddy.Command.REPORT_NAMES, data);
      }
    }
  }

  public void pipeRequest(String source, String destination, int[] indata)
  {
  }
 
  private void sendCommandToGroup(int command, int... data)
  {
    for (String nickname : playerList)
      sendCommand(serverName, nickname, command, data);
  }
}

The application class Buddy is in a has-a relation with the BuddyServer and creates a server instance after asking for the room and player name. The TcpAgent connect list is checked for three situations:

  • The connection to the TcpRelay fails
  • The maximum number of players reached (room full)
  • We are waiting for more players to join the game

Then the application loops until the server transmitted the player names. Here comes the code for the application class. You may change NB_PLAYERS and start the corresponding number of application instances.

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

public class Buddy
{
  protected static final int NB_PLAYERS = 3;
  private final boolean debug = true;
  private BuddyServer buddyServer;
  private final String serverName = "BuddyServer";
  private String sessionID = "7%=iaMM7as%*/&)";
  private ModelessOptionPane mop;
  private TcpAgent agent;
  private String[] playerNames = null;
  private String currentPlayerName;

  // Protocol tags
  public static interface Command
  {
    int REPORT_NAMES = 0;
    int DISCONNECT = 1;
  }

  private class MyTcpAgentAdapter extends TcpAgentAdapter
  {
    public void dataReceived(String source, int[] data)
    {
      switch (data[0])
      {
        case Command.REPORT_NAMES:
          playerNames = TcpTools.split(TcpTools.intAryToString(data, 1)",");
          break;

        case Command.DISCONNECT:
          String player = TcpTools.intAryToString(data, 1);
          agent.disconnect();
          buddyServer.disconnect();
          mop.setText(player + " disconnected. Game stopped.");
          break;
      }
    }

    public void notifyBridgeConnection(boolean connected)
    {
      if (!connected)
      {
        mop.setText("Buddy with game server disconnected. Game stopped.");
        agent.disconnect();
      }
    }
  }

  public Buddy()
  {
    String gameRoom = debug ? "123"
      : requestEntry("Enter a unique room name (ASCII 3..15 chars):");
    sessionID = sessionID + gameRoom;
    agent = new TcpAgent(sessionID, serverName);
    String requestedName = debug ? "max"
      : requestEntry("Enter your name (ASCII 3..15 chars):");
    agent.addTcpAgentListener(new MyTcpAgentAdapter());
    connect(gameRoom, requestedName);
  }

  private void connect(String gameRoom, String requestedName)
  {
    mop = new ModelessOptionPane("Trying to connect to relay...");
    mop.setTitle("Game Room: " + gameRoom);
    buddyServer = new BuddyServer(sessionID, gameRoom, serverName);

    
TcpTools.delay(2000); // Let server come-up

    ArrayList<String> connectList = agent.connectToRelay(requestedName, 6000);
    if (connectList.isEmpty())
    {
      mop.setText("Connection to relay failed. Terminating now...");
      TcpTools.delay(3000);
      System.exit(0);
    }

    int nb = connectList.size();
    currentPlayerName = connectList.get(0);
    if (nb > NB_PLAYERS)
    {
      mop.setText("Game room fill. Terminating now...");
      CardTable.delay(3000);
      System.exit(0);
    }

    if (nb < NB_PLAYERS)
      mop.setText("Connection established. Name: " + currentPlayerName
        + "\nCurrently " + nb + " player(s) in game room."
        + "\nWaiting for " + (NB_PLAYERS - nb) + " more player(s)...");

    while (playerNames == null)  // Wait until player names are reported
      TcpTools.delay(10);

    mop.setTitle("Current player: " + currentPlayerName);
    String participants = "";
    for (int i = 0; i < playerNames.length; i++)
      participants += playerNames[i] + "; ";
    mop.setText("Game ready to start. Participants:\n" + participants);
  }

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

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

Execute the program for each player on the same or different computers using WebStart.

9.2 The startup phase

During the game startup, information between the players and the server are exchanged. This guarantees that the player programs are always in a well-defined state when they receive information from the server. After the connecting phase, typically the following steps are run through:

  • All players initialize the game environment (game window, etc).
  • They send a ready_to_play message to the server
  • After this messages is received from each player, the server sends a start_configuration message containing the information for the start state (playing cards after dealing out, etc.)
  • All players report to the server when the start configuration is received and established
  • The server sends a message to say which player have the first move. Only the starting player gains access to mouse/keyboard actions. All other players must wait

9.3 The playing phase

Now the game is in progress, the current game state information of all players must be stored and updated. It is a game design decision, which information is kept within the player application and which information is "known" to the server, but our experience shows that it is often better to keep the current game state information duplicated at each player application to avoid extensive information exchange. When a player makes a move, the move information is transmitted to the game server that does nothing else than tranferring the information to every player application where the state is updated. Of course for the player the information about other players is hidden (internal to the player application).

During the game, typically the following cycle is run through:

  • The player reports the turn information to the server
  • The server transfers the turn information to the waiting player. They update the game state accordingly
  • All players send a ready message to the server
  • The server designates the next turn to one of the players

The game rules are "known" to all players and it is the player application that checks if the player's move is according to the rules. Because all state information is stored within the player application, each player can check if the game is over. It then transmits this information to the server that must handle the game over situation (establish a score, re-intialize a new round or a new game).

(For some games it may be better to keep all game state information and the game rules at the server, and the player applications only transmit their moves.)

In the following example we use a game design where game information is kept locally. It uses the JCardGame library, an addon to the JGameGrid library (see http://www.aplu.ch/jcardgame). The connecting phase follows strictly the code shown above.

CardPlayer is the application class:

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

public class CardPlayer
{
  private final static boolean debug = true;
  private final String serverName = "CardServer";
  private String sessionID = "CardGame &4**&/**()";
  private TcpAgent agent;
  private CardServer cardServer;
  private CardTable cardTable;
  private String[] playerNames = null;
  private String currentPlayerName;
  private int currentPlayerIndex;
  private final int nbPlayers = 2;

  // Protocol tags
  public static interface Command
  {
    int REPORT_NAMES = 0;
    int READY_FOR_TALON = 1;
    int DISCONNECT = 2;
    int TALON_DATA = 3;
    int READY_TO_PLAY = 4;
    int MY_TURN = 5;
    int OTHER_TURN = 6;
    int CARD_TO_LINE = 7;
  }

  private class MyTcpAgentAdapter extends TcpAgentAdapter
  {
    public void dataReceived(String source, int[] data)
    {
      switch (data[0])
      {
        case Command.REPORT_NAMES:
          playerNames = TcpTools.split(TcpTools.intAryToString(data, 1)",");
          break;

        case Command.DISCONNECT:
          String client = TcpTools.intAryToString(data, 1);
          agent.disconnect();
          cardServer.disconnect();
          if (cardTable != null)
            cardTable.stopGame(client);
          break;

        case Command.TALON_DATA:
          int size = data.length - 1;
          int[] cardNumbers = new int[size];
          System.arraycopy(data, 1, cardNumbers, 0, size);
          cardTable.initHands(cardNumbers);
          break;

        case Command.MY_TURN:
          cardTable.setMyTurn();
          break;

        case Command.OTHER_TURN:
          cardTable.setOtherTurn();
          break;

        case Command.CARD_TO_LINE:
          cardTable.moveCardToLine(data[1], data[2]);
          break;
      }
    }

    public void notifyBridgeConnection(boolean connected)
    {
      if (!connected && cardTable != null)
      {
        cardTable.
          setStatusText("Client with game server disconnected. Game stopped.");
        agent.disconnect();
      }
    }
  }

  public CardPlayer()
  {
   String gameRoom = debug ? "123"
      : requestEntry("Enter a unique room name (ASCII 3..15 chars):");
    sessionID = sessionID + gameRoom;
    agent = new TcpAgent(sessionID, serverName);
    String requestedName = debug ? "max"
      : requestEntry("Enter your name (ASCII 3..15 chars):");
    agent.addTcpAgentListener(new MyTcpAgentAdapter());
    connect(gameRoom, requestedName);
  }

  private void connect(String gameRoom, String requestedName)
  {
    ModelessOptionPane mop = 
      new ModelessOptionPane("Trying to connect to relay...");
    cardServer = new CardServer(sessionID, gameRoom, serverName);

    TcpTools.delay(2000); // Let server come-up

    ArrayList<String> connectList = agent.connectToRelay(requestedName, 6000);
    if (connectList.isEmpty())
    {
      mop.setText("Connection to relay failed. Terminating now...");
      CardTable.delay(3000);
      System.exit(0);
    }

    int nb = connectList.size();
    currentPlayerName = connectList.get(0);
    if (nb > nbPlayers)
    {
      mop.setText("Game room fill. Terminating now...");
      CardTable.delay(3000);
      System.exit(0);
    }

    if (nb < nbPlayers)
      mop.setText("Connection established. Name: " + currentPlayerName
        + "\nCurrently " + nb + " player(s) in game room."
        + "\nWaiting for " + (nbPlayers - nb) + " more player(s)...");

    while (playerNames == null)  // Wait until player names are reported
      TcpTools.delay(10);

    for (int i = 0; i < nbPlayers; i++)
    {
      if (playerNames[i].equals(currentPlayerName))
      {
        currentPlayerIndex = i;
        break;
      }
    }
    mop.setVisible(false);
    cardTable = new CardTable(agent, playerNames, currentPlayerIndex);
    agent.sendCommand("", CardPlayer.Command.READY_FOR_TALON);
  }

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

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

 

CardTable is derived from CardGame and performs all the game specific visual operations.

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

public class CardTable extends CardGame
{
  public static enum Suit
  {
    SPADES, HEARTS, DIAMONDS, CLUBS
  }

  public static enum Rank
  {
    ACE, KING, QUEEN
  }
  //
  protected static Deck deck = new Deck(Suit.values(), Rank.values()"cover");
  private final int nbPlayers = 2;
  private final int nbStartCards = 5;
  private final int handWidth = 300;
  private final Location[] handLocations =
  {
    new Location(300525),
    new Location(30075),
  };
  private final Location lineLocation = new Location(300300);
  private Hand[] hands = new Hand[nbPlayers];
  private Hand line = new Hand(deck);
  private int currentPlayerIndex;
  private String[] playerNames;
  private TcpAgent agent;

  public CardTable(TcpAgent agent, String[] playerNames,int currentPlayerIndex)
  {
    super(60060030);
    this.agent = agent;
    this.playerNames = new String[nbPlayers];
    for (int i = 0; i < nbPlayers; i++)
      this.playerNames[i] = playerNames[i];
    this.currentPlayerIndex = currentPlayerIndex;
    setTitle("Current player's name: " + playerNames[currentPlayerIndex]);
  }

  protected void initHands(int[] cardNumbers)
  {
    for (int i = 0; i < nbPlayers; i++)
    {
      hands[i] = new Hand(deck);
      for (int k = 0; k < nbStartCards; k++)
        hands[i].insert(cardNumbers[* nbStartCards + k]false);
    }
    for (int i = nbStartCards * nbPlayers; i < cardNumbers.length; i++)
      line.insert(cardNumbers[i]true);
    line.setView(thisnew RowLayout(lineLocation, 400));
    line.draw();
    
    hands[currentPlayerIndex].sort(Hand.SortType.SUITPRIORITY, false);
    hands[currentPlayerIndex].addCardListener(new CardAdapter()
    {
      public void leftDoubleClicked(Card card)
      {
        hands[currentPlayerIndex].setTouchEnabled(false);
        agent.sendCommand("", CardPlayer.Command.CARD_TO_LINE,
          currentPlayerIndex, card.getCardNumber());
        card.transfer(line, true);
        agent.sendCommand("", CardPlayer.Command.READY_TO_PLAY);
      }
    });

    RowLayout[] layouts = new RowLayout[nbPlayers];
    for (int i = 0; i < nbPlayers; i++)
    {
      int k = (currentPlayerIndex + i) % nbPlayers;
      layouts[k] = new RowLayout(handLocations[i], handWidth);
      layouts[k].setRotationAngle(180 * i);
      if (!= currentPlayerIndex)
        hands[k].setVerso(true);
      hands[k].setView(this, layouts[k]);
      hands[k].setTargetArea(new TargetArea(lineLocation));
      hands[k].draw();
    }
    agent.sendCommand("", CardPlayer.Command.READY_TO_PLAY);
  }

  protected void moveCardToLine(int playerIndex, int cardNumber)
  {
    Card card = hands[playerIndex].getCard(cardNumber);
    card.setVerso(false);
    hands[playerIndex].transfer(card, line, true);
    agent.sendCommand("", CardPlayer.Command.READY_TO_PLAY);
  }

  protected void stopGame(String client)
  {
    setStatusText(client + " disconnected. Game stopped.");
    setMouseEnabled(false);
    doPause();
  }

  protected void setMyTurn()
  {
    setStatusText(
      "It's your turn. Double click on one of your cards to play it.");
    hands[currentPlayerIndex].setTouchEnabled(true);
  }

  protected void setOtherTurn()
  {
    setStatusText("Wait for you turn.");
  }
}

 

Finally comes the CardServer class that is embedded in the CardPlayer class. Instead of our own sendCommandToGroup() we take the built-in method and use a delay of 100 ms between successive transmission to the clients to avoid overload of the TCP/IP channel on heavily loaded systems with many players.

import ch.aplu.tcp.*;
import java.util.*;
import ch.aplu.jcardgame.*;
import ch.aplu.util.*;
import java.awt.*;

public class CardServer extends TcpBridge implements TcpBridgeListener
{
  private ArrayList<String> playerList = new ArrayList<String>();
  private String serverName;
  private int clientCount = 0;
  private int nbPlayers = 2;
  private int turnPlayerIndex = 0;
  private int nbReady = 0;

  public CardServer(String sessionID, String gameRoom, String serverName)
  {
    super(sessionID, serverName);
    this.serverName = serverName;
    addTcpBridgeListener(this);
    ArrayList<String> connectList = connectToRelay(6000);
    if (connectList.isEmpty())
    {
      ch.aplu.util.Console console = new ch.aplu.util.Console();
      System.out.println("Connection to relay failed");
      return;
    }
    if (!connectList.get(0).equals(serverName))
    {
      System.out.println("A server instance is already running.");
      disconnect();
    }
    else
    {
      final ch.aplu.util.Console console =
        new ch.aplu.util.Console(new Position(00),
        new Size(300400)new Font("Courier.PLAIN"1010));
      console.addExitListener(new ExitListener()
      {
        public void notifyExit()
        {
          console.end();
        }
      });
      System.out.println("Server in game room '" + gameRoom + "' running.");
    }
  }

  public void notifyRelayConnection(boolean connected)
  {
  }

  public void notifyAgentConnection(String agentName, boolean connected)
  {
    if (!connected && playerList.contains(agentName))
    {
      System.out.println("Player: " + agentName + " disconnected");
      if (playerList.contains(agentName))
        sendCommandToGroup(serverName, playerList, 100
          CardPlayer.Command.DISCONNECT, TcpTools.stringToIntAry(agentName));
      playerList.remove(agentName);
    }
    else
    {
      if (playerList.size() >= nbPlayers)
        return;

      System.out.println("Player: " + agentName + " connected");
      playerList.add(agentName);
      if (playerList.size() == nbPlayers)
      {
        String names = "";
        for (int i = 0; i < playerList.size(); i++)
        {
          names += playerList.get(i);
          if (< playerList.size() - 1)
            names += ',';
        }
        int[] data = TcpTools.stringToIntAry(names);
        sendCommandToGroup(serverName, playerList, 100,
          CardPlayer.Command.REPORT_NAMES, data);
      }
    }
  }

  public void pipeRequest(String source, String destination, int[] indata)
  {
    switch (indata[0])
    {
      case CardPlayer.Command.READY_FOR_TALON:
        System.out.println("Got READY_FOR_TALON from " + source);
        clientCount++;
        if (clientCount == nbPlayers)
        {
          clientCount = 0;
          sendStartTalon();
          System.out.println("Sent TALON_DATA to all");
        }
        break;

      case CardPlayer.Command.READY_TO_PLAY:
        nbReady++;
        System.out.println("Got READY_TO_PLAY from " + source);
        if (nbReady == nbPlayers)
        {
          nbReady = 0;
          giveTurn();
        }

        break;

      case CardPlayer.Command.CARD_TO_LINE:
        for (int i = 0; i < playerList.size(); i++)
        {
          if (!playerList.get(i).equals(source))
          {
            sendCommand(serverName, playerList.get(i),
              CardPlayer.Command.CARD_TO_LINE, indata[1], indata[2]);
            System.out.println("Sent CARD_TO_LINE to " + playerList.get(i));
          }
        }
        break;
    }
  }

  private void giveTurn()
  {
    for (int i = 0; i < playerList.size(); i++)
    {
      if (!= turnPlayerIndex)
      {
        sendCommand(serverName, playerList.get(i),
          CardPlayer.Command.OTHER_TURN);
        System.out.println("Sent OTHER_TURN to " + playerList.get(i));
      }
    }
    sendCommand(serverName, playerList.get(turnPlayerIndex),
      CardPlayer.Command.MY_TURN);
    System.out.println("Sent MY_TURN to " + playerList.get(turnPlayerIndex));
    turnPlayerIndex += 1;
    turnPlayerIndex %= nbPlayers;
  }

  private void sendStartTalon()
  {
    Hand talon = CardTable.deck.dealingOut(00)[0]// All cards, shuffled
    int[] cardNumbers = new int[talon.getNumberOfCards()];
    for (int i = 0; i < talon.getNumberOfCards(); i++)
      cardNumbers[i] = talon.get(i).getCardNumber();
    sendCommandToGroup(serverName, playerList, 100,
      CardPlayer.Command.TALON_DATA, cardNumbers);
  }
}

Execute the program for each player on the same or different computers using WebStart.

tcpex