Reversi User Interaction

This page is part of the Reversi Tutorial.

Step 4. Implement User Interaction
User interaction encompasses a few important parts of the game. One is converting raw mouse input into more discrete actions that make sense in the context of the game. Another is displaying feedback to the user while they are providing that input (highlighting potential moves for example). Lastly we need to translate a user's request for action into an actual change in the game state.

Converting User Input into Game Actions
Firstly, let's tackle converting raw mouse input into the main action that our players will take: picking where to place their next piece. The simplest way to do this would be to just listen for mouse clicks and when one comes in, figure out what tile the mouse is in and we're done. However, that doesn't provide very useful feedback to the player who might accidentally click on the wrong spot or might try clicking on a spot that is not a legal move.

Instead we'll provide a partially transparent piece as a cursor to let the player know which spot they are hovering the mouse over and whether or not that spot is a legal move. To do that, we're going to create a special derivation of our PieceSprite for displaying the cursor. Create a new source file called CursorSprite.java:

package com.samskivert.reversi;

import java.awt.AlphaComposite; import java.awt.Composite; import java.awt.Graphics2D;

/** * Displays a "potential move" cursor to the player. */ public class CursorSprite extends PieceSprite {   public CursorSprite {       super(new ReversiObject.Piece); _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); }

public void setColor (int color) {       _piece.owner = color; }

public void setPosition (int x, int y)   { _piece.x = x;       _piece.y = y;        updatePiece(_piece); }

public ReversiObject.Piece getPiece {       return _piece; }

@Override // from PieceSprite public void paint (Graphics2D gfx) {       Composite ocomp = gfx.getComposite; gfx.setComposite(_comp); super.paint(gfx); gfx.setComposite(ocomp); }

protected AlphaComposite _comp; }

We will use this to render a piece at 50% transparency to let a user know when the mouse is hovering over a legal move location.

Now we need to properly display the cursor based on the movement of the mouse. Add the following to ReversiBoardView:

