Consider number of separate overworld areas when picking a grid layout
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user