com.threerings.puzzle.drop.data.DropBoard Maven / Gradle / Ivy
The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.puzzle.drop.data;
import java.util.Arrays;
import java.awt.Point;
import java.awt.Rectangle;
import com.samskivert.util.StringUtil;
import com.threerings.puzzle.data.Board;
import com.threerings.puzzle.drop.client.DropControllerDelegate;
import com.threerings.puzzle.drop.util.DropBoardUtil;
import static com.threerings.puzzle.Log.log;
/**
* A class that provides for various useful logical operations to be enacted on a two-dimensional
* board and provides an easier mechanism for referencing pieces by position.
*/
public class DropBoard extends Board
implements DropPieceCodes
{
/** The rotation constant for rotation around a central piece. */
public static final int RADIAL_ROTATION = 0;
/** The rotation constant for rotation wherein the block occupies the same columns when
* rotating. */
public static final int INPLACE_ROTATION = 1;
/** An operation that does naught but clear pieces, which proves to be generally useful. */
public static final PieceOperation CLEAR_OP = new PieceOperation () {
public boolean execute (DropBoard board, int col, int row) {
board.setPiece(col, row, PIECE_NONE);
return true;
}
};
/**
* An interface to be implemented by classes that would like to apply some operation to each
* piece in a column or row segment in the board.
*/
public interface PieceOperation
{
/**
* Called for each piece in the board segment the operation is being applied to.
*
* @return true if the operation should continue to be applied if being applied to multiple
* pieces, or false if it should terminate after this application.
*/
public boolean execute (DropBoard board, int col, int row);
}
/**
* Constructs an empty drop board for use when unserializing.
*/
public DropBoard ()
{
this(null, 0, 0);
}
/**
* Constructs a drop board of the given dimensions with its pieces initialized to PIECE_NONE.
*/
public DropBoard (int bwid, int bhei)
{
this(new int[bwid*bhei], bwid, bhei);
fill(PIECE_NONE);
}
/**
* Constructs a drop board of the given dimensions with its pieces initialized to the given
* piece.
*/
public DropBoard (int bwid, int bhei, int piece)
{
this(new int[bwid*bhei], bwid, bhei);
fill(piece);
}
/**
* Constructs a drop board with the given board and dimensions.
*/
public DropBoard (int[] board, int bwid, int bhei)
{
_board = board;
_bwid = bwid;
_bhei = bhei;
}
/**
* Returns the width of the board in columns.
*/
public int getWidth()
{
return _bwid;
}
/**
* Returns the height of the board in rows.
*/
public int getHeight()
{
return _bhei;
}
/**
* Returns the piece at the given column and row in the board.
*/
public int getPiece (int col, int row)
{
if (!inBounds(col, row)) {
log.warning("Requested piece from invalid coordinate",
"col", col, "row", row, "width", _bwid, "height", _bhei);
}
try {
return _board[coordsToIndex(col, row)];
} catch (Exception e) {
log.warning("Failed getting piece", "col", col, "row", row, e);
return -1;
}
}
/**
* For boards that are always filled, this method is called to obtain pieces to fill the board.
*/
public int getNextPiece ()
{
return PIECE_NONE;
}
/**
* Returns the distance the piece at the given column and row can drop until it hits a
* non-empty piece (defined as {@link #PIECE_NONE}).
*/
public int getDropDistance (int col, int row)
{
int dist = 0;
for (int yy = row + 1; yy < _bhei; yy++) {
if (getPiece(col, yy) != PIECE_NONE) {
return dist;
}
dist++;
}
return dist;
}
/**
* Returns whether the given row in the board is empty.
*/
public boolean isRowEmpty (int row)
{
for (int col = 0; col < _bwid; col++) {
if (getPiece(col, row) != PIECE_NONE) {
return false;
}
}
return true;
}
/**
* Returns whether all of the pieces at the given coordinates can be dropped one row.
*/
public boolean isValidDrop (int[] rows, int[] cols, float pctdone)
{
int bottom = _bhei - 1;
for (int ii = 0; ii < rows.length; ii++) {
// pieces at bottom can't be dropped
if (rows[ii] >= bottom) {
return false;
}
// pieces with pieces below them can't be dropped
int row = rows[ii] + 1;
if (row >= 0 && getPiece(cols[ii], row) != PIECE_NONE) {
return false;
}
}
return true;
}
/**
* Returns true if the specified coordinate is within the bounds of the board, false if it is
* not.
*/
public boolean inBounds (int col, int row)
{
return (col >= 0 && row >= 0 && col < getWidth() && row < getHeight());
}
/**
* Returns whether the specified block in the board is empty. The block is allowed to occupy
* space off the top of the board as long as it is within the horizontal board bounds.
*
* @param col the left coordinate of the block.
* @param row the bottom coordinate of the block.
* @param wid the width of the block.
* @param hei the height of the block.
*/
public boolean isBlockEmpty (int col, int row, int wid, int hei)
{
for (int ypos = row; ypos > (row - hei); ypos--) {
for (int xpos = col; xpos < (col + wid); xpos++) {
// only allow movement off the top of the board that's within the horizontal screen
// bounds and in a column that's not topped out
if (ypos < 0) {
if ((xpos < 0 || xpos >= _bwid) || (getPiece(xpos, 0) != PIECE_NONE)) {
return false;
} else {
continue;
}
}
// don't allow movement outside the side or bottom bounds
if (xpos < 0 ||
xpos >= _bwid ||
ypos >= _bhei) {
return false;
}
// make sure no piece is present
if (getPiece(xpos, ypos) != PIECE_NONE) {
return false;
}
}
}
return true;
}
/**
* Rotates the given block in the given direction and returns its final state as (orient,
* col, row, popped)
, where orient
is the final orientation of the drop
* block; col
and row
are the final column and row coordinates,
* respectively, of the central drop block piece. popped
will be set to 1 if the
* piece was popped up, 0 otherwise.
*/
public int[] getForgivingRotation (
int[] rows, int[] cols, int orient, int dir, int rtype, float pctdone, boolean canPopup)
{
int px = cols[0], py = rows[0];
// log.info("Starting rotation", "px", px, "py", py, "orient", orient, "pctdone", pctdone);
// try rotating the block in the given direction through all four possible orientations
for (int ii = 0; ii < 4; ii++) {
int oidx = orient/2;
// adjust the position of the central piece
px += ROTATE_DX[rtype][dir][oidx];
py += ROTATE_DY[rtype][dir][oidx];
// update the orientation
orient = DropBoardUtil.getRotatedOrientation(orient, dir);
oidx = orient/2;
// because isBlockEmpty() always assumes the origin of the block is in the lower-left,
// we need to adjust the coordinates of the drop block's "central" piece accordingly
int ox = px + ORIENT_ORIGIN_DX[oidx];
int oy = py + ORIENT_ORIGIN_DY[oidx];
// if we're less than 50 percent through with our fall, we want to check our current
// coordinates for validity; if we're more, we want to check the row below our current
// coordinates
if (pctdone > 0.5) {
oy += 1;
}
// try each of three coercions: nothing, one left, one right
for (int cx : COERCE_DX) {
// check if our hypothetical new coordinates are empty
if (isBlockEmpty(ox + cx, oy, ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) {
// log.info("Block is empty", "ox", ox + "cx", cx, "oy", oy, "oidx", oidx,
// "orient", DirectionUtil.toShortString(orient), "owid", ORIENT_WIDTHS[oidx],
// "ohei", ORIENT_HEIGHTS[oidx]);
return new int[] { orient, px + cx, py, 0 };
}
}
// if our piece is facing south and we're using radial rotation then we need to try
// popping the piece up a row to check for a fit
if (canPopup && rtype == RADIAL_ROTATION && orient == SOUTH) {
// check if our hypothetical new coordinates are empty
if (isBlockEmpty(ox, oy - 1, ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) {
// log.info("Popped-up block is empty",
// "ox", ox, "oy", (oy - 1), "oidx", oidx,
// "orient", DirectionUtil.toShortString(orient), "owid", ORIENT_WIDTHS[oidx],
// "ohei", ORIENT_HEIGHTS[oidx], "bhei", _bhei);
return new int[] { orient, px, py - 1, 1 };
}
}
}
// this should never happen since even in the most tightly constrained case where the block
// is entirely surrounded by other pieces there are always two valid orientations.
log.warning("**** We're horked and couldn't rotate at all!");
// System.exit(0);
return null;
}
/**
* Returns a {@link Point} object containing the coordinates to place the bottom-left of the
* given block at after moving it the given distance on the x- and y-axes, or
* null
if the move is not valid. Note that only the final block position is
* checked.
*
* @param col the leftmost column of the block.
* @param row the bottommost row of the block.
* @param wid the width of the block.
* @param hei the height of the block.
* @param dx the distance to move the block in columns.
* @param dy the distance to move the block in rows.
* @param pctdone the percentage of the inter-block distance that the piece has fallen thus
* far.
*/
public Point getForgivingMove (int col, int row, int wid, int hei, int dx, int dy, float pctdone)
{
// try placing the block in the desired position and, failing that, at the same horizontal
// position but one row farther down
int xpos = col + dx, ypos = row + dy;
// if we're above the halfway mark, we check our current neighbors to see if we can move
// there; if we're below the halfway mark we check the next row down
if (pctdone >= 0.5) {
ypos += 1;
}
// if the block we wish to occupy is empty, we're all good
return (isBlockEmpty(xpos, ypos, wid, hei)) ? new Point(xpos, row + dy) : null;
}
/**
* Populates the given array with the column levels for this board.
*/
public void getColumnLevels (byte[] columns)
{
int bwid = getWidth(), bhei = getHeight();
for (int col = 0; col < bwid; col++) {
int dist = getDropDistance(col, -1);
columns[col] = (byte)(bhei - dist);
}
}
/**
* Called by the {@link DropControllerDelegate} when it's time to apply a rising row of pieces
* to the board. Shifts all of the pieces in the given board up one row and places the given
* row of pieces at the bottom of the board.
*/
public void applyRisingPieces (int[] pieces)
{
// shift all pieces up one row
int end = _bhei - 1;
for (int yy = 0; yy < end; yy++) {
for (int xx = 0; xx < _bwid; xx++) {
setPiece(xx, yy, getPiece(xx, yy + 1));
}
}
// apply the row pieces to the board
int ypos = _bhei - 1;
for (int xx = 0; xx < _bwid; xx++) {
setPiece(xx, ypos, pieces[xx]);
}
}
/**
* Returns true if the specified row (which count down, with zero at the top of the board)
* contains any pieces.
*
* @param row the row to check for pieces.
* @param blankPiece the blank piece value, non-instances of which will be sought.
*/
public boolean rowContainsPieces (int row, int blankPiece)
{
for (int x = 0; x < _bwid; x++) {
if (getPiece(x, row) != blankPiece) {
return true;
}
}
return false;
}
/**
* Fills the board contents with the given piece.
*/
public void fill (int piece)
{
Arrays.fill(_board, piece);
}
/**
* Sets the piece at the given coordinates.
*
* @return true if the piece was set, false if it was invalid.
*/
public boolean setPiece (int col, int row, int piece)
{
if (col >= 0 && row >= 0 && col < _bwid && row < _bhei) {
_board[coordsToIndex(col, row)] = piece;
return true;
} else {
log.warning("Attempt to set piece outside board bounds",
"col", col, "row", row, "p", piece);
return false;
}
}
/**
* Sets the pieces within the specified rectangle to the given piece.
*/
public void setRect (int x, int y, int width, int height, int piece)
{
for (int yy = y; yy > (y - height); yy--) {
for (int xx = x; xx < (x + width); xx++) {
setPiece(xx, yy, piece);
}
}
}
/**
* Sets the pieces in the given board segment to the specified piece.
*
* @param dir the direction of the segment; one of {@link #HORIZONTAL} or {@link #VERTICAL}.
* @param col the starting column of the segment.
* @param row the starting row of the segment.
* @param len the length of the segment in pieces.
* @param piece the piece to set in the segment.
*
* @return false if the segment was only partially applied because some pieces were outside
* the bounds of the board, true if it was completely applied.
*/
public boolean setSegment (int dir, int col, int row, int len, int piece)
{
_setPieceOp.init(piece);
applyOp(dir, col, row, len, _setPieceOp);
return !_setPieceOp.getError();
}
/**
* Sets the pieces in the given board segment to the specified pieces.
*
* @param dir the direction of the segment; one of {@link #HORIZONTAL} or {@link #VERTICAL}.
* @param col the starting column of the segment.
* @param row the starting row of the segment.
* @param pieces the pieces to set in the segment.
*/
public void setSegment (int dir, int col, int row, int[] pieces)
{
_setSegmentOp.init(dir, pieces);
applyOp(dir, col, row, pieces.length, _setSegmentOp);
}
/**
* Applies a specified {@link PieceOperation} to all pieces in the specified row or column
* starting at the specified coordinates and spanning the remainder of the row or column
* (depending on the application direction) in the board.
*
* @param dir the direction to iterate in; one of {@link #HORIZONTAL} or {@link #VERTICAL}.
* @param col the starting column of the segment.
* @param row the starting row of the segment.
* @param op the piece operation to apply to each piece.
*/
public void applyOp (int dir, int col, int row, PieceOperation op)
{
int len = (dir == HORIZONTAL) ? _bwid - col : row + 1;
applyOp(dir, col, row, len, op);
}
/**
* Applies a specified {@link PieceOperation} to all pieces in a row or column segment
* starting at the specified coordinates and of the specified length in the board.
*
* @param dir the direction to iterate in; one of {@link #HORIZONTAL} or {@link #VERTICAL}.
* @param col the starting leftmost column of the segment.
* @param row the starting bottommost row of the segment.
* @param len the number of pieces in the segment.
* @param op the piece operation to apply to each piece.
*/
public void applyOp (int dir, int col, int row, int len, PieceOperation op)
{
if (dir == HORIZONTAL) {
int end = Math.min(col + len, _bwid);
for (int ii = col; ii < end; ii++) {
if (!op.execute(this, ii, row)) {
break;
}
}
} else {
int end = Math.max(row - len, -1);
for (int ii = row; ii > end; ii--) {
if (!op.execute(this, col, ii)) {
break;
}
}
}
}
/**
* Applies a specified {@link PieceOperation} to the specified piece in the board.
*
* @param col the column of the piece.
* @param row the row of the piece.
* @param op the piece operation to apply to the piece.
*/
public void applyOp (int col, int row, PieceOperation op)
{
op.execute(this, col, row);
}
@Override
public void dump ()
{
dumpAndCompare(null);
}
@Override
public void dumpAndCompare (Board other)
{
if (other != null && !(other instanceof DropBoard)) {
throw new IllegalArgumentException("Can't compare drop board to non-drop-board.");
}
DropBoard dother = (DropBoard)other;
int padwid = getPadWidth();
if (other != null) {
// padwid = (padwid * 2) + 1;
padwid *= 2;
}
for (int y = 0; y < _bhei; y++) {
StringBuilder buf = new StringBuilder();
for (int x = 0; x < _bwid; x++) {
int piece = getPiece(x, y);
String str = formatPiece(piece);
if (dother != null) {
int opiece = dother.getPiece(x, y);
if (opiece != piece) {
str += "|" + formatPiece(opiece);
}
}
buf.append(StringUtil.pad(str, padwid));
}
System.err.println(buf.toString());
}
}
@Override
public String toString ()
{
StringBuilder buf = new StringBuilder();
buf.append("[wid=").append(_bwid);
buf.append(", hei=").append(_bhei);
return buf.append("]").toString();
}
@Override
public boolean equals (Board other)
{
// make sure we're comparing the same class type
if (!this.getClass().getName().equals(other.getClass().getName())) {
throw new IllegalArgumentException(
"Can't compare board of different class types [src=" + this.getClass().getName() +
", other=" + other.getClass().getName() + "].");
}
// we're certainly not equal if our dimensions differ
DropBoard dother = (DropBoard)other;
if (dother.getWidth() != _bwid ||
dother.getHeight() != _bhei) {
return false;
}
// check each board piece
for (int xx = 0; xx < _bwid; xx++) {
for (int yy = 0; yy < _bhei; yy++) {
if (getPiece(xx, yy) != dother.getPiece(xx, yy)) {
return false;
}
}
}
// we're equal
return true;
}
/**
* Returns whether the given coordinates are within the board bounds.
*/
public boolean isValidPosition (int x, int y)
{
return (x >= 0 &&
y >= 0 &&
x < _bwid &&
y < _bhei);
}
/**
* Returns the bounds of this board. Note that a single rectangle is re-used internally and so
* the caller should not modify the returned rectangle.
*/
public Rectangle getBounds ()
{
if (_bounds == null) {
_bounds = new Rectangle(0, 0, _bwid, _bhei);
}
return _bounds;
}
/**
* Returns the size of the board in pieces.
*/
public int size ()
{
return (_bwid*_bhei);
}
/**
* Copies the contents of this board directly into the supplied board, overwriting the
* destination board in its entirety.
*/
public void copyInto (DropBoard board)
{
// make sure the target board is a valid target
if (board.getWidth() != _bwid || board.getHeight() != _bhei) {
log.warning("Can't copy board into destination board with different dimensions",
"src", this, "dest", board);
return;
}
// copy our pieces directly into the board, avoiding any unsightly object allocation which
// is largely the point of this method, after all.
int[] dest = board.getBoard();
System.arraycopy(_board, 0, dest, 0, (_bwid*_bhei));
}
/**
* Returns the raw board data associated with this board. One shouldn't fiddle about with this
* unless one knows what one is doing.
*/
public int[] getBoard ()
{
return _board;
}
/**
* Sets the board data and board dimensions.
*/
public void setBoard (int[] board, int bwid, int bhei)
{
_board = board;
_bwid = bwid;
_bhei = bhei;
}
/**
* Sets the board pieces.
*/
public void setBoard (int[] board)
{
int size = size();
if (board.length < size) {
log.warning("Attempt to set board with invalid data size",
"len", board.length, "expected", size);
return;
}
_board = board;
}
@Override
public DropBoard clone ()
{
DropBoard board = (DropBoard)super.clone();
board._board = _board.clone();
return board;
}
/**
* Converts from column & row to an index into our board array.
*/
protected int coordsToIndex (int col, int row)
{
return (row * _bwid) + col;
}
/**
* Converts from an index into our board array to a column.
*/
protected int indexToCol (int idx)
{
return idx % _bwid;
}
/**
* Converts from an index into our board array to a row.
*/
protected int indexToRow (int idx)
{
return idx / _bwid;
}
/**
* Returns the number of characters to which a single piece should be padded when dumping the
* board for debugging purposes.
*/
protected int getPadWidth ()
{
return DEFAULT_PAD_WIDTH;
}
/**
* Returns a string representation of the given piece for use when dumping the board.
*/
protected String formatPiece (int piece)
{
return (piece == PIECE_NONE) ? "." : String.valueOf(piece);
}
/** An operation that sets the pieces in a board segment to a specified array of pieces. */
protected static class SetSegmentOperation implements PieceOperation
{
/**
* Sets the array of pieces to be placed in the board segment.
*/
public void init (int dir, int[] pieces) {
_dir = dir;
_pieces = pieces;
_idx = (dir == HORIZONTAL) ? _pieces.length - 1 : 0;
}
// documentation inherited
public boolean execute (DropBoard board, int col, int row) {
if (_dir == HORIZONTAL) {
board.setPiece(col, row, _pieces[_idx--]);
} else {
board.setPiece(col, row, _pieces[_idx++]);
}
return true;
}
/** The orientation in which the pieces are to be placed. */
protected int _dir;
/** The current piece index. */
protected int _idx;
/** The pieces to set in the board. */
protected int[] _pieces;
}
/** An operation that sets all pieces to a specified piece. */
protected static class SetPieceOperation implements PieceOperation
{
/**
* Sets the piece to be placed in the board segment.
*/
public void init (int piece) {
_piece = piece;
_error = false;
}
/**
* Returns true if we attempted to set a piece outside the bounds of the board during the
* course of our operation.
*/
public boolean getError () {
return _error;
}
// documentation inherited
public boolean execute (DropBoard board, int col, int row) {
if (!board.setPiece(col, row, _piece)) {
_error = true;
}
return true;
}
/** The piece to set in the board. */
protected int _piece;
/** Set to true if an error occurred setting a piece. */
protected boolean _error;
}
/** The board data. */
protected int[] _board;
/** The board dimensions in pieces. */
protected int _bwid, _bhei;
/** The bounds of this board. */
protected transient Rectangle _bounds;
// used to reconfigure the block when rotating it
protected static final int[][][] ROTATE_DX = {
// W N E S W N E S
{{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL
{{ -1, 1, 0, 0 }, { -1, 0, 0, 1 }}, // INPLACE
// CCW CW
};
// used to reconfigure the block when rotating it
protected static final int[][][] ROTATE_DY = {
// W N E S W N E S
{{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL
{{ -1, 0, 0, 1 }, { 0, 0, -1, 1 }}, // INPLACE
// CCW CW
};
// used to compute the bounds of the isBlockEmpty() block based on the drop block's orientation
// and "root" block position
protected static final int[] ORIENT_WIDTHS = { 2, 1, 2, 1 };
protected static final int[] ORIENT_HEIGHTS = { 1, 2, 1, 2 };
// used to compute the origin of the isBlockEmpty() block based on the drop block's orientation
// and "root" block position
protected static final int[] ORIENT_ORIGIN_DX = { -1, 0, 0, 0 };
protected static final int[] ORIENT_ORIGIN_DY = { 0, 0, 0, 1 };
// used to coerce the block when rotating either a space to the left or right (or not at all)
protected static final int[] COERCE_DX = { 0, 1, -1 };
/** The operation used to set the pieces in a board segment. */
protected static final SetSegmentOperation _setSegmentOp = new SetSegmentOperation();
/** The operation used to set a piece in a board segment. */
protected static final SetPieceOperation _setPieceOp = new SetPieceOperation();
/** The number of characters to which each board piece should be padded when outputting for
* debug purposes. */
protected static final int DEFAULT_PAD_WIDTH = 3;
}