Open the ‘board.gd‘ script.
We start by adding constants right after the ‘extends Node2D‘ line.
extends Node2D
const BOARD_WIDTH := 10
const BOARD_HEIGHT := 20
const CELL_SIZE := 24 # pixels
# Types of pieces
# Each shape is a list of Vector2i, relative to a pivot (0,0)
const SHAPES := {
"I": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(2,0)],
"O": [Vector2i(0,0), Vector2i(1,0), Vector2i(0,1), Vector2i(1,1)],
"T": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(0,1)],
"L": [Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0), Vector2i(1,1)],
"J": [Vector2i(-1,1), Vector2i(-1,0), Vector2i(0,0), Vector2i(1,0)],
"S": [Vector2i(0,0), Vector2i(1,0), Vector2i(-1,1), Vector2i(0,1)]
}
# Link shape keys to colors
const COLORS := {
"I": Color(0.0, 1.0, 1.0),
"O": Color(1.0, 1.0, 0.0),
"T": Color(0.6, 0.0, 0.8),
"L": Color(1.0, 0.5, 0.0),
"J": Color(0.0, 0.0, 1.0),
"S": Color(0.0, 0.5, 0.0)
}
# 2D grid: 0 = empty, else a String shape key
var grid: Array = []
# Current falling piece
var current_shape_key: String
var current_blocks: Array[Vector2i] = []
var current_pos: Vector2i
var current_color: Color
# Next piece
var next_shape_key: String = ""
var rng := RandomNumberGenerator.new()
# Drop timing
var drop_interval := 0.7
var drop_timer := 0.0
var is_game_over := false
# Signals to update HUD and preview
signal lines_cleared(total_lines: int, score: int, highscore: int)
signal next_piece_changed(shape_key: String, shapes: Dictionary, colors: Dictionary)
signal game_over(score: int, highscore: int, total_lines: int)
var score := 0
var highscore := 0
var total_lines_cleared := 0
Add a reset game function ‘func reset_game()‘.
func reset_game() -> void:
score = 0
total_lines_cleared = 0
drop_timer = 0.0
is_game_over = false
_init_grid()
_spawn_initial_pieces()
# Update HUD to show reset stats
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
queue_redraw()Now we initialize the board. Add ‘func _ready()‘ as follows.
func _ready() -> void:
rng.randomize()
_load_highscore()
reset_game()Now we add the new functions, starting with ‘func _init_grid()‘.
func _init_grid() -> void:
grid.resize(BOARD_HEIGHT)
for y in range(BOARD_HEIGHT):
grid[y] = []
grid[y].resize(BOARD_WIDTH)
for x in range(BOARD_WIDTH):
grid[y][x] = "" # empty string = no blocksSpawning pieces and next preview.
func _spawn_initial_pieces() -> void:
next_shape_key = _random_shape()
_spawn_new_piece()
func _random_shape() -> String:
var keys := SHAPES.keys()
return keys[rng.randi() % keys.size()]Next piece.
func _spawn_new_piece() -> void:
# Move "next" to "current"
current_shape_key = next_shape_key
# Get the raw shape array (untyped)
var shape_array: Array = SHAPES[current_shape_key]
# Fill our typed Array[Vector2i]
current_blocks.clear()
for p in shape_array:
current_blocks.append(p as Vector2i)
# Color for this piece
current_color = Color.WHITE
if COLORS.has(current_shape_key):
current_color = COLORS[current_shape_key] as Color
# Spawn near top center
current_pos = Vector2i(BOARD_WIDTH / 2, 1)
# Choose the next upcoming piece
next_shape_key = _random_shape()
emit_signal("next_piece_changed", next_shape_key, SHAPES, COLORS)
# Check for immediate collision → game over
if _is_colliding(current_pos, current_blocks):
_game_over()Movement & rotation.
- move_left = left arrow/A
- move_right = right arrow/D
- soft_drop = down arrow/S
- rotate = up arrow/W/space

