Spielprogrammierung mit Java
HomeAufgabenDruckenJava-Online

TCP Four In A Row


Four In A Row (auch: 4 gewinnt oder Connect Four) ist ein bekanntes Zweipersonen-Strategiespiel mit dem Ziel, als Erster vier der eigenen Spielsteine in eine Linie (horizontal, vertikal oder diagonal) zu bringen. Das Brettspiel wird auf einem senkrecht stehenden, hohlen Spielbrett gespielt, in das die Spieler abwechselnd ihre Spielsteine fallen lassen. Das Spielbrett besteht aus sieben Spalten (senkrecht) und sechs Reihen (waagrecht). Jeder Spieler besitzt 21 gleichfarbige Spielsteine. Wenn ein Spieler einen Spielstein in eine Spalte fallen lässt, besetzt dieser den untersten freien Platz der Spalte. Gewinner ist der Spieler, der es als Erster schafft, vier seiner Spielsteine waagerecht, senkrecht oder diagonal in eine Linie zu bringen. Das Spiel endet unentschieden, wenn das Spielbrett komplett gefüllt ist, ohne dass ein Spieler eine Viererlinie gebildet hat.

Diese Implementierung des Spieles ermöglicht zwei Spielern, die sich mit der gleichen SessionID anmelden, über das Internet zu spielen. Die Verwaltung der Session ID's übernimmt unser TcpRelay Server, der ebenfalls dafür sorgt, dass alle Daten über den Port 80 versendet und empfangen und somit nicht von Firewalls angehalten werden.

 

Spiel starten

Die beiden Spieler starten das Spiel mit dem oben stehenden Link und geben die gleiche Session ID ein.

Zum Testen können Sie die Applikation auch zweimal auf dem gleichen Computer starten.

Beispiel im Online-Editor bearbeiten

Programmcode für lokale Bearbeitung downloaden: TcpFourInARow1.zip

Erklärungen zum untenstehenden Programmcode:

isMouseEnabled = false
Die Maus wird zu Beginn des Spieles desaktiviert und wird dann jeweils nur beim aktiven Spieler aktiviert

init()

Die Methode init() wird beim Start und bei jeder Wiederholung des Spieles aufgerufen
for (int i = 0; i < size; i++)
   for (int k = 0; k < size; k++)
     gameState[i][k] = ' '
Ähnlich wie beim Spiel TicTacToe, wird die Verteilung des Spielsteine auf dem Brett in einem zweidimensionalen Array festgehalten. Bei der Initialisierung werden alle Felder auf "leer" gesetzt.
class Token Token ist ein eigenständiges Objekt. Der Token (Spielstein) weiss, welcher Spieler ihn in die Bewegung gesetzt hat (rot oder gelb) und wie er sich bewegen muss. Er merkt, wenn er an der definitiven Position angekommen ist und teilt dies mit tokenArrived() der Applikationsklasse mit
createNewToken()
addActor(token, new Location(6, 0), Location.SOUTH)
Die Methode createNewToken() erzeugt neuen Spielstein.
Mit add Actor() wird dieser oben rechts (in der Zelle (6, 0)) positioniert
setPaintOrder(Background.class, Token.class) Die Klasse Background braucht man lediglich dazu, um die Spielsteine hinter dem gelochten Brett fallen zu lassen. Das gelochte Brett ist ein Actor mit transparenten Löchern, der bzgl. Sichtbarkeit vor den Steinen positioniert wird

tcpNode.sendMessage("" + Command.drag + mouseLoc.x)
tcpNode.sendMessage("" + Command.move + mouseLoc.x)

Um die Position der Maus und die Position des Mausklicks dem Partner mitzuteilen, werden die Commands drag bzw. move mit der aktuellen x-Koordinate gesendet

getPartner(Player player)
   return (player == Player.RED) ?
   Player.YELLOW : Player.RED
Wechselt den Spieler
tokenArrived()

Die Methode tokenArrived() deklariert, was geschehen soll, wenn ein Stein an seiner Endposition ankommt:

gameState[loc.x][loc.y] =
(tokenPlayer == Player.RED ? 'r' : 'y')

Wird ein roter Spielstein gesetzt, so wird an dieser Stelle das Zeichen r gespeichert, beim gelben Stein das Zeichen y

tcpNode.sendMessage("" + Command.ready)

Dem Partner muss mitgeteilt werden, dass der Zug fertig ist. Da die beiden Computer eventuell nicht gleich schnell sind, ist es wichtig, dass das Spiel für den nächsten Zug erst freigegeben wird, wenn beide Spieler "ready" sind

checkFour()

 

Mit der Methode checkFour() wird die aktuelle Spielsituation wie folgt überprüft: Die horizontale, vertikale und diagonale Belegung des Spielbretts wird in einem kommagetrennten Stringmuster der Form "rryy ry,yry  yy " dargestellt (leere Zellen durch Leerzeichen)

