Lesson 08

Rotation and wall kicks

Rotating a piece is a coordinate trick: spin each offset 90 degrees, then test the result against the board. If it collides with a wall we nudge it back on — a simplified wall kick — so rotation feels forgiving.

▸ By the end of this lesson: Working clockwise rotation with basic wall kicks, blocked when truly impossible

8.1The 90-degree rotation formula

To rotate an offset (x, y) clockwise around the origin: the new point is (-y, x). That's the entire math. Add a rotation helper to piece.gd that produces the candidate offsets without committing them yet:

piece.gd (append)gdscript
# Return what the offsets WOULD be after a clockwise rotation.
# We don't apply them until Main confirms the move is legal.
func rotated_offsets() -> Array:
	var result: Array = []
	for off in offsets:
		# (x, y) -> (-y, x) rotates 90 deg clockwise
		result.append(Vector2i(-off.y, off.x))
	return result

# Commit a new set of offsets and redraw.
func apply_offsets(new_offsets: Array) -> void:
	offsets = new_offsets
	_redraw()

The O piece

The square (O) piece rotates onto itself, so rotation looks like nothing happens — that's correct. Every other shape visibly turns.

8.2Test the rotation against the board

In main.gd, handle the rotate action. We build the candidate cells from the rotated offsets and check them. If they fit, commit; if not, try small horizontal nudges (wall kicks) before giving up.

main.gd (replace _input)gdscript
func _input(event: InputEvent) -> void:
	if active_piece == null:
		return
	if event.is_action_pressed("hard_drop"):
		_hard_drop()
	elif event.is_action_pressed("rotate_cw"):
		_try_rotate()
main.gdgdscript
func _try_rotate() -> void:
	var candidate: Array = active_piece.rotated_offsets()
	# Kicks to attempt in order: no shift, right 1, left 1, right 2, left 2.
	var kicks: Array = [
		Vector2i(0, 0), Vector2i(1, 0), Vector2i(-1, 0),
		Vector2i(2, 0), Vector2i(-2, 0),
	]
	for kick in kicks:
		if _offsets_fit(candidate, active_piece.origin + kick):
			active_piece.origin += kick
			active_piece.apply_offsets(candidate)
			return
	# No kick worked — rotation is blocked, do nothing.

# Do these offsets fit if the piece origin were at test_origin?
func _offsets_fit(test_offsets: Array, test_origin: Vector2i) -> bool:
	for off in test_offsets:
		var cell: Vector2i = test_origin + off
		if not board.is_free(cell.x, cell.y):
			return false
	return true

Test before you commit

Notice we compute the candidate offsets and only apply them once a kick fits. If you rotate first and check second, a blocked rotation leaves the piece stuck in a wall. Always validate, then commit.

8.3Why wall kicks matter

Without kicks, rotating an I piece flat against the left wall would push cells to column -1 and simply fail — frustrating. The kick list tries shifting the rotated piece right or left by 1–2 cells so it snaps back onto the board. This is a simplified version of the SRS kick system real puzzle games use.

  1. Run with F5. Press Up/W to rotate. Every shape turns clockwise.
  2. Shove a long piece against a wall and rotate — it kicks back onto the board instead of failing.
  3. Bury a piece in a tight gap and try to rotate — if there's truly no room, it stays put.

Checkpoint — expected state