import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; public ReversiBoardView (ToyBoxContext ctx) {       super(ctx.getFrameManager); _ctx = ctx; // listen for mouse motion addMouseMotionListener(new MouseMotionAdapter {           public void mouseMoved (MouseEvent e) {                int tx = e.getX / PieceSprite.SIZE;                int ty = e.getY / PieceSprite.SIZE;                _cursor.setPosition(tx, ty);            }        }); }   /** * Activates "placing" mode which allows the user to place a piece of the * specified color. */   public void setPlacingMode (int color) {       if (color != -1) { _cursor.setColor(color); addSprite(_cursor); } else if (isManaged(_cursor)) { removeSprite(_cursor); }   }    /** Displays a cursor when we're allowing the user to place a piece. */   protected CursorSprite _cursor = new CursorSprite;

We create a cursor sprite that we keep around all the time (even when it's not being displayed). We do this so that we can make sure the sprite is always configured with the correct mouse position. That way when it becomes a player's turn, we can just set the color of the cursor and add it to the view and it will already be in the right position.

In our mouseMoved callback we convert screen coordinates into board coordinates (which is easy because we draw everything relative to (0, 0)) and we update the current position of the cursor sprite.

Let's test this out with our ReversiBoardViewTest. Add the following line to the end of initInterface:

_view.setPlacingMode(ReversiObject.WHITE);

and then run 'ant viewtest' to see our new code in action. Works like a charm!

We're not yet displaying only legal moves, but we'll get to that later. First, let's implement click handling. Add the following code to ReversiBoardView:

import java.awt.event.MouseAdapter; public ReversiBoardView (ToyBoxContext ctx, ReversiController ctrl) {       super(ctx.getFrameManager); _ctx = ctx; _ctrl = ctrl; // listen for mouse motion and presses addMouseListener(new MouseAdapter {           public void mousePressed (MouseEvent e) {                _ctrl.piecePlaced(_cursor.getPiece);                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);            }        }); }   /** The game controller to which we dispatch user actions. */   protected ReversiController _ctrl;

Notice that we don't care about the mouse coordinates when the mouse is pressed. We know that our CursorSprite has a properly configured Piece inside of it with the current coordinates of the Piece that the user is seeing on the board, which is exactly where they want to move. So we just send the whole Piece off to the controller to let it know where the user wants to place a piece.

Now modify ReversiPanel to pass the controller reference when we're constructed:

// create and add our board view add(_bview = new ReversiBoardView(ctx, ctrl), BorderLayout.CENTER);

Then add the piecePlaced method to the ReversiController:

/** * Called when a player requests to play a piece during their turn. */   public void piecePlaced (ReversiObject.Piece piece) {       System.out.println("Piece placed +" + piece.x + "+" + piece.y + "."); }

We'll do something more exciting with that piece in a minute when we get the server involved in things.

Note that we need to make one more change, which is to update ReversiBoardViewTest to reflect the new constructor argument we added to ReversiBoardView:

protected JComponent createInterface (ToyBoxContext ctx) {       return _view = new ReversiBoardView(ctx, new ReversiController); }

Since we're testing, we just create a fake ReversiController and pass it to the view. Bear in mind that our view test harness will only take us so far. At some point we're going to have to actually run the game so that we can operate with a properly initialized controller and a real ReversiObject and what not. However, in this case, we can get away with one last use of the view test harness to see that when we click the mouse, piecePlaced prints out the coordinates at which we placed the piece and the view properly turns off piece placement mode.

Great. Now the time has come to start talking to the server and making real changes to the game state.

ReversiManager
We've worked with just about every piece of code in the skeleton game now except one, the ReversiManager. This code runs on the server during a Reversi game and handles the communication between the clients and effects actual changes to the game state.

While it would be possible to allow the clients to modify the game state directly and have the server be a simple message passing system that didn't know anything about the game in question, that tends to be undesirable because then you have to trust that the clients are not cheating. If no one cares about your game, they're probably not going to cheat at it, but if they do care about your game, someone will eventually try to cheat. Since we hope to make games that people care about, we try to tackle this problem from the start.

The first line of defense against client cheating is not to trust the client. Instead of allowing the client to change the game state directly, the client makes requests to the server indicating its desires and the server validates that the client is requesting a legal move and a legal time and that everything is in order and then the server makes the changes to the game state. This turns out to be a pretty easy way to structure the game and because we have some code running on the server, we can rely on that code to do other things we wouldn't want to trust the clients to do like generate random numbers, shuffle cards, and so forth.

In Reversi, the server doesn't need to do much other than to make sure a player is requesting a legal move and then add the requested piece to the board and "flip" any pieces that were affected by that placement.

Previously we got all the way up to the point where the client knew where the player wanted to move, so now let's see how it forwards that move request up to the server. First add the following code to ReversiController.java:

public void piecePlaced (ReversiObject.Piece piece) {       // tell the server we want to place our piece here _gameobj.manager.invoke("placePiece", piece); }

Calling _gameobj.manager.invoke does a bunch of magic, the result of which is that a method gets called on the ReversiManager on the server. Let's add that method. Open ReversiManager.java in your editor, and add:

import com.threerings.crowd.data.BodyObject; /**    * Called when a client sends a request to place a piece on the board. */   public void placePiece (BodyObject player, ReversiObject.Piece piece) {       // for now we just blindly add the piece to the board, yee haw! _gameobj.addToPieces(piece); }

Notice the signature of the method we just added. It's named "placePiece" which is the name that we passed to the invoke call on the client. It also takes a Piece argument and we're passing a Piece argument to the invoke call on the client. But it has a BodyObject argument before the Piece argument. All manager methods that are called by the server in this way will have a BodyObject argument followed by whatever arguments are passed from the client. The BodyObject is an object maintained by the server that represents the client that sent the remote method call. The manager can look at the BodyObject to determine which player issued the method call and take action appropriately. We'll get into this more later when we talk about validating client requests.

But first, we've reached a very exciting milestone. It should be possible for us to now run two clients, have them start a game with one another and have one of them place a piece on the board and have the other client see that happen. We need to slip in one little bit of hackery to make this work, we need to turn on piece placement mode in ReversiBoardView:

public void willEnterPlace (PlaceObject plobj) {       _gameobj = (ReversiObject)plobj; _gameobj.addListener(this); // create sprites for all pieces currently on the board for (ReversiObject.Piece piece : _gameobj.pieces) { addPieceSprite(piece); }       // temporary hackery to allow us to place a piece setPlacingMode(ReversiObject.BLACK);</b> }

Now open up three terminals and run:

% ant dist server

% ant -Dusername=laurel client

% ant -Dusername=hardy client

Create a table, join it with each of the clients and you should see the game display come up with your board view showing and with each client ready to place a piece onto the board. Click in one of the clients and you should see that piece show up in the other client's display. Something like this:



It's not the prettiest thing in the world, but it's exciting to see two game clients actually talking to one another to do game-related things.

From here, we need to explain how to handle players taking turns and we need to go back and properly implement our server side logic. That is all covered in the next section on implementing the game logic.

Next step: Implement the Game Logic

Up to the Reversi Tutorial.