Files
alttpr-python/source/overworld/LayoutGenerator.py

1423 lines
73 KiB
Python

import copy
import logging
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
ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False
PREVENT_WRAPPED_LARGE_SCREENS = False
DRAW_IMAGE = False
large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
# ============================================================================
# DATA STRUCTURES
# ============================================================================
class Screen:
"""
Represents a game map screen.
"""
__slots__ = ('id', 'big', 'dark_world', 'parallel',
'edges', 'mixed_state')
def __init__(
self,
id: int,
big: bool = False,
dark_world: bool = False,
parallel: Optional['Screen'] = None,
edges: Optional[Dict[str, OWEdge]] = None,
mixed_state: str = "normal"
):
self.id = id
self.big = big
self.dark_world = dark_world
self.parallel = parallel
self.edges = edges if edges is not None else {}
self.mixed_state = mixed_state # "normal" or "swapped"
class WorldPiece:
"""
Represents a piece within a world containing screens to be placed on the grid.
"""
__slots__ = ('screens', 'grid', 'width', 'height', 'north_edges', 'south_edges',
'west_edges', 'east_edges', 'north_edges_water', 'south_edges_water',
'west_edges_water', 'east_edges_water')
def __init__(
self,
screens: List[List[Optional[Screen]]],
grid: Optional[List[List[int]]] = None,
width: int = 0,
height: int = 0,
north_edges: Optional[List[List[List[OWEdge]]]] = None,
south_edges: Optional[List[List[List[OWEdge]]]] = None,
west_edges: Optional[List[List[List[OWEdge]]]] = None,
east_edges: Optional[List[List[List[OWEdge]]]] = None,
north_edges_water: Optional[List[List[List[OWEdge]]]] = None,
south_edges_water: Optional[List[List[List[OWEdge]]]] = None,
west_edges_water: Optional[List[List[List[OWEdge]]]] = None,
east_edges_water: Optional[List[List[List[OWEdge]]]] = None
):
self.screens = screens
self.grid = grid if grid is not None else []
self.width = width
self.height = height
self.north_edges = north_edges if north_edges is not None else []
self.south_edges = south_edges if south_edges is not None else []
self.west_edges = west_edges if west_edges is not None else []
self.east_edges = east_edges if east_edges is not None else []
self.north_edges_water = north_edges_water if north_edges_water is not None else []
self.south_edges_water = south_edges_water if south_edges_water is not None else []
self.west_edges_water = west_edges_water if west_edges_water is not None else []
self.east_edges_water = east_edges_water if east_edges_water is not None else []
class Piece:
"""
Represents a piece consisting of a main and optionally a parallel world piece.
"""
__slots__ = ('main', 'parallel', 'world', 'width', 'height',
'invalid_wrap_row', 'invalid_wrap_column', 'restriction',
'crossed_groups', 'delay', 'order', 'edge_sides', 'max_edges_per_side')
def __init__(
self,
main: WorldPiece,
parallel: Optional[WorldPiece] = None,
world: int = 0,
width: int = 0,
height: int = 0,
invalid_wrap_row: Optional[List[int]] = None,
invalid_wrap_column: Optional[List[int]] = None,
restriction: Optional[List[int]] = None,
crossed_groups: Optional[List[List[int]]] = None,
delay: int = 0,
order: float = 0.0,
edge_sides: int = 0,
max_edges_per_side: int = 0
):
self.main = main
self.parallel = parallel
self.world = world # 0 or 1
self.width = width
self.height = height
self.invalid_wrap_row = invalid_wrap_row if invalid_wrap_row is not None else []
self.invalid_wrap_column = invalid_wrap_column if invalid_wrap_column is not None else []
self.restriction = restriction
self.crossed_groups = crossed_groups if crossed_groups is not None else []
self.delay = delay
self.order = order
self.edge_sides = edge_sides
self.max_edges_per_side = max_edges_per_side
class GridInfo:
"""
Container for grid layout information during placement runs.
Stores screen IDs and edge information for both Light and Dark worlds.
"""
__slots__ = (
'grid', 'north_edges_grid', 'south_edges_grid', 'west_edges_grid', 'east_edges_grid',
'north_edges_water_grid', 'south_edges_water_grid', 'west_edges_water_grid', 'east_edges_water_grid',
'crossed_groups', 'edge_connection_seed'
)
def __init__(
self,
grid: List[List[List[int]]],
north_edges_grid: List[List[List[List[OWEdge]]]],
south_edges_grid: List[List[List[List[OWEdge]]]],
west_edges_grid: List[List[List[List[OWEdge]]]],
east_edges_grid: List[List[List[List[OWEdge]]]],
north_edges_water_grid: List[List[List[List[OWEdge]]]],
south_edges_water_grid: List[List[List[List[OWEdge]]]],
west_edges_water_grid: List[List[List[List[OWEdge]]]],
east_edges_water_grid: List[List[List[List[OWEdge]]]],
crossed_groups: List[List[int]],
edge_connection_seed: float
):
self.grid = grid
self.north_edges_grid = north_edges_grid
self.south_edges_grid = south_edges_grid
self.west_edges_grid = west_edges_grid
self.east_edges_grid = east_edges_grid
self.north_edges_water_grid = north_edges_water_grid
self.south_edges_water_grid = south_edges_water_grid
self.west_edges_water_grid = west_edges_water_grid
self.east_edges_water_grid = east_edges_water_grid
self.crossed_groups = crossed_groups
self.edge_connection_seed = edge_connection_seed
class LayoutGeneratorOptions:
"""
Configuration options for layout generation.
"""
__slots__ = ('horizontal_wrap', 'vertical_wrap',
'large_screen_pool', 'distortion_chance', 'random_order',
'multi_choice', 'max_delay', 'first_ignore_bonus_points',
'penalty_full_edge_mismatch', 'penalty_partial_edge_mismatch', 'bonus_partial_edge_match',
'bonus_full_edge_match', 'bonus_crossed_group_match', 'bonus_fill_parallel',
'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')
def __init__(
self,
horizontal_wrap: bool = True,
vertical_wrap: bool = True,
large_screen_pool: bool = False,
distortion_chance: float = 0.0,
random_order: int = 0,
multi_choice: int = 1,
max_delay: int = 10,
first_ignore_bonus_points: int = 0,
penalty_full_edge_mismatch: float = 1,
penalty_partial_edge_mismatch: float = 0,
bonus_partial_edge_match: float = 1,
bonus_full_edge_match: float = 1,
bonus_crossed_group_match: float = 1,
bonus_fill_parallel: float = 0,
forced_non_crossed_edges: Set[str] = [],
forced_crossed_edges: Set[str] = [],
crossed_chance: float = 0.5,
crossed_limit: int = -1,
check_reachability: bool = True,
sort_by_edge_sides: bool = False,
sort_by_max_edges_per_side: bool = False,
sort_by_piece_size: bool = False,
min_runs: int = 100,
max_runs: int = 10000,
target_runs_times_successes: int = 5000
):
self.horizontal_wrap = horizontal_wrap
self.vertical_wrap = vertical_wrap
self.large_screen_pool = large_screen_pool
self.distortion_chance = distortion_chance
self.random_order = random_order
self.multi_choice = multi_choice
self.max_delay = max_delay
self.first_ignore_bonus_points = first_ignore_bonus_points
self.penalty_full_edge_mismatch = penalty_full_edge_mismatch
self.penalty_partial_edge_mismatch = penalty_partial_edge_mismatch
self.bonus_partial_edge_match = bonus_partial_edge_match
self.bonus_full_edge_match = bonus_full_edge_match
self.bonus_crossed_group_match = bonus_crossed_group_match
self.bonus_fill_parallel = bonus_fill_parallel
self.forced_non_crossed_edges = forced_non_crossed_edges
self.forced_crossed_edges = forced_crossed_edges
self.check_reachability = check_reachability
self.crossed_chance = crossed_chance
self.crossed_limit = crossed_limit
self.sort_by_edge_sides = sort_by_edge_sides
self.sort_by_max_edges_per_side = sort_by_max_edges_per_side
self.sort_by_piece_size = sort_by_piece_size
self.min_runs = min_runs
self.max_runs = max_runs
self.target_runs_times_successes = target_runs_times_successes
class LayoutGeneratorResult:
"""
Result object for the layout generation.
"""
__slots__ = ('grid_info', 'score', 'worst_score', 'average_score', 'successes', 'failures')
def __init__(
self,
grid_info: Optional[GridInfo] = None,
score: int = 0,
worst_score: int = 0,
average_score: float = 0.0,
successes: int = 0,
failures: int = 0
):
self.grid_info = grid_info
self.score = score
self.worst_score = worst_score
self.average_score = average_score
self.successes = successes
self.failures = failures
class PiecePlacementResult:
"""
Result object for the layout generator placement operations.
"""
__slots__ = ('success', 'piece', 'score_major', 'score_minor')
def __init__(
self,
success: bool = False,
piece: Optional[Piece] = None,
score_major: float = 0,
score_minor: float = 0
):
self.success = success
self.piece = piece
self.score_major = score_major
self.score_minor = score_minor
# ============================================================================
# GRID INITIALIZATION
# ============================================================================
def create_empty_grid_info(edge_connection_seed: float) -> GridInfo:
return GridInfo(
grid=[[[-1] * 8 for _ in range(8)] for _ in range(2)],
north_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
south_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
west_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
east_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
north_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
south_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
west_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
east_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)],
crossed_groups=[[-1] * 8 for _ in range(8)],
edge_connection_seed=edge_connection_seed
)
def initialize_screens(world: World, player: int) -> Dict[int, Screen]:
overworld_screens: Dict[int, Screen] = {}
screen_edges_map = group_owedges_by_screens(world, player)
for screen_id in range(0x80):
if screen_id - 0x01 not in large_screen_ids and screen_id - 0x08 not in large_screen_ids and screen_id - 0x09 not in large_screen_ids:
is_vanilla_dark = screen_id >= 0x40
is_big = screen_id in large_screen_ids
is_flipped = world.owMixed[player] and screen_id in world.owswaps[player][0]
screen = Screen(
id=screen_id,
big=is_big,
dark_world=not is_vanilla_dark if is_flipped else is_vanilla_dark,
mixed_state="swapped" if is_flipped else "normal"
)
if screen_id in screen_edges_map:
for edge in screen_edges_map[screen_id]:
screen.edges[edge.name] = edge
overworld_screens[screen_id] = screen
for light_id in range(0x40):
dark_id = light_id + 0x40
if light_id in overworld_screens:
overworld_screens[light_id].parallel = overworld_screens[dark_id]
overworld_screens[dark_id].parallel = overworld_screens[light_id]
return overworld_screens
def group_owedges_by_screens(world: World, player: int) -> Dict[int, List[OWEdge]]:
screen_edges: Dict[int, List[OWEdge]] = {}
edges: List[OWEdge] = world.owedges
for edge in edges:
# Skip edges that lead to/from special screens
if edge.player == player and not edge.specialEntrance and not edge.specialExit:
owIndex = edge.owIndex
if owIndex not in screen_edges:
screen_edges[owIndex] = []
screen_edges[owIndex].append(edge)
return screen_edges
def initialize_large_screen_data(overworld_screens: Dict[int, Screen]) -> Tuple[Dict[int, Dict], Dict[int, Dict], Dict[int, Dict]]:
i: Dict[int, Dict] = {}
il: Dict[int, Dict] = {}
iw: Dict[int, Dict] = {}
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x00, [], [], [], [], ["Lost Woods EN"], [], ["Lost Woods SW", "Lost Woods SC"], ["Lost Woods SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x40, [], [], [], [], ["Skull Woods EN"], [], ["Skull Woods SW", "Skull Woods SC"], ["Skull Woods SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x03, [], [], [], [], ["West Death Mountain EN"], ["West Death Mountain ES"], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x43, [], [], [], [], ["West Dark Death Mountain EN"], ["West Dark Death Mountain ES"], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x05, [], [], ["East Death Mountain WN"], ["East Death Mountain WS"], ["East Death Mountain EN"], [], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x45, [], [], ["East Dark Death Mountain WN"], ["East Dark Death Mountain WS"], ["East Dark Death Mountain EN"], [], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x18, ["Kakariko NW", "Kakariko NC"], ["Kakariko NE"], [], [], [], ["Kakariko ES"], [], ["Kakariko SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x58, ["Village of Outcasts NW", "Village of Outcasts NC"], ["Village of Outcasts NE"], [], [], [], ["Village of Outcasts ES"], [], ["Village of Outcasts SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1B, [], [], ["Hyrule Castle WN"], [], [], ["Hyrule Castle ES"], ["Hyrule Castle SW"], ["Hyrule Castle SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5B, [], [], [], [], [], ["Pyramid ES"], ["Pyramid SW"], ["Pyramid SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1E, [], [], [], [], [], [], ["Eastern Palace SW"], ["Eastern Palace SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5E, [], [], [], [], [], [], ["Palace of Darkness SW"], ["Palace of Darkness SE"])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x30, [], [], [], [], [], ["Desert EC", "Desert ES"], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x70, [], [], [], [], [], [], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x35, ["Lake Hylia NW"], ["Lake Hylia NC", "Lake Hylia NE"], [], ["Lake Hylia WS"], [], ["Lake Hylia EC", "Lake Hylia ES"], [], [])
define_large_screen_quadrants(overworld_screens, i, il, iw, 0x75, ["Ice Lake NW"], ["Ice Lake NC", "Ice Lake NE"], [], ["Ice Lake WS"], [], ["Ice Lake EC", "Ice Lake ES"], [], [])
return (i, il, iw)
def define_large_screen_quadrants(
overworld_screens: Dict[int, Screen],
large_screen_quadrant_info: Dict[int, Dict],
large_screen_quadrant_info_land: Dict[int, Dict],
large_screen_quadrant_info_water: Dict[int, Dict],
screen_id: int,
north1: List[str], north2: List[str],
west1: List[str], west2: List[str],
east1: List[str], east2: List[str],
south1: List[str], south2: List[str]
) -> None:
"""
Define edge info for large screens
Maps edge names to quadrants (NW, NE, SW, SE)
Edge names are the actual edge names from OWEdges.py like "Lost Woods SW", "Kakariko NW", etc.
"""
edges = overworld_screens[screen_id].edges
info = {
"NW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []},
"NE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []},
"SW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []},
"SE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}
}
info["NW"][Direction.North] = [edges[name] for name in north1]
info["NE"][Direction.North] = [edges[name] for name in north2]
info["NW"][Direction.West] = [edges[name] for name in west1]
info["SW"][Direction.West] = [edges[name] for name in west2]
info["NE"][Direction.East] = [edges[name] for name in east1]
info["SE"][Direction.East] = [edges[name] for name in east2]
info["SW"][Direction.South] = [edges[name] for name in south1]
info["SE"][Direction.South] = [edges[name] for name in south2]
large_screen_quadrant_info[screen_id] = info
large_screen_quadrant_info_land[screen_id] = {
"NW": {}, "NE": {}, "SW": {}, "SE": {}
}
large_screen_quadrant_info_water[screen_id] = {
"NW": {}, "NE": {}, "SW": {}, "SE": {}
}
for quadrant_name in ["NW", "NE", "SW", "SE"]:
for direction in [Direction.North, Direction.West, Direction.East, Direction.South]:
large_screen_quadrant_info_land[screen_id][quadrant_name][direction] = \
[edge for edge in info[quadrant_name][direction] if edge.terrain != Terrain.Water]
large_screen_quadrant_info_water[screen_id][quadrant_name][direction] = \
[edge for edge in info[quadrant_name][direction] if edge.terrain == Terrain.Water]
# ============================================================================
# PIECE CREATION
# ============================================================================
def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions, crossed_group_b: List[int], overworld_screens: Dict[int, Screen], large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> List[Piece]:
piece_list: List[Piece] = []
used_screens_set = set()
all_large_screens = [s for s in overworld_screens.values() if s.big]
all_small_screens = [s for s in overworld_screens.values() if not s.big]
if world.owParallel[player]:
# In Parallel, only use light world screens
# Each piece will automatically handle both worlds through parallel mechanism
all_large_screens = [s for s in all_large_screens if not s.dark_world]
all_small_screens = [s for s in all_small_screens if not s.dark_world]
# In Standard mode, screens 0x1B, 0x2B, 0x2C are glued together as a single piece
if world.mode[player] == 'standard':
castle_screen = overworld_screens.get(0x1B)
central_bonk_screen = overworld_screens.get(0x2B)
links_house_screen = overworld_screens.get(0x2C)
if castle_screen and central_bonk_screen and links_house_screen:
piece = create_piece(world, player, [
[0x1B, 0x1B],
[0x1B, 0x1B],
[0x2B, 0x2C]
], overworld_screens)
if options.large_screen_pool:
piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]
piece_list.append(piece)
used_screens_set.add(castle_screen)
used_screens_set.add(central_bonk_screen)
used_screens_set.add(links_house_screen)
if world.owParallel[player]:
used_screens_set.add(castle_screen.parallel)
used_screens_set.add(central_bonk_screen.parallel)
used_screens_set.add(links_house_screen.parallel)
# Add large screens
for screen in all_large_screens:
if screen not in used_screens_set:
piece = create_piece(world, player, [[screen.id, screen.id], [screen.id, screen.id]], overworld_screens)
if options.large_screen_pool:
piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]
piece_list.append(piece)
used_screens_set.add(screen)
if world.owParallel[player]:
used_screens_set.add(screen.parallel)
# Add small screens
for screen in all_small_screens:
if screen not in used_screens_set:
piece = create_piece(world, player, [[screen.id]], overworld_screens)
if options.large_screen_pool:
piece.restriction = [s.id for s in overworld_screens.values() if not s.big]
piece_list.append(piece)
used_screens_set.add(screen)
if world.owParallel[player]:
used_screens_set.add(screen.parallel)
# Add piece data
for piece in piece_list:
add_piece_data(world, player, piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water)
# Handle crossed groups
if world.owCrossed[player] == 'polar' and world.owMixed[player]:
piece.crossed_groups = [[] for _ in range(8)]
for k in range(piece.height):
for l in range(piece.width):
piece.crossed_groups[k].append(-1)
screen = piece.main.screens[k][l]
if screen:
piece.crossed_groups[k][l] = 1 if screen.mixed_state == "swapped" else 0
else:
if piece.parallel and piece.parallel.screens[k][l]:
piece.crossed_groups[k][l] = 1 if piece.parallel.screens[k][l].mixed_state == "swapped" else 0
if world.owCrossed[player] == 'grouped':
piece.crossed_groups = [[] for _ in range(8)]
for k in range(piece.height):
for l in range(piece.width):
piece.crossed_groups[k].append(-1)
screen_id = piece.main.grid[k][l]
if screen_id != -1:
piece.crossed_groups[k][l] = 1 if screen_id in crossed_group_b else 0
else:
if piece.parallel and piece.parallel.screens[k][l]:
piece.crossed_groups[k][l] = 1 if piece.parallel.grid[k][l] in crossed_group_b else 0
return piece_list
def create_piece(world: World, player: int, grid: List[List[int]], overworld_screens: Dict[int, Screen]) -> Piece:
"""
Create piece from grid of screen IDs
Takes 2D array of screen IDs and creates main and parallel pieces
"""
piece = Piece(
main=WorldPiece(screens=[]),
width=len(grid[0]),
height=len(grid)
)
if world.owParallel[player]:
piece.parallel = WorldPiece(screens=[])
found_screens = set()
for i in range(piece.height):
new_row = []
new_row_parallel = []
piece.main.screens.append(new_row)
if world.owParallel[player]:
piece.parallel.screens.append(new_row_parallel)
for j in range(piece.width):
screen = overworld_screens.get(grid[i][j])
new_row.append(screen)
if world.owParallel[player]:
new_row_parallel.append(screen.parallel if screen else None)
if screen and screen not in found_screens:
found_screens.add(screen)
piece.world = 1 if screen.dark_world else 0
if screen.big and PREVENT_WRAPPED_LARGE_SCREENS:
# For large screens, prevent wrapping at the second row/column
# This ensures the 2x2 piece doesn't split across the grid boundary
if (i + 1) not in piece.invalid_wrap_row:
piece.invalid_wrap_row.append(i + 1)
if (j + 1) not in piece.invalid_wrap_column:
piece.invalid_wrap_column.append(j + 1)
return piece
def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None:
"""
Add computed data to piece
Calls add_piece_grid_info for main and parallel pieces
"""
num_pieces = 2 if piece.parallel else 1
for p in range(num_pieces):
world_piece = piece.main if p == 0 else piece.parallel
world_piece.width = len(world_piece.screens[0])
world_piece.height = len(world_piece.screens)
add_piece_grid_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water)
piece.width = piece.main.width
piece.height = piece.main.height
# Calculate edge_sides and max_edges_per_side: 0 for multi-cell pieces
if piece.width == 1 and piece.height == 1:
edge_sides = 0
max_edges_per_side = 0
# Count edge sides and max edges for main piece and parallel piece (if exists)
for world_piece in ([piece.main, piece.parallel] if piece.parallel else [piece.main]):
north_count = len(world_piece.north_edges[0][0]) + (len(world_piece.north_edges_water[0][0]) if world_piece.north_edges_water else 0)
if north_count > 0:
edge_sides += 1
max_edges_per_side = max(max_edges_per_side, north_count)
south_count = len(world_piece.south_edges[0][0]) + (len(world_piece.south_edges_water[0][0]) if world_piece.south_edges_water else 0)
if south_count > 0:
edge_sides += 1
max_edges_per_side = max(max_edges_per_side, south_count)
west_count = len(world_piece.west_edges[0][0]) + (len(world_piece.west_edges_water[0][0]) if world_piece.west_edges_water else 0)
if west_count > 0:
edge_sides += 1
max_edges_per_side = max(max_edges_per_side, west_count)
east_count = len(world_piece.east_edges[0][0]) + (len(world_piece.east_edges_water[0][0]) if world_piece.east_edges_water else 0)
if east_count > 0:
edge_sides += 1
max_edges_per_side = max(max_edges_per_side, east_count)
piece.edge_sides = edge_sides
piece.max_edges_per_side = max_edges_per_side
else:
piece.edge_sides = 0
piece.max_edges_per_side = 0
def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None:
"""
Populate piece edge information
Initializes 8x8 edge arrays and extracts edges from screens
"""
piece.grid = [[] for _ in range(8)]
piece.north_edges = [[] for _ in range(8)]
piece.south_edges = [[] for _ in range(8)]
piece.west_edges = [[] for _ in range(8)]
piece.east_edges = [[] for _ in range(8)]
if not world.owTerrain[player]:
piece.north_edges_water = [[] for _ in range(8)]
piece.south_edges_water = [[] for _ in range(8)]
piece.west_edges_water = [[] for _ in range(8)]
piece.east_edges_water = [[] for _ in range(8)]
for k in range(piece.height):
for l in range(piece.width):
piece.grid[k].append(piece.screens[k][l].id if piece.screens[k][l] else -1)
piece.north_edges[k].append([])
piece.south_edges[k].append([])
piece.west_edges[k].append([])
piece.east_edges[k].append([])
if not world.owTerrain[player]:
piece.north_edges_water[k].append([])
piece.south_edges_water[k].append([])
piece.west_edges_water[k].append([])
piece.east_edges_water[k].append([])
done_large = set()
for k in range(piece.height):
for l in range(piece.width):
screen = piece.screens[k][l]
if not screen:
continue
if screen.big:
if screen.id not in done_large:
done_large.add(screen.id)
quadrant_info = (large_screen_quadrant_info[screen.id] if world.owTerrain[player]
else large_screen_quadrant_info_land[screen.id])
piece.north_edges[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest]
piece.north_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest]
piece.south_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest]
piece.south_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest]
piece.west_edges[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest]
piece.west_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest]
piece.east_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest]
piece.east_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest]
if not world.owTerrain[player]:
quadrant_info = large_screen_quadrant_info_water[screen.id]
piece.north_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest]
piece.north_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest]
piece.south_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest]
piece.south_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest]
piece.west_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest]
piece.west_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest]
piece.east_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest]
piece.east_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest]
else:
for edge in sorted(screen.edges.values(), key=lambda e: e.midpoint):
if not edge.dest:
if edge.direction == Direction.North:
target = piece.north_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.north_edges_water[k][l]
target.append(edge)
elif edge.direction == Direction.South:
target = piece.south_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.south_edges_water[k][l]
target.append(edge)
elif edge.direction == Direction.West:
target = piece.west_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.west_edges_water[k][l]
target.append(edge)
elif edge.direction == Direction.East:
target = piece.east_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.east_edges_water[k][l]
target.append(edge)
# ============================================================================
# PLACEMENT ALGORITHM
# ============================================================================
def random_place_piece(
world: World,
player: int,
grid_info: GridInfo,
options: LayoutGeneratorOptions,
pieces: List[Piece],
ignore_bonus_points: bool
) -> PiecePlacementResult:
"""
Core placement algorithm
Evaluates all valid positions and scores each based on edge compatibility
Performance is critical within these deeply nested loops, every optimization matters
"""
use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped'
is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted'
keep_similar = ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player]
width = 8
height = 8
horizontal_wrap = options.horizontal_wrap
vertical_wrap = options.vertical_wrap
distortion_chance = options.distortion_chance
use_distortion = distortion_chance > 0
crossed_chance = options.crossed_chance
crossworld_weights = (1 - crossed_chance, crossed_chance) if is_unrestricted_crossed else (1, 0)
if not is_unrestricted_crossed:
crossed_score_weight = 1
penalty_full_edge_mismatch = options.penalty_full_edge_mismatch
penalty_partial_edge_mismatch = options.penalty_partial_edge_mismatch
bonus_partial_edge_match = 0 if ignore_bonus_points else options.bonus_partial_edge_match
bonus_full_edge_match = 0 if ignore_bonus_points else options.bonus_full_edge_match
bonus_crossed_group_match = 0 if ignore_bonus_points else options.bonus_crossed_group_match
bonus_fill_parallel = 0 if ignore_bonus_points else options.bonus_fill_parallel
can_stop_early = penalty_full_edge_mismatch >= 0 and penalty_partial_edge_mismatch >= 0
grid = grid_info.grid
crossed_groups = grid_info.crossed_groups
north_edges_grid = grid_info.north_edges_grid
south_edges_grid = grid_info.south_edges_grid
west_edges_grid = grid_info.west_edges_grid
east_edges_grid = grid_info.east_edges_grid
north_edges_water_grid = grid_info.north_edges_water_grid
south_edges_water_grid = grid_info.south_edges_water_grid
west_edges_water_grid = grid_info.west_edges_water_grid
east_edges_water_grid = grid_info.east_edges_water_grid
best_choices = []
max_score_major = -1000000
max_score_minor = -1000000
for c, piece in enumerate(pieces):
piece_main = piece.main
piece_parallel = piece.parallel
wrld = piece.world
invalid_wrap_row = piece.invalid_wrap_row
invalid_wrap_column = piece.invalid_wrap_column
restriction = piece.restriction
piece_width = piece.width
piece_height = piece.height
piece_crossed_groups = piece.crossed_groups
grid_main_world = grid[wrld]
grid_other_world = grid[1 - wrld]
i_range = height if vertical_wrap else height - piece_height + 1
for i in range(i_range):
if i >= height - piece_height + 1 and (height - i) in invalid_wrap_row:
continue
j_range = width if horizontal_wrap else width - piece_width + 1
for j in range(j_range):
if j >= width - piece_width + 1 and (width - j) in invalid_wrap_column:
continue
if restriction and (i * 8 + j) not in restriction:
continue
# Check for overlap
overlap = False
for k in range(piece_height):
row_idx = (i + k) % height
for l in range(piece_width):
col_idx = (j + l) % width
if grid_main_world[row_idx][col_idx] != -1 and piece_main.screens[k][l]:
overlap = True
break
if use_crossed_groups and crossed_groups[row_idx][col_idx] != -1 and crossed_groups[row_idx][col_idx] != piece_crossed_groups[k][l]:
overlap = True
break
if piece_parallel and grid_other_world[row_idx][col_idx] != -1 and piece_parallel.screens[k][l]:
overlap = True
break
if not overlap:
score_major = 0
score_minor = 0
# Calculate scores based on edge compatibility
for k in range(piece_height):
row_idx = (i + k) % height
row_above = (i + k + height - 1) % height
row_below = (i + k + 1) % height
i_plus_k = i + k
for l in range(piece_width):
col_idx = (j + l) % width
col_left = (j + l + width - 1) % width
col_right = (j + l + 1) % width
j_plus_l = j + l
num_pieces = 2 if piece_parallel else 1
for p in range(num_pieces):
world_piece = piece_main if p == 0 else piece_parallel
cw = wrld if p == 0 else 1 - wrld
if not world_piece.screens[k][l]:
continue
# Add small bias when the crossed group is already determined and matches the piece to avoid issues later on
if use_crossed_groups and not piece_parallel and crossed_groups[row_idx][col_idx] == piece_crossed_groups[k][l]:
score_minor += bonus_crossed_group_match
if not piece_parallel and grid_other_world[row_idx][col_idx] != -1:
score_minor += bonus_fill_parallel
for terrain in range(1 if world.owTerrain[player] else 2):
north_piece = world_piece.north_edges if terrain == 0 else world_piece.north_edges_water
south_piece = world_piece.south_edges if terrain == 0 else world_piece.south_edges_water
west_piece = world_piece.west_edges if terrain == 0 else world_piece.west_edges_water
east_piece = world_piece.east_edges if terrain == 0 else world_piece.east_edges_water
north_edges = north_edges_grid if terrain == 0 else north_edges_water_grid
south_edges = south_edges_grid if terrain == 0 else south_edges_water_grid
west_edges = west_edges_grid if terrain == 0 else west_edges_water_grid
east_edges = east_edges_grid if terrain == 0 else east_edges_water_grid
# Check boundary edges
if not vertical_wrap and i_plus_k == 0 and (not use_distortion or distortion_chance <= random.random()):
if north_piece[k][l]:
score_major -= penalty_full_edge_mismatch
else:
score_minor += bonus_full_edge_match
if not vertical_wrap and i_plus_k == height - 1 and (not use_distortion or distortion_chance <= random.random()):
if south_piece[k][l]:
score_major -= penalty_full_edge_mismatch
else:
score_minor += bonus_full_edge_match
if not horizontal_wrap and j_plus_l == 0 and (not use_distortion or distortion_chance <= random.random()):
if west_piece[k][l]:
score_major -= penalty_full_edge_mismatch
else:
score_minor += bonus_full_edge_match
if not horizontal_wrap and j_plus_l == width - 1 and (not use_distortion or distortion_chance <= random.random()):
if east_piece[k][l]:
score_major -= penalty_full_edge_mismatch
else:
score_minor += bonus_full_edge_match
for other_world_index in range(2 if is_unrestricted_crossed else 1):
# Check neighbor compatibility (north)
if is_unrestricted_crossed:
w = cw if other_world_index == 0 else 1 - cw
crossed_score_weight = crossworld_weights[other_world_index]
elif use_crossed_groups and crossed_groups[row_above][col_idx] != piece_crossed_groups[k][l]:
w = 1 - cw
else:
w = cw
if (i_plus_k != 0 or vertical_wrap) and grid[w][row_above][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()):
piece_edges = len(north_piece[k][l])
grid_edges = len(south_edges[w][row_above][col_idx])
if piece_edges == grid_edges:
score_minor += bonus_full_edge_match * crossed_score_weight
elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)):
score_minor += bonus_partial_edge_match * crossed_score_weight
score_major -= penalty_partial_edge_mismatch * crossed_score_weight
else:
score_major -= penalty_full_edge_mismatch * crossed_score_weight
# Check south neighbor
if is_unrestricted_crossed:
w = cw if other_world_index == 0 else 1 - cw
crossed_score_weight = crossworld_weights[other_world_index]
elif use_crossed_groups and crossed_groups[row_below][col_idx] != piece_crossed_groups[k][l]:
w = 1 - cw
else:
w = cw
if (i_plus_k != height - 1 or vertical_wrap) and grid[w][row_below][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()):
piece_edges = len(south_piece[k][l])
grid_edges = len(north_edges[w][row_below][col_idx])
if piece_edges == grid_edges:
score_minor += bonus_full_edge_match * crossed_score_weight
elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)):
score_minor += bonus_partial_edge_match * crossed_score_weight
score_major -= penalty_partial_edge_mismatch * crossed_score_weight
else:
score_major -= penalty_full_edge_mismatch * crossed_score_weight
# Check west neighbor
if is_unrestricted_crossed:
w = cw if other_world_index == 0 else 1 - cw
crossed_score_weight = crossworld_weights[other_world_index]
elif use_crossed_groups and crossed_groups[row_idx][col_left] != piece_crossed_groups[k][l]:
w = 1 - cw
else:
w = cw
if (j_plus_l != 0 or horizontal_wrap) and grid[w][row_idx][col_left] != -1 and (not use_distortion or distortion_chance <= random.random()):
piece_edges = len(west_piece[k][l])
grid_edges = len(east_edges[w][row_idx][col_left])
if piece_edges == grid_edges:
score_minor += bonus_full_edge_match * crossed_score_weight
elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)):
score_minor += bonus_partial_edge_match * crossed_score_weight
score_major -= penalty_partial_edge_mismatch * crossed_score_weight
else:
score_major -= penalty_full_edge_mismatch * crossed_score_weight
# Check east neighbor
if is_unrestricted_crossed:
w = cw if other_world_index == 0 else 1 - cw
crossed_score_weight = crossworld_weights[other_world_index]
elif use_crossed_groups and crossed_groups[row_idx][col_right] != piece_crossed_groups[k][l]:
w = 1 - cw
else:
w = cw
if (j_plus_l != width - 1 or horizontal_wrap) and grid[w][row_idx][col_right] != -1 and (not use_distortion or distortion_chance <= random.random()):
piece_edges = len(east_piece[k][l])
grid_edges = len(west_edges[w][row_idx][col_right])
if piece_edges == grid_edges:
score_minor += bonus_full_edge_match * crossed_score_weight
elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)):
score_minor += bonus_partial_edge_match * crossed_score_weight
score_major -= penalty_partial_edge_mismatch * crossed_score_weight
else:
score_major -= penalty_full_edge_mismatch * crossed_score_weight
if can_stop_early and score_major < max_score_major:
break
# This is so an we can break out of all remaining checks for the current placement option
else:
continue
break
else:
continue
break
else:
continue
break
else:
continue
break
if score_major == max_score_major and score_minor == max_score_minor:
best_choices.append((c, i, j))
if score_major > max_score_major or (score_major == max_score_major and score_minor > max_score_minor):
max_score_major = score_major
max_score_minor = score_minor
best_choices = [(c, i, j)]
if not best_choices:
return PiecePlacementResult(success=False, piece=None, score_major=0, score_minor=0)
# Select random best choice
piece_index, row, column = random.choice(best_choices)
used_score_major = max_score_major
used_score_minor = max_score_minor
piece = pieces[piece_index]
wrld = piece.world
# Place the piece on the grid
for k in range(piece.height):
row_idx = (row + k) % height
for l in range(piece.width):
col_idx = (column + l) % width
num_pieces = 2 if piece.parallel else 1
for p in range(num_pieces):
world_piece = piece.main if p == 0 else piece.parallel
w = wrld if p == 0 else 1 - wrld
grid[w][row_idx][col_idx] = world_piece.grid[k][l]
north_edges_grid[w][row_idx][col_idx] = world_piece.north_edges[k][l]
south_edges_grid[w][row_idx][col_idx] = world_piece.south_edges[k][l]
west_edges_grid[w][row_idx][col_idx] = world_piece.west_edges[k][l]
east_edges_grid[w][row_idx][col_idx] = world_piece.east_edges[k][l]
if not world.owTerrain[player]:
north_edges_water_grid[w][row_idx][col_idx] = world_piece.north_edges_water[k][l]
south_edges_water_grid[w][row_idx][col_idx] = world_piece.south_edges_water[k][l]
west_edges_water_grid[w][row_idx][col_idx] = world_piece.west_edges_water[k][l]
east_edges_water_grid[w][row_idx][col_idx] = world_piece.east_edges_water[k][l]
if use_crossed_groups:
crossed_groups[row_idx][col_idx] = piece_crossed_groups[k][l]
return PiecePlacementResult(success=True, piece=piece, score_major=used_score_major, score_minor=used_score_minor)
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:
total_score = 0
best_score = -1000000
worst_score = 1000000
best_grid_info = None
successes = 0
failures = 0
run = 0
while run < options.min_runs or (run * successes < options.target_runs_times_successes and run < options.max_runs):
run += 1
connected_edges = connected_edges_cache.copy()
piece_list = pieces_to_place.copy()
grid_info = create_empty_grid_info(random.random())
for piece in piece_list:
piece.delay = 0
major_score = 0
# Order pieces by size, max_edges_per_side, edge_sides, and randomness
random.shuffle(piece_list)
if options.sort_by_edge_sides:
piece_list.sort(key=lambda p: p.edge_sides)
if options.sort_by_max_edges_per_side:
piece_list.sort(key=lambda p: p.max_edges_per_side, reverse=True)
if options.sort_by_piece_size:
piece_list.sort(key=lambda p: p.width * p.height, reverse=True)
if options.random_order > 0:
for i, piece in enumerate(piece_list):
piece.order = i + random.random() * (options.random_order + 1)
piece_list.sort(key=lambda p: p.order)
# Place pieces
placed_pieces = set()
while piece_list:
pieces = [piece_list[0]]
if piece_list[0].delay < options.max_delay:
for i in range(1, min(options.multi_choice, len(piece_list))):
pieces.append(piece_list[i])
result = random_place_piece(world, player, grid_info, options, pieces, len(placed_pieces) < options.first_ignore_bonus_points)
if not result.success:
failures += 1
break
if result.piece != piece_list[0]:
piece_list[0].delay += 1
placed_pieces.add(result.piece)
piece_list.remove(result.piece)
major_score += result.score_major
else:
# 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
if not valid_layout:
failures += 1
continue
logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges")
successes += 1
score = -disabled_count
else:
successes += 1
score = major_score
total_score += score
if score > best_score:
best_score = score
best_grid_info = grid_info
if score < worst_score:
worst_score = score
if best_grid_info is None:
return LayoutGeneratorResult(
successes=successes,
failures=failures
)
return LayoutGeneratorResult(
grid_info=best_grid_info,
score=best_score,
worst_score=worst_score,
average_score=total_score / successes,
successes=successes,
failures=failures
)
def get_prioritized_edges(world: World, player: int) -> List[str]:
prio_edges = []
if world.accessibility[player] != 'none':
prio_edges += ['Desert EC']
if not world.is_tile_swapped(0x3A, player):
prio_edges += ['Desert Pass WC']
if world.is_tile_swapped(0x13, player):
prio_edges += ['Sanctuary WN']
if world.owParallel[player]:
prio_edges += ['Dark Chapel WN']
if world.owParallel[player]:
prio_edges += ['Flute Boy SC', 'Stumpy SC']
else:
if world.is_tile_swapped(0x2A, player):
prio_edges += ['Flute Boy SC']
else:
prio_edges += ['Stumpy SC']
if world.owTerrain[player]:
prio_edges += ['Octoballoon NW', 'Bomber Corner NW']
if world.is_tile_swapped(0x2D, player):
prio_edges += ['Stone Bridge EC']
if world.owParallel[player]:
prio_edges += ['Hammer Bridge EC']
if not world.is_tile_swapped(0x35, player):
prio_edges += ['Ice Lake ES']
if world.owParallel[player]:
prio_edges += ['Lake Hylia ES']
return prio_edges
escape_screen_ids = set([0x1B, 0x2B, 0x2C])
def connect_edges_for_screen_layout(world: World, player: int, grid_info: GridInfo, options: LayoutGeneratorOptions, connected_edges: List[str], prio_edges: List[str], overworld_screens: Dict[int, Screen], final_placement: bool) -> int:
use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped'
is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted'
is_standard = world.mode[player] == 'standard'
edge_random = _random.Random(grid_info.edge_connection_seed)
left_to_connect: List[Direction, int, int, int, int] = []
make_non_crossed = set()
make_crossed = set()
make_disabled = set()
undecided = []
# Collect information about all edge sets to connect
for dir in [Direction.East, Direction.South]:
for i in range(7 if dir == Direction.South and not options.vertical_wrap else 8):
for j in range(7 if dir == Direction.East and not options.horizontal_wrap else 8):
forced_escape = False
forced_non_crossed = False
forced_crossed = False
has_edges_1 = [False, False]
has_edges_2 = [False, False]
for w in range(2):
for terrain in range(1 if world.owTerrain[player] else 2):
left_to_connect.append((dir, w, i, j, terrain))
if is_unrestricted_crossed:
if dir == Direction.East:
west_edges = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid
east_edges = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid
edge_set_1 = east_edges[w][i][j]
edge_set_2 = west_edges[w][i][(j + 1) % 8]
if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][i][(j + 1) % 8] in escape_screen_ids:
forced_escape = True
else:
north_edges = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid
south_edges = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid
edge_set_1 = south_edges[w][i][j]
edge_set_2 = north_edges[w][(i + 1) % 8][j]
if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][(i + 1) % 8][j] in escape_screen_ids:
forced_escape = True
if any(edge for edge in edge_set_1 if edge.name in options.forced_non_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_non_crossed_edges):
forced_non_crossed = True
if any(edge for edge in edge_set_1 if edge.name in options.forced_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_crossed_edges):
forced_crossed = True
if edge_set_1:
has_edges_1[w] = True
if edge_set_2:
has_edges_2[w] = True
if is_unrestricted_crossed:
if forced_escape:
make_non_crossed.add((dir, i, j))
elif forced_non_crossed and forced_crossed:
make_disabled.add((dir, i, j))
elif forced_non_crossed:
make_non_crossed.add((dir, i, j))
elif forced_crossed:
make_crossed.add((dir, i, j))
elif has_edges_1[0] != has_edges_1[1] and has_edges_2[0] != has_edges_2[1]:
# On both sides of the transition only one world has any edges, so make sure we can connect those
(make_non_crossed if has_edges_1[0] == has_edges_2[0] else make_crossed).add((dir, i, j))
else:
undecided.append((dir, i, j))
if is_unrestricted_crossed:
# Make outstanding crossed choices
if options.crossed_limit > 0:
edge_random.shuffle(undecided)
remaining_crossed_edges = len(undecided) if options.crossed_limit < 0 else max(0, options.crossed_limit - len(make_crossed))
if remaining_crossed_edges > 0:
for x in undecided:
if edge_random.random() < options.crossed_chance:
make_crossed.add(x)
remaining_crossed_edges -= 1
if remaining_crossed_edges == 0:
break
# Connect the edge sets
for dir, w, i, j, terrain in left_to_connect:
if not is_unrestricted_crossed or not (dir, i, j) in make_disabled:
world_idx = w
if dir == Direction.East:
edges_1 = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid
edges_2 = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid
if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[i][(j + 1) % 8]:
world_idx = 1 - w
elif is_unrestricted_crossed and (dir, i, j) in make_crossed:
world_idx = 1 - w
connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][i][(j + 1) % 8], edge_random, connected_edges, prio_edges, final_placement)
else:
edges_1 = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid
edges_2 = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid
if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[(i + 1) % 8][j]:
world_idx = 1 - w
elif is_unrestricted_crossed and (dir, i, j) in make_crossed:
world_idx = 1 - w
connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][(i + 1) % 8][j], edge_random, connected_edges, prio_edges, final_placement)
# Count disabled edges
disabled_count = 0
for screen in overworld_screens.values():
for edge in screen.edges.values():
if not edge.dest:
disabled_count += 1
return disabled_count
def connect_edge_sets(world: World, player: int, edge_set_1: List[OWEdge], edge_set_2: List[OWEdge], edge_random: _random.Random, connected_edges: List[str], prio_edges: List[str], final_placement: bool) -> None:
if edge_set_1 and edge_set_2:
if world.owParallel[player]:
# Make sure that we do not connect parallel with non-parallel edges
parallel_edge_set_1 = [edge for edge in edge_set_1 if edge.parallel]
parallel_edge_set_2 = [edge for edge in edge_set_2 if edge.parallel]
if any(parallel_edge_set_1) and any(parallel_edge_set_2):
# Special case for screens that have both types of edges in the same direction (Dig Game and Frog)
if len(edge_set_1) == 2 and len(edge_set_2) == 2 and not edge_set_1[0].parallel and edge_set_1[1].parallel and not edge_set_2[0].parallel and edge_set_2[1].parallel:
connect_two_way(world, edge_set_1[0].name, edge_set_2[0].name, player, connected_edges, final_placement)
# Check if the edges already got connected when handling the other world
if any(edge for edge in parallel_edge_set_1 if edge.dest) or any(edge for edge in parallel_edge_set_2 if edge.dest):
return
# Special case for Maze Race and Kakariko Suburb with Keep Similar, only connect those when handling the other world
if ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player] and ((len(edge_set_1) == 1 and (edge_set_1[0].name == 'Maze Race ES' or edge_set_1[0].name == 'Kakariko Suburb WS')) or (len(edge_set_2) == 1 and (edge_set_2[0].name == 'Maze Race ES' or edge_set_2[0].name == 'Kakariko Suburb WS'))):
return
edge_set_1 = parallel_edge_set_1
edge_set_2 = parallel_edge_set_2
else:
non_parallel_edge_set_1 = [edge for edge in edge_set_1 if not edge.parallel]
non_parallel_edge_set_2 = [edge for edge in edge_set_2 if not edge.parallel]
if not any(non_parallel_edge_set_1) or not any(non_parallel_edge_set_2):
return
edge_set_1 = non_parallel_edge_set_1
edge_set_2 = non_parallel_edge_set_2
if len(edge_set_1) == len(edge_set_2):
for k in range(len(edge_set_1)):
connect_two_way(world, edge_set_1[k].name, edge_set_2[k].name, player, connected_edges, final_placement)
elif not ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING or not world.owKeepSimilar[player]:
if len(edge_set_1) < len(edge_set_2):
edge_set_1, edge_set_2 = edge_set_2, edge_set_1
# Not all edges from edge_set_1 can get connected
prio_set = [edge for edge in edge_set_1 if edge.name in prio_edges]
if len(prio_set) == len(edge_set_2):
for k in range(len(prio_set)):
connect_two_way(world, prio_set[k].name, edge_set_2[k].name, player, connected_edges, final_placement)
elif len(prio_set) < len(edge_set_2):
unconnected_edges = edge_random.sample([edge.name for edge in edge_set_1 if edge.name not in prio_edges], len(edge_set_1) - len(edge_set_2))
edges_to_connect = [edge for edge in edge_set_1 if edge.name not in unconnected_edges]
for k in range(len(edge_set_2)):
connect_two_way(world, edges_to_connect[k].name, edge_set_2[k].name, player, connected_edges, final_placement)
else:
raise Exception("There should never be multiple edges with high priority in an edge set")
# ============================================================================
# GRID FORMATTING
# ============================================================================
def format_grid_for_spoiler(grid: List[List[int]]) -> str:
lines = []
header = " "
for col in range(8):
header += f" {col} "
lines.append(header)
for row in range(8):
border_line = " +"
for col in range(8):
if row > 0 and is_same_large_screen(grid, row, col, row - 1, col):
border_line += " "
else:
border_line += "--"
# Check if we need a corner or continuation
if col < 7:
has_horizontal_left = row == 0 or not is_same_large_screen(grid, row, col, row - 1, col)
has_horizontal_right = row == 0 or not is_same_large_screen(grid, row, col + 1, row - 1, col + 1)
has_vertical_top = row == 0 or not is_same_large_screen(grid, row - 1, col, row - 1, col + 1)
has_vertical_bottom = not is_same_large_screen(grid, row, col, row, col + 1)
if has_vertical_bottom or has_vertical_top:
if has_horizontal_left or has_horizontal_right:
border_line += "+"
else:
border_line += "|"
else:
if has_horizontal_left or has_horizontal_right:
border_line += "-"
else:
border_line += " "
else:
border_line += "+"
lines.append(border_line)
row_name = "ABCDEFGH"[row]
content_line = f"{row_name}({row * 8:02X})|"
for col in range(8):
screen_id = grid[row][col]
if screen_id == -1:
content_line += "--"
else:
content_line += f"{screen_id:02X}"
# Check if we need a vertical separator after this cell
if col < 7:
if is_same_large_screen(grid, row, col, row, col + 1):
content_line += " "
else:
content_line += "|"
else:
content_line += "|"
lines.append(content_line)
bottom_border = " +"
for col in range(8):
bottom_border += "--"
if col < 7:
# Check if the bottom cells are part of the same large screen
if is_same_large_screen(grid, 7, col, 7, col + 1):
bottom_border += "-"
else:
bottom_border += "+"
else:
bottom_border += "+"
lines.append(bottom_border)
return "\n".join(lines)
def is_same_large_screen(grid: List[List[int]], row1: int, col1: int, row2: int, col2: int) -> bool:
id1 = grid[row1 % 8][col1 % 8]
id2 = grid[row2 % 8][col2 % 8]
if id1 == -1 or id2 == -1:
return False
return id1 == id2 and id1 in large_screen_ids
# ============================================================================
# MAIN EXECUTION
# ============================================================================
def generate_random_grid_layout(world: World, player: int, connected_edges: List[str], crossed_group_b: List[int], forced_non_crossed: Set[str], forced_crossed: Set[str], crossed_limit: int, crossed_chance: float):
"""Main execution function"""
import time
horizontal_wrap = False
vertical_wrap = False
if world.customizer:
grid_options = world.customizer.get_owgrid()
if grid_options and player in grid_options:
grid_options = grid_options[player]
horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True
vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True
first_ignore_bonus = 2
if not world.owParallel[player]:
first_ignore_bonus *= 2
if world.owCrossed[player] == 'unrestricted':
first_ignore_bonus *= 2
options = LayoutGeneratorOptions(
horizontal_wrap=horizontal_wrap,
vertical_wrap=vertical_wrap,
large_screen_pool=False,
distortion_chance=0.0,
random_order=6 if world.owParallel[player] else 12,
multi_choice=1,
max_delay=10,
penalty_full_edge_mismatch=1,
penalty_partial_edge_mismatch=1,
bonus_partial_edge_match=1,
bonus_full_edge_match=1,
bonus_crossed_group_match=1,
bonus_fill_parallel=1 if world.owCrossed[player] == 'unrestricted' else 0,
first_ignore_bonus_points=first_ignore_bonus,
forced_non_crossed_edges=forced_non_crossed,
forced_crossed_edges=forced_crossed,
crossed_chance=crossed_chance,
crossed_limit=crossed_limit,
check_reachability=True,
sort_by_edge_sides=world.owParallel[player] or not world.owTerrain[player],
sort_by_max_edges_per_side=False,
sort_by_piece_size=True,
min_runs=100,
max_runs=10000,
target_runs_times_successes=5000
)
overworld_screens = initialize_screens(world, player)
large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water = initialize_large_screen_data(overworld_screens)
prio_edges = get_prioritized_edges(world, player)
pieces_to_place = create_piece_list(world, player, options, crossed_group_b, overworld_screens, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water)
start_time = time.time()
result = get_random_layout(world, player, connected_edges, pieces_to_place, options, prio_edges, overworld_screens)
elapsed_time = time.time() - start_time
if result.grid_info:
connect_edges_for_screen_layout(world, player, result.grid_info, options, connected_edges, prio_edges, overworld_screens, True)
grid = result.grid_info.grid
# Make new grid containing cell IDs for the overworld map
map_grid = copy.deepcopy(grid)
for w in range(2):
for i in range(8):
for j in range(8):
screen_id = map_grid[w][i][j]
if screen_id in large_screen_ids and map_grid[w][i][(j + 1) % 8] == screen_id and map_grid[w][(i + 1) % 8][j] == screen_id and map_grid[w][(i + 1) % 8][(j + 1) % 8] == screen_id:
map_grid[w][i][(j + 1) % 8] = screen_id + 0x01
map_grid[w][(i + 1) % 8][j] = screen_id + 0x08
map_grid[w][(i + 1) % 8][(j + 1) % 8] = screen_id + 0x09
world.owgrid[player] = map_grid
world.owlayoutmap_lw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[0], []))}
world.owlayoutmap_dw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[1], []))}
world.spoiler.set_map('layout_grid_lw', format_grid_for_spoiler(grid[0]), grid[0], player)
if not world.owParallel[player]:
world.spoiler.set_map('layout_grid_dw', format_grid_for_spoiler(grid[1]), grid[1], player)
logger = logging.getLogger('')
logger.debug(f"\nLayout generation statistics:")
logger.debug(f" Best score: {result.score}")
logger.debug(f" Worst score: {result.worst_score}")
logger.debug(f" Average score: {result.average_score:.2f}")
logger.debug(f" Successes: {result.successes}")
logger.debug(f" Failures: {result.failures}")
logger.debug(f" Generation time: {elapsed_time:.3f}s")
if DRAW_IMAGE:
logger.debug("Creating layout visualization...")
try:
from source.overworld.LayoutVisualizer import visualize_layout
visualize_layout(grid, "visualizations", overworld_screens, large_screen_quadrant_info)
except Exception as e:
logger.warning(f"Warning: Could not create visualization: {e}")
else:
raise Exception(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds")