Aegidius
 Plüss   Aplulogo
     
 www.aplu.ch      Print Text 
© 2021, V10.4
 
  TCPCom Module
 
Online TCP Games
 

 

In almost all game scenarios some kind of information must be exchanged between the players. If more than one person is involved in a real-time computer game, each player runs a program on his own computer and the programs must exchange game information. That's why TCP communication is essential for online computer games. To reduce the complexity of the game development a game library (game engine) is needed. In the following we use GameGrid, a professional, but education oriented game library for Java, Android and Python. By combining GameGrid with tcpcom, you can create sophisticated multi-person online games where the game partners are located anywhere in the world.

 

Pearl Game: A Two Persons Online Game

As example we construct the well-known Nim game, where items are aligned in several rows. One after the other the players remove one or more items, but all in the same row. The player who must remove the last item, loses. For the computer game, the current game situation is shown as graphics and the items are called "pearls". A pearl can be removed by clicking on it. When the current player finishes his draw, he clicks the OK button.
nimgame
  From: https://en.wikipedia.org/wiki/Nim

In this simple implementation, the two player's programs are not completely identical because one of them is a TCPCom server and the other one a TCPCom client. Like in all client/server applications the server must always start first.

But it is rather straight-forward to to implement a kind of "peer-to-peer" communication, where the programs are identical. When started, the program first acts as a client that tries to connect to an already running server. If the connection fails, the program acts as a server. In a advanced online game scenario a dedicated game server runs constantly on a remote host. A special socket based library TCPJLib is available to develop such multiuser online games using a TCP relay server.

Because an important part of the program is the same for the server and the client, the common code is transferred to PearlShare.java (for Java) and pearshare.py (for Python) in order to avoid code duplication. This file also contains game state variables, that can be made static (global). For example the boolean variable isMyMove indicates if it is the current player's or the partner's move.

Pearl Game Server in Java:

import ch.aplu.jgamegrid.*;
import ch.aplu.tcpcom.*;

public class PearlServer extends GameGrid
  implements TCPServerListener, GGMouseListener
{
  private TCPServer server;
  private boolean gameStarted = false;

  public PearlServer()
  {
    super(8, 6, 70, false);
    addStatusBar(30);
    addMouseListener(this, GGMouse.lPress | GGMouse.lRelease);
    PearlShare.initGame(this);
    show();
    int port = 5000;
    server = new TCPServer(port);
    server.addTCPServerListener(this);
  }

  public void onStateChanged(String state, String msg)
  {
    if (state.equals(TCPServer.PORT_IN_USE))
      setStatusText("TCP port occupied. Restart IDE.");
    else if (state.equals(TCPServer.LISTENING))
    {
      if (!PearlShare.isOver && !gameStarted)
        setStatusText("Waiting for a partner to play");
      if (gameStarted)
      {
        setStatusText("Partner lost.");
        PearlShare.isMyMove = false;
      }
    }
    else if (state.equals(TCPServer.CONNECTED))
    {
      setStatusText("Client connected. Wait for partner's move!");
      gameStarted = true;
    }

    else if (state.equals(TCPServer.MESSAGE))
      PearlShare.handleMessage(this, msg);
  }

  public boolean mouseEvent(GGMouse mouse)
  {
    if (mouse.getEvent() == GGMouse.lPress)
      PearlShare.handleMousePress(this, server, mouse);
    if (mouse.getEvent() == GGMouse.lRelease)
      PearlShare.handleMouseRelease(this, server, mouse);
    return true;
  }


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

Execute the program locally using WebStart

There is no need to call server.terminate() in this case, because the TCP port is released when the Java Virtual Machine terminates by clicking the game window's close button. The same holds for the client.

Pearl Game Client in Java:

import ch.aplu.jgamegrid.*;
import ch.aplu.tcpcom.*;

public class PearlClient extends GameGrid
  implements TCPClientListener, GGMouseListener
{
  private TCPClient client;

  public PearlClient()
  {
    super(8, 6, 70, false);
    addStatusBar(30);
    addMouseListener(this, GGMouse.lPress | GGMouse.lRelease);
    PearlShare.initGame(this);
    show();
    int port = 5000;
    String host = "localhost";
    client = new TCPClient(host, port);
    client.addTCPClientListener(this);
    client.connect();
  }

  public void onStateChanged(String state, String msg)
  {
    if (state.equals(TCPClient.CONNECTED))
    {
      setStatusText("Connection established. " 
                  + "Remove any number of pearls from same row and click OK!");
      PearlShare.isMyMove = true; // You start!
    }
    else if (state.equals(TCPClient.CONNECTION_FAILED))
      setStatusText("Connection failed");
    else if (state.equals(TCPClient.DISCONNECTED))
    {
      setStatusText("Partner lost");
      PearlShare.isMyMove = false;
    }
    else if (state.equals(TCPClient.MESSAGE))
      PearlShare.handleMessage(this, msg);
  }

  public boolean mouseEvent(GGMouse mouse)
  {
    if (mouse.getEvent() == GGMouse.lPress)
      PearlShare.handleMousePress(this, client, mouse);
    if (mouse.getEvent() == GGMouse.lRelease)
      PearlShare.handleMouseRelease(this, client, mouse);
    return true;
  }


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

Execute the program locally using WebStart

Despite the rule to use the static keyword with reticence, it is convenient to give access to "global" variables and methods without the need to create an instance. Different behavior in the shared code for the server and client program can be achieved with parameters. It is a matter of taste to design the code differently.

Pearl Game Share in Java:

import ch.aplu.jgamegrid.*;
import ch.aplu.tcpcom.*;

public class PearlShare
{
  static Actor btn;
  static int activeRow = -1;
  static int nbPearl = 18;
  static int nbRemoved = 0;
  static boolean isMyMove = false;
  static boolean isOver = false;

  static void initGame(GameGrid gg)
  {
    int nbRows = 4;
    int nb = 6;
    for (int k = 0; k < nbRows; k++)
    {
      for (int i = 0; i < nb; i++)
      {
        Actor pearl = new Actor("sprites/token_1.png");
        gg.addActor(pearl, new Location(1 + i, 1 + k));
      }
      nb -= 1;
    }
    btn = new Actor("sprites/btn_ok.gif", 2);
    gg.addActor(btn, new Location(6, 5));
  }

  static void handleMousePress(GameGrid gg, Object node, GGMouse mouse)
  {
    if (!isMyMove || isOver)
      return;
    Location btnLoc = new Location(6, 5);
    Location loc = gg.toLocationInGrid(mouse.getX(), mouse.getY());
    if (loc.equals(btnLoc))
    {
      btn.show(1);
      gg.refresh();
      if (nbRemoved == 0) // ok btn pressed
        gg.setStatusText("You have to remove at least 1 pearl!");
      else
      {
        sendMessage(node, "ok");
        gg.setStatusText("Wait!");
        nbRemoved = 0;
        activeRow = -1;
        isMyMove = false;
      }
    }
    else
    {
      int x = loc.x;
      int y = loc.y;
      if (activeRow != -1 && activeRow != y)
        gg.setStatusText("You must remove pearls from the same row.");
      else
      {
        Actor actor = gg.getOneActorAt(loc);
        if (actor != null)
        {
          actor.removeSelf();
          gg.refresh();
          sendMessage(node, "" + x + y);
          activeRow = y;
          nbPearl -= 1;
          nbRemoved += 1;
          if (nbPearl == 0)
          {
            isOver = true;
            gg.setStatusText("End of game. You lost!");
          }
        }
      }
    }
  }

  static void handleMouseRelease(GameGrid gg, Object node, GGMouse mouse)
  {
    btn.show(0);
    gg.refresh();
  }

  static void sendMessage(Object node, String msg)
  {
    if (node instanceof TCPServer)
      ((TCPServer)node).sendMessage(msg);
    else if (node instanceof TCPClient)
      ((TCPClient)node).sendMessage(msg);
  }

  static void handleMessage(GameGrid gg, String msg)
  {
    if (msg.equals("ok"))
    {
      isMyMove = true;
      gg.setStatusText("Remove any number of pearls from same row and click OK!");
    }
    else
    {
      int x = msg.charAt(0) - 48;
      int y = msg.charAt(1) - 48;
      Location loc = new Location(x, y);
      gg.getOneActorAt(loc).removeSelf();
      gg.refresh();
      nbPearl -= 1;
      if (nbPearl == 0)
      {
        isOver = true;
        gg.setStatusText("End of Game. You won.");
      }
    }
  }
}

Since GameGrid is a game library written in Java, it cannot be used in standard Python. But since the TigerJython IDE is based on Jython, GameGrid is supported with no additional installation.

Unlike in Java, server.terminate() must be executed to release the TCP port because the Java Virtual Machine only terminates when the TigerJython IDE is closed.

Pear Game Server in Python (TigerJython):

from gamegrid import *
from tcpcom import TCPServer
import pearlshare
                                                                          
def onMousePressed(e):
    pearlshare.handleMousePress(e, server)

def onMouseReleased(e):
    pearlshare.handleMouseRelease(e)

def onNotifyExit():
    server.terminate()
    dispose()
    
def onStateChanged(state, msg):
    global gameStarted, partnerLost
    if state == TCPServer.PORT_IN_USE: 
        setStatusText("TCP port occupied. Restart IDE.")
    elif state == TCPServer.LISTENING:
        if gameStarted:
            setStatusText("Connection lost. Restart server.")
            pearlshare.isMyMove = False
            server.terminate()
        else:
            setStatusText("Waiting for a partner to play")
    elif state == TCPServer.CONNECTED:
        if gameStarted:
            return
        setStatusText("Client connected. Wait for partner's move!")
        gameStarted = True
    elif state == TCPServer.MESSAGE: 
        pearlshare.handleMessage(msg)    
 
makeGameGrid(8, 6, 70, False, mousePressed = onMousePressed,
                              mouseReleased = onMouseReleased, 
                              notifyExit = onNotifyExit)
addStatusBar(30)
pearlshare.initGame()
show()
gameStarted = False
port = 5000
server = TCPServer(port, stateChanged = onStateChanged)
server.terminate()

By using import pearlshare all access of variables and functions in the external module must be prefixed with its name. The module variables have read and write access rights like global variables.

Pear Game Client in Python (TigerJython):

from tcpcom import TCPClient
import time

def onStateChanged(state, msg):
    print "State: " + state + ". Message: " + msg

host = "192.168.0.5"
#host = inputString("Host Address?")           
port = 5000 # IP port
client = TCPClient(host, port, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    msgDlg("Connected. OK to terminate")
    client.disconnect()

The boolean variables isMyMove and isOver are used to specify the current game state.

Pear Game Share in Python (TigerJython):

from gamegrid import *

activeRow = -1
nbPearl = 18
nbRemoved = 0
isMyMove = False
isOver = False     

def initGame():
    global btn
    nbRows = 4
    nb = 6
    for k in range(nbRows):
        for i in range(nb):
             pearl = Actor("sprites/token_1.png")
             addActor(pearl, Location(i + 1, k + 1))
        nb -=1    
    btn = Actor("sprites/btn_ok.gif", 2)
    addActor(btn, Location(6, 5))

def handleMouseRelease(e):
    btn.show(0)
    refresh()

def handleMousePress(e, node):
    global isMyMove, nbPearl, isOver, activeRow, nbRemoved
    if not isMyMove or isOver:
        return
    btnLoc = Location(6,5)    
    loc = toLocationInGrid(e.getX(), e.getY())
    if loc == btnLoc:
      btn.show(1)
      refresh()
      if nbRemoved == 0: # ok btn pressed
        setStatusText("You have to remove at least 1 pearl!")
      else:
        node.sendMessage("ok")          
        setStatusText("Wait!")
        nbRemoved = 0
        activeRow = -1
        isMyMove = False          
        
    else:
        x = loc.x
        y = loc.y  
        if activeRow != -1 and activeRow != y:
            setStatusText("You must remove pearls from the same row.")
        else:
            actor = getOneActorAt(loc)
            if actor != None:      
                actor.removeSelf()
                refresh()        
                node.sendMessage(str(x) + str(y))
                activeRow = y 
                nbPearl -= 1
                nbRemoved += 1
                if nbPearl == 0:    
                    isOver = True
                    setStatusText("End of game. You lost!")

def handleMessage(msg):                                                                
    global nbPearl, isMyMove, isOver
    if msg == "ok":
        isMyMove = True
        setStatusText("Remove any number of pearls from same row and click OK!")
    else:
        x = int(msg[0])
        y = int(msg[1])
        loc = Location(x, y)            
        getOneActorAt(loc).removeSelf()
        refresh()            
        nbPearl -= 1
        if nbPearl == 0:
            isOver = True
            setStatusText("End of Game. You won.")    
                                    
 
 
 

pearls