/**
* Copyright 1999-2009 The Pegadi Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//=======================================================================
// File : TetrisApplet.java (derived from...)
// Software : Tetris
// Author : Steve Fu (steve@intrinsa.com foureyes@aimnet.com)
// Date : 04/22/96
// Version : 1.1 ( JDK 1.0)
//
// Copyright (c) 1996 Steve Fu. All Rights Reserved.
//
// Permission to use, modify, copy, distribute this software for
// NON-COMMERCIAL or COMMERCIAL purposes is hereby granted subject
// to the following exceptions:
// 1. This copyright information must remain as whole or part of
// header of this file or its modified versions.
// 2. Credits must be given to the original author, Steve Fu, for
// any redistribution of the software.
//
// AUTHOR MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
// THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE, OR NON-INFRINGEMENT. AUTHOR SHALL NOT BE LIABLE FOR
// ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
// DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
//
//=======================================================================
/**
* Model and view for tetris game.
*
* @author HÃ¥vard Wigtil
*/
package org.pegadi.games.tetris;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
class TetrisWindow extends JPanel implements Runnable {
/** Images for the bricks. */
protected Image[] mBricks;
/** Media tracker to control image loading. */
protected MediaTracker mTracker;
private TetrisGame mGame = null;
private FallingBlock mFallingBlock = null;
private Image mOffScrnImage = null ; //Double Buffer screen image
private boolean pause = false;
/** Resources for this class. */
ResourceBundle res;
/** String resources for this class. */
ResourceBundle strings;
/** List of score listeners. */
java.util.List<ScoreListener> scoreListeners;
/** List of brick listeners. */
java.util.List<BrickListener> brickListeners;
private final Logger log = LoggerFactory.getLogger(getClass());
boolean stop = false;
/**
* Create a new instance.
*
* @since 0.1
*/
public TetrisWindow() {
res = ResourceBundle.getBundle("org.pegadi.games.tetris.TetrisWindow");
strings = ResourceBundle.getBundle("org.pegadi.games.tetris.TetrisStrings");
mTracker = new MediaTracker(this);
scoreListeners = new ArrayList<ScoreListener>();
brickListeners = new ArrayList<BrickListener>();
loadBrickImages();
try {
jbInit();
}
catch(Exception e) {
log.error("Could not init TetrisWindow", e);
}
}
public void init()
{
int w = TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE + 1;
int h = TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE + 1;
stop = false;
mOffScrnImage = createImage( w, h );
try{
while( (checkImage(mOffScrnImage,null)&ALLBITS) == 0 )
Thread.sleep(100);
Graphics g0 = mOffScrnImage.getGraphics();
g0.setColor(Color.white);
g0.fillRect(0,0, w, h);
g0.setColor(Color.black);
g0.drawRect(0, 0, w-1, h-1);
}
catch(Exception e){
log.error("Exception in init", e);
}
}
public void startNewGame(TetrisGame theGame)
{
stop = false;
mGame = theGame ;
mFallingBlock = mGame.createNewBlock();
fireBrickChanged(mFallingBlock, mGame.getNextBlock());
fireScoreChanged(mGame.getScore(), mGame.getLevel(), mGame.getLines(), false);
int w = TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE + 1;
int h = TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE + 1;
Graphics g0 = mOffScrnImage.getGraphics();
g0.setColor(Color.white);
g0.fillRect(0,0, w, h);
g0.setColor(Color.black);
g0.drawRect(0, 0, w-1, h-1);
updateWindow();
}
public void endGame() {
drawGameOver();
fireScoreChanged(mGame.getScore(), mGame.getLevel(), mGame.getLines(), true);
mGame = null ;
}
/**
* Toggle pause status of the game.
*
* @param pause The new pause status.
*/
public void setPause(boolean pause) {
this.pause = pause;
if (pause) {
drawPause();
}
else {
repaintBkgd(mOffScrnImage.getGraphics(), 0, 0,
TetrisGame.BLOCKWNDW, TetrisGame.BLOCKWNDH);
drawFallingBlk(mOffScrnImage.getGraphics());
}
}
/**
* Returns <code>true</code> if the game is paused.
*
* @return The pause status.
*/
public boolean isPaused() {
return pause;
}
public void run()
{
long mWaitTime;
long pre, post,diff;
long mTotalTime = 0; /** Total time the game has spent playing. */
long mLastFrame = 0; /** Time of the last frame. */
long mIdealTime = 0;
while (!stop)
{
try{
pre = System.currentTimeMillis();
mIdealTime += (TetrisGame.MAXLEVEL - mGame.getLevel()+1) * 40 + 10;
mWaitTime = mIdealTime - mTotalTime;
if(mWaitTime > 0) {
Thread.sleep(mWaitTime);
}
diff = System.currentTimeMillis() - pre;
mTotalTime += diff;
}
catch(Exception e) {
log.error("Exception!", e);
}
if (!pause) {
// Move down one line
if ( !gotoNext()) {
break; //game over
}
}
}
endGame();
}
/**
* Moves the piece one line down, or starts a new piece if it stops.
*/
protected synchronized boolean gotoNext() {
int fbkX, fbkY, fbkW, fbkH;
Graphics g0;
fbkX = mFallingBlock.getX();
fbkY = mFallingBlock.getY();
fbkW = mFallingBlock.getW();
fbkH = mFallingBlock.getH();
g0 = mOffScrnImage.getGraphics();
if (fbkY >= 0 ) repaintBkgd(g0, fbkX, fbkY, fbkW, fbkH);
if (mFallingBlock.moveBlockDown()) {
drawFallingBlk(g0);
}
else {
if ( fbkY >= 0 ) {
int linecount;
mGame.modifyMap();
repaintBkgd(g0, fbkX, fbkY, fbkW, fbkH);
mGame.increaseScore(10 * mGame.getLevel());
linecount = checkLine(fbkY, fbkY+fbkH-1);
if (linecount > 0) {
switch (linecount) {
case 1: mGame.increaseScore(100); break;
case 2: mGame.increaseScore(250); break;
case 3: mGame.increaseScore(400); break;
case 4: mGame.increaseScore(800); break;
default: log.warn("WARNING: Game indicates that more than 4 lines was removed!");
break;
}
mGame.increaseLines(linecount);
if (mGame.getLevel() < 10) {
if ((mGame.getLevel() * 10) < mGame.getLines()) {
mGame.increaseLevel();
}
}
}
fireScoreChanged(mGame.getScore(), mGame.getLevel(), mGame.getLines(), false);
mFallingBlock = mGame.createNewBlock();
fireBrickChanged(mFallingBlock, mGame.getNextBlock());
}
else {
return false ; //game over
}
}
updateWindow();
//mParent.requestFocus();
return true;
}
public synchronized boolean handleKey(KeyEvent key) {
if ( mGame == null ) return true;
if ( mFallingBlock.getY() < 0 ) return true ;
Graphics g = mOffScrnImage.getGraphics();
// Pause
// Pause must be handeled before all keys that affect the pieces
if (key.getKeyCode() == 'p' || key.getKeyCode() == 'P') {
if (!pause) {
setPause(true);
}
else {
setPause(false);
}
return true;
}
// Ignore all keys but pause key
else if (pause) {
return true;
}
//left key
else if (key.getKeyCode() == 'J' || key.getKeyCode() == 'j' ||
key.getKeyCode() == KeyEvent.VK_LEFT) {
if (mFallingBlock.moveBlockLeft()) {
repaintBkgd(g, mFallingBlock.getX()+1,
mFallingBlock.getY(),
mFallingBlock.getW(),
mFallingBlock.getH());
drawFallingBlk(g);
updateWindow();
}
return true;
}
//right key
else if (key.getKeyCode() == 'L' || key.getKeyCode() == 'l' ||
key.getKeyCode() == KeyEvent.VK_RIGHT) {
if (mFallingBlock.moveBlockRight()) {
repaintBkgd(g, mFallingBlock.getX()-1,
mFallingBlock.getY(),
mFallingBlock.getW(),
mFallingBlock.getH());
drawFallingBlk(g);
updateWindow();
}
return true;
}
//rotate key
else if (key.getKeyCode() == 'K' || key.getKeyCode() == 'k' ||
key.getKeyCode() == KeyEvent.VK_UP) {
repaintBkgd(g, mFallingBlock.getX(),
mFallingBlock.getY(),
mFallingBlock.getW(),
mFallingBlock.getH());
if (mFallingBlock.rotateBlock()) {
drawFallingBlk(g);
updateWindow();
}
else {
drawFallingBlk(g);
updateWindow();
}
return true;
}
//space key
else if (key.getKeyCode() == ' ' ) {
repaintBkgd(g, mFallingBlock.getX(),
mFallingBlock.getY(),
mFallingBlock.getW(),
mFallingBlock.getH());
int score = 0;
while( mFallingBlock.moveBlockDown() ) score += 2 ;
if ( score > 0 ) {
mGame.increaseScore(score);
}
gotoNext();
return true;
}
//down key
else if (key.getKeyCode() == 'M' || key.getKeyCode() == 'm' ||
key.getKeyCode() == KeyEvent.VK_DOWN) {
mGame.increaseScore( 2 );
gotoNext();
return true;
}
return false;
}
public void paintComponent(Graphics g) {
if ( mOffScrnImage != null ) {
g.drawImage(mOffScrnImage, 0, 0, null);
}
else {
g.setColor(getBackground());
g.fillRect(0,0,getSize().width, getSize().height);
}
}
/**
* Draw the backbuffer to the window.
*
* @since 0.1
*/
protected void updateWindow() {
repaint(new Rectangle(0, 0, getSize().width, getSize().height));
}
/**
* Redraws the current block. Also redraws the frame. The last block
* position has been previously deleted.
*
* @param g The Graphics to draw to
*/
protected void drawFallingBlk(Graphics g) {
Image brickImage = getBrickImage(mFallingBlock.getType());
int fbkX, fbkY, fbkW, fbkH;
fbkX = mFallingBlock.getX();
fbkY = mFallingBlock.getY();
fbkW = mFallingBlock.getW();
fbkH = mFallingBlock.getH();
// Redraws the brick according to the map
for ( int i = 0; i< fbkH; i++ ) {
for ( int j = 0; j< fbkW; j++ ) {
if ( mFallingBlock.getMapEntry(i,j) == 1 ) {
g.drawImage(brickImage,
(j+fbkX)*TetrisGame.BRICKSIZE,
(i+fbkY)*TetrisGame.BRICKSIZE,
null);
}
}
}
//repaint frame line
g.setColor(Color.black);
g.drawRect(0, 0,
TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE ,
TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE );
}
/**
* Redraws the background after the map has been modified.
*/
protected void repaintBkgd(Graphics g, int x, int y, int w, int h) {
g.setColor(Color.white);
g.fillRect(x*TetrisGame.BRICKSIZE,
y*TetrisGame.BRICKSIZE,
w*TetrisGame.BRICKSIZE,
h*TetrisGame.BRICKSIZE);
Image brickImage = getBrickImage(mFallingBlock.getType());
for ( int i=( y>=0 ? y : 0); i< y+h; i++ )
for ( int j = x; j< x+w; j++ )
if ( mGame.getWndMap(i,j) >= 0 )
g.drawImage(getBrickImage(mGame.getWndMap(i,j)),
j*TetrisGame.BRICKSIZE,
i*TetrisGame.BRICKSIZE,
null);
//repaint frame line
g.setColor(Color.black);
g.drawRect(0, 0,
TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE ,
TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE );
}
protected void drawGameOver(){
Graphics g0 = mOffScrnImage.getGraphics();
int w = TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE + 1;
int h = TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE + 1;
g0.setColor(Color.white);
g0.fillRect( 1, h/8, w-2, h/8);
g0.setColor(Color.black);
g0.drawRect( 1, h/8, w-2, h/8);
g0.setColor(Color.black);
g0.setFont(new Font("sans-serif", Font.BOLD, 16));
g0.drawString(strings.getString("game_over_indicator"), w/4+8, h/8+24);
updateWindow();
}
/**
* Draw the pause indicator. The indicator is drawn to the back buffer,
* and the back buffer is displayed after drawing.
*/
protected void drawPause() {
Graphics g = mOffScrnImage.getGraphics();
g.setFont(new Font("SansSerif", Font.BOLD, 16));
FontMetrics fm = g.getFontMetrics();
String text = strings.getString("pause_indicator");
int w = this.getSize().width-1;
int h = this.getSize().height;
int sw = fm.stringWidth(text);
int base = h/2;
int y1 = base - fm.getMaxAscent() - 5;
int y2 = fm.getMaxAscent() + fm.getMaxDescent() + 10;
g.setColor(Color.white);
g.fillRect(0, y1, w, y2);
g.setColor(Color.black);
g.drawRect(0, y1, w, y2);
g.drawString(text, (w - sw)/2, base);
updateWindow();
}
protected int checkLine( int line1, int line2 ) {
int linecount = mGame.checkLine(line1, line2);
if ( linecount > 0 ) {
Graphics g = mOffScrnImage.getGraphics();
repaintBkgd(g, 0,0, TetrisGame.BLOCKWNDW, line2 + 1 );
updateWindow();
}
return linecount ;
}
public Dimension getMinimumSize(){
return( new Dimension( TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE + 1,
TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE + 1) );
}
public Dimension getPreferredSize(){
return( new Dimension( TetrisGame.BLOCKWNDW * TetrisGame.BRICKSIZE + 1,
TetrisGame.BLOCKWNDH * TetrisGame.BRICKSIZE + 1) );
}
/**
* Returns the requested brick image. Will return the last image
* in the array if the index is out of bounds.
*
* @param i The index to fetch.
* @return The image.
* @since 0.1
*/
Image getBrickImage(int i) {
if (i > mBricks.length) {
return mBricks[mBricks.length - 1];
}
else {
return mBricks[i];
}
}
/**
* Load the graphics for the bricks.
*
* @since 0.1
*/
protected void loadBrickImages() {
URL url;
String brick;
Toolkit toolkit = Toolkit.getDefaultToolkit();
mBricks = new Image[7];
for (int i = 0; i < 7; i++) {
log.info("Loading brick {}...", (i+1));
brick = res.getString("brick_" + (i+1));
log.info("Path is {}", brick);
url = getClass().getResource(brick);
if (url != null) {
log.info("URL is {}", url.toExternalForm());
mBricks[i] = toolkit.getImage(url);
mTracker.addImage(mBricks[i], 0);
}
else {
log.warn("URL for image {} is NULL!");
}
try {
mTracker.waitForAll();
} catch (InterruptedException e) {
log.error("Interrupted while waiting for images to load.", e);
}
}
}
/**
* Adds a listener that will be notified of score events.
*
* @param listener The listener
* @see org.pegadi.games.tetris.ScoreListener
* @see org.pegadi.games.tetris.ScoreEvent
*/
public void addScoreListener(ScoreListener listener) {
scoreListeners.add(listener);
}
/**
* Removes a listener from the list to notify.
*
* @param listener The listener
* @see org.pegadi.games.tetris.ScoreListener
* @see org.pegadi.games.tetris.ScoreEvent
*/
public void removeScoreListener(ScoreListener listener) {
scoreListeners.remove(listener);
}
/**
* Notifies all listeners that the score has changed
*
* @param score The new score
* @param level The level
* @param lines Number of lines
*/
protected void fireScoreChanged(long score, int level, int lines, boolean gameOver) {
ScoreEvent e = new ScoreEvent(this, score, level, lines, gameOver);
for (ScoreListener scoreListener : scoreListeners) {
scoreListener.scoreChanged(e);
}
}
/**
* Adds a listener that will be notified of brick events.
*
* @param listener The listener
* @see org.pegadi.games.tetris.BrickListener
* @see org.pegadi.games.tetris.BrickEvent
*/
public void addBrickListener(BrickListener listener) {
brickListeners.add(listener);
}
/**
* Removes a listener from the list to notify.
*
* @param listener The listener
* @see org.pegadi.games.tetris.BrickListener
* @see org.pegadi.games.tetris.BrickEvent
*/
public void removeBrickListener(BrickListener listener) {
brickListeners.remove(listener);
}
/**
* Notifies all listeners of the new brick.
*
* @param current The current (new) brick.
* @param next The next brick after the current.
*/
protected void fireBrickChanged(Brick current, Brick next) {
log.info("Current = {}, next = {}", current, next);
BrickEvent e = new BrickEvent(this, current, next);
for (BrickListener brickListener : brickListeners) {
brickListener.brickChanged(e);
}
}
/**
* NB! Convenience method while transferring to the new regime.
* Notifies all listeners of the new brick.
*
* @param current The current (new) brick.
* @param next The next brick after the current.
*/
protected void fireBrickChanged(FallingBlock current, FallingBlock next) {
Brick c = new Brick(getBrickImage(current.getType()), current);
Brick n = new Brick(getBrickImage(next.getType()), next);
fireBrickChanged(c, n);
}
private void jbInit() {
this.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(KeyEvent e) {
this_keyPressed(e);
}
});
}
void this_keyPressed(KeyEvent e) {
this.handleKey(e);
}
public void stop() {
stop = true;
}
}