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)
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
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
def validate_layout(world, player):
if world.accessibility[player] == 'none':
return True
entrance_connectors = {
'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'],
'Kakariko Suburb Area': ['Maze Race Ledge'],
@@ -1458,7 +1455,7 @@ def validate_layout(world, player):
while unreachable_count != len(unreachable_regions):
# find 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:
region = world.get_region(region_name, player)
unreachable_regions[region_name] = region
@@ -1501,9 +1498,55 @@ def validate_layout(world, player):
if len(unreachable_regions):
return False
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 = [
#('Links House ES', 'Octoballoon WS'),
#('Links House NE', 'Lost Woods Pass SW')

View File

@@ -4,7 +4,7 @@ import RaceRandom as random
import random as _random
from typing import List, Dict, Optional, Set, Tuple
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
DRAW_IMAGE = True
@@ -155,7 +155,7 @@ class LayoutGeneratorOptions:
'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability',
'crossed_chance', 'crossed_limit',
'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__(
self,
@@ -183,7 +183,8 @@ class LayoutGeneratorOptions:
sort_by_piece_size: bool = False,
min_runs: int = 100,
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.vertical_wrap = vertical_wrap
@@ -210,6 +211,7 @@ class LayoutGeneratorOptions:
self.min_runs = min_runs
self.max_runs = max_runs
self.target_runs_times_successes = target_runs_times_successes
self.score_mult_separate_areas = score_mult_separate_areas
class LayoutGeneratorResult:
"""
@@ -1655,14 +1657,18 @@ def place_single_restriction_pieces(
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:
skip_validate_layout = world.accessibility[player] == 'none'
score_mult_separate_areas = options.score_mult_separate_areas
total_score = 0
best_score = -1000000
worst_score = 1000000
best_grid_info = None
separate_areas = None
# Pre-place pieces with single-element restriction lists
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)
logger = logging.getLogger('')
successes = 0
failures = 0
@@ -1716,21 +1722,18 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str
# Successfully placed all pieces
if options.check_reachability:
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)
# 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
valid_layout = skip_validate_layout or validate_layout(world, player)
if not valid_layout:
clean_up_connected_edges(world, player, connected_edges_cache, connected_edges)
failures += 1
continue
logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges")
successes += 1
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:
successes += 1
score = major_score
@@ -1759,6 +1762,15 @@ def get_random_layout(world: World, player: int, connected_edges_cache: List[str
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]:
prio_edges = []
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,
min_runs=100,
max_runs=10000,
target_runs_times_successes=5000
target_runs_times_successes=5000,
score_mult_separate_areas=4
)
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" Failures: {result.failures}")
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:
logger.debug("Creating layout visualization...")