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 = "" var height = 6; # unit_string = level_unit_distribution[level_unit_distribution.size() - 1] # height = 8 for x in unit_string.length(): pawn_string += "p" # "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