Tutorial 1.1 - Step 3

Learn to code with step-by-step lessons. A place for students to work through programming fundamentals and build skills.

Step 3 - Moving pieces and collision

← Step 2 · Tutorial 1.1 · Step 4 →


Goal

Make the current piece move left, right, and down with the keyboard. When a move would put the piece outside the grid or on top of an existing block, don’t allow it (rollback). When moving down causes a collision, lock the piece: add its blocks to the board and spawn a new piece. Use a try → check → rollback pattern: change position, check collision, and if there’s a collision undo the change (and for down, lock and spawn).

By the end of this step you should have:


Problem-solving: try → check → rollback

This pattern is used everywhere in Tetris:

  1. Try the move (e.g. origin[1] += 1 for down).
  2. Check if that state is invalid (collision with wall or board).
  3. If invalid, rollback (restore the previous origin). If the move was down and we collided, that means the piece has landed - so lock it and spawn a new piece.

Your task

  1. Implement check_collision() on Piece: build a list of (col, row) for each cell of the piece (using origin and offsets[self.offset_num]). Check: is any cell’s row ≥ game_area[1]? Column < 0 or ≥ game_area[0]? Is any cell’s (col, row) already in the board’s block locations? Return True if any of these happen.
  2. Implement move(direction): save the current origin, then add (-1,0), (1,0), or (0,1) depending on direction. Call check_collision(). If True, set origin back to the saved value. If the collision was on down, call update_board() and new_piece().
  3. Implement update_board(): for each offset in the current rotation, append to game.board a dict like {"location": (origin[0]+dx, origin[1]+dy), "image": ...}.
  4. Implement new_piece(): remove the current piece from game.pieces and append a new Piece(game, ...). (You can pick a random type or cycle through a list.)
  5. In the game loop, handle key events and/or a timer to call pieces[0].move("left"), move("right"), and move("down").

Hints

Hint 1 - How to get the piece’s cell positions? For each `(dx, dy)` in `self.offsets[self.offset_num]`, the cell is at `(self.origin[0] + dx, self.origin[1] + dy)`. Build a list of these tuples. Compare with `game_area` (columns 0 to game_area[0]-1, rows 0 to game_area[1]-1) and with every `block["location"]` in `game.board`.
Hint 2 - Rollback for move Before changing `origin`, do `old_origin = self.origin.copy()` (or a list copy). Then change `origin`. If `check_collision()` is True, set `self.origin = old_origin`. For down-only, if you collided and direction was "down", then also call `update_board()` and `new_piece()` before or after rollback - but rollback the position so the piece isn’t drawn in the wrong place (the blocks are already in the board).
Hint 3 - When to spawn a new piece? Only when **moving down** causes a collision. So inside `move()`, when `direction == "down"` and `check_collision()` is True: rollback, then `update_board()`, then `new_piece()`. The current piece is then “replaced” by the new one in `pieces[0]`.

Run it

Run your game and play. Press A/D (or your keys) to move, and S or wait for the timer to drop. When the piece lands, it should lock and a new piece should appear. This is the first moment your project feels like a real game - you’re controlling something on the screen. Compare with the solution when ready.

→ See the solution for Step 3

Then go to Step 4 - Rotating pieces.