/*
* @(#)Game.java
*
* This work 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 2 of
* the License, or (at your option) any later version.
*
* This work 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.
*
* Copyright (c) 2003 Per Cederberg. All rights reserved.
*/
package net.percederberg.tetris;
import java.awt.Button;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import org.jopenray.server.event.Event;
import org.jopenray.server.event.EventManager;
import org.jopenray.server.thinclient.InputListener;
import org.jopenray.server.thinclient.ThinClient;
/**
* The Tetris game. This class controls all events in the game and handles all
* the game logics. The game is started through user interaction with the
* graphical game component provided by this class.
*
* @version 1.2
* @author Per Cederberg, per@percederberg.net
*/
public class Game implements InputListener {
/**
* The main square board. This board is used for the game itself.
*/
private SquareBoard board = null;
/**
* The preview square board. This board is used to display a preview of the
* figures.
*/
private SquareBoard previewBoard;
/**
* The figures used on both boards. All figures are reutilized in order to
* avoid creating new objects while the game is running. Special care has to
* be taken when the preview figure and the current figure refers to the
* same object.
*/
private Figure[] figures = { new Figure(Figure.SQUARE_FIGURE),
new Figure(Figure.LINE_FIGURE), new Figure(Figure.S_FIGURE),
new Figure(Figure.Z_FIGURE), new Figure(Figure.RIGHT_ANGLE_FIGURE),
new Figure(Figure.LEFT_ANGLE_FIGURE),
new Figure(Figure.TRIANGLE_FIGURE) };
/**
* The graphical game component. This component is created on the first call
* to getComponent().
*/
private GamePanel component = null;
/**
* The thread that runs the game. When this variable is set to null, the
* game thread will terminate.
*/
private GameThread thread = null;
/**
* The game level. The level will be increased for every 20 lines removed
* from the square board.
*/
private int level = 1;
/**
* The current score. The score is increased for every figure that is
* possible to place on the main board.
*/
private int score = 0;
/**
* The current figure. The figure will be updated when
*/
private Figure figure = null;
/**
* The next figure.
*/
private Figure nextFigure = null;
/**
* The rotation of the next figure.
*/
private int nextRotation = 0;
/**
* The figure preview flag. If this flag is set, the figure will be shown in
* the figure preview board.
*/
private boolean preview = true;
/**
* The move lock flag. If this flag is set, the current figure cannot be
* moved. This flag is set when a figure is moved all the way down, and
* reset when a new figure is displayed.
*/
private boolean moveLock = false;
private ThinClient client;
/**
* Creates a new Tetris game. The square board will be given the default
* size of 10x20.
*/
public Game(ThinClient c) {
this(c, 10, 20);
}
/**
* Creates a new Tetris game. The square board will be given the specified
* size.
*
* @param width
* the width of the square board (in positions)
* @param height
* the height of the square board (in positions)
*/
public Game(ThinClient c, int width, int height) {
board = new SquareBoard(c, width, height);
previewBoard = new SquareBoard(c, 5, 5);
board.setMessage("Press start");
thread = new GameThread();
component = new GamePanel();
this.client = c;
}
/**
* Kills the game running thread and makes necessary clean-up. After calling
* this method, no further methods in this class should be called. Neither
* should the component returned earlier be trusted upon.
*/
public void quit() {
thread = null;
}
/**
* Returns a new component that draws the game.
*
* @return the component that draws the game
*/
public Component getComponent() {
return component;
}
/**
* Handles a game start event. Both the main and preview square boards will
* be reset, and all other game parameters will be reset. Finally the game
* thread will be launched.
*/
public void handleStart() {
// Reset score and figures
level = 1;
score = 0;
figure = null;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// Reset components
board.setMessage(null);
board.clear();
previewBoard.clear();
handleLevelModification();
handleScoreModification();
component.button.setLabel("Pause");
// Start game thread
thread.reset();
}
/**
* Handles a game over event. This will stop the game thread, reset all
* figures and print a game over message.
*/
private void handleGameOver() {
if (score > 0) {
EventManager.getInstance().add(
new Event("Tetris highscore", client.getName() + " Score: "
+ score + " (Level " + level + ")",
Event.TYPE_WARNING));
}
// Stop game thred
thread.setPaused(true);
// Reset figures
if (figure != null) {
figure.detach();
}
figure = null;
if (nextFigure != null) {
nextFigure.detach();
}
nextFigure = null;
// Handle components
board.setMessage("Game Over");
component.button.setLabel("Start");
}
/**
* Handles a game pause event. This will pause the game thread and print a
* pause message on the game board.
*/
private void handlePause() {
thread.setPaused(true);
board.setMessage("Paused");
component.button.setLabel("Resume");
}
/**
* Handles a game resume event. This will resume the game thread and remove
* any messages on the game board.
*/
private void handleResume() {
board.setMessage(null);
component.button.setLabel("Pause");
thread.setPaused(false);
}
/**
* Handles a level modification event. This will modify the level label and
* adjust the thread speed.
*/
private void handleLevelModification() {
component.levelLabel.setText("Level: " + level);
thread.adjustSpeed();
}
/**
* Handle a score modification event. This will modify the score label.
*/
private void handleScoreModification() {
component.scoreLabel.setText("Score: " + score);
}
/**
* Handles a figure start event. This will move the next figure to the
* current figure position, while also creating a new preview figure. If the
* figure cannot be introduced onto the game board, a game over event will
* be launched.
*/
private void handleFigureStart() {
int rotation;
// Move next figure to current
figure = nextFigure;
moveLock = false;
rotation = nextRotation;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// Attach figure to game board
figure.setRotation(rotation);
if (!figure.attach(board, false)) {
figure.attach(previewBoard, true);
figure.detach();
handleGameOver();
}
}
/**
* Handles a figure landed event. This will check that the figure is
* completely visible, or a game over event will be launched. After this
* control, any full lines will be removed. If no full lines could be
* removed, a figure start event is launched directly.
*/
private void handleFigureLanded() {
// Check and detach figure
if (figure.isAllVisible()) {
score += 10;
handleScoreModification();
} else {
handleGameOver();
return;
}
figure.detach();
figure = null;
// Check for full lines or create new figure
if (board.hasFullLines()) {
board.removeFullLines();
if (level < 9 && board.getRemovedLines() / 20 > level) {
level = board.getRemovedLines() / 20;
handleLevelModification();
}
} else {
handleFigureStart();
}
}
/**
* Handles a timer event. This will normally move the figure down one step,
* but when a figure has landed or isn't ready other events will be
* launched. This method is synchronized to avoid race conditions with other
* asynchronous events (keyboard and mouse).
*/
private synchronized void handleTimer() {
if (figure == null) {
handleFigureStart();
} else if (figure.hasLanded()) {
handleFigureLanded();
} else {
figure.moveDown();
}
}
/**
* Handles a button press event. This will launch different events depending
* on the state of the game, as the button semantics change as the game
* changes. This method is synchronized to avoid race conditions with other
* asynchronous events (timer and keyboard).
*/
private synchronized void handleButtonPressed() {
if (nextFigure == null) {
handleStart();
} else if (thread.isPaused()) {
handleResume();
} else {
handlePause();
}
}
/**
* Handles a keyboard event. This will result in different actions being
* taken, depending on the key pressed. In some cases, other events will be
* launched. This method is synchronized to avoid race conditions with other
* asynchronous events (timer and mouse).
*
* @param e
* the key event
*/
private synchronized void handleKeyEvent(KeyEvent e) {
// Handle start, pause and resume
if (e.getKeyCode() == KeyEvent.VK_P) {
handleButtonPressed();
return;
}
// Don't proceed if stopped or paused
if (figure == null || moveLock || thread.isPaused()) {
return;
}
// Handle remaining key events
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
figure.moveLeft();
break;
case KeyEvent.VK_RIGHT:
figure.moveRight();
break;
case KeyEvent.VK_DOWN:
figure.moveAllWayDown();
moveLock = true;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_SPACE:
if (e.isControlDown()) {
figure.rotateRandom();
} else if (e.isShiftDown()) {
figure.rotateClockwise();
} else {
figure.rotateCounterClockwise();
}
break;
case KeyEvent.VK_S:
if (level < 9) {
level++;
handleLevelModification();
}
break;
case KeyEvent.VK_N:
preview = !preview;
if (preview && figure != nextFigure) {
nextFigure.attach(previewBoard, true);
nextFigure.detach();
} else {
previewBoard.clear();
}
break;
}
}
/**
* Returns a random figure. The figures come from the figures array, and
* will not be initialized.
*
* @return a random figure
*/
private Figure randomFigure() {
return figures[(int) (Math.random() * figures.length)];
}
/**
* The game time thread. This thread makes sure that the timer events are
* launched appropriately, making the current figure fall. This thread can
* be reused across games, but should be set to paused state when no game is
* running.
*/
private class GameThread extends Thread {
/**
* The game pause flag. This flag is set to true while the game should
* pause.
*/
private boolean paused = true;
/**
* The number of milliseconds to sleep before each automatic move. This
* number will be lowered as the game progresses.
*/
private int sleepTime = 500;
/**
* Creates a new game thread with default values.
*/
public GameThread() {
}
/**
* Resets the game thread. This will adjust the speed and start the game
* thread if not previously started.
*/
public void reset() {
adjustSpeed();
setPaused(false);
if (!isAlive()) {
this.start();
}
}
/**
* Checks if the thread is paused.
*
* @return true if the thread is paused, or false otherwise
*/
public boolean isPaused() {
return paused;
}
/**
* Sets the thread pause flag.
*
* @param paused
* the new paused flag value
*/
public void setPaused(boolean paused) {
this.paused = paused;
}
/**
* Adjusts the game speed according to the current level. The sleeping
* time is calculated with a function making larger steps initially an
* smaller as the level increases. A level above ten (10) doesn't have
* any further effect.
*/
public void adjustSpeed() {
sleepTime = 4500 / (level + 5) - 250;
if (sleepTime < 50) {
sleepTime = 50;
}
}
/**
* Runs the game.
*/
public void run() {
while (thread == this) {
// Make the time step
handleTimer();
// Sleep for some time
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignore) {
// Do nothing
}
// Sleep if paused
while (paused && thread == this) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignore) {
// Do nothing
}
}
}
}
}
/**
* A game panel component. Contains all the game components.
*/
private class GamePanel extends Container {
/**
* The component size. If the component has been resized, that will be
* detected when the paint method executes. If this value is set to
* null, the component dimensions are unknown.
*/
private Dimension size = null;
/**
* The score label.
*/
private Label scoreLabel = new Label("Score: 0");
/**
* The level label.
*/
private Label levelLabel = new Label("Level: 1");
/**
* The generic button.
*/
private Button button = new Button("Start");
/**
* Creates a new game panel. All the components will also be added to
* the panel.
*/
public GamePanel() {
super();
initComponents();
}
/**
* Paints the game component. This method is overridden from the default
* implementation in order to set the correct background color.
*
* @param g
* the graphics context to use
*/
public void paint(Graphics g) {
Rectangle rect = g.getClipBounds();
if (size == null || !size.equals(getSize())) {
size = getSize();
resizeComponents();
}
g.setColor(getBackground());
g.fillRect(rect.x, rect.y, rect.width, rect.height);
super.paint(g);
}
/**
* Initializes all the components, and places them in the panel.
*/
private void initComponents() {
GridBagConstraints c;
// Set layout manager and background
setLayout(new GridBagLayout());
setBackground(Color.WHITE);
// Add game board
c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
c.gridheight = 4;
c.weightx = 1.0;
c.weighty = 1.0;
c.fill = GridBagConstraints.BOTH;
this.add(board.getComponent(), c);
// Add next figure board
c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 0;
c.weightx = 0.2;
c.weighty = 0.18;
c.fill = GridBagConstraints.BOTH;
c.insets = new Insets(15, 15, 0, 15);
// Add score label
scoreLabel
.setForeground(Configuration.getColor("label", "#000000"));
scoreLabel.setAlignment(Label.CENTER);
c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 1;
c.weightx = 0.3;
c.weighty = 0.05;
c.anchor = GridBagConstraints.CENTER;
c.fill = GridBagConstraints.BOTH;
c.insets = new Insets(0, 15, 0, 15);
// Add level label
levelLabel
.setForeground(Configuration.getColor("label", "#000000"));
levelLabel.setAlignment(Label.CENTER);
c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 2;
c.weightx = 0.3;
c.weighty = 0.05;
c.anchor = GridBagConstraints.CENTER;
c.fill = GridBagConstraints.BOTH;
c.insets = new Insets(0, 15, 0, 15);
// Add generic button
button.setBackground(Configuration.getColor("button", "#d4d0c8"));
c = new GridBagConstraints();
c.gridx = 1;
c.gridy = 3;
c.weightx = 0.3;
c.weighty = 1.0;
c.anchor = GridBagConstraints.NORTH;
c.fill = GridBagConstraints.HORIZONTAL;
c.insets = new Insets(15, 15, 15, 15);
// Add event handling
enableEvents(KeyEvent.KEY_EVENT_MASK);
this.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
handleKeyEvent(e);
}
});
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleButtonPressed();
component.requestFocus();
}
});
}
/**
* Resizes all the static components, and invalidates the current
* layout.
*/
private void resizeComponents() {
Dimension size = scoreLabel.getSize();
Font font;
int unitSize;
// Calculate the unit size
size = board.getComponent().getSize();
size.width /= board.getBoardWidth();
size.height /= board.getBoardHeight();
if (size.width > size.height) {
unitSize = size.height;
} else {
unitSize = size.width;
}
// Adjust font sizes
font = new Font("SansSerif", Font.BOLD, 3 + (int) (unitSize / 1.8));
scoreLabel.setFont(font);
levelLabel.setFont(font);
font = new Font("SansSerif", Font.PLAIN, 2 + unitSize / 2);
button.setFont(font);
// Invalidate layout
scoreLabel.invalidate();
levelLabel.invalidate();
button.invalidate();
}
}
@Override
public void keyPressed(int key, boolean shift, boolean ctrl, boolean alt,
boolean meta, boolean altGr) {
if (nextFigure == null) {
handleStart();
return;
}
// Handle start, pause and resume
if (key == KeyEvent.VK_P) {
handleButtonPressed();
return;
}
// Don't proceed if stopped or paused
if (figure == null || moveLock || thread.isPaused()) {
return;
}
// Handle remaining key events
switch (key) {
case KeyEvent.VK_LEFT:
figure.moveLeft();
break;
case KeyEvent.VK_RIGHT:
figure.moveRight();
break;
case KeyEvent.VK_DOWN:
figure.moveAllWayDown();
moveLock = true;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_SPACE:
if (ctrl) {
figure.rotateRandom();
} else if (shift) {
figure.rotateClockwise();
} else {
figure.rotateCounterClockwise();
}
break;
case KeyEvent.VK_S:
if (level < 9) {
level++;
handleLevelModification();
}
break;
case KeyEvent.VK_N:
preview = !preview;
if (preview && figure != nextFigure) {
nextFigure.attach(previewBoard, true);
nextFigure.detach();
} else {
previewBoard.clear();
}
break;
}
}
@Override
public void keyReleased(int key) {
// TODO Auto-generated method stub
}
@Override
public void mouseMoved(int mouseX, int mouseY) {
// TODO Auto-generated method stub
}
@Override
public void mousePressed(int button, int mouseX, int mouseY) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(int button, int mouseX, int mouseY) {
// TODO Auto-generated method stub
}
@Override
public void mouseWheelDown(int mouseX, int mouseY) {
// TODO Auto-generated method stub
}
@Override
public void mouseWheelUp(int mouseX, int mouseY) {
// TODO Auto-generated method stub
}
}