/* DroidFish - An Android chess program. Copyright (C) 2011 Peter Ă–sterlund, peterosterlund2@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.if3games.chessonline; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.if3games.chessonline.gamelogic.Move; import com.if3games.chessonline.gamelogic.Piece; import com.if3games.chessonline.gamelogic.Position; import com.if3games.chessonline.gamelogic.UndoInfo; import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Handler; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public abstract class ChessBoard extends View { public Position pos; public int selectedSquare; public boolean userSelectedSquare; // True if selectedSquare was set by user tap/click, // false if selectedSquare used to highlight last move public float cursorX, cursorY; public boolean cursorVisible; protected int x0, y0, sqSize; int pieceXDelta, pieceYDelta; // top/left pixel draw position relative to square public boolean flipped; public boolean drawSquareLabels; boolean toggleSelection; boolean highlightLastMove; // If true, last move is marked with a rectangle boolean blindMode; // If true, no chess pieces and arrows are drawn List<Move> moveHints; /** Decoration for a square. Currently the only possible decoration is a number. */ public final static class SquareDecoration implements Comparable<SquareDecoration> { int sq; int number; public SquareDecoration(int sq, int number) { this.sq = sq; this.number = number; } @Override public int compareTo(SquareDecoration another) { int M0 = 100000; int n = number; int s1 = (n > 0) ? M0 - n : ((n == 0) ? 0 : -M0-n); n = another.number; int s2 = (n > 0) ? M0 - n : ((n == 0) ? 0 : -M0-n); return s2 - s1; } } private ArrayList<SquareDecoration> decorations; protected Paint darkPaint; protected Paint brightPaint; private Paint selectedSquarePaint; private Paint cursorSquarePaint; private Paint whitePiecePaint; private Paint blackPiecePaint; private Paint labelPaint; private Paint decorationPaint; private ArrayList<Paint> moveMarkPaint; public ChessBoard(Context context, AttributeSet attrs) { super(context, attrs); pos = new Position(); selectedSquare = -1; userSelectedSquare = false; cursorX = cursorY = 0; cursorVisible = false; x0 = y0 = sqSize = 0; pieceXDelta = pieceYDelta = -1; flipped = false; drawSquareLabels = false; toggleSelection = false; highlightLastMove = true; blindMode = false; darkPaint = new Paint(); brightPaint = new Paint(); selectedSquarePaint = new Paint(); selectedSquarePaint.setStyle(Paint.Style.STROKE); selectedSquarePaint.setAntiAlias(true); cursorSquarePaint = new Paint(); cursorSquarePaint.setStyle(Paint.Style.STROKE); cursorSquarePaint.setAntiAlias(true); whitePiecePaint = new Paint(); whitePiecePaint.setAntiAlias(true); blackPiecePaint = new Paint(); blackPiecePaint.setAntiAlias(true); labelPaint = new Paint(); labelPaint.setAntiAlias(true); decorationPaint = new Paint(); decorationPaint.setAntiAlias(true); moveMarkPaint = new ArrayList<Paint>(); for (int i = 0; i < 6; i++) { Paint p = new Paint(); p.setStyle(Paint.Style.FILL); p.setAntiAlias(true); moveMarkPaint.add(p); } if (isInEditMode()) return; Typeface chessFont = Typeface.createFromAsset(getContext().getAssets(), "fonts/ChessCases.ttf"); whitePiecePaint.setTypeface(chessFont); blackPiecePaint.setTypeface(chessFont); setColors(); } /** Must be called for new color theme to take effect. */ final void setColors() { ColorTheme ct = ColorTheme.instance(); darkPaint.setColor(ct.getColor(ColorTheme.DARK_SQUARE)); brightPaint.setColor(ct.getColor(ColorTheme.BRIGHT_SQUARE)); selectedSquarePaint.setColor(ct.getColor(ColorTheme.SELECTED_SQUARE)); cursorSquarePaint.setColor(ct.getColor(ColorTheme.CURSOR_SQUARE)); whitePiecePaint.setColor(ct.getColor(ColorTheme.BRIGHT_PIECE)); blackPiecePaint.setColor(ct.getColor(ColorTheme.DARK_PIECE)); labelPaint.setColor(ct.getColor(ColorTheme.SQUARE_LABEL)); decorationPaint.setColor(ct.getColor(ColorTheme.DECORATION)); for (int i = 0; i < 6; i++) moveMarkPaint.get(i).setColor(ct.getColor(ColorTheme.ARROW_0 + i)); invalidate(); } private Handler handlerTimer = new Handler(); private final class AnimInfo { AnimInfo() { startTime = -1; } boolean paused; long posHash; // Position the animation is valid for long startTime; // Time in milliseconds when animation was started long stopTime; // Time in milliseconds when animation should stop long now; // Current time in milliseconds int piece1, from1, to1, hide1; int piece2, from2, to2, hide2; public final boolean updateState() { now = System.currentTimeMillis(); return animActive(); } private final boolean animActive() { if (paused || (startTime < 0) || (now >= stopTime) || (posHash != pos.zobristHash())) return false; return true; } public final boolean squareHidden(int sq) { if (!animActive()) return false; return (sq == hide1) || (sq == hide2); } public final void draw(Canvas canvas) { if (!animActive()) return; double animState = (now - startTime) / (double)(stopTime - startTime); drawAnimPiece(canvas, piece2, from2, to2, animState); drawAnimPiece(canvas, piece1, from1, to1, animState); long now2 = System.currentTimeMillis(); long delay = 20 - (now2 - now); // System.out.printf("delay:%d\n", delay); if (delay < 1) delay = 1; handlerTimer.postDelayed(new Runnable() { @Override public void run() { invalidate(); } }, delay); } private void drawAnimPiece(Canvas canvas, int piece, int from, int to, double animState) { if (piece == Piece.EMPTY) return; final int xCrd1 = getXCrd(Position.getX(from)); final int yCrd1 = getYCrd(Position.getY(from)); final int xCrd2 = getXCrd(Position.getX(to)); final int yCrd2 = getYCrd(Position.getY(to)); final int xCrd = xCrd1 + (int)Math.round((xCrd2 - xCrd1) * animState); final int yCrd = yCrd1 + (int)Math.round((yCrd2 - yCrd1) * animState); drawPiece(canvas, xCrd, yCrd, piece); } } private AnimInfo anim = new AnimInfo(); /** * Set up move animation. The animation will start the next time setPosition is called. * @param sourcePos The source position for the animation. * @param move The move leading to the target position. * @param forward True if forward direction, false for undo move. */ public final void setAnimMove(Position sourcePos, Move move, boolean forward) { anim.startTime = -1; anim.paused = true; // Animation starts at next position update if (forward) { // The animation will be played when pos == targetPos Position targetPos = new Position(sourcePos); UndoInfo ui = new UndoInfo(); targetPos.makeMove(move, ui); anim.posHash = targetPos.zobristHash(); } else { anim.posHash = sourcePos.zobristHash(); } int animTime; // Animation duration in milliseconds. { int dx = Position.getX(move.to) - Position.getX(move.from); int dy = Position.getY(move.to) - Position.getY(move.from); double dist = Math.sqrt(dx * dx + dy * dy); double t = Math.sqrt(dist) * 100; animTime = (int)Math.round(t); } if (animTime > 0) { anim.startTime = System.currentTimeMillis(); anim.stopTime = anim.startTime + animTime; anim.piece2 = Piece.EMPTY; anim.from2 = -1; anim.to2 = -1; anim.hide2 = -1; if (forward) { int p = sourcePos.getPiece(move.from); anim.piece1 = p; anim.from1 = move.from; anim.to1 = move.to; anim.hide1 = anim.to1; int p2 = sourcePos.getPiece(move.to); if (p2 != Piece.EMPTY) { // capture anim.piece2 = p2; anim.from2 = move.to; anim.to2 = move.to; } else if ((p == Piece.WKING) || (p == Piece.BKING)) { boolean wtm = Piece.isWhite(p); if (move.to == move.from + 2) { // O-O anim.piece2 = wtm ? Piece.WROOK : Piece.BROOK; anim.from2 = move.to + 1; anim.to2 = move.to - 1; anim.hide2 = anim.to2; } else if (move.to == move.from - 2) { // O-O-O anim.piece2 = wtm ? Piece.WROOK : Piece.BROOK; anim.from2 = move.to - 2; anim.to2 = move.to + 1; anim.hide2 = anim.to2; } } } else { int p = sourcePos.getPiece(move.from); anim.piece1 = p; if (move.promoteTo != Piece.EMPTY) anim.piece1 = Piece.isWhite(anim.piece1) ? Piece.WPAWN : Piece.BPAWN; anim.from1 = move.to; anim.to1 = move.from; anim.hide1 = anim.to1; if ((p == Piece.WKING) || (p == Piece.BKING)) { boolean wtm = Piece.isWhite(p); if (move.to == move.from + 2) { // O-O anim.piece2 = wtm ? Piece.WROOK : Piece.BROOK; anim.from2 = move.to - 1; anim.to2 = move.to + 1; anim.hide2 = anim.to2; } else if (move.to == move.from - 2) { // O-O-O anim.piece2 = wtm ? Piece.WROOK : Piece.BROOK; anim.from2 = move.to + 1; anim.to2 = move.to - 2; anim.hide2 = anim.to2; } } } } } /** * Set the board to a given state. * @param pos */ final public void setPosition(Position pos) { boolean doInvalidate = false; if (anim.paused = true) { anim.paused = false; doInvalidate = true; } if (!this.pos.equals(pos)) { this.pos = new Position(pos); doInvalidate = true; } if (doInvalidate) invalidate(); } /** Set/clear the board flipped status. */ final public void setFlipped(boolean flipped) { if (this.flipped != flipped) { this.flipped = flipped; invalidate(); } } /** Set/clear the board flipped status. */ final public void setDrawSquareLabels(boolean drawSquareLabels) { if (this.drawSquareLabels != drawSquareLabels) { this.drawSquareLabels = drawSquareLabels; invalidate(); } } /** Set/clear the board blindMode status. */ final public void setBlindMode(boolean blindMode) { if (this.blindMode != blindMode) { this.blindMode = blindMode; invalidate(); } } /** * Set/clear the selected square. * @param square The square to select, or -1 to clear selection. */ final public void setSelection(int square) { if (square != selectedSquare) { selectedSquare = square; invalidate(); } userSelectedSquare = true; } protected abstract int getWidth(int sqSize); protected abstract int getHeight(int sqSize); protected abstract int getSqSizeW(int width); protected abstract int getSqSizeH(int height); protected abstract int getMaxHeightPercentage(); protected abstract int getMaxWidthPercentage(); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); int height = getMeasuredHeight(); int sqSizeW = getSqSizeW(width); int sqSizeH = getSqSizeH(height); int sqSize = Math.min(sqSizeW, sqSizeH); pieceXDelta = pieceYDelta = -1; labelBounds = null; if (height > width) { int p = getMaxHeightPercentage(); height = Math.min(getHeight(sqSize), height * p / 100); } else { int p = getMaxWidthPercentage(); width = Math.min(getWidth(sqSize), width * p / 100); } setMeasuredDimension(width, height); } protected abstract void computeOrigin(int width, int height); protected abstract int getXFromSq(int sq); protected abstract int getYFromSq(int sq); @Override protected void onDraw(Canvas canvas) { if (isInEditMode()) return; // long t0 = System.currentTimeMillis(); boolean animActive = anim.updateState(); final int width = getWidth(); final int height = getHeight(); sqSize = Math.min(getSqSizeW(width), getSqSizeH(height)); blackPiecePaint.setTextSize(sqSize); whitePiecePaint.setTextSize(sqSize); labelPaint.setTextSize(sqSize/4.0f); decorationPaint.setTextSize(sqSize/3.0f); computeOrigin(width, height); for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { final int xCrd = getXCrd(x); final int yCrd = getYCrd(y); Paint paint = Position.darkSquare(x, y) ? darkPaint : brightPaint; canvas.drawRect(xCrd, yCrd, xCrd+sqSize, yCrd+sqSize, paint); int sq = Position.getSquare(x, y); if (!animActive || !anim.squareHidden(sq)) { int p = pos.getPiece(sq); drawPiece(canvas, xCrd, yCrd, p); } if (drawSquareLabels) { if (x == (flipped ? 7 : 0)) { drawLabel(canvas, xCrd, yCrd, false, false, "12345678".charAt(y)); } if (y == (flipped ? 7 : 0)) { drawLabel(canvas, xCrd, yCrd, true, true, "abcdefgh".charAt(x)); } } } } drawExtraSquares(canvas); if (!animActive && (selectedSquare != -1)) { int selX = getXFromSq(selectedSquare); int selY = getYFromSq(selectedSquare); selectedSquarePaint.setStrokeWidth(sqSize/(float)16); int x0 = getXCrd(selX); int y0 = getYCrd(selY); canvas.drawRect(x0, y0, x0 + sqSize, y0 + sqSize, selectedSquarePaint); } if (cursorVisible) { int x = Math.round(cursorX); int y = Math.round(cursorY); int x0 = getXCrd(x); int y0 = getYCrd(y); cursorSquarePaint.setStrokeWidth(sqSize/(float)16); canvas.drawRect(x0, y0, x0 + sqSize, y0 + sqSize, cursorSquarePaint); } if (!animActive) { drawMoveHints(canvas); drawDecorations(canvas); } anim.draw(canvas); // long t1 = System.currentTimeMillis(); // System.out.printf("draw: %d\n", t1-t0); } private final void drawMoveHints(Canvas canvas) { if ((moveHints == null) || blindMode) return; float h = (float)(sqSize / 2.0); float d = (float)(sqSize / 8.0); double v = 35 * Math.PI / 180; double cosv = Math.cos(v); double sinv = Math.sin(v); double tanv = Math.tan(v); int n = Math.min(moveMarkPaint.size(), moveHints.size()); for (int i = 0; i < n; i++) { Move m = moveHints.get(i); if ((m == null) || (m.from == m.to)) continue; float x0 = getXCrd(Position.getX(m.from)) + h; float y0 = getYCrd(Position.getY(m.from)) + h; float x1 = getXCrd(Position.getX(m.to)) + h; float y1 = getYCrd(Position.getY(m.to)) + h; float x2 = (float)(Math.hypot(x1 - x0, y1 - y0) + d); float y2 = 0; float x3 = (float)(x2 - h * cosv); float y3 = (float)(y2 - h * sinv); float x4 = (float)(x3 - d * sinv); float y4 = (float)(y3 + d * cosv); float x5 = (float)(x4 + (-d/2 - y4) / tanv); float y5 = (float)(-d / 2); float x6 = 0; float y6 = y5 / 2; Path path = new Path(); path.moveTo(x2, y2); path.lineTo(x3, y3); // path.lineTo(x4, y4); path.lineTo(x5, y5); path.lineTo(x6, y6); path.lineTo(x6, -y6); path.lineTo(x5, -y5); // path.lineTo(x4, -y4); path.lineTo(x3, -y3); path.close(); Matrix mtx = new Matrix(); mtx.postRotate((float)(Math.atan2(y1 - y0, x1 - x0) * 180 / Math.PI)); mtx.postTranslate(x0, y0); path.transform(mtx); Paint p = moveMarkPaint.get(i); canvas.drawPath(path, p); } } abstract protected void drawExtraSquares(Canvas canvas); protected final void drawPiece(Canvas canvas, int xCrd, int yCrd, int p) { if (blindMode) return; String psb, psw; boolean rotate = false; switch (p) { default: case Piece.EMPTY: psb = null; psw = null; break; case Piece.WKING: psb = "H"; psw = "k"; break; case Piece.WQUEEN: psb = "I"; psw = "l"; break; case Piece.WROOK: psb = "J"; psw = "m"; break; case Piece.WBISHOP: psb = "K"; psw = "n"; break; case Piece.WKNIGHT: psb = "L"; psw = "o"; break; case Piece.WPAWN: psb = "M"; psw = "p"; break; case Piece.BKING: psb = "N"; psw = "q"; rotate = true; break; case Piece.BQUEEN: psb = "O"; psw = "r"; rotate = true; break; case Piece.BROOK: psb = "P"; psw = "s"; rotate = true; break; case Piece.BBISHOP: psb = "Q"; psw = "t"; rotate = true; break; case Piece.BKNIGHT: psb = "R"; psw = "u"; rotate = true; break; case Piece.BPAWN: psb = "S"; psw = "v"; rotate = true; break; } if (psb != null) { if (pieceXDelta < 0) { Rect bounds = new Rect(); blackPiecePaint.getTextBounds("H", 0, 1, bounds); pieceXDelta = (sqSize - (bounds.left + bounds.right)) / 2; pieceYDelta = (sqSize - (bounds.top + bounds.bottom)) / 2; } rotate ^= flipped; rotate = false; // Disabled for now if (rotate) { canvas.save(); canvas.rotate(180, xCrd + sqSize * 0.5f, yCrd + sqSize * 0.5f); } xCrd += pieceXDelta; yCrd += pieceYDelta; canvas.drawText(psw, xCrd, yCrd, whitePiecePaint); canvas.drawText(psb, xCrd, yCrd, blackPiecePaint); if (rotate) canvas.restore(); } } private Rect labelBounds = null; private final void drawLabel(Canvas canvas, int xCrd, int yCrd, boolean right, boolean bottom, char c) { String s = Character.toString(c); if (labelBounds == null) { labelBounds = new Rect(); labelPaint.getTextBounds("f", 0, 1, labelBounds); } int margin = sqSize / 16; if (right) { xCrd += sqSize - labelBounds.right - margin; } else { xCrd += -labelBounds.left + margin; } if (bottom) { yCrd += sqSize - labelBounds.bottom - margin; } else { yCrd += -labelBounds.top + margin; } canvas.drawText(s, xCrd, yCrd, labelPaint); } protected abstract int getXCrd(int x); protected abstract int getYCrd(int y); protected abstract int getXSq(int xCrd); protected abstract int getYSq(int yCrd); /** * Compute the square corresponding to the coordinates of a mouse event. * @param evt Details about the mouse event. * @return The square corresponding to the mouse event, or -1 if outside board. */ public int eventToSquare(MotionEvent evt) { int xCrd = (int)(evt.getX()); int yCrd = (int)(evt.getY()); int sq = -1; if (sqSize > 0) { int x = getXSq(xCrd); int y = getYSq(yCrd); if ((x >= 0) && (x < 8) && (y >= 0) && (y < 8)) { sq = Position.getSquare(x, y); } } return sq; } protected abstract Move mousePressed(int sq); public static class OnTrackballListener { public void onTrackballEvent(MotionEvent event) { } } private OnTrackballListener otbl = null; public final void setOnTrackballListener(OnTrackballListener onTrackballListener) { otbl = onTrackballListener; } @Override public boolean onTrackballEvent(MotionEvent event) { if (otbl != null) { otbl.onTrackballEvent(event); return true; } return false; } protected abstract int minValidY(); protected abstract int maxValidX(); protected abstract int getSquare(int x, int y); public final Move handleTrackballEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: invalidate(); if (cursorVisible) { int x = Math.round(cursorX); int y = Math.round(cursorY); cursorX = x; cursorY = y; int sq = getSquare(x, y); return mousePressed(sq); } return null; } cursorVisible = true; int c = flipped ? -1 : 1; cursorX += c * event.getX(); cursorY -= c * event.getY(); if (cursorX < 0) cursorX = 0; if (cursorX > maxValidX()) cursorX = maxValidX(); if (cursorY < minValidY()) cursorY = minValidY(); if (cursorY > 7) cursorY = 7; invalidate(); return null; } public final void setMoveHints(List<Move> moveHints) { boolean equal = false; if ((this.moveHints == null) || (moveHints == null)) { equal = this.moveHints == moveHints; } else { equal = this.moveHints.equals(moveHints); } if (!equal) { this.moveHints = moveHints; invalidate(); } } public final void setSquareDecorations(ArrayList<SquareDecoration> decorations) { boolean equal = false; if ((this.decorations == null) || (decorations == null)) { equal = this.decorations == decorations; } else { equal = this.decorations.equals(decorations); } if (!equal) { this.decorations = decorations; if (this.decorations != null) Collections.sort(this.decorations); invalidate(); } } private final void drawDecorations(Canvas canvas) { if (decorations == null) return; long decorated = 0; for (SquareDecoration sd : decorations) { int sq = sd.sq; if ((sd.sq < 0) || (sd.sq >= 64)) continue; if (((1L << sq) & decorated) != 0) continue; decorated |= 1L << sq; int xCrd = getXCrd(Position.getX(sq)); int yCrd = getYCrd(Position.getY(sq)); int num = sd.number; String s; if (num > 0) s = "+" + String.valueOf(num); else if (num < 0) s = String.valueOf(num); else s = "0"; Rect bounds = new Rect(); decorationPaint.getTextBounds(s, 0, s.length(), bounds); xCrd += (sqSize - (bounds.left + bounds.right)) / 2; yCrd += (sqSize - (bounds.top + bounds.bottom)) / 2; canvas.drawText(s, xCrd, yCrd, decorationPaint); } } public final int getSelectedSquare() { return selectedSquare; } }