Consider number of separate overworld areas when picking a grid layout

This commit is contained in:
Catobat
2026-02-21 21:19:16 +01:00
parent b8884e9010
commit 6a59f58230
2 changed files with 79 additions and 22 deletions

View File

@@ -567,7 +567,7 @@ def link_overworld(world, player):
remove_connected(forward_edge_sets, back_edge_sets) remove_connected(forward_edge_sets, back_edge_sets)
assert len(connected_edges) == len(default_connections) * 2, connected_edges assert len(connected_edges) == len(default_connections) * 2, connected_edges
valid_layout = validate_layout(world, player) valid_layout = world.accessibility[player] == 'none' or validate_layout(world, player)
tries -= 1 tries -= 1
assert valid_layout, 'Could not find a valid OW layout' assert valid_layout, 'Could not find a valid OW layout'
@@ -1369,9 +1369,6 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F
return explored_regions return explored_regions
def validate_layout(world, player): def validate_layout(world, player):
if world.accessibility[player] == 'none':
return True
entrance_connectors = { entrance_connectors = {
'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'], 'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'],
'Kakariko Suburb Area': ['Maze Race Ledge'], 'Kakariko Suburb Area': ['Maze Race Ledge'],
@@ -1458,7 +1455,7 @@ def validate_layout(world, player):
while unreachable_count != len(unreachable_regions): while unreachable_count != len(unreachable_regions):
# find unreachable regions # find unreachable regions
unreachable_regions = {} unreachable_regions = {}
for region_name in list(OWTileRegions.copy().keys()): for region_name in list(OWTileRegions.keys()):
if region_name not in explored_regions and region_name not in isolated_regions: if region_name not in explored_regions and region_name not in isolated_regions:
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
unreachable_regions[region_name] = region unreachable_regions[region_name] = region
@@ -1501,9 +1498,55 @@ def validate_layout(world, player):
if len(unreachable_regions): if len(unreachable_regions):
return False return False
return True return True
def get_separate_ow_areas(world, player):
"""
Returns a list of separated areas in the overworld layout.
It looks at the distinct connected components when only considering
OW edge and whirlpool connections (no entrances, portals, mirror, or flute).
Uses Union-Find to handle directed edges properly (treats them as undirected).
"""
parent = {}
def find(x):
if x not in parent:
parent[x] = x
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
def union(x, y):
root_x = find(x)
root_y = find(y)
if root_x != root_y:
parent[root_y] = root_x
all_regions = set(OWTileRegions.keys()) - set(isolated_regions)
considered_exit_spot_types = set(['OpenTerrain', 'OWTerrain', 'Ledge', 'OWEdge', 'Whirlpool'])
# Initialize all regions in Union-Find
for region_name in all_regions:
find(region_name)
# Build connections by examining all edges (treating directed as undirected)
for region_name in all_regions:
region = world.get_region(region_name, player)
for exit in region.exits:
if exit.spot_type in considered_exit_spot_types and exit.connected_region is not None and exit.connected_region.name in all_regions:
union(region_name, exit.connected_region.name)
# Group regions by their root
areas = {}
for region_name in all_regions:
root = find(region_name)
if root not in areas:
areas[root] = []
areas[root].append(region_name)
return list(areas.values())
test_connections = [ test_connections = [
#('Links House ES', 'Octoballoon WS'), #('Links House ES', 'Octoballoon WS'),
#('Links House NE', 'Lost Woods Pass SW') #('Links House NE', 'Lost Woods Pass SW')

View File

@@ -4,7 +4,7 @@ import RaceRandom as random
import random as _random import random as _random
from typing import List, Dict, Optional, Set, Tuple from typing import List, Dict, Optional, Set, Tuple
from BaseClasses import OWEdge, World, Direction, Terrain from BaseClasses import OWEdge, World, Direction, Terrain
from OverworldShuffle import connect_two_way, validate_layout from OverworldShuffle import connect_two_way, get_separate_ow_areas, validate_layout
ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False
DRAW_IMAGE = True DRAW_IMAGE = True
@@ -155,7 +155,7 @@ class LayoutGeneratorOptions:
'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability', 'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability',
'crossed_chance', 'crossed_limit', 'crossed_chance', 'crossed_limit',
'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size', 'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size',
'min_runs', 'max_runs', 'target_runs_times_successes') 'min_runs', 'max_runs', 'target_runs_times_successes', 'score_mult_separate_areas')
def __init__( def __init__(
self, self,
@@ -183,7 +183,8 @@ class LayoutGeneratorOptions:
sort_by_piece_size: bool = False, sort_by_piece_size: bool = False,
min_runs: int = 100, min_runs: int = 100,
max_runs: int = 10000, max_runs: int = 10000,
target_runs_times_successes: int = 5000 target_runs_times_successes: int = 5000,
score_mult_separate_areas: float = 4
): ):
self.horizontal_wrap = horizontal_wrap self.horizontal_wrap = horizontal_wrap
self.vertical_wrap = vertical_wrap self.vertical_wrap = vertical_wrap
@@ -210,6 +211,7 @@ class LayoutGeneratorOptions:
self.min_runs = min_runs self.min_runs = min_runs
self.max_runs = max_runs self.max_runs = max_runs
self.target_runs_times_successes = target_runs_times_successes self.target_runs_times_successes = target_runs_times_successes
self.score_mult_separate_areas = score_mult_separate_areas
class LayoutGeneratorResult: class LayoutGeneratorResult:
""" """
@@ -1655,14 +1657,18 @@ def place_single_restriction_pieces(
return remaining_pieces, placed_count return remaining_pieces, placed_count
def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult:
skip_validate_layout = world.accessibility[player] == 'none'
score_mult_separate_areas = options.score_mult_separate_areas
total_score = 0 total_score = 0
best_score = -1000000 best_score = -1000000
worst_score = 1000000 worst_score = 1000000
best_grid_info = None best_grid_info = None
separate_areas = None
# Pre-place pieces with single-element restriction lists # Pre-place pieces with single-element restriction lists
base_grid_info = create_empty_grid_info(0.0) base_grid_info = create_empty_grid_info(0.0)
remaining_pieces, preplaced_count = place_single_restriction_pieces(world, player, base_grid_info, options, pieces_to_place) remaining_pieces, preplaced_count = place_single_restriction_pieces(world, player, base_grid_info, options, pieces_to_place)
logger = logging.getLogger('')
successes = 0 successes = 0
failures = 0 failures = 0
@@ -1716,21 +1722,18 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str
# Successfully placed all pieces # Successfully placed all pieces
if options.check_reachability: if options.check_reachability:
disabled_count = connect_edges_for_screen_layout(world, player, grid_info, options, connected_edges, prio_edges, overworld_screens, False) disabled_count = connect_edges_for_screen_layout(world, player, grid_info, options, connected_edges, prio_edges, overworld_screens, False)
valid_layout = validate_layout(world, player) valid_layout = skip_validate_layout or validate_layout(world, player)
# Clean up connected entrances and edges
for edge_name in connected_edges:
if edge_name not in connected_edges_cache:
entrance = world.get_entrance(edge_name, player)
entrance.connected_region.entrances.remove(entrance)
entrance.connected_region = None
edge = world.get_owedge(edge_name, player)
edge.dest = None
if not valid_layout: if not valid_layout:
clean_up_connected_edges(world, player, connected_edges_cache, connected_edges)
failures += 1 failures += 1
continue continue
logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges")
successes += 1
score = -disabled_count score = -disabled_count
if score_mult_separate_areas > 0:
separate_areas = len(get_separate_ow_areas(world, player))
score -= score_mult_separate_areas * separate_areas
logger.debug("Found valid layout with " + str(disabled_count) + " disabled edges and " + str(separate_areas) + " separate areas")
clean_up_connected_edges(world, player, connected_edges_cache, connected_edges)
successes += 1
else: else:
successes += 1 successes += 1
score = major_score score = major_score
@@ -1759,6 +1762,15 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str
failures=failures failures=failures
) )
def clean_up_connected_edges(world: World, player: int, connected_edges_cache: List[str], connected_edges: List[str]) -> None:
for edge_name in connected_edges:
if edge_name not in connected_edges_cache:
entrance = world.get_entrance(edge_name, player)
entrance.connected_region.entrances.remove(entrance)
entrance.connected_region = None
edge = world.get_owedge(edge_name, player)
edge.dest = None
def get_prioritized_edges(world: World, player: int) -> List[str]: def get_prioritized_edges(world: World, player: int) -> List[str]:
prio_edges = [] prio_edges = []
if world.accessibility[player] != 'none': if world.accessibility[player] != 'none':
@@ -2080,7 +2092,8 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List
sort_by_piece_size=True, sort_by_piece_size=True,
min_runs=100, min_runs=100,
max_runs=10000, max_runs=10000,
target_runs_times_successes=5000 target_runs_times_successes=5000,
score_mult_separate_areas=4
) )
overworld_screens = initialize_screens(world, player) overworld_screens = initialize_screens(world, player)
@@ -2112,6 +2125,7 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List
logger.debug(f" Successes: {result.successes}") logger.debug(f" Successes: {result.successes}")
logger.debug(f" Failures: {result.failures}") logger.debug(f" Failures: {result.failures}")
logger.debug(f" Generation time: {elapsed_time:.3f}s") logger.debug(f" Generation time: {elapsed_time:.3f}s")
logger.debug(f" Layouts per second: {(result.successes+result.failures)/elapsed_time:.3f}")
if DRAW_IMAGE: if DRAW_IMAGE:
logger.debug("Creating layout visualization...") logger.debug("Creating layout visualization...")