Reversi Game Logic

This page is part of the Reversi Tutorial.

Step 5. Implement the Game Logic
We've got our game state defined and our client displays it properly, but now we need to implement the actual rules of the game. Reversi is nice and simple in that regard: players take turns placing a piece of their own color on the board and any pieces "captured" by the piece they placed are flipped over to that player's color. Let's start with "players take turns"...

Turn-based Game Support
It turns out that many multiplayer games are turn-based and for that reason, the toolkit provides a basic framework for handling turn based games. Let's dive right into the changes we need to make to use that code. First we need to modify ReversiObject. It turns out that doing that in two phases is easiest. First add:

import com.threerings.util.Name; public class ReversiObject extends GameObject {     /** The username of the current turn holder or null. */     public Name turnHolder; }

Be sure to save the file when you've made your changes, and then run the special ant task to create the setter methods and constants:

% ant gendobj

Now re-open ReversiObject.java and make a few more changes:

import com.threerings.parlor.turn.data.TurnGameObject; public class ReversiObject extends GameObject implements TurnGameObject {     // from interface TurnGameObject public String getTurnHolderFieldName {         return TURN_HOLDER; }     // from interface TurnGameObject public Name getTurnHolder {         return turnHolder; }     // from interface TurnGameObject public Name[] getPlayers {         return players; } }

Now we're starting to see some actual game logic (though some of it is not yet implemented). Note that the turn-based game framework will use the ReversiObject.turnHolder field to keep track of which player's turn it is. We haven't mentioned it yet, but GameObject (which ReversiObject extends) contains an array with the username of each of the players in the game:

/** The usernames of the players involved in this game. */   public Name[] players;

This is very handy as we can use the index into that array of a particular player's username as an identifier for them elsewhere. For example, the 0th player in the array will use black pieces and the 1st player will use white pieces.

Next some changes to ReversiManager:

import com.threerings.parlor.turn.server.TurnGameManager; import com.threerings.parlor.turn.server.TurnGameManagerDelegate; public class ReversiManager extends GameManager implements TurnGameManager {     public ReversiManager {         // we're a turn based game, so we use a turn game manager delegate addDelegate(_turndel = new TurnGameManagerDelegate(this) {             protected void setNextTurnHolder  {                  _turnIdx = 1 - _turnIdx; // TODO              }          }); }     // from interface TurnGameManager public void turnWillStart {         // nothing to do here }     // from interface TurnGameManager public void turnDidStart {         // nothing to do here }     // from interface TurnGameManager public void turnDidEnd {         // TODO: figure out when to end the game }     /** Handles our turn based game flow. */     protected TurnGameManagerDelegate _turndel; }

The TurnGameManagerDelegate manages the flow of the game as it switches from one players turn to another. When a turn ends, we receive the turnDidEnd callback, where we check to see if the game should be ended and do so. If we don't end the game, then the TurnGameManagerDelegate will call setNextTurnHolder to find out whose turn is next.

We need to make our ReversiBoardView visible to our ReversiController. Edit ReversiPanel.java and change the protected _bview to a public bview:

public class ReversiPanel extends PlacePanel {     /** The board view. */     public ReversiBoardView bview; public ReversiPanel (ToyBoxContext ctx, ReversiController ctrl) {         super(ctrl); _ctx = ctx; // this is used to look up localized strings MessageBundle msgs = _ctx.getMessageManager.getBundle("reversi"); // give ourselves a wee bit of a border setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); setLayout(new BorderLayout); // create and add our board view add(bview = new ReversiBoardView(ctx, ctrl), BorderLayout.CENTER); // ...      protected ReversiBoardView _bview;  }

Then there's a bit of wiring up that we need to do in ReversiController:

import com.threerings.util.Name; import com.threerings.parlor.turn.client.TurnGameController; import com.threerings.parlor.turn.client.TurnGameControllerDelegate; public class ReversiController extends GameController implements TurnGameController {     public ReversiController {         addDelegate(_turndel = new TurnGameControllerDelegate(this)); }     // from interface TurnGameController public void turnDidChange (Name turnHolder) {         // if it's our turn, activate piece placement _panel.bview.setPlacingMode(_turndel.isOurTurn ? _color : -1); } public void willEnterPlace (PlaceObject plobj) {         super.willEnterPlace(plobj); // get a casted reference to our game object _gameobj = (ReversiObject)plobj; // determine our piece color (-1 if we're not a player) _color = _gameobj.getPlayerIndex(((ToyBoxContext)_ctx).getUsername); // if it's our turn, activate piece placement _panel.bview.setPlacingMode(_turndel.isOurTurn ? _color : -1); }     /** Handles turn-game related stuff. */     protected TurnGameControllerDelegate _turndel; /** Our piece color. */     protected int _color;</b> }

The TurnGameControllerDelegate will call turnDidChange automatically when the turn changes and we use that to decide when to turn on piece placement mode in our board view. Notice that we also turn on piece placement mode in willEnterPlace if it is our turn. If a player's client crashed and they reconnected to the server and rejoined their game in progress, it is possible for them to pick up where they left off, and so we facilitate that by handling the case where a player enters the game and its already started and it is their turn.

Implementing the Game Logic
The time has come to actually write the interesting code that implements the logic of Reversi. This code will tell us whether a particular move is legal and will handle flipping over pieces when a legal move is made. Sometimes a game is simple enough that the logic can be included directly in the GameObject but in this case we want to use a different representation of the game state to do our logic calculations, so we separate it out into a class that is used in conjunction with the ReversiObject.

Go ahead and create ReversiLogic.java:

package com.samskivert.reversi;

import java.util.ArrayList; import java.util.Arrays;

/** * Performs some analysis of Reversi board state to determine move legality and * piece flipping. */ public class ReversiLogic {   public ReversiLogic (int size) {       _size = size; _state = new int[size*size]; }

public void setState (Iterable<ReversiObject.Piece> pieces) {       Arrays.fill(_state, -1); for (ReversiObject.Piece piece : pieces) { _state[piece.y * _size + piece.x] = piece.owner; }   }

/**    * Returns the index into the {@link ReversiObject#players} array of the * player to whom control should transition. */   public int getNextTurnHolderIndex (int curTurnIdx) {       // if the next player can move, they're up        if (hasLegalMoves(1-curTurnIdx)) { return 1-curTurnIdx; }

// otherwise see if the current player can still move if (hasLegalMoves(curTurnIdx)) { return curTurnIdx; }

// otherwise the game is over return -1; }

/**    * Returns true if the supplied piece represents a legal move for the * owning player. */   public boolean isLegalMove (ReversiObject.Piece piece) {       // disallow moves on out of bounds and already occupied spots if (!inBounds(piece.x, piece.y) || getColor(piece.x, piece.y) != -1) { return false; }

// determine whether this piece "captures" pieces of the opposite color for (int ii = 0; ii < DX.length; ii++) { // look in this direction for captured pieces boolean sawOther = false, sawSelf = false; int x = piece.x, y = piece.y;           for (int dd = 0; dd < _size; dd++) { x += DX[ii]; y += DY[ii];

// stop when we end up off the board if (!inBounds(x, y)) { break; }

int color = getColor(x, y); if (color == -1) { break; } else if (color == 1-piece.owner) { sawOther = true; } else if (color == piece.owner) { sawSelf = true; break; }           }

// if we saw at least one other piece and one of our own, we have a           // legal move if (sawOther && sawSelf) { return true; }       }        return false; }

/**    * Returns true if the player with the specified color has legal moves. */   public boolean hasLegalMoves (int color) {       // search every board position for a legal move ReversiObject.Piece piece = new ReversiObject.Piece; piece.owner = color; for (int yy = 0; yy < _size; yy++) { for (int xx = 0; xx < _size; xx++) { if (getColor(xx, yy) != -1) { continue; }               piece.x = xx; piece.y = yy; if (isLegalMove(piece)) { return true; }           }        }        return false; }

/**    * Determines which pieces should be flipped based on the placement of the * specified piece onto the board. The pieces in question are changed to    * the appropriate color and updated in the game object. */   public void flipPieces (ReversiObject.Piece placed, ReversiObject gameobj) {       ArrayList<ReversiObject.Piece> toflip = new ArrayList<ReversiObject.Piece>;

// determine where this piece "captures" pieces of the opposite color for (int ii = 0; ii < DX.length; ii++) { // look in this direction for captured pieces int x = placed.x, y = placed.y;           for (int dd = 0; dd < _size; dd++) { x += DX[ii]; y += DY[ii];

// stop when we end up off the board if (!inBounds(x, y)) { break; }

int color = getColor(x, y); if (color == -1) { break;

} else if (color == 1-placed.owner) { // add the piece at this coordinates to our to flip list for (ReversiObject.Piece piece : gameobj.pieces) { if (piece.x == x && piece.y == y) { toflip.add(piece); break; }                   }

} else if (color == placed.owner) { // flip all the toflip pieces because we found our pair for (ReversiObject.Piece piece : toflip) { piece.owner = 1-piece.owner; gameobj.updatePieces(piece); }                   break; }           }            toflip.clear; }   }

protected final int getColor (int x, int y)   { return _state[y * _size + x]; }

protected final boolean inBounds (int x, int y)   { return x >= 0 && y >= 0 && x < _size && y < _size; }

protected int _size; protected int[] _state;

protected static final int[] DX = { -1, 0, 1, -1, 1, -1, 0, 1 }; protected static final int[] DY = { -1, -1, -1, 0, 0, 1, 1, 1 }; }

I won't go too closely into the details of how this class works other than to point out that it maintains the state of the board as an array and the calling code must first call setState to set up that array with the current state of the game (which is just a set of Piece objects) and then calls can be made to isLegalMove and so on.

Now that we have our logic class, we need to wire it into the server and the client.

Implementing the Server-side Logic
We've got the turn-game stuff wired up and we have our fancy new logic class. We can use those to finish up the main game logic. First add code to ReversiObject to assign piece ids before adding a piece:

/** * Places the supplied piece onto the board, first assigning it a unique * piece id. */   public void placePiece (Piece piece) {       // assign this piece a new unique piece id        piece.pieceId = ++_nextPieceId; // add this new piece to the set addToPieces(piece); }   /** Used to assign ids to pieces. */   protected transient int _nextPieceId;</b>

This illustrates a useful point. Since this code is only ever called on the server, we mark the _nextPieceId field as transient. This means that the framework will not transfer the contents of that field over the wire when the ReversiObject is sent to the client. All we care about is that every piece has a unique id assigned to it, so this value is never needed by the client.

Next we make some modifications to ReversiManager:

public ReversiManager {       // we're a turn based game, so we use a turn game manager delegate addDelegate(_turndel = new TurnGameManagerDelegate(this) {           protected void setNextTurnHolder  {                _logic.setState(_gameobj.pieces);                _turnIdx = _logic.getNextTurnHolderIndex(_turnIdx);</b>            }        }); }   public void placePiece (BodyObject player, ReversiObject.Piece piece) {       // update our logic with the current state of the board _logic.setState(_gameobj.pieces); // make sure it's this player's turn int pidx = _turndel.getTurnHolderIndex; if (_playerOids[pidx] != player.getOid) { System.err.println("Requested to place piece by non-turn holder " +                              "[who=" + player.who +                               ", turnHolder=" + _gameobj.turnHolder + "]."); // make sure this is a legal move } else if (_logic.isLegalMove(piece)) { // place this piece on the board _gameobj.placePiece(piece); // have our logic figure out which pieces need flipping _logic.flipPieces(piece, _gameobj); // and finally end the turn _turndel.endTurn; } else { System.err.println("Received illegal move request " +                              "[who=" + player.who +                               ", piece=" + piece + "]."); }</b> }   public void turnDidEnd {       // if neither player has legal moves, the game is over _logic.setState(_gameobj.pieces); if (!_logic.hasLegalMoves(ReversiObject.BLACK) &&           !_logic.hasLegalMoves(ReversiObject.WHITE)) { endGame; }</b> }   public void didInit {       super.didInit; // get a casted reference to our game configuration _gameconf = (ToyBoxGameConfig)_config; // create our game logic instance _logic = new ReversiLogic(8); // TODO: get board size from config</b> }   /** Used to determine legality of moves, etc. */ protected ReversiLogic _logic;</b>

We've done two things here: first we are actually assigning the next turn holder correctly, based on the rules of Reversi which are that a player must pass their turn if they have no legal moves and the game ends if no players have any legal moves remaining.

Second, our new placePiece method makes sure that we only allow the current turn holder to make a move and we validate that the move is legal. If it is legal, we add the piece to the game state (by calling ReversiObject.placePiece) and then we flip any pieces that were captured by that piece (by calling ReversiLogic.flipPieces). Then we call TurnGameManagerDelegate.endTurn which lets the turn game system know that the turn is over and it should start the next turn (or end the game if appropriate).

Set Up the Starting State
Now is a good time to add code to set the proper starting board position. A Reversi game starts with two pieces of each color in the center of the board. We override the gameWillStart method of ReversiManager to put those pieces in their proper place:

protected void gameWillStart {       super.gameWillStart; // start the game with the standard arrangement of pieces for (int ii = 0; ii < STARTERS.length; ii += 3) { ReversiObject.Piece piece = new ReversiObject.Piece; piece.x = STARTERS[ii]; piece.y = STARTERS[ii+1]; piece.owner = STARTERS[ii+2]; _gameobj.placePiece(piece); }</b> }   /** The starting set of pieces. */   protected static final int[] STARTERS = { 3, 3, ReversiObject.BLACK, 3, 4, ReversiObject.WHITE, 4, 4, ReversiObject.BLACK, 4, 3, ReversiObject.WHITE, };</b>

Using the Logic on the Client
Right now, the client lets the player click on any spot they want to make their move, but the server will reject a move request if it is not legal. We want to save the server and the player the trouble and just not let them click on spots that are not legal moves. That means we'll want to use the ReversiLogic class on the client to check whether a particular cursor position is legal or not.

This means changes to CursorSprite:

public void setPosition (int x, int y, ReversiLogic logic) {       _piece.x = x;        _piece.y = y;        _legal = logic.isLegalMove(_piece); updatePiece(_piece); }   public void paint (Graphics2D gfx) {       if (_legal) { Composite ocomp = gfx.getComposite; gfx.setComposite(_comp); super.paint(gfx); gfx.setComposite(ocomp); }</b> }   protected boolean _legal = false;

We compute whether our current cursor position is legal and we only render the cursor if the position is legal.

Then a few simple changes to ReversiBoardView:

public ReversiBoardView (ToyBoxContext ctx, ReversiController ctrl) {       super(ctx.getFrameManager); _ctx = ctx; _ctrl = ctrl; // create our logic class _logic = new ReversiLogic(8); // TODO</b> // listen for mouse motion and presses addMouseListener(new MouseAdapter {           public void mousePressed (MouseEvent e) {                ReversiObject.Piece piece = _cursor.getPiece;                if (_logic.isLegalMove(piece)) {                    _ctrl.piecePlaced(piece);                    setPlacingMode(-1);                }</b>            }        }); addMouseMotionListener(new MouseMotionAdapter {           public void mouseMoved (MouseEvent e) {                int tx = e.getX / PieceSprite.SIZE;                int ty = e.getY / PieceSprite.SIZE;                _cursor.setPosition(tx, ty, _logic);            }        }); }   public void setPlacingMode (int color) {       // update our logic with the current board state _logic.setState(_gameobj.pieces);</b> if (color != -1) { _cursor.setColor(color); addSprite(_cursor); } else if (isManaged(_cursor)) { removeSprite(_cursor); }   }    /** Used to determine legal moves. */   protected ReversiLogic _logic;</b>

We need to create our logic instance when the board view is created and we need to configure it with the current state of the board when we are put into placement mode. Lastly we need to make sure we don't try to submit a move unless it is legal.

You are now ready to play your first game of Reversi! Fire up the server and two clients and give it a shot.



Next step: Polishing Things Up

Up to the Reversi Tutorial.