Reversi Game Logic

From Gardenwiki

Jump to: navigation, search

This page is part of the Reversi Tutorial.

Contents

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;
 }

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;

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);
           }
       });
   }
 
   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 + "].");
       }
   }
 
   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();
       }
   }
 
   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
   }
 
   /** Used to determine legality of moves, etc. */
   protected ReversiLogic _logic;

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);
       }
   }
 
   /** 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,
   };

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);
       }
   }
 
   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
 
       // 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);
               }
           }
       });
       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);
 
       if (color != -1) {
           _cursor.setColor(color);
           addSprite(_cursor);
       } else if (isManaged(_cursor)) {
           removeSprite(_cursor);
       }
   }
 
   /** Used to determine legal moves. */
   protected ReversiLogic _logic;

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.

Image:Real_Game_Shot.png

Next step: Polishing Things Up

Up to the Reversi Tutorial.

Personal tools