Skip to content

Commit

Permalink
Merge pull request #1 from Elagatua/feature/triforce-blitz-rebase
Browse files Browse the repository at this point in the history
Triforce blitz v0.1
  • Loading branch information
Elagatua authored Jan 5, 2022
2 parents 84696bf + d620fc3 commit d5344d1
Show file tree
Hide file tree
Showing 30 changed files with 408 additions and 29 deletions.
13 changes: 12 additions & 1 deletion Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from Rules import set_shop_rules
from Location import DisableType
from LocationList import location_groups
from ItemPool import songlist, get_junk_item, item_groups, remove_junk_items, remove_junk_set
from ItemPool import songlist, get_junk_item, item_groups, remove_junk_items, remove_junk_set, triforce_blitz_items
from ItemList import item_table
from Item import ItemFactory
from Search import Search
Expand Down Expand Up @@ -367,6 +367,13 @@ def fill_restrictive(window, worlds, base_search, locations, itempool, count=-1)
items_search.collect_all(itempool)
logging.getLogger('').debug(f'Placing {len(itempool)} items among {len(locations)} potential locations.')

dungeons = [dungeon for world in worlds for dungeon in world.dungeons]
all_dungeon_locations = []

for dungeon in dungeons:
dungeon_locations = [location for region in dungeon.regions for location in region.locations]
all_dungeon_locations.extend(dungeon_locations)

# loop until there are no items or locations
while itempool and locations:
# if remaining count is 0, return. Negative means unbounded.
Expand Down Expand Up @@ -431,6 +438,10 @@ def fill_restrictive(window, worlds, base_search, locations, itempool, count=-1)
if not can_reach:
continue

# Triforce blitz pieces can only be placed in dungeons
if item_to_place.name in triforce_blitz_items and location not in all_dungeon_locations:
continue

if location.disabled == DisableType.PENDING:
if not max_search.can_beat_game(False):
continue
Expand Down
3 changes: 2 additions & 1 deletion Goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from HintList import goalTable, getHintGroup, hintExclusions
from Search import Search
from ItemPool import triforce_items


