Implement piece merging
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from DungeonGenerator import GenerationException
|
||||
import RaceRandom as random
|
||||
import random as _random
|
||||
from typing import List, Dict, Optional, Set, Tuple
|
||||
@@ -6,7 +7,6 @@ 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 = True
|
||||
|
||||
large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
|
||||
@@ -78,8 +78,7 @@ 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',
|
||||
__slots__ = ('main', 'parallel', 'world', 'width', 'height', 'restriction',
|
||||
'crossed_groups', 'delay', 'order', 'edge_sides', 'max_edges_per_side')
|
||||
|
||||
def __init__(
|
||||
@@ -89,8 +88,6 @@ class Piece:
|
||||
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,
|
||||
@@ -103,8 +100,6 @@ class Piece:
|
||||
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
|
||||
@@ -400,74 +395,42 @@ def define_large_screen_quadrants(
|
||||
|
||||
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]
|
||||
|
||||
# Determine which screens to process
|
||||
all_screens = list(overworld_screens.values())
|
||||
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]
|
||||
all_screens = [s for s in all_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, 0x1C],
|
||||
[0x23, 0x24],
|
||||
[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:
|
||||
base_id = screen.id
|
||||
if options.split_large_screens:
|
||||
for quadrant_offset in [0x00, 0x01, 0x08, 0x09]:
|
||||
piece = create_piece(world, player, [[base_id + quadrant_offset]], overworld_screens)
|
||||
if options.large_screen_pool:
|
||||
piece.restriction = [large_screen_id + quadrant_offset for large_screen_id in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]]
|
||||
piece_list.append(piece)
|
||||
else:
|
||||
piece = create_piece(world, player, [[base_id, base_id + 0x01], [base_id + 0x08, base_id + 0x09]], overworld_screens)
|
||||
# Phase 1: Create individual 1x1 pieces for all cells
|
||||
for screen in all_screens:
|
||||
if screen.big:
|
||||
# Create 4 pieces for large screen quadrants
|
||||
for offset in [0x00, 0x01, 0x08, 0x09]:
|
||||
piece = create_piece(world, player, [[screen.id + offset]], overworld_screens)
|
||||
if options.large_screen_pool:
|
||||
piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]
|
||||
piece.restriction = [large_id + offset for large_id in [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:
|
||||
else:
|
||||
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
|
||||
# Phase 2: Apply options via merging
|
||||
|
||||
# Merge large screens if not split
|
||||
if not options.split_large_screens:
|
||||
for large_id in large_screen_ids:
|
||||
if large_id in [s.id for s in all_screens if s.big]:
|
||||
piece_list = merge_pieces(piece_list, [[large_id, large_id + 0x01], [large_id + 0x08, large_id + 0x09]], world, player, overworld_screens)
|
||||
|
||||
# Standard mode: merge castle area
|
||||
if world.mode[player] == 'standard':
|
||||
piece_list = merge_pieces(piece_list, [[0x23, 0x24], [0x2B, 0x2C]], world, player, overworld_screens)
|
||||
|
||||
# Phase 3: 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
|
||||
@@ -502,13 +465,13 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr
|
||||
Takes 2D array of cell IDs and creates main and parallel pieces
|
||||
"""
|
||||
piece = Piece(
|
||||
main=WorldPiece(),
|
||||
main=WorldPiece(width=len(grid[0]), height=len(grid)),
|
||||
width=len(grid[0]),
|
||||
height=len(grid)
|
||||
)
|
||||
|
||||
if world.owParallel[player]:
|
||||
piece.parallel = WorldPiece()
|
||||
piece.parallel = WorldPiece(width=len(grid[0]), height=len(grid))
|
||||
|
||||
found_screens = set()
|
||||
|
||||
@@ -526,25 +489,254 @@ def create_piece(world: World, player: int, grid: List[List[int]], overworld_scr
|
||||
for j in range(piece.width):
|
||||
cell_id = grid[i][j]
|
||||
new_row.append(cell_id)
|
||||
screen = overworld_screens.get(get_screen_id_from_cell(cell_id))
|
||||
new_screen_row.append(screen)
|
||||
if world.owParallel[player] and screen:
|
||||
new_row_parallel.append(cell_id - screen.id + screen.parallel.id)
|
||||
new_screen_row_parallel.append(screen.parallel)
|
||||
|
||||
if screen and screen not in found_screens:
|
||||
screen = None if cell_id == -1 else overworld_screens.get(get_screen_id_from_cell(cell_id))
|
||||
if screen:
|
||||
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)
|
||||
new_screen_row.append(screen)
|
||||
if world.owParallel[player]:
|
||||
if screen:
|
||||
new_row_parallel.append(cell_id - screen.id + screen.parallel.id)
|
||||
new_screen_row_parallel.append(screen.parallel)
|
||||
else:
|
||||
new_row_parallel.append(-1)
|
||||
new_screen_row_parallel.append(None)
|
||||
|
||||
worlds = set(s.dark_world for s in found_screens if s is not None)
|
||||
if len(worlds) != 1:
|
||||
raise GenerationException("Piece contains screens from both Light World and Dark World")
|
||||
piece.world = 1 if True in worlds else 0
|
||||
|
||||
return piece
|
||||
|
||||
def get_piece_cells(piece: Piece) -> Set[int]:
|
||||
"""Get all cell IDs contained in a piece."""
|
||||
cells = set()
|
||||
for row in piece.main.grid:
|
||||
for cell in row:
|
||||
if cell != -1:
|
||||
cells.add(cell)
|
||||
return cells
|
||||
|
||||
def expand_arrangement(arrangement: List[List[int]], pieces: List[Piece]) -> List[List[int]]:
|
||||
"""
|
||||
Expand an arrangement to include all cells from the pieces being merged.
|
||||
|
||||
When merging pieces, if a piece contains cells not in the original arrangement,
|
||||
we need to expand the arrangement to include those cells in their correct
|
||||
relative positions.
|
||||
|
||||
Raises an exception if the relative positions of cells within pieces conflict
|
||||
with the requested arrangement (e.g., contradictory merge operations).
|
||||
"""
|
||||
# Build a mapping of cell_id -> (row, col) for all cells in all pieces
|
||||
# relative to a common coordinate system
|
||||
cell_positions: Dict[int, Tuple[int, int]] = {}
|
||||
# Also track position -> cell_id to detect when two cells would occupy the same position
|
||||
position_to_cell: Dict[Tuple[int, int], int] = {}
|
||||
|
||||
# First, map cells from the original arrangement
|
||||
for i, row in enumerate(arrangement):
|
||||
for j, cell in enumerate(row):
|
||||
if cell != -1:
|
||||
cell_positions[cell] = (i, j)
|
||||
position_to_cell[(i, j)] = cell
|
||||
|
||||
# For each piece, determine where its cells should go
|
||||
for piece in pieces:
|
||||
# Find a cell that's already in our arrangement to anchor this piece
|
||||
anchor_cell = None
|
||||
anchor_piece_pos = None
|
||||
for i, row in enumerate(piece.main.grid):
|
||||
for j, cell in enumerate(row):
|
||||
if cell != -1 and cell in cell_positions:
|
||||
anchor_cell = cell
|
||||
anchor_piece_pos = (i, j)
|
||||
break
|
||||
if anchor_cell is not None:
|
||||
break
|
||||
|
||||
# Calculate offset between piece coordinates and arrangement coordinates
|
||||
anchor_arr_pos = cell_positions[anchor_cell]
|
||||
offset_row = anchor_arr_pos[0] - anchor_piece_pos[0]
|
||||
offset_col = anchor_arr_pos[1] - anchor_piece_pos[1]
|
||||
|
||||
# Add all cells from this piece to cell_positions, checking for conflicts
|
||||
for i, row in enumerate(piece.main.grid):
|
||||
for j, cell in enumerate(row):
|
||||
if cell != -1:
|
||||
new_pos = (i + offset_row, j + offset_col)
|
||||
if cell in cell_positions:
|
||||
# Cell already has a position - verify it's consistent
|
||||
if cell_positions[cell] != new_pos:
|
||||
raise GenerationException(
|
||||
f"Cannot merge: cell 0x{cell:02X} has conflicting positions. "
|
||||
f"Existing position {cell_positions[cell]} conflicts with "
|
||||
f"position {new_pos} from piece containing cells "
|
||||
f"{[c for row in piece.main.grid for c in row if c != -1]}. "
|
||||
f"This indicates contradictory merge operations."
|
||||
)
|
||||
elif new_pos in position_to_cell:
|
||||
# Position is already occupied by a different cell
|
||||
existing_cell = position_to_cell[new_pos]
|
||||
raise GenerationException(
|
||||
f"Cannot merge: cell 0x{cell:02X} would be placed at position {new_pos}, "
|
||||
f"but that position is already occupied by cell 0x{existing_cell:02X}. "
|
||||
f"This indicates contradictory merge operations."
|
||||
)
|
||||
else:
|
||||
cell_positions[cell] = new_pos
|
||||
position_to_cell[new_pos] = cell
|
||||
|
||||
# Find the bounding box of all cells
|
||||
if not cell_positions:
|
||||
return arrangement
|
||||
|
||||
min_row = min(pos[0] for pos in cell_positions.values())
|
||||
max_row = max(pos[0] for pos in cell_positions.values())
|
||||
min_col = min(pos[1] for pos in cell_positions.values())
|
||||
max_col = max(pos[1] for pos in cell_positions.values())
|
||||
|
||||
# Create new arrangement with normalized coordinates
|
||||
new_height = max_row - min_row + 1
|
||||
new_width = max_col - min_col + 1
|
||||
new_arrangement = [[-1] * new_width for _ in range(new_height)]
|
||||
|
||||
for cell, (row, col) in cell_positions.items():
|
||||
new_arrangement[row - min_row][col - min_col] = cell
|
||||
|
||||
return new_arrangement
|
||||
|
||||
def calculate_merged_restrictions(pieces: List[Piece], arrangement: List[List[int]]) -> Optional[List[int]]:
|
||||
"""
|
||||
Calculate restrictions for the merged piece.
|
||||
|
||||
For each piece with restrictions, we translate the restrictions to account
|
||||
for the piece's position in the merged arrangement. The final restriction
|
||||
is the intersection of all translated restrictions.
|
||||
|
||||
For example, when merging 4 quadrant pieces into a 2x2:
|
||||
- NW piece (at position 0,0) has restrictions like [0x00, 0x03, ...] - no translation needed
|
||||
- NE piece (at position 0,1) has restrictions like [0x01, 0x04, ...] - translate left by 1
|
||||
- SW piece (at position 1,0) has restrictions like [0x08, 0x0B, ...] - translate up by 1
|
||||
- SE piece (at position 1,1) has restrictions like [0x09, 0x0C, ...] - translate up and left by 1
|
||||
|
||||
After translation, all should give [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]
|
||||
"""
|
||||
if not any(p.restriction for p in pieces):
|
||||
return None
|
||||
|
||||
# Build mapping from cell to position in arrangement
|
||||
cell_to_new_pos = {}
|
||||
for i, row in enumerate(arrangement):
|
||||
for j, cell in enumerate(row):
|
||||
if cell != -1:
|
||||
cell_to_new_pos[cell] = (i, j)
|
||||
|
||||
# For each piece, translate its restrictions
|
||||
translated_restrictions = []
|
||||
for piece in pieces:
|
||||
if piece.restriction is None:
|
||||
continue
|
||||
|
||||
# Find the first cell in this piece and its position in the arrangement
|
||||
piece_cell = None
|
||||
piece_old_pos = None
|
||||
for i, row in enumerate(piece.main.grid):
|
||||
for j, cell in enumerate(row):
|
||||
if cell != -1 and cell in cell_to_new_pos:
|
||||
piece_cell = cell
|
||||
piece_old_pos = (i, j)
|
||||
break
|
||||
if piece_cell is not None:
|
||||
break
|
||||
|
||||
if piece_cell is None:
|
||||
continue
|
||||
|
||||
new_pos = cell_to_new_pos[piece_cell]
|
||||
# The offset is how much we need to shift the restriction positions
|
||||
# to get the top-left corner position of the merged piece
|
||||
offset_row = new_pos[0] - piece_old_pos[0]
|
||||
offset_col = new_pos[1] - piece_old_pos[1]
|
||||
|
||||
# Translate restrictions: shift each restriction position back by the offset
|
||||
# to get the position where the merged piece's top-left corner would be
|
||||
translated = []
|
||||
for r in piece.restriction:
|
||||
r_row = r // 8
|
||||
r_col = r % 8
|
||||
new_r_row = r_row - offset_row
|
||||
new_r_col = r_col - offset_col
|
||||
if 0 <= new_r_row < 8 and 0 <= new_r_col < 8:
|
||||
translated.append(new_r_row * 8 + new_r_col)
|
||||
translated_restrictions.append(set(translated))
|
||||
|
||||
# Intersection of all translated restrictions
|
||||
result = translated_restrictions[0]
|
||||
for tr in translated_restrictions[1:]:
|
||||
result &= tr
|
||||
|
||||
return list(result)
|
||||
|
||||
def merge_pieces(piece_list: List[Piece], arrangement: List[List[int]], world: World, player: int, overworld_screens: Dict[int, Screen]) -> List[Piece]:
|
||||
"""
|
||||
Merge pieces according to the specified arrangement.
|
||||
|
||||
The arrangement is a 2D list where:
|
||||
- Positive values are cell IDs that must be included
|
||||
- -1 indicates a flexible/empty position
|
||||
|
||||
Example: [[0x00, 0x01], [0x08, 0x09]] merges 4 pieces into a 2x2 piece
|
||||
|
||||
If a piece being merged contains additional cells not in the arrangement,
|
||||
the arrangement is automatically expanded to include all cells from all
|
||||
pieces being merged.
|
||||
"""
|
||||
# Collect all cell IDs from arrangement, excluding -1
|
||||
target_cells = set()
|
||||
for row in arrangement:
|
||||
for cell in row:
|
||||
if cell != -1:
|
||||
target_cells.add(cell)
|
||||
|
||||
# Find all pieces containing any of the target cells
|
||||
pieces_to_merge = []
|
||||
remaining_pieces = []
|
||||
|
||||
for piece in piece_list:
|
||||
piece_cells = get_piece_cells(piece)
|
||||
if piece_cells & target_cells:
|
||||
pieces_to_merge.append(piece)
|
||||
else:
|
||||
remaining_pieces.append(piece)
|
||||
|
||||
# Validate: all target cells must be found
|
||||
found_cells = set()
|
||||
for piece in pieces_to_merge:
|
||||
piece_cells = get_piece_cells(piece)
|
||||
# Check for overlapping cells between pieces (indicates contradictory merges)
|
||||
overlap = found_cells & piece_cells
|
||||
if overlap:
|
||||
raise GenerationException(f"Cannot merge: cells {overlap} appear in multiple pieces (contradictory merge operations)")
|
||||
found_cells.update(piece_cells)
|
||||
|
||||
if not target_cells.issubset(found_cells):
|
||||
missing = target_cells - found_cells
|
||||
raise GenerationException(f"Cannot merge: cells {missing} not found in any piece")
|
||||
|
||||
# If pieces contain additional cells not in the arrangement, expand the arrangement
|
||||
if found_cells != target_cells:
|
||||
arrangement = expand_arrangement(arrangement, pieces_to_merge)
|
||||
|
||||
# Create the merged piece
|
||||
merged_piece = create_piece(world, player, arrangement, overworld_screens)
|
||||
|
||||
# Calculate merged restrictions
|
||||
merged_piece.restriction = calculate_merged_restrictions(pieces_to_merge, arrangement)
|
||||
|
||||
remaining_pieces.append(merged_piece)
|
||||
return remaining_pieces
|
||||
|
||||
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
|
||||
@@ -553,13 +745,8 @@ def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadran
|
||||
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 = piece.width
|
||||
world_piece.height = piece.height
|
||||
add_world_piece_edge_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
|
||||
@@ -739,8 +926,6 @@ def random_place_piece(
|
||||
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
|
||||
@@ -751,13 +936,8 @@ def random_place_piece(
|
||||
|
||||
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
|
||||
|
||||
@@ -1268,7 +1448,7 @@ def connect_edge_sets(world: World, player: int, edge_set_1: List[OWEdge], edge_
|
||||
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")
|
||||
raise GenerationException("There should never be multiple edges with high priority in an edge set")
|
||||
|
||||
# ============================================================================
|
||||
# GRID FORMATTING
|
||||
@@ -1452,4 +1632,4 @@ def generate_random_grid_layout(world: World, player: int, connected_edges: List
|
||||
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")
|
||||
raise GenerationException(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds")
|
||||
Reference in New Issue
Block a user