ChessBuilder/Systems/Game/Map/MapGenerator.gd

367 lines
12 KiB
GDScript

extends RefCounted
class_name ChessMapGenerator
var min_levels = 10
var max_levels = 20
var max_connections_per_node = 4
var min_nodes_per_level = 1
var max_nodes_per_level = 4
var positions_per_level = 6
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", "rbnqknbnr", "rnbnqknbnr"]
# ["", "", "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 generate_map():
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": {
"is_escape": true,
},
"elo": final_elo
}
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)
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
# dont change final node colour
if connection.from == valid_node.id || valid_node.id == 1:
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):
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):
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, )
Utils.RoomType.NORMAL:
data.metadata = generate_chess_data(data)
Utils.RoomType.BOSS:
data.metadata = generate_chess_data(data)
Utils.RoomType.FINAL:
data.metadata = { "is_escape": true}
Utils.RoomType.SHOP:
data.metadata = generate_shop_data(data)
Utils.RoomType.EVENT:
data.metadata = generate_event_data(data)
_:
data.metadata = {}
return data
func generate_shop_data(node):
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"gold": 1000,
"cards": generate_shop_cards(),
}
func generate_shop_cards():
var shop_cards = []
var all_cards = []
var card_classes = [
HopscotchCard,
FieryCapeCard,
FieryTrailCard,
ExplosiveBootsCard,
DoubleTimeCard,
DrunkDrivingCard,
SupernovaCard
]
for card_class in card_classes:
var card = card_class.new()
all_cards.append(card)
all_cards.shuffle()
var num_shop_cards = min(randi_range(5, 7), all_cards.size())
for i in range(num_shop_cards):
shop_cards.append(all_cards[i % card_classes.size()])
return shop_cards
func generate_starting_data(node):
return {
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"elo": node.elo,
}
func generate_event_data(node):
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"fen": "8/4p3/8/8/8/8 w KQkq - 0 1",
"game_type": "maze",
"win_condition": Utils.WinCondition.Tile,
"elo": node.elo,
}
# "rnbqkbnr1/pppppppp1/9/9/9/9/9/PPPPPPPP1/RNBQKBNR1 w KQkq - 0 1"
func generate_chess_data(node):
# level_unit_distribution
# current_max_level
var rng = float(node.level) / int(current_max_level)
# (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 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 > 7 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:
# for y in unit_string.length() - 1:
# fen += "*"
# fen += "1/"
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)
return {
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
"fen": fen + fen_ending,
"game_type": "chess",
"win_condition": Utils.WinCondition.King,
"elo": node.elo,
}
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))