tool class_name Taquin extends Control signal state_changed(previous, new) enum Direction { UP, DOWN, LEFT, RIGHT } enum State { MAIN, WINNING, GAME_OVER, } const _state_transitions = { State.MAIN : [ State.WINNING ], State.WINNING : [ State.GAME_OVER ], State.GAME_OVER : [ State.MAIN ] } const Piece = preload("res://src/Piece.tscn") const Utils = preload("res://src/Utils.gd") export var rows: int = NewGamePanel.normal_rows export var columns: int = NewGamePanel.normal_columns export var shuffle_iterations: int = NewGamePanel.normal_iterations export var artwork_texture: Texture export(State) var current_state = State.MAIN export var autoload_fresh_game := true var board_size := Vector2.ZERO var interpiece := 4 var min_padding := 15 var padding := Vector2(min_padding, min_padding) var pieces: Array = [] var rng := RandomNumberGenerator.new() var current_animation_path := "AnimationPlayer/MockPiece:position" var swipe := Vector2(0, 0) var is_sliding := false var minimum_slide_length := 5.0 var current_sliding_piece: Piece = null var missing_piece: Piece = null var current_origin := Vector2.ZERO var current_goal := Vector2.ZERO var current_axis := Vector2.ZERO var current_touch_slide := Vector2.ZERO var local_min_position := Vector2.ZERO var local_max_position := Vector2.ZERO var hint_active := false setget set_hint_active, get_hint_active #var _artwork_path := NewGamePanel.default_artwork_path onready var hint_tween = $HintTween 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 = (board_size.x - (2 * min_padding) - ((columns - 1) * interpiece)) / columns var h_size: int = (board_size.y - (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 = board_size.x - columns * piece_size - (columns - 1) * interpiece p.y = board_size.y - rows * piece_size - (rows - 1) * interpiece p = p / Vector2(2, 2) return p func _ready() -> void: $AnimationPlayer/MockPiece.visible = false $Particles2D.emitting = false board_size.x = min(rect_size.x, rect_size.y) board_size.y = board_size.x $Background.rect_size = board_size $Background.rect_position = (rect_size - $Background.rect_size) / 2 $Background.set_anchors_preset(Control.PRESET_CENTER) rng.randomize() if autoload_fresh_game: start_fresh() if artwork_texture == null: artwork_texture = NewGamePanel.default_artwork_texture new_game(NewGamePanel.normal_columns, NewGamePanel.normal_rows, NewGamePanel.normal_iterations, artwork_texture) func _unhandled_input(event): # Forward keyboard event _gui_input(event) func _gui_input(event): if $AnimationPlayer.is_playing(): # Disable input during animation return if hint_active or hint_tween.is_active(): return match current_state: # If we are in the winning animation, fast-forward to game over screen State.WINNING: transition_to(State.GAME_OVER) return State.GAME_OVER: return # # Handle keyboard input # if event.is_action_pressed("ui_up"): move_piece(Direction.UP, 1.0) return if event.is_action_pressed("ui_down"): move_piece(Direction.DOWN, 1.0) return if event.is_action_pressed("ui_left"): move_piece(Direction.LEFT, 1.0) return if event.is_action_pressed("ui_right"): move_piece(Direction.RIGHT, 1.0) return # # Handle touch input # if event is InputEventScreenDrag: swipe = event.relative # We check that the slide to have a minimum length # before detecting the direction to avoid jittering. if swipe.length() > minimum_slide_length and not is_sliding: print("sliding started") is_sliding = true current_touch_slide = Vector2.ZERO 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_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_touch_slide += swipe if current_sliding_piece != null: var delta = current_touch_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: 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 commit_slide(true, true) else: reset_position(current_sliding_piece) reset_slide() 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.taquin_current_index 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 var piece = pieces[destination.x][destination.y] current_origin = piece.position return piece func move_piece(direction, speed: float) -> bool: current_sliding_piece = sliding_piece_for_direction(direction) if current_sliding_piece == null: reset_slide() return false if speed > 0.0: 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(current_sliding_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, current_sliding_piece.position) moving_piece_animation.track_set_key_value(moving_piece_track_index, 1, missing_piece.position) $AnimationPlayer.play("MovingPiece") else: var previous_position = current_sliding_piece.position current_sliding_piece.position = missing_piece.position missing_piece.position = previous_position commit_slide(false, false) return true func commit_slide(audio: bool, check_solved: bool): assert(current_sliding_piece != null) assert(current_origin != Vector2.ZERO) var x_delta = missing_piece.taquin_current_index.x - current_sliding_piece.taquin_current_index.x var y_delta = missing_piece.taquin_current_index.y - current_sliding_piece.taquin_current_index.y assert(abs(x_delta) + abs(y_delta) == 1) swap_pieces(missing_piece, current_sliding_piece) reset_position(missing_piece) if audio: $AudioStreamPlayer.play() ensure_validity() reset_slide() update() if check_solved: check_solved() func reset_slide(): current_sliding_piece = null current_origin = Vector2.ZERO func reset_position(p: Piece): p.position = position_for_index(p.taquin_current_index, p.size) func swap_pieces(a: Piece, b: Piece) -> void: var a_index := a.taquin_current_index a.taquin_current_index = b.taquin_current_index pieces[b.taquin_current_index.x][b.taquin_current_index.y] = a b.taquin_current_index = a_index pieces[a_index.x][a_index.y] = b func shuffle(count: int, speed: float) -> void: print_debug("") var previous_direction: int = Direction.DOWN while count > 0: var direction = rng.randi_range(Direction.UP, Direction.RIGHT) # Avoid reversing the previous move if direction == Direction.UP and previous_direction == Direction.DOWN: continue if direction == Direction.DOWN and previous_direction == Direction.UP: continue if direction == Direction.RIGHT and previous_direction == Direction.LEFT: continue if direction == Direction.LEFT and previous_direction == Direction.RIGHT: continue # Retry until the move is valid if move_piece(direction, speed): previous_direction = direction count -= 1 debug_print_direction(direction) func check_solved() -> bool: for c in range(columns): for r in range(rows): if pieces[c][r].order != 1 + c + r * columns: return false transition_to(State.WINNING) return true func ensure_validity() -> void: for c in range(columns): for r in range(rows): var piece = pieces[c][r] assert(piece.taquin_current_index.x == c) assert(piece.taquin_current_index.y == r) func set_pieces_reflection(value: bool) -> void: for c in range(columns): for r in range(rows): var piece = pieces[c][r] piece.set_reflection(value) func current_state_name() -> String: return State.keys()[current_state] func transition_to(state): if current_state == state: return assert(state in _state_transitions[current_state]) var previous_state = current_state current_state = state match current_state: State.WINNING: $Particles2D.emitting = true set_pieces_reflection(true) $Timer.start(-1) State.GAME_OVER: $Particles2D.emitting = false $Timer.stop() emit_signal("state_changed", previous_state, current_state) func save() -> Dictionary: # order start from top-left (1) and iterate over every row # eg. 1 2 3 # 4 5 6 # 7 8 9 var serialized_pieces = [] for r in range(rows): for c in range(columns): var piece: Piece = pieces[c][r] serialized_pieces.append(piece.order) return { "rows": rows, "columns": columns, "pieces": serialized_pieces, "hidden_piece": serialized_pieces.size(), # "artwork_path": _artwork_path, } func load(saved_state) -> bool: print("load save state: ", saved_state) if not saved_state.has_all(["rows", "columns", "pieces", "hidden_piece"]): assert(false, "Invalid save state") return false rows = saved_state["rows"] columns = saved_state["columns"] artwork_texture = Utils.deserialize_texture(NewGamePanel.cached_artwork_path) assert(artwork_texture != null) init(saved_state["pieces"], saved_state["hidden_piece"], artwork_texture) return true func init(pieces_order: Array, hidden_piece: int, artwork: Texture) -> void: var piece_size: int = compute_piece_size() padding = compute_padding(piece_size) print("piece size: ", piece_size) print("padding: ", padding) if pieces.size() > 0: for c in range(pieces.size()): for r in range(pieces[c].size()): pieces[c][r].queue_free() pieces.clear() for c in range(columns): var pieces_row: Array = [] for r in range(rows): var piece = Piece.instance() # Uniforms piece.size = piece_size piece.piece_scale = Vector2(piece_size, piece_size) / board_size piece.texture = artwork_texture # order start from top-left (1) and iterate over every row # eg. 1 2 3 # 4 5 6 # 7 8 9 piece.order = pieces_order[c + r * columns] piece.taquin_original_index = Vector2((piece.order - 1) % columns, (piece.order - 1) / columns) piece.taquin_original_position = position_for_index(piece.taquin_original_index, piece.size) piece.taquin_current_index = Vector2(c, r) piece.taquin_current_position = position_for_index(piece.taquin_current_index, piece.size) piece.taquin_original_normalized_position = piece.taquin_original_position / board_size piece.position = piece.taquin_current_position if piece.order == hidden_piece: piece.visible = false missing_piece = piece $Background.add_child(piece) pieces_row.append(piece) pieces.append(pieces_row) assert(missing_piece != null) func new_game(columns: int, rows: int, shuffle_iterations: int, artwork_texture: Texture) -> void: self.columns = columns self.rows = rows self.shuffle_iterations = shuffle_iterations print_debug("%d x %d with %d shuffles" % [columns, rows, shuffle_iterations]) var pieces_order: Array = [] for order in range(1, rows * columns + 1): pieces_order.append(order) var hidden_piece = rows * columns # Last piece is hidden init(pieces_order, hidden_piece, artwork_texture) shuffle(shuffle_iterations, 0.0) func start_fresh(): if artwork_texture == null: print_debug("Load texture from: ", NewGamePanel.default_artwork_path) artwork_texture = Utils.load_texture_from_path(NewGamePanel.default_artwork_path) new_game(NewGamePanel.normal_columns, NewGamePanel.normal_rows, NewGamePanel.normal_iterations, artwork_texture) # # Hints # func show_hints() -> void: set_hint_active(true) hint_tween.remove_all() for c in range(columns): for r in range(rows): var piece: Piece = pieces[c][r] if piece.taquin_current_index != piece.taquin_original_index: piece.taquin_current_position = piece.position hint_tween.interpolate_property(piece, "position", piece.position, piece.taquin_original_position, 1.0) hint_tween.start() func discard_hints() -> void: hint_tween.remove_all() for c in range(columns): for r in range(rows): var piece: Piece = pieces[c][r] if piece.taquin_current_index != piece.taquin_original_index: hint_tween.interpolate_property(piece, "position", piece.position, piece.taquin_current_position, 0.2) hint_tween.interpolate_callback(self, 0.2, "set_hint_active", false) hint_tween.start() func get_hint_active() -> bool: return hint_active func set_hint_active(value: bool): hint_active = value # # Signals # func _on_Timer_timeout(): transition_to(State.GAME_OVER) func _on_AnimationPlayer_animation_finished(anim_name): match anim_name: "MovingPiece": commit_slide(true, true) func _on_NewGamePanel_start_triggered(preferences, texture): var difficulty_mode = preferences.get_value("game", "difficulty", "normal") print_debug("difficulty mode: ", difficulty_mode) var columns = preferences.get_value("game", "columns", NewGamePanel.normal_columns) var rows = preferences.get_value("game", "rows", NewGamePanel.normal_rows) var shuffle_iterations = preferences.get_value("game", "shuffle_iterations", NewGamePanel.normal_iterations) artwork_texture = texture new_game(columns, rows, shuffle_iterations, artwork_texture) func _on_Hints_button_down(): if not hint_active: show_hints() func _on_Hints_button_up(): discard_hints()