getDiagonalLocations() Die Methode getDiagonalLocations() aus der Klassenbibliothek JGameGrid gibt alle locations auf einer Diagonalen zu einer bestimmten Zelle zurück
contains() Mit der Methode contains() wird getestet, ob vier aneinander grenzende r oder y vorkommen (contains("rrrr") bzw. contains("yyyy")
gameOver() Spielende. Der Gewinner bzw. Verlierer wird in der Stauszeile angezeigt. Der Verlierer kann mit Klick auf das Brett ein neues Spiel starten

Programmcode:

// TcpFourInARow.java

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

public class TcpFourInARow extends GameGrid
  implements GGMouseListenerTcpNodeListener
{
  interface Command
  {
    char start = 's';
    char ready = 'r';
    char drag = 'd';
    char move = 'm';
    char over = 'o';
  }
  private final static int size = 7;
  private final TcpNode tcpNode = new TcpNode();
  private final String nickname = "four";
  private String sessionID = "&4()xr";
  private char[][] gameState = new char[size][size];
  private Player player;
  private Token token;
  private boolean isMyMove = false;
  private boolean isMouseEnabled = false;
  private boolean isFirst;
  private boolean isOver;
  private boolean isWinner;
  private String moveInfo =
    "Drag the token to a column, then click to drop it.";

  public TcpFourInARow()
  {
    super(size, size, 10 * size, nullnullfalse);
    setTitle("Tcp Four In A Row");
    setBgColor(Color.white);
    setSimulationPeriod(25);
    addActor(new Background(), new Location(3, 3));
    addStatusBar(30);
    setPaintOrder(Background.classToken.class);
    addMouseListener(thisGGMouse.lClick | GGMouse.move);
    isMouseEnabled = false;
    tcpNode.addTcpNodeListener(this);
    show();
    connect();
    doRun();
  }

  public void init()
  {
    isFirst = isMyMove;
    ;
    removeActors(Token.class);
    getBg().clear();
    for (int = 0; i < size; i++)
      for (int = 0; k < size; k++)
        gameState[i][k] = ' ';
    setTitle("Your color: " + (player == Player.RED ? "Red" : "Yellow"));
    setStatusText("Game started."
      + (isMyMove ? "   It is you to play." : "   Wait to play."));
    createNewToken();
    isOver = false;
  }

  public boolean mouseEvent(GGMouse mouse)
  {
    if (!isMouseEnabled)
      return true;
    Location mouseLoc = toLocation(mouse.getX(), mouse.getY());
    if (mouse.getEvent() == GGMouse.lClick)
    {
      if (isOver)
      {
        tcpNode.sendMessage("" + Command.over);
        return true;
      }
      if (getOneActorAt(new Location(mouseLoc.x, 1), Token.class) == null)
      {
        if (token == null)  // Don't use dropped token
          return true;
        token.setX(mouseLoc.x);
        isMouseEnabled = false;
        token.setActEnabled(true);
        token = null;  // Signal start of fall down
        setStatusText("Wait to play.");
        tcpNode.sendMessage("" + Command.move + mouseLoc.x);
      }
      else
        setStatusText("This column is full.");
      return true;
    }

    if (mouse.getEvent() == GGMouse.move && !isOver)
    {
      if (token != null && token.getX() != mouseLoc.x)
      {
        token.setX(mouseLoc.x);
        tcpNode.sendMessage("" + Command.drag + mouseLoc.x);
      }
    }
    return true;
  }

  private void connect()
  {
    String id = requestEntry("Enter unique game room name (more than 2 characters):");
    sessionID = sessionID + id;
    tcpNode.connect(sessionID, nickname);
  }

  public void messageReceived(String sender, String text)
  {
    char command = text.charAt(0);
    int column;
    switch (command)
    {
      case Command.start:
        player = Player.YELLOW;   // Waiting player is yellow
        init();
        break;
      case Command.move:
        column = text.charAt(1) - 48;
        token.setX(column);
        token.setActEnabled(true)// Start falling down
        token = null;  // Signal start of fall down
        setStatusText("Wait to play.");
        break;
      case Command.drag:
        column = text.charAt(1) - 48;
        if (token != null)
          token.setX(column);
        break;
      case Command.ready:
        isMouseEnabled = true;  // Master can use mouse
        break;
      case Command.over:
        if (isWinner)
          isMyMove = false;
        else
          isMyMove = true;
        init();
        if (isWinner)
          tcpNode.sendMessage("" + Command.over);  // Confirm restart after init
        break;
    }
  }

  public void nodeStateChanged(TcpNodeState state)
  {
  }

  public void statusReceived(String text)
  {
    if (text.contains("In session:--- (0)"))  // We are first player
    {
      setStatusText("Connected. Waiting for a partner...");
    }
    else if (text.contains("In session:--- (1)")// We are second player
    {
      setStatusText(moveInfo);
      isMyMove = true;  // Second player starts
      isMouseEnabled = true;
      player = Player.RED;  // Second player is red
      init();
      tcpNode.sendMessage("" + Command.start);
    }
    else if (text.contains("In session:--- ")// We are next player
    {
      setStatusText("Game in progress. Terminating now...");
      TcpTools.delay(4000);
      System.exit(0);
    }
    else if (text.equals("Disconnected:--- four")
      || text.equals("Disconnected:--- four(1)"))
    {
      setStatusText("Partner disconnected. Waiting for a partner...");
      setTitle("Tcp Four In A Row");
      isMouseEnabled = false;
      removeActors(Token.class);
      getBg().clear();
      isMyMove = false;
    }
  }

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

  protected void tokenArrived(Location loc, Player tokenPlayer)
  {
    gameState[loc.x][loc.y] = (tokenPlayer == Player.RED ? 'r' : 'y');
    isMyMove = !isMyMove;  // Switch move order
    Player winner = checkFour();
    if (winner != null)
      gameOver(winner);
    else
    {
      if (isBoardFull())
        gameOver(null);
      else
      {
        if (isMyMove)
          setStatusText(moveInfo);
        else
          tcpNode.sendMessage("" + Command.ready);

        createNewToken();
      }
    }
  }

  private Player checkFour()
  {
    // Convert gameState into string pattern
    StringBuffer pattern = new StringBuffer();

    // Horizontal
    for (int i = 0; i < size; i++)
    {
      for (int k = 0; k < size; k++)
        pattern.append(gameState[i][k]);
      pattern.append(',');
    }

    // Vertical
    for (int k = 0; k < size; k++)
    {
      for (int i = 0; i < size; i++)
        pattern.append(gameState[i][k]);
      pattern.append(',');
    }

    ArrayList<Location> locs;
    // Diagonal downwards
    for (int k = 1; k < 4; k++)
    {
      for (Location loc : getDiagonalLocations(new Location(0, k), true))
        pattern.append(gameState[loc.x][loc.y]);
      pattern.append(',');
    }
    for (int i = 1; i < 4; i++)
    {
      for (Location loc : getDiagonalLocations(new Location(i, 1), true))
        pattern.append(gameState[loc.x][loc.y]);
      pattern.append(',');
    }
    // Diagonal upwards
    for (int k = 4; k < 7; k++)
    {
      for (Location loc : getDiagonalLocations(new Location(0, k), false))
        pattern.append(gameState[loc.x][loc.y]);
      pattern.append(',');
    }
    for (int i = 1; i < 4; i++)
    {
      for (Location loc : getDiagonalLocations(new Location(i, 6), false))
        pattern.append(gameState[loc.x][loc.y]);
      pattern.append(',');
    }

    if (pattern.toString().contains("rrrr"))
    {
      return Player.RED;
    }
    if (pattern.toString().contains("yyyy"))
    {
      return Player.YELLOW;
    }
    return null;
  }

  private void gameOver(Player winner)
  {
    boolean tie = false;
    if (winner == null)
    {
      tie = true;
      winner = isFirst ? player : getPartner(player);
    }
    if (winner == player)
    {
      if (tie)
        setStatusText("It's a draw! Wait for partner to challenge you.");
      else
        setStatusText("You won! Wait for partner to challenge you.");
      isWinner = true;
    }
    else
    {
      if (tie)
        setStatusText("It's a draw! Click on the board to play again.");
      else
        setStatusText("You lost! Click on the board to play again.");
      isMouseEnabled = true;
      isWinner = false;
    }
    getBg().setFont(new Font("SansSerif", Font.BOLD, 48));
    getBg().setPaintColor(Color.red);
    getBg().drawText("Game Over"new Point(10, 55));
    isOver = true;
  }

  private void createNewToken()
  {
    if (isMyMove)
      token = new Token(this, player);
    else
      token = new Token(thisgetPartner(player));
    addActor(token, new Location(6, 0), Location.SOUTH);
    setPaintOrder(Background.class, Token.class);

  }

  private boolean isBoardFull()
  {
    return getActors().size() == 43;
  }

  private Player getPartner(Player player)
  {
    return (player == Player.RED) ? Player.YELLOW : Player.RED;
  }

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


// ------------class Token -----------

class Token extends Actor
{
  private TcpFourInARow gg;
  private Player player;
  private int nb = 0;

  public Token(TcpFourInARow gg, Player player)
  {
    super("sprites/token.png"2);
    this.gg = gg;
    this.player = player;
    show(player == Player.RED ? 1 : 0);
    setActEnabled(false);
  }

  public void act()
  {
    Location nextLoc = new Location(getX(), getY() + 1);
    if (gameGrid.getOneActorAt(nextLoc, Token.class) == null && isMoveValid())
    {
      // For smooth drop animation
      if (nb == 6)
      {
        nb = 0;
        setLocationOffset(new Point(0, 0));
        move();
      }
      else
        setLocationOffset(new Point(0, 10 * nb));
      nb++;
    }
    else
    {
      setActEnabled(false);
      gg.tokenArrived(getLocation(), player);
    }
  }
}

// ---- Player ----------------
enum Player
{
  RED,
  YELLOW
}

// -------- class Background ------------
class Background extends Actor
{

  public Background()
  {
    super(false"sprites/4inARowBG.png");
  }

  public void reset()
  {
    setOnTop();
  }
}