From 48c6a6fb4c04f906dcad757c9ec9d1c5cd5a1cbf Mon Sep 17 00:00:00 2001 From: Spineraks Date: Wed, 21 Aug 2024 19:59:21 +0200 Subject: [PATCH 01/50] YachtDice: implement new game (#3482) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/yachtdice/Items.py | 118 + worlds/yachtdice/Locations.py | 79 + worlds/yachtdice/Options.py | 332 +++ worlds/yachtdice/Rules.py | 239 ++ worlds/yachtdice/YachtWeights.py | 3562 ++++++++++++++++++++++++ worlds/yachtdice/__init__.py | 533 ++++ worlds/yachtdice/docs/en_Yacht Dice.md | 15 + worlds/yachtdice/docs/setup_en.md | 21 + 10 files changed, 4903 insertions(+) create mode 100644 worlds/yachtdice/Items.py create mode 100644 worlds/yachtdice/Locations.py create mode 100644 worlds/yachtdice/Options.py create mode 100644 worlds/yachtdice/Rules.py create mode 100644 worlds/yachtdice/YachtWeights.py create mode 100644 worlds/yachtdice/__init__.py create mode 100644 worlds/yachtdice/docs/en_Yacht Dice.md create mode 100644 worlds/yachtdice/docs/setup_en.md diff --git a/README.md b/README.md index 0d9a41de9f..0e57bce53b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Currently, the following games are supported: * Old School Runescape * Kingdom Hearts 1 * Mega Man 2 +* Yacht Dice For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 6a3c8f45c1..cd1e859af9 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -202,6 +202,9 @@ # The Witness /worlds/witness/ @NewSoupVi @blastron +# Yacht Dice +/worlds/yachtdice/ @spinerak + # Yoshi's Island /worlds/yoshisisland/ @PinkSwitch diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py new file mode 100644 index 0000000000..fa52c93ad6 --- /dev/null +++ b/worlds/yachtdice/Items.py @@ -0,0 +1,118 @@ +import typing + +from BaseClasses import Item, ItemClassification + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + + +class YachtDiceItem(Item): + game: str = "Yacht Dice" + + +# the starting index is chosen semi-randomly to be 16871244000 + + +item_table = { + # victory item, always placed manually at goal location + "Victory": ItemData(16871244000 - 1, ItemClassification.progression), + "Dice": ItemData(16871244000, ItemClassification.progression), + "Dice Fragment": ItemData(16871244001, ItemClassification.progression), + "Roll": ItemData(16871244002, ItemClassification.progression), + "Roll Fragment": ItemData(16871244003, ItemClassification.progression), + "Fixed Score Multiplier": ItemData(16871244005, ItemClassification.progression), + "Step Score Multiplier": ItemData(16871244006, ItemClassification.progression), + "Category Ones": ItemData(16871244103, ItemClassification.progression), + "Category Twos": ItemData(16871244104, ItemClassification.progression), + "Category Threes": ItemData(16871244105, ItemClassification.progression), + "Category Fours": ItemData(16871244106, ItemClassification.progression), + "Category Fives": ItemData(16871244107, ItemClassification.progression), + "Category Sixes": ItemData(16871244108, ItemClassification.progression), + "Category Choice": ItemData(16871244109, ItemClassification.progression), + "Category Inverse Choice": ItemData(16871244110, ItemClassification.progression), + "Category Pair": ItemData(16871244111, ItemClassification.progression), + "Category Three of a Kind": ItemData(16871244112, ItemClassification.progression), + "Category Four of a Kind": ItemData(16871244113, ItemClassification.progression), + "Category Tiny Straight": ItemData(16871244114, ItemClassification.progression), + "Category Small Straight": ItemData(16871244115, ItemClassification.progression), + "Category Large Straight": ItemData(16871244116, ItemClassification.progression), + "Category Full House": ItemData(16871244117, ItemClassification.progression), + "Category Yacht": ItemData(16871244118, ItemClassification.progression), + "Category Distincts": ItemData(16871244123, ItemClassification.progression), + "Category Two times Ones": ItemData(16871244124, ItemClassification.progression), + "Category Half of Sixes": ItemData(16871244125, ItemClassification.progression), + "Category Twos and Threes": ItemData(16871244126, ItemClassification.progression), + "Category Sum of Odds": ItemData(16871244127, ItemClassification.progression), + "Category Sum of Evens": ItemData(16871244128, ItemClassification.progression), + "Category Double Threes and Fours": ItemData(16871244129, ItemClassification.progression), + "Category Quadruple Ones and Twos": ItemData(16871244130, ItemClassification.progression), + "Category Micro Straight": ItemData(16871244131, ItemClassification.progression), + "Category Three Odds": ItemData(16871244132, ItemClassification.progression), + "Category 1-2-1 Consecutive": ItemData(16871244133, ItemClassification.progression), + "Category Three Distinct Dice": ItemData(16871244134, ItemClassification.progression), + "Category Two Pair": ItemData(16871244135, ItemClassification.progression), + "Category 2-1-2 Consecutive": ItemData(16871244136, ItemClassification.progression), + "Category Five Distinct Dice": ItemData(16871244137, ItemClassification.progression), + "Category 4&5 Full House": ItemData(16871244138, ItemClassification.progression), + # filler items + "Encouragement": ItemData(16871244200, ItemClassification.filler), + "Fun Fact": ItemData(16871244201, ItemClassification.filler), + "Story Chapter": ItemData(16871244202, ItemClassification.filler), + "Good RNG": ItemData(16871244203, ItemClassification.filler), + "Bad RNG": ItemData(16871244204, ItemClassification.trap), + "Bonus Point": ItemData(16871244205, ItemClassification.useful), # not included in logic + # These points are included in the logic and might be necessary to progress. + "1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing), + "10 Points": ItemData(16871244302, ItemClassification.progression), + "100 Points": ItemData(16871244303, ItemClassification.progression), +} + +# item groups for better hinting +item_groups = { + "Score Multiplier": { + "Step Score Multiplier", + "Fixed Score Multiplier" + }, + "Categories": { + "Category Ones", + "Category Twos", + "Category Threes", + "Category Fours", + "Category Fives", + "Category Sixes", + "Category Choice", + "Category Inverse Choice", + "Category Pair", + "Category Three of a Kind", + "Category Four of a Kind", + "Category Tiny Straight", + "Category Small Straight", + "Category Large Straight", + "Category Full House", + "Category Yacht", + "Category Distincts", + "Category Two times Ones", + "Category Half of Sixes", + "Category Twos and Threes", + "Category Sum of Odds", + "Category Sum of Evens", + "Category Double Threes and Fours", + "Category Quadruple Ones and Twos", + "Category Micro Straight", + "Category Three Odds", + "Category 1-2-1 Consecutive", + "Category Three Distinct Dice", + "Category Two Pair", + "Category 2-1-2 Consecutive", + "Category Five Distinct Dice", + "Category 4&5 Full House", + }, + "Points": { + "100 Points", + "10 Points", + "1 Point", + "Bonus Point" + }, +} diff --git a/worlds/yachtdice/Locations.py b/worlds/yachtdice/Locations.py new file mode 100644 index 0000000000..a9a236466f --- /dev/null +++ b/worlds/yachtdice/Locations.py @@ -0,0 +1,79 @@ +import typing + +from BaseClasses import Location + + +class LocData(typing.NamedTuple): + id: int + region: str + score: int + + +class YachtDiceLocation(Location): + game: str = "Yacht Dice" + + def __init__(self, player: int, name: str, score: int, address: typing.Optional[int], parent): + super().__init__(player, name, address, parent) + self.yacht_dice_score = score + + +all_locations = {} +starting_index = 16871244500 # 500 more than the starting index for items (not necessary, but this is what it is now) + + +def all_locations_fun(max_score): + """ + Function that is called when this file is loaded, which loads in ALL possible locations, score 1 to 1000 + """ + return {f"{i} score": LocData(starting_index + i, "Board", i) for i in range(1, max_score + 1)} + + +def ini_locations(goal_score, max_score, number_of_locations, dif, skip_early_locations, number_of_players): + """ + function that loads in all locations necessary for the game, so based on options. + will make sure that goal_score and max_score are included locations + """ + scaling = 2 # parameter that determines how many low-score location there are. + # need more low-score locations or lower difficulties: + if dif == 1: + scaling = 3 + elif dif == 2: + scaling = 2.3 + + scores = [] + # the scores follow the function int( 1 + (percentage ** scaling) * (max_score-1) ) + # however, this will have many low values, sometimes repeating. + # to avoid repeating scores, highest_score keeps tracks of the highest score location + # and the next score will always be at least highest_score + 1 + # note that current_score is at most max_score-1 + highest_score = 0 + start_score = 0 + + if skip_early_locations: + scaling = 1.95 + if number_of_players > 2: + scaling = max(1.2, 2.2 - number_of_players * 0.1) + + for i in range(number_of_locations - 1): + percentage = i / number_of_locations + current_score = int(start_score + 1 + (percentage**scaling) * (max_score - start_score - 2)) + if current_score <= highest_score: + current_score = highest_score + 1 + highest_score = current_score + scores += [current_score] + + if goal_score != max_score: + # if the goal score is not in the list, find the closest one and make it the goal. + if goal_score not in scores: + closest_num = min(scores, key=lambda x: abs(x - goal_score)) + scores[scores.index(closest_num)] = goal_score + + scores += [max_score] + + location_table = {f"{score} score": LocData(starting_index + score, "Board", score) for score in scores} + + return location_table + + +# we need to run this function to initialize all scores from 1 to 1000, even though not all are used +all_locations = all_locations_fun(1000) diff --git a/worlds/yachtdice/Options.py b/worlds/yachtdice/Options.py new file mode 100644 index 0000000000..e687936224 --- /dev/null +++ b/worlds/yachtdice/Options.py @@ -0,0 +1,332 @@ +from dataclasses import dataclass + +from Options import Choice, OptionGroup, PerGameCommonOptions, Range + + +class GameDifficulty(Choice): + """ + Difficulty. This option determines how difficult the scores are to achieve. + Easy: for beginners. No luck required, just roll the dice and have fun. Lower final goal. + Medium: intended difficulty. If you play smart, you will finish the game without any trouble. + Hard: you will need to play smart and be lucky. + Extreme: really hard mode, which requires many brain wrinkles and insane luck. NOT RECOMMENDED FOR MULTIWORLDS. + """ + + display_name = "Game difficulty" + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_extreme = 4 + default = 2 + + +class ScoreForLastCheck(Range): + """ + The items in the item pool will always allow you to reach a score of 1000. + By default, the last check is also at a score of 1000. + However, you can set the score for the last check to be lower. This will make the game shorter and easier. + """ + + display_name = "Score for last check" + range_start = 500 + range_end = 1000 + default = 1000 + + +class ScoreForGoal(Range): + """ + This option determines what score you need to reach to finish the game. + It cannot be higher than the score for the last check (if it is, this option is changed automatically). + """ + + display_name = "Score for goal" + range_start = 500 + range_end = 1000 + default = 777 + + +class MinimalNumberOfDiceAndRolls(Choice): + """ + The minimal number of dice and rolls in the pool. + These are guaranteed, unlike the later items. + You can never get more than 8 dice and 5 rolls. + You start with one dice and one roll. + """ + + display_name = "Minimal number of dice and rolls in pool" + option_5_dice_and_3_rolls = 2 + option_5_dice_and_5_rolls = 3 + option_6_dice_and_4_rolls = 4 + option_7_dice_and_3_rolls = 5 + option_8_dice_and_2_rolls = 6 + default = 2 + + +class NumberDiceFragmentsPerDice(Range): + """ + Dice can be split into fragments, gathering enough will give you an extra dice. + You start with one dice, and there will always be one full dice in the pool. + The other dice are split into fragments, according to this option. + Setting this to 1 fragment per dice just puts "Dice" objects in the pool. + """ + + display_name = "Number of dice fragments per dice" + range_start = 1 + range_end = 5 + default = 4 + + +class NumberRollFragmentsPerRoll(Range): + """ + Rolls can be split into fragments, gathering enough will give you an extra roll. + You start with one roll, and there will always be one full roll in the pool. + The other three rolls are split into fragments, according to this option. + Setting this to 1 fragment per roll just puts "Roll" objects in the pool. + """ + + display_name = "Number of roll fragments per roll" + range_start = 1 + range_end = 5 + default = 4 + + +class AlternativeCategories(Range): + """ + There are 16 default categories, but there are also 16 alternative categories. + These alternative categories can be randomly selected to replace the default categories. + They are a little strange, but can give a fun new experience. + In the game, you can hover over categories to check what they do. + This option determines the number of alternative categories in your game. + """ + + display_name = "Number of alternative categories" + range_start = 0 + range_end = 16 + default = 0 + + +class ChanceOfDice(Range): + """ + The item pool is always filled in such a way that you can reach a score of 1000. + Extra progression items are added that will help you on your quest. + You can set the weight for each extra progressive item in the following options. + + Of course, more dice = more points! + """ + + display_name = "Weight of adding Dice" + range_start = 0 + range_end = 100 + default = 5 + + +class ChanceOfRoll(Range): + """ + With more rolls, you will be able to reach higher scores. + """ + + display_name = "Weight of adding Roll" + range_start = 0 + range_end = 100 + default = 20 + + +class ChanceOfFixedScoreMultiplier(Range): + """ + Getting a Fixed Score Multiplier will boost all future scores by 10%. + """ + + display_name = "Weight of adding Fixed Score Multiplier" + range_start = 0 + range_end = 100 + default = 30 + + +class ChanceOfStepScoreMultiplier(Range): + """ + The Step Score Multiplier boosts your multiplier after every roll by 1%, and resets on sheet reset. + So, keep high scoring categories for later to get the most out of them. + By default, this item is not included. It is fun however, you just need to know the above strategy. + """ + + display_name = "Weight of adding Step Score Multiplier" + range_start = 0 + range_end = 100 + default = 0 + + +class ChanceOfDoubleCategory(Range): + """ + This option allows categories to appear multiple times. + Each time you get a category after the first, its score value gets doubled. + """ + + display_name = "Weight of adding Category copy" + range_start = 0 + range_end = 100 + default = 50 + + +class ChanceOfPoints(Range): + """ + Are you tired of rolling dice countless times and tallying up points one by one, all by yourself? + Worry not, as this option will simply add some points items to the item pool! + And getting one of these points items gives you... points! + Imagine how nice it would be to find tons of them. Or even better, having others find them FOR you! + """ + + display_name = "Weight of adding Points" + range_start = 0 + range_end = 100 + default = 20 + + +class PointsSize(Choice): + """ + If you choose to add points to the item pool, you can choose to have many small points, + medium-size points, a few larger points, or a mix of them. + """ + + display_name = "Size of points" + option_small = 1 + option_medium = 2 + option_large = 3 + option_mix = 4 + default = 2 + + +class MinimizeExtraItems(Choice): + """ + Besides necessary items, Yacht Dice has extra useful/filler items in the item pool. + It is possible however to decrease the number of locations and extra items. + This option will: + - decrease the number of locations at the start (you'll start with 2 dice and 2 rolls). + - will limit the number of dice/roll fragments per dice/roll to 2. + - in multiplayer games, it will reduce the number of filler items. + """ + + display_name = "Minimize extra items" + option_no_dont = 1 + option_yes_please = 2 + default = 1 + + +class AddExtraPoints(Choice): + """ + Yacht Dice typically has space for extra items. + This option determines if bonus points are put into the item pool. + They make the game a little bit easier, as they are not considered in the logic. + + All Of It: fill all locations with extra points + Sure: put some bonus points in + Never: do not put any bonus points + """ + + display_name = "Extra bonus in the pool" + option_all_of_it = 1 + option_sure = 2 + option_never = 3 + default = 2 + + +class AddStoryChapters(Choice): + """ + Yacht Dice typically has space for more items. + This option determines if extra story chapters are put into the item pool. + Note: if you have extra points on "all_of_it", there will not be story chapters. + + All Of It: fill all locations with story chapters + Sure: if there is space left, put in 10 story chapters. + Never: do not put any story chapters in, I do not like reading (but I am glad you are reading THIS!) + """ + + display_name = "Extra story chapters in the pool" + option_all_of_it = 1 + option_sure = 2 + option_never = 3 + default = 3 + + +class WhichStory(Choice): + """ + The most important part of Yacht Dice is the narrative. + Of course you will need to add story chapters to the item pool. + You can read story chapters in the feed on the website and there are several stories to choose from. + """ + + display_name = "Story" + option_the_quest_of_the_dice_warrior = 1 + option_the_tragedy_of_fortunas_gambit = 2 + option_the_dicey_animal_dice_game = 3 + option_whispers_of_fate = 4 + option_a_yacht_dice_odyssey = 5 + option_a_rollin_rhyme_adventure = 6 + option_random_story = -1 + default = -1 + + +class AllowManual(Choice): + """ + If allowed, players can roll IRL dice and input them manually into the game. + By sending "manual" in the chat, an input field appears where you can type your dice rolls. + Of course, we cannot check anymore if the player is playing fair. + """ + + display_name = "Allow manual inputs" + option_yes_allow = 1 + option_no_dont_allow = 2 + default = 1 + + +@dataclass +class YachtDiceOptions(PerGameCommonOptions): + game_difficulty: GameDifficulty + score_for_last_check: ScoreForLastCheck + score_for_goal: ScoreForGoal + + minimal_number_of_dice_and_rolls: MinimalNumberOfDiceAndRolls + number_of_dice_fragments_per_dice: NumberDiceFragmentsPerDice + number_of_roll_fragments_per_roll: NumberRollFragmentsPerRoll + + alternative_categories: AlternativeCategories + + allow_manual_input: AllowManual + + # the following options determine what extra items are shuffled into the pool: + weight_of_dice: ChanceOfDice + weight_of_roll: ChanceOfRoll + weight_of_fixed_score_multiplier: ChanceOfFixedScoreMultiplier + weight_of_step_score_multiplier: ChanceOfStepScoreMultiplier + weight_of_double_category: ChanceOfDoubleCategory + weight_of_points: ChanceOfPoints + points_size: PointsSize + + minimize_extra_items: MinimizeExtraItems + add_bonus_points: AddExtraPoints + add_story_chapters: AddStoryChapters + which_story: WhichStory + + +yd_option_groups = [ + OptionGroup( + "Extra progression items", + [ + ChanceOfDice, + ChanceOfRoll, + ChanceOfFixedScoreMultiplier, + ChanceOfStepScoreMultiplier, + ChanceOfDoubleCategory, + ChanceOfPoints, + PointsSize, + ], + ), + OptionGroup( + "Other items", + [ + MinimizeExtraItems, + AddExtraPoints, + AddStoryChapters, + WhichStory + ], + ), +] diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py new file mode 100644 index 0000000000..1db5cebccd --- /dev/null +++ b/worlds/yachtdice/Rules.py @@ -0,0 +1,239 @@ +import math +from collections import Counter, defaultdict +from typing import List, Optional + +from BaseClasses import MultiWorld + +from worlds.generic.Rules import set_rule + +from .YachtWeights import yacht_weights + +# This module adds logic to the apworld. +# In short, we ran a simulation for every possible combination of dice and rolls you can have, per category. +# This simulation has a good strategy for locking dice. +# This gives rise to an approximate discrete distribution per category. +# We calculate the distribution of the total score. +# We then pick a correct percentile to reflect the correct score that should be in logic. +# The score is logic is *much* lower than the actual maximum reachable score. + + +class Category: + def __init__(self, name, quantity=1): + self.name = name + self.quantity = quantity # how many times you have the category + + # return mean score of a category + def mean_score(self, num_dice, num_rolls): + if num_dice <= 0 or num_rolls <= 0: + return 0 + mean_score = 0 + for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items(): + mean_score += key * value / 100000 + return mean_score * self.quantity + + +class ListState: + def __init__(self, state: List[str]): + self.state = state + self.item_counts = Counter(state) + + def count(self, item: str, player: Optional[str] = None) -> int: + return self.item_counts[item] + + +def extract_progression(state, player, frags_per_dice, frags_per_roll, allowed_categories): + """ + method to obtain a list of what items the player has. + this includes categories, dice, rolls and score multiplier etc. + First, we convert the state if it's a list, so we can use state.count(item, player) + """ + if isinstance(state, list): + state = ListState(state=state) + + number_of_dice = state.count("Dice", player) + state.count("Dice Fragment", player) // frags_per_dice + number_of_rerolls = state.count("Roll", player) + state.count("Roll Fragment", player) // frags_per_roll + number_of_fixed_mults = state.count("Fixed Score Multiplier", player) + number_of_step_mults = state.count("Step Score Multiplier", player) + + categories = [ + Category(category_name, state.count(category_name, player)) + for category_name in allowed_categories + if state.count(category_name, player) # want all categories that have count >= 1 + ] + + extra_points_in_logic = state.count("1 Point", player) + extra_points_in_logic += state.count("10 Points", player) * 10 + extra_points_in_logic += state.count("100 Points", player) * 100 + + return ( + categories, + number_of_dice, + number_of_rerolls, + number_of_fixed_mults * 0.1, + number_of_step_mults * 0.01, + extra_points_in_logic, + ) + + +# We will store the results of this function as it is called often for the same parameters. + + +yachtdice_cache = {} + + +def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff, player): + """ + Function that returns the feasible score in logic based on items obtained. + """ + tup = ( + tuple([c.name + str(c.quantity) for c in categories]), + num_dice, + num_rolls, + fixed_mult, + step_mult, + diff, + ) # identifier + + if player not in yachtdice_cache: + yachtdice_cache[player] = {} + + if tup in yachtdice_cache[player]: + return yachtdice_cache[player][tup] + + # sort categories because for the step multiplier, you will want low-scoring categories first + categories.sort(key=lambda category: category.mean_score(num_dice, num_rolls)) + + # function to add two discrete distribution. + # defaultdict is a dict where you don't need to check if an id is present, you can just use += (lot faster) + def add_distributions(dist1, dist2): + combined_dist = defaultdict(float) + for val1, prob1 in dist1.items(): + for val2, prob2 in dist2.items(): + combined_dist[val1 + val2] += prob1 * prob2 + return dict(combined_dist) + + # function to take the maximum of "times" i.i.d. dist1. + # (I have tried using defaultdict here too but this made it slower.) + def max_dist(dist1, mults): + new_dist = {0: 1} + for mult in mults: + temp_dist = {} + for val1, prob1 in new_dist.items(): + for val2, prob2 in dist1.items(): + new_val = int(max(val1, val2 * mult)) + new_prob = prob1 * prob2 + + # Update the probability for the new value + if new_val in temp_dist: + temp_dist[new_val] += new_prob + else: + temp_dist[new_val] = new_prob + new_dist = temp_dist + + return new_dist + + # Returns percentile value of a distribution. + def percentile_distribution(dist, percentile): + sorted_values = sorted(dist.keys()) + cumulative_prob = 0 + + for val in sorted_values: + cumulative_prob += dist[val] + if cumulative_prob >= percentile: + return val + + # Return the last value if percentile is higher than all probabilities + return sorted_values[-1] + + # parameters for logic. + # perc_return is, per difficulty, the percentages of total score it returns (it averages out the values) + # diff_divide determines how many shots the logic gets per category. Lower = more shots. + perc_return = [[0], [0.1, 0.5], [0.3, 0.7], [0.55, 0.85], [0.85, 0.95]][diff] + diff_divide = [0, 9, 7, 3, 2][diff] + + # calculate total distribution + total_dist = {0: 1} + for j, category in enumerate(categories): + if num_dice <= 0 or num_rolls <= 0: + dist = {0: 100000} + else: + dist = yacht_weights[category.name, min(8, num_dice), min(8, num_rolls)].copy() + + for key in dist.keys(): + dist[key] /= 100000 + + cat_mult = 2 ** (category.quantity - 1) + + # for higher difficulties, the simulation gets multiple tries for categories. + max_tries = j // diff_divide + mults = [(1 + fixed_mult + step_mult * ii) * cat_mult for ii in range(max(0, j - max_tries), j + 1)] + dist = max_dist(dist, mults) + + total_dist = add_distributions(total_dist, dist) + + # save result into the cache, then return it + outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return) + yachtdice_cache[player][tup] = max(5, math.floor(outcome)) # at least 5. + + # cache management; we rarely/never need more than 400 entries. But if for some reason it became large, + # delete the first entry of the player cache. + if len(yachtdice_cache[player]) > 400: + # Remove the oldest item + oldest_tup = next(iter(yachtdice_cache[player])) + del yachtdice_cache[player][oldest_tup] + + return yachtdice_cache[player][tup] + + +def dice_simulation_fill_pool(state, frags_per_dice, frags_per_roll, allowed_categories, difficulty, player): + """ + Returns the feasible score that one can reach with the current state, options and difficulty. + This function is called with state being a list, during filling of item pool. + """ + categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression( + state, "state_is_a_list", frags_per_dice, frags_per_roll, allowed_categories + ) + return ( + dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player) + expoints + ) + + +def dice_simulation_state_change(state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty): + """ + Returns the feasible score that one can reach with the current state, options and difficulty. + This function is called with state being a AP state object, while doing access rules. + """ + + if state.prog_items[player]["state_is_fresh"] == 0: + state.prog_items[player]["state_is_fresh"] = 1 + categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression( + state, player, frags_per_dice, frags_per_roll, allowed_categories + ) + state.prog_items[player]["maximum_achievable_score"] = ( + dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player) + + expoints + ) + + return state.prog_items[player]["maximum_achievable_score"] + + +def set_yacht_rules(world: MultiWorld, player: int, frags_per_dice, frags_per_roll, allowed_categories, difficulty): + """ + Sets rules on reaching scores + """ + + for location in world.get_locations(player): + set_rule( + location, + lambda state, curscore=location.yacht_dice_score, player=player: dice_simulation_state_change( + state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty + ) + >= curscore, + ) + + +def set_yacht_completion_rules(world: MultiWorld, player: int): + """ + Sets rules on completion condition + """ + world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py new file mode 100644 index 0000000000..ee387fdf21 --- /dev/null +++ b/worlds/yachtdice/YachtWeights.py @@ -0,0 +1,3562 @@ +# A file containing the results of our simulations. +# Every entry consists of a key. This key has input category, number of dice, and number of rolls. +# The value then shows a list of all possible scores to get, and how many times of 100000 it achieved. + +# example: ("Category Choice", 2, 2): +# {8: 13639, 9: 12220, 10: 13755, 5: 4889, 6: 9840, 7: 14772, 12: 7780, 11: 15622, 2: 1269, 3: 2445, 4: 3769} +# this example shows the outcomes for the category "Category Choice", with 2 dice and 2 rolls. +# 13639 out of 100000 times, a score of 8 was achieved for example. +yacht_weights = { + ("Category Ones", 0, 0): {0: 100000}, + ("Category Ones", 0, 1): {0: 100000}, + ("Category Ones", 0, 2): {0: 100000}, + ("Category Ones", 0, 3): {0: 100000}, + ("Category Ones", 0, 4): {0: 100000}, + ("Category Ones", 0, 5): {0: 100000}, + ("Category Ones", 0, 6): {0: 100000}, + ("Category Ones", 0, 7): {0: 100000}, + ("Category Ones", 0, 8): {0: 100000}, + ("Category Ones", 1, 0): {0: 100000}, + ("Category Ones", 1, 1): {0: 83416, 1: 16584}, + ("Category Ones", 1, 2): {0: 69346, 1: 30654}, + ("Category Ones", 1, 3): {0: 57756, 1: 42244}, + ("Category Ones", 1, 4): {0: 48709, 1: 51291}, + ("Category Ones", 1, 5): {0: 40214, 1: 59786}, + ("Category Ones", 1, 6): {0: 33491, 1: 66509}, + ("Category Ones", 1, 7): {0: 27838, 1: 72162}, + ("Category Ones", 1, 8): {0: 23094, 1: 76906}, + ("Category Ones", 2, 0): {0: 100000}, + ("Category Ones", 2, 1): {0: 69715, 1: 30285}, + ("Category Ones", 2, 2): {0: 48066, 1: 51934}, + ("Category Ones", 2, 3): {0: 33544, 1: 48585, 2: 17871}, + ("Category Ones", 2, 4): {0: 23342, 1: 50092, 2: 26566}, + ("Category Ones", 2, 5): {0: 16036, 1: 48250, 2: 35714}, + ("Category Ones", 2, 6): {0: 11355, 1: 44545, 2: 44100}, + ("Category Ones", 2, 7): {0: 7812, 1: 40248, 2: 51940}, + ("Category Ones", 2, 8): {0: 5395, 1: 35484, 2: 59121}, + ("Category Ones", 3, 0): {0: 100000}, + ("Category Ones", 3, 1): {0: 57462, 1: 42538}, + ("Category Ones", 3, 2): {0: 33327, 1: 44253, 2: 22420}, + ("Category Ones", 3, 3): {0: 19432, 1: 42237, 2: 38331}, + ("Category Ones", 3, 4): {0: 11191, 1: 36208, 2: 38606, 3: 13995}, + ("Category Ones", 3, 5): {0: 6536, 1: 28891, 2: 43130, 3: 21443}, + ("Category Ones", 3, 6): {0: 3697, 1: 22501, 2: 44196, 3: 29606}, + ("Category Ones", 3, 7): {0: 2134, 2: 60499, 3: 37367}, + ("Category Ones", 3, 8): {0: 1280, 2: 53518, 3: 45202}, + ("Category Ones", 4, 0): {0: 100000}, + ("Category Ones", 4, 1): {0: 48178, 1: 38635, 2: 13187}, + ("Category Ones", 4, 2): {0: 23349, 1: 40775, 2: 35876}, + ("Category Ones", 4, 3): {0: 11366, 1: 32547, 2: 35556, 3: 20531}, + ("Category Ones", 4, 4): {0: 5331, 1: 23241, 2: 37271, 3: 34157}, + ("Category Ones", 4, 5): {0: 2640, 2: 49872, 3: 47488}, + ("Category Ones", 4, 6): {0: 1253, 2: 39816, 3: 39298, 4: 19633}, + ("Category Ones", 4, 7): {0: 6915, 2: 24313, 3: 41680, 4: 27092}, + ("Category Ones", 4, 8): {0: 4228, 3: 61312, 4: 34460}, + ("Category Ones", 5, 0): {0: 100000}, + ("Category Ones", 5, 1): {0: 40042, 1: 40202, 2: 19756}, + ("Category Ones", 5, 2): {0: 16212, 1: 35432, 2: 31231, 3: 17125}, + ("Category Ones", 5, 3): {0: 6556, 1: 23548, 2: 34509, 3: 35387}, + ("Category Ones", 5, 4): {0: 2552, 2: 44333, 3: 32048, 4: 21067}, + ("Category Ones", 5, 5): {0: 8783, 2: 23245, 3: 34614, 4: 33358}, + ("Category Ones", 5, 6): {0: 4513, 3: 49603, 4: 32816, 5: 13068}, + ("Category Ones", 5, 7): {0: 2295, 3: 40470, 4: 37869, 5: 19366}, + ("Category Ones", 5, 8): {0: 73, 3: 33115, 4: 40166, 5: 26646}, + ("Category Ones", 6, 0): {0: 100000}, + ("Category Ones", 6, 1): {0: 33501, 1: 40042, 2: 26457}, + ("Category Ones", 6, 2): {0: 11326, 1: 29379, 2: 32368, 3: 26927}, + ("Category Ones", 6, 3): {0: 3764, 2: 46660, 3: 28928, 4: 20648}, + ("Category Ones", 6, 4): {0: 1231, 2: 29883, 3: 31038, 4: 37848}, + ("Category Ones", 6, 5): {0: 4208, 3: 41897, 4: 30878, 5: 23017}, + ("Category Ones", 6, 6): {0: 1850, 3: 30396, 4: 33022, 5: 34732}, + ("Category Ones", 6, 7): {0: 5503, 4: 48099, 5: 32432, 6: 13966}, + ("Category Ones", 6, 8): {0: 2896, 4: 39616, 5: 37005, 6: 20483}, + ("Category Ones", 7, 0): {0: 100000}, + ("Category Ones", 7, 1): {0: 27838, 1: 39224, 2: 32938}, + ("Category Ones", 7, 2): {0: 7796, 1: 23850, 2: 31678, 3: 23224, 4: 13452}, + ("Category Ones", 7, 3): {0: 2247, 2: 35459, 3: 29131, 4: 33163}, + ("Category Ones", 7, 4): {0: 5252, 3: 41207, 4: 28065, 5: 25476}, + ("Category Ones", 7, 5): {0: 174, 3: 29347, 4: 28867, 5: 26190, 6: 15422}, + ("Category Ones", 7, 6): {0: 4625, 4: 38568, 5: 30596, 6: 26211}, + ("Category Ones", 7, 7): {0: 230, 4: 30109, 5: 32077, 6: 37584}, + ("Category Ones", 7, 8): {0: 5519, 5: 45718, 6: 33357, 7: 15406}, + ("Category Ones", 8, 0): {0: 100000}, + ("Category Ones", 8, 1): {0: 23156, 1: 37295, 2: 26136, 3: 13413}, + ("Category Ones", 8, 2): {0: 5472, 2: 48372, 3: 25847, 4: 20309}, + ("Category Ones", 8, 3): {0: 8661, 3: 45896, 4: 24664, 5: 20779}, + ("Category Ones", 8, 4): {0: 2807, 3: 29707, 4: 27157, 5: 23430, 6: 16899}, + ("Category Ones", 8, 5): {0: 5173, 4: 36033, 5: 27792, 6: 31002}, + ("Category Ones", 8, 6): {0: 255, 4: 25642, 5: 27508, 6: 27112, 7: 19483}, + ("Category Ones", 8, 7): {0: 4236, 5: 35323, 6: 30438, 7: 30003}, + ("Category Ones", 8, 8): {0: 310, 5: 27692, 6: 30830, 7: 41168}, + ("Category Twos", 0, 0): {0: 100000}, + ("Category Twos", 0, 1): {0: 100000}, + ("Category Twos", 0, 2): {0: 100000}, + ("Category Twos", 0, 3): {0: 100000}, + ("Category Twos", 0, 4): {0: 100000}, + ("Category Twos", 0, 5): {0: 100000}, + ("Category Twos", 0, 6): {0: 100000}, + ("Category Twos", 0, 7): {0: 100000}, + ("Category Twos", 0, 8): {0: 100000}, + ("Category Twos", 1, 0): {0: 100000}, + ("Category Twos", 1, 1): {0: 83475, 2: 16525}, + ("Category Twos", 1, 2): {0: 69690, 2: 30310}, + ("Category Twos", 1, 3): {0: 57818, 2: 42182}, + ("Category Twos", 1, 4): {0: 48418, 2: 51582}, + ("Category Twos", 1, 5): {0: 40301, 2: 59699}, + ("Category Twos", 1, 6): {0: 33558, 2: 66442}, + ("Category Twos", 1, 7): {0: 28182, 2: 71818}, + ("Category Twos", 1, 8): {0: 23406, 2: 76594}, + ("Category Twos", 2, 0): {0: 100000}, + ("Category Twos", 2, 1): {0: 69724, 2: 30276}, + ("Category Twos", 2, 2): {0: 48238, 2: 42479, 4: 9283}, + ("Category Twos", 2, 3): {0: 33290, 2: 48819, 4: 17891}, + ("Category Twos", 2, 4): {0: 23136, 2: 49957, 4: 26907}, + ("Category Twos", 2, 5): {0: 16146, 2: 48200, 4: 35654}, + ("Category Twos", 2, 6): {0: 11083, 2: 44497, 4: 44420}, + ("Category Twos", 2, 7): {0: 7662, 2: 40343, 4: 51995}, + ("Category Twos", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Twos", 3, 0): {0: 100000}, + ("Category Twos", 3, 1): {0: 58021, 2: 34522, 4: 7457}, + ("Category Twos", 3, 2): {0: 33548, 2: 44261, 4: 22191}, + ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, + ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, + ("Category Twos", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, + ("Category Twos", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, + ("Category Twos", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, + ("Category Twos", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Twos", 4, 0): {0: 100000}, + ("Category Twos", 4, 1): {0: 48235, 2: 38602, 4: 13163}, + ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, + ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, + ("Category Twos", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, + ("Category Twos", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, + ("Category Twos", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, + ("Category Twos", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, + ("Category Twos", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Twos", 5, 0): {0: 100000}, + ("Category Twos", 5, 1): {0: 40028, 2: 40241, 4: 19731}, + ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, + ("Category Twos", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, + ("Category Twos", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, + ("Category Twos", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, + ("Category Twos", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, + ("Category Twos", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, + ("Category Twos", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Twos", 6, 0): {0: 100000}, + ("Category Twos", 6, 1): {0: 33502, 2: 40413, 4: 26085}, + ("Category Twos", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, + ("Category Twos", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, + ("Category Twos", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, + ("Category Twos", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, + ("Category Twos", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, + ("Category Twos", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, + ("Category Twos", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Twos", 7, 0): {0: 100000}, + ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, + ("Category Twos", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, + ("Category Twos", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, + ("Category Twos", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, + ("Category Twos", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, + ("Category Twos", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, + ("Category Twos", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, + ("Category Twos", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Twos", 8, 0): {0: 100000}, + ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, + ("Category Twos", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, + ("Category Twos", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, + ("Category Twos", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, + ("Category Twos", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, + ("Category Twos", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, + ("Category Twos", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, + ("Category Twos", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Threes", 0, 0): {0: 100000}, + ("Category Threes", 0, 1): {0: 100000}, + ("Category Threes", 0, 2): {0: 100000}, + ("Category Threes", 0, 3): {0: 100000}, + ("Category Threes", 0, 4): {0: 100000}, + ("Category Threes", 0, 5): {0: 100000}, + ("Category Threes", 0, 6): {0: 100000}, + ("Category Threes", 0, 7): {0: 100000}, + ("Category Threes", 0, 8): {0: 100000}, + ("Category Threes", 1, 0): {0: 100000}, + ("Category Threes", 1, 1): {0: 83343, 3: 16657}, + ("Category Threes", 1, 2): {0: 69569, 3: 30431}, + ("Category Threes", 1, 3): {0: 57872, 3: 42128}, + ("Category Threes", 1, 4): {0: 48081, 3: 51919}, + ("Category Threes", 1, 5): {0: 40271, 3: 59729}, + ("Category Threes", 1, 6): {0: 33201, 3: 66799}, + ("Category Threes", 1, 7): {0: 27903, 3: 72097}, + ("Category Threes", 1, 8): {0: 23240, 3: 76760}, + ("Category Threes", 2, 0): {0: 100000}, + ("Category Threes", 2, 1): {0: 69419, 3: 30581}, + ("Category Threes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, + ("Category Threes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, + ("Category Threes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, + ("Category Threes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, + ("Category Threes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, + ("Category Threes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, + ("Category Threes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Threes", 3, 0): {0: 100000}, + ("Category Threes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, + ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, + ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, + ("Category Threes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, + ("Category Threes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, + ("Category Threes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, + ("Category Threes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Threes", 4, 0): {0: 100000}, + ("Category Threes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, + ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, + ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, + ("Category Threes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, + ("Category Threes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, + ("Category Threes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, + ("Category Threes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Threes", 5, 0): {0: 100000}, + ("Category Threes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, + ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, + ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, + ("Category Threes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, + ("Category Threes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, + ("Category Threes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, + ("Category Threes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, + ("Category Threes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Threes", 6, 0): {0: 100000}, + ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, + ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, + ("Category Threes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, + ("Category Threes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, + ("Category Threes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, + ("Category Threes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, + ("Category Threes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, + ("Category Threes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Threes", 7, 0): {0: 100000}, + ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, + ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, + ("Category Threes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, + ("Category Threes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, + ("Category Threes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, + ("Category Threes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, + ("Category Threes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, + ("Category Threes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Threes", 8, 0): {0: 100000}, + ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, + ("Category Threes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, + ("Category Threes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, + ("Category Threes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, + ("Category Threes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, + ("Category Threes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, + ("Category Threes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, + ("Category Threes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, + ("Category Fours", 0, 0): {0: 100000}, + ("Category Fours", 0, 1): {0: 100000}, + ("Category Fours", 0, 2): {0: 100000}, + ("Category Fours", 0, 3): {0: 100000}, + ("Category Fours", 0, 4): {0: 100000}, + ("Category Fours", 0, 5): {0: 100000}, + ("Category Fours", 0, 6): {0: 100000}, + ("Category Fours", 0, 7): {0: 100000}, + ("Category Fours", 0, 8): {0: 100000}, + ("Category Fours", 1, 0): {0: 100000}, + ("Category Fours", 1, 1): {0: 83260, 4: 16740}, + ("Category Fours", 1, 2): {0: 69514, 4: 30486}, + ("Category Fours", 1, 3): {0: 58017, 4: 41983}, + ("Category Fours", 1, 4): {0: 48389, 4: 51611}, + ("Category Fours", 1, 5): {0: 40201, 4: 59799}, + ("Category Fours", 1, 6): {0: 33496, 4: 66504}, + ("Category Fours", 1, 7): {0: 28052, 4: 71948}, + ("Category Fours", 1, 8): {0: 23431, 4: 76569}, + ("Category Fours", 2, 0): {0: 100000}, + ("Category Fours", 2, 1): {0: 69379, 4: 30621}, + ("Category Fours", 2, 2): {0: 48538, 4: 42240, 8: 9222}, + ("Category Fours", 2, 3): {0: 33756, 4: 48555, 8: 17689}, + ("Category Fours", 2, 4): {0: 23070, 4: 49916, 8: 27014}, + ("Category Fours", 2, 5): {0: 16222, 4: 48009, 8: 35769}, + ("Category Fours", 2, 6): {0: 11125, 4: 44400, 8: 44475}, + ("Category Fours", 2, 7): {0: 7919, 4: 40216, 8: 51865}, + ("Category Fours", 2, 8): {0: 5348, 4: 35757, 8: 58895}, + ("Category Fours", 3, 0): {0: 100000}, + ("Category Fours", 3, 1): {0: 57914, 4: 34622, 8: 7464}, + ("Category Fours", 3, 2): {0: 33621, 4: 44110, 8: 22269}, + ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 30898, 12: 7524}, + ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 39024, 12: 13840}, + ("Category Fours", 3, 5): {0: 6367, 4: 29116, 8: 43192, 12: 21325}, + ("Category Fours", 3, 6): {0: 3643, 4: 22457, 8: 44477, 12: 29423}, + ("Category Fours", 3, 7): {0: 2178, 4: 16802, 8: 43275, 12: 37745}, + ("Category Fours", 3, 8): {0: 1255, 4: 12301, 8: 41132, 12: 45312}, + ("Category Fours", 4, 0): {0: 100000}, + ("Category Fours", 4, 1): {0: 48465, 4: 38398, 8: 13137}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 27073, 12: 8720}, + ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 26861, 16: 7185}, + ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 34222, 16: 12796}, + ("Category Fours", 4, 6): {0: 1314, 4: 10001, 8: 29850, 12: 39425, 16: 19410}, + ("Category Fours", 4, 7): {0: 592, 4: 6231, 8: 24250, 12: 41917, 16: 27010}, + ("Category Fours", 4, 8): {0: 302, 8: 23055, 12: 41866, 16: 34777}, + ("Category Fours", 5, 0): {0: 100000}, + ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 16028, 12: 3630}, + ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 13998, 16: 3319}, + ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 24783, 16: 10458}, + ("Category Fours", 5, 4): {0: 2635, 4: 13889, 8: 30079, 12: 32428, 16: 17263, 20: 3706}, + ("Category Fours", 5, 5): {0: 1160, 4: 7756, 8: 23332, 12: 34254, 16: 25803, 20: 7695}, + ("Category Fours", 5, 6): {0: 434, 8: 20773, 12: 32910, 16: 32752, 20: 13131}, + ("Category Fours", 5, 7): {0: 169, 8: 13536, 12: 29123, 16: 37701, 20: 19471}, + ("Category Fours", 5, 8): {0: 1267, 8: 7340, 12: 24807, 16: 40144, 20: 26442}, + ("Category Fours", 6, 0): {0: 100000}, + ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 20225, 12: 6287}, + ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 19179, 16: 7441}, + ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 15808, 20: 5155}, + ("Category Fours", 6, 4): {0: 1284, 4: 7889, 8: 21748, 12: 31107, 16: 25281, 20: 12691}, + ("Category Fours", 6, 5): {0: 462, 8: 17601, 12: 27817, 16: 31233, 20: 18386, 24: 4501}, + ("Category Fours", 6, 6): {0: 1783, 8: 8344, 12: 22156, 16: 32690, 20: 26192, 24: 8835}, + ("Category Fours", 6, 7): {0: 767, 12: 20974, 16: 31490, 20: 32639, 24: 14130}, + ("Category Fours", 6, 8): {0: 357, 12: 13912, 16: 27841, 20: 37380, 24: 20510}, + ("Category Fours", 7, 0): {0: 100000}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 23327, 12: 9563}, + ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 23169, 16: 13222}, + ("Category Fours", 7, 3): {0: 2194, 4: 11153, 8: 24107, 12: 29411, 16: 21390, 20: 11745}, + ("Category Fours", 7, 4): {0: 560, 8: 19291, 12: 26330, 16: 28118, 20: 18174, 24: 7527}, + ("Category Fours", 7, 5): {0: 1858, 8: 7862, 12: 19425, 16: 29003, 20: 26113, 24: 15739}, + ("Category Fours", 7, 6): {0: 679, 12: 16759, 16: 25831, 20: 30724, 24: 20147, 28: 5860}, + ("Category Fours", 7, 7): {0: 13, 12: 10063, 16: 20524, 20: 31843, 24: 27368, 28: 10189}, + ("Category Fours", 7, 8): {4: 864, 16: 19910, 20: 30153, 24: 33428, 28: 15645}, + ("Category Fours", 8, 0): {0: 100000}, + ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 25964, 12: 13600}, + ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 14387, 20: 6107}, + ("Category Fours", 8, 3): {0: 1277, 4: 7349, 8: 18330, 12: 27186, 16: 25138, 20: 14371, 24: 6349}, + ("Category Fours", 8, 4): {0: 289, 8: 11929, 12: 20282, 16: 26960, 20: 23292, 24: 12927, 28: 4321}, + ("Category Fours", 8, 5): {0: 835, 12: 16706, 16: 23588, 20: 27754, 24: 20767, 28: 10350}, + ("Category Fours", 8, 6): {0: 21, 12: 8911, 16: 17296, 20: 27398, 24: 27074, 28: 15457, 32: 3843}, + ("Category Fours", 8, 7): {0: 745, 16: 15069, 20: 23737, 24: 30628, 28: 22590, 32: 7231}, + ("Category Fours", 8, 8): {0: 1949, 16: 7021, 20: 18630, 24: 31109, 28: 29548, 32: 11743}, + ("Category Fives", 0, 0): {0: 100000}, + ("Category Fives", 0, 1): {0: 100000}, + ("Category Fives", 0, 2): {0: 100000}, + ("Category Fives", 0, 3): {0: 100000}, + ("Category Fives", 0, 4): {0: 100000}, + ("Category Fives", 0, 5): {0: 100000}, + ("Category Fives", 0, 6): {0: 100000}, + ("Category Fives", 0, 7): {0: 100000}, + ("Category Fives", 0, 8): {0: 100000}, + ("Category Fives", 1, 0): {0: 100000}, + ("Category Fives", 1, 1): {0: 83338, 5: 16662}, + ("Category Fives", 1, 2): {0: 69499, 5: 30501}, + ("Category Fives", 1, 3): {0: 57799, 5: 42201}, + ("Category Fives", 1, 4): {0: 48311, 5: 51689}, + ("Category Fives", 1, 5): {0: 40084, 5: 59916}, + ("Category Fives", 1, 6): {0: 33440, 5: 66560}, + ("Category Fives", 1, 7): {0: 27730, 5: 72270}, + ("Category Fives", 1, 8): {0: 23210, 5: 76790}, + ("Category Fives", 2, 0): {0: 100000}, + ("Category Fives", 2, 1): {0: 69299, 5: 27864, 10: 2837}, + ("Category Fives", 2, 2): {0: 48156, 5: 42526, 10: 9318}, + ("Category Fives", 2, 3): {0: 33225, 5: 49153, 10: 17622}, + ("Category Fives", 2, 4): {0: 23218, 5: 50075, 10: 26707}, + ("Category Fives", 2, 5): {0: 15939, 5: 48313, 10: 35748}, + ("Category Fives", 2, 6): {0: 11340, 5: 44324, 10: 44336}, + ("Category Fives", 2, 7): {0: 7822, 5: 40388, 10: 51790}, + ("Category Fives", 2, 8): {0: 5386, 5: 35636, 10: 58978}, + ("Category Fives", 3, 0): {0: 100000}, + ("Category Fives", 3, 1): {0: 58034, 5: 34541, 10: 7425}, + ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 19403, 15: 2904}, + ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 30794, 15: 7492}, + ("Category Fives", 3, 4): {0: 11196, 5: 36192, 10: 38673, 15: 13939}, + ("Category Fives", 3, 5): {0: 6561, 5: 29163, 10: 43014, 15: 21262}, + ("Category Fives", 3, 6): {0: 3719, 5: 22181, 10: 44611, 15: 29489}, + ("Category Fives", 3, 7): {0: 2099, 5: 16817, 10: 43466, 15: 37618}, + ("Category Fives", 3, 8): {0: 1281, 5: 12473, 10: 40936, 15: 45310}, + ("Category Fives", 4, 0): {0: 100000}, + ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 27041, 15: 8893}, + ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 17250, 20: 3208}, + ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 26968, 20: 7218}, + ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 1291, 5: 9959, 10: 29833, 15: 39417, 20: 19500}, + ("Category Fives", 4, 7): {0: 623, 5: 6231, 10: 24360, 15: 41779, 20: 27007}, + ("Category Fives", 4, 8): {0: 313, 10: 23001, 15: 41957, 20: 34729}, + ("Category Fives", 5, 0): {0: 100000}, + ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 16029, 15: 3499}, + ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 13793, 20: 3266}, + ("Category Fives", 5, 3): {0: 6526, 5: 23716, 10: 34430, 15: 25017, 20: 10311}, + ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 17219, 25: 3811}, + ("Category Fives", 5, 5): {0: 1063, 5: 7876, 10: 23203, 15: 34489, 20: 25757, 25: 7612}, + ("Category Fives", 5, 6): {0: 429, 5: 4091, 10: 16696, 15: 32855, 20: 32891, 25: 13038}, + ("Category Fives", 5, 7): {0: 159, 10: 13509, 15: 29416, 20: 37778, 25: 19138}, + ("Category Fives", 5, 8): {0: 1179, 10: 7453, 15: 24456, 20: 40615, 25: 26297}, + ("Category Fives", 6, 0): {0: 100000}, + ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 20181, 15: 6176}, + ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 19004, 20: 7397}, + ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 15759, 25: 5185}, + ("Category Fives", 6, 4): {0: 1201, 5: 8226, 10: 21518, 15: 31229, 20: 25160, 25: 12666}, + ("Category Fives", 6, 5): {0: 433, 10: 17879, 15: 27961, 20: 30800, 25: 18442, 30: 4485}, + ("Category Fives", 6, 6): {0: 141, 10: 10040, 15: 22226, 20: 32744, 25: 26341, 30: 8508}, + ("Category Fives", 6, 7): {0: 772, 10: 4724, 15: 16206, 20: 31363, 25: 32784, 30: 14151}, + ("Category Fives", 6, 8): {0: 297, 15: 13902, 20: 28004, 25: 37178, 30: 20619}, + ("Category Fives", 7, 0): {0: 100000}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 23567, 15: 9453}, + ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 10140, 25: 3122}, + ("Category Fives", 7, 3): {0: 2262, 5: 11013, 10: 24443, 15: 29307, 20: 21387, 25: 11588}, + ("Category Fives", 7, 4): {0: 618, 5: 4583, 10: 14761, 15: 26159, 20: 28335, 25: 18050, 30: 7494}, + ("Category Fives", 7, 5): {0: 183, 10: 9616, 15: 19685, 20: 28915, 25: 26000, 30: 12883, 35: 2718}, + ("Category Fives", 7, 6): {0: 670, 15: 16878, 20: 25572, 25: 30456, 30: 20695, 35: 5729}, + ("Category Fives", 7, 7): {0: 255, 15: 9718, 20: 20696, 25: 31883, 30: 27333, 35: 10115}, + ("Category Fives", 7, 8): {0: 927, 15: 4700, 20: 15292, 25: 30298, 30: 33015, 35: 15768}, + ("Category Fives", 8, 0): {0: 100000}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 10392, 20: 3069}, + ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 14056, 25: 6230}, + ("Category Fives", 8, 3): {0: 1258, 5: 7317, 10: 18783, 15: 27375, 20: 24542, 25: 14322, 30: 6403}, + ("Category Fives", 8, 4): {0: 271, 10: 11864, 15: 20267, 20: 27158, 25: 23589, 30: 12529, 35: 4322}, + ("Category Fives", 8, 5): {0: 943, 10: 4260, 15: 12456, 20: 23115, 25: 27968, 30: 20704, 35: 10554}, + ("Category Fives", 8, 6): {0: 281, 15: 8625, 20: 17201, 25: 27484, 30: 27178, 35: 15414, 40: 3817}, + ("Category Fives", 8, 7): {0: 746, 20: 14964, 25: 23717, 30: 30426, 35: 22677, 40: 7470}, + ("Category Fives", 8, 8): {0: 261, 20: 8927, 25: 18714, 30: 31084, 35: 29126, 40: 11888}, + ("Category Sixes", 0, 0): {0: 100000}, + ("Category Sixes", 0, 1): {0: 100000}, + ("Category Sixes", 0, 2): {0: 100000}, + ("Category Sixes", 0, 3): {0: 100000}, + ("Category Sixes", 0, 4): {0: 100000}, + ("Category Sixes", 0, 5): {0: 100000}, + ("Category Sixes", 0, 6): {0: 100000}, + ("Category Sixes", 0, 7): {0: 100000}, + ("Category Sixes", 0, 8): {0: 100000}, + ("Category Sixes", 1, 0): {0: 100000}, + ("Category Sixes", 1, 1): {0: 83168, 6: 16832}, + ("Category Sixes", 1, 2): {0: 69548, 6: 30452}, + ("Category Sixes", 1, 3): {0: 57697, 6: 42303}, + ("Category Sixes", 1, 4): {0: 48043, 6: 51957}, + ("Category Sixes", 1, 5): {0: 39912, 6: 60088}, + ("Category Sixes", 1, 6): {0: 33499, 6: 66501}, + ("Category Sixes", 1, 7): {0: 28251, 6: 71749}, + ("Category Sixes", 1, 8): {0: 23206, 6: 76794}, + ("Category Sixes", 2, 0): {0: 100000}, + ("Category Sixes", 2, 1): {0: 69463, 6: 27651, 12: 2886}, + ("Category Sixes", 2, 2): {0: 47896, 6: 42794, 12: 9310}, + ("Category Sixes", 2, 3): {0: 33394, 6: 48757, 12: 17849}, + ("Category Sixes", 2, 4): {0: 23552, 6: 49554, 12: 26894}, + ("Category Sixes", 2, 5): {0: 16090, 6: 48098, 12: 35812}, + ("Category Sixes", 2, 6): {0: 11073, 6: 44833, 12: 44094}, + ("Category Sixes", 2, 7): {0: 7737, 6: 40480, 12: 51783}, + ("Category Sixes", 2, 8): {0: 5379, 6: 35672, 12: 58949}, + ("Category Sixes", 3, 0): {0: 100000}, + ("Category Sixes", 3, 1): {0: 57718, 6: 34818, 12: 7464}, + ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 19159, 18: 2903}, + ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 30952, 18: 7436}, + ("Category Sixes", 3, 4): {0: 11144, 6: 36281, 12: 38817, 18: 13758}, + ("Category Sixes", 3, 5): {0: 6414, 6: 28891, 12: 43114, 18: 21581}, + ("Category Sixes", 3, 6): {0: 3870, 6: 22394, 12: 44318, 18: 29418}, + ("Category Sixes", 3, 7): {0: 2188, 6: 16803, 12: 43487, 18: 37522}, + ("Category Sixes", 3, 8): {0: 1289, 6: 12421, 12: 41082, 18: 45208}, + ("Category Sixes", 4, 0): {0: 100000}, + ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, + ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 26935, 18: 8731}, + ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 17390, 24: 3157}, + ("Category Sixes", 4, 4): {0: 5324, 6: 23265, 12: 37209, 18: 26929, 24: 7273}, + ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 1282, 6: 9997, 12: 29855, 18: 39379, 24: 19487}, + ("Category Sixes", 4, 7): {0: 588, 6: 6202, 12: 24396, 18: 41935, 24: 26879}, + ("Category Sixes", 4, 8): {0: 317, 6: 3863, 12: 19042, 18: 42180, 24: 34598}, + ("Category Sixes", 5, 0): {0: 100000}, + ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 16206, 18: 3497}, + ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 13612, 24: 3281}, + ("Category Sixes", 5, 3): {0: 6456, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 17115, 30: 3771}, + ("Category Sixes", 5, 5): {0: 1119, 6: 7775, 12: 23063, 18: 34716, 24: 25568, 30: 7759}, + ("Category Sixes", 5, 6): {0: 392, 6: 4171, 12: 16724, 18: 32792, 24: 32829, 30: 13092}, + ("Category Sixes", 5, 7): {0: 197, 12: 13627, 18: 29190, 24: 37560, 30: 19426}, + ("Category Sixes", 5, 8): {0: 1246, 12: 7404, 18: 24560, 24: 40134, 30: 26656}, + ("Category Sixes", 6, 0): {0: 100000}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 20198, 18: 6268}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 19196, 24: 7514}, + ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 15863, 30: 5104}, + ("Category Sixes", 6, 4): {0: 1286, 6: 8066, 12: 21653, 18: 31264, 24: 25039, 30: 12692}, + ("Category Sixes", 6, 5): {0: 413, 6: 3777, 12: 13962, 18: 27705, 24: 30919, 30: 18670, 36: 4554}, + ("Category Sixes", 6, 6): {0: 146, 12: 10040, 18: 22320, 24: 32923, 30: 26086, 36: 8485}, + ("Category Sixes", 6, 7): {0: 814, 12: 4698, 18: 16286, 24: 31577, 30: 32487, 36: 14138}, + ("Category Sixes", 6, 8): {0: 328, 18: 14004, 24: 28064, 30: 37212, 36: 20392}, + ("Category Sixes", 7, 0): {0: 100000}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 23499, 18: 9665}, + ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 10316, 30: 3102}, + ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 9209, 36: 2529}, + ("Category Sixes", 7, 4): {0: 603, 6: 4459, 12: 14673, 18: 26303, 24: 28335, 30: 18228, 36: 7399}, + ("Category Sixes", 7, 5): {0: 172, 12: 9654, 18: 19381, 24: 29254, 30: 25790, 36: 12992, 42: 2757}, + ("Category Sixes", 7, 6): {0: 704, 12: 3864, 18: 13039, 24: 25760, 30: 30698, 36: 20143, 42: 5792}, + ("Category Sixes", 7, 7): {0: 257, 18: 9857, 24: 20557, 30: 31709, 36: 27546, 42: 10074}, + ("Category Sixes", 7, 8): {0: 872, 18: 4658, 24: 15419, 30: 30259, 36: 33183, 42: 15609}, + ("Category Sixes", 8, 0): {0: 100000}, + ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 10483, 24: 3123}, + ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 14170, 30: 6166}, + ("Category Sixes", 8, 3): {0: 1246, 6: 7112, 12: 18757, 18: 27277, 24: 24802, 30: 14351, 36: 6455}, + ("Category Sixes", 8, 4): {0: 301, 12: 12044, 18: 20247, 24: 27146, 30: 23403, 36: 12524, 42: 4335}, + ("Category Sixes", 8, 5): {0: 859, 12: 4241, 18: 12477, 24: 23471, 30: 27655, 36: 20803, 42: 10494}, + ("Category Sixes", 8, 6): {0: 277, 18: 8656, 24: 17373, 30: 27347, 36: 27024, 42: 15394, 48: 3929}, + ("Category Sixes", 8, 7): {0: 766, 18: 3503, 24: 11451, 30: 23581, 36: 30772, 42: 22654, 48: 7273}, + ("Category Sixes", 8, 8): {6: 262, 24: 8866, 30: 18755, 36: 31116, 42: 28870, 48: 12131}, + ("Category Choice", 0, 0): {0: 100000}, + ("Category Choice", 0, 1): {0: 100000}, + ("Category Choice", 0, 2): {0: 100000}, + ("Category Choice", 0, 3): {0: 100000}, + ("Category Choice", 0, 4): {0: 100000}, + ("Category Choice", 0, 5): {0: 100000}, + ("Category Choice", 0, 6): {0: 100000}, + ("Category Choice", 0, 7): {0: 100000}, + ("Category Choice", 0, 8): {0: 100000}, + ("Category Choice", 1, 0): {0: 100000}, + ("Category Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, + ("Category Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, + ("Category Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, + ("Category Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, + ("Category Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, + ("Category Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, + ("Category Choice", 1, 7): {1: 8893, 6: 91107}, + ("Category Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Choice", 2, 0): {0: 100000}, + ("Category Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, + ("Category Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, + ("Category Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, + ("Category Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, + ("Category Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, + ("Category Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, + ("Category Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, + ("Category Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Choice", 3, 0): {0: 100000}, + ("Category Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, + ("Category Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, + ("Category Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, + ("Category Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, + ("Category Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, + ("Category Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, + ("Category Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, + ("Category Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Choice", 4, 0): {0: 100000}, + ("Category Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, + ("Category Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, + ("Category Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, + ("Category Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, + ("Category Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, + ("Category Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, + ("Category Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, + ("Category Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Choice", 5, 0): {0: 100000}, + ("Category Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, + ("Category Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, + ("Category Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, + ("Category Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, + ("Category Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, + ("Category Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, + ("Category Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, + ("Category Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Choice", 6, 0): {0: 100000}, + ("Category Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, + ("Category Choice", 6, 2): {8: 1504, 16: 8676, 20: 10032, 22: 14673, 26: 27312, 27: 16609, 29: 12133, 31: 9061}, + ("Category Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, + ("Category Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, + ("Category Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, + ("Category Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, + ("Category Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, + ("Category Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Choice", 7, 0): {0: 100000}, + ("Category Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, + ("Category Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, + ("Category Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, + ("Category Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, + ("Category Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, + ("Category Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, + ("Category Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, + ("Category Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Choice", 8, 0): {0: 100000}, + ("Category Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, + ("Category Choice", 8, 2): {11: 5900, 26: 10331, 29: 11435, 31: 14533, 34: 23939, 36: 13855, 38: 10165, 40: 9842}, + ("Category Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, + ("Category Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, + ("Category Choice", 8, 5): {16: 4986, 32: 9031, 35: 10214, 37: 14528, 41: 25608, 42: 16131, 44: 11245, 46: 8257}, + ("Category Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, + ("Category Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, + ("Category Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Inverse Choice", 0, 0): {0: 100000}, + ("Category Inverse Choice", 0, 1): {0: 100000}, + ("Category Inverse Choice", 0, 2): {0: 100000}, + ("Category Inverse Choice", 0, 3): {0: 100000}, + ("Category Inverse Choice", 0, 4): {0: 100000}, + ("Category Inverse Choice", 0, 5): {0: 100000}, + ("Category Inverse Choice", 0, 6): {0: 100000}, + ("Category Inverse Choice", 0, 7): {0: 100000}, + ("Category Inverse Choice", 0, 8): {0: 100000}, + ("Category Inverse Choice", 1, 0): {0: 100000}, + ("Category Inverse Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, + ("Category Inverse Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, + ("Category Inverse Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, + ("Category Inverse Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, + ("Category Inverse Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, + ("Category Inverse Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, + ("Category Inverse Choice", 1, 7): {1: 8893, 6: 91107}, + ("Category Inverse Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Inverse Choice", 2, 0): {0: 100000}, + ("Category Inverse Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, + ("Category Inverse Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, + ("Category Inverse Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, + ("Category Inverse Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, + ("Category Inverse Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, + ("Category Inverse Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, + ("Category Inverse Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, + ("Category Inverse Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Inverse Choice", 3, 0): {0: 100000}, + ("Category Inverse Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, + ("Category Inverse Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, + ("Category Inverse Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, + ("Category Inverse Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, + ("Category Inverse Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, + ("Category Inverse Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, + ("Category Inverse Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, + ("Category Inverse Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Inverse Choice", 4, 0): {0: 100000}, + ("Category Inverse Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, + ("Category Inverse Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, + ("Category Inverse Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, + ("Category Inverse Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, + ("Category Inverse Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, + ("Category Inverse Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, + ("Category Inverse Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, + ("Category Inverse Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Inverse Choice", 5, 0): {0: 100000}, + ("Category Inverse Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, + ("Category Inverse Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, + ("Category Inverse Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, + ("Category Inverse Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, + ("Category Inverse Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, + ("Category Inverse Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, + ("Category Inverse Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, + ("Category Inverse Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Inverse Choice", 6, 0): {0: 100000}, + ("Category Inverse Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, + ("Category Inverse Choice", 6, 2): { + 8: 1504, + 16: 8676, + 20: 10032, + 22: 14673, + 26: 27312, + 27: 16609, + 29: 12133, + 31: 9061, + }, + ("Category Inverse Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, + ("Category Inverse Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, + ("Category Inverse Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, + ("Category Inverse Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, + ("Category Inverse Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, + ("Category Inverse Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Inverse Choice", 7, 0): {0: 100000}, + ("Category Inverse Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, + ("Category Inverse Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, + ("Category Inverse Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, + ("Category Inverse Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, + ("Category Inverse Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, + ("Category Inverse Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, + ("Category Inverse Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, + ("Category Inverse Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Inverse Choice", 8, 0): {0: 100000}, + ("Category Inverse Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, + ("Category Inverse Choice", 8, 2): { + 11: 5900, + 26: 10331, + 29: 11435, + 31: 14533, + 34: 23939, + 36: 13855, + 38: 10165, + 40: 9842, + }, + ("Category Inverse Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, + ("Category Inverse Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, + ("Category Inverse Choice", 8, 5): { + 16: 4986, + 32: 9031, + 35: 10214, + 37: 14528, + 41: 25608, + 42: 16131, + 44: 11245, + 46: 8257, + }, + ("Category Inverse Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, + ("Category Inverse Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, + ("Category Inverse Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Pair", 0, 0): {0: 100000}, + ("Category Pair", 0, 1): {0: 100000}, + ("Category Pair", 0, 2): {0: 100000}, + ("Category Pair", 0, 3): {0: 100000}, + ("Category Pair", 0, 4): {0: 100000}, + ("Category Pair", 0, 5): {0: 100000}, + ("Category Pair", 0, 6): {0: 100000}, + ("Category Pair", 0, 7): {0: 100000}, + ("Category Pair", 0, 8): {0: 100000}, + ("Category Pair", 1, 0): {0: 100000}, + ("Category Pair", 1, 1): {0: 100000}, + ("Category Pair", 1, 2): {0: 100000}, + ("Category Pair", 1, 3): {0: 100000}, + ("Category Pair", 1, 4): {0: 100000}, + ("Category Pair", 1, 5): {0: 100000}, + ("Category Pair", 1, 6): {0: 100000}, + ("Category Pair", 1, 7): {0: 100000}, + ("Category Pair", 1, 8): {0: 100000}, + ("Category Pair", 2, 0): {0: 100000}, + ("Category Pair", 2, 1): {0: 83388, 10: 16612}, + ("Category Pair", 2, 2): {0: 69422, 10: 30578}, + ("Category Pair", 2, 3): {0: 57830, 10: 42170}, + ("Category Pair", 2, 4): {0: 48195, 10: 51805}, + ("Category Pair", 2, 5): {0: 40117, 10: 59883}, + ("Category Pair", 2, 6): {0: 33286, 10: 66714}, + ("Category Pair", 2, 7): {0: 27917, 10: 72083}, + ("Category Pair", 2, 8): {0: 23354, 10: 76646}, + ("Category Pair", 3, 0): {0: 100000}, + ("Category Pair", 3, 1): {0: 55518, 10: 44482}, + ("Category Pair", 3, 2): {0: 30904, 10: 69096}, + ("Category Pair", 3, 3): {0: 17242, 10: 82758}, + ("Category Pair", 3, 4): {0: 9486, 10: 90514}, + ("Category Pair", 3, 5): {0: 5362, 10: 94638}, + ("Category Pair", 3, 6): {0: 2909, 10: 97091}, + ("Category Pair", 3, 7): {0: 1574, 10: 98426}, + ("Category Pair", 3, 8): {0: 902, 10: 99098}, + ("Category Pair", 4, 0): {0: 100000}, + ("Category Pair", 4, 1): {0: 27789, 10: 72211}, + ("Category Pair", 4, 2): {0: 7799, 10: 92201}, + ("Category Pair", 4, 3): {0: 2113, 10: 97887}, + ("Category Pair", 4, 4): {0: 601, 10: 99399}, + ("Category Pair", 4, 5): {0: 155, 10: 99845}, + ("Category Pair", 4, 6): {0: 43, 10: 99957}, + ("Category Pair", 4, 7): {0: 10, 10: 99990}, + ("Category Pair", 4, 8): {0: 3, 10: 99997}, + ("Category Pair", 5, 0): {0: 100000}, + ("Category Pair", 5, 1): {0: 9298, 10: 90702}, + ("Category Pair", 5, 2): {0: 863, 10: 99137}, + ("Category Pair", 5, 3): {0: 79, 10: 99921}, + ("Category Pair", 5, 4): {0: 2, 10: 99998}, + ("Category Pair", 5, 5): {0: 2, 10: 99998}, + ("Category Pair", 5, 6): {10: 100000}, + ("Category Pair", 5, 7): {10: 100000}, + ("Category Pair", 5, 8): {10: 100000}, + ("Category Pair", 6, 0): {0: 100000}, + ("Category Pair", 6, 1): {0: 1541, 10: 98459}, + ("Category Pair", 6, 2): {0: 23, 10: 99977}, + ("Category Pair", 6, 3): {10: 100000}, + ("Category Pair", 6, 4): {10: 100000}, + ("Category Pair", 6, 5): {10: 100000}, + ("Category Pair", 6, 6): {10: 100000}, + ("Category Pair", 6, 7): {10: 100000}, + ("Category Pair", 6, 8): {10: 100000}, + ("Category Pair", 7, 0): {0: 100000}, + ("Category Pair", 7, 1): {10: 100000}, + ("Category Pair", 7, 2): {10: 100000}, + ("Category Pair", 7, 3): {10: 100000}, + ("Category Pair", 7, 4): {10: 100000}, + ("Category Pair", 7, 5): {10: 100000}, + ("Category Pair", 7, 6): {10: 100000}, + ("Category Pair", 7, 7): {10: 100000}, + ("Category Pair", 7, 8): {10: 100000}, + ("Category Pair", 8, 0): {0: 100000}, + ("Category Pair", 8, 1): {10: 100000}, + ("Category Pair", 8, 2): {10: 100000}, + ("Category Pair", 8, 3): {10: 100000}, + ("Category Pair", 8, 4): {10: 100000}, + ("Category Pair", 8, 5): {10: 100000}, + ("Category Pair", 8, 6): {10: 100000}, + ("Category Pair", 8, 7): {10: 100000}, + ("Category Pair", 8, 8): {10: 100000}, + ("Category Three of a Kind", 0, 0): {0: 100000}, + ("Category Three of a Kind", 0, 1): {0: 100000}, + ("Category Three of a Kind", 0, 2): {0: 100000}, + ("Category Three of a Kind", 0, 3): {0: 100000}, + ("Category Three of a Kind", 0, 4): {0: 100000}, + ("Category Three of a Kind", 0, 5): {0: 100000}, + ("Category Three of a Kind", 0, 6): {0: 100000}, + ("Category Three of a Kind", 0, 7): {0: 100000}, + ("Category Three of a Kind", 0, 8): {0: 100000}, + ("Category Three of a Kind", 1, 0): {0: 100000}, + ("Category Three of a Kind", 1, 1): {0: 100000}, + ("Category Three of a Kind", 1, 2): {0: 100000}, + ("Category Three of a Kind", 1, 3): {0: 100000}, + ("Category Three of a Kind", 1, 4): {0: 100000}, + ("Category Three of a Kind", 1, 5): {0: 100000}, + ("Category Three of a Kind", 1, 6): {0: 100000}, + ("Category Three of a Kind", 1, 7): {0: 100000}, + ("Category Three of a Kind", 1, 8): {0: 100000}, + ("Category Three of a Kind", 2, 0): {0: 100000}, + ("Category Three of a Kind", 2, 1): {0: 100000}, + ("Category Three of a Kind", 2, 2): {0: 100000}, + ("Category Three of a Kind", 2, 3): {0: 100000}, + ("Category Three of a Kind", 2, 4): {0: 100000}, + ("Category Three of a Kind", 2, 5): {0: 100000}, + ("Category Three of a Kind", 2, 6): {0: 100000}, + ("Category Three of a Kind", 2, 7): {0: 100000}, + ("Category Three of a Kind", 2, 8): {0: 100000}, + ("Category Three of a Kind", 3, 0): {0: 100000}, + ("Category Three of a Kind", 3, 1): {0: 97222, 20: 2778}, + ("Category Three of a Kind", 3, 2): {0: 88880, 20: 11120}, + ("Category Three of a Kind", 3, 3): {0: 78187, 20: 21813}, + ("Category Three of a Kind", 3, 4): {0: 67476, 20: 32524}, + ("Category Three of a Kind", 3, 5): {0: 57476, 20: 42524}, + ("Category Three of a Kind", 3, 6): {0: 48510, 20: 51490}, + ("Category Three of a Kind", 3, 7): {0: 40921, 20: 59079}, + ("Category Three of a Kind", 3, 8): {0: 34533, 20: 65467}, + ("Category Three of a Kind", 4, 0): {0: 100000}, + ("Category Three of a Kind", 4, 1): {0: 90316, 20: 9684}, + ("Category Three of a Kind", 4, 2): {0: 68401, 20: 31599}, + ("Category Three of a Kind", 4, 3): {0: 49383, 20: 50617}, + ("Category Three of a Kind", 4, 4): {0: 34399, 20: 65601}, + ("Category Three of a Kind", 4, 5): {0: 24154, 20: 75846}, + ("Category Three of a Kind", 4, 6): {0: 16802, 20: 83198}, + ("Category Three of a Kind", 4, 7): {0: 11623, 20: 88377}, + ("Category Three of a Kind", 4, 8): {0: 8105, 20: 91895}, + ("Category Three of a Kind", 5, 0): {0: 100000}, + ("Category Three of a Kind", 5, 1): {0: 78629, 20: 21371}, + ("Category Three of a Kind", 5, 2): {0: 46013, 20: 53987}, + ("Category Three of a Kind", 5, 3): {0: 25698, 20: 74302}, + ("Category Three of a Kind", 5, 4): {0: 14205, 20: 85795}, + ("Category Three of a Kind", 5, 5): {0: 7932, 20: 92068}, + ("Category Three of a Kind", 5, 6): {0: 4357, 20: 95643}, + ("Category Three of a Kind", 5, 7): {0: 2432, 20: 97568}, + ("Category Three of a Kind", 5, 8): {0: 1378, 20: 98622}, + ("Category Three of a Kind", 6, 0): {0: 100000}, + ("Category Three of a Kind", 6, 1): {0: 63231, 20: 36769}, + ("Category Three of a Kind", 6, 2): {0: 26818, 20: 73182}, + ("Category Three of a Kind", 6, 3): {0: 11075, 20: 88925}, + ("Category Three of a Kind", 6, 4): {0: 4749, 20: 95251}, + ("Category Three of a Kind", 6, 5): {0: 1982, 20: 98018}, + ("Category Three of a Kind", 6, 6): {0: 827, 20: 99173}, + ("Category Three of a Kind", 6, 7): {0: 358, 20: 99642}, + ("Category Three of a Kind", 6, 8): {0: 146, 20: 99854}, + ("Category Three of a Kind", 7, 0): {0: 100000}, + ("Category Three of a Kind", 7, 1): {0: 45975, 20: 54025}, + ("Category Three of a Kind", 7, 2): {0: 13207, 20: 86793}, + ("Category Three of a Kind", 7, 3): {0: 3727, 20: 96273}, + ("Category Three of a Kind", 7, 4): {0: 1097, 20: 98903}, + ("Category Three of a Kind", 7, 5): {0: 313, 20: 99687}, + ("Category Three of a Kind", 7, 6): {0: 96, 20: 99904}, + ("Category Three of a Kind", 7, 7): {0: 22, 20: 99978}, + ("Category Three of a Kind", 7, 8): {0: 8, 20: 99992}, + ("Category Three of a Kind", 8, 0): {0: 100000}, + ("Category Three of a Kind", 8, 1): {0: 29316, 20: 70684}, + ("Category Three of a Kind", 8, 2): {0: 5027, 20: 94973}, + ("Category Three of a Kind", 8, 3): {0: 857, 20: 99143}, + ("Category Three of a Kind", 8, 4): {0: 162, 20: 99838}, + ("Category Three of a Kind", 8, 5): {0: 25, 20: 99975}, + ("Category Three of a Kind", 8, 6): {0: 4, 20: 99996}, + ("Category Three of a Kind", 8, 7): {0: 1, 20: 99999}, + ("Category Three of a Kind", 8, 8): {20: 100000}, + ("Category Four of a Kind", 0, 0): {0: 100000}, + ("Category Four of a Kind", 0, 1): {0: 100000}, + ("Category Four of a Kind", 0, 2): {0: 100000}, + ("Category Four of a Kind", 0, 3): {0: 100000}, + ("Category Four of a Kind", 0, 4): {0: 100000}, + ("Category Four of a Kind", 0, 5): {0: 100000}, + ("Category Four of a Kind", 0, 6): {0: 100000}, + ("Category Four of a Kind", 0, 7): {0: 100000}, + ("Category Four of a Kind", 0, 8): {0: 100000}, + ("Category Four of a Kind", 1, 0): {0: 100000}, + ("Category Four of a Kind", 1, 1): {0: 100000}, + ("Category Four of a Kind", 1, 2): {0: 100000}, + ("Category Four of a Kind", 1, 3): {0: 100000}, + ("Category Four of a Kind", 1, 4): {0: 100000}, + ("Category Four of a Kind", 1, 5): {0: 100000}, + ("Category Four of a Kind", 1, 6): {0: 100000}, + ("Category Four of a Kind", 1, 7): {0: 100000}, + ("Category Four of a Kind", 1, 8): {0: 100000}, + ("Category Four of a Kind", 2, 0): {0: 100000}, + ("Category Four of a Kind", 2, 1): {0: 100000}, + ("Category Four of a Kind", 2, 2): {0: 100000}, + ("Category Four of a Kind", 2, 3): {0: 100000}, + ("Category Four of a Kind", 2, 4): {0: 100000}, + ("Category Four of a Kind", 2, 5): {0: 100000}, + ("Category Four of a Kind", 2, 6): {0: 100000}, + ("Category Four of a Kind", 2, 7): {0: 100000}, + ("Category Four of a Kind", 2, 8): {0: 100000}, + ("Category Four of a Kind", 3, 0): {0: 100000}, + ("Category Four of a Kind", 3, 1): {0: 100000}, + ("Category Four of a Kind", 3, 2): {0: 100000}, + ("Category Four of a Kind", 3, 3): {0: 100000}, + ("Category Four of a Kind", 3, 4): {0: 100000}, + ("Category Four of a Kind", 3, 5): {0: 100000}, + ("Category Four of a Kind", 3, 6): {0: 100000}, + ("Category Four of a Kind", 3, 7): {0: 100000}, + ("Category Four of a Kind", 3, 8): {0: 100000}, + ("Category Four of a Kind", 4, 0): {0: 100000}, + ("Category Four of a Kind", 4, 1): {0: 99516, 30: 484}, + ("Category Four of a Kind", 4, 2): {0: 96122, 30: 3878}, + ("Category Four of a Kind", 4, 3): {0: 89867, 30: 10133}, + ("Category Four of a Kind", 4, 4): {0: 81771, 30: 18229}, + ("Category Four of a Kind", 4, 5): {0: 72893, 30: 27107}, + ("Category Four of a Kind", 4, 6): {0: 64000, 30: 36000}, + ("Category Four of a Kind", 4, 7): {0: 55921, 30: 44079}, + ("Category Four of a Kind", 4, 8): {0: 48175, 30: 51825}, + ("Category Four of a Kind", 5, 0): {0: 100000}, + ("Category Four of a Kind", 5, 1): {0: 97938, 30: 2062}, + ("Category Four of a Kind", 5, 2): {0: 86751, 30: 13249}, + ("Category Four of a Kind", 5, 3): {0: 70886, 30: 29114}, + ("Category Four of a Kind", 5, 4): {0: 54807, 30: 45193}, + ("Category Four of a Kind", 5, 5): {0: 41729, 30: 58271}, + ("Category Four of a Kind", 5, 6): {0: 30960, 30: 69040}, + ("Category Four of a Kind", 5, 7): {0: 22207, 30: 77793}, + ("Category Four of a Kind", 5, 8): {0: 16027, 30: 83973}, + ("Category Four of a Kind", 6, 0): {0: 100000}, + ("Category Four of a Kind", 6, 1): {0: 94810, 30: 5190}, + ("Category Four of a Kind", 6, 2): {0: 73147, 30: 26853}, + ("Category Four of a Kind", 6, 3): {0: 49873, 30: 50127}, + ("Category Four of a Kind", 6, 4): {0: 31913, 30: 68087}, + ("Category Four of a Kind", 6, 5): {0: 19877, 30: 80123}, + ("Category Four of a Kind", 6, 6): {0: 11973, 30: 88027}, + ("Category Four of a Kind", 6, 7): {0: 7324, 30: 92676}, + ("Category Four of a Kind", 6, 8): {0: 4221, 30: 95779}, + ("Category Four of a Kind", 7, 0): {0: 100000}, + ("Category Four of a Kind", 7, 1): {0: 89422, 30: 10578}, + ("Category Four of a Kind", 7, 2): {0: 57049, 30: 42951}, + ("Category Four of a Kind", 7, 3): {0: 30903, 30: 69097}, + ("Category Four of a Kind", 7, 4): {0: 15962, 30: 84038}, + ("Category Four of a Kind", 7, 5): {0: 8148, 30: 91852}, + ("Category Four of a Kind", 7, 6): {0: 3943, 30: 96057}, + ("Category Four of a Kind", 7, 7): {0: 1933, 30: 98067}, + ("Category Four of a Kind", 7, 8): {0: 912, 30: 99088}, + ("Category Four of a Kind", 8, 0): {0: 100000}, + ("Category Four of a Kind", 8, 1): {0: 81614, 30: 18386}, + ("Category Four of a Kind", 8, 2): {0: 40524, 30: 59476}, + ("Category Four of a Kind", 8, 3): {0: 17426, 30: 82574}, + ("Category Four of a Kind", 8, 4): {0: 6958, 30: 93042}, + ("Category Four of a Kind", 8, 5): {0: 2862, 30: 97138}, + ("Category Four of a Kind", 8, 6): {0: 1049, 30: 98951}, + ("Category Four of a Kind", 8, 7): {0: 401, 30: 99599}, + ("Category Four of a Kind", 8, 8): {0: 156, 30: 99844}, + ("Category Tiny Straight", 0, 0): {0: 100000}, + ("Category Tiny Straight", 0, 1): {0: 100000}, + ("Category Tiny Straight", 0, 2): {0: 100000}, + ("Category Tiny Straight", 0, 3): {0: 100000}, + ("Category Tiny Straight", 0, 4): {0: 100000}, + ("Category Tiny Straight", 0, 5): {0: 100000}, + ("Category Tiny Straight", 0, 6): {0: 100000}, + ("Category Tiny Straight", 0, 7): {0: 100000}, + ("Category Tiny Straight", 0, 8): {0: 100000}, + ("Category Tiny Straight", 1, 0): {0: 100000}, + ("Category Tiny Straight", 1, 1): {0: 100000}, + ("Category Tiny Straight", 1, 2): {0: 100000}, + ("Category Tiny Straight", 1, 3): {0: 100000}, + ("Category Tiny Straight", 1, 4): {0: 100000}, + ("Category Tiny Straight", 1, 5): {0: 100000}, + ("Category Tiny Straight", 1, 6): {0: 100000}, + ("Category Tiny Straight", 1, 7): {0: 100000}, + ("Category Tiny Straight", 1, 8): {0: 100000}, + ("Category Tiny Straight", 2, 0): {0: 100000}, + ("Category Tiny Straight", 2, 1): {0: 100000}, + ("Category Tiny Straight", 2, 2): {0: 100000}, + ("Category Tiny Straight", 2, 3): {0: 100000}, + ("Category Tiny Straight", 2, 4): {0: 100000}, + ("Category Tiny Straight", 2, 5): {0: 100000}, + ("Category Tiny Straight", 2, 6): {0: 100000}, + ("Category Tiny Straight", 2, 7): {0: 100000}, + ("Category Tiny Straight", 2, 8): {0: 100000}, + ("Category Tiny Straight", 3, 0): {0: 100000}, + ("Category Tiny Straight", 3, 1): {0: 91672, 20: 8328}, + ("Category Tiny Straight", 3, 2): {0: 79082, 20: 20918}, + ("Category Tiny Straight", 3, 3): {0: 66490, 20: 33510}, + ("Category Tiny Straight", 3, 4): {0: 55797, 20: 44203}, + ("Category Tiny Straight", 3, 5): {0: 46967, 20: 53033}, + ("Category Tiny Straight", 3, 6): {0: 39595, 20: 60405}, + ("Category Tiny Straight", 3, 7): {0: 33384, 20: 66616}, + ("Category Tiny Straight", 3, 8): {0: 28747, 20: 71253}, + ("Category Tiny Straight", 4, 0): {0: 100000}, + ("Category Tiny Straight", 4, 1): {0: 78812, 20: 21188}, + ("Category Tiny Straight", 4, 2): {0: 55525, 20: 44475}, + ("Category Tiny Straight", 4, 3): {0: 38148, 20: 61852}, + ("Category Tiny Straight", 4, 4): {0: 26432, 20: 73568}, + ("Category Tiny Straight", 4, 5): {0: 18225, 20: 81775}, + ("Category Tiny Straight", 4, 6): {0: 12758, 20: 87242}, + ("Category Tiny Straight", 4, 7): {0: 8991, 20: 91009}, + ("Category Tiny Straight", 4, 8): {0: 6325, 20: 93675}, + ("Category Tiny Straight", 5, 0): {0: 100000}, + ("Category Tiny Straight", 5, 1): {0: 64979, 20: 35021}, + ("Category Tiny Straight", 5, 2): {0: 36509, 20: 63491}, + ("Category Tiny Straight", 5, 3): {0: 20576, 20: 79424}, + ("Category Tiny Straight", 5, 4): {0: 11585, 20: 88415}, + ("Category Tiny Straight", 5, 5): {0: 6874, 20: 93126}, + ("Category Tiny Straight", 5, 6): {0: 3798, 20: 96202}, + ("Category Tiny Straight", 5, 7): {0: 2214, 20: 97786}, + ("Category Tiny Straight", 5, 8): {0: 1272, 20: 98728}, + ("Category Tiny Straight", 6, 0): {0: 100000}, + ("Category Tiny Straight", 6, 1): {0: 52157, 20: 47843}, + ("Category Tiny Straight", 6, 2): {0: 23641, 20: 76359}, + ("Category Tiny Straight", 6, 3): {0: 10883, 20: 89117}, + ("Category Tiny Straight", 6, 4): {0: 5127, 20: 94873}, + ("Category Tiny Straight", 6, 5): {0: 2442, 20: 97558}, + ("Category Tiny Straight", 6, 6): {0: 1158, 20: 98842}, + ("Category Tiny Straight", 6, 7): {0: 542, 20: 99458}, + ("Category Tiny Straight", 6, 8): {0: 252, 20: 99748}, + ("Category Tiny Straight", 7, 0): {0: 100000}, + ("Category Tiny Straight", 7, 1): {0: 41492, 20: 58508}, + ("Category Tiny Straight", 7, 2): {0: 15072, 20: 84928}, + ("Category Tiny Straight", 7, 3): {0: 5905, 20: 94095}, + ("Category Tiny Straight", 7, 4): {0: 2246, 20: 97754}, + ("Category Tiny Straight", 7, 5): {0: 942, 20: 99058}, + ("Category Tiny Straight", 7, 6): {0: 337, 20: 99663}, + ("Category Tiny Straight", 7, 7): {0: 155, 20: 99845}, + ("Category Tiny Straight", 7, 8): {0: 61, 20: 99939}, + ("Category Tiny Straight", 8, 0): {0: 100000}, + ("Category Tiny Straight", 8, 1): {0: 32993, 20: 67007}, + ("Category Tiny Straight", 8, 2): {0: 10074, 20: 89926}, + ("Category Tiny Straight", 8, 3): {0: 3158, 20: 96842}, + ("Category Tiny Straight", 8, 4): {0: 1060, 20: 98940}, + ("Category Tiny Straight", 8, 5): {0: 356, 20: 99644}, + ("Category Tiny Straight", 8, 6): {0: 117, 20: 99883}, + ("Category Tiny Straight", 8, 7): {0: 32, 20: 99968}, + ("Category Tiny Straight", 8, 8): {0: 10, 20: 99990}, + ("Category Small Straight", 0, 0): {0: 100000}, + ("Category Small Straight", 0, 1): {0: 100000}, + ("Category Small Straight", 0, 2): {0: 100000}, + ("Category Small Straight", 0, 3): {0: 100000}, + ("Category Small Straight", 0, 4): {0: 100000}, + ("Category Small Straight", 0, 5): {0: 100000}, + ("Category Small Straight", 0, 6): {0: 100000}, + ("Category Small Straight", 0, 7): {0: 100000}, + ("Category Small Straight", 0, 8): {0: 100000}, + ("Category Small Straight", 1, 0): {0: 100000}, + ("Category Small Straight", 1, 1): {0: 100000}, + ("Category Small Straight", 1, 2): {0: 100000}, + ("Category Small Straight", 1, 3): {0: 100000}, + ("Category Small Straight", 1, 4): {0: 100000}, + ("Category Small Straight", 1, 5): {0: 100000}, + ("Category Small Straight", 1, 6): {0: 100000}, + ("Category Small Straight", 1, 7): {0: 100000}, + ("Category Small Straight", 1, 8): {0: 100000}, + ("Category Small Straight", 2, 0): {0: 100000}, + ("Category Small Straight", 2, 1): {0: 100000}, + ("Category Small Straight", 2, 2): {0: 100000}, + ("Category Small Straight", 2, 3): {0: 100000}, + ("Category Small Straight", 2, 4): {0: 100000}, + ("Category Small Straight", 2, 5): {0: 100000}, + ("Category Small Straight", 2, 6): {0: 100000}, + ("Category Small Straight", 2, 7): {0: 100000}, + ("Category Small Straight", 2, 8): {0: 100000}, + ("Category Small Straight", 3, 0): {0: 100000}, + ("Category Small Straight", 3, 1): {0: 100000}, + ("Category Small Straight", 3, 2): {0: 100000}, + ("Category Small Straight", 3, 3): {0: 100000}, + ("Category Small Straight", 3, 4): {0: 100000}, + ("Category Small Straight", 3, 5): {0: 100000}, + ("Category Small Straight", 3, 6): {0: 100000}, + ("Category Small Straight", 3, 7): {0: 100000}, + ("Category Small Straight", 3, 8): {0: 100000}, + ("Category Small Straight", 4, 0): {0: 100000}, + ("Category Small Straight", 4, 1): {0: 94516, 30: 5484}, + ("Category Small Straight", 4, 2): {0: 82700, 30: 17300}, + ("Category Small Straight", 4, 3): {0: 67926, 30: 32074}, + ("Category Small Straight", 4, 4): {0: 54265, 30: 45735}, + ("Category Small Straight", 4, 5): {0: 42130, 30: 57870}, + ("Category Small Straight", 4, 6): {0: 32536, 30: 67464}, + ("Category Small Straight", 4, 7): {0: 25008, 30: 74992}, + ("Category Small Straight", 4, 8): {0: 19595, 30: 80405}, + ("Category Small Straight", 5, 0): {0: 100000}, + ("Category Small Straight", 5, 1): {0: 84528, 30: 15472}, + ("Category Small Straight", 5, 2): {0: 60775, 30: 39225}, + ("Category Small Straight", 5, 3): {0: 39543, 30: 60457}, + ("Category Small Straight", 5, 4): {0: 24760, 30: 75240}, + ("Category Small Straight", 5, 5): {0: 15713, 30: 84287}, + ("Category Small Straight", 5, 6): {0: 10199, 30: 89801}, + ("Category Small Straight", 5, 7): {0: 6618, 30: 93382}, + ("Category Small Straight", 5, 8): {0: 4205, 30: 95795}, + ("Category Small Straight", 6, 0): {0: 100000}, + ("Category Small Straight", 6, 1): {0: 73121, 30: 26879}, + ("Category Small Straight", 6, 2): {0: 41832, 30: 58168}, + ("Category Small Straight", 6, 3): {0: 21949, 30: 78051}, + ("Category Small Straight", 6, 4): {0: 11304, 30: 88696}, + ("Category Small Straight", 6, 5): {0: 6063, 30: 93937}, + ("Category Small Straight", 6, 6): {0: 3362, 30: 96638}, + ("Category Small Straight", 6, 7): {0: 1799, 30: 98201}, + ("Category Small Straight", 6, 8): {0: 1069, 30: 98931}, + ("Category Small Straight", 7, 0): {0: 100000}, + ("Category Small Straight", 7, 1): {0: 61837, 30: 38163}, + ("Category Small Straight", 7, 2): {0: 28202, 30: 71798}, + ("Category Small Straight", 7, 3): {0: 12187, 30: 87813}, + ("Category Small Straight", 7, 4): {0: 5427, 30: 94573}, + ("Category Small Straight", 7, 5): {0: 2444, 30: 97556}, + ("Category Small Straight", 7, 6): {0: 1144, 30: 98856}, + ("Category Small Straight", 7, 7): {0: 588, 30: 99412}, + ("Category Small Straight", 7, 8): {0: 258, 30: 99742}, + ("Category Small Straight", 8, 0): {0: 100000}, + ("Category Small Straight", 8, 1): {0: 51394, 30: 48606}, + ("Category Small Straight", 8, 2): {0: 19090, 30: 80910}, + ("Category Small Straight", 8, 3): {0: 7104, 30: 92896}, + ("Category Small Straight", 8, 4): {0: 2645, 30: 97355}, + ("Category Small Straight", 8, 5): {0: 1010, 30: 98990}, + ("Category Small Straight", 8, 6): {0: 408, 30: 99592}, + ("Category Small Straight", 8, 7): {0: 153, 30: 99847}, + ("Category Small Straight", 8, 8): {0: 78, 30: 99922}, + ("Category Large Straight", 0, 0): {0: 100000}, + ("Category Large Straight", 0, 1): {0: 100000}, + ("Category Large Straight", 0, 2): {0: 100000}, + ("Category Large Straight", 0, 3): {0: 100000}, + ("Category Large Straight", 0, 4): {0: 100000}, + ("Category Large Straight", 0, 5): {0: 100000}, + ("Category Large Straight", 0, 6): {0: 100000}, + ("Category Large Straight", 0, 7): {0: 100000}, + ("Category Large Straight", 0, 8): {0: 100000}, + ("Category Large Straight", 1, 0): {0: 100000}, + ("Category Large Straight", 1, 1): {0: 100000}, + ("Category Large Straight", 1, 2): {0: 100000}, + ("Category Large Straight", 1, 3): {0: 100000}, + ("Category Large Straight", 1, 4): {0: 100000}, + ("Category Large Straight", 1, 5): {0: 100000}, + ("Category Large Straight", 1, 6): {0: 100000}, + ("Category Large Straight", 1, 7): {0: 100000}, + ("Category Large Straight", 1, 8): {0: 100000}, + ("Category Large Straight", 2, 0): {0: 100000}, + ("Category Large Straight", 2, 1): {0: 100000}, + ("Category Large Straight", 2, 2): {0: 100000}, + ("Category Large Straight", 2, 3): {0: 100000}, + ("Category Large Straight", 2, 4): {0: 100000}, + ("Category Large Straight", 2, 5): {0: 100000}, + ("Category Large Straight", 2, 6): {0: 100000}, + ("Category Large Straight", 2, 7): {0: 100000}, + ("Category Large Straight", 2, 8): {0: 100000}, + ("Category Large Straight", 3, 0): {0: 100000}, + ("Category Large Straight", 3, 1): {0: 100000}, + ("Category Large Straight", 3, 2): {0: 100000}, + ("Category Large Straight", 3, 3): {0: 100000}, + ("Category Large Straight", 3, 4): {0: 100000}, + ("Category Large Straight", 3, 5): {0: 100000}, + ("Category Large Straight", 3, 6): {0: 100000}, + ("Category Large Straight", 3, 7): {0: 100000}, + ("Category Large Straight", 3, 8): {0: 100000}, + ("Category Large Straight", 4, 0): {0: 100000}, + ("Category Large Straight", 4, 1): {0: 100000}, + ("Category Large Straight", 4, 2): {0: 100000}, + ("Category Large Straight", 4, 3): {0: 100000}, + ("Category Large Straight", 4, 4): {0: 100000}, + ("Category Large Straight", 4, 5): {0: 100000}, + ("Category Large Straight", 4, 6): {0: 100000}, + ("Category Large Straight", 4, 7): {0: 100000}, + ("Category Large Straight", 4, 8): {0: 100000}, + ("Category Large Straight", 5, 0): {0: 100000}, + ("Category Large Straight", 5, 1): {0: 96929, 40: 3071}, + ("Category Large Straight", 5, 2): {0: 87056, 40: 12944}, + ("Category Large Straight", 5, 3): {0: 75101, 40: 24899}, + ("Category Large Straight", 5, 4): {0: 63617, 40: 36383}, + ("Category Large Straight", 5, 5): {0: 53149, 40: 46851}, + ("Category Large Straight", 5, 6): {0: 44321, 40: 55679}, + ("Category Large Straight", 5, 7): {0: 36948, 40: 63052}, + ("Category Large Straight", 5, 8): {0: 30661, 40: 69339}, + ("Category Large Straight", 6, 0): {0: 100000}, + ("Category Large Straight", 6, 1): {0: 90756, 40: 9244}, + ("Category Large Straight", 6, 2): {0: 69805, 40: 30195}, + ("Category Large Straight", 6, 3): {0: 49814, 40: 50186}, + ("Category Large Straight", 6, 4): {0: 35102, 40: 64898}, + ("Category Large Straight", 6, 5): {0: 24385, 40: 75615}, + ("Category Large Straight", 6, 6): {0: 17018, 40: 82982}, + ("Category Large Straight", 6, 7): {0: 11739, 40: 88261}, + ("Category Large Straight", 6, 8): {0: 7972, 40: 92028}, + ("Category Large Straight", 7, 0): {0: 100000}, + ("Category Large Straight", 7, 1): {0: 82840, 40: 17160}, + ("Category Large Straight", 7, 2): {0: 52821, 40: 47179}, + ("Category Large Straight", 7, 3): {0: 31348, 40: 68652}, + ("Category Large Straight", 7, 4): {0: 18166, 40: 81834}, + ("Category Large Straight", 7, 5): {0: 10690, 40: 89310}, + ("Category Large Straight", 7, 6): {0: 6051, 40: 93949}, + ("Category Large Straight", 7, 7): {0: 3617, 40: 96383}, + ("Category Large Straight", 7, 8): {0: 1941, 40: 98059}, + ("Category Large Straight", 8, 0): {0: 100000}, + ("Category Large Straight", 8, 1): {0: 73520, 40: 26480}, + ("Category Large Straight", 8, 2): {0: 39031, 40: 60969}, + ("Category Large Straight", 8, 3): {0: 19156, 40: 80844}, + ("Category Large Straight", 8, 4): {0: 9304, 40: 90696}, + ("Category Large Straight", 8, 5): {0: 4420, 40: 95580}, + ("Category Large Straight", 8, 6): {0: 2141, 40: 97859}, + ("Category Large Straight", 8, 7): {0: 1037, 40: 98963}, + ("Category Large Straight", 8, 8): {0: 511, 40: 99489}, + ("Category Full House", 0, 0): {0: 100000}, + ("Category Full House", 0, 1): {0: 100000}, + ("Category Full House", 0, 2): {0: 100000}, + ("Category Full House", 0, 3): {0: 100000}, + ("Category Full House", 0, 4): {0: 100000}, + ("Category Full House", 0, 5): {0: 100000}, + ("Category Full House", 0, 6): {0: 100000}, + ("Category Full House", 0, 7): {0: 100000}, + ("Category Full House", 0, 8): {0: 100000}, + ("Category Full House", 1, 0): {0: 100000}, + ("Category Full House", 1, 1): {0: 100000}, + ("Category Full House", 1, 2): {0: 100000}, + ("Category Full House", 1, 3): {0: 100000}, + ("Category Full House", 1, 4): {0: 100000}, + ("Category Full House", 1, 5): {0: 100000}, + ("Category Full House", 1, 6): {0: 100000}, + ("Category Full House", 1, 7): {0: 100000}, + ("Category Full House", 1, 8): {0: 100000}, + ("Category Full House", 2, 0): {0: 100000}, + ("Category Full House", 2, 1): {0: 100000}, + ("Category Full House", 2, 2): {0: 100000}, + ("Category Full House", 2, 3): {0: 100000}, + ("Category Full House", 2, 4): {0: 100000}, + ("Category Full House", 2, 5): {0: 100000}, + ("Category Full House", 2, 6): {0: 100000}, + ("Category Full House", 2, 7): {0: 100000}, + ("Category Full House", 2, 8): {0: 100000}, + ("Category Full House", 3, 0): {0: 100000}, + ("Category Full House", 3, 1): {0: 100000}, + ("Category Full House", 3, 2): {0: 100000}, + ("Category Full House", 3, 3): {0: 100000}, + ("Category Full House", 3, 4): {0: 100000}, + ("Category Full House", 3, 5): {0: 100000}, + ("Category Full House", 3, 6): {0: 100000}, + ("Category Full House", 3, 7): {0: 100000}, + ("Category Full House", 3, 8): {0: 100000}, + ("Category Full House", 4, 0): {0: 100000}, + ("Category Full House", 4, 1): {0: 100000}, + ("Category Full House", 4, 2): {0: 100000}, + ("Category Full House", 4, 3): {0: 100000}, + ("Category Full House", 4, 4): {0: 100000}, + ("Category Full House", 4, 5): {0: 100000}, + ("Category Full House", 4, 6): {0: 100000}, + ("Category Full House", 4, 7): {0: 100000}, + ("Category Full House", 4, 8): {0: 100000}, + ("Category Full House", 5, 0): {0: 100000}, + ("Category Full House", 5, 1): {0: 96155, 25: 3845}, + ("Category Full House", 5, 2): {0: 81391, 25: 18609}, + ("Category Full House", 5, 3): {0: 64300, 25: 35700}, + ("Category Full House", 5, 4): {0: 49669, 25: 50331}, + ("Category Full House", 5, 5): {0: 38019, 25: 61981}, + ("Category Full House", 5, 6): {0: 29751, 25: 70249}, + ("Category Full House", 5, 7): {0: 22960, 25: 77040}, + ("Category Full House", 5, 8): {0: 18650, 25: 81350}, + ("Category Full House", 6, 0): {0: 100000}, + ("Category Full House", 6, 1): {0: 82989, 25: 17011}, + ("Category Full House", 6, 2): {0: 47153, 25: 52847}, + ("Category Full House", 6, 3): {0: 24151, 25: 75849}, + ("Category Full House", 6, 4): {0: 12519, 25: 87481}, + ("Category Full House", 6, 5): {0: 6524, 25: 93476}, + ("Category Full House", 6, 6): {0: 3606, 25: 96394}, + ("Category Full House", 6, 7): {0: 1959, 25: 98041}, + ("Category Full House", 6, 8): {0: 1026, 25: 98974}, + ("Category Full House", 7, 0): {0: 100000}, + ("Category Full House", 7, 1): {0: 60232, 25: 39768}, + ("Category Full House", 7, 2): {0: 18894, 25: 81106}, + ("Category Full House", 7, 3): {0: 5682, 25: 94318}, + ("Category Full House", 7, 4): {0: 1706, 25: 98294}, + ("Category Full House", 7, 5): {0: 522, 25: 99478}, + ("Category Full House", 7, 6): {0: 146, 25: 99854}, + ("Category Full House", 7, 7): {0: 54, 25: 99946}, + ("Category Full House", 7, 8): {0: 18, 25: 99982}, + ("Category Full House", 8, 0): {0: 100000}, + ("Category Full House", 8, 1): {0: 35909, 25: 64091}, + ("Category Full House", 8, 2): {0: 5712, 25: 94288}, + ("Category Full House", 8, 3): {0: 930, 25: 99070}, + ("Category Full House", 8, 4): {0: 165, 25: 99835}, + ("Category Full House", 8, 5): {0: 19, 25: 99981}, + ("Category Full House", 8, 6): {0: 6, 25: 99994}, + ("Category Full House", 8, 7): {25: 100000}, + ("Category Full House", 8, 8): {25: 100000}, + ("Category Yacht", 0, 0): {0: 100000}, + ("Category Yacht", 0, 1): {0: 100000}, + ("Category Yacht", 0, 2): {0: 100000}, + ("Category Yacht", 0, 3): {0: 100000}, + ("Category Yacht", 0, 4): {0: 100000}, + ("Category Yacht", 0, 5): {0: 100000}, + ("Category Yacht", 0, 6): {0: 100000}, + ("Category Yacht", 0, 7): {0: 100000}, + ("Category Yacht", 0, 8): {0: 100000}, + ("Category Yacht", 1, 0): {0: 100000}, + ("Category Yacht", 1, 1): {0: 100000}, + ("Category Yacht", 1, 2): {0: 100000}, + ("Category Yacht", 1, 3): {0: 100000}, + ("Category Yacht", 1, 4): {0: 100000}, + ("Category Yacht", 1, 5): {0: 100000}, + ("Category Yacht", 1, 6): {0: 100000}, + ("Category Yacht", 1, 7): {0: 100000}, + ("Category Yacht", 1, 8): {0: 100000}, + ("Category Yacht", 2, 0): {0: 100000}, + ("Category Yacht", 2, 1): {0: 100000}, + ("Category Yacht", 2, 2): {0: 100000}, + ("Category Yacht", 2, 3): {0: 100000}, + ("Category Yacht", 2, 4): {0: 100000}, + ("Category Yacht", 2, 5): {0: 100000}, + ("Category Yacht", 2, 6): {0: 100000}, + ("Category Yacht", 2, 7): {0: 100000}, + ("Category Yacht", 2, 8): {0: 100000}, + ("Category Yacht", 3, 0): {0: 100000}, + ("Category Yacht", 3, 1): {0: 100000}, + ("Category Yacht", 3, 2): {0: 100000}, + ("Category Yacht", 3, 3): {0: 100000}, + ("Category Yacht", 3, 4): {0: 100000}, + ("Category Yacht", 3, 5): {0: 100000}, + ("Category Yacht", 3, 6): {0: 100000}, + ("Category Yacht", 3, 7): {0: 100000}, + ("Category Yacht", 3, 8): {0: 100000}, + ("Category Yacht", 4, 0): {0: 100000}, + ("Category Yacht", 4, 1): {0: 100000}, + ("Category Yacht", 4, 2): {0: 100000}, + ("Category Yacht", 4, 3): {0: 100000}, + ("Category Yacht", 4, 4): {0: 100000}, + ("Category Yacht", 4, 5): {0: 100000}, + ("Category Yacht", 4, 6): {0: 100000}, + ("Category Yacht", 4, 7): {0: 100000}, + ("Category Yacht", 4, 8): {0: 100000}, + ("Category Yacht", 5, 0): {0: 100000}, + ("Category Yacht", 5, 1): {0: 100000}, + ("Category Yacht", 5, 2): {0: 98727, 50: 1273}, + ("Category Yacht", 5, 3): {0: 95347, 50: 4653}, + ("Category Yacht", 5, 4): {0: 89969, 50: 10031}, + ("Category Yacht", 5, 5): {0: 83124, 50: 16876}, + ("Category Yacht", 5, 6): {0: 75023, 50: 24977}, + ("Category Yacht", 5, 7): {0: 67007, 50: 32993}, + ("Category Yacht", 5, 8): {0: 58618, 50: 41382}, + ("Category Yacht", 6, 0): {0: 100000}, + ("Category Yacht", 6, 1): {0: 99571, 50: 429}, + ("Category Yacht", 6, 2): {0: 94726, 50: 5274}, + ("Category Yacht", 6, 3): {0: 84366, 50: 15634}, + ("Category Yacht", 6, 4): {0: 70782, 50: 29218}, + ("Category Yacht", 6, 5): {0: 56573, 50: 43427}, + ("Category Yacht", 6, 6): {0: 44206, 50: 55794}, + ("Category Yacht", 6, 7): {0: 33578, 50: 66422}, + ("Category Yacht", 6, 8): {0: 25079, 50: 74921}, + ("Category Yacht", 7, 0): {0: 100000}, + ("Category Yacht", 7, 1): {0: 98833, 50: 1167}, + ("Category Yacht", 7, 2): {0: 87511, 50: 12489}, + ("Category Yacht", 7, 3): {0: 68252, 50: 31748}, + ("Category Yacht", 7, 4): {0: 49065, 50: 50935}, + ("Category Yacht", 7, 5): {0: 33364, 50: 66636}, + ("Category Yacht", 7, 6): {0: 21483, 50: 78517}, + ("Category Yacht", 7, 7): {0: 13597, 50: 86403}, + ("Category Yacht", 7, 8): {0: 8483, 50: 91517}, + ("Category Yacht", 8, 0): {0: 100000}, + ("Category Yacht", 8, 1): {0: 97212, 50: 2788}, + ("Category Yacht", 8, 2): {0: 76962, 50: 23038}, + ("Category Yacht", 8, 3): {0: 50533, 50: 49467}, + ("Category Yacht", 8, 4): {0: 29981, 50: 70019}, + ("Category Yacht", 8, 5): {0: 16776, 50: 83224}, + ("Category Yacht", 8, 6): {0: 9079, 50: 90921}, + ("Category Yacht", 8, 7): {0: 4705, 50: 95295}, + ("Category Yacht", 8, 8): {0: 2363, 50: 97637}, + ("Category Distincts", 1, 1): {1: 100000}, + ("Category Distincts", 1, 2): {1: 100000}, + ("Category Distincts", 1, 3): {1: 100000}, + ("Category Distincts", 1, 4): {1: 100000}, + ("Category Distincts", 1, 5): {1: 100000}, + ("Category Distincts", 1, 6): {1: 100000}, + ("Category Distincts", 1, 7): {1: 100000}, + ("Category Distincts", 1, 8): {1: 100000}, + ("Category Distincts", 2, 1): {1: 16804, 2: 83196}, + ("Category Distincts", 2, 2): {1: 2686, 2: 97314}, + ("Category Distincts", 2, 3): {1: 463, 2: 99537}, + ("Category Distincts", 2, 4): {1: 66, 2: 99934}, + ("Category Distincts", 2, 5): {1: 11, 2: 99989}, + ("Category Distincts", 2, 6): {1: 1, 2: 99999}, + ("Category Distincts", 2, 7): {2: 100000}, + ("Category Distincts", 2, 8): {2: 100000}, + ("Category Distincts", 3, 1): {1: 2760, 2: 41714, 3: 55526}, + ("Category Distincts", 3, 2): {1: 78, 3: 99922}, + ("Category Distincts", 3, 3): {1: 4866, 3: 95134}, + ("Category Distincts", 3, 4): {2: 1659, 3: 98341}, + ("Category Distincts", 3, 5): {2: 575, 3: 99425}, + ("Category Distincts", 3, 6): {2: 200, 3: 99800}, + ("Category Distincts", 3, 7): {2: 69, 3: 99931}, + ("Category Distincts", 3, 8): {2: 22, 3: 99978}, + ("Category Distincts", 4, 1): {1: 494, 3: 71611, 4: 27895}, + ("Category Distincts", 4, 2): {1: 1893, 3: 36922, 4: 61185}, + ("Category Distincts", 4, 3): {2: 230, 4: 99770}, + ("Category Distincts", 4, 4): {2: 21, 4: 99979}, + ("Category Distincts", 4, 5): {2: 4906, 4: 95094}, + ("Category Distincts", 4, 6): {3: 2494, 4: 97506}, + ("Category Distincts", 4, 7): {3: 1297, 4: 98703}, + ("Category Distincts", 4, 8): {3: 611, 4: 99389}, + ("Category Distincts", 5, 1): {1: 5798, 3: 38538, 4: 55664}, + ("Category Distincts", 5, 2): {2: 196, 4: 68119, 5: 31685}, + ("Category Distincts", 5, 3): {2: 3022, 4: 44724, 5: 52254}, + ("Category Distincts", 5, 4): {3: 722, 4: 31632, 5: 67646}, + ("Category Distincts", 5, 5): {3: 215, 4: 21391, 5: 78394}, + ("Category Distincts", 5, 6): {3: 55, 5: 99945}, + ("Category Distincts", 5, 7): {3: 15, 5: 99985}, + ("Category Distincts", 5, 8): {3: 6463, 5: 93537}, + ("Category Distincts", 6, 1): {1: 2027, 3: 22985, 4: 50464, 5: 24524}, + ("Category Distincts", 6, 2): {2: 3299, 4: 35174, 5: 61527}, + ("Category Distincts", 6, 3): {3: 417, 5: 79954, 6: 19629}, + ("Category Distincts", 6, 4): {3: 7831, 5: 61029, 6: 31140}, + ("Category Distincts", 6, 5): {3: 3699, 5: 54997, 6: 41304}, + ("Category Distincts", 6, 6): {4: 1557, 5: 47225, 6: 51218}, + ("Category Distincts", 6, 7): {4: 728, 5: 40465, 6: 58807}, + ("Category Distincts", 6, 8): {4: 321, 5: 33851, 6: 65828}, + ("Category Distincts", 7, 1): {1: 665, 4: 57970, 5: 41365}, + ("Category Distincts", 7, 2): {2: 839, 5: 75578, 6: 23583}, + ("Category Distincts", 7, 3): {3: 6051, 5: 50312, 6: 43637}, + ("Category Distincts", 7, 4): {3: 1796, 5: 38393, 6: 59811}, + ("Category Distincts", 7, 5): {4: 529, 5: 27728, 6: 71743}, + ("Category Distincts", 7, 6): {4: 164, 6: 99836}, + ("Category Distincts", 7, 7): {4: 53, 6: 99947}, + ("Category Distincts", 7, 8): {4: 14, 6: 99986}, + ("Category Distincts", 8, 1): {1: 7137, 4: 36582, 5: 56281}, + ("Category Distincts", 8, 2): {2: 233, 5: 59964, 6: 39803}, + ("Category Distincts", 8, 3): {3: 1976, 5: 34748, 6: 63276}, + ("Category Distincts", 8, 4): {4: 389, 5: 21008, 6: 78603}, + ("Category Distincts", 8, 5): {4: 78, 6: 99922}, + ("Category Distincts", 8, 6): {4: 7177, 6: 92823}, + ("Category Distincts", 8, 7): {4: 4179, 6: 95821}, + ("Category Distincts", 8, 8): {5: 2440, 6: 97560}, + ("Category Two times Ones", 0, 0): {0: 100000}, + ("Category Two times Ones", 0, 1): {0: 100000}, + ("Category Two times Ones", 0, 2): {0: 100000}, + ("Category Two times Ones", 0, 3): {0: 100000}, + ("Category Two times Ones", 0, 4): {0: 100000}, + ("Category Two times Ones", 0, 5): {0: 100000}, + ("Category Two times Ones", 0, 6): {0: 100000}, + ("Category Two times Ones", 0, 7): {0: 100000}, + ("Category Two times Ones", 0, 8): {0: 100000}, + ("Category Two times Ones", 1, 0): {0: 100000}, + ("Category Two times Ones", 1, 1): {0: 83475, 2: 16525}, + ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, + ("Category Two times Ones", 1, 3): {0: 57818, 2: 42182}, + ("Category Two times Ones", 1, 4): {0: 48418, 2: 51582}, + ("Category Two times Ones", 1, 5): {0: 40301, 2: 59699}, + ("Category Two times Ones", 1, 6): {0: 33558, 2: 66442}, + ("Category Two times Ones", 1, 7): {0: 28182, 2: 71818}, + ("Category Two times Ones", 1, 8): {0: 23406, 2: 76594}, + ("Category Two times Ones", 2, 0): {0: 100000}, + ("Category Two times Ones", 2, 1): {0: 69724, 2: 30276}, + ("Category Two times Ones", 2, 2): {0: 48238, 2: 42479, 4: 9283}, + ("Category Two times Ones", 2, 3): {0: 33290, 2: 48819, 4: 17891}, + ("Category Two times Ones", 2, 4): {0: 23136, 2: 49957, 4: 26907}, + ("Category Two times Ones", 2, 5): {0: 16146, 2: 48200, 4: 35654}, + ("Category Two times Ones", 2, 6): {0: 11083, 2: 44497, 4: 44420}, + ("Category Two times Ones", 2, 7): {0: 7662, 2: 40343, 4: 51995}, + ("Category Two times Ones", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Two times Ones", 3, 0): {0: 100000}, + ("Category Two times Ones", 3, 1): {0: 58021, 2: 34522, 4: 7457}, + ("Category Two times Ones", 3, 2): {0: 33548, 2: 44261, 4: 22191}, + ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, + ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, + ("Category Two times Ones", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, + ("Category Two times Ones", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, + ("Category Two times Ones", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, + ("Category Two times Ones", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Two times Ones", 4, 0): {0: 100000}, + ("Category Two times Ones", 4, 1): {0: 48235, 2: 38602, 4: 13163}, + ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, + ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, + ("Category Two times Ones", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, + ("Category Two times Ones", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, + ("Category Two times Ones", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, + ("Category Two times Ones", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, + ("Category Two times Ones", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Two times Ones", 5, 0): {0: 100000}, + ("Category Two times Ones", 5, 1): {0: 40028, 2: 40241, 4: 19731}, + ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, + ("Category Two times Ones", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, + ("Category Two times Ones", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, + ("Category Two times Ones", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, + ("Category Two times Ones", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, + ("Category Two times Ones", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, + ("Category Two times Ones", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Two times Ones", 6, 0): {0: 100000}, + ("Category Two times Ones", 6, 1): {0: 33502, 2: 40413, 4: 26085}, + ("Category Two times Ones", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, + ("Category Two times Ones", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, + ("Category Two times Ones", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, + ("Category Two times Ones", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, + ("Category Two times Ones", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, + ("Category Two times Ones", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, + ("Category Two times Ones", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Two times Ones", 7, 0): {0: 100000}, + ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, + ("Category Two times Ones", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, + ("Category Two times Ones", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, + ("Category Two times Ones", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, + ("Category Two times Ones", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, + ("Category Two times Ones", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, + ("Category Two times Ones", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, + ("Category Two times Ones", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Two times Ones", 8, 0): {0: 100000}, + ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, + ("Category Two times Ones", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, + ("Category Two times Ones", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, + ("Category Two times Ones", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, + ("Category Two times Ones", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, + ("Category Two times Ones", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, + ("Category Two times Ones", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, + ("Category Two times Ones", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Half of Sixes", 0, 0): {0: 100000}, + ("Category Half of Sixes", 0, 1): {0: 100000}, + ("Category Half of Sixes", 0, 2): {0: 100000}, + ("Category Half of Sixes", 0, 3): {0: 100000}, + ("Category Half of Sixes", 0, 4): {0: 100000}, + ("Category Half of Sixes", 0, 5): {0: 100000}, + ("Category Half of Sixes", 0, 6): {0: 100000}, + ("Category Half of Sixes", 0, 7): {0: 100000}, + ("Category Half of Sixes", 0, 8): {0: 100000}, + ("Category Half of Sixes", 1, 0): {0: 100000}, + ("Category Half of Sixes", 1, 1): {0: 83343, 3: 16657}, + ("Category Half of Sixes", 1, 2): {0: 69569, 3: 30431}, + ("Category Half of Sixes", 1, 3): {0: 57872, 3: 42128}, + ("Category Half of Sixes", 1, 4): {0: 48081, 3: 51919}, + ("Category Half of Sixes", 1, 5): {0: 40271, 3: 59729}, + ("Category Half of Sixes", 1, 6): {0: 33201, 3: 66799}, + ("Category Half of Sixes", 1, 7): {0: 27903, 3: 72097}, + ("Category Half of Sixes", 1, 8): {0: 23240, 3: 76760}, + ("Category Half of Sixes", 2, 0): {0: 100000}, + ("Category Half of Sixes", 2, 1): {0: 69419, 3: 30581}, + ("Category Half of Sixes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, + ("Category Half of Sixes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, + ("Category Half of Sixes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, + ("Category Half of Sixes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, + ("Category Half of Sixes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, + ("Category Half of Sixes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, + ("Category Half of Sixes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Half of Sixes", 3, 0): {0: 100000}, + ("Category Half of Sixes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, + ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, + ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, + ("Category Half of Sixes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, + ("Category Half of Sixes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, + ("Category Half of Sixes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, + ("Category Half of Sixes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Half of Sixes", 4, 0): {0: 100000}, + ("Category Half of Sixes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, + ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, + ("Category Half of Sixes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, + ("Category Half of Sixes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, + ("Category Half of Sixes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, + ("Category Half of Sixes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, + ("Category Half of Sixes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, + ("Category Half of Sixes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Half of Sixes", 5, 0): {0: 100000}, + ("Category Half of Sixes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, + ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, + ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, + ("Category Half of Sixes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, + ("Category Half of Sixes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, + ("Category Half of Sixes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, + ("Category Half of Sixes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, + ("Category Half of Sixes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Half of Sixes", 6, 0): {0: 100000}, + ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, + ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, + ("Category Half of Sixes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, + ("Category Half of Sixes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, + ("Category Half of Sixes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, + ("Category Half of Sixes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, + ("Category Half of Sixes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, + ("Category Half of Sixes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Half of Sixes", 7, 0): {0: 100000}, + ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, + ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, + ("Category Half of Sixes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, + ("Category Half of Sixes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, + ("Category Half of Sixes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, + ("Category Half of Sixes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, + ("Category Half of Sixes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, + ("Category Half of Sixes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Half of Sixes", 8, 0): {0: 100000}, + ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, + ("Category Half of Sixes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, + ("Category Half of Sixes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, + ("Category Half of Sixes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, + ("Category Half of Sixes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, + ("Category Half of Sixes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, + ("Category Half of Sixes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, + ("Category Half of Sixes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, + ("Category Twos and Threes", 1, 1): {0: 66466, 3: 33534}, + ("Category Twos and Threes", 1, 2): {0: 55640, 3: 44360}, + ("Category Twos and Threes", 1, 3): {0: 46223, 3: 53777}, + ("Category Twos and Threes", 1, 4): {0: 38552, 3: 61448}, + ("Category Twos and Threes", 1, 5): {0: 32320, 3: 67680}, + ("Category Twos and Threes", 1, 6): {0: 26733, 3: 73267}, + ("Category Twos and Threes", 1, 7): {0: 22289, 3: 77711}, + ("Category Twos and Threes", 1, 8): {0: 18676, 3: 81324}, + ("Category Twos and Threes", 2, 1): {0: 44565, 2: 21965, 3: 25172, 5: 8298}, + ("Category Twos and Threes", 2, 2): {0: 30855, 3: 51429, 6: 17716}, + ("Category Twos and Threes", 2, 3): {0: 21509, 3: 51178, 6: 27313}, + ("Category Twos and Threes", 2, 4): {0: 14935, 3: 48581, 6: 36484}, + ("Category Twos and Threes", 2, 5): {0: 10492, 3: 44256, 6: 45252}, + ("Category Twos and Threes", 2, 6): {0: 10775, 3: 35936, 6: 53289}, + ("Category Twos and Threes", 2, 7): {0: 7375, 3: 32469, 6: 60156}, + ("Category Twos and Threes", 2, 8): {0: 5212, 3: 35730, 6: 59058}, + ("Category Twos and Threes", 3, 1): {0: 29892, 2: 22136, 3: 27781, 6: 20191}, + ("Category Twos and Threes", 3, 2): {0: 17285, 3: 44257, 6: 38458}, + ("Category Twos and Threes", 3, 3): {0: 9889, 3: 36505, 6: 40112, 8: 13494}, + ("Category Twos and Threes", 3, 4): {0: 5717, 3: 28317, 6: 43044, 9: 22922}, + ("Category Twos and Threes", 3, 5): {0: 5795, 3: 19123, 6: 45004, 9: 30078}, + ("Category Twos and Threes", 3, 6): {0: 3273, 3: 21888, 6: 36387, 9: 38452}, + ("Category Twos and Threes", 3, 7): {0: 1917, 3: 16239, 6: 35604, 9: 46240}, + ("Category Twos and Threes", 3, 8): {0: 1124, 3: 12222, 6: 33537, 9: 53117}, + ("Category Twos and Threes", 4, 1): {0: 19619, 3: 46881, 6: 33500}, + ("Category Twos and Threes", 4, 2): {0: 9395, 3: 33926, 6: 37832, 9: 18847}, + ("Category Twos and Threes", 4, 3): {0: 4538, 3: 22968, 6: 38891, 9: 33603}, + ("Category Twos and Threes", 4, 4): {0: 4402, 3: 12654, 6: 35565, 9: 34784, 11: 12595}, + ("Category Twos and Threes", 4, 5): {0: 2065, 3: 14351, 6: 23592, 9: 38862, 12: 21130}, + ("Category Twos and Threes", 4, 6): {0: 1044, 3: 9056, 6: 20013, 9: 41255, 12: 28632}, + ("Category Twos and Threes", 4, 7): {0: 6310, 7: 24021, 9: 34297, 12: 35372}, + ("Category Twos and Threes", 4, 8): {0: 3694, 6: 18611, 9: 34441, 12: 43254}, + ("Category Twos and Threes", 5, 1): {0: 13070, 3: 33021, 5: 24568, 6: 16417, 8: 12924}, + ("Category Twos and Threes", 5, 2): {0: 5213, 3: 24275, 6: 37166, 9: 24746, 11: 8600}, + ("Category Twos and Threes", 5, 3): {0: 4707, 3: 10959, 6: 31388, 9: 33265, 12: 19681}, + ("Category Twos and Threes", 5, 4): {0: 1934, 3: 12081, 6: 17567, 9: 35282, 12: 33136}, + ("Category Twos and Threes", 5, 5): {0: 380, 2: 7025, 6: 13268, 9: 33274, 12: 33255, 14: 12798}, + ("Category Twos and Threes", 5, 6): {0: 3745, 6: 15675, 9: 22902, 12: 44665, 15: 13013}, + ("Category Twos and Threes", 5, 7): {0: 1969, 6: 10700, 9: 19759, 12: 39522, 15: 28050}, + ("Category Twos and Threes", 5, 8): {0: 13, 2: 7713, 10: 23957, 12: 32501, 15: 35816}, + ("Category Twos and Threes", 6, 1): {0: 8955, 3: 26347, 5: 24850, 8: 39848}, + ("Category Twos and Threes", 6, 2): {0: 2944, 3: 16894, 6: 32156, 9: 37468, 12: 10538}, + ("Category Twos and Threes", 6, 3): {0: 2484, 3: 13120, 6: 15999, 9: 32271, 12: 24898, 14: 11228}, + ("Category Twos and Threes", 6, 4): {0: 320, 2: 6913, 6: 10814, 9: 28622, 12: 31337, 15: 21994}, + ("Category Twos and Threes", 6, 5): {0: 3135, 6: 12202, 9: 16495, 12: 33605, 15: 26330, 17: 8233}, + ("Category Twos and Threes", 6, 6): {0: 98, 3: 8409, 9: 12670, 12: 31959, 15: 38296, 18: 8568}, + ("Category Twos and Threes", 6, 7): {0: 4645, 9: 15210, 12: 21906, 15: 44121, 18: 14118}, + ("Category Twos and Threes", 6, 8): {0: 2367, 9: 10679, 12: 18916, 15: 38806, 18: 29232}, + ("Category Twos and Threes", 7, 1): {0: 5802, 3: 28169, 6: 26411, 9: 31169, 11: 8449}, + ("Category Twos and Threes", 7, 2): {0: 4415, 6: 34992, 9: 31238, 12: 20373, 14: 8982}, + ("Category Twos and Threes", 7, 3): {0: 471, 2: 8571, 6: 10929, 9: 28058, 12: 28900, 14: 14953, 16: 8118}, + ("Category Twos and Threes", 7, 4): {0: 3487, 6: 12139, 9: 14001, 12: 30314, 15: 23096, 18: 16963}, + ("Category Twos and Threes", 7, 5): {0: 40, 2: 7460, 12: 36006, 15: 31388, 18: 25106}, + ("Category Twos and Threes", 7, 6): {0: 3554, 9: 11611, 12: 15116, 15: 32501, 18: 27524, 20: 9694}, + ("Category Twos and Threes", 7, 7): {0: 157, 6: 8396, 13: 19880, 15: 22333, 18: 39121, 21: 10113}, + ("Category Twos and Threes", 7, 8): {0: 31, 5: 4682, 12: 14446, 15: 20934, 18: 44127, 21: 15780}, + ("Category Twos and Threes", 8, 1): {0: 3799, 3: 22551, 6: 23754, 8: 29290, 10: 11990, 12: 8616}, + ("Category Twos and Threes", 8, 2): {0: 902, 4: 14360, 6: 13750, 9: 29893, 13: 30770, 15: 10325}, + ("Category Twos and Threes", 8, 3): {0: 2221, 4: 8122, 9: 23734, 12: 28527, 16: 28942, 18: 8454}, + ("Category Twos and Threes", 8, 4): {0: 140, 3: 8344, 12: 33635, 15: 28711, 18: 20093, 20: 9077}, + ("Category Twos and Threes", 8, 5): {0: 3601, 9: 10269, 12: 12458, 15: 28017, 18: 24815, 21: 20840}, + ("Category Twos and Threes", 8, 6): {0: 4104, 11: 10100, 15: 25259, 18: 30949, 21: 29588}, + ("Category Twos and Threes", 8, 7): {0: 3336, 12: 10227, 15: 14149, 18: 31155, 21: 29325, 23: 11808}, + ("Category Twos and Threes", 8, 8): {3: 7, 5: 7726, 16: 17997, 18: 21517, 21: 40641, 24: 12112}, + ("Category Sum of Odds", 1, 1): {0: 50084, 1: 16488, 3: 16584, 5: 16844}, + ("Category Sum of Odds", 1, 2): {0: 44489, 3: 27886, 5: 27625}, + ("Category Sum of Odds", 1, 3): {0: 27892, 3: 32299, 5: 39809}, + ("Category Sum of Odds", 1, 4): {0: 30917, 3: 19299, 5: 49784}, + ("Category Sum of Odds", 1, 5): {0: 25892, 3: 15941, 5: 58167}, + ("Category Sum of Odds", 1, 6): {0: 21678, 3: 13224, 5: 65098}, + ("Category Sum of Odds", 1, 7): {0: 17840, 3: 11191, 5: 70969}, + ("Category Sum of Odds", 1, 8): {0: 14690, 5: 85310}, + ("Category Sum of Odds", 2, 1): {0: 24611, 1: 19615, 3: 22234, 6: 25168, 8: 8372}, + ("Category Sum of Odds", 2, 2): {0: 11216, 3: 33181, 6: 32416, 8: 15414, 10: 7773}, + ("Category Sum of Odds", 2, 3): {0: 13730, 3: 17055, 5: 34933, 8: 18363, 10: 15919}, + ("Category Sum of Odds", 2, 4): {0: 9599, 3: 11842, 5: 34490, 8: 19129, 10: 24940}, + ("Category Sum of Odds", 2, 5): {0: 6652, 5: 40845, 8: 18712, 10: 33791}, + ("Category Sum of Odds", 2, 6): {0: 10404, 5: 20970, 8: 26124, 10: 42502}, + ("Category Sum of Odds", 2, 7): {0: 7262, 5: 26824, 8: 15860, 10: 50054}, + ("Category Sum of Odds", 2, 8): {0: 4950, 5: 23253, 8: 14179, 10: 57618}, + ("Category Sum of Odds", 3, 1): {0: 12467, 1: 16736, 4: 20970, 6: 29252, 8: 11660, 10: 8915}, + ("Category Sum of Odds", 3, 2): {0: 8635, 3: 15579, 6: 27649, 9: 30585, 13: 17552}, + ("Category Sum of Odds", 3, 3): {0: 5022, 6: 32067, 8: 21631, 11: 24032, 13: 17248}, + ("Category Sum of Odds", 3, 4): {0: 8260, 6: 17955, 8: 18530, 11: 28631, 13: 14216, 15: 12408}, + ("Category Sum of Odds", 3, 5): {0: 4685, 5: 13863, 8: 14915, 11: 30363, 13: 16370, 15: 19804}, + ("Category Sum of Odds", 3, 6): {0: 2766, 5: 10213, 8: 11372, 10: 30968, 13: 17133, 15: 27548}, + ("Category Sum of Odds", 3, 7): {0: 543, 3: 8448, 10: 28784, 13: 26258, 15: 35967}, + ("Category Sum of Odds", 3, 8): {0: 3760, 6: 8911, 11: 27672, 13: 16221, 15: 43436}, + ("Category Sum of Odds", 4, 1): {0: 18870, 5: 28873, 6: 18550, 9: 20881, 11: 12826}, + ("Category Sum of Odds", 4, 2): {0: 7974, 6: 23957, 9: 27982, 11: 15953, 13: 13643, 15: 10491}, + ("Category Sum of Odds", 4, 3): {0: 1778, 3: 8154, 8: 25036, 11: 24307, 13: 18030, 15: 14481, 18: 8214}, + ("Category Sum of Odds", 4, 4): {0: 1862, 4: 8889, 8: 11182, 11: 21997, 13: 19483, 16: 20879, 20: 15708}, + ("Category Sum of Odds", 4, 5): {0: 5687, 7: 8212, 11: 18674, 13: 17578, 16: 25572, 18: 12704, 20: 11573}, + ("Category Sum of Odds", 4, 6): {0: 6549, 11: 17161, 13: 15290, 16: 28355, 18: 14865, 20: 17780}, + ("Category Sum of Odds", 4, 7): {0: 5048, 10: 11824, 13: 12343, 16: 29544, 18: 15947, 20: 25294}, + ("Category Sum of Odds", 4, 8): {0: 3060, 10: 8747, 15: 29415, 18: 25762, 20: 33016}, + ("Category Sum of Odds", 5, 1): {0: 3061, 3: 22078, 6: 26935, 9: 23674, 11: 15144, 14: 9108}, + ("Category Sum of Odds", 5, 2): {0: 5813, 7: 19297, 9: 14666, 11: 17165, 14: 21681, 16: 10586, 18: 10792}, + ("Category Sum of Odds", 5, 3): {0: 3881, 6: 9272, 9: 10300, 11: 13443, 14: 24313, 16: 13969, 19: 16420, 21: 8402}, + ("Category Sum of Odds", 5, 4): {0: 4213, 8: 9656, 13: 24199, 16: 22188, 18: 16440, 20: 14313, 23: 8991}, + ("Category Sum of Odds", 5, 5): {0: 4997, 10: 9128, 13: 11376, 16: 20859, 18: 17548, 21: 20120, 25: 15972}, + ("Category Sum of Odds", 5, 6): { + 0: 4581, + 11: 8516, + 14: 11335, + 16: 10647, + 18: 16866, + 21: 24256, + 23: 11945, + 25: 11854, + }, + ("Category Sum of Odds", 5, 7): {0: 176, 6: 8052, 16: 17535, 18: 14878, 21: 27189, 23: 14100, 25: 18070}, + ("Category Sum of Odds", 5, 8): {0: 2, 2: 6622, 15: 12097, 18: 12454, 21: 28398, 23: 15254, 25: 25173}, + ("Category Sum of Odds", 6, 1): {0: 11634, 4: 12188, 6: 16257, 9: 23909, 11: 13671, 13: 13125, 16: 9216}, + ("Category Sum of Odds", 6, 2): {0: 1403, 4: 8241, 10: 22151, 12: 14245, 14: 15279, 17: 19690, 21: 18991}, + ("Category Sum of Odds", 6, 3): { + 0: 6079, + 9: 10832, + 12: 10094, + 14: 13221, + 17: 22538, + 19: 12673, + 21: 15363, + 24: 9200, + }, + ("Category Sum of Odds", 6, 4): {0: 5771, 11: 9419, 16: 22239, 19: 22715, 21: 12847, 23: 12798, 25: 9237, 28: 4974}, + ("Category Sum of Odds", 6, 5): { + 0: 2564, + 11: 8518, + 17: 20753, + 19: 14121, + 21: 13179, + 23: 15752, + 25: 14841, + 28: 10272, + }, + ("Category Sum of Odds", 6, 6): {0: 4310, 14: 8668, 19: 20891, 21: 12052, 23: 16882, 26: 19954, 30: 17243}, + ("Category Sum of Odds", 6, 7): { + 0: 5233, + 16: 8503, + 19: 11127, + 21: 10285, + 23: 16141, + 26: 23993, + 28: 12043, + 30: 12675, + }, + ("Category Sum of Odds", 6, 8): {0: 510, 12: 8107, 21: 17013, 23: 14396, 26: 26771, 28: 13964, 30: 19239}, + ("Category Sum of Odds", 7, 1): { + 0: 2591, + 2: 8436, + 5: 11759, + 7: 13733, + 9: 15656, + 11: 14851, + 13: 12301, + 15: 11871, + 18: 8802, + }, + ("Category Sum of Odds", 7, 2): { + 0: 4730, + 8: 8998, + 11: 10573, + 13: 13099, + 15: 13819, + 17: 13594, + 19: 12561, + 21: 12881, + 24: 9745, + }, + ("Category Sum of Odds", 7, 3): { + 0: 2549, + 9: 8523, + 15: 19566, + 17: 12251, + 19: 13562, + 21: 13473, + 23: 11918, + 27: 18158, + }, + ("Category Sum of Odds", 7, 4): {0: 6776, 14: 9986, 19: 20914, 22: 21006, 24: 12685, 26: 10835, 30: 17798}, + ("Category Sum of Odds", 7, 5): { + 0: 2943, + 14: 8009, + 20: 20248, + 22: 11896, + 24: 14166, + 26: 12505, + 28: 13136, + 30: 10486, + 33: 6611, + }, + ("Category Sum of Odds", 7, 6): { + 2: 1990, + 15: 8986, + 22: 19198, + 24: 13388, + 26: 12513, + 28: 15893, + 30: 15831, + 35: 12201, + }, + ("Category Sum of Odds", 7, 7): { + 4: 559, + 14: 8153, + 21: 11671, + 24: 12064, + 26: 11473, + 28: 16014, + 31: 20785, + 33: 10174, + 35: 9107, + }, + ("Category Sum of Odds", 7, 8): {0: 3, 8: 5190, 21: 8049, 24: 10585, 28: 25255, 31: 24333, 33: 12445, 35: 14140}, + ("Category Sum of Odds", 8, 1): {0: 7169, 7: 19762, 9: 14044, 11: 14858, 13: 13399, 15: 10801, 17: 11147, 20: 8820}, + ("Category Sum of Odds", 8, 2): { + 0: 7745, + 11: 10927, + 14: 10849, + 16: 13103, + 18: 13484, + 20: 12487, + 22: 10815, + 24: 11552, + 27: 9038, + }, + ("Category Sum of Odds", 8, 3): { + 0: 3867, + 12: 9356, + 18: 19408, + 20: 12379, + 22: 12519, + 24: 12260, + 26: 11008, + 28: 10726, + 31: 8477, + }, + ("Category Sum of Odds", 8, 4): { + 1: 3971, + 15: 9176, + 21: 18732, + 23: 12900, + 25: 13405, + 27: 11603, + 29: 10400, + 33: 19813, + }, + ("Category Sum of Odds", 8, 5): { + 1: 490, + 12: 8049, + 20: 9682, + 23: 10177, + 25: 12856, + 27: 12369, + 29: 12781, + 32: 18029, + 34: 11315, + 38: 4252, + }, + ("Category Sum of Odds", 8, 6): { + 4: 86, + 11: 8038, + 22: 9157, + 25: 10729, + 27: 11053, + 29: 13606, + 31: 12383, + 33: 14068, + 35: 12408, + 38: 8472, + }, + ("Category Sum of Odds", 8, 7): { + 6: 1852, + 20: 8020, + 27: 17455, + 29: 12898, + 31: 12181, + 33: 15650, + 35: 17577, + 40: 14367, + }, + ("Category Sum of Odds", 8, 8): { + 4: 8, + 11: 8008, + 26: 10314, + 29: 11446, + 31: 10714, + 33: 16060, + 36: 21765, + 38: 10622, + 40: 11063, + }, + ("Category Sum of Evens", 1, 1): {0: 49585, 2: 16733, 4: 16854, 6: 16828}, + ("Category Sum of Evens", 1, 2): {0: 33244, 2: 11087, 4: 28025, 6: 27644}, + ("Category Sum of Evens", 1, 3): {0: 22259, 4: 42357, 6: 35384}, + ("Category Sum of Evens", 1, 4): {0: 18511, 4: 35651, 6: 45838}, + ("Category Sum of Evens", 1, 5): {0: 15428, 4: 29656, 6: 54916}, + ("Category Sum of Evens", 1, 6): {0: 12927, 4: 24370, 6: 62703}, + ("Category Sum of Evens", 1, 7): {0: 14152, 4: 17087, 6: 68761}, + ("Category Sum of Evens", 1, 8): {0: 11920, 4: 14227, 6: 73853}, + ("Category Sum of Evens", 2, 1): {0: 25229, 2: 16545, 4: 19538, 6: 21987, 10: 16701}, + ("Category Sum of Evens", 2, 2): {0: 11179, 4: 27164, 6: 24451, 8: 13966, 10: 15400, 12: 7840}, + ("Category Sum of Evens", 2, 3): {0: 8099, 4: 16354, 6: 20647, 8: 17887, 10: 24736, 12: 12277}, + ("Category Sum of Evens", 2, 4): {0: 5687, 4: 11219, 6: 20711, 8: 14290, 10: 26976, 12: 21117}, + ("Category Sum of Evens", 2, 5): {0: 3991, 6: 27157, 8: 11641, 10: 26842, 12: 30369}, + ("Category Sum of Evens", 2, 6): {0: 2741, 6: 23123, 10: 35050, 12: 39086}, + ("Category Sum of Evens", 2, 7): {0: 1122, 6: 20538, 10: 30952, 12: 47388}, + ("Category Sum of Evens", 2, 8): {0: 3950, 6: 14006, 10: 27341, 12: 54703}, + ("Category Sum of Evens", 3, 1): {0: 12538, 2: 12516, 4: 16530, 6: 21270, 8: 13745, 10: 11209, 14: 12192}, + ("Category Sum of Evens", 3, 2): {0: 7404, 4: 10459, 6: 15644, 8: 15032, 10: 18955, 12: 15021, 16: 17485}, + ("Category Sum of Evens", 3, 3): {0: 2176, 6: 14148, 8: 12295, 10: 20247, 12: 18001, 14: 15953, 16: 17180}, + ("Category Sum of Evens", 3, 4): {0: 4556, 8: 15062, 10: 17232, 12: 18975, 14: 15832, 16: 18749, 18: 9594}, + ("Category Sum of Evens", 3, 5): {0: 2575, 8: 10825, 10: 13927, 12: 19533, 14: 14402, 16: 21954, 18: 16784}, + ("Category Sum of Evens", 3, 6): {0: 1475, 6: 7528, 10: 10614, 12: 19070, 14: 12940, 16: 23882, 18: 24491}, + ("Category Sum of Evens", 3, 7): {0: 862, 6: 5321, 12: 26291, 14: 10985, 16: 24254, 18: 32287}, + ("Category Sum of Evens", 3, 8): {0: 138, 4: 4086, 12: 22703, 16: 32516, 18: 40557}, + ("Category Sum of Evens", 4, 1): {0: 6214, 4: 20921, 6: 17434, 8: 15427, 10: 14158, 12: 11354, 16: 14492}, + ("Category Sum of Evens", 4, 2): { + 0: 2868, + 6: 13362, + 8: 10702, + 10: 15154, + 12: 15715, + 14: 14104, + 16: 12485, + 20: 15610, + }, + ("Category Sum of Evens", 4, 3): { + 0: 573, + 8: 10496, + 10: 10269, + 12: 12879, + 14: 16224, + 16: 17484, + 18: 13847, + 20: 10518, + 22: 7710, + }, + ("Category Sum of Evens", 4, 4): { + 0: 1119, + 6: 5124, + 12: 17394, + 14: 12763, + 16: 17947, + 18: 16566, + 20: 13338, + 22: 15749, + }, + ("Category Sum of Evens", 4, 5): {0: 3477, 12: 12738, 16: 26184, 18: 18045, 20: 14172, 22: 16111, 24: 9273}, + ("Category Sum of Evens", 4, 6): {0: 991, 12: 10136, 16: 21089, 18: 18805, 20: 13848, 22: 20013, 24: 15118}, + ("Category Sum of Evens", 4, 7): {0: 2931, 16: 21174, 18: 18952, 20: 12601, 22: 21947, 24: 22395}, + ("Category Sum of Evens", 4, 8): {0: 1798, 12: 6781, 18: 27146, 20: 11505, 22: 23056, 24: 29714}, + ("Category Sum of Evens", 5, 1): { + 0: 3192, + 4: 13829, + 6: 13373, + 8: 13964, + 10: 14656, + 12: 13468, + 14: 10245, + 18: 17273, + }, + ("Category Sum of Evens", 5, 2): { + 0: 3217, + 8: 10390, + 12: 22094, + 14: 13824, + 16: 14674, + 18: 12124, + 22: 16619, + 24: 7058, + }, + ("Category Sum of Evens", 5, 3): { + 0: 3904, + 12: 11004, + 14: 10339, + 16: 13128, + 18: 14686, + 20: 15282, + 22: 13294, + 26: 18363, + }, + ("Category Sum of Evens", 5, 4): { + 0: 43, + 4: 4025, + 14: 10648, + 16: 10437, + 18: 12724, + 20: 14710, + 22: 16005, + 24: 12896, + 28: 18512, + }, + ("Category Sum of Evens", 5, 5): { + 0: 350, + 8: 4392, + 16: 11641, + 18: 10297, + 20: 12344, + 22: 16826, + 24: 15490, + 26: 12235, + 28: 16425, + }, + ("Category Sum of Evens", 5, 6): { + 0: 374, + 10: 4670, + 18: 13498, + 22: 25729, + 24: 17286, + 26: 13565, + 28: 15274, + 30: 9604, + }, + ("Category Sum of Evens", 5, 7): {0: 1473, 18: 11310, 22: 21341, 24: 18114, 26: 13349, 28: 19048, 30: 15365}, + ("Category Sum of Evens", 5, 8): {0: 1, 4: 3753, 20: 10318, 22: 11699, 24: 18376, 26: 12500, 28: 21211, 30: 22142}, + ("Category Sum of Evens", 6, 1): { + 0: 4767, + 6: 15250, + 8: 11527, + 10: 13220, + 12: 13855, + 14: 12217, + 16: 10036, + 20: 19128, + }, + ("Category Sum of Evens", 6, 2): { + 0: 1380, + 6: 5285, + 12: 13888, + 14: 10495, + 16: 12112, + 18: 12962, + 20: 12458, + 22: 10842, + 26: 14076, + 28: 6502, + }, + ("Category Sum of Evens", 6, 3): { + 0: 1230, + 16: 17521, + 18: 10098, + 20: 12628, + 22: 13809, + 24: 13594, + 26: 11930, + 30: 19190, + }, + ("Category Sum of Evens", 6, 4): {0: 1235, 18: 15534, 22: 22081, 24: 13471, 26: 13991, 28: 12906, 32: 20782}, + ("Category Sum of Evens", 6, 5): {0: 1241, 20: 15114, 24: 21726, 26: 13874, 28: 15232, 30: 12927, 34: 19886}, + ("Category Sum of Evens", 6, 6): {0: 1224, 22: 15886, 26: 21708, 28: 15982, 30: 15534, 32: 12014, 34: 17652}, + ("Category Sum of Evens", 6, 7): {4: 1437, 24: 17624, 28: 24727, 30: 17083, 32: 13001, 34: 15604, 36: 10524}, + ("Category Sum of Evens", 6, 8): {4: 1707, 24: 11310, 28: 20871, 30: 18101, 32: 12842, 34: 18840, 36: 16329}, + ("Category Sum of Evens", 7, 1): { + 0: 6237, + 8: 15390, + 10: 11183, + 12: 12690, + 14: 12463, + 16: 11578, + 20: 17339, + 22: 8870, + 26: 4250, + }, + ("Category Sum of Evens", 7, 2): { + 0: 1433, + 14: 16705, + 18: 19797, + 20: 11747, + 22: 12101, + 24: 10947, + 28: 16547, + 32: 10723, + }, + ("Category Sum of Evens", 7, 3): { + 0: 2135, + 14: 5836, + 20: 13766, + 22: 10305, + 24: 12043, + 26: 13153, + 28: 12644, + 30: 10884, + 34: 19234, + }, + ("Category Sum of Evens", 7, 4): { + 0: 1762, + 22: 16471, + 26: 20839, + 28: 12907, + 30: 13018, + 32: 11907, + 34: 10022, + 38: 13074, + }, + ("Category Sum of Evens", 7, 5): { + 4: 1630, + 24: 14719, + 28: 20377, + 30: 12713, + 32: 13273, + 34: 13412, + 36: 10366, + 40: 13510, + }, + ("Category Sum of Evens", 7, 6): { + 4: 1436, + 26: 14275, + 30: 20680, + 32: 12798, + 34: 15385, + 36: 13346, + 38: 10011, + 40: 12069, + }, + ("Category Sum of Evens", 7, 7): { + 6: 2815, + 24: 6584, + 30: 16532, + 32: 11106, + 34: 15613, + 36: 15702, + 38: 12021, + 40: 12478, + 42: 7149, + }, + ("Category Sum of Evens", 7, 8): {10: 1490, 30: 16831, 34: 23888, 36: 16970, 38: 12599, 40: 16137, 42: 12085}, + ("Category Sum of Evens", 8, 1): { + 0: 3709, + 8: 10876, + 12: 19246, + 14: 11696, + 16: 11862, + 18: 11145, + 22: 16877, + 24: 9272, + 28: 5317, + }, + ("Category Sum of Evens", 8, 2): { + 0: 1361, + 16: 14530, + 20: 17637, + 22: 10922, + 24: 11148, + 26: 10879, + 30: 17754, + 34: 15769, + }, + ("Category Sum of Evens", 8, 3): { + 2: 1601, + 22: 14895, + 26: 18464, + 28: 11561, + 30: 12249, + 32: 11747, + 34: 10070, + 38: 19413, + }, + ("Category Sum of Evens", 8, 4): { + 0: 2339, + 20: 5286, + 26: 11746, + 30: 19858, + 32: 12344, + 34: 12243, + 36: 11307, + 40: 16632, + 42: 8245, + }, + ("Category Sum of Evens", 8, 5): { + 4: 1798, + 28: 14824, + 32: 18663, + 34: 12180, + 36: 12458, + 38: 12260, + 40: 10958, + 44: 16859, + }, + ("Category Sum of Evens", 8, 6): { + 6: 2908, + 26: 6292, + 32: 13573, + 34: 10367, + 36: 12064, + 38: 12862, + 40: 13920, + 42: 11359, + 46: 16655, + }, + ("Category Sum of Evens", 8, 7): { + 8: 2652, + 28: 6168, + 34: 13922, + 36: 10651, + 38: 12089, + 40: 14999, + 42: 13899, + 44: 10574, + 46: 15046, + }, + ("Category Sum of Evens", 8, 8): { + 10: 2547, + 30: 6023, + 36: 15354, + 38: 10354, + 40: 14996, + 42: 16214, + 44: 11803, + 46: 13670, + 48: 9039, + }, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 16591, 8: 16660}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 27694, 8: 27631}, + ("Category Double Threes and Fours", 1, 3): {0: 29592, 6: 35261, 8: 35147}, + ("Category Double Threes and Fours", 1, 4): {0: 24601, 6: 29406, 8: 45993}, + ("Category Double Threes and Fours", 1, 5): {0: 20499, 6: 24420, 8: 55081}, + ("Category Double Threes and Fours", 1, 6): {0: 17116, 6: 20227, 8: 62657}, + ("Category Double Threes and Fours", 1, 7): {0: 14193, 6: 17060, 8: 68747}, + ("Category Double Threes and Fours", 1, 8): {0: 11977, 6: 13924, 8: 74099}, + ("Category Double Threes and Fours", 2, 1): {0: 44382, 6: 22191, 8: 22251, 14: 11176}, + ("Category Double Threes and Fours", 2, 2): {0: 19720, 6: 24652, 8: 24891, 14: 23096, 16: 7641}, + ("Category Double Threes and Fours", 2, 3): {0: 8765, 6: 21008, 8: 20929, 12: 12201, 14: 24721, 16: 12376}, + ("Category Double Threes and Fours", 2, 4): {0: 6164, 6: 14466, 8: 22828, 14: 35406, 16: 21136}, + ("Category Double Threes and Fours", 2, 5): {0: 4307, 6: 10005, 8: 22620, 14: 32879, 16: 30189}, + ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 28513, 14: 29530, 16: 39078}, + ("Category Double Threes and Fours", 2, 7): {0: 2042, 8: 24335, 14: 26250, 16: 47373}, + ("Category Double Threes and Fours", 2, 8): {0: 1385, 8: 23166, 14: 20907, 16: 54542}, + ("Category Double Threes and Fours", 3, 1): {0: 29378, 6: 22335, 8: 22138, 14: 16783, 16: 9366}, + ("Category Double Threes and Fours", 3, 2): { + 0: 8894, + 6: 16518, + 8: 16277, + 12: 10334, + 14: 20757, + 16: 12265, + 22: 14955, + }, + ("Category Double Threes and Fours", 3, 3): { + 0: 2643, + 8: 18522, + 12: 11066, + 14: 21922, + 16: 11045, + 20: 17235, + 22: 17567, + }, + ("Category Double Threes and Fours", 3, 4): { + 0: 1523, + 8: 13773, + 14: 26533, + 16: 18276, + 20: 11695, + 22: 18521, + 24: 9679, + }, + ("Category Double Threes and Fours", 3, 5): {0: 845, 8: 10218, 14: 20245, 16: 20293, 22: 31908, 24: 16491}, + ("Category Double Threes and Fours", 3, 6): {0: 499, 8: 7230, 14: 15028, 16: 20914, 22: 31835, 24: 24494}, + ("Category Double Threes and Fours", 3, 7): {0: 1298, 8: 5434, 16: 30595, 22: 29980, 24: 32693}, + ("Category Double Threes and Fours", 3, 8): {0: 178, 6: 4363, 16: 27419, 22: 27614, 24: 40426}, + ("Category Double Threes and Fours", 4, 1): {0: 19809, 6: 19538, 8: 19765, 14: 22348, 18: 12403, 22: 6137}, + ("Category Double Threes and Fours", 4, 2): { + 0: 3972, + 8: 19440, + 14: 27646, + 16: 12978, + 20: 11442, + 22: 11245, + 24: 6728, + 28: 6549, + }, + ("Category Double Threes and Fours", 4, 3): { + 0: 745, + 6: 7209, + 14: 19403, + 18: 11744, + 20: 15371, + 22: 15441, + 26: 13062, + 30: 17025, + }, + ("Category Double Threes and Fours", 4, 4): { + 0: 371, + 6: 4491, + 14: 13120, + 16: 10176, + 20: 11583, + 22: 18508, + 24: 10280, + 28: 15624, + 30: 15847, + }, + ("Category Double Threes and Fours", 4, 5): { + 0: 163, + 6: 4251, + 16: 15796, + 22: 26145, + 24: 17306, + 28: 10930, + 30: 16244, + 32: 9165, + }, + ("Category Double Threes and Fours", 4, 6): {0: 79, 16: 14439, 22: 21763, 24: 18861, 30: 29518, 32: 15340}, + ("Category Double Threes and Fours", 4, 7): {0: 1042, 16: 12543, 22: 13634, 24: 20162, 30: 30259, 32: 22360}, + ("Category Double Threes and Fours", 4, 8): {0: 20, 6: 2490, 16: 6901, 22: 10960, 24: 20269, 30: 29442, 32: 29918}, + ("Category Double Threes and Fours", 5, 1): { + 0: 13122, + 6: 16411, + 8: 16451, + 14: 24768, + 16: 10392, + 22: 14528, + 26: 4328, + }, + ("Category Double Threes and Fours", 5, 2): { + 0: 1676, + 8: 10787, + 14: 20218, + 18: 11102, + 20: 12668, + 22: 12832, + 26: 10994, + 30: 15390, + 34: 4333, + }, + ("Category Double Threes and Fours", 5, 3): { + 0: 223, + 14: 12365, + 16: 7165, + 20: 11385, + 22: 11613, + 26: 15182, + 28: 13665, + 32: 14400, + 36: 14002, + }, + ("Category Double Threes and Fours", 5, 4): { + 0: 95, + 6: 2712, + 16: 8862, + 22: 18696, + 26: 12373, + 28: 13488, + 30: 14319, + 34: 12414, + 38: 17041, + }, + ("Category Double Threes and Fours", 5, 5): { + 0: 1333, + 14: 5458, + 22: 13613, + 24: 10772, + 28: 11201, + 30: 16810, + 32: 10248, + 36: 14426, + 38: 16139, + }, + ("Category Double Threes and Fours", 5, 6): { + 0: 16, + 16: 6354, + 24: 16213, + 30: 25369, + 32: 16845, + 36: 10243, + 38: 15569, + 40: 9391, + }, + ("Category Double Threes and Fours", 5, 7): { + 0: 161, + 12: 3457, + 24: 12437, + 30: 21495, + 32: 18636, + 38: 28581, + 40: 15233, + }, + ("Category Double Threes and Fours", 5, 8): { + 0: 478, + 16: 4861, + 26: 10119, + 30: 13694, + 32: 19681, + 38: 29177, + 40: 21990, + }, + ("Category Double Threes and Fours", 6, 1): { + 0: 8738, + 6: 13463, + 8: 12988, + 14: 24653, + 16: 11068, + 22: 19621, + 26: 5157, + 30: 4312, + }, + ("Category Double Threes and Fours", 6, 2): { + 0: 784, + 6: 5735, + 14: 13407, + 16: 8170, + 20: 11349, + 22: 11356, + 26: 12465, + 28: 10790, + 30: 11527, + 38: 14417, + }, + ("Category Double Threes and Fours", 6, 3): { + 0: 72, + 14: 8986, + 22: 13700, + 26: 12357, + 28: 12114, + 32: 15882, + 36: 19286, + 40: 13540, + 44: 4063, + }, + ("Category Double Threes and Fours", 6, 4): { + 0: 439, + 18: 7427, + 22: 9284, + 28: 14203, + 30: 10836, + 34: 14646, + 36: 12511, + 38: 10194, + 42: 10202, + 46: 10258, + }, + ("Category Double Threes and Fours", 6, 5): { + 0: 166, + 20: 7618, + 24: 5198, + 30: 17479, + 34: 12496, + 36: 12190, + 38: 14163, + 42: 12571, + 46: 18119, + }, + ("Category Double Threes and Fours", 6, 6): { + 0: 1843, + 22: 5905, + 30: 12997, + 32: 10631, + 36: 10342, + 38: 16439, + 40: 10795, + 44: 13485, + 46: 17563, + }, + ("Category Double Threes and Fours", 6, 7): { + 0: 31, + 12: 2221, + 24: 5004, + 32: 15743, + 38: 24402, + 40: 17005, + 46: 25241, + 48: 10353, + }, + ("Category Double Threes and Fours", 6, 8): { + 8: 79, + 16: 4037, + 32: 12559, + 38: 20863, + 40: 18347, + 46: 27683, + 48: 16432, + }, + ("Category Double Threes and Fours", 7, 1): { + 0: 5803, + 6: 10242, + 8: 10404, + 14: 22886, + 16: 10934, + 22: 19133, + 24: 7193, + 28: 8167, + 32: 5238, + }, + ("Category Double Threes and Fours", 7, 2): { + 0: 357, + 14: 17082, + 22: 17524, + 26: 11974, + 28: 11132, + 32: 13186, + 36: 13959, + 40: 10028, + 44: 4758, + }, + ("Category Double Threes and Fours", 7, 3): { + 0: 361, + 18: 7136, + 22: 5983, + 28: 13899, + 32: 12974, + 34: 10088, + 36: 10081, + 40: 14481, + 44: 14127, + 46: 6547, + 50: 4323, + }, + ("Category Double Threes and Fours", 7, 4): { + 0: 1182, + 18: 4299, + 30: 16331, + 34: 11316, + 36: 10741, + 40: 16028, + 44: 18815, + 48: 15225, + 52: 6063, + }, + ("Category Double Threes and Fours", 7, 5): { + 0: 45, + 12: 3763, + 32: 17140, + 38: 19112, + 42: 13655, + 44: 11990, + 46: 11137, + 50: 10646, + 54: 12512, + }, + ("Category Double Threes and Fours", 7, 6): { + 8: 2400, + 28: 5277, + 32: 5084, + 38: 16047, + 42: 12133, + 44: 11451, + 46: 14027, + 50: 13198, + 54: 20383, + }, + ("Category Double Threes and Fours", 7, 7): { + 6: 1968, + 30: 5585, + 38: 12210, + 40: 10376, + 46: 25548, + 48: 15392, + 54: 21666, + 56: 7255, + }, + ("Category Double Threes and Fours", 7, 8): { + 8: 42, + 20: 2293, + 32: 4653, + 40: 15068, + 46: 23170, + 48: 17057, + 54: 25601, + 56: 12116, + }, + ("Category Double Threes and Fours", 8, 1): { + 0: 3982, + 8: 15658, + 14: 20388, + 16: 10234, + 20: 10167, + 22: 10162, + 28: 15330, + 32: 8758, + 36: 5321, + }, + ("Category Double Threes and Fours", 8, 2): { + 0: 161, + 6: 3169, + 14: 7106, + 22: 16559, + 28: 16400, + 32: 12950, + 36: 16399, + 40: 10090, + 44: 11474, + 48: 5692, + }, + ("Category Double Threes and Fours", 8, 3): { + 0: 856, + 16: 4092, + 30: 13686, + 34: 12838, + 38: 15010, + 42: 17085, + 46: 14067, + 50: 11844, + 52: 6500, + 56: 4022, + }, + ("Category Double Threes and Fours", 8, 4): { + 0: 36, + 12: 2795, + 30: 9742, + 36: 11726, + 40: 12404, + 44: 18791, + 48: 14662, + 52: 15518, + 54: 8066, + 58: 6260, + }, + ("Category Double Threes and Fours", 8, 5): { + 6: 8, + 12: 2948, + 30: 5791, + 38: 10658, + 42: 10175, + 46: 19359, + 50: 14449, + 52: 10531, + 56: 13257, + 60: 12824, + }, + ("Category Double Threes and Fours", 8, 6): { + 0: 2, + 12: 2528, + 32: 4832, + 40: 11436, + 46: 17832, + 50: 13016, + 52: 11631, + 54: 12058, + 58: 11458, + 62: 15207, + }, + ("Category Double Threes and Fours", 8, 7): { + 6: 2, + 12: 2204, + 40: 9320, + 46: 14688, + 50: 11494, + 52: 10602, + 54: 14541, + 58: 13849, + 62: 23300, + }, + ("Category Double Threes and Fours", 8, 8): { + 8: 1, + 16: 1773, + 42: 8766, + 48: 17452, + 54: 24338, + 56: 15722, + 62: 22745, + 64: 9203, + }, + ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 4: 16803, 8: 16630}, + ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 4: 27448, 8: 27743}, + ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 4: 23184, 8: 39716}, + ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 4: 19221, 8: 49816}, + ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 4: 16079, 8: 58605}, + ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 4: 13237, 8: 65258}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 4: 11100, 8: 71224}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 4: 9323, 8: 75706}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 24842, 12: 8319}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 15172, 16: 7713}, + ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 4: 17158, 8: 34907, 12: 18539, 16: 15630}, + ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 4: 11981, 8: 34465, 12: 19108, 16: 24903}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 4: 8302, 8: 32470, 12: 18612, 16: 34144}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 4569, 4: 5737, 8: 29716, 12: 17216, 16: 42762}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 3146, 8: 30463, 12: 15756, 16: 50635}, + ("Category Quadruple Ones and Twos", 2, 8): {0: 2265, 8: 26302, 12: 14167, 16: 57266}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 11557, 16: 8682}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 16799, 20: 8629}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 4: 9447, 8: 22255, 12: 21685, 16: 24084, 20: 11167, 24: 6299}, + ("Category Quadruple Ones and Twos", 3, 4): { + 0: 2864, + 4: 5531, + 8: 17681, + 12: 18400, + 16: 28524, + 20: 14552, + 24: 12448, + }, + ("Category Quadruple Ones and Twos", 3, 5): {0: 1676, 8: 16697, 12: 14755, 16: 30427, 20: 16602, 24: 19843}, + ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 8: 10259, 12: 11326, 16: 31125, 20: 16984, 24: 27625}, + ("Category Quadruple Ones and Twos", 3, 7): {0: 1688, 8: 7543, 12: 8769, 16: 29367, 20: 17085, 24: 35548}, + ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 8: 5277, 12: 6388, 16: 27741, 20: 16170, 24: 43483}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 11167, 24: 6071}, + ("Category Quadruple Ones and Twos", 4, 2): { + 0: 4023, + 4: 9776, + 8: 19015, + 12: 22094, + 16: 20986, + 20: 13805, + 24: 10301, + }, + ("Category Quadruple Ones and Twos", 4, 3): { + 0: 1848, + 8: 17116, + 12: 16853, + 16: 22831, + 20: 18400, + 24: 14480, + 28: 8472, + }, + ("Category Quadruple Ones and Twos", 4, 4): { + 0: 930, + 8: 10375, + 12: 12063, + 16: 21220, + 20: 19266, + 24: 20615, + 28: 9443, + 32: 6088, + }, + ("Category Quadruple Ones and Twos", 4, 5): { + 0: 1561, + 12: 12612, + 16: 18209, + 20: 17910, + 24: 25474, + 28: 12864, + 32: 11370, + }, + ("Category Quadruple Ones and Twos", 4, 6): { + 0: 722, + 12: 7979, + 16: 14796, + 20: 15416, + 24: 28256, + 28: 14675, + 32: 18156, + }, + ("Category Quadruple Ones and Twos", 4, 7): { + 0: 115, + 12: 5304, + 16: 11547, + 20: 12289, + 24: 29181, + 28: 16052, + 32: 25512, + }, + ("Category Quadruple Ones and Twos", 4, 8): {0: 164, 8: 2971, 16: 8888, 20: 9679, 24: 28785, 28: 16180, 32: 33333}, + ("Category Quadruple Ones and Twos", 5, 1): { + 0: 13112, + 4: 16534, + 8: 24718, + 12: 18558, + 16: 14547, + 20: 7055, + 24: 5476, + }, + ("Category Quadruple Ones and Twos", 5, 2): { + 0: 1764, + 4: 5529, + 8: 12216, + 12: 17687, + 16: 20808, + 20: 18149, + 24: 12849, + 28: 6991, + 32: 4007, + }, + ("Category Quadruple Ones and Twos", 5, 3): { + 0: 719, + 8: 8523, + 12: 11074, + 16: 17322, + 20: 19002, + 24: 18643, + 28: 12827, + 32: 7960, + 36: 3930, + }, + ("Category Quadruple Ones and Twos", 5, 4): { + 0: 1152, + 12: 9790, + 16: 12913, + 20: 15867, + 24: 20749, + 28: 16398, + 32: 14218, + 36: 8913, + }, + ("Category Quadruple Ones and Twos", 5, 5): { + 0: 98, + 12: 5549, + 16: 8863, + 20: 12037, + 24: 20010, + 28: 17568, + 32: 19789, + 36: 9319, + 40: 6767, + }, + ("Category Quadruple Ones and Twos", 5, 6): { + 0: 194, + 8: 2663, + 16: 5734, + 20: 8436, + 24: 17830, + 28: 16864, + 32: 24246, + 36: 12115, + 40: 11918, + }, + ("Category Quadruple Ones and Twos", 5, 7): { + 0: 1449, + 20: 9396, + 24: 14936, + 28: 14969, + 32: 27238, + 36: 14094, + 40: 17918, + }, + ("Category Quadruple Ones and Twos", 5, 8): { + 0: 747, + 20: 6034, + 24: 11929, + 28: 12517, + 32: 28388, + 36: 15339, + 40: 25046, + }, + ("Category Quadruple Ones and Twos", 6, 1): { + 0: 8646, + 4: 13011, + 8: 21357, + 12: 19385, + 16: 17008, + 20: 10409, + 24: 6249, + 28: 3935, + }, + ("Category Quadruple Ones and Twos", 6, 2): { + 0: 844, + 8: 10311, + 12: 12792, + 16: 17480, + 20: 18814, + 24: 16492, + 28: 11889, + 32: 6893, + 36: 4485, + }, + ("Category Quadruple Ones and Twos", 6, 3): { + 0: 1241, + 12: 9634, + 16: 11685, + 20: 15584, + 24: 17967, + 28: 16506, + 32: 13314, + 36: 8034, + 40: 6035, + }, + ("Category Quadruple Ones and Twos", 6, 4): { + 0: 1745, + 16: 9804, + 20: 10562, + 24: 15746, + 28: 17174, + 32: 17787, + 36: 12820, + 40: 9289, + 44: 5073, + }, + ("Category Quadruple Ones and Twos", 6, 5): { + 0: 2076, + 20: 10247, + 24: 12264, + 28: 14810, + 32: 19588, + 36: 16002, + 40: 14682, + 44: 6410, + 48: 3921, + }, + ("Category Quadruple Ones and Twos", 6, 6): { + 0: 884, + 20: 5943, + 24: 8774, + 28: 11481, + 32: 19145, + 36: 16864, + 40: 19906, + 44: 9386, + 48: 7617, + }, + ("Category Quadruple Ones and Twos", 6, 7): { + 0: 1386, + 24: 8138, + 28: 8372, + 32: 17207, + 36: 16148, + 40: 24051, + 44: 11862, + 48: 12836, + }, + ("Category Quadruple Ones and Twos", 6, 8): { + 0: 1841, + 28: 9606, + 32: 14489, + 36: 14585, + 40: 26779, + 44: 13821, + 48: 18879, + }, + ("Category Quadruple Ones and Twos", 7, 1): { + 0: 5780, + 4: 10185, + 8: 17905, + 12: 18364, + 16: 18160, + 20: 13115, + 24: 8617, + 32: 7874, + }, + ("Category Quadruple Ones and Twos", 7, 2): { + 0: 1795, + 12: 12828, + 16: 13204, + 20: 16895, + 24: 17562, + 28: 15061, + 32: 11122, + 36: 6507, + 40: 5026, + }, + ("Category Quadruple Ones and Twos", 7, 3): { + 0: 2065, + 16: 10495, + 20: 11008, + 24: 14839, + 28: 16393, + 32: 16118, + 36: 12681, + 40: 8773, + 48: 7628, + }, + ("Category Quadruple Ones and Twos", 7, 4): { + 0: 1950, + 20: 9612, + 24: 10535, + 28: 13596, + 32: 16527, + 36: 15938, + 40: 14071, + 44: 9192, + 48: 8579, + }, + ("Category Quadruple Ones and Twos", 7, 5): { + 0: 223, + 20: 5144, + 24: 6337, + 28: 9400, + 32: 14443, + 36: 15955, + 40: 17820, + 44: 13369, + 48: 10702, + 56: 6607, + }, + ("Category Quadruple Ones and Twos", 7, 6): { + 0: 271, + 24: 5976, + 28: 5988, + 32: 11398, + 36: 13738, + 40: 19063, + 44: 15587, + 48: 15867, + 52: 7202, + 56: 4910, + }, + ("Category Quadruple Ones and Twos", 7, 7): { + 0: 1032, + 28: 5724, + 32: 8275, + 36: 10801, + 40: 18184, + 44: 16470, + 48: 20467, + 52: 9969, + 56: 9078, + }, + ("Category Quadruple Ones and Twos", 7, 8): { + 0: 1508, + 32: 7832, + 36: 7770, + 40: 16197, + 44: 15477, + 48: 24388, + 52: 12403, + 56: 14425, + }, + ("Category Quadruple Ones and Twos", 8, 1): { + 0: 3811, + 4: 7682, + 8: 14638, + 12: 17214, + 16: 18191, + 20: 14651, + 24: 10976, + 28: 6591, + 36: 6246, + }, + ("Category Quadruple Ones and Twos", 8, 2): { + 0: 906, + 12: 7768, + 16: 9421, + 20: 13623, + 24: 16213, + 28: 16246, + 32: 14131, + 36: 10076, + 40: 6198, + 48: 5418, + }, + ("Category Quadruple Ones and Twos", 8, 3): { + 0: 224, + 8: 2520, + 20: 11222, + 24: 10733, + 28: 13934, + 32: 15751, + 36: 14882, + 40: 12409, + 44: 8920, + 48: 5462, + 52: 3943, + }, + ("Category Quadruple Ones and Twos", 8, 4): { + 0: 233, + 20: 5163, + 24: 6057, + 28: 9073, + 32: 12990, + 36: 14756, + 40: 15851, + 44: 13795, + 48: 10706, + 52: 6310, + 56: 5066, + }, + ("Category Quadruple Ones and Twos", 8, 5): { + 0: 76, + 12: 2105, + 28: 8316, + 32: 8993, + 36: 12039, + 40: 15561, + 44: 15382, + 48: 15278, + 52: 10629, + 56: 7377, + 60: 4244, + }, + ("Category Quadruple Ones and Twos", 8, 6): { + 4: 262, + 32: 10321, + 36: 8463, + 40: 13177, + 44: 14818, + 48: 17731, + 52: 14024, + 56: 12425, + 60: 5446, + 64: 3333, + }, + ("Category Quadruple Ones and Twos", 8, 7): { + 8: 300, + 32: 5443, + 36: 5454, + 40: 10276, + 44: 12582, + 48: 18487, + 52: 15549, + 56: 17187, + 60: 8149, + 64: 6573, + }, + ("Category Quadruple Ones and Twos", 8, 8): { + 8: 354, + 36: 5678, + 40: 7484, + 44: 9727, + 48: 17080, + 52: 15898, + 56: 21877, + 60: 10773, + 64: 11129, + }, + ("Category Micro Straight", 1, 1): {0: 100000}, + ("Category Micro Straight", 1, 2): {0: 100000}, + ("Category Micro Straight", 1, 3): {0: 100000}, + ("Category Micro Straight", 1, 4): {0: 100000}, + ("Category Micro Straight", 1, 5): {0: 100000}, + ("Category Micro Straight", 1, 6): {0: 100000}, + ("Category Micro Straight", 1, 7): {0: 100000}, + ("Category Micro Straight", 1, 8): {0: 100000}, + ("Category Micro Straight", 2, 1): {0: 72326, 10: 27674}, + ("Category Micro Straight", 2, 2): {0: 48546, 10: 51454}, + ("Category Micro Straight", 2, 3): {0: 32619, 10: 67381}, + ("Category Micro Straight", 2, 4): {0: 21659, 10: 78341}, + ("Category Micro Straight", 2, 5): {0: 14288, 10: 85712}, + ("Category Micro Straight", 2, 6): {0: 9882, 10: 90118}, + ("Category Micro Straight", 2, 7): {0: 6502, 10: 93498}, + ("Category Micro Straight", 2, 8): {0: 4161, 10: 95839}, + ("Category Micro Straight", 3, 1): {0: 41943, 10: 58057}, + ("Category Micro Straight", 3, 2): {0: 15524, 10: 84476}, + ("Category Micro Straight", 3, 3): {0: 5700, 10: 94300}, + ("Category Micro Straight", 3, 4): {0: 2127, 10: 97873}, + ("Category Micro Straight", 3, 5): {0: 744, 10: 99256}, + ("Category Micro Straight", 3, 6): {0: 260, 10: 99740}, + ("Category Micro Straight", 3, 7): {0: 115, 10: 99885}, + ("Category Micro Straight", 3, 8): {0: 34, 10: 99966}, + ("Category Micro Straight", 4, 1): {0: 22307, 10: 77693}, + ("Category Micro Straight", 4, 2): {0: 4420, 10: 95580}, + ("Category Micro Straight", 4, 3): {0: 806, 10: 99194}, + ("Category Micro Straight", 4, 4): {0: 205, 10: 99795}, + ("Category Micro Straight", 4, 5): {0: 20, 10: 99980}, + ("Category Micro Straight", 4, 6): {0: 5, 10: 99995}, + ("Category Micro Straight", 4, 7): {0: 1, 10: 99999}, + ("Category Micro Straight", 4, 8): {0: 1, 10: 99999}, + ("Category Micro Straight", 5, 1): {0: 11685, 10: 88315}, + ("Category Micro Straight", 5, 2): {0: 1141, 10: 98859}, + ("Category Micro Straight", 5, 3): {0: 119, 10: 99881}, + ("Category Micro Straight", 5, 4): {0: 11, 10: 99989}, + ("Category Micro Straight", 5, 5): {0: 1, 10: 99999}, + ("Category Micro Straight", 5, 6): {10: 100000}, + ("Category Micro Straight", 5, 7): {10: 100000}, + ("Category Micro Straight", 5, 8): {10: 100000}, + ("Category Micro Straight", 6, 1): {0: 5937, 10: 94063}, + ("Category Micro Straight", 6, 2): {0: 307, 10: 99693}, + ("Category Micro Straight", 6, 3): {0: 9, 10: 99991}, + ("Category Micro Straight", 6, 4): {0: 1, 10: 99999}, + ("Category Micro Straight", 6, 5): {10: 100000}, + ("Category Micro Straight", 6, 6): {10: 100000}, + ("Category Micro Straight", 6, 7): {10: 100000}, + ("Category Micro Straight", 6, 8): {10: 100000}, + ("Category Micro Straight", 7, 1): {0: 3072, 10: 96928}, + ("Category Micro Straight", 7, 2): {0: 85, 10: 99915}, + ("Category Micro Straight", 7, 3): {0: 2, 10: 99998}, + ("Category Micro Straight", 7, 4): {10: 100000}, + ("Category Micro Straight", 7, 5): {10: 100000}, + ("Category Micro Straight", 7, 6): {10: 100000}, + ("Category Micro Straight", 7, 7): {10: 100000}, + ("Category Micro Straight", 7, 8): {10: 100000}, + ("Category Micro Straight", 8, 1): {0: 1544, 10: 98456}, + ("Category Micro Straight", 8, 2): {0: 15, 10: 99985}, + ("Category Micro Straight", 8, 3): {10: 100000}, + ("Category Micro Straight", 8, 4): {10: 100000}, + ("Category Micro Straight", 8, 5): {10: 100000}, + ("Category Micro Straight", 8, 6): {10: 100000}, + ("Category Micro Straight", 8, 7): {10: 100000}, + ("Category Micro Straight", 8, 8): {10: 100000}, + ("Category Three Odds", 1, 1): {0: 100000}, + ("Category Three Odds", 1, 2): {0: 100000}, + ("Category Three Odds", 1, 3): {0: 100000}, + ("Category Three Odds", 1, 4): {0: 100000}, + ("Category Three Odds", 1, 5): {0: 100000}, + ("Category Three Odds", 1, 6): {0: 100000}, + ("Category Three Odds", 1, 7): {0: 100000}, + ("Category Three Odds", 1, 8): {0: 100000}, + ("Category Three Odds", 2, 1): {0: 100000}, + ("Category Three Odds", 2, 2): {0: 100000}, + ("Category Three Odds", 2, 3): {0: 100000}, + ("Category Three Odds", 2, 4): {0: 100000}, + ("Category Three Odds", 2, 5): {0: 100000}, + ("Category Three Odds", 2, 6): {0: 100000}, + ("Category Three Odds", 2, 7): {0: 100000}, + ("Category Three Odds", 2, 8): {0: 100000}, + ("Category Three Odds", 3, 1): {0: 87592, 20: 12408}, + ("Category Three Odds", 3, 2): {0: 57855, 20: 42145}, + ("Category Three Odds", 3, 3): {0: 32668, 20: 67332}, + ("Category Three Odds", 3, 4): {0: 17508, 20: 82492}, + ("Category Three Odds", 3, 5): {0: 9156, 20: 90844}, + ("Category Three Odds", 3, 6): {0: 4572, 20: 95428}, + ("Category Three Odds", 3, 7): {0: 2325, 20: 97675}, + ("Category Three Odds", 3, 8): {0: 1116, 20: 98884}, + ("Category Three Odds", 4, 1): {0: 68669, 20: 31331}, + ("Category Three Odds", 4, 2): {0: 26140, 20: 73860}, + ("Category Three Odds", 4, 3): {0: 7837, 20: 92163}, + ("Category Three Odds", 4, 4): {0: 2169, 20: 97831}, + ("Category Three Odds", 4, 5): {0: 516, 20: 99484}, + ("Category Three Odds", 4, 6): {0: 156, 20: 99844}, + ("Category Three Odds", 4, 7): {0: 40, 20: 99960}, + ("Category Three Odds", 4, 8): {0: 12, 20: 99988}, + ("Category Three Odds", 5, 1): {0: 49908, 20: 50092}, + ("Category Three Odds", 5, 2): {0: 10373, 20: 89627}, + ("Category Three Odds", 5, 3): {0: 1640, 20: 98360}, + ("Category Three Odds", 5, 4): {0: 223, 20: 99777}, + ("Category Three Odds", 5, 5): {0: 24, 20: 99976}, + ("Category Three Odds", 5, 6): {0: 3, 20: 99997}, + ("Category Three Odds", 5, 7): {0: 1, 20: 99999}, + ("Category Three Odds", 5, 8): {20: 100000}, + ("Category Three Odds", 6, 1): {0: 34566, 20: 65434}, + ("Category Three Odds", 6, 2): {0: 3766, 20: 96234}, + ("Category Three Odds", 6, 3): {0: 291, 20: 99709}, + ("Category Three Odds", 6, 4): {0: 22, 20: 99978}, + ("Category Three Odds", 6, 5): {20: 100000}, + ("Category Three Odds", 6, 6): {20: 100000}, + ("Category Three Odds", 6, 7): {20: 100000}, + ("Category Three Odds", 6, 8): {20: 100000}, + ("Category Three Odds", 7, 1): {0: 22722, 20: 77278}, + ("Category Three Odds", 7, 2): {0: 1291, 20: 98709}, + ("Category Three Odds", 7, 3): {0: 38, 20: 99962}, + ("Category Three Odds", 7, 4): {0: 2, 20: 99998}, + ("Category Three Odds", 7, 5): {20: 100000}, + ("Category Three Odds", 7, 6): {20: 100000}, + ("Category Three Odds", 7, 7): {20: 100000}, + ("Category Three Odds", 7, 8): {20: 100000}, + ("Category Three Odds", 8, 1): {0: 14556, 20: 85444}, + ("Category Three Odds", 8, 2): {0: 430, 20: 99570}, + ("Category Three Odds", 8, 3): {0: 3, 20: 99997}, + ("Category Three Odds", 8, 4): {20: 100000}, + ("Category Three Odds", 8, 5): {20: 100000}, + ("Category Three Odds", 8, 6): {20: 100000}, + ("Category Three Odds", 8, 7): {20: 100000}, + ("Category Three Odds", 8, 8): {20: 100000}, + ("Category 1-2-1 Consecutive", 1, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 4, 1): {0: 96371, 30: 3629}, + ("Category 1-2-1 Consecutive", 4, 2): {0: 86605, 30: 13395}, + ("Category 1-2-1 Consecutive", 4, 3): {0: 75037, 30: 24963}, + ("Category 1-2-1 Consecutive", 4, 4): {0: 63656, 30: 36344}, + ("Category 1-2-1 Consecutive", 4, 5): {0: 53869, 30: 46131}, + ("Category 1-2-1 Consecutive", 4, 6): {0: 45131, 30: 54869}, + ("Category 1-2-1 Consecutive", 4, 7): {0: 37535, 30: 62465}, + ("Category 1-2-1 Consecutive", 4, 8): {0: 31425, 30: 68575}, + ("Category 1-2-1 Consecutive", 5, 1): {0: 86632, 30: 13368}, + ("Category 1-2-1 Consecutive", 5, 2): {0: 62779, 30: 37221}, + ("Category 1-2-1 Consecutive", 5, 3): {0: 46034, 30: 53966}, + ("Category 1-2-1 Consecutive", 5, 4): {0: 34983, 30: 65017}, + ("Category 1-2-1 Consecutive", 5, 5): {0: 28056, 30: 71944}, + ("Category 1-2-1 Consecutive", 5, 6): {0: 23150, 30: 76850}, + ("Category 1-2-1 Consecutive", 5, 7): {0: 19577, 30: 80423}, + ("Category 1-2-1 Consecutive", 5, 8): {0: 17613, 30: 82387}, + ("Category 1-2-1 Consecutive", 6, 1): {0: 71928, 30: 28072}, + ("Category 1-2-1 Consecutive", 6, 2): {0: 40724, 30: 59276}, + ("Category 1-2-1 Consecutive", 6, 3): {0: 26723, 30: 73277}, + ("Category 1-2-1 Consecutive", 6, 4): {0: 19685, 30: 80315}, + ("Category 1-2-1 Consecutive", 6, 5): {0: 15460, 30: 84540}, + ("Category 1-2-1 Consecutive", 6, 6): {0: 12526, 30: 87474}, + ("Category 1-2-1 Consecutive", 6, 7): {0: 10014, 30: 89986}, + ("Category 1-2-1 Consecutive", 6, 8): {0: 8251, 30: 91749}, + ("Category 1-2-1 Consecutive", 7, 1): {0: 55544, 30: 44456}, + ("Category 1-2-1 Consecutive", 7, 2): {0: 24840, 30: 75160}, + ("Category 1-2-1 Consecutive", 7, 3): {0: 15102, 30: 84898}, + ("Category 1-2-1 Consecutive", 7, 4): {0: 10541, 30: 89459}, + ("Category 1-2-1 Consecutive", 7, 5): {0: 7720, 30: 92280}, + ("Category 1-2-1 Consecutive", 7, 6): {0: 5554, 30: 94446}, + ("Category 1-2-1 Consecutive", 7, 7): {0: 4106, 30: 95894}, + ("Category 1-2-1 Consecutive", 7, 8): {0: 3025, 30: 96975}, + ("Category 1-2-1 Consecutive", 8, 1): {0: 40693, 30: 59307}, + ("Category 1-2-1 Consecutive", 8, 2): {0: 14827, 30: 85173}, + ("Category 1-2-1 Consecutive", 8, 3): {0: 8195, 30: 91805}, + ("Category 1-2-1 Consecutive", 8, 4): {0: 5383, 30: 94617}, + ("Category 1-2-1 Consecutive", 8, 5): {0: 3395, 30: 96605}, + ("Category 1-2-1 Consecutive", 8, 6): {0: 2299, 30: 97701}, + ("Category 1-2-1 Consecutive", 8, 7): {0: 1412, 30: 98588}, + ("Category 1-2-1 Consecutive", 8, 8): {0: 872, 30: 99128}, + ("Category Three Distinct Dice", 1, 1): {0: 100000}, + ("Category Three Distinct Dice", 1, 2): {0: 100000}, + ("Category Three Distinct Dice", 1, 3): {0: 100000}, + ("Category Three Distinct Dice", 1, 4): {0: 100000}, + ("Category Three Distinct Dice", 1, 5): {0: 100000}, + ("Category Three Distinct Dice", 1, 6): {0: 100000}, + ("Category Three Distinct Dice", 1, 7): {0: 100000}, + ("Category Three Distinct Dice", 1, 8): {0: 100000}, + ("Category Three Distinct Dice", 2, 1): {0: 100000}, + ("Category Three Distinct Dice", 2, 2): {0: 100000}, + ("Category Three Distinct Dice", 2, 3): {0: 100000}, + ("Category Three Distinct Dice", 2, 4): {0: 100000}, + ("Category Three Distinct Dice", 2, 5): {0: 100000}, + ("Category Three Distinct Dice", 2, 6): {0: 100000}, + ("Category Three Distinct Dice", 2, 7): {0: 100000}, + ("Category Three Distinct Dice", 2, 8): {0: 100000}, + ("Category Three Distinct Dice", 3, 1): {0: 44707, 20: 55293}, + ("Category Three Distinct Dice", 3, 2): {0: 15078, 20: 84922}, + ("Category Three Distinct Dice", 3, 3): {0: 5056, 20: 94944}, + ("Category Three Distinct Dice", 3, 4): {0: 1688, 20: 98312}, + ("Category Three Distinct Dice", 3, 5): {0: 516, 20: 99484}, + ("Category Three Distinct Dice", 3, 6): {0: 182, 20: 99818}, + ("Category Three Distinct Dice", 3, 7): {0: 56, 20: 99944}, + ("Category Three Distinct Dice", 3, 8): {0: 15, 20: 99985}, + ("Category Three Distinct Dice", 4, 1): {0: 16721, 20: 83279}, + ("Category Three Distinct Dice", 4, 2): {0: 1826, 20: 98174}, + ("Category Three Distinct Dice", 4, 3): {0: 203, 20: 99797}, + ("Category Three Distinct Dice", 4, 4): {0: 18, 20: 99982}, + ("Category Three Distinct Dice", 4, 5): {0: 3, 20: 99997}, + ("Category Three Distinct Dice", 4, 6): {20: 100000}, + ("Category Three Distinct Dice", 4, 7): {20: 100000}, + ("Category Three Distinct Dice", 4, 8): {20: 100000}, + ("Category Three Distinct Dice", 5, 1): {0: 5904, 20: 94096}, + ("Category Three Distinct Dice", 5, 2): {0: 236, 20: 99764}, + ("Category Three Distinct Dice", 5, 3): {0: 12, 20: 99988}, + ("Category Three Distinct Dice", 5, 4): {20: 100000}, + ("Category Three Distinct Dice", 5, 5): {20: 100000}, + ("Category Three Distinct Dice", 5, 6): {20: 100000}, + ("Category Three Distinct Dice", 5, 7): {20: 100000}, + ("Category Three Distinct Dice", 5, 8): {20: 100000}, + ("Category Three Distinct Dice", 6, 1): {0: 1992, 20: 98008}, + ("Category Three Distinct Dice", 6, 2): {0: 21, 20: 99979}, + ("Category Three Distinct Dice", 6, 3): {20: 100000}, + ("Category Three Distinct Dice", 6, 4): {20: 100000}, + ("Category Three Distinct Dice", 6, 5): {20: 100000}, + ("Category Three Distinct Dice", 6, 6): {20: 100000}, + ("Category Three Distinct Dice", 6, 7): {20: 100000}, + ("Category Three Distinct Dice", 6, 8): {20: 100000}, + ("Category Three Distinct Dice", 7, 1): {0: 692, 20: 99308}, + ("Category Three Distinct Dice", 7, 2): {0: 4, 20: 99996}, + ("Category Three Distinct Dice", 7, 3): {20: 100000}, + ("Category Three Distinct Dice", 7, 4): {20: 100000}, + ("Category Three Distinct Dice", 7, 5): {20: 100000}, + ("Category Three Distinct Dice", 7, 6): {20: 100000}, + ("Category Three Distinct Dice", 7, 7): {20: 100000}, + ("Category Three Distinct Dice", 7, 8): {20: 100000}, + ("Category Three Distinct Dice", 8, 1): {0: 243, 20: 99757}, + ("Category Three Distinct Dice", 8, 2): {0: 1, 20: 99999}, + ("Category Three Distinct Dice", 8, 3): {20: 100000}, + ("Category Three Distinct Dice", 8, 4): {20: 100000}, + ("Category Three Distinct Dice", 8, 5): {20: 100000}, + ("Category Three Distinct Dice", 8, 6): {20: 100000}, + ("Category Three Distinct Dice", 8, 7): {20: 100000}, + ("Category Three Distinct Dice", 8, 8): {20: 100000}, + ("Category Two Pair", 1, 1): {0: 100000}, + ("Category Two Pair", 1, 2): {0: 100000}, + ("Category Two Pair", 1, 3): {0: 100000}, + ("Category Two Pair", 1, 4): {0: 100000}, + ("Category Two Pair", 1, 5): {0: 100000}, + ("Category Two Pair", 1, 6): {0: 100000}, + ("Category Two Pair", 1, 7): {0: 100000}, + ("Category Two Pair", 1, 8): {0: 100000}, + ("Category Two Pair", 2, 1): {0: 100000}, + ("Category Two Pair", 2, 2): {0: 100000}, + ("Category Two Pair", 2, 3): {0: 100000}, + ("Category Two Pair", 2, 4): {0: 100000}, + ("Category Two Pair", 2, 5): {0: 100000}, + ("Category Two Pair", 2, 6): {0: 100000}, + ("Category Two Pair", 2, 7): {0: 100000}, + ("Category Two Pair", 2, 8): {0: 100000}, + ("Category Two Pair", 3, 1): {0: 100000}, + ("Category Two Pair", 3, 2): {0: 100000}, + ("Category Two Pair", 3, 3): {0: 100000}, + ("Category Two Pair", 3, 4): {0: 100000}, + ("Category Two Pair", 3, 5): {0: 100000}, + ("Category Two Pair", 3, 6): {0: 100000}, + ("Category Two Pair", 3, 7): {0: 100000}, + ("Category Two Pair", 3, 8): {0: 100000}, + ("Category Two Pair", 4, 1): {0: 93065, 30: 6935}, + ("Category Two Pair", 4, 2): {0: 82102, 30: 17898}, + ("Category Two Pair", 4, 3): {0: 71209, 30: 28791}, + ("Category Two Pair", 4, 4): {0: 61609, 30: 38391}, + ("Category Two Pair", 4, 5): {0: 53036, 30: 46964}, + ("Category Two Pair", 4, 6): {0: 45705, 30: 54295}, + ("Category Two Pair", 4, 7): {0: 39398, 30: 60602}, + ("Category Two Pair", 4, 8): {0: 33673, 30: 66327}, + ("Category Two Pair", 5, 1): {0: 72847, 30: 27153}, + ("Category Two Pair", 5, 2): {0: 46759, 30: 53241}, + ("Category Two Pair", 5, 3): {0: 29462, 30: 70538}, + ("Category Two Pair", 5, 4): {0: 18351, 30: 81649}, + ("Category Two Pair", 5, 5): {0: 11793, 30: 88207}, + ("Category Two Pair", 5, 6): {0: 7385, 30: 92615}, + ("Category Two Pair", 5, 7): {0: 4610, 30: 95390}, + ("Category Two Pair", 5, 8): {0: 2938, 30: 97062}, + ("Category Two Pair", 6, 1): {0: 44431, 30: 55569}, + ("Category Two Pair", 6, 2): {0: 17183, 30: 82817}, + ("Category Two Pair", 6, 3): {0: 6759, 30: 93241}, + ("Category Two Pair", 6, 4): {0: 2562, 30: 97438}, + ("Category Two Pair", 6, 5): {0: 948, 30: 99052}, + ("Category Two Pair", 6, 6): {0: 375, 30: 99625}, + ("Category Two Pair", 6, 7): {0: 138, 30: 99862}, + ("Category Two Pair", 6, 8): {0: 57, 30: 99943}, + ("Category Two Pair", 7, 1): {0: 19888, 30: 80112}, + ("Category Two Pair", 7, 2): {0: 3935, 30: 96065}, + ("Category Two Pair", 7, 3): {0: 801, 30: 99199}, + ("Category Two Pair", 7, 4): {0: 175, 30: 99825}, + ("Category Two Pair", 7, 5): {0: 31, 30: 99969}, + ("Category Two Pair", 7, 6): {0: 7, 30: 99993}, + ("Category Two Pair", 7, 7): {0: 2, 30: 99998}, + ("Category Two Pair", 7, 8): {30: 100000}, + ("Category Two Pair", 8, 1): {0: 6791, 30: 93209}, + ("Category Two Pair", 8, 2): {0: 588, 30: 99412}, + ("Category Two Pair", 8, 3): {0: 61, 30: 99939}, + ("Category Two Pair", 8, 4): {0: 6, 30: 99994}, + ("Category Two Pair", 8, 5): {30: 100000}, + ("Category Two Pair", 8, 6): {30: 100000}, + ("Category Two Pair", 8, 7): {30: 100000}, + ("Category Two Pair", 8, 8): {30: 100000}, + ("Category 2-1-2 Consecutive", 1, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 5, 1): {0: 98403, 40: 1597}, + ("Category 2-1-2 Consecutive", 5, 2): {0: 90651, 40: 9349}, + ("Category 2-1-2 Consecutive", 5, 3): {0: 80100, 40: 19900}, + ("Category 2-1-2 Consecutive", 5, 4): {0: 69131, 40: 30869}, + ("Category 2-1-2 Consecutive", 5, 5): {0: 58252, 40: 41748}, + ("Category 2-1-2 Consecutive", 5, 6): {0: 49405, 40: 50595}, + ("Category 2-1-2 Consecutive", 5, 7): {0: 41585, 40: 58415}, + ("Category 2-1-2 Consecutive", 5, 8): {0: 34952, 40: 65048}, + ("Category 2-1-2 Consecutive", 6, 1): {0: 93465, 40: 6535}, + ("Category 2-1-2 Consecutive", 6, 2): {0: 73416, 40: 26584}, + ("Category 2-1-2 Consecutive", 6, 3): {0: 54041, 40: 45959}, + ("Category 2-1-2 Consecutive", 6, 4): {0: 38535, 40: 61465}, + ("Category 2-1-2 Consecutive", 6, 5): {0: 27366, 40: 72634}, + ("Category 2-1-2 Consecutive", 6, 6): {0: 18924, 40: 81076}, + ("Category 2-1-2 Consecutive", 6, 7): {0: 13387, 40: 86613}, + ("Category 2-1-2 Consecutive", 6, 8): {0: 9134, 40: 90866}, + ("Category 2-1-2 Consecutive", 7, 1): {0: 84168, 40: 15832}, + ("Category 2-1-2 Consecutive", 7, 2): {0: 52659, 40: 47341}, + ("Category 2-1-2 Consecutive", 7, 3): {0: 30435, 40: 69565}, + ("Category 2-1-2 Consecutive", 7, 4): {0: 17477, 40: 82523}, + ("Category 2-1-2 Consecutive", 7, 5): {0: 9782, 40: 90218}, + ("Category 2-1-2 Consecutive", 7, 6): {0: 5316, 40: 94684}, + ("Category 2-1-2 Consecutive", 7, 7): {0: 2995, 40: 97005}, + ("Category 2-1-2 Consecutive", 7, 8): {0: 1689, 40: 98311}, + ("Category 2-1-2 Consecutive", 8, 1): {0: 71089, 40: 28911}, + ("Category 2-1-2 Consecutive", 8, 2): {0: 33784, 40: 66216}, + ("Category 2-1-2 Consecutive", 8, 3): {0: 14820, 40: 85180}, + ("Category 2-1-2 Consecutive", 8, 4): {0: 6265, 40: 93735}, + ("Category 2-1-2 Consecutive", 8, 5): {0: 2600, 40: 97400}, + ("Category 2-1-2 Consecutive", 8, 6): {0: 1155, 40: 98845}, + ("Category 2-1-2 Consecutive", 8, 7): {0: 487, 40: 99513}, + ("Category 2-1-2 Consecutive", 8, 8): {0: 190, 40: 99810}, + ("Category Five Distinct Dice", 1, 1): {0: 100000}, + ("Category Five Distinct Dice", 1, 2): {0: 100000}, + ("Category Five Distinct Dice", 1, 3): {0: 100000}, + ("Category Five Distinct Dice", 1, 4): {0: 100000}, + ("Category Five Distinct Dice", 1, 5): {0: 100000}, + ("Category Five Distinct Dice", 1, 6): {0: 100000}, + ("Category Five Distinct Dice", 1, 7): {0: 100000}, + ("Category Five Distinct Dice", 1, 8): {0: 100000}, + ("Category Five Distinct Dice", 2, 1): {0: 100000}, + ("Category Five Distinct Dice", 2, 2): {0: 100000}, + ("Category Five Distinct Dice", 2, 3): {0: 100000}, + ("Category Five Distinct Dice", 2, 4): {0: 100000}, + ("Category Five Distinct Dice", 2, 5): {0: 100000}, + ("Category Five Distinct Dice", 2, 6): {0: 100000}, + ("Category Five Distinct Dice", 2, 7): {0: 100000}, + ("Category Five Distinct Dice", 2, 8): {0: 100000}, + ("Category Five Distinct Dice", 3, 1): {0: 100000}, + ("Category Five Distinct Dice", 3, 2): {0: 100000}, + ("Category Five Distinct Dice", 3, 3): {0: 100000}, + ("Category Five Distinct Dice", 3, 4): {0: 100000}, + ("Category Five Distinct Dice", 3, 5): {0: 100000}, + ("Category Five Distinct Dice", 3, 6): {0: 100000}, + ("Category Five Distinct Dice", 3, 7): {0: 100000}, + ("Category Five Distinct Dice", 3, 8): {0: 100000}, + ("Category Five Distinct Dice", 4, 1): {0: 100000}, + ("Category Five Distinct Dice", 4, 2): {0: 100000}, + ("Category Five Distinct Dice", 4, 3): {0: 100000}, + ("Category Five Distinct Dice", 4, 4): {0: 100000}, + ("Category Five Distinct Dice", 4, 5): {0: 100000}, + ("Category Five Distinct Dice", 4, 6): {0: 100000}, + ("Category Five Distinct Dice", 4, 7): {0: 100000}, + ("Category Five Distinct Dice", 4, 8): {0: 100000}, + ("Category Five Distinct Dice", 5, 1): {0: 90907, 25: 9093}, + ("Category Five Distinct Dice", 5, 2): {0: 68020, 25: 31980}, + ("Category Five Distinct Dice", 5, 3): {0: 47692, 25: 52308}, + ("Category Five Distinct Dice", 5, 4): {0: 32383, 25: 67617}, + ("Category Five Distinct Dice", 5, 5): {0: 21631, 25: 78369}, + ("Category Five Distinct Dice", 5, 6): {0: 14366, 25: 85634}, + ("Category Five Distinct Dice", 5, 7): {0: 9568, 25: 90432}, + ("Category Five Distinct Dice", 5, 8): {0: 6360, 25: 93640}, + ("Category Five Distinct Dice", 6, 1): {0: 75051, 25: 24949}, + ("Category Five Distinct Dice", 6, 2): {0: 38409, 25: 61591}, + ("Category Five Distinct Dice", 6, 3): {0: 17505, 25: 82495}, + ("Category Five Distinct Dice", 6, 4): {0: 7862, 25: 92138}, + ("Category Five Distinct Dice", 6, 5): {0: 3538, 25: 96462}, + ("Category Five Distinct Dice", 6, 6): {0: 1645, 25: 98355}, + ("Category Five Distinct Dice", 6, 7): {0: 714, 25: 99286}, + ("Category Five Distinct Dice", 6, 8): {0: 341, 25: 99659}, + ("Category Five Distinct Dice", 7, 1): {0: 58588, 25: 41412}, + ("Category Five Distinct Dice", 7, 2): {0: 19487, 25: 80513}, + ("Category Five Distinct Dice", 7, 3): {0: 6043, 25: 93957}, + ("Category Five Distinct Dice", 7, 4): {0: 1799, 25: 98201}, + ("Category Five Distinct Dice", 7, 5): {0: 544, 25: 99456}, + ("Category Five Distinct Dice", 7, 6): {0: 169, 25: 99831}, + ("Category Five Distinct Dice", 7, 7): {0: 59, 25: 99941}, + ("Category Five Distinct Dice", 7, 8): {0: 11, 25: 99989}, + ("Category Five Distinct Dice", 8, 1): {0: 43586, 25: 56414}, + ("Category Five Distinct Dice", 8, 2): {0: 9615, 25: 90385}, + ("Category Five Distinct Dice", 8, 3): {0: 1944, 25: 98056}, + ("Category Five Distinct Dice", 8, 4): {0: 383, 25: 99617}, + ("Category Five Distinct Dice", 8, 5): {0: 77, 25: 99923}, + ("Category Five Distinct Dice", 8, 6): {0: 18, 25: 99982}, + ("Category Five Distinct Dice", 8, 7): {0: 3, 25: 99997}, + ("Category Five Distinct Dice", 8, 8): {0: 2, 25: 99998}, + ("Category 4&5 Full House", 1, 1): {0: 100000}, + ("Category 4&5 Full House", 1, 2): {0: 100000}, + ("Category 4&5 Full House", 1, 3): {0: 100000}, + ("Category 4&5 Full House", 1, 4): {0: 100000}, + ("Category 4&5 Full House", 1, 5): {0: 100000}, + ("Category 4&5 Full House", 1, 6): {0: 100000}, + ("Category 4&5 Full House", 1, 7): {0: 100000}, + ("Category 4&5 Full House", 1, 8): {0: 100000}, + ("Category 4&5 Full House", 2, 1): {0: 100000}, + ("Category 4&5 Full House", 2, 2): {0: 100000}, + ("Category 4&5 Full House", 2, 3): {0: 100000}, + ("Category 4&5 Full House", 2, 4): {0: 100000}, + ("Category 4&5 Full House", 2, 5): {0: 100000}, + ("Category 4&5 Full House", 2, 6): {0: 100000}, + ("Category 4&5 Full House", 2, 7): {0: 100000}, + ("Category 4&5 Full House", 2, 8): {0: 100000}, + ("Category 4&5 Full House", 3, 1): {0: 100000}, + ("Category 4&5 Full House", 3, 2): {0: 100000}, + ("Category 4&5 Full House", 3, 3): {0: 100000}, + ("Category 4&5 Full House", 3, 4): {0: 100000}, + ("Category 4&5 Full House", 3, 5): {0: 100000}, + ("Category 4&5 Full House", 3, 6): {0: 100000}, + ("Category 4&5 Full House", 3, 7): {0: 100000}, + ("Category 4&5 Full House", 3, 8): {0: 100000}, + ("Category 4&5 Full House", 4, 1): {0: 100000}, + ("Category 4&5 Full House", 4, 2): {0: 100000}, + ("Category 4&5 Full House", 4, 3): {0: 100000}, + ("Category 4&5 Full House", 4, 4): {0: 100000}, + ("Category 4&5 Full House", 4, 5): {0: 100000}, + ("Category 4&5 Full House", 4, 6): {0: 100000}, + ("Category 4&5 Full House", 4, 7): {0: 100000}, + ("Category 4&5 Full House", 4, 8): {0: 100000}, + ("Category 4&5 Full House", 5, 1): {0: 99724, 50: 276}, + ("Category 4&5 Full House", 5, 2): {0: 96607, 50: 3393}, + ("Category 4&5 Full House", 5, 3): {0: 88788, 50: 11212}, + ("Category 4&5 Full House", 5, 4): {0: 77799, 50: 22201}, + ("Category 4&5 Full House", 5, 5): {0: 65797, 50: 34203}, + ("Category 4&5 Full House", 5, 6): {0: 54548, 50: 45452}, + ("Category 4&5 Full House", 5, 7): {0: 44898, 50: 55102}, + ("Category 4&5 Full House", 5, 8): {0: 36881, 50: 63119}, + ("Category 4&5 Full House", 6, 1): {0: 98841, 50: 1159}, + ("Category 4&5 Full House", 6, 2): {0: 88680, 50: 11320}, + ("Category 4&5 Full House", 6, 3): {0: 70215, 50: 29785}, + ("Category 4&5 Full House", 6, 4): {0: 50801, 50: 49199}, + ("Category 4&5 Full House", 6, 5): {0: 35756, 50: 64244}, + ("Category 4&5 Full House", 6, 6): {0: 24698, 50: 75302}, + ("Category 4&5 Full House", 6, 7): {0: 17145, 50: 82855}, + ("Category 4&5 Full House", 6, 8): {0: 11846, 50: 88154}, + ("Category 4&5 Full House", 7, 1): {0: 97090, 50: 2910}, + ("Category 4&5 Full House", 7, 2): {0: 77440, 50: 22560}, + ("Category 4&5 Full House", 7, 3): {0: 51372, 50: 48628}, + ("Category 4&5 Full House", 7, 4): {0: 30566, 50: 69434}, + ("Category 4&5 Full House", 7, 5): {0: 17866, 50: 82134}, + ("Category 4&5 Full House", 7, 6): {0: 10521, 50: 89479}, + ("Category 4&5 Full House", 7, 7): {0: 6204, 50: 93796}, + ("Category 4&5 Full House", 7, 8): {0: 3670, 50: 96330}, + ("Category 4&5 Full House", 8, 1): {0: 94172, 50: 5828}, + ("Category 4&5 Full House", 8, 2): {0: 64693, 50: 35307}, + ("Category 4&5 Full House", 8, 3): {0: 35293, 50: 64707}, + ("Category 4&5 Full House", 8, 4): {0: 17749, 50: 82251}, + ("Category 4&5 Full House", 8, 5): {0: 8740, 50: 91260}, + ("Category 4&5 Full House", 8, 6): {0: 4550, 50: 95450}, + ("Category 4&5 Full House", 8, 7): {0: 2218, 50: 97782}, + ("Category 4&5 Full House", 8, 8): {0: 1084, 50: 98916}, +} diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py new file mode 100644 index 0000000000..c36c59544f --- /dev/null +++ b/worlds/yachtdice/__init__.py @@ -0,0 +1,533 @@ +import math +from typing import Dict + +from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial + +from worlds.AutoWorld import WebWorld, World + +from .Items import YachtDiceItem, item_groups, item_table +from .Locations import YachtDiceLocation, all_locations, ini_locations +from .Options import ( + AddExtraPoints, + AddStoryChapters, + GameDifficulty, + MinimalNumberOfDiceAndRolls, + MinimizeExtraItems, + PointsSize, + YachtDiceOptions, + yd_option_groups, +) +from .Rules import dice_simulation_fill_pool, set_yacht_completion_rules, set_yacht_rules + + +class YachtDiceWeb(WebWorld): + tutorials = [ + Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Yacht Dice. This guide covers single-player, multiworld, and website.", + "English", + "setup_en.md", + "setup/en", + ["Spineraks"], + ) + ] + + option_groups = yd_option_groups + + +class YachtDiceWorld(World): + """ + Yacht Dice is a straightforward game, custom-made for Archipelago, + where you cast your dice to chart a course for high scores, + unlocking valuable treasures along the way. + Discover more dice, extra rolls, multipliers, + and unlockable categories to navigate the depths of the game. + Roll your way to victory by reaching the target score! + """ + + game: str = "Yacht Dice" + options_dataclass = YachtDiceOptions + + web = YachtDiceWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + + location_name_to_id = {name: data.id for name, data in all_locations.items()} + + item_name_groups = item_groups + + ap_world_version = "2.1.1" + + def _get_yachtdice_data(self): + return { + # "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), + "seed_name": self.multiworld.seed_name, + "player_name": self.multiworld.get_player_name(self.player), + "player_id": self.player, + "race": self.multiworld.is_race, + } + + def generate_early(self): + """ + In generate early, we fill the item-pool, then determine the number of locations, and add filler items. + """ + self.itempool = [] + self.precollected = [] + + # number of dice and rolls in the pull + opt_dice_and_rolls = self.options.minimal_number_of_dice_and_rolls + + if opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_3_rolls: + num_of_dice = 5 + num_of_rolls = 3 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_5_rolls: + num_of_dice = 5 + num_of_rolls = 5 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_6_dice_and_4_rolls: + num_of_dice = 6 + num_of_rolls = 4 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_7_dice_and_3_rolls: + num_of_dice = 7 + num_of_rolls = 3 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_8_dice_and_2_rolls: + num_of_dice = 8 + num_of_rolls = 2 + else: + raise Exception(f"[Yacht Dice] Unknown MinimalNumberOfDiceAndRolls options {opt_dice_and_rolls}") + + # amount of dice and roll fragments needed to get a dice or roll + self.frags_per_dice = self.options.number_of_dice_fragments_per_dice.value + self.frags_per_roll = self.options.number_of_roll_fragments_per_roll.value + + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + self.frags_per_dice = min(self.frags_per_dice, 2) + self.frags_per_roll = min(self.frags_per_roll, 2) + + # set difficulty + diff_value = self.options.game_difficulty + if diff_value == GameDifficulty.option_easy: + self.difficulty = 1 + elif diff_value == GameDifficulty.option_medium: + self.difficulty = 2 + elif diff_value == GameDifficulty.option_hard: + self.difficulty = 3 + elif diff_value == GameDifficulty.option_extreme: + self.difficulty = 4 + else: + raise Exception(f"[Yacht Dice] Unknown GameDifficulty options {diff_value}") + + # Create a list with the specified number of 1s + num_ones = self.options.alternative_categories.value + categorylist = [1] * num_ones + [0] * (16 - num_ones) + + # Shuffle the list to randomize the order + self.random.shuffle(categorylist) + + # A list of all possible categories. + # Every entry in the list has two categories, one 'default' category and one 'alt'. + # You get either of the two for every entry, so a total of 16 unique categories. + all_categories = [ + ["Category Choice", "Category Double Threes and Fours"], + ["Category Inverse Choice", "Category Quadruple Ones and Twos"], + ["Category Ones", "Category Distincts"], + ["Category Twos", "Category Two times Ones"], + ["Category Threes", "Category Half of Sixes"], + ["Category Fours", "Category Twos and Threes"], + ["Category Fives", "Category Sum of Odds"], + ["Category Sixes", "Category Sum of Evens"], + ["Category Pair", "Category Micro Straight"], + ["Category Three of a Kind", "Category Three Odds"], + ["Category Four of a Kind", "Category 1-2-1 Consecutive"], + ["Category Tiny Straight", "Category Three Distinct Dice"], + ["Category Small Straight", "Category Two Pair"], + ["Category Large Straight", "Category 2-1-2 Consecutive"], + ["Category Full House", "Category Five Distinct Dice"], + ["Category Yacht", "Category 4&5 Full House"], + ] + + # categories used in this game. + self.possible_categories = [] + + for index, cats in enumerate(all_categories): + self.possible_categories.append(cats[categorylist[index]]) + + # Add Choice and Inverse choice (or their alts) to the precollected list. + if index == 0 or index == 1: + self.precollected.append(cats[categorylist[index]]) + else: + self.itempool.append(cats[categorylist[index]]) + + # Also start with one Roll and one Dice + self.precollected.append("Dice") + num_of_dice_to_add = num_of_dice - 1 + self.precollected.append("Roll") + num_of_rolls_to_add = num_of_rolls - 1 + + self.skip_early_locations = False + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + self.precollected.append("Dice") + num_of_dice_to_add -= 1 + self.precollected.append("Roll") + num_of_rolls_to_add -= 1 + self.skip_early_locations = True + + if num_of_dice_to_add > 0: + self.itempool.append("Dice") + num_of_dice_to_add -= 1 + if num_of_rolls_to_add > 0: + self.itempool.append("Roll") + num_of_rolls_to_add -= 1 + + # if one fragment per dice, just add "Dice" objects + if num_of_dice_to_add > 0: + if self.frags_per_dice == 1: + self.itempool += ["Dice"] * num_of_dice_to_add # minus one because one is in start inventory + else: + self.itempool += ["Dice Fragment"] * (self.frags_per_dice * num_of_dice_to_add) + + # if one fragment per roll, just add "Roll" objects + if num_of_rolls_to_add > 0: + if self.frags_per_roll == 1: + self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory + else: + self.itempool.append("Roll") # always add a full roll to make generation easier (will be early) + self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add) + + already_items = len(self.itempool) + + # Yacht Dice needs extra filler items so it doesn't get stuck in generation. + # For now, we calculate the number of extra items we'll need later. + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + extra_percentage = max(0.1, 0.8 - self.multiworld.players / 10) + elif self.options.minimize_extra_items == MinimizeExtraItems.option_no_dont: + extra_percentage = 0.72 + else: + raise Exception(f"[Yacht Dice] Unknown MinimizeExtraItems options {self.options.minimize_extra_items}") + extra_locations_needed = max(10, math.ceil(already_items * extra_percentage)) + + # max score is the value of the last check. Goal score is the score needed to 'finish' the game + self.max_score = self.options.score_for_last_check.value + self.goal_score = min(self.max_score, self.options.score_for_goal.value) + + # Yacht Dice adds items into the pool until a score of at least 1000 is reached. + # the yaml contains weights, which determine how likely it is that specific items get added. + # If all weights are 0, some of them will be made to be non-zero later. + weights: Dict[str, float] = { + "Dice": self.options.weight_of_dice.value, + "Roll": self.options.weight_of_roll.value, + "Fixed Score Multiplier": self.options.weight_of_fixed_score_multiplier.value, + "Step Score Multiplier": self.options.weight_of_step_score_multiplier.value, + "Double category": self.options.weight_of_double_category.value, + "Points": self.options.weight_of_points.value, + } + + # if the player wants extra rolls or dice, fill the pool with fragments until close to an extra roll/dice + if weights["Dice"] > 0 and self.frags_per_dice > 1: + self.itempool += ["Dice Fragment"] * (self.frags_per_dice - 1) + if weights["Roll"] > 0 and self.frags_per_roll > 1: + self.itempool += ["Roll Fragment"] * (self.frags_per_roll - 1) + + # calibrate the weights, since the impact of each of the items is different + weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice + weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll + + extra_points_added = 0 + multipliers_added = 0 + items_added = 0 + + def get_item_to_add(weights, extra_points_added, multipliers_added, items_added): + items_added += 1 + + all_items = self.itempool + self.precollected + dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment") + if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice: + weights["Dice"] = 0 # don't allow >=9 dice + roll_fragments_in_pool = all_items.count("Roll") * self.frags_per_roll + all_items.count("Roll Fragment") + if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll: + weights["Roll"] = 0 # don't allow >= 6 rolls + + # Don't allow too many multipliers + if multipliers_added > 50: + weights["Fixed Score Multiplier"] = 0 + weights["Step Score Multiplier"] = 0 + + # Don't allow too many extra points + if extra_points_added > 300: + weights["Points"] = 0 + + # if all weights are zero, allow to add fixed score multiplier, double category, points. + if sum(weights.values()) == 0: + if multipliers_added <= 50: + weights["Fixed Score Multiplier"] = 1 + weights["Double category"] = 1 + if extra_points_added <= 300: + weights["Points"] = 1 + + # Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item + which_item_to_add = self.random.choices(list(weights.keys()), weights=list(weights.values()))[0] + + if which_item_to_add == "Dice": + weights["Dice"] /= 1 + self.frags_per_dice + return "Dice" if self.frags_per_dice == 1 else "Dice Fragment" + elif which_item_to_add == "Roll": + weights["Roll"] /= 1 + self.frags_per_roll + return "Roll" if self.frags_per_roll == 1 else "Roll Fragment" + elif which_item_to_add == "Fixed Score Multiplier": + weights["Fixed Score Multiplier"] /= 1.05 + multipliers_added += 1 + return "Fixed Score Multiplier" + elif which_item_to_add == "Step Score Multiplier": + weights["Step Score Multiplier"] /= 1.1 + multipliers_added += 1 + return "Step Score Multiplier" + elif which_item_to_add == "Double category": + # Below entries are the weights to add each category. + # Prefer to add choice or number categories, because the other categories are too "all or nothing", + # which often don't give any points, until you get overpowered, and then they give all points. + cat_weights = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] + weights["Double category"] /= 1.1 + return self.random.choices(self.possible_categories, weights=cat_weights)[0] + elif which_item_to_add == "Points": + score_dist = self.options.points_size + probs = {"1 Point": 1, "10 Points": 0, "100 Points": 0} + if score_dist == PointsSize.option_small: + probs = {"1 Point": 0.9, "10 Points": 0.1, "100 Points": 0} + elif score_dist == PointsSize.option_medium: + probs = {"1 Point": 0, "10 Points": 1, "100 Points": 0} + elif score_dist == PointsSize.option_large: + probs = {"1 Point": 0, "10 Points": 0.3, "100 Points": 0.7} + elif score_dist == PointsSize.option_mix: + probs = {"1 Point": 0.3, "10 Points": 0.4, "100 Points": 0.3} + else: + raise Exception(f"[Yacht Dice] Unknown PointsSize options {score_dist}") + choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0] + if choice == "1 Point": + weights["Points"] /= 1.01 + extra_points_added += 1 + return "1 Point" + elif choice == "10 Points": + weights["Points"] /= 1.1 + extra_points_added += 10 + return "10 Points" + elif choice == "100 Points": + weights["Points"] /= 2 + extra_points_added += 100 + return "100 Points" + else: + raise Exception("Unknown point value (Yacht Dice)") + else: + raise Exception(f"Invalid index when adding new items in Yacht Dice: {which_item_to_add}") + + # adding 17 items as a start seems like the smartest way to get close to 1000 points + for _ in range(17): + self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added)) + + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + + # if we overshoot, remove items until you get below 1000, then return the last removed item + if score_in_logic > 1000: + removed_item = "" + while score_in_logic > 1000: + removed_item = self.itempool.pop() + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + self.itempool.append(removed_item) + else: + # Keep adding items until a score of 1000 is in logic + while score_in_logic < 1000: + item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added) + self.itempool.append(item_to_add) + if item_to_add == "1 Point": + score_in_logic += 1 + elif item_to_add == "10 Points": + score_in_logic += 10 + elif item_to_add == "100 Points": + score_in_logic += 100 + else: + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + + # count the number of locations in the game. + already_items = len(self.itempool) + 1 # +1 because of Victory item + + # We need to add more filler/useful items if there are many items in the pool to guarantee successful generation + extra_locations_needed += (already_items - 45) // 15 + self.number_of_locations = already_items + extra_locations_needed + + # From here, we will count the number of items in the self.itempool, and add useful/filler items to the pool, + # making sure not to exceed the number of locations. + + # first, we flood the entire pool with extra points (useful), if that setting is chosen. + if self.options.add_bonus_points == AddExtraPoints.option_all_of_it: # all of the extra points + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 100) + + # second, we flood the entire pool with story chapters (filler), if that setting is chosen. + if self.options.add_story_chapters == AddStoryChapters.option_all_of_it: # all of the story chapters + already_items = len(self.itempool) + 1 + number_of_items = min(self.number_of_locations - already_items, 100) + number_of_items = (number_of_items // 10) * 10 # story chapters always come in multiples of 10 + self.itempool += ["Story Chapter"] * number_of_items + + # add some extra points (useful) + if self.options.add_bonus_points == AddExtraPoints.option_sure: # add extra points if wanted + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) + + # add some story chapters (filler) + if self.options.add_story_chapters == AddStoryChapters.option_sure: # add extra points if wanted + already_items = len(self.itempool) + 1 + if self.number_of_locations - already_items >= 10: + self.itempool += ["Story Chapter"] * 10 + + # add some more extra points if there is still room + if self.options.add_bonus_points == AddExtraPoints.option_sure: + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) + + # add some encouragements filler-items if there is still room + already_items = len(self.itempool) + 1 + self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5) + + # add some fun facts filler-items if there is still room + already_items = len(self.itempool) + 1 + self.itempool += ["Fun Fact"] * min(self.number_of_locations - already_items, 5) + + # finally, add some "Good RNG" and "Bad RNG" items to complete the item pool + # these items are filler and do not do anything. + + # probability of Good and Bad rng, based on difficulty for fun :) + + p = 1.1 - 0.25 * self.difficulty + already_items = len(self.itempool) + 1 + self.itempool += self.random.choices( + ["Good RNG", "Bad RNG"], weights=[p, 1 - p], k=self.number_of_locations - already_items + ) + + # we are done adding items. Now because of the last step, number of items should be number of locations + already_items = len(self.itempool) + 1 + if already_items != self.number_of_locations: + raise Exception( + f"[Yacht Dice] Number in self.itempool is not number of locations " + f"{already_items} {self.number_of_locations}." + ) + + # add precollected items using push_precollected. Items in self.itempool get created in create_items + for item in self.precollected: + self.multiworld.push_precollected(self.create_item(item)) + + # make sure one dice and one roll is early, so that you will have 2 dice and 2 rolls soon + self.multiworld.early_items[self.player]["Dice"] = 1 + self.multiworld.early_items[self.player]["Roll"] = 1 + + def create_items(self): + self.multiworld.itempool += [self.create_item(name) for name in self.itempool] + + def create_regions(self): + # call the ini_locations function, that generates locations based on the inputs. + location_table = ini_locations( + self.goal_score, + self.max_score, + self.number_of_locations, + self.difficulty, + self.skip_early_locations, + self.multiworld.players, + ) + + # simple menu-board construction + menu = Region("Menu", self.player, self.multiworld) + board = Region("Board", self.player, self.multiworld) + + # add locations to board, one for every location in the location_table + board.locations = [ + YachtDiceLocation(self.player, loc_name, loc_data.score, loc_data.id, board) + for loc_name, loc_data in location_table.items() + if loc_data.region == board.name + ] + + # Add the victory item to the correct location. + # The website declares that the game is complete when the victory item is obtained. + victory_location_name = f"{self.goal_score} score" + self.get_location(victory_location_name).place_locked_item(self.create_item("Victory")) + + # add the regions + connection = Entrance(self.player, "New Board", menu) + menu.exits.append(connection) + connection.connect(board) + self.multiworld.regions += [menu, board] + + def set_rules(self): + """ + set rules per location, and add the rule for beating the game + """ + set_yacht_rules( + self.multiworld, + self.player, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + ) + set_yacht_completion_rules(self.multiworld, self.player) + + def fill_slot_data(self): + """ + make slot data, which consists of yachtdice_data, options, and some other variables. + """ + yacht_dice_data = self._get_yachtdice_data() + yacht_dice_options = self.options.as_dict( + "game_difficulty", + "score_for_last_check", + "score_for_goal", + "number_of_dice_fragments_per_dice", + "number_of_roll_fragments_per_roll", + "which_story", + "allow_manual_input", + ) + slot_data = {**yacht_dice_data, **yacht_dice_options} # combine the two + slot_data["number_of_dice_fragments_per_dice"] = self.frags_per_dice + slot_data["number_of_roll_fragments_per_roll"] = self.frags_per_roll + slot_data["goal_score"] = self.goal_score + slot_data["last_check_score"] = self.max_score + slot_data["allowed_categories"] = self.possible_categories + slot_data["ap_world_version"] = self.ap_world_version + return slot_data + + def create_item(self, name: str) -> Item: + item_data = item_table[name] + item = YachtDiceItem(name, item_data.classification, item_data.code, self.player) + return item + + # We overwrite these function to monitor when states have changed. See also dice_simulation in Rules.py + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change: + state.prog_items[self.player]["state_is_fresh"] = 0 + + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change: + state.prog_items[self.player]["state_is_fresh"] = 0 + + return change diff --git a/worlds/yachtdice/docs/en_Yacht Dice.md b/worlds/yachtdice/docs/en_Yacht Dice.md new file mode 100644 index 0000000000..53eefe9e9c --- /dev/null +++ b/worlds/yachtdice/docs/en_Yacht Dice.md @@ -0,0 +1,15 @@ +# Yacht Dice + +Welcome to Yacht Dice, the ultimate dice-rolling adventure in Archipelago! Cast your dice, chase high scores, and unlock valuable treasures. Discover new dice, extra rolls, multipliers, and special scoring categories to enhance your game. Roll your way to victory by reaching the target score! + +## Understanding Location Checks +In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed on the website. + +## Items and Their Effects +When you receive an item, it could be extra dice, extra rolls, score multipliers, or new scoring categories. These boosts help you sail towards higher scores and more loot. Other items include extra points, lore, and fun facts to enrich your journey. + +## Winning the Game +Victory in Yacht Dice is all about reaching the target score. You can set your own target score, which is displayed on the website. Once you hit it, you've conquered the game! + +## How to Access Options +Need to tweak your game? Head over to the [player options page](../player-options) for all your configuration options and to export your config file. diff --git a/worlds/yachtdice/docs/setup_en.md b/worlds/yachtdice/docs/setup_en.md new file mode 100644 index 0000000000..ae5f92d93e --- /dev/null +++ b/worlds/yachtdice/docs/setup_en.md @@ -0,0 +1,21 @@ +# Yacht Dice Randomizer Setup Guide + +## Required Software + +- A browser (you are probably using one right now!). +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases). + +## Playing the game +Open the Yacht Dice website. There are two options: +- Download the latest release from [Yacht Dice Releases](https://github.com/spinerak/ArchipelagoYachtDice/releases) and unzip the Website.zip. Then open player.html in your browser. +- Cruise over to the [Yacht Dice website](https://yacht-dice-ap.netlify.app/). This also works on mobile. If the website is not available, use the first option. + +Both options have an "offline" play option to try out the game without having to generate a game first. + +## Play with Archipelago + +- Create your yaml file via the [Yacht Dice Player Options Page](../player-options). +- After generating, open the Yacht Dice website. After the tutoroll, fill in the room information. +- After logging in, you are good to go. The website has a built-in client, where you can chat and send commands. + +For more information on yaml files, generating Archipelago games, and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). From 3cdcb8c455d7f1951a4becf8bf06d2933feb3a28 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Wed, 21 Aug 2024 21:40:40 +0200 Subject: [PATCH 02/50] Yacht Dice: setup: change release-link to latest (#3827) On the installation page, link to the latest release, instead of the page with all releases --- worlds/yachtdice/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/yachtdice/docs/setup_en.md b/worlds/yachtdice/docs/setup_en.md index ae5f92d93e..c76cd398ce 100644 --- a/worlds/yachtdice/docs/setup_en.md +++ b/worlds/yachtdice/docs/setup_en.md @@ -7,7 +7,7 @@ ## Playing the game Open the Yacht Dice website. There are two options: -- Download the latest release from [Yacht Dice Releases](https://github.com/spinerak/ArchipelagoYachtDice/releases) and unzip the Website.zip. Then open player.html in your browser. +- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open player.html in your browser. - Cruise over to the [Yacht Dice website](https://yacht-dice-ap.netlify.app/). This also works on mobile. If the website is not available, use the first option. Both options have an "offline" play option to try out the game without having to generate a game first. From e35addf5b26a380e36af425445f6b9b459ccddfb Mon Sep 17 00:00:00 2001 From: B1t Date: Thu, 22 Aug 2024 11:59:11 -0600 Subject: [PATCH 03/50] ALTTP: Minor Tweaks to the Adjuster UI (#2533) * Tweak ALTTP Adjuster padding/size to accommodate resizing - Set minsize so the actions buttons on bottom are always visible. - Added a minor amount of padding around the top level objects. - Increased the size of the entry fields for roms to match general button size. - Updated layout calls so vertical spacing doesn't increase between fields when maximizing the window - Added a little bit of spacing on the rom label so it more closely lines up with the other rom selection field * Tweak ALTTP Adjuster padding/size to accommodate resizing - Set minsize so the actions buttons on bottom are always visible. - Added a minor amount of padding around the top level objects. - Increased the size of the entry fields for roms to match general button size. - Updated layout calls so vertical spacing doesn't increase between fields when maximizing the window - Added a little bit of spacing on the rom label so it more closely lines up with the other rom selection field --- LttPAdjuster.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 9c5bd10244..7e33a3d5ef 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -14,7 +14,7 @@ import tkinter as tk from argparse import Namespace from concurrent.futures import as_completed, ThreadPoolExecutor from glob import glob -from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \ +from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \ IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage from tkinter.constants import DISABLED, NORMAL from urllib.parse import urlparse @@ -29,7 +29,8 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s GAME_ALTTP = "A Link to the Past" - +WINDOW_MIN_HEIGHT = 525 +WINDOW_MIN_WIDTH = 425 class AdjusterWorld(object): def __init__(self, sprite_pool): @@ -242,16 +243,17 @@ def adjustGUI(): from argparse import Namespace from Utils import __version__ as MWVersion adjustWindow = Tk() + adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT) adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion) set_icon(adjustWindow) rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow) - bottomFrame2 = Frame(adjustWindow) + bottomFrame2 = Frame(adjustWindow, padx=8, pady=2) romFrame, romVar = get_rom_frame(adjustWindow) - romDialogFrame = Frame(adjustWindow) + romDialogFrame = Frame(adjustWindow, padx=8, pady=2) baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust') romVar2 = StringVar() romEntry2 = Entry(romDialogFrame, textvariable=romVar2) @@ -261,9 +263,9 @@ def adjustGUI(): romVar2.set(rom) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) - romDialogFrame.pack(side=TOP, expand=True, fill=X) - baseRomLabel2.pack(side=LEFT) - romEntry2.pack(side=LEFT, expand=True, fill=X) + romDialogFrame.pack(side=TOP, expand=False, fill=X) + baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8)) + romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton2.pack(side=LEFT) def adjustRom(): @@ -331,12 +333,11 @@ def adjustGUI(): messagebox.showinfo(title="Success", message="Settings saved to persistent storage") adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) - rom_options_frame.pack(side=TOP) + rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True) adjustButton.pack(side=LEFT, padx=(5,5)) saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings) saveButton.pack(side=LEFT, padx=(5,5)) - bottomFrame2.pack(side=TOP, pady=(5,5)) tkinter_center_window(adjustWindow) @@ -576,7 +577,7 @@ class AttachTooltip(object): def get_rom_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romFrame = Frame(parent) + romFrame = Frame(parent, padx=8, pady=8) baseRomLabel = Label(romFrame, text='LttP Base Rom: ') romVar = StringVar(value=adjuster_settings.baserom) romEntry = Entry(romFrame, textvariable=romVar) @@ -596,20 +597,19 @@ def get_rom_frame(parent=None): romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect) baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT, expand=True, fill=X) + romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton.pack(side=LEFT) - romFrame.pack(side=TOP, expand=True, fill=X) + romFrame.pack(side=TOP, fill=X) return romFrame, romVar def get_rom_options_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romOptionsFrame = LabelFrame(parent, text="Rom options") - romOptionsFrame.columnconfigure(0, weight=1) - romOptionsFrame.columnconfigure(1, weight=1) + romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8) + for i in range(5): - romOptionsFrame.rowconfigure(i, weight=1) + romOptionsFrame.rowconfigure(i, weight=0, pad=4) vars = Namespace() vars.MusicVar = IntVar() @@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None): spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect) baseSpriteLabel.pack(side=LEFT) - spriteEntry.pack(side=LEFT) + spriteEntry.pack(side=LEFT, expand=True, fill=X) spriteSelectButton.pack(side=LEFT) oofDialogFrame = Frame(romOptionsFrame) From 31852801c9849c9f9c015030839086b3188fc6bd Mon Sep 17 00:00:00 2001 From: Kappatechy Date: Thu, 22 Aug 2024 15:35:29 -0600 Subject: [PATCH 04/50] LTTP: Fix a bug in Triforce Pieces Mode: Extra (#3784) When triforce_pieces_mode is set to "extra", the number of Triforce pieces in the pool should be equal to the number required plus the number extra. The number available was being used in this calculation, instead of the number required. --- worlds/alttp/ItemPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 125475561b..c6c1770414 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -682,7 +682,7 @@ def get_pool_core(world, player: int): if 'triforce_hunt' in goal: if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra: - treasure_hunt_total = (world.triforce_pieces_available[player].value + treasure_hunt_total = (world.triforce_pieces_required[player].value + world.triforce_pieces_extra[player].value) elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage: percentage = float(world.triforce_pieces_percentage[player].value) / 100 From f390b33c17132ef37563847496106479992111db Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:23:05 +0200 Subject: [PATCH 05/50] The Witness: Ban Excluded Panels from Panel Hunt (#3818) * excluded panels should not be picked by panel hunt * ban excluded panels from panel hunt * Get rid of an unused variable --- worlds/witness/__init__.py | 2 +- worlds/witness/entity_hunt.py | 1 + worlds/witness/locations.py | 2 +- worlds/witness/player_logic.py | 5 ++--- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index b228842019..3b599bc9e7 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -90,7 +90,7 @@ class WitnessWorld(World): "laser_ids_to_hints": self.laser_ids_to_hints, "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, - "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], + "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES], "entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME, "panel_hunt_required_absolute": self.panel_hunt_required_count } diff --git a/worlds/witness/entity_hunt.py b/worlds/witness/entity_hunt.py index 29b914799f..34cf7d3d7f 100644 --- a/worlds/witness/entity_hunt.py +++ b/worlds/witness/entity_hunt.py @@ -77,6 +77,7 @@ class EntityHuntPicker: return ( self.player_logic.solvability_guaranteed(panel_hex) + and panel_hex not in self.player_logic.EXCLUDED_ENTITIES and not ( # Due to an edge case, Discards have to be on in disable_non_randomized even if Discard Shuffle is off. # However, I don't think they should be hunt panels in this case. diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index f1c1655039..d095b8bed4 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -44,7 +44,7 @@ class WitnessPlayerLocations: self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] - for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS + for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES } self.CHECK_PANELHEX_TO_ID = { diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 5125dfef0a..3321983dd8 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -108,8 +108,7 @@ class WitnessPlayerLogic: self.EVENT_ITEM_PAIRS: Dict[str, Tuple[str, str]] = {} self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set() self.DISABLE_EVERYTHING_BEHIND: Set[str] = set() - self.PRECOMPLETED_LOCATIONS: Set[str] = set() - self.EXCLUDED_LOCATIONS: Set[str] = set() + self.EXCLUDED_ENTITIES: Set[str] = set() self.ADDED_CHECKS: Set[str] = set() self.VICTORY_LOCATION = "0x0356B" @@ -659,7 +658,7 @@ class WitnessPlayerLogic: self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"]) elif loc_obj["entityType"] == "Panel": - self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"]) + self.EXCLUDED_ENTITIES.add(loc_obj["entity_hex"]) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None From 74aab81f79bc0c2e0356efbcf1ad7b4211b23ce8 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:23:22 +0200 Subject: [PATCH 06/50] Purge the world: multiworld evil from osrs (#3751) --- worlds/osrs/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index d74dc7cfd9..1b7ca9c1e0 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -56,8 +56,8 @@ class OSRSWorld(World): locations_by_category: typing.Dict[str, typing.List[LocationRow]] - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.region_name_to_data = {} self.location_name_to_data = {} From 64b654d42ec016c4e94b81842f3b6bac73f54740 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:15:05 +0200 Subject: [PATCH 07/50] Core, some worlds: Rename sweep_for_events to sweep_for_advancements (#3571) * Rename sweep_for_events to sweep_for_advancements * more event->advancement renames * oops accidentally deleted the deprecation thing in the force push * Update TestDungeon.py * Update BaseClasses.py * Update BaseClasses.py * oops * utils.deprecate * treble, you had no idea how right you were * Update test_panel_hunt.py * Update BaseClasses.py Co-authored-by: Fabian Dill --------- Co-authored-by: Fabian Dill --- BaseClasses.py | 35 +++++++++++++---------- Fill.py | 14 ++++----- test/bases.py | 6 ++-- test/general/test_fill.py | 6 ++-- worlds/alttp/Dungeons.py | 6 ++-- worlds/alttp/test/dungeons/TestDungeon.py | 4 +-- worlds/lingo/__init__.py | 2 +- worlds/oot/EntranceShuffle.py | 6 ++-- worlds/oot/Rules.py | 2 +- worlds/oot/__init__.py | 6 ++-- worlds/pokemon_rb/__init__.py | 2 +- worlds/pokemon_rb/regions.py | 4 +-- worlds/witness/__init__.py | 2 +- worlds/witness/test/test_panel_hunt.py | 4 +-- 14 files changed, 52 insertions(+), 47 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1188e72261..61f3f8f67c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -430,7 +430,7 @@ class MultiWorld(): subworld = self.worlds[player] for item in subworld.get_pre_fill_items(): subworld.collect(ret, item) - ret.sweep_for_events() + ret.sweep_for_advancements() if use_cache: self._all_state = ret @@ -661,7 +661,7 @@ class CollectionState(): multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] - events: Set[Location] + advancements: Set[Location] path: Dict[Union[Region, Entrance], PathValue] locations_checked: Set[Location] stale: Dict[int, bool] @@ -673,7 +673,7 @@ class CollectionState(): self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} - self.events = set() + self.advancements = set() self.path = {} self.locations_checked = set() self.stale = {player: True for player in parent.get_all_ids()} @@ -722,7 +722,7 @@ class CollectionState(): self.reachable_regions.items()} ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in self.blocked_connections.items()} - ret.events = self.events.copy() + ret.advancements = self.advancements.copy() ret.path = self.path.copy() ret.locations_checked = self.locations_checked.copy() for function in self.additional_copy_functions: @@ -755,19 +755,24 @@ class CollectionState(): return self.multiworld.get_region(spot, player).can_reach(self) def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: + Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. " + "Please switch over to sweep_for_advancements.") + return self.sweep_for_advancements(locations) + + def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.multiworld.get_filled_locations() - reachable_events = True - # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.advancement and location not in self.events} + reachable_advancements = True + # since the loop has a good chance to run more than once, only filter the advancements once + locations = {location for location in locations if location.advancement and location not in self.advancements} - while reachable_events: - reachable_events = {location for location in locations if location.can_reach(self)} - locations -= reachable_events - for event in reachable_events: - self.events.add(event) - assert isinstance(event.item, Item), "tried to collect Event with no Item" - self.collect(event.item, True, event) + while reachable_advancements: + reachable_advancements = {location for location in locations if location.can_reach(self)} + locations -= reachable_advancements + for advancement in reachable_advancements: + self.advancements.add(advancement) + assert isinstance(advancement.item, Item), "tried to collect Event with no Item" + self.collect(advancement.item, True, advancement) # item name related def has(self, item: str, player: int, count: int = 1) -> bool: @@ -871,7 +876,7 @@ class CollectionState(): self.stale[item.player] = True if changed and not prevent_sweep: - self.sweep_for_events() + self.sweep_for_advancements() return changed diff --git a/Fill.py b/Fill.py index 15d5842e29..e2fcff0035 100644 --- a/Fill.py +++ b/Fill.py @@ -29,7 +29,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] new_state = base_state.copy() for item in itempool: new_state.collect(item, True) - new_state.sweep_for_events(locations=locations) + new_state.sweep_for_advancements(locations=locations) return new_state @@ -329,8 +329,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo pool.append(location.item) state.remove(location.item) location.item = None - if location in state.events: - state.events.remove(location) + if location in state.advancements: + state.advancements.remove(location) locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) @@ -363,7 +363,7 @@ def distribute_early_items(multiworld: MultiWorld, early_priority_locations: typing.List[Location] = [] loc_indexes_to_remove: typing.Set[int] = set() base_state = multiworld.state.copy() - base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) + base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) for i, loc in enumerate(fill_locations): if loc.can_reach(base_state): if loc.progress_type == LocationProgressType.PRIORITY: @@ -558,7 +558,7 @@ def flood_items(multiworld: MultiWorld) -> None: progress_done = False # sweep once to pick up preplaced items - multiworld.state.sweep_for_events() + multiworld.state.sweep_for_advancements() # fill multiworld from top of itempool while we can while not progress_done: @@ -746,7 +746,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: ), items_to_test): reducing_state.collect(location.item, True, location) - reducing_state.sweep_for_events(locations=locations_to_test) + reducing_state.sweep_for_advancements(locations=locations_to_test) if multiworld.has_beaten_game(balancing_state): if not multiworld.has_beaten_game(reducing_state): @@ -829,7 +829,7 @@ def distribute_planned(multiworld: MultiWorld) -> None: warn(warning, force) swept_state = multiworld.state.copy() - swept_state.sweep_for_events() + swept_state.sweep_for_advancements() reachable = frozenset(multiworld.get_reachable_locations(swept_state)) early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) diff --git a/test/bases.py b/test/bases.py index 83461b158f..a3ea233174 100644 --- a/test/bases.py +++ b/test/bases.py @@ -24,7 +24,7 @@ class TestBase(unittest.TestCase): for item in items: item.classification = ItemClassification.progression state.collect(item, prevent_sweep=True) - state.sweep_for_events() + state.sweep_for_advancements() state.update_reachable_regions(1) self._state_cache[self.multiworld, tuple(items)] = state return state @@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase): if isinstance(items, Item): items = (items,) for item in items: - if item.location and item.advancement and item.location in self.multiworld.state.events: - self.multiworld.state.events.remove(item.location) + if item.location and item.advancement and item.location in self.multiworld.state.advancements: + self.multiworld.state.advancements.remove(item.location) self.multiworld.state.remove(item) def can_reach_location(self, location: str) -> bool: diff --git a/test/general/test_fill.py b/test/general/test_fill.py index db24b70691..2dba147aca 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase): location_pool = player1.locations[1:] + player2.locations item_pool = player1.prog_items[:-1] + player2.prog_items fill_restrictive(multiworld, multiworld.state, location_pool, item_pool) - multiworld.state.sweep_for_events() # collect everything + multiworld.state.sweep_for_advancements() # collect everything # all of player2's locations and items should be accessible (not all of player1's) for item in player2.prog_items: @@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase): item = player1.prog_items[0] item.code = None location.place_locked_item(item) - multiworld.state.sweep_for_events() - multiworld.state.sweep_for_events() + multiworld.state.sweep_for_advancements() + multiworld.state.sweep_for_advancements() self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times") diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 150d52cc6c..8405fc4480 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): pass for item in pre_fill_items: multiworld.worlds[item.player].collect(all_state_base, item) - all_state_base.sweep_for_events() + all_state_base.sweep_for_advancements() # Remove completion condition so that minimal-accessibility worlds place keys properly for player in {item.player for item in in_dungeon_items}: @@ -262,8 +262,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player])) loc = multiworld.get_location(key_loc, player) - if loc in all_state_base.events: - all_state_base.events.remove(loc) + if loc in all_state_base.advancements: + all_state_base.advancements.remove(loc) fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True, name="LttP Dungeon Items") diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 796bfeec3f..5ab1b23065 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase): for item in items: item.classification = ItemClassification.progression - state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_events() and picking up - state.sweep_for_events() # key drop keys repeatedly + state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up + state.sweep_for_advancements() # key drop keys repeatedly self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 9853be73fa..2a61a71f5f 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -85,7 +85,7 @@ class LingoWorld(World): state.collect(self.create_item(self.player_logic.forced_good_item), True) all_locations = self.multiworld.get_locations(self.player) - state.sweep_for_events(locations=all_locations) + state.sweep_for_advancements(locations=all_locations) unreachable_locations = [location for location in all_locations if not state.can_reach_location(location.name, self.player)] diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 058fdbed00..cda442ffb1 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -445,7 +445,7 @@ def shuffle_random_entrances(ootworld): # Gather locations to keep reachable for validation all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_advancements(locations=ootworld.get_locations()) locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances @@ -791,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all all_state = all_state_orig.copy() none_state = none_state_orig.copy() - all_state.sweep_for_events(locations=ootworld.get_locations()) - none_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_advancements(locations=ootworld.get_locations()) + none_state.sweep_for_advancements(locations=ootworld.get_locations()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 529411f6fc..4bbf15435c 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -228,7 +228,7 @@ def set_shop_rules(ootworld): def set_entrances_based_rules(ootworld): all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_advancements(locations=ootworld.get_locations()) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): # If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 24be303f82..ee78958b2d 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -847,7 +847,7 @@ class OOTWorld(World): # Make sure to only kill actual internal events, not in-game "events" all_state = self.get_state_with_complete_itempool() all_locations = self.get_locations() - all_state.sweep_for_events(locations=all_locations) + all_state.sweep_for_advancements(locations=all_locations) reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if (loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable] @@ -875,7 +875,7 @@ class OOTWorld(World): state = base_state.copy() for item in self.get_pre_fill_items(): self.collect(state, item) - state.sweep_for_events(locations=self.get_locations()) + state.sweep_for_advancements(locations=self.get_locations()) return state # Prefill shops, songs, and dungeon items @@ -887,7 +887,7 @@ class OOTWorld(World): state = CollectionState(self.multiworld) for item in self.itempool: self.collect(state, item) - state.sweep_for_events(locations=self.get_locations()) + state.sweep_for_advancements(locations=self.get_locations()) # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 277d73b225..c1d8431898 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -296,7 +296,7 @@ class PokemonRedBlueWorld(World): if attempt > 1: for mon in poke_data.pokemon_data.keys(): state.collect(self.create_item(mon), True) - state.sweep_for_events() + state.sweep_for_advancements() self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index a9206fe667..938c39b320 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -2439,7 +2439,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): state_copy = state.copy() state_copy.collect(item, True) - state.sweep_for_events(locations=event_locations) + state.sweep_for_advancements(locations=event_locations) new_reachable_entrances = len([entrance for entrance in entrances if entrance in reachable_entrances or entrance.parent_region.can_reach(state_copy)]) return new_reachable_entrances > len(reachable_entrances) @@ -2480,7 +2480,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): while entrances: state.update_reachable_regions(player) - state.sweep_for_events(locations=event_locations) + state.sweep_for_advancements(locations=event_locations) multiworld.random.shuffle(entrances) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 3b599bc9e7..02d11373b2 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -219,7 +219,7 @@ class WitnessWorld(World): # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. state = CollectionState(self.multiworld) - state.sweep_for_events(locations=event_locations) + state.sweep_for_advancements(locations=event_locations) num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) diff --git a/worlds/witness/test/test_panel_hunt.py b/worlds/witness/test/test_panel_hunt.py index 2fc16f787e..af6855dc69 100644 --- a/worlds/witness/test/test_panel_hunt.py +++ b/worlds/witness/test/test_panel_hunt.py @@ -23,7 +23,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase): for _ in range(100): state_100.collect(panel_hunt_item, True) - state_100.sweep_for_events([self.world.get_location("Tutorial Gate Open Solved")]) + state_100.sweep_for_advancements([self.world.get_location("Tutorial Gate Open Solved")]) self.assertTrue(self.multiworld.completion_condition[self.player](state_100)) @@ -33,7 +33,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase): for _ in range(99): state_99.collect(panel_hunt_item, True) - state_99.sweep_for_events([self.world.get_location("Tutorial Gate Open Solved")]) + state_99.sweep_for_advancements([self.world.get_location("Tutorial Gate Open Solved")]) self.assertFalse(self.multiworld.completion_condition[self.player](state_99)) From 43cb9611fb28a02f594ed2170281043285ae8101 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 23 Aug 2024 17:05:30 -0700 Subject: [PATCH 08/50] Core: some typing and cleaning in `BaseClasses.py` (#3391) * Core: some typing and cleaning in `BaseClasses.py` * more backwards `__repr__` * double-quote string * remove some end-of-line whitespace --- BaseClasses.py | 91 +++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 61f3f8f67c..29264f34ab 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -11,8 +11,10 @@ from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag -from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \ - TypedDict, Union, Type, ClassVar +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, + Optional, Protocol, Set, Tuple, Union, Type) + +from typing_extensions import NotRequired, TypedDict import NetUtils import Options @@ -22,16 +24,16 @@ if typing.TYPE_CHECKING: from worlds import AutoWorld -class Group(TypedDict, total=False): +class Group(TypedDict): name: str game: str world: "AutoWorld.World" - players: Set[int] - item_pool: Set[str] - replacement_items: Dict[int, Optional[str]] - local_items: Set[str] - non_local_items: Set[str] - link_replacement: bool + players: AbstractSet[int] + item_pool: NotRequired[Set[str]] + replacement_items: NotRequired[Dict[int, Optional[str]]] + local_items: NotRequired[Set[str]] + non_local_items: NotRequired[Set[str]] + link_replacement: NotRequired[bool] class ThreadBarrierProxy: @@ -48,6 +50,11 @@ class ThreadBarrierProxy: "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") +class HasNameAndPlayer(Protocol): + name: str + player: int + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -156,7 +163,7 @@ class MultiWorld(): self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} for player in range(1, players + 1): - def set_player_attr(attr, val): + def set_player_attr(attr: str, val) -> None: self.__dict__.setdefault(attr, {})[player] = val set_player_attr('plando_items', []) set_player_attr('plando_texts', {}) @@ -165,13 +172,13 @@ class MultiWorld(): set_player_attr('completion_condition', lambda state: True) self.worlds = {} self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " - "world's random object instead (usually self.random)") + "world's random object instead (usually self.random)") self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: return self.player_ids + tuple(self.groups) - def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: + def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]: """Create a group with name and return the assigned player ID and group. If a group of this name already exists, the set of players is extended instead of creating a new one.""" from worlds import AutoWorld @@ -195,7 +202,7 @@ class MultiWorld(): return new_id, new_group - def get_player_groups(self, player) -> Set[int]: + def get_player_groups(self, player: int) -> Set[int]: return {group_id for group_id, group in self.groups.items() if player in group["players"]} def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None): @@ -258,7 +265,7 @@ class MultiWorld(): "link_replacement": replacement_prio.index(item_link["link_replacement"]), } - for name, item_link in item_links.items(): + for _name, item_link in item_links.items(): current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups pool = set() local_items = set() @@ -388,7 +395,7 @@ class MultiWorld(): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) - def get_name_string_for_object(self, obj) -> str: + def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' def get_player_name(self, player: int) -> str: @@ -439,7 +446,7 @@ class MultiWorld(): def get_items(self) -> List[Item]: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]: + def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]: if resolve_group_locations: player_groups = self.get_player_groups(player) return [location for location in self.get_locations() if @@ -448,7 +455,7 @@ class MultiWorld(): return [location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player] - def find_item(self, item, player: int) -> Location: + def find_item(self, item: str, player: int) -> Location: return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) @@ -806,7 +813,7 @@ class CollectionState(): if found >= count: return True return False - + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" @@ -821,7 +828,7 @@ class CollectionState(): def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -900,7 +907,7 @@ class Entrance: addresses = None target = None - def __init__(self, player: int, name: str = '', parent: Region = None): + def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None: self.name = name self.parent_region = parent self.player = player @@ -920,9 +927,6 @@ class Entrance: region.entrances.append(self) def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1048,7 +1052,7 @@ class Region: self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -1088,9 +1092,6 @@ class Region: rules[connecting_region] if rules and connecting_region in rules else None) def __repr__(self): - return self.__str__() - - def __str__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' @@ -1109,9 +1110,9 @@ class Location: locked: bool = False show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT - always_allow = staticmethod(lambda state, item: False) + always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) - item_rule = staticmethod(lambda item: True) + item_rule: Callable[[Item], bool] = staticmethod(lambda item: True) item: Optional[Item] = None def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): @@ -1120,11 +1121,15 @@ class Location: self.address = address self.parent_region = parent - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items) - or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) - and self.item_rule(item) - and (not check_access or self.can_reach(state)))) + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + return (( + self.always_allow(state, item) + and item.name not in state.multiworld.worlds[item.player].options.non_local_items + ) or ( + (self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) + and self.item_rule(item) + and (not check_access or self.can_reach(state)) + )) def can_reach(self, state: CollectionState) -> bool: # Region.can_reach is just a cache lookup, so placing it first for faster abort on average @@ -1139,9 +1144,6 @@ class Location: self.locked = True def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1163,7 +1165,7 @@ class Location: @property def native_item(self) -> bool: """Returns True if the item in this location matches game.""" - return self.item and self.item.game == self.game + return self.item is not None and self.item.game == self.game @property def hint_text(self) -> str: @@ -1246,9 +1248,6 @@ class Item: return hash((self.name, self.player)) def __repr__(self) -> str: - return self.__str__() - - def __str__(self) -> str: if self.location and self.location.parent_region and self.location.parent_region.multiworld: return self.location.parent_region.multiworld.get_name_string_for_object(self) return f"{self.name} (Player {self.player})" @@ -1326,9 +1325,9 @@ class Spoiler: # in the second phase, we cull each sphere such that the game is still beatable, # reducing each range of influence to the bare minimum required inside it - restore_later = {} + restore_later: Dict[Location, Item] = {} for num, sphere in reversed(tuple(enumerate(collection_spheres))): - to_delete = set() + to_delete: Set[Location] = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, @@ -1346,7 +1345,7 @@ class Spoiler: sphere -= to_delete # second phase, sphere 0 - removed_precollected = [] + removed_precollected: List[Item] = [] for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) multiworld.precollected_items[item.player].remove(item) @@ -1499,9 +1498,9 @@ class Spoiler: if self.paths: outfile.write('\n\nPaths:\n\n') - path_listings = [] + path_listings: List[str] = [] for location, path in sorted(self.paths.items()): - path_lines = [] + path_lines: List[str] = [] for region, exit in path: if exit is not None: path_lines.append("{} -> {}".format(region, exit)) From 56dbba6a31a8be06d698155c3ef2d419c5b8388c Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:05:42 -0400 Subject: [PATCH 09/50] Celeste 64: Typo #3840 oops --- worlds/celeste64/docs/guide_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/celeste64/docs/guide_en.md b/worlds/celeste64/docs/guide_en.md index 24fea92e35..74ab94b913 100644 --- a/worlds/celeste64/docs/guide_en.md +++ b/worlds/celeste64/docs/guide_en.md @@ -28,7 +28,7 @@ An Example `AP.json` file: ``` { - "Url": "archipelago:12345", + "Url": "archipelago.gg:12345", "SlotName": "Maddy", "Password": "" } From 6efa065867332fc7fa91d3cc4add38c9fff96231 Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:06:08 -0500 Subject: [PATCH 10/50] Kingdom Hearts: Make Ceiling Division Human-Readable #3839 --- worlds/kh1/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py index c8cb71ffd6..e1f72f5b3e 100644 --- a/worlds/kh1/Rules.py +++ b/worlds/kh1/Rules.py @@ -1,5 +1,6 @@ from BaseClasses import CollectionState from worlds.generic.Rules import add_rule +from math import ceil SINGLE_PUPPIES = ["Puppy " + str(i).rjust(2,"0") for i in range(1,100)] TRIPLE_PUPPIES = ["Puppies " + str(3*(i-1)+1).rjust(2, "0") + "-" + str(3*(i-1)+3).rjust(2, "0") for i in range(1,34)] @@ -28,7 +29,7 @@ def has_puppies_all(state: CollectionState, player: int, puppies_required: int) return state.has("All Puppies", player) def has_puppies_triplets(state: CollectionState, player: int, puppies_required: int) -> bool: - return state.has_from_list_unique(TRIPLE_PUPPIES, player, -(puppies_required / -3)) + return state.has_from_list_unique(TRIPLE_PUPPIES, player, ceil(puppies_required / 3)) def has_puppies_individual(state: CollectionState, player: int, puppies_required: int) -> bool: return state.has_from_list_unique(SINGLE_PUPPIES, player, puppies_required) From e61d521ba8df5a1dfd1dda44a4e7c7e9989865a9 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:08:04 +0200 Subject: [PATCH 11/50] The Witness: Shuffle Dog (#3425) * Town Pet the Dog * Add shuffle dog to options presets * I cri evritim * I guess it's as good a time as any * :( * fix the soft conflict * add all the shuffle dog options to some of the unit tests bc why not * Laser Panels are just 'General' now, I'm pretty sure * Could I really call it allsanity? --- worlds/witness/__init__.py | 20 +++++++++++-------- worlds/witness/data/static_locations.py | 2 ++ worlds/witness/data/static_logic.py | 3 +++ worlds/witness/locations.py | 6 +----- worlds/witness/options.py | 14 +++++++++++-- worlds/witness/player_items.py | 2 +- worlds/witness/player_logic.py | 5 ++++- worlds/witness/presets.py | 6 ++++++ .../witness/test/test_roll_other_options.py | 3 +++ 9 files changed, 44 insertions(+), 17 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 02d11373b2..33c63eddbe 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -189,12 +189,13 @@ class WitnessWorld(World): event_locations.append(location_obj) # Place other locked items - dog_puzzle_skip = self.create_item("Puzzle Skip") - self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) - self.own_itempool.append(dog_puzzle_skip) + if self.options.shuffle_dog == "puzzle_skip": + dog_puzzle_skip = self.create_item("Puzzle Skip") + self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) - self.items_placed_early.append("Puzzle Skip") + self.own_itempool.append(dog_puzzle_skip) + self.items_placed_early.append("Puzzle Skip") if self.options.early_symbol_item: # Pick an early item to place on the tutorial gate. @@ -213,7 +214,7 @@ class WitnessWorld(World): self.own_itempool.append(gate_item) self.items_placed_early.append(random_early_item) - # There are some really restrictive settings in The Witness. + # There are some really restrictive options in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. # This is done both to prevent generation failures, but also to make the early game less linear. # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. @@ -221,11 +222,14 @@ class WitnessWorld(World): state = CollectionState(self.multiworld) state.sweep_for_advancements(locations=event_locations) - num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) + num_early_locs = sum( + 1 for loc in self.multiworld.get_reachable_locations(state, self.player) + if loc.address and not loc.item + ) - # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items + # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items - needed_size = 3 + needed_size = 2 needed_size += self.options.puzzle_randomization == "sigma_expert" needed_size += self.options.shuffle_symbols needed_size += self.options.shuffle_doors > 0 diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index d9566080a0..5c5ad554dd 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -104,6 +104,8 @@ GENERAL_LOCATIONS = { "Town RGB House Upstairs Right", "Town RGB House Sound Room Right", + "Town Pet the Dog", + "Windmill Theater Entry Panel", "Theater Exit Left Panel", "Theater Exit Right Panel", diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index b61b0f9d2f..87e1015257 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -147,6 +147,9 @@ class StaticWitnessLogicObj: elif "EP" in entity_name: entity_type = "EP" location_type = "EP" + elif "Pet the Dog" in entity_name: + entity_type = "Event" + location_type = "Good Boi" elif entity_hex.startswith("0xFF"): entity_type = "Event" location_type = None diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index d095b8bed4..49a4437c5a 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -19,7 +19,7 @@ class WitnessPlayerLocations: def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """Defines locations AFTER logic changes due to options""" - self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi"} self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy() if world.options.shuffle_discarded_panels: @@ -53,10 +53,6 @@ class WitnessPlayerLocations: if static_witness_logic.ENTITIES_BY_NAME[ch]["locationType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] - dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] - self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id - self.CHECK_PANELHEX_TO_ID = dict( sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1]) ) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 6f7222d5f9..f91e5218c3 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -129,12 +129,18 @@ class ShuffleEnvironmentalPuzzles(Choice): option_obelisk_sides = 2 -class ShuffleDog(Toggle): +class ShuffleDog(Choice): """ - Adds petting the Town dog into the location pool. + Adds petting the dog statue in Town into the location pool. + Alternatively, you can force it to be a Puzzle Skip. """ display_name = "Pet the Dog" + option_off = 0 + option_puzzle_skip = 1 + option_random_item = 2 + default = 1 + class EnvironmentalPuzzlesDifficulty(Choice): """ @@ -424,6 +430,7 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + shuffle_dog: ShuffleDog witness_option_groups = [ @@ -471,5 +478,8 @@ witness_option_groups = [ ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + ]), + OptionGroup("Silly Options", [ + ShuffleDog, ]) ] diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 44a959f2b4..3e09fe2ddb 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -215,7 +215,7 @@ class WitnessPlayerItems: item = self.item_data[item_name] if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child - # items were removed from the pool when we pruned out all progression items not in the settings. + # items were removed from the pool when we pruned out all progression items not in the options. output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code) for child_item in item.definition.child_item_names] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 3321983dd8..7313d8238c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -609,6 +609,9 @@ class WitnessPlayerLogic: adjustment_linesets_in_order.append(get_complex_doors()) adjustment_linesets_in_order.append(get_complex_additional_panels()) + if not world.options.shuffle_dog: + adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF80 (Town Pet the Dog)"]) + if world.options.shuffle_boat: adjustment_linesets_in_order.append(get_boat()) @@ -890,7 +893,7 @@ class WitnessPlayerLogic: ) def determine_unrequired_entities(self, world: "WitnessWorld") -> None: - """Figure out which major items are actually useless in this world's settings""" + """Figure out which major items are actually useless in this world's options""" # Gather quick references to relevant options eps_shuffled = world.options.shuffle_EPs diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 2a53484a4c..105514c91e 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -37,6 +37,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # For relative beginners who want to move to the next step. @@ -73,6 +75,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # Allsanity but without the BS (no expert, no tedious EPs). @@ -109,5 +113,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = { "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.option_random_item, }, } diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py index 3912ce252e..bea278a042 100644 --- a/worlds/witness/test/test_roll_other_options.py +++ b/worlds/witness/test/test_roll_other_options.py @@ -12,6 +12,7 @@ class TestExpertNonRandomizedEPs(WitnessTestBase): "victory_condition": "challenge", "shuffle_discarded_panels": False, "shuffle_boat": False, + "shuffle_dog": "off", } @@ -24,6 +25,7 @@ class TestVanillaAutoElevatorsPanels(WitnessTestBase): "early_caves": True, "shuffle_vault_boxes": True, "mountain_lasers": 11, + "shuffle_dog": "puzzle_skip", } @@ -46,6 +48,7 @@ class TestMaxEntityShuffle(WitnessTestBase): "obelisk_keys": True, "shuffle_lasers": "anywhere", "victory_condition": "mountain_box_long", + "shuffle_dog": "random_item", } From 35c9061c9cb3fbb12687e7afba73824c27e4ec1e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:08:46 +0200 Subject: [PATCH 12/50] The Witness: Switch to world.player_name (#3693) * lint * player_name * oops lmao * shorten --- worlds/witness/__init__.py | 15 ++++++++------- worlds/witness/hints.py | 13 +++++-------- worlds/witness/player_logic.py | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 33c63eddbe..ee5eba9150 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -128,10 +128,10 @@ class WitnessWorld(World): ) if not has_locally_relevant_progression and self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + warning(f"{self.player_name}'s Witness world doesn't have any progression" f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.") elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough" + raise OptionError(f"{self.player_name}'s Witness world doesn't have enough" f" progression items that can be placed in other players' worlds. Please turn on Symbol" f" Shuffle, Door Shuffle, or Obelisk Keys.") @@ -251,9 +251,10 @@ class WitnessWorld(World): self.player_locations.add_location_late(loc) self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) - player = self.multiworld.get_player_name(self.player) - - warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") + warning( + f"""Location "{loc}" had to be added to {self.player_name}'s world + due to insufficient sphere 1 size.""" + ) def create_items(self) -> None: # Determine pool size. @@ -290,7 +291,7 @@ class WitnessWorld(World): self.multiworld.push_precollected(self.create_item(inventory_item_name)) if len(item_pool) > pool_size: - error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})" + error(f"{self.player_name}'s Witness world has too few locations ({pool_size})" f" to place its necessary items ({len(item_pool)}).") return @@ -300,7 +301,7 @@ class WitnessWorld(World): num_puzzle_skips = self.options.puzzle_skip_amount.value if num_puzzle_skips > remaining_item_slots: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" + warning(f"{self.player_name}'s Witness world has insufficient locations" f" to place all requested puzzle skips.") num_puzzle_skips = remaining_item_slots item_pool["Puzzle Skip"] = num_puzzle_skips diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index c8ddf260d4..cd1d38f6e7 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" + location_name += " (" + world.player_name + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item.player) + ")" + item_name += " (" + world.player_name + ")" hint_text = "" area: Optional[str] = None @@ -388,8 +388,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp while len(hints) < hint_amount: if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Ran out of items/locations to hint for player {player_name}.") + logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.") break location_hint: Optional[WitnessLocationHint] @@ -590,8 +589,7 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount, hunt_panels)) if len(hinted_areas) < amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Was not able to make {amount} area hints for player {player_name}. " + logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. " f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.") return hints, unhinted_locations_per_area @@ -680,8 +678,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, # If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount if len(generated_hints) != hint_amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. " + logging.warning(f"Couldn't generate {hint_amount} hints for player {world.player_name}. " f"Generated {len(generated_hints)} instead.") return generated_hints diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 7313d8238c..b0e330c90c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -774,8 +774,7 @@ class WitnessPlayerLogic: # If we are disabling a laser, something has gone wrong. if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser": laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"] - player_name = world.multiworld.get_player_name(world.player) - raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}." + raise RuntimeError(f"Somehow, {laser_name} was disabled for player {world.player_name}." f" This is not allowed to happen, please report to Violet.") newly_discovered_disabled_entities.add(entity) From 6f617e302d65058fc4602991f465b3db2fbebe44 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 23 Aug 2024 20:09:50 -0400 Subject: [PATCH 13/50] Launcher: Update message that displays when installing a custom apworld for a game in main (#3607) --- worlds/LauncherComponents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 18c1a1661e..d127bbea36 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path break if found_already_loaded: raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n" - "so a Launcher restart is required to use the new installation.") + "so a Launcher restart is required to use the new installation.\n" + "If the Launcher is not open, no action needs to be taken.") world_source = worlds.WorldSource(str(target), is_zip=True) bisect.insort(worlds.world_sources, world_source) world_source.load() From 5c5f2ffc947a6fc2befd09f528694964e44edc9f Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 23 Aug 2024 19:12:01 -0500 Subject: [PATCH 14/50] kvui: assert kivy is not imported before kvui (#3823) --- kvui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kvui.py b/kvui.py index f83590a819..65cf52c7a4 100644 --- a/kvui.py +++ b/kvui.py @@ -5,6 +5,8 @@ import typing import re from collections import deque +assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility" + if sys.platform == "win32": import ctypes From d1a7fd7da118d9470c037f7374445f0f086ede6a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Fri, 23 Aug 2024 17:51:52 -0700 Subject: [PATCH 15/50] Pokemon Emerald: Send current map to trackers (#3726) --- worlds/pokemon_emerald/client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index a830957e9c..eeae3a5248 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -137,6 +137,8 @@ class PokemonEmeraldClient(BizHawkClient): previous_death_link: float ignore_next_death_link: bool + current_map: Optional[int] + def __init__(self) -> None: super().__init__() self.local_checked_locations = set() @@ -150,6 +152,7 @@ class PokemonEmeraldClient(BizHawkClient): self.death_counter = None self.previous_death_link = 0 self.ignore_next_death_link = False + self.current_map = None async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: from CommonClient import logger @@ -243,6 +246,7 @@ class PokemonEmeraldClient(BizHawkClient): sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little") + await self.handle_tracker_info(ctx, guards) await self.handle_death_link(ctx, guards) await self.handle_received_items(ctx, guards) await self.handle_wonder_trade(ctx, guards) @@ -403,6 +407,31 @@ class PokemonEmeraldClient(BizHawkClient): # Exit handler and return to main loop to reconnect pass + async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: + # Current map + sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") + + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(sb1_address + 0x4, 2, "System Bus")], + [guards["SAVE BLOCK 1"]] + ) + if read_result is None: # Save block moved + return + + current_map = int.from_bytes(read_result[0], "big") + if current_map != self.current_map: + self.current_map = current_map + await ctx.send_msgs([{ + "cmd": "Bounce", + "slots": [ctx.slot], + "tags": ["Tracker"], + "data": { + "type": "MapUpdate", + "mapId": current_map, + }, + }]) + async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: """ Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game From 0fcca25870be3ec343d13a3ab7b4e0d63c13b84b Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:41:00 -0500 Subject: [PATCH 16/50] Core: deepcopy plando items #3841 --- Generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index d7dd6523e7..6220c0eb81 100644 --- a/Generate.py +++ b/Generate.py @@ -511,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b continue logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") if PlandoOptions.items in plando_options: - ret.plando_items = game_weights.get("plando_items", []) + ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) if ret.game == "A Link to the Past": roll_alttp_settings(ret, game_weights) From 83367c694602beff5fd84e1160909cc85ce771fb Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 24 Aug 2024 04:53:56 -0400 Subject: [PATCH 17/50] ALttP: Fix accessibility (locations -> full) (#3801) --- worlds/alttp/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index f596749ae6..a664f8ac9b 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -412,7 +412,7 @@ def global_rules(multiworld: MultiWorld, player: int): lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player)) set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) - if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]: + if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]: set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player) set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player), @@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int): location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))) # this seemed to be causing generation failure, disable for now - # if world.accessibility[player] != 'locations': + # if world.accessibility[player] != 'full': # set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) # It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements. From dddffa1660423a379faf3a4808b962679084dc68 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 24 Aug 2024 03:54:33 -0500 Subject: [PATCH 18/50] LTTP: fix own_dungeon setting from not being placed in the player's own world (#3816) --- worlds/alttp/__init__.py | 2 + .../alttp/test/options/test_dungeon_fill.py | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 worlds/alttp/test/options/test_dungeon_fill.py diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3176f7a7fc..3cdbb1cb45 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -356,6 +356,8 @@ class ALTTPWorld(World): self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] if option == "original_dungeon": self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group] + else: + self.options.local_items.value |= self.dungeon_local_item_names self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key] diff --git a/worlds/alttp/test/options/test_dungeon_fill.py b/worlds/alttp/test/options/test_dungeon_fill.py new file mode 100644 index 0000000000..17501b65d8 --- /dev/null +++ b/worlds/alttp/test/options/test_dungeon_fill.py @@ -0,0 +1,60 @@ +from unittest import TestCase + +from BaseClasses import MultiWorld +from test.general import gen_steps, setup_multiworld +from worlds.AutoWorld import call_all +from worlds.generic.Rules import locality_rules +from ... import ALTTPWorld +from ...Options import DungeonItem + + +class DungeonFillTestBase(TestCase): + multiworld: MultiWorld + world_1: ALTTPWorld + world_2: ALTTPWorld + options = ( + "big_key_shuffle", + "small_key_shuffle", + "key_drop_shuffle", + "compass_shuffle", + "map_shuffle", + ) + + def setUp(self): + self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ()) + self.world_1 = self.multiworld.worlds[1] + self.world_2 = self.multiworld.worlds[2] + + def generate_with_options(self, option_value: int): + for option in self.options: + getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value + + for step in gen_steps: + call_all(self.multiworld, step) + # this is where locality rules are set in normal generation which we need to verify this test + if step == "set_rules": + locality_rules(self.multiworld) + + def test_original_dungeons(self): + self.generate_with_options(DungeonItem.option_original_dungeon) + for location in self.multiworld.get_filled_locations(): + with (self.subTest(location=location)): + if location.parent_region.dungeon is None: + self.assertIs(location.item.dungeon, None) + else: + self.assertEqual(location.player, location.item.player, + f"{location.item} does not belong to {location}'s player") + if location.item.dungeon is None: + continue + self.assertIs(location.item.dungeon, location.parent_region.dungeon, + f"{location.item} was not placed in its original dungeon.") + + def test_own_dungeons(self): + self.generate_with_options(DungeonItem.option_own_dungeons) + for location in self.multiworld.get_filled_locations(): + with self.subTest(location=location): + if location.parent_region.dungeon is None: + self.assertIs(location.item.dungeon, None) + else: + self.assertEqual(location.player, location.item.player, + f"{location.item} does not belong to {location}'s player") From e99f027b424eb37949ece2c768d75ddf3b96ae38 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 25 Aug 2024 02:19:42 +1000 Subject: [PATCH 19/50] Muse Dash: Update to 4.7.0 - Let's Rhythm Jam! (#3837) * Update to Muse Dash 4.7.0 Muse Dash - Let's Rhythm Jam! * Add the replaced song to the removed list. * Oops add the other secret song to this list. * Add trailing comma Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- worlds/musedash/MuseDashCollection.py | 2 ++ worlds/musedash/MuseDashData.txt | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 576a106df7..9e8c9214a1 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -46,6 +46,8 @@ class MuseDashCollections: "CHAOS Glitch", "FM 17314 SUGAR RADIO", "Yume Ou Mono Yo Secret", + "Echo over you... Secret", + "Tsukuyomi Ni Naru Replaced", ] album_items: Dict[str, AlbumData] = {} diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 6f48d6af9f..1f1a2a011c 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -553,7 +553,7 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 -Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9| +Tsukuyomi Ni Naru Replaced|74-2|CHUNITHM COURSE MUSE|True|5|7|9| The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 @@ -567,3 +567,16 @@ SUPERHERO|75-0|Novice Rider Pack|False|2|4|7| Highway_Summer|75-1|Novice Rider Pack|True|2|4|6| Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9| Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7| +Echo over you... Secret|0-55|Default Music|False|6|8|10| +Echo over you...|0-56|Default Music|False|1|4|0| +Tsukuyomi Ni Naru|74-6|CHUNITHM COURSE MUSE|True|5|8|10| +disco light|76-0|MUSE RADIO FM105|True|5|7|9| +room light feat.chancylemon|76-1|MUSE RADIO FM105|True|3|5|7| +Invisible|76-2|MUSE RADIO FM105|True|3|5|8| +Christmas Season-LLABB|76-3|MUSE RADIO FM105|True|1|4|7| +Hyouryu|77-0|Let's Rhythm Jam!|False|6|8|10| +The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11 +Hydra|77-2|Let's Rhythm Jam!|False|4|7|11| +Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9| +LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9| +Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|10| From 0fb69dce33be7876c4491b4682f88af5c78fb19b Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 24 Aug 2024 19:08:27 -0700 Subject: [PATCH 20/50] Pokemon Emerald: Fix map update sending to all trackers (#3846) --- worlds/pokemon_emerald/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index eeae3a5248..7f16015a3f 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -425,7 +425,6 @@ class PokemonEmeraldClient(BizHawkClient): await ctx.send_msgs([{ "cmd": "Bounce", "slots": [ctx.slot], - "tags": ["Tracker"], "data": { "type": "MapUpdate", "mapId": current_map, From 906b23088ce19fa0d57c91ceb0eade769bb2cc43 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:31:49 +0200 Subject: [PATCH 21/50] The Witness: Rules Optimisation (#3617) * Attempt at optimizing rules * docstrings * Python 3.8 * Lasers optimisation * Simplify conversion code and make it even faster * mypy * ruff * Neat * Add redirect to the other two modes * Update WitnessLogic.txt * Update WitnessLogicExpert.txt * Update WitnessLogicVanilla.txt * Use NamedTuple * Ruff * mypy thing * Mypy stuff * Move Redirect Event to Desert Region so it has a better name --- worlds/witness/__init__.py | 7 +- worlds/witness/data/WitnessLogic.txt | 3 +- worlds/witness/data/WitnessLogicExpert.txt | 3 +- worlds/witness/data/WitnessLogicVanilla.txt | 3 +- worlds/witness/hints.py | 3 +- worlds/witness/player_items.py | 2 +- worlds/witness/player_logic.py | 23 ++-- worlds/witness/regions.py | 8 +- worlds/witness/rules.py | 138 +++++++++++++------- worlds/witness/test/__init__.py | 7 +- worlds/witness/test/test_panel_hunt.py | 27 ++-- 11 files changed, 139 insertions(+), 85 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ee5eba9150..6229e5ffc9 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -204,8 +204,11 @@ class WitnessWorld(World): ] if early_items: random_early_item = self.random.choice(early_items) - if self.options.puzzle_randomization == "sigma_expert" or self.options.victory_condition == "panel_hunt": - # In Expert, only tag the item as early, rather than forcing it onto the gate. + if ( + self.options.puzzle_randomization == "sigma_expert" + or self.options.victory_condition == "panel_hunt" + ): + # In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate. self.multiworld.local_early_items[self.player][random_early_item] = 1 else: # Force the item onto the tutorial gate check and remove it from our random pool. diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index b7814626ad..fabd142881 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B Door - 0x09FEE (Light Room Entry) - 0x0C339 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True Laser - 0x012FB (Laser) - 0x03608 +159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True 159020 - 0x3351D (Sand Snake EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Triangles 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots Door - 0x00085 (Vault Door) - 0x002A6 diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 1d1d010fde..200138dee1 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B Door - 0x09FEE (Light Room Entry) - 0x0C339 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True Laser - 0x012FB (Laser) - 0x03608 +159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True 159020 - 0x3351D (Sand Snake EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Arrows 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol Door - 0x00085 (Vault Door) - 0x002A6 diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 851031ab72..67a42ba7e4 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B Door - 0x09FEE (Light Room Entry) - 0x0C339 158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True Laser - 0x012FB (Laser) - 0x03608 +159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True 159020 - 0x3351D (Sand Snake EP) - True - True 159030 - 0x0053C (Facade Right EP) - True - True 159031 - 0x00771 (Facade Left EP) - True - True @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Triangles 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares Door - 0x00085 (Vault Door) - 0x002A6 diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index cd1d38f6e7..09c3f0b101 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -712,8 +712,7 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) -> if hint.vague_location_hint and location.player == local_player_number: assert hint.area is not None # A local vague location hint should have an area argument return location.address, "containing_area:" + hint.area - else: - return location.address, location.player # Scouting does not matter for other players (currently) + return location.address, location.player # Scouting does not matter for other players (currently) # Is junk / undefined hint return -1, local_player_number diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 3e09fe2ddb..4142ea5e04 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -42,7 +42,7 @@ class WitnessPlayerItems: player_locations: WitnessPlayerLocations) -> None: """Adds event items after logic changes due to options""" - self._world: "WitnessWorld" = world + self._world: WitnessWorld = world self._multiworld: MultiWorld = world.multiworld self._player_id: int = world.player self._logic: WitnessPlayerLogic = player_logic diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index b0e330c90c..027d1834d9 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -116,18 +116,19 @@ class WitnessPlayerLogic: self.HUNT_ENTITIES: Set[str] = set() self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "+1 Laser (Symmetry Laser)", - "0x012FB": "+1 Laser (Desert Laser)", + "0x00509": "+1 Laser", + "0x012FB": "+1 Laser (Unredirected)", "0x09F98": "Desert Laser Redirection", - "0x01539": "+1 Laser (Quarry Laser)", - "0x181B3": "+1 Laser (Shadows Laser)", - "0x014BB": "+1 Laser (Keep Laser)", - "0x17C65": "+1 Laser (Monastery Laser)", - "0x032F9": "+1 Laser (Town Laser)", - "0x00274": "+1 Laser (Jungle Laser)", - "0x0C2B2": "+1 Laser (Bunker Laser)", - "0x00BF6": "+1 Laser (Swamp Laser)", - "0x028A4": "+1 Laser (Treehouse Laser)", + "0xFFD03": "+1 Laser (Redirected)", + "0x01539": "+1 Laser", + "0x181B3": "+1 Laser", + "0x014BB": "+1 Laser", + "0x17C65": "+1 Laser", + "0x032F9": "+1 Laser", + "0x00274": "+1 Laser", + "0x0C2B2": "+1 Laser", + "0x00BF6": "+1 Laser", + "0x028A4": "+1 Laser", "0x17C34": "Mountain Entry", "0xFFF00": "Bottom Floor Discard Turns On", } diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 6d1f8093af..7ff8c440ad 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -3,7 +3,7 @@ Defines Region for The Witness, assigns locations to them, and connects them with the proper requirements """ from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from BaseClasses import Entrance, Region @@ -38,7 +38,7 @@ class WitnessPlayerRegions: self.created_region_names: Set[str] = set() @staticmethod - def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: + def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]: from .rules import _meets_item_requirements """ @@ -79,7 +79,9 @@ class WitnessPlayerRegions: source_region ) - connection.access_rule = self.make_lambda(final_requirement, world) + rule = self.make_lambda(final_requirement, world) + if rule is not None: + connection.access_rule = rule source_region.exits.append(connection) connection.connect(target_region) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index eecea8f30b..2f3210a214 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -2,7 +2,8 @@ Defines the rules by which locations can be accessed, depending on the items received """ -from typing import TYPE_CHECKING +from collections import Counter +from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union from BaseClasses import CollectionState @@ -15,50 +16,22 @@ from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: from . import WitnessWorld -laser_hexes = [ - "0x028A4", - "0x00274", - "0x032F9", - "0x01539", - "0x181B3", - "0x0C2B2", - "0x00509", - "0x00BF6", - "0x014BB", - "0x012FB", - "0x17C65", -] + +class SimpleItemRepresentation(NamedTuple): + item_name: str + item_count: int -def _can_do_panel_hunt(world: "WitnessWorld") -> CollectionRule: +def _can_do_panel_hunt(world: "WitnessWorld") -> SimpleItemRepresentation: required = world.panel_hunt_required_count - player = world.player - return lambda state: state.has("+1 Panel Hunt", player, required) - - -def _has_laser(laser_hex: str, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: - player = world.player - laser_name = static_witness_logic.ENTITIES_BY_HEX[laser_hex]["checkName"] - - # Workaround for intentional naming inconsistency - if laser_name == "Symmetry Island Laser": - laser_name = "Symmetry Laser" - - if laser_hex == "0x012FB" and redirect_required: - return lambda state: state.has_all([f"+1 Laser ({laser_name})", "Desert Laser Redirection"], player) - - return lambda state: state.has(f"+1 Laser ({laser_name})", player) + return SimpleItemRepresentation("+1 Panel Hunt", required) def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: - laser_lambdas = [] + if redirect_required: + return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Redirected)"], world.player, amount) - for laser_hex in laser_hexes: - has_laser_lambda = _has_laser(laser_hex, world, redirect_required) - - laser_lambdas.append(has_laser_lambda) - - return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount + return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Unredirected)"], world.player, amount) def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: @@ -196,7 +169,13 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> ) -def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic) -> CollectionRule: +def _has_item(item: str, world: "WitnessWorld", + player_logic: WitnessPlayerLogic) -> Union[CollectionRule, SimpleItemRepresentation]: + """ + Convert a single element of a WitnessRule into a CollectionRule, unless it is referring to an item, + in which case we return it as an item-count pair ("SimpleItemRepresentation"). This allows some optimisation later. + """ + assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly." if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: @@ -223,27 +202,90 @@ def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: Witne return lambda state: _can_do_theater_to_tunnels(state, world) prog_item = static_witness_logic.get_parent_progressive_item(item) - return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) + needed_amount = player_logic.MULTI_AMOUNTS[item] + + simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount) + return simple_rule -def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule: +def optimize_requirement_option(requirement_option: List[Union[CollectionRule, SimpleItemRepresentation]])\ + -> List[Union[CollectionRule, SimpleItemRepresentation]]: """ - Checks whether item and panel requirements are met for - a panel + This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. """ - lambda_conversion = [ - [_has_item(item, world, world.player, world.player_logic) for item in subset] + direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)] + if not direct_items: + return requirement_option + + max_per_item: Dict[str, int] = Counter() + for item_rule in direct_items: + max_per_item[item_rule[0]] = max(max_per_item[item_rule[0]], item_rule[1]) + + return [ + rule for rule in requirement_option + if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]]) + ] + + +def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]], + player: int) -> List[CollectionRule]: + """ + Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules. + If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts() + """ + converted_sublist = [] + + for rule in requirement: + if not isinstance(rule, tuple): + converted_sublist.append(rule) + continue + + collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] + item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] + + if len(item_rules) == 0: + item_rules_converted = [] + elif len(item_rules) == 1: + item = item_rules[0][0] + count = item_rules[0][1] + item_rules_converted = [lambda state: state.has(item, player, count)] + else: + item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules} + item_rules_converted = [lambda state: state.has_all_counts(item_counts, player)] + + return collection_rules + item_rules_converted + + +def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]: + """ + Converts a WitnessRule into a CollectionRule. + """ + player = world.player + + if requirements == frozenset({frozenset()}): + return None + + rule_conversion = [ + [_has_item(item, world, world.player_logic) for item in subset] for subset in requirements ] + optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion] + + fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion] + + if len(fully_converted_rules) == 1: + if len(fully_converted_rules[0]) == 1: + return fully_converted_rules[0][0] + return lambda state: all(condition(state) for condition in fully_converted_rules[0]) return lambda state: any( all(condition(state) for condition in sub_requirement) - for sub_requirement in lambda_conversion + for sub_requirement in fully_converted_rules ) -def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule: +def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRule]: """ Lambdas are created in a for loop so values need to be captured """ @@ -268,6 +310,8 @@ def set_rules(world: "WitnessWorld") -> None: entity_hex = associated_entity["entity_hex"] rule = make_lambda(entity_hex, world) + if rule is None: + continue location = world.get_location(location) diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py index d1b90ca47d..4453609ddc 100644 --- a/worlds/witness/test/__init__.py +++ b/worlds/witness/test/__init__.py @@ -1,10 +1,11 @@ -from test.bases import WorldTestBase -from test.general import gen_steps, setup_multiworld -from test.multiworld.test_multiworlds import MultiworldTestBase from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast from BaseClasses import CollectionState, Entrance, Item, Location, Region +from test.bases import WorldTestBase +from test.general import gen_steps, setup_multiworld +from test.multiworld.test_multiworlds import MultiworldTestBase + from .. import WitnessWorld diff --git a/worlds/witness/test/test_panel_hunt.py b/worlds/witness/test/test_panel_hunt.py index af6855dc69..2f8434802b 100644 --- a/worlds/witness/test/test_panel_hunt.py +++ b/worlds/witness/test/test_panel_hunt.py @@ -1,5 +1,6 @@ -from BaseClasses import CollectionState, Item -from worlds.witness.test import WitnessTestBase, WitnessMultiworldTestBase +from BaseClasses import CollectionState + +from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase class TestMaxPanelHuntMinChecks(WitnessTestBase): @@ -13,7 +14,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase): "shuffle_vault_boxes": False, } - def test_correct_panels_were_picked(self): + def test_correct_panels_were_picked(self) -> None: with self.subTest("Check that 100 Hunt Panels were actually picked."): self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100) @@ -63,45 +64,45 @@ class TestPanelHuntPostgame(WitnessMultiworldTestBase): "shuffle_discarded_panels": True, } - def test_panel_hunt_postgame(self): + def test_panel_hunt_postgame(self) -> None: for player_minus_one, options in enumerate(self.options_per_world): player = player_minus_one + 1 postgame_option = options["panel_hunt_postgame"] - with self.subTest(f"Test that \"{postgame_option}\" results in 40 Hunt Panels."): + with self.subTest(f'Test that "{postgame_option}" results in 40 Hunt Panels.'): self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40) # Test that the box gets extra checks from panel_hunt_postgame - with self.subTest("Test that \"everything_is_eligible\" has no Mountaintop Box Hunt Panels."): + with self.subTest('Test that "everything_is_eligible" has no Mountaintop Box Hunt Panels.'): self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False) - with self.subTest("Test that \"disable_mountain_lasers_locations\" has a Hunt Panel for Short, but not Long."): + with self.subTest('Test that "disable_mountain_lasers_locations" has a Hunt Panel for Short, but not Long.'): self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False) self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False) - with self.subTest("Test that \"disable_challenge_lasers_locations\" has a Hunt Panel for Long, but not Short."): + with self.subTest('Test that "disable_challenge_lasers_locations" has a Hunt Panel for Long, but not Short.'): self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False) self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False) - with self.subTest("Test that \"disable_anything_locked_by_lasers\" has both Mountaintop Box Hunt Panels."): + with self.subTest('Test that "disable_anything_locked_by_lasers" has both Mountaintop Box Hunt Panels.'): self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False) self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False) # Check panel_hunt_postgame locations get disabled - with self.subTest("Test that \"everything_is_eligible\" does not disable any locked-by-lasers panels."): + with self.subTest('Test that "everything_is_eligible" does not disable any locked-by-lasers panels.'): self.assert_location_exists("Mountain Floor 1 Right Row 5", 1) self.assert_location_exists("Mountain Bottom Floor Discard", 1) - with self.subTest("Test that \"disable_mountain_lasers_locations\" disables only Shortbox-Locked panels."): + with self.subTest('Test that "disable_mountain_lasers_locations" disables only Shortbox-Locked panels.'): self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2) self.assert_location_exists("Mountain Bottom Floor Discard", 2) - with self.subTest("Test that \"disable_challenge_lasers_locations\" disables only Longbox-Locked panels."): + with self.subTest('Test that "disable_challenge_lasers_locations" disables only Longbox-Locked panels.'): self.assert_location_exists("Mountain Floor 1 Right Row 5", 3) self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3) - with self.subTest("Test that \"everything_is_eligible\" disables only Shortbox-Locked panels."): + with self.subTest('Test that "everything_is_eligible" disables only Shortbox-Locked panels.'): self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4) self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4) From 9a4e84efdc766825dff7fe8a58e545e100f4772f Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Thu, 29 Aug 2024 07:11:02 +0100 Subject: [PATCH 22/50] AHIT: Fix moderate logic rules using add_rule instead of set_rule (#3850) The moderate logic for the Mafia Town Clock Tower Chest and Top of Ruined Tower with nothing, and for clearing Rock the Boat without Ice Hat were mistakenly using `add_rule` instead of `set_rule`, which was adding the condition of `and True` which had no effect. This patch corrects these moderate logic rules to use `set_rule` instead. --- worlds/ahit/Rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b716b793a7..5524802a88 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE), "or") # Moderate: Clock Tower Chest + Ruined Tower with nothing - add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) - add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: @@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"): if world.is_dlc1(): # Moderate: clear Rock the Boat without Ice Hat - add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) - add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) # Moderate: clear Deep Sea without Ice Hat set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), From 701a7faa714c5ea40e531b7025aa56af124de2ab Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Thu, 29 Aug 2024 07:11:42 +0100 Subject: [PATCH 23/50] AHIT: Fix Time Rift - Alpine Skyline entrance logic (#3851) The `Time Rift - Alpine Skyline` region was incorrectly accessible from Alpine Free Roam without Hookshot Badge or Umbrella. One of the two regions that connects to the `Time Rift - Alpine Skyline` region is `Alpine Free Roam`. The problem here is that `Alpine Free Roam` corresponds to the intro section of Alpine Free Roam, but the Time Rift is actually found in-game in what equates to the `Alpine Skyline Area` region. The entrance connecting `Alpine Free Roam` to `Alpine Skyline Area` (`AFR -> Alpine Skyline Area`) requires the Hookshot Badge (and Umbrella if umbrella logic is enabled), but because the entrance to `Time Rift - Alpine Skyline` is placed in `Alpine Free Roam` instead, it was missing the hookshot/umbrella requirements. The missing Hookshot Badge and Umbrella requirements have been added to `Rules.set_rift_rules()` and `Rules.set_default_rift_rules()`. The entrances to the `Time Rift - Curly Tail Trail` and `Time Rift - The Twilight Bell` regions are also in the `Alpine Free Roam` region, but the logic for both of those entrances require event items that are only accessible from the `Alpine Skyline Area` region. --- worlds/ahit/Rules.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 5524802a88..183248a0e6 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): for entrance in regions["Time Rift - Alpine Skyline"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + if entrance.parent_region.name == "Alpine Free Roam": + add_rule(entrance, + lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True)) if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: @@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"): for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + if entrance.parent_region.name == "Alpine Free Roam": + add_rule(entrance, + lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True)) if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: From 97c313c1c4ed7fc889ce7ab074d0312b0f5829f8 Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Thu, 29 Aug 2024 02:12:58 -0400 Subject: [PATCH 24/50] APSudoku: Update setup guide, remove extraneous options page link (#3849) * APSudoku: Update setup guide, remove extraneous options page link * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * clean up instructions * IP -> address --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/apsudoku/__init__.py | 2 +- worlds/apsudoku/docs/setup_en.md | 36 +++++++++++++++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/worlds/apsudoku/__init__.py b/worlds/apsudoku/__init__.py index c6bd02bdc2..04422ddb23 100644 --- a/worlds/apsudoku/__init__.py +++ b/worlds/apsudoku/__init__.py @@ -4,7 +4,7 @@ from BaseClasses import Tutorial from ..AutoWorld import WebWorld, World class AP_SudokuWebWorld(WebWorld): - options_page = "games/Sudoku/info/en" + options_page = False theme = 'partyTime' setup_en = Tutorial( diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md index cf2c755bd8..ef5a87e0b0 100644 --- a/worlds/apsudoku/docs/setup_en.md +++ b/worlds/apsudoku/docs/setup_en.md @@ -1,9 +1,7 @@ # APSudoku Setup Guide ## Required Software -- [APSudoku](https://github.com/EmilyV99/APSudoku) -- Windows (most tested on Win10) -- Other platforms might be able to build from source themselves; and may be included in the future. +- [APSudoku](https://github.com/APSudoku/APSudoku) ## General Concept @@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot ## Installation Procedures -Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file. +Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform. ## Joining a MultiWorld Game -1. Run APSudoku.exe -2. Under the 'Archipelago' tab at the top-right: - - Enter the server url & port number +1. Run the APSudoku executable. +2. Under `Settings` → `Connection` at the top-right: + - Enter the server address and port number - Enter the name of the slot you wish to connect to - Enter the room password (optional) - Select DeathLink related settings (optional) - - Press connect -3. Go back to the 'Sudoku' tab - - Click the various '?' buttons for information on how to play / control -4. Choose puzzle difficulty -5. Try to solve the Sudoku. Click 'Check' when done. + - Press `Connect` +4. Under the `Sudoku` tab + - Choose puzzle difficulty + - Click `Start` to generate a puzzle +5. Try to solve the Sudoku. Click `Check` when done + - A correct solution rewards you with 1 hint for a location in the world you are connected to + - An incorrect solution has no penalty, unless DeathLink is enabled (see below) +Info: +- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`. +- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features +- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md) +- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted) +- Click the various `?` buttons for information on controls/how to play ## DeathLink Support -If 'DeathLink' is enabled when you click 'Connect': -- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting). -- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. +If `DeathLink` is enabled when you click `Connect`: +- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting). +- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. - On receiving a DeathLink from another player, your puzzle resets. From ab5b986716dcb2c0f032048ed289eef9ea8aee72 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 28 Aug 2024 23:14:08 -0700 Subject: [PATCH 25/50] Pokemon Emerald: Move magma grunt (#3836) --- worlds/pokemon_emerald/rom.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 75d7d57584..2c0b5021d0 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -114,6 +114,14 @@ class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin): def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None: + # TODO: Remove when the base patch is updated to include this change + # Moves an NPC to avoid overlapping people during trainersanity + patch.write_token( + APTokenTypes.WRITE, + 0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord + struct.pack(" Date: Thu, 29 Aug 2024 02:15:49 -0400 Subject: [PATCH 26/50] LADX: Filter braces out of player names for hint text (#3831) * Filter braces out of player names for hint text * Filter out another spot --- worlds/ladx/LADXR/generator.py | 6 +++++- worlds/ladx/LADXR/locations/shop.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index e6f608a921..69e856f354 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -280,6 +280,8 @@ def generateRom(args, world: "LinksAwakeningWorld"): name = "Your" else: name = f"{world.multiworld.player_name[location.item.player]}'s" + # filter out { and } since they cause issues with string.format later on + name = name.replace("{", "").replace("}", "") if isinstance(location, LinksAwakeningLocation): location_name = location.ladxr_item.metadata.name @@ -288,7 +290,9 @@ def generateRom(args, world: "LinksAwakeningWorld"): hint = f"{name} {location.item} is at {location_name}" if location.player != world.player: - hint += f" in {world.multiworld.player_name[location.player]}'s world" + # filter out { and } since they cause issues with string.format later on + player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "") + hint += f" in {player_name}'s world" # Cap hint size at 85 # Realistically we could go bigger but let's be safe instead diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py index b68726665f..bee053716a 100644 --- a/worlds/ladx/LADXR/locations/shop.py +++ b/worlds/ladx/LADXR/locations/shop.py @@ -18,7 +18,8 @@ class ShopItem(ItemInfo): mw_text = "" if multiworld: mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}" - + # filter out { and } since they cause issues with string.format later on + mw_text = mw_text.replace("{", "").replace("}", "") if self.custom_item_name: name = self.custom_item_name From d52827ebd2d86f05677d8d61c6dbd9ddd0cb0670 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 29 Aug 2024 09:41:57 +0300 Subject: [PATCH 27/50] Stardew Valley: Fix Crimsonfish region (#3687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - Add Unit test for all the fish that require a specific region to be reachable * - Move the crimsonfish to the tide pools region * - Improved the unit test to be more thorough, add extended family fish to the test * - Moved the son of crimsonfish to the correct region as well * FFMQ: Fix reset protection (#3710) * Revert reset protection * Fix reset protection --------- Co-authored-by: alchav * - Take shipsanity moss out of shipsanity crops (#3709) * sc2: Removing unused dependency in requirements.txt (#3697) * sc2: Removing unused dependency in requirements.txt * sc2: Add missing newline in requirements.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * WebHost: Fix NamedRange values clamping to the range (#3613) If a NamedRange has a `special_range_names` entry outside the `range_start` and `range_end`, the HTML5 range input will clamp the submitted value to the closest value in the range. These means that, for example, Pokemon RB's "HM Compatibility" option's "Vanilla (-1)" option would instead get posted as "0" rather than "-1". This change updates NamedRange to behave like TextChoice, where the select element has a `name` attribute matching the option, and there is an additional element to be able to provide an option other than the select element's choices. This uses a different suffix of `-range` rather than `-custom` that TextChoice uses. The reason is we need some way to decide whether to use the custom value or the select value, and that method needs to work without JavaScript. For TextChoice this is easy, if the custom field is empty use the select element. For NamedRange this is more difficult as the browser will always submit *something*. My choice was to only use the value from the range if the select box is set to "custom". Since this only happens with JS as "custom' is hidden, I made the range hidden under no-JS. If it's preferred, I could make the select box hidden instead. Let me know. This PR also makes the `js-required` class set `display: none` with `!important` as otherwise the class wouldn't work on any rule that had `display: flex` with more specificity than a single class. * Timespinner: migrate to new options api and correct random (#2485) * Implemented new options system into Timespinner * Fixed typo * Fixed typo * Fixed slotdata maybe * Fixes * more fixes * Fixed failing unit tests * Implemented options backwards comnpatibility * Fixed option fallbacks * Implemented review results * Fixed logic bug * Fixed python 3.8/3.9 compatibility * Replaced one more multiworld option usage * Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Core: migrate item links out of main (#2914) * Core: move item linking out of main * add a test that item link option correctly validates * remove unused fluff --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Core: Rework accessibility (#1481) * rename locations accessibility to "full" and make old locations accessibility debug only * fix a bug in oot * reorder lttp tests to not override its overrides * changed the wrong word in the dict * :forehead: * update the manual lttp yaml * use __debug__ * update pokemon and messenger * fix conflicts from 993 * fix stardew presets * add that locations may be inaccessible to description * use reST format and make the items description one line so that it renders correctly on webhost * forgot i renamed that * add aliases for back compat * some cleanup * fix imports * fix test failure * only check "items" players when the item is progression * Revert "only check "items" players when the item is progression" This reverts commit ecbf986145e6194aa99a39c481d8ecd0736d5a4c. * remove some unnecessary diffs * CV64: Add ItemsAccessibility * put items description at the bottom of the docstring since that's it's visual order * : * rename accessibility reference in pokemon rb dexsanity * make the rendered tooltips look nicer * Shivers: New features and removes two missed options using the old options API (#3287) * Adds an option to have pot pieces placed local/non-local/anywhere Shivers nearly always finishes last in multiworld games due to the fact you need all 20 pot pieces to win and the pot pieces open very few location checks. This option allows the pieces to be placed locally. This should allow Shivers to be finished earlier. * New option: Choose how many ixupi captures are needed for goal completion New option: Choose how many ixupi captures are needed for goal completion * Fixes rule logic for location 'puzzle solved three floor elevator' Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. * Merge branch 'main' of https://github.com/GodlFire/Shivers * Revert "Merge branch 'main' of https://github.com/GodlFire/Shivers" This reverts commit bb08c3f0c2ef148fd24d7c7820cdfe936f7196e2. * Fixes issue with office elevator rule logic. * Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' * Moves plaque location to front for better tracker referencing. * Tiki should be Shaman. * Hanging should be Gallows. * Merrick spelling. * Clarity change. * Changes new option to use new option API Changes new option to use new option API * Added sub regions for Ixupi -Added sub regions for Ixupi and moved ixupi capture checks into the sub region. -Added missing wax capture possible spot in Shaman room * Adds option for ixupi captures to be priority locations Adds option for ixupi captures to be priority locations * Consistency Consistency * Changes ixupi captures priority to default on toggle Changes ixupi captures priority to default on toggle * Docs update -Updated link to randomizer -Update some text to reflect the latest functionality -Replaced 'setting' with 'option' * New features/bug fixes -Adds an option to have completed pots in the item pool -Moved subterranean world information plaque to maze staircase * Cleanup Cleanup * Fixed name for moved location When moving a location and renaming it I forgot to fix the name in a second spot. * Squashed commit of the following: commit 630a3bdfb9414d8c57154f29253fce0cf67b6436 Merge: 8477d3c8 5e579200 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Mon Apr 1 19:08:48 2024 -0600 Merge pull request #10 from ArchipelagoMW/main Merge main into branch commit 5e5792009cd3089ae61c5fdd208de1b79d183cb4 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 12:08:21 2024 -0500 LttP: delete playerSettings.yaml (#3062) commit 9aeeeb077a9e894cd2ace51b58d537bcf7607d5b Author: CaitSith2 Date: Mon Apr 1 06:07:56 2024 -0700 ALttP: Re-mark light/dark world regions after applying plando connections (#2964) commit 35458380e6e08eab85203942b6415fd964907c84 Author: Bryce Wilson Date: Mon Apr 1 07:07:11 2024 -0600 Pokemon Emerald: Fix wonder trade race condition (#2983) commit 4ac1866689d01dc6693866ee8b1236ad6fea114b Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 08:06:31 2024 -0500 ALTTP: Skull Woods Inverted fix (#2980) commit 4aa03da66e1a8c99fc31c163c1a23fb0bd772c15 Author: Fabian Dill Date: Mon Apr 1 15:06:02 2024 +0200 Factorio: fix attempting to create savegame with not filename safe characters (#2842) commit 24a03bc8b6b406c0925eedf415dcef47e17fdbaa Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon Apr 1 08:02:26 2024 -0500 KDL3: fix shuffled animals not actually being random (#3060) commit f813a7005fadb1c56bb93fee6147b63d9df2b720 Author: Aaron Wagener Date: Sun Mar 31 11:11:10 2024 -0500 The Messenger: update docs formatting and fix outdated info (#3033) * The Messenger: update docs formatting and fix outdated info * address review feedback * 120 chars commit 2a0b7e0def5c00cc2ac273b22581b3cde3b6f6a6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Sun Mar 31 09:55:55 2024 -0600 CV64: A couple of very small docs corrections. (#3057) commit 03d47e460e434b897b313c2ba452d785ecbacebe Author: Ixrec Date: Sun Mar 31 16:55:08 2024 +0100 A Short Hike: Clarify installation instructions (#3058) * Clarify installation instructions * don't mention 'config' folder since it isn't created until the game starts commit e546c0f7ff2456ddb919a1b65a437a1c61b07479 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun Mar 31 10:50:31 2024 -0500 Yoshi's Island: add patch suffix (#3061) commit 2ec93ba82a969865a8addc98feb076898978c8e3 Author: Bryce Wilson Date: Sun Mar 31 09:48:59 2024 -0600 Pokemon Emerald: Fix inconsistent location name (#3065) commit 4e3d3963941934c77573e6e0b699edf9e26cd647 Author: Aaron Wagener Date: Sun Mar 31 10:47:11 2024 -0500 The Messenger: Fix precollected notes not being removed from the itempool (#3066) * The Messenger: fix precollected notes not being properly removed from pool * The Messenger: bump required client version commit 72c53513f8bdab5506ffa972c1bf6f8573f097d7 Author: Fabian Dill Date: Sun Mar 31 03:57:59 2024 +0200 WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063) commit b7ac6a4cbd54d5f8e6672e4a6c6ea708e7e6d4de Author: Aaron Wagener Date: Fri Mar 29 20:14:53 2024 -0500 The Messenger: Fix various portal shuffle issues (#2976) * put constants in a bit more sensical order * fix accidental incorrect scoping * fix plando rules not being respected * add docstrings for the plando functions * fix the portal output pools being overwritten * use shuffle and pop instead of removing by content so plando can go to the same area twice * move portal pool rebuilding outside mapping creation * remove plando_connection cleansing since it isn't shared with transition shuffle commit 5f0112e78365d19f04e22af92d6ad1f52d264b1f Author: Zach Parks Date: Fri Mar 29 19:13:51 2024 -0500 Tracker: Add starting inventory to trackers and received items table. (#3051) commit bb481256de2a511d3b114f164061d440026be4c4 Author: Aaron Wagener Date: Thu Mar 28 21:48:40 2024 -0500 Core: Make fill failure error more human parseable (#3023) commit 301d9de9758e360ccec5399f3f9d922f1c034e45 Author: Aaron Wagener Date: Thu Mar 28 19:31:59 2024 -0500 Docs: adding games rework (#2892) * Docs: complete adding games.md rework * remove all the now unused images * review changes * address medic's review * address more comments commit 9dc708978bd00890afcd3426f829a5ac53cbe136 Author: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Thu Mar 28 18:26:58 2024 -0600 Hylics 2: Fix invalid multiworld data, use `self.random` instead of `self.multiworld.random` (#3001) * Hylics 2: Fixes * Rewrite loop commit 4391d1f4c13cdf2295481d8c51f9ef8f58bf8347 Author: Bryce Wilson Date: Thu Mar 28 18:05:39 2024 -0600 Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025) commit 5d9d4ed9f1e44309f1b53f12413ad260f1b6c983 Author: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri Mar 29 01:01:31 2024 +0100 SoE: update to pyevermizer v0.48.0 (#3050) commit c97215e0e755224593fdd00894731b59aa415e19 Author: Scipio Wright Date: Thu Mar 28 17:23:37 2024 -0400 TUNIC: Minor refactor of the vanilla_portals function (#3009) * Remove unused, change an if to an elif * Remove unused import commit eb66886a908ad75bbe71fac9bb81a0177e05e816 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu Mar 28 16:23:01 2024 -0500 SC2: Don't Filter Excluded Victory Locations (#3018) commit de860623d17d274289e3e4ab13650f2382e2e0b8 Author: Fabian Dill Date: Thu Mar 28 22:21:56 2024 +0100 Core: differentiate between unknown worlds and broken worlds in error message (#2903) commit 74b2bf51613a968eb57a5b138a7ad191324b2dd8 Author: Bryce Wilson Date: Thu Mar 28 15:20:55 2024 -0600 Pokemon Emerald: Exclude norman trainer location during norman goal (#3038) commit 74ac66b03228988d0885cff556f962a04873cc54 Author: BadMagic100 Date: Thu Mar 28 08:49:19 2024 -0700 Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 80d7ac416493a540548aad67981202a1483b5e53 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu Mar 28 09:41:32 2024 -0500 KDL3: RC1 Fixes and Enhancement (#3022) * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs commit 77311719fa0fa5b67fe92f437c3cfed16bd5136f Author: Ziktofel Date: Thu Mar 28 15:38:34 2024 +0100 SC2: Fix HERC upgrades (#3044) commit cfc1541be9e92f1f59b21f4a81f96fc88f4d9f7e Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 28 15:19:32 2024 +0100 Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 4d954afd9b2311248083fc389ac737995985be86 Author: Scipio Wright Date: Thu Mar 28 10:11:20 2024 -0400 TUNIC: Add link to AP plando guide to connection plando section of game page (#2993) commit 17748a4bf1cfd5cc11c6596a09ffc1f01434340f Author: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Thu Mar 28 10:00:10 2024 -0400 Launcher, Docs: Update UI and Set-Up Guide to Reference Options (#2950) commit 9182fe563fc18ed4ccaa8370cfed88407140398e Author: Entropynines <163603868+Entropynines@users.noreply.github.com> Date: Thu Mar 28 06:56:35 2024 -0700 README: Remove outdated information about launchers (#2966) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit bcf223081facd030aa706dc7430a72bcf2fdadc9 Author: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu Mar 28 09:54:56 2024 -0400 TLOZ: Fix markdown issue with game info page (#2985) commit fa93488f3fceac6c2f51851766543cab3ba121e6 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 28 09:46:00 2024 -0400 Docs: Consistent naming for "connection plando" (#2994) commit db15dd4bde442aad99048224bdb0d7dc28c26717 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Thu Mar 28 08:45:19 2024 -0500 A Short Hike: Fix incorrect info in docs (#3016) commit 01cdb0d761a82349afaeb7222b4b59cb1766f4a0 Author: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu Mar 28 09:44:23 2024 -0400 SMW: Update World Doc for v2.0 Features (#3034) Co-authored-by: Scipio Wright commit d0ac2b744eac438570e6a2333e76fa212be66534 Author: panicbit Date: Thu Mar 28 10:11:26 2024 +0100 LADX: fix local and non-local instrument placement (#2987) * LADX: fix local and non-local instrument placement * change confusing variable name commit 14f5f0127eb753eaf0431a54bebc82f5e74a1cb9 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:42:35 2024 -0400 Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002) * fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) * add test to ensure fishing rod need fish shop * fishing rod should be indexed from 0 like a mentally sane person would do. * fishing rod 0 isn't real, but it definitely can hurt you. * reeeeeeeee commit cf133dde7275e171d388fb466b9ed719ab7ed7c8 Author: Bryce Wilson Date: Thu Mar 28 02:32:27 2024 -0600 Pokemon Emerald: Fix typo (#3020) commit ca1812181106a3645e7f7af417590024b377b25e Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:27:49 2024 -0400 Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970) commit 1d4512590e0b78355e5c10174a9c6749e1098a72 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed Mar 27 21:09:09 2024 +0100 requirements.txt: _ instead of - to make PyCharm happy (#3043) commit f7b415dab00338443b68eba51f42614fc40b9152 Author: agilbert1412 Date: Tue Mar 26 19:40:58 2024 +0300 Stardew valley: Game version documentation (#2990) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 702f006c848c05b847e85f7dbedeef68b70cdcc6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Tue Mar 26 07:31:36 2024 -0600 CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015) commit 98ce8f8844fd0c62214a5774609382cf6a6bc829 Author: Yussur Mustafa Oraji Date: Tue Mar 26 14:29:25 2024 +0100 sm64ex: New Options API and WebHost fix (#2979) commit ea47b90367b4a220c346d8057f3aeb4207d226a1 Author: Scipio Wright Date: Tue Mar 26 09:25:41 2024 -0400 TUNIC: You can grapple down here without the ladder, neat (#3019) commit bf3856866c5ea385d0ac58014c71addfdc92637e Author: agilbert1412 Date: Sun Mar 24 23:53:49 2024 +0300 Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014) commit c0368ae0d48b4b2807c5238aeb7b14937282fc3e Author: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun Mar 24 13:53:20 2024 -0700 SC2: Fixed missing upgrade from custom tracker (#3013) commit 36c83073ad8c2ae1912d390ee3976ba0e2eb3f4a Author: Salzkorn Date: Sun Mar 24 21:52:41 2024 +0100 SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992) commit 2b24539ea5b387a3b62063c8177c373e2e3f8389 Author: Ziktofel Date: Sun Mar 24 21:52:16 2024 +0100 SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986) commit 7e904a1c78c91fb502706fe030a1f1765f734de4 Author: Ziktofel Date: Sun Mar 24 21:51:46 2024 +0100 SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978) commit bdd498db2321417374d572bff8beede083fef2b2 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri Mar 22 15:36:27 2024 -0500 ALTTP: Fix #2290's crashes (#2973) commit 355223b8f0af1ee729ffa8b53eb717aa5bf283a4 Author: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Fri Mar 22 15:35:00 2024 -0500 Yoshi's Island: Implement New Game (#2141) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit aaa3472d5d8d8a7a710bd38386d9eb34046a5578 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri Mar 22 21:30:51 2024 +0100 The Witness: Fix seed bleed issue (#3008) commit 96d93c1ae313bb031e983c0d40d8be199b302df1 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Fri Mar 22 15:30:23 2024 -0500 A Short Hike: Add option to customize filler coin count (#3004) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit ca549df20a0a07c30ee2e1bbc2498492b919604d Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 15:29:24 2024 -0500 CommonClient: fix hint tab overlapping (#2957) Co-authored-by: Remy Jette commit 44988d430dc7d91eaeac7aad681dc024bc19ccce Author: Star Rauchenberger Date: Fri Mar 22 15:28:41 2024 -0500 Lingo: Add trap weights option (#2837) commit 11b32f17abebc08a6140506a375179f8a46bcfe6 Author: Danaël V <104455676+ReverM@users.noreply.github.com> Date: Fri Mar 22 12:46:14 2024 -0400 Docs: replacing "setting" to "option" in world docs (#2622) * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md Added non-AP World specific information * Update contributing.md Fixed broken link * Some minor touchups * Update Contributing.md Draft for version with picture * Update contributing.md Small word change * Minor updates for conciseness, mostly * Changed all instances of settings to options in info and setup guides I combed through all world docs and swapped "setting" to "option" when this was refering to yaml options. I also changed a leftover "setting" in option.py * Update contributing.md * Update contributing.md * Update setup_en.md Woops I forgot one * Update Options.py Reverted changes regarding options.py * Update worlds/noita/docs/en_Noita.md Co-authored-by: Scipio Wright * Update worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md revert change waiting for that page to be updated * Update worlds/witness/docs/setup_en.md * Update worlds/witness/docs/en_The Witness.md * Update worlds/soe/docs/multiworld_en.md Fixed Typo Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/witness/docs/en_The Witness.md * Update worlds/adventure/docs/en_Adventure.md * Update worlds/witness/docs/setup_en.md * Updated Stardew valley to hopefully get rid of the merge conflicts * Didn't work :dismay: * Delete worlds/sc2wol/docs/setup_en.md I think this will fix the merge issue * Now it should work * Woops --------- Co-authored-by: Scipio Wright Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 218cd45844f9d733618af9088941156cd79b80bc Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 03:02:38 2024 -0500 APProcedurePatch: fix RLE/COPY incorrect sizing (#3006) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead * Update Files.py --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 4196bde597cdbb6186ff614294fd54ff043a0c99 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 21 16:38:36 2024 -0400 Docs: Fixing special_range_names example (#3005) commit 40f843f54d5970302caeb2a21b76a4845cf5c0ed Author: Star Rauchenberger Date: Thu Mar 21 11:00:53 2024 -0500 Lingo: Minor game data fixes (#3003) commit da333fbb0c88feedd4821a7bade3f56028a02111 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Thu Mar 21 09:52:16 2024 -0600 Shivers: Adds missing logic rule for skull dial door location (#2997) commit 43084da23c719133fcae672e20c9b046e6ef8067 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 21 16:51:29 2024 +0100 The Witness: Fix newlines in Witness option tooltips (#2971) commit 14816743fca366b52422ccb19add59d4960f17a3 Author: Scipio Wright Date: Thu Mar 21 11:50:07 2024 -0400 TUNIC: Shuffle Ladders option (#2919) commit 30a0aa2c85a7015e2072b5781ed1078965f62f4b Author: Star Rauchenberger Date: Thu Mar 21 10:46:53 2024 -0500 Lingo: Add item/location groups (#2789) commit f4b7c28a33bb163768871616023a8cf3879840b4 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed Mar 20 17:45:32 2024 -0500 APProcedurePatch: hotfix changing class variables to instance variables (#2996) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 12864f7b24028fa56135e599f0fe1642c9d2d377 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Wed Mar 20 22:44:09 2024 +0100 A Short Hike: Implement New Game (#2577) commit db02e9d2aabc0f4c1302ac761b3f5547ef00c7c5 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Wed Mar 20 15:03:25 2024 -0600 Castlevania 64: Implement New Game (#2472) commit 32315776ac0ac1a714eb9d58688c479e2038c658 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Wed Mar 20 16:57:45 2024 -0400 Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967) commit e9620bea777ff1008a09c24a70bf523c94f22c29 Author: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Wed Mar 20 16:56:00 2024 -0400 SM64: Goal Logic and Hint Bugfixes (#2886) commit 183ca35bbaf6c805fdb53396d21d0cba34f9cc5e Author: qwint Date: Wed Mar 20 08:39:37 2024 -0500 CommonClient: Port Casting Bug (#2975) commit fcaaa197a19a3be03965c504ca78dd2c21ce1f84 Author: TheLX5 Date: Wed Mar 20 05:56:19 2024 -0700 SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981) commit 8f7b63a787a0ef05625ae2fad1768251aced0c87 Author: TheLX5 Date: Wed Mar 20 05:56:04 2024 -0700 SMW: Blocksanity logic fixes (#2988) commit 6f64bb98693556ac2635791381cc9651c365b324 Author: Scipio Wright Date: Wed Mar 20 08:46:31 2024 -0400 Noita: Remove newline from option description so it doesn't look bad on webhost (#2969) commit d0a9d0e2d1df641668f4f806b45f9577e69229f6 Author: Bryce Wilson Date: Wed Mar 20 06:43:13 2024 -0600 Pokemon Emerald: Bump required client version (#2963) commit 94650a02de62956eee8e7e41f61e8a41506b5842 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue Mar 19 17:08:29 2024 -0500 Core: implement APProcedurePatch and APTokenMixin (#2536) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * ensure returning bytes, add token type checking * Apply suggestions from code review Co-authored-by: Doug Hoskisson * pep8 --------- Co-authored-by: beauxq Co-authored-by: Doug Hoskisson * Changes pot_completed_list to a instance variable instead of global. Changes pot_completed_list to a instance variable instead of global. The global variable was unintentional and was causing missmatch in pre_fill which would cause generation error. * Removing deprecated options getter * Adds back fix from main branch Adds back fix from main branch * Removing messenger changes that somehow got on my branch? Removing messenger changes that somehow got on my branch? * Removing messenger changes that are somehow on the Shivers branch Removing messenger changes that are somehow on the Shivers branch * Still trying to remove Messenger changes on Shivers branch Still trying to remove Messenger changes on Shivers branch * Review comments addressed. Early lobby access set as default. Review comments addressed. Early lobby access set as default. * Review comments addressed Review comments addressed * Review comments addressed. Option for priority locations removed. Option to have ixupi captures a priority has been removed and can be added again if Priority Fill is changed. See Issues #3467. * Minor Change Minor Change * Fixed ID 10 T Error Fixed ID 10 T Error * Front door option added to slot data Front door option added to slot data * Add missing .value on slot data Add missing .value on slot data * Small change to slot data Small change to slot data * Small change to slot data Why didn't this change get pushed github... * Forgot list Forgot list --------- Co-authored-by: Kory Dondzila Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Bomb Rush Cyberfunk: Fix Coil quest being in glitched logic too early (#3720) * Update Rules.py * Update Rules.py * Options: Always verify keys for VerifyKeys options (#3280) * Options: Always verify keys for VerifyKeys options * fix PlandoTexts * use OptionError and give a slightly better error message for which option it is * add the player name to the error * don't create an unnecessary list --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Docs: Add FFMQ French Setup Guide + Minor fixes to English Guide (#3590) * Add docs * Fix character * Configuration Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * ajuster Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * inclure Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * doublon Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * remplissage Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * autre Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * pouvoir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * mappemonde Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * virgule Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * fournir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * snes9x Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 3 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * options Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * lien Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * de laquelle Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Étape de génération Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 4 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * également Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * adresse Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Connect Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * seed Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Changer fichier yaml pour de configuration * Fix capitalization Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix capitalization 2 Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix typo+Add link to fr/en info page --------- Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Spire: Convert options, clean up random calls, and add DeathLink (#3704) * Convert StS options * probably a bad idea * Update worlds/spire/Options.py Co-authored-by: Scipio Wright --------- Co-authored-by: Kono Tyran Co-authored-by: Scipio Wright * Core: fix missing import for `MultiWorld.link_items()` (#3731) * Pokemon R/B: Removing Floats from NamedRange #3717 * Docs: Missed Full Accessibility mention/conversion #3734 * ChecksFinder: Refactor/Cleaning (#3725) * Update ChecksFinder * minor cleanup * Check for compatible name * Enable APWorld * Update setup_en.md * Update en_ChecksFinder.md * The client is getting updated instead * Qwint suggestions, ' -> ", streamline fill_slot_data * Oops, too many refactors --------- Co-authored-by: SunCat * OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * TUNIC: Fix missing traversal req #3740 * TUNIC: Sort entrances in the spoiler log (#3733) * Sort entrances in spoiler log * Rearrange portal list to closer match the vanilla game order, for better spoiler and because I already did this mod-side * Add break (thanks vi) * KH2: Update the docs to support steam in the setup guide (#3711) * doc updates * add steam link * Update worlds/kh2/docs/setup_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update setup_en.md * Forgot to include these * Consistent styling * :) * version 3.3.0 --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * RoR2: Remove recursion from explore mode access rules (#3681) The access rules for " Chest n", " Shrine n" etc. locations recursively called state.can_reach() for the n-1 location name, with the n=1 location being the only location to have the actual access rule set. This patch removes the recursion, instead setting the actual access rule directly on each location, increasing the performance of checking accessibility of n>1 locations. Risk of Rain 2 was already quite fast to generate despite the recursion in the access rules, but with this patch, generating a multiworld with 200 copies of the template RoR2 yaml (and progression balancing disabled through a meta.yaml) goes from about 18s to about 6s for me. From generating the same seed before and after this patch, the same result is produced. * Aquaria: Logic bug fixes (#3679) * Fixing logic bugs * Require energy attack in the cathedral and energy form in the body * King Jelly can be beaten easily with only the Dual Form * I think that I have a problem with my left and right... * There is a monster that is blocking the path, soo need attack to pass * The Li cage is not accessible without the Sunken city boss * Removing useless space. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Two more minors logic modification * Adapting tests to af9b6cd * Reformat the Region file --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * HK: add grub hunt goal (#3203) * makes grub hunt goal option that calculates the total available grubs (including item link replacements) and requires all of them to be gathered for goal completion * update slot data name for grub count * add option to set number needed for grub hub * updates to grub hunt goal based on review * copy/paste fix * account for 'any' goal and fix overriding non-grub goals * making sure godhome is in logic for any and removing redundancy on completion condition * fix typing * i hate typing * move to stage_pre_fill * modify "any" goal so all goals are in logic under minimal settings * rewrite grub counting to create lookups for grubs and groups that can be reused * use generator instead of list comprehension * fix whitespace merging wrong * minor code cleanup * DS3: Version 3.0.0 (#3128) * Update worlds/dark_souls_3/Locations.py Co-authored-by: Scipio Wright * Fix Covetous Silver Serpent Ring location * Update location groups This should cover pretty much all of the seriously hidden items. It also splits out miniboss drops, mimic drops, and hostile NPC drops. * Remove the "Guarded by Keys" group On reflection, I don't think this is actually that useful. It'll also get a lot muddier once we can randomize shops and ashes become pseudo-"keys". * Restore Knight Slayer's Ring classification * Support infusions/upgrades in the new DS3 mod system * Support random starting loadouts * Make an item's NPC status orthogonal to its category * Track location groups with flags * Track Archipelago/Offline mismatches on the server Also fix a few incorrect item names. * Add additional locations that are now randomizable * Don't put soul and multiple items in shops * Add an option to enable whether NG+ items/locations are included * Clean up useful item categorization There are so many weapons in the game now, it doesn't make sense to treat them all as useful * Add more variety to filler items * Iron out a few bugs and incompatibilities * Fix more silly bugs * Get tests passing * Update options to cover new item types Also recategorize some items. * Verify the default values of `Option`s. Since `Option.verify()` can handle normalization of option names, this allows options to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default. This also makes it easier to catch errors if a world author accidentally sets an invalid default. * Make a few more improvements and fixes * Randomize Path of the Dragon * Mark items that unlock checks as useful These items all unlock missable checks, but they're still good to ahve in the game for variety's sake. * Guarantee more NPC quests are completable * Fix a syntax error * Fix rule definition * Support enemy randomization * Support online Yhorm randomization * Remove a completed TODO * Fix tests * Fix force_unique * Add an option to smooth out upgrade item progression * Add helpers for setting location/entrance rules * Support smoother soul item progression * Fill extra smoothing items into conditional locations as well as other worlds * Add health item smoothing * Handle infusions at item generation time * Handle item upgrades at genreation time * Fix Grave Warden's Ashes * Don't overwrite old rules * Randomize items based on spheres instead of DS3 locations * Add a smoothing option for weapon upgrades * Add rules for crow trades * Small fixes * Fix a few more bugs * Fix more bugs * Try to prevent Path of the Dragon from going somewhere it doesn't work * Add the ability to provide enemy presets * Various fixes and features * Bug fixes * Better Coiled Sword placement * Structure DarkSouls3Location more like DarkSouls3Item * Add events to make DS3's spheres more even * Restructure locations to work like items do now * Add rules for more missable locations * Don't add two Storm Rulers * Place Hawk Ring in Farron Keep * Mark the Grass Crest Shield as useful * Mark new progression items * Fix a bug * Support newer better Path of the Dragon code * Don't lock the player out of Coiled Sword * Don't create events for missable locations * Don't throw strings * Don't smooth event items * Properly categorize Butcher Knife * Be more careful about placing Yhorm in low-randomization scenarios * Don't try to smooth DLC items with DLC disabled * Fix another Yhorm bug * Fix upgrade/infusion logic * Remove the PoolType option This distinction is no longer meaningful now that every location in the game of each type is randomized * Categorize HWL: Red Eye Orb as an NPC location * Don't place Storm Ruler on CA: Coiled Sword * Define flatten() locally to make this APWorld capable * Fix some more Leonhard weirdness * Fix unique item randomization * Don't double Twin Dragon Greatshield * Remove debugging print * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Add a missing dlc_enabled check * Use nicer options syntax * Bump data_version * Mention where Yhorm is in which world * Better handle excluded events * Add a newline to Yhorm location * Better way of handling excluded unradomized progression locations * Fix a squidge of nondeterminism * Only smooth items from this world * Don't smooth progression weapons * Remove a location that doesn't actually exist in-game * Classify Power Within as useful * Clarify location names * Fix location requirements * Clean up randomization options * Properly name Coiled Sword location * Add an option for configuring how missable items are handled * Fix some bugs from location name updates * Fix location guide link * Fix a couple locations that were busted offline * Update detailed location descriptions * Fix some bugs when generating for a multiworld * Inject Large Leather Shield * Fix a few location issues * Don't allow progression_skip_balancing for unnecessary locs * Update some location info * Don't uniquify the wrong items * Fix some more location issues * More location fixes * Use hyphens instead of parens for location descriptions * Update and fix more locations * Fix Soul of Cinder boss name * Fix some logic issues * Add item groups and document item/location groups * Fix the display name for "Impatient Mimics" * Properly handle Transposing Kiln and Pyromancer's Flame * Testing * Some fixes to NPC quests, late basin, and transposing kiln * Improve a couple location names * Split out and improve missable NPC item logic * Don't allow crow trades to have foreign items * Fix a variable capture bug * Make sure early items are accessible early even with early Castle * Mark ID giant slave drops as missable * Make sure late basin means that early items aren't behind it * Make is_location_available explicitly private * Add an _add_item_rule utility that checks availability * Clear excluded items if excluded_locations == "unnecessary" * Don't allow upgrades/infusions in crow trades * Fix the documentation for deprecated options * Create events for all excluded locations This allows `can_reach` logic to work even if the locations are randomized. * Fix up Patches' and Siegward's logic based on some manual testing * Factor out more sub-methods for setting location rules * Oops, left these in * Fixing name * Left that in too * Changing to NamedRange to support special_range_names * Alphabetizing * Don't call _is_location_available on foreign locations * Add missing Leonhard items * Changing late basin to have a post-small-doll option * Update basin option, add logic for some of Leonhard Hawkwood and Orbeck * Simplifying an option, fixing a copy-paste error * Removing trailing whitespace * Changing lost items to go into start inventory * Revert Basin changes * Oops * Update Options.py * Reverting small doll changes * Farron Keep boss requirement logic * Add Scroll for late_dlc * Fixing excluded unnecessary locations * Adding Priestess Ring as being after UG boss * Removing missable from Corvian Titanite Slab * Adding KFF Yhorm boss locks * Screams about Creighton * Elite Knight Set isn't permanently missable * Adding Kiln requirement to KFF * fixing valid_keys and item groups * Fixing an option-checker * Throwing unplaceable Storm Ruler into start inventory * Update locations * Refactor item injection * Update setup doc * Small fixes * Fix another location name * Fix injection calculation * Inject guaranteed items along with progression items * Mark boss souls as required for access to regions This allows us to set quest requirements for boss souls and have them automatically propagated to regions, means we need less machinery for Yhorm bosses, and allows us to get rid of a few region-transition events. * Make sure Sirris's quest can be completed before Pontiff * Removing unused list * Changing dict to list * Removing unused test * Update __init__.py * self.multiworld.random -> self.random (#9) * Fix some miscellaneous location issues * Rewrite the DS3 intro page/FAQ * Removing modifying the itempool after fill (#7) Co-authored-by: Natalie Weizenbaum * Small fixes to the setup guide (#10) Small fixes, adding an example for connecting * Expanded Late Basin of Vows and Late DLC (#6) * Add proper requirements for CD: Black Eye Orb * Fix Aldrich's name * Document the differences with the 2.x.x branch * Don't crash if there are more items than locations in smoothing * Apply suggestions from code review Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Code review * Fix _replace_with_filler * Don't use the shared flatten function in SM * Track local items separately rather than iterating the multiworld * Various formatting/docs changes suggested by PyCharm (#12) * Drop deprecated options * Rename "offline randomizer" to "static randomizer" which is clearer * Move `enable_*_locations` under removed options. * Avoid excluded locations for locally-filled items * Adding Removed options to error (#14) * Changes for WebHost options display and the options overhaul * unpack iterators in item list (#13) * Allow worlds to add options to prebuilt groups Previously, this crashed because `typing.NamedTuple` fields such as `group.name` aren't assignable. Now it will only fail for group names that are actually incorrectly cased, and will fail with a better error message. * Style changes, rename exclude behavior options, remove guaranteed items option * Spacing/Formatting (#18) * Various Fixes (#19) * Universally Track Yhorm (#20) * Account for excluded and missable * These are behaviors now * This is singular, apparently * Oops * Fleshing out the priority process * Missable Titanite Lizards and excluded locations (#22) * Small style/efficiency changes * Final passthrough fixes (#24) * Use rich option formatting * Make the behavior option values actual behaviors (#25) * Use != * Remove unused flatten utility * Some changes from review (#28) * Fixing determinism and making smooth faster (#29) * Style change * PyCharm and Mypy fixes (#26) Co-authored-by: Scipio Wright * Change yhorm default (#30) * Add indirect condition (#27) * Update worlds/dark_souls_3/docs/locations_en.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Ship all item IDs to the client This avoids issues where items might get skipped if, for instance, they're only in the starting inventory. * Make sure to send AP IDs for infused/upgraded weapons * Make `RandomEnemyPresetOption` compatible with ArchipelagoMW/Archipelago#3280 (#31) * Fix cast * More typing and small fixes (#32) --------- Co-authored-by: Scipio Wright Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Doug Hoskisson Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Core: Check parent_region.can_reach first in Location.can_reach (#3724) * Core: Check parent_region.can_reach first in Location.can_reach The comment about self.access_rule computing faster on average appears to no longer be correct with the current caching system for region accessibility, resulting in self.parent_region.can_reach computing faster on average. Generation of template yamls for each game that does not require a rom to generate, generated with `python -O .\Generate.py --seed 1` (all durations averaged over at 4 or 5 generations): Full generation with `spoiler: 1` and no progression balancing: 89.9s -> 72.6s Only output from above case: 2.6s -> 2.2s Full generation with `spoiler: 3` and no progression balancing: 769.9s -> 627.1s Only playthrough calculation + paths from above case: 680.5s -> 555.3s Full generation with `spoiler: 1` with default progression balancing: 123.5s -> 98.3s Only progression balancing from above case: 11.3s -> 9.6s * Update BaseClasses.py * Update BaseClasses.py * Update BaseClasses.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Core: Speed up CollectionState.copy() using built-in copy methods (#3678) All the types being copied are built-in types with their own `copy()` methods, so using the `copy` module was a bit overkill and also slower. This patch replaces the use of the `copy` module in `CollectionState.copy()` with using the built-in `.copy()` methods. The copying of `reachable_regions` and `blocked_connections` was also iterating the keys of each dictionary and then looking up the value in the dictionary for that key. It is faster, and I think more readable, to iterate the dictionary's `.items()` instead. For me, when generating a multiworld including the template yaml of every world with `python -O .\Generate.py --skip_output`, this patch saves about 2.1s. The overall generation duration for these yamls varies quite a lot, but averages around 160s for me, so on average this patch reduced overall generation duration (excluding output duration) by around 1.3%. Timing comparisons were made by calling time.perf_counter() at the start and end of `CollectionState.copy()`'s body, and summing the differences between the starts and ends of the method body into a global variable that was printed at the end of generation. Additional timing comparisons were made, using the `timeit` module, of the individual function calls or dictionary comprehensions used to perform the copying. The main performance cost was `copy.deepcopy()`, which gets slow as the number of keys multiplied by the number of values within the sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]` with 100 keys and where each Counter contains 100 keys was 30x slower than most other tested copying methods. Increasing the number of dict keys or Counter keys only makes it slower. * HK: fix iterating all worlds instead of only HK worlds in stage_pre_fill (#3750) Would cause generation to fail when generating with HK and another game. Mistake in 6803c373e5ff. * DOOM, DOOM II: Update steam URLs (#3746) * TLOZ: world: multiworld (#3752) * SoE: fix determinism (#3745) Fixes randomly placed ingredients not being deterministic (depending on settings) and in turn also fixes logic not being deterministic if they get replaced by fragments. * Core: fix invalid __package__ of zipped worlds (#3686) * fix invalid package fix * add comment describing fix * Clique: Update to new options API (#3759) * Timespinner: Fix eels check logic #3777 * TUNIC: Add note to Universal Tracker stuff #3772 * Core: change start inventory from pool to warn when nothing to remove (#3158) * makes start inventory from pool warn and fixes the itempool to match when it can not find a matching item to remove * calc the difference correctly * save new filler and non-removed items differently so we don't remove existing items at random * Undertale: Fix slot_data and options.as_dict() (#3774) * Undertale: Fixing slot_data * Booleans were difficult * Core: Error on empty options.as_dict (#3773) * Error on empty options.as_dict * ValueError instead * Apply suggestions from code review Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Core: Remove broken unused code from Options.py (#3781) "Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained * Core: Two Small Fixes (#3782) * Core: recontextualize `CollectionState.collect` (#3723) * Core: renamed `CollectionState.collect` arg from `event` to `prevent_sweep` and remove forced collection * Update TestDungeon.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Core: dump all item placements for generation failures. (#3237) * Core: dump all item placements for generation failures * pass the multiworld from remaining fill * change how the args get handled to fix formatting --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Tests: fix the all games multiworld test (#3788) * TUNIC: Swap from multiworld.get to world.get for applicable things (#3789) * Swap from multiworld.get to world.get for applicable things * Why was this even here in the first place? * I have no idea (#3791) * TUNIC: Add off and on aliases for the Entrance Rando option #3794 * Stardew Valley: Add Quality Bobber in the logic rules for fish quality gold and above #3792 * Core: Require excluded locations to be reachable with full/locations accessibility (#3802) * Make excludeds reachable * Update all_state tests * Lingo: Fixed Initiated-side Eight Door not opening (#3793) * TUNIC: Give the fox a gun (in logic) (very small PR) (#3790) * Add bomb wall logic * Remove option call from can_shop * Gun for the envoy blocking Quarry * has_sword -> can_shop on cube cave entrance region * TLOZ: Fix non-deterministic item pool generation (#3779) * TLOZ: Fix non-deterministic item pool generation The way the item pool was constructed involved iterating unions of sets. Sets are unordered, so the order of iteration of these combined sets would be non-deterministic, resulting in the items in the item pool being generated in a different order with the same seed. Rather than creating unions of sets at all, the original code has been replaced with using Counter objects. As a dict subclass, Counter maintains insertion order, and its update() method makes it simple to combine the separate item dictionaries into a single dictionary with the total count of each item across each of the separate item dictionaries. Fixes #3664 - After investigating more deeply, the only differences I could find between generations of the same seed was the order of items created by TLOZ, so this patch appears to fix the non-deterministic generation issue. I did manage to reproduce the non-deterministic behaviour with just TLOZ in the end, but it was very rare. I'm not entirely sure why generating with SMZ3 specifically would cause the non-deterministic behaviour in TLOZ to be frequently present, whereas generating with other games or multiple TLOZ yamls would not. * Change import order --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Docs: Update 'tag' documentation (#3632) * Add tag docs for HintGame * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Make Tracker/TextOnly consistent with previous commit * Apply suggestion Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * fix spacing * Apply suggestion Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * apply suggestion correcting footnotes Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * [OSRS] Fixes Incorrect filler item names causing failures on tests. (#3768) * Updates filler item names to match the actual item names * Adds more descriptive error message in case this error comes back * Properly raises exception instead of just text * Replaces exception with assert * Fix !remaining for cross-world items (#3732) * Fix !remaining for other worlds * Typing fixes for the previous change * Update LocationStore test to match what get_remaining now returns * Core: early_local != local_early #3780 * Pokemon Emerald: Ensure dig tutor is always usable (#3660) * Pokemon Emerald: Ensure dig tutor is always usable * Pokemon Emerald: Clarify comment Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Core: type for `CommonContext.ui` (#3796) * Core: type for `CommonContext.ui` * use `Optional` * VVVVVV: Make unnecessary Trinkets filler (#3806) * Make unnecessary trinkets filler * Proper syntax Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Kingdom Hearts: Implement New Game (#3201) * Added Final Ansem Goal * Update __init__.py * Update Rules.py * New EotW logic * Update __init__.py * Update __init__.py * Update Items.py * Update Rules.py * Rename Location to be more meaningful, logic fixes * Removed Aerith locations * Change to allow randomized keyblade stats * Fixed incorrect option description. Fixed victory locations for alternative win condition settings * Commit * Lots of changes * Fixes * Fixes * Update Rules.py * Update Rules.py * Update Rules.py * Update Rules.py * Fixes * Update Rules.py * Update Rules.py * Update Options.py * Old Book is not required * Added Jungle Slider * Add Cid Check * Add Wonderland Book Check * Add OC Green Trinity * Add Inferno Band Event * Add Kurt Zisa Zantetsuken and Unknown EXP Necklace checks * Update Locations.py * Fix Final Ansem Goal * Update __init__.py * Update __init__.py * Add options to exclude super bosses and 100 acre wood * Fix puppies trp, remove cid check * Fix 100 Acre Wood Option * Material to Empty Bottle * Fixed rules, location names, etc * Fix super bosses * Add item + location groups, level sanity * Fix location and item group names * Add Bad Starting Weapons Option * Logic Error for 100 Acre Wood * Update Rules.py * Update __init__.py * Fixes related to randomized keyblade stats and super bosses * Credits and Fixes * Logic fixes, location name group changes * Update Options.py * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Scipio Wright * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Scipio Wright * Update .gitignore * Update CODEOWNERS * Update docs/CODEOWNERS Co-authored-by: Scipio Wright * Fixed Atlantica item group name * Update CODEOWNERS * Update Client.py * Update Items.py * Update __init__.py Co-authored-by: Scipio Wright * Update Rules.py Co-authored-by: Scipio Wright * Update Rules.py Co-authored-by: Scipio Wright * Update Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Fixed report group name * Fixes for PR * Update Options.py * Push changes for making the Final Rest Door appear, few option fixes * Update Rules.py * Website formatting, 0 min for reports, option description typo * Create KH1Client.py * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Scipio Wright * Update Options.py * Update Options.py * Update Rules.py * Update Rules.py * Update Rules.py * Add Donald and Goofy Death Link * Add fight logic for optional bosses * Update __init__.py * Update Options.py * Update worlds/kh1/Options.py Co-authored-by: Scipio Wright * Update Client.py * Update kh1_en.md * Update __init__.py * Cleaning up for PR * Update Client.py * Added event locations for vanilla items * Add proper location groups and auto hint synth shop items when entering * so many changes * Update Rules.py * fixed oathkeeper and crabclaw logic * Update Rules.py * Update Rules.py * Update Rules.py * Update Rules.py * Update en_Kingdom Hearts.md * Update en_Kingdom Hearts.md * fixing text * Update kh1_en.md * Addition of new key items * Update Regions.py * Push for start item from pool test * Update worlds/kh1/Options.py Co-authored-by: Scipio Wright * Document update * Update Rules.py * Added starting world range and final rest goal option * Update kh1_en.md * Update en_Kingdom Hearts.md * Update __init__.py * Update __init__.py * Clean up options descriptions * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/Options.py Co-authored-by: Scipio Wright * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Client.py Co-authored-by: Scipio Wright * Fix grammar in document * Update __init__.py * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Removed return type * Update __init__.py * Update __init__.py * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright * Update __init__.py * Fix missing i replacement, rework set rules to use "self" instead of a million arguments * Update KH1Client.py Co-authored-by: Doug Hoskisson * Reformat rules, fix bug with exp mult, add to readme * Clean up regions, fix client * Fix item send prompt * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/en_Kingdom Hearts.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/docs/kh1_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/test/test_goal.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Locations.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Locations.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Locations.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/__init__.py Co-authored-by: Doug Hoskisson * Fix so many suggestions * removed junk in missable locations option * Update __init__.py * Change credits order * Update en_Kingdom Hearts.md * Standardize punctuation * Update en_Kingdom Hearts.md * Update en_Kingdom Hearts.md * Update Regions.py * Removed "disclude" options in generation fillers * Update Rules.py * Update __init__.py * Fix cemetery typo * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Add option groups and option presets * Update worlds/kh1/__init__.py That's a good idea! Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Presets.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * fixed HB rule and formatting on a line in Items.py * Fix logic bug with Geppetto's House postcard * Update Rules.py * Update Options.py * Update __init__.py * Update __init__.py * Huge under-the-hood update for PR * More updates for PR * Update worlds/kh1/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/kh1/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update __init__.py --------- Co-authored-by: Scipio Wright Co-authored-by: Doug Hoskisson Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Core: Fix incorrect default state checked in MultiWorld.can_beat_game (#3813) `MultiWorld.can_beat_game()` with no arguments would initially check if `self.state` is beatable, but then would create an empty state, `state = CollectionState(self)`, to sweep spheres from to determine if the game is beatable. The issue was that `self.state` and the new empty state could be different. Currently, it seems that everywhere in Archipelago's codebase that calls `MultiWorld.can_beat_game()` with no arguments or `starting_state=None` has a `self.state` that only contains precollected items, so the new empty state happens to result in an equivalent state, but this should not be relied upon to always be the case. This patch changes `can_beat_game()` to initially check if the new empty state is beatable instead of `self.state`. This appears to be a bug introduced way back in 27b6dd8bd761 Fixes #3742 * The Witness: Fix Tunnels Theater Flower EP Access Logic + Add Unit Test for it (and Expert PP2) (#3807) * Tunnels Theater Flowers fix + Flowers&PP2 Unit Tests * copypaste * Can just do it like this * This is even better probably * Also do some cleanup :3 * God damnit * Docs: `NetworkItem.player` (#3811) * Docs: `NetworkItem.player` In many contexts, it's difficult to tell whether this is the sending player or the receiving player. * correct player info * Update NetUtils.py Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener * Minecraft: Update to new options system. (#3765) * Move to new options system. switch to using self.random reformat rules file. * further reformats * fix tests to use new options system. * fix slot data to not use self.multiworld * I hate python * new starting_items docstring to prepare for 1.20.5+ item components. fix invalid json being output to starting_items * more typing fixes. * stupid quotes around type declarations * removed unused variable in ItemPool.py change null check in Structures.py * update rules "self" variable to a "world: MinecraftWorld" variable * get key, and not value for required bosses. * The Witness: Panel Hunt Mode (#3265) * Add panel hunt options * Make sure all panels are either solvable or disabled in panel hunt * Pick huntable panels * Discards in disable non randomized * Set up panel hunt requirement * Panel hunt functional * Make it so an event can have multiple names * Panel hunt with events * Add hunt entities to slot data * ruff * add to hint data, no client sneding yet * encode panel hunt amount in compact hint data * Remove print statement * my b * consistent * meh * additions for lcient * Nah * Victory panels ineligible for panel hunt * Panel Hunt Postgame option * cleanup * Add data generation file * pull out set * always disable gate ep in panel hunt * Disallow certain challenge panels from being panel hunt panels * Make panelhuntpostgame its own function, so it can be called even if normal postgame is enabled * disallow PP resets from panel hunt * Disable challenge timer and elevetor start respectively in disable hunt postgame * Fix panelhunt postgame * lol * When you test that the bug is fixed but not that the non-bug is not unfixed * Prevent Obelisks from being panel hunt panels * Make picking panels for panel hunt a bit more sophisticated, if less random * Better function maybe ig * Ok maybe that was a bit too much * Give advanced players some control over panel hunt * lint * correct the logic for amount to pick * decided the jingle thing was dumb, I'll figure sth out client side. Same area discouragement is now a configurable factor, and the logic has been significantly rewritten * comment * Make the option visible * Safety * Change assert slightly * We do a little logging * number tweak & we do a lil logging * we do a little more logging * Ruff * Panel Hunt Option Group * Idk how that got here * Update worlds/witness/options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/witness/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * remove merge error * Update worlds/witness/player_logic.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * True * Don't have underwater sliding bridge when you have above water sliding bridge * These are not actually connected lol * get rid of unnecessary variable * Refactor compact hint function again * lint * Pull out Entity Hunt Picking into its own class, split it into many functions. Kept a lot of the comments tho * forgot to actually add the new file * some more refactoring & docstrings * consistent naming * flip elif change * Comment about naming * Make static eligible panels a constant I can refer back to * slight formatting change * pull out options-based eligibility into its own function * better text and stuff * lint * this is not necessary * capitalisation * Fix same area discouragement 0 * Simplify data file generation * Simplify data file generation * prevent div 0 * Add Vault Boxes -> Vault Panels to replacements * Update options.py * Update worlds/witness/entity_hunt.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update entity_hunt.py * Fix some events not working * assert * remove now unused function * lint * Lasers Activate, Lasers don't Solve * lint * oops * mypy * lint * Add simple panel hunt unit test * Add Panel Hunt Tests * Add more Panel Hunt Tests * Disallow Box Short for normal panel hunt --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * The Witness: Add "vague" hints making use of other games' region names and location groups (#2921) * Vague hints work! But, the client will probably reveal some of the info through scouts atm * Fall back on Everywhere if necessary * Some of these failsafes are not necessary now * Limit region size to 100 as well * Actually... like this. * Nutmeg * Lol * -1 for own player but don't scout * Still make always/priority ITEM hints * fix * uwu notices your bug * The hints should, like, actually work, you know? * Make it a Toggle * Update worlds/witness/hints.py Co-authored-by: Bryce Wilson * Update worlds/witness/hints.py Co-authored-by: Bryce Wilson * Make some suggested changes * Make that ungodly equation a bit clearer in terms of formatting * make that not sorted * Add a warning about the feature in the option tooltip * Make using region names experimental * reword option tooltip * Note about singleplayer * Slight rewording again * Reorder the order of priority a bit * this condition is unnecessary now * comment * No wait the order has to be like this * Okay now I think it's correct * Another comment * Align option tooltip with new behavior * slight rewording again * reword reword reword reword * - * ethics * Update worlds/witness/options.py Co-authored-by: Bryce Wilson * Rename and slight behavior change for local hints * I think I overengineered this system before. Make it more consistent and clear now * oops I used checks by accident * oops * OMEGA OOPS * Accidentally commited a print statemetn * Vi don't commit nonsense challenge difficulty impossible * This isn't always true but it's good enough * Update options.py * Update worlds/witness/options.py Co-authored-by: Scipio Wright * Scipio :3 * switch to is_event instead of checking against location.address * oop * Update test_roll_other_options.py * Fix that unit test problem lol * Oh is this not fixed in the apworld? --------- Co-authored-by: Bryce Wilson Co-authored-by: Scipio Wright * Mega Man 2: Implement New Game (#3256) * initial (broken) commit * small work on init * Update Items.py * beginning work, some rom patches * commit progress from bh branch * deathlink, fix soft-reset kill, e-tank loss * begin work on targeting new bhclient * write font * definitely didn't forget to add the other two hashes no * update to modern options, begin colors * fix 6th letter bug * palette shuffle + logic rewrite * fix a bunch of pointers * fix color changes, deathlink, and add wily 5 req * adjust weapon weakness generation * Update Rules.py * attempt wily 5 softlock fix * add explicit test for rbm weaknesses * fix difficulty and hard reset * fix connect deathlink and off by one item color * fix atomic fire again * de-jank deathlink * rewrite wily5 rule * fix rare solo-gen fill issue, hopefully * Update Client.py * fix wily 5 requirements * undo fill hook * fix picopico-kun rules * for real this time * update minimum damage requirement * begin move to procedure patch * finish move to APPP, allow rando boobeam, color updates * fix color bug, UT support? * what do you mean I forgot the procedure * fix UT? * plando weakness and fixes * sfx when item received, more time stopper edge cases * Update test_weakness.py * fix rules and color bug * fix color bug, support reduced flashing * major world overhaul * Update Locations.py * fix first found bugs * mypy cleanup * headerless roms * Update Rom.py * further cleanup * work on energylink * el fixes * update to energylink 2.0 packet * energylink balancing * potentially break other clients, more balancing * Update Items.py * remove startup change from basepatch we write that in patch, since we also need to clean the area before applying * el balancing and feedback * hopefully less test failures? * implement world version check * add weapon/health option * Update Rom.py * x/x2 * specials * Update Color.py * Update Options.py * finally apply location groups * bump minor version number instead * fix duplicate stage sends * validate wily 5, tests * see if renaming fixes * add shuffled weakness * remove passwords * refresh rbm select, fix wily 5 validation * forgot we can't check 0 * oops I broke the basepatch (remove failing test later) * fix solo gen fill error? * fix webhost patch recognition * fix imports, basepatch * move to flexibility metric for boss validation * special case boobeam trap * block strobe on stage select init * more energylink balancing * bump world version * wily HP inaccurate in validation * fix validation edge case * save last completed wily to data storage * mypy and pep8 cleanup * fix file browse validation * fix test failure, add enemy weakness * remove test seed * update enemy damage * inno setup * Update en_Mega Man 2.md * setup guide * Update en_Mega Man 2.md * finish plando weakness section * starting rbm edge case * remove * imports * properly wrap later weakness additions in regen playthrough * fix import * forgot readme * remove time stopper special casing since we moved to proper wily 5 validation, this special casing is no longer important * properly type added locations * Update CODEOWNERS * add animation reduction * deprioritize Time Stopper in rush checks * special case wily phase 1 * fix key error * forgot the test * music and general cleanup * the great rename * fix import * thanks pycharm * reorder palette shuffle * account for alien on shuffled weakness * apply suggestions * fix seedbleed * fix invalid buster passthrough * fix weakness landing beneath required amount * fix failsafe * finish music * fix Time Stopper on Flash/Alien * asar pls * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world helpers * init cleanup * apostrophes * clearer wording * mypy and cleanup * options doc cleanup * Update rom.py * rules cleanup * Update __init__.py * Update __init__.py * move to defaultdict * cleanup world helpers * Update __init__.py * remove unnecessary line from fill hook * forgot the other one * apply code review * remove collect * Update rules.py * forgot another --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Blasphemous: Total overhaul (#3355) * Blasphemous: WIP overhaul * Entrance rule mistake * stuff * Getting closer * Real?? Maybe?? * Don't fail me now 🙏 * Add starting location tests * More tests (it still doesn't work actually 😔) * REAL * Add unreachable regions to test_reachability.py * PR ready - Remove unused functions from init - Use group exclusive functions in rules - Style changes * Bump required client version * Clean up unused imports * Change slot data * Review fixes - Prevent strength calculations from including excess items - Add new lines to ends of files - Fix missed deprecated option and random usage in init * Update option docstrings, add groups * Add preprocessor files * Update option docstrings again actually * Update player strength calculation * Rename group methods * Fix missing logic for RESCUED_CHERUB_06 * Register indirect conditions * Register indirect conditions (part 2) * Update extracted logic, change slot data key * Add region to excluded list * A capital letter * Use camelCase keys in preprocessor * Write some of new setup guide * Remove indents before list points * Change locationinfo to list of dictonaries * Finish docs, update extractor config and data * Mark region_data.py as generated * Suggested changes * More suggested changes * Suggested changes again - Use OptionError - Create list of disabled locations before looping - Check if options are equal to str instead of int - Clean up start location override - Reword some of setup guide - Organize location list - Remove unnecessary escaped quotes from option docstrings - Add world type to test base * C# moment * Requested changes * Update .gitattributes --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * MM2: fix Wily 5 Time Stopper rule (#3824) * fix time stopper rule * that was the entirely wrong rule actually * YachtDice: implement new game (#3482) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Yacht Dice: setup: change release-link to latest (#3827) On the installation page, link to the latest release, instead of the page with all releases * ALTTP: Minor Tweaks to the Adjuster UI (#2533) * Tweak ALTTP Adjuster padding/size to accommodate resizing - Set minsize so the actions buttons on bottom are always visible. - Added a minor amount of padding around the top level objects. - Increased the size of the entry fields for roms to match general button size. - Updated layout calls so vertical spacing doesn't increase between fields when maximizing the window - Added a little bit of spacing on the rom label so it more closely lines up with the other rom selection field * Tweak ALTTP Adjuster padding/size to accommodate resizing - Set minsize so the actions buttons on bottom are always visible. - Added a minor amount of padding around the top level objects. - Increased the size of the entry fields for roms to match general button size. - Updated layout calls so vertical spacing doesn't increase between fields when maximizing the window - Added a little bit of spacing on the rom label so it more closely lines up with the other rom selection field * LTTP: Fix a bug in Triforce Pieces Mode: Extra (#3784) When triforce_pieces_mode is set to "extra", the number of Triforce pieces in the pool should be equal to the number required plus the number extra. The number available was being used in this calculation, instead of the number required. * The Witness: Ban Excluded Panels from Panel Hunt (#3818) * excluded panels should not be picked by panel hunt * ban excluded panels from panel hunt * Get rid of an unused variable * Purge the world: multiworld evil from osrs (#3751) * Core, some worlds: Rename sweep_for_events to sweep_for_advancements (#3571) * Rename sweep_for_events to sweep_for_advancements * more event->advancement renames * oops accidentally deleted the deprecation thing in the force push * Update TestDungeon.py * Update BaseClasses.py * Update BaseClasses.py * oops * utils.deprecate * treble, you had no idea how right you were * Update test_panel_hunt.py * Update BaseClasses.py Co-authored-by: Fabian Dill --------- Co-authored-by: Fabian Dill * Core: some typing and cleaning in `BaseClasses.py` (#3391) * Core: some typing and cleaning in `BaseClasses.py` * more backwards `__repr__` * double-quote string * remove some end-of-line whitespace * Celeste 64: Typo #3840 oops * Kingdom Hearts: Make Ceiling Division Human-Readable #3839 * The Witness: Shuffle Dog (#3425) * Town Pet the Dog * Add shuffle dog to options presets * I cri evritim * I guess it's as good a time as any * :( * fix the soft conflict * add all the shuffle dog options to some of the unit tests bc why not * Laser Panels are just 'General' now, I'm pretty sure * Could I really call it allsanity? * The Witness: Switch to world.player_name (#3693) * lint * player_name * oops lmao * shorten * Launcher: Update message that displays when installing a custom apworld for a game in main (#3607) * kvui: assert kivy is not imported before kvui (#3823) * Pokemon Emerald: Send current map to trackers (#3726) --------- Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: alchav Co-authored-by: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Remy Jette Co-authored-by: Jarno Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: GodlFire <46984098+GodlFire@users.noreply.github.com> Co-authored-by: Kory Dondzila Co-authored-by: Trevor L <80716066+TRPG0@users.noreply.github.com> Co-authored-by: wildham <64616385+wildham0@users.noreply.github.com> Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Kono Tyran Co-authored-by: Scipio Wright Co-authored-by: SunCat Co-authored-by: digiholic Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Co-authored-by: Mysteryem Co-authored-by: Louis M Co-authored-by: qwint Co-authored-by: Natalie Weizenbaum Co-authored-by: Exempt-Medic Co-authored-by: Doug Hoskisson Co-authored-by: Kaito Sinclaire Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Star Rauchenberger Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com> Co-authored-by: Bryce Wilson Co-authored-by: Scrungip <95324612+Scrungip@users.noreply.github.com> Co-authored-by: gaithern <36639398+gaithern@users.noreply.github.com> Co-authored-by: KonoTyran Co-authored-by: Spineraks Co-authored-by: B1t Co-authored-by: Kappatechy Co-authored-by: Fabian Dill Co-authored-by: PoryGone <98504756+PoryGone@users.noreply.github.com> --- worlds/stardew_valley/data/fish_data.py | 5 +- .../stardew_valley/test/TestDynamicGoals.py | 1 + worlds/stardew_valley/test/__init__.py | 8 ++- .../stardew_valley/test/rules/TestFishing.py | 61 +++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 worlds/stardew_valley/test/rules/TestFishing.py diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index 26b1a0d58a..dfa8891077 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -26,6 +26,7 @@ class FishItem: fresh_water = (Region.farm, Region.forest, Region.town, Region.mountain) ocean = (Region.beach,) +tide_pools = (Region.tide_pools,) town_river = (Region.town,) mountain_lake = (Region.mountain,) forest_pond = (Region.forest,) @@ -118,13 +119,13 @@ midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 5 spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60) angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False) -crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False) +crimsonfish = create_fish(Fish.crimsonfish, tide_pools, season.summer, 95, True, False) glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False) legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False) mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False) ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True) -son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True) +son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, tide_pools, season.summer, 95, True, True) glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True) legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True) radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True) diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py index bfa58dd340..b0e6d6c626 100644 --- a/worlds/stardew_valley/test/TestDynamicGoals.py +++ b/worlds/stardew_valley/test/TestDynamicGoals.py @@ -27,6 +27,7 @@ def collect_fishing_abilities(tester: SVTestBase): tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Beach Bridge"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index c2c2a6a20b..4dee0ebf6d 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -258,15 +258,19 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): def collect_lots_of_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25)) + real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items + required_prog_items = int(round(real_total_prog_items * 0.25)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) + self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items def collect_all_the_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95)) + real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items + required_prog_items = int(round(real_total_prog_items * 0.95)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) + self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items def collect_everything(self): non_event_items = [item for item in self.multiworld.get_items() if item.code] diff --git a/worlds/stardew_valley/test/rules/TestFishing.py b/worlds/stardew_valley/test/rules/TestFishing.py new file mode 100644 index 0000000000..04a1528dd8 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestFishing.py @@ -0,0 +1,61 @@ +from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \ + ElevatorProgression, SpecialOrderLocations +from ...strings.fish_names import Fish +from ...test import SVTestBase + + +class TestNeedRegionToCatchFish(SVTestBase): + options = { + SeasonRandomization.internal_name: SeasonRandomization.option_disabled, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + Fishsanity.internal_name: Fishsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + } + + def test_catch_fish_requires_region_unlock(self): + fish_and_items = { + Fish.crimsonfish: ["Beach Bridge"], + Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"], + Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades + Fish.mutant_carp: ["Rusty Key"], + Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"], + Fish.lionfish: ["Boat Repair"], + Fish.blue_discus: ["Island Obelisk", "Island West Turtle"], + Fish.stingray: ["Boat Repair", "Island Resort"], + Fish.ghostfish: ["Progressive Weapon"], + Fish.stonefish: ["Progressive Weapon"], + Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"], + Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"], + Fish.sandfish: ["Bus Repair"], + Fish.scorpion_carp: ["Desert Obelisk"], + # Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary + Fish.son_of_crimsonfish: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.radioactive_carp: ["Beach Bridge", "Rusty Key", "Boat Repair", "Island West Turtle", "Qi Walnut Room"], + Fish.glacierfish_jr: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + } + self.original_state = self.multiworld.state.copy() + for fish in fish_and_items: + with self.subTest(f"Region rules for {fish}"): + self.collect_all_the_money() + item_names = fish_and_items[fish] + location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + items = [] + for item_name in item_names: + items.append(self.collect(item_name)) + with self.subTest(f"{fish} can be reached with {item_names}"): + self.assert_reach_location_true(location, self.multiworld.state) + for item_required in items: + self.multiworld.state = self.original_state.copy() + with self.subTest(f"{fish} requires {item_required.name}"): + for item_to_collect in items: + if item_to_collect.name != item_required.name: + self.collect(item_to_collect) + self.assert_reach_location_false(location, self.multiworld.state) + + self.multiworld.state = self.original_state.copy() From 0f64bd08e19e8d07b100ecdda63943edd48de385 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 29 Aug 2024 02:43:13 -0400 Subject: [PATCH 28/50] ChecksFinder: itempool naming/typing (#3797) * Rename itempool * Update comment --- worlds/checksfinder/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index e064a1c419..9ba57b0591 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -44,15 +44,15 @@ class ChecksFinderWorld(World): self.multiworld.regions += [menu, board] def create_items(self): - # Generate item pool - itempool = [] + # Generate list of items + items_to_create = [] # Add the map width and height stuff - itempool += ["Map Width"] * 5 # 10 - 5 - itempool += ["Map Height"] * 5 # 10 - 5 + items_to_create += ["Map Width"] * 5 # 10 - 5 + items_to_create += ["Map Height"] * 5 # 10 - 5 # Add the map bombs - itempool += ["Map Bombs"] * 15 # 20 - 5 - # Convert itempool into real items - itempool = [self.create_item(item) for item in itempool] + items_to_create += ["Map Bombs"] * 15 # 20 - 5 + # Convert list into real items + itempool = [self.create_item(item) for item in items_to_create] self.multiworld.itempool += itempool From 08dc7e522efbf03aff6732f890bc44406509ad1b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 29 Aug 2024 03:42:46 -0400 Subject: [PATCH 29/50] TUNIC: Add note about plando items to ER hint-creation failure error message (#3825) * Add note about plando items to entrance rando option description * Update error text to specifically call out plando items * Remove option description change --- worlds/tunic/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 47c66591f9..bbffd9c144 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -339,7 +339,8 @@ class TunicWorld(World): except KeyError: # logic bug, proceed with warning since it takes a long time to update AP warning(f"{location.name} is not logically accessible for {self.player_name}. " - "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.") + "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. " + "If you are using Plando Items (excluding early locations), then this is likely the cause.") hint_text = "Inaccessible" else: while connection != ("Menu", None): From b1be59745133c9154821dc77b4b77684cab293b4 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 30 Aug 2024 03:26:49 -0700 Subject: [PATCH 30/50] DS3: Explicitly track item equality by name when sending IDs (#3853) We had been keeping a set of items and defining item equality, but item equality really only makes sense if you consider distinct IDs to be distinct items. But that means the set ends up having multiple copies of the same item, causing a bug where some items had the wrong upgrade level in the game. This also removes the equality definition, which was only used by this one set. --- worlds/dark_souls_3/Items.py | 9 --------- worlds/dark_souls_3/__init__.py | 11 +++++++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index 19cd79a994..044e3616f7 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -238,15 +238,6 @@ class DS3ItemData: ds3_code = cast(int, self.ds3_code) + level, filler = False, ) - - def __hash__(self) -> int: - return (self.name, self.ds3_code).__hash__() - - def __eq__(self, other: Any) -> bool: - if isinstance(other, self.__class__): - return self.name == other.name and self.ds3_code == other.ds3_code - else: - return False class DarkSouls3Item(Item): diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 159a870c76..c31a3681df 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1504,16 +1504,19 @@ class DarkSouls3World(World): # We include all the items the game knows about so that users can manually request items # that aren't randomized, and then we _also_ include all the items that are placed in # practice `item_dictionary.values()` doesn't include upgraded or infused weapons. - all_items = { - cast(DarkSouls3Item, location.item).data + items_by_name = { + location.item.name: cast(DarkSouls3Item, location.item).data for location in self.multiworld.get_filled_locations() # item.code None is used for events, which we want to skip if location.item.code is not None and location.item.player == self.player - }.union(item_dictionary.values()) + } + for item in item_dictionary.values(): + if item.name not in items_by_name: + items_by_name[item.name] = item ap_ids_to_ds3_ids: Dict[str, int] = {} item_counts: Dict[str, int] = {} - for item in all_items: + for item in items_by_name.values(): if item.ap_code is None: continue if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code if item.count != 1: item_counts[str(item.ap_code)] = item.count From 920cffda2d79577e96733e5c18bacdefba795835 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 31 Aug 2024 06:15:00 -0500 Subject: [PATCH 31/50] KDL3: Version 2.0.0 (#3323) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * Update Rom.py * convert KDL3 to APPP * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * initial base for local items, need to finish * coo not clean * handle local items for real, appp cleanup * actually make bosses send their locations * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs * add prefill items * fix kine fill error * Update Rom.py * Update Files.py * mypy and softlock fix * Update Gifting.py * mypy phase 1 * fix rare async client bug * Update __init__.py * typing cleanup * fix stone softlock because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside * Update Rom.py * Add option groups * Rename to lowercase * finish rename * whoops broke the world * fix animal duplication bug * overhaul filler generation * add Miku flavor * Update gifting.py * fix issues related to max_hs increase * Update test_locations.py * fix boss shuffle not working if level shuffle is disabled * fix bleeding default levels * Update options.py * thought this would print seed * yay bad merges * forgot options too * yeah lets just break generation while at it * this is probably a problem * cap required heart stars * Revert "cap required heart stars" This reverts commit 759efd3e2b14ec2855082de041ac989cb9c5d500. * fix duplication removal placement, deprecated test option * forgot that we need to account for what we place * move location ids * rewrite trap handling * further stage renumber fixes * forgot one more * basic UT support * fix local heart star checks * fix pattern --------- Co-authored-by: beauxq Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/kdl3/Locations.py | 940 ------------------ worlds/kdl3/Rom.py | 577 ----------- worlds/kdl3/Room.py | 95 -- worlds/kdl3/__init__.py | 185 ++-- worlds/kdl3/{Aesthetics.py => aesthetics.py} | 35 +- worlds/kdl3/{Client.py => client.py} | 70 +- .../kdl3/{ClientAddrs.py => client_addrs.py} | 0 .../kdl3/{Compression.py => compression.py} | 0 worlds/kdl3/data/kdl3_basepatch.bsdiff4 | Bin 2411 -> 2646 bytes worlds/kdl3/{Gifting.py => gifting.py} | 19 +- worlds/kdl3/{Items.py => items.py} | 8 +- worlds/kdl3/locations.py | 940 ++++++++++++++++++ worlds/kdl3/{Names => names}/__init__.py | 0 .../animal_friend_spawns.py} | 11 + .../enemy_abilities.py} | 2 +- .../location_name.py} | 0 worlds/kdl3/{Options.py => options.py} | 66 +- worlds/kdl3/{Presets.py => presets.py} | 1 + worlds/kdl3/{Regions.py => regions.py} | 143 +-- worlds/kdl3/rom.py | 602 +++++++++++ worlds/kdl3/room.py | 133 +++ worlds/kdl3/{Rules.py => rules.py} | 145 +-- worlds/kdl3/{data => src}/APPauseIcons.dat | Bin worlds/kdl3/src/kdl3_basepatch.asm | 180 +++- worlds/kdl3/test/__init__.py | 2 + worlds/kdl3/test/test_goal.py | 14 +- worlds/kdl3/test/test_locations.py | 50 +- worlds/kdl3/test/test_shuffles.py | 258 +++-- 28 files changed, 2420 insertions(+), 2056 deletions(-) delete mode 100644 worlds/kdl3/Locations.py delete mode 100644 worlds/kdl3/Rom.py delete mode 100644 worlds/kdl3/Room.py rename worlds/kdl3/{Aesthetics.py => aesthetics.py} (91%) rename worlds/kdl3/{Client.py => client.py} (90%) rename worlds/kdl3/{ClientAddrs.py => client_addrs.py} (100%) rename worlds/kdl3/{Compression.py => compression.py} (100%) rename worlds/kdl3/{Gifting.py => gifting.py} (90%) rename worlds/kdl3/{Items.py => items.py} (95%) create mode 100644 worlds/kdl3/locations.py rename worlds/kdl3/{Names => names}/__init__.py (100%) rename worlds/kdl3/{Names/AnimalFriendSpawns.py => names/animal_friend_spawns.py} (95%) rename worlds/kdl3/{Names/EnemyAbilities.py => names/enemy_abilities.py} (99%) rename worlds/kdl3/{Names/LocationName.py => names/location_name.py} (100%) rename worlds/kdl3/{Options.py => options.py} (82%) rename worlds/kdl3/{Presets.py => presets.py} (98%) rename worlds/kdl3/{Regions.py => regions.py} (66%) create mode 100644 worlds/kdl3/rom.py create mode 100644 worlds/kdl3/room.py rename worlds/kdl3/{Rules.py => rules.py} (70%) rename worlds/kdl3/{data => src}/APPauseIcons.dat (100%) diff --git a/worlds/kdl3/Locations.py b/worlds/kdl3/Locations.py deleted file mode 100644 index 4d039a1349..0000000000 --- a/worlds/kdl3/Locations.py +++ /dev/null @@ -1,940 +0,0 @@ -import typing -from BaseClasses import Location, Region -from .Names import LocationName - -if typing.TYPE_CHECKING: - from .Room import KDL3Room - - -class KDL3Location(Location): - game: str = "Kirby's Dream Land 3" - room: typing.Optional["KDL3Room"] = None - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): - super().__init__(player, name, address, parent) - if not address: - self.show_in_spoiler = False - - -stage_locations = { - 0x770001: LocationName.grass_land_1, - 0x770002: LocationName.grass_land_2, - 0x770003: LocationName.grass_land_3, - 0x770004: LocationName.grass_land_4, - 0x770005: LocationName.grass_land_5, - 0x770006: LocationName.grass_land_6, - 0x770007: LocationName.ripple_field_1, - 0x770008: LocationName.ripple_field_2, - 0x770009: LocationName.ripple_field_3, - 0x77000A: LocationName.ripple_field_4, - 0x77000B: LocationName.ripple_field_5, - 0x77000C: LocationName.ripple_field_6, - 0x77000D: LocationName.sand_canyon_1, - 0x77000E: LocationName.sand_canyon_2, - 0x77000F: LocationName.sand_canyon_3, - 0x770010: LocationName.sand_canyon_4, - 0x770011: LocationName.sand_canyon_5, - 0x770012: LocationName.sand_canyon_6, - 0x770013: LocationName.cloudy_park_1, - 0x770014: LocationName.cloudy_park_2, - 0x770015: LocationName.cloudy_park_3, - 0x770016: LocationName.cloudy_park_4, - 0x770017: LocationName.cloudy_park_5, - 0x770018: LocationName.cloudy_park_6, - 0x770019: LocationName.iceberg_1, - 0x77001A: LocationName.iceberg_2, - 0x77001B: LocationName.iceberg_3, - 0x77001C: LocationName.iceberg_4, - 0x77001D: LocationName.iceberg_5, - 0x77001E: LocationName.iceberg_6, -} - -heart_star_locations = { - 0x770101: LocationName.grass_land_tulip, - 0x770102: LocationName.grass_land_muchi, - 0x770103: LocationName.grass_land_pitcherman, - 0x770104: LocationName.grass_land_chao, - 0x770105: LocationName.grass_land_mine, - 0x770106: LocationName.grass_land_pierre, - 0x770107: LocationName.ripple_field_kamuribana, - 0x770108: LocationName.ripple_field_bakasa, - 0x770109: LocationName.ripple_field_elieel, - 0x77010A: LocationName.ripple_field_toad, - 0x77010B: LocationName.ripple_field_mama_pitch, - 0x77010C: LocationName.ripple_field_hb002, - 0x77010D: LocationName.sand_canyon_mushrooms, - 0x77010E: LocationName.sand_canyon_auntie, - 0x77010F: LocationName.sand_canyon_caramello, - 0x770110: LocationName.sand_canyon_hikari, - 0x770111: LocationName.sand_canyon_nyupun, - 0x770112: LocationName.sand_canyon_rob, - 0x770113: LocationName.cloudy_park_hibanamodoki, - 0x770114: LocationName.cloudy_park_piyokeko, - 0x770115: LocationName.cloudy_park_mrball, - 0x770116: LocationName.cloudy_park_mikarin, - 0x770117: LocationName.cloudy_park_pick, - 0x770118: LocationName.cloudy_park_hb007, - 0x770119: LocationName.iceberg_kogoesou, - 0x77011A: LocationName.iceberg_samus, - 0x77011B: LocationName.iceberg_kawasaki, - 0x77011C: LocationName.iceberg_name, - 0x77011D: LocationName.iceberg_shiro, - 0x77011E: LocationName.iceberg_angel, -} - -boss_locations = { - 0x770200: LocationName.grass_land_whispy, - 0x770201: LocationName.ripple_field_acro, - 0x770202: LocationName.sand_canyon_poncon, - 0x770203: LocationName.cloudy_park_ado, - 0x770204: LocationName.iceberg_dedede, -} - -consumable_locations = { - 0x770300: LocationName.grass_land_1_u1, - 0x770301: LocationName.grass_land_1_m1, - 0x770302: LocationName.grass_land_2_u1, - 0x770303: LocationName.grass_land_3_u1, - 0x770304: LocationName.grass_land_3_m1, - 0x770305: LocationName.grass_land_4_m1, - 0x770306: LocationName.grass_land_4_u1, - 0x770307: LocationName.grass_land_4_m2, - 0x770308: LocationName.grass_land_4_m3, - 0x770309: LocationName.grass_land_6_u1, - 0x77030A: LocationName.grass_land_6_u2, - 0x77030B: LocationName.ripple_field_2_u1, - 0x77030C: LocationName.ripple_field_2_m1, - 0x77030D: LocationName.ripple_field_3_m1, - 0x77030E: LocationName.ripple_field_3_u1, - 0x77030F: LocationName.ripple_field_4_m2, - 0x770310: LocationName.ripple_field_4_u1, - 0x770311: LocationName.ripple_field_4_m1, - 0x770312: LocationName.ripple_field_5_u1, - 0x770313: LocationName.ripple_field_5_m2, - 0x770314: LocationName.ripple_field_5_m1, - 0x770315: LocationName.sand_canyon_1_u1, - 0x770316: LocationName.sand_canyon_2_u1, - 0x770317: LocationName.sand_canyon_2_m1, - 0x770318: LocationName.sand_canyon_4_m1, - 0x770319: LocationName.sand_canyon_4_u1, - 0x77031A: LocationName.sand_canyon_4_m2, - 0x77031B: LocationName.sand_canyon_5_u1, - 0x77031C: LocationName.sand_canyon_5_u3, - 0x77031D: LocationName.sand_canyon_5_m1, - 0x77031E: LocationName.sand_canyon_5_u4, - 0x77031F: LocationName.sand_canyon_5_u2, - 0x770320: LocationName.cloudy_park_1_m1, - 0x770321: LocationName.cloudy_park_1_u1, - 0x770322: LocationName.cloudy_park_4_u1, - 0x770323: LocationName.cloudy_park_4_m1, - 0x770324: LocationName.cloudy_park_5_m1, - 0x770325: LocationName.cloudy_park_6_u1, - 0x770326: LocationName.iceberg_3_m1, - 0x770327: LocationName.iceberg_5_u1, - 0x770328: LocationName.iceberg_5_u2, - 0x770329: LocationName.iceberg_5_u3, - 0x77032A: LocationName.iceberg_6_m1, - 0x77032B: LocationName.iceberg_6_u1, -} - -level_consumables = { - 1: [0, 1], - 2: [2], - 3: [3, 4], - 4: [5, 6, 7, 8], - 6: [9, 10], - 8: [11, 12], - 9: [13, 14], - 10: [15, 16, 17], - 11: [18, 19, 20], - 13: [21], - 14: [22, 23], - 16: [24, 25, 26], - 17: [27, 28, 29, 30, 31], - 19: [32, 33], - 22: [34, 35], - 23: [36], - 24: [37], - 27: [38], - 29: [39, 40, 41], - 30: [42, 43], -} - -star_locations = { - 0x770401: LocationName.grass_land_1_s1, - 0x770402: LocationName.grass_land_1_s2, - 0x770403: LocationName.grass_land_1_s3, - 0x770404: LocationName.grass_land_1_s4, - 0x770405: LocationName.grass_land_1_s5, - 0x770406: LocationName.grass_land_1_s6, - 0x770407: LocationName.grass_land_1_s7, - 0x770408: LocationName.grass_land_1_s8, - 0x770409: LocationName.grass_land_1_s9, - 0x77040a: LocationName.grass_land_1_s10, - 0x77040b: LocationName.grass_land_1_s11, - 0x77040c: LocationName.grass_land_1_s12, - 0x77040d: LocationName.grass_land_1_s13, - 0x77040e: LocationName.grass_land_1_s14, - 0x77040f: LocationName.grass_land_1_s15, - 0x770410: LocationName.grass_land_1_s16, - 0x770411: LocationName.grass_land_1_s17, - 0x770412: LocationName.grass_land_1_s18, - 0x770413: LocationName.grass_land_1_s19, - 0x770414: LocationName.grass_land_1_s20, - 0x770415: LocationName.grass_land_1_s21, - 0x770416: LocationName.grass_land_1_s22, - 0x770417: LocationName.grass_land_1_s23, - 0x770418: LocationName.grass_land_2_s1, - 0x770419: LocationName.grass_land_2_s2, - 0x77041a: LocationName.grass_land_2_s3, - 0x77041b: LocationName.grass_land_2_s4, - 0x77041c: LocationName.grass_land_2_s5, - 0x77041d: LocationName.grass_land_2_s6, - 0x77041e: LocationName.grass_land_2_s7, - 0x77041f: LocationName.grass_land_2_s8, - 0x770420: LocationName.grass_land_2_s9, - 0x770421: LocationName.grass_land_2_s10, - 0x770422: LocationName.grass_land_2_s11, - 0x770423: LocationName.grass_land_2_s12, - 0x770424: LocationName.grass_land_2_s13, - 0x770425: LocationName.grass_land_2_s14, - 0x770426: LocationName.grass_land_2_s15, - 0x770427: LocationName.grass_land_2_s16, - 0x770428: LocationName.grass_land_2_s17, - 0x770429: LocationName.grass_land_2_s18, - 0x77042a: LocationName.grass_land_2_s19, - 0x77042b: LocationName.grass_land_2_s20, - 0x77042c: LocationName.grass_land_2_s21, - 0x77042d: LocationName.grass_land_3_s1, - 0x77042e: LocationName.grass_land_3_s2, - 0x77042f: LocationName.grass_land_3_s3, - 0x770430: LocationName.grass_land_3_s4, - 0x770431: LocationName.grass_land_3_s5, - 0x770432: LocationName.grass_land_3_s6, - 0x770433: LocationName.grass_land_3_s7, - 0x770434: LocationName.grass_land_3_s8, - 0x770435: LocationName.grass_land_3_s9, - 0x770436: LocationName.grass_land_3_s10, - 0x770437: LocationName.grass_land_3_s11, - 0x770438: LocationName.grass_land_3_s12, - 0x770439: LocationName.grass_land_3_s13, - 0x77043a: LocationName.grass_land_3_s14, - 0x77043b: LocationName.grass_land_3_s15, - 0x77043c: LocationName.grass_land_3_s16, - 0x77043d: LocationName.grass_land_3_s17, - 0x77043e: LocationName.grass_land_3_s18, - 0x77043f: LocationName.grass_land_3_s19, - 0x770440: LocationName.grass_land_3_s20, - 0x770441: LocationName.grass_land_3_s21, - 0x770442: LocationName.grass_land_3_s22, - 0x770443: LocationName.grass_land_3_s23, - 0x770444: LocationName.grass_land_3_s24, - 0x770445: LocationName.grass_land_3_s25, - 0x770446: LocationName.grass_land_3_s26, - 0x770447: LocationName.grass_land_3_s27, - 0x770448: LocationName.grass_land_3_s28, - 0x770449: LocationName.grass_land_3_s29, - 0x77044a: LocationName.grass_land_3_s30, - 0x77044b: LocationName.grass_land_3_s31, - 0x77044c: LocationName.grass_land_4_s1, - 0x77044d: LocationName.grass_land_4_s2, - 0x77044e: LocationName.grass_land_4_s3, - 0x77044f: LocationName.grass_land_4_s4, - 0x770450: LocationName.grass_land_4_s5, - 0x770451: LocationName.grass_land_4_s6, - 0x770452: LocationName.grass_land_4_s7, - 0x770453: LocationName.grass_land_4_s8, - 0x770454: LocationName.grass_land_4_s9, - 0x770455: LocationName.grass_land_4_s10, - 0x770456: LocationName.grass_land_4_s11, - 0x770457: LocationName.grass_land_4_s12, - 0x770458: LocationName.grass_land_4_s13, - 0x770459: LocationName.grass_land_4_s14, - 0x77045a: LocationName.grass_land_4_s15, - 0x77045b: LocationName.grass_land_4_s16, - 0x77045c: LocationName.grass_land_4_s17, - 0x77045d: LocationName.grass_land_4_s18, - 0x77045e: LocationName.grass_land_4_s19, - 0x77045f: LocationName.grass_land_4_s20, - 0x770460: LocationName.grass_land_4_s21, - 0x770461: LocationName.grass_land_4_s22, - 0x770462: LocationName.grass_land_4_s23, - 0x770463: LocationName.grass_land_4_s24, - 0x770464: LocationName.grass_land_4_s25, - 0x770465: LocationName.grass_land_4_s26, - 0x770466: LocationName.grass_land_4_s27, - 0x770467: LocationName.grass_land_4_s28, - 0x770468: LocationName.grass_land_4_s29, - 0x770469: LocationName.grass_land_4_s30, - 0x77046a: LocationName.grass_land_4_s31, - 0x77046b: LocationName.grass_land_4_s32, - 0x77046c: LocationName.grass_land_4_s33, - 0x77046d: LocationName.grass_land_4_s34, - 0x77046e: LocationName.grass_land_4_s35, - 0x77046f: LocationName.grass_land_4_s36, - 0x770470: LocationName.grass_land_4_s37, - 0x770471: LocationName.grass_land_5_s1, - 0x770472: LocationName.grass_land_5_s2, - 0x770473: LocationName.grass_land_5_s3, - 0x770474: LocationName.grass_land_5_s4, - 0x770475: LocationName.grass_land_5_s5, - 0x770476: LocationName.grass_land_5_s6, - 0x770477: LocationName.grass_land_5_s7, - 0x770478: LocationName.grass_land_5_s8, - 0x770479: LocationName.grass_land_5_s9, - 0x77047a: LocationName.grass_land_5_s10, - 0x77047b: LocationName.grass_land_5_s11, - 0x77047c: LocationName.grass_land_5_s12, - 0x77047d: LocationName.grass_land_5_s13, - 0x77047e: LocationName.grass_land_5_s14, - 0x77047f: LocationName.grass_land_5_s15, - 0x770480: LocationName.grass_land_5_s16, - 0x770481: LocationName.grass_land_5_s17, - 0x770482: LocationName.grass_land_5_s18, - 0x770483: LocationName.grass_land_5_s19, - 0x770484: LocationName.grass_land_5_s20, - 0x770485: LocationName.grass_land_5_s21, - 0x770486: LocationName.grass_land_5_s22, - 0x770487: LocationName.grass_land_5_s23, - 0x770488: LocationName.grass_land_5_s24, - 0x770489: LocationName.grass_land_5_s25, - 0x77048a: LocationName.grass_land_5_s26, - 0x77048b: LocationName.grass_land_5_s27, - 0x77048c: LocationName.grass_land_5_s28, - 0x77048d: LocationName.grass_land_5_s29, - 0x77048e: LocationName.grass_land_6_s1, - 0x77048f: LocationName.grass_land_6_s2, - 0x770490: LocationName.grass_land_6_s3, - 0x770491: LocationName.grass_land_6_s4, - 0x770492: LocationName.grass_land_6_s5, - 0x770493: LocationName.grass_land_6_s6, - 0x770494: LocationName.grass_land_6_s7, - 0x770495: LocationName.grass_land_6_s8, - 0x770496: LocationName.grass_land_6_s9, - 0x770497: LocationName.grass_land_6_s10, - 0x770498: LocationName.grass_land_6_s11, - 0x770499: LocationName.grass_land_6_s12, - 0x77049a: LocationName.grass_land_6_s13, - 0x77049b: LocationName.grass_land_6_s14, - 0x77049c: LocationName.grass_land_6_s15, - 0x77049d: LocationName.grass_land_6_s16, - 0x77049e: LocationName.grass_land_6_s17, - 0x77049f: LocationName.grass_land_6_s18, - 0x7704a0: LocationName.grass_land_6_s19, - 0x7704a1: LocationName.grass_land_6_s20, - 0x7704a2: LocationName.grass_land_6_s21, - 0x7704a3: LocationName.grass_land_6_s22, - 0x7704a4: LocationName.grass_land_6_s23, - 0x7704a5: LocationName.grass_land_6_s24, - 0x7704a6: LocationName.grass_land_6_s25, - 0x7704a7: LocationName.grass_land_6_s26, - 0x7704a8: LocationName.grass_land_6_s27, - 0x7704a9: LocationName.grass_land_6_s28, - 0x7704aa: LocationName.grass_land_6_s29, - 0x7704ab: LocationName.ripple_field_1_s1, - 0x7704ac: LocationName.ripple_field_1_s2, - 0x7704ad: LocationName.ripple_field_1_s3, - 0x7704ae: LocationName.ripple_field_1_s4, - 0x7704af: LocationName.ripple_field_1_s5, - 0x7704b0: LocationName.ripple_field_1_s6, - 0x7704b1: LocationName.ripple_field_1_s7, - 0x7704b2: LocationName.ripple_field_1_s8, - 0x7704b3: LocationName.ripple_field_1_s9, - 0x7704b4: LocationName.ripple_field_1_s10, - 0x7704b5: LocationName.ripple_field_1_s11, - 0x7704b6: LocationName.ripple_field_1_s12, - 0x7704b7: LocationName.ripple_field_1_s13, - 0x7704b8: LocationName.ripple_field_1_s14, - 0x7704b9: LocationName.ripple_field_1_s15, - 0x7704ba: LocationName.ripple_field_1_s16, - 0x7704bb: LocationName.ripple_field_1_s17, - 0x7704bc: LocationName.ripple_field_1_s18, - 0x7704bd: LocationName.ripple_field_1_s19, - 0x7704be: LocationName.ripple_field_2_s1, - 0x7704bf: LocationName.ripple_field_2_s2, - 0x7704c0: LocationName.ripple_field_2_s3, - 0x7704c1: LocationName.ripple_field_2_s4, - 0x7704c2: LocationName.ripple_field_2_s5, - 0x7704c3: LocationName.ripple_field_2_s6, - 0x7704c4: LocationName.ripple_field_2_s7, - 0x7704c5: LocationName.ripple_field_2_s8, - 0x7704c6: LocationName.ripple_field_2_s9, - 0x7704c7: LocationName.ripple_field_2_s10, - 0x7704c8: LocationName.ripple_field_2_s11, - 0x7704c9: LocationName.ripple_field_2_s12, - 0x7704ca: LocationName.ripple_field_2_s13, - 0x7704cb: LocationName.ripple_field_2_s14, - 0x7704cc: LocationName.ripple_field_2_s15, - 0x7704cd: LocationName.ripple_field_2_s16, - 0x7704ce: LocationName.ripple_field_2_s17, - 0x7704cf: LocationName.ripple_field_3_s1, - 0x7704d0: LocationName.ripple_field_3_s2, - 0x7704d1: LocationName.ripple_field_3_s3, - 0x7704d2: LocationName.ripple_field_3_s4, - 0x7704d3: LocationName.ripple_field_3_s5, - 0x7704d4: LocationName.ripple_field_3_s6, - 0x7704d5: LocationName.ripple_field_3_s7, - 0x7704d6: LocationName.ripple_field_3_s8, - 0x7704d7: LocationName.ripple_field_3_s9, - 0x7704d8: LocationName.ripple_field_3_s10, - 0x7704d9: LocationName.ripple_field_3_s11, - 0x7704da: LocationName.ripple_field_3_s12, - 0x7704db: LocationName.ripple_field_3_s13, - 0x7704dc: LocationName.ripple_field_3_s14, - 0x7704dd: LocationName.ripple_field_3_s15, - 0x7704de: LocationName.ripple_field_3_s16, - 0x7704df: LocationName.ripple_field_3_s17, - 0x7704e0: LocationName.ripple_field_3_s18, - 0x7704e1: LocationName.ripple_field_3_s19, - 0x7704e2: LocationName.ripple_field_3_s20, - 0x7704e3: LocationName.ripple_field_3_s21, - 0x7704e4: LocationName.ripple_field_4_s1, - 0x7704e5: LocationName.ripple_field_4_s2, - 0x7704e6: LocationName.ripple_field_4_s3, - 0x7704e7: LocationName.ripple_field_4_s4, - 0x7704e8: LocationName.ripple_field_4_s5, - 0x7704e9: LocationName.ripple_field_4_s6, - 0x7704ea: LocationName.ripple_field_4_s7, - 0x7704eb: LocationName.ripple_field_4_s8, - 0x7704ec: LocationName.ripple_field_4_s9, - 0x7704ed: LocationName.ripple_field_4_s10, - 0x7704ee: LocationName.ripple_field_4_s11, - 0x7704ef: LocationName.ripple_field_4_s12, - 0x7704f0: LocationName.ripple_field_4_s13, - 0x7704f1: LocationName.ripple_field_4_s14, - 0x7704f2: LocationName.ripple_field_4_s15, - 0x7704f3: LocationName.ripple_field_4_s16, - 0x7704f4: LocationName.ripple_field_4_s17, - 0x7704f5: LocationName.ripple_field_4_s18, - 0x7704f6: LocationName.ripple_field_4_s19, - 0x7704f7: LocationName.ripple_field_4_s20, - 0x7704f8: LocationName.ripple_field_4_s21, - 0x7704f9: LocationName.ripple_field_4_s22, - 0x7704fa: LocationName.ripple_field_4_s23, - 0x7704fb: LocationName.ripple_field_4_s24, - 0x7704fc: LocationName.ripple_field_4_s25, - 0x7704fd: LocationName.ripple_field_4_s26, - 0x7704fe: LocationName.ripple_field_4_s27, - 0x7704ff: LocationName.ripple_field_4_s28, - 0x770500: LocationName.ripple_field_4_s29, - 0x770501: LocationName.ripple_field_4_s30, - 0x770502: LocationName.ripple_field_4_s31, - 0x770503: LocationName.ripple_field_4_s32, - 0x770504: LocationName.ripple_field_4_s33, - 0x770505: LocationName.ripple_field_4_s34, - 0x770506: LocationName.ripple_field_4_s35, - 0x770507: LocationName.ripple_field_4_s36, - 0x770508: LocationName.ripple_field_4_s37, - 0x770509: LocationName.ripple_field_4_s38, - 0x77050a: LocationName.ripple_field_4_s39, - 0x77050b: LocationName.ripple_field_4_s40, - 0x77050c: LocationName.ripple_field_4_s41, - 0x77050d: LocationName.ripple_field_4_s42, - 0x77050e: LocationName.ripple_field_4_s43, - 0x77050f: LocationName.ripple_field_4_s44, - 0x770510: LocationName.ripple_field_4_s45, - 0x770511: LocationName.ripple_field_4_s46, - 0x770512: LocationName.ripple_field_4_s47, - 0x770513: LocationName.ripple_field_4_s48, - 0x770514: LocationName.ripple_field_4_s49, - 0x770515: LocationName.ripple_field_4_s50, - 0x770516: LocationName.ripple_field_4_s51, - 0x770517: LocationName.ripple_field_5_s1, - 0x770518: LocationName.ripple_field_5_s2, - 0x770519: LocationName.ripple_field_5_s3, - 0x77051a: LocationName.ripple_field_5_s4, - 0x77051b: LocationName.ripple_field_5_s5, - 0x77051c: LocationName.ripple_field_5_s6, - 0x77051d: LocationName.ripple_field_5_s7, - 0x77051e: LocationName.ripple_field_5_s8, - 0x77051f: LocationName.ripple_field_5_s9, - 0x770520: LocationName.ripple_field_5_s10, - 0x770521: LocationName.ripple_field_5_s11, - 0x770522: LocationName.ripple_field_5_s12, - 0x770523: LocationName.ripple_field_5_s13, - 0x770524: LocationName.ripple_field_5_s14, - 0x770525: LocationName.ripple_field_5_s15, - 0x770526: LocationName.ripple_field_5_s16, - 0x770527: LocationName.ripple_field_5_s17, - 0x770528: LocationName.ripple_field_5_s18, - 0x770529: LocationName.ripple_field_5_s19, - 0x77052a: LocationName.ripple_field_5_s20, - 0x77052b: LocationName.ripple_field_5_s21, - 0x77052c: LocationName.ripple_field_5_s22, - 0x77052d: LocationName.ripple_field_5_s23, - 0x77052e: LocationName.ripple_field_5_s24, - 0x77052f: LocationName.ripple_field_5_s25, - 0x770530: LocationName.ripple_field_5_s26, - 0x770531: LocationName.ripple_field_5_s27, - 0x770532: LocationName.ripple_field_5_s28, - 0x770533: LocationName.ripple_field_5_s29, - 0x770534: LocationName.ripple_field_5_s30, - 0x770535: LocationName.ripple_field_5_s31, - 0x770536: LocationName.ripple_field_5_s32, - 0x770537: LocationName.ripple_field_5_s33, - 0x770538: LocationName.ripple_field_5_s34, - 0x770539: LocationName.ripple_field_5_s35, - 0x77053a: LocationName.ripple_field_5_s36, - 0x77053b: LocationName.ripple_field_5_s37, - 0x77053c: LocationName.ripple_field_5_s38, - 0x77053d: LocationName.ripple_field_5_s39, - 0x77053e: LocationName.ripple_field_5_s40, - 0x77053f: LocationName.ripple_field_5_s41, - 0x770540: LocationName.ripple_field_5_s42, - 0x770541: LocationName.ripple_field_5_s43, - 0x770542: LocationName.ripple_field_5_s44, - 0x770543: LocationName.ripple_field_5_s45, - 0x770544: LocationName.ripple_field_5_s46, - 0x770545: LocationName.ripple_field_5_s47, - 0x770546: LocationName.ripple_field_5_s48, - 0x770547: LocationName.ripple_field_5_s49, - 0x770548: LocationName.ripple_field_5_s50, - 0x770549: LocationName.ripple_field_5_s51, - 0x77054a: LocationName.ripple_field_6_s1, - 0x77054b: LocationName.ripple_field_6_s2, - 0x77054c: LocationName.ripple_field_6_s3, - 0x77054d: LocationName.ripple_field_6_s4, - 0x77054e: LocationName.ripple_field_6_s5, - 0x77054f: LocationName.ripple_field_6_s6, - 0x770550: LocationName.ripple_field_6_s7, - 0x770551: LocationName.ripple_field_6_s8, - 0x770552: LocationName.ripple_field_6_s9, - 0x770553: LocationName.ripple_field_6_s10, - 0x770554: LocationName.ripple_field_6_s11, - 0x770555: LocationName.ripple_field_6_s12, - 0x770556: LocationName.ripple_field_6_s13, - 0x770557: LocationName.ripple_field_6_s14, - 0x770558: LocationName.ripple_field_6_s15, - 0x770559: LocationName.ripple_field_6_s16, - 0x77055a: LocationName.ripple_field_6_s17, - 0x77055b: LocationName.ripple_field_6_s18, - 0x77055c: LocationName.ripple_field_6_s19, - 0x77055d: LocationName.ripple_field_6_s20, - 0x77055e: LocationName.ripple_field_6_s21, - 0x77055f: LocationName.ripple_field_6_s22, - 0x770560: LocationName.ripple_field_6_s23, - 0x770561: LocationName.sand_canyon_1_s1, - 0x770562: LocationName.sand_canyon_1_s2, - 0x770563: LocationName.sand_canyon_1_s3, - 0x770564: LocationName.sand_canyon_1_s4, - 0x770565: LocationName.sand_canyon_1_s5, - 0x770566: LocationName.sand_canyon_1_s6, - 0x770567: LocationName.sand_canyon_1_s7, - 0x770568: LocationName.sand_canyon_1_s8, - 0x770569: LocationName.sand_canyon_1_s9, - 0x77056a: LocationName.sand_canyon_1_s10, - 0x77056b: LocationName.sand_canyon_1_s11, - 0x77056c: LocationName.sand_canyon_1_s12, - 0x77056d: LocationName.sand_canyon_1_s13, - 0x77056e: LocationName.sand_canyon_1_s14, - 0x77056f: LocationName.sand_canyon_1_s15, - 0x770570: LocationName.sand_canyon_1_s16, - 0x770571: LocationName.sand_canyon_1_s17, - 0x770572: LocationName.sand_canyon_1_s18, - 0x770573: LocationName.sand_canyon_1_s19, - 0x770574: LocationName.sand_canyon_1_s20, - 0x770575: LocationName.sand_canyon_1_s21, - 0x770576: LocationName.sand_canyon_1_s22, - 0x770577: LocationName.sand_canyon_2_s1, - 0x770578: LocationName.sand_canyon_2_s2, - 0x770579: LocationName.sand_canyon_2_s3, - 0x77057a: LocationName.sand_canyon_2_s4, - 0x77057b: LocationName.sand_canyon_2_s5, - 0x77057c: LocationName.sand_canyon_2_s6, - 0x77057d: LocationName.sand_canyon_2_s7, - 0x77057e: LocationName.sand_canyon_2_s8, - 0x77057f: LocationName.sand_canyon_2_s9, - 0x770580: LocationName.sand_canyon_2_s10, - 0x770581: LocationName.sand_canyon_2_s11, - 0x770582: LocationName.sand_canyon_2_s12, - 0x770583: LocationName.sand_canyon_2_s13, - 0x770584: LocationName.sand_canyon_2_s14, - 0x770585: LocationName.sand_canyon_2_s15, - 0x770586: LocationName.sand_canyon_2_s16, - 0x770587: LocationName.sand_canyon_2_s17, - 0x770588: LocationName.sand_canyon_2_s18, - 0x770589: LocationName.sand_canyon_2_s19, - 0x77058a: LocationName.sand_canyon_2_s20, - 0x77058b: LocationName.sand_canyon_2_s21, - 0x77058c: LocationName.sand_canyon_2_s22, - 0x77058d: LocationName.sand_canyon_2_s23, - 0x77058e: LocationName.sand_canyon_2_s24, - 0x77058f: LocationName.sand_canyon_2_s25, - 0x770590: LocationName.sand_canyon_2_s26, - 0x770591: LocationName.sand_canyon_2_s27, - 0x770592: LocationName.sand_canyon_2_s28, - 0x770593: LocationName.sand_canyon_2_s29, - 0x770594: LocationName.sand_canyon_2_s30, - 0x770595: LocationName.sand_canyon_2_s31, - 0x770596: LocationName.sand_canyon_2_s32, - 0x770597: LocationName.sand_canyon_2_s33, - 0x770598: LocationName.sand_canyon_2_s34, - 0x770599: LocationName.sand_canyon_2_s35, - 0x77059a: LocationName.sand_canyon_2_s36, - 0x77059b: LocationName.sand_canyon_2_s37, - 0x77059c: LocationName.sand_canyon_2_s38, - 0x77059d: LocationName.sand_canyon_2_s39, - 0x77059e: LocationName.sand_canyon_2_s40, - 0x77059f: LocationName.sand_canyon_2_s41, - 0x7705a0: LocationName.sand_canyon_2_s42, - 0x7705a1: LocationName.sand_canyon_2_s43, - 0x7705a2: LocationName.sand_canyon_2_s44, - 0x7705a3: LocationName.sand_canyon_2_s45, - 0x7705a4: LocationName.sand_canyon_2_s46, - 0x7705a5: LocationName.sand_canyon_2_s47, - 0x7705a6: LocationName.sand_canyon_2_s48, - 0x7705a7: LocationName.sand_canyon_3_s1, - 0x7705a8: LocationName.sand_canyon_3_s2, - 0x7705a9: LocationName.sand_canyon_3_s3, - 0x7705aa: LocationName.sand_canyon_3_s4, - 0x7705ab: LocationName.sand_canyon_3_s5, - 0x7705ac: LocationName.sand_canyon_3_s6, - 0x7705ad: LocationName.sand_canyon_3_s7, - 0x7705ae: LocationName.sand_canyon_3_s8, - 0x7705af: LocationName.sand_canyon_3_s9, - 0x7705b0: LocationName.sand_canyon_3_s10, - 0x7705b1: LocationName.sand_canyon_4_s1, - 0x7705b2: LocationName.sand_canyon_4_s2, - 0x7705b3: LocationName.sand_canyon_4_s3, - 0x7705b4: LocationName.sand_canyon_4_s4, - 0x7705b5: LocationName.sand_canyon_4_s5, - 0x7705b6: LocationName.sand_canyon_4_s6, - 0x7705b7: LocationName.sand_canyon_4_s7, - 0x7705b8: LocationName.sand_canyon_4_s8, - 0x7705b9: LocationName.sand_canyon_4_s9, - 0x7705ba: LocationName.sand_canyon_4_s10, - 0x7705bb: LocationName.sand_canyon_4_s11, - 0x7705bc: LocationName.sand_canyon_4_s12, - 0x7705bd: LocationName.sand_canyon_4_s13, - 0x7705be: LocationName.sand_canyon_4_s14, - 0x7705bf: LocationName.sand_canyon_4_s15, - 0x7705c0: LocationName.sand_canyon_4_s16, - 0x7705c1: LocationName.sand_canyon_4_s17, - 0x7705c2: LocationName.sand_canyon_4_s18, - 0x7705c3: LocationName.sand_canyon_4_s19, - 0x7705c4: LocationName.sand_canyon_4_s20, - 0x7705c5: LocationName.sand_canyon_4_s21, - 0x7705c6: LocationName.sand_canyon_4_s22, - 0x7705c7: LocationName.sand_canyon_4_s23, - 0x7705c8: LocationName.sand_canyon_5_s1, - 0x7705c9: LocationName.sand_canyon_5_s2, - 0x7705ca: LocationName.sand_canyon_5_s3, - 0x7705cb: LocationName.sand_canyon_5_s4, - 0x7705cc: LocationName.sand_canyon_5_s5, - 0x7705cd: LocationName.sand_canyon_5_s6, - 0x7705ce: LocationName.sand_canyon_5_s7, - 0x7705cf: LocationName.sand_canyon_5_s8, - 0x7705d0: LocationName.sand_canyon_5_s9, - 0x7705d1: LocationName.sand_canyon_5_s10, - 0x7705d2: LocationName.sand_canyon_5_s11, - 0x7705d3: LocationName.sand_canyon_5_s12, - 0x7705d4: LocationName.sand_canyon_5_s13, - 0x7705d5: LocationName.sand_canyon_5_s14, - 0x7705d6: LocationName.sand_canyon_5_s15, - 0x7705d7: LocationName.sand_canyon_5_s16, - 0x7705d8: LocationName.sand_canyon_5_s17, - 0x7705d9: LocationName.sand_canyon_5_s18, - 0x7705da: LocationName.sand_canyon_5_s19, - 0x7705db: LocationName.sand_canyon_5_s20, - 0x7705dc: LocationName.sand_canyon_5_s21, - 0x7705dd: LocationName.sand_canyon_5_s22, - 0x7705de: LocationName.sand_canyon_5_s23, - 0x7705df: LocationName.sand_canyon_5_s24, - 0x7705e0: LocationName.sand_canyon_5_s25, - 0x7705e1: LocationName.sand_canyon_5_s26, - 0x7705e2: LocationName.sand_canyon_5_s27, - 0x7705e3: LocationName.sand_canyon_5_s28, - 0x7705e4: LocationName.sand_canyon_5_s29, - 0x7705e5: LocationName.sand_canyon_5_s30, - 0x7705e6: LocationName.sand_canyon_5_s31, - 0x7705e7: LocationName.sand_canyon_5_s32, - 0x7705e8: LocationName.sand_canyon_5_s33, - 0x7705e9: LocationName.sand_canyon_5_s34, - 0x7705ea: LocationName.sand_canyon_5_s35, - 0x7705eb: LocationName.sand_canyon_5_s36, - 0x7705ec: LocationName.sand_canyon_5_s37, - 0x7705ed: LocationName.sand_canyon_5_s38, - 0x7705ee: LocationName.sand_canyon_5_s39, - 0x7705ef: LocationName.sand_canyon_5_s40, - 0x7705f0: LocationName.cloudy_park_1_s1, - 0x7705f1: LocationName.cloudy_park_1_s2, - 0x7705f2: LocationName.cloudy_park_1_s3, - 0x7705f3: LocationName.cloudy_park_1_s4, - 0x7705f4: LocationName.cloudy_park_1_s5, - 0x7705f5: LocationName.cloudy_park_1_s6, - 0x7705f6: LocationName.cloudy_park_1_s7, - 0x7705f7: LocationName.cloudy_park_1_s8, - 0x7705f8: LocationName.cloudy_park_1_s9, - 0x7705f9: LocationName.cloudy_park_1_s10, - 0x7705fa: LocationName.cloudy_park_1_s11, - 0x7705fb: LocationName.cloudy_park_1_s12, - 0x7705fc: LocationName.cloudy_park_1_s13, - 0x7705fd: LocationName.cloudy_park_1_s14, - 0x7705fe: LocationName.cloudy_park_1_s15, - 0x7705ff: LocationName.cloudy_park_1_s16, - 0x770600: LocationName.cloudy_park_1_s17, - 0x770601: LocationName.cloudy_park_1_s18, - 0x770602: LocationName.cloudy_park_1_s19, - 0x770603: LocationName.cloudy_park_1_s20, - 0x770604: LocationName.cloudy_park_1_s21, - 0x770605: LocationName.cloudy_park_1_s22, - 0x770606: LocationName.cloudy_park_1_s23, - 0x770607: LocationName.cloudy_park_2_s1, - 0x770608: LocationName.cloudy_park_2_s2, - 0x770609: LocationName.cloudy_park_2_s3, - 0x77060a: LocationName.cloudy_park_2_s4, - 0x77060b: LocationName.cloudy_park_2_s5, - 0x77060c: LocationName.cloudy_park_2_s6, - 0x77060d: LocationName.cloudy_park_2_s7, - 0x77060e: LocationName.cloudy_park_2_s8, - 0x77060f: LocationName.cloudy_park_2_s9, - 0x770610: LocationName.cloudy_park_2_s10, - 0x770611: LocationName.cloudy_park_2_s11, - 0x770612: LocationName.cloudy_park_2_s12, - 0x770613: LocationName.cloudy_park_2_s13, - 0x770614: LocationName.cloudy_park_2_s14, - 0x770615: LocationName.cloudy_park_2_s15, - 0x770616: LocationName.cloudy_park_2_s16, - 0x770617: LocationName.cloudy_park_2_s17, - 0x770618: LocationName.cloudy_park_2_s18, - 0x770619: LocationName.cloudy_park_2_s19, - 0x77061a: LocationName.cloudy_park_2_s20, - 0x77061b: LocationName.cloudy_park_2_s21, - 0x77061c: LocationName.cloudy_park_2_s22, - 0x77061d: LocationName.cloudy_park_2_s23, - 0x77061e: LocationName.cloudy_park_2_s24, - 0x77061f: LocationName.cloudy_park_2_s25, - 0x770620: LocationName.cloudy_park_2_s26, - 0x770621: LocationName.cloudy_park_2_s27, - 0x770622: LocationName.cloudy_park_2_s28, - 0x770623: LocationName.cloudy_park_2_s29, - 0x770624: LocationName.cloudy_park_2_s30, - 0x770625: LocationName.cloudy_park_2_s31, - 0x770626: LocationName.cloudy_park_2_s32, - 0x770627: LocationName.cloudy_park_2_s33, - 0x770628: LocationName.cloudy_park_2_s34, - 0x770629: LocationName.cloudy_park_2_s35, - 0x77062a: LocationName.cloudy_park_2_s36, - 0x77062b: LocationName.cloudy_park_2_s37, - 0x77062c: LocationName.cloudy_park_2_s38, - 0x77062d: LocationName.cloudy_park_2_s39, - 0x77062e: LocationName.cloudy_park_2_s40, - 0x77062f: LocationName.cloudy_park_2_s41, - 0x770630: LocationName.cloudy_park_2_s42, - 0x770631: LocationName.cloudy_park_2_s43, - 0x770632: LocationName.cloudy_park_2_s44, - 0x770633: LocationName.cloudy_park_2_s45, - 0x770634: LocationName.cloudy_park_2_s46, - 0x770635: LocationName.cloudy_park_2_s47, - 0x770636: LocationName.cloudy_park_2_s48, - 0x770637: LocationName.cloudy_park_2_s49, - 0x770638: LocationName.cloudy_park_2_s50, - 0x770639: LocationName.cloudy_park_2_s51, - 0x77063a: LocationName.cloudy_park_2_s52, - 0x77063b: LocationName.cloudy_park_2_s53, - 0x77063c: LocationName.cloudy_park_2_s54, - 0x77063d: LocationName.cloudy_park_3_s1, - 0x77063e: LocationName.cloudy_park_3_s2, - 0x77063f: LocationName.cloudy_park_3_s3, - 0x770640: LocationName.cloudy_park_3_s4, - 0x770641: LocationName.cloudy_park_3_s5, - 0x770642: LocationName.cloudy_park_3_s6, - 0x770643: LocationName.cloudy_park_3_s7, - 0x770644: LocationName.cloudy_park_3_s8, - 0x770645: LocationName.cloudy_park_3_s9, - 0x770646: LocationName.cloudy_park_3_s10, - 0x770647: LocationName.cloudy_park_3_s11, - 0x770648: LocationName.cloudy_park_3_s12, - 0x770649: LocationName.cloudy_park_3_s13, - 0x77064a: LocationName.cloudy_park_3_s14, - 0x77064b: LocationName.cloudy_park_3_s15, - 0x77064c: LocationName.cloudy_park_3_s16, - 0x77064d: LocationName.cloudy_park_3_s17, - 0x77064e: LocationName.cloudy_park_3_s18, - 0x77064f: LocationName.cloudy_park_3_s19, - 0x770650: LocationName.cloudy_park_3_s20, - 0x770651: LocationName.cloudy_park_3_s21, - 0x770652: LocationName.cloudy_park_3_s22, - 0x770653: LocationName.cloudy_park_4_s1, - 0x770654: LocationName.cloudy_park_4_s2, - 0x770655: LocationName.cloudy_park_4_s3, - 0x770656: LocationName.cloudy_park_4_s4, - 0x770657: LocationName.cloudy_park_4_s5, - 0x770658: LocationName.cloudy_park_4_s6, - 0x770659: LocationName.cloudy_park_4_s7, - 0x77065a: LocationName.cloudy_park_4_s8, - 0x77065b: LocationName.cloudy_park_4_s9, - 0x77065c: LocationName.cloudy_park_4_s10, - 0x77065d: LocationName.cloudy_park_4_s11, - 0x77065e: LocationName.cloudy_park_4_s12, - 0x77065f: LocationName.cloudy_park_4_s13, - 0x770660: LocationName.cloudy_park_4_s14, - 0x770661: LocationName.cloudy_park_4_s15, - 0x770662: LocationName.cloudy_park_4_s16, - 0x770663: LocationName.cloudy_park_4_s17, - 0x770664: LocationName.cloudy_park_4_s18, - 0x770665: LocationName.cloudy_park_4_s19, - 0x770666: LocationName.cloudy_park_4_s20, - 0x770667: LocationName.cloudy_park_4_s21, - 0x770668: LocationName.cloudy_park_4_s22, - 0x770669: LocationName.cloudy_park_4_s23, - 0x77066a: LocationName.cloudy_park_4_s24, - 0x77066b: LocationName.cloudy_park_4_s25, - 0x77066c: LocationName.cloudy_park_4_s26, - 0x77066d: LocationName.cloudy_park_4_s27, - 0x77066e: LocationName.cloudy_park_4_s28, - 0x77066f: LocationName.cloudy_park_4_s29, - 0x770670: LocationName.cloudy_park_4_s30, - 0x770671: LocationName.cloudy_park_4_s31, - 0x770672: LocationName.cloudy_park_4_s32, - 0x770673: LocationName.cloudy_park_4_s33, - 0x770674: LocationName.cloudy_park_4_s34, - 0x770675: LocationName.cloudy_park_4_s35, - 0x770676: LocationName.cloudy_park_4_s36, - 0x770677: LocationName.cloudy_park_4_s37, - 0x770678: LocationName.cloudy_park_4_s38, - 0x770679: LocationName.cloudy_park_4_s39, - 0x77067a: LocationName.cloudy_park_4_s40, - 0x77067b: LocationName.cloudy_park_4_s41, - 0x77067c: LocationName.cloudy_park_4_s42, - 0x77067d: LocationName.cloudy_park_4_s43, - 0x77067e: LocationName.cloudy_park_4_s44, - 0x77067f: LocationName.cloudy_park_4_s45, - 0x770680: LocationName.cloudy_park_4_s46, - 0x770681: LocationName.cloudy_park_4_s47, - 0x770682: LocationName.cloudy_park_4_s48, - 0x770683: LocationName.cloudy_park_4_s49, - 0x770684: LocationName.cloudy_park_4_s50, - 0x770685: LocationName.cloudy_park_5_s1, - 0x770686: LocationName.cloudy_park_5_s2, - 0x770687: LocationName.cloudy_park_5_s3, - 0x770688: LocationName.cloudy_park_5_s4, - 0x770689: LocationName.cloudy_park_5_s5, - 0x77068a: LocationName.cloudy_park_5_s6, - 0x77068b: LocationName.cloudy_park_6_s1, - 0x77068c: LocationName.cloudy_park_6_s2, - 0x77068d: LocationName.cloudy_park_6_s3, - 0x77068e: LocationName.cloudy_park_6_s4, - 0x77068f: LocationName.cloudy_park_6_s5, - 0x770690: LocationName.cloudy_park_6_s6, - 0x770691: LocationName.cloudy_park_6_s7, - 0x770692: LocationName.cloudy_park_6_s8, - 0x770693: LocationName.cloudy_park_6_s9, - 0x770694: LocationName.cloudy_park_6_s10, - 0x770695: LocationName.cloudy_park_6_s11, - 0x770696: LocationName.cloudy_park_6_s12, - 0x770697: LocationName.cloudy_park_6_s13, - 0x770698: LocationName.cloudy_park_6_s14, - 0x770699: LocationName.cloudy_park_6_s15, - 0x77069a: LocationName.cloudy_park_6_s16, - 0x77069b: LocationName.cloudy_park_6_s17, - 0x77069c: LocationName.cloudy_park_6_s18, - 0x77069d: LocationName.cloudy_park_6_s19, - 0x77069e: LocationName.cloudy_park_6_s20, - 0x77069f: LocationName.cloudy_park_6_s21, - 0x7706a0: LocationName.cloudy_park_6_s22, - 0x7706a1: LocationName.cloudy_park_6_s23, - 0x7706a2: LocationName.cloudy_park_6_s24, - 0x7706a3: LocationName.cloudy_park_6_s25, - 0x7706a4: LocationName.cloudy_park_6_s26, - 0x7706a5: LocationName.cloudy_park_6_s27, - 0x7706a6: LocationName.cloudy_park_6_s28, - 0x7706a7: LocationName.cloudy_park_6_s29, - 0x7706a8: LocationName.cloudy_park_6_s30, - 0x7706a9: LocationName.cloudy_park_6_s31, - 0x7706aa: LocationName.cloudy_park_6_s32, - 0x7706ab: LocationName.cloudy_park_6_s33, - 0x7706ac: LocationName.iceberg_1_s1, - 0x7706ad: LocationName.iceberg_1_s2, - 0x7706ae: LocationName.iceberg_1_s3, - 0x7706af: LocationName.iceberg_1_s4, - 0x7706b0: LocationName.iceberg_1_s5, - 0x7706b1: LocationName.iceberg_1_s6, - 0x7706b2: LocationName.iceberg_2_s1, - 0x7706b3: LocationName.iceberg_2_s2, - 0x7706b4: LocationName.iceberg_2_s3, - 0x7706b5: LocationName.iceberg_2_s4, - 0x7706b6: LocationName.iceberg_2_s5, - 0x7706b7: LocationName.iceberg_2_s6, - 0x7706b8: LocationName.iceberg_2_s7, - 0x7706b9: LocationName.iceberg_2_s8, - 0x7706ba: LocationName.iceberg_2_s9, - 0x7706bb: LocationName.iceberg_2_s10, - 0x7706bc: LocationName.iceberg_2_s11, - 0x7706bd: LocationName.iceberg_2_s12, - 0x7706be: LocationName.iceberg_2_s13, - 0x7706bf: LocationName.iceberg_2_s14, - 0x7706c0: LocationName.iceberg_2_s15, - 0x7706c1: LocationName.iceberg_2_s16, - 0x7706c2: LocationName.iceberg_2_s17, - 0x7706c3: LocationName.iceberg_2_s18, - 0x7706c4: LocationName.iceberg_2_s19, - 0x7706c5: LocationName.iceberg_3_s1, - 0x7706c6: LocationName.iceberg_3_s2, - 0x7706c7: LocationName.iceberg_3_s3, - 0x7706c8: LocationName.iceberg_3_s4, - 0x7706c9: LocationName.iceberg_3_s5, - 0x7706ca: LocationName.iceberg_3_s6, - 0x7706cb: LocationName.iceberg_3_s7, - 0x7706cc: LocationName.iceberg_3_s8, - 0x7706cd: LocationName.iceberg_3_s9, - 0x7706ce: LocationName.iceberg_3_s10, - 0x7706cf: LocationName.iceberg_3_s11, - 0x7706d0: LocationName.iceberg_3_s12, - 0x7706d1: LocationName.iceberg_3_s13, - 0x7706d2: LocationName.iceberg_3_s14, - 0x7706d3: LocationName.iceberg_3_s15, - 0x7706d4: LocationName.iceberg_3_s16, - 0x7706d5: LocationName.iceberg_3_s17, - 0x7706d6: LocationName.iceberg_3_s18, - 0x7706d7: LocationName.iceberg_3_s19, - 0x7706d8: LocationName.iceberg_3_s20, - 0x7706d9: LocationName.iceberg_3_s21, - 0x7706da: LocationName.iceberg_4_s1, - 0x7706db: LocationName.iceberg_4_s2, - 0x7706dc: LocationName.iceberg_4_s3, - 0x7706dd: LocationName.iceberg_5_s1, - 0x7706de: LocationName.iceberg_5_s2, - 0x7706df: LocationName.iceberg_5_s3, - 0x7706e0: LocationName.iceberg_5_s4, - 0x7706e1: LocationName.iceberg_5_s5, - 0x7706e2: LocationName.iceberg_5_s6, - 0x7706e3: LocationName.iceberg_5_s7, - 0x7706e4: LocationName.iceberg_5_s8, - 0x7706e5: LocationName.iceberg_5_s9, - 0x7706e6: LocationName.iceberg_5_s10, - 0x7706e7: LocationName.iceberg_5_s11, - 0x7706e8: LocationName.iceberg_5_s12, - 0x7706e9: LocationName.iceberg_5_s13, - 0x7706ea: LocationName.iceberg_5_s14, - 0x7706eb: LocationName.iceberg_5_s15, - 0x7706ec: LocationName.iceberg_5_s16, - 0x7706ed: LocationName.iceberg_5_s17, - 0x7706ee: LocationName.iceberg_5_s18, - 0x7706ef: LocationName.iceberg_5_s19, - 0x7706f0: LocationName.iceberg_5_s20, - 0x7706f1: LocationName.iceberg_5_s21, - 0x7706f2: LocationName.iceberg_5_s22, - 0x7706f3: LocationName.iceberg_5_s23, - 0x7706f4: LocationName.iceberg_5_s24, - 0x7706f5: LocationName.iceberg_5_s25, - 0x7706f6: LocationName.iceberg_5_s26, - 0x7706f7: LocationName.iceberg_5_s27, - 0x7706f8: LocationName.iceberg_5_s28, - 0x7706f9: LocationName.iceberg_5_s29, - 0x7706fa: LocationName.iceberg_5_s30, - 0x7706fb: LocationName.iceberg_5_s31, - 0x7706fc: LocationName.iceberg_5_s32, - 0x7706fd: LocationName.iceberg_5_s33, - 0x7706fe: LocationName.iceberg_5_s34, - 0x7706ff: LocationName.iceberg_6_s1, - -} - -location_table = { - **stage_locations, - **heart_star_locations, - **boss_locations, - **consumable_locations, - **star_locations -} diff --git a/worlds/kdl3/Rom.py b/worlds/kdl3/Rom.py deleted file mode 100644 index 5a846ab8be..0000000000 --- a/worlds/kdl3/Rom.py +++ /dev/null @@ -1,577 +0,0 @@ -import typing -from pkgutil import get_data - -import Utils -from typing import Optional, TYPE_CHECKING -import hashlib -import os -import struct - -import settings -from worlds.Files import APDeltaPatch -from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ - get_gooey_palette -from .Compression import hal_decompress -import bsdiff4 - -if TYPE_CHECKING: - from . import KDL3World - -KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" -KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" - -level_pointers = { - 0x770001: 0x0084, - 0x770002: 0x009C, - 0x770003: 0x00B8, - 0x770004: 0x00D8, - 0x770005: 0x0104, - 0x770006: 0x0124, - 0x770007: 0x014C, - 0x770008: 0x0170, - 0x770009: 0x0190, - 0x77000A: 0x01B0, - 0x77000B: 0x01E8, - 0x77000C: 0x0218, - 0x77000D: 0x024C, - 0x77000E: 0x0270, - 0x77000F: 0x02A0, - 0x770010: 0x02C4, - 0x770011: 0x02EC, - 0x770012: 0x0314, - 0x770013: 0x03CC, - 0x770014: 0x0404, - 0x770015: 0x042C, - 0x770016: 0x044C, - 0x770017: 0x0478, - 0x770018: 0x049C, - 0x770019: 0x04E4, - 0x77001A: 0x0504, - 0x77001B: 0x0530, - 0x77001C: 0x0554, - 0x77001D: 0x05A8, - 0x77001E: 0x0640, - 0x770200: 0x0148, - 0x770201: 0x0248, - 0x770202: 0x03C8, - 0x770203: 0x04E0, - 0x770204: 0x06A4, - 0x770205: 0x06A8, -} - -bb_bosses = { - 0x770200: 0xED85F1, - 0x770201: 0xF01360, - 0x770202: 0xEDA3DF, - 0x770203: 0xEDC2B9, - 0x770204: 0xED7C3F, - 0x770205: 0xEC29D2, -} - -level_sprites = { - 0x19B2C6: 1827, - 0x1A195C: 1584, - 0x19F6F3: 1679, - 0x19DC8B: 1717, - 0x197900: 1872 -} - -stage_tiles = { - 0: [ - 0, 1, 2, - 16, 17, 18, - 32, 33, 34, - 48, 49, 50 - ], - 1: [ - 3, 4, 5, - 19, 20, 21, - 35, 36, 37, - 51, 52, 53 - ], - 2: [ - 6, 7, 8, - 22, 23, 24, - 38, 39, 40, - 54, 55, 56 - ], - 3: [ - 9, 10, 11, - 25, 26, 27, - 41, 42, 43, - 57, 58, 59, - ], - 4: [ - 12, 13, 64, - 28, 29, 65, - 44, 45, 66, - 60, 61, 67 - ], - 5: [ - 14, 15, 68, - 30, 31, 69, - 46, 47, 70, - 62, 63, 71 - ] -} - -heart_star_address = 0x2D0000 -heart_star_size = 456 -consumable_address = 0x2F91DD -consumable_size = 698 - -stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] - -music_choices = [ - 2, # Boss 1 - 3, # Boss 2 (Unused) - 4, # Boss 3 (Miniboss) - 7, # Dedede - 9, # Event 2 (used once) - 10, # Field 1 - 11, # Field 2 - 12, # Field 3 - 13, # Field 4 - 14, # Field 5 - 15, # Field 6 - 16, # Field 7 - 17, # Field 8 - 18, # Field 9 - 19, # Field 10 - 20, # Field 11 - 21, # Field 12 (Gourmet Race) - 23, # Dark Matter in the Hyper Zone - 24, # Zero - 25, # Level 1 - 26, # Level 2 - 27, # Level 4 - 28, # Level 3 - 29, # Heart Star Failed - 30, # Level 5 - 31, # Minigame - 38, # Animal Friend 1 - 39, # Animal Friend 2 - 40, # Animal Friend 3 -] -# extra room pointers we don't want to track other than for music -room_pointers = [ - 3079990, # Zero - 2983409, # BB Whispy - 3150688, # BB Acro - 2991071, # BB PonCon - 2998969, # BB Ado - 2980927, # BB Dedede - 2894290 # BB Zero -] - -enemy_remap = { - "Waddle Dee": 0, - "Bronto Burt": 2, - "Rocky": 3, - "Bobo": 5, - "Chilly": 6, - "Poppy Bros Jr.": 7, - "Sparky": 8, - "Polof": 9, - "Broom Hatter": 11, - "Cappy": 12, - "Bouncy": 13, - "Nruff": 15, - "Glunk": 16, - "Togezo": 18, - "Kabu": 19, - "Mony": 20, - "Blipper": 21, - "Squishy": 22, - "Gabon": 24, - "Oro": 25, - "Galbo": 26, - "Sir Kibble": 27, - "Nidoo": 28, - "Kany": 29, - "Sasuke": 30, - "Yaban": 32, - "Boten": 33, - "Coconut": 34, - "Doka": 35, - "Icicle": 36, - "Pteran": 39, - "Loud": 40, - "Como": 41, - "Klinko": 42, - "Babut": 43, - "Wappa": 44, - "Mariel": 45, - "Tick": 48, - "Apolo": 49, - "Popon Ball": 50, - "KeKe": 51, - "Magoo": 53, - "Raft Waddle Dee": 57, - "Madoo": 58, - "Corori": 60, - "Kapar": 67, - "Batamon": 68, - "Peran": 72, - "Bobin": 73, - "Mopoo": 74, - "Gansan": 75, - "Bukiset (Burning)": 76, - "Bukiset (Stone)": 77, - "Bukiset (Ice)": 78, - "Bukiset (Needle)": 79, - "Bukiset (Clean)": 80, - "Bukiset (Parasol)": 81, - "Bukiset (Spark)": 82, - "Bukiset (Cutter)": 83, - "Waddle Dee Drawing": 84, - "Bronto Burt Drawing": 85, - "Bouncy Drawing": 86, - "Kabu (Dekabu)": 87, - "Wapod": 88, - "Propeller": 89, - "Dogon": 90, - "Joe": 91 -} - -miniboss_remap = { - "Captain Stitch": 0, - "Yuki": 1, - "Blocky": 2, - "Jumper Shoot": 3, - "Boboo": 4, - "Haboki": 5 -} - -ability_remap = { - "No Ability": 0, - "Burning Ability": 1, - "Stone Ability": 2, - "Ice Ability": 3, - "Needle Ability": 4, - "Clean Ability": 5, - "Parasol Ability": 6, - "Spark Ability": 7, - "Cutter Ability": 8, -} - - -class RomData: - def __init__(self, file: str, name: typing.Optional[str] = None): - self.file = bytearray() - self.read_from_file(file) - self.name = name - - def read_byte(self, offset: int): - return self.file[offset] - - def read_bytes(self, offset: int, length: int): - return self.file[offset:offset + length] - - def write_byte(self, offset: int, value: int): - self.file[offset] = value - - def write_bytes(self, offset: int, values: typing.Sequence) -> None: - self.file[offset:offset + len(values)] = values - - def write_to_file(self, file: str): - with open(file, 'wb') as outfile: - outfile.write(self.file) - - def read_from_file(self, file: str): - with open(file, 'rb') as stream: - self.file = bytearray(stream.read()) - - def apply_patch(self, patch: bytes): - self.file = bytearray(bsdiff4.patch(bytes(self.file), patch)) - - def write_crc(self): - crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF - inv = crc ^ 0xFFFF - self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) - - -def handle_level_sprites(stages, sprites, palettes): - palette_by_level = list() - for palette in palettes: - palette_by_level.extend(palette[10:16]) - for i in range(5): - for j in range(6): - palettes[i][10 + j] = palette_by_level[stages[i][j] - 1] - palettes[i] = [x for palette in palettes[i] for x in palette] - tiles_by_level = list() - for spritesheet in sprites: - decompressed = hal_decompress(spritesheet) - tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] - tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) - for world in range(5): - levels = [stages[world][x] - 1 for x in range(6)] - world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)] - for i in range(6): - for x in range(12): - world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] - sprites[world] = list() - for tile in world_tiles: - sprites[world].extend(tile) - # insert our fake compression - sprites[world][0:0] = [0xe3, 0xff] - sprites[world][1026:1026] = [0xe3, 0xff] - sprites[world][2052:2052] = [0xe0, 0xff] - sprites[world].append(0xff) - return sprites, palettes - - -def write_heart_star_sprites(rom: RomData): - compressed = rom.read_bytes(heart_star_address, heart_star_size) - decompressed = hal_decompress(compressed) - patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) - patched = bytearray(bsdiff4.patch(decompressed, patch)) - rom.write_bytes(0x1AF7DF, patched) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD000, patched) - rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) - - -def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool): - compressed = rom.read_bytes(consumable_address, consumable_size) - decompressed = hal_decompress(compressed) - patched = bytearray(decompressed) - if consumables: - patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - if stars: - patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD500, patched) - rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) - - -class KDL3DeltaPatch(APDeltaPatch): - hash = [KDL3UHASH, KDL3JHASH] - game = "Kirby's Dream Land 3" - patch_file_ending = ".apkdl3" - - @classmethod - def get_source_data(cls) -> bytes: - return get_base_rom_bytes() - - def patch(self, target: str): - super().patch(target) - rom = RomData(target) - target_language = rom.read_byte(0x3C020) - rom.write_byte(0x7FD9, target_language) - write_heart_star_sprites(rom) - if rom.read_bytes(0x3D014, 1)[0] > 0: - stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] - palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes] - palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] - sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] - sprites, palettes = handle_level_sprites(stages, sprites, palettes) - for addr, palette in zip(stage_palettes, palettes): - rom.write_bytes(addr, palette) - for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): - rom.write_bytes(addr, level_sprite) - rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, - 0x50, 0xC4, 0x39]) - write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0) - rom_name = rom.read_bytes(0x3C000, 21) - rom.write_bytes(0x7FC0, rom_name) - rom.write_crc() - rom.write_to_file(target) - - -def patch_rom(world: "KDL3World", rom: RomData): - rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) - tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat")) - rom.write_bytes(0x3F000, tiles) - - # Write open world patch - if world.options.open_world: - rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ]) - # changes the stage flag function to compare $5AC1 to $5AC1, - # always running the "new stage" function - # This has further checks present for bosses already, so we just - # need to handle regular stages - # write check for boss to be unlocked - - if world.options.consumables: - # reroute maxim tomatoes to use the 1-UP function, then null out the function - rom.write_bytes(0x3002F, [0x37, 0x00]) - rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026 - 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 - 0xA4, 0xD2, # LDY $D2 - 0x6B, # RTL - 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10 - ]) - - # stars handling is built into the rom, so no changes there - - rooms = world.rooms - if world.options.music_shuffle > 0: - if world.options.music_shuffle == 1: - shuffled_music = music_choices.copy() - world.random.shuffle(shuffled_music) - music_map = dict(zip(music_choices, shuffled_music)) - # Avoid putting star twinkle in the pool - music_map[5] = world.random.choice(music_choices) - # Heart Star music doesn't work on regular stages - music_map[8] = world.random.choice(music_choices) - for room in rooms: - room.music = music_map[room.music] - for room in room_pointers: - old_music = rom.read_byte(room + 2) - rom.write_byte(room + 2, music_map[old_music]) - for i in range(5): - # level themes - old_music = rom.read_byte(0x133F2 + i) - rom.write_byte(0x133F2 + i, music_map[old_music]) - # Zero - rom.write_byte(0x9AE79, music_map[0x18]) - # Heart Star success and fail - rom.write_byte(0x4A388, music_map[0x08]) - rom.write_byte(0x4A38D, music_map[0x1D]) - elif world.options.music_shuffle == 2: - for room in rooms: - room.music = world.random.choice(music_choices) - for room in room_pointers: - rom.write_byte(room + 2, world.random.choice(music_choices)) - for i in range(5): - # level themes - rom.write_byte(0x133F2 + i, world.random.choice(music_choices)) - # Zero - rom.write_byte(0x9AE79, world.random.choice(music_choices)) - # Heart Star success and fail - rom.write_byte(0x4A388, world.random.choice(music_choices)) - rom.write_byte(0x4A38D, world.random.choice(music_choices)) - - for room in rooms: - room.patch(rom) - - if world.options.virtual_console in [1, 3]: - # Flash Reduction - rom.write_byte(0x9AE68, 0x10) - rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ]) - rom.write_byte(0x9AEA1, 0x08) - rom.write_byte(0x9AEC9, 0x01) - rom.write_bytes(0x9AED2, [0xA9, 0x1F]) - rom.write_byte(0x9AEE1, 0x08) - - if world.options.virtual_console in [2, 3]: - # Hyper Zone BB colors - rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ]) - rom.write_bytes(0x2C8217, [0xFF, 0x1E, ]) - - # boss requirements - rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], - world.boss_requirements[2], world.boss_requirements[3], - world.boss_requirements[4])) - rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) - rom.write_byte(0x3D00C, world.options.goal_speed.value) - rom.write_byte(0x3D00E, world.options.open_world.value) - rom.write_byte(0x3D010, world.options.death_link.value) - rom.write_byte(0x3D012, world.options.goal.value) - rom.write_byte(0x3D014, world.options.stage_shuffle.value) - rom.write_byte(0x3D016, world.options.ow_boss_requirement.value) - rom.write_byte(0x3D018, world.options.consumables.value) - rom.write_byte(0x3D01A, world.options.starsanity.value) - rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0) - rom.write_byte(0x3D01E, world.options.strict_bosses.value) - # don't write gifting for solo game, since there's no one to send anything to - - for level in world.player_levels: - for i in range(len(world.player_levels[level])): - rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2), - struct.pack("H", level_pointers[world.player_levels[level][i]])) - rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2), - struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) - if (i == 0) or (i > 0 and i % 6 != 0): - rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2), - struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) - - for i in range(6): - if world.boss_butch_bosses[i]: - rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i])) - - # copy ability shuffle - if world.options.copy_ability_randomization.value > 0: - for enemy in world.copy_abilities: - if enemy in miniboss_remap: - rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - else: - rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - # following only needs done on non-door rando - # incredibly lucky this follows the same order (including 5E == star block) - rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - - if world.options.copy_ability_randomization == 2: - for enemy in enemy_remap: - # we just won't include it for minibosses - rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2))) - - # write jumping goal - rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target)) - rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target)) - - from Utils import __version__ - rom.name = bytearray( - f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] - rom.name.extend([0] * (21 - len(rom.name))) - rom.write_bytes(0x3C000, rom.name) - rom.write_byte(0x3C020, world.options.game_language.value) - - # handle palette - if world.options.kirby_flavor_preset.value != 0: - for addr in kirby_target_palettes: - target = kirby_target_palettes[addr] - palette = get_kirby_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - if world.options.gooey_flavor_preset.value != 0: - for addr in gooey_target_palettes: - target = gooey_target_palettes[addr] - palette = get_gooey_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - -def get_base_rom_bytes() -> bytes: - rom_file: str = get_base_rom_path() - base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) - if not base_rom_bytes: - base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) - - basemd5 = hashlib.md5() - basemd5.update(base_rom_bytes) - if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: - raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " - "Get the correct game and version, then dump it") - get_base_rom_bytes.base_rom_bytes = base_rom_bytes - return base_rom_bytes - - -def get_base_rom_path(file_name: str = "") -> str: - options: settings.Settings = settings.get_settings() - if not file_name: - file_name = options["kdl3_options"]["rom_file"] - if not os.path.exists(file_name): - file_name = Utils.user_path(file_name) - return file_name diff --git a/worlds/kdl3/Room.py b/worlds/kdl3/Room.py deleted file mode 100644 index 256955b924..0000000000 --- a/worlds/kdl3/Room.py +++ /dev/null @@ -1,95 +0,0 @@ -import struct -import typing -from BaseClasses import Region, ItemClassification - -if typing.TYPE_CHECKING: - from .Rom import RomData - -animal_map = { - "Rick Spawn": 0, - "Kine Spawn": 1, - "Coo Spawn": 2, - "Nago Spawn": 3, - "ChuChu Spawn": 4, - "Pitch Spawn": 5 -} - - -class KDL3Room(Region): - pointer: int = 0 - level: int = 0 - stage: int = 0 - room: int = 0 - music: int = 0 - default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]] - animal_pointers: typing.List[int] - enemies: typing.List[str] - entity_load: typing.List[typing.List[int]] - consumables: typing.List[typing.Dict[str, typing.Union[int, str]]] - - def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits, - animal_pointers, enemies, entity_load, consumables, consumable_pointer): - super().__init__(name, player, multiworld, hint) - self.level = level - self.stage = stage - self.room = room - self.pointer = pointer - self.music = music - self.default_exits = default_exits - self.animal_pointers = animal_pointers - self.enemies = enemies - self.entity_load = entity_load - self.consumables = consumables - self.consumable_pointer = consumable_pointer - - def patch(self, rom: "RomData"): - rom.write_byte(self.pointer + 2, self.music) - animals = [x.item.name for x in self.locations if "Animal" in x.name] - if len(animals) > 0: - for current_animal, address in zip(animals, self.animal_pointers): - rom.write_byte(self.pointer + address + 7, animal_map[current_animal]) - if self.multiworld.worlds[self.player].options.consumables: - load_len = len(self.entity_load) - for consumable in self.consumables: - location = next(x for x in self.locations if x.name == consumable["name"]) - assert location.item - is_progression = location.item.classification & ItemClassification.progression - if load_len == 8: - # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them - if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) - and any(x in self.entity_load for x in [[2, 22], [3, 22]])): - replacement_target = self.entity_load.index( - next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) - if is_progression: - vtype = 0 - else: - vtype = 2 - rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype) - self.entity_load[replacement_target] = [vtype, 22] - else: - if is_progression: - # we need to see if 1-ups are in our load list - if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): - self.entity_load.append([0, 22]) - else: - if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): - # edge case: if (1, 22) is in, we need to load (3, 22) instead - if [1, 22] in self.entity_load: - self.entity_load.append([3, 22]) - else: - self.entity_load.append([2, 22]) - if load_len < len(self.entity_load): - rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len])) - rom.write_bytes(self.pointer + 104 + (load_len * 2), - bytes(struct.pack("H", self.consumable_pointer))) - if is_progression: - if [1, 22] in self.entity_load: - vtype = 1 - else: - vtype = 0 - else: - if [3, 22] in self.entity_load: - vtype = 3 - else: - vtype = 2 - rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 8c9f3cc46a..12f56a0230 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -1,25 +1,25 @@ import logging -import typing -from BaseClasses import Tutorial, ItemClassification, MultiWorld +from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item from Fill import fill_restrictive from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld -from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ - trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights -from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations -from .Names.AnimalFriendSpawns import animal_friend_spawns -from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive -from .Regions import create_levels, default_levels -from .Options import KDL3Options -from .Presets import kdl3_options_presets -from .Names import LocationName -from .Room import KDL3Room -from .Rules import set_rules -from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH -from .Client import KDL3SNIClient +from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ + trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\ + lookup_item_to_id +from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations +from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets +from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive +from .regions import create_levels, default_levels +from .options import KDL3Options, kdl3_option_groups +from .presets import kdl3_options_presets +from .names import location_name +from .room import KDL3Room +from .rules import set_rules +from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH +from .client import KDL3SNIClient -from typing import Dict, TextIO, Optional, List +from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type import os import math import threading @@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld): ) ] options_presets = kdl3_options_presets + option_groups = kdl3_option_groups class KDL3World(World): @@ -61,35 +62,35 @@ class KDL3World(World): """ game = "Kirby's Dream Land 3" - options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options options: KDL3Options - item_name_to_id = {item: item_table[item].code for item in item_table} + item_name_to_id = lookup_item_to_id location_name_to_id = {location_table[location]: location for location in location_table} item_name_groups = item_names web = KDL3WebWorld() - settings: typing.ClassVar[KDL3Settings] + settings: ClassVar[KDL3Settings] def __init__(self, multiworld: MultiWorld, player: int): - self.rom_name = None + self.rom_name: bytes = bytes() self.rom_name_available_event = threading.Event() super().__init__(multiworld, player) self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() self.required_heart_stars: int = 0 # we fill this during create_items - self.boss_requirements: Dict[int, int] = dict() + self.boss_requirements: List[int] = [] self.player_levels = default_levels.copy() self.stage_shuffle_enabled = False - self.boss_butch_bosses: List[Optional[bool]] = list() - self.rooms: Optional[List[KDL3Room]] = None - - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld) -> None: - rom_file: str = get_base_rom_path() - if not os.path.exists(rom_file): - raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}") + self.boss_butch_bosses: List[Optional[bool]] = [] + self.rooms: List[KDL3Room] = [] create_regions = create_levels - def create_item(self, name: str, force_non_progression=False) -> KDL3Item: + def generate_early(self) -> None: + if self.options.total_heart_stars != -1: + logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. " + f"Please use \"max_heart_stars\" instead.") + self.options.max_heart_stars.value = self.options.total_heart_stars.value + + def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item: item = item_table[name] classification = ItemClassification.filler if item.progression and not force_non_progression: @@ -99,7 +100,7 @@ class KDL3World(World): classification = ItemClassification.trap return KDL3Item(name, classification, item.code, self.player) - def get_filler_item_name(self, include_stars=True) -> str: + def get_filler_item_name(self, include_stars: bool = True) -> str: if include_stars: return self.random.choices(list(total_filler_weights.keys()), weights=list(total_filler_weights.values()))[0] @@ -112,8 +113,8 @@ class KDL3World(World): self.options.slow_trap_weight.value, self.options.ability_trap_weight.value])[0] - def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str], - level: int, stage: int): + def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str], + level: int, stage: int) -> Optional[str]: valid_rooms = [room for room in self.rooms if (room.level < level) or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge valid_enemies = set() @@ -124,6 +125,10 @@ class KDL3World(World): return None # a valid enemy got placed by a more restrictive placement return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies])) + def get_pre_fill_items(self) -> List[Item]: + return [self.create_item(item) + for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]] + def pre_fill(self) -> None: if self.options.copy_ability_randomization: # randomize copy abilities @@ -207,10 +212,32 @@ class KDL3World(World): # If Kine is ever the last animal friend placed, he will cause fill errors on closed world animal_pool.sort() locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] - items = [self.create_item(animal) for animal in animal_pool] - allstate = self.multiworld.get_all_state(False) + items: List[Item] = [self.create_item(animal) for animal in animal_pool] + allstate = CollectionState(self.multiworld) + for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]: + self.collect(allstate, self.create_item(item)) self.random.shuffle(locations) fill_restrictive(self.multiworld, allstate, locations, items, True, True) + + # Need to ensure all of these are unique items, and replace them if they aren't + for spawns in problematic_sets: + placed = [self.get_location(spawn).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + if len(placed_names) != len(placed): + # have a duplicate + animals = [] + for spawn in spawns: + spawn_location = self.get_location(spawn) + if spawn_location.item.name not in animals: + animals.append(spawn_location.item.name) + else: + new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn", + "ChuChu Spawn", "Nago Spawn", "Pitch Spawn"] + if x not in placed_names and x not in animals]) + spawn_location.item = None + spawn_location.place_locked_item(self.create_item(new_animal)) + animals.append(new_animal) + # logically, this should be sound pre-ER. May need to adjust around it with ER in the future else: animal_friends = animal_friend_spawns.copy() for animal in animal_friends: @@ -225,21 +252,20 @@ class KDL3World(World): remaining_items = len(location_table) - len(itempool) if not self.options.consumables: remaining_items -= len(consumable_locations) - remaining_items -= len(star_locations) - if self.options.starsanity: - # star fill, keep consumable pool locked to consumable and fill 767 stars specifically - star_items = list(star_item_weights.keys()) - star_weights = list(star_item_weights.values()) - itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights, - k=767)]) - total_heart_stars = self.options.total_heart_stars + if not self.options.starsanity: + remaining_items -= len(star_locations) + max_heart_stars = self.options.max_heart_stars.value + if max_heart_stars > remaining_items: + max_heart_stars = remaining_items # ensure at least 1 heart star required per world - required_heart_stars = max(int(total_heart_stars * required_percentage), 5) - filler_items = total_heart_stars - required_heart_stars - filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0)) - trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0)) - filler_amount -= trap_amount - non_required_heart_stars = filler_items - filler_amount - trap_amount + required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99) + filler_items = remaining_items - required_heart_stars + converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0)) + non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars + filler_items -= non_required_heart_stars + trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0)) + + filler_items -= trap_amount self.required_heart_stars = required_heart_stars # handle boss requirements here requirements = [required_heart_stars] @@ -261,8 +287,8 @@ class KDL3World(World): requirements.insert(i - 1, quotient * i) self.boss_requirements = requirements itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) - itempool.extend([self.create_item(self.get_filler_item_name(False)) - for _ in range(filler_amount + (remaining_items - total_heart_stars))]) + itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value))) + for _ in range(filler_items)]) itempool.extend([self.create_item(self.get_trap_item_name()) for _ in range(trap_amount)]) itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) @@ -273,15 +299,15 @@ class KDL3World(World): self.multiworld.get_location(location_table[self.player_levels[level][stage]] .replace("Complete", "Stage Completion"), self.player) \ .place_locked_item(KDL3Item( - f"{LocationName.level_names_inverse[level]} - Stage Completion", + f"{location_name.level_names_inverse[level]} - Stage Completion", ItemClassification.progression, None, self.player)) set_rules = set_rules def generate_basic(self) -> None: self.stage_shuffle_enabled = self.options.stage_shuffle > 0 - goal = self.options.goal - goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player) + goal = self.options.goal.value + goal_location = self.multiworld.get_location(location_name.goals[goal], self.player) goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player)) for level in range(1, 6): self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ @@ -300,60 +326,65 @@ class KDL3World(World): else: self.boss_butch_bosses = [False for _ in range(6)] - def generate_output(self, output_directory: str): - rom_path = "" + def generate_output(self, output_directory: str) -> None: try: - rom = RomData(get_base_rom_path()) - patch_rom(self, rom) + patch = KDL3ProcedurePatch() + patch_rom(self, patch) - rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") - rom.write_to_file(rom_path) - self.rom_name = rom.name + self.rom_name = patch.name - patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rom_path) - patch.write() + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) except Exception: raise finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected - if os.path.exists(rom_path): - os.unlink(rom_path) - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: Dict[str, Any]) -> None: # wait for self.rom_name to be available. self.rom_name_available_event.wait() + assert isinstance(self.rom_name, bytes) rom_name = getattr(self, "rom_name", None) # we skip in case of error, so that the original error in the output thread is the one that gets raised if rom_name: - new_name = base64.b64encode(bytes(self.rom_name)).decode() + new_name = base64.b64encode(self.rom_name).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + def fill_slot_data(self) -> Mapping[str, Any]: + # UT support + return {"player_levels": self.player_levels} + + def interpret_slot_data(self, slot_data: Mapping[str, Any]): + # UT support + player_levels = {int(key): value for key, value in slot_data["player_levels"].items()} + return {"player_levels": player_levels} + def write_spoiler(self, spoiler_handle: TextIO) -> None: if self.stage_shuffle_enabled: spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") - for level in LocationName.level_names: - for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)): + for level in location_name.level_names: + for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)): spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n") if self.options.animal_randomization: spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") - for level in self.player_levels: + for lvl in self.player_levels: for stage in range(6): - rooms = [room for room in self.rooms if room.level == level and room.stage == stage] + rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage] animals = [] for room in rooms: animals.extend([location.item.name.replace(" Spawn", "") - for location in room.locations if "Animal" in location.name]) - spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}" + for location in room.locations if "Animal" in location.name + and location.item is not None]) + spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}" f": {', '.join(animals)}\n") if self.options.copy_ability_randomization: spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") for enemy in self.copy_abilities: spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") - def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.stage_shuffle_enabled: - regions = {LocationName.level_names[level]: level for level in LocationName.level_names} + regions = {location_name.level_names[level]: level for level in location_name.level_names} level_hint_data = {} for level in regions: for stage in range(7): @@ -361,6 +392,6 @@ class KDL3World(World): self.player).name.replace(" - Complete", "") stage_regions = [room for room in self.rooms if stage_name in room.name] for region in stage_regions: - for location in [location for location in region.locations if location.address]: + for location in [location for location in list(region.get_locations()) if location.address]: level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}" hint_data[self.player] = level_hint_data diff --git a/worlds/kdl3/Aesthetics.py b/worlds/kdl3/aesthetics.py similarity index 91% rename from worlds/kdl3/Aesthetics.py rename to worlds/kdl3/aesthetics.py index 8c7363908f..8b798ff93e 100644 --- a/worlds/kdl3/Aesthetics.py +++ b/worlds/kdl3/aesthetics.py @@ -1,5 +1,9 @@ import struct -from .Options import KirbyFlavorPreset, GooeyFlavorPreset +from .options import KirbyFlavorPreset, GooeyFlavorPreset +from typing import TYPE_CHECKING, Optional, Dict, List, Tuple + +if TYPE_CHECKING: + from . import KDL3World kirby_flavor_presets = { 1: { @@ -223,6 +227,23 @@ kirby_flavor_presets = { "14": "E6E6FA", "15": "976FBD", }, + 14: { + "1": "373B3E", + "2": "98d5d3", + "3": "1aa5ab", + "4": "168f95", + "5": "4f5559", + "6": "1dbac2", + "7": "137a7f", + "8": "093a3c", + "9": "86cecb", + "10": "a0afbc", + "11": "62bfbb", + "12": "50b8b4", + "13": "bec8d1", + "14": "bce4e2", + "15": "91a2b1", + } } gooey_flavor_presets = { @@ -398,21 +419,21 @@ gooey_target_palettes = { } -def get_kirby_palette(world): +def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.kirby_flavor_preset.value if palette == KirbyFlavorPreset.option_custom: return world.options.kirby_flavor.value return kirby_flavor_presets.get(palette, None) -def get_gooey_palette(world): +def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.gooey_flavor_preset.value if palette == GooeyFlavorPreset.option_custom: return world.options.gooey_flavor.value return gooey_flavor_presets.get(palette, None) -def rgb888_to_bgr555(red, green, blue) -> bytes: +def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes: red = red >> 3 green = green >> 3 blue = blue >> 3 @@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes: return struct.pack("H", outcol) -def get_palette_bytes(palette, target, offset, factor): +def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes: output_data = bytearray() for color in target: hexcol = palette[color] if hexcol.startswith("#"): hexcol = hexcol.replace("#", "") colint = int(hexcol, 16) - col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) + col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) col = tuple(int(int(factor*x) + offset) for x in col) byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) output_data.extend(bytearray(byte_data)) - return output_data + return bytes(output_data) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/client.py similarity index 90% rename from worlds/kdl3/Client.py rename to worlds/kdl3/client.py index 1ca21d550e..97bf68cbd9 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/client.py @@ -11,13 +11,13 @@ from MultiServer import mark_raw from NetUtils import ClientStatus, color from Utils import async_start from worlds.AutoSNIClient import SNIClient -from .Locations import boss_locations -from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes -from .ClientAddrs import consumable_addrs, star_addrs +from .locations import boss_locations +from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes +from .client_addrs import consumable_addrs, star_addrs from typing import TYPE_CHECKING if TYPE_CHECKING: - from SNIClient import SNIClientCommandProcessor + from SNIClient import SNIClientCommandProcessor, SNIContext snes_logger = logging.getLogger("SNES") @@ -81,17 +81,16 @@ deathlink_messages = defaultdict(lambda: " was defeated.", { @mark_raw -def cmd_gift(self: "SNIClientCommandProcessor"): +def cmd_gift(self: "SNIClientCommandProcessor") -> None: """Toggles gifting for the current game.""" - if not getattr(self.ctx, "gifting", None): - self.ctx.gifting = True - else: - self.ctx.gifting = not self.ctx.gifting - self.output(f"Gifting set to {self.ctx.gifting}") + handler = self.ctx.client_handler + assert isinstance(handler, KDL3SNIClient) + handler.gifting = not handler.gifting + self.output(f"Gifting set to {handler.gifting}") async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { f"{self.ctx.slot}": { - "IsOpen": self.ctx.gifting, + "IsOpen": handler.gifting, **kdl3_gifting_options } })) @@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"): class KDL3SNIClient(SNIClient): game = "Kirby's Dream Land 3" patch_suffix = ".apkdl3" - levels = None - consumables = None - stars = None - item_queue: typing.List = [] - initialize_gifting = False + levels: typing.Dict[int, typing.List[int]] = {} + consumables: typing.Optional[bool] = None + stars: typing.Optional[bool] = None + item_queue: typing.List[int] = [] + initialize_gifting: bool = False + gifting: bool = False giftbox_key: str = "" motherbox_key: str = "" client_random: random.Random = random.Random() - async def deathlink_kill_player(self, ctx) -> None: + async def deathlink_kill_player(self, ctx: "SNIContext") -> None: from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) if game_state[0] == 0xFF: @@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient): ctx.death_state = DeathState.dead ctx.last_death_link = time.time() - async def validate_rom(self, ctx) -> bool: + async def validate_rom(self, ctx: "SNIContext") -> bool: from SNIClient import snes_read rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3": @@ -141,7 +141,7 @@ class KDL3SNIClient(SNIClient): ctx.game = self.game ctx.rom = rom_name - ctx.items_handling = 0b111 # always remote items + ctx.items_handling = 0b101 # default local items with remote start inventory ctx.allow_collect = True if "gift" not in ctx.command_processor.commands: ctx.command_processor.commands["gift"] = cmd_gift @@ -149,9 +149,10 @@ class KDL3SNIClient(SNIClient): death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1) if death_link: await ctx.update_death_link(bool(death_link[0] & 0b1)) + ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled return True - async def pop_item(self, ctx, in_stage): + async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None: from SNIClient import snes_buffered_write, snes_read if len(self.item_queue) > 0: item = self.item_queue.pop() @@ -168,8 +169,8 @@ class KDL3SNIClient(SNIClient): else: self.item_queue.append(item) # no more slots, get it next go around - async def pop_gift(self, ctx): - if ctx.stored_data[self.giftbox_key]: + async def pop_gift(self, ctx: "SNIContext") -> None: + if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]: from SNIClient import snes_read, snes_buffered_write key, gift = ctx.stored_data[self.giftbox_key].popitem() await pop_object(ctx, self.giftbox_key, key) @@ -214,7 +215,7 @@ class KDL3SNIClient(SNIClient): quality = min(10, quality * 2) else: # it's not really edible, but he'll eat it anyway - quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0] + quality = self.client_random.choices(range(0, 2), [75, 25])[0] kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) @@ -224,7 +225,8 @@ class KDL3SNIClient(SNIClient): else: snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) - async def pick_gift_recipient(self, ctx, gift): + async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None: + assert ctx.slot if gift != 4: gift_base = kdl3_gifts[gift] else: @@ -238,7 +240,7 @@ class KDL3SNIClient(SNIClient): if desire > most_applicable: most_applicable = desire most_applicable_slot = int(slot) - elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]: + elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]: # only send to ourselves if no one else will take it most_applicable_slot = int(slot) # print(most_applicable, most_applicable_slot) @@ -257,7 +259,7 @@ class KDL3SNIClient(SNIClient): item_uuid: item, }) - async def game_watcher(self, ctx) -> None: + async def game_watcher(self, ctx: "SNIContext") -> None: try: from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) @@ -278,11 +280,12 @@ class KDL3SNIClient(SNIClient): await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0])) self.initialize_gifting = True # can't check debug anymore, without going and copying the value. might be important later. - if self.levels is None: + if not self.levels: self.levels = dict() for i in range(5): level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) - self.levels[i] = unpack("HHHHHHH", level_data) + self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little") + for idx in range(0, len(level_data), 2)] self.levels[5] = [0x0205, # Hyper Zone 0, # MG-5, can't send from here 0x0300, # Boss Butch @@ -371,7 +374,7 @@ class KDL3SNIClient(SNIClient): stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) for i in range(30): - loc_id = 0x770000 + i + 1 + loc_id = 0x770000 + i if stages[i] == 1 and loc_id not in ctx.checked_locations: new_checks.append(loc_id) elif loc_id in ctx.checked_locations: @@ -381,8 +384,8 @@ class KDL3SNIClient(SNIClient): heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) for i in range(5): start_ind = i * 7 - for j in range(1, 7): - level_ind = start_ind + j - 1 + for j in range(6): + level_ind = start_ind + j loc_id = 0x770100 + (6 * i) + j if heart_stars[level_ind] and loc_id not in ctx.checked_locations: new_checks.append(loc_id) @@ -401,6 +404,9 @@ class KDL3SNIClient(SNIClient): if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01: new_checks.append(star) + if not game_state: + return + if game_state[0] != 0xFF: await self.pop_gift(ctx) await self.pop_item(ctx, game_state[0] != 0xFF) @@ -408,7 +414,7 @@ class KDL3SNIClient(SNIClient): # boss status boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) - boss_flag = unpack("H", boss_flag_bytes)[0] + boss_flag = int.from_bytes(boss_flag_bytes, "little") for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()): if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations: new_checks.append(boss) diff --git a/worlds/kdl3/ClientAddrs.py b/worlds/kdl3/client_addrs.py similarity index 100% rename from worlds/kdl3/ClientAddrs.py rename to worlds/kdl3/client_addrs.py diff --git a/worlds/kdl3/Compression.py b/worlds/kdl3/compression.py similarity index 100% rename from worlds/kdl3/Compression.py rename to worlds/kdl3/compression.py diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 index cd002121cd38dcb319ba2148ced46c9592c3905b..3b6b338d5a92ccec33693c644e73d13b15dbce46 100644 GIT binary patch literal 2646 zcmYM0dpOgJAIHBl&6Kej!zPWGSx$zL4oA0;#E^`}G z@=Gp-3TKo{ooTC7L@L*Gq;yi^*YEc`&vV{?z5n>UKcDCGJg-0AbdHC&rzeH{2Kc{X zH2yXK^?!yQ9O-0F+e_f=jTypO06^2~>i5L)jaPT-!TP8~5bprsJJ2K&m9<6(4@f}` zD%gPs#A4Yen`uNDjAu-n0k~086>S5|$GxE~mD}}?7IDIl4BykQJlEt}6XDl0XT8RELEq%`#bdEK+LHA#HGWeP9E<)u zECB4mR%{!8&#GRlk7$E(ssH@w_mH*`0JK+ES65 zp@E6|s252Q>pLLr2(f}gDCaX6NIfznGADBcI)&xZAnzv{WrtiK{1w=hX(J&cNtU*h7t_FblQN0)u4hU!@}JkY2Yn zW5a5{jF&sxdrVLRDgy&?pHQ0Aq}}4?91l%Er@a|$J{h?AJEw)&B&qn&JTaPB_cql> zt8IO7&e=0NGBjL`g&DP@;cf;#z`~IdZYbK>CGJ6B@w`111dAQ%mNnSgdhfB^DgV!1 zs?G;jsvR0(oBVC3%GVrT^viXL67Q!@hcFl(UT+Q@+Jn+6(;IE+HrrG35FI46&1Mc4 zpZ!e>TpG}-mMz6^((#GLHT;=UC$zPHRg^csLEszPe^?s3W5KOzDM5mDi5t6<{7!jD znATy$mhWjT!P_G#{#XO#ah@{ND$@&NFdYzV&g9=XeFxgAU>Gv93Nk29FnPX>`!=R9 z+fxurxovB1YC-2f5GMQ}Qh!wAt(?pTK7ZoKLq?>74T*oz?GO5knxXqMz z@EUPIlL~CMN|@kyXEjJ(t@J&6HZp9B5_n=7*%2WZSE{h)1i z^smKlNlVKQCC7Nw_*MQ`(#O>?!k5*RuS=^*ZA#7WBaw?QtG3*pd;N#atQuRW@2}4- z;Sd(LXFZ0Ma31l|3^hNkz}-teM|aM7Q^?Qa_{sCAi&!j7Aa#}k6j)j^tpaOZ12}D` z*|f`F`*D!kGjQsJ2f{urRi(h8$w0x1HZM8c?8guizs=aD~=pDuCV z$S^OHE+XVor4k^6k${q4@FNcc`5-`n%;iV7oe+M`GWB5DTgrVdg`eE_ zU#{^mK?Wp82CAq^0LT6Ty-o=`c>dH?suhNk+tn-Z4mVD&8$8Brt$~A}FWkOz@%VFn+`jcOEv4QV_d_}_nk<`slcqooyk!P!&2)P%Ke^#D zKs2w;yuf$JSkPz|yaCqTfajf&CPrCQ<(^B2WN5}&R%#PV%D(o)F9j3XU7Div3}2(` zcIFS)loZ#{A9yx#>;G-{?lNs;uCR!iYX0~XG5uFtJHTe(Ad~+*Ix6R?xkmWeKgqYD)%X;5R2k*%~<#dPK z=?a$|Wu{u%xqi5)Th}n;do8{_*XQt1MH84ijR@n4ZgO1<(Dl%%Zo0uMsntmX2;S6G z8`Vw!D0+ zx!`ez;#SmEpoO>VYAUo(C>PzqVdnx9n&a!lq=fbhlbJL5givj#2x_}}AnRa5MEb3_ zsGo1mjy2lVm6`|(>q^l6G;)y^#9Gq^j>tSkZ%fS>30OO0JMCxRqYa#QcRE{8+szc$Z)=&dkoW^q?=@5$^ddWPD&{$Ub4B;ZY;V#QXI+%7K)qLgA5paUnt;t7c2}+4 z*MBUvpjsxrejuPC^BvDiu>{j40J=}gA>qr}sDoS2qlV(g7ud@KJ?7Eqr#5YSnGzqO z@0i%A2Zu3>Yj?wb&OM3Q@0f1BU@f3~BXaQ9qk~&W5_6-&h7r=oJ9L{ZW&XQfq|Vpp zH1E3L(bv^Zt*{SB)mR3$63}o9H)1bipyc$yHjVo{F=2hZ|H-3Nt*xI2?YhF;M?H!w z_NmH0R_l%B?;ws=M@B|J$!_Q<-EQa`RiEwGTWr1Y*A~;*fh!k|qjU*|x#jmKZZ1B5 zjeL=Qjy^Wx#zJUsg&~ieaWOI^{;Q@lt|}nwr2b!Amo^4A?f9CsE@)>#y6Dr>uDhIt z2_5$6OY*35T~y$)m91>E{)p>)vxy|c4n zCKdgCkQ?huXfcGd}N6;FvkjXAL;gnmys4Y_&g}csYI|(;6OK z_)NZMq<3svP+R|tWY#Wr<-(J;YR6`ivf9t)=9Lwu_y|7@x-?lxY?#AT8lH`fxOr~- zAwg2shUSE4rojqLdMY=6l;q?WU>!T!rKAP*T3!cEWYw9siltk0O;XHP{_4E<(w$3s zIV7X9P?$@}7eraQVqv`Y^20_guoMnD@;GV2``wws$7~iU%#_3K0OguOa!Mn{snaLS z*fXe-UX6=H%X#0@%Pz*ffZrW{c6VoQ;iTDHfg&3#(!ob0HiXZvT3NBYwm501xcTwS zX6KP>o8YAjmMR?uCF0!icgxn~bu}v+$}jB?Tm_+0%m~OpqL9; zgG*x3sO6egnoELZ;?km(d8cKxS*|&Vnx(hy+;i@|&xiLs=lSxU=fm^!Vt5Dm`hvt+ z;4gK-|6>5szm3DJ=B#sdog|1`*Kw8fF$CWo*=8AqXpfU|e7&|Bs0sy8YKnsBc zm>6V=A%Fq+ZVMIWpnBYELjd0cMUxQeD198#!~;>sqeG-BXGAWHu3hEfr(Gi1$doc| zpSI_x$dgUvB4++zM!@a*%k=bM@y~y}wYo-Jl|H#%BY8AW5K*Uca|8T~XpJ#MyNU_DdEA+EuYv#eq;K#4(i54p{zcC}udOe=f6&mZ$@2AF zTgYkrb4gR9UR`T`m2>Jx6>Jx;;lZ`GhaJL%2^I=;){!{t+%N}W1TIL#o#SEp?51b{ z4uH7UC_r4HYgL{H1)k|bOc*UrKy73!f{fwd9={T{??rD#5`WRdh%BpDDY@*+b>vp< z`b7jCagl)?1rQQ*1cw76D!_i%8Vc?5W2CIg$avAoRU9x^G3` zP@e*TXi@K%|5z*qvyf}y?+hvq1uH;6g(9BI&qe5nlBDApl^F;m#G5BTxMnExL?AH{ zkx$D70HsucrczylVH1i(6o4oM5MRRo*G5Faxhre41I{LKZ|ulRHaLBJJn=Km%pPu9 z1=p$IOgbUXqbmSeFIxLYT_IZ1H#{}Ng%vo4He9H@uD&(& zVwc4me!yOg)QwLv4rht&nj)A@Hc|Xgd&+Z16y&Xxh&|+!dyxM}e0n=P`*T|JGC1HC z(y}L4-?x8odsO?tCC0|889cbB{(c5Pj!#KWEtRqkwb@So5&kgf4ECRlEQW%ziqg-x z%RDJh$5b!y$Uw23ayUAFEg#bYMIyjQ33n{F<1Jcd*b+OVJZA1ddZ8T5gS zxyy_z9k*)bvzxA7G&J+S4DHX(LHo7dv)TTc=Nnn2GvBkHYVlr}7E1SQ>6lL#83ymI z?Q4zK{I(aBk8LCwq}!}vTn|5I?dn^o%YIN~XyxTn5Rw!-Y`$yKSKLxaXmamL{wSU0 zdVLolmu>40eVQ>DR!09dGUz5}w4rpROMiPjWyh79sGyxJ`EuRgLG31o*-$NBX$fuG za;Y(+FX_|MYH8o<@*S7!Jo?4C*mt=ve8zQ=o`*<$3>rt9(Rc7C-*xPm>&wpS)5(v^ z1-EfJ9eQ~`h6N6M>V6(vu3l^WNp8=s9T>4|d2f8+=?k>P4tbxujzqmN>bE#A6zRDU z3HaE{TbmM0g!E=Bt7M4Uv^LvbujKrVLd!7fh2RyLZm}cl+j7H|cIo8YpWoCHQ#ER%;T6_oW#6i%%V-;ZL4Psb{7J-u=k5C zg?rB^;k1y){<{JrnH9!Gnxw8LkpZjUrEKfsM!e&;No4ahLu)_o`Lgzavn!QbZfhRS z4MiE8vNWHrhNSI7$oz&Xhty0C4A#QOw@Y|Nf--d0;Pt69oMQbzQ}X=Q;2njhin-7$ zUMGhp@PTf#0$_J)x7Y*@z&w8)Ijn1Cg?L!ubEhLb_DIWm!7bZLyg#lip7=lLIu_qX z10e2S$(8=<&%-OH52*c1niEMUMi2HJdA8nzTV_8?ozKZB>VaNrIL@d5&Fe4{BVP#$ zNPy8KaiDb#9R)z@=zpz#gEy#fVf14eTk7gqJG~Kw`e*CS&A-B%1OOlaK%|L^agvZt zp&p}*!C2M;F|N=&Q31pH=93h_Xx-PVLSR%X6_gQN^uA76%x1CkFV{?1*Gx=Fl0?8F zpn^>}1B);Wph~pB1CW~2lXy!g?Pd6*Z{y@tWQ)IeCrLR^%~*P#3K!Lrz!0o4)P>*W zu0+$J>pQLy5lBb3U|vbvO_v|c5m!4c_bJi*punIMPrB>%n-ug;_|#^-dJpy)YMNkt z5JI}=U3&m88bDl+o45+T!xCSdjsfvw`D}y4tO7DK*n7c(HTemyyMNr|qNV!km;l zD=Z8G%0l9t1$+K+>H7Fwx8T%ZRoC1Y-G7(*y*0~ZhL)-6R5U`o(>T}cAKQT zRtbg8OXaHZ5nM!DZPkiiP<_LNxAaR#b*!V>Q*U5nx^~mf4!^kFc%+l}dF9*RG!jBb zvb85+_N+c=y0rLu=!q$ALgMamYM*$;r{)Lw2CCO_BMrBuRQ5u?dAL&#EkX$&KO;a4 zqWV9SHwD|yciRCG;9=O}?eCBAz&T_%>TCmJq#`CHzNk0+)=`2D3~IYYg#R#32Q zdGj(IQ*ak^;KMv`c2ct3Q~UTG@knc+R)9ef(oshGCTk~_x- R2S2ZGPW5pM7JXgjzX0o>8Uz3U diff --git a/worlds/kdl3/Gifting.py b/worlds/kdl3/gifting.py similarity index 90% rename from worlds/kdl3/Gifting.py rename to worlds/kdl3/gifting.py index 8ccba7ec1a..e162609100 100644 --- a/worlds/kdl3/Gifting.py +++ b/worlds/kdl3/gifting.py @@ -1,8 +1,11 @@ # Small subfile to handle gifting info such as desired traits and giftbox management import typing +if typing.TYPE_CHECKING: + from SNIClient import SNIContext -async def update_object(ctx, key: str, value: typing.Dict): + +async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typing.Any]) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -16,7 +19,7 @@ async def update_object(ctx, key: str, value: typing.Dict): ]) -async def pop_object(ctx, key: str, value: str): +async def pop_object(ctx: "SNIContext", key: str, value: str) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -30,14 +33,14 @@ async def pop_object(ctx, key: str, value: str): ]) -async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool): +async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None: ctx.set_notify(motherbox_key, giftbox_key) await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}": - { - "IsOpen": is_open, - **kdl3_gifting_options - }}) - ctx.gifting = is_open + { + "IsOpen": is_open, + **kdl3_gifting_options + }}) + ctx.client_handler.gifting = is_open kdl3_gifting_options = { diff --git a/worlds/kdl3/Items.py b/worlds/kdl3/items.py similarity index 95% rename from worlds/kdl3/Items.py rename to worlds/kdl3/items.py index 66c7f8fee3..72687a6065 100644 --- a/worlds/kdl3/Items.py +++ b/worlds/kdl3/items.py @@ -77,9 +77,9 @@ filler_item_weights = { } star_item_weights = { - "Little Star": 4, - "Medium Star": 2, - "Big Star": 1 + "Little Star": 16, + "Medium Star": 8, + "Big Star": 4 } total_filler_weights = { @@ -102,4 +102,4 @@ item_names = { "Animal Friend": set(animal_friend_table), } -lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} +lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} diff --git a/worlds/kdl3/locations.py b/worlds/kdl3/locations.py new file mode 100644 index 0000000000..4fa1bfad70 --- /dev/null +++ b/worlds/kdl3/locations.py @@ -0,0 +1,940 @@ +import typing +from BaseClasses import Location, Region +from .names import location_name + +if typing.TYPE_CHECKING: + from .room import KDL3Room + + +class KDL3Location(Location): + game: str = "Kirby's Dream Land 3" + room: typing.Optional["KDL3Room"] = None + + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): + super().__init__(player, name, address, parent) + if not address: + self.show_in_spoiler = False + + +stage_locations = { + 0x770000: location_name.grass_land_1, + 0x770001: location_name.grass_land_2, + 0x770002: location_name.grass_land_3, + 0x770003: location_name.grass_land_4, + 0x770004: location_name.grass_land_5, + 0x770005: location_name.grass_land_6, + 0x770006: location_name.ripple_field_1, + 0x770007: location_name.ripple_field_2, + 0x770008: location_name.ripple_field_3, + 0x770009: location_name.ripple_field_4, + 0x77000A: location_name.ripple_field_5, + 0x77000B: location_name.ripple_field_6, + 0x77000C: location_name.sand_canyon_1, + 0x77000D: location_name.sand_canyon_2, + 0x77000E: location_name.sand_canyon_3, + 0x77000F: location_name.sand_canyon_4, + 0x770010: location_name.sand_canyon_5, + 0x770011: location_name.sand_canyon_6, + 0x770012: location_name.cloudy_park_1, + 0x770013: location_name.cloudy_park_2, + 0x770014: location_name.cloudy_park_3, + 0x770015: location_name.cloudy_park_4, + 0x770016: location_name.cloudy_park_5, + 0x770017: location_name.cloudy_park_6, + 0x770018: location_name.iceberg_1, + 0x770019: location_name.iceberg_2, + 0x77001A: location_name.iceberg_3, + 0x77001B: location_name.iceberg_4, + 0x77001C: location_name.iceberg_5, + 0x77001D: location_name.iceberg_6, +} + +heart_star_locations = { + 0x770100: location_name.grass_land_tulip, + 0x770101: location_name.grass_land_muchi, + 0x770102: location_name.grass_land_pitcherman, + 0x770103: location_name.grass_land_chao, + 0x770104: location_name.grass_land_mine, + 0x770105: location_name.grass_land_pierre, + 0x770106: location_name.ripple_field_kamuribana, + 0x770107: location_name.ripple_field_bakasa, + 0x770108: location_name.ripple_field_elieel, + 0x770109: location_name.ripple_field_toad, + 0x77010A: location_name.ripple_field_mama_pitch, + 0x77010B: location_name.ripple_field_hb002, + 0x77010C: location_name.sand_canyon_mushrooms, + 0x77010D: location_name.sand_canyon_auntie, + 0x77010E: location_name.sand_canyon_caramello, + 0x77010F: location_name.sand_canyon_hikari, + 0x770110: location_name.sand_canyon_nyupun, + 0x770111: location_name.sand_canyon_rob, + 0x770112: location_name.cloudy_park_hibanamodoki, + 0x770113: location_name.cloudy_park_piyokeko, + 0x770114: location_name.cloudy_park_mrball, + 0x770115: location_name.cloudy_park_mikarin, + 0x770116: location_name.cloudy_park_pick, + 0x770117: location_name.cloudy_park_hb007, + 0x770118: location_name.iceberg_kogoesou, + 0x770119: location_name.iceberg_samus, + 0x77011A: location_name.iceberg_kawasaki, + 0x77011B: location_name.iceberg_name, + 0x77011C: location_name.iceberg_shiro, + 0x77011D: location_name.iceberg_angel, +} + +boss_locations = { + 0x770200: location_name.grass_land_whispy, + 0x770201: location_name.ripple_field_acro, + 0x770202: location_name.sand_canyon_poncon, + 0x770203: location_name.cloudy_park_ado, + 0x770204: location_name.iceberg_dedede, +} + +consumable_locations = { + 0x770300: location_name.grass_land_1_u1, + 0x770301: location_name.grass_land_1_m1, + 0x770302: location_name.grass_land_2_u1, + 0x770303: location_name.grass_land_3_u1, + 0x770304: location_name.grass_land_3_m1, + 0x770305: location_name.grass_land_4_m1, + 0x770306: location_name.grass_land_4_u1, + 0x770307: location_name.grass_land_4_m2, + 0x770308: location_name.grass_land_4_m3, + 0x770309: location_name.grass_land_6_u1, + 0x77030A: location_name.grass_land_6_u2, + 0x77030B: location_name.ripple_field_2_u1, + 0x77030C: location_name.ripple_field_2_m1, + 0x77030D: location_name.ripple_field_3_m1, + 0x77030E: location_name.ripple_field_3_u1, + 0x77030F: location_name.ripple_field_4_m2, + 0x770310: location_name.ripple_field_4_u1, + 0x770311: location_name.ripple_field_4_m1, + 0x770312: location_name.ripple_field_5_u1, + 0x770313: location_name.ripple_field_5_m2, + 0x770314: location_name.ripple_field_5_m1, + 0x770315: location_name.sand_canyon_1_u1, + 0x770316: location_name.sand_canyon_2_u1, + 0x770317: location_name.sand_canyon_2_m1, + 0x770318: location_name.sand_canyon_4_m1, + 0x770319: location_name.sand_canyon_4_u1, + 0x77031A: location_name.sand_canyon_4_m2, + 0x77031B: location_name.sand_canyon_5_u1, + 0x77031C: location_name.sand_canyon_5_u3, + 0x77031D: location_name.sand_canyon_5_m1, + 0x77031E: location_name.sand_canyon_5_u4, + 0x77031F: location_name.sand_canyon_5_u2, + 0x770320: location_name.cloudy_park_1_m1, + 0x770321: location_name.cloudy_park_1_u1, + 0x770322: location_name.cloudy_park_4_u1, + 0x770323: location_name.cloudy_park_4_m1, + 0x770324: location_name.cloudy_park_5_m1, + 0x770325: location_name.cloudy_park_6_u1, + 0x770326: location_name.iceberg_3_m1, + 0x770327: location_name.iceberg_5_u1, + 0x770328: location_name.iceberg_5_u2, + 0x770329: location_name.iceberg_5_u3, + 0x77032A: location_name.iceberg_6_m1, + 0x77032B: location_name.iceberg_6_u1, +} + +level_consumables = { + 1: [0, 1], + 2: [2], + 3: [3, 4], + 4: [5, 6, 7, 8], + 6: [9, 10], + 8: [11, 12], + 9: [13, 14], + 10: [15, 16, 17], + 11: [18, 19, 20], + 13: [21], + 14: [22, 23], + 16: [24, 25, 26], + 17: [27, 28, 29, 30, 31], + 19: [32, 33], + 22: [34, 35], + 23: [36], + 24: [37], + 27: [38], + 29: [39, 40, 41], + 30: [42, 43], +} + +star_locations = { + 0x770401: location_name.grass_land_1_s1, + 0x770402: location_name.grass_land_1_s2, + 0x770403: location_name.grass_land_1_s3, + 0x770404: location_name.grass_land_1_s4, + 0x770405: location_name.grass_land_1_s5, + 0x770406: location_name.grass_land_1_s6, + 0x770407: location_name.grass_land_1_s7, + 0x770408: location_name.grass_land_1_s8, + 0x770409: location_name.grass_land_1_s9, + 0x77040a: location_name.grass_land_1_s10, + 0x77040b: location_name.grass_land_1_s11, + 0x77040c: location_name.grass_land_1_s12, + 0x77040d: location_name.grass_land_1_s13, + 0x77040e: location_name.grass_land_1_s14, + 0x77040f: location_name.grass_land_1_s15, + 0x770410: location_name.grass_land_1_s16, + 0x770411: location_name.grass_land_1_s17, + 0x770412: location_name.grass_land_1_s18, + 0x770413: location_name.grass_land_1_s19, + 0x770414: location_name.grass_land_1_s20, + 0x770415: location_name.grass_land_1_s21, + 0x770416: location_name.grass_land_1_s22, + 0x770417: location_name.grass_land_1_s23, + 0x770418: location_name.grass_land_2_s1, + 0x770419: location_name.grass_land_2_s2, + 0x77041a: location_name.grass_land_2_s3, + 0x77041b: location_name.grass_land_2_s4, + 0x77041c: location_name.grass_land_2_s5, + 0x77041d: location_name.grass_land_2_s6, + 0x77041e: location_name.grass_land_2_s7, + 0x77041f: location_name.grass_land_2_s8, + 0x770420: location_name.grass_land_2_s9, + 0x770421: location_name.grass_land_2_s10, + 0x770422: location_name.grass_land_2_s11, + 0x770423: location_name.grass_land_2_s12, + 0x770424: location_name.grass_land_2_s13, + 0x770425: location_name.grass_land_2_s14, + 0x770426: location_name.grass_land_2_s15, + 0x770427: location_name.grass_land_2_s16, + 0x770428: location_name.grass_land_2_s17, + 0x770429: location_name.grass_land_2_s18, + 0x77042a: location_name.grass_land_2_s19, + 0x77042b: location_name.grass_land_2_s20, + 0x77042c: location_name.grass_land_2_s21, + 0x77042d: location_name.grass_land_3_s1, + 0x77042e: location_name.grass_land_3_s2, + 0x77042f: location_name.grass_land_3_s3, + 0x770430: location_name.grass_land_3_s4, + 0x770431: location_name.grass_land_3_s5, + 0x770432: location_name.grass_land_3_s6, + 0x770433: location_name.grass_land_3_s7, + 0x770434: location_name.grass_land_3_s8, + 0x770435: location_name.grass_land_3_s9, + 0x770436: location_name.grass_land_3_s10, + 0x770437: location_name.grass_land_3_s11, + 0x770438: location_name.grass_land_3_s12, + 0x770439: location_name.grass_land_3_s13, + 0x77043a: location_name.grass_land_3_s14, + 0x77043b: location_name.grass_land_3_s15, + 0x77043c: location_name.grass_land_3_s16, + 0x77043d: location_name.grass_land_3_s17, + 0x77043e: location_name.grass_land_3_s18, + 0x77043f: location_name.grass_land_3_s19, + 0x770440: location_name.grass_land_3_s20, + 0x770441: location_name.grass_land_3_s21, + 0x770442: location_name.grass_land_3_s22, + 0x770443: location_name.grass_land_3_s23, + 0x770444: location_name.grass_land_3_s24, + 0x770445: location_name.grass_land_3_s25, + 0x770446: location_name.grass_land_3_s26, + 0x770447: location_name.grass_land_3_s27, + 0x770448: location_name.grass_land_3_s28, + 0x770449: location_name.grass_land_3_s29, + 0x77044a: location_name.grass_land_3_s30, + 0x77044b: location_name.grass_land_3_s31, + 0x77044c: location_name.grass_land_4_s1, + 0x77044d: location_name.grass_land_4_s2, + 0x77044e: location_name.grass_land_4_s3, + 0x77044f: location_name.grass_land_4_s4, + 0x770450: location_name.grass_land_4_s5, + 0x770451: location_name.grass_land_4_s6, + 0x770452: location_name.grass_land_4_s7, + 0x770453: location_name.grass_land_4_s8, + 0x770454: location_name.grass_land_4_s9, + 0x770455: location_name.grass_land_4_s10, + 0x770456: location_name.grass_land_4_s11, + 0x770457: location_name.grass_land_4_s12, + 0x770458: location_name.grass_land_4_s13, + 0x770459: location_name.grass_land_4_s14, + 0x77045a: location_name.grass_land_4_s15, + 0x77045b: location_name.grass_land_4_s16, + 0x77045c: location_name.grass_land_4_s17, + 0x77045d: location_name.grass_land_4_s18, + 0x77045e: location_name.grass_land_4_s19, + 0x77045f: location_name.grass_land_4_s20, + 0x770460: location_name.grass_land_4_s21, + 0x770461: location_name.grass_land_4_s22, + 0x770462: location_name.grass_land_4_s23, + 0x770463: location_name.grass_land_4_s24, + 0x770464: location_name.grass_land_4_s25, + 0x770465: location_name.grass_land_4_s26, + 0x770466: location_name.grass_land_4_s27, + 0x770467: location_name.grass_land_4_s28, + 0x770468: location_name.grass_land_4_s29, + 0x770469: location_name.grass_land_4_s30, + 0x77046a: location_name.grass_land_4_s31, + 0x77046b: location_name.grass_land_4_s32, + 0x77046c: location_name.grass_land_4_s33, + 0x77046d: location_name.grass_land_4_s34, + 0x77046e: location_name.grass_land_4_s35, + 0x77046f: location_name.grass_land_4_s36, + 0x770470: location_name.grass_land_4_s37, + 0x770471: location_name.grass_land_5_s1, + 0x770472: location_name.grass_land_5_s2, + 0x770473: location_name.grass_land_5_s3, + 0x770474: location_name.grass_land_5_s4, + 0x770475: location_name.grass_land_5_s5, + 0x770476: location_name.grass_land_5_s6, + 0x770477: location_name.grass_land_5_s7, + 0x770478: location_name.grass_land_5_s8, + 0x770479: location_name.grass_land_5_s9, + 0x77047a: location_name.grass_land_5_s10, + 0x77047b: location_name.grass_land_5_s11, + 0x77047c: location_name.grass_land_5_s12, + 0x77047d: location_name.grass_land_5_s13, + 0x77047e: location_name.grass_land_5_s14, + 0x77047f: location_name.grass_land_5_s15, + 0x770480: location_name.grass_land_5_s16, + 0x770481: location_name.grass_land_5_s17, + 0x770482: location_name.grass_land_5_s18, + 0x770483: location_name.grass_land_5_s19, + 0x770484: location_name.grass_land_5_s20, + 0x770485: location_name.grass_land_5_s21, + 0x770486: location_name.grass_land_5_s22, + 0x770487: location_name.grass_land_5_s23, + 0x770488: location_name.grass_land_5_s24, + 0x770489: location_name.grass_land_5_s25, + 0x77048a: location_name.grass_land_5_s26, + 0x77048b: location_name.grass_land_5_s27, + 0x77048c: location_name.grass_land_5_s28, + 0x77048d: location_name.grass_land_5_s29, + 0x77048e: location_name.grass_land_6_s1, + 0x77048f: location_name.grass_land_6_s2, + 0x770490: location_name.grass_land_6_s3, + 0x770491: location_name.grass_land_6_s4, + 0x770492: location_name.grass_land_6_s5, + 0x770493: location_name.grass_land_6_s6, + 0x770494: location_name.grass_land_6_s7, + 0x770495: location_name.grass_land_6_s8, + 0x770496: location_name.grass_land_6_s9, + 0x770497: location_name.grass_land_6_s10, + 0x770498: location_name.grass_land_6_s11, + 0x770499: location_name.grass_land_6_s12, + 0x77049a: location_name.grass_land_6_s13, + 0x77049b: location_name.grass_land_6_s14, + 0x77049c: location_name.grass_land_6_s15, + 0x77049d: location_name.grass_land_6_s16, + 0x77049e: location_name.grass_land_6_s17, + 0x77049f: location_name.grass_land_6_s18, + 0x7704a0: location_name.grass_land_6_s19, + 0x7704a1: location_name.grass_land_6_s20, + 0x7704a2: location_name.grass_land_6_s21, + 0x7704a3: location_name.grass_land_6_s22, + 0x7704a4: location_name.grass_land_6_s23, + 0x7704a5: location_name.grass_land_6_s24, + 0x7704a6: location_name.grass_land_6_s25, + 0x7704a7: location_name.grass_land_6_s26, + 0x7704a8: location_name.grass_land_6_s27, + 0x7704a9: location_name.grass_land_6_s28, + 0x7704aa: location_name.grass_land_6_s29, + 0x7704ab: location_name.ripple_field_1_s1, + 0x7704ac: location_name.ripple_field_1_s2, + 0x7704ad: location_name.ripple_field_1_s3, + 0x7704ae: location_name.ripple_field_1_s4, + 0x7704af: location_name.ripple_field_1_s5, + 0x7704b0: location_name.ripple_field_1_s6, + 0x7704b1: location_name.ripple_field_1_s7, + 0x7704b2: location_name.ripple_field_1_s8, + 0x7704b3: location_name.ripple_field_1_s9, + 0x7704b4: location_name.ripple_field_1_s10, + 0x7704b5: location_name.ripple_field_1_s11, + 0x7704b6: location_name.ripple_field_1_s12, + 0x7704b7: location_name.ripple_field_1_s13, + 0x7704b8: location_name.ripple_field_1_s14, + 0x7704b9: location_name.ripple_field_1_s15, + 0x7704ba: location_name.ripple_field_1_s16, + 0x7704bb: location_name.ripple_field_1_s17, + 0x7704bc: location_name.ripple_field_1_s18, + 0x7704bd: location_name.ripple_field_1_s19, + 0x7704be: location_name.ripple_field_2_s1, + 0x7704bf: location_name.ripple_field_2_s2, + 0x7704c0: location_name.ripple_field_2_s3, + 0x7704c1: location_name.ripple_field_2_s4, + 0x7704c2: location_name.ripple_field_2_s5, + 0x7704c3: location_name.ripple_field_2_s6, + 0x7704c4: location_name.ripple_field_2_s7, + 0x7704c5: location_name.ripple_field_2_s8, + 0x7704c6: location_name.ripple_field_2_s9, + 0x7704c7: location_name.ripple_field_2_s10, + 0x7704c8: location_name.ripple_field_2_s11, + 0x7704c9: location_name.ripple_field_2_s12, + 0x7704ca: location_name.ripple_field_2_s13, + 0x7704cb: location_name.ripple_field_2_s14, + 0x7704cc: location_name.ripple_field_2_s15, + 0x7704cd: location_name.ripple_field_2_s16, + 0x7704ce: location_name.ripple_field_2_s17, + 0x7704cf: location_name.ripple_field_3_s1, + 0x7704d0: location_name.ripple_field_3_s2, + 0x7704d1: location_name.ripple_field_3_s3, + 0x7704d2: location_name.ripple_field_3_s4, + 0x7704d3: location_name.ripple_field_3_s5, + 0x7704d4: location_name.ripple_field_3_s6, + 0x7704d5: location_name.ripple_field_3_s7, + 0x7704d6: location_name.ripple_field_3_s8, + 0x7704d7: location_name.ripple_field_3_s9, + 0x7704d8: location_name.ripple_field_3_s10, + 0x7704d9: location_name.ripple_field_3_s11, + 0x7704da: location_name.ripple_field_3_s12, + 0x7704db: location_name.ripple_field_3_s13, + 0x7704dc: location_name.ripple_field_3_s14, + 0x7704dd: location_name.ripple_field_3_s15, + 0x7704de: location_name.ripple_field_3_s16, + 0x7704df: location_name.ripple_field_3_s17, + 0x7704e0: location_name.ripple_field_3_s18, + 0x7704e1: location_name.ripple_field_3_s19, + 0x7704e2: location_name.ripple_field_3_s20, + 0x7704e3: location_name.ripple_field_3_s21, + 0x7704e4: location_name.ripple_field_4_s1, + 0x7704e5: location_name.ripple_field_4_s2, + 0x7704e6: location_name.ripple_field_4_s3, + 0x7704e7: location_name.ripple_field_4_s4, + 0x7704e8: location_name.ripple_field_4_s5, + 0x7704e9: location_name.ripple_field_4_s6, + 0x7704ea: location_name.ripple_field_4_s7, + 0x7704eb: location_name.ripple_field_4_s8, + 0x7704ec: location_name.ripple_field_4_s9, + 0x7704ed: location_name.ripple_field_4_s10, + 0x7704ee: location_name.ripple_field_4_s11, + 0x7704ef: location_name.ripple_field_4_s12, + 0x7704f0: location_name.ripple_field_4_s13, + 0x7704f1: location_name.ripple_field_4_s14, + 0x7704f2: location_name.ripple_field_4_s15, + 0x7704f3: location_name.ripple_field_4_s16, + 0x7704f4: location_name.ripple_field_4_s17, + 0x7704f5: location_name.ripple_field_4_s18, + 0x7704f6: location_name.ripple_field_4_s19, + 0x7704f7: location_name.ripple_field_4_s20, + 0x7704f8: location_name.ripple_field_4_s21, + 0x7704f9: location_name.ripple_field_4_s22, + 0x7704fa: location_name.ripple_field_4_s23, + 0x7704fb: location_name.ripple_field_4_s24, + 0x7704fc: location_name.ripple_field_4_s25, + 0x7704fd: location_name.ripple_field_4_s26, + 0x7704fe: location_name.ripple_field_4_s27, + 0x7704ff: location_name.ripple_field_4_s28, + 0x770500: location_name.ripple_field_4_s29, + 0x770501: location_name.ripple_field_4_s30, + 0x770502: location_name.ripple_field_4_s31, + 0x770503: location_name.ripple_field_4_s32, + 0x770504: location_name.ripple_field_4_s33, + 0x770505: location_name.ripple_field_4_s34, + 0x770506: location_name.ripple_field_4_s35, + 0x770507: location_name.ripple_field_4_s36, + 0x770508: location_name.ripple_field_4_s37, + 0x770509: location_name.ripple_field_4_s38, + 0x77050a: location_name.ripple_field_4_s39, + 0x77050b: location_name.ripple_field_4_s40, + 0x77050c: location_name.ripple_field_4_s41, + 0x77050d: location_name.ripple_field_4_s42, + 0x77050e: location_name.ripple_field_4_s43, + 0x77050f: location_name.ripple_field_4_s44, + 0x770510: location_name.ripple_field_4_s45, + 0x770511: location_name.ripple_field_4_s46, + 0x770512: location_name.ripple_field_4_s47, + 0x770513: location_name.ripple_field_4_s48, + 0x770514: location_name.ripple_field_4_s49, + 0x770515: location_name.ripple_field_4_s50, + 0x770516: location_name.ripple_field_4_s51, + 0x770517: location_name.ripple_field_5_s1, + 0x770518: location_name.ripple_field_5_s2, + 0x770519: location_name.ripple_field_5_s3, + 0x77051a: location_name.ripple_field_5_s4, + 0x77051b: location_name.ripple_field_5_s5, + 0x77051c: location_name.ripple_field_5_s6, + 0x77051d: location_name.ripple_field_5_s7, + 0x77051e: location_name.ripple_field_5_s8, + 0x77051f: location_name.ripple_field_5_s9, + 0x770520: location_name.ripple_field_5_s10, + 0x770521: location_name.ripple_field_5_s11, + 0x770522: location_name.ripple_field_5_s12, + 0x770523: location_name.ripple_field_5_s13, + 0x770524: location_name.ripple_field_5_s14, + 0x770525: location_name.ripple_field_5_s15, + 0x770526: location_name.ripple_field_5_s16, + 0x770527: location_name.ripple_field_5_s17, + 0x770528: location_name.ripple_field_5_s18, + 0x770529: location_name.ripple_field_5_s19, + 0x77052a: location_name.ripple_field_5_s20, + 0x77052b: location_name.ripple_field_5_s21, + 0x77052c: location_name.ripple_field_5_s22, + 0x77052d: location_name.ripple_field_5_s23, + 0x77052e: location_name.ripple_field_5_s24, + 0x77052f: location_name.ripple_field_5_s25, + 0x770530: location_name.ripple_field_5_s26, + 0x770531: location_name.ripple_field_5_s27, + 0x770532: location_name.ripple_field_5_s28, + 0x770533: location_name.ripple_field_5_s29, + 0x770534: location_name.ripple_field_5_s30, + 0x770535: location_name.ripple_field_5_s31, + 0x770536: location_name.ripple_field_5_s32, + 0x770537: location_name.ripple_field_5_s33, + 0x770538: location_name.ripple_field_5_s34, + 0x770539: location_name.ripple_field_5_s35, + 0x77053a: location_name.ripple_field_5_s36, + 0x77053b: location_name.ripple_field_5_s37, + 0x77053c: location_name.ripple_field_5_s38, + 0x77053d: location_name.ripple_field_5_s39, + 0x77053e: location_name.ripple_field_5_s40, + 0x77053f: location_name.ripple_field_5_s41, + 0x770540: location_name.ripple_field_5_s42, + 0x770541: location_name.ripple_field_5_s43, + 0x770542: location_name.ripple_field_5_s44, + 0x770543: location_name.ripple_field_5_s45, + 0x770544: location_name.ripple_field_5_s46, + 0x770545: location_name.ripple_field_5_s47, + 0x770546: location_name.ripple_field_5_s48, + 0x770547: location_name.ripple_field_5_s49, + 0x770548: location_name.ripple_field_5_s50, + 0x770549: location_name.ripple_field_5_s51, + 0x77054a: location_name.ripple_field_6_s1, + 0x77054b: location_name.ripple_field_6_s2, + 0x77054c: location_name.ripple_field_6_s3, + 0x77054d: location_name.ripple_field_6_s4, + 0x77054e: location_name.ripple_field_6_s5, + 0x77054f: location_name.ripple_field_6_s6, + 0x770550: location_name.ripple_field_6_s7, + 0x770551: location_name.ripple_field_6_s8, + 0x770552: location_name.ripple_field_6_s9, + 0x770553: location_name.ripple_field_6_s10, + 0x770554: location_name.ripple_field_6_s11, + 0x770555: location_name.ripple_field_6_s12, + 0x770556: location_name.ripple_field_6_s13, + 0x770557: location_name.ripple_field_6_s14, + 0x770558: location_name.ripple_field_6_s15, + 0x770559: location_name.ripple_field_6_s16, + 0x77055a: location_name.ripple_field_6_s17, + 0x77055b: location_name.ripple_field_6_s18, + 0x77055c: location_name.ripple_field_6_s19, + 0x77055d: location_name.ripple_field_6_s20, + 0x77055e: location_name.ripple_field_6_s21, + 0x77055f: location_name.ripple_field_6_s22, + 0x770560: location_name.ripple_field_6_s23, + 0x770561: location_name.sand_canyon_1_s1, + 0x770562: location_name.sand_canyon_1_s2, + 0x770563: location_name.sand_canyon_1_s3, + 0x770564: location_name.sand_canyon_1_s4, + 0x770565: location_name.sand_canyon_1_s5, + 0x770566: location_name.sand_canyon_1_s6, + 0x770567: location_name.sand_canyon_1_s7, + 0x770568: location_name.sand_canyon_1_s8, + 0x770569: location_name.sand_canyon_1_s9, + 0x77056a: location_name.sand_canyon_1_s10, + 0x77056b: location_name.sand_canyon_1_s11, + 0x77056c: location_name.sand_canyon_1_s12, + 0x77056d: location_name.sand_canyon_1_s13, + 0x77056e: location_name.sand_canyon_1_s14, + 0x77056f: location_name.sand_canyon_1_s15, + 0x770570: location_name.sand_canyon_1_s16, + 0x770571: location_name.sand_canyon_1_s17, + 0x770572: location_name.sand_canyon_1_s18, + 0x770573: location_name.sand_canyon_1_s19, + 0x770574: location_name.sand_canyon_1_s20, + 0x770575: location_name.sand_canyon_1_s21, + 0x770576: location_name.sand_canyon_1_s22, + 0x770577: location_name.sand_canyon_2_s1, + 0x770578: location_name.sand_canyon_2_s2, + 0x770579: location_name.sand_canyon_2_s3, + 0x77057a: location_name.sand_canyon_2_s4, + 0x77057b: location_name.sand_canyon_2_s5, + 0x77057c: location_name.sand_canyon_2_s6, + 0x77057d: location_name.sand_canyon_2_s7, + 0x77057e: location_name.sand_canyon_2_s8, + 0x77057f: location_name.sand_canyon_2_s9, + 0x770580: location_name.sand_canyon_2_s10, + 0x770581: location_name.sand_canyon_2_s11, + 0x770582: location_name.sand_canyon_2_s12, + 0x770583: location_name.sand_canyon_2_s13, + 0x770584: location_name.sand_canyon_2_s14, + 0x770585: location_name.sand_canyon_2_s15, + 0x770586: location_name.sand_canyon_2_s16, + 0x770587: location_name.sand_canyon_2_s17, + 0x770588: location_name.sand_canyon_2_s18, + 0x770589: location_name.sand_canyon_2_s19, + 0x77058a: location_name.sand_canyon_2_s20, + 0x77058b: location_name.sand_canyon_2_s21, + 0x77058c: location_name.sand_canyon_2_s22, + 0x77058d: location_name.sand_canyon_2_s23, + 0x77058e: location_name.sand_canyon_2_s24, + 0x77058f: location_name.sand_canyon_2_s25, + 0x770590: location_name.sand_canyon_2_s26, + 0x770591: location_name.sand_canyon_2_s27, + 0x770592: location_name.sand_canyon_2_s28, + 0x770593: location_name.sand_canyon_2_s29, + 0x770594: location_name.sand_canyon_2_s30, + 0x770595: location_name.sand_canyon_2_s31, + 0x770596: location_name.sand_canyon_2_s32, + 0x770597: location_name.sand_canyon_2_s33, + 0x770598: location_name.sand_canyon_2_s34, + 0x770599: location_name.sand_canyon_2_s35, + 0x77059a: location_name.sand_canyon_2_s36, + 0x77059b: location_name.sand_canyon_2_s37, + 0x77059c: location_name.sand_canyon_2_s38, + 0x77059d: location_name.sand_canyon_2_s39, + 0x77059e: location_name.sand_canyon_2_s40, + 0x77059f: location_name.sand_canyon_2_s41, + 0x7705a0: location_name.sand_canyon_2_s42, + 0x7705a1: location_name.sand_canyon_2_s43, + 0x7705a2: location_name.sand_canyon_2_s44, + 0x7705a3: location_name.sand_canyon_2_s45, + 0x7705a4: location_name.sand_canyon_2_s46, + 0x7705a5: location_name.sand_canyon_2_s47, + 0x7705a6: location_name.sand_canyon_2_s48, + 0x7705a7: location_name.sand_canyon_3_s1, + 0x7705a8: location_name.sand_canyon_3_s2, + 0x7705a9: location_name.sand_canyon_3_s3, + 0x7705aa: location_name.sand_canyon_3_s4, + 0x7705ab: location_name.sand_canyon_3_s5, + 0x7705ac: location_name.sand_canyon_3_s6, + 0x7705ad: location_name.sand_canyon_3_s7, + 0x7705ae: location_name.sand_canyon_3_s8, + 0x7705af: location_name.sand_canyon_3_s9, + 0x7705b0: location_name.sand_canyon_3_s10, + 0x7705b1: location_name.sand_canyon_4_s1, + 0x7705b2: location_name.sand_canyon_4_s2, + 0x7705b3: location_name.sand_canyon_4_s3, + 0x7705b4: location_name.sand_canyon_4_s4, + 0x7705b5: location_name.sand_canyon_4_s5, + 0x7705b6: location_name.sand_canyon_4_s6, + 0x7705b7: location_name.sand_canyon_4_s7, + 0x7705b8: location_name.sand_canyon_4_s8, + 0x7705b9: location_name.sand_canyon_4_s9, + 0x7705ba: location_name.sand_canyon_4_s10, + 0x7705bb: location_name.sand_canyon_4_s11, + 0x7705bc: location_name.sand_canyon_4_s12, + 0x7705bd: location_name.sand_canyon_4_s13, + 0x7705be: location_name.sand_canyon_4_s14, + 0x7705bf: location_name.sand_canyon_4_s15, + 0x7705c0: location_name.sand_canyon_4_s16, + 0x7705c1: location_name.sand_canyon_4_s17, + 0x7705c2: location_name.sand_canyon_4_s18, + 0x7705c3: location_name.sand_canyon_4_s19, + 0x7705c4: location_name.sand_canyon_4_s20, + 0x7705c5: location_name.sand_canyon_4_s21, + 0x7705c6: location_name.sand_canyon_4_s22, + 0x7705c7: location_name.sand_canyon_4_s23, + 0x7705c8: location_name.sand_canyon_5_s1, + 0x7705c9: location_name.sand_canyon_5_s2, + 0x7705ca: location_name.sand_canyon_5_s3, + 0x7705cb: location_name.sand_canyon_5_s4, + 0x7705cc: location_name.sand_canyon_5_s5, + 0x7705cd: location_name.sand_canyon_5_s6, + 0x7705ce: location_name.sand_canyon_5_s7, + 0x7705cf: location_name.sand_canyon_5_s8, + 0x7705d0: location_name.sand_canyon_5_s9, + 0x7705d1: location_name.sand_canyon_5_s10, + 0x7705d2: location_name.sand_canyon_5_s11, + 0x7705d3: location_name.sand_canyon_5_s12, + 0x7705d4: location_name.sand_canyon_5_s13, + 0x7705d5: location_name.sand_canyon_5_s14, + 0x7705d6: location_name.sand_canyon_5_s15, + 0x7705d7: location_name.sand_canyon_5_s16, + 0x7705d8: location_name.sand_canyon_5_s17, + 0x7705d9: location_name.sand_canyon_5_s18, + 0x7705da: location_name.sand_canyon_5_s19, + 0x7705db: location_name.sand_canyon_5_s20, + 0x7705dc: location_name.sand_canyon_5_s21, + 0x7705dd: location_name.sand_canyon_5_s22, + 0x7705de: location_name.sand_canyon_5_s23, + 0x7705df: location_name.sand_canyon_5_s24, + 0x7705e0: location_name.sand_canyon_5_s25, + 0x7705e1: location_name.sand_canyon_5_s26, + 0x7705e2: location_name.sand_canyon_5_s27, + 0x7705e3: location_name.sand_canyon_5_s28, + 0x7705e4: location_name.sand_canyon_5_s29, + 0x7705e5: location_name.sand_canyon_5_s30, + 0x7705e6: location_name.sand_canyon_5_s31, + 0x7705e7: location_name.sand_canyon_5_s32, + 0x7705e8: location_name.sand_canyon_5_s33, + 0x7705e9: location_name.sand_canyon_5_s34, + 0x7705ea: location_name.sand_canyon_5_s35, + 0x7705eb: location_name.sand_canyon_5_s36, + 0x7705ec: location_name.sand_canyon_5_s37, + 0x7705ed: location_name.sand_canyon_5_s38, + 0x7705ee: location_name.sand_canyon_5_s39, + 0x7705ef: location_name.sand_canyon_5_s40, + 0x7705f0: location_name.cloudy_park_1_s1, + 0x7705f1: location_name.cloudy_park_1_s2, + 0x7705f2: location_name.cloudy_park_1_s3, + 0x7705f3: location_name.cloudy_park_1_s4, + 0x7705f4: location_name.cloudy_park_1_s5, + 0x7705f5: location_name.cloudy_park_1_s6, + 0x7705f6: location_name.cloudy_park_1_s7, + 0x7705f7: location_name.cloudy_park_1_s8, + 0x7705f8: location_name.cloudy_park_1_s9, + 0x7705f9: location_name.cloudy_park_1_s10, + 0x7705fa: location_name.cloudy_park_1_s11, + 0x7705fb: location_name.cloudy_park_1_s12, + 0x7705fc: location_name.cloudy_park_1_s13, + 0x7705fd: location_name.cloudy_park_1_s14, + 0x7705fe: location_name.cloudy_park_1_s15, + 0x7705ff: location_name.cloudy_park_1_s16, + 0x770600: location_name.cloudy_park_1_s17, + 0x770601: location_name.cloudy_park_1_s18, + 0x770602: location_name.cloudy_park_1_s19, + 0x770603: location_name.cloudy_park_1_s20, + 0x770604: location_name.cloudy_park_1_s21, + 0x770605: location_name.cloudy_park_1_s22, + 0x770606: location_name.cloudy_park_1_s23, + 0x770607: location_name.cloudy_park_2_s1, + 0x770608: location_name.cloudy_park_2_s2, + 0x770609: location_name.cloudy_park_2_s3, + 0x77060a: location_name.cloudy_park_2_s4, + 0x77060b: location_name.cloudy_park_2_s5, + 0x77060c: location_name.cloudy_park_2_s6, + 0x77060d: location_name.cloudy_park_2_s7, + 0x77060e: location_name.cloudy_park_2_s8, + 0x77060f: location_name.cloudy_park_2_s9, + 0x770610: location_name.cloudy_park_2_s10, + 0x770611: location_name.cloudy_park_2_s11, + 0x770612: location_name.cloudy_park_2_s12, + 0x770613: location_name.cloudy_park_2_s13, + 0x770614: location_name.cloudy_park_2_s14, + 0x770615: location_name.cloudy_park_2_s15, + 0x770616: location_name.cloudy_park_2_s16, + 0x770617: location_name.cloudy_park_2_s17, + 0x770618: location_name.cloudy_park_2_s18, + 0x770619: location_name.cloudy_park_2_s19, + 0x77061a: location_name.cloudy_park_2_s20, + 0x77061b: location_name.cloudy_park_2_s21, + 0x77061c: location_name.cloudy_park_2_s22, + 0x77061d: location_name.cloudy_park_2_s23, + 0x77061e: location_name.cloudy_park_2_s24, + 0x77061f: location_name.cloudy_park_2_s25, + 0x770620: location_name.cloudy_park_2_s26, + 0x770621: location_name.cloudy_park_2_s27, + 0x770622: location_name.cloudy_park_2_s28, + 0x770623: location_name.cloudy_park_2_s29, + 0x770624: location_name.cloudy_park_2_s30, + 0x770625: location_name.cloudy_park_2_s31, + 0x770626: location_name.cloudy_park_2_s32, + 0x770627: location_name.cloudy_park_2_s33, + 0x770628: location_name.cloudy_park_2_s34, + 0x770629: location_name.cloudy_park_2_s35, + 0x77062a: location_name.cloudy_park_2_s36, + 0x77062b: location_name.cloudy_park_2_s37, + 0x77062c: location_name.cloudy_park_2_s38, + 0x77062d: location_name.cloudy_park_2_s39, + 0x77062e: location_name.cloudy_park_2_s40, + 0x77062f: location_name.cloudy_park_2_s41, + 0x770630: location_name.cloudy_park_2_s42, + 0x770631: location_name.cloudy_park_2_s43, + 0x770632: location_name.cloudy_park_2_s44, + 0x770633: location_name.cloudy_park_2_s45, + 0x770634: location_name.cloudy_park_2_s46, + 0x770635: location_name.cloudy_park_2_s47, + 0x770636: location_name.cloudy_park_2_s48, + 0x770637: location_name.cloudy_park_2_s49, + 0x770638: location_name.cloudy_park_2_s50, + 0x770639: location_name.cloudy_park_2_s51, + 0x77063a: location_name.cloudy_park_2_s52, + 0x77063b: location_name.cloudy_park_2_s53, + 0x77063c: location_name.cloudy_park_2_s54, + 0x77063d: location_name.cloudy_park_3_s1, + 0x77063e: location_name.cloudy_park_3_s2, + 0x77063f: location_name.cloudy_park_3_s3, + 0x770640: location_name.cloudy_park_3_s4, + 0x770641: location_name.cloudy_park_3_s5, + 0x770642: location_name.cloudy_park_3_s6, + 0x770643: location_name.cloudy_park_3_s7, + 0x770644: location_name.cloudy_park_3_s8, + 0x770645: location_name.cloudy_park_3_s9, + 0x770646: location_name.cloudy_park_3_s10, + 0x770647: location_name.cloudy_park_3_s11, + 0x770648: location_name.cloudy_park_3_s12, + 0x770649: location_name.cloudy_park_3_s13, + 0x77064a: location_name.cloudy_park_3_s14, + 0x77064b: location_name.cloudy_park_3_s15, + 0x77064c: location_name.cloudy_park_3_s16, + 0x77064d: location_name.cloudy_park_3_s17, + 0x77064e: location_name.cloudy_park_3_s18, + 0x77064f: location_name.cloudy_park_3_s19, + 0x770650: location_name.cloudy_park_3_s20, + 0x770651: location_name.cloudy_park_3_s21, + 0x770652: location_name.cloudy_park_3_s22, + 0x770653: location_name.cloudy_park_4_s1, + 0x770654: location_name.cloudy_park_4_s2, + 0x770655: location_name.cloudy_park_4_s3, + 0x770656: location_name.cloudy_park_4_s4, + 0x770657: location_name.cloudy_park_4_s5, + 0x770658: location_name.cloudy_park_4_s6, + 0x770659: location_name.cloudy_park_4_s7, + 0x77065a: location_name.cloudy_park_4_s8, + 0x77065b: location_name.cloudy_park_4_s9, + 0x77065c: location_name.cloudy_park_4_s10, + 0x77065d: location_name.cloudy_park_4_s11, + 0x77065e: location_name.cloudy_park_4_s12, + 0x77065f: location_name.cloudy_park_4_s13, + 0x770660: location_name.cloudy_park_4_s14, + 0x770661: location_name.cloudy_park_4_s15, + 0x770662: location_name.cloudy_park_4_s16, + 0x770663: location_name.cloudy_park_4_s17, + 0x770664: location_name.cloudy_park_4_s18, + 0x770665: location_name.cloudy_park_4_s19, + 0x770666: location_name.cloudy_park_4_s20, + 0x770667: location_name.cloudy_park_4_s21, + 0x770668: location_name.cloudy_park_4_s22, + 0x770669: location_name.cloudy_park_4_s23, + 0x77066a: location_name.cloudy_park_4_s24, + 0x77066b: location_name.cloudy_park_4_s25, + 0x77066c: location_name.cloudy_park_4_s26, + 0x77066d: location_name.cloudy_park_4_s27, + 0x77066e: location_name.cloudy_park_4_s28, + 0x77066f: location_name.cloudy_park_4_s29, + 0x770670: location_name.cloudy_park_4_s30, + 0x770671: location_name.cloudy_park_4_s31, + 0x770672: location_name.cloudy_park_4_s32, + 0x770673: location_name.cloudy_park_4_s33, + 0x770674: location_name.cloudy_park_4_s34, + 0x770675: location_name.cloudy_park_4_s35, + 0x770676: location_name.cloudy_park_4_s36, + 0x770677: location_name.cloudy_park_4_s37, + 0x770678: location_name.cloudy_park_4_s38, + 0x770679: location_name.cloudy_park_4_s39, + 0x77067a: location_name.cloudy_park_4_s40, + 0x77067b: location_name.cloudy_park_4_s41, + 0x77067c: location_name.cloudy_park_4_s42, + 0x77067d: location_name.cloudy_park_4_s43, + 0x77067e: location_name.cloudy_park_4_s44, + 0x77067f: location_name.cloudy_park_4_s45, + 0x770680: location_name.cloudy_park_4_s46, + 0x770681: location_name.cloudy_park_4_s47, + 0x770682: location_name.cloudy_park_4_s48, + 0x770683: location_name.cloudy_park_4_s49, + 0x770684: location_name.cloudy_park_4_s50, + 0x770685: location_name.cloudy_park_5_s1, + 0x770686: location_name.cloudy_park_5_s2, + 0x770687: location_name.cloudy_park_5_s3, + 0x770688: location_name.cloudy_park_5_s4, + 0x770689: location_name.cloudy_park_5_s5, + 0x77068a: location_name.cloudy_park_5_s6, + 0x77068b: location_name.cloudy_park_6_s1, + 0x77068c: location_name.cloudy_park_6_s2, + 0x77068d: location_name.cloudy_park_6_s3, + 0x77068e: location_name.cloudy_park_6_s4, + 0x77068f: location_name.cloudy_park_6_s5, + 0x770690: location_name.cloudy_park_6_s6, + 0x770691: location_name.cloudy_park_6_s7, + 0x770692: location_name.cloudy_park_6_s8, + 0x770693: location_name.cloudy_park_6_s9, + 0x770694: location_name.cloudy_park_6_s10, + 0x770695: location_name.cloudy_park_6_s11, + 0x770696: location_name.cloudy_park_6_s12, + 0x770697: location_name.cloudy_park_6_s13, + 0x770698: location_name.cloudy_park_6_s14, + 0x770699: location_name.cloudy_park_6_s15, + 0x77069a: location_name.cloudy_park_6_s16, + 0x77069b: location_name.cloudy_park_6_s17, + 0x77069c: location_name.cloudy_park_6_s18, + 0x77069d: location_name.cloudy_park_6_s19, + 0x77069e: location_name.cloudy_park_6_s20, + 0x77069f: location_name.cloudy_park_6_s21, + 0x7706a0: location_name.cloudy_park_6_s22, + 0x7706a1: location_name.cloudy_park_6_s23, + 0x7706a2: location_name.cloudy_park_6_s24, + 0x7706a3: location_name.cloudy_park_6_s25, + 0x7706a4: location_name.cloudy_park_6_s26, + 0x7706a5: location_name.cloudy_park_6_s27, + 0x7706a6: location_name.cloudy_park_6_s28, + 0x7706a7: location_name.cloudy_park_6_s29, + 0x7706a8: location_name.cloudy_park_6_s30, + 0x7706a9: location_name.cloudy_park_6_s31, + 0x7706aa: location_name.cloudy_park_6_s32, + 0x7706ab: location_name.cloudy_park_6_s33, + 0x7706ac: location_name.iceberg_1_s1, + 0x7706ad: location_name.iceberg_1_s2, + 0x7706ae: location_name.iceberg_1_s3, + 0x7706af: location_name.iceberg_1_s4, + 0x7706b0: location_name.iceberg_1_s5, + 0x7706b1: location_name.iceberg_1_s6, + 0x7706b2: location_name.iceberg_2_s1, + 0x7706b3: location_name.iceberg_2_s2, + 0x7706b4: location_name.iceberg_2_s3, + 0x7706b5: location_name.iceberg_2_s4, + 0x7706b6: location_name.iceberg_2_s5, + 0x7706b7: location_name.iceberg_2_s6, + 0x7706b8: location_name.iceberg_2_s7, + 0x7706b9: location_name.iceberg_2_s8, + 0x7706ba: location_name.iceberg_2_s9, + 0x7706bb: location_name.iceberg_2_s10, + 0x7706bc: location_name.iceberg_2_s11, + 0x7706bd: location_name.iceberg_2_s12, + 0x7706be: location_name.iceberg_2_s13, + 0x7706bf: location_name.iceberg_2_s14, + 0x7706c0: location_name.iceberg_2_s15, + 0x7706c1: location_name.iceberg_2_s16, + 0x7706c2: location_name.iceberg_2_s17, + 0x7706c3: location_name.iceberg_2_s18, + 0x7706c4: location_name.iceberg_2_s19, + 0x7706c5: location_name.iceberg_3_s1, + 0x7706c6: location_name.iceberg_3_s2, + 0x7706c7: location_name.iceberg_3_s3, + 0x7706c8: location_name.iceberg_3_s4, + 0x7706c9: location_name.iceberg_3_s5, + 0x7706ca: location_name.iceberg_3_s6, + 0x7706cb: location_name.iceberg_3_s7, + 0x7706cc: location_name.iceberg_3_s8, + 0x7706cd: location_name.iceberg_3_s9, + 0x7706ce: location_name.iceberg_3_s10, + 0x7706cf: location_name.iceberg_3_s11, + 0x7706d0: location_name.iceberg_3_s12, + 0x7706d1: location_name.iceberg_3_s13, + 0x7706d2: location_name.iceberg_3_s14, + 0x7706d3: location_name.iceberg_3_s15, + 0x7706d4: location_name.iceberg_3_s16, + 0x7706d5: location_name.iceberg_3_s17, + 0x7706d6: location_name.iceberg_3_s18, + 0x7706d7: location_name.iceberg_3_s19, + 0x7706d8: location_name.iceberg_3_s20, + 0x7706d9: location_name.iceberg_3_s21, + 0x7706da: location_name.iceberg_4_s1, + 0x7706db: location_name.iceberg_4_s2, + 0x7706dc: location_name.iceberg_4_s3, + 0x7706dd: location_name.iceberg_5_s1, + 0x7706de: location_name.iceberg_5_s2, + 0x7706df: location_name.iceberg_5_s3, + 0x7706e0: location_name.iceberg_5_s4, + 0x7706e1: location_name.iceberg_5_s5, + 0x7706e2: location_name.iceberg_5_s6, + 0x7706e3: location_name.iceberg_5_s7, + 0x7706e4: location_name.iceberg_5_s8, + 0x7706e5: location_name.iceberg_5_s9, + 0x7706e6: location_name.iceberg_5_s10, + 0x7706e7: location_name.iceberg_5_s11, + 0x7706e8: location_name.iceberg_5_s12, + 0x7706e9: location_name.iceberg_5_s13, + 0x7706ea: location_name.iceberg_5_s14, + 0x7706eb: location_name.iceberg_5_s15, + 0x7706ec: location_name.iceberg_5_s16, + 0x7706ed: location_name.iceberg_5_s17, + 0x7706ee: location_name.iceberg_5_s18, + 0x7706ef: location_name.iceberg_5_s19, + 0x7706f0: location_name.iceberg_5_s20, + 0x7706f1: location_name.iceberg_5_s21, + 0x7706f2: location_name.iceberg_5_s22, + 0x7706f3: location_name.iceberg_5_s23, + 0x7706f4: location_name.iceberg_5_s24, + 0x7706f5: location_name.iceberg_5_s25, + 0x7706f6: location_name.iceberg_5_s26, + 0x7706f7: location_name.iceberg_5_s27, + 0x7706f8: location_name.iceberg_5_s28, + 0x7706f9: location_name.iceberg_5_s29, + 0x7706fa: location_name.iceberg_5_s30, + 0x7706fb: location_name.iceberg_5_s31, + 0x7706fc: location_name.iceberg_5_s32, + 0x7706fd: location_name.iceberg_5_s33, + 0x7706fe: location_name.iceberg_5_s34, + 0x7706ff: location_name.iceberg_6_s1, + +} + +location_table = { + **stage_locations, + **heart_star_locations, + **boss_locations, + **consumable_locations, + **star_locations +} diff --git a/worlds/kdl3/Names/__init__.py b/worlds/kdl3/names/__init__.py similarity index 100% rename from worlds/kdl3/Names/__init__.py rename to worlds/kdl3/names/__init__.py diff --git a/worlds/kdl3/Names/AnimalFriendSpawns.py b/worlds/kdl3/names/animal_friend_spawns.py similarity index 95% rename from worlds/kdl3/Names/AnimalFriendSpawns.py rename to worlds/kdl3/names/animal_friend_spawns.py index 4520cf1438..5c1ba39697 100644 --- a/worlds/kdl3/Names/AnimalFriendSpawns.py +++ b/worlds/kdl3/names/animal_friend_spawns.py @@ -1,3 +1,5 @@ +from typing import List + grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu @@ -197,3 +199,12 @@ animal_friend_spawns = { iceberg_6_a5: "ChuChu Spawn", iceberg_6_a6: "Nago Spawn", } + +problematic_sets: List[List[str]] = [ + # Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not. + [ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3], + [sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3], + [cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3], + [iceberg_6_a1, iceberg_6_a2, iceberg_6_a3], + [iceberg_6_a4, iceberg_6_a5, iceberg_6_a6] +] diff --git a/worlds/kdl3/Names/EnemyAbilities.py b/worlds/kdl3/names/enemy_abilities.py similarity index 99% rename from worlds/kdl3/Names/EnemyAbilities.py rename to worlds/kdl3/names/enemy_abilities.py index 016e3033ab..ace15054da 100644 --- a/worlds/kdl3/Names/EnemyAbilities.py +++ b/worlds/kdl3/names/enemy_abilities.py @@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability', enemy_restrictive: List[Tuple[List[str], List[str]]] = [ # abilities, enemies, set_all (False to set any) - (["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 + (["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 # Sand Canyon 6 (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']), (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']), diff --git a/worlds/kdl3/Names/LocationName.py b/worlds/kdl3/names/location_name.py similarity index 100% rename from worlds/kdl3/Names/LocationName.py rename to worlds/kdl3/names/location_name.py diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/options.py similarity index 82% rename from worlds/kdl3/Options.py rename to worlds/kdl3/options.py index e0a4f12f15..b9163794ad 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/options.py @@ -1,13 +1,21 @@ import random from dataclasses import dataclass +from typing import List -from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions, PlandoConnections -from .Names import LocationName +from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ + PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections +from .names import location_name + + +class RemoteItems(DefaultOnToggle): + """ + Enables receiving items from your own world, primarily for co-op play. + """ + display_name = "Remote Items" class KDL3PlandoConnections(PlandoConnections): - entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)} class Goal(Choice): @@ -30,6 +38,7 @@ class Goal(Choice): return cls.name_lookup[value].upper() return super().get_option_name(value) + class GoalSpeed(Choice): """ Normal: the goal is unlocked after purifying the five bosses @@ -40,13 +49,14 @@ class GoalSpeed(Choice): option_fast = 1 -class TotalHeartStars(Range): +class MaxHeartStars(Range): """ Maximum number of heart stars to include in the pool of items. + If fewer available locations exist in the pool than this number, the number of available locations will be used instead. """ display_name = "Max Heart Stars" range_start = 5 # set to 5 so strict bosses does not degrade - range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down + range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars default = 30 @@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses): Singularity: All (non-Zero) bosses will be replaced with a single boss Supports plando placement. """ - bosses = frozenset(LocationName.boss_names.keys()) + bosses = frozenset(location_name.boss_names.keys()) - locations = frozenset(LocationName.level_names.keys()) + locations = frozenset(location_name.level_names.keys()) duplicate_bosses = True @@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice): option_orange = 11 option_lime = 12 option_lavender = 13 - option_custom = 14 + option_miku = 14 + option_custom = 15 default = 0 @classmethod @@ -296,6 +307,7 @@ class KirbyFlavor(OptionDict): A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Kirby Flavor" default = { "1": "B01810", "2": "F0E0E8", @@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict): "14": "F8F8F8", "15": "B03830", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class GooeyFlavorPreset(Choice): @@ -352,6 +365,7 @@ class GooeyFlavor(OptionDict): A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Gooey Flavor" default = { "1": "000808", "2": "102838", @@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict): "8": "D0C0C0", "9": "F8F8F8", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class MusicShuffle(Choice): @@ -402,14 +417,27 @@ class Gifting(Toggle): display_name = "Gifting" +class TotalHeartStars(NamedRange): + """ + Deprecated. Use max_heart_stars instead. Supported for only one version. + """ + default = -1 + range_start = 5 + range_end = 99 + special_range_names = { + "default": -1 + } + visibility = Visibility.none + + @dataclass -class KDL3Options(PerGameCommonOptions): +class KDL3Options(PerGameCommonOptions, DeathLinkMixin): + remote_items: RemoteItems plando_connections: KDL3PlandoConnections - death_link: DeathLink game_language: GameLanguage goal: Goal goal_speed: GoalSpeed - total_heart_stars: TotalHeartStars + max_heart_stars: MaxHeartStars heart_stars_required: HeartStarsRequired filler_percentage: FillerPercentage trap_percentage: TrapPercentage @@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions): gooey_flavor: GooeyFlavor music_shuffle: MusicShuffle virtual_console: VirtualConsoleChanges + + total_heart_stars: TotalHeartStars # remove in 2 versions + + +kdl3_option_groups: List[OptionGroup] = [ + OptionGroup("Goal Options", [Goal, GoalSpeed, MaxHeartStars, HeartStarsRequired, JumpingTarget, ]), + OptionGroup("World Options", [RemoteItems, StrictBosses, OpenWorld, OpenWorldBossRequirement, ConsumableChecks, + StarChecks, FillerPercentage, TrapPercentage, GooeyTrapPercentage, + SlowTrapPercentage, AbilityTrapPercentage, LevelShuffle, BossShuffle, + AnimalRandomization, CopyAbilityRandomization, BossRequirementRandom, + Gifting, ]), + OptionGroup("Cosmetic Options", [GameLanguage, BossShuffleAllowBB, KirbyFlavorPreset, KirbyFlavor, + GooeyFlavorPreset, GooeyFlavor, MusicShuffle, VirtualConsoleChanges, ]), +] diff --git a/worlds/kdl3/Presets.py b/worlds/kdl3/presets.py similarity index 98% rename from worlds/kdl3/Presets.py rename to worlds/kdl3/presets.py index d3a7146ded..491ad9dca9 100644 --- a/worlds/kdl3/Presets.py +++ b/worlds/kdl3/presets.py @@ -25,6 +25,7 @@ all_random = { "ow_boss_requirement": "random", "boss_requirement_random": "random", "consumables": "random", + "starsanity": "random", "kirby_flavor_preset": "random", "gooey_flavor_preset": "random", "music_shuffle": "random", diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/regions.py similarity index 66% rename from worlds/kdl3/Regions.py rename to worlds/kdl3/regions.py index 407dcf9680..c47e5dee40 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/regions.py @@ -1,60 +1,62 @@ import orjson import os from pkgutil import get_data +from copy import deepcopy -from typing import TYPE_CHECKING, List, Dict, Optional, Union -from BaseClasses import Region +from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable +from BaseClasses import Region, CollectionState from worlds.generic.Rules import add_item_rule -from .Locations import KDL3Location -from .Names import LocationName -from .Options import BossShuffle -from .Room import KDL3Room +from .locations import KDL3Location +from .names import location_name +from .options import BossShuffle +from .room import KDL3Room if TYPE_CHECKING: from . import KDL3World default_levels = { - 1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200], - 2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201], - 3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202], - 4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203], - 5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204], + 1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200], + 2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201], + 3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202], + 4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203], + 5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204], } first_stage_blacklist = { # We want to confirm that the first stage can be completed without any items - 0x77000B, # 2-5 needs Kine - 0x770011, # 3-5 needs Cutter - 0x77001C, # 5-4 needs Burning + 0x77000A, # 2-5 needs Kine + 0x770010, # 3-5 needs Cutter + 0x77001B, # 5-4 needs Burning } first_world_limit = { # We need to limit the number of very restrictive stages in level 1 on solo gens *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks + 0x770006, 0x770007, - 0x770008, - 0x770013, - 0x77001E, + 0x770012, + 0x77001D, } def generate_valid_level(world: "KDL3World", level: int, stage: int, - possible_stages: List[int], placed_stages: List[int]): + possible_stages: List[int], placed_stages: List[Optional[int]]) -> int: new_stage = world.random.choice(possible_stages) if level == 1: if stage == 0 and new_stage in first_stage_blacklist: + possible_stages.remove(new_stage) return generate_valid_level(world, level, stage, possible_stages, placed_stages) elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and - new_stage in first_world_limit and - sum(p_stage in first_world_limit for p_stage in placed_stages) + new_stage in first_world_limit and + sum(p_stage in first_world_limit for p_stage in placed_stages) >= (2 if world.options.open_world else 1)): return generate_valid_level(world, level, stage, possible_stages, placed_stages) return new_stage -def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): - level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} +def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None: + level_names = {location_name.level_names[level]: level for level in location_name.level_names} room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) rooms: Dict[str, KDL3Room] = dict() for room_entry in room_data: @@ -63,7 +65,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"], room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"]) room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else - None for location in room_entry["locations"] + None for location in room_entry["locations"] if (not any(x in location for x in ["1-Up", "Maxim"]) or world.options.consumables.value) and ("Star" not in location or world.options.starsanity.value)}, @@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if room.stage == 7: first_rooms[0x770200 + room.level - 1] = room else: - first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room - exits = dict() + first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room + exits: Dict[str, Callable[[CollectionState], bool]] = dict() for def_exit in room.default_exits: target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" access_rule = tuple(def_exit["access_rule"]) @@ -115,50 +117,54 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if world.options.open_world: level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) else: - world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\ + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player) \ .parent_region.add_exits([first_rooms[0x770200 + level - 1].name]) -def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict: - levels: Dict[int, List[Optional[int]]] = { - 1: [None] * 7, - 2: [None] * 7, - 3: [None] * 7, - 4: [None] * 7, - 5: [None] * 7, - } +def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]: + if shuffle_mode: + levels: Dict[int, List[Optional[int]]] = { + 1: [None] * 7, + 2: [None] * 7, + 3: [None] * 7, + 4: [None] * 7, + 5: [None] * 7, + } - possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.options.plando_connections: - for connection in world.options.plando_connections: - try: - entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) - stage_world, stage_stage = connection.exit.rsplit(" ", 1) - new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1] - levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage - possible_stages.remove(new_stage) + possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] + if world.options.plando_connections: + for connection in world.options.plando_connections: + try: + entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) + stage_world, stage_stage = connection.exit.rsplit(" ", 1) + new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1] + levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage + possible_stages.remove(new_stage) - except Exception: - raise Exception( - f"Invalid connection: {connection.entrance} =>" - f" {connection.exit} for player {world.player} ({world.player_name})") + except Exception: + raise Exception( + f"Invalid connection: {connection.entrance} =>" + f" {connection.exit} for player {world.player} ({world.player_name})") - for level in range(1, 6): - for stage in range(6): - # Randomize bosses separately - try: + for level in range(1, 6): + for stage in range(6): + # Randomize bosses separately if levels[level][stage] is None: stage_candidates = [candidate for candidate in possible_stages - if (enforce_world and candidate in default_levels[level]) - or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) - or (enforce_pattern == enforce_world) + if (shuffle_mode == 1 and candidate in default_levels[level]) + or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage) + or (shuffle_mode == 3) ] + if not stage_candidates: + raise Exception( + f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level]) possible_stages.remove(new_stage) levels[level][stage] = new_stage - except Exception: - raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") - + else: + levels = deepcopy(default_levels) + for level in levels: + levels[level][6] = None # now handle bosses boss_shuffle: Union[int, str] = world.options.boss_shuffle.value plando_bosses = [] @@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte boss_shuffle = BossShuffle.options[options.pop()] for option in options: if "-" in option: - loc, boss = option.split("-") + loc, plando_boss = option.split("-") loc = loc.title() - boss = boss.title() - levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss] - plando_bosses.append(LocationName.boss_names[boss]) + plando_boss = plando_boss.title() + levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss] + plando_bosses.append(location_name.boss_names[plando_boss]) else: option = option.title() for level in levels: if levels[level][6] is None: - levels[level][6] = LocationName.boss_names[option] - plando_bosses.append(LocationName.boss_names[option]) + levels[level][6] = location_name.boss_names[option] + plando_bosses.append(location_name.boss_names[option]) if boss_shuffle > 0: if boss_shuffle == BossShuffle.option_full: @@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None: 5: level5, } level_shuffle = world.options.stage_shuffle.value - if level_shuffle != 0: - world.player_levels = generate_valid_levels( - world, - level_shuffle == 1, - level_shuffle == 2) + if hasattr(world.multiworld, "re_gen_passthrough"): + world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"] + else: + world.player_levels = generate_valid_levels(world, level_shuffle) generate_rooms(world, levels) - level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location) + level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location) menu.connect(level1, "Start Game") level1.connect(level2, "To Level 2") diff --git a/worlds/kdl3/rom.py b/worlds/kdl3/rom.py new file mode 100644 index 0000000000..3dd10ce1c4 --- /dev/null +++ b/worlds/kdl3/rom.py @@ -0,0 +1,602 @@ +import typing +from pkgutil import get_data + +import Utils +from typing import Optional, TYPE_CHECKING, Tuple, Dict, List +import hashlib +import os +import struct + +import settings +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension +from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ + get_gooey_palette +from .compression import hal_decompress +import bsdiff4 + +if TYPE_CHECKING: + from . import KDL3World + +KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" +KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" + +level_pointers = { + 0x770000: 0x0084, + 0x770001: 0x009C, + 0x770002: 0x00B8, + 0x770003: 0x00D8, + 0x770004: 0x0104, + 0x770005: 0x0124, + 0x770006: 0x014C, + 0x770007: 0x0170, + 0x770008: 0x0190, + 0x770009: 0x01B0, + 0x77000A: 0x01E8, + 0x77000B: 0x0218, + 0x77000C: 0x024C, + 0x77000D: 0x0270, + 0x77000E: 0x02A0, + 0x77000F: 0x02C4, + 0x770010: 0x02EC, + 0x770011: 0x0314, + 0x770012: 0x03CC, + 0x770013: 0x0404, + 0x770014: 0x042C, + 0x770015: 0x044C, + 0x770016: 0x0478, + 0x770017: 0x049C, + 0x770018: 0x04E4, + 0x770019: 0x0504, + 0x77001A: 0x0530, + 0x77001B: 0x0554, + 0x77001C: 0x05A8, + 0x77001D: 0x0640, + 0x770200: 0x0148, + 0x770201: 0x0248, + 0x770202: 0x03C8, + 0x770203: 0x04E0, + 0x770204: 0x06A4, + 0x770205: 0x06A8, +} + +bb_bosses = { + 0x770200: 0xED85F1, + 0x770201: 0xF01360, + 0x770202: 0xEDA3DF, + 0x770203: 0xEDC2B9, + 0x770204: 0xED7C3F, + 0x770205: 0xEC29D2, +} + +level_sprites = { + 0x19B2C6: 1827, + 0x1A195C: 1584, + 0x19F6F3: 1679, + 0x19DC8B: 1717, + 0x197900: 1872 +} + +stage_tiles = { + 0: [ + 0, 1, 2, + 16, 17, 18, + 32, 33, 34, + 48, 49, 50 + ], + 1: [ + 3, 4, 5, + 19, 20, 21, + 35, 36, 37, + 51, 52, 53 + ], + 2: [ + 6, 7, 8, + 22, 23, 24, + 38, 39, 40, + 54, 55, 56 + ], + 3: [ + 9, 10, 11, + 25, 26, 27, + 41, 42, 43, + 57, 58, 59, + ], + 4: [ + 12, 13, 64, + 28, 29, 65, + 44, 45, 66, + 60, 61, 67 + ], + 5: [ + 14, 15, 68, + 30, 31, 69, + 46, 47, 70, + 62, 63, 71 + ] +} + +heart_star_address = 0x2D0000 +heart_star_size = 456 +consumable_address = 0x2F91DD +consumable_size = 698 + +stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] + +music_choices = [ + 2, # Boss 1 + 3, # Boss 2 (Unused) + 4, # Boss 3 (Miniboss) + 7, # Dedede + 9, # Event 2 (used once) + 10, # Field 1 + 11, # Field 2 + 12, # Field 3 + 13, # Field 4 + 14, # Field 5 + 15, # Field 6 + 16, # Field 7 + 17, # Field 8 + 18, # Field 9 + 19, # Field 10 + 20, # Field 11 + 21, # Field 12 (Gourmet Race) + 23, # Dark Matter in the Hyper Zone + 24, # Zero + 25, # Level 1 + 26, # Level 2 + 27, # Level 4 + 28, # Level 3 + 29, # Heart Star Failed + 30, # Level 5 + 31, # Minigame + 38, # Animal Friend 1 + 39, # Animal Friend 2 + 40, # Animal Friend 3 +] +# extra room pointers we don't want to track other than for music +room_music = { + 3079990: 23, # Zero + 2983409: 2, # BB Whispy + 3150688: 2, # BB Acro + 2991071: 2, # BB PonCon + 2998969: 2, # BB Ado + 2980927: 7, # BB Dedede + 2894290: 23 # BB Zero +} + +enemy_remap = { + "Waddle Dee": 0, + "Bronto Burt": 2, + "Rocky": 3, + "Bobo": 5, + "Chilly": 6, + "Poppy Bros Jr.": 7, + "Sparky": 8, + "Polof": 9, + "Broom Hatter": 11, + "Cappy": 12, + "Bouncy": 13, + "Nruff": 15, + "Glunk": 16, + "Togezo": 18, + "Kabu": 19, + "Mony": 20, + "Blipper": 21, + "Squishy": 22, + "Gabon": 24, + "Oro": 25, + "Galbo": 26, + "Sir Kibble": 27, + "Nidoo": 28, + "Kany": 29, + "Sasuke": 30, + "Yaban": 32, + "Boten": 33, + "Coconut": 34, + "Doka": 35, + "Icicle": 36, + "Pteran": 39, + "Loud": 40, + "Como": 41, + "Klinko": 42, + "Babut": 43, + "Wappa": 44, + "Mariel": 45, + "Tick": 48, + "Apolo": 49, + "Popon Ball": 50, + "KeKe": 51, + "Magoo": 53, + "Raft Waddle Dee": 57, + "Madoo": 58, + "Corori": 60, + "Kapar": 67, + "Batamon": 68, + "Peran": 72, + "Bobin": 73, + "Mopoo": 74, + "Gansan": 75, + "Bukiset (Burning)": 76, + "Bukiset (Stone)": 77, + "Bukiset (Ice)": 78, + "Bukiset (Needle)": 79, + "Bukiset (Clean)": 80, + "Bukiset (Parasol)": 81, + "Bukiset (Spark)": 82, + "Bukiset (Cutter)": 83, + "Waddle Dee Drawing": 84, + "Bronto Burt Drawing": 85, + "Bouncy Drawing": 86, + "Kabu (Dekabu)": 87, + "Wapod": 88, + "Propeller": 89, + "Dogon": 90, + "Joe": 91 +} + +miniboss_remap = { + "Captain Stitch": 0, + "Yuki": 1, + "Blocky": 2, + "Jumper Shoot": 3, + "Boboo": 4, + "Haboki": 5 +} + +ability_remap = { + "No Ability": 0, + "Burning Ability": 1, + "Stone Ability": 2, + "Ice Ability": 3, + "Needle Ability": 4, + "Clean Ability": 5, + "Parasol Ability": 6, + "Spark Ability": 7, + "Cutter Ability": 8, +} + + +class RomData: + def __init__(self, file: bytes, name: typing.Optional[str] = None): + self.file = bytearray(file) + self.name = name + + def read_byte(self, offset: int) -> int: + return self.file[offset] + + def read_bytes(self, offset: int, length: int) -> bytearray: + return self.file[offset:offset + length] + + def write_byte(self, offset: int, value: int) -> None: + self.file[offset] = value + + def write_bytes(self, offset: int, values: typing.Sequence[int]) -> None: + self.file[offset:offset + len(values)] = values + + def get_bytes(self) -> bytes: + return bytes(self.file) + + +def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray], palettes: List[List[bytearray]]) \ + -> Tuple[List[bytearray], List[bytearray]]: + palette_by_level = list() + for palette in palettes: + palette_by_level.extend(palette[10:16]) + out_palettes = list() + for i in range(5): + for j in range(6): + palettes[i][10 + j] = palette_by_level[stages[i][j]] + out_palettes.append(bytearray([x for palette in palettes[i] for x in palette])) + tiles_by_level = list() + for spritesheet in sprites: + decompressed = hal_decompress(spritesheet) + tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] + tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) + out_sprites = list() + for world in range(5): + levels = [stages[world][x] for x in range(6)] + world_tiles: typing.List[bytes] = [bytes() for _ in range(72)] + for i in range(6): + for x in range(12): + world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] + out_sprites.append(bytearray()) + for tile in world_tiles: + out_sprites[world].extend(tile) + # insert our fake compression + out_sprites[world][0:0] = [0xe3, 0xff] + out_sprites[world][1026:1026] = [0xe3, 0xff] + out_sprites[world][2052:2052] = [0xe0, 0xff] + out_sprites[world].append(0xff) + return out_sprites, out_palettes + + +def write_heart_star_sprites(rom: RomData) -> None: + compressed = rom.read_bytes(heart_star_address, heart_star_size) + decompressed = hal_decompress(compressed) + patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) + patched = bytearray(bsdiff4.patch(decompressed, patch)) + rom.write_bytes(0x1AF7DF, patched) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD000, patched) + rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) + + +def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> None: + compressed = rom.read_bytes(consumable_address, consumable_size) + decompressed = hal_decompress(compressed) + patched = bytearray(decompressed) + if consumables: + patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + if stars: + patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD500, patched) + rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) + + +class KDL3PatchExtensions(APPatchExtension): + game = "Kirby's Dream Land 3" + + @staticmethod + def apply_post_patch(_: APProcedurePatch, rom: bytes) -> bytes: + rom_data = RomData(rom) + write_heart_star_sprites(rom_data) + if rom_data.read_bytes(0x3D014, 1)[0] > 0: + stages = [struct.unpack("HHHHHHH", rom_data.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] + palettes = [rom_data.read_bytes(full_pal, 512) for full_pal in stage_palettes] + read_palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] + sprites = [rom_data.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] + sprites, palettes = handle_level_sprites(stages, sprites, read_palettes) + for addr, palette in zip(stage_palettes, palettes): + rom_data.write_bytes(addr, palette) + for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): + rom_data.write_bytes(addr, level_sprite) + rom_data.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, + 0x50, 0xC4, 0x39]) + write_consumable_sprites(rom_data, rom_data.read_byte(0x3D018) > 0, rom_data.read_byte(0x3D01A) > 0) + return rom_data.get_bytes() + + +class KDL3ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [KDL3UHASH, KDL3JHASH] + game = "Kirby's Dream Land 3" + patch_file_ending = ".apkdl3" + procedure = [ + ("apply_bsdiff4", ["kdl3_basepatch.bsdiff4"]), + ("apply_tokens", ["token_patch.bin"]), + ("apply_post_patch", []), + ("calc_snes_crc", []) + ] + name: bytes # used to pass to __init__ + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None: + patch.write_file("kdl3_basepatch.bsdiff4", + get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) + + # Write open world patch + if world.options.open_world: + patch.write_token(APTokenTypes.WRITE, 0x143C7, bytes([0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])) + # changes the stage flag function to compare $5AC1 to $5AC1, + # always running the "new stage" function + # This has further checks present for bosses already, so we just + # need to handle regular stages + # write check for boss to be unlocked + + if world.options.consumables: + # reroute maxim tomatoes to use the 1-UP function, then null out the function + patch.write_token(APTokenTypes.WRITE, 0x3002F, bytes([0x37, 0x00])) + patch.write_token(APTokenTypes.WRITE, 0x30037, bytes([0xA9, 0x26, 0x00, # LDA #$0026 + 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 + 0xA4, 0xD2, # LDY $D2 + 0x6B, # RTL + 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, + 0xEA, # NOP #10 + ])) + + # stars handling is built into the rom, so no changes there + + rooms = world.rooms + if world.options.music_shuffle > 0: + if world.options.music_shuffle == 1: + shuffled_music = music_choices.copy() + world.random.shuffle(shuffled_music) + music_map = dict(zip(music_choices, shuffled_music)) + # Avoid putting star twinkle in the pool + music_map[5] = world.random.choice(music_choices) + # Heart Star music doesn't work on regular stages + music_map[8] = world.random.choice(music_choices) + for room in rooms: + room.music = music_map[room.music] + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, bytes([music_map[room_music[room_ptr]]])) + for i, old_music in zip(range(5), [25, 26, 28, 27, 30]): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, bytes([music_map[old_music]])) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, music_map[0x18].to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, music_map[0x08].to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, music_map[0x1D].to_bytes(1, "little")) + elif world.options.music_shuffle == 2: + for room in rooms: + room.music = world.random.choice(music_choices) + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, + world.random.choice(music_choices).to_bytes(1, "little")) + for i in range(5): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, + world.random.choice(music_choices).to_bytes(1, "little")) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, world.random.choice(music_choices).to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, world.random.choice(music_choices).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, world.random.choice(music_choices).to_bytes(1, "little")) + + for room in rooms: + room.patch(patch, bool(world.options.consumables.value), not bool(world.options.remote_items.value)) + + if world.options.virtual_console in [1, 3]: + # Flash Reduction + patch.write_token(APTokenTypes.WRITE, 0x9AE68, b"\x10") + patch.write_token(APTokenTypes.WRITE, 0x9AE8E, bytes([0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])) + patch.write_token(APTokenTypes.WRITE, 0x9AEA1, b"\x08") + patch.write_token(APTokenTypes.WRITE, 0x9AEC9, b"\x01") + patch.write_token(APTokenTypes.WRITE, 0x9AED2, bytes([0xA9, 0x1F])) + patch.write_token(APTokenTypes.WRITE, 0x9AEE1, b"\x08") + + if world.options.virtual_console in [2, 3]: + # Hyper Zone BB colors + patch.write_token(APTokenTypes.WRITE, 0x2C5E16, bytes([0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])) + patch.write_token(APTokenTypes.WRITE, 0x2C8217, bytes([0xFF, 0x1E, ])) + + # boss requirements + patch.write_token(APTokenTypes.WRITE, 0x3D000, + struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], + world.boss_requirements[2], world.boss_requirements[3], + world.boss_requirements[4])) + patch.write_token(APTokenTypes.WRITE, 0x3D00A, + struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) + patch.write_token(APTokenTypes.WRITE, 0x3D00C, world.options.goal_speed.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D00E, world.options.open_world.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D010, ((world.options.remote_items.value << 1) + + world.options.death_link.value).to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D012, world.options.goal.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D014, world.options.stage_shuffle.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little") + if world.multiworld.players > 1 else bytes([0, 0])) + patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little")) + # don't write gifting for solo game, since there's no one to send anything to + + for level in world.player_levels: + for i in range(len(world.player_levels[level])): + patch.write_token(APTokenTypes.WRITE, 0x3F002E + ((level - 1) * 14) + (i * 2), + struct.pack("H", level_pointers[world.player_levels[level][i]])) + patch.write_token(APTokenTypes.WRITE, 0x3D020 + (level - 1) * 14 + (i * 2), + struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) + if (i == 0) or (i > 0 and i % 6 != 0): + patch.write_token(APTokenTypes.WRITE, 0x3D080 + (level - 1) * 12 + (i * 2), + struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) + + for i in range(6): + if world.boss_butch_bosses[i]: + patch.write_token(APTokenTypes.WRITE, 0x3F0000 + (level_pointers[0x770200 + i]), + struct.pack("I", bb_bosses[0x770200 + i])) + + # copy ability shuffle + if world.options.copy_ability_randomization.value > 0: + for enemy in world.copy_abilities: + if enemy in miniboss_remap: + patch.write_token(APTokenTypes.WRITE, 0xB417E + (miniboss_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + else: + patch.write_token(APTokenTypes.WRITE, 0xB3CAC + (enemy_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + # following only needs done on non-door rando + # incredibly lucky this follows the same order (including 5E == star block) + patch.write_token(APTokenTypes.WRITE, 0x2F77EA, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F7811, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BC4, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BEB, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC06, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC2D, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9E7B, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9EA2, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA951, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA978, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA132, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA159, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA3E8, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA40F, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F90E2, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9109, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + + if world.options.copy_ability_randomization == 2: + for enemy in enemy_remap: + # we just won't include it for minibosses + patch.write_token(APTokenTypes.WRITE, 0xB3E40 + (enemy_remap[enemy] << 1), + struct.pack("h", world.random.randint(-1, 2))) + + # write jumping goal + patch.write_token(APTokenTypes.WRITE, 0x94F8, struct.pack("H", world.options.jumping_target)) + patch.write_token(APTokenTypes.WRITE, 0x944E, struct.pack("H", world.options.jumping_target)) + + from Utils import __version__ + patch_name = bytearray( + f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] + patch_name.extend([0] * (21 - len(patch_name))) + patch.name = bytes(patch_name) + patch.write_token(APTokenTypes.WRITE, 0x3C000, patch.name) + patch.write_token(APTokenTypes.WRITE, 0x3C020, world.options.game_language.value.to_bytes(1, "little")) + + patch.write_token(APTokenTypes.COPY, 0x7FC0, (21, 0x3C000)) + patch.write_token(APTokenTypes.COPY, 0x7FD9, (1, 0x3C020)) + + # handle palette + if world.options.kirby_flavor_preset.value != 0: + for addr in kirby_target_palettes: + target = kirby_target_palettes[addr] + palette = get_kirby_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + if world.options.gooey_flavor_preset.value != 0: + for addr in gooey_target_palettes: + target = gooey_target_palettes[addr] + palette = get_gooey_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +def get_base_rom_bytes() -> bytes: + rom_file: str = get_base_rom_path() + base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: + raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " + "Get the correct game and version, then dump it") + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: settings.Settings = settings.get_settings() + if not file_name: + file_name = options["kdl3_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/kdl3/room.py b/worlds/kdl3/room.py new file mode 100644 index 0000000000..bcc1c7a709 --- /dev/null +++ b/worlds/kdl3/room.py @@ -0,0 +1,133 @@ +import struct +from typing import Optional, Dict, TYPE_CHECKING, List, Union +from BaseClasses import Region, ItemClassification, MultiWorld +from worlds.Files import APTokenTypes +from .client_addrs import consumable_addrs, star_addrs + +if TYPE_CHECKING: + from .rom import KDL3ProcedurePatch + +animal_map = { + "Rick Spawn": 0, + "Kine Spawn": 1, + "Coo Spawn": 2, + "Nago Spawn": 3, + "ChuChu Spawn": 4, + "Pitch Spawn": 5 +} + + +class KDL3Room(Region): + pointer: int = 0 + level: int = 0 + stage: int = 0 + room: int = 0 + music: int = 0 + default_exits: List[Dict[str, Union[int, List[str]]]] + animal_pointers: List[int] + enemies: List[str] + entity_load: List[List[int]] + consumables: List[Dict[str, Union[int, str]]] + + def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str], level: int, + stage: int, room: int, pointer: int, music: int, + default_exits: List[Dict[str, List[str]]], + animal_pointers: List[int], enemies: List[str], + entity_load: List[List[int]], + consumables: List[Dict[str, Union[int, str]]], consumable_pointer: int) -> None: + super().__init__(name, player, multiworld, hint) + self.level = level + self.stage = stage + self.room = room + self.pointer = pointer + self.music = music + self.default_exits = default_exits + self.animal_pointers = animal_pointers + self.enemies = enemies + self.entity_load = entity_load + self.consumables = consumables + self.consumable_pointer = consumable_pointer + + def patch(self, patch: "KDL3ProcedurePatch", consumables: bool, local_items: bool) -> None: + patch.write_token(APTokenTypes.WRITE, self.pointer + 2, self.music.to_bytes(1, "little")) + animals = [x.item.name for x in self.locations if "Animal" in x.name and x.item] + if len(animals) > 0: + for current_animal, address in zip(animals, self.animal_pointers): + patch.write_token(APTokenTypes.WRITE, self.pointer + address + 7, + animal_map[current_animal].to_bytes(1, "little")) + if local_items: + for location in self.get_locations(): + if location.item is None or location.item.player != self.player: + continue + item = location.item.code + if item is None: + continue + item_idx = item & 0x00000F + location_idx = location.address & 0xFFFF + if location_idx & 0xF00 in (0x300, 0x400, 0x500, 0x600): + # consumable or star, need remapped + location_base = location_idx & 0xF00 + if location_base == 0x300: + # consumable + location_idx = consumable_addrs[location_idx & 0xFF] | 0x1000 + else: + # star + location_idx = star_addrs[location.address] | 0x2000 + if item & 0x000070 == 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x10])) + elif item & 0x000010 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x20])) + elif item & 0x000020 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x40])) + elif item & 0x000040 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x80])) + + if consumables: + load_len = len(self.entity_load) + for consumable in self.consumables: + location = next(x for x in self.locations if x.name == consumable["name"]) + assert location.item is not None + is_progression = location.item.classification & ItemClassification.progression + if load_len == 8: + # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them + if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) + and any(x in self.entity_load for x in [[2, 22], [3, 22]])): + replacement_target = self.entity_load.index( + next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) + if is_progression: + vtype = 0 + else: + vtype = 2 + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (replacement_target * 2), + vtype.to_bytes(1, "little")) + self.entity_load[replacement_target] = [vtype, 22] + else: + if is_progression: + # we need to see if 1-ups are in our load list + if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): + self.entity_load.append([0, 22]) + else: + if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): + # edge case: if (1, 22) is in, we need to load (3, 22) instead + if [1, 22] in self.entity_load: + self.entity_load.append([3, 22]) + else: + self.entity_load.append([2, 22]) + if load_len < len(self.entity_load): + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (load_len * 2), + bytes(self.entity_load[load_len])) + patch.write_token(APTokenTypes.WRITE, self.pointer + 104 + (load_len * 2), + bytes(struct.pack("H", self.consumable_pointer))) + if is_progression: + if [1, 22] in self.entity_load: + vtype = 1 + else: + vtype = 0 + else: + if [3, 22] in self.entity_load: + vtype = 3 + else: + vtype = 2 + assert isinstance(consumable["pointer"], int) + patch.write_token(APTokenTypes.WRITE, self.pointer + consumable["pointer"] + 7, + vtype.to_bytes(1, "little")) diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/rules.py similarity index 70% rename from worlds/kdl3/Rules.py rename to worlds/kdl3/rules.py index 6a85ef84f0..a08e99257e 100644 --- a/worlds/kdl3/Rules.py +++ b/worlds/kdl3/rules.py @@ -1,7 +1,7 @@ from worlds.generic.Rules import set_rule, add_rule -from .Names import LocationName, EnemyAbilities -from .Locations import location_table -from .Options import GoalSpeed +from .names import location_name, enemy_abilities, animal_friend_spawns +from .locations import location_table +from .options import GoalSpeed import typing if typing.TYPE_CHECKING: @@ -10,9 +10,9 @@ if typing.TYPE_CHECKING: def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int, - ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]): + ow_boss_req: int, player_levels: typing.Dict[int, typing.List[int]]) -> bool: if open_world: - return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) + return state.has(f"{location_name.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) else: return state.can_reach(location_table[player_levels[level][5]], "Location", player) @@ -86,11 +86,11 @@ ability_map: typing.Dict[str, typing.Callable[["CollectionState", int], bool]] = } -def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: # check animal requirements if not (can_reach_coo(state, player) and can_reach_kine(state, player)): return False - for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]: + for abilities, bukisets in enemy_abilities.enemy_restrictive[1:5]: iterator = iter(x for x in bukisets if copy_abilities[x] in abilities) target_bukiset = next(iterator, None) can_reach = False @@ -103,7 +103,7 @@ def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typi return can_reach_parasol(state, player) and can_reach_stone(state, player) -def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: can_reach = True for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}: can_reach = can_reach & ability_map[copy_abilities[enemy]](state, player) @@ -112,114 +112,114 @@ def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: t def set_rules(world: "KDL3World") -> None: # Level 1 - set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player), lambda state: can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_chao, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_mine, world.player), lambda state: can_reach_kine(state, world.player)) # Level 2 - set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_kamuribana, world.player), lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_bakasa, world.player), lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_toad, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_mama_pitch, world.player), lambda state: (can_reach_pitch(state, world.player) and can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) # Level 3 - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5, world.player), lambda state: can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_auntie, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_nyupun, world.player), lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_rob, world.player), lambda state: can_assemble_rob(state, world.player, world.copy_abilities) ) # Level 4 - set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_hibanamodoki, world.player), lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_piyokeko, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_mikarin, world.player), lambda state: can_reach_coo(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_pick, world.player), lambda state: can_reach_rick(state, world.player)) # Level 5 - set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_4, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_kogoesou, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_samus, world.player), lambda state: can_reach_ice(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_name, world.player), lambda state: (can_reach_coo(state, world.player) and can_reach_burning(state, world.player) and can_reach_chuchu(state, world.player))) # ChuChu is guaranteed here, but we use this for consistency - set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_shiro, world.player), lambda state: can_reach_nago(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_angel, world.player), lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities)) # Consumables if world.options.consumables: - set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_u1, world.player), lambda state: can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_m1, world.player), lambda state: can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_2_u1, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_u1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_3_u1, world.player), lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_u1, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_m2, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_u1, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m2, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_u1, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_m2, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), lambda state: can_reach_cutter(state, world.player)) if world.options.starsanity: @@ -274,50 +274,57 @@ def set_rules(world: "KDL3World") -> None: # copy ability access edge cases # Kirby cannot eat enemies fully submerged in water. Vast majority of cases, the enemy can be brought to the surface # and eaten by inhaling while falling on top of them - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_2_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_3_E6, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) # Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E5, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E1, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E2, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E4, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E9, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) + # animal friend rules + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player), + lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), + lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) + and can_reach_burning(state, world.player)) + for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 5 Boss - Purified"], - [LocationName.grass_land_whispy, LocationName.ripple_field_acro, - LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado, - LocationName.iceberg_dedede], + [location_name.grass_land_whispy, location_name.ripple_field_acro, + location_name.sand_canyon_poncon, location_name.cloudy_park_ado, + location_name.iceberg_dedede], range(1, 6)): set_rule(world.multiworld.get_location(boss_flag, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) set_rule(world.multiworld.get_location(purification, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) @@ -327,12 +334,12 @@ def set_rules(world: "KDL3World") -> None: for level in range(2, 6): set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Defeated", world.player)) if world.options.strict_bosses: for level in range(2, 6): add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Purified", world.player)) if world.options.goal_speed == GoalSpeed.option_normal: add_rule(world.multiworld.get_entrance("To Level 6", world.player), diff --git a/worlds/kdl3/data/APPauseIcons.dat b/worlds/kdl3/src/APPauseIcons.dat similarity index 100% rename from worlds/kdl3/data/APPauseIcons.dat rename to worlds/kdl3/src/APPauseIcons.dat diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm index e419d0632f..95c85f032c 100644 --- a/worlds/kdl3/src/kdl3_basepatch.asm +++ b/worlds/kdl3/src/kdl3_basepatch.asm @@ -58,6 +58,10 @@ org $01AFC8 org $01B013 SEC ; Remove Dedede Bad Ending +org $01B050 + JSL HookBossPurify + NOP + org $02B7B0 ; Zero unlock LDA $80A0 CMP #$0001 @@ -160,7 +164,6 @@ CopyAbilityAnimalOverride: STA $39DF, X RTL -org $079A00 HeartStarCheck: TXA CMP #$0000 ; is this level 1 @@ -201,7 +204,6 @@ HeartStarCheck: SEC RTL -org $079A80 OpenWorldUnlock: PHX LDX $900E ; Are we on open world? @@ -224,7 +226,6 @@ OpenWorldUnlock: PLX RTL -org $079B00 MainLoopHook: STA $D4 INC $3524 @@ -239,16 +240,18 @@ MainLoopHook: BEQ .Return ; return if we are LDA $5541 ; gooey status BPL .Slowness ; gooey is already spawned + LDA $39D1 ; is kirby alive? + BEQ .Slowness ; branch if he isn't + ; maybe BMI here too? LDA $8080 CMP #$0000 ; did we get a gooey trap BEQ .Slowness ; branch if we did not JSL GooeySpawn - STZ $8080 + DEC $8080 .Slowness: LDA $8082 ; slowness BEQ .Eject ; are we under the effects of a slowness trap - DEC - STA $8082 ; dec by 1 each frame + DEC $8082 ; dec by 1 each frame .Eject: PHX PHY @@ -258,14 +261,13 @@ MainLoopHook: BEQ .PullVars ; branch if we haven't received eject LDA #$2000 ; select button press STA $60C1 ; write to controller mirror - STZ $8084 + DEC $8084 .PullVars: PLY PLX .Return: RTL -org $079B80 HeartStarGraphicFix: LDA #$0000 PHX @@ -288,7 +290,7 @@ HeartStarGraphicFix: ASL TAX LDA $07D080, X ; table of original stage number - CMP #$0003 ; is the current stage a minigame stage? + CMP #$0002 ; is the current stage a minigame stage? BEQ .ReturnTrue ; branch if so CLC BRA .Return @@ -299,7 +301,6 @@ HeartStarGraphicFix: PLX RTL -org $079BF0 ParseItemQueue: ; Local item queue parsing NOP @@ -336,8 +337,6 @@ ParseItemQueue: AND #$000F ASL TAY - LDA $8080,Y - BNE .LoopCheck JSL .ApplyNegative RTL .ApplyAbility: @@ -418,35 +417,73 @@ ParseItemQueue: CPY #$0005 BCS .PlayNone LDA $8080,Y - BNE .Return + CPY #$0002 + BNE .Increment + CLC LDA #$0384 + ADC $8080, Y + BVC .PlayNegative + LDA #$FFFF + .PlayNegative: STA $8080,Y LDA #$00A7 BRA .PlaySFXLong + .Increment: + INC + STA $8080, Y + BRA .PlayNegative .PlayNone: LDA #$0000 BRA .PlaySFXLong -org $079D00 AnimalFriendSpawn: PHA CPX #$0002 ; is this an animal friend? BNE .Return XBA PHA + PHX + PHA + LDX #$0000 + .CheckSpawned: + LDA $05CA, X + BNE .Continue + LDA #$0002 + CMP $074A, X + BNE .ContinueCheck + PLA + PHA + XBA + CMP $07CA, X + BEQ .AlreadySpawned + .ContinueCheck: + INX + INX + BRA .CheckSpawned + .Continue: + PLA + PLX ASL TAY PLA INC CMP $8000, Y ; do we have this animal friend BEQ .Return ; we have this animal friend + .False: INX .Return: PLY LDA #$9999 RTL + .AlreadySpawned: + PLA + PLX + ASL + TAY + PLA + BRA .False + -org $079E00 WriteBWRAM: LDY #$6001 ;starting addr LDA #$1FFE ;bytes to write @@ -479,7 +516,6 @@ WriteBWRAM: .Return: RTL -org $079E80 ConsumableSet: PHA PHX @@ -507,7 +543,6 @@ ConsumableSet: ASL TAX LDA $07D020, X ; current stage - DEC ASL #6 TAX PLA @@ -519,8 +554,16 @@ ConsumableSet: BRA .LoopHead ; return to loop head .ApplyCheck: LDA $A000, X ; consumables index + PHA ORA #$0001 STA $A000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$1000 + JSL ApplyLocalCheck + .Return: PLY PLX PLA @@ -528,7 +571,6 @@ ConsumableSet: AND #$00FF RTL -org $079F00 NormalGoalSet: PHX LDA $07D012 @@ -549,7 +591,6 @@ NormalGoalSet: STA $5AC1 ; cutscene RTL -org $079F80 FinalIcebergFix: PHX PHY @@ -572,7 +613,7 @@ FinalIcebergFix: ASL TAX LDA $07D020, X - CMP #$001E + CMP #$001D BEQ .ReturnTrue CLC BRA .Return @@ -583,7 +624,6 @@ FinalIcebergFix: PLX RTL -org $07A000 StrictBosses: PHX LDA $901E ; Do we have strict bosses enabled? @@ -610,7 +650,6 @@ StrictBosses: LDA $53CD RTL -org $07A030 NintenHalken: LDX #$0005 .Halken: @@ -628,7 +667,6 @@ NintenHalken: LDA #$0001 RTL -org $07A080 StageCompleteSet: PHX LDA $5AC1 ; completed stage cutscene @@ -656,9 +694,17 @@ StageCompleteSet: ASL TAX LDA $9020, X ; load the stage we completed - DEC ASL TAX + PHX + LDA $8200, X + AND #$00FF + BNE .ApplyClear + TXA + LSR + JSL ApplyLocalCheck + .ApplyClear: + PLX LDA #$0001 ORA $8200, X STA $8200, X @@ -668,7 +714,6 @@ StageCompleteSet: CMP $53CB RTL -org $07A100 OpenWorldBossUnlock: PHX PHY @@ -699,7 +744,6 @@ OpenWorldBossUnlock: .LoopStage: PLX LDY $9020, X ; get stage id - DEY INX INX PHA @@ -732,7 +776,6 @@ OpenWorldBossUnlock: PLX RTL -org $07A180 GooeySpawn: PHY PHX @@ -768,7 +811,6 @@ GooeySpawn: PLY RTL -org $07A200 SpeedTrap: PHX LDX $8082 ; do we have slowness @@ -780,7 +822,6 @@ SpeedTrap: EOR #$FFFF RTL -org $07A280 HeartStarVisual: CPX #$0000 BEQ .SkipInx @@ -844,7 +885,6 @@ HeartStarVisual: .Return: RTL -org $07A300 LoadFont: JSL $00D29F ; play sfx PHX @@ -915,7 +955,6 @@ LoadFont: PLX RTL -org $07A380 HeartStarVisual2: LDA #$2C80 STA $0000, Y @@ -1029,14 +1068,12 @@ HeartStarVisual2: STA $0000, Y RTL -org $07A480 HeartStarSelectFix: PHX TXA ASL TAX LDA $9020, X - DEC TAX .LoopHead: CMP #$0006 @@ -1051,15 +1088,31 @@ HeartStarSelectFix: AND #$00FF RTL -org $07A500 HeartStarCutsceneFix: TAX LDA $53D3 DEC STA $5AC3 + LDA $53A7, X + AND #$00FF + BNE .Return + PHX + TXA + .Loop: + CMP #$0007 + BCC .Continue + SEC + SBC #$0007 + DEX + BRA .Loop + .Continue: + TXA + ORA #$0100 + JSL ApplyLocalCheck + PLX + .Return RTL -org $07A510 GiftGiving: CMP #$0008 .This: @@ -1075,7 +1128,6 @@ GiftGiving: PLX JML $CABC18 -org $07A550 PauseMenu: JSL $00D29F PHX @@ -1136,7 +1188,6 @@ PauseMenu: PLX RTL -org $07A600 StarsSet: PHA PHX @@ -1166,7 +1217,6 @@ StarsSet: ASL TAX LDA $07D020, X - DEC ASL ASL ASL @@ -1183,8 +1233,15 @@ StarsSet: BRA .2LoopHead .2LoopEnd: LDA $B000, X + PHA ORA #$0001 STA $B000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$2000 + JSL ApplyLocalCheck .Return: PLY PLX @@ -1199,6 +1256,48 @@ StarsSet: STA $39D7 BRA .Return +ApplyLocalCheck: +; args: A-address of check following $08B000 + TAX + LDA $09B000, X + AND #$00FF + TAY + LDX #$0000 + .Loop: + LDA $C000, X + BEQ .Apply + INX + INX + CPX #$0010 + BCC .Loop + BRA .Return ; this is dangerous, could lose a check here + .Apply: + TYA + STA $C000, X + .Return: + RTL + +HookBossPurify: + ORA $B0 + STA $53D5 + LDA $B0 + LDX #$0000 + LSR + .Loop: + BIT #$0001 + BNE .Apply + LSR + LSR + INX + CPX #$0005 + BCS .Return + BRA .Loop + .Apply: + TXA + ORA #$0200 + JSL ApplyLocalCheck + .Return: + RTL org $07C000 db "KDL3_BASEPATCH_ARCHI" @@ -1234,4 +1333,7 @@ org $07E040 db $3A, $01 db $3B, $05 db $3C, $05 - db $3D, $05 \ No newline at end of file + db $3D, $05 + +org $07F000 +incbin "APPauseIcons.dat" \ No newline at end of file diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 4d3f4d70fa..92f1d7261f 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -6,6 +6,8 @@ from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all +# mypy: ignore-errors +# This is a copy of core code, and I'm not smart enough to solve the errors in here class KDL3TestBase(WorldTestBase): diff --git a/worlds/kdl3/test/test_goal.py b/worlds/kdl3/test/test_goal.py index ce53642a97..2c6ae614d4 100644 --- a/worlds/kdl3/test/test_goal.py +++ b/worlds/kdl3/test/test_goal.py @@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "fast", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -51,14 +51,14 @@ class TestNormalGoal(KDL3TestBase): self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.assertBeatable(True) - def test_kine(self): + def test_kine(self) -> None: self.collect_by_name(["Cutter", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_cutter(self): + def test_cutter(self) -> None: self.collect_by_name(["Kine", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_burning(self): + def test_burning(self) -> None: self.collect_by_name(["Cutter", "Kine", "Heart Star"]) self.assertBeatable(False) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index bde9abc409..024f1b11a5 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,6 +1,6 @@ from . import KDL3TestBase +from ..names import location_name from Options import PlandoConnection -from ..Names import LocationName import typing @@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase): # these ensure we can always reach all stages physically } - def test_simple_heart_stars(self): - self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"]) - self.run_location_test(LocationName.grass_land_chao, ["Stone"]) - self.run_location_test(LocationName.grass_land_mine, ["Kine"]) - self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"]) - self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"]) - self.run_location_test(LocationName.ripple_field_toad, ["Needle"]) - self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) - self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"]) - self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]), - self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"]) - self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"]) - self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"]) - self.run_location_test(LocationName.cloudy_park_pick, ["Rick"]) - self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) - self.run_location_test(LocationName.iceberg_samus, ["Ice"]) - self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) - self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", + def test_simple_heart_stars(self) -> None: + self.run_location_test(location_name.grass_land_muchi, ["ChuChu"]) + self.run_location_test(location_name.grass_land_chao, ["Stone"]) + self.run_location_test(location_name.grass_land_mine, ["Kine"]) + self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"]) + self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"]) + self.run_location_test(location_name.ripple_field_toad, ["Needle"]) + self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) + self.run_location_test(location_name.sand_canyon_auntie, ["Clean"]) + self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]) + self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"]) + self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"]) + self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"]) + self.run_location_test(location_name.cloudy_park_pick, ["Rick"]) + self.run_location_test(location_name.iceberg_kogoesou, ["Burning"]) + self.run_location_test(location_name.iceberg_samus, ["Ice"]) + self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"]) + self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"]) - def run_location_test(self, location: str, itempool: typing.List[str]): + def run_location_test(self, location: str, itempool: typing.List[str]) -> None: items = itempool.copy() while len(itempool) > 0: self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed)) @@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase): "plando_options": "connections" } - def test_shiro(self): + def test_shiro(self) -> None: self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.collect_by_name("Nago") self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py index d676b641b0..3ba376d068 100644 --- a/worlds/kdl3/test/test_shuffles.py +++ b/worlds/kdl3/test/test_shuffles.py @@ -1,47 +1,61 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional from . import KDL3TestBase -from ..Room import KDL3Room +from ..room import KDL3Room +from ..names import animal_friend_spawns class TestCopyAbilityShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter_and_burning_reachable(self): + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -63,7 +77,7 @@ class TestCopyAbilityShuffle(KDL3TestBase): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -74,13 +88,13 @@ class TestCopyAbilityShuffle(KDL3TestBase): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -103,91 +117,147 @@ class TestAnimalShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") class TestAllShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") - def test_cutter_and_burning_reachable(self): + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") + + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -209,7 +279,7 @@ class TestAllShuffle(KDL3TestBase): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -220,13 +290,13 @@ class TestAllShuffle(KDL3TestBase): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -242,4 +312,4 @@ class TestAllShuffle(KDL3TestBase): self.collect_by_name(["Cutter"]) self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), - ''.join(str(self.multiworld.seed)).join(collected_abilities)) + f"Seed: {self.multiworld.seed}, Collected: {collected_abilities}") From 8ed466bf245e600cefb186fea547960b6b3de31f Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Sat, 31 Aug 2024 06:30:42 -0500 Subject: [PATCH 32/50] Shivers: Add collect behavior option. (#3854) * Add collect behavior option. * Add comma Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/shivers/Options.py | 16 ++++++++++++++++ worlds/shivers/__init__.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index 2f33eb18e5..72791bef3e 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -92,6 +92,21 @@ class FullPots(Choice): option_mixed = 2 +class PuzzleCollectBehavior(Choice): + """ + Defines what happens to puzzles on collect. + - Solve None: No puzzles will be solved when collected. + - Prevent Out Of Logic Access: All puzzles, except Red Door and Skull Door, will be solved when collected. + This prevents out of logic access to Gods Room and Slide. + - Solve All: All puzzles will be solved when collected. (original behavior) + """ + display_name = "Puzzle Collect Behavior" + option_solve_none = 0 + option_prevent_out_of_logic_access = 1 + option_solve_all = 2 + default = 1 + + @dataclass class ShiversOptions(PerGameCommonOptions): ixupi_captures_needed: IxupiCapturesNeeded @@ -104,3 +119,4 @@ class ShiversOptions(PerGameCommonOptions): early_lightning: EarlyLightning location_pot_pieces: LocationPotPieces full_pots: FullPots + puzzle_collect_behavior: PuzzleCollectBehavior diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index a2d7bc1464..3ca87ae164 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -219,7 +219,8 @@ class ShiversWorld(World): "ElevatorsStaySolved": self.options.elevators_stay_solved.value, "EarlyBeth": self.options.early_beth.value, "EarlyLightning": self.options.early_lightning.value, - "FrontDoorUsable": self.options.front_door_usable.value + "FrontDoorUsable": self.options.front_door_usable.value, + "PuzzleCollectBehavior": self.options.puzzle_collect_behavior.value, } From f81335d614fdec431564062d9f71b4553a5c9355 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Sat, 31 Aug 2024 04:44:09 -0700 Subject: [PATCH 33/50] DS3: Don't return early in the location loop (#3856) This caused behavior errors when some locations in a group were excluded and others were not. --- worlds/dark_souls_3/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c31a3681df..f6e5cde615 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1292,10 +1292,10 @@ class DarkSouls3World(World): locations = location if isinstance(location, list) else [location] for location in locations: data = location_dictionary[location] - if data.dlc and not self.options.enable_dlc: return - if data.ngp and not self.options.enable_ngp: return + if data.dlc and not self.options.enable_dlc: continue + if data.ngp and not self.options.enable_ngp: continue - if not self._is_location_available(location): return + if not self._is_location_available(location): continue if isinstance(rule, str): assert item_dictionary[rule].classification == ItemClassification.progression rule = lambda state, item=rule: state.has(item, self.player) From b37bb60891a9a45838491a621562f8f970e34c55 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 31 Aug 2024 07:44:48 -0400 Subject: [PATCH 34/50] DS3: Prevent prioritized+excluded locations (#3855) --- worlds/dark_souls_3/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index f6e5cde615..46c7ef1336 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1252,6 +1252,9 @@ class DarkSouls3World(World): lambda item: not item.advancement ) + # Prevent the player from prioritizing and "excluding" the same location + self.options.priority_locations.value -= allow_useful_locations + if self.options.excluded_location_behavior == "allow_useful": self.options.exclude_locations.value.clear() From 7e0219c214dca799e85b908e0f7a14d5430ca460 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 31 Aug 2024 07:49:33 -0400 Subject: [PATCH 35/50] SM and SMZ3 option_definitions deprecation fix (#3372) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * replaced deprecated use of option_definitions for SM and SMZ3 by options_dataclass * fixed missed references to option_definitions * Update worlds/sm/variaRandomizer/utils/utils.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * fixed conflicts and made SMZ3 accessibility related code more future proof * Update worlds/smz3/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/smz3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/sm/Options.py | 121 ++++++++++++----------- worlds/sm/__init__.py | 38 ++++--- worlds/sm/variaRandomizer/randomizer.py | 14 +-- worlds/sm/variaRandomizer/utils/utils.py | 72 +++++++------- worlds/smz3/Options.py | 36 +++---- worlds/smz3/__init__.py | 40 ++++---- 6 files changed, 161 insertions(+), 160 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 223179529c..3dad16ad3a 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,6 +1,7 @@ import typing -from Options import Choice, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle +from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle from .variaRandomizer.utils.objectives import _goals +from dataclasses import dataclass class StartItemsRemovesFromPool(Toggle): """Remove items in starting inventory from pool.""" @@ -372,62 +373,62 @@ class RelaxedRoundRobinCF(Toggle): """ display_name = "Relaxed round robin Crystal Flash" -sm_options: typing.Dict[str, type(Option)] = { - "start_inventory_removes_from_pool": StartItemsRemovesFromPool, - "preset": Preset, - "start_location": StartLocation, - "remote_items": RemoteItems, - "death_link": DeathLink, - #"majors_split": "Full", - #"scav_num_locs": "10", - #"scav_randomized": "off", - #"scav_escape": "off", - "max_difficulty": MaxDifficulty, - #"progression_speed": "medium", - #"progression_difficulty": "normal", - "morph_placement": MorphPlacement, - #"suits_restriction": SuitsRestriction, - "hide_items": HideItems, - "strict_minors": StrictMinors, - "missile_qty": MissileQty, - "super_qty": SuperQty, - "power_bomb_qty": PowerBombQty, - "minor_qty": MinorQty, - "energy_qty": EnergyQty, - "area_randomization": AreaRandomization, - "area_layout": AreaLayout, - "doors_colors_rando": DoorsColorsRando, - "allow_grey_doors": AllowGreyDoors, - "boss_randomization": BossRandomization, - #"minimizer": "off", - #"minimizer_qty": "45", - #"minimizer_tourian": "off", - "escape_rando": EscapeRando, - "remove_escape_enemies": RemoveEscapeEnemies, - "fun_combat": FunCombat, - "fun_movement": FunMovement, - "fun_suits": FunSuits, - "layout_patches": LayoutPatches, - "varia_tweaks": VariaTweaks, - "nerfed_charge": NerfedCharge, - "gravity_behaviour": GravityBehaviour, - #"item_sounds": "on", - "elevators_speed": ElevatorsSpeed, - "fast_doors": DoorsSpeed, - "spin_jump_restart": SpinJumpRestart, - "rando_speed": SpeedKeep, - "infinite_space_jump": InfiniteSpaceJump, - "refill_before_save": RefillBeforeSave, - "hud": Hud, - "animals": Animals, - "no_music": NoMusic, - "random_music": RandomMusic, - "custom_preset": CustomPreset, - "varia_custom_preset": VariaCustomPreset, - "tourian": Tourian, - "custom_objective": CustomObjective, - "custom_objective_list": CustomObjectiveList, - "custom_objective_count": CustomObjectiveCount, - "objective": Objective, - "relaxed_round_robin_cf": RelaxedRoundRobinCF, - } +@dataclass +class SMOptions(PerGameCommonOptions): + start_inventory_removes_from_pool: StartItemsRemovesFromPool + preset: Preset + start_location: StartLocation + remote_items: RemoteItems + death_link: DeathLink + #majors_split: "Full" + #scav_num_locs: "10" + #scav_randomized: "off" + #scav_escape: "off" + max_difficulty: MaxDifficulty + #progression_speed": "medium" + #progression_difficulty": "normal" + morph_placement: MorphPlacement + #suits_restriction": SuitsRestriction + hide_items: HideItems + strict_minors: StrictMinors + missile_qty: MissileQty + super_qty: SuperQty + power_bomb_qty: PowerBombQty + minor_qty: MinorQty + energy_qty: EnergyQty + area_randomization: AreaRandomization + area_layout: AreaLayout + doors_colors_rando: DoorsColorsRando + allow_grey_doors: AllowGreyDoors + boss_randomization: BossRandomization + #minimizer: "off" + #minimizer_qty: "45" + #minimizer_tourian: "off" + escape_rando: EscapeRando + remove_escape_enemies: RemoveEscapeEnemies + fun_combat: FunCombat + fun_movement: FunMovement + fun_suits: FunSuits + layout_patches: LayoutPatches + varia_tweaks: VariaTweaks + nerfed_charge: NerfedCharge + gravity_behaviour: GravityBehaviour + #item_sounds: "on" + elevators_speed: ElevatorsSpeed + fast_doors: DoorsSpeed + spin_jump_restart: SpinJumpRestart + rando_speed: SpeedKeep + infinite_space_jump: InfiniteSpaceJump + refill_before_save: RefillBeforeSave + hud: Hud + animals: Animals + no_music: NoMusic + random_music: RandomMusic + custom_preset: CustomPreset + varia_custom_preset: VariaCustomPreset + tourian: Tourian + custom_objective: CustomObjective + custom_objective_list: CustomObjectiveList + custom_objective_count: CustomObjectiveCount + objective: Objective + relaxed_round_robin_cf: RelaxedRoundRobinCF diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 826b144779..bf9d6d087e 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -15,7 +15,7 @@ from worlds.generic.Rules import add_rule, set_rule logger = logging.getLogger("Super Metroid") -from .Options import sm_options +from .Options import SMOptions from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols import Utils @@ -96,10 +96,11 @@ class SMWorld(World): a wide range of options to randomize Item locations, required skills and even the connections between the main Areas! """ - game: str = "Super Metroid" topology_present = True - option_definitions = sm_options + options_dataclass = SMOptions + options: SMOptions + settings: typing.ClassVar[SMSettings] item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} @@ -129,27 +130,27 @@ class SMWorld(World): Logic.factory('vanilla') dummy_rom_file = Utils.user_path(SMSettings.RomFile.copy_to) # actual rom set in generate_output - self.variaRando = VariaRandomizer(self.multiworld, dummy_rom_file, self.player) + self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player) self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty) # keeps Nothing items local so no player will ever pickup Nothing # doing so reduces contribution of this world to the Multiworld the more Nothing there is though - self.multiworld.local_items[self.player].value.add('Nothing') - self.multiworld.local_items[self.player].value.add('No Energy') + self.options.local_items.value.add('Nothing') + self.options.local_items.value.add('No Energy') if (self.variaRando.args.morphPlacement == "early"): self.multiworld.local_early_items[self.player]['Morph Ball'] = 1 - self.remote_items = self.multiworld.remote_items[self.player] + self.remote_items = self.options.remote_items if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.accessibility.value = Accessibility.option_minimal logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] - if self.multiworld.start_inventory_removes_from_pool[self.player]: + if self.options.start_inventory_removes_from_pool: for item in self.startItems: if (item in itemPool): itemPool.remove(item) @@ -317,10 +318,10 @@ class SMWorld(World): player=self.player) def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value + if self.multiworld.random.randint(0, 100) < self.options.minor_qty.value: + power_bombs = self.options.power_bomb_qty.value + missiles = self.options.missile_qty.value + super_missiles = self.options.super_qty.value roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) if roll <= power_bombs: return "Power Bomb" @@ -633,7 +634,7 @@ class SMWorld(World): deathLink: List[ByteEdit] = [{ "sym": symbols["config_deathlink"], "offset": 0, - "values": [self.multiworld.death_link[self.player].value] + "values": [self.options.death_link.value] }] remoteItem: List[ByteEdit] = [{ "sym": symbols["config_remote_items"], @@ -859,10 +860,7 @@ class SMWorld(World): def fill_slot_data(self): slot_data = {} if not self.multiworld.is_race: - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value - + slot_data = self.options.as_dict(*self.options_dataclass.type_hints) slot_data["Preset"] = { "Knows": {}, "Settings": {"hardRooms": Settings.SettingsDict[self.player].hardRooms, "bossesDifficulty": Settings.SettingsDict[self.player].bossesDifficulty, @@ -887,14 +885,14 @@ class SMWorld(World): return slot_data def write_spoiler(self, spoiler_handle: TextIO): - if self.multiworld.area_randomization[self.player].value != 0: + if self.options.area_randomization.value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if not src.Boss])) - if self.multiworld.boss_randomization[self.player].value != 0: + if self.options.boss_randomization.value != 0: spoiler_handle.write('\n\nBoss Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index dab078598e..8a7a2ea0e2 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -250,13 +250,13 @@ class VariaRandomizer: parser.add_argument('--tourianList', help="list to choose from when random", dest='tourianList', nargs='?', default=None) - def __init__(self, world, rom, player): + def __init__(self, options, rom, player): # parse args self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values self.player = player args = self.args args.rom = rom - # args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key) + # args.startLocation = to_pascal_case_with_space(options.startLocation.current_key) if args.output is None and args.rom is None: raise Exception("Need --output or --rom parameter") @@ -288,7 +288,7 @@ class VariaRandomizer: # print(msg) # optErrMsgs.append(msg) - preset = loadRandoPreset(world, self.player, args) + preset = loadRandoPreset(options, args) # use the skill preset from the rando preset if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None: args.paramsFileName = "/".join((appDir, getPresetDir(preset), preset+".json")) @@ -302,12 +302,12 @@ class VariaRandomizer: preset = args.preset else: if preset == 'custom': - PresetLoader.factory(world.custom_preset[player].value).load(self.player) + PresetLoader.factory(options.custom_preset.value).load(self.player) elif preset == 'varia_custom': - if len(world.varia_custom_preset[player].value) == 0: + if len(options.varia_custom_preset.value) == 0: raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' - preset_name = next(iter(world.varia_custom_preset[player].value)) + preset_name = next(iter(options.varia_custom_preset.value)) payload = '{{"preset": "{}"}}'.format(preset_name) headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} response = requests.post(url, data=payload, headers=headers) @@ -463,7 +463,7 @@ class VariaRandomizer: args.startLocation = random.choice(possibleStartAPs) elif args.startLocation not in possibleStartAPs: args.startLocation = 'Landing Site' - world.start_location[player] = StartLocation(StartLocation.default) + options.start_location = StartLocation(StartLocation.default) #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) #dumpErrorMsgs(args.output, optErrMsgs) diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 01029f2f60..f7d699b665 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -358,35 +358,35 @@ def convertParam(randoParams, param, inverse=False): return "random" raise Exception("invalid value for parameter {}".format(param)) -def loadRandoPreset(world, player, args): +def loadRandoPreset(options, args): defaultMultiValues = getDefaultMultiValues() diffs = ["easy", "medium", "hard", "harder", "hardcore", "mania", "infinity"] presetValues = getPresetValues() - args.animals = world.animals[player].value - args.noVariaTweaks = not world.varia_tweaks[player].value - args.maxDifficulty = diffs[world.max_difficulty[player].value] - #args.suitsRestriction = world.suits_restriction[player].value - args.hideItems = world.hide_items[player].value - args.strictMinors = world.strict_minors[player].value - args.noLayout = not world.layout_patches[player].value - args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value] - args.nerfedCharge = world.nerfed_charge[player].value - args.area = world.area_randomization[player].current_key + args.animals = options.animals.value + args.noVariaTweaks = not options.varia_tweaks.value + args.maxDifficulty = diffs[options.max_difficulty.value] + #args.suitsRestriction = options.suits_restriction.value + args.hideItems = options.hide_items.value + args.strictMinors = options.strict_minors.value + args.noLayout = not options.layout_patches.value + args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][options.gravity_behaviour.value] + args.nerfedCharge = options.nerfed_charge.value + args.area = options.area_randomization.current_key if (args.area == "true"): args.area = "full" if args.area != "off": - args.areaLayoutBase = not world.area_layout[player].value - args.escapeRando = world.escape_rando[player].value - args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value - args.doorsColorsRando = world.doors_colors_rando[player].value - args.allowGreyDoors = world.allow_grey_doors[player].value - args.bosses = world.boss_randomization[player].value - if world.fun_combat[player].value: + args.areaLayoutBase = not options.area_layout.value + args.escapeRando = options.escape_rando.value + args.noRemoveEscapeEnemies = not options.remove_escape_enemies.value + args.doorsColorsRando = options.doors_colors_rando.value + args.allowGreyDoors = options.allow_grey_doors.value + args.bosses = options.boss_randomization.value + if options.fun_combat.value: args.superFun.append("Combat") - if world.fun_movement[player].value: + if options.fun_movement.value: args.superFun.append("Movement") - if world.fun_suits[player].value: + if options.fun_suits.value: args.superFun.append("Suits") ipsPatches = { "spin_jump_restart":"spinjumprestart", @@ -396,36 +396,36 @@ def loadRandoPreset(world, player, args): "refill_before_save":"refill_before_save", "relaxed_round_robin_cf":"relaxed_round_robin_cf"} for settingName, patchName in ipsPatches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName + '.ips') patches = {"no_music":"No_Music", "infinite_space_jump":"Infinite_Space_Jump"} for settingName, patchName in patches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName) - args.hud = world.hud[player].value - args.morphPlacement = defaultMultiValues["morphPlacement"][world.morph_placement[player].value] + args.hud = options.hud.value + args.morphPlacement = defaultMultiValues["morphPlacement"][options.morph_placement.value] #args.majorsSplit #args.scavNumLocs #args.scavRandomized - args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value] + args.startLocation = defaultMultiValues["startLocation"][options.start_location.value] #args.progressionDifficulty #args.progressionSpeed - args.missileQty = world.missile_qty[player].value / float(10) - args.superQty = world.super_qty[player].value / float(10) - args.powerBombQty = world.power_bomb_qty[player].value / float(10) - args.minorQty = world.minor_qty[player].value - args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value] - args.objectiveRandom = world.custom_objective[player].value - args.objectiveList = list(world.custom_objective_list[player].value) - args.nbObjective = world.custom_objective_count[player].value - args.objective = list(world.objective[player].value) - args.tourian = defaultMultiValues["tourian"][world.tourian[player].value] + args.missileQty = options.missile_qty.value / float(10) + args.superQty = options.super_qty.value / float(10) + args.powerBombQty = options.power_bomb_qty.value / float(10) + args.minorQty = options.minor_qty.value + args.energyQty = defaultMultiValues["energyQty"][options.energy_qty.value] + args.objectiveRandom = options.custom_objective.value + args.objectiveList = list(options.custom_objective_list.value) + args.nbObjective = options.custom_objective_count.value + args.objective = list(options.objective.value) + args.tourian = defaultMultiValues["tourian"][options.tourian.value] #args.minimizerN #args.minimizerTourian - return presetValues[world.preset[player].value] + return presetValues[options.preset.value] def getRandomizerDefaultParameters(): defaultParams = {} diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index 8c5efc431f..7df01f8710 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from dataclasses import dataclass class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -126,20 +127,19 @@ class EnergyBeep(DefaultOnToggle): """Toggles the low health energy beep in Super Metroid.""" display_name = "Energy Beep" - -smz3_options: typing.Dict[str, type(Option)] = { - "accessibility": ItemsAccessibility, - "sm_logic": SMLogic, - "sword_location": SwordLocation, - "morph_location": MorphLocation, - "goal": Goal, - "key_shuffle": KeyShuffle, - "open_tower": OpenTower, - "ganon_vulnerable": GanonVulnerable, - "open_tourian": OpenTourian, - "spin_jumps_animation": SpinJumpsAnimation, - "heart_beep_speed": HeartBeepSpeed, - "heart_color": HeartColor, - "quick_swap": QuickSwap, - "energy_beep": EnergyBeep - } +@dataclass +class SMZ3Options(PerGameCommonOptions): + accessibility: ItemsAccessibility + sm_logic: SMLogic + sword_location: SwordLocation + morph_location: MorphLocation + goal: Goal + key_shuffle: KeyShuffle + open_tower: OpenTower + ganon_vulnerable: GanonVulnerable + open_tourian: OpenTourian + spin_jumps_animation: SpinJumpsAnimation + heart_beep_speed: HeartBeepSpeed + heart_color: HeartColor + quick_swap: QuickSwap + energy_beep: EnergyBeep diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 690e5172a2..5e6a6ac609 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -22,8 +22,8 @@ from worlds.AutoWorld import World, AutoLogicRegister, WebWorld from .Client import SMZ3SNIClient from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch -from .Options import smz3_options -from Options import Accessibility +from .Options import SMZ3Options +from Options import Accessibility, ItemsAccessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -68,7 +68,9 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - option_definitions = smz3_options + options_dataclass = SMZ3Options + options: SMZ3Options + item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id @@ -189,14 +191,14 @@ class SMZ3World(World): self.config = Config() self.config.GameMode = GameMode.Multiworld self.config.Z3Logic = Z3Logic.Normal - self.config.SMLogic = SMLogic(self.multiworld.sm_logic[self.player].value) - self.config.SwordLocation = SwordLocation(self.multiworld.sword_location[self.player].value) - self.config.MorphLocation = MorphLocation(self.multiworld.morph_location[self.player].value) - self.config.Goal = Goal(self.multiworld.goal[self.player].value) - self.config.KeyShuffle = KeyShuffle(self.multiworld.key_shuffle[self.player].value) - self.config.OpenTower = OpenTower(self.multiworld.open_tower[self.player].value) - self.config.GanonVulnerable = GanonVulnerable(self.multiworld.ganon_vulnerable[self.player].value) - self.config.OpenTourian = OpenTourian(self.multiworld.open_tourian[self.player].value) + self.config.SMLogic = SMLogic(self.options.sm_logic.value) + self.config.SwordLocation = SwordLocation(self.options.sword_location.value) + self.config.MorphLocation = MorphLocation(self.options.morph_location.value) + self.config.Goal = Goal(self.options.goal.value) + self.config.KeyShuffle = KeyShuffle(self.options.key_shuffle.value) + self.config.OpenTower = OpenTower(self.options.open_tower.value) + self.config.GanonVulnerable = GanonVulnerable(self.options.ganon_vulnerable.value) + self.config.OpenTourian = OpenTourian(self.options.open_tourian.value) self.local_random = random.Random(self.multiworld.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(self.config, self.multiworld.get_player_name(self.player), self.player, self.multiworld.seed_name) @@ -222,7 +224,7 @@ class SMZ3World(World): else: progressionItems = self.progression # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local - self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) + self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) @@ -244,7 +246,7 @@ class SMZ3World(World): set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player])) for loc in region.Locations: l = self.locations[loc.Name] - if self.multiworld.accessibility[self.player] != 'full': + if self.options.accessibility.value != ItemsAccessibility.option_full: l.always_allow = lambda state, item, loc=loc: \ item.game == "SMZ3" and \ loc.alwaysAllow(item.item, state.smz3state[self.player]) @@ -405,12 +407,12 @@ class SMZ3World(World): patch = {} # smSpinjumps - if (self.multiworld.spin_jumps_animation[self.player].value == 1): + if (self.options.spin_jumps_animation.value == 1): patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) # z3HeartBeep values = [ 0x00, 0x80, 0x40, 0x20, 0x10] - index = self.multiworld.heart_beep_speed[self.player].value + index = self.options.heart_beep_speed.value patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) # z3HeartColor @@ -420,17 +422,17 @@ class SMZ3World(World): [0x2C, [0xC9, 0x69]], [0x28, [0xBC, 0x02]] ] - index = self.multiworld.heart_color[self.player].value + index = self.options.heart_color.value (hud, fileSelect) = values[index if index < len(values) else 0] for i in range(0, 20, 2): patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) # z3QuickSwap - patch[0x40004B] = bytearray([0x01 if self.multiworld.quick_swap[self.player].value else 0x00]) + patch[0x40004B] = bytearray([0x01 if self.options.quick_swap.value else 0x00]) # smEnergyBeepOff - if (self.multiworld.energy_beep[self.player].value == 0): + if (self.options.energy_beep.value == 0): for ([addr, value]) in [ [0x90EA9B, 0x80], [0x90F337, 0x80], @@ -551,7 +553,7 @@ class SMZ3World(World): # some small or big keys (those always_allow) can be unreachable in-game # while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't # so we need to remove those exceptions as progression items - if self.multiworld.accessibility[self.player] == 'items': + if self.options.accessibility.value == ItemsAccessibility.option_items: state = CollectionState(self.multiworld) locs = [self.multiworld.get_location("Swamp Palace - Big Chest", self.player), self.multiworld.get_location("Skull Woods - Big Chest", self.player), From 8a809be67a02ac44ebdbe748869585e703e40f6f Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 31 Aug 2024 21:57:43 +0300 Subject: [PATCH 36/50] Stardew Valley - Prize Ticket and Mystery Box grinding requires the abilty to redeem them #3728 --- worlds/stardew_valley/logic/grind_logic.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py index ccd8c5dacc..e0ac84639d 100644 --- a/worlds/stardew_valley/logic/grind_logic.py +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -5,6 +5,7 @@ from .base_logic import BaseLogic, BaseLogicMixin from .book_logic import BookLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from ..options import Booksanity from ..stardew_rule import StardewRule, HasProgressionPercent @@ -13,6 +14,7 @@ from ..strings.craftable_names import Consumable from ..strings.currency_names import Currency from ..strings.fish_names import WaterChest from ..strings.geode_names import Geode +from ..strings.region_names import Region from ..strings.tool_names import Tool if TYPE_CHECKING: @@ -31,26 +33,28 @@ class GrindLogicMixin(BaseLogicMixin): self.grind = GrindLogic(*args, **kwargs) -class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): +class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: + opening_rule = self.logic.region.can_reach(Region.blacksmith) mystery_box_rule = self.logic.has(Consumable.mystery_box) book_of_mysteries_rule = self.logic.true_ \ if self.options.booksanity == Booksanity.option_none \ else self.logic.book.has_book_power(Book.book_of_mysteries) # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. time_rule = self.logic.time.has_lived_months(quantity // 14) - return self.logic.and_(mystery_box_rule, - book_of_mysteries_rule, - time_rule) + return self.logic.and_(opening_rule, mystery_box_rule, + book_of_mysteries_rule, time_rule,) def can_grind_artifact_troves(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Geode.artifact_trove), + opening_rule = self.logic.region.can_reach(Region.blacksmith) + return self.logic.and_(opening_rule, self.logic.has(Geode.artifact_trove), # Assuming one per month if the player does not grind it. self.logic.time.has_lived_months(quantity)) def can_grind_prize_tickets(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Currency.prize_ticket), + claiming_rule = self.logic.region.can_reach(Region.mayor_house) + return self.logic.and_(claiming_rule, self.logic.has(Currency.prize_ticket), # Assuming two per month if the player does not grind it. self.logic.time.has_lived_months(quantity // 2)) From 499dad53b1a3943019f1bf57897e48edf563150e Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sat, 31 Aug 2024 20:00:19 +0100 Subject: [PATCH 37/50] AHIT: Fix thug shops having 0 items after the first shop rolls 0 items (#3799) Once a thug shop rolled 0 as the number of items it should have, all remaining iterations would do nothing because neither the `count == -1` condition nor the `count >= 1` condition would be met. This caused all remaining thug shops to have zero items. This also caused the item counts of remaining thug shops to be absent from slot data, which was how this issue was found. I found the old code confusing and, rather than try to figure out how to fix it, I opted to rewrite it. With the new code, a local variable dictionary tracks the number of created locations for each thug and no more locations are created for a thug once their number of locations equals the number of shop items that thug rolled. --- worlds/ahit/Regions.py | 49 +++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 8cb3782bde..c70f08b475 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -968,40 +968,35 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R def create_thug_shops(world: "HatInTimeWorld"): min_items: int = world.options.NyakuzaThugMinShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value - count = -1 - step = 0 - old_name = "" + + thug_location_counts: Dict[str, int] = {} for key, data in shop_locations.items(): - if data.nyakuza_thug == "": + thug_name = data.nyakuza_thug + if thug_name == "": + # Different shop type. continue - if old_name != "" and old_name == data.nyakuza_thug: + if thug_name not in world.nyakuza_thug_items: + shop_item_count = world.random.randint(min_items, max_items) + world.nyakuza_thug_items[thug_name] = shop_item_count + else: + shop_item_count = world.nyakuza_thug_items[thug_name] + + if shop_item_count <= 0: continue - try: - if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: - continue - except KeyError: - pass + location_count = thug_location_counts.setdefault(thug_name, 0) + if location_count >= shop_item_count: + # Already created all the locations for this thug. + continue - if count == -1: - count = world.random.randint(min_items, max_items) - world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) - if count <= 0: - continue - - if count >= 1: - region = world.multiworld.get_region(data.region, world.player) - loc = HatInTimeLocation(world.player, key, data.id, region) - region.locations.append(loc) - world.shop_locs.append(loc.name) - - step += 1 - if step >= count: - old_name = data.nyakuza_thug - step = 0 - count = -1 + # Create the shop location. + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + thug_location_counts[thug_name] = location_count + 1 def create_events(world: "HatInTimeWorld") -> int: From fc8462f4e9f782bd123ce6123efab91c6850228a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 31 Aug 2024 22:51:41 +0200 Subject: [PATCH 38/50] The Witness: Add Beginner Mode option preset #3691 --- worlds/witness/presets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 105514c91e..8993048065 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -3,6 +3,13 @@ from typing import Any, Dict from .options import * witness_option_presets: Dict[str, Dict[str, Any]] = { + # Best for beginners. This is just default options, but with a much easier goal that skips the Mountain puzzles. + "Beginner Mode": { + "victory_condition": VictoryCondition.option_mountain_box_short, + + "puzzle_skip_amount": 15, + }, + # Great for short syncs & scratching that "speedrun with light routing elements" itch. "Short & Dense": { "progression_balancing": 30, From 456b4adaa177ed13fda00b66b93cec1ef9c7333f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:36:29 -0400 Subject: [PATCH 39/50] ALttP/Docs: Correcting the plando docs (#3835) * Correcting some text * Reword sentence --- worlds/alttp/docs/plando_en.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/worlds/alttp/docs/plando_en.md b/worlds/alttp/docs/plando_en.md index af8cbfe1b0..13224cb4d5 100644 --- a/worlds/alttp/docs/plando_en.md +++ b/worlds/alttp/docs/plando_en.md @@ -2,8 +2,8 @@ ## Configuration -1. Plando features have to be enabled first, before they can be used (opt-in). -2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml +1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in). +2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml file with a text editor. 3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value to `bosses, items, texts, connections` @@ -66,6 +66,7 @@ boss_shuffle: - ignored if only one world is generated - can be a number, to target that slot in the multiworld - can be a name, to target that player's world + - can be a list of names, to target those players' worlds - can be true, to target any other player's world - can be false, to target own world and is the default - can be null, to target a random world @@ -132,17 +133,15 @@ plando_items: ### Texts -- This module is disabled by default. - Has the options `text`, `at`, and `percentage` +- All of these options support subweights - percentage is the percentage chance for this text to be placed, can be omitted entirely for 100% - text is the text to be placed. - - can be weighted. - `\n` is a newline. - `@` is the entered player's name. - Warning: Text Mapper does not support full unicode. - [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758) - at is the location within the game to attach the text to. - - can be weighted. - [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499) #### Example @@ -162,7 +161,6 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war ### Connections -- This module is disabled by default. - Has the options `percentage`, `entrance`, `exit` and `direction`. - All options support subweights - percentage is the percentage chance for this to be connected, can be omitted entirely for 100% From 34a3b5f058766c650499eb48c2eced7e06c14c9b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 31 Aug 2024 17:37:18 -0400 Subject: [PATCH 40/50] TUNIC: Add alias for Ladders in Overworld Town #3862 --- worlds/tunic/items.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 3e7f2c1a43..e0ee17831a 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -235,9 +235,10 @@ extra_groups: Dict[str, Set[str]] = { "Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't "Ladders to Bell": {"Ladders to West Bell"}, - "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell + "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided Ladders in Well was Ladders to West Bell "Ladders in Atoll": {"Ladders in South Atoll"}, "Ladders in Ruined Atoll": {"Ladders in South Atoll"}, + "Ladders in Town": {"Ladders in Overworld Town"}, # fuzzy matching decided this was Ladders in South Atoll } item_name_groups.update(extra_groups) From 1a41e1acc8417d2791cc50c2f8082f57ef076ea1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:34:50 +0200 Subject: [PATCH 41/50] customserver: fix memory leak (#3864) --- MultiServer.py | 18 ++++++++++++++++++ WebHostLib/customserver.py | 12 +++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index b7c0e0f745..fb539f5671 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -67,6 +67,21 @@ def update_dict(dictionary, entries): return dictionary +def queue_gc(): + import gc + from threading import Thread + + gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None) + def async_collect(): + time.sleep(2) + setattr(queue_gc, "_thread", None) + gc.collect() + if not gc_thread: + gc_thread = Thread(target=async_collect) + setattr(queue_gc, "_thread", gc_thread) + gc_thread.start() + + # functions callable on storable data on the server by clients modify_functions = { # generic: @@ -551,6 +566,9 @@ class Context: self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.") else: self.save_dirty = False + if not atexit_save: # if atexit is used, that keeps a reference anyway + queue_gc() + self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True) self.auto_saver_thread.start() diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ccffc40b38..a2eef108b0 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -72,6 +72,14 @@ class WebHostContext(Context): self.video = {} self.tags = ["AP", "WebHost"] + def __del__(self): + try: + import psutil + from Utils import format_SI_prefix + self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") + except ImportError: + self.logger.debug("Context destroyed") + def _load_game_data(self): for key, value in self.static_server_data.items(): # NOTE: attributes are mutable and shared, so they will have to be copied before being modified @@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx = WebHostContext(static_server_data, logger) ctx.load(room_id) ctx.init_save() + assert ctx.server is None try: ctx.server = websockets.serve( functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) @@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx.auto_shutdown = Room.get(id=room_id).timeout if ctx.saving: setattr(asyncio.current_task(), "save", lambda: ctx._save(True)) + assert ctx.shutdown_task is None ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) await ctx.shutdown_task @@ -325,7 +335,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) - gc.collect(0) + gc.collect() task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) From 6f46397185ea945ed4db7d1404980c8f2d92253d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:41:55 +0200 Subject: [PATCH 42/50] Rogue Legacy: Crash generation when there are overlapping IDs (#3865) Client literally does not work when there are overlapping IDs. Phar is not currently intending to fix it. https://discord.com/channels/731205301247803413/929585237695029268/1269684436853723156 --- worlds/rogue_legacy/__init__.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index eb65769954..78e56a794c 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -49,6 +49,30 @@ class RLWorld(World): return {option_name: self.get_setting(option_name).value for option_name in rl_options} def generate_early(self): + location_ids_used_per_game = { + world.game: set(world.location_id_to_name) for world in self.multiworld.worlds.values() + } + item_ids_used_per_game = { + world.game: set(world.item_id_to_name) for world in self.multiworld.worlds.values() + } + overlapping_games = set() + + for id_lookup in (location_ids_used_per_game, item_ids_used_per_game): + for game_1, ids_1 in id_lookup.items(): + for game_2, ids_2 in id_lookup.items(): + if game_1 == game_2: + continue + + if ids_1 & ids_2: + overlapping_games.add(tuple(sorted([game_1, game_2]))) + + if overlapping_games: + raise RuntimeError( + "In this multiworld, there are games with overlapping item/location IDs.\n" + "The current Rogue Legacy does not support these and a fix is not currently planned.\n" + f"The overlapping games are: {overlapping_games}" + ) + # Check validation of names. additional_lady_names = len(self.get_setting("additional_lady_names").value) additional_sir_names = len(self.get_setting("additional_sir_names").value) From 3ab71daa8d14bfc4d83836c185a48692cbeaf518 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 1 Sep 2024 21:59:37 +0200 Subject: [PATCH 43/50] MultiServer: put some limits in place (#3858) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- MultiServer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index fb539f5671..e0b137fd68 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1221,6 +1221,10 @@ class CommonCommandProcessor(CommandProcessor): timer = int(seconds, 10) except ValueError: timer = 10 + else: + if timer > 60 * 60: + raise ValueError(f"{timer} is invalid. Maximum is 1 hour.") + async_start(countdown(self.ctx, timer)) return True @@ -2057,6 +2061,8 @@ class ServerCommandProcessor(CommonCommandProcessor): item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) + if amount > 100: + raise ValueError(f"{amount} is invalid. Maximum is 100.") new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items) From 73701292b599c658b5d2f728230cd20743759181 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:08:16 +0200 Subject: [PATCH 44/50] Core, CI: Add Python 3.12 support (#3290) * Core, CI: add py3.12 compat * Stardew Valley: Fix tests for Py3.12 * ModuleUpdate: always install pkg_resources * Docs: update supported python versions * WebHost: update pony to upstream 0.7.18 * CI: test hosting update to py3.12 * Update docs/running from source.md --- .github/workflows/unittests.yml | 7 ++++--- ModuleUpdate.py | 6 +++--- WebHostLib/requirements.txt | 2 +- docs/running from source.md | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 3ad29b0077..9a3a6d1121 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -37,12 +37,13 @@ jobs: - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} + - {version: '3.12'} include: - python: {version: '3.8'} # win7 compat os: windows-latest - - python: {version: '3.11'} # current + - python: {version: '3.12'} # current os: windows-latest - - python: {version: '3.11'} # current + - python: {version: '3.12'} # current os: macos-latest steps: @@ -70,7 +71,7 @@ jobs: os: - ubuntu-latest python: - - {version: '3.11'} # current + - {version: '3.12'} # current steps: - uses: actions/checkout@v4 diff --git a/ModuleUpdate.py b/ModuleUpdate.py index ed041bef46..f49182bb78 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None: if not update_ran: update_ran = True + install_pkg_resources(yes=yes) + import pkg_resources + if force: update_command() return - install_pkg_resources(yes=yes) - import pkg_resources - prev = "" # if a line ends in \ we store here and merge later for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 3452c9d416..c61a153d24 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,6 +1,6 @@ flask>=3.0.3 werkzeug>=3.0.3 -pony>=0.7.17 +pony>=0.7.18 waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 diff --git a/docs/running from source.md b/docs/running from source.md index 34083a603d..4bd335648d 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r What you'll need: * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version - * **Python 3.12 is currently unsupported** + * Python 3.12.x is currently the newest supported version * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler * possibly optional, read operating system specific sections @@ -31,7 +31,7 @@ After this, you should be able to run the programs. Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) - * **Python 3.12 is currently unsupported** + * [read above](#General) which versions are supported * **Optional**: Download and install Visual Studio Build Tools from [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). From 765721888ab3cf9cd7c9ebeb8f65f6005e0b5b19 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 3 Sep 2024 01:26:46 +0200 Subject: [PATCH 45/50] WebHost: config override (#3701) --- WebHost.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebHost.py b/WebHost.py index 08ef3c4307..e597de2476 100644 --- a/WebHost.py +++ b/WebHost.py @@ -1,3 +1,4 @@ +import argparse import os import multiprocessing import logging @@ -31,6 +32,15 @@ def get_app() -> "Flask": import yaml app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") + # inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it. + parser = argparse.ArgumentParser() + parser.add_argument('--config_override', default=None, + help="Path to yaml config file that overrules config.yaml.") + args = parser.parse_known_args()[0] + if args.config_override: + import yaml + app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load) + logging.info(f"Updated config from {args.config_override}") if not app.config["HOST_ADDRESS"]: logging.info("Getting public IP, as HOST_ADDRESS is empty.") app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() From d63efa5846f48892b24fa0559873328044510d0b Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 3 Sep 2024 02:22:48 +0200 Subject: [PATCH 46/50] Core: update dependencies (#3869) --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index db4f544503..6fe14c9f32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ colorama>=0.4.6 -websockets>=12.0 -PyYAML>=6.0.1 -jellyfish>=1.0.3 +websockets>=13.0.1 +PyYAML>=6.0.2 +jellyfish>=1.1.0 jinja2>=3.1.4 schema>=0.7.7 kivy>=2.3.0 bsdiff4>=1.2.4 platformdirs>=4.2.2 -certifi>=2024.6.2 -cython>=3.0.10 +certifi>=2024.8.30 +cython>=3.0.11 cymem>=2.0.8 -orjson>=3.10.3 -typing_extensions>=4.12.1 +orjson>=3.10.7 +typing_extensions>=4.12.2 From 2aa0653b6dc1cc7e0cdaa94bc140f8a804ded4a2 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 3 Sep 2024 02:31:42 +0200 Subject: [PATCH 47/50] WebHost: update dependencies (#3871) --- WebHostLib/requirements.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c61a153d24..c593cd63df 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,10 +1,11 @@ flask>=3.0.3 -werkzeug>=3.0.3 -pony>=0.7.18 +werkzeug>=3.0.4 +pony>=0.7.19 waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 -Flask-Limiter>=3.7.0 +Flask-Limiter>=3.8.0 bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.1; python_version >= '3.9' +bokeh>=3.4.3; python_version == '3.9' +bokeh>=3.5.2; python_version >= '3.10' markupsafe>=2.1.5 From b2949dfbe85ac18da113430fb1a545cb14b8c47e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 4 Sep 2024 08:19:00 -0500 Subject: [PATCH 48/50] KDL3: Account for additional animal in pool #3874 --- worlds/kdl3/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 12f56a0230..f01c82dd16 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -201,16 +201,13 @@ class KDL3World(World): else: animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"] animal_pool = [self.random.choice(animal_base) - for _ in range(len(animal_friend_spawns) - 9)] + for _ in range(len(animal_friend_spawns) - 10)] # have to guarantee one of each animal animal_pool.extend(animal_base) if guaranteed_animal == "Kine Spawn": animal_pool.append("Coo Spawn") else: animal_pool.append("Kine Spawn") - # Weird fill hack, this forces ChuChu to be the last animal friend placed - # If Kine is ever the last animal friend placed, he will cause fill errors on closed world - animal_pool.sort() locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] items: List[Item] = [self.create_item(animal) for animal in animal_pool] allstate = CollectionState(self.multiworld) From b8d7ef24f78aed5a281d2b811244793496498160 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 4 Sep 2024 08:21:02 -0500 Subject: [PATCH 49/50] The Messenger: remove an invalid entrance (#3873) --- worlds/messenger/connections.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 978917c555..69dd7aa7f2 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -114,7 +114,6 @@ CONNECTIONS: Dict[str, Dict[str, List[str]]] = { "Forlorn Temple - Rocket Maze Checkpoint", ], "Rocket Maze Checkpoint": [ - "Forlorn Temple - Sunny Day Checkpoint", "Forlorn Temple - Climb Shop", ], }, From d65863ffa2f06ebdcc1b521a17b3e82f7ee0053e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 4 Sep 2024 11:00:47 -0700 Subject: [PATCH 50/50] Pokemon Emerald: Fix wrong place for initialization (#3870) --- worlds/pokemon_emerald/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 7f16015a3f..cda829def9 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -122,6 +122,7 @@ class PokemonEmeraldClient(BizHawkClient): game = "Pokemon Emerald" system = "GBA" patch_suffix = ".apemerald" + local_checked_locations: Set[int] local_set_events: Dict[str, bool] local_found_key_items: Dict[str, bool] @@ -139,8 +140,7 @@ class PokemonEmeraldClient(BizHawkClient): current_map: Optional[int] - def __init__(self) -> None: - super().__init__() + def initialize_client(self): self.local_checked_locations = set() self.local_set_events = {} self.local_found_key_items = {} @@ -182,9 +182,7 @@ class PokemonEmeraldClient(BizHawkClient): ctx.want_slot_data = True ctx.watcher_timeout = 0.125 - self.death_counter = None - self.previous_death_link = 0 - self.ignore_next_death_link = False + self.initialize_client() return True