Lots of cross gen work

This commit is contained in:
aerinon
2020-03-05 16:47:57 -07:00
parent 84d639ab60
commit c1783082d8
5 changed files with 378 additions and 102 deletions

View File

@@ -1024,6 +1024,7 @@ class Direction(Enum):
Up = 4 Up = 4
Down = 5 Down = 5
@unique @unique
class Hook(Enum): class Hook(Enum):
North = 0 North = 0
@@ -1285,6 +1286,7 @@ class Sector(object):
self.branch_factor = None self.branch_factor = None
self.dead_end_cnt = None self.dead_end_cnt = None
self.entrance_sector = None self.entrance_sector = None
self.destination_entrance = False
self.equations = None self.equations = None
def region_set(self): def region_set(self):

View File

@@ -397,11 +397,6 @@ def determine_entrance_list(world, player):
return entrance_map, potential_entrances, connections return entrance_map, potential_entrances, connections
# todo: kill drop exceptions
def drop_exception(name):
return name in ['Skull Pot Circle', 'Skull Back Drop']
def add_shuffled_entrances(sectors, region_list, entrance_list): def add_shuffled_entrances(sectors, region_list, entrance_list):
for sector in sectors: for sector in sectors:
for region in sector.regions: for region in sector.regions:
@@ -419,8 +414,6 @@ def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key):
if key not in entrance_map.keys(): if key not in entrance_map.keys():
key = ' '.join(key.split(' ')[:-1]) key = ' '.join(key.split(' ')[:-1])
entrance_map[key].append(region.name) entrance_map[key].append(region.name)
if drop_exception(region.name): # only because they have unique regions
entrance_list.append(region.name)
def remove_drop_origins(entrance_list): def remove_drop_origins(entrance_list):
@@ -1443,7 +1436,7 @@ def check_if_regions_visited(state, check_paths):
def check_for_pinball_fix(state, bad_region, world, player): def check_for_pinball_fix(state, bad_region, world, player):
pinball_region = world.get_region('Skull Pinball', player) pinball_region = world.get_region('Skull Pinball', player)
if bad_region.name == 'Skull 2 West Lobby' and state.visited_at_all(pinball_region): #revisit this for entrance shuffle if bad_region.name == 'Skull 2 West Lobby' and state.visited_at_all(pinball_region): # revisit this for entrance shuffle
door = world.get_door('Skull Pinball WS', player) door = world.get_door('Skull Pinball WS', player)
room = world.get_room(door.roomIndex, player) room = world.get_room(door.roomIndex, player)
if room.doorList[door.doorListPos][1] == DoorKind.Trap: if room.doorList[door.doorListPos][1] == DoorKind.Trap:

View File

