ChessBuilder/Systems/Game/Map/MapGenerator.gd

794 lines
28 KiB
GDScript

extends RefCounted
class_name ChessMapGenerator
var min_levels = 5
var max_levels = 6
var max_connections_per_node = 4
var min_nodes_per_level = 1
var max_nodes_per_level = 4
var positions_per_level = 4
var starting_elo = 1000
var final_elo = 2100
var current_max_level = 0;
# var level_unit_distribution = ["", "", "", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnbr", "rbnbnqknbnbr"]
# var level_unit_distribution = ["", "nk", "nkr", "rnkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr"]
var level_unit_distribution = ["", "nk", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr"]
# ["", "nk", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnnr", "rnnbnqknbnnr"]
# ["", "", "nkr", "rnkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnbr", "rbnbnqknbnbr"]
var _rng = RandomNumberGenerator.new()
var _next_id = 0
func _init(seed_value = null):
# Set seed for reproducible maps
if seed_value != null:
_rng.seed = seed_value
else:
_rng.randomize()
func change_preset_mode(mode, player):
# var game = get_node_or_null("/root/Board") as ChessGame
# var player = game.player
var run_count = player.run_count
match mode:
"vanilla":
min_levels = 5
max_levels = 6
var elo_step = Utils.VANILLA_ELO_STEP
var base_elo = Utils.MIN_ELO + run_count * elo_step
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
final_elo = starting_elo + elo_step
"deeper":
min_levels = 10
max_levels = 16
var elo_step = Utils.DEEPER_ELO_STEP
var base_elo = Utils.MIN_ELO + run_count * elo_step
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
final_elo = starting_elo + elo_step
_:
min_levels = 5
max_levels = 6
var elo_step = Utils.VANILLA_ELO_STEP
var base_elo = Utils.MIN_ELO + run_count * elo_step
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
final_elo = starting_elo + elo_step
func generate_map(mode, player):
var isVanilla = mode == "vanilla"
change_preset_mode(mode, player)
var nodes = []
var connections = []
_next_id = 0
var num_levels = _rng.randi_range(min_levels, max_levels)
current_max_level = num_levels;
var elo_step = float(final_elo - starting_elo) / (num_levels - 1)
var start_node = {
"id": _get_next_id(),
"type": Utils.RoomType.STARTING,
"level": 0,
"position": Vector2(3, 0),
"metadata": {
"is_escape": false,
},
"elo": starting_elo
}
nodes.append(start_node)
# Create final boss node
var final_node = {
"id": _get_next_id(),
"type": Utils.RoomType.FINAL,
"level": num_levels - 1,
"position": Vector2(3, num_levels - 1),
"metadata": {},
"elo": final_elo
}
# final_node.metadata = generate_final_data(final_node)
final_node = generate_node_data(final_node, player)
# print("final_node ====", final_node)
nodes.append(final_node)
var levels_nodes = {0: [start_node], (num_levels - 1): [final_node]}
for level in range(1, num_levels - 1):
var level_nodes = []
var level_elo = starting_elo + (elo_step * level)
# Change this so that its more weighted towards the lower end with rare occurances of max_nodes_per_level rather than an even split
var num_nodes = get_weighted_node_count(min_nodes_per_level, max_nodes_per_level)
var available_positions = []
for pos in range(positions_per_level):
available_positions.append(pos)
available_positions.shuffle()
for i in range(num_nodes):
var node_type = _get_random_room_type(level, num_levels)
var node = {
"id": _get_next_id(),
"type": node_type,
"level": level,
"position": Vector2(available_positions[i], level),
"elo": level_elo,
"metadata": {}
}
node = generate_node_data(node, player)
nodes.append(node)
level_nodes.append(node)
levels_nodes[level] = level_nodes
# Connect nodes between levels
# First connect starting node to level 1
if levels_nodes.has(1) and levels_nodes[1].size() > 0:
var num_connections = min(_rng.randi_range(2, 3), levels_nodes[1].size())
var targets = levels_nodes[1].duplicate()
targets.shuffle()
for i in range(num_connections):
connections.append({
"from": start_node.id,
"to": targets[i].id
})
# Keep track of which nodes are connected
var connected_nodes = [start_node.id]
for level in range(1, num_levels - 1):
if not levels_nodes.has(level) or not levels_nodes.has(level + 1):
continue
var current_level_nodes = levels_nodes[level]
var next_level_nodes = levels_nodes[level + 1].duplicate()
next_level_nodes.shuffle()
# For each node in current level that is connected from previous level
for node in current_level_nodes:
if _is_node_connected_to(node.id, connections):
# Add to connected nodes
connected_nodes.append(node.id)
# Connect to 1-2 nodes in next level if not the final level
if level < num_levels - 2:
var num_next_connections = _rng.randi_range(1, max_connections_per_node)
num_next_connections = min(num_next_connections, next_level_nodes.size())
var shouldTrim = _rng.randi_range(1, 15) > 3 || num_next_connections > 3
for i in range(num_next_connections):
if i < next_level_nodes.size():
connections.append({
"from": node.id,
"to": next_level_nodes[i].id
})
# # Remove the selected nodes so they aren't picked again
# if lots of ocnnection earlier and deeper than lvl 3
# if num_next_connections >= 2 && level > 3:
if shouldTrim:
for i in range(num_next_connections):
if next_level_nodes.size() > 0:
next_level_nodes.pop_front()
# Connect to final boss if at the level before
elif level == num_levels - 2:
connections.append({
"from": node.id,
"to": final_node.id
})
# Remove any nodes that aren't connected
var valid_nodes = []
for node in nodes:
if connected_nodes.has(node.id) or node.id == final_node.id:
valid_nodes.append(node)
# Clean up connections to removed nodes
var valid_connections = []
for conn in connections:
var from_valid = false
var to_valid = false
for node in valid_nodes:
if node.id == conn.from:
from_valid = true
if node.id == conn.to:
to_valid = true
if from_valid and to_valid:
valid_connections.append(conn)
var index = 0
for valid_node in valid_nodes:
var isLeaf = true;
for connection in valid_connections:
# if theres outgoing connection we arent at a dead end
if connection.from == valid_node.id:
isLeaf = false
break;
valid_node.metadata.is_escape = isLeaf
valid_nodes[index] = valid_node;
index += 1
return {
"nodes": valid_nodes,
"connections": valid_connections,
"levels": num_levels,
"seed": _rng.seed
}
func _get_random_room_type(level, total_levels):
# return Utils.RoomType.BOSS
var boss_chance = 0.1 + (level / float(total_levels) * 0.1)
var shop_chance = 0.1 + (level / float(total_levels) * 0.05)
var event_chance = 0.05
if level == total_levels - 2:
boss_chance += 0.3
var roll = _rng.randf()
if roll < boss_chance:
return Utils.RoomType.BOSS
elif roll < boss_chance + shop_chance:
return Utils.RoomType.SHOP
elif roll < boss_chance + shop_chance + event_chance:
return Utils.RoomType.EVENT
else:
return Utils.RoomType.NORMAL
func _is_node_connected_to(node_id, connections):
for conn in connections:
if conn.to == node_id:
return true
return false
func _get_next_id():
var id = _next_id
_next_id += 1
return id
func get_weighted_node_count(min_count, max_count):
# Use exponential distribution to weight toward lower values
var range_size = max_count - min_count + 1
var rand_val = _rng.randf()
# Apply exponential weighting (higher exponent = more weight toward minimum)
var weight_exponent = 2 # Adjust this to control the curve
var weighted_val = pow(rand_val, weight_exponent)
var node_count = min_count + floor(weighted_val * range_size)
if rand_val > 0.70:
node_count = max_count
return node_count
func generate_node_data(node, player):
var data = {
"id": node.id,
"type": node.type,
"level": node.level,
"position": node.position,
"elo": node.elo,
"metadata": node.metadata
}
match data.type:
Utils.RoomType.STARTING:
data.metadata = generate_starting_data(data, player)
Utils.RoomType.NORMAL:
data.metadata = generate_chess_data(data, player)
Utils.RoomType.BOSS:
data.metadata = generate_boss_data(data, player)
Utils.RoomType.FINAL:
data.metadata = generate_final_data(data, player)
Utils.RoomType.SHOP:
data.metadata = generate_shop_data(data, player)
Utils.RoomType.EVENT:
data.metadata = generate_event_data(data, player)
_:
data.metadata = {}
return data
func generate_boss_data(node, player):
# level_unit_distribution
# current_max_level
var rng = float(node.level) / int(current_max_level)
var game_type = Utils.BossType.ZERG
var height = 6;
# (current_value, min_value, max_value, min_index, max_index)
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
var unit_string = level_unit_distribution[index]
var special_unit = ""
var pawn_string = ""
var enemy_unit_depth = 3
if game_type == Utils.BossType.ZERG:
index = 7
height = 7
# if index > 7:
unit_string = level_unit_distribution[index]
elif game_type == Utils.BossType.DOUBLETROUBLE:
index = 9
height = 9
elif game_type == Utils.BossType.WARLARD:
index = 10
height = 10
unit_string = "1rnbqkbnr1"
special_unit = "rnnnqkbbbr"
# if game_type == "zerg":
# index = map_to_array_index(node.level, 5, current_max_level - 2, 1, level_unit_distribution.size() - 1);
for x in unit_string.length():
pawn_string += "p"
var fen = "";
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
if node.level >= 7 and node.level <= 10:
# height = node.level
enemy_unit_depth = 3
elif node.level > 10:
# height = 10
enemy_unit_depth = 4
for x in height - enemy_unit_depth:
if x == 0:
fen += unit_string + "/"
elif x == 1:
fen += pawn_string + "/"
# elif x == height - 3:
# fen += "u"
# # for y in unit_string.length() - 1:
# # fen += "*"
# fen += "u/"
else:
fen += str(unit_string.length()) + "/"
if game_type == Utils.BossType.ZERG:
for x in enemy_unit_depth - 1:
fen += pawn_string.to_upper() + "/"
fen += pawn_string.to_upper()
elif game_type == Utils.BossType.WARLARD:
fen += pawn_string.to_upper() + "/" + special_unit.to_upper()
else:
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
var fen_ending = " w KQkq - 0 1"
# print("generate_chess_data ", fen + fen_ending)
# change condition
var win_condition = Utils.WinConditionType.TURN_NUMBER
var win_target_turn = null
var loss_target_unit = null
var win_target_unit = null
var boss_turn_additional = null
var loss_condition = Utils.LossConditionType.UNIT_LOST
if game_type == Utils.BossType.ZERG:
win_condition = Utils.WinConditionType.TURN_NUMBER
# smallest board = 2 x 6 = 12
# largest board = 10 x 12 = 120
# smallest turn target 10
# largest turn target 50
var roomSize = height * unit_string.length()
var clamped_value = clamp(roomSize, 12, 120)
var percentage = (clamped_value - 12) / (120 - 12)
win_target_turn = 10 + percentage * (50 - 10)
loss_target_unit = "King"
loss_condition = Utils.LossConditionType.UNIT_LOST
elif game_type == Utils.BossType.DOUBLETROUBLE:
boss_turn_additional = 2
win_condition = Utils.WinConditionType.CAPTURE_UNIT
win_target_unit = "King"
loss_target_unit = "King"
loss_condition = Utils.LossConditionType.UNIT_LOST
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"fen": fen + fen_ending,
"game_type": "chess",
"boss_type": game_type,
"has_opponent": true,
"boss_turn_additional": boss_turn_additional,
"win_condition": win_condition,
"win_target_turn": win_target_turn,
"win_target_unit": win_target_unit,
"loss_target_unit": loss_target_unit,
"loss_condition": loss_condition,
"reward": {
"gold": 50 * node.level,
"cards": [],
"selection": generate_shop_cards(3, player),
"selection_limit": 2
},
"elo": node.elo,
}
func generate_shop_data(node, player):
var num_shop_cards = min(randi_range(5, 7), player.unlocked_cards.size())
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"cards": generate_shop_cards(num_shop_cards, player),
}
func generate_shop_cards(num_shop_cards, player):
var shop_cards = []
var all_cards = []
for card_class in player.unlocked_cards:
var card = create_new_card_instance(card_class)
all_cards.append(card)
all_cards.shuffle()
for i in range(num_shop_cards):
shop_cards.append(all_cards[i % player.unlocked_cards.size()])
return shop_cards
func generate_starting_data(node, player):
return {
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"elo": node.elo,
}
func generate_event_data(node, player):
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
var unit_string = level_unit_distribution[index]
var height = 6
var width = unit_string.length()
if node.level > 7 and node.level <= 10:
height = node.level
elif node.level > 10:
height = 10
# var mazedata = generate_maze(width, height)
var mazedata = generate_maze(12, 12)
# print("**************************")
# print("**************************")
# print("**************************")
# print(mazedata)
# print("**************************")
# print("**************************")
# print("**************************")
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"fen": mazedata.fen + " w KQkq - 0 1",
"game_type": "maze",
"has_opponent": false,
"win_condition": Utils.WinConditionType.TILE_REACHED,
"win_target": [mazedata.end],
"win_target_unit": "King",
"elo": node.elo,
"reward": {
"gold": 150 * node.level,
"cards": [],
"selection": generate_shop_cards(5, player),
"selection_limit": 1
},
}
# "rnbqkbnr1/pppppppp1/9/9/9/9/9/PPPPPPPP1/RNBQKBNR1 w KQkq - 0 1"
func generate_final_data(node, player):
# level_unit_distribution
# current_max_level
var rng = float(node.level) / int(current_max_level)
# print(node.level, " ", 2, " ", current_max_level - 2, " ", 1, " ", level_unit_distribution.size() - 1)
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
# print("generate_chess_data ", index)
var unit_string = level_unit_distribution[index]
var pawn_string = ""
for x in unit_string.length():
pawn_string += "p"
var height = 6;
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
var fen = "";
if node.level > 8 and node.level <= 10:
height = node.level
elif node.level > 10:
height = 10
for x in height - 2:
if x == 0:
fen += unit_string + "/"
elif x == 1:
fen += pawn_string + "/"
# elif x == height - 3:
# fen += "u"
# # for y in unit_string.length() - 1:
# # fen += "*"
# fen += "u/"
else:
fen += str(unit_string.length()) + "/"
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
var fen_ending = " w KQkq - 0 1"
# print("generate_chess_data ", fen + fen_ending)
var win_condition = Utils.WinConditionType.CAPTURE_UNIT # Default
var loss_condition = Utils.LossConditionType.UNIT_LOST # Default
# Randomly select a win condition with increasing variety at higher levels
var rng_val = _rng.randf()
var conditions_pool = [Utils.WinConditionType.CAPTURE_UNIT] # Default pool
var additional_metadata = {}
var result = {
"is_escape": true,
"fen": fen + fen_ending,
"game_type": "chess",
"win_condition": win_condition,
"loss_condition": loss_condition,
"has_opponent": true,
"elo": node.elo,
"reward": {
"gold": 50 * node.level
}
}
# Merge additional metadata
for key in additional_metadata:
result[key] = additional_metadata[key]
# print("final_node ", result)
return result
func generate_chess_data(node, player):
# level_unit_distribution
# current_max_level
var rng = float(node.level) / int(current_max_level)
# print(node.level, " ", 2, " ", current_max_level - 2, " ", 1, " ", level_unit_distribution.size() - 1)
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
# print("generate_chess_data ", index)
var unit_string = level_unit_distribution[index]
var pawn_string = ""
for x in unit_string.length():
pawn_string += "p"
var height = 6;
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
var fen = "";
if node.level > 8 and node.level <= 10:
height = node.level
elif node.level > 10:
height = 10
for x in height - 2:
if x == 0:
fen += unit_string + "/"
elif x == 1:
fen += pawn_string + "/"
# elif x == height - 3:
# fen += "u"
# # for y in unit_string.length() - 1:
# # fen += "*"
# fen += "u/"
else:
fen += str(unit_string.length()) + "/"
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
var fen_ending = " w KQkq - 0 1"
# print("generate_chess_data ", fen + fen_ending)
var win_condition = Utils.WinConditionType.CAPTURE_UNIT # Default
var loss_condition = Utils.LossConditionType.UNIT_LOST # Default
# Randomly select a win condition with increasing variety at higher levels
var rng_val = _rng.randf()
var conditions_pool = [Utils.WinConditionType.CAPTURE_UNIT] # Default pool
# # Add more varied win conditions at higher levels
# if node.level >= 3:
# conditions_pool.append(Utils.WinConditionType.BOARD_CLEARED)
# if node.level >= 5:
# conditions_pool.append(Utils.WinConditionType.TILE_REACHED)
# if node.level >= 7:
# conditions_pool.append(Utils.WinConditionType.TURN_NUMBER)
# # For certain special nodes, always use specific conditions
# if node.type == Utils.RoomType.EVENT:
# win_condition = Utils.WinConditionType.TILE_REACHED
# elif node.type == Utils.RoomType.BOSS and node.level > 5:
# # Bosses at higher levels have more varied win conditions
# win_condition = conditions_pool[_rng.randi() % conditions_pool.size()]
# else:
# # Regular nodes have weighted randomness
# if rng_val < 0.7: # 70% chance of default capture king
# win_condition = Utils.WinConditionType.CAPTURE_UNIT
# else:
# # Pick randomly from the conditions pool
# win_condition = conditions_pool[_rng.randi() % conditions_pool.size()]
# Create additional metadata for specific win conditions
var additional_metadata = {}
# match win_condition:
# Utils.WinConditionType.TILE_REACHED:
# # Generate target tiles for the Tile win condition
# var target_tiles = []
# var target_count = 1 # Default to 1 target tile
# # Determine target unit (default is any piece)
# var target_unit = ""
# if _rng.randf() < 0.5: # 50% chance of specific piece
# var pieces = ["Pawn", "Knight", "Bishop", "Rook", "Queen"]
# target_unit = pieces[_rng.randi() % pieces.size()]
# # Create target tiles at the enemy's back rank
# var target_x = _rng.randi() % unit_string.length()
# target_tiles.append(str(target_x) + "-0") # Top row
# additional_metadata["target_tiles"] = target_tiles
# additional_metadata["target_unit"] = target_unit
# Utils.WinConditionType.TURN_NUMBER:
# # Set a target turn number
# additional_metadata["target_turn"] = (node.level * 2) + _rng.randi_range(5, 10)
# # Also add a turn limit for loss condition
# loss_condition = Utils.LossConditionType.TURN_NUMBER
# additional_metadata["turn_limit"] = additional_metadata["target_turn"] + _rng.randi_range(5, 15)
# Build the result metadata
var result = {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"fen": fen + fen_ending,
"game_type": "chess",
"win_condition": win_condition,
"loss_condition": loss_condition,
"has_opponent": true,
"elo": node.elo,
"reward": {
"gold": 50 * node.level
}
}
# Merge additional metadata
for key in additional_metadata:
result[key] = additional_metadata[key]
return result
func map_to_array_index(current_value, min_value, max_value, min_index, max_index):
# Ensure the current value is within bounds
var clamped_value = clamp(current_value, min_value, max_value)
# Calculate how far along the input range we are (0.0 to 1.0)
var normalized_position = float(clamped_value - min_value) / float(max_value - min_value)
# Map this to our target index range
var index_range = max_index - min_index
var mapped_index = min_index + round(normalized_position * index_range)
# Ensure we're returning an integer within the valid array index range
return int(clamp(mapped_index, min_index, max_index))
func generate_maze(width: int, height: int) -> Dictionary:
# Ensure dimensions are odd to have proper walls
if width % 2 == 0:
width += 1
if height % 2 == 0:
height += 1
# Initialize the maze with all walls
var maze = []
for y in range(height):
var row = []
for x in range(width):
row.append("*") # * represents wall
maze.append(row)
# Use a recursive backtracking algorithm to generate the maze
var rng = RandomNumberGenerator.new()
rng.randomize()
# Start at a random odd position
var start_x = rng.randi_range(0, width/2-1) * 2 + 1
var start_y = rng.randi_range(0, height/2-1) * 2 + 1
# Carve the maze recursively
_carve_maze(maze, start_x, start_y, width, height, rng)
# Pick a random end point (far from start point)
var end_x = 0
var end_y = 0
var max_distance = 0
for y in range(1, height, 2):
for x in range(1, width, 2):
if maze[y][x] == " ": # Only consider path cells
var distance = abs(x - start_x) + abs(y - start_y)
if distance > max_distance:
max_distance = distance
end_x = x
end_y = y
maze[start_y][start_x] = "k"
# Mark the end position with a space (keep it as a path)
# It's already a space, but we ensure it here
maze[end_y][end_x] = " "
# Convert the maze to a FEN-like string
var fen_string = ""
for y in range(height):
var empty_count = 0
for x in range(width):
if maze[y][x] == " ": # Empty space
empty_count += 1
else:
if empty_count > 0:
fen_string += str(empty_count)
empty_count = 0
fen_string += maze[y][x]
# Add any remaining empty count
if empty_count > 0:
fen_string += str(empty_count)
# Add row separator (except for the last row)
if y < height - 1:
fen_string += "/"
return {
"fen": fen_string,
"start": str(start_x) + "-"+ str(start_y),
"end": str(end_x) + "-"+ str(end_y)
}
# Recursive function to carve out the maze
func _carve_maze(maze, x, y, width, height, rng):
# Mark the current cell as a path
maze[y][x] = " "
# Define the four possible directions (up, right, down, left)
var directions = [[0, -2], [2, 0], [0, 2], [-2, 0]]
# Shuffle directions for randomness
directions.shuffle()
# Try each direction
for dir in directions:
var nx = x + dir[0]
var ny = y + dir[1]
# Check if the new position is valid and unvisited
if nx > 0 and nx < width-1 and ny > 0 and ny < height-1 and maze[ny][nx] == "*":
# Carve a path between the current cell and the new cell
maze[y + dir[1]/2][x + dir[0]/2] = " "
# Recursively carve from the new cell
_carve_maze(maze, nx, ny, width, height, rng)
func create_new_card_instance(template_card: Card) -> Card:
var new_card = null
var script = template_card.get_script()
if script:
new_card = script.new()
else:
print("Warning: Could not get script from card: " + template_card.cardName)
var class_list = ProjectSettings.get_global_class_list()
var card_class_name = template_card.get_class()
for class_info in class_list:
if class_info["class"] == card_class_name:
var card_script = load(class_info["path"])
if card_script:
new_card = card_script.new()
break
return new_card