This is generally the most difficult part of the project:
in response to up-arrow key presses, we should rotate the falling piece 90
degrees counterclockwise. We do this in the same way we handled other
changes to the falling piece: we make the change, test if it is legal,
and if not, we unmake the change.
Explaining rotateFallingPiece:
As noted, this method is similar to moveFallingPiece, in that it makes the
rotation and then calls fallingPieceIsLegal (the same function used by
moveFallingPiece) and undoes any illegal changes. As for the actual
rotation, this is accomplished by changing the two-dimensional list of
booleans that represent the falling piece. A new 2d list is created,
and cells in the old list are mapped to cells in the new list according to a 90-degree
counterclockwise rotation. To see how this works, consider this
picture, which shows a grid that is rotated counterclockwise (the corners
are highlighted to make the rotation clear):
[To avoid any confusion, note that it would not be possible to generate
these particular boards during an actual Tetris game.]
First, we see that the dimensions reverse: in this example, the old
grid was 7 rows by 10 columns, whereas the new grid is 10 rows by 7 columns.
Next, consider what happens as we move in the old grid from red to green
(that is, moving downward with rows increasing from 0 to 6): this maps
in the new grid to moving across with columns increasing from 0 to 6 .
Thus, our new column is equal to our old row. That is the
easier dimension. Now consider the other dimension, as we move in the
old grid from red to white (that is, moving across with columns increasing
from 0 to 9: this maps in the new grid to moving up with rows
decreasing from 9 to 0. Thus, our new row is equal to (9
minus our old column). More generally, we replace "9" with "one
less than the number of old columns".
Writing rotateFallingPiece:
Following the plan just described, we start by storing the old piece
(the 2d list of booleans), its location, and its dimensions in local
variables (because we may need these to undo our move if it turns out
to be illegal). Next, we compute the new dimensions, by reversing
the old dimensions.
Next, we compute the new location. Our goal is to keep the center of
the falling piece constant (or, given that this is not possible if we have
an even number of rows or columns, to keep the center as constant as possible).
Keeping the center of the falling piece constant during rotation is the
most difficult part of Tetris,
so read this part very carefully (though that is always good advice!).
Besides making rotation more intuitive, we want to keep the center
constant so that if we rotate around and around, the center does not
"drift" -- a full 360 degree turn should bring us back to where we
started. We'll present two alternatives to meet these conditions.
You may implement either one, as they are equivalent.
Alternative
#1: Write a helper function, fallingPieceCenter(canvas), that
returns the (row,col), as a tuple, of the center of the fallingPiece
(for example, in the vertical, this is the fallingPiece's top row plus
half its total rows). Call this function in rotateFallingPiece
before the rotation to get (oldCenterRow, oldCenterCol). Then, change
the dimensions of the fallingPiece (swap its rows and cols) but do not
change its location yet. Call fallingPieceCenter(canvas) again to
get (newCenterRow, newCenterCol). Now we have to adjust the
fallingPiece position, but by how much? We observe that we want
the final center to be the same as the original center (right?).
If we subtract newCenterRow, we'll move to the top, then if we
add oldCenterRow, we'll be centered vertically. This can be done
in a single step if we add the difference of (oldCenterRow -
newCenterRow) to the fallingPieceRow. We do the analogous
operation for cols, too, of course. And that's it!
Alternative
#2 (probably more challenging, particularly in Python): Instead
of writing the fallingPieceCenter helper function, here we observe that
we just need to adjust the left column and top row of the falling piece
by subtracting half of the
change in the size of each dimension that results from each turn,
where the change in the size of each dimension equals the difference
between the number of rows and columns (though you have to think about
whether this difference should be positive or negative -- you may need
a conditional or some arithmetic trickery to get this right).
Read and re-read the preceding paragraph. Draw pictures.
Make sense out of it. When you finally convert it into Python,
you will find there are two simple lines that make
rotation-about-a-fixed-center work:
fallingPieceRow -=
<something>
fallingPieceCol -= <something else>
The preceding 2 lines are simple to write (once you figure out what
goes on the right-hand side), but tricky to confirm, and
even trickier to come up with independently.
Regardless of which alternative you used, we now have
the new location and dimensions, so we create an entirely new piece (that is, a
new 2d list of booleans) and load it with a rotation of the old piece
according to the algorithm described above, and set fallingPiece equal to
this new 2d list.
Finally, we check if this rotation makes the falling piece go off the
board or collide with a non-empty cell on the board (simply reusing our
code from the previous steps, where we wrote a function that tests if
the current board is legal or not), and if either of these conditions
occurs, we restore the piece, its location, and its dimensions to their
original values.
Updating keyPressed:
We modify the keyPressed handler to call rotateFallingPiece in response to
an up-arrow key press.
Testing the code
Hint: Remember to press the up-arrow key to rotate the falling piece
(and try to move it off the board!), and to press some non-arrow keys to
start with new falling pieces to test the code! Verify that the piece
rotates counterclockwise, that the center basically stays fixed, and in
particular that 360 degree turns result in no change to the falling piece.
David Kosbie |