Tetris Swing implemention


Recently、I've seen an interesting post:  http://www.iteye.com/topic/595321,it's a java implement ation of Tetris.While being a long time game plyer(My first game was the gold diggame and IBM XT back in 1988,subsequently I playd Koei's Romance of 3 kingdoms,from-Xmars,frome II filt,Hears,Hers。
 
Though I like this post a lot,the code there suffers multi-threading problem.While I don't claim I am a good game developer or an expert on multi-threading,I try to fix is problem.At the same time,coderst-waderst-ine e e e e e e-ingt-inderst-inderst-inderst 
 
Most of the code and the gif files are from the original author,I modified some of them.
 
The first class the tetris block、for more details、check the wiki page:  http://en.wikipedia.org/wiki/Tetris.
 
 
package my.test1;

import java.awt.Image;
import javax.swing.ImageIcon;

public class TetrisBlock
{
    public int type;
    public int orientation;
    public int color = 1;
    public int x;
    public int y;

    private static int[][][][] blockmeshs =
        {
            {
                {{0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 1, 0, 0}},/* l */
                {{0, 0, 0, 0},
                 {1, 1, 1, 1},
                 {0, 0, 0, 0},
                 {0, 0, 0, 0}}},/*-*/
            {
                {{0, 0, 0, 0},
                 {1, 1, 0, 0},
                 {0, 1, 1, 0},
                 {0, 0, 0, 0}},/* z */
                {{0, 0, 0, 0},
                 {0, 0, 1, 0},
                 {0, 1, 1, 0},
                 {0, 1, 0, 0}}},/* z| */
            {
                {{0, 0, 0, 0},
                 {0, 1, 1, 0},
                 {1, 1, 0, 0},
                 {0, 0, 0, 0}},/* xz */
                {{0, 1, 0, 0},
                 {0, 1, 1, 0},
                 {0, 0, 1, 0},
                 {0, 0, 0, 0}}},/* xz| */
            {
                {{0, 0, 0, 0},
                 {0, 1, 1, 0},
                 {0, 1, 1, 0},
                 {0, 0, 0, 0}}},/** []*/
            {
                {{0, 1, 1, 0},
                 {0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 0, 0, 0},
                 {1, 1, 1, 0},
                 {0, 0, 1, 0},
                 {0, 0, 0, 0}},
                {{0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {1, 1, 0, 0},
                 {0, 0, 0, 0}},
                {{1, 0, 0, 0},
                 {1, 1, 1, 0},
                 {0, 0, 0, 0},
                 {0, 0, 0, 0}}},/* f */
            {
                {{1, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 0, 1, 0},
                 {1, 1, 1, 0},
                 {0, 0, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 1, 1, 0},
                 {0, 0, 0, 0}},
                {{0, 0, 0, 0},
                 {1, 1, 1, 0},
                 {1, 0, 0, 0},
                 {0, 0, 0, 0}}},/* xf */
            {
                {{0, 1, 0, 0},
                 {1, 1, 1, 0},
                 {0, 0, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 1, 0, 0},
                 {0, 1, 1, 0},
                 {0, 1, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 0, 0, 0},
                 {1, 1, 1, 0},
                 {0, 1, 0, 0},
                 {0, 0, 0, 0}},
                {{0, 1, 0, 0},
                 {1, 1, 0, 0},
                 {0, 1, 0, 0},
                 {0, 0, 0, 0}} /* t */
            }
        };

    private static Image[] images =
        {
            new ImageIcon("domaintest/pics/red.gif").getImage(), // this is just a place holder, not used.
            new ImageIcon("domaintest/pics/lightblue.gif").getImage(),
            new ImageIcon("domaintest/pics/pink.gif").getImage(),
            new ImageIcon("domaintest/pics/blue.gif").getImage(),
            new ImageIcon("domaintest/pics/orange.gif").getImage(),
            new ImageIcon("domaintest/pics/green.gif").getImage(),
            new ImageIcon("domaintest/pics/red.gif").getImage() // this is the real red image
        };

    public static Image image(int color)
    {
        return images[color];
    }

    public static int blocksize() { return 6; } // the length of the really used images.

    public boolean isOccupied(int row, int col)
    {
        return blockmeshs[type][orientation][row][col] != 0;
    }
    
    public void rotate()
    {
        orientation++;
        if (orientation >= blockmeshs[type].length)
            orientation = 0;
    }

    public void settle(TetrisBoard tetrisBoard)
    {
        int[][] b = blockmeshs[type][orientation];
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                int a = b[i][j];
                if (a != 0)
                {
                    tetrisBoard.settle(y+i, x+j, color);
                }
            }
        }

        for (int i=y+3; i>y; i--)
        {
            tetrisBoard.removeFilledRow(i);
        }
    }

    public boolean canMoveDown(TetrisBoard tetrisBoard)
    {
        int[][] b = blockmeshs[type][orientation];
        int yy = y + 1;
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                if (yy + i >= tetrisBoard.length() && b[i][j] != 0)
                    return false;
                if (yy+i < tetrisBoard.length() && x+j < tetrisBoard.width() &&
                    tetrisBoard.isOccupied(yy+i, x+j) && b[i][j] != 0)
                    return false;
            }
        }

        return true;
    }

    public boolean canMoveLeft(TetrisBoard tetrisBoard)
    {
        int[][] b = blockmeshs[type][orientation];
        int xx = x - 1;
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                if (xx + j <= -1 && b[i][j] != 0)
                    return false;
                if (y+i < tetrisBoard.length() && xx+j < tetrisBoard.width() &&
                    tetrisBoard.isOccupied(y+i, xx+j) && b[i][j] != 0)
                    return false;
            }
        }

        return true;
    }

    public boolean canMoveRight(TetrisBoard tetrisBoard)
    {
        int[][] b = blockmeshs[type][orientation];
        int xx = x + 1;
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                if (xx + j >= tetrisBoard.width() && b[i][j] != 0)
                    return false;
                if (y+i < tetrisBoard.length() && xx+j < tetrisBoard.width() &&
                    tetrisBoard.isOccupied(y+i, xx+j) && b[i][j] != 0)
                    return false;
            }
        }

        return true;
    }

    public boolean canRotate(TetrisBoard tetrisBoard)
    {
        int oo = orientation + 1;
        if (oo >= blockmeshs[type].length)
            oo = 0;

        int[][] b = blockmeshs[type][oo];
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                if (y+i < tetrisBoard.length() && x+j < tetrisBoard.width() &&
                    tetrisBoard.isOccupied(y+i, x+j) && b[i][j] != 0)
                    return false;
            }
        }

        return true;
    }
}
 The tetris block is encoded in a 4 X 4 matix(look at the 1's in the matix).All rotations of each block form an array.All such arrays form a biggaray carred blockmeshs.The shs she sh.The Shoroud be 7 imeffect the
 
Though this class has>200 line of code,half of them are static data,so I can live with it.This class can be unit-tested without Swing. 
 
The Tetris Board class is:
 
package my.test1;

public class TetrisBoard
{
    private int length = 21;
    private int width = 10;
    public int[][] board = new int[length][width];

    public int width() { return width; }
    
    public int length() { return length;}

    public boolean isOccupied(int row, int col)
    {
        if (row < 0 || row >= length || col < 0 || col >= width)
            return false;
        return board[row][col] != 0;
    }

    public int colorOf(int row, int col)
    {
        return board[row][col];
    }
    
    public void settle(int row, int col, int color)
    {
        if (row < length && col < width)
            board[row][col] = color;
    }

    public void removeFilledRow(int row)
    {
        if (row >= length) return;
        
        boolean notFilled = false;
        for (int j=0; j<board[0].length; j++)
        {
            if (board[row][j] == 0)
            {
                notFilled = true;
                break;
            }
        }

        if (!notFilled)
        {
            for (int j=row; j>0; j--)
                System.arraycopy(board[j-1], 0, board[j], 0, board[0].length);
        }
    }
}
 
The se two classis are tightly coupled.I chose to put some methods in one rather than another,the main motivation is performance(multi dimension array can can be slow,though it doesn't marter much case.case.
 
The next class is the compsition of the above two:
 
package my.test1;

/**
 * Other features, such as scores, next shape, stop/restart, change speed
 */
import java.util.Random;

public class Tetris
{
    public TetrisBlock movingBlock = new TetrisBlock();
    public TetrisBoard board = new TetrisBoard();

    public boolean finished = false;
    private Random generator = new Random();

    public synchronized void play()
    {
        if (movingBlock.canMoveDown(board))
            movingBlock.y++;
        else
        {
            if (movingBlock.y == 0)
            {
                finished = true;
            }
            else
            {
                movingBlock.settle(board);
                newBlock();
            }
        }
    }

    private void newBlock()
    {
        movingBlock = new TetrisBlock();
        movingBlock.x = 3;
        movingBlock.y = 0;
        movingBlock.type = generator.nextInt(TetrisBlock.blocksize()+1);
        movingBlock.color = generator.nextInt(TetrisBlock.blocksize()) + 1; // should be
        movingBlock.orientation = 0;
    }
}
 The method Play()is synchronized because the data can be modified by this class and user input(arrow keys)
 
Now we are done with the game logic、it's time to work on the GUI.The first class is the drawing class:
 
package my.test1;

import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;

public class TetrisPanel extends JPanel
{
    public Tetris tetris;

    public TetrisPanel(Tetris tetris)
    {
        super();
        this.setFocusable(true);        
        this.setPreferredSize(new Dimension(150, 315));
        this.tetris = tetris;
        addKeyListener(new TetrisGuiListener(tetris, this));
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        g.fillRect(0, 0, 150, 315);
        for (int i=0; i<tetris.board.length(); i++)
        {
            for (int j=0; j<tetris.board.width(); j++)
            {
                if (tetris.board.isOccupied(i, j))
                    g.drawImage(TetrisBlock.image(tetris.board.colorOf(i, j)),
                            j * 15, i * 15 , null);
            }
        }       
        
        for (int i=0; i<4; i++)
        {
            for (int j=0; j<4; j++)
            {
                if (tetris.movingBlock.isOccupied(i, j) && (i + tetris.movingBlock.y) <= 21 &&
                    !tetris.board.isOccupied(i + tetris.movingBlock.y, j + tetris.movingBlock.x))
                    g.drawImage(TetrisBlock.image(tetris.movingBlock.color),
                        ((j + tetris.movingBlock.x) * 15), ((i + tetris.movingBlock.y) * 15) , null);
            }
        }
    }
}
 We just override the paintComponent()method with our logic.The second parts of it is a little nonintuitive because we need a special logic to write the last block.My current design to draw the parther partatchore
 
The listener class is as follows:
 
package my.test1;

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class TetrisGuiListener implements KeyListener
{
    private Tetris tetris;
    private Component gui;

    public TetrisGuiListener(Tetris tetris, Component gui)
    {
        this.tetris = tetris;
        this.gui = gui;
    }

    public void keyPressed(KeyEvent e)
    {        
        synchronized(tetris)
        {            
            if (e.getKeyCode() == 65 || e.getKeyCode() == 37)
            {
                if (tetris.movingBlock.canMoveLeft(tetris.board))
                    tetris.movingBlock.x--;
            }
            else if (e.getKeyCode() == 68 || e.getKeyCode() == 39)
            {
                if (tetris.movingBlock.canMoveRight(tetris.board))
                    tetris.movingBlock.x++;
            }
            else if (e.getKeyCode() == 83 || e.getKeyCode() == 40)
            {
                if (tetris.movingBlock.canMoveDown(tetris.board))
                    tetris.movingBlock.y++;
            }
            else if (e.getKeyCode() == 87 || e.getKeyCode() == 38)
            {
                if (tetris.movingBlock.canRotate(tetris.board))
                    tetris.movingBlock.rotate();
            }

            gui.repaint();
        }
    }

    public void keyTyped(KeyEvent e) { }

    public void keyReleased(KeyEvent e) { }
}
 Again,we need to synchronize the data sinc this classing on the EDT.
 
The next class is a wrapper of Tetris class、using SwingWorkカー:
 
package my.test1;
/**
 * http://download.oracle.com/javase/tutorial/uiswing/concurrency/index.html
 * http://java.sun.com/developer/technicalArticles/javase/swingworker/
 * http://www.javaworld.com/javaworld/jw-08-2007/jw-08-swingthreading.html?page=1
 * http://developerlife.com/tutorials/?p=15
 * http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html
 * http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html
 * http://java.sun.com/products/jfc/tsc/articles/threads/threads3.html
 * http://stackoverflow.com/questions/1505427/multithreading-with-java-swing-for-a-simple-2d-animation
 * http://www.javaranch.com/journal/200410/JavaDesigns-SwingMultithreading.html
 * http://kenai.com/projects/trident/pages/SimpleSwingExample
 * http://java.sun.com/products/jfc/tsc/articles/painting/
 */
import java.awt.Component;
import java.util.List;
import javax.swing.SwingWorker;

public class TetrisGame extends SwingWorker<Tetris, Tetris>
{
    private Tetris tetris;
    private Component gui;

    public TetrisGame(Tetris tetris, Component gui)
    {
        this.tetris = tetris;
        this.gui = gui;
    }

    @Override
    protected Tetris doInBackground() throws Exception
    {
        try
        {
            while (!tetris.finished) // && !isCancelled())
            {
                tetris.play();
                publish(tetris);
                try { Thread.sleep(200); } catch (Exception ex) { ex.printStackTrace(); }
            }

            return tetris;
        }
        catch (Throwable t)
        {
            t.printStackTrace();
            return null;
        }
    }

    // This is called fro EDT
    @Override
    protected void process(List<Tetris> tetrisList)
    {
        Tetris t = tetrisList.get(tetrisList.size()-1);
        synchronized(t)
        {
            gui.repaint();
        }
    }
}
 For more information on SwingWork,check the links in the java doc section.This class,except the method markd,will run in a separate thread.The separate thread.EDT both modify the Tetris.class.
 
Finally、a window class to stith everthing together.
 
package my.test1;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;


public class TetrisWindow extends JFrame
{
    public TetrisWindow()
    {
        super("Tetris");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setPreferredSize(new Dimension(160, 355));
        this.setResizable(false);

        Tetris tetris = new Tetris();
        TetrisPanel tetrisPanel = new TetrisPanel(tetris);
        Container content = getContentPane();
        content.setLayout(new FlowLayout());
        content.add(tetrisPanel);

        this.pack();

        TetrisGame game = new TetrisGame(tetris, tetrisPanel);
        game.execute();
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable() {
            public void run()
            {
                new TetrisWindow().setVisible(true);
            }
        });
    }
}
 Remember,we need to fire off the window from EDT too,so we use SwingUtilityes.
 
Here is a screenshot.
 
Other references on this topic:
http://www.cs.unc.edu/~plto/COMP 14/Asignments/tetris/tetris.
http://www.ibm.com/developerworks/java/library/j-tetris/
http://gametuto.com/tetris-tutorial-in-c-render-independent/
http://zetcode.com/tutorials/javaswingtutorial/thetetrisgame/