Next Cycle
Back to Exercise Overview

Cycle 1: Enter into game room, initialize game table, deal out cards

Problem definition:

Create a player application that can be started on the same or different computers attached to the internet. When the application starts, the number of players in the range 2..4, the name of the game room and the name of the player is asked. If the boolean constant debug may is set to true, these parameters are preset to 2, "blue", "max" to speed up starting during development.

The first player application starts a game server derived from TcpBridge that connects to the TcpRelay. After the game server is up, the player derived from TcpAgent also connects to the TcpRelay, so the player is logically linked to the game server and data may be exchanged using the send() and sendCommand() methods.

The application then shows a square card table with 2 to 4 players sitting side by side. The server sends an array of card numbers of a randomly shuffled card deck to every player and the cards are dealt out. The card hands are shown face down except for the owner hand that shows the cards face up. The owner hand is always positioned at the bottom (to the south) of the card table and the other players sit in the order they entered in the game room. To handle this situation we use two integer identifiers running from 0 to 3, the handId that labels the hand locations (0 to south, 1 to west, 2 to north, 3 to east) and playerId that designates the players (0 the first player, 1 the second, 2 the third, 3 the last player entering the game room). The player class defines an instance variable myPlayerId that is set to the playerId of the current player. The mapping between playerId and handId is specific for each player:

private int toPlayerId(int handId)
{
  return (myPlayerId + handId) % nbPlayers;
}

private int toHandId(int playerId)
{
  int n = playerId - myPlayerId;
  return n >= 0 ? n : (nbPlayers + n);
}

After the player applications received the card numbers they deal out the cards to the players and show the hands. We keep much of the game information locally to the player applications in order to minimize the data transfer, but most of the information is hidden to the user.

The game startup process follows closely the embedded server pattern as explained in the TcpJLib Tutorial Lesson 5 (see). Study the concept and try to understand the code presented there. Copy the CardPlayer, CardTable and CardServer classes to your IDE, compile and execute the CardPlayer application.

We use our knowledge from the tutorial example to create a versions of these classes adapted to our problem. To simplify your work we give you the code that partially solves the problem.

 

Work to do yourself:

We start with the application class SchwarzPeter where we ask the main game options. We pass these values to the other classes by protected instance variables to avoid excess of constructor parameters.

// SchwarzPeter.java
// Cycle 1: Entering game room, initializing game table, dealing out

import javax.swing.JOptionPane;

public class SchwarzPeter
{
  protected static final boolean debug = true;

  private static final String sessionPrefix = "BackPeter &4-;>**&/**??==";
  protected static final String serverName = "CardServer";
  protected static String sessionID;
  protected static String playerName;
  protected static int nbPlayers;

  public static void main(String[] args)
  {
    nbPlayers = debug ? 2
      : requestNumber("Enter number of players (2..4):");
    String roomName = debug ? "123"
      : requestEntry("Enter a unique room name (ASCII 3..15 chars):");
    sessionID = sessionPrefix + roomName;
    playerName = debug ? "max"
      : requestEntry("Enter your name (ASCII 3..15 chars):");
    new Player();
  }

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

  private static int requestNumber(String prompt)
  {
    while (true)
    {
      String entry = JOptionPane.showInputDialog(null, prompt, "");
      if (entry == null)
        System.exit(0);
      try
      {
        int nb = Integer.parseInt(entry);
        if (nb >= && nb <= 4)
          return nb;
      }
      catch (NumberFormatException ex)
      {
      }
    }
  }
}

 

Next we consider the class Player. It is left to you to implement the DECK_DATA case in the dataReceived() callback (search for TODO).

// Player.java
// Cycle 1: see TODO where to put your code

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

public class Player extends TcpAgent
{
  private int nbPlayers = SchwarzPeter.nbPlayers;
  private Server cardServer;
  private CardTable cardTable;
  private String[] playerNames = null;
  private String currentPlayerName;
  private int myPlayerId;

  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);
          disconnect();
          cardServer.disconnect();
          if (cardTable != null)
            cardTable.stopGame(client);
          break;

        case Command.DECK_DATA:
          System.out.println("TODO: Got series " + Arrays.toString(data));
          break;
      }
    }

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

  public Player()
  {
    super(SchwarzPeter.sessionID, SchwarzPeter.serverName);
    this.nbPlayers = nbPlayers;
    addTcpAgentListener(new MyTcpAgentAdapter());
    ModelessOptionPane mop = 
      new ModelessOptionPane("Trying to connect to relay...");
    cardServer = new Server();
    TcpTools.delay(2000); // Let server come-up

    ArrayList<String> connectList = connectToRelay(SchwarzPeter.playerName, 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 full. 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))
      {
        myPlayerId = i;
        break;
      }
    }
    mop.setVisible(false);
    cardTable = new CardTable(this, playerNames, myPlayerId);
    sendCommand("", Command.READY_FOR_TALON);
  }
}

 

