import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.io.IOException; /** This class is the applet that runs the Tetrii game - a form of tetris where * the player controls multiple games at the same time. See the `help' window * for a more complete description. *

* I am not sure how scoring should go. The algorithm I use is to give a * score between 1 and 10 depending on the difficulty of the orientation, * a bonus of 1 for each level that was dropped, and another bonus of 3, * 9, 27, 81 points for completing 1, 2, 3, 4 lines at once. The speed of * dropping is 2 seconds / (3 + level). *

* 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. * * Files in the distribution: * t.java the entry point for the program as an application * tetrii.java the main applet object * Board.java an object providing a single tetris board * Piece.java defines the different types of pieces and their behaviors * Cell.java the individual cells that make up a board * help.java popups up a help dialogue window * tetrii.help the text to put in the help window * variableInt.java a utility class used to provide pointer-like functionality * my.java common utility functions * Constants.java some constants that need identical values in different classes * tetrii.html a simple HTML file to access the applet * * @author Russell Young tetrii@young-0.com * @version 1.0 */ public class tetrii extends Applet implements Constants { public final int DEFAULT_WIDTH = 10; public final int DEFAULT_HEIGHT = 20; public final int DEFAULT_BOARDS = 2; public final int DEFAULT_PCT = 25; public final int MAX_BOARDS = 6; public final int MAX_PCT = 66; public final int MAX_FILLED = 15; public final int SETUP_X = 200; public final int SETUP_Y = 300; public final int GAME_X = 300; public final int PERGAME_Y = 20; public final int BASEGAME_Y = 200; stopwatch watch = null; int totalScore = 0, totalLines = 0; Board[] boards; variableInt numberOfBoards, height, width, filled, pct; Label[] score; Button button; Frame controlFrame; Panel focusHere; int[] lines, levels, scores; /////////////////////////////////////// // APPLET METHODS // /////////////////////////////////////// /** The required method to initialize the game - just initialize the * variables */ public void init() { setBackground(Color.white); Piece.initialize(); height = new variableInt(DEFAULT_HEIGHT); width = new variableInt(DEFAULT_WIDTH); filled = new variableInt(0); pct = new variableInt(DEFAULT_PCT); scores = new int[MAX_BOARDS]; lines = new int[MAX_BOARDS]; levels = new int[MAX_BOARDS]; numberOfBoards = new variableInt(DEFAULT_BOARDS); } /** the required method to start the game - put up the setup window and wait * for input */ public void start() { (watch = new stopwatch()).start(); setupWindow(); } ///////////////////////////////////////////////// // Build and handle the main window // ///////////////////////////////////////////////// /** Makes the setup window to allow the user to set up the game */ protected void setupWindow() { removeAll(); windowSize(SETUP_X, SETUP_Y); setLayout(new BorderLayout()); add("Center", controls()); Panel p = new Panel(); add("South", p); Button b = new Button("Go"); p.add(b); b.addActionListener(new setupButtons(this)); b = new Button("Exit"); p.add(b); b.addActionListener(new setupButtons(this)); b = new Button("Help"); p.add(b); b.addActionListener(new setupButtons(this)); invalidate(); validateTree(); repaint(); requestFocus(); } protected void windowSize(int x, int y) {getParent().setSize(x, y);} /** Makes the window that reports the score and allows control of the game */ protected void gameWindow() { int i, j = 10; Canvas c; Label l, l1; removeAll(); windowSize(GAME_X, BASEGAME_Y + PERGAME_Y * numberOfBoards.value()); Panel p, scoreboard; if (boards != null) for (i = 0; i < boards.length; i++) boards[i].dispose(); boards = new Board[numberOfBoards.value()]; setLayout(new BorderLayout()); add("Center", p = new Panel()); p.add(scoreboard = new Panel()); getAllInput input = new getAllInput(this); // buttons Button quit; Checkbox cb; p = new Panel(); p.add(button = new Button("Start")); p.add(quit = new Button("Quit")); gameButtons bh = new gameButtons(this); button.addActionListener(bh); quit.addActionListener(bh); p.add(cb = new Checkbox("Controls", false)); cb.addItemListener(new controlOn(this)); add("South", p); add("North", focusHere = new Panel()); button.addFocusListener(new focusTo(focusHere)); quit.addFocusListener(new focusTo(focusHere)); scoreboard.setLayout(new GridLayout(0, 1)); p = new Panel(new GridLayout(0, 2)); p.add(new Label("Wasted time")); p.add(l = new Label()); p.add(new Label("Game time")); p.add(l1 = new Label(" 0:00")); focusHere.add(p); watch.clear(); watch.updateLabels(l, l1); String headers = (filled.value() == 0) ? " board points lines level" : " board points lines level filled"; scoreboard.add(new Label(headers)); scoreboard.add(new Label("-----------------------------")); score = new Label[numberOfBoards.value() + 1]; for (i = 0; i < numberOfBoards.value(); i++) { boards[i] = new Board(this, 1 + i, height.value(), width()); boards[i].fillRandom(filled.value(), pct.value()); scoreboard.add("Center", score[i] = new Label(makeScore(i + 1, 0, 0, 0, boards[i].filled()))); } scoreboard.add(l = new Label("-----------------------------")); scoreboard.add(score[i] = new Label("")); scoreboard.add("Center", score[numberOfBoards.value()] = new Label(makeTotals(0, 0))); focusHere.addKeyListener(input); invalidate(); validateTree(); requestFocus(); } /** Makes a string giving the score and situation for a single board * @param id an int distinguishing the board (from 1 on up) * @param s the int points to add on to the score * @param l the int number of lines newly completed * @param lev the int current level of this board * @param orig the int number of cells initially filled remaining, or -1 * @return a String formatted to be displayed as a single Label on the * scoreboard Panel */ protected String makeScore(int id, int s, int l, int lev, int orig) { String last; if (orig < 0) last = ""; else if (orig == 0) last = " " + watch.showElapsed(); else last = my.pad("" + orig, 6); return(my.pad("" + id, 8) + my.pad("" + s, 6) + my.pad("" + l, 6) + my.pad("" + lev, 6) + last); } /** Makes a string giving the total score and lines * @param s the int points to add on to the total score * @param l the int number of lines newly completed to add to the total * @return a String formatted to be displayed as a single Label on the * scoreboard Panel */ protected String makeTotals(int s, int l) { return(my.pad("Totals:", 8) + my.pad("" + s, 6) + my.pad("" + l, 6));} /** Called by the individual boards to update the scores. * @param id an int distinguishing the board (from 1 on up) * @param s the int points to add on to the score * @param l the int number of lines newly completed * @param lev the int current level of this board */ void score(int id, int s, int l, int lev, int orig) { int zeroBased = id - 1; score[zeroBased].setText(makeScore(id, scores[zeroBased] += s, lines[zeroBased] += l, levels[zeroBased] = lev, orig )); score[numberOfBoards.value()].setText(makeTotals(totalScore += s, totalLines += l)); if (orig == 0) { for (int i = 0; i < numberOfBoards.value(); i++) if (boards[i].filled() != 0) return; lost(-1); } } public void informWon() { int i; for (i = 0; i < boards.length && boards[i].filled() != 0; i++); if (i == boards.length) score[i].setText("You Win!"); } /** Finds the board that a command is to go to * @param i the int giving the 0-based id of the board to find * @return the Board requested, or null if none */ Board getBoard(int i) {return(i < numberOfBoards.value() ? boards[i] : null);} /** Builds the scrollbars needed for the setup screen * @return a Panel with all controls set up */ protected Panel controls() { Scrollbar s; Label l, l1; Panel p1, p2; Panel p = new Panel(new GridLayout(0, 2)); /* First add the timefields */ p.add(new Label("Wasted time")); l = new Label(""); p.add(l); watch.updateLabels(l, null); p2 = new Panel(); p1 = new Panel(); p1.add(l = new Label("" + numberOfBoards.value())); p1.add(s = new Scrollbar(Scrollbar.HORIZONTAL, numberOfBoards.value(), 0, 1, 6)); p2.add(p1); p.add(new Label("Boards")); p.add(p2); s.addAdjustmentListener(new nudge(numberOfBoards, l)); p1 = new Panel(); p2 = new Panel(); p1.add(l = new Label("" + height.value())); p1.add(s = new Scrollbar(Scrollbar.HORIZONTAL, height.value(), 0, 10, 40)); p.add(new Label("Height")); p2.add(p1); p.add(p2); s.addAdjustmentListener(new nudge(height, l)); p1 = new Panel(); p2 = new Panel(); p1.add(l = new Label("" + width())); p1.add(s = new Scrollbar(Scrollbar.HORIZONTAL, width(), 0, 6, 20)); p.add(new Label("Width")); p2.add(p1); p.add(p2); s.addAdjustmentListener(new nudge(width, l)); p1 = new Panel(); Panel fillPct = new Panel(); p1.add(l = new Label("" + pct.value())); p1.add(s = new Scrollbar(Scrollbar.HORIZONTAL, pct.value(), 0, 0, MAX_PCT)); fillPct.add(p1); s.addAdjustmentListener(new nudge(pct, l)); p1 = new Panel(); p2 = new Panel(); p1.add(l = new Label("" + filled.value())); p1.add(s = new Scrollbar(Scrollbar.HORIZONTAL, filled.value(), 0, 0, MAX_FILLED)); p.add(new Label("Filled rows")); p2.add(p1); p.add(p2); fillPct.setVisible(filled.value() > 0); s.addAdjustmentListener(new nudgeFilled(filled, l, fillPct)); p.add(new Label("Fill %")); p.add(fillPct); return(p); } /** Gets the int width (in Cells) of all the Boards * @return the int width (in Cells) of all the Boards */ public int width() {return(width.value());} /** Gets the number of Boards in the current game * @return the int number of Boards in the current game */ int numberOfBoards() {return(numberOfBoards.value());} ///////////////////////////////////////////////// // Controlling the game // ///////////////////////////////////////////////// /** Called when any board loses */ void lost(int id) { watch.pause(); for (int i = 0; i < numberOfBoards.value(); ) boards[i++].stop(); button.setLabel("Restart"); } /** called to exit the program */ void exit() { int i; // This does not work for an applet // System.exit(0); if (boards != null) for (i = 0; i < boards.length; i++) boards[i].dispose(); destroy(); } /** Starts the game (what did you think) */ void startGame(boolean clear) { int i, j; for (i = 0; i < MAX_BOARDS; i++) scores[i] = lines[i] = levels[i] = 0; totalLines = totalScore = 0; for (i = 0, j = numberOfBoards.value(); i < j; i++) { boards[i].start(clear); if (clear) boards[i].fillRandom(filled.value(), pct.value()); } watch.restart(true); button.setLabel("Pause"); } /** Pauses all the boards */ void pause() { watch.pause(); for (int i = 0; i < numberOfBoards.value(); ) boards[i++].pause(); button.setLabel("Continue"); } /** resumes all the boards */ void cont() { watch.restart(); for (int i = 0; i < numberOfBoards.value(); ) boards[i++].restart(); button.setLabel("Pause"); } /** quits the game, return to setup mode */ void quitGame() { for (int i = 0; i < numberOfBoards.value(); ) boards[i++].stop(); setupWindow(); } } /** This is the handler for the scrollbars */ class nudge implements AdjustmentListener { variableInt variable; Label label; /** Makes a new handler to adjust an int value as the Scrollbar is moved. * @param v a variableInt (pointer to int) where the current value should be * @param l the Label where the current value is displayed */ nudge(variableInt v, Label l) { variable = v; label = l; } /** The method required by the AdjustmentListener interface * @param e the AdjustmentEvent */ public void adjustmentValueChanged(AdjustmentEvent e) { variable.value(e.getValue()); label.setText("" + variable.value()); } } class nudgeFilled extends nudge { Component other; nudgeFilled(variableInt v, Label l, Component other) { super(v, l); this.other = other; } public void adjustmentValueChanged(AdjustmentEvent e) { variable.value(e.getValue()); label.setText("" + variable.value()); other.setVisible(variable.value() > 0); } } /** This is the handler for keyed input to the main window */ class getAllInput implements KeyListener { private tetrii caller; private Board board; /** Constructs a Key handler * @param t the tetrii object that called this */ getAllInput(tetrii t) {caller = t;} /** The method required by the KeyListener interface. Pass the desired command * to the currently selected Board. * @param ke the KeyEvent */ public void keyPressed(KeyEvent ke) { Board b = null; int code = ke.getKeyCode(); if (code == KeyEvent.VK_CAPS_LOCK) Board.capsLock(true); else switch(code) { case KeyEvent.VK_1: case KeyEvent.VK_2: case KeyEvent.VK_3: case KeyEvent.VK_4: case KeyEvent.VK_5: case KeyEvent.VK_6: if ((b = caller.getBoard(code - KeyEvent.VK_1)) != null) board = b; break; case KeyEvent.VK_NUMPAD1: case KeyEvent.VK_NUMPAD2: case KeyEvent.VK_NUMPAD3: case KeyEvent.VK_NUMPAD4: case KeyEvent.VK_NUMPAD5: case KeyEvent.VK_NUMPAD6: if ((b = caller.getBoard(code - KeyEvent.VK_NUMPAD1)) != null) board = b; break; case KeyEvent.VK_LEFT: case KeyEvent.VK_A: case KeyEvent.VK_Q: case KeyEvent.VK_Z: if (board != null) board.translate(Piece.LEFT); break; case KeyEvent.VK_RIGHT: case KeyEvent.VK_E: case KeyEvent.VK_D: case KeyEvent.VK_C: if (board != null) board.translate(Piece.RIGHT); break; case KeyEvent.VK_UP: if (board != null) board.rotate(Piece.COUNTERCLOCKWISE); break; case KeyEvent.VK_DOWN: case KeyEvent.VK_W: case KeyEvent.VK_S: case KeyEvent.VK_X: if (board != null) board.rotate(Piece.CLOCKWISE); break; case KeyEvent.VK_SPACE: if (board != null) board.drop(); break; default: System.err.println("Got key " + code); } } /** null method required by interface */ public void keyReleased(KeyEvent ke) { if (ke.getKeyCode() == KeyEvent.VK_CAPS_LOCK) Board.capsLock(false); } /** null method required by interface */ public void keyTyped(KeyEvent ke) {} } /** This is the handler for the setup buttons, ""Go", "Exit", "Help" */ class setupButtons implements ActionListener { public final static String HELP_FILE = "/home/russell/java/tetris/tetrii.help"; tetrii tet; /** Constructs a new handler * @param t the tetrii that called this */ setupButtons(tetrii t) {tet = t;} public void actionPerformed(ActionEvent e) { if ("Go".equals(e.getActionCommand())) tet.gameWindow(); else if ("Help".equals(e.getActionCommand())) { Component c = tet; while (!((c = c.getParent()) instanceof Frame)); try {new help((Frame)c, HELP_FILE);} catch(IOException ioe) { System.err.println("IO exception reading " + HELP_FILE);} } else tet.exit(); } } /** This is the handler for the game buttons, "Start", Pause", "Continue", * "Quit" */ class gameButtons implements ActionListener { tetrii caller; gameButtons(tetrii caller) {this.caller = caller;} public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if ("Start".equals(command)) caller.startGame(false); else if ("Restart".equals(command)) caller.startGame(true); else if ("Pause".equals(command)) caller.pause(); else if ("Continue".equals(command)) caller.cont(); else if ("Quit".equals(command)) caller.quitGame(); else System.err.println("Got command " + e); } } class control implements ActionListener { Board myBoard; control(Board b) {myBoard = b;} public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if ("<-".equals(command)) myBoard.translate(Piece.LEFT); else if ("->".equals(command)) myBoard.translate(Piece.RIGHT); else if ("Drop".equals(command)) myBoard.drop(); else if ("Clock".equals(command)) myBoard.rotate(Piece.CLOCKWISE); else if ("CClock".equals(command)) myBoard.rotate(Piece.COUNTERCLOCKWISE); else System.err.println("Got command " + e); } } /** This listener transfers the focus to a given component. It is needed * because I want to use SPACE to drop the piece on the selected board * when using keyboard input, but if a button is focused SPACE is coopted * to cause a button push. Solution: do not allow a button to be selected. */ class focusTo implements FocusListener { Component comp; focusTo(Component c) {comp = c;} public void focusGained(FocusEvent fe) {comp.requestFocus();} public void focusLost(FocusEvent fe) {} } class controlOn implements ItemListener { tetrii caller; Frame cw = null; controlOn(tetrii t) {caller = t;} public void itemStateChanged(ItemEvent ie) { if (ie.getStateChange() == ItemEvent.SELECTED) cw = new controlWindow(caller.boards, caller.width()); else cw.dispose(); } public void close() { if (cw != null) cw.dispose(); } } class controlWindow extends Frame implements Constants { static String[] control = {"Left", "Right", "Clockwise", "CClockwise", "Drop"}; controlWindow(Board[] boards, int width) { super("Control"); Button b; int i, j; int numberOfBoards = boards.length; setLayout(new GridLayout(1, 0)); ActionListener[] handlers = new ActionListener[numberOfBoards]; for (i = 0; i < numberOfBoards; i++) add(makeOne(new control(boards[i]))); setSize((CELL_WIDTH * width + 10) * numberOfBoards, CELL_HEIGHT * 6); show(); } Panel makeOne(ActionListener handler) { Panel p = new Panel(new BorderLayout()); Panel p1 = new Panel(new GridLayout(0, 1)); Button b; p.add("West", b = new Button("<-")); b.addActionListener(handler); p.add("East", b = new Button("->")); b.addActionListener(handler); p1.add(b = new Button("Clock")); b.addActionListener(handler); p1.add(b = new Button("CClock")); b.addActionListener(handler); p.add("Center", p1); p.add("South", b = new Button("Drop")); b.addActionListener(handler); return(p); } } /** Keeps a watch on both the total time played and on the elapsed time * in the current game */ class stopwatch extends Thread { public final int TICKS = 500; Label totalLabel, elapsedLabel; boolean running, exit; long current, elapsed, start, drew; /** Makes a stopwatch with output to the two given fields * @param totalLabel Label to fill in the total time * @param elapsedLabel Label to fill in this game's time */ stopwatch(Label totalLabel, Label elapsedLabel) { this.totalLabel = totalLabel; this.elapsedLabel = elapsedLabel; start = System.currentTimeMillis(); drew = elapsed = current = 0; } /** Constructor with null display */ stopwatch() {this(null, null);} /** Changes the Labels where the time is displayed * @param t the Label to display the total time, or null for no display * @param e the Label to display the game time, or null for no display */ void updateLabels(Label t, Label e) { totalLabel = t; elapsedLabel = e; } /** Starts the total time watch, and preps the game watch */ public void run() { running = exit = false; while (!exit) { try {sleep(TICKS);} catch (InterruptedException ie) {} if (running) { elapsed += (System.currentTimeMillis() - current); current = System.currentTimeMillis(); if ((elapsedLabel != null) && (elapsed - drew > 1000)) { elapsedLabel.setText(showTime(elapsed)); drew = elapsed; } } if (totalLabel != null) totalLabel.setText(showTime(System.currentTimeMillis() - start)); } } /** Clears the game clock */ void clear() { running = false; drew = elapsed = 0; } /** Restarts the game clock after it has been paused */ void restart() {restart(false);} void restart(boolean clear) { if (clear) clear(); current = System.currentTimeMillis(); running = true; } /** Pauses the game clock, might as well update it too */ void pause() { running = false; elapsed += (System.currentTimeMillis() - current); current = System.currentTimeMillis(); if ((elapsedLabel != null) && (elapsed - drew > 1000)) { elapsedLabel.setText(showTime(elapsed)); drew = elapsed; } } /** Exits the clock */ void exit() {exit = true;} /** Formats the elapsed time into a nice String for display * @param ticks the number of milliseconds elapsed * @return String in the form MM:SS */ String showTime(long ticks) { long seconds = (ticks / 1000) % 60; long minutes = ticks / 60000; return (my.pad("" + minutes, 6) + ":" + my.pad("" + seconds, 2, '0')); } /** Returns a String giving the elapsed time * @return String giving elapsed time for current game */ String showElapsed() {return(showTime(elapsed));} }