From 9a357e275406e7e9759230b5914e9a86fda8baeb Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:12:12 -0500 Subject: [PATCH 01/15] Dependency graph grid --- test_complexity.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test_complexity.py diff --git a/test_complexity.py b/test_complexity.py new file mode 100644 index 0000000..e124df1 --- /dev/null +++ b/test_complexity.py @@ -0,0 +1,9 @@ +exec(open('level_gen/multi-block.py').read().split('# if __name__')[0]) + +for n in range(1, 7): + grid = generate_multi_block_grid(n) + ii_count = sum(row.count('II') for row in grid) + gg_count = sum(row.count('GG') for row in grid) + print(f'\n=== Complexity {n} ===') + print(f'Grid size: {len(grid)}x{len(grid[0])} | II: {ii_count} | GG: {gg_count} | Valid: {ii_count <= gg_count}') + print('\n'.join(''.join(row) for row in grid)) From 6c4b54ddc74135cc44e9679964ef18e60e6a1924 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:19:25 -0500 Subject: [PATCH 02/15] dependency graph generator --- level_gen/dependency.py | 282 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 level_gen/dependency.py diff --git a/level_gen/dependency.py b/level_gen/dependency.py new file mode 100644 index 0000000..9bc0a81 --- /dev/null +++ b/level_gen/dependency.py @@ -0,0 +1,282 @@ +""" +Simple utility to build PDDL problem files for the 'bloxorz' domain +with support for bridges - 3x4 compact version (no yellow tiles). + +Tile legend: + XX - Normal tile + II - Start tile + GG - Goal tile + - Empty space (no tile) + U# - Disabled toggle tile (bridge, where # is a digit) + E# - Hard enable button tile (where # is a digit) + e# - Soft enable button tile (where # is a digit) + +Constraints (applied to special tiles: II, GG, E#, e#): + - Special tiles are not adjacent + - Special tiles are not on the same wall + - Maximum 1 row and 1 column in between special tiles (at most 2 apart) + - Same row only allowed if it's row 2 (middle row, index 1) + - Cannot be in same column + - Bridge entry/exit tiles must be valid + - Every section must have at least one special tile (II, GG, or E#) + - Enable buttons must be accessible from start position +""" + +import random +from time import time + +# Optional solver integration for validation +try: + from ..bloxorz.solve import solve_bloxorz_maze + SOLVER_AVAILABLE = True +except ImportError: + SOLVER_AVAILABLE = False + + +def on_same_wall(pos1, pos2, rows, cols): + """Check if two positions are on the same wall/edge of the grid.""" + r1, c1 = pos1 + r2, c2 = pos2 + + if r1 == 0 and r2 == 0: + return True + if r1 == rows - 1 and r2 == rows - 1: + return True + if c1 == 0 and c2 == 0: + return True + if c1 == cols - 1 and c2 == cols - 1: + return True + + return False + + +def valid_position_pair(r1, c1, r2, c2, rows, cols): + """Check if two positions satisfy placement constraints within a section. + + Constraints: + - Not adjacent (orthogonally) + - Not on same wall + - Max 1 row and 1 col in between (at most 2 apart) + - Same row only if it's row 2 (middle row, index 1 in 0-indexed) + - Cannot be in same column + """ + # Not adjacent (orthogonal only: same row and adjacent col, or same col and adjacent row) + adjacent = ((r1 == r2 and abs(c1 - c2) == 1) or + (c1 == c2 and abs(r1 - r2) == 1)) + if adjacent: + return False + + # Not on same wall + if on_same_wall((r1, c1), (r2, c2), rows, cols): + return False + + # Max 1 row and 1 col in between (so at most 2 rows apart and 2 cols apart) + if abs(r1 - r2) > 2 or abs(c1 - c2) > 2: + return False + + # Same row only allowed if it's a middle row (index 1 or 2 in 0-indexed for 4 rows) + if r1 == r2 and r1 not in (1, 2): + return False + + # Cannot be in same column + if c1 == c2: + return False + + return True + + +def generate_dependency_grid(n, rows, cols, validate_solvable=True): + """Generate a 3x4 grid with bridges (no yellow tiles). + + Applies position constraints, ensures bridge entry/exit tiles are accessible, + and verifies button accessibility. Creates n bridge sections, each 2 tiles long, + connecting board sections. + + Args: + n: Number of bridges to generate (0 or more) + validate_solvable: If True and solver is available, only return grids that have a solution. + """ + max_attempts = 10000 + + for attempt in range(max_attempts): + total_rows = rows * (n + 1) + n * 2 + grid = [["XX" for _ in range(cols)] for _ in range(total_rows)] + + for bridge_id in range(1, n + 1): + bridge_row_start = rows * bridge_id + (bridge_id - 1) * 2 + bridge_col = random.randint(0, cols - 1) + + for row_offset in [0, 1]: + for c in range(cols): + grid[bridge_row_start + row_offset][c] = f"U{bridge_id}" if c == bridge_col else " " + + board_section = random.randint(0, n) + start_r_base = random.randint(0, rows - 1) + start_r = board_section * (rows + 2) + start_r_base + start_c = random.randint(0, cols - 1) + + possible_sections = [s for s in range(n + 1) if s != board_section] if n > 0 else [board_section] + board_section_goal = random.choice(possible_sections) + goal_r_base = random.randint(0, rows - 1) + goal_r = board_section_goal * (rows + 2) + goal_r_base + goal_c = random.randint(0, cols - 1) + + if board_section == board_section_goal: + position_attempts = 0 + while not valid_position_pair(start_r_base, start_c, goal_r_base, goal_c, rows, cols): + goal_r_base = random.randint(0, rows - 1) + goal_r = board_section_goal * (rows + 2) + goal_r_base + goal_c = random.randint(0, cols - 1) + position_attempts += 1 + if position_attempts > 1000: + break + if position_attempts > 1000: + continue + + grid[start_r][start_c] = "II" + grid[goal_r][goal_c] = "GG" + + sections_with_special_tiles = {board_section, board_section_goal} + accessible_sections = {board_section} + + placed = True + for bridge_id in range(1, n + 1): + placed = False + # Button for bridge_id should be in section bridge_id-1 or bridge_id (adjacent to the bridge) + # and must be accessible from start + if bridge_id - 1 in accessible_sections: + section_for_button = bridge_id - 1 + elif bridge_id in accessible_sections: + section_for_button = bridge_id + else: + # If neither side is accessible, we need to regenerate + break + + sections_with_special_tiles.add(section_for_button) + + # Get positions of start and goal in this section (if any) + start_in_section = board_section == section_for_button + goal_in_section = board_section_goal == section_for_button + + def is_valid_button_pos(r_base, c): + if start_in_section and not valid_position_pair(r_base, c, start_r_base, start_c, rows, cols): + return False + if goal_in_section and not valid_position_pair(r_base, c, goal_r_base, goal_c, rows, cols): + return False + return True + + # Randomly choose between hard (E) and soft (e) enable button + button_type = random.choice(["E", "e"]) + + # Try random positions first + for _ in range(100): + er_base = random.randint(0, rows - 1) + er = section_for_button * (rows + 2) + er_base + ec = random.randint(0, cols - 1) + if grid[er][ec] == "XX" and is_valid_button_pos(er_base, ec): + grid[er][ec] = f"{button_type}{bridge_id}" + placed = True + accessible_sections.update([bridge_id - 1, bridge_id]) + break + + # Exhaustive search if random failed + if not placed: + section_start_row = section_for_button * (rows + 2) + section_end_row = section_start_row + rows + for r in range(section_start_row, section_end_row): + for c in range(cols): + if grid[r][c] == "XX" and is_valid_button_pos(r - section_start_row, c): + grid[r][c] = f"{button_type}{bridge_id}" + placed = True + accessible_sections.update([bridge_id - 1, bridge_id]) + break + if placed: + break + + if not placed: + # Failed to place button, regenerate + break + + if not placed or len(sections_with_special_tiles) < n + 1: + continue + + # Validate bridge entry/exit tiles are valid + valid_bridge_connections = True + for bridge_id in range(1, n + 1): + bridge_row_start = rows * bridge_id + (bridge_id - 1) * 2 + bridge_col = next((c for c in range(cols) if grid[bridge_row_start][c] == f"U{bridge_id}"), None) + + if bridge_col is not None: + for check_row in [bridge_row_start - 1, bridge_row_start + 2]: + tile = grid[check_row][bridge_col] + if tile not in ("XX", "II", "GG") and not tile.startswith(("E", "e")): + valid_bridge_connections = False + break + if not valid_bridge_connections: + break + + if valid_bridge_connections: + # Optional: Validate solvability using solver + if validate_solvable and SOLVER_AVAILABLE: + grid_str = "\n".join("".join(row) for row in grid) + try: + plan = solve_bloxorz_maze(grid_str) + if plan is None: + # Unsolvable, try again + continue + except Exception as e: + # Solver error, skip validation for this attempt + print(f"Solver error (continuing): {e}") + pass + + return grid + + return None + + +def write_grid_to_file(grid, filename): + """Write the generated grid to a text file.""" + with open(filename, "w") as f: + for row in grid: + f.write("".join(row) + "\n") + + +def generate_dependency_problem(n) -> str: + """Generate a dependency graph problem with n bridges and return as a string.""" + seed = int(time() * 1000) % 1000 + random.seed(seed) + while (grid := generate_dependency_grid(n, rows=3, cols=4)) is None: + pass + grid_string = "\n".join("".join(row) for row in grid) + return grid_string + + +# if __name__ == "__main__": +# import time +# import sys + +# seed = int(time.time() * 1000) % 1000 +# random.seed(seed) + +# num_bridges = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + +# print(f"Starting generation with seed {seed} and {num_bridges} bridge(s)...") + +# grid = generate_dependency_grid(num_bridges, rows=3, cols=4) + +# if grid is None: +# print(f"Failed to generate valid grid after 10000 attempts. Constraints may be too restrictive.") +# exit(1) + +# print(f"Grid generation completed!") +# grid_file = f"levels/dependency-{seed}.txt" +# pddl_file = f"levels-pddl/dependency-problem-{seed}.pddl" + +# write_grid_to_file(grid, grid_file) + +# print(f"Generated grid (seed: {seed}):") +# for row in grid: +# print("".join(row)) + +# print(f"\nGenerated grid file: {grid_file}") +# print(f"Generated PDDL problem file: {pddl_file}") From 40c798e33622c6d9a42b880aa14e3bfcefe60f6d Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:37:01 -0500 Subject: [PATCH 03/15] four directional bridges --- level_gen/dependency.py | 308 ++++++++++++++++++++++++++++------------ 1 file changed, 218 insertions(+), 90 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 9bc0a81..9dfc4e9 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -10,8 +10,10 @@ U# - Disabled toggle tile (bridge, where # is a digit) E# - Hard enable button tile (where # is a digit) e# - Soft enable button tile (where # is a digit) + D# - Hard disable button tile (where # is a digit) + d# - Soft disable button tile (where # is a digit) -Constraints (applied to special tiles: II, GG, E#, e#): +Constraints (applied to special tiles: II, GG, E#, e#, D#, d#): - Special tiles are not adjacent - Special tiles are not on the same wall - Maximum 1 row and 1 column in between special tiles (at most 2 apart) @@ -20,6 +22,7 @@ - Bridge entry/exit tiles must be valid - Every section must have at least one special tile (II, GG, or E#) - Enable buttons must be accessible from start position + - Disable buttons create trap sections that can block the player """ import random @@ -86,11 +89,10 @@ def valid_position_pair(r1, c1, r2, c2, rows, cols): def generate_dependency_grid(n, rows, cols, validate_solvable=True): - """Generate a 3x4 grid with bridges (no yellow tiles). + """Generate a 3x4 grid with bridges in random directions, organized by sections. Applies position constraints, ensures bridge entry/exit tiles are accessible, - and verifies button accessibility. Creates n bridge sections, each 2 tiles long, - connecting board sections. + and verifies button accessibility. Creates n bridges connecting n+1 sections. Args: n: Number of bridges to generate (0 or more) @@ -99,121 +101,247 @@ def generate_dependency_grid(n, rows, cols, validate_solvable=True): max_attempts = 10000 for attempt in range(max_attempts): - total_rows = rows * (n + 1) + n * 2 - grid = [["XX" for _ in range(cols)] for _ in range(total_rows)] + # Build grid dynamically with each bridge creating a new 3x4 section + # Start with section 0 + grid = [["XX" for _ in range(cols)] for _ in range(rows)] + + sections = { + 0: { + 'row_start': 0, + 'row_end': rows, + 'col_start': 0, + 'col_end': cols + } + } + bridges = [] + + # Track current grid dimensions + max_row = rows + max_col = cols + + # Track orientation counts to ensure mix + orientation_counts = {'vertical': 0, 'horizontal': 0} for bridge_id in range(1, n + 1): - bridge_row_start = rows * bridge_id + (bridge_id - 1) * 2 - bridge_col = random.randint(0, cols - 1) + # Bias toward the less-used orientation to ensure a mix + if orientation_counts['vertical'] > orientation_counts['horizontal'] + 1: + orientation = 'horizontal' + elif orientation_counts['horizontal'] > orientation_counts['vertical'] + 1: + orientation = 'vertical' + else: + orientation = random.choice(['vertical', 'horizontal']) + + orientation_counts[orientation] += 1 - for row_offset in [0, 1]: - for c in range(cols): - grid[bridge_row_start + row_offset][c] = f"U{bridge_id}" if c == bridge_col else " " + if orientation == 'vertical': + # Vertical bridge connects downward to new section below + # Always use original 4-column width for vertical sections + bridge_col = random.randint(0, cols - 1) + bridge_row_start = max_row + + # Add 2 bridge rows (only 4 columns wide) + for _ in range(2): + new_row = [" " for _ in range(cols)] + new_row[bridge_col] = f"U{bridge_id}" + # Pad to current grid width + while len(new_row) < max_col: + new_row.append(" ") + grid.append(new_row) + + max_row += 2 + + # Add new 3x4 section below (only 4 columns wide) + section_row_start = max_row + for _ in range(rows): + new_row = ["XX" for _ in range(cols)] + # Pad to current grid width + while len(new_row) < max_col: + new_row.append(" ") + grid.append(new_row) + + max_row += rows + + sections[bridge_id] = { + 'row_start': section_row_start, + 'row_end': max_row, + 'col_start': 0, + 'col_end': cols # Only 4 columns + } + + bridges.append({ + 'id': bridge_id, + 'orientation': 'vertical', + 'row_start': bridge_row_start, + 'row_end': bridge_row_start + 1, + 'col': bridge_col, + 'connects': (bridge_id - 1, bridge_id) + }) + + else: # horizontal + # Horizontal bridge connects rightward from any existing section + # Pick a random existing section to connect from + source_section_id = random.choice(list(sections.keys())) + source_section = sections[source_section_id] + + # Pick a row within the source section + bridge_row = random.randint(source_section['row_start'], source_section['row_end'] - 1) + bridge_col_start = max_col + + # Extend existing rows - only bridge_row gets the actual bridge + for r in range(len(grid)): + if source_section['row_start'] <= r < source_section['row_end']: + # Only the specific bridge_row gets the bridge tiles + if r == bridge_row: + grid[r].extend([f"U{bridge_id}", f"U{bridge_id}"]) + else: + grid[r].extend([" ", " "]) + # All rows in the source section range get the new section + grid[r].extend(["XX" for _ in range(cols)]) + else: + # Other rows just get padding to maintain rectangular grid + grid[r].extend([" " for _ in range(2 + cols)]) + + section_col_start = max_col + 2 + max_col += 2 + cols + + sections[bridge_id] = { + 'row_start': source_section['row_start'], + 'row_end': source_section['row_end'], + 'col_start': section_col_start, + 'col_end': max_col + } + + bridges.append({ + 'id': bridge_id, + 'orientation': 'horizontal', + 'row': bridge_row, + 'col_start': bridge_col_start, + 'col_end': bridge_col_start + 1, + 'connects': (source_section_id, bridge_id) + }) - board_section = random.randint(0, n) - start_r_base = random.randint(0, rows - 1) - start_r = board_section * (rows + 2) + start_r_base - start_c = random.randint(0, cols - 1) + # Choose start section + start_section = random.randint(0, n) + start_section_info = sections[start_section] - possible_sections = [s for s in range(n + 1) if s != board_section] if n > 0 else [board_section] - board_section_goal = random.choice(possible_sections) - goal_r_base = random.randint(0, rows - 1) - goal_r = board_section_goal * (rows + 2) + goal_r_base - goal_c = random.randint(0, cols - 1) + # Find XX tiles in start section + start_xx_tiles = [ + (r, c) for r in range(start_section_info['row_start'], start_section_info['row_end']) + for c in range(start_section_info['col_start'], start_section_info['col_end']) + if grid[r][c] == "XX" + ] - if board_section == board_section_goal: - position_attempts = 0 - while not valid_position_pair(start_r_base, start_c, goal_r_base, goal_c, rows, cols): - goal_r_base = random.randint(0, rows - 1) - goal_r = board_section_goal * (rows + 2) + goal_r_base - goal_c = random.randint(0, cols - 1) - position_attempts += 1 - if position_attempts > 1000: - break - if position_attempts > 1000: - continue + if not start_xx_tiles: + continue + start_r, start_c = random.choice(start_xx_tiles) grid[start_r][start_c] = "II" + + # Choose goal section (prefer different section if possible) + possible_goal_sections = [s for s in range(n + 1) if s != start_section] if n > 0 else [start_section] + goal_section = random.choice(possible_goal_sections) + goal_section_info = sections[goal_section] + + # Find XX tiles in goal section + goal_xx_tiles = [ + (r, c) for r in range(goal_section_info['row_start'], goal_section_info['row_end']) + for c in range(goal_section_info['col_start'], goal_section_info['col_end']) + if grid[r][c] == "XX" + ] + + if not goal_xx_tiles: + continue + + goal_r, goal_c = random.choice(goal_xx_tiles) grid[goal_r][goal_c] = "GG" - sections_with_special_tiles = {board_section, board_section_goal} - accessible_sections = {board_section} + # Track which sections have special tiles and are accessible + sections_with_special_tiles = {start_section, goal_section} + accessible_sections = {start_section} - placed = True - for bridge_id in range(1, n + 1): - placed = False - # Button for bridge_id should be in section bridge_id-1 or bridge_id (adjacent to the bridge) - # and must be accessible from start - if bridge_id - 1 in accessible_sections: - section_for_button = bridge_id - 1 - elif bridge_id in accessible_sections: - section_for_button = bridge_id + # Place buttons for each bridge (must be in accessible section) + all_buttons_placed = True + for bridge_info in bridges: + bridge_id = bridge_info['id'] + section_a, section_b = bridge_info['connects'] + + # Button must be in an accessible section adjacent to the bridge + if section_a in accessible_sections: + button_section = section_a + elif section_b in accessible_sections: + button_section = section_b else: - # If neither side is accessible, we need to regenerate + # No accessible section found, regenerate + all_buttons_placed = False break - sections_with_special_tiles.add(section_for_button) + sections_with_special_tiles.add(button_section) + button_section_info = sections[button_section] - # Get positions of start and goal in this section (if any) - start_in_section = board_section == section_for_button - goal_in_section = board_section_goal == section_for_button + # Find available XX tiles in button section + available_tiles = [ + (r, c) for r in range(button_section_info['row_start'], button_section_info['row_end']) + for c in range(button_section_info['col_start'], button_section_info['col_end']) + if grid[r][c] == "XX" + ] - def is_valid_button_pos(r_base, c): - if start_in_section and not valid_position_pair(r_base, c, start_r_base, start_c, rows, cols): - return False - if goal_in_section and not valid_position_pair(r_base, c, goal_r_base, goal_c, rows, cols): - return False - return True + if not available_tiles: + all_buttons_placed = False + break # Randomly choose between hard (E) and soft (e) enable button button_type = random.choice(["E", "e"]) - # Try random positions first - for _ in range(100): - er_base = random.randint(0, rows - 1) - er = section_for_button * (rows + 2) + er_base - ec = random.randint(0, cols - 1) - if grid[er][ec] == "XX" and is_valid_button_pos(er_base, ec): - grid[er][ec] = f"{button_type}{bridge_id}" - placed = True - accessible_sections.update([bridge_id - 1, bridge_id]) - break - - # Exhaustive search if random failed - if not placed: - section_start_row = section_for_button * (rows + 2) - section_end_row = section_start_row + rows - for r in range(section_start_row, section_end_row): - for c in range(cols): - if grid[r][c] == "XX" and is_valid_button_pos(r - section_start_row, c): - grid[r][c] = f"{button_type}{bridge_id}" - placed = True - accessible_sections.update([bridge_id - 1, bridge_id]) - break - if placed: - break + # Place button on a random available tile in this section + button_r, button_c = random.choice(available_tiles) + grid[button_r][button_c] = f"{button_type}{bridge_id}" - if not placed: - # Failed to place button, regenerate - break + # Once button is placed, both sections connected by this bridge become accessible + accessible_sections.update([section_a, section_b]) - if not placed or len(sections_with_special_tiles) < n + 1: + if not all_buttons_placed or len(sections_with_special_tiles) < n + 1: continue # Validate bridge entry/exit tiles are valid valid_bridge_connections = True - for bridge_id in range(1, n + 1): - bridge_row_start = rows * bridge_id + (bridge_id - 1) * 2 - bridge_col = next((c for c in range(cols) if grid[bridge_row_start][c] == f"U{bridge_id}"), None) + for bridge_info in bridges: + bridge_id = bridge_info['id'] - if bridge_col is not None: - for check_row in [bridge_row_start - 1, bridge_row_start + 2]: - tile = grid[check_row][bridge_col] - if tile not in ("XX", "II", "GG") and not tile.startswith(("E", "e")): + if bridge_info['orientation'] == 'vertical': + # Check tiles above and below vertical bridge + bridge_col = bridge_info['col'] + row_before = bridge_info['row_start'] - 1 + row_after = bridge_info['row_end'] + 1 + + if row_before >= 0: + tile_before = grid[row_before][bridge_col] + if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e")): + valid_bridge_connections = False + break + + if row_after < len(grid): + tile_after = grid[row_after][bridge_col] + if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e")): + valid_bridge_connections = False + break + + else: # horizontal + # Check tiles left and right of horizontal bridge + bridge_row = bridge_info['row'] + col_before = bridge_info['col_start'] - 1 + col_after = bridge_info['col_end'] + 1 + + if col_before >= 0: + tile_before = grid[bridge_row][col_before] + if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e")): + valid_bridge_connections = False + break + + if col_after < len(grid[0]): + tile_after = grid[bridge_row][col_after] + if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e")): valid_bridge_connections = False break - if not valid_bridge_connections: - break if valid_bridge_connections: # Optional: Validate solvability using solver From bc882b92c2cb61d9e5314c3058c73e2898a1b929 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:38:46 -0500 Subject: [PATCH 04/15] disable buttons --- level_gen/dependency.py | 149 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 140 insertions(+), 9 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 9dfc4e9..25e015f 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -88,11 +88,12 @@ def valid_position_pair(r1, c1, r2, c2, rows, cols): return True -def generate_dependency_grid(n, rows, cols, validate_solvable=True): +def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True): """Generate a 3x4 grid with bridges in random directions, organized by sections. Applies position constraints, ensures bridge entry/exit tiles are accessible, and verifies button accessibility. Creates n bridges connecting n+1 sections. + Optionally adds num_traps trap sections with disable buttons. Args: n: Number of bridges to generate (0 or more) @@ -220,6 +221,94 @@ def generate_dependency_grid(n, rows, cols, validate_solvable=True): 'connects': (source_section_id, bridge_id) }) + # Add trap sections with disable buttons + trap_bridges = [] + next_id = n + 1 + + for trap_num in range(num_traps): + trap_bridge_id = next_id + trap_num + trap_orientation = random.choice(['vertical', 'horizontal']) + + # Pick a random existing section to connect the trap from + source_section_id = random.choice(list(sections.keys())) + source_section = sections[source_section_id] + + if trap_orientation == 'vertical': + # Create vertical trap section + bridge_col = random.randint(0, cols - 1) + bridge_row_start = max_row + + # Add 2 bridge rows + for _ in range(2): + new_row = [" " for _ in range(cols)] + new_row[bridge_col] = f"U{trap_bridge_id}" + while len(new_row) < max_col: + new_row.append(" ") + grid.append(new_row) + + max_row += 2 + + # Add trap section (dead end) + section_row_start = max_row + for _ in range(rows): + new_row = ["XX" for _ in range(cols)] + while len(new_row) < max_col: + new_row.append(" ") + grid.append(new_row) + + max_row += rows + + sections[trap_bridge_id] = { + 'row_start': section_row_start, + 'row_end': max_row, + 'col_start': 0, + 'col_end': cols + } + + trap_bridges.append({ + 'id': trap_bridge_id, + 'orientation': 'vertical', + 'row_start': bridge_row_start, + 'row_end': bridge_row_start + 1, + 'col': bridge_col, + 'connects': (source_section_id, trap_bridge_id), + 'is_trap': True + }) + + else: # horizontal trap + bridge_row = random.randint(source_section['row_start'], source_section['row_end'] - 1) + bridge_col_start = max_col + + for r in range(len(grid)): + if source_section['row_start'] <= r < source_section['row_end']: + if r == bridge_row: + grid[r].extend([f"U{trap_bridge_id}", f"U{trap_bridge_id}"]) + else: + grid[r].extend([" ", " "]) + grid[r].extend(["XX" for _ in range(cols)]) + else: + grid[r].extend([" " for _ in range(2 + cols)]) + + section_col_start = max_col + 2 + max_col += 2 + cols + + sections[trap_bridge_id] = { + 'row_start': source_section['row_start'], + 'row_end': source_section['row_end'], + 'col_start': section_col_start, + 'col_end': max_col + } + + trap_bridges.append({ + 'id': trap_bridge_id, + 'orientation': 'horizontal', + 'row': bridge_row, + 'col_start': bridge_col_start, + 'col_end': bridge_col_start + 1, + 'connects': (source_section_id, trap_bridge_id), + 'is_trap': True + }) + # Choose start section start_section = random.randint(0, n) start_section_info = sections[start_section] @@ -299,12 +388,49 @@ def generate_dependency_grid(n, rows, cols, validate_solvable=True): # Once button is placed, both sections connected by this bridge become accessible accessible_sections.update([section_a, section_b]) + # Place disable buttons for trap bridges (in any accessible section) + for trap_bridge_info in trap_bridges: + trap_bridge_id = trap_bridge_info['id'] + trap_section_a, trap_section_b = trap_bridge_info['connects'] + + # Place disable button in any accessible section (not in the trap itself) + available_sections = [s for s in accessible_sections if s != trap_section_b] + if not available_sections: + all_buttons_placed = False + break + + disable_button_section = random.choice(available_sections) + sections_with_special_tiles.add(disable_button_section) + disable_section_info = sections[disable_button_section] + + # Find available XX tiles + available_tiles = [ + (r, c) for r in range(disable_section_info['row_start'], disable_section_info['row_end']) + for c in range(disable_section_info['col_start'], disable_section_info['col_end']) + if grid[r][c] == "XX" + ] + + if not available_tiles: + all_buttons_placed = False + break + + # Randomly choose between hard (D) and soft (d) disable button + disable_button_type = random.choice(["D", "d"]) + + # Place disable button + disable_r, disable_c = random.choice(available_tiles) + grid[disable_r][disable_c] = f"{disable_button_type}{trap_bridge_id}" + + # Mark the trap section as having a special tile + sections_with_special_tiles.add(trap_section_b) + if not all_buttons_placed or len(sections_with_special_tiles) < n + 1: continue # Validate bridge entry/exit tiles are valid valid_bridge_connections = True - for bridge_info in bridges: + all_bridges = bridges + trap_bridges + for bridge_info in all_bridges: bridge_id = bridge_info['id'] if bridge_info['orientation'] == 'vertical': @@ -315,13 +441,13 @@ def generate_dependency_grid(n, rows, cols, validate_solvable=True): if row_before >= 0: tile_before = grid[row_before][bridge_col] - if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e")): + if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e", "D", "d")): valid_bridge_connections = False break if row_after < len(grid): tile_after = grid[row_after][bridge_col] - if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e")): + if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e", "D", "d")): valid_bridge_connections = False break @@ -333,13 +459,13 @@ def generate_dependency_grid(n, rows, cols, validate_solvable=True): if col_before >= 0: tile_before = grid[bridge_row][col_before] - if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e")): + if tile_before not in ("XX", "II", "GG") and not tile_before.startswith(("E", "e", "D", "d")): valid_bridge_connections = False break if col_after < len(grid[0]): tile_after = grid[bridge_row][col_after] - if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e")): + if tile_after not in ("XX", "II", "GG") and not tile_after.startswith(("E", "e", "D", "d")): valid_bridge_connections = False break @@ -369,11 +495,16 @@ def write_grid_to_file(grid, filename): f.write("".join(row) + "\n") -def generate_dependency_problem(n) -> str: - """Generate a dependency graph problem with n bridges and return as a string.""" +def generate_dependency_problem(n, num_traps=0) -> str: + """Generate a dependency graph problem with n bridges and return as a string. + + Args: + n: Number of regular bridges + num_traps: Number of trap sections with disable buttons (default 0) + """ seed = int(time() * 1000) % 1000 random.seed(seed) - while (grid := generate_dependency_grid(n, rows=3, cols=4)) is None: + while (grid := generate_dependency_grid(n, rows=3, cols=4, num_traps=num_traps)) is None: pass grid_string = "\n".join("".join(row) for row in grid) return grid_string From 4209ebaa24aed3af368b9f4fb9134e6f4c35ddc6 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:45:46 -0500 Subject: [PATCH 05/15] dead sections as bait --- level_gen/dependency.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 25e015f..465b50a 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -326,9 +326,16 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) start_r, start_c = random.choice(start_xx_tiles) grid[start_r][start_c] = "II" - # Choose goal section (prefer different section if possible) - possible_goal_sections = [s for s in range(n + 1) if s != start_section] if n > 0 else [start_section] - goal_section = random.choice(possible_goal_sections) + # Place goal in a trap section if traps exist, otherwise in a random section + if trap_bridges: + # Choose a random trap section for the goal + trap_section_ids = [tb['connects'][1] for tb in trap_bridges] + goal_section = random.choice(trap_section_ids) + else: + # Choose goal section (prefer different section if possible) + possible_goal_sections = [s for s in range(n + 1) if s != start_section] if n > 0 else [start_section] + goal_section = random.choice(possible_goal_sections) + goal_section_info = sections[goal_section] # Find XX tiles in goal section From 7e76b556386868bc5bda10d46ab597597f4498db Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 00:48:22 -0500 Subject: [PATCH 06/15] max one disable and one enable button per section --- analyze_dependency.py | 79 +++++++++++++++++++++++++++++++++++++++++ level_gen/dependency.py | 47 ++++++++++++++++++------ 2 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 analyze_dependency.py diff --git a/analyze_dependency.py b/analyze_dependency.py new file mode 100644 index 0000000..daca695 --- /dev/null +++ b/analyze_dependency.py @@ -0,0 +1,79 @@ +"""Analyze the dependency graph of a generated puzzle.""" + +import sys +sys.path.insert(0, '.') + +from level_gen.dependency import generate_dependency_grid +import random + +# Generate a puzzle with known seed +random.seed(42) +grid = generate_dependency_grid(4, rows=3, cols=4, num_traps=2, validate_solvable=False) + +if grid is None: + print("Failed to generate grid") + sys.exit(1) + +grid_str = '\n'.join(''.join(row) for row in grid) + +print("PUZZLE:") +print("="*70) +print(grid_str) +print("\n" + "="*70) +print("\nDEPENDENCY GRAPH STRUCTURE:") +print("="*70) + +print(""" +NODES: +------ +1. Sections (S0, S1, S2, ..., Sn): Physical areas of the grid + - S0: Initial section (contains start tile II) + - S1-Sn: Sections connected by bridges + - Trap sections: Dead-end sections (contain goal GG) + +2. Buttons (Enable/Disable controls): + - E#/e#: Enable buttons (open bridge U#) + - D#/d#: Disable buttons (close bridge U#) + +EDGES (Dependencies): +--------------------- +1. Section → Button: "Button is in this section" + - Can only press button if you can reach its section + +2. Button → Bridge: "Button controls this bridge" + - Enable button (E#/e#) opens bridge U# + - Disable button (D#/d#) closes bridge U# + +3. Bridge → Section: "Bridge connects to this section" + - Open bridge allows access to connected section + +DEPENDENCY FLOW: +---------------- +START (S0) → E1 → Bridge-U1 → S1 → E2 → Bridge-U2 → S2 → ... → Sn (GOAL) + +With trap sections: +- D# buttons in main path can close bridges to trap sections +- Goal is in a trap section, creating tension: + * Must reach goal before pressing D# button + * Or must avoid pressing D# button entirely + +EXAMPLE STRUCTURE: +------------------ +S0 (start) + ├─ E1 button → enables U1 bridge → S1 + │ ├─ E2 button → enables U2 bridge → S2 + │ └─ D5 button → disables U5 bridge → trap S5 + └─ D6 button → disables U6 bridge → trap S6 (contains GOAL) + +SOLVING CONSTRAINT: +------------------- +Must visit sections in order that respects button dependencies: +1. Can only enable a bridge from a reachable section +2. Must reach goal before any disable button is pressed +3. Pressing disable buttons creates unsolvable state (soft lock) +""") + +print("\nThis creates a partially ordered constraint satisfaction problem where:") +print("- Enable buttons create forward progress (unlock new sections)") +print("- Disable buttons create traps (lock access to sections)") +print("- Goal placement in trap section forces careful navigation") diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 465b50a..e23af49 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -355,6 +355,10 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) sections_with_special_tiles = {start_section, goal_section} accessible_sections = {start_section} + # Track which sections already have buttons (at most one enable and one disable per section) + sections_with_enable_button = set() + sections_with_disable_button = set() + # Place buttons for each bridge (must be in accessible section) all_buttons_placed = True for bridge_info in bridges: @@ -362,16 +366,28 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) section_a, section_b = bridge_info['connects'] # Button must be in an accessible section adjacent to the bridge - if section_a in accessible_sections: - button_section = section_a - elif section_b in accessible_sections: - button_section = section_b - else: + # Try to find a section without an enable button already + possible_sections = [] + if section_a in accessible_sections and section_a not in sections_with_enable_button: + possible_sections.append(section_a) + if section_b in accessible_sections and section_b not in sections_with_enable_button: + possible_sections.append(section_b) + + # If both have enable buttons, allow reuse (will fail in regeneration) + if not possible_sections: + if section_a in accessible_sections: + possible_sections.append(section_a) + elif section_b in accessible_sections: + possible_sections.append(section_b) + + if not possible_sections: # No accessible section found, regenerate all_buttons_placed = False break + button_section = random.choice(possible_sections) sections_with_special_tiles.add(button_section) + sections_with_enable_button.add(button_section) button_section_info = sections[button_section] # Find available XX tiles in button section @@ -401,13 +417,24 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) trap_section_a, trap_section_b = trap_bridge_info['connects'] # Place disable button in any accessible section (not in the trap itself) - available_sections = [s for s in accessible_sections if s != trap_section_b] - if not available_sections: - all_buttons_placed = False - break + # Prefer sections without disable buttons already + available_sections_no_disable = [ + s for s in accessible_sections + if s != trap_section_b and s not in sections_with_disable_button + ] + + if available_sections_no_disable: + disable_button_section = random.choice(available_sections_no_disable) + else: + # All have disable buttons, try any accessible section + available_sections = [s for s in accessible_sections if s != trap_section_b] + if not available_sections: + all_buttons_placed = False + break + disable_button_section = random.choice(available_sections) - disable_button_section = random.choice(available_sections) sections_with_special_tiles.add(disable_button_section) + sections_with_disable_button.add(disable_button_section) disable_section_info = sections[disable_button_section] # Find available XX tiles From 028ef98441e2c611e8e82ee9e8b29a511224472c Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 01:04:01 -0500 Subject: [PATCH 07/15] fixed section tracking --- analyze_dependency.py | 79 ----------------------------------------- level_gen/dependency.py | 58 ++++++++++++++++-------------- 2 files changed, 31 insertions(+), 106 deletions(-) delete mode 100644 analyze_dependency.py diff --git a/analyze_dependency.py b/analyze_dependency.py deleted file mode 100644 index daca695..0000000 --- a/analyze_dependency.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Analyze the dependency graph of a generated puzzle.""" - -import sys -sys.path.insert(0, '.') - -from level_gen.dependency import generate_dependency_grid -import random - -# Generate a puzzle with known seed -random.seed(42) -grid = generate_dependency_grid(4, rows=3, cols=4, num_traps=2, validate_solvable=False) - -if grid is None: - print("Failed to generate grid") - sys.exit(1) - -grid_str = '\n'.join(''.join(row) for row in grid) - -print("PUZZLE:") -print("="*70) -print(grid_str) -print("\n" + "="*70) -print("\nDEPENDENCY GRAPH STRUCTURE:") -print("="*70) - -print(""" -NODES: ------- -1. Sections (S0, S1, S2, ..., Sn): Physical areas of the grid - - S0: Initial section (contains start tile II) - - S1-Sn: Sections connected by bridges - - Trap sections: Dead-end sections (contain goal GG) - -2. Buttons (Enable/Disable controls): - - E#/e#: Enable buttons (open bridge U#) - - D#/d#: Disable buttons (close bridge U#) - -EDGES (Dependencies): ---------------------- -1. Section → Button: "Button is in this section" - - Can only press button if you can reach its section - -2. Button → Bridge: "Button controls this bridge" - - Enable button (E#/e#) opens bridge U# - - Disable button (D#/d#) closes bridge U# - -3. Bridge → Section: "Bridge connects to this section" - - Open bridge allows access to connected section - -DEPENDENCY FLOW: ----------------- -START (S0) → E1 → Bridge-U1 → S1 → E2 → Bridge-U2 → S2 → ... → Sn (GOAL) - -With trap sections: -- D# buttons in main path can close bridges to trap sections -- Goal is in a trap section, creating tension: - * Must reach goal before pressing D# button - * Or must avoid pressing D# button entirely - -EXAMPLE STRUCTURE: ------------------- -S0 (start) - ├─ E1 button → enables U1 bridge → S1 - │ ├─ E2 button → enables U2 bridge → S2 - │ └─ D5 button → disables U5 bridge → trap S5 - └─ D6 button → disables U6 bridge → trap S6 (contains GOAL) - -SOLVING CONSTRAINT: -------------------- -Must visit sections in order that respects button dependencies: -1. Can only enable a bridge from a reachable section -2. Must reach goal before any disable button is pressed -3. Pressing disable buttons creates unsolvable state (soft lock) -""") - -print("\nThis creates a partially ordered constraint satisfaction problem where:") -print("- Enable buttons create forward progress (unlock new sections)") -print("- Disable buttons create traps (lock access to sections)") -print("- Goal placement in trap section forces careful navigation") diff --git a/level_gen/dependency.py b/level_gen/dependency.py index e23af49..f03db02 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -7,11 +7,12 @@ II - Start tile GG - Goal tile - Empty space (no tile) - U# - Disabled toggle tile (bridge, where # is a digit) - E# - Hard enable button tile (where # is a digit) - e# - Soft enable button tile (where # is a digit) - D# - Hard disable button tile (where # is a digit) - d# - Soft disable button tile (where # is a digit) + U# - Disabled toggle tile (bridge, where # is a digit) - controlled by enable buttons + A# - Enabled toggle tile (bridge, where # is a digit) - controlled by disable buttons + E# - Hard enable button tile (where # is a digit) - enables U# bridges + e# - Soft enable button tile (where # is a digit) - enables U# bridges + D# - Hard disable button tile (where # is a digit) - disables A# bridges + d# - Soft disable button tile (where # is a digit) - disables A# bridges Constraints (applied to special tiles: II, GG, E#, e#, D#, d#): - Special tiles are not adjacent @@ -191,7 +192,7 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) # Extend existing rows - only bridge_row gets the actual bridge for r in range(len(grid)): if source_section['row_start'] <= r < source_section['row_end']: - # Only the specific bridge_row gets the bridge tiles + # Only the specific bridge_row gets the bridge tiles (U# = disabled toggle for enable buttons) if r == bridge_row: grid[r].extend([f"U{bridge_id}", f"U{bridge_id}"]) else: @@ -238,10 +239,10 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) bridge_col = random.randint(0, cols - 1) bridge_row_start = max_row - # Add 2 bridge rows + # Add 2 bridge rows (A# = enabled toggle for disable buttons) for _ in range(2): new_row = [" " for _ in range(cols)] - new_row[bridge_col] = f"U{trap_bridge_id}" + new_row[bridge_col] = f"A{trap_bridge_id}" while len(new_row) < max_col: new_row.append(" ") grid.append(new_row) @@ -366,22 +367,15 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) section_a, section_b = bridge_info['connects'] # Button must be in an accessible section adjacent to the bridge - # Try to find a section without an enable button already + # Strictly enforce: only place in sections without an enable button possible_sections = [] if section_a in accessible_sections and section_a not in sections_with_enable_button: possible_sections.append(section_a) if section_b in accessible_sections and section_b not in sections_with_enable_button: possible_sections.append(section_b) - # If both have enable buttons, allow reuse (will fail in regeneration) if not possible_sections: - if section_a in accessible_sections: - possible_sections.append(section_a) - elif section_b in accessible_sections: - possible_sections.append(section_b) - - if not possible_sections: - # No accessible section found, regenerate + # No section available without violating constraint, regenerate all_buttons_placed = False break @@ -417,22 +411,18 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) trap_section_a, trap_section_b = trap_bridge_info['connects'] # Place disable button in any accessible section (not in the trap itself) - # Prefer sections without disable buttons already + # Strictly enforce: only place in sections without a disable button available_sections_no_disable = [ s for s in accessible_sections if s != trap_section_b and s not in sections_with_disable_button ] - if available_sections_no_disable: - disable_button_section = random.choice(available_sections_no_disable) - else: - # All have disable buttons, try any accessible section - available_sections = [s for s in accessible_sections if s != trap_section_b] - if not available_sections: - all_buttons_placed = False - break - disable_button_section = random.choice(available_sections) + if not available_sections_no_disable: + # No section available without violating constraint, regenerate + all_buttons_placed = False + break + disable_button_section = random.choice(available_sections_no_disable) sections_with_special_tiles.add(disable_button_section) sections_with_disable_button.add(disable_button_section) disable_section_info = sections[disable_button_section] @@ -535,7 +525,21 @@ def generate_dependency_problem(n, num_traps=0) -> str: Args: n: Number of regular bridges num_traps: Number of trap sections with disable buttons (default 0) + + Note: Total bridges (n + num_traps) is capped at 9 """ + # Enforce maximum of 9 total bridges + total_bridges = n + num_traps + if total_bridges > 9: + # Adjust to keep total at 9 + if n > 6: # Reserve at least 3 for traps if requested + n = 9 - num_traps + if n < 1: + n = 6 + num_traps = 3 + else: + num_traps = 9 - n + seed = int(time() * 1000) % 1000 random.seed(seed) while (grid := generate_dependency_grid(n, rows=3, cols=4, num_traps=num_traps)) is None: From 7428c249869520add118593aeaae1b5378a4f677 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 01:32:19 -0500 Subject: [PATCH 08/15] added constraint on where the disable button can be --- level_gen/dependency.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index f03db02..b0ce2a3 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -427,11 +427,28 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) sections_with_disable_button.add(disable_button_section) disable_section_info = sections[disable_button_section] - # Find available XX tiles + # Find the specific trap bridge this button controls to avoid its row/col + incoming_bridge_rows = set() + incoming_bridge_cols = set() + + # Only check the trap bridge that this disable button controls + if trap_bridge_info['orientation'] == 'horizontal': + # For horizontal bridges, avoid the bridge row if it overlaps with our section + if (disable_section_info['row_start'] <= trap_bridge_info['row'] < disable_section_info['row_end']): + incoming_bridge_rows.add(trap_bridge_info['row']) + else: # vertical + # For vertical bridges, avoid the bridge column if it overlaps with our section's column range + # Vertical bridges are always at columns 0-3 (absolute grid positions) + bridge_col = trap_bridge_info['col'] + # Check if this column is within our section's column range + if disable_section_info['col_start'] <= bridge_col < disable_section_info['col_end']: + incoming_bridge_cols.add(bridge_col) + + # Find available XX tiles, excluding those in bridge rows/cols available_tiles = [ (r, c) for r in range(disable_section_info['row_start'], disable_section_info['row_end']) for c in range(disable_section_info['col_start'], disable_section_info['col_end']) - if grid[r][c] == "XX" + if grid[r][c] == "XX" and r not in incoming_bridge_rows and c not in incoming_bridge_cols ] if not available_tiles: From 92166020ab3569aae5040920cba37d74f6146f43 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 01:33:59 -0500 Subject: [PATCH 09/15] remove unnecessary code --- level_gen/dependency.py | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index b0ce2a3..1cffa4a 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -562,35 +562,4 @@ def generate_dependency_problem(n, num_traps=0) -> str: while (grid := generate_dependency_grid(n, rows=3, cols=4, num_traps=num_traps)) is None: pass grid_string = "\n".join("".join(row) for row in grid) - return grid_string - - -# if __name__ == "__main__": -# import time -# import sys - -# seed = int(time.time() * 1000) % 1000 -# random.seed(seed) - -# num_bridges = int(sys.argv[1]) if len(sys.argv) > 1 else 1 - -# print(f"Starting generation with seed {seed} and {num_bridges} bridge(s)...") - -# grid = generate_dependency_grid(num_bridges, rows=3, cols=4) - -# if grid is None: -# print(f"Failed to generate valid grid after 10000 attempts. Constraints may be too restrictive.") -# exit(1) - -# print(f"Grid generation completed!") -# grid_file = f"levels/dependency-{seed}.txt" -# pddl_file = f"levels-pddl/dependency-problem-{seed}.pddl" - -# write_grid_to_file(grid, grid_file) - -# print(f"Generated grid (seed: {seed}):") -# for row in grid: -# print("".join(row)) - -# print(f"\nGenerated grid file: {grid_file}") -# print(f"Generated PDDL problem file: {pddl_file}") + return grid_string \ No newline at end of file From 1e411968c0a6ddd3741e70a8e0907961319e1fed Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 01:38:20 -0500 Subject: [PATCH 10/15] added constraints on the tile placement within sections --- level_gen/dependency.py | 91 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 1cffa4a..7b546ff 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -89,6 +89,20 @@ def valid_position_pair(r1, c1, r2, c2, rows, cols): return True +def get_special_tiles_in_section(grid, section_info): + """Get all special tiles (II, GG, E#, e#, D#, d#) in a section with their positions.""" + special_tiles = [] + for r in range(section_info['row_start'], section_info['row_end']): + for c in range(section_info['col_start'], section_info['col_end']): + tile = grid[r][c] + if tile in ("II", "GG") or tile.startswith(("E", "e", "D", "d")): + # Convert to relative position within section + r_rel = r - section_info['row_start'] + c_rel = c - section_info['col_start'] + special_tiles.append((r_rel, c_rel, tile)) + return special_tiles + + def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True): """Generate a 3x4 grid with bridges in random directions, organized by sections. @@ -324,6 +338,7 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) if not start_xx_tiles: continue + # Place start tile - no constraints yet since it's the first special tile start_r, start_c = random.choice(start_xx_tiles) grid[start_r][start_c] = "II" @@ -349,8 +364,32 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) if not goal_xx_tiles: continue - goal_r, goal_c = random.choice(goal_xx_tiles) - grid[goal_r][goal_c] = "GG" + # Place goal tile - check constraints if in same section as start + goal_placed = False + if goal_section == start_section: + # Get special tiles in this section (should only be start at this point) + special_tiles = get_special_tiles_in_section(grid, goal_section_info) + # Try to find a valid position for goal + random.shuffle(goal_xx_tiles) + for goal_r, goal_c in goal_xx_tiles: + goal_r_rel = goal_r - goal_section_info['row_start'] + goal_c_rel = goal_c - goal_section_info['col_start'] + # Check against all existing special tiles + valid = True + for special_r, special_c, _ in special_tiles: + if not valid_position_pair(goal_r_rel, goal_c_rel, special_r, special_c, rows, cols): + valid = False + break + if valid: + grid[goal_r][goal_c] = "GG" + goal_placed = True + break + if not goal_placed: + continue + else: + # Different section, no constraints needed + goal_r, goal_c = random.choice(goal_xx_tiles) + grid[goal_r][goal_c] = "GG" # Track which sections have special tiles and are accessible sections_with_special_tiles = {start_section, goal_section} @@ -398,9 +437,27 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) # Randomly choose between hard (E) and soft (e) enable button button_type = random.choice(["E", "e"]) - # Place button on a random available tile in this section - button_r, button_c = random.choice(available_tiles) - grid[button_r][button_c] = f"{button_type}{bridge_id}" + # Place button with constraint checking + special_tiles = get_special_tiles_in_section(grid, button_section_info) + random.shuffle(available_tiles) + button_placed = False + for button_r, button_c in available_tiles: + button_r_rel = button_r - button_section_info['row_start'] + button_c_rel = button_c - button_section_info['col_start'] + # Check against all existing special tiles in this section + valid = True + for special_r, special_c, _ in special_tiles: + if not valid_position_pair(button_r_rel, button_c_rel, special_r, special_c, rows, cols): + valid = False + break + if valid: + grid[button_r][button_c] = f"{button_type}{bridge_id}" + button_placed = True + break + + if not button_placed: + all_buttons_placed = False + break # Once button is placed, both sections connected by this bridge become accessible accessible_sections.update([section_a, section_b]) @@ -458,9 +515,27 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) # Randomly choose between hard (D) and soft (d) disable button disable_button_type = random.choice(["D", "d"]) - # Place disable button - disable_r, disable_c = random.choice(available_tiles) - grid[disable_r][disable_c] = f"{disable_button_type}{trap_bridge_id}" + # Place disable button with constraint checking + special_tiles = get_special_tiles_in_section(grid, disable_section_info) + random.shuffle(available_tiles) + disable_placed = False + for disable_r, disable_c in available_tiles: + disable_r_rel = disable_r - disable_section_info['row_start'] + disable_c_rel = disable_c - disable_section_info['col_start'] + # Check against all existing special tiles in this section + valid = True + for special_r, special_c, _ in special_tiles: + if not valid_position_pair(disable_r_rel, disable_c_rel, special_r, special_c, rows, cols): + valid = False + break + if valid: + grid[disable_r][disable_c] = f"{disable_button_type}{trap_bridge_id}" + disable_placed = True + break + + if not disable_placed: + all_buttons_placed = False + break # Mark the trap section as having a special tile sections_with_special_tiles.add(trap_section_b) From 839c0d50451acc19ccc72ff6516f7acdc4223b81 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 01:45:37 -0500 Subject: [PATCH 11/15] added main --- level_gen/dependency.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 7b546ff..afbaffd 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -637,4 +637,41 @@ def generate_dependency_problem(n, num_traps=0) -> str: while (grid := generate_dependency_grid(n, rows=3, cols=4, num_traps=num_traps)) is None: pass grid_string = "\n".join("".join(row) for row in grid) - return grid_string \ No newline at end of file + return grid_string + + +if __name__ == "__main__": + import sys + + # Parse command line arguments + total_bridges = int(sys.argv[1]) if len(sys.argv) > 1 else 8 + output_file = sys.argv[2] if len(sys.argv) > 2 else None + + # Cap at 9 total bridges + total_bridges = min(total_bridges, 9) + + # Algorithm decides split: roughly 1/3 to 1/2 should be traps for interesting puzzles + # For small numbers, ensure at least some variety + if total_bridges <= 2: + num_traps = 1 if total_bridges == 2 else 0 + num_bridges = total_bridges - num_traps + elif total_bridges <= 4: + num_traps = random.randint(1, 2) + num_bridges = total_bridges - num_traps + else: + # For larger grids, allocate 30-40% to traps + num_traps = random.randint(int(total_bridges * 0.3), int(total_bridges * 0.4)) + num_bridges = total_bridges - num_traps + + print(f"Generating grid with {total_bridges} total bridges ({num_bridges} regular, {num_traps} traps)...") + grid = generate_dependency_problem(num_bridges, num_traps) + + if output_file: + with open(output_file, 'w') as f: + f.write(grid) + print(f"Grid saved to {output_file}") + else: + print("\nGenerated Grid:") + print("=" * 70) + print(grid) + print("=" * 70) \ No newline at end of file From 871d46441215785d8cc4fd27e0b644711bb34df0 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 10:20:44 -0500 Subject: [PATCH 12/15] fixed U#/A# placement --- level_gen/dependency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index afbaffd..22e6c97 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -297,7 +297,7 @@ def generate_dependency_grid(n, rows, cols, num_traps=0, validate_solvable=True) for r in range(len(grid)): if source_section['row_start'] <= r < source_section['row_end']: if r == bridge_row: - grid[r].extend([f"U{trap_bridge_id}", f"U{trap_bridge_id}"]) + grid[r].extend([f"A{trap_bridge_id}", f"A{trap_bridge_id}"]) else: grid[r].extend([" ", " "]) grid[r].extend(["XX" for _ in range(cols)]) From ca8127b66c580ce34323c445db9c954bb42c9a0d Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 10:48:05 -0500 Subject: [PATCH 13/15] comments --- level_gen/dependency.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 22e6c97..4f53262 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -612,13 +612,8 @@ def write_grid_to_file(grid, filename): def generate_dependency_problem(n, num_traps=0) -> str: - """Generate a dependency graph problem with n bridges and return as a string. - - Args: - n: Number of regular bridges - num_traps: Number of trap sections with disable buttons (default 0) - - Note: Total bridges (n + num_traps) is capped at 9 + """ + Generate a dependency graph problem with n bridges and return as a string. """ # Enforce maximum of 9 total bridges total_bridges = n + num_traps From d850cbf8e9b072a0f02e78608a701a32ccb77f79 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 10:48:39 -0500 Subject: [PATCH 14/15] comments --- level_gen/dependency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/level_gen/dependency.py b/level_gen/dependency.py index 4f53262..fbe4145 100644 --- a/level_gen/dependency.py +++ b/level_gen/dependency.py @@ -646,7 +646,7 @@ def generate_dependency_problem(n, num_traps=0) -> str: total_bridges = min(total_bridges, 9) # Algorithm decides split: roughly 1/3 to 1/2 should be traps for interesting puzzles - # For small numbers, ensure at least some variety + # For small numbers it ensure at least some variety if total_bridges <= 2: num_traps = 1 if total_bridges == 2 else 0 num_bridges = total_bridges - num_traps From 4990824f2aa64e160e15276532bdd7ce5402a486 Mon Sep 17 00:00:00 2001 From: VDrag457 Date: Mon, 8 Dec 2025 11:05:32 -0500 Subject: [PATCH 15/15] delete test file --- test_complexity.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 test_complexity.py diff --git a/test_complexity.py b/test_complexity.py deleted file mode 100644 index e124df1..0000000 --- a/test_complexity.py +++ /dev/null @@ -1,9 +0,0 @@ -exec(open('level_gen/multi-block.py').read().split('# if __name__')[0]) - -for n in range(1, 7): - grid = generate_multi_block_grid(n) - ii_count = sum(row.count('II') for row in grid) - gg_count = sum(row.count('GG') for row in grid) - print(f'\n=== Complexity {n} ===') - print(f'Grid size: {len(grid)}x{len(grid[0])} | II: {ii_count} | GG: {gg_count} | Valid: {ii_count <= gg_count}') - print('\n'.join(''.join(row) for row in grid))