

/*
Tetris by Bjorn Carlin
Applet released September 2000
Source released December 2001

This source code is in the public domain,
you may use it in your own projects. Please
give credit where credit is due.

Applet works best with (width,height) = (96,200), (250,300) or (292,400)
*/


import java.awt.*;
import java.awt.event.*;
import java.applet.Applet;
import java.io.*;


public class Tetris extends Applet implements MouseListener, KeyListener, Runnable
{
	final boolean					kDebugDraw = false;
	
	// board
	final int						kBoardHeight = 25; // 20 usable
	final int						kBoardWidth = 12; // 10 usable
	final int						kPieceOriginX = 6;
	final int						kPieceOriginY = 1;
	int								tileSize;
	
	int[][]							board = new int[kBoardWidth][kBoardHeight];
	
	// colors
	final Color[]					kColor = new Color[] {
		new Color(0x00000000), new Color(0x00404040),
		new Color(0x0032639D),
		new Color(0x0063329D), new Color(0x009D329D),
		new Color(0x009D3232), new Color(0x009D6332),
		new Color(0x009D9D32), new Color(0x00329D32) };
	final int						kEmptyTile = 0;
	final int						kBorderTile = 1;
	final int						kFirstPiece = 2;
	
	final int						kPieceTiles = 4;
	final int						kPieceShapes = 7;
	
	// pieces
	int								piecePivotX;
	int								piecePivotY;
	int								pieceColor;
	int								pieceAngles; // 1, 2, or 4
	int								pieceRotations; // 0-3
	int[]							pieceX = new int[kPieceTiles];
	int[]							pieceY = new int[kPieceTiles];
	
	int								nextPiecePivotX;
	int								nextPiecePivotY;
	int								nextPieceColor;
	int								nextPieceAngles; // 1, 2, or 4
	int[]							nextPieceX = new int[kPieceTiles];
	int[]							nextPieceY = new int[kPieceTiles];
	
	int[][]							storedPiece = new int[][] {
		/* S */ {-1, 1, 0, 1, 0, 0, 1, 0},
		/* Z */ {-1, 0, 0, 0, 0, 1, 1, 1},
		/* J */ {-1, 0, 0, 0, 1, 0, 1, 1},
		/* L */ {-1, 1,-1, 0, 0, 0, 1, 0},
		/* T */ {-1, 0, 0, 0, 0, 1, 1, 0},
		/* o */ {-1, 0,-1, 1, 0, 0, 0, 1},
		/* I */ {-2, 0,-1, 0, 0, 0, 1, 0}};
	int[]							storedPieceAngles = new int[]
		{2, 2, 4, 4, 4, 1, 2};
	
	// game
	final int						kMaxStallDrops = 3;
	final int[]						kFramesPerDrop = new int[]
		{90, 70, 53, 40, 30, 22, 15, 12, 9, 6, 5};
	final int						kKeyRepeatDelay = 20;
	final int						kKeyRepeatFrequency = 2; // lower numbers = faster
	
	boolean							gameInProgress;
	boolean							gamePaused;
	boolean							gameOver;
	boolean							sleeping;
	int								frame;
	int								time; // game time in seconds
	int								level;
	int								pieces;
	int								lines;
	int								score;
	long							pauseTime;
	int								stallDrops;
	
	// thread
	final int						kFrameLength = 10; // 100 fps
	Thread							gameThread;
	long							firstFrameTime;
	
	// score
	final int						kForceDropScore = 1; // per tile * level
	final int						kRemoveLineScore = 50; // per line, squared * level
	final int						kMaxLevel = 10;
	final int						kLinesPerLevel = 10;
	
	// keys
	final int						kKeySets = 3;
	final int						kKeys = 4;
	final int						kLeft = 0;
	final int						kRight = 1;
	final int						kRotate = 2;
	final int						kDrop = 3;
	
	boolean[]						keyDown = new boolean[kKeys];
	int[]							keyToggleExecutionFrame = new int[kKeys];
	boolean[]						keyAcceptDown = new boolean[kKeys];
	int[][]							keyCode = new int[][] {
		{KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN}, 
		{KeyEvent.VK_J, KeyEvent.VK_L, KeyEvent.VK_I, KeyEvent.VK_K},
		{KeyEvent.VK_NUMPAD4, KeyEvent.VK_NUMPAD6, KeyEvent.VK_NUMPAD8, KeyEvent.VK_NUMPAD5}};
	