@@ -2,15 +2,16 @@ import random
import collections import collections
import itertools import itertools
from collections import defaultdict, deque from collections import defaultdict, deque
import logging
from functools import reduce from functools import reduce
import logging
import math
import operator as op import operator as op
from typing import List from typing import List
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys
from BaseClasses import Hook, hook_from_door from BaseClasses import Hook, hook_from_door
from Regions import key_only_locations, dungeon_events, flooded_keys_reverse from Regions import key_only_locations, dungeon_events, flooded_keys_reverse
from Dungeons import dungeon_regions from Dungeons import dungeon_regions, split_region_starts
class GraphPiece: class GraphPiece:
@@ -100,7 +101,7 @@ def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world,
continue continue
prev_choices = choices_master[depth] prev_choices = choices_master[depth]
# make a choice # make a choice
hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices) hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices, name)
if hanger is None: if hanger is None:
backtrack = True backtrack = True
else: else:
@@ -157,9 +158,12 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
dungeon = {} dungeon = {}
start = ExplorationState(dungeon=name) start = ExplorationState(dungeon=name)
start.big_key_special = bk_special start.big_key_special = bk_special
def exception(d):
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map,
valid_doors, bk_needed, world, player) valid_doors, bk_needed, world, player, exception)
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception)
either_crystal = True # if all hooks from the origin are either, explore all bits with either either_crystal = True # if all hooks from the origin are either, explore all bits with either
for hook, crystal in dungeon['Origin'].hooks.items(): for hook, crystal in dungeon['Origin'].hooks.items():
if crystal != CrystalBarrier.Either: if crystal != CrystalBarrier.Either:
@@ -177,11 +181,11 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
init_state = ExplorationState(crystal_start, dungeon=name) init_state = ExplorationState(crystal_start, dungeon=name)
init_state.big_key_special = start.big_key_special init_state.big_key_special = start.big_key_special
o_state = extend_reachable_state_improved([parent], init_state, proposed_map, o_state = extend_reachable_state_improved([parent], init_state, proposed_map,
valid_doors, False, world, player) valid_doors, False, world, player, exception)
o_state_cache[door.name] = o_state o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map) piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception)
dungeon[door.name] = piece dungeon[door.name] = piece
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player) check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception)
# catalog hooks: Dict<Hook, List<Door, Crystal, Door>> # catalog hooks: Dict<Hook, List<Door, Crystal, Door>>
# and hangers: Dict<Hang, List<Door>> # and hangers: Dict<Hang, List<Door>>
@@ -201,7 +205,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
return dungeon, hangers, avail_hooks return dungeon, hangers, avail_hooks
def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player): def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception):
not_blue = set() not_blue = set()
not_blue.update(hanger_set) not_blue.update(hanger_set)
doors_to_check = set() doors_to_check = set()
@@ -229,20 +233,22 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do
hang_type = hanger_from_door(door) # am I hangable on a hook? hang_type = hanger_from_door(door) # am I hangable on a hook?
hook_type = hook_from_door(door) # am I hookable onto a hanger? hook_type = hook_from_door(door) # am I hookable onto a hanger?
if (hang_type in blue_hooks and not door.stonewall) or hook_type in blue_hangers: if (hang_type in blue_hooks and not door.stonewall) or hook_type in blue_hangers:
explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors, world, player) explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors,
world, player, exception)
doors_to_check.add(door) doors_to_check.add(door)
not_blue.difference_update(doors_to_check) not_blue.difference_update(doors_to_check)
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player): def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player, exception):
parent = door.entrance.parent_region parent = door.entrance.parent_region
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon) blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon)
blue_start.big_key_special = o_state.big_key_special blue_start.big_key_special = o_state.big_key_special
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player) b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False,
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map) world, player, exception)
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception)
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices): def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name):
# choose a hanger # choose a hanger
all_hooks = {} all_hooks = {}
origin = dungeon['Origin'] origin = dungeon['Origin']
@@ -294,6 +300,8 @@ def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
if len(hook_candidates) > 0: if len(hook_candidates) > 0:
hook_candidates.sort(key=lambda x: x.name) # sort for deterministic seeds hook_candidates.sort(key=lambda x: x.name) # sort for deterministic seeds
hook = random.choice(tuple(hook_candidates)) hook = random.choice(tuple(hook_candidates))
elif name == 'Skull Woods 2' and next_hanger.name == 'Skull Pinball WS':
continue
else: else:
return None, None return None, None
@@ -463,7 +471,7 @@ def stonewall_valid(stonewall):
return True return True
def create_graph_piece_from_state(door, o_state, b_state, proposed_map): def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception):
# todo: info about dungeon events - not sure about that # todo: info about dungeon events - not sure about that
graph_piece = GraphPiece() graph_piece = GraphPiece()
all_unattached = {} all_unattached = {}
@@ -485,7 +493,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
all_unattached[exp_d.door] = exp_d.crystal all_unattached[exp_d.door] = exp_d.crystal
h_crystal = door.crystal if door is not None else None h_crystal = door.crystal if door is not None else None
for d, crystal in all_unattached.items(): for d, crystal in all_unattached.items():
if (door is None or d != door) and not d.blocked and d not in proposed_map.keys(): if (door is None or d != door) and (not d.blocked or exception(d))and d not in proposed_map.keys():
graph_piece.hooks[d] = crystal graph_piece.hooks[d] = crystal
if d == door: if d == door:
h_crystal = crystal h_crystal = crystal
@@ -515,7 +523,7 @@ type_map = {
} }
def opposite_h_type(h_type): def opposite_h_type(h_type) -> Hook:
return type_map[h_type] return type_map[h_type]
@@ -755,9 +763,9 @@ class ExplorationState(object):
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors) self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player): def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player, exception):
for door in get_doors(world, region, player): for door in get_doors(world, region, player):
if self.can_traverse(door): if self.can_traverse(door, exception):
if door.controller is not None: if door.controller is not None:
door = door.controller door = door.controller
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors.keys(): if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors.keys():
@@ -814,9 +822,9 @@ class ExplorationState(object):
def visited_at_all(self, region): def visited_at_all(self, region):
return region in self.visited_blue or region in self.visited_orange return region in self.visited_blue or region in self.visited_orange
def can_traverse(self, door): def can_traverse(self, door, exception=None):
if door.blocked: if door.blocked:
return False return exception(door) if exception else False
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True return True
@@ -906,11 +914,11 @@ def extend_reachable_state(search_regions, state, world, player):
return local_state return local_state
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player): def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player, exception):
local_state = state.copy() local_state = state.copy()
for region in search_regions: for region in search_regions:
local_state.visit_region(region) local_state.visit_region(region)
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player) local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception)
while len(local_state.avail_doors) > 0: while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door() explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey: if explorable_door.door.bigKey:
@@ -927,7 +935,7 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d
connect_region): connect_region):
flag = explorable_door.flag or explorable_door.door.bigKey flag = explorable_door.flag or explorable_door.door.bigKey
local_state.visit_region(connect_region, bk_Flag=flag) local_state.visit_region(connect_region, bk_Flag=flag)
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player) local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
return local_state return local_state
@@ -993,6 +1001,7 @@ class DungeonBuilder(object):
self.c_locked = False self.c_locked = False
self.dead_ends = 0 self.dead_ends = 0
self.branches = 0 self.branches = 0
self.forced_loops = 0
self.total_conn_lack = 0 self.total_conn_lack = 0
self.conn_needed = defaultdict(int) self.conn_needed = defaultdict(int)
self.conn_supplied = defaultdict(int) self.conn_supplied = defaultdict(int)
@@ -1037,6 +1046,12 @@ class DungeonBuilder(object):
pol += sector.polarity() pol += sector.polarity()
return pol return pol
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s' % self.name
def simple_dungeon_builder(name, sector_list): def simple_dungeon_builder(name, sector_list):
define_sector_features(sector_list) define_sector_features(sector_list)
@@ -1048,11 +1063,13 @@ def simple_dungeon_builder(name, sector_list):
return builder return builder
def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_entrances=None): def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_entrances=None, split_dungeon_entrances=None):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Shuffling Dungeon Sectors') logger.info('Shuffling Dungeon Sectors')
if dungeon_entrances is None: if dungeon_entrances is None:
dungeon_entrances = default_dungeon_entrances dungeon_entrances = default_dungeon_entrances
if split_dungeon_entrances is None:
split_dungeon_entrances = split_region_starts
define_sector_features(all_sectors) define_sector_features(all_sectors)
candidate_sectors = dict.fromkeys(all_sectors) candidate_sectors = dict.fromkeys(all_sectors)
global_pole = GlobalPolarity(candidate_sectors) global_pole = GlobalPolarity(candidate_sectors)
@@ -1067,13 +1084,24 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
if key == 'Hyrule Castle' and world.mode[player] == 'standard': if key == 'Hyrule Castle' and world.mode[player] == 'standard':
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
entrances_map, potentials, connections = connections_tuple
accessible_sectors, reverse_d_map = set(), {}
for key in dungeon_entrances.keys(): for key in dungeon_entrances.keys():
current_dungeon = dungeon_map[key] current_dungeon = dungeon_map[key]
current_dungeon.all_entrances = dungeon_entrances[key] current_dungeon.all_entrances = dungeon_entrances[key]
for r_name in current_dungeon.all_entrances: for r_name in current_dungeon.all_entrances:
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) sector = find_sector(r_name, candidate_sectors)
# categorize sectors assign_sector(sector, current_dungeon, candidate_sectors, global_pole)
if r_name in entrances_map[key]:
if sector:
accessible_sectors.add(sector)
else:
if not sector:
sector = find_sector(r_name, all_sectors)
reverse_d_map[sector] = key
# categorize sectors
identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances)
for name, builder in dungeon_map.items(): for name, builder in dungeon_map.items():
calc_allowance_and_dead_ends(builder, connections_tuple) calc_allowance_and_dead_ends(builder, connections_tuple)
@@ -1114,6 +1142,52 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
return dungeon_map return dungeon_map
def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances):
accessible_overworld, found_connections, explored = set(), set(), False
while not explored:
explored = True
for ent_name, region in connections.items():
if ent_name in found_connections:
continue
sector = find_sector(ent_name, reverse_d_map.keys())
if sector in accessible_sectors:
found_connections.add(ent_name)
accessible_overworld.add(region) # todo: drops don't give ow access
explored = False
elif region in accessible_overworld:
found_connections.add(ent_name)
accessible_sectors.add(sector)
explored = False
else:
d_name = reverse_d_map[sector]
if d_name not in split_dungeon_entrances:
for r_name in dungeon_entrances[d_name]:
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
if ent_sector in accessible_sectors and ent_name not in dead_entrances:
sector.destination_entrance = True
found_connections.add(ent_name)
accessible_sectors.add(sector)
accessible_overworld.add(region)
explored = False
break
elif d_name in split_dungeon_entrances.keys():
split_section = None
for split_name, split_list in split_dungeon_entrances[d_name].items():
if ent_name in split_list:
split_section = split_name
break
for r_name in split_dungeon_entrances[d_name][split_section]:
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
if ent_sector in accessible_sectors and ent_name not in dead_entrances:
sector.destination_entrance = True
found_connections.add(ent_name)
accessible_sectors.add(sector)
accessible_overworld.add(region)
explored = False
break
def calc_allowance_and_dead_ends(builder, connections_tuple): def calc_allowance_and_dead_ends(builder, connections_tuple):
entrances_map, potentials, connections = connections_tuple entrances_map, potentials, connections = connections_tuple
needed_connections = [x for x in builder.all_entrances if x not in entrances_map[builder.name]] needed_connections = [x for x in builder.all_entrances if x not in entrances_map[builder.name]]
@@ -1183,7 +1257,7 @@ def define_sector_features(sectors):
def assign_sector(sector, dungeon, candidate_sectors, global_pole): def assign_sector(sector, dungeon, candidate_sectors, global_pole):
if sector is not None: if sector:
del candidate_sectors[sector] del candidate_sectors[sector]
dungeon.sectors.append(sector) dungeon.sectors.append(sector)
global_pole.consume(sector) global_pole.consume(sector)
@@ -1344,11 +1418,17 @@ def identify_polarity_issues(dungeon_map):
other_mag = sum_magnitude(others) other_mag = sum_magnitude(others)
sector_mag = sector.magnitude() sector_mag = sector.magnitude()
check_flags(sector_mag, connection_flags) check_flags(sector_mag, connection_flags)
unconnected_sector = True
for i in PolSlot: for i in PolSlot:
if sector_mag[i.value] > 0 and other_mag[i.value] == 0 and not self_connecting(sector, i, sector_mag): if sector_mag[i.value] == 0 or other_mag[i.value] > 0 or self_connecting(sector, i, sector_mag):
builder.mag_needed[i] = [x for x in PolSlot if other_mag[x.value] > 0] unconnected_sector = False
if name not in unconnected_builders.keys(): break
unconnected_builders[name] = builder if unconnected_sector:
for i in PolSlot:
if sector_mag[i.value] > 0 and other_mag[i.value] == 0 and not self_connecting(sector, i, sector_mag):
builder.mag_needed[i] = [x for x in PolSlot if other_mag[x.value] > 0]
if name not in unconnected_builders.keys():
unconnected_builders[name] = builder
ttl_mag = sum_magnitude(builder.sectors) ttl_mag = sum_magnitude(builder.sectors)
for slot in PolSlot: for slot in PolSlot:
for slot2 in PolSlot: for slot2 in PolSlot:
@@ -1382,7 +1462,8 @@ def identify_simple_branching_issues(dungeon_map):
if name == 'Skull Woods 2': # i dislike this special case todo: identify destination entrances if name == 'Skull Woods 2': # i dislike this special case todo: identify destination entrances
builder.conn_supplied[Hook.West] += 1 builder.conn_supplied[Hook.West] += 1
builder.conn_needed[Hook.East] -= 1 builder.conn_needed[Hook.East] -= 1
if builder.dead_ends > builder.branches + builder.allowance: builder.forced_loops = calc_forced_loops(builder.sectors)
if builder.dead_ends + builder.forced_loops * 2 > builder.branches + builder.allowance:
problem_builders[name] = builder problem_builders[name] = builder
for h_type in Hook: for h_type in Hook:
lack = builder.conn_balance[h_type] = builder.conn_supplied[h_type] - builder.conn_needed[h_type] lack = builder.conn_balance[h_type] = builder.conn_supplied[h_type] - builder.conn_needed[h_type]
@@ -1392,6 +1473,28 @@ def identify_simple_branching_issues(dungeon_map):
return problem_builders return problem_builders
def calc_forced_loops(sector_list):
forced_loops = 0
for sector in sector_list:
h_mag = sector.hook_magnitude()
other_sectors = [x for x in sector_list if x != sector]
other_mag = sum_hook_magnitude(other_sectors)
loop_parts = 0
for hook in Hook:
opp = opposite_h_type(hook).value
if h_mag[hook.value] > other_mag[opp] and loop_present(hook, opp, h_mag, other_mag):
loop_parts += (h_mag[hook.value] - other_mag[opp]) / 2
forced_loops += math.floor(loop_parts)
return forced_loops
def loop_present(hook, opp, h_mag, other_mag):
if hook == Hook.Stairs:
return h_mag[hook.value] - other_mag[opp] >= 2
else:
return h_mag[opp] >= h_mag[hook.value] - other_mag[opp]
def is_entrance_sector(builder, sector): def is_entrance_sector(builder, sector):
for entrance in builder.all_entrances: for entrance in builder.all_entrances:
r_set = sector.region_set() r_set = sector.region_set()
@@ -1483,20 +1586,22 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
# step 4: fix dead ends again # step 4: fix dead ends again
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
problem_builders = identify_branching_issues_2(dungeon_map) problem_builders = identify_branching_issues(dungeon_map)
while len(problem_builders) > 0: while len(problem_builders) > 0:
for name, builder in problem_builders.items(): for name, builder in problem_builders.items():
candidates = find_branching_candidates(builder, neutral_choices) candidates = find_branching_candidates(builder, neutral_choices)
# if len(candidates) <= 0: valid, choice = False, None
# problem_builders = {} while not valid:
# continue if len(candidates) <= 0:
choice = random.choice(candidates) raise Exception('Cross Dungeon Builder: Complex branch problems: %s' % name)
if valid_polarized_assignment(builder, choice): choice = random.choice(candidates)
neutral_choices.remove(choice) candidates.remove(choice)
for sector in choice: valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice)
assign_sector(sector, builder, polarized_sectors, global_pole) neutral_choices.remove(choice)
for sector in choice:
assign_sector(sector, builder, polarized_sectors, global_pole)
builder.unfulfilled.clear() builder.unfulfilled.clear()
problem_builders = identify_branching_issues_2(problem_builders) problem_builders = identify_branching_issues(problem_builders)
# step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc. # step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc.
comb_w_replace = len(dungeon_map) ** len(neutral_choices) comb_w_replace = len(dungeon_map) ** len(neutral_choices)
@@ -1534,8 +1639,9 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger):
random.shuffle(odd_builders) random.shuffle(odd_builders)
for builder in odd_builders: for builder in odd_builders:
while sum_polarity(builder.sectors).charge() % 2 != 0: while sum_polarity(builder.sectors).charge() % 2 != 0:
odd_candidates = find_odd_sectors_ranked_by_charge(builder, polarized_sectors) grouped_choices: List[List] = find_forced_groupings(polarized_sectors, dungeon_map)
sub_candidates, valid, best_charge, candidate = [], False, min(list(odd_candidates.keys())), None odd_candidates = find_odd_sectors_ranked_by_charge(builder, grouped_choices)
sub_candidates, valid, best_charge, candidate_list = [], False, min(list(odd_candidates.keys())), None
while not valid: while not valid:
if len(sub_candidates) == 0: if len(sub_candidates) == 0:
if len(odd_candidates) == 0: if len(odd_candidates) == 0:
@@ -1543,10 +1649,11 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger):
while best_charge not in odd_candidates.keys(): while best_charge not in odd_candidates.keys():
best_charge += 2 best_charge += 2
sub_candidates = odd_candidates.pop(best_charge) sub_candidates = odd_candidates.pop(best_charge)
candidate = random.choice(sub_candidates) candidate_list = random.choice(sub_candidates)
sub_candidates.remove(candidate) sub_candidates.remove(candidate_list)
valid = global_pole.is_valid_choice(dungeon_map, builder, [candidate]) valid = global_pole.is_valid_choice(dungeon_map, builder, candidate_list) and valid_branch_only(builder, candidate_list)
assign_sector(candidate, builder, polarized_sectors, global_pole) for candidate in candidate_list:
assign_sector(candidate, builder, polarized_sectors, global_pole)
# step 3b: neutralize all builders # step 3b: neutralize all builders
builder_order = list(dungeon_map.values()) builder_order = list(dungeon_map.values())
@@ -1697,7 +1804,7 @@ def find_connection_candidates(mag_needed, sector_pool):
def find_simple_branching_candidates(builder, sector_pool): def find_simple_branching_candidates(builder, sector_pool):
candidates = defaultdict(list) candidates = defaultdict(list)
charges = defaultdict(list) charges = defaultdict(list)
outflow_needed = builder.dead_ends > builder.branches + builder.allowance outflow_needed = builder.dead_ends + builder.forced_loops * 2 > builder.branches + builder.allowance
original_lack = builder.total_conn_lack original_lack = builder.total_conn_lack
best_lack = original_lack best_lack = original_lack
for sector in sector_pool: for sector in sector_pool:
@@ -1709,7 +1816,9 @@ def find_simple_branching_candidates(builder, sector_pool):
lack = builder.conn_balance[hook] + sector.conn_balance[hook] lack = builder.conn_balance[hook] + sector.conn_balance[hook]
if lack < 0: if lack < 0:
ttl_lack += -lack ttl_lack += -lack
if ttl_lack < original_lack or original_lack >= 0: forced_loops = calc_forced_loops(builder.sectors + [sector])
valid_branches = builder.dead_ends + forced_loops * 2 + sector.dead_ends() <= builder.branches + builder.allowance + sector.branches()
if valid_branches and (ttl_lack < original_lack or original_lack >= 0):
candidates[ttl_lack].append(sector) candidates[ttl_lack].append(sector)
charges[ttl_lack].append((builder.polarity()+sector.polarity()).charge()) charges[ttl_lack].append((builder.polarity()+sector.polarity()).charge())
if ttl_lack < best_lack: if ttl_lack < best_lack:
@@ -1731,12 +1840,12 @@ def calc_sector_balance(sector): # todo: move to base class?
sector.conn_balance[hanger_from_door(door)] += 1 sector.conn_balance[hanger_from_door(door)] += 1
def find_odd_sectors_ranked_by_charge(builder, polarized_sectors): def find_odd_sectors_ranked_by_charge(builder, grouped_candidates):
polarity = builder.polarity() polarity = builder.polarity()
candidates = defaultdict(list) candidates = defaultdict(list)
for candidate in [x for x in polarized_sectors if x.polarity().charge() % 2 != 0]: for candidate_list in [x for x in grouped_candidates if sum_polarity(x).charge() % 2 != 0]:
p_charge = (polarity + candidate.polarity()).charge() p_charge = (polarity + sum_polarity(candidate_list)).charge()
candidates[p_charge].append(candidate) candidates[p_charge].append(candidate_list)
return candidates return candidates
@@ -1802,7 +1911,8 @@ def weed_candidates(builder, candidates, best_charge):
ttl_balance += lack ttl_balance += lack
if lack < 0: if lack < 0:
ttl_lack += -lack ttl_lack += -lack
if ttl_balance >= 0 and builder.dead_ends + ttl_deads <= builder.branches + ttl_branches + builder.allowance: forced_loops = calc_forced_loops(builder.sectors + cand)
if ttl_balance >= 0 and builder.dead_ends + ttl_deads + forced_loops * 2 <= builder.branches + ttl_branches + builder.allowance:
if best_lack is None or ttl_lack < best_lack: if best_lack is None or ttl_lack < best_lack:
best_lack = ttl_lack best_lack = ttl_lack
official_cand = [cand] official_cand = [cand]
@@ -1833,10 +1943,8 @@ def find_branching_candidates(builder, neutral_choices):
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
if builder.unfulfilled[hanger_from_door(door)] > 0: if builder.unfulfilled[hanger_from_door(door)] > 0:
door_match = True door_match = True
if door_match and flow_match: if (door_match and flow_match) or len(resolve_equations(builder, choice)) == 0:
candidates.append(choice) candidates.append(choice)
if len(candidates) == 0:
raise Exception('Cross Dungeon Builder: No more branching candidates! %s' % builder.name)
return candidates return candidates
@@ -1876,6 +1984,50 @@ def neutralize_the_rest(sector_pool):
return neutral_choices return neutral_choices
def find_forced_groupings(sector_pool, dungeon_map):
dungeon_hooks = {}
for name, builder in dungeon_map.items():
dungeon_hooks[name] = sum_hook_magnitude(builder.sectors)
groupings = []
queue = deque(sector_pool)
skips = set()
while len(queue) > 0:
grouping = queue.pop()
is_list = isinstance(grouping, List)
if not is_list and grouping in skips:
continue
grouping = grouping if is_list else [grouping]
hook_mag = sum_hook_magnitude(grouping)
force_found = False
for val in Hook:
if hook_mag[val.value] == 1:
opp = opposite_h_type(val).value
num_found = hook_mag[opp]
for name, hooks in dungeon_hooks.items():
if hooks[opp] > 0:
num_found += hooks[opp]
other_sectors = [x for x in sector_pool if x not in grouping]
other_sector_mag = sum_hook_magnitude(other_sectors)
if other_sector_mag[opp] > 0:
num_found += other_sector_mag[opp]
if num_found == 1:
forced_sector = None
for sec in other_sectors:
if sec.hook_magnitude()[opp] > 0:
forced_sector = sec
break
if forced_sector:
grouping.append(forced_sector)
skips.add(forced_sector)
queue.append(grouping)
force_found = True
if force_found:
break
if not force_found:
groupings.append(grouping)
return groupings
def valid_assignment(builder, sector_list): def valid_assignment(builder, sector_list):
if not valid_polarized_assignment(builder, sector_list): if not valid_polarized_assignment(builder, sector_list):
return False return False
@@ -1897,9 +2049,26 @@ def valid_connected_assignment(builder, sector_list):
return True return True
def valid_polarized_assignment(builder, sector_list): def valid_branch_assignment(builder, sector_list):
if not valid_connected_assignment(builder, sector_list): if not valid_connected_assignment(builder, sector_list):
return False return False
return valid_branch_only(builder, sector_list)
def valid_branch_only(builder, sector_list):
forced_loops = calc_forced_loops(builder.sectors + sector_list)
ttl_deads = 0
ttl_branches = 0
for s in sector_list:
# calc_sector_balance(sector) # do I want to check lack here? see weed_candidates
ttl_deads += s.dead_ends()
ttl_branches += s.branches()
return builder.dead_ends + ttl_deads + forced_loops * 2 <= builder.branches + ttl_branches + builder.allowance
def valid_polarized_assignment(builder, sector_list):
if not valid_branch_assignment(builder, sector_list):
return False
return (sum_polarity(sector_list) + sum_polarity(builder.sectors)).is_neutral() return (sum_polarity(sector_list) + sum_polarity(builder.sectors)).is_neutral()
@@ -1909,7 +2078,7 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole):
choices = random.choices(list(dungeon_map.keys()), k=len(sector_list)) choices = random.choices(list(dungeon_map.keys()), k=len(sector_list))
for i, choice in enumerate(choices): for i, choice in enumerate(choices):
builder = dungeon_map[choice] builder = dungeon_map[choice]
if valid_polarized_assignment(builder, [sector_list[i]]): if valid_assignment(builder, [sector_list[i]]):
assign_sector(sector_list[i], builder, neutral_sectors, global_pole) assign_sector(sector_list[i], builder, neutral_sectors, global_pole)
@@ -1964,13 +2133,13 @@ def check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole):
if hook_mag[hook.value] != 0: if hook_mag[hook.value] != 0:
dead_cnt[hook.value] += 1 dead_cnt[hook.value] += 1
for hook in Hook: for hook in Hook:
opp = opposite_h_type(hook) opp = opposite_h_type(hook).value
if dead_cnt[hook.value] > other_magnitude[opp.value]: if dead_cnt[hook.value] > other_magnitude[opp]:
raise Exception('Impossible to satisfy all these dead ends') raise Exception('Impossible to satisfy all these dead ends')
elif dead_cnt[hook.value] == other_magnitude[opp.value]: elif dead_cnt[hook.value] == other_magnitude[opp]:
candidates = [x for x in dead_end_sectors if x.hook_magnitude()[hook.value] > 0] candidates = [x for x in dead_end_sectors if x.hook_magnitude()[hook.value] > 0]
for sector in other_sectors: for sector in other_sectors:
if sector.hook_magnitude()[opp.value] > 0 and sector.is_entrance_sector() and sector.branching_factor() == 2: if sector.hook_magnitude()[opp] > 0 and sector.is_entrance_sector() and sector.branching_factor() == 2:
builder = None builder = None
for b in dungeon_map.values(): for b in dungeon_map.values():
if sector in b.sectors: if sector in b.sectors:
@@ -1998,9 +2167,9 @@ def check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole):
for val in Hook: for val in Hook:
if magnitude[val.value] == 1: if magnitude[val.value] == 1:
found_hooks = [] found_hooks = []
opp = opposite_h_type(val) opp = opposite_h_type(val).value
for name, hooks in dungeon_hooks.items(): for name, hooks in dungeon_hooks.items():
if hooks[opp.value] > 0 and not dungeon_map[name].c_locked: if hooks[opp] > 0 and not dungeon_map[name].c_locked:
found_hooks.append(name) found_hooks.append(name)
if len(found_hooks) == 1: if len(found_hooks) == 1:
done = False done = False
@@ -2048,6 +2217,7 @@ class DoorEquation:
self.cost = defaultdict(list) self.cost = defaultdict(list)
self.benefit = defaultdict(list) self.benefit = defaultdict(list)
self.required = False self.required = False
self.access_id = None
def copy(self): def copy(self):
eq = DoorEquation(self.door) eq = DoorEquation(self.door)
@@ -2076,6 +2246,15 @@ class DoorEquation:
return False return False
return True return True
def neutral_profit(self):
better_found = False
for key in Hook:
if len(self.cost[key]) > len(self.benefit[key]):
return False
if len(self.cost[key]) < len(self.benefit[key]):
better_found = True
return better_found
def can_cover_cost(self, current_access): def can_cover_cost(self, current_access):
for key, door_list in self.cost.items(): for key, door_list in self.cost.items():
if len(door_list) > current_access[key]: if len(door_list) > current_access[key]:
@@ -2083,7 +2262,7 @@ class DoorEquation:
return True return True
def identify_branching_issues_2(dungeon_map): def identify_branching_issues(dungeon_map):
unconnected_builders = {} unconnected_builders = {}
for name, builder in dungeon_map.items(): for name, builder in dungeon_map.items():
unreached_doors = resolve_equations(builder, []) unreached_doors = resolve_equations(builder, [])
@@ -2097,17 +2276,40 @@ def identify_branching_issues_2(dungeon_map):
def resolve_equations(builder, sector_list): def resolve_equations(builder, sector_list):
unreached_doors = defaultdict(list) unreached_doors = defaultdict(list)
equations = copy_door_equations(builder, sector_list) equations = copy_door_equations(builder, sector_list)
current_access = defaultdict(int)
reached_doors = set() reached_doors = set()
current_access = {}
sector_split = {} # those sectors that belong to a certain sector
if builder.name in split_region_starts.keys():
for name, region_list in split_region_starts[builder.name].items():
current_access[name] = defaultdict(int)
for r_name in region_list:
sector = find_sector(r_name, builder.sectors)
sector_split[sector] = name
else:
current_access[builder.name] = defaultdict(int)
# resolve all that provide more access # resolve all that provide more access
free_sector, eq_list, free_eq = find_free_equation(equations) free_sector, eq_list, free_eq = find_free_equation(equations)
while free_eq is not None: while free_eq is not None:
resolve_equation(free_eq, eq_list, free_sector, current_access, reached_doors, equations) if free_sector in sector_split.keys():
access_id = sector_split[free_sector]
access = current_access[access_id]
else:
access_id = next(iter(current_access.keys()))
access = current_access[access_id]
resolve_equation(free_eq, eq_list, free_sector, access_id, access, reached_doors, equations)
free_sector, eq_list, free_eq = find_free_equation(equations) free_sector, eq_list, free_eq = find_free_equation(equations)
while len(equations) > 0: while len(equations) > 0:
eq, eq_list, sector = find_priority_equation(equations, current_access) valid_access = next_access(current_access)
if eq is not None: eq, eq_list, sector, access, access_id = None, None, None, None, None
resolve_equation(eq, eq_list, sector, current_access, reached_doors, equations) if len(valid_access) == 1:
access_id, access = valid_access[0]
eq, eq_list, sector = find_priority_equation(equations, access_id, access)
elif len(valid_access) > 1:
access_id, access = valid_access[0]
eq, eq_list, sector = find_greedy_equation(equations, access_id, access)
if eq:
resolve_equation(eq, eq_list, sector, access_id, access, reached_doors, equations)
else: else:
for sector, eq_list in equations.items(): for sector, eq_list in equations.items():
for eq in eq_list: for eq in eq_list:
@@ -2116,39 +2318,65 @@ def resolve_equations(builder, sector_list):
return unreached_doors return unreached_doors
def next_access(current_access):
valid_ones = [(x, y) for x, y in current_access.items() if sum(y.values()) > 0]
valid_ones.sort(key=lambda x: sum(x[1].values()))
return valid_ones
# an equations with no change to access (check) # an equations with no change to access (check)
# the highest benefit equations, that can be paid for (check) # the highest benefit equations, that can be paid for (check)
# 0-benefit required transforms # 0-benefit required transforms
# 0-benefit transforms (how to pick between these?) # 0-benefit transforms (how to pick between these?)
# negative benefit transforms (dead end) # negative benefit transforms (dead end)
def find_priority_equation(equations, current_access): def find_priority_equation(equations, access_id, current_access):
flex = calc_flex(equations, current_access) flex = calc_flex(equations, current_access)
required = calc_required(equations, current_access) required = calc_required(equations, current_access)
wanted_candidates = []
best_profit = None best_profit = None
triplet_candidates = [] all_candidates = []
local_profit_map = {} local_profit_map = {}
for sector, eq_list in equations.items(): for sector, eq_list in equations.items():
eq_list.sort(key=lambda eq: eq.profit(), reverse=True) eq_list.sort(key=lambda eq: eq.profit(), reverse=True)
best_local_profit = None best_local_profit = None
for eq in eq_list: for eq in eq_list:
profit = eq.profit() profit = eq.profit()
if best_local_profit is None or profit > best_local_profit: if eq.can_cover_cost(current_access) and (eq.access_id is None or eq.access_id == access_id):
best_local_profit = profit if eq.neutral_profit() or eq.neutral():
if eq.can_cover_cost(current_access): return eq, eq_list, sector # don't need to compare - just use it now
if eq.neutral(): if best_local_profit is None or profit > best_local_profit:
return eq, eq_list, sector # don't need to compare best_local_profit = profit
if best_profit is None or profit >= best_profit: all_candidates.append((eq, eq_list, sector))
if best_profit is None or profit > best_profit: elif best_profit is None or profit >= best_profit:
triplet_candidates = [(eq, eq_list, sector)] if best_profit is None or profit > best_profit:
best_profit = profit wanted_candidates = [eq]
else: best_profit = profit
triplet_candidates.append((eq, eq_list, sector)) else:
wanted_candidates.append(eq)
local_profit_map[sector] = best_local_profit local_profit_map[sector] = best_local_profit
filtered_candidates = filter_requirements(all_candidates, equations, required, current_access)
if len(filtered_candidates) == 0:
filtered_candidates = all_candidates # probably bad things
if len(filtered_candidates) == 0:
return None, None, None # can't pay for anything
if len(filtered_candidates) == 1:
return filtered_candidates[0]
triplet_candidates = []
best_profit = None
for eq, eq_list, sector in filtered_candidates:
profit = eq.profit()
if best_profit is None or profit >= best_profit:
if best_profit is None or profit > best_profit:
triplet_candidates = [(eq, eq_list, sector)]
best_profit = profit
else:
triplet_candidates.append((eq, eq_list, sector))
filtered_candidates = filter_requirements(triplet_candidates, equations, required, current_access) filtered_candidates = filter_requirements(triplet_candidates, equations, required, current_access)
if len(filtered_candidates) == 0: if len(filtered_candidates) == 0:
filtered_candidates = triplet_candidates filtered_candidates = triplet_candidates
if len(filtered_candidates) == 0:
return None, None, None # can't pay for anything
if len(filtered_candidates) == 1: if len(filtered_candidates) == 1:
return filtered_candidates[0] return filtered_candidates[0]
@@ -2167,7 +2395,45 @@ def find_priority_equation(equations, current_access):
good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit()] good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit()]
if len(good_local_candidates) == 0: if len(good_local_candidates) == 0:
good_local_candidates = flexible_candidates good_local_candidates = flexible_candidates
return good_local_candidates[0] # just pick one I guess if len(good_local_candidates) == 1:
return good_local_candidates[0]
leads_to_profit = [x for x in good_local_candidates if can_enable_wanted(x[0], wanted_candidates)]
if len(leads_to_profit) == 0:
leads_to_profit = good_local_candidates
return leads_to_profit[0] # just pick one I guess
def find_greedy_equation(equations, access_id, current_access):
all_candidates = []
for sector, eq_list in equations.items():
eq_list.sort(key=lambda eq: eq.profit(), reverse=True)
for eq in eq_list:
if eq.can_cover_cost(current_access) and (eq.access_id is None or eq.access_id == access_id):
all_candidates.append((eq, eq_list, sector))
if len(all_candidates) == 0:
return None, None, None # can't pay for anything
if len(all_candidates) == 1:
return all_candidates[0]
filtered_candidates = [x for x in all_candidates if x[0].profit() + 2 >= len(x[2].outstanding_doors)]
if len(filtered_candidates) == 0:
filtered_candidates = all_candidates # terrible! ugly dead ends
if len(filtered_candidates) == 1:
return filtered_candidates[0]
triplet_candidates = []
worst_profit = None
for eq, eq_list, sector in filtered_candidates:
profit = eq.profit()
if worst_profit is None or profit <= worst_profit:
if worst_profit is None or profit < worst_profit:
triplet_candidates = [(eq, eq_list, sector)]
worst_profit = profit
else:
triplet_candidates.append((eq, eq_list, sector))
if len(triplet_candidates) == 0:
triplet_candidates = filtered_candidates # probably bad things
return triplet_candidates[0] # just pick one?
def calc_required(equations, current_access): def calc_required(equations, current_access):
@@ -2262,7 +2528,19 @@ def filter_requirements(triplet_candidates, equations, required, current_access)
return valid_candidates return valid_candidates
def resolve_equation(equation, eq_list, sector, current_access, reached_doors, equations): def can_enable_wanted(test_eq, wanted_candidates):
for wanted in wanted_candidates:
covered = True
for key, door_list in wanted.cost.items():
if len(test_eq.benefit[key]) < len(door_list):
covered = False
break
if covered:
return True
return False
def resolve_equation(equation, eq_list, sector, access_id, current_access, reached_doors, equations):
for key, door_list in equation.cost.items(): for key, door_list in equation.cost.items():
if current_access[key] - len(door_list) < 0: if current_access[key] - len(door_list) < 0:
raise Exception('Cannot pay for this connection') raise Exception('Cannot pay for this connection')
@@ -2286,6 +2564,9 @@ def resolve_equation(equation, eq_list, sector, current_access, reached_doors, e
eq_list.remove(r_eq) eq_list.remove(r_eq)
if len(eq_list) == 0: if len(eq_list) == 0:
del equations[sector] del equations[sector]
else:
for eq in eq_list:
eq.access_id = access_id
def find_free_equation(equations): def find_free_equation(equations):
@@ -2310,7 +2591,7 @@ def copy_door_equations(builder, sector_list):
def calc_sector_equations(sector, builder): def calc_sector_equations(sector, builder):
equations = [] equations = []
is_entrance = is_entrance_sector(builder, sector) is_entrance = is_entrance_sector(builder, sector) and not sector.destination_entrance
if is_entrance: if is_entrance:
flagged_equations = [] flagged_equations = []
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
@@ -2370,7 +2651,7 @@ def calc_door_equation(door, sector, look_for_entrance):
if d.req_event is not None and d.req_event not in found_events: if d.req_event is not None and d.req_event not in found_events:
event_doors.add(d) event_doors.add(d)
else: else:
connect = ext.connected_region connect = ext.connected_region if ext.door.controller is None else d.entrance.parent_region
if connect is not None and connect.type == RegionType.Dungeon and connect not in visited: if connect is not None and connect.type == RegionType.Dungeon and connect not in visited:
visited.add(connect) visited.add(connect)
queue.append(connect) queue.append(connect)

View File

@@ -58,7 +58,7 @@ def start():
logger.warning('Generation failed: %s', err) logger.warning('Generation failed: %s', err)
seed = random.randint(0, 999999999) seed = random.randint(0, 999999999)
for fail in failures: for fail in failures:
logger.info('%s seed failed with: %s', fail[1], fail[0]) logger.info('%s\tseed failed with: %s', fail[1], fail[0])
fail_rate = 100 * len(failures) / args.count fail_rate = 100 * len(failures) / args.count
success_rate = 100 * (args.count - len(failures)) / args.count success_rate = 100 * (args.count - len(failures)) / args.count
fail_rate = str(fail_rate).split('.') fail_rate = str(fail_rate).split('.')

View File

@@ -24,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
__version__ = '0.0.g-dev' __version__ = '0.0.h-dev'
def main(args, seed=None): def main(args, seed=None):