Tetris 07 – Board script

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 blocks

Spawning 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 = rotated

Collision 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 false

Locking 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_lines

Drop 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 = 0

This concludes the ‘board.gd‘ script. Save the script.

Introduction


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *