Added tetris-game. /JL
This commit is contained in:
276
tetris-game/scripts/board.gd
Normal file
276
tetris-game/scripts/board.gd
Normal 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
|
||||
|
||||
1
tetris-game/scripts/board.gd.uid
Normal file
1
tetris-game/scripts/board.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yc60e2spsmjh
|
||||
34
tetris-game/scripts/game_over.gd
Normal file
34
tetris-game/scripts/game_over.gd
Normal 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()
|
||||
1
tetris-game/scripts/game_over.gd.uid
Normal file
1
tetris-game/scripts/game_over.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dq8ifo7mfpflr
|
||||
83
tetris-game/scripts/main.gd
Normal file
83
tetris-game/scripts/main.gd
Normal 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()
|
||||
1
tetris-game/scripts/main.gd.uid
Normal file
1
tetris-game/scripts/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctadm2gu04xgn
|
||||
59
tetris-game/scripts/next_preview.gd
Normal file
59
tetris-game/scripts/next_preview.gd
Normal 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)
|
||||
1
tetris-game/scripts/next_preview.gd.uid
Normal file
1
tetris-game/scripts/next_preview.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d0wlixgjpcnyc
|
||||
Reference in New Issue
Block a user