taqin/game/src/Taquin.gd

293 lines
8.8 KiB
GDScript

tool
extends Control
class_name Taquin
var Piece = preload("res://src/Piece.tscn")
export var rows: int = 4
export var columns: int = 4
export var width: int = 512
export var height: int = 512
export var difficulty: int = 10
enum Direction { UP, DOWN, LEFT, RIGHT }
var interpiece: int = 4
var min_padding = 15
var padding = Vector2(min_padding, min_padding)
var pieces: Array = []
var missing_piece: Vector2
var rng = RandomNumberGenerator.new()
var current_animation_path: String = "AnimationPlayer/MockPiece:position"
var swipe = Vector2(0, 0)
var is_sliding = false
var current_sliding_piece: Piece = null
var current_origin = Vector2.ZERO
var current_goal = Vector2.ZERO
var current_axis = Vector2.ZERO
var current_slide = Vector2.ZERO
var local_min_position = Vector2.ZERO
var local_max_position = Vector2.ZERO
func position_for_index(index: Vector2, size: int) -> Vector2:
return padding + Vector2(index.x * (size + interpiece), index.y * (size + interpiece))
func compute_piece_size() -> int:
var w_size: int = (width - (2 * min_padding) - ((columns - 1) * interpiece)) / columns
var h_size: int = (height - (2 * min_padding) - ((rows - 1) * interpiece)) / rows
return int(min(w_size, h_size))
func compute_padding(piece_size: int) -> Vector2:
var p = Vector2(0, 0)
p.x = width - columns * piece_size - (columns - 1) * interpiece
p.y = height - rows * piece_size - (rows - 1) * interpiece
p = p / Vector2(2, 2)
return p
func _ready() -> void:
$AnimationPlayer/MockPiece.visible = false
$Particles2D.emitting = false
$Background.rect_size.x = width
$Background.rect_size.y = height
rng.randomize()
var piece_size: int = compute_piece_size()
padding = compute_padding(piece_size)
print("piece size: ", piece_size)
print("padding: ", padding)
for c in range(columns):
var pieces_row: Array = []
for r in range(rows):
var piece = Piece.instance()
piece.size = piece_size
piece.position = position_for_index(Vector2(c, r), piece.size)
piece.order = 1 + c + r * columns
piece.taquin_index = Vector2(c, r)
piece.piece_scale = Vector2((float(piece_size) / width), (float(piece_size) / height))
piece.taquin_position = Vector2(float(piece.position.x) / width, float(piece.position.y) / height)
if r == rows - 1 && c == columns - 1:
piece.visible = false
missing_piece.x = c
missing_piece.y = r
$Background.add_child(piece)
pieces_row.append(piece)
pieces.append(pieces_row)
shuffle(difficulty)
func _input(event):
if $AnimationPlayer.is_playing():
# Disable input during animation
return
var game_state = get_node("/root/Main/GameState") as GameState
if game_state != null:
match game_state.current_state:
# If we are in the winning animation, fast-forward to game over screen
GameState.State.WINNING:
game_state.transition_to(GameState.State.GAME_OVER)
return
GameState.State.GAME_OVER:
return
#
# Handle keyboard input
#
if event.is_action_pressed("ui_up"):
move_piece(Direction.DOWN)
if event.is_action_pressed("ui_down"):
move_piece(Direction.UP)
if event.is_action_pressed("ui_left"):
move_piece(Direction.RIGHT)
if event.is_action_pressed("ui_right"):
move_piece(Direction.LEFT)
#
# Handle touch input
#
if event is InputEventScreenDrag:
# print("screen drag")
swipe = event.relative
if not is_sliding:
is_sliding = true
current_slide = Vector2(0, 0)
var angle = swipe.angle()
var direction = direction_for_angle(angle)
debug_print_direction(direction)
current_sliding_piece = sliding_piece_for_direction(direction)
if current_sliding_piece != null:
current_origin = current_sliding_piece.position
current_axis = axis_for_direction(direction)
var local_end_position = current_axis * (current_sliding_piece.size + interpiece)
local_min_position = Vector2(min(0, local_end_position.x), min(0, local_end_position.y))
local_max_position = Vector2(max(0, local_end_position.x), max(0, local_end_position.y))
current_goal = current_sliding_piece.position + local_end_position
current_slide += swipe
if current_sliding_piece != null:
var delta = current_slide.project(current_axis)
delta.x = clamp(delta.x, local_min_position.x, local_max_position.x)
delta.y = clamp(delta.y, local_min_position.y, local_max_position.y)
current_sliding_piece.position = current_origin + delta
if event is InputEventScreenTouch:
# print("screen touch")
if not event.pressed: # Touch released
is_sliding = false
if current_sliding_piece != null:
var current_position = current_sliding_piece.position
if current_position.distance_to(current_origin) > current_position.distance_to(current_goal):
current_sliding_piece.position = current_goal
var origin_taquin_position = current_sliding_piece.taquin_index
swap(missing_piece, origin_taquin_position)
missing_piece = origin_taquin_position
else:
current_sliding_piece.position = current_origin
func debug_print_direction(direction: int):
match direction:
Direction.UP:
print("Direction ⬆️ UP")
Direction.DOWN:
print("Direction ⬇️ DOWN")
Direction.LEFT:
print("Direction ⬅️ LEFT")
Direction.RIGHT:
print("Direction ➡️ RIGHT")
_:
assert(false)
func axis_for_direction(direction: int) -> Vector2:
match direction:
Direction.UP:
return Vector2.UP
Direction.DOWN:
return Vector2.DOWN
Direction.LEFT:
return Vector2.LEFT
Direction.RIGHT:
return Vector2.RIGHT
_:
assert(false)
return Vector2.ZERO
func direction_for_angle(angle: float) -> int:
if angle < PI / 4 and angle >= - PI / 4:
return Direction.RIGHT
if angle >= PI / 4 and angle < PI - PI / 4:
return Direction.DOWN
if angle >= - PI + PI / 4 and angle < - PI / 4:
return Direction.UP
if angle >= PI - PI / 4 or angle < -PI + PI / 4:
return Direction.LEFT
assert(false)
return Direction.DOWN
func sliding_piece_for_direction(direction) -> Piece:
var destination: Vector2 = missing_piece
match direction:
Direction.UP:
destination.y += 1
Direction.DOWN:
destination.y -= 1
Direction.LEFT:
destination.x += 1
Direction.RIGHT:
destination.x -= 1
if (destination.x < 0 || destination.x >= columns
|| destination.y < 0 || destination.y >= rows):
print("\/!\\ Impossible move")
return null
return pieces[destination.x][destination.y]
func move_piece(direction) -> bool:
var destination: Vector2 = missing_piece
match direction:
Direction.UP:
destination.y -= 1
Direction.DOWN:
destination.y += 1
Direction.LEFT:
destination.x -= 1
Direction.RIGHT:
destination.x += 1
if (destination.x < 0 || destination.x >= columns
|| destination.y < 0 || destination.y >= rows):
print("impossible move")
return false
var moving_piece: Piece = pieces[destination.x][destination.y]
var moving_piece_animation: Animation = $AnimationPlayer.get_animation("MovingPiece")
assert(moving_piece_animation != null)
assert(moving_piece_animation.get_track_count() > 0)
var moving_piece_track_index: int = moving_piece_animation.find_track(current_animation_path)
assert(moving_piece_track_index != -1)
var new_animation_path: String = str($AnimationPlayer.get_parent().get_path_to(moving_piece), ":position")
moving_piece_animation.track_set_path(moving_piece_track_index, new_animation_path)
current_animation_path = new_animation_path
moving_piece_animation.track_set_key_value(moving_piece_track_index, 0, position_for_index(destination, moving_piece.size))
moving_piece_animation.track_set_key_value(moving_piece_track_index, 1, position_for_index(missing_piece, moving_piece.size))
$AnimationPlayer.play("MovingPiece")
swap(missing_piece, destination)
missing_piece = destination
update()
if check_solved():
var game_state = get_node("/root/Main/GameState") as GameState
if game_state != null:
game_state.transition_to(GameState.State.WINNING)
return true
func swap(src: Vector2, dst: Vector2) -> void:
var tmp: Piece = pieces[src.x][src.y]
pieces[src.x][src.y] = pieces[dst.x][dst.y]
pieces[src.x][src.y].taquin_index = src
pieces[dst.x][dst.y] = tmp
tmp.taquin_position = dst
# pieces[src.x][src.y].position = position_for_index(src, tmp.size)
# pieces[dst.x][dst.y].position = position_for_index(dst, tmp.size)
func shuffle(count: int) -> void:
while count > 0:
count -= 1
var direction = rng.randi_range(Direction.UP, Direction.RIGHT)
move_piece(direction)
func check_solved() -> bool:
for c in range(columns):
for r in range(rows):
if pieces[c][r].number != 1 + c + r * columns:
return false
return true
func _on_GameState_state_changed(previous, current):
match current:
GameState.State.WINNING:
$Particles2D.emitting = true
$Timer.start(-1)
GameState.State.GAME_OVER:
$Particles2D.emitting = false
$Timer.stop()
func _on_Timer_timeout():
var game_state = get_node("/root/Main/GameState") as GameState
if game_state != null:
game_state.transition_to(GameState.State.GAME_OVER)