From d5573ace270bc906dc493b385b92934556cadfe9 Mon Sep 17 00:00:00 2001 From: Jan Lerking Date: Sat, 6 Dec 2025 16:14:25 +0100 Subject: [PATCH] Added tetris-game. /JL --- tetris-game/.editorconfig | 4 + tetris-game/.gitattributes | 2 + tetris-game/.gitignore | 3 + tetris-game/icon.svg | 1 + tetris-game/icon.svg.import | 37 +++ tetris-game/project.godot | 44 +++ tetris-game/scenes/board.tscn | 7 + tetris-game/scenes/game_over.tscn | 69 +++++ tetris-game/scenes/hud_left.tscn | 40 +++ tetris-game/scenes/hud_right.tscn | 22 ++ tetris-game/scenes/main.tscn | 51 ++++ tetris-game/scripts/board.gd | 276 ++++++++++++++++++ tetris-game/scripts/board.gd.uid | 1 + tetris-game/scripts/game_over.gd | 34 +++ tetris-game/scripts/game_over.gd.uid | 1 + tetris-game/scripts/main.gd | 83 ++++++ tetris-game/scripts/main.gd.uid | 1 + tetris-game/scripts/next_preview.gd | 59 ++++ tetris-game/scripts/next_preview.gd.uid | 1 + tetris-game/shaders/blur_overlay.gdshader | 39 +++ tetris-game/shaders/blur_overlay.gdshader.uid | 1 + 21 files changed, 776 insertions(+) create mode 100644 tetris-game/.editorconfig create mode 100644 tetris-game/.gitattributes create mode 100644 tetris-game/.gitignore create mode 100644 tetris-game/icon.svg create mode 100644 tetris-game/icon.svg.import create mode 100644 tetris-game/project.godot create mode 100644 tetris-game/scenes/board.tscn create mode 100644 tetris-game/scenes/game_over.tscn create mode 100644 tetris-game/scenes/hud_left.tscn create mode 100644 tetris-game/scenes/hud_right.tscn create mode 100644 tetris-game/scenes/main.tscn create mode 100644 tetris-game/scripts/board.gd create mode 100644 tetris-game/scripts/board.gd.uid create mode 100644 tetris-game/scripts/game_over.gd create mode 100644 tetris-game/scripts/game_over.gd.uid create mode 100644 tetris-game/scripts/main.gd create mode 100644 tetris-game/scripts/main.gd.uid create mode 100644 tetris-game/scripts/next_preview.gd create mode 100644 tetris-game/scripts/next_preview.gd.uid create mode 100644 tetris-game/shaders/blur_overlay.gdshader create mode 100644 tetris-game/shaders/blur_overlay.gdshader.uid diff --git a/tetris-game/.editorconfig b/tetris-game/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/tetris-game/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/tetris-game/.gitattributes b/tetris-game/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/tetris-game/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/tetris-game/.gitignore b/tetris-game/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/tetris-game/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/tetris-game/icon.svg b/tetris-game/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/tetris-game/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tetris-game/icon.svg.import b/tetris-game/icon.svg.import new file mode 100644 index 0000000..17fa796 --- /dev/null +++ b/tetris-game/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dck747bvw17v0" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/tetris-game/project.godot b/tetris-game/project.godot new file mode 100644 index 0000000..fcbdb20 --- /dev/null +++ b/tetris-game/project.godot @@ -0,0 +1,44 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Tetris Game" +run/main_scene="uid://ckvfgxn4jyoi6" +config/features=PackedStringArray("4.5", "Forward Plus") +config/icon="res://icon.svg" + +[input] + +move_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +soft_drop={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +rotate={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/tetris-game/scenes/board.tscn b/tetris-game/scenes/board.tscn new file mode 100644 index 0000000..e671859 --- /dev/null +++ b/tetris-game/scenes/board.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://d0rk850ey8e0g"] + +[ext_resource type="Script" uid="uid://yc60e2spsmjh" path="res://scripts/board.gd" id="1_p0ybc"] + +[node name="Board" type="Node2D"] +position = Vector2(100, 0) +script = ExtResource("1_p0ybc") diff --git a/tetris-game/scenes/game_over.tscn b/tetris-game/scenes/game_over.tscn new file mode 100644 index 0000000..d48dd1c --- /dev/null +++ b/tetris-game/scenes/game_over.tscn @@ -0,0 +1,69 @@ +[gd_scene load_steps=2 format=3 uid="uid://dkspo8vx4qcvb"] + +[ext_resource type="Script" uid="uid://dq8ifo7mfpflr" path="res://scripts/game_over.gd" id="1_xeevv"] + +[node name="GameOver" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +scale = Vector2(2, 2) +size_flags_horizontal = 4 +size_flags_vertical = 4 +script = ExtResource("1_xeevv") + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Panel" type="Panel" parent="CenterContainer"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -67.0 +offset_top = -69.5 +offset_right = 67.0 +offset_bottom = 69.5 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="LabelTitle" type="Label" parent="CenterContainer/Panel/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 +text = "GAME OVER" + +[node name="LabelScore" type="Label" parent="CenterContainer/Panel/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="LabelHighScore" type="Label" parent="CenterContainer/Panel/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="LabelLines" type="Label" parent="CenterContainer/Panel/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/Panel/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="ButtonAgain" type="Button" parent="CenterContainer/Panel/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Play Again" + +[node name="ButtonQuit" type="Button" parent="CenterContainer/Panel/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Quit" diff --git a/tetris-game/scenes/hud_left.tscn b/tetris-game/scenes/hud_left.tscn new file mode 100644 index 0000000..2c6b072 --- /dev/null +++ b/tetris-game/scenes/hud_left.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://ds6jmtmpkvtxp"] + +[node name="HUDLeft" type="Control"] +layout_mode = 3 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -20.0 +offset_right = 40.0 +offset_bottom = 20.0 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="LabelScoreTitle" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Score" + +[node name="LabelScoreValue" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "0" + +[node name="LabelHighScoreTitle" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Highscore" + +[node name="LabelHighScoreValue" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "0" + +[node name="LabelLinesTitle" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Lines" + +[node name="LabelLinesValue" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "0" diff --git a/tetris-game/scenes/hud_right.tscn b/tetris-game/scenes/hud_right.tscn new file mode 100644 index 0000000..068a439 --- /dev/null +++ b/tetris-game/scenes/hud_right.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=2 format=3 uid="uid://dsc4ofx4h3gos"] + +[ext_resource type="Script" uid="uid://d0wlixgjpcnyc" path="res://scripts/next_preview.gd" id="1_hyjpj"] + +[node name="HUDRight" type="Control"] +layout_mode = 3 +anchors_preset = 0 +offset_left = 350.0 +offset_right = 390.0 +offset_bottom = 40.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="LabelNextTitle" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Next" + +[node name="NextPreview" type="Node2D" parent="VBoxContainer"] +script = ExtResource("1_hyjpj") diff --git a/tetris-game/scenes/main.tscn b/tetris-game/scenes/main.tscn new file mode 100644 index 0000000..6ad45e2 --- /dev/null +++ b/tetris-game/scenes/main.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=7 format=3 uid="uid://ckvfgxn4jyoi6"] + +[ext_resource type="Script" uid="uid://ctadm2gu04xgn" path="res://scripts/main.gd" id="1_tbgi4"] +[ext_resource type="PackedScene" uid="uid://d0rk850ey8e0g" path="res://scenes/board.tscn" id="2_tefeu"] +[ext_resource type="PackedScene" uid="uid://ds6jmtmpkvtxp" path="res://scenes/hud_left.tscn" id="3_o6xl0"] +[ext_resource type="PackedScene" uid="uid://dsc4ofx4h3gos" path="res://scenes/hud_right.tscn" id="4_tipki"] +[ext_resource type="Shader" uid="uid://ct7v5mfajkvir" path="res://shaders/blur_overlay.gdshader" id="5_85g3d"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_lquwl"] +shader = ExtResource("5_85g3d") +shader_parameter/blur_radius = 8.00000038 +shader_parameter/samples = 7 +shader_parameter/blur_strength = 0.70000003325 +shader_parameter/dim_amount = 0.150000007125 + +[node name="Main" type="Node2D"] +script = ExtResource("1_tbgi4") + +[node name="GameRoot" type="Node2D" parent="."] + +[node name="Board" parent="GameRoot" instance=ExtResource("2_tefeu")] + +[node name="HUDLeft" parent="GameRoot" instance=ExtResource("3_o6xl0")] +offset_top = 150.0 +offset_bottom = 190.0 + +[node name="HUDRight" parent="GameRoot" instance=ExtResource("4_tipki")] +offset_top = 24.0 +offset_bottom = 64.0 + +[node name="UILayer" type="CanvasLayer" parent="GameRoot"] + +[node name="UIRoot" type="Control" parent="GameRoot/UILayer"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_use_anchors_ = true + +[node name="BlurOverlay" type="ColorRect" parent="GameRoot/UILayer/UIRoot"] +visible = false +material = SubResource("ShaderMaterial_lquwl") +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 diff --git a/tetris-game/scripts/board.gd b/tetris-game/scripts/board.gd new file mode 100644 index 0000000..9b4f87f --- /dev/null +++ b/tetris-game/scripts/board.gd @@ -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 + diff --git a/tetris-game/scripts/board.gd.uid b/tetris-game/scripts/board.gd.uid new file mode 100644 index 0000000..bff6089 --- /dev/null +++ b/tetris-game/scripts/board.gd.uid @@ -0,0 +1 @@ +uid://yc60e2spsmjh diff --git a/tetris-game/scripts/game_over.gd b/tetris-game/scripts/game_over.gd new file mode 100644 index 0000000..2105509 --- /dev/null +++ b/tetris-game/scripts/game_over.gd @@ -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() diff --git a/tetris-game/scripts/game_over.gd.uid b/tetris-game/scripts/game_over.gd.uid new file mode 100644 index 0000000..1f55566 --- /dev/null +++ b/tetris-game/scripts/game_over.gd.uid @@ -0,0 +1 @@ +uid://dq8ifo7mfpflr diff --git a/tetris-game/scripts/main.gd b/tetris-game/scripts/main.gd new file mode 100644 index 0000000..f4ac144 --- /dev/null +++ b/tetris-game/scripts/main.gd @@ -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() diff --git a/tetris-game/scripts/main.gd.uid b/tetris-game/scripts/main.gd.uid new file mode 100644 index 0000000..7a5151c --- /dev/null +++ b/tetris-game/scripts/main.gd.uid @@ -0,0 +1 @@ +uid://ctadm2gu04xgn diff --git a/tetris-game/scripts/next_preview.gd b/tetris-game/scripts/next_preview.gd new file mode 100644 index 0000000..d9ed4bb --- /dev/null +++ b/tetris-game/scripts/next_preview.gd @@ -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) diff --git a/tetris-game/scripts/next_preview.gd.uid b/tetris-game/scripts/next_preview.gd.uid new file mode 100644 index 0000000..cd7509c --- /dev/null +++ b/tetris-game/scripts/next_preview.gd.uid @@ -0,0 +1 @@ +uid://d0wlixgjpcnyc diff --git a/tetris-game/shaders/blur_overlay.gdshader b/tetris-game/shaders/blur_overlay.gdshader new file mode 100644 index 0000000..bfc87cf --- /dev/null +++ b/tetris-game/shaders/blur_overlay.gdshader @@ -0,0 +1,39 @@ +shader_type canvas_item; + +uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; + +// Radius in pixels +uniform float blur_radius : hint_range(0.0, 32.0) = 8.0; +// Number of samples in each direction +uniform int samples : hint_range(1, 20) = 6; +// 0 = no blur, 1 = fully blurred +uniform float blur_strength : hint_range(0.0, 1.0) = 0.6; +// Optional slight dimming of background +uniform float dim_amount : hint_range(0.0, 0.5) = 0.1; + +void fragment() { + vec4 original = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0); + + vec2 pixel = SCREEN_PIXEL_SIZE * blur_radius; + + vec4 blurred = vec4(0.0); + int count = 0; + + for (int x = -samples; x <= samples; x++) { + for (int y = -samples; y <= samples; y++) { + vec2 offset = vec2(float(x), float(y)) * pixel; + blurred += textureLod(SCREEN_TEXTURE, SCREEN_UV + offset, 0.0); + count++; + } + } + + blurred /= float(count); + + // Blend between original and blurred + vec4 color = mix(original, blurred, blur_strength); + + // Slight dimming, if wanted + color.rgb *= (1.0 - dim_amount); + + COLOR = color; +} \ No newline at end of file diff --git a/tetris-game/shaders/blur_overlay.gdshader.uid b/tetris-game/shaders/blur_overlay.gdshader.uid new file mode 100644 index 0000000..89fd154 --- /dev/null +++ b/tetris-game/shaders/blur_overlay.gdshader.uid @@ -0,0 +1 @@ +uid://ct7v5mfajkvir