/** * 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; } }