import java.awt.*; import java.awt.event.*; import java.lang.Math; /** This is a single tetris board used in tetrii. It runs a timer to drop * pieces, passes commands on to the appropriate Piece and Cells, and reports * score information back to its creator. *

* The board consists of Cells in an array, where each Cell knows its neighbors. * Edge Cells think they border a "sink" Cell called `none', whose 4 neighbors * are all itself. A timer moves the current Piece down at a set rate, and * it is controlled by various ways at the same time. * * @author Russell Young tetrii@young-0.com * @version 1.0 */ /* Feel free to use, distribute, or change, with the only restriction that you * should not remove my name from the original code, and you should document * your changes so there is no confusion about whose bugs they are. */ public class Board implements Constants { private Graphics mainGraphics, nextGraphics; private Cell upperLeft, startRoot, nextUpperLeft, nextRoot; Piece piece, nextPiece, np = null; private Frame frame; gridCanvas nextCanvas, gameCanvas; boolean exit; protected int level, score, lines, oldLines; private int dropBonus = 0; int height, width, id; protected boolean paused = false; protected tetrii caller; protected Timer timer; static boolean capsLock = false; protected int filled = -1; /** The bonuses awarded for completing lines */ protected static int[] lineBonuses = {0, 3, 9, 27, 81}; // Allocating these arrays here and passing them downstream to Pieces // saves memory allocations and cleanups private Cell[] cells, oldCells, nextCells; /** Make a board * @param caller the tetrii that created this * @param id an int 1-based ID for this board * @param height an int giving height of the visible board in cells * @param width an int giving width of the visible board in cells */ public Board(tetrii caller, int id, int height, int width) { this.caller = caller; this.id = id; this.height = height; this.width = width; cells = new Cell[CELLS_PER_PIECE]; oldCells = new Cell[CELLS_PER_PIECE]; nextCells = new Cell[CELLS_PER_PIECE]; // Initialize the board cells upperLeft = Cell.initializeArray(width, height + 2); // Make the board go from -2 to height, to allow spin room above the visible // Board for (Cell c = upperLeft; c.on(); c = c.down()) c.Y.value(c.Y.value() - 2); startRoot = upperLeft.right(width / 2).down().down(); // Now make the next-piece display area nextUpperLeft = Cell.initializeArray(width, 2); nextRoot = nextUpperLeft.right(width / 2).down(); // Now make the frame frame = new Frame("Tetris " + id); frame.setLayout(new BorderLayout(5, 5)); frame.setBackground(Color.white); frame.add("Center", gameCanvas = new gridCanvas(this)); frame.add("North", nextCanvas = new gridCanvas(nextUpperLeft)); /* This doesn't work as I expected (in 1.3), so the listener is added to everything to make sure the keystroke is read. */ frame.addKeyListener(new getInput(this, 0)); frame.addMouseListener(new getMouseInput(this)); nextCanvas.addKeyListener(new getInput(this, 2)); nextCanvas.addMouseListener(new getMouseInput(this)); gameCanvas.addKeyListener(new getInput(this, 1)); gameCanvas.addMouseListener(new getMouseInput(this)); nextCanvas.setBackground(Color.black); gameCanvas.setBackground(Color.black); nextCanvas.setSize(CELL_WIDTH * width + 1, CELL_HEIGHT * 2 + 1); gameCanvas.setSize(CELL_WIDTH * width + 1, CELL_HEIGHT * height + 1); frame.pack(); frame.setResizable(false); frame.show(); nextGraphics = nextCanvas.getGraphics(); mainGraphics = gameCanvas.getGraphics(); } void fillRandom(int rows, int pct) { double dPct = pct / 100.0; if (rows == 0) return; filled = 0; if (rows >= height) rows = height - 2; Cell c1, c2; for (c1 = upperLeft(false).down(height - rows); c1.on(); c1 = c1.down()) for (c2 = c1; c2.on(); c2 = c2.right()) if (dPct > Math.random()) { c2.occupied(true); c2.setColor(Piece.INITIAL); filled++; } } /** Starts the game on this board * @param restart boolean, true if the game is being restarted, and the * board needs clearing. */ public void start(boolean restart) { dropBonus = level = score = lines = oldLines = 0; exit = false; if (restart) clear(); nextCells[0] = nextRoot; nextPiece = Piece.getNext(); nextPiece.draw(nextGraphics, nextCells); oldCells[0] = cells[0] = startRoot; piece = Piece.getNext(); piece.draw(mainGraphics, cells); (timer = new Timer(this)).start(); } /** Clears all Cells of the board for a replay */ void clear() { Cell c, c1; for (c = upperLeft(true); c.on(); c = c.down()) for (c1 = c; c1.on(); c1 = c1.right()) c1.clear(); for (c = nextUpperLeft; c.on(); c = c.down()) for (c1 = c; c1.on(); c1 = c1.right()) c1.clear(); filled = -1; nextCanvas.repaint(); gameCanvas.repaint(); } /** Returns the upper left cell of the board * @param real boolean telling if the desired value is the upper left * Cell of the visible board, or if it includes the invisible rows above * the top * @return the Cell at the upper left */ Cell upperLeft(boolean real) { return(real ? upperLeft : upperLeft.down().down());} /** Some board has lost, stop the game */ void stop() { exit = true; if (timer != null) timer.resume(); // In case it is paused } /** Dispose of this Board's frame */ void dispose() { if (timer != null) timer.stop(); frame.dispose(); } /** Pause this game until a restart command is given */ void pause() { paused = true; timer.suspend(); } /** Restart a paused game */ void restart() { paused = false; timer.resume(); } /** Inform the caller to restart all paused games */ void restartAll() {caller.cont();} /** Inform the caller to pause all games */ void pauseAll() {caller.pause();} /** Give the delay between steps for the current level * @return long giving milliseconds to sleep before the next drop */ long sleepInterval() {if (exit) return(0); return(2000/(3 + level));} /** Updates the score for this Board, and reports it to the caller for total * scores * @param add an int telling the points just scores */ public void score(int add) { dropBonus = 0; score += add; level = lines / 10; caller.score(id, add, lines - oldLines, level, filled); } /** Translates the current piece in a given direction * @param direction an int (defined in the Constants interface) telling the * direction to translate * @return boolean true if the translation took place, false if there was a * piece in the way */ synchronized boolean translate(int direction) { if (piece.translate(direction, cells)) { piece.draw(mainGraphics, cells, oldCells); oldCells[0] = cells[0]; return(true); } return(false); } /** drops the current piece. Move it down as far as it will go */ synchronized void drop() {while (translate(Piece.DOWN)) dropBonus++;} /** Rotates the current piece in a given direction * @param direction an int (defined in the Constants interface) telling the * direction to rotate * @return boolean true if the rotation took place, false if there was a * piece in the way */ synchronized boolean rotate(int direction) { Piece n = piece.rotate(direction, cells); if (n != null) { n.draw(mainGraphics, cells, piece, oldCells); piece = n; oldCells[0] = cells[0]; return(true); } return(false); } /** Moves the current piece down 1 row because the timer has expired * @return boolean false if the piece cannot be moved, true if it can */ synchronized boolean tick() { if (translate(DOWN)) return(true); if (!piece.check(cells)) caller.lost(id); else { piece.occupy(cells); score(piece.score() + dropBonus + completedLines()); piece = nextPiece; oldCells[0] = cells[0] = startRoot; piece.draw(mainGraphics, cells); nextPiece.clear(nextCells, width, nextGraphics); nextPiece = (np == null ? Piece.getNext() : np); np = null; nextPiece.draw(nextGraphics, nextCells); } return(false); } /** Checks for completed lines, and remove all that exist * @return int a bonus for the completed lines */ protected int completedLines() { Cell c0, c1, n; int l = 0; oldLines = lines; for (c1 = cells[0]; (c0 = c1.left()).on(); c1 = c0); if ((c0 = c1.down()).on()) c1 = c0; while (c1.on()) { for (c0 = c1; c0.on() && c0.occupied(); c0 = c0.right()) ; n = c1.up(); if (!c0.on()) { l++; removeLine(c1); } c1 = n; } return(lineBonuses[l]); } /** Removes the line containing the cell c * @param c a Cell in the line to be removed */ protected void removeLine(Cell c) { Cell c1, c2, c3; int i, j; lines++; // Make sure we are working from the left while (c.left().on()) c = c.left(); // remove the row c3 = c.up(); for (c2 = upperLeft(true), c1 = c; c1.on(); c1 = c1.right(), c2 = c2.right()) { // Splice the rows above and below c1.up().down(c1.down()); c1.down().up(c1.up()); // Move this row to the top and clear it c1.up(null); c1.down(c2); c2.up(c1); if (c1.getColor() == Piece.INITIAL) filled--; c1.clear(); if (filled == 0) { caller.informWon(); stop(); } } mainGraphics.copyArea(0, 0, CELL_WIDTH * width, CELL_HEIGHT * c.Y.value(), 0, CELL_HEIGHT); // fix Y coordinate for all the dropped rows for (i = c.Y.value() ; c3.on(); c3 = c3.up()) { j = c3.Y.value(); c3.Y.value(i); i = j; } upperLeft = upperLeft.up(); startRoot = startRoot.up(); } int filled() {return(filled);} /** Cheats by letting the player select the next Piece. * @param x int giving the X coordinate (in pixels) of the desired next piece * @param y int giving the Y coordinate (in pixels) of the desired next piece */ void np(int x, int y) { np = Piece.getPieceForColor (upperLeft(false).down(y / CELL_HEIGHT).right(x / CELL_WIDTH).getColor()); } /** Reports the best guess for the caps lock key (is there a better way to * do this?) * @return boolean true for capslock on */ static boolean capsLock() {return(capsLock);} /** sets the capslock state */ static void capsLock(boolean value) {capsLock = value;} /** A debugging function, not normally used * @param i an int telling how many rows from the top to print */ private void showBoard(int i) { Cell c, c1; for (c = upperLeft(true); (i-- > 0) && c.on(); c = c.down()) for (c1 = c; c1.on(); c1 = c1.right()) c1.show(); } } /** This waits for a time that depends on the current level, and then causes * the piece to drop a row */ class Timer extends Thread { Board board; Timer(Board b) {board = b;} public void run() { while (!board.exit) if (board.tick()) { try {sleep(board.sleepInterval());} catch (InterruptedException ie) {} } } } /** Reads key input for each board, used for the combined mouse-keyboard control * method. If I really wanted to do it nicely this could be dynamically * configurable. */ class getInput implements KeyListener, Constants { private Board myBoard; int id; getInput(Board b, int id) {this.id = id; myBoard = b;} public void keyPressed(KeyEvent ke) { int code = ke.getKeyCode(); if (code == KeyEvent.VK_CAPS_LOCK) myBoard.capsLock(true); else if (myBoard.paused) {if (code == KeyEvent.VK_C) myBoard.restartAll(); } else switch(code) { case KeyEvent.VK_LEFT: case KeyEvent.VK_Q: case KeyEvent.VK_A: case KeyEvent.VK_Z: myBoard.translate(LEFT); break; case KeyEvent.VK_RIGHT: case KeyEvent.VK_E: case KeyEvent.VK_D: case KeyEvent.VK_C: myBoard.translate(RIGHT); break; case KeyEvent.VK_UP: myBoard.rotate(COUNTERCLOCKWISE); break; case KeyEvent.VK_DOWN: case KeyEvent.VK_W: case KeyEvent.VK_S: case KeyEvent.VK_X: myBoard.rotate(CLOCKWISE); break; case KeyEvent.VK_SPACE: myBoard.drop(); break; case KeyEvent.VK_P: if (!myBoard.paused) myBoard.pauseAll(); break; } } public void keyReleased(KeyEvent ke) { if (ke.getKeyCode() == KeyEvent.VK_CAPS_LOCK) myBoard.capsLock(false); } public void keyTyped(KeyEvent ke) {} } /** On a 3-button mouse, left means left, right means right, center means * rotate clockwise. In addition, the loophole for cheating is here. When I * wrote an X version there was a button that would add 50 points to the score, * in case anyone ever seriously challenged me. That seemed a little crude, * so now it is set so that ctrl-mouse will set the next-next piece (after the * one in the next window) to be the one with the color the mouse pointer is * at. */ class getMouseInput implements MouseListener { private Board myBoard; getMouseInput(Board b) {myBoard = b;} public void mousePressed(MouseEvent e) { if (myBoard.paused || (myBoard.piece == null)) return; if (e.isControlDown()) myBoard.np(e.getX(), e.getY()); else if (myBoard.capsLock() || e.isShiftDown()) myBoard.drop(); else switch (e.getModifiers()) { case 0: // I don't know why I get 0 rather than BUTTON1_MASK case MouseEvent.BUTTON1_MASK: myBoard.translate(myBoard.LEFT); break; case MouseEvent.BUTTON2_MASK: myBoard.rotate(myBoard.CLOCKWISE); break; case MouseEvent.BUTTON3_MASK: myBoard.translate(myBoard.RIGHT); break; default: System.out.println("Unknown mouse command, " + e.getModifiers()); } } public void mouseClicked(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} } /** A class that defines the paint method to redraw grids. A little kludgy * in that it will take either a Cell (at the upper left part of the grid) * or a Board to redraw. */ class gridCanvas extends Canvas { Cell upperLeft = null; Board myBoard = null; gridCanvas(Board b) {myBoard = b;} gridCanvas(Cell ul) {upperLeft = ul;} public void paint(Graphics g) { Cell across, down; down = (myBoard == null) ? upperLeft : myBoard.upperLeft(false); for (; down.on(); down = down.down()) for (across = down; across.on(); across = across.right()) across.paint(g); } }