/**
* Programmer: Jacob Scott
* Program Name: ChessBoard
* Description: for handling the chess board
* Date: Jul 28, 2011
*/
package me.desht.chesscraft.chess;
import chesspresso.Chess;
import chesspresso.position.Position;
import me.desht.chesscraft.ChessCraft;
import me.desht.chesscraft.Messages;
import me.desht.chesscraft.chess.pieces.ChessSet;
import me.desht.chesscraft.chess.pieces.ChessSetFactory;
import me.desht.chesscraft.chess.pieces.ChessStone;
import me.desht.chesscraft.chess.pieces.PieceDesigner;
import me.desht.chesscraft.enums.BoardRotation;
import me.desht.chesscraft.enums.HighlightStyle;
import me.desht.chesscraft.exceptions.ChessException;
import me.desht.dhutils.LogUtils;
import me.desht.dhutils.PersistableLocation;
import me.desht.dhutils.block.CraftMassBlockUpdate;
import me.desht.dhutils.block.MassBlockUpdate;
import me.desht.dhutils.cuboid.Cuboid;
import me.desht.dhutils.cuboid.Cuboid.CuboidDirection;
import org.bukkit.DyeColor;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.material.MaterialData;
import org.bukkit.material.Wool;
public class ChessBoard {
// the center of the A1 square (lower-left on the board)
private final PersistableLocation a1Center;
// the lower-left-most part (outer corner) of the a1 square (depends on rotation)
private final PersistableLocation a1Corner;
// the upper-right-most part (outer corner) of the h8 square (depends on rotation)
private final PersistableLocation h8Corner;
// region that defines the board itself - just the squares
private final Cuboid board;
// area above the board squares
private final Cuboid areaBoard;
// region outset by the frame
private final Cuboid frameBoard;
// area <i>above</i> the board
private final Cuboid aboveFullBoard;
// the full board region (board, frame, and area above)
private final Cuboid fullBoard;
// this is the direction the control panel faces
// (once upon a time, it was the direction white faced, but directions aren't what they used to be...)
private final BoardRotation rotation;
// if highlight_last_move, what squares (indices) are highlighted
private int fromSquare = Chess.NO_SQUARE, toSquare = Chess.NO_SQUARE;
// the currently selected square, if any
private int selectedSquare = Chess.NO_SQUARE;
// settings related to how the board is drawn
private BoardStyle boardStyle = null;
// the set of chess pieces that go with this board
private ChessSet chessSet = null;
// are we in designer mode?
private PieceDesigner designer = null;
// note a full redraw needed if the board or piece style change
private boolean redrawNeeded;
/**
* Board constructor.
*
* @param origin
* @param rotation
* @param boardStyleName
* @param pieceStyleName
* @throws ChessException
*/
public ChessBoard(Location origin, BoardRotation rotation, String boardStyleName, String pieceStyleName) throws ChessException {
setBoardStyle(boardStyleName);
setChessSet(pieceStyleName != null && !pieceStyleName.isEmpty() ? pieceStyleName : boardStyle.getPieceStyleName());
this.rotation = rotation;
a1Center = new PersistableLocation(origin);
a1Corner = initA1Corner(origin, rotation);
h8Corner = initH8Corner(a1Corner.getLocation());
board = new Cuboid(a1Corner.getLocation(), h8Corner.getLocation());
areaBoard = board.expand(CuboidDirection.Up, boardStyle.getHeight());
frameBoard = board.outset(CuboidDirection.Horizontal, boardStyle.getFrameWidth());
aboveFullBoard = frameBoard.shift(CuboidDirection.Up, 1).expand(CuboidDirection.Up, boardStyle.getHeight() - 1);
fullBoard = frameBoard.expand(CuboidDirection.Up, boardStyle.getHeight() + 1);
validateBoardPosition();
}
private PersistableLocation initA1Corner(Location origin, BoardRotation rotation) {
Location a1 = new Location(origin.getWorld(), origin.getBlockX(), origin.getBlockY(), origin.getBlockZ());
int offset = boardStyle.getSquareSize() / 2;
switch (rotation) {
case NORTH:
a1.add(offset, 0, offset); break;
case EAST:
a1.add(-offset, 0, offset); break;
case SOUTH:
a1.add(-offset, 0, -offset); break;
case WEST:
a1.add(offset, 0, -offset); break;
}
return new PersistableLocation(a1);
}
private PersistableLocation initH8Corner(Location a1) {
Location h8 = new Location(a1.getWorld(), a1.getBlockX(), a1.getBlockY(), a1.getBlockZ());
int size = boardStyle.getSquareSize();
switch (rotation) {
case NORTH:
h8.add(-size * 8 + 1, 0, -size * 8 + 1); break;
case EAST:
h8.add(size * 8 - 1, 0, -size * 8 + 1); break;
case SOUTH:
h8.add(size * 8 - 1, 0, size * 8 - 1); break;
case WEST:
h8.add(-size * 8 + 1, 0, size * 8 - 1); break;
}
return new PersistableLocation(h8);
}
/**
* Ensure this board isn't built too high and doesn't intersect any other boards
*
* @throws ChessException if an intersection would occur
*/
private void validateBoardPosition() throws ChessException {
Cuboid bounds = getFullBoard();
if (bounds.getUpperSW().getBlock().getLocation().getY() > bounds.getUpperSW().getWorld().getMaxHeight()) {
throw new ChessException(Messages.getString("BoardView.boardTooHigh")); //$NON-NLS-1$
}
for (BoardView bv : BoardViewManager.getManager().listBoardViews()) {
if (bv.getA1Square().getWorld() != bounds.getWorld()) {
continue;
}
for (Block b : bounds.corners()) {
if (bv.getOuterBounds().contains(b)) {
throw new ChessException(Messages.getString("BoardView.boardWouldIntersect", bv.getName())); //$NON-NLS-1$
}
}
}
}
public Location getA1Center() {
return a1Center.getLocation();
}
/**
* @return the outer-most corner of the A1 square
*/
public Location getA1Corner() {
return a1Corner.getLocation();
}
/**
* @return the outer-most corner of the H8 square
*/
public Location getH8Corner() {
return h8Corner.getLocation();
}
/**
* @return the region that defines the board itself - just the squares
*/
public Cuboid getBoard() {
return board;
}
/**
* @return the region outset by the frame
*/
public Cuboid getFrameBoard() {
return frameBoard;
}
/**
* @return the the full board region (board, frame, and area above)
*/
public Cuboid getFullBoard() {
return fullBoard;
}
/**
* @return the name of the board style used
*/
public String getBoardStyleName() {
return boardStyle != null ? boardStyle.getName() : null;
}
/**
* @return the name of the piece style being used
*/
public String getPieceStyleName() {
return chessSet != null ? chessSet.getName() : null;
}
/**
* @return the BoardStyle object associated with this chessboard
*/
public BoardStyle getBoardStyle() {
return boardStyle;
}
/**
* @return the ChessSet object associated with this chessboard
*/
public ChessSet getChessSet() {
return chessSet;
}
/**
* @return the direction of the board (from the white to black sides of the
* board)
*/
public BoardRotation getRotation() {
return rotation;
}
public boolean isDesigning() {
return designer != null;
}
public PieceDesigner getDesigner() {
return designer;
}
public void setDesigner(PieceDesigner designer) {
this.designer = designer;
}
public final void setChessSet(String pieceStyle) throws ChessException {
if (boardStyle == null) {
return;
}
ChessSet newChessSet = ChessSetFactory.getChessSet(pieceStyle);
boardStyle.verifyCompatibility(newChessSet);
chessSet.syncToPosition(null, this);
chessSet = newChessSet;
redrawNeeded = true;
}
public final void setBoardStyle(String boardStyleName) throws ChessException {
BoardStyle newStyle = BoardStyle.loadStyle(boardStyleName);
setBoardStyle(newStyle, boardStyle == null || !(boardStyle.getName().equals(newStyle.getName())));
}
public final void setBoardStyle(BoardStyle newStyle, boolean changeChessSet) {
// We don't allow any changes to the board's dimensions; only changes to
// the appearance of the board.
if (boardStyle != null &&
(boardStyle.getFrameWidth() != newStyle.getFrameWidth() ||
boardStyle.getSquareSize() != newStyle.getSquareSize() ||
boardStyle.getHeight() != newStyle.getHeight())) {
throw new ChessException("New board style dimensions do not match the current board dimensions");
}
boardStyle = newStyle;
if (changeChessSet) {
chessSet = ChessSetFactory.getChessSet(boardStyle.getPieceStyleName());
}
redrawNeeded = true;
}
/**
* @return the selectedSquare
*/
public int getSelectedSquare() {
return selectedSquare;
}
/**
* @param selectedSquare the selectedSquare to set
*/
public void setSelectedSquare(int selectedSquare) {
if (this.selectedSquare != Chess.NO_SQUARE) {
// un-highlight the previous selection
paintBoardSquare(this.selectedSquare, null);
}
this.selectedSquare = selectedSquare;
if (this.selectedSquare != Chess.NO_SQUARE) {
highlightSelectedBoardSquare(selectedSquare);
}
}
/**
* @return the redrawNeeded
*/
public boolean isRedrawNeeded() {
return redrawNeeded;
}
/**
* Reload the board and piece styles in-use
*
* @throws ChessException if board or piece style cannot be loaded
*/
void reloadStyles() throws ChessException {
if (boardStyle != null) {
setBoardStyle(boardStyle.getName());
}
if (chessSet != null) {
setChessSet(chessSet.getName());
}
}
/**
* Paint everything! (board, frame, enclosure, control panel, lighting)
*/
void paintAll(MassBlockUpdate mbu) {
if (designer == null) {
fullBoard.fill(0, (byte)0, mbu);
}
paintEnclosure(mbu);
paintFrame(mbu);
paintBoard(mbu);
if (designer != null) {
paintDesignIndicators(mbu);
}
if (fromSquare >= 0 || toSquare >= 0) {
highlightSquares(fromSquare, toSquare);
}
fullBoard.forceLightLevel(boardStyle.getLightLevel());
redrawNeeded = false;
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(fullBoard);
}
}
private void paintEnclosure(MassBlockUpdate mbu) {
aboveFullBoard.getFace(CuboidDirection.North).fill(boardStyle.getEnclosureMaterial(), mbu);
aboveFullBoard.getFace(CuboidDirection.East).fill(boardStyle.getEnclosureMaterial(), mbu);
aboveFullBoard.getFace(CuboidDirection.South).fill(boardStyle.getEnclosureMaterial(), mbu);
aboveFullBoard.getFace(CuboidDirection.West).fill(boardStyle.getEnclosureMaterial(), mbu);
fullBoard.getFace(CuboidDirection.Up).fill(boardStyle.getEnclosureMaterial(), mbu);
if (!boardStyle.getEnclosureMaterial().equals(boardStyle.getStrutsMaterial())) {
paintStruts(mbu);
}
}
private void paintStruts(MassBlockUpdate mbu) {
MaterialData struts = boardStyle.getStrutsMaterial();
// vertical struts at the frame corners
Cuboid c = new Cuboid(frameBoard.getLowerNE()).shift(CuboidDirection.Up, 1).expand(CuboidDirection.Up, boardStyle.getHeight());
c.fill(struts, mbu);
c = c.shift(CuboidDirection.South, frameBoard.getSizeX() - 1);
c.fill(struts, mbu);
c = c.shift(CuboidDirection.West, frameBoard.getSizeZ() - 1);
c.fill(struts, mbu);
c = c.shift(CuboidDirection.North, frameBoard.getSizeZ() - 1);
c.fill(struts, mbu);
// horizontal struts along roof edge
Cuboid roof = frameBoard.shift(CuboidDirection.Up, boardStyle.getHeight() + 1);
roof.getFace(CuboidDirection.East).fill(struts, mbu);
roof.getFace(CuboidDirection.North).fill(struts, mbu);
roof.getFace(CuboidDirection.West).fill(struts, mbu);
roof.getFace(CuboidDirection.South).fill(struts, mbu);
}
private void paintFrame(MassBlockUpdate mbu) {
int fw = boardStyle.getFrameWidth();
MaterialData fm = boardStyle.getFrameMaterial();
frameBoard.getFace(CuboidDirection.West).expand(CuboidDirection.East, fw - 1).fill(fm, mbu);
frameBoard.getFace(CuboidDirection.South).expand(CuboidDirection.North, fw - 1).fill(fm, mbu);
frameBoard.getFace(CuboidDirection.East).expand(CuboidDirection.West, fw - 1).fill(fm, mbu);
frameBoard.getFace(CuboidDirection.North).expand(CuboidDirection.South, fw - 1).fill(fm, mbu);
}
private void paintBoard(MassBlockUpdate mbu) {
for (int sqi = 0; sqi < Chess.NUM_OF_SQUARES; sqi++ ) {
paintBoardSquare(sqi, mbu);
}
}
private void paintBoardSquare(int sqi, MassBlockUpdate mbu) {
paintBoardSquare(Chess.sqiToRow(sqi), Chess.sqiToCol(sqi), mbu);
}
private void paintBoardSquare(int row, int col, MassBlockUpdate mbu) {
Cuboid square = getSquare(row, col);
boolean black = (col + (row % 2)) % 2 == 0;
if (mbu == null) {
square.fill(black ? boardStyle.getBlackSquareMaterial() : boardStyle.getWhiteSquareMaterial());
} else {
square.fill(black ? boardStyle.getBlackSquareMaterial() : boardStyle.getWhiteSquareMaterial(), mbu);
}
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(square);
}
}
private void highlightBoardSquare(int sqi) {
highlightBoardSquare(Chess.sqiToRow(sqi), Chess.sqiToCol(sqi));
}
private void highlightSelectedBoardSquare(int sqi) {
Cuboid sq = getSquare(Chess.sqiToRow(sqi), Chess.sqiToCol(sqi));
MaterialData squareHighlightColor = boardStyle.getSelectedHighlightMaterial();
sq.getFace(CuboidDirection.East).fill(squareHighlightColor);
sq.getFace(CuboidDirection.North).fill(squareHighlightColor);
sq.getFace(CuboidDirection.West).fill(squareHighlightColor);
sq.getFace(CuboidDirection.South).fill(squareHighlightColor);
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(sq);
}
}
private void highlightBoardSquare(int row, int col) {
Cuboid sq = getSquare(row, col);
MaterialData squareHighlightColor = boardStyle.getHighlightMaterial(col + (row % 2) % 2 == 1);
switch (boardStyle.getHighlightStyle()) {
case EDGES:
sq.getFace(CuboidDirection.East).fill(squareHighlightColor);
sq.getFace(CuboidDirection.North).fill(squareHighlightColor);
sq.getFace(CuboidDirection.West).fill(squareHighlightColor);
sq.getFace(CuboidDirection.South).fill(squareHighlightColor);
break;
case CORNERS:
for (Block b : sq.corners()) {
b.setTypeIdAndData(squareHighlightColor.getItemTypeId(), squareHighlightColor.getData(), false);
}
break;
case CHECKERED:
case CHEQUERED:
for (Block b : sq) {
if ((b.getLocation().getBlockX() - b.getLocation().getBlockZ()) % 2 == 0) {
b.setTypeIdAndData(squareHighlightColor.getItemTypeId(), squareHighlightColor.getData(), false);
}
}
break;
default:
break;
}
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(sq);
}
}
/**
* Paint all chess pieces according to the given Chesspresso Position.
*
* @param chessGame
*/
void paintChessPieces(Position chessGame) {
if (chessSet.hasMovablePieces()) {
chessSet.syncToPosition(chessGame, this);
} else {
for (int row = 0; row < 8; ++row) {
for (int col = 0; col < 8; ++col) {
paintChessPiece(row, col, chessGame.getStone(row * 8 + col));
}
}
}
}
/**
* Draw the chess piece represented by stone into the given row and column. The actual blocks
* drawn depend on the board's current chess set.
*
* @param row
* @param col
* @param stone
*/
public void paintChessPiece(int row, int col, int stone) {
// for entity sets, just check that the entity is still at (row,col)
// for block sets, get the stone and paste its data into the region at (row,col)
ChessSet cSet = designer != null ? designer.getChessSet() : chessSet;
if (cSet.hasMovablePieces()) {
// we don't paint movable pieces; moveChessPiece() can handle those
return;
}
Cuboid region = getPieceRegion(row, col);
MassBlockUpdate mbu = CraftMassBlockUpdate.createMassBlockUpdater(ChessCraft.getInstance(), getBoard().getWorld());
region.fill(0, (byte)0, mbu);
if (stone != Chess.NO_STONE) {
ChessStone cStone = cSet.getStone(stone, getRotation());
if (cStone != null) {
cStone.paint(region, mbu);
} else {
LogUtils.severe("unknown chess stone " + stone);
}
}
region.expand(CuboidDirection.Down, 1).forceLightLevel(boardStyle.getLightLevel());
mbu.notifyClients();
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(region);
}
}
public void moveChessPiece(int fromSqi, int toSqi, int captureSqi, int promoteStone) {
if (chessSet.hasMovablePieces()) {
ChessStone stone = chessSet.getStoneAt(fromSqi);
int colour = Chess.stoneToColor(stone.getStone());
Location to = getSquare(Chess.sqiToRow(toSqi), Chess.sqiToCol(toSqi)).getCenter().add(0, 0.5, 0);
float yaw = getRotation().getYaw();
if (colour == Chess.BLACK) {
yaw = (yaw + 180) % 360;
}
to.setYaw(yaw);
chessSet.movePiece(fromSqi, toSqi, captureSqi, to, promoteStone);
} else {
// TODO: maybe some particle effect showing move direction?
}
}
/**
* Board is in designer mode - paint some markers on unused squares
*/
private void paintDesignIndicators(MassBlockUpdate mbu) {
Wool marker = new Wool(DyeColor.RED); // make configurable?
for (int row = 0; row < 8; ++row) {
for (int col = 0; col < 8; ++col) {
if (row < 2 && col < 5 || row == 6 && col == 0 || row == 7 && col < 5) {
continue;
}
Cuboid sq = getSquare(row, col).shift(CuboidDirection.Up, 1).inset(CuboidDirection.Horizontal, 1);
sq.fill(marker, mbu);
}
}
}
/**
* Highlight two squares on the chessboard, erasing previous highlight, if
* any. Use the highlight square colors per-square color, if set, or just
* the global highlight block otherwise.
*
* @param from square index of the first square
* @param to square index of the second square
*/
void highlightSquares(int from, int to) {
if (boardStyle.getHighlightStyle() == HighlightStyle.NONE) {
return;
}
// erase the old highlight, if any
if (fromSquare >= 0 || toSquare >= 0) {
if (boardStyle.getHighlightStyle() == HighlightStyle.LINE) {
drawHighlightLine(fromSquare, toSquare, false);
} else {
paintBoardSquare(fromSquare, null);
paintBoardSquare(toSquare, null);
}
}
fromSquare = from;
toSquare = to;
// draw the new highlight
if (from >= 0 || to >= 0) {
if (boardStyle.getHighlightStyle() == HighlightStyle.LINE) {
drawHighlightLine(fromSquare, toSquare, true);
} else {
highlightBoardSquare(fromSquare);
highlightBoardSquare(toSquare);
}
}
}
/**
* Use Bresenham's algorithm to draw line between two squares on the board
*
* @param from Square index of the first square
* @param to Square index of the second square
* @param isHighlighting True if drawing a highlight, false if erasing it
*/
private void drawHighlightLine(int from, int to, boolean isHighlighting) {
if (from < 0 || to < 0 || from >= 64 || to >= 64) {
return;
}
Cuboid s1 = getSquare(Chess.sqiToRow(from), Chess.sqiToCol(from));
Cuboid s2 = getSquare(Chess.sqiToRow(to), Chess.sqiToCol(to));
Location loc1 = s1.getRelativeBlock(s1.getSizeX() / 2, 0, s1.getSizeZ() / 2).getLocation();
Location loc2 = s2.getRelativeBlock(s2.getSizeX() / 2, 0, s2.getSizeZ() / 2).getLocation();
int dx = Math.abs(loc1.getBlockX() - loc2.getBlockX());
int dz = Math.abs(loc1.getBlockZ() - loc2.getBlockZ());
int sx = loc1.getBlockX() < loc2.getBlockX() ? 1 : -1;
int sz = loc1.getBlockZ() < loc2.getBlockZ() ? 1 : -1;
int err = dx - dz;
while (loc1.getBlockX() != loc2.getBlockX() || loc1.getBlockZ() != loc2.getBlockZ()) {
int sqi = getSquareAt(loc1);
MaterialData m = isHighlighting ?
boardStyle.getHighlightMaterial(Chess.isWhiteSquare(sqi)) :
(Chess.isWhiteSquare(sqi) ? boardStyle.getWhiteSquareMaterial() : boardStyle.getBlackSquareMaterial());
loc1.getBlock().setTypeIdAndData(m.getItemTypeId(), m.getData(), false);
int e2 = 2 * err;
if (e2 > -dz) {
err -= dz;
loc1.add(sx, 0, 0);
}
if (e2 < dx) {
err += dx;
loc1.add(0, 0, sz);
}
}
}
/**
* Clear full area associated with this board
*/
void clearAll() {
MassBlockUpdate mbu = CraftMassBlockUpdate.createMassBlockUpdater(ChessCraft.getInstance(), getBoard().getWorld());
fullBoard.fill(0, (byte)0, mbu);
mbu.notifyClients();
if (ChessCraft.getInstance().getDynmapIntegration() != null) {
ChessCraft.getInstance().getDynmapIntegration().triggerUpdate(fullBoard);
}
}
/**
* Get the Cuboid region for this square <i>of the chessboard itself</i>
*
* @param row
* @param col
* @return a Cuboid representing the square
*/
public Cuboid getSquare(int row, int col) {
if (row < 0 || col < 0 || row > 7 || col > 7) {
throw new ChessException("ChessBoard: getSquare: bad (row, col): (" + row + "," + col + ")");
}
Cuboid sq = new Cuboid(a1Corner.getLocation());
int s = boardStyle.getSquareSize();
CuboidDirection dir = rotation.getDirection();
CuboidDirection dirRight = rotation.getRight().getDirection();
sq = sq.shift(dir, row * s).shift(dirRight, col * s);
sq = sq.expand(dir, s - 1).expand(dirRight, s - 1);
return sq;
}
/**
* Get the region above a square into which a chesspiece gets drawn
*
* @param row
* the board row
* @param col
* the board column
* @return a Cuboid representing the drawing space
*/
public Cuboid getPieceRegion(int row, int col) {
return getSquare(row, col).expand(CuboidDirection.Up, boardStyle.getHeight() - 1).shift(CuboidDirection.Up, 1);
}
/**
* Get the Chesspresso square index of the given location
*
* @param loc desired location
* @return the square index, or Chess.NO_SQUARE if not on the board
*/
int getSquareAt(Location loc) {
if (!areaBoard.contains(loc)) {
return Chess.NO_SQUARE;
}
int row = 0, col = 0;
switch (rotation) {
case NORTH:
row = 7 - ((loc.getBlockX() - areaBoard.getLowerX()) / boardStyle.getSquareSize());
col = 7 - ((loc.getBlockZ() - areaBoard.getLowerZ()) / boardStyle.getSquareSize());
break;
case EAST:
row = 7 - ((loc.getBlockZ() - areaBoard.getLowerZ()) / boardStyle.getSquareSize());
col = -((areaBoard.getLowerX() - loc.getBlockX()) / boardStyle.getSquareSize());
break;
case SOUTH:
row = -((areaBoard.getLowerX() - loc.getBlockX()) / boardStyle.getSquareSize());
col = -((areaBoard.getLowerZ() - loc.getBlockZ()) / boardStyle.getSquareSize());
break;
case WEST:
row = -((areaBoard.getLowerZ() - loc.getBlockZ()) / boardStyle.getSquareSize());
col = 7 - ((loc.getBlockX() - areaBoard.getLowerX()) / boardStyle.getSquareSize());
break;
}
return Chess.coorToSqi(col, row);
}
}