ld50/src/Game.gd

674 lines
22 KiB
GDScript3

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()