	// key event queue
	int[]							keyToggleFrame = new int[30];
	int[]							keyToggleCode = new int[30];
	
	// image buffer
	int[][]							lastUpdateBoard = new int[kBoardWidth][kBoardHeight];
	boolean[][]						needUpdateBoard = new boolean[kBoardWidth][kBoardHeight];
	Image							buffer;
	Graphics						bufferGraphics;
	Font							infoFont;
	Font							smallFont;
	Font							largeFont;
	boolean							lastUpdateDimmed;
	boolean							showInfo;
	int								lastUpdateInfo;
	boolean							needMajorUpdate;
	
	
	public void init() {
		addKeyListener(this);
		addMouseListener(this);
		gameThread = new Thread(this);
		gameThread.start();
		
		tileSize = getSize().height / kBoardHeight;
		showInfo = (getSize().width - kBoardWidth * tileSize >= 100);
		
		gamePaused = false;
		gameInProgress = false;
		gameOver = false;
		sleeping = false;
		buffer = createImage(getSize().width, getSize().height);
		bufferGraphics = buffer.getGraphics();
		resetBoard();
		
		infoFont = new Font("Serif", Font.PLAIN, 14);
		largeFont = new Font("Serif", Font.BOLD, getSize().height / 10);
		smallFont = new Font("Serif", Font.PLAIN, tileSize + 2);
		
		requestFocus();
		
		repaint();
	}
	
	public void start() {
		if (sleeping) {
			sleeping = false;
			gameThread.interrupt();
		}
	}
	
	public void stop() {
		sleeping = true;
	}
	
	public void leave() {
		int							tileCounter;
		int							x;
		int							y;
		boolean						filledLine;
		int							removedLines;
		
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
			board[piecePivotX + pieceX[tileCounter]][piecePivotY + pieceY[tileCounter]] = pieceColor + kPieceShapes * pieces;
		
		// check for filled lines
		removedLines = 0;
		
		for (y = 1; y < kBoardHeight - 1; y++) {
			filledLine = true;
			
			for (x = 1; x < kBoardWidth - 1; x++)
				if (board[x][y] == kEmptyTile)
					filledLine = false;
			
			if (filledLine && y != 1) {
				// copy all above lines
				int					copyY;
				
				// start from line y, move upwards
				for (copyY = y; copyY > 1; copyY--)
					for (x = 1; x < kBoardWidth - 1; x++)
						board[x][copyY] = board[x][copyY - 1];
			}
			
			if (filledLine)
				removedLines++;
		}
		
		score += level * removedLines * removedLines * kRemoveLineScore;
		lines += removedLines;
		
		if (lines > (level - 1) * kLinesPerLevel)
			level = lines / kLinesPerLevel + 1;
		if (level > kMaxLevel)
			level = kMaxLevel;
	}
	
	public void fall() {
		boolean						willStick;
		int							tileCounter;
		
		willStick = false;
		
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
			if (board[piecePivotX + pieceX[tileCounter]][piecePivotY + pieceY[tileCounter] + 1] != kEmptyTile)
				willStick = true;
		
		if (willStick) {
			stallDrops++;
			if (stallDrops == kMaxStallDrops) {
				// can't fall and has rested enough
				leave();
				newPiece();
			}
		} else {
			// can fall
			piecePivotY++;
			stallDrops = 0;
		}
	}
	
	public void forceDrop() {
		int							tileCounter;
		int							testDrop;
		int							minDrop;
		
		minDrop = kBoardHeight;
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			testDrop = 0;
			while (board[piecePivotX + pieceX[tileCounter]][piecePivotY + pieceY[tileCounter] + testDrop + 1] == kEmptyTile)
				testDrop++;
			
			if (testDrop < minDrop)
				minDrop = testDrop;
		}
		
		piecePivotY += minDrop;
		score += level * kForceDropScore * minDrop;
		
