Csci 210: Tetris I

(Laura Toma adapted from Eric Chown based on a lab developed at Stanford by Nick Parlante)

Overview

In this lab you will start developing a Tetris game. The design will emphasize elemental Object Oriented Programming (OOP) design -- using encapsulation, to divide a big scary problem into many friendly little independently testable problems. In the first part of Tetris (this lab) you will set up the Piece class. In the second part (next lab) you will builds the Board class and some other fun bits. That will probably be more than enough, so I'll probably provide the last part for you.

For reasons that will become clear later, there is a theme of efficiency in this design. We are not just writing classes that implement Tetris. We are writing classes that implement Tetris quickly.

Background

You all know Tetris..

Piece

There are seven pieces in standard Tetris.

Each standard piece is composed of four blocks. The two "L" and "dog" pieces are mirror images of each other, but we'll just think of them as similar but distinct pieces.

A piece can be rotated 90 degrees counter-clockwise to yield another piece. Enough rotations get you back to the original piece--- for example rotating a dog twice brings you back to the original state. Essentially, each tetris piece belongs to a family of between one and four distinct rotations. The square has one, the dogs have two, and the L's have four. For example, here are the four rotations (going counter-clockwise) of the left hand L:

Our abstraction will be that a piece object represents a single Tetris piece in a single rotation, so the above diagram shows four different piece objects.

Body

A piece is represented by the coordinates of its blocks which are known as the body of the piece. Each Piece has its own little coordinate system with its (0,0) origin in the lower left hand corner of the rectangle that encloses the body. The coordinates of blocks in the body are relative to the origin of the piece. So, the four points of the square piece are then:

(0,0)  <= the lower left-hand block
(0,1)  <= the upper left-hand block     
(1,0)  <= the lower right-hand block        
(1,1)  <= the upper right-hand block

Notice that not all pieces will actually have a block at (0,0). For example, the body of the following rotation of the right dog:

has the body:

[(0,1),(0,2),(1,0),(1,1)] 

A piece is completely defined by its body -- all its other qualities, such as its height and width, can be computed from the body. The above right dog was a width of 2 and height of 3. Another quality which turns out to be useful for playing Tetris quickly is the skirt of a piece.

Skirt

The skirt will be an int[] array, as long as the piece is wide, that stores the lowest y value for each x value in the piece coordinate system.

The skirt of this piece is {1, 0}. We assume that pieces are not disconnected (do not have holes in them)---for every x in the piece coordinate system, there is at least one block in the piece for that x.

Rotations

The Piece class needs to provide a way for clients to access the various piece rotations. When a client has a particular piece (e.g. the right dog) and the player hits the rotate key, the client will need to immediately know what the piece will be when it is rotated. To accomplish this we will let the client ask each piece for the "next rotation." This will be accomplished by a method that returns a piece object that is simply the current object after rotation. This is an example of an "immutable" paradigm -- there is not a rotate() message that changes the receiver. Instead, the piece objects and their rotations are all created ahead of time and are read-only. The client is given the ability to iterate over them (the String class is another example of the immutable paradigm). This is a much faster solution than computing rotations on the fly.

For efficiency, we will pre-compute all the rotations just once such that a Piece will actually be a circular list of all of the rotations. Given a piece object, the client will be able to get the "next" piece by just retrieving the next item in the circular list. In essence, this allows the client to obtain each rotation in constant time. This strategy will be used on virtually all public methods over the course of this project. For example the skirt should be computed at piece creation time so that at runtime it is merely returned, not computed then returned.

It is worth making the last point again: all of the rotations, skirts, etc. are computed when the initial pieces are made. This is done exactly once. Once they are made then the Tetris game during its game play just accesses what already exists.

Piece.java code

The Piece.java starter files has a few simple things filled in and it includes the prototypes for the public methods you need to implement. Do not change the public prototypes ---- your Piece will need to fit in with the later components. You will want to add your own private helper methods which can have whatever prototypes you like. You can find this file (and all the others for this lab) here.

Rotation strategy

The overall piece rotation strategy uses a single, static array with the "first" rotation for each of the 7 pieces. In the code you can find this in the getPieces method which is already written for you. Each of the first pieces will become the first node in a little circular linked list of the rotations of that piece. The first node will be passed into the pieceRow method which is the key method you have to write for the first part of Tetris.

The player uses nextRotation() to iterate through all the rotations of a tetris piece. The array is allocated the first time the client calls getPieces() -- this trick is called "lazy evaluation" -- build the thing only when it's actually used. getPieces will set up the first Piece in the list and ask pieceRow to build the rest of the list. pieceRow will work by starting with the first Piece, rotating it to get a new Piece, linking that new Piece to the first Piece, and then repeating the process until the rotated Piece is equal to the new Piece (at that point you will have generated the entire list and need to make the list circulur).

The hard work in the program is figuring out how to start with an initial array of points and use them as a starting point to generate a circular list. Each item in the list will itself be a Piece (which means that you'll need to call the Piece constructor).

Rotation tactics

You will need to figure out an algorithm to do the actual rotation. Get a nice sharp pencil. Draw a piece and its rotation. Write out the coordinates of both bodies. Think about the transform that converts from a body to the rotated body. The transform uses reflections (flipping over) around various axes. Ask yourself how the height of the original is related to the width of the rotated Piece and vice versa.

Example: The right dog has body [(0,1),(0,2),(1,0),(1,1)]. After one rotation its body is [(1, 0),(0,0),(1,1),(2,1)]. After another rotation its body is back to the original.

Equals

Remember == normally only works with primitive types. You must determine how to tell if two pieces are equal (e.g. by looking at the individual blocks). This is absolutely crucial in making your piece class work correctly.

Private helpers

You will want private methods behind the scenes to compute the rotation of piece and assemble all the rotations into a list. Also notice that the computation of the width/height/skirt happens when making new pieces and when computing rotations -- don't have two copies of that code.

Generality

Our strategy uses a single Piece class to represent all the different pieces distinguished only by the different state in their body arrays. The code should be general enough to deal with body arrays of different sizes -- the constant 4 should not be used in any special way.

JPiece test

Each row in the PieceTester window below is an instance of JPieceTest. Each JPieceTest component takes a single piece, and draws all its rotations in a row. The code is provided for you so you can test your Piece code.

Here's how the JPieceTest works:

  1. Divides the component into 4 sections. Draws each rotation in its own section from left to right, stopping when getting to the end of the distinct rotations.
  2. Allows space for a square 4 blocks x 4 blocks at the upper-left of each section. Draws the blocks of the piece starting at the bottom of that square. The square won't fit the section exactly, since the section may be rectangular.
  3. When drawing the blocks in the piece, leaves a one-pixel border not filled in around each block, so there's a little space around each block. Each block is drawn as a black square, except the blocks that are the skirt if of the piece -- they are drawn as yellow squares. Draws the blocks in yellow by bracketing it with
    g.setColor(Color.yellow);
    //draw block
    ...
    g.setColor(Color.black);
    
  4. At the bottom of the square, draws the width and height of the block with a string like "w:3 h:2". The string is red.
The last part of the JPieceTest class is a test main, to test your Piece class, that draws all the pieces in a window that looks like this: