Design Check: Thursday, September 17th, in class
Check Point: Thursday, October 1st, in class
Due Date: Wednesday, October 14th, 11:59pm
Your assignment is to write the CS5 version of the incredibly addictive game of Tetris, without getting addicted to it yourself. If you are not a Tetris addict, you should run the Tetris demo as soon as possible to get a good feel for how the game works. There are many versions of Tetris out there, with slight differences among them; to give you a better idea of what your assignment entails, read the following description of how the CS5 version of Tetris behaves.
When the game starts, only an empty board with borders drawn around its edges should be displayed. A Tetris piece, chosen randomly from the seven possible Tetris pieces, shown below, should appear at the top of the board. This piece should fall by moving down the board, one square at a time. A piece cannot fall into a square already occupied by a previously fallen piece. When a piece can fall no further, it should stop moving; a new random piece should then appear at the top of the board and begin to fall. As pieces fall, rows (or horizontal lines) of occupied squares spanning the board's width may form. When such a line is formed, it disappears and all the squares above it fall down one line to fill the newly empty squares. This process continues until a new piece appears and has no room to fall because it is already resting on a previously fallen piece. The game is then over, and everything on the board should stop completely. Your game should also have a "quit" button, which ends the game, and a "reset" button which will remove all of the squares from the board, letting the player start anew with another random piece falling from the top of the board.
While a piece is falling, the player may rotate or shift it by pressing certain keys on the keyboard. Pressing `j' should shift the piece one square to the left. Pressing `l' should shift the piece one square to the right. Pressing `k' should rotate the piece clockwise by ninety degrees. At regular intervals, the piece should simply fall vertically one square at a time. The player should be able to drop the piece quickly by pressing the space bar. By dropping a piece, the player forfeits his/her chance to manipulate the piece any further and the piece simply falls as far as it can. The player should be able to pause the game at any time by pressing `p'. When the game is paused, the player should not be able to manipulate the piece in any way. If you wish to change which keys do which actions, be sure to make a note of this (preferably a very large one) so we don't end up thinking your program is non-functional. Similarly, if you decide to make the "quit" and "reset" buttons keypresses as well (`r' and `q') please let us know.
As a piece moves, it would not make sense for it to be able to move through previously fallen pieces or beyond the edges of the board. In order to prevent these illegal moves, when a piece wants to move to a new location, it should check that the new location is not already occupied by a previously fallen piece and that it is not beyond the edges of the board. These checks should be made whenever a piece wants to shift left, shift right, fall, or rotate.
In order to encourage you to code in small installments, there will be a check point about halfway through Tetris (due October 1st). At the check point, you should have implemented:
There are several new concepts used in this assignment. Before you even start thinking about Tetris, you should make sure that you understand all of the concepts listed in the New Concepts section at the begining of this handout and all of the AWT and support classes listed below. You should review the lecture foils, go to TA hours, ask questions in class, do anything you need to do in order to understand these concepts. If you thoroughly understand these concepts before you start, this program will be easier to design and to code.
The next thing you should do is run the Tetris demo. If you are not familiar with Tetris, you should run the demo a lot. But be careful, because Tetris is one of those games where you end up playing for 16 hours straight all the while muttering ``must beat high score!'' Once you are familiar with how the CS5 version of the game behaves, you should start thinking about your design. You will want to think about:
You want your squares to be able to draw themselves on the screen. The problem here is that in awt, the way you draw a square is to get the graphics objects from the Applet (or other Component), and then call drawRect()
on it. In order for squares or pieces to draw themselves, they need to know about your App, so they can do this. Also, keep in mind that no matter what you draw to the App, nothing will show up until you call the repaint()
method. To avoid flickering, only call repaint after you've drawn everything that you want to draw to the screen. Experiment with this to see what effect you like the best.
Useful awt classes:
After playing with the demo for a while, you will notice that a player inputs a lot of information very rapidly. In order to shift a piece, rotate it, and drop it, a player would want to input anywhere from three to ten commands in a matter of seconds. This input could have been received through buttons, with which you are already familiar. However, from a player's point of view, watching falling pieces on the screen and also clicking on the appropriate buttons to move the pieces is not that easy. For this reason, you should not use buttons in this GUI; instead, you should use keyboard input from the player.
For this assignment, you will need to have separate key interactions for
each one of the player's valid inputs: shifting left, shifting right,
rotating, and dropping a piece, and pausing the game. This brings up a
critical design issue. It makes sense to create ``key interactor''
objects for your applet to delegate key presses to. We have provided
a KeyInteractor class (cs005.event.KeyInteractor
) for you with
abstract methods keyPress()
and keyRelease()
which
get called when the key is pressed and released respectively.
Since each of the four different key interactors operate on the current piece, they will need to send messages to it and they will all need a way in which to communicate with the current piece. If each of the four interactors has a reference to the current piece, then every time you create a new piece, you will have to update each interactor's reference.
A good design choice to simplify this is to have the four interactors all have a reference to the same object for the duration of the program; let's call this class the piece surrogate. This object would then contain the (one) reference to the current piece that constantly needs to be updated. This way, when a new piece is created, you will only have to update one reference (the surrogate's reference), as opposed to four references (each of four different key interactors' references). The four different key interactors can then communicate with the proxy, which will in turn communicate with the actual current piece.
The difficulty with animating Tetris pieces lies in the fact that we
want to do two things at once. We want to have the piece move down the
screen and be able to detect keypresses. To do this, you need to create
something that is responsible for the animation (in this case, moving
the tetris piece down the screen). We have provided you with an
Animator class (cs005.app.Animator
) that should start when
you want the animation to begin, and stop when the animation is
finished. The Animator has a method called action()
that
gets called once every so often. You should override that method with
the code that actually does the animation.
Do we want the actual pieces themselves to inherit from the Animator, or perhaps just a class that deals solely with the current piece to inherit from Animator? This is a question that you will have to address in your design. Whether you choose to have the actual pieces themselves inherit from Animator, or you choose to have some other class inherit from Animator is entirely up to you. While either choice will work, one might be better than the other in this program. Keep in mind that whichever you choose, you should state your reasons for doing so.
In this assignment it makes sense to use random numbers to determine
which of the seven pieces will fall next. We have created a
BoundedRandom class (cs005.util.BoundedRandom
), which you
can use to generate random integers by calling the getRandom(int
min, int max)
method on an instance of it.
Buttons are slightly different in Java 1.1. In order to add
functionality to a button, you must add what's called a Listener that
handles button clicks. We have provided a ButtonListener class
(cs005.event.ButtonListener
) for you with an abstract
method called buttonClick()
which gets called every time
the button it is "listening" to gets clicked. To add a ButtonListener
to a Button, pass it to the button's addActionListener()
method.
The key to dropping a piece is to make sure that the squares that the piece wants to fall through are not already occupied by a previously fallen piece. In other words, if you are keeping track of already occupied squares within a board, then as the current piece falls, you simply have to check with the board to see if the squares where it wants to move are already occupied.
This same sort of checking should be used for rotations. You should check with the board to make sure that locations where the piece wants to be after it rotates are free. If they are free, then the piece can rotate; if any one of these squares is not free, then the piece cannot rotate. In order to relieve some of the mathematical grunt work involved in figuring out rotations, we are giving you a nifty formula that performs rotation operations. See Appendix A for this nifty formula and it's explanation.
Pausing a piece involves no checking, all that is required is that the animation stop when `p' is pressed and resume when it's pressed again.
Before discussing some of the design issues in coding the Tetris board, let's review what it needs to accomplish. Once a piece has fallen, the squares from that piece should remain on the screen in their original color, should block other pieces' motion, and should be able to be removed from the board in rows. This may mean removing only part of what used to be a single Tetris piece. Once a row has been removed, all the rows above it should move down.
First, how can we keep all the squares around after pieces have fallen? How can we collect an arbitrary number of squares as pieces fall and organize them such that the functionality listed above is easy to implement? It is probably a good idea to store the squares in a matrix (array of arrays), and have the Board display those squares that are in the matrix to the screen.
When you run the Tetris demo, you may notice that there are two layers of blocks that surround the board. These blocks are not only there to provide a graphical edge to the board, but also to eliminate the need for boundary checking. Tetris pieces never need to ask whether the square they want to move into is on the board or not. When the current piece wants to move, it will simply ask the board if the squares to its left, right, or bottom are already occupied. Because of the border of pre-existing blocks, if the piece wants to move beyond an edge of the play area, the board will inform the piece that it cannot move there since those spaces are already occupied. Note that there is no need to explicitly check if we are beyond the edges of the board or outside the boundaries of the internal representation of the board, because we never are.
So how do we pad the board? If you want your playing area to be 10x20, then make its board 14x24. Each side of the board is now padded with two layers of ``already occupied'' blocks. Remember that array indicies are inclusive and they cannot be negative; so you may have to offset the actual position of the graphical pieces based on the borders of the board and whether you want your pieces to start falling above the top of the drawing area (i.e. off the screen).
One last thing: Why do we want to have two padding layers on each side, as opposed to just one? You will definitely want to have two layers; think about why as you design your program. (hint: if there were only one padded layer to the left of the play area, what would be the logical index for a square one to the left of the padded layer? What would happen when you tried to check for filled squares in this location? Would you ever need to check these squares?).
For this assignment, you should most definitely design the entire program carefully before you write even one line of java code. Part of your design process should include a lot of hand simulation; that is, stepping through your design to make sure that it deals with every last detail. Remember to look for classes that share responsibilities, capabilities, or interfaces and make an interface or superclass for them. Look for the main puzzles in the program and design patterns that might solve them. Try to avoid unnecessary if and switch statements by exploiting polymorphism and abstract methods. Try to consolidate as much of your user interaction as possible by exploiting delegation. If you have any questions about your design, or some other aspect of the program, come to TA hours, or ask a question after class or during the Friday session.
Then begin coding small chunks at a time. First, get one piece to simply fall down the board. Then, get it to shift left and right, and finally, make it rotate and drop. Once you have all of this working for one piece, it should be fairly simple to get the other pieces to exhibit the same behaviors. Finally, implement the detection and removal of lines, the updating of the board, pause and the end of the game scenario.
There is plenty of room for creativity in this assignment, however it is important that you get your program to meet the basic specs before you start adding Bells and Whistles. It's a good idea to consider extensions when you design your program so they can be added easily. As always, extentions that make us say "Keen!" will be given special consideration.
Here are some suggestions:
This is a challenging program, especially concidering that its been a
few months since most of you have written a large java program. Check
out all of the online documentation and design thoroughly before you
start coding. There will be a mandatory design check and you will be
expected to come prepared with some sort of design for your program,
pseudocode, and some actual java code. You have five weeks for this
program. If you start early and work steadily it will not consume your
life. If you wait until the last minute, it probably
will.
To move a point 90 degrees counter-clockwise in a circle around another point:
xLocation = centerOfRotationX - centerOfRotationY + oldYLocation
yLocation = centerOfRotationY + centerOfRotationX - oldXLocation
where xLocation and yLocation are the new coordinates of the point being moved, centerOfRotationX and centerOfRotationY are the coordinates of the fixed point around which this point is moving, and oldXLocation and oldYLocation are the original coordinates of the point being moved. Note that this assumes that the positive y axis points down (as it does in awt). We will cover this formula in more detail at the help session.