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

The source code of all examples is included in the TcpJLib distribution.

 

Example 2: A multi-user implementation of the Nim game

The TcpJLib package was developed to give JGameGrid, our sprite-based game framework, an extra power. (Please consult our website to inform you about the features of JGameGrid.) Nim is played everywhere on the world, often using matches aligned on a table.

Nim is a two-player mathematical game of strategy in which players take turns removing objects from distinct heaps. On each turn, a player must remove at least one object, and may remove any number of objects provided they all come from the same heap. Nim is usually played as a misère game, in which the player to take the last object loses. [Ref. http://en.wikipedia.org/wiki/Nim]

There is no need to restrict the game to two players. We implement Nim as it is often played in Switzerland using a line of 15 matches and where each player must take 1, 2 or 3 matches per move. The player that takes the last match loses.

Because we want to construct a full-fledged game, we make some extra effort to handle special situations (players tries to log-in when the game is in progress, players logs-out during the game, restarting the game with the same group, etc.)

During the game, information is exchanged between the players (clients) and the game server, but not directly between players. For the developing process it is completely irrelevant that all transmissions are tunneled through a relay server. We may imagine the following game topology with machines located in far distances, despite all programs may be developed and run on the same machine.

tcpjlib

To specify the kind of information, we have to adhere to a convention or a protocol. In this example we simply prefix all data transmissions with an integer command tag that is known to the clients and the server as well. Because the tags are integers, we don't use an enumeration but an interface:

 private interface Command
  
{
    
int GAME_RUNNING = 0;
    
int NUMBER_OF_PLAYERS = 1;
    
int INIT_PLAYGROUND = 2;
    
int GAME_STARTING = 3;
    
int REMOTE_MOVE = 4;
    
int LOCAL_MOVE = 5;
    
int REPORT_OK = 6;
    
int REPORT_POSITION = 7;
    
int WINNER = 8;
    
int LOSER = 9;
    
int PLAYER_DISCONNECT = 10;
  
}

We decide to implement the following design principles:

  • The game server must be running before any client connects to the relay
  • The server determines the starting number of matches
    (but the graphics does not yet support different numbers, it is left to you to implement it)
  • The player removes a match with a mouse left-click
  • The server and every player manage the number of remaining matches individually
  • The win/lost situation is transmitted from the server
  • The server decides who starts the game. In the first round it is an arbitrary player,
    afterwards it is the player who lost the game
  • If a player disconnects during the game, the game is terminated and the other players must wait for the completion of the group

The game server does not use any graphics but a Console as logging device. Of course the log could be disabled, but it is recommandable to display some kind of modal dialog to show that the server is up and running.

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

public class GGNimServer extends TcpBridge implements TcpBridgeListener
{
  private final int nbMatchesStart = 15;
  private final static String serverName = "NimServer";
  private static String sessionID;
  private static Console c;
  private Vector<String> players = new Vector<String>();
  private int groupSize;
  private int nbMatches;

  // Protocol tags
  private interface Command
  {
    int GAME_RUNNING = 0;
    int NUMBER_OF_PLAYERS = 1;
    int INIT_PLAYGROUND = 2;
    int GAME_STARTING = 3;
    int REMOTE_MOVE = 4;
    int LOCAL_MOVE = 5;
    int REPORT_OK = 6;
    int REPORT_POSITION = 7;
    int WINNER = 8;
    int LOSER = 9;
    int PLAYER_DISCONNECT = 10;
  }

  static
  {
    c.print("Enter a unique session ID: ");
    sessionID = c.readLine();
  }

  public GGNimServer()
  {
    super(sessionID, serverName);

    // Request group size
    boolean ok = false;
    while (!ok)
    {
      String groupSizeStr =
        JOptionPane.showInputDialog(null,
        "Number of players in group (1..10)""");
      if (groupSizeStr == null)
        System.exit(0);
      try
      {
        groupSize = Integer.parseInt(groupSizeStr.trim());
      }
      catch (NumberFormatException ex)
      {
      }
      if (groupSize > 0 && groupSize <= 10)
        ok = true;
    }
    c.println("Server '" + serverName + "' connecting to relay "
      + getRelay() + "...");
    addTcpBridgeListener(this);
    ArrayList<String> connectList = connectToRelay(6000);
    if (connectList.isEmpty())
      errorQuit("Connection failed (timeout reached)");
    if (!connectList.get(0).equals(serverName))
      errorQuit("An instance of '" + serverName + "' already running.");
    c.println("Connection established");
  }

  private void errorQuit(String msg)
  {
    c.println(msg);
    c.println("Shutdown now...");
    TcpTools.delay(3000);
    System.exit(1);
  }

  public void notifyRelayConnection(boolean connected)
  {
    if (!connected)
      c.println("Connection to relay broken.");
  }

  public void notifyAgentConnection(String agentName, boolean connected)
  {
    String str = "";
    if (!connected)
    {
      c.println("Player: " + agentName + " disconnected");
      if (players.contains(agentName))
      {
        players.remove(agentName);
        sendCommandToGroup(Command.PLAYER_DISCONNECT,
          TcpTools.stringToIntAry(agentName));
        sendCommandToGroup(Command.NUMBER_OF_PLAYERS, players.size());
        sendCommandToGroup(Command.INIT_PLAYGROUND, nbMatchesStart);
      }
      return;
    }

    c.println("Player: " + agentName + " connected");
    if (players.size() == groupSize)
    {
      c.println("Game already running");
      sendCommand(serverName, agentName, Command.GAME_RUNNING);
      return;
    }

    // Send starting number of matches
    sendCommand(serverName, agentName, Command.INIT_PLAYGROUND, nbMatchesStart);

    // Add player to group
    players.add(agentName);
    sendCommandToGroup(Command.NUMBER_OF_PLAYERS, players.size());

    if (players.size() == groupSize)
    {
      // Group completed. Game started.
      for (String nickname : players)
        str += nickname + "&&";
      sendCommandToGroup(Command.GAME_STARTING, TcpTools.stringToIntAry(str));
      nbMatches = nbMatchesStart;
      TcpTools.delay(5000);
      // A random player starts to play
      String startPlayer = players.get((int)(groupSize * Math.random()));
      giveMoveTo(startPlayer);
    }
  }

  private void giveMoveTo(String player)
  {
    int indexOfPlayer = players.indexOf(player);
    sendCommand(serverName, player, Command.LOCAL_MOVE);
    for (int i = 0; i < groupSize; i++)
    {
      if (i != indexOfPlayer)
        sendCommand(serverName, players.get(i), Command.REMOTE_MOVE,
          TcpTools.stringToIntAry(player));
    }
  }

  private void sendCommandToGroup(int command, int... data)
  {
    for (String nickname : players)
      sendCommand(serverName, nickname, command, data);
  }

  public void pipeRequest(String source, String destination, int[] indata)
  {
    // Check if group is still complete
    if (groupSize != players.size())
      return;
    switch (indata[0])
    {
      case Command.REPORT_POSITION:  // Pass command to other players
        c.println("Player " + source + " reports position " + indata[1]);
        nbMatches--;
        c.println("Number of remaining matches: " + nbMatches);
        for (String player : players)
        {
          if (!player.equals(source))
            sendCommand(serverName, player, Command.REPORT_POSITION, indata[1]);
        }
        break;

      case Command.REPORT_OK:
        c.println("Player " + source + " reports OK click");
        int nextPlayerIndex = (players.indexOf(source) + 1) % groupSize;
        String nextPlayer = players.get(nextPlayerIndex);
        if (nbMatches <= 0) // Game over
        {
          sendCommand(serverName, source, Command.LOSER);
          for (String player : players)
          {
            if (!player.equals(source))
              sendCommand(serverName, player, Command.WINNER,
                TcpTools.stringToIntAry(source));
          }
          TcpTools.delay(4000);
          sendCommandToGroup(Command.INIT_PLAYGROUND, nbMatchesStart);
          nbMatches = nbMatchesStart;
          giveMoveTo(source);
        }
        else
        {
          c.println("Give move to next player: " + nextPlayer);
          giveMoveTo(nextPlayer);
        }
        break;
    }
  }

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

Execute the program locally using WebStart.

The game clients are derived from GameGrid in order to use GameGrid's methods directly. It contains a TcpAgent that performs all data exchange. The mouse and the button are disabled when the player is not allowed to use them.

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

public class GGNimPlayer extends GameGrid
  implements GGMouseListener, GGButtonListener
{
  // ------------------- Inner class MyTcpAgentAdapter -------------
  private class MyTcpAgentAdapter implements TcpAgentListener
  {
    private boolean isReady = true;

    public void notifyRelayConnection(boolean connected)
    {
    }

    public void notifyAgentConnection(String agentName, boolean connected)
    {
    }

    public void notifyBridgeConnection(boolean connect)
    {
      if (!connect)
      {
        setTitle("Game server disconnected. Game terminated.");
        okBtn.setEnabled(false);
        isMouseEnabled = false;
      }
    }

    public void dataReceived(String source, int[] data)
    {
      if (!isReady)
        return;

      String str = "";
      switch (data[0])
      {
        case Command.GAME_RUNNING:
          setTitle("Game in process.\nPlease wait for next game.\n"
            + "Terminating now...");
          TcpTools.delay(5000);
          System.exit(0);
          break;

        case Command.PLAYER_DISCONNECT:
          disconnectedPlayer = TcpTools.intAryToString(data, 1);
          break;

        case Command.NUMBER_OF_PLAYERS:
          initPlayground();
          if (!disconnectedPlayer.equals(""))
            disconnectedPlayer = "Disconnected: " + disconnectedPlayer + ". ";
          setTitle(disconnectedPlayer
            + "Current number in group: " + data[1]
            + ". Waiting for more players to join...");
          disconnectedPlayer = "";
          break;

        case Command.GAME_STARTING:
          str = "Group complete. In group: ";
          String[] names =
            TcpTools.split(TcpTools.intAryToString(data, 1), "&&");
          for (int i = 0; i < names.length; i++)
          {
            if (i < names.length - 1)
              str += names[i] + "; ";
            else
              str += names[i];
          }
          setTitle(str + ". Wait start selection...");
          break;

        case Command.INIT_PLAYGROUND:
          nbMatches = data[1];
          initPlayground();
          break;

        case Command.REPORT_POSITION:
          Location loc = new Location(data[1], 4);
          getOneActorAt(loc).removeSelf();
          nbMatches--;
          setTitle(nbMatches + " left. " + remotePlayer + " is playing.");
          refresh();
          break;

        case Command.LOCAL_MOVE:
          activate();
          setTitle(nbMatches + " left. It's you to play.");
          nbTaken = 0;
          okBtn.setEnabled(true);
          isMouseEnabled = true;
          break;

        case Command.REMOTE_MOVE:
          remotePlayer = TcpTools.intAryToString(data, 1);
          setTitle(nbMatches + " left. " + remotePlayer + " will play.");
          break;

        case Command.WINNER:
          String loser = TcpTools.intAryToString(data, 1);
          setTitle("Game over. Player " + loser + " lost the game.");
          break;

        case Command.LOSER:
          setTitle("Game over. You lost the game.");
          break;
      }
    }
  }

  // --------------- Inner class Match --------------
  private class Match extends Actor
  {
    public Match()
    {
      super("sprites/match.gif");
    }
  }
  // ------------------- End of inner class ------------------------
  //
  private final static String serverName = "NimServer";
  private String agentName;
  private String remotePlayer = "";
  private String disconnectedPlayer = "";
  private int nbMatches;
  private GGBackground bg;
  private GGButton okBtn = new GGButton("sprites/ok.gif"true);
  private int nbTaken;
  private volatile boolean isMouseEnabled;
  private TcpAgent agent;

  // Protocol tags
  private interface Command
  {
    int GAME_RUNNING = 0;
    int NUMBER_OF_PLAYERS = 1;
    int INIT_PLAYGROUND = 2;
    int GAME_STARTING = 3;
    int REMOTE_MOVE = 4;
    int LOCAL_MOVE = 5;
    int REPORT_OK = 6;
    int REPORT_POSITION = 7;
    int WINNER = 8;
    int LOSER = 9;
    int PLAYER_DISCONNECT = 10;
  }

  public GGNimPlayer()
  {
    super(56, 9, 12, false);
    String sessionID = 
      requestEntry(
"Enter a unique session ID (ASCII >3 chars):");
    agent = new TcpAgent(sessionID, serverName);
    agentName = requestEntry("Enter your name (ASCII >3 chars):");
    initGameWindow();
    addMouseListener(this, GGMouse.lPress);
    setTitle("Connecting to relay " + agent.getRelay() + "...");
    agent.addTcpAgentListener(new MyTcpAgentAdapter());
    ArrayList<String> connectList =
      agent.connectToRelay(agentName, 6000);
    if (connectList.isEmpty())
    {
      setTitle("Connection to relay failed. Terminating now...");
      TcpTools.delay(3000);
      System.exit(1);
    }
    setTitle("Connection established. Personal name: " + connectList.get(0));
    // Game server must be up and running
    if (!agent.isBridgeConnected())
    {
      setTitle("Game server not found. Terminating now...");
      TcpTools.delay(3000);
      System.exit(1);
    }
  }

  private void initGameWindow()
  {
    bg = getBg();
    bg.clear(Color.blue);
    addActor(okBtn, new Location(50, 4));
    okBtn.addButtonListener(this);
    addMouseListener(this, GGMouse.lPress);
    show();
  }

  private void initPlayground()
  {
    okBtn.setEnabled(false);
    isMouseEnabled = false;
    removeActors(Match.class);
    for (int i = 0; i < nbMatches; i++)
      addActor(new Match(), new Location(2 + 3 * i, 4));
  }

  public boolean mouseEvent(GGMouse mouse)
  {
    if (!isMouseEnabled)
      return true;
    Location loc = toLocationInGrid(mouse.getX(), mouse.getY());
    Actor actor = null;
    for (int y = 2; y < 7; y++)
    {
      actor = getOneActorAt(new Location(loc.x, y), Match.class);
      if (actor != null)
        break;
    }
    if (actor != null)
    {
      if (nbTaken == 3)
        setTitle("Take a maximum of 3. Click 'OK' to continue");
      else
      {
        actor.removeSelf();
        agent.sendCommand(
          agentName, Command.REPORT_POSITION, actor.getLocation().x);

        nbMatches--;
        setTitle(nbMatches + " matches remaining. Click 'OK' to continue");
        nbTaken++;
        refresh();
      }
    }
    return false;
  }

  public void buttonClicked(GGButton button)
  {
    if (nbTaken == 0)
      setTitle("You have to remove at least 1 match");
    else
    {
      agent.sendCommand(agentName, Command.REPORT_OK);
      okBtn.setEnabled(false);
      isMouseEnabled = false;
    }
  }

  public void buttonPressed(GGButton button)
  {
  }

  public void buttonReleased(GGButton button)
  {
  }

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

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

Execute the program locally using WebStart. Use the same session ID as for the server.