Added tetris-game. /JL

This commit is contained in:
2025-12-06 16:14:25 +01:00
parent dfed3309c2
commit d5573ace27
21 changed files with 776 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
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
for y in range(BOARD_HEIGHT - 1, -1, -1):
var is_full := true
for x in range(BOARD_WIDTH):
if grid[y][x] == "":
is_full = false
break
if is_full:
_drop_lines_above(y)
cleared_lines += 1
y += 1 # re-check same row index (now replaced)
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

View File

@@ -0,0 +1 @@
uid://yc60e2spsmjh

View File

@@ -0,0 +1,34 @@
extends Control
@export var final_score: int = 0
@export var final_highscore: int = 0
@export var total_lines: int = 0
@onready var score_label: Label = $CenterContainer/Panel/VBoxContainer/LabelScore
@onready var highscore_label: Label = $CenterContainer/Panel/VBoxContainer/LabelHighScore
@onready var lines_label: Label = $CenterContainer/Panel/VBoxContainer/LabelLines
@onready var retry_button: Button = $CenterContainer/Panel/VBoxContainer/HBoxContainer/ButtonAgain
@onready var quit_button: Button = $CenterContainer/Panel/VBoxContainer/HBoxContainer/ButtonQuit
func _ready() -> void:
_update_labels()
retry_button.pressed.connect(_on_retry_pressed)
quit_button.pressed.connect(_on_quit_pressed)
func _update_labels() -> void:
if score_label:
score_label.text = "Score: %d" % final_score
if highscore_label:
highscore_label.text = "Highscore: %d" % final_highscore
if lines_label:
lines_label.text = "Lines: %d" % total_lines
func _on_retry_pressed() -> void:
var main := get_tree().current_scene
if main and main.has_method("reset_game"):
main.reset_game()
queue_free() # remove overlay
func _on_quit_pressed() -> void:
get_tree().quit()

View File

@@ -0,0 +1 @@
uid://dq8ifo7mfpflr

View File

@@ -0,0 +1,83 @@
extends Node2D
const GAME_OVER_SCENE := preload("res://scenes/game_over.tscn")
@onready var game_root: Node2D = $GameRoot
@onready var board: Node2D = $GameRoot/Board
@onready var hud_left: Control = $GameRoot/HUDLeft
@onready var hud_right: Control = $GameRoot/HUDRight
@onready var ui_root: Control = $GameRoot/UILayer/UIRoot
@onready var blur_overlay: ColorRect = $GameRoot/UILayer/UIRoot/BlurOverlay # New node
@onready var label_score_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelScoreValue
@onready var label_highscore_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelHighScoreValue
@onready var label_lines_value: Label = $GameRoot/HUDLeft/VBoxContainer/LabelLinesValue
@onready var next_preview: Node2D = $GameRoot/HUDRight/VBoxContainer/NextPreview
func _ready() -> void:
board.connect("lines_cleared", Callable(self, "_on_lines_cleared"))
board.connect("next_piece_changed", Callable(self, "_on_next_piece_changed"))
board.connect("game_over", Callable(self, "_on_game_over"))
# Center once and whenever the window size changes
get_viewport().size_changed.connect(_on_viewport_size_changed)
_on_viewport_size_changed()
func _on_viewport_size_changed() -> void:
var window_size: Vector2 = get_viewport_rect().size
var layout_bounds := _get_layout_bounds()
var layout_center := layout_bounds.position + layout_bounds.size * 0.5
var target_center := window_size * 0.5
# Move the whole game so the *layout* center matches the window center
game_root.position = target_center - layout_center
func _on_lines_cleared(total_lines: int, score: int, highscore: int) -> void:
label_score_value.text = str(score)
label_highscore_value.text = str(highscore)
label_lines_value.text = str(total_lines)
func _get_layout_bounds() -> Rect2:
# Board rectangle (we know its size from constants)
var board_size := Vector2(
board.BOARD_WIDTH * board.CELL_SIZE,
board.BOARD_HEIGHT * board.CELL_SIZE
)
var bounds := Rect2(board.position, board_size)
# Merge HUDLeft and HUDRight rectangles into the bounds
if hud_left:
bounds = bounds.merge(hud_left.get_rect())
if hud_right:
bounds = bounds.merge(hud_right.get_rect())
return bounds
func _on_next_piece_changed(shape_key: String, shapes: Dictionary, colors: Dictionary) -> void:
next_preview.update_preview(shape_key, shapes, colors)
func _on_game_over(score: int, highscore: int, total_lines: int) -> void:
# 1) Turn on blur behind the overlay
if blur_overlay:
blur_overlay.visible = true
# 2) Show the GameOver UI on top
var game_over_layer := GAME_OVER_SCENE.instantiate()
game_over_layer.set_anchors_preset(Control.PRESET_CENTER) # New
game_over_layer.scale = Vector2(2, 2) # New
game_over_layer.final_score = score
game_over_layer.final_highscore = highscore
game_over_layer.total_lines = total_lines
ui_root.add_child(game_over_layer)
game_over_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
func reset_game() -> void:
# Hide blur again
if blur_overlay:
blur_overlay.visible = false
if board.has_method("reset_game"):
board.reset_game()

View File

@@ -0,0 +1 @@
uid://ctadm2gu04xgn

View File

@@ -0,0 +1,59 @@
extends Node2D
const CELL_SIZE := 16
var shape_key: String = ""
var shapes: Dictionary = {}
var colors: Dictionary = {}
func update_preview(new_shape_key: String, new_shapes: Dictionary, new_colors: Dictionary) -> void:
shape_key = new_shape_key
shapes = new_shapes
colors = new_colors
queue_redraw()
func _draw() -> void:
# Debug frame so we can see that the node exists
var debug_rect := Rect2(Vector2.ZERO, Vector2(80, 80))
draw_rect(debug_rect, Color(0.2, 0.2, 0.2, 1.0), false, 2.0)
if shape_key == "" or not shapes.has(shape_key):
return
var blocks: Array = shapes[shape_key]
# Find bounding box of the shape
var min_x := 999
var max_x := -999
var min_y := 999
var max_y := -999
for b in blocks:
var v: Vector2i = b
if v.x < min_x:
min_x = v.x
if v.x > max_x:
max_x = v.x
if v.y < min_y:
min_y = v.y
if v.y > max_y:
max_y = v.y
var width := (max_x - min_x + 1) * CELL_SIZE
var height := (max_y - min_y + 1) * CELL_SIZE
# Center the shape in our local 80x80 area
var total_size := Vector2(width, height)
var preview_center := Vector2(40, 40) # middle of debug_rect
var offset := preview_center - total_size * 0.5
var color: Color = Color.WHITE
if colors.has(shape_key):
color = colors[shape_key] as Color
for b in blocks:
var v: Vector2i = b
var x := (v.x - min_x) * CELL_SIZE
var y := (v.y - min_y) * CELL_SIZE
var rect := Rect2(offset + Vector2(x, y), Vector2(CELL_SIZE, CELL_SIZE))
draw_rect(rect.grow(-1), color, true)

View File

@@ -0,0 +1 @@
uid://d0wlixgjpcnyc