Now it's time to implement the graphical user interface using the convenient JCardGame addon of the JGameGrid library. It is almost complete, but you have invoke initHands() at the right place. The insertion of the cards from the cardNumber array is also left to you (search for TODO). But remember you must exclude the twin of the ugly card.

// CardTable.java
// Cycle 1: see TODO where to put your code

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

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

  public static enum Rank
  {
    ACE, KING, QUEEN, JACK, TEN, NINE, EIGHT, SEVEN, SIX
  }
  //
  protected static Deck deck = new Deck(Suit.values(), Rank.values(), "cover");
  private Card uglyCard = new Card(deck, Suit.SPADES, Rank.JACK);
  private Card uglyCardTwin = new Card(deck, Suit.HEARTS, Rank.JACK);
  private final int nbPlayers = SchwarzPeter.nbPlayers;
  private final int handWidth = 300;
  private final Location[] handLocations =
  {
    new Location(300, 525),
    new Location(75, 300),
    new Location(300, 75),
    new Location(525, 300)
  };
  private final Location[] stockLocations =
  {
    new Location(540, 500),
    new Location(100, 400),
    new Location(60, 100),
    new Location(500, 200),
  };
  private Hand[] hands = new Hand[nbPlayers];
  private Hand[] stocks = new Hand[nbPlayers];
  private String[] playerNames;
  private TcpAgent agent;
  private int myPlayerId;

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

  protected void initHands(int[] cardNumbers)
  {
    for (int i = 0; i < nbPlayers; i++)
    {
      stocks[i] = new Hand(deck);
      hands[i] = new Hand(deck);
      hands[i].setTouchEnabled(true);
      RowLayout handLayout = new RowLayout(handLocations[i], handWidth);
      handLayout.setRotationAngle(90 * i);
      hands[i].setView(this, handLayout);
      StackLayout stockLayout = new StackLayout(stockLocations[i]);
      stockLayout.setRotationAngle(90 * i);
      stocks[i].setView(this, stockLayout);
    }

    // Insert cards into hands
    int playerId = 0;
    for (int i = 0; i < cardNumbers.length; i++)
    {

      Card card = new Card(deck, cardNumbers[i]);
      System.out.println("TODO: insert " + card);
      delay(200);
      playerId = (playerId + 1) % nbPlayers;
    }
    agent.sendCommand("", Command.READY_TO_PLAY);
  }

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

  private int toHandId(int playerId)
  {
    int n = playerId - myPlayerId;
    return n >= 0 ? n : (nbPlayers + n);
  }

  private int toPlayerId(int handId)
  {
    return (myPlayerId + handId% nbPlayers;
  }
}

 

Finally here is the class Server. Here, nothing is to do for you. Try to understand each line of the code.

// Server.java
// Cycle 1

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

public class Server extends TcpBridge implements TcpBridgeListener
{
  private ArrayList<String> playerList = new ArrayList<String>();
  private int clientCount = 0;
  private int nbPlayers = SchwarzPeter.nbPlayers;

  public Server()
  {
    super(SchwarzPeter.sessionID, SchwarzPeter.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(SchwarzPeter.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(0, 0),
        new Size(300, 400), new Font("Courier.PLAIN"10, 10));
      console.addExitListener(new ExitListener()
      {
        public void notifyExit()
        {
          console.end();
        }
      });
      System.out.println("Server up and 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("", playerList, 100, 
          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 (i < playerList.size() - 1)
            names += ',';
        }
        int[] data = TcpTools.stringToIntAry(names);
        sendCommandToGroup("", playerList, 100,
          Command.REPORT_NAMES, data);
      }
    }
  }

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

      case Command.READY_TO_PLAY:
        System.out.println("Got READY_TO_PLAY from " + source);
        break;
    }
  }

  private void sendDeck()
  {
    Hand fullHand = CardTable.deck.toHand(!SchwarzPeter.debug);
    int[] cardNumbers = new int[fullHand.getNumberOfCards()];
    for (int i = 0; i < fullHand.getNumberOfCards(); i++)
      cardNumbers[i] = fullHand.get(i).getCardNumber();
    sendCommandToGroup("", playerList, 100,
      Command.DECK_DATA, cardNumbers);
  }
}

 

All classes make use of the command tags defined in the interface Command:

// Command.java
// Cycle 1

public interface Command
{
  int REPORT_NAMES = 0;
  int READY_FOR_TALON = 1;
  int DISCONNECT = 2;
  int DECK_DATA = 3;
  int READY_TO_PLAY = 4;
}

Download the program code as presented here.
Execute the solution you should obtain.

Next Cycle
Back to Exercise Overview