		leave();
		newPiece();
	}
	
	public boolean tryRotate(int deltaX, boolean forwards) {
		int							tileCounter;
		int[]						newX = new int[kPieceTiles];
		int[]						newY = new int[kPieceTiles];
		int							newRotations;
		
		// calculate new position
		if (!forwards) {
			// "backwards" = clockwise
			
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
				newX[tileCounter] = - pieceY[tileCounter];
				newY[tileCounter] = pieceX[tileCounter];
			}
			
			newRotations = 0;
		} else {
			// "forwards" = counter-clockwise
			
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
				newX[tileCounter] = pieceY[tileCounter];
				newY[tileCounter] = - pieceX[tileCounter];
			}
			
			newRotations = (pieceRotations + 1) % pieceAngles;
		}
		
		// check if move is valid
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			if (piecePivotX + deltaX + newX[tileCounter] < 0)
				return false; // out of bounds, long piece next to left edge
			if (board[piecePivotX + deltaX + newX[tileCounter]][piecePivotY + newY[tileCounter]] != kEmptyTile)
				return false; // invalid move
		}
		
		// valid move
		piecePivotX += deltaX;
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			pieceX[tileCounter] = newX[tileCounter];
			pieceY[tileCounter] = newY[tileCounter];
		}
		pieceRotations = newRotations;
		
		return true;
	}
	
	public void rotate() {
		boolean						forwards;
		
		if (pieceAngles == 1)
			return; // piece doesn't rotate
		
		// calculate new position
		if (pieceAngles == 2 && pieceRotations == 1) {
			// in second of two angles, rotate "backwards" = clockwise
			forwards = false;
		} else {
			// rotate "forwards" = counter-clockwise
			forwards = true;
		}
		
		if (tryRotate(0, forwards))
			return;
		if (canMove(-1, false) && tryRotate(-1, forwards))
			return;
		if (canMove(1, false) && tryRotate(1, forwards))
			return;
		if (canMove(1, false) && canMove(2, false) && tryRotate(2, forwards))
			return;
	}
	
	public boolean canMove(int deltaX, boolean followThrough) {
		int							tileCounter;
		
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			if (piecePivotX + pieceX[tileCounter] < 0 || piecePivotX + pieceX[tileCounter] >= kBoardWidth)
				return false; // outside of board
			if (board[piecePivotX + pieceX[tileCounter] + deltaX][piecePivotY + pieceY[tileCounter]] != kEmptyTile)
				return false; // will hit tile
		}
		
		// can move
		if (followThrough)
			piecePivotX += deltaX;
		
		return true;
	}
	
	public void moveLeft() {
		canMove(-1, true);
	}
	
	public void moveRight() {
		canMove(1, true);
	}
	
	public void newNextPiece() {
		int							tileCounter;
		int							nextPiece;
		int							x;
		int							y;
		
		nextPiece = (int) (Math.random() * kPieceShapes);
		
		nextPiecePivotX = kPieceOriginX;
		nextPiecePivotY = kPieceOriginY;
		
		nextPieceColor = nextPiece + kFirstPiece;
		nextPieceAngles = storedPieceAngles[nextPiece];
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			nextPieceX[tileCounter] = storedPiece[nextPiece][2 * tileCounter];
			nextPieceY[tileCounter] = storedPiece[nextPiece][2 * tileCounter + 1];
		}
		
		// force update of old nextPiece location
		for (x = 1; x < kBoardWidth; x++)
			for (y = 1; y < 3; y++)
				lastUpdateBoard[x][y] = -4;
	}
	
	public void newPiece() {
		int							tileCounter;
		
		// check if there is room for a new piece
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
			if (board[nextPiecePivotX + nextPieceX[tileCounter]]
					[nextPiecePivotY + 1 + nextPieceY[tileCounter]] != kEmptyTile) {
				// the tile is already occupied
				endGame();
				return;
			}
		
		// copy nextPiece to piece
		piecePivotX = nextPiecePivotX;
		piecePivotY = nextPiecePivotY + 1;
		pieceColor = nextPieceColor;
		pieceAngles = nextPieceAngles;
		pieceRotations = 0;
		
		for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
			pieceX[tileCounter] = nextPieceX[tileCounter];
			pieceY[tileCounter] = nextPieceY[tileCounter];
		}
		
		newNextPiece();
		
		stallDrops = 0;
		pieces++;
	}
	
	public void advanceFrame() {
		frame++;
		
		executeKeyToggle(frame);
		
		// move piece left
		if (keyDown[kLeft] && !keyDown[kRight]) { // left but not right pressed
			if (keyToggleExecutionFrame[kLeft] == frame) // first frame
				moveLeft();
			else if (frame - keyToggleExecutionFrame[kLeft] > kKeyRepeatDelay) { // is repeating and
				if ((frame - keyToggleExecutionFrame[kLeft]) % kKeyRepeatFrequency == 0) // frequency matches with frame
					moveLeft();
			}
		}
		
		// move piece right
		if (keyDown[kRight] && !keyDown[kLeft]) { // right but not left pressed
			if (keyToggleExecutionFrame[kRight] == frame) // first frame
				moveRight();
			else if (frame - keyToggleExecutionFrame[kRight] > kKeyRepeatDelay) { // is repeating and
				if ((frame - keyToggleExecutionFrame[kRight]) % kKeyRepeatFrequency == 0) // frequency matches with frame
					moveRight();
			}
		}
		
		// rotate piece
		if (keyDown[kRotate] && (keyToggleExecutionFrame[kRotate] == frame))
			rotate();
		
		// drop piece
		if (keyDown[kDrop] && (keyToggleExecutionFrame[kDrop] == frame))
			forceDrop();
		else if (frame % kFramesPerDrop[level] == 0)
			fall();
	}
	
	public void run() {
		while (true) {
			if (gameInProgress && !gamePaused) {
				int					targetFrame;
				
				targetFrame = ((int) (System.currentTimeMillis() - firstFrameTime)) / kFrameLength;
				
				while (frame < targetFrame)
					advanceFrame();
				
				time = frame * kFrameLength / 1000;
				
				repaint();
			}
			
			Thread.yield();
			
			if (sleeping) {
				try {
					Thread.sleep(86400000);
				} catch (InterruptedException e) {}
			}
		}
	}
	
	public void newGame() {
		if (gameInProgress)
			endGame();
		
		frame = 0;
		time = 0;
		level = 1;
		pieces = 0;
		lines = 0;
		score = 0;
		firstFrameTime = System.currentTimeMillis();
		
		resetBoard();
		clearKeyToggle();
		
		// init this and next piece
		newNextPiece();
		newPiece();
		
		gameInProgress = true;
		gamePaused = false;
		gameOver = false;
		
		repaint();
	}
	
	public void endGame() {
		if (!gameInProgress)
			return;
		
		gameInProgress = false;
		gamePaused = false;
		gameOver = true;
		
		repaint();
	}
	
	public void pauseGame(boolean paused) {
		if (gamePaused == paused)
			return;
		
		gamePaused = paused;
		
		// fool the game that no time has passed
		if (gamePaused)
			pauseTime = System.currentTimeMillis();
		else
			firstFrameTime += System.currentTimeMillis() - pauseTime;
		
		repaint();
	}
	
	public void resetBoard() {
		int							x;
		int							y;
		
		for (x = 0; x < kBoardWidth; x++)
			for (y = 0; y < kBoardHeight; y++) {
				if ((x == 0) || (y == 0) || (x == kBoardWidth - 1) || (y == kBoardHeight - 1))
					board[x][y] = kBorderTile;
				else
					board[x][y] = kEmptyTile;
				
				lastUpdateBoard[x][y] = -1;
			}
	}
	
	public void drawTile(int x, int y, int tile, Color fillColor) {
		bufferGraphics.setColor(fillColor);
		bufferGraphics.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
		
		if (tile != kEmptyTile) {
			if (tileSize >= 12) {
				// inner of two border pixels
				if (x != 0 && tile != lastUpdateBoard[x - 1][y]) {
					bufferGraphics.setColor(fillColor.brighter());
					bufferGraphics.drawLine(x * tileSize + 1, y * tileSize,
						x * tileSize + 1, (y + 1) * tileSize - 1);
				}
				if (y != 0 && tile != lastUpdateBoard[x][y - 1]) {
					bufferGraphics.setColor(fillColor.brighter());
					bufferGraphics.drawLine(x * tileSize, y * tileSize + 1,
						(x + 1) * tileSize - 1, y * tileSize + 1);
				}
				if (x != kBoardWidth - 1 && tile != lastUpdateBoard[x + 1][y]) {
					bufferGraphics.setColor(fillColor.darker());
					bufferGraphics.drawLine((x + 1) * tileSize - 2, y * tileSize,
						(x + 1) * tileSize - 2, (y + 1) * tileSize - 1);
				}
				if (y != kBoardHeight - 1 && tile != lastUpdateBoard[x][y + 1]) {
					bufferGraphics.setColor(fillColor.darker());
					bufferGraphics.drawLine(x * tileSize, (y + 1) * tileSize - 2,
						(x + 1) * tileSize - 1, (y + 1) * tileSize - 2);
				}
			}
			
			// outer border pixels
			if (x != 0 && tile != lastUpdateBoard[x - 1][y]) {
				bufferGraphics.setColor(fillColor.brighter());
				bufferGraphics.drawLine(x * tileSize, y * tileSize,
					x * tileSize, (y + 1) * tileSize - 1);
			}
			if (y != 0 && tile != lastUpdateBoard[x][y - 1]) {
				bufferGraphics.setColor(fillColor.brighter());
				bufferGraphics.drawLine(x * tileSize, y * tileSize,
					(x + 1) * tileSize - 1, y * tileSize);
			}
			if (x != kBoardWidth - 1 && tile != lastUpdateBoard[x + 1][y]) {
				bufferGraphics.setColor(fillColor.darker());
				bufferGraphics.drawLine((x + 1) * tileSize - 1, y * tileSize,
					(x + 1) * tileSize - 1, (y + 1) * tileSize - 1);
			}
			if (y != kBoardHeight - 1 && tile != lastUpdateBoard[x][y + 1]) {
				bufferGraphics.setColor(fillColor.darker());
				bufferGraphics.drawLine(x * tileSize, (y + 1) * tileSize - 1,
					(x + 1) * tileSize - 1, (y + 1) * tileSize - 1);
			}
		}
	}
	
	public void update(Graphics theGraphics) {
		int							x;
		int							y;
		int							left;
		int							top;
		int							updateLeft = kBoardWidth;
		int							updateRight = 0;
		int							updateTop = kBoardHeight;
		int							updateBottom = 0;
		int							tileCounter;
		Color						fillColor;
		boolean						dimmed;
		int							infoWidth;
		int							infoHeight;
		
		int[]						frozenPieceX = new int[kPieceTiles];
		int[]						frozenPieceY = new int[kPieceTiles];
		int[]						frozenNextPieceX = new int[kPieceTiles];
		int[]						frozenNextPieceY = new int[kPieceTiles];
		
		if (needMajorUpdate) {
			updateLeft = 0;
			updateRight = getSize().width / tileSize + 1;
			updateTop = 0;
			updateBottom = kBoardHeight;
			needMajorUpdate = false;
		}
		
		if (kDebugDraw) {
			theGraphics.setColor(Color.lightGray);
			theGraphics.fillRect(0, 0, getSize().width, getSize().height);
			theGraphics.setColor(Color.black);
		}
		
		dimmed = (gamePaused || !gameInProgress);
		
		infoWidth = getSize().width - kBoardWidth * tileSize;
		infoHeight = 100;
		if (showInfo) {
			if (!gameInProgress || (time + score + pieces != lastUpdateInfo) || dimmed) {
				bufferGraphics.setColor(Color.lightGray);
				bufferGraphics.fillRect(kBoardWidth * tileSize, 0, infoWidth, infoHeight);
				
				bufferGraphics.setColor(Color.black);
				
				bufferGraphics.setFont(infoFont);
				bufferGraphics.drawString("time: " + time / 60 + ":" + ((time % 60) / 10) + "" + time % 10,
					kBoardWidth * tileSize + 50 - bufferGraphics.getFontMetrics().stringWidth("time: "), 20);
				bufferGraphics.drawString("score: " + score,
					kBoardWidth * tileSize + 50 - bufferGraphics.getFontMetrics().stringWidth("score: "), 36);
				bufferGraphics.drawString("lines: " + lines,
					kBoardWidth * tileSize + 50 - bufferGraphics.getFontMetrics().stringWidth("lines: "), 52);
				bufferGraphics.drawString("level: " + level,
					kBoardWidth * tileSize + 50 - bufferGraphics.getFontMetrics().stringWidth("level: "), 68);
				bufferGraphics.drawString("pieces: " + pieces,
					kBoardWidth * tileSize + 50 - bufferGraphics.getFontMetrics().stringWidth("pieces: "), 84);
				lastUpdateInfo = time + score + pieces;
				
				theGraphics.setClip(tileSize * kBoardWidth, 0, infoWidth, infoHeight);
				theGraphics.drawImage(buffer, 0, 0, null);
			}
			if (!gameInProgress || dimmed) {
				bufferGraphics.setColor(Color.lightGray);
				bufferGraphics.fillRect(kBoardWidth * tileSize, infoHeight, infoWidth, getSize().height - infoHeight);
				updateRight = 100;
			}
		}
		else if (infoWidth > 0) {
			bufferGraphics.setColor(Color.lightGray);
			bufferGraphics.fillRect(kBoardWidth * tileSize, 0, infoWidth, getSize().height);
			updateRight = 100;
		}
		bufferGraphics.setColor(Color.black);
		
		// check board for update needs
		if (lastUpdateDimmed == dimmed) {
			for (y = 0; y < kBoardHeight; y++)
				for (x = 0; x < kBoardWidth; x++)
					needUpdateBoard[x][y] = false;
		} else {
			for (y = 0; y < kBoardHeight; y++)
				for (x = 0; x < kBoardWidth; x++)
					needUpdateBoard[x][y] = true;
		}
		
		for (y = 0; y < kBoardHeight; y++) {
			for (x = 0; x < kBoardWidth; x++) {
				if (board[x][y] != lastUpdateBoard[x][y]) {
					needUpdateBoard[x][y] = true;
					
					if (lastUpdateBoard[x][y] != -3) {
						// was _not_ occupied by a falling piece
						// update adjacent tiles too
						if (x != 0)
							needUpdateBoard[x - 1][y] = true;
						if (y != 0)
							needUpdateBoard[x][y - 1] = true;
						if (x != kBoardWidth - 1)
							needUpdateBoard[x + 1][y] = true;
						if (y != kBoardHeight - 1)
							needUpdateBoard[x][y + 1] = true;
					}
					
					lastUpdateBoard[x][y] = board[x][y];
				}
			}
		}
		
		// draw board
		for (y = 0; y < kBoardHeight; y++) {
			for (x = 0; x < kBoardWidth; x++) {
				if (needUpdateBoard[x][y]) {
					fillColor = kColor[((board[x][y] - kFirstPiece) % kPieceShapes) + kFirstPiece];
					if (dimmed)
						fillColor = fillColor.darker();
					
					drawTile(x, y, board[x][y], fillColor);
					
					if (x < updateLeft)
						updateLeft = x;
					if (x > updateRight)
						updateRight = x;
					if (y < updateTop)
						updateTop = y;
					if (y > updateBottom)
						updateBottom = y;
				}
			}
		}
		
		if (gameInProgress) {
			// draw nextPiece
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
				frozenNextPieceX[tileCounter] = nextPiecePivotX + nextPieceX[tileCounter];
				frozenNextPieceY[tileCounter] = nextPiecePivotY + nextPieceY[tileCounter];
			}
			
			if ((piecePivotY - nextPiecePivotY < 4) || dimmed || updateTop < 3) {
				fillColor = kColor[nextPieceColor].darker();
				if (dimmed)
					fillColor = fillColor.darker();
				
				for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
					lastUpdateBoard[frozenNextPieceX[tileCounter]][frozenNextPieceY[tileCounter]] = -2;
				
				for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
					x = frozenNextPieceX[tileCounter];
					y = frozenNextPieceY[tileCounter];
					
					bufferGraphics.setColor(fillColor);
					
					drawTile(x, y, -2, fillColor);
					
					if (x < updateLeft)
						updateLeft = x;
					if (x > updateRight)
						updateRight = x;
					if (y < updateTop)
						updateTop = y;
					if (y > updateBottom)
						updateBottom = y;
				}
				
				for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
					lastUpdateBoard[frozenNextPieceX[tileCounter]][frozenNextPieceY[tileCounter]] = kEmptyTile;
			}
			
			// draw piece
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
				frozenPieceX[tileCounter] = piecePivotX + pieceX[tileCounter];
				frozenPieceY[tileCounter] = piecePivotY + pieceY[tileCounter];
			}
			
			fillColor = kColor[pieceColor];
			if (dimmed)
				fillColor = fillColor.darker();
			
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++)
				lastUpdateBoard[frozenPieceX[tileCounter]][frozenPieceY[tileCounter]] = -3;
			
			for (tileCounter = 0; tileCounter < kPieceTiles; tileCounter++) {
				x = frozenPieceX[tileCounter];
				y = frozenPieceY[tileCounter];
				
				bufferGraphics.setColor(fillColor);
				
				drawTile(x, y, -3, fillColor);
				
				if (x < updateLeft)
					updateLeft = x;
				if (x > updateRight)
					updateRight = x;
				if (y < updateTop)
					updateTop = y;
				if (y > updateBottom)
					updateBottom = y;
			}
		}
		
		if (gameInProgress && gamePaused) {
			bufferGraphics.setColor(Color.lightGray);
			bufferGraphics.setFont(largeFont);
			bufferGraphics.drawString("P", (tileSize * (kBoardWidth - 6) - bufferGraphics.getFontMetrics().stringWidth("P")) / 2, 6 * tileSize);
			bufferGraphics.drawString("A", (tileSize * (kBoardWidth - 3) - bufferGraphics.getFontMetrics().stringWidth("A")) / 2, 9 * tileSize);
			bufferGraphics.drawString("U", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("U")) / 2, 12 * tileSize);
			bufferGraphics.drawString("S", (tileSize * (kBoardWidth + 3) - bufferGraphics.getFontMetrics().stringWidth("S")) / 2, 15 * tileSize);
			bufferGraphics.drawString("E", (tileSize * (kBoardWidth + 6) - bufferGraphics.getFontMetrics().stringWidth("E")) / 2, 18 * tileSize);
			bufferGraphics.setFont(smallFont);
			bufferGraphics.drawString("click to continue", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("click to continue")) / 2, 21 * tileSize);
			updateLeft = 0;
			updateTop = 0;
			if (updateRight < kBoardWidth - 1)
				updateRight = kBoardWidth - 1;
			updateBottom = kBoardHeight - 1;
		}
		else if (!gameInProgress) {
			bufferGraphics.setColor(Color.lightGray);
			bufferGraphics.setFont(smallFont);
			bufferGraphics.drawString("click to start", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("click to start")) / 2, 18 * tileSize);
			updateLeft = 0;
			updateTop = 0;
			if (updateRight < kBoardWidth - 1)
				updateRight = kBoardWidth - 1;
			updateBottom = kBoardHeight - 1;
		}
		if (!gameInProgress && gameOver) {
			bufferGraphics.setColor(Color.lightGray);
			bufferGraphics.setFont(largeFont);
			bufferGraphics.drawString("GAME", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("GAME")) / 2, 6 * tileSize);
			bufferGraphics.drawString("OVER", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("OVER")) / 2, 9 * tileSize);
			updateLeft = 0;
			updateTop = 0;
			if (updateRight < kBoardWidth - 1)
				updateRight = kBoardWidth - 1;
			updateBottom = kBoardHeight - 1;
		}
		if (!gameInProgress && !gameOver) {
			bufferGraphics.setColor(Color.lightGray);
			bufferGraphics.setFont(largeFont);
			bufferGraphics.drawString("Return", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("Return")) / 2, 6 * tileSize);
			bufferGraphics.setFont(smallFont);
			bufferGraphics.drawString("of the", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("of the")) / 2, 8 * tileSize);
			bufferGraphics.setFont(largeFont);
			bufferGraphics.drawString("Tetris", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("Tetris")) / 2, 11 * tileSize);
			bufferGraphics.setFont(smallFont);
			bufferGraphics.drawString("by Bjšrn Carlin", (tileSize * kBoardWidth - bufferGraphics.getFontMetrics().stringWidth("by Bjšrn Carlin")) / 2, 14 * tileSize);
			updateLeft = 0;
			updateTop = 0;
			if (updateRight < kBoardWidth - 1)
				updateRight = kBoardWidth - 1;
			updateBottom = kBoardHeight - 1;
		}
		
		bufferGraphics.setColor(Color.black);
		lastUpdateDimmed = dimmed;
		
		theGraphics.setClip(tileSize * updateLeft, tileSize * updateTop, tileSize * (updateRight - updateLeft + 1), tileSize * (updateBottom - updateTop + 1));
		theGraphics.drawImage(buffer, 0, 0, null);
	}
	
	public void paint(Graphics theGraphics) {
		needMajorUpdate = true;
		update(theGraphics);
	}
	
	public void mouseClicked(MouseEvent theEvent) {
		if (gameInProgress) {
			pauseGame(!gamePaused);
		} else {
			newGame();
		}
	}
	
	public void addKeyToggle(int frame, int code) {
		int							slotCounter;
		
		for (slotCounter = 0; slotCounter < keyToggleFrame.length; slotCounter++)
			if (keyToggleFrame[slotCounter] == 0x7FFFFFFF) {
				keyToggleFrame[slotCounter] = frame;
				keyToggleCode[slotCounter] = code;
				return;
			}
	}
	
	public void executeKeyToggle(int frame) {
		int							slotCounter;
		int							keyCounter;
		boolean						alreadyExecuted[] = new boolean[kKeys];
		
		for (keyCounter = 0; keyCounter < kKeys; keyCounter++)
			alreadyExecuted[keyCounter] = false;
		
		for (slotCounter = 0; slotCounter < keyToggleFrame.length; slotCounter++)
			if (keyToggleFrame[slotCounter] <= frame)
				if (!alreadyExecuted[keyToggleCode[slotCounter]]) {
					// toggle key keyToggleCode
					keyDown[keyToggleCode[slotCounter]] = !keyDown[keyToggleCode[slotCounter]];
					keyToggleExecutionFrame[keyToggleCode[slotCounter]] = frame;
					keyToggleFrame[slotCounter] = 0x7FFFFFFF;
					
					alreadyExecuted[keyToggleCode[slotCounter]] = true;
				}
	}
	
	public void clearKeyToggle() {
		int							slotCounter;
		int							keyCounter;
		
		for (slotCounter = 0; slotCounter < keyToggleFrame.length; slotCounter++)
			keyToggleFrame[slotCounter] = 0x7FFFFFFF;
		
		for (keyCounter = 0; keyCounter < kKeys; keyCounter++) {
			keyDown[keyCounter] = false;
			keyAcceptDown[keyCounter] = true;
			keyToggleExecutionFrame[keyCounter] = 0;
		}
	}
	
	public void keyPressed(KeyEvent theEvent) {
		int							keyEventCode;
		
		keyEventCode = theEvent.getKeyCode();
		
		if (gameInProgress && !gamePaused) {
			int						keySetCounter;
			int						keyCounter;
			
			for (keySetCounter = 0; keySetCounter < kKeySets; keySetCounter++)
				for (keyCounter = 0; keyCounter < kKeys; keyCounter++)
					if (keyEventCode == keyCode[keySetCounter][keyCounter])
						if (keyAcceptDown[keyCounter]) {
							int			frame;
							
							frame = ((int) (theEvent.getWhen() - firstFrameTime)) / kFrameLength;
							addKeyToggle(frame, keyCounter);
							keyAcceptDown[keyCounter] = false;
							
							return;
						}
		}
		
		if (gameInProgress && keyEventCode == KeyEvent.VK_P)
			pauseGame(!gamePaused);
		else if (gameInProgress && keyEventCode == KeyEvent.VK_PAUSE)
			pauseGame(true);
		else if (gameInProgress && keyEventCode == KeyEvent.VK_ESCAPE)
			pauseGame(true);
		else if (keyEventCode == KeyEvent.VK_N)
			newGame();
		else if (keyEventCode == KeyEvent.VK_G)
			newGame();
		else if (keyEventCode == KeyEvent.VK_SPACE && level < kMaxLevel)
			level++;
	}
	
	public void keyReleased(KeyEvent theEvent) {
		int							keyEventCode;
		
		keyEventCode = theEvent.getKeyCode();
		
		if (gameInProgress && !gamePaused) {
			int						keySetCounter;
			int						keyCounter;
			
			for (keySetCounter = 0; keySetCounter < kKeySets; keySetCounter++)
				for (keyCounter = 0; keyCounter < kKeys; keyCounter++)
					if (keyEventCode == keyCode[keySetCounter][keyCounter])
						if (!keyAcceptDown[keyCounter]) {
							int			frame;
							
							frame = ((int) (theEvent.getWhen() - firstFrameTime)) / kFrameLength;
							addKeyToggle(frame, keyCounter);
							keyAcceptDown[keyCounter] = true;
							
							return;
						}
		}
	}
	
	// dummies
	public void keyTyped(KeyEvent theEvent) {}
	public void mousePressed(MouseEvent theEvent) {}
	public void mouseReleased(MouseEvent theEvent) {}
	public void mouseEntered(MouseEvent theEvent) {}
	public void mouseExited(MouseEvent theEvent) {}
}


