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)
|
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')
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
|||||||
Reference in New Issue
Block a user