Skip to content

Commit

Permalink
Merge pull request #2 from TestRunnerSRL/Dev
Browse files Browse the repository at this point in the history
Update fork to latest Dev
  • Loading branch information
DobbyDigital authored Sep 26, 2021
2 parents c95d62b + 3a7a4b7 commit e53760c
Show file tree
Hide file tree
Showing 41 changed files with 8,596 additions and 128 deletions.
32 changes: 28 additions & 4 deletions GUI/src/app/pages/generator/generator.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class GeneratorComponent implements OnInit {
return filteredTabList;
}

generateSeed(fromPatchFile: boolean = false, webRaceSeed: boolean = false) {
generateSeed(fromPatchFile: boolean = false, webRaceSeed: boolean = false, goalHintsConfirmed: boolean = false) {

this.generateSeedButtonEnabled = false;
this.seedString = this.seedString.trim().replace(/[^a-zA-Z0-9_-]/g, '');
Expand All @@ -163,16 +163,40 @@ export class GeneratorComponent implements OnInit {
//console.log(this.global.generator_settingsMap);
//console.log(this.global.generator_customColorMap);

if (this.global.getGlobalVar('electronAvailable')) { //Electron

//Delay the generation if settings are currently locked to avoid race conditions
//Delay the generation if settings are currently locked to avoid race conditions.
//Do this here so the goal hint confirmation dialog can be defined once for
//Electron and Web
if (this.global.getGlobalVar('electronAvailable')) {
if (this.settingsLocked) {
setTimeout(() => {
this.generateSeed(fromPatchFile, webRaceSeed);
}, 50);

return;
}
}

let goalErrorText = "The selected hint distribution includes the Goal hint type. This can drastically increase generation time for large multiworld seeds. Continue?";
let goalDistros = this.global.getGlobalVar('generatorGoalDistros');

if (!goalHintsConfirmed && goalDistros.indexOf(this.global.generator_settingsMap["hint_dist"]) > -1 && this.global.generator_settingsMap["world_count"] > 5) {
this.dialogService.open(ConfirmationWindow, {
autoFocus: true, closeOnBackdropClick: false, closeOnEsc: false, hasBackdrop: true, hasScroll: false, context: { dialogHeader: "Goal Hint Warning", dialogMessage: goalErrorText }
}).onClose.subscribe(confirmed => {
//User acknowledged increased generation time for multiworld seeds with goal hints
if (confirmed) {
this.generateSeed(fromPatchFile, webRaceSeed, true);
}
});

this.generateSeedButtonEnabled = true;
this.cd.markForCheck();
this.cd.detectChanges();

return;
}

if (this.global.getGlobalVar('electronAvailable')) { //Electron

//Hack: Fix Generation Count being None occasionally
if (!this.global.generator_settingsMap["count"] || this.global.generator_settingsMap["count"] < 1)
Expand Down
5 changes: 4 additions & 1 deletion GUI/src/app/providers/GUIGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export class GUIGlobal {
["generatorSettingsArray", []],
["generatorSettingsObj", {}],
["generatorCosmeticsArray", []],
["generatorCosmeticsObj", {}]
["generatorCosmeticsObj", {}],
["generatorGoalDistros", []]
]);
}

Expand Down Expand Up @@ -483,12 +484,14 @@ export class GUIGlobal {
console.log("JSON Settings Data:", guiSettings);
console.log("Last User Settings:", userSettings);
console.log("Final Settings Map", this.generator_settingsMap);
console.log("Goal Hint Distros", guiSettings.distroArray);

//Save settings after parsing them
this.setGlobalVar('generatorSettingsArray', guiSettings.settingsArray);
this.setGlobalVar('generatorSettingsObj', guiSettings.settingsObj);
this.setGlobalVar('generatorCosmeticsArray', guiSettings.cosmeticsArray);
this.setGlobalVar('generatorCosmeticsObj', guiSettings.cosmeticsObj);
this.setGlobalVar('generatorGoalDistros', guiSettings.distroArray);

this.generator_presets = guiSettings.presets;
}
Expand Down
344 changes: 344 additions & 0 deletions Goals.py

Large diffs are not rendered by default.

73 changes: 50 additions & 23 deletions HintList.py

Large diffs are not rendered by default.

106 changes: 99 additions & 7 deletions Hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,7 @@ def get_woth_hint(spoiler, world, checked):
else:
location_text = get_hint_area(location)

if world.settings.triforce_hunt:
return (GossipText('#%s# is on the path of gold.' % location_text, ['Light Blue']), location)
else:
return (GossipText('#%s# is on the way of the hero.' % location_text, ['Light Blue']), location)

return (GossipText('#%s# is on the way of the hero.' % location_text, ['Light Blue']), location)

def get_checked_areas(world, checked):
def get_area_from_name(check):
Expand All @@ -366,6 +362,102 @@ def get_area_from_name(check):

return set(get_area_from_name(check) for check in checked)

def get_goal_category(spoiler, world, goal_categories):
cat_sizes = []
cat_names = []
zero_weights = True
goal_category = None
for cat_name, category in goal_categories.items():
# Only add weights if the category has goals with hintable items
if world.id in spoiler.goal_locations and cat_name in spoiler.goal_locations[world.id]:
# Build lists for weighted choice
if category.weight > 0:
zero_weights = False
cat_sizes.append(category.weight)
cat_names.append(category.name)
# Depends on category order to choose next in the priority list
# Each category is guaranteed a hint first round, then weighted based on goal count
if not goal_category and category.name not in world.hinted_categories:
goal_category = category
world.hinted_categories.append(category.name)

# random choice if each category has at least one hint
if not goal_category and len(cat_names) > 0:
if zero_weights:
goal_category = goal_categories[random.choice(cat_names)]
else:
goal_category = goal_categories[random.choices(cat_names, weights=cat_sizes)[0]]

return goal_category

def get_goal_hint(spoiler, world, checked):
goal_category = get_goal_category(spoiler, world, world.goal_categories)

# check if no goals were generated (and thus no categories available)
if not goal_category:
return None

goals = goal_category.goals
goal_locations = []

# Choose random goal and check if any locations are already hinted.
# If all locations for a goal are hinted, remove the goal from the list and try again.
# If all locations for all goals are hinted, try remaining goal categories
# If all locations for all goal categories are hinted, return no hint.
while not goal_locations:
if not goals:
del world.goal_categories[goal_category.name]
goal_category = get_goal_category(spoiler, world, world.goal_categories)
if not goal_category:
return None
else:
goals = goal_category.goals

weights = []
zero_weights = True
for goal in goals:
if goal.weight > 0:
zero_weights = False
weights.append(goal.weight)

if zero_weights:
goal = random.choice(goals)
else:
goal = random.choices(goals, weights=weights)[0]

goal_locations = list(filter(lambda location:
location[0].name not in checked
and location[0].name not in world.hint_exclusions
and location[0].name not in world.hint_type_overrides['goal']
and location[0].item.name not in world.item_hint_type_overrides['goal'],
goal.required_locations))

if not goal_locations:
goals.remove(goal)

# Goal weight to zero mitigates double hinting this goal
# Once all goals in a category are 0, selection is true random
goal.weight = 0
location_tuple = random.choice(goal_locations)
location = location_tuple[0]
world_ids = location_tuple[3]
world_id = random.choice(world_ids)
checked.add(location.name)

if location.parent_region.dungeon:
location_text = getHint(location.parent_region.dungeon.name, world.settings.clearer_hints).text
else:
location_text = get_hint_area(location)

if world_id == world.id:
player_text = "the"
goal_text = goal.hint_text
else:
player_text = "Player %s's" % (world_id + 1)
goal_text = spoiler.goal_categories[world_id][goal_category.name].get_goal(goal.name).hint_text

return (GossipText('#%s# is on %s %s.' % (location_text, player_text, goal_text), [goal.color, 'Light Blue']), location)


def get_barren_hint(spoiler, world, checked):
if not hasattr(world, 'get_barren_hint_prev'):
Expand Down Expand Up @@ -612,6 +704,7 @@ def get_junk_hint(spoiler, world, checked):
'trial': lambda spoiler, world, checked: None,
'always': lambda spoiler, world, checked: None,
'woth': get_woth_hint,
'goal': get_goal_hint,
'barren': get_barren_hint,
'item': get_good_item_hint,
'sometimes': get_sometimes_hint,
Expand All @@ -628,6 +721,7 @@ def get_junk_hint(spoiler, world, checked):
'trial',
'always',
'woth',
'goal',
'barren',
'item',
'song',
Expand Down Expand Up @@ -705,8 +799,6 @@ def buildGossipHints(spoiler, worlds):

# builds out general hints based on location and whether an item is required or not
def buildWorldGossipHints(spoiler, world, checkedLocations=None):
# rebuild hint exclusion list
hintExclusions(world, clear_cache=True)

world.barren_dungeon = 0
world.woth_dungeon = 0
Expand Down
21 changes: 1 addition & 20 deletions Item.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,26 +150,7 @@ def majoritem(self):

@property
def goalitem(self):
if self.name == 'Triforce Piece':
return self.world.settings.triforce_hunt
if self.name == 'Light Arrows':
return self.world.settings.bridge == 'vanilla'
if self.info.medallion:
settings = ['medallions', 'dungeons']
if self.name in ['Shadow Medallion', 'Spirit Medallion']:
settings.append('vanilla')
return self.world.settings.bridge in settings \
or self.world.settings.shuffle_ganon_bosskey in ['medallions', 'dungeons'] \
or (self.world.settings.shuffle_ganon_bosskey == 'on_lacs' and self.world.settings.lacs_condition in settings)
if self.info.stone:
return self.world.settings.bridge in ['stones', 'dungeons'] \
or self.world.settings.shuffle_ganon_bosskey in ['stones', 'dungeons'] \
or (self.world.settings.shuffle_ganon_bosskey == 'on_lacs' and self.world.settings.lacs_condition in ['stones', 'dungeons'])
if self.type == 'Token':
return self.world.settings.bridge == 'tokens' \
or self.world.settings.shuffle_ganon_bosskey == 'tokens' \
or (self.world.settings.shuffle_ganon_bosskey == 'on_lacs' and self.world.settings.lacs_condition == 'tokens')
#TODO check Bingo goals
return self.name in self.world.goal_items


def __str__(self):
Expand Down
5 changes: 1 addition & 4 deletions ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@
'Bombchus (5)': 1,
'Bombchus (10)': 0,
'Bombchus (20)': 0,
'Nayrus Love': 0,
'Magic Meter': 1,
'Nayrus Love': 1,
'Double Defense': 0,
'Deku Stick Capacity': 0,
'Deku Nut Capacity': 0,
Expand Down Expand Up @@ -1337,9 +1337,6 @@ def get_pool_core(world):
for item,max in item_difficulty_max[world.settings.item_pool_value].items():
replace_max_item(pool, item, max)

if world.settings.damage_multiplier in ['ohko', 'quadruple'] and world.settings.item_pool_value == 'minimal':
pending_junk_pool.append('Nayrus Love')

world.distribution.alter_pool(world, pool)

# Make sure our pending_junk_pool is empty. If not, remove some random junk here.
Expand Down
60 changes: 6 additions & 54 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from Search import Search, RewindableSearch
from EntranceShuffle import set_entrances
from LocationList import set_drop_location_names
from Goals import update_goal_items, maybe_set_light_arrows, replace_goal_names


class dummy_window():
Expand Down Expand Up @@ -120,6 +121,8 @@ def resolve_settings(settings, window=dummy_window()):
def generate(settings, window=dummy_window()):
worlds = build_world_graphs(settings, window=window)
place_items(settings, worlds, window=window)
if worlds[0].enable_goal_hints:
replace_goal_names(worlds)
return make_spoiler(settings, worlds, window=window)


Expand Down Expand Up @@ -189,7 +192,7 @@ def make_spoiler(settings, worlds, window=dummy_window()):
if settings.create_spoiler or settings.hints != 'none':
window.update_status('Calculating Hint Data')
logger.info('Calculating hint data.')
update_required_items(spoiler)
update_goal_items(spoiler)
buildGossipHints(spoiler, worlds)
window.update_progress(55)
elif settings.misc_hints:
Expand Down Expand Up @@ -541,66 +544,13 @@ def copy_worlds(worlds):
return worlds


def maybe_set_light_arrows(location):
if not location.item.world.light_arrow_location and location.item and location.item.name == 'Light Arrows':
location.item.world.light_arrow_location = location
logging.getLogger('').debug(f'Light Arrows [{location.item.world.id}] set to [{location.name}]')


def find_light_arrows(spoiler):
search = Search([world.state for world in spoiler.worlds])
for location in search.iter_reachable_locations(search.progression_locations()):
search.collect(location.item)
maybe_set_light_arrows(location)


def update_required_items(spoiler):
worlds = spoiler.worlds

# get list of all of the progressive items that can appear in hints
# all_locations: all progressive items. have to collect from these
# item_locations: only the ones that should appear as "required"/WotH
all_locations = [location for world in worlds for location in world.get_filled_locations()]
# Set to test inclusion against
item_locations = {location for location in all_locations if location.item.majoritem and not location.locked and location.item.name != 'Triforce Piece'}

# if the playthrough was generated, filter the list of locations to the
# locations in the playthrough. The required locations is a subset of these
# locations. Can't use the locations directly since they are location to the
# copied spoiler world, so must compare via name and world id
if spoiler.playthrough:
translate = lambda loc: worlds[loc.world.id].get_location(loc.name)
spoiler_locations = set(map(translate, itertools.chain.from_iterable(spoiler.playthrough.values())))
item_locations &= spoiler_locations
# Skip even the checks
_maybe_set_light_arrows = lambda _: None
else:
_maybe_set_light_arrows = maybe_set_light_arrows

required_locations = []

search = Search([world.state for world in worlds])

for location in search.iter_reachable_locations(all_locations):
# Try to remove items one at a time and see if the game is still beatable
if location in item_locations:
old_item = location.item
location.item = None
# copies state! This is very important as we're in the middle of a search
# already, but beneficially, has search it can start from
if not search.can_beat_game():
required_locations.append(location)
location.item = old_item
_maybe_set_light_arrows(location)
search.state_list[location.item.world.id].collect(location.item)

# Filter the required location to only include location in the world
required_locations_dict = {}
for world in worlds:
required_locations_dict[world.id] = list(filter(lambda location: location.world.id == world.id, required_locations))
spoiler.required_locations = required_locations_dict


def create_playthrough(spoiler):
worlds = spoiler.worlds
if worlds[0].check_beatable_only and not Search([world.state for world in worlds]).can_beat_game():
Expand Down Expand Up @@ -643,6 +593,8 @@ def create_playthrough(spoiler):
search.state_list[location.item.world.id].collect(location.item)
maybe_set_light_arrows(location)
logger.info('Collected %d spheres', len(collection_spheres))
spoiler.full_playthrough = dict((location.name, i + 1) for i, sphere in enumerate(collection_spheres) for location in sphere)
spoiler.max_sphere = len(collection_spheres)

# Reduce each sphere in reverse order, by checking if the game is beatable
# when we remove the item. We do this to make sure that progressive items
Expand Down
Loading

0 comments on commit e53760c

Please sign in to comment.