Complete a Tetris game on the RDC Controller. The starter already handles piece generation, gravity, collision checking, locking, row clearing, scoring, and rendering. Even without button controls, pieces will appear, fall, and stack automatically. Each shuffled group of seven pieces contains every piece type exactly once.
The game uses six buttons (Button2 to Button7). Button1 and Button8 are not used.
You must implement all six controls below. You are free to assign any control to any button — there is no fixed mapping. (The demo videos show one example layout; your layout may differ.)
- Move the active piece one cell left.
- Move the active piece one cell right.
- Rotate the active piece clockwise.
- Rotate the active piece anticlockwise.
- Soft drop: move down one cell (short press) / hard drop: drop to the bottom and lock (long press).
- Pause/resume (short press), replace the active piece (long press), restart after Game Over (long press).
The logical game board uses whole-cell coordinates. (0, 0) is the top-left cell of the Tetris board:
x increases →
0 1 2 ... 9
+------------------+
y = 0 --> | (0,0) | top
y = 1 --> | |
... | |
y = 19 --> | | bottom
+------------------+
y increases downward
- Moving right increases
x; moving left decreasesx. - Moving down increases
y; there are no half-cell positions. - Access the board using
board[y][x]. Valid coordinates arex = 0..9andy = 0..19.
The HUD occupies text row zero of the TFT, but it is outside the logical Tetris board. The board is drawn lower on the screen, so a piece at logical y = 0 does not overlap the title or score.
Please download the skeleton code for Tutorial 1 Homework here:
Set up the project the same way as you were taught in Tutorial 1! Review the slides again for the steps if you can't remember.
void Tetris_DrawHUD(const TetrisGame *game)
{
if (game->state == TETRIS_GAME_OVER) {
// TODO: display Game Over.
} else if (game->state == TETRIS_PAUSED) {
// TODO: display Paused.
} else {
// TODO: display the title and score.
}
}
void Tetris_DrawCell(uint32_t px, uint32_t py, uint16_t color)
{
// TODO: draw the cell background or border.
// TODO: draw the coloured cell.
}You are required to do the following:
- Show the title and score during play, Paused while paused, and Game Over when the game ends.
- Draw a 7 × 7 pixel background for each cell, then draw a 5 × 5 pixel coloured block in the centre. This leaves a 1-pixel border on every side. Moving pieces must leave no trail.
Marking Scheme:
- The title, score, Paused, and Game Over status are displayed correctly and remain within text row zero @1
- Each cell has a 7 × 7 pixel background and a centred 5 × 5 pixel coloured block, with no trail after movement @1
WhatsApp.Video.2026-09-28.at.17.24.00.mp4
void Tetris_Move(TetrisGame *game)
{
/** TODO: read the buttons and update previous levels before the guard. */
if (!game->has_active) {
/** HINTS: If we return here, the button state will be correctly updated without actually moving anything. */
return;
}
/** TODO: detect press edges and try the requested move. */
}-
Read the two buttons you assigned to moving left and right, and move only once for each new press.
Hints
[!TIP] Review the example at the end of 02-clock.md. Treat each new press as a rising edge of the logical pressed state. You need to detect every new press, not only the first press after startup. What value can you remember between function calls to compare the current and previous button states?
-
Move only when the new position is valid. Opposite presses cancel each other.
Hints
[!TIP] Copy
game->activeinto a newTetrisPiece, change its position, and update the active piece only if the new position is valid.
You might find this useful:
bool Tetris_CanPlace(const TetrisGame *game, const TetrisPiece *piece);Marking Scheme:
- The piece moves left and right only when the destination is valid @1
- Each new press moves the piece once @1
WhatsApp.Video.2026-09-28.at.17.23.52.mp4
void Tetris_Rotate(TetrisGame *game)
{
/** TODO: read the buttons and update previous levels before the guard. */
if (!game->has_active) {
/** HINTS: If we return here, the button state will be correctly updated without actually moving anything. */
return;
}
/** TODO: detect press edges and try the requested turn. */
}You are required to do the following:
- Rotate once for each new press of the two buttons you assigned to rotation. Opposite presses cancel each other.
- Accept the rotation only when the piece still fits. Wall kicks are not required.
You might find this useful:
bool Tetris_CanPlace(const TetrisGame *game, const TetrisPiece *piece);Marking Scheme:
- The block can rotate clockwise and anticlockwise when the proposed orientation is valid @1
- Each press rotates only once, opposite presses cancel, and blocked rotations do not pass through walls or landed blocks or implement wall kick correctly according to here@1
WhatsApp.Video.2026-09-28.at.17.22.36.mp4
void Tetris_Drop(TetrisGame *game)
{
/** TODO: read your drop button and update its previous level before the guard. */
if (!game->has_active) {
return;
}
/** TODO: track the gesture and apply its downward action. */
}You are required to do the following:
-
Distinguish a short press from a long press on the button you assigned to dropping.
-
A short press moves the active piece down by one valid cell, or locks it if it cannot move down.
Hints
[!TIP] Handle a short press when the button is released before the long-press interval. In the logical pressed state used here, releasing the button produces a falling edge.
-
A long press moves the active piece to its lowest valid position and locks it exactly once.
Hints
[!TIP] A straightforward method is to move the piece down one cell at a time until the next position is invalid, then keep the last valid position. Call
Tetris_FinishPiece(game)to lock the piece stored insidegame->active. -
Releasing after a long press must not cause a short drop, and continuing to hold the drop button must not affect the next piece.
You might find these useful:
bool Tetris_CanPlace(const TetrisGame *game, const TetrisPiece *piece);
void Tetris_FinishPiece(TetrisGame *game);Marking Scheme:
- A short press moves down once or locks when blocked @1
- A long press drops to the lowest valid position and locks once @1
- Holding or releasing after a long press does not affect the next piece @1
WhatsApp.Video.2026-09-28.at.17.22.31.mp4
void Tetris_UpdateGame(TetrisGame *game, uint32_t now)
{
/** TODO: add the pause, replace and restart behaviour. */
switch (game->state) {
case TETRIS_SPAWN:
game->state = Tetris_Spawn(game) ? TETRIS_FALLING : TETRIS_GAME_OVER;
game->last_fall_ms = now;
break;
case TETRIS_FALLING:
if (!game->has_active) {
game->state = TETRIS_SPAWN;
}
break;
case TETRIS_PAUSED:
case TETRIS_GAME_OVER:
break;
}
}You are required to do the following:
- A short press on the button you assigned to pause pauses or resumes the game. Nothing moves while paused.
- A long press on that button replaces the active piece once. Another replacement is allowed after the new piece locks.
- A long press on that button restarts the game after Game Over.
You might find these useful:
void Tetris_Reset(TetrisGame *game, uint32_t now);
bool Tetris_Spawn(TetrisGame *game);Marking Scheme:
- Short press pauses or resumes the game @1
- Long press replaces the active piece once @1
- Replacement is available again only after the piece locks @1
- Long press restarts the game after Game Over @1