Lesson 10

Speeding up and losing

A puzzle game needs pressure and a fail state. We tie fall speed to lines cleared so the game gets harder, and we detect game over when a fresh piece can't fit at the top.

▸ By the end of this lesson: Rising difficulty as you clear lines, and a proper game-over stop

10.1Level-based speed-up

Derive a level from total lines cleared and shrink fall_interval accordingly. Add to main.gd:

main.gdgdscript
var level: int = 1
const LINES_PER_LEVEL: int = 8
const MIN_INTERVAL: float = 0.12

func _update_level() -> void:
	var new_level: int = 1 + int(lines_cleared_total / LINES_PER_LEVEL)
	if new_level != level:
		level = new_level
		# Each level shaves the interval; never below MIN_INTERVAL.
		fall_interval = max(MIN_INTERVAL, 0.6 - float(level - 1) * 0.05)
		print("Level up! Level %d, interval %.2f" % [level, fall_interval])

Call it from _award_score so levels track with clears:

main.gd (replace _award_score)gdscript
func _award_score(cleared: int) -> void:
	cleared = clampi(cleared, 0, 4)
	score += LINE_SCORES[cleared]
	lines_cleared_total += cleared
	_update_level()

Clamp the floor

max(MIN_INTERVAL, ...) stops the game becoming literally unplayable at high levels. Tune MIN_INTERVAL and the 0.05 step to taste once the game is playable.

10.2Detect game over on spawn

Game over means a new piece overlaps existing blocks the instant it appears. Check it in spawn_piece(). Update it in main.gd:

main.gd (replace spawn_piece)gdscript
var game_over: bool = false

func spawn_piece() -> void:
	if game_over:
		return
	var shape: Dictionary = Shapes.random_shape()
	# Peek: would this shape fit at the spawn origin?
	if not _shape_fits_at(shape, SPAWN_ORIGIN):
		_trigger_game_over()
		return
	active_piece = PieceScene.instantiate()
	add_child(active_piece)
	active_piece.setup(shape, SPAWN_ORIGIN)
	_fall_accum = 0.0

# Check a raw shape dict at an origin, before instancing a Piece.
func _shape_fits_at(shape: Dictionary, o: Vector2i) -> bool:
	for off in shape["cells"]:
		var cell: Vector2i = o + off
		if not board.is_free(cell.x, cell.y):
			return false
	return true
main.gdgdscript
func _trigger_game_over() -> void:
	game_over = true
	active_piece = null
	print("GAME OVER — final score %d, lines %d" % [score, lines_cleared_total])

10.3Stop the game loop when over

Guard _process and _input so nothing runs after game over:

main.gd (replace _process)gdscript
func _process(delta: float) -> void:
	if game_over or active_piece == null:
		return
	_handle_horizontal(delta)
	_handle_soft_drop(delta)
	_fall_accum += delta
	if _fall_accum >= fall_interval:
		_fall_accum = 0.0
		_step_down()
  1. Run with F5. Clear a bunch of lines and feel the fall speed ramp up.
  2. Deliberately stack pieces to the top. When a new piece can't fit, Output prints GAME OVER and the game freezes.

Peek before you place

We test the raw shape dict before instancing a Piece node. If you instance first and check second, a game-over piece briefly appears jammed into the stack. Peek, then place.

Checkpoint — expected state