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.
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".
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.
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.