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 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() func _ready() -> void: rng.randomize() _load_highscore() reset_game() 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() 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 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()] 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() 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() 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() 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 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 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() 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 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] = "" 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: Color = Color.WHITE if COLORS.has(key): color = COLORS[key] as Color _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 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) 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