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 _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) 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 = {} Utils.RoomType.FINAL: data.metadata = { "is_escape": true} Utils.RoomType.SHOP: data.metadata = generate_shop_data(data) Utils.RoomType.EVENT: data.metadata = {} _: 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_chess_data(node): return { "is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false, "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "elo": node.elo, }