extends Container class_name Game # Declare member variables here. Examples: # var a = 2 # var b = "text" var pf_scale var turn = 1 var is_player_turn = true var selected_piece = null # Holds Vector2(x, y) => { piece, } var board_squares = {} var height var width var landing_piece = null var landing_pos = null var current_state = 0 var states = { 0: { "description": "Player turn", "directive": "Make a move", }, 1: { "description": "Opponent's turn", "directive": "Wait for the opponent to make a move", } } var flash_help = null var rng = null var ai_target = null var ai_piece = null var reinforcements = null var reinforcements_size = 0 var reinforcements_coords = [] var reinforcement_buff = null const on_ai_lose_piece = { 50: "/root/Game/Huh", 75: "/root/Game/StopThat", 90: "/root/Game/Yammering", } const on_player_lose_piece = { 75: "/root/Game/Hehe", 90: "/root/Game/Dust", } const piece_types = { "pawn": "res://src/pieces/Pawn.tscn", "rook": "res://src/pieces/Rook.tscn", "bishop": "res://src/pieces/Bishop.tscn", "knight": "res://src/pieces/Knight.tscn", "king": "res://src/pieces/King.tscn", "queen": "res://src/pieces/Queen.tscn", } func new_piece(piece_type, group, position = null): if not piece_types.has(piece_type): return null var piece = ResourceLoader.load(piece_types[piece_type]).instance() if position != null: if !set_piece_position(piece, position, true): print("Failed to place piece ", piece, " at positiion ", position) piece.queue_free() return null piece.set_position(Vector2( position.x * 128, position.y * 128 )) var pf = get_node("/root/Game/MarginContainer/Playfield") piece.add_to_group("pieces") piece.add_to_group(group) if group == "opponent": piece.get_node("Body").set_modulate( Color(0.8, 0.01, 0.01, 1.0) ) pf.add_child(piece) piece.connect("click", self, "_on_piece_click") piece.connect("hold_start", self, "_on_hold_start") piece.connect("hold_stop", self, "_on_hold_stop") #print("Created piece ", piece, " of type ", piece_type, " at ", position, " for group ", group) return piece func _on_hold_start(piece, event): if piece.is_in_group("player"): if self.current_state != 0: print("Can't move a piece, it's not your turn") get_node("BottomBar/Help").set_text("Cannot move a piece, it's not your turn") self.flash_help = 2 piece.cancel_hold() return self._on_piece_click(piece, event) else: print("Can't move opponent's piece: ", piece) get_node("BottomBar/Help").set_text("Cannot move an opponent's piece") self.flash_help = 3 piece.cancel_hold() func _on_hold_stop(piece, event): if self.selected_piece != null: # deselect get_node("/root/Game/PanelRight/VBox/PieceInfo").set_visible(false) var p = self.selected_piece if p.is_in_group("player"): p.get_node("Body").set_modulate(Color(1, 1, 1, 1)) else: p.get_node("Body").set_modulate(Color(0.8, .01, .01, 1)) var moves = get_valid_piece_moves(p) clear_square_hilights_for_moves(moves) self.selected_piece = null # Try to land the piece on the next physics frame so we # can use raycasts self.landing_piece = piece self.landing_pos = event.position func get_valid_piece_moves(piece, verbose = false): var square = square_of_piece(piece) var possible_moves = piece.get_possible_moves(Vector2(square.x, square.y)) # @TODO Filter based on game state var moves = [] for d in possible_moves: for m in d: var target_square = null if self.board_squares.has(m['pos']): target_square = self.board_squares[m['pos']] if target_square == null: # Move it off the board if verbose: print("Move to ", m['pos'], " is not valid due to not being on the board") break if target_square['piece']: # something here if pieces_hostile(piece, target_square['piece']): if not m['attack']: if verbose: print("Move to ", m['pos'], " is not valid due to not being an attack move") break else: m["target"] = target_square['piece'] moves.append(m) if not m['jump']: break else: if verbose: print("Move to ", m['pos'], " is not valid due to same-team piece is spot") if not m['jump']: break else: # empty if not m['attack_only']: moves.append(m) else: if verbose: print("Move to ", m['pos'], " is not valid due to missing target") break return moves func pieces_hostile(p1, p2): var p1_player = p1.is_in_group("player") var p2_player = p2.is_in_group("player") if p1_player == p2_player: return false return true func _on_piece_click(piece, event): if self.selected_piece != null: # deselect get_node("/root/Game/PanelRight/VBox/PieceInfo").set_visible(false) var p = self.selected_piece if p.is_in_group("player"): p.get_node("Body").set_modulate(Color(1, 1, 1, 1)) else: p.get_node("Body").set_modulate(Color(0.8, .01, .01, 1)) var moves = get_valid_piece_moves(p) clear_square_hilights_for_moves(moves) if self.selected_piece == piece: self.selected_piece = null return if piece == null: self.selected_piece = null return #print("Selected piece: ", piece) if piece.is_in_group("player"): piece.get_node("Body").set_modulate(Color(0, 1, 0, 1)) else: piece.get_node("Body").set_modulate(Color(1.0, 0, 0, 1)) self.selected_piece = piece var square = square_of_piece(piece) var moves = get_valid_piece_moves(piece) set_square_hilights_for_moves(moves) get_node("/root/Game/PanelRight/VBox/PieceInfo").set_piece_info(piece) get_node("/root/Game/PanelRight/VBox/PieceInfo").set_visible(true) func clear_square_hilights_for_moves(moves): var pf = get_node("/root/Game/MarginContainer/Playfield") for m in moves: if pf.squares.has(m["pos"]): var edge = pf.squares[m["pos"]].get_ref().get_node("Body") edge.set_modulate(Color(1, 1, 1, 1.0)) func set_square_hilights_for_moves(moves): var pf = get_node("/root/Game/MarginContainer/Playfield") for m in moves: if pf.squares.has(m["pos"]): var edge = pf.squares[m["pos"]].get_ref().get_node("Body") if not m["attack"]: edge.set_modulate(Color(0, 1, 0, 1)) else: edge.set_modulate(Color(1, 0, 0, 1)) static func position_in_playfield(x, y, width, height): return x >= 0 and x < width and y >= 0 and y < height func square_of_piece(piece): for k in self.board_squares.keys(): if self.board_squares[k]["piece"] == piece: return self.board_squares[k] return null func set_piece_position(piece, position, destroy = false): var square = self.board_squares[position] if square["piece"] != null: if destroy: square["piece"].queue_free() square["piece"] = null else: print("Warning, piece collision during set_piece_position at ", position) return false square["piece"] = piece return true # Called when the node enters the scene tree for the first time. func _ready(): var pf = get_node("/root/Game/MarginContainer/Playfield") var start_height = 8 var start_width = 8 pf.initialize(start_width, start_height) self.height = start_height self.width = start_width # Translate the pf to it sits on top left # We want to scale it to fit within the margin container var pf_size = Vector2(width * 128.0, height * 128.0) var larger = pf_size.x if pf_size.y > larger: larger = pf_size.y # Container is 768x768 var scale = 768 / larger print("Scaling playfield by ", scale) pf.apply_scale(Vector2(scale, scale)) pf.translate(Vector2( 64 * scale, 64 * scale )) self.pf_scale = scale self.rng = RandomNumberGenerator.new() self.rng.randomize() get_node("/root/Game/EndMenu/VBoxContainer/New").connect("pressed", self, "_on_new_game_pressed") get_node("/root/Game/EndMenu/VBoxContainer/Quit").connect("pressed", self, "_on_quit_game_pressed") get_node("/root/Game/EndMenu/VBoxContainer/Fail Game").connect("pressed", self, "_on_fail_game") get_node("/root/Game/EndMenu/VBoxContainer/Win Game").connect("pressed", self, "_on_win_game") get_node("/root/Game/PanelRight/VBox/PieceInfo").connect("stat_change_requested", self, "_on_piece_stat_change_requested") reset_game_state() func _on_piece_stat_change_requested(piece, attribute, value): if piece.get(attribute) != null: piece.set(attribute, value) get_node("/root/Game/PanelRight/VBox/PieceInfo").set_piece_info(piece) func _input(ev): if Input.is_action_just_pressed("ui_cancel"): print(ev) self._on_escape(false, "Paused") func _on_escape(force_visible = false, message = null): var n = get_node("/root/Game/EndMenu") if message != null: n.get_node("VBoxContainer/Label").set_text(message) if n.is_visible() and not force_visible: n.set_visible(false) else: n.popup_centered() func _on_new_game_pressed(): get_node("/root/Game/EndMenu").set_visible(false) self.reset_game_state() func _on_quit_game_pressed(): get_node("/root/Game/EndMenu").set_visible(false) get_tree().quit() func reset_game_state(): get_node("/root/Game/EndSong").stop() self.is_player_turn = true self.turn = 0 for p in get_tree().get_nodes_in_group("pieces"): p.queue_free() self.board_squares = {} var x = 0; var y = 0; while x < self.width: y = 0 while y < self.height: self.board_squares[Vector2(x, y)] = { "x": x, "y": y, "piece": null, "reinforcement": null, } y += 1 x += 1 # Create starting pieces # 8 pawns per side, starting at col 0 + (width-8)/2 # on rows 2, height - 2 var start_x = (self.width - 8) / 2 var y_opponent = 1 var y_player = self.height - 2 var i = 0 while i < 8: x = start_x + i new_piece("pawn", "player", Vector2(x, y_player)) new_piece("pawn", "opponent", Vector2(x, y_opponent)) i += 1 new_piece("rook", "player", Vector2(start_x, self.height - 1)) new_piece("rook", "player", Vector2(start_x + 7, self.height - 1)) new_piece("rook", "opponent", Vector2(start_x, 0)) new_piece("rook", "opponent", Vector2(start_x + 7, 0)) new_piece("bishop", "player", Vector2(start_x + 2, self.height -1)) new_piece("bishop", "player", Vector2(start_x + 5, self.height -1)) new_piece("bishop", "opponent", Vector2(start_x + 2, 0)) new_piece("bishop", "opponent", Vector2(start_x + 5, 0)) new_piece("knight", "player", Vector2(start_x + 1, self.height -1)) new_piece("knight", "player", Vector2(start_x + 6, self.height -1)) new_piece("knight", "opponent", Vector2(start_x + 1, 0)) new_piece("knight", "opponent", Vector2(start_x + 6, 0)) new_piece("king", "player", Vector2(start_x + 4, self.height - 1)) new_piece("king", "opponent", Vector2(start_x + 4, 0)) new_piece("queen", "player", Vector2(start_x + 3, self.height - 1)) new_piece("queen", "opponent", Vector2(start_x + 3, 0)) # Visual updates self.current_state = 3 self.turn = 0 self._on_phase_end() self._reset_help() func _on_phase_end(): self.current_state += 1; if not self.current_state in self.states.keys(): self.current_state = 0 self._on_new_turn() if self.current_state == 0: get_node("PanelRight/VBox/HBox/VBox/HBox/SkipTurnButton").set_disabled(false) else: get_node("PanelRight/VBox/HBox/VBox/HBox/SkipTurnButton").set_disabled(true) get_node("TopBar/Bottom/Instruction").set_text( self.states[self.current_state]["description"] + " - " + self.states[self.current_state]["directive"] ) func choose_reinforcements(size, opponent: bool = true, at_spawn = true): var min_square = 0 var max_square = (self.height * self.width) - 1 if at_spawn: max_square = (self.width)*2 - 1 var range_size = max_square - min_square var arrival_coords = [] var n = 0 while n < size: var try = 0 var square_index = 0 var square = null while try < 10: # Try to get an unoccupied square square_index = self.rng.randi() % range_size var square_coord = Vector2( floor(square_index % self.width), floor(square_index / self.width) ) if at_spawn and not opponent: square_coord.y += self.height - 2 square = self.board_squares[square_coord] if square['piece'] == null and square['reinforcement'] == null: # unoccupied, we'll take it arrival_coords.append(square_coord) var types = self.piece_types.keys() square['reinforcement'] = types[self.rng.randi()%types.size()] break # occupied, try again square = null try += 1 if square == null: print("Failed to find a spot to spawn reinforcement") n += 1 return arrival_coords func _on_new_turn(): self.turn += 1 var just_spawned = false if self.reinforcements != null: var pf = get_node("/root/Game/MarginContainer/Playfield") self.reinforcements -= 1 if self.reinforcements == 1: # decide on what will arrive self.reinforcements_coords = choose_reinforcements(self.reinforcements_size) # show the player where they will arrive for coord in self.reinforcements_coords: pf.squares[coord].get_ref().set_reinforcement(self.board_squares[coord]['reinforcement']) print("Reinforcement at coord ", coord, ": ", self.board_squares[coord]['reinforcement']) get_node("BottomBar/Help").set_text("Reinforcement arrival locations analyzed") self.flash_help = 3 elif self.reinforcements == 0: # spawn them for coord in self.reinforcements_coords: pf.squares[coord].get_ref().set_reinforcement(null) var square = self.board_squares[coord] if square['piece'] == null: new_piece(square['reinforcement'], "opponent", coord) square = self.board_squares[coord] if self.reinforcement_buff != null: square['piece'].speed += self.reinforcement_buff square['piece'].damage += self.reinforcement_buff square['piece'].health += self.reinforcement_buff if self.reinforcement_buff >= 5: square['piece'].health = true else: print("Reinforcement arrival at ", coord, " blocked by piece!") get_node("BottomBar/Help").set_text("Reinforcement at " + str(coord) + " telefragged.") self.flash_help = 2 square['reinforcement'] = null just_spawned = true self.reinforcements = null self.reinforcements_size = 0 self.reinforcements_coords = [] # Check for reinforcements var opponent_pieces = get_tree().get_nodes_in_group("opponent") # @TODO hardcoded value for number of pieces a side should have on the board # If they are less than 1/4 strength ensure that reinforcements are queued if opponent_pieces.size() <= 3 and self.reinforcements == null: self.reinforcements = 2 # 2 turns away self.reinforcements_size = 4 get_node("BottomBar/Help").set_text("Multiple opponent reinforcements detected inbound") get_node("/root/Game/ThinkYouCan").play() self.flash_help = 3 if self.reinforcement_buff != null: self.reinforcement_buff += 1 if self.reinforcement_buff == null: self.reinforcement_buff = 0 if self.reinforcements == null and not just_spawned: var chance = lerp(0, 50, 1 - float(opponent_pieces.size())/16.0) var i = self.rng.randi() % 100 print("Roll for reinforcements ", i, " < ", chance) if i < chance: self.reinforcements = 2 self.reinforcements_size = 1 get_node("BottomBar/Help").set_text("Inbound opponent reinforcements detected") self.flash_help = 3 get_node("TopBar/Top/HBoxContainer/Turn").set_text(str(self.turn)) func _reset_help(): get_node("BottomBar/Help").set_text("Delay losing as long as possible. Your will lose.\nClick on piece to see it's possible moves and stats.\nDrag and drop a piece to make a move.") func _on_game_end(force_condition = null): get_node("/root/Game/TopBar/Bottom/Instruction").set_text("Game over") self.current_state = 99 var player_victory = false if force_condition == null: if get_tree().get_nodes_in_group("opponent").empty(): player_victory = true else: player_victory = force_condition if not player_victory: get_node("/root/Game/EndSong").play() get_node("/root/Game/BottomBar/Help").set_text("Unsurprisingly, the result was known before-hand.") else: get_node("/root/Game/BottomBar/Help").set_text("Well, I'll be damned. I didn't think this would happen!") get_node("/root/Game/Impossible").play() # Show a popup for new one, or quit self._on_escape(true, "Game over!") func _on_fail_game(): _on_game_end(false) func _on_win_game(): _on_game_end(true) # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): if self.flash_help != null: if self.flash_help > 0: self.flash_help -= delta else: self._reset_help() self.flash_help = null var opponent_pieces = get_tree().get_nodes_in_group("opponent") if opponent_pieces.empty() or get_tree().get_nodes_in_group("player").empty() and self.current_state != 99: # The game is over self._on_game_end() if self.current_state == 1: if self.ai_target != null: var target_square = self.board_squares[self.ai_target] var target = get_node("/root/Game/MarginContainer/Playfield").squares[self.ai_target].get_ref() if self.ai_piece.get_global_position().distance_to(target.get_global_position()) >= 5.0: self.ai_piece.set_global_position( self.ai_piece.get_global_position().move_toward( target.get_global_position(), 5.0 ) ) else: # End movement var square = square_of_piece(self.ai_piece) if target_square['piece'] != null: if square['piece'].damage >= target_square['piece'].health: target_square['piece'].queue_free() square['piece'].kills += 1 get_node("PanelRight/VBox/PieceInfo").set_piece_info(square['piece']) var c = self.rng.randi() % 100 var index_to_play = null for idx in self.on_player_lose_piece.keys(): if c < idx: break index_to_play = idx if index_to_play != null: print("ai loss Chance to play: ", c, " got index ", index_to_play) get_node(self.on_player_lose_piece[index_to_play]).play() else: # Deal damage target_square['piece'].health -= square['piece'].damage # @TODO Sound effect # @TODO Visual indication of damage dealt # Bounce piece back target_square = square square['piece'] = null target_square['piece'] = self.ai_piece self.ai_piece.set_position(Vector2(target_square['x']*128, target_square['y']*128)) self.ai_piece.at_spawn = false self.ai_target = null self.ai_piece = null self._on_phase_end() else: # @TODO Find a way to run in a BG thread or split workload across frames # if it takes too long to narrow down a move. var moves = [] for piece in get_tree().get_nodes_in_group("opponent"): moves += get_valid_piece_moves(piece) # Our highest priority moves are to take another piece var priority_moves = [] for m in moves: if m.has("target"): priority_moves.append(m) if not priority_moves.empty(): # @TODO Check for the most "valuable" piece to take var i = self.rng.randi() % (priority_moves.size()) self.ai_target = priority_moves[i]['pos'] self.ai_piece = priority_moves[i]['source'] print("Opponent moving ", self.ai_piece, " to ", self.ai_target, " from ", square_of_piece(self.ai_piece)) elif not moves.empty(): # @TODO Sort our moves to try and get the furthest forward # possible var i = self.rng.randi() % (moves.size()) self.ai_target = moves[i]['pos'] self.ai_piece = moves[i]['source'] print("Opponent moving ", self.ai_piece, " to ", self.ai_target, " from ", square_of_piece(self.ai_piece)) else: # @TODO Would be a good time to spawn a new piece for the opponent self._on_phase_end() print("No possible moves") func _physics_process(delta): if self.landing_piece != null: var piece = self.landing_piece var square = square_of_piece(piece) var moves = get_valid_piece_moves(piece) # Try Land the piece var matches = get_world_2d().direct_space_state.intersect_point( self.landing_pos, 32, [], 2147483647, true, true) var s = null for m in matches: if m["collider"].get_parent() is Square: print(m["collider"].get_parent()) s = m["collider"].get_parent() if s == null: # No valid position, we'll return the piece to where it should be piece.set_position(Vector2( square["x"] * 128, square["y"] * 128 )) else: var pf = get_node("/root/Game/MarginContainer/Playfield") var dest = null for k in pf.squares.keys(): if pf.squares[k].get_ref() == s: dest = k if dest != null: print(dest) var dest_valid = false; for m in moves: if m['pos'] == dest: dest_valid = true if dest_valid: var dest_square = self.board_squares[dest] if dest_square['piece'] != null: # @TODO If the target doesn't die, we need to bounce back if square['piece'].damage >= dest_square['piece'].health: dest_square['piece'].queue_free() square['piece'].kills += 1 get_node("PanelRight/VBox/PieceInfo").set_piece_info(square['piece']) var c = self.rng.randi() % 100 var index_to_play = null for idx in self.on_ai_lose_piece.keys(): if c < idx: break index_to_play = idx; if index_to_play != null: print("ai loss Chance to play: ", c, " got index ", index_to_play) get_node(self.on_ai_lose_piece[index_to_play]).play() else: # @TODO Play a sound effect # @TODO Visual indication of damage dealt dest_square['piece'].health -= square['piece'].damage dest_square = square square['piece'] = null dest_square['piece'] = piece piece.set_position(Vector2(dest_square['x']*128, dest_square['y']*128)) piece.at_spawn = false self._on_phase_end() else: # invalid destination bounce back piece.set_position(Vector2(square['x']*128, square['y']*128)) else: piece.set_position(Vector2(square['x']*128, square['y']*128)) self.landing_piece = null func _on_EndSong_finished(): get_node("/root/Game/EndSong").stop() func _on_Yammering_finished(): get_node("/root/Game/Yammering").stop() func _on_Dust_finished(): get_node("/root/Game/Dust").stop() func _on_ThinkYouCan_finished(): get_node("/root/Game/ThinkYouCan").stop() func _on_Impossible_finished(): get_node("/root/Game/Impossible").stop() func _on_Hehe_finished(): get_node("/root/Game/Hehe").stop() func _on_Huh_finished(): get_node("/root/Game/Huh").stop() func _on_StopThat_finished(): get_node("/root/Game/StopThat").stop() func _on_SkipTurnButton_pressed(): self._on_phase_end()