Lesson 10
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.
Derive a level from total lines cleared and shrink fall_interval accordingly. Add to main.gd:
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:
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.
Game over means a new piece overlaps existing blocks the instant it appears. Check it in spawn_piece(). Update it in main.gd:
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 truefunc _trigger_game_over() -> void:
game_over = true
active_piece = null
print("GAME OVER — final score %d, lines %d" % [score, lines_cleared_total])Guard _process and _input so nothing runs after game over:
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()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
MIN_INTERVAL._process and _input stop cleanly on game over.