Now add the ‘func _process()’ function.
func _process(delta: float) -> void:
if is_game_over:
return
drop_timer += delta
if drop_timer >= drop_interval:
drop_timer = 0.0
_try_move(Vector2i(0, 1), true)
_handle_input()
queue_redraw()Handle input.
func _handle_input() -> void:
if is_game_over:
return
if Input.is_action_just_pressed("move_left"):
_try_move(Vector2i(-1, 0))
if Input.is_action_just_pressed("move_right"):
_try_move(Vector2i(1, 0))
if Input.is_action_pressed("soft_drop"):
_try_move(Vector2i(0, 1))
if Input.is_action_just_pressed("rotate"):
_try_rotate()Movement with collision check.
func _try_move(offset: Vector2i, lock_on_fail: bool = false) -> void:
var new_pos := current_pos + offset
if not _is_colliding(new_pos, current_blocks):
current_pos = new_pos
elif lock_on_fail and offset.y > 0:
_lock_piece()Rotation.
func _try_rotate() -> void:
var rotated: Array[Vector2i] = []
for b in current_blocks:
# (x, y) → (-y, x)
rotated.append(Vector2i(-b.y, b.x))
if not _is_colliding(current_pos, rotated):
current_blocks = rotatedCollision check.
func _is_colliding(pos: Vector2i, blocks: Array[Vector2i]) -> bool:
for b in blocks:
var x := pos.x + b.x
var y := pos.y + b.y
if x < 0 or x >= BOARD_WIDTH or y < 0 or y >= BOARD_HEIGHT:
return true
if grid[y][x] != "":
return true
return falseLocking pieces & clearing lines.
func _lock_piece() -> void:
# Transfer current piece into grid
var points: int
for b in current_blocks:
var x := current_pos.x + b.x
var y := current_pos.y + b.y
if y >= 0 and y < BOARD_HEIGHT and x >= 0 and x < BOARD_WIDTH:
grid[y][x] = current_shape_key
var cleared := _clear_full_lines()
# scoring (simple rules)
if cleared > 0:
total_lines_cleared += cleared
match cleared:
1:
points = 100
2:
points = 300
3:
points = 500
4:
points = 800
_:
points = cleared * 200
score += points
if score > highscore:
highscore = score
_save_highscore()
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
_spawn_new_piece()
Clear full lines.
func _clear_full_lines() -> int:
var cleared_lines := 0
var y := BOARD_HEIGHT - 1
while y >= 0:
var is_full := true
for x in range(BOARD_WIDTH):
if grid[y][x] == "":
is_full = false
break
if is_full:
# Remove this row and drop everything above it down by 1
_drop_lines_above(y)
cleared_lines += 1
# IMPORTANT:
# Do NOT change y here.
# After dropping, a new row has moved into index y,
# so we re-check the same y in the next loop iteration.
else:
# Only move up when the current row was not cleared
y -= 1
return cleared_linesDrop lines above.
func _drop_lines_above(row: int) -> void:
for y in range(row, 0, -1):
for x in range(BOARD_WIDTH):
grid[y][x] = grid[y - 1][x]
# top row becomes empty
for x in range(BOARD_WIDTH):
grid[0][x] = ""Drawing board & pieces.
func _draw() -> void:
# Draw background grid (optional)
for y in range(BOARD_HEIGHT):
for x in range(BOARD_WIDTH):
var rect := Rect2(
Vector2(x * CELL_SIZE, y * CELL_SIZE),
Vector2(CELL_SIZE, CELL_SIZE)
)
draw_rect(rect, Color(0.05, 0.05, 0.05), false, 1.0)
# Draw placed blocks
for y in range(BOARD_HEIGHT):
for x in range(BOARD_WIDTH):
var key: String = grid[y][x]
if key != "":
var color := COLORS.get(key, Color.WHITE)
_draw_cell(x, y, color)
# Draw current falling piece
for b in current_blocks:
var x := current_pos.x + b.x
var y := current_pos.y + b.y
_draw_cell(x, y, current_color)
func _draw_cell(x: int, y: int, color: Color) -> void:
var pos := Vector2(x * CELL_SIZE, y * CELL_SIZE)
var rect := Rect2(pos, Vector2(CELL_SIZE, CELL_SIZE))
draw_rect(rect.grow(-1), color, true) # small border
Game over & reset.
func _game_over() -> void:
is_game_over = true
# Make sure HUD shows final values
emit_signal("lines_cleared", total_lines_cleared, score, highscore)
# Inform main scene that the game ended
emit_signal("game_over", score, highscore, total_lines_cleared)Highscore saving/loading.
func _save_highscore() -> void:
var cfg := ConfigFile.new()
cfg.set_value("stats", "highscore", highscore)
cfg.save("user://tetris.cfg")
func _load_highscore() -> void:
var cfg := ConfigFile.new()
var err := cfg.load("user://tetris.cfg")
if err == OK:
highscore = int(cfg.get_value("stats", "highscore", 0))
else:
highscore = 0This concludes the ‘board.gd‘ script. Save the script.
Leave a Reply