validColors = [
Expand Down Expand Up @@ -147,7 +148,7 @@ def update_goal_items(spoiler):
# 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'}
item_locations = {location for location in all_locations if location.item.majoritem and not location.locked and location.item.name not in triforce_items}

# required_locations[category.name][goal.name][world_id] = [...]
required_locations = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
Expand Down
3 changes: 3 additions & 0 deletions HintList.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ def tokens_required_by_settings(world):
# # sets color to white (currently only used for dungeon reward hints).
hintTable = {
'Triforce Piece': (["a triumph fork", "cheese", "a gold fragment"], "a Piece of the Triforce", "item"),
'Triforce of Power': (["a triumph fork", "cheese", "a gold fragment"], "the Triforce of Power", "item"),
'Triforce of Wisdom': (["a triumph fork", "cheese", "a gold fragment"], "the Triforce of Wisdom", "item"),
'Triforce of Courage': (["a triumph fork", "cheese", "a gold fragment"], "the Triforce of Courage", "item"),
'Magic Meter': (["mystic training", "pixie dust", "a green rectangle"], "a Magic Meter", 'item'),
'Double Defense': (["a white outline", "damage decrease", "strengthened love"], "Double Defense", 'item'),
'Slingshot': (["a seed shooter", "a rubberband", "a child's catapult"], "a Slingshot", 'item'),
Expand Down
103 changes: 91 additions & 12 deletions Hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,35 @@ def isRestrictedDungeonItem(dungeon, item):
return False


def add_hint(spoiler, world, groups, gossip_text, count, location=None, force_reachable=False):
def add_hint(spoiler, world, groups, gossip_text, count, location=None, force_reachable=False, hint_type=None):
random.shuffle(groups)
skipped_groups = []
duplicates = []
first = True
success = True

# Prevent randomizer from placing always hints in specified locations
if hint_type == 'always' and 'remove_always_stones' in world.hint_dist_user:
removed_stones = world.hint_dist_user['remove_always_stones']
for group in groups:
gossip_names = [gossipLocations[id].name for id in group]
if any(map(lambda name: name in removed_stones, gossip_names)):
groups.remove(group)
skipped_groups.append(group)

# early failure if not enough
if len(groups) < int(count):
return False

# move all priority stones to the front of the list so they get picked first
if 'priority_stones' in world.hint_dist_user:
priority_stones = world.hint_dist_user['priority_stones']
priority_groups = list(map(lambda group: list(set(priority_stones) & set([gossipLocations[id].name for id in group])), groups))
for index, group in enumerate(priority_groups):
if group:
priority_group = groups.pop(index)
groups.insert(0, priority_group)

# Randomly round up, if we have enough groups left
total = int(random.random() + count) if len(groups) > count else int(count)
while total:
Expand All @@ -159,6 +179,7 @@ def add_hint(spoiler, world, groups, gossip_text, count, location=None, force_re
if any(map(lambda id: gossipLocations[id].reachable, group)):
stone_names = [gossipLocations[id].location for id in group]
stone_locations = [world.get_location(stone_name) for stone_name in stone_names]

if not first or any(map(lambda stone_location: can_reach_hint(spoiler.worlds, stone_location, location), stone_locations)):
if first and location:
# just name the event item after the gossip stone directly
Expand Down Expand Up @@ -351,14 +372,14 @@ 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):
def get_goal_category(spoiler, world, goal_categories, skip_empty = True):
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]:
if (world.id in spoiler.goal_locations and cat_name in spoiler.goal_locations[world.id]) or not skip_empty:
# Build lists for weighted choice
if category.weight > 0:
zero_weights = False
Expand Down Expand Up @@ -447,6 +468,57 @@ def get_goal_hint(spoiler, world, checked):

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

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

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

goals = goal_category.goals
goal = None
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:
if not goals:
del world.goal_categories[goal_category.name]
goal_category = get_goal_category(spoiler, world, world.goal_categories, False)
if not goal_category:
return None
else:
goals = goal_category.goals

unchecked_goals = list(filter(lambda goal:
goal.name not in checked,
goals
))

if not unchecked_goals:
return None

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

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

goal_locations = goal.required_locations

checked.add(goal.name)
item_count = len(goal_locations)
item_text = 'step' if item_count == 1 else 'steps'

return (GossipText('#%s# requires #%d# %s.' % (goal.hint_text, item_count, item_text), ['Light Blue', goal.color]), None)

def get_barren_hint(spoiler, world, checked):
if not hasattr(world, 'get_barren_hint_prev'):
Expand Down Expand Up @@ -790,6 +862,7 @@ def get_junk_hint(spoiler, world, checked):
'always': lambda spoiler, world, checked: None,
'woth': get_woth_hint,
'goal': get_goal_hint,
'goal-count': get_goal_count_hint,
'barren': get_barren_hint,
'item': get_good_item_hint,
'sometimes': get_sometimes_hint,
Expand All @@ -807,6 +880,7 @@ def get_junk_hint(spoiler, world, checked):
'always',
'woth',
'goal',
'goal-count',
'barren',
'item',
'song',
Expand Down Expand Up @@ -1035,23 +1109,28 @@ def buildWorldGossipHints(spoiler, world, checkedLocations=None):
if '#' not in location_text:
location_text = '#%s#' % location_text
item_text = getHint(getItemGenericName(location.item), world.settings.clearer_hints).text
add_hint(spoiler, world, stoneGroups, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
add_hint(spoiler, world, stoneGroups, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']),
hint_dist['always'][1], location, force_reachable=True, hint_type='always')
logging.getLogger('').debug('Placed always hint for %s.', location.name)

# Add trial hints, only if hint copies > 0
if hint_dist['trial'][1] > 0:
if world.settings.trials_random and world.settings.trials == 6:
add_hint(spoiler, world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True)
add_hint(spoiler, world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']),
hint_dist['trial'][1], force_reachable=True, hint_type='trial')
elif world.settings.trials_random and world.settings.trials == 0:
add_hint(spoiler, world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True)
add_hint(spoiler, world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']),
hint_dist['trial'][1], force_reachable=True, hint_type='trial')
elif world.settings.trials < 6 and world.settings.trials > 3:
for trial,skipped in world.skipped_trials.items():
if skipped:
add_hint(spoiler, world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True)
add_hint(spoiler, world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']),
hint_dist['trial'][1], force_reachable=True, hint_type='trial')
elif world.settings.trials <= 3 and world.settings.trials > 0:
for trial,skipped in world.skipped_trials.items():
if not skipped:
add_hint(spoiler, world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True)
add_hint(spoiler, world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']),
hint_dist['trial'][1], force_reachable=True, hint_type='trial')

# Add user-specified hinted item locations if using a built-in hint distribution
# Raise error if hint copies is zero
Expand All @@ -1063,7 +1142,7 @@ def buildWorldGossipHints(spoiler, world, checkedLocations=None):
hint = get_specific_item_hint(spoiler, world, checkedLocations | checkedAlwaysLocations)
if hint:
gossip_text, location = hint
place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, hint_dist['named-item'][1], location)
place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, hint_dist['named-item'][1], location, hint_type='named-item')
if not place_ok:
raise Exception('Not enough gossip stones for user-provided item hints')

Expand Down Expand Up @@ -1132,15 +1211,15 @@ def buildWorldGossipHints(spoiler, world, checkedLocations=None):
hint_dist[hint_type] = (0.0, copies)
else:
gossip_text, location = hint
place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, copies, location)
place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, copies, location, hint_type=hint_type)
if place_ok:
hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1
if location is None:
logging.getLogger('').debug('Placed %s hint.', hint_type)
else:
logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name)
logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location)
if not place_ok and custom_fixed:
logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name)
logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location)
fixed_hint_types.insert(0, hint_type)


Expand Down
4 changes: 4 additions & 0 deletions Item.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ def goalitem(self):
def __str__(self):
return str(self.__unicode__())


def __repr__(self):
return str(self.__unicode__())


def __unicode__(self):
return '%s' % self.name
Expand Down
3 changes: 3 additions & 0 deletions ItemList.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@
'Double Defense': ('Item', True, 0xB8, None),
'Magic Bean Pack': ('Item', True, 0xC9, None),
'Triforce Piece': ('Item', True, 0xCA, {'progressive': float('Inf')}),
'Triforce of Power': ('Item', True, 0xCA, {'progressive': float('Inf')}),
'Triforce of Wisdom': ('Item', True, 0xCA, {'progressive': float('Inf')}),
'Triforce of Courage': ('Item', True, 0xCA, {'progressive': float('Inf')}),
'Zeldas Letter': ('Item', True, 0x0B, None),
'Time Travel': ('Event', True, None, None),
'Scarecrow Song': ('Event', True, None, None),
Expand Down
23 changes: 22 additions & 1 deletion ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@
},
}

triforce_items = ([
'Triforce Piece',
'Triforce of Power',
'Triforce of Wisdom',
'Triforce of Courage'
])


triforce_blitz_items = ([
'Triforce of Power',
'Triforce of Wisdom',
'Triforce of Courage'
])


TriforceCounts = {
'plentiful': Decimal(2.00),
'balanced': Decimal(1.50),
Expand Down Expand Up @@ -689,7 +704,10 @@
'Bombchus (10)',
'Bombchus (20)',
'Odd Potion',
'Triforce Piece'
'Triforce Piece',
'Triforce of Power',
'Triforce of Wisdom',
'Triforce of Courage',
]

item_groups = {
Expand Down Expand Up @@ -1309,6 +1327,9 @@ def get_pool_core(world):
triforce_count = int((TriforceCounts[world.settings.item_pool_value] * world.settings.triforce_goal_per_world).to_integral_value(rounding=ROUND_HALF_UP))
pending_junk_pool.extend(['Triforce Piece'] * triforce_count)

if world.settings.triforce_blitz:
pending_junk_pool.extend(triforce_blitz_items)

if world.settings.shuffle_ganon_bosskey == 'on_lacs':
placed_items['ToT Light Arrows Cutscene'] = 'Boss Key (Ganons Castle)'
elif world.settings.shuffle_ganon_bosskey == 'vanilla':
Expand Down
4 changes: 4 additions & 0 deletions Location.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def __str__(self):
return str(self.__unicode__())


def __repr__(self):
return str(self.__unicode__())


def __unicode__(self):
return '%s' % self.name

Expand Down
2 changes: 2 additions & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def build_world_graphs(settings, window=dummy_window()):

if settings.triforce_hunt:
settings.distribution.configure_triforce_hunt(worlds)
if settings.triforce_blitz:
settings.distribution.configure_triforce_blitz(worlds)

logger.info('Setting Entrances.')
set_entrances(worlds)
Expand Down
4 changes: 4 additions & 0 deletions Patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,10 @@ def set_entrance_updates(entrances):
rom.write_int16(rom.sym('triforce_pieces_requied'), world.triforce_goal)
rom.write_int16(rom.sym('triforce_hunt_enabled'), 1)

if world.settings.triforce_blitz:
rom.write_int16(rom.sym('triforce_pieces_requied'), 3)
rom.write_int16(rom.sym('triforce_hunt_enabled'), 1)

# Set up Ganon's Boss Key conditions.
symbol = rom.sym('GANON_BOSS_KEY_CONDITION')
count_symbol = rom.sym('GANON_BOSS_KEY_CONDITION_COUNT')
Expand Down
14 changes: 13 additions & 1 deletion Plandomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from Hints import gossipLocations, GossipText
from Item import ItemFactory, ItemIterator, IsItem
from ItemList import item_table
from ItemPool import item_groups, get_junk_item
from ItemPool import item_groups, get_junk_item, triforce_blitz_items
from Location import LocationIterator, LocationFactory, IsLocation
from LocationList import location_groups, location_table
from Search import Search
Expand Down Expand Up @@ -301,6 +301,8 @@ def pattern_matcher(self, pattern):
# Special handling for things not included in base_pool
if self.distribution.settings.triforce_hunt:
self.major_group.append('Triforce Piece')
if self.distribution.settings.triforce_blitz:
self.major_group.extend(triforce_blitz_items)
major_tokens = ((self.distribution.settings.shuffle_ganon_bosskey == 'on_lacs' and
self.distribution.settings.lacs_condition == 'tokens') or
self.distribution.settings.shuffle_ganon_bosskey == 'tokens' or self.distribution.settings.bridge == 'tokens')
Expand Down Expand Up @@ -1030,6 +1032,16 @@ def configure_triforce_hunt(self, worlds):
world.total_starting_triforce_count = total_starting_count # used later in Rules.py


def configure_triforce_blitz(self, worlds):

for world in worlds:
total_count = 0
for item in triforce_blitz_items:
total_count += world.distribution.item_pool[item].count

world.triforce_count = total_count


def reset(self):
for world in self.world_dists:
world.update({}, update_all=True)
Expand Down
Loading

0 comments on commit d5344d1

Please sign in to comment.