/* Copyright 1996 Rujith de Silva. Modified 2002-04-07. */ package rujith.pentominoes; import java.awt.Canvas; import java.awt.Color; import java.awt.Dimension; import java.awt.Event; import java.awt.Graphics; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Stack; /** * A board on which pentomino pieces can be placed and moved. */ public class Board extends Canvas { // The shapes of the 12 pentomino pieces. Each is encoded as a // (5x2) array giving the positions of the 5 squares of each // piece. static final int data[][][] = { { { 0, -2 }, /* I */ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, { { 0, -2 }, /* L */ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 1 } }, { { 0, -2 }, /* Y */ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 0 } }, { { 0, -2 }, /* N */ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, { { 0, 0 }, /* V */ { 0, 1 }, { 0, 2 }, { 1, 0 }, { 2, 0 } }, { { 0, -1 }, /* T */ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 2, 0 } }, { { 0, -1 }, /* P */ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, -1 } }, { { 0, -1 }, /* U */ { 0, 0 }, { 0, 1 }, { 1, -1 }, { 1, 1 } }, { { 0, -1 }, /* F */ { 0, 0 }, { 0, 1 }, { 1, 0 }, { -1, 1 } }, { { 0, -1 }, /* X */ { 0, 0 }, { 0, 1 }, { 1, 0 }, { -1, 0 } }, { { 0, -1 }, /* Z */ { 0, 0 }, { 0, 1 }, { 1, -1 }, { -1, 1 } }, { { 0, 0 }, /* W */ { 0, -1 }, { 1, -1 }, { -1, 0 }, { -1, 1 } } }; // The starting positions for the twelve pentomino pieces. static final int startpos[][] = { { 2, 12 }, { 5, 12 }, { 8, 12 }, { 11, 12 }, { 14, 10 }, { 18, 11 }, { 22, 11 }, { 2, 18 }, { 6, 18 }, { 10, 18 }, { 14, 18 }, { 18, 18 } }; // The size of the board. static final int xsize = 25, ysize = 25; // The size, in pixels, of each square on the board. static final int squareSize = 14; // The Dimensions of the board static final Dimension thisSize = new Dimension(xsize * squareSize, ysize * squareSize); /** * The layer on which the pieces reside. */ final protected BoardPiece board[][] = new BoardPiece[xsize][ysize]; /** * The top layer on which the 'active' piece resides while it's * being moved. This lets it move over the other pieces. */ final protected BoardPiece boardtop[][] = new BoardPiece[xsize][ysize]; // The squares which have changes are pushed onto a Stack. This // lets all the changes to be accumulated, and then all re-drawn // in one go. private Stack stack = new Stack(); // The Stack may contain the same square multiple times, if that // square has been changed several times. This is harmless, but // decreases performance. So the pending squares are kept track // of in an array, and this is checked before the square is added // to the Stack. The intent is not to eliminate duplicate entries // in the Stack, but merely to reduce the likelihood of that // happening. private boolean updated[][] = new boolean[xsize][ysize]; // The twelve pieces on the board . private BoardPiece bps[] = new BoardPiece[12]; // The position, direction, etc., of the piece being moved. private int startx, starty; private int startDirection; private Position pos = new Position(0.0, 0.0); // The currently active board piece. private BoardPiece currentbp = null; // True if the currently active board piece is on the top layer. private boolean topbpSelected = false; // Whether a drag is in progress. boolean inDrag = false; // The various colours on the board and the pieces. static final Color boardColor = new Color (100, 160, 255); static final Color targetColor = new Color (0, 80, 180); static final Color pieceColor = new Color (240, 0, 0); static final Color pieceHighlightColor = new Color (255, 150, 100); static Color pieceTopColor = new Color (255, 230, 180); // The portion of the board that serves as the target for the // pieces, i.e., the rectangle into which the user attempts to // place all the pieces. static final int targetminx = 2; static final int targetminy = 2; private int height = 6; private int targetmaxy = targetminy + height; private int targetmaxx = targetminx + (60 / height); // Whether the whole board needs re-painting, or just the pieces. private boolean needPaint = false; // The callback to the widget controlling the board. final private BoardListener bc; // The status of the board. 1 if a piece is being translated, 2 // if a piece is being rotated and dragged, 0 otherwise. private int boardStatus = 0; /** * Create a new board. * * @param bc BoardListener to be notified of changes in the Board. */ public Board(BoardListener bc) { this.bc = bc; this.setBackground (Color.lightGray); /* resize (squareSize * xsize, squareSize * ysize); */ this.pos.nonRotateRadius = squareSize * 1.1; for (int i = 0; i < 12; ++i) this.bps[i] = new BoardPiece(data[i], this, startpos[i][0], startpos[i][1]); this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { Board.this.mousePressed(e); } public void mouseReleased(MouseEvent e) { Board.this.mouseReleased(e); } // When does mouseClicked get called? }); this.addMouseMotionListener(new MouseMotionListener() { public void mouseDragged(MouseEvent e) { Board.this.mouseDragged(e); } public void mouseMoved(MouseEvent e) { Board.this.mouseMoved(e); } }); } /** * @return Current board status (0, 1 or 2). */ public int getBoardStatus() { return this.boardStatus; } /** * @return Current height of the target area of the board. */ public int getHeight() { return this.height; } /** * @param height Set the height of the target area of the board. */ public void setHeight(int height) { if (height >= 3 && height <= 6 && this.height != height) { this.height = height; this.targetmaxy = targetminy + height; this.targetmaxx = targetminx + (60 / height); this.needPaint = true; this.repaint(); } } // Is the given position within the target. Used in painting the // board. boolean inTarget (int x, int y) { return x >= targetminx && x < this.targetmaxx && y >= targetminy && y < this.targetmaxy; } /** * Mark the current square as needing to be repainted. This is * because a piece has moved over or off that square. * * @param x X-position of the square * @param y Y-position of the square */ public void updateSquare(int x, int y) { if (this.inBounds(x, y)) { synchronized (stack) { if (! this.updated[x][y]) { this.updated[x][y] = true; this.stack.push (new Point (x, y)); } } } } // Return a square that needs to be painted. Null if none. private Point getUpdateSquare () { synchronized (this.stack) { if (this.stack.empty()) return null; Point retval = (Point) this.stack.pop(); this.updated[retval.x][retval.y] = false; return retval; } } /** * Return true if the specified position is in-bounds. */ static boolean inBounds(int x, int y) { return x >= 0 && x < xsize && y >= 0 && y < ysize; } //////////////////////////////////////////////////////////// // // Here's a caveat that applies to the next group of methods. The // Board can be in several states, including: 'normal' state; // mouse over a piece; piece being dragged; piece stuck over // another, etc. The transitions between these states can be // quite complex. For example, when the mouse is released, the // active piece may be swapped, returned to the inactive state, // and then the covered piece that had been under the active piece // may get highlighted, because the mouse was over it. I had // programmed all these transitions pretty well. But then a // friend told me about trying to do SHIFT-drags, etc., and I // realized that these involve new transitions, such as somebody // pressing SHIFT in the middle of a drag. So I added more code // to get those transitions also to work. But now the code is too // convoluted and unstructured that any further work should really // start with a re-write of the existing code in terms of state // transitions. Rujith 2002-04-07 // ////////////////////////////////////////////////////////////// // Move the piece to the top layer when the mouse is pressed over // it. void mousePressed(MouseEvent e) { // Only recognize simple mouse gestures. if (! checkMask(e.getModifiers(), MouseEvent.BUTTON1_MASK)) { return; } int x = e.getX(); int y = e.getY(); int bx = x / squareSize; int by = y / squareSize; if (inBounds (bx, by)) { BoardPiece mbp = this.getVisibleBoardPiece(bx, by); // Start dragging a piece only if there wasn't already a // piece on the top layer, OR if the piece is already on // the top layer (because it had got "stuck" there during // a previous drag). if (mbp != null && (! this.topbpSelected || this.currentbp == mbp)) { if (this.currentbp != null && this.currentbp != mbp) { this.currentbp.setStatus(0); this.currentbp = null; } // Set up the position, preparatory to its being // dragged. Initialize its position to the center of // the square. this.pos.x = (mbp.xpos + 0.5) * squareSize; this.pos.y = (mbp.ypos + 0.5) * squareSize; // Initialize the position's "drag-handle" to the // mouse position. Decide whether to allow rotations, // based on distance between the "drag-handle" and the // piece's centre. if (this.pos.reset (x, y)) this.boardStatus = 2; else this.boardStatus = 1; this.bc.changed(this.boardStatus); if (! this.topbpSelected) { mbp.toTop(); this.topbpSelected = true; } this.currentbp = mbp; // Remember the start position of the drag. this.startx = x; this.starty = y; // Remember the starting orientation of the piece. this.startDirection = this.currentbp.getDirection(); this.inDrag = true; } } } // Move the piece back to the bottom layer (if possible) when the // mouse is released. void mouseReleased(MouseEvent e) { // Only handle simple mouse gestures. if (! checkMask(e.getModifiers(), MouseEvent.BUTTON1_MASK)) { return; } int x = e.getX(); int y = e.getY(); this.boardStatus = 0; this.bc.changed(this.boardStatus); if (this.topbpSelected) { // Flip the piece if it hasn't been dragged. if (Math.abs (startx - x) <= 1 && Math.abs (starty - y) <= 1) { this.currentbp.doSwap(); } // Try to move the piece to the bottom layer. This may // fail if there's a piece on the bottom layer that's // blocking this piece. if (this.currentbp.toBottom()) { // It succeeded, so no longer a piece on the top layer. topbpSelected = false; } } // Update the active piece based on the mouse position. This // is because the piece may not be under the cursor at the end // of the drag. In fact, a DIFFERENT piece may be under the // cursor. this.updateCurrentBoardPiece(e); this.inDrag = false; } // Handle the mouse being moved. void mouseMoved(MouseEvent e) { // Handle simple mouse gestures. if (checkMask(e.getModifiers(), MouseEvent.BUTTON1_MASK)) { this.updateCurrentBoardPiece(e); } else { // Reset to a 'base' state otherwise. this.resetCurrentBoardPiece(); } } // Update the current board piece, according to the mouse // position. void updateCurrentBoardPiece(MouseEvent e) { int x = e.getX(); int y = e.getY(); int bx = x / squareSize; int by = y / squareSize; // Only handle mouse moves if there isn't a piece on the top // layer. That happens if the top piece was dragged and // released over another piece. Then it gets "stuck" on the // top layer. In that case, the user is forced to drag the // top piece away, and no other pieces respond to the mouse // until then. if (! this.topbpSelected && inBounds(bx, by)) { BoardPiece mbp = this.board[bx][by]; // If the active piece has changed, reset the board. if (mbp != this.currentbp) { this.boardStatus = 0; this.bc.changed(this.boardStatus); // Reset the previously active piece, if any. if (this.currentbp != null) { this.currentbp.setStatus(0); this.currentbp = null; } // If a new piece has become active ... if (mbp != null) { this.currentbp = mbp; this.currentbp.setStatus(1); } } } } // Reset the current board-piece. void resetCurrentBoardPiece() { if (this.currentbp == null) { return; } if (this.topbpSelected) { if (! this.currentbp.toBottom()) { this.inDrag = false; this.boardStatus = 0; this.bc.changed(this.boardStatus); return; } } this.topbpSelected = false; this.currentbp.setStatus(0); this.boardStatus = 0; this.bc.changed(this.boardStatus); this.inDrag = false; this.currentbp = null; } // Handle the piece being dragged. void mouseDragged(MouseEvent e) { // System.out.println("Drag: " + e.getModifiers()); // Only recognize simple mouse gestures, otherwise reset to // the 'normal' state. if (! checkMask (e.getModifiers(), MouseEvent.BUTTON1_MASK)) { this.resetCurrentBoardPiece(); return; } int x = e.getX(); int y = e.getY(); // Do the drag if there's a piece on the top layer, and if in // the middle of a drag. if (this.topbpSelected && this.inDrag) { // Move the position according to the movement of its // "drag-handle". this.pos.move(x, y); // Calculate how the position has moved and rotated. int newbpx = (int) (this.pos.x / squareSize); int newbpy = (int) (this.pos.y / squareSize); int newdir = (((int) ((this.pos.azimuth + 2.0 * Math.PI) / (Math.PI / 2.0) + 0.5)) + this.startDirection) % 4; // Update the piece's position if necessary. if ((newbpx != this.currentbp.xpos || newbpy != this.currentbp.ypos)) this.currentbp.doMove (newbpx, newbpy); // Update the piece's direction if necessary. if (newdir != this.currentbp.getDirection()) this.currentbp.changeDir(newdir); } } /** * @param x X-position on board * @param y Y-position on board * * @return Piece at that position, or null. */ public BoardPiece piece(int x, int y) { if (inBounds(x, y)) return this.board[x][y]; return null; } /** * @param x X-position on board * @param y Y-position on board * * @return Piece on the top layer at that position, or null. */ public BoardPiece pieceTop(int x, int y) { if (inBounds(x, y)) return this.boardtop[x][y]; return null; } // Get the visible board piece at the given position. private BoardPiece getVisibleBoardPiece(int x, int y) { if (! inBounds(x, y)) return null; // Return the piece on the top layer, if any. BoardPiece piece = this.boardtop[x][y]; if (piece != null) return piece; // Else return the piece on the bottom layer. return this.board[x][y]; } // Get the colour to draw a piece, according to its status. static private Color getPieceColor(BoardPiece bp) { switch (bp.getStatus ()) { case 0: return pieceColor; case 1: return pieceHighlightColor; default: return pieceTopColor; } } static BoardPiece getPiece(BoardPiece layer[][], int x, int y) { if (inBounds(x, y)) return layer[x][y]; return null; } // Paint a square on the board. private void paintSquare(Graphics g, int x, int y) { Color sqc; // Get the piece at this position. BoardPiece sqbp = this.getVisibleBoardPiece(x, y); // Get the colour to draw on this square. if (sqbp != null) sqc = getPieceColor(sqbp); else if (inTarget(x, y)) sqc = targetColor; else sqc = boardColor; // Fill the square with the colour. g.setColor(sqc); g.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); if (sqbp != null) { // Now this is tricky. For each of the four surrounding // squares, need to draw a black line between that square // and this one, if the piece at that square is different // from this square. Note that this means that some // separating lines may be re-drawn more than once. // The code to prevent that is actually quite complicated, // so I didn't tackle it. g.setColor(Color.black); // Figure out on which layer to check for the pieces on // the surrounding squares. BoardPiece layer[][] = sqbp.getStatus() == 2 ? this.boardtop : this.board; // Handle the four surrounding squares one by one. if (getPiece(layer, x - 1, y) != sqbp) g.drawLine (x * squareSize, y * squareSize, x * squareSize, (y + 1) * squareSize - 1); if (getPiece(layer, x, y - 1) != sqbp) g.drawLine (x * squareSize, y * squareSize, (x + 1) * squareSize - 1, y * squareSize); if (getPiece(layer, x + 1, y) != sqbp) g.drawLine ((x + 1) * squareSize - 1, y * squareSize, (x + 1) * squareSize - 1, (y + 1) * squareSize - 1); if (getPiece(layer, x, y + 1) != sqbp) g.drawLine (x * squareSize, (y + 1) * squareSize - 1, (x + 1) * squareSize - 1, (y + 1) * squareSize - 1); } } /** * Paint a piece on the board. * * @param g Graphics object to paint onto * @param bp BoardPiece being painted */ protected void paintPiece(Graphics g, BoardPiece bp) { if (bp != null) { for (int i = 0; i < 5; ++i) { this.paintSquare(g, bp.current[i][0] + bp.xpos, bp.current[i][1] + bp.ypos); } } } // Paint all the squares that have changed. private void updateSquares(Graphics g) { Point d = null; // Get the squares that have changed from the Stack. while ((d = this.getUpdateSquare()) != null) { this.paintSquare(g, d.x, d.y); } } /** * Paint the board. * * @param g Graphics object to paint it onto */ public void paint(Graphics g) { // Fill the whole board. g.setColor(boardColor); g.fillRect(0, 0, xsize * squareSize, ysize * squareSize); // Fill the target on the board. g.setColor(targetColor); g.fillRect(targetminx * squareSize, targetminy * squareSize, (this.targetmaxx - targetminx) * squareSize, (this.targetmaxy - targetminy) * squareSize); // Paint each of the pieces for (int i = 0; i < 12; ++i) this.paintPiece(g, this.bps[i]); // Paint any squares that have changed - probably should not // be none at this point. this.updateSquares(g); // Whole board now does not need to be painted. this.needPaint = false; } /** * Update the graphics on the board. * * @param g Graphics to paint the board onto */ public void update(Graphics g) { // Paint the whole board if necessary if (this.needPaint) this.paint(g); // Paint the changed squares. this.updateSquares(g); } /** * @return Board's preferred size. */ public Dimension getPreferredSize() { return this.thisSize; } /** * @return Board's minimum size. */ public Dimension getMinimumSize() { return this.thisSize; } // Check that no bits are set in the value except the bits set in // the mask. Return true if no such unwanted bits are set in the // value. static boolean checkMask(int value, int mask) { return (value & (~ mask)) == 0; } }