From 59e875dc6d45c6aa266515128aa7872fcb8bb334 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 26 Jul 2021 11:28:45 -0600 Subject: [PATCH 01/33] updated readme and version --- Main.py | 2 +- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Main.py b/Main.py index 8909301a..e92bbf61 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0.12u' +__version__ = '0.4.0-dev' class EnemizerError(RuntimeError): diff --git a/README.md b/README.md index 31ff824b..e997d70d 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ Alternatively, run ```Gui.py``` for a simple graphical user interface. # Commonly Missed Things and Differences from other Randomizers +Most of these apply only when the door shuffle is not vanilla. + ### Starting Item -You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and can’t erase blocks like the Mirror +You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and can’t erase blocks like the Mirror. ### Navigation @@ -58,7 +60,7 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du ### Boss Differences -* You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If hints are on, there is a special one about a cracked floor. +* You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If you bring the maiden to the boss arena, she will hint were the cracked floor can be found. If hints are on, there is a special one about the cracked floor. * GT Bosses do not respawn after killing them in this mode. * Enemizer change: The attic/maiden sequence is now active and required when Blind is the boss of Theives' Town even when bosses are shuffled. @@ -70,7 +72,7 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du ### Misc -* Compass counts no longer function after you get the Triforce +* Compass counts no longer function after you get the Triforce (this is actually true in all randomizers) # Settings @@ -124,7 +126,7 @@ The rooms are left alone and it is up to the discretion of the player whether to #### Force -The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game. +The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.cause then you would need time to check the map in a d ### Standardize Palettes (--standardize_palettes) No effect if door shuffle is not on crossed @@ -239,6 +241,37 @@ Arrow Capacity upgrades are now replaced by Rupees wherever it might end up. The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item (represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys retain their original base price, but may be discounted. +## Logic Level + +### Overworld Glitches + +Set `--logic` to `owglitches` to make overworld glitches required in the logic. + +## Shuffle Links House + +In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode. + +## Reduce Flashing + +Accessibility option to reducing some flashing animations in the game. + +## Pseudo-boots + +Option to start with ability to dash, but not able to make any boots required logical checks or traversal. + +## Experimental Features + +The treasure check counter is turned on. Also, you will start as a bunny if your spawn point is in the dark world. + +## Triforce Hunt Settings + +A collection of settings to control the triforce piece pool. + +* --triforce_goal_min: Minimum number of pieces to collect to win +* --triforce_goal_max: Maximum number of pieces to collect to win +* --triforce_pool_min: Minimum number of pieces in item pool +* --triforce_pool_max: Maximum number of pieces in item pool +* --triforce_min_difference: Minimum difference between pool and goal to win ## Seed @@ -280,6 +313,24 @@ Include mobs and pots drop in the item pool. (default: not enabled) Includes shop locations in the item pool. +``` +--pseudoboots +``` + +Start with dash ability, but no way to use boots to accomplish checks + +``` +--shufflelinks +``` + +Whether to shuffle links house in most ER modes. + +``` +--experimental +``` + +Enables experimental features + ``` --mixed_travel ``` @@ -290,4 +341,10 @@ How to handle certain glitches in crossed dungeon mode. (default: prevent) --standardize_palettes (mode) ``` -Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize) \ No newline at end of file +Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize) + +``` +--reduce_flashing +``` + +Reduces amount of flashing in some animations \ No newline at end of file From a1eb077f285732583b6680213a1631a8ac7dce0d Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 08:58:31 -0600 Subject: [PATCH 02/33] Boss music fix --- RELEASENOTES.md | 138 +----------------------------------------- Rom.py | 2 +- data/base2current.bps | Bin 136267 -> 136271 bytes 3 files changed, 3 insertions(+), 137 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0ccceb80..163c6087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,138 +1,4 @@ -# New Features +### Bug Fix -## Maiden Hint for Theives Town Attic +Fix for boss music in non-DR modes (Thanks codemann8) -In crossed dungeon mode, if you bring the maiden to the boss room when the attic is not bombed (and thus no light in the room), she mentions the dungeon where you can find the cracked floor. - -## Shuffle Links House - -Links house can now be shuffled in different ER settings. It will be limited to the Light World (or Dark World in inverted) if Crossed or Insanity shuffle is not one. It it also limited if door shuffle settings allow the Sanctuary to be in the dark world. (This is prevent having no Light World spawn points in Open modes) This setting is ignored by standard mode. THe CLI parameter is --shufflelinks - -## OWG Glitch Logic - -Thanks to qadan, cheuer, & compiling - -## Pseudo Boots - -Thanks to Bonta. You can now start with pseudo boots that let you move fast, but have no other logical uses (bonking open things, hovering, etc) - -## Pendant/Crystal Indicator - -For accessibility, you now get a C or P indicator to the left of the magic bar on the HUD when instead a Crystal or Pendant. Requires ownership of the map of that dungeon for display. Thanks to kan. - -# Bug Fixes and Notes. - -* 0.4.0.12 - * ER Inverted fix for HC Ledge, and Aga Tower choosing Links House incorrectly - * Credits again - hopefully for good - * Incorporated music fixes for now (may revisit later) - * Secure random re-incorporated -* 0.4.0.11 - * Some minor base rom fixes - * Improved distribution of bombable/dashable doors -* 0.4.0.10 - * Renamed to pseudoboots - * Some release note updates -* 0.4.0.9 - * Fixes for stats and P/C indicator (thanks Kara) - * Swamp lobby fixes (thanks Catobat) - * Fix for --hints flag on CLI -* 0.4.0.8 - * Ganon jokes added for when silvers aren't available - * Some text updated (Blind jokes, uncle text) - * Fixed some enemizer Mystery settings - * Added a setting that's random enemy shuffle without Unkillable Thieves possible - * Fixed shop spoiler when money balancing/multiworld balancing - * Fixed a problem with insanity - * Fixed an issue with the credit stats specific to DR (e.g. collection rate total) - * More helpful error message when bps is missing? - * Minor generation issues involving enemizer and the link sprite - * Baserom updates (from Bonta, kan, qwertymodo, ardnaxelark) - * Boss icon on dungeon map (if you have a compass) - * Progressive bow sprite replacement - * Quickswap - consecutive special swaps - * Bonk Counter - * One mind - * MSU fix - * Chest turn tracking (not yet in credits) - * Damaged and magic stats in credits (gt bk removed) - * Fix for infinite bombs - * Pseudo boots option - * Always allowed medallions for swordless (no option yet) -* 0.4.0.7 - * Reduce flashing option added - * Sprite author credit added - * Ranged Crystal switch rules tweaked - * Baserom update: includes Credits Speedup, reduced flashing option, msu resume (but turned off by default) - * Create link sprite's zspr from local ROM and no longer attempts to download it from website - * Some minor bug fixes -* 0.4.0.6 - * Hints now default to off - * The maiden gives you a hint to the attic if you bring her to the unlit boss room - * Beemizer support and fix for shopsanity - * Capacity upgrades removed in hard/expert item difficulties - * Swamp Hub added to lobby shuffle with ugly cave entrance. - * TR Lava Escape added to lobby shuffle. - * Hyrule Main Lobby and Sanctuary can now have a more visible outside exit, and rugs modified to be fully clipped. -* 0.4.0.5 - * Insanity - less restrictions on exiting (all modes) - * Fix for simple bosses shuffle - * Fix for boss shuffle from Mystery.py - * Minor msu fade out bug (thanks codemann8) - * Other bug fixes (thanks Catobat) -* 0.4.0.4 - * Added --shufflelinks option - * Moved spawning as a bunny indoors to experimental - * Baserom bug fixes -* 0.4.0.3 - * Fixed a bug where Sanctuary could be chosen as a lobby for a DW dungeon in non-crossed ER modes -* 0.4.0.2 - * Fixed a bug where Defeat Ganon is not possible - * Fixed the item counter total - * Fixed the bunny state when starting out in Sanc in a dark world dungeon -* 0.4.0.1 - * Moved stonewall pre-opening to not happen in experimental - * Updated baserom - * Boss RNG perseved between files - * Vanilla prize pack fix - * Starting equipment fix - * Post-Aga world state option - * Code optimzation - * Bottle quickswap via double shoulder - * Credits update - * Accessibility option - * Sewer map/compass fix - * Fixed a standard bug where the exits to the ledge would be unavailable if the pyramid was pre-opened - * DR ASM optimization - * Removed Archery Game from Take-Any caves in inverted - * Fixed a problem with new YAML parser -* 0.4.0.0 - * Mystery yaml parser updated to a package maintained version (Thanks StructuralMike) - * Bomb-logic and extend crystal switch logic (Thanks StructuralMike) - * Fixed logic for moved locations in playthrough (Thanks compiling) - * OWG Glitch logic added - -# Known Issues - -* Shopsanity Issues - * Hints for items in shops can be misleading (ER) - * Forfeit in Multiworld not granting all shop items -* Potential keylocks in multi-entrance dungeons -* Incorrect vanilla key logic for Mire - -## Other Notes - -### Triforce Hunt Options - -Thanks to deathFouton! - ---triforce_pool and --triforce_goal added to the CLI. - -Also, to the Mystery.py he added the following options: -* triforce_goal_min -* triforce_goal_max -* triforce_pool_min -* triforce_pool_max -* triforce_min_difference - -See the example yaml file for demonstrated usage. \ No newline at end of file diff --git a/Rom.py b/Rom.py index 40249d56..355509c0 100644 --- a/Rom.py +++ b/Rom.py @@ -30,7 +30,7 @@ from EntranceShuffle import door_addresses, exit_ids JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9c2878d1035bb3889784906a55a92a26' +RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a904b07d50f9fadac099414d033b0c27b309563b..5f41171067dccc266f98a3c0b2daee5a07a03f06 100644 GIT binary patch delta 5253 zcmW+(30xD$8qef_a3_F(pb}OQ5Y&KLDIVZ~C?bL<#i*!Y@x`lXttafF0TZ$bVF+7X zFbfP|I9zC|h!+Z42%^SU>)Bex*0!iM6?>?C`WBPl%=h1KuJ4@V$}#cvW8z{g>WBgH zHp_n{I({Y4bevQq;#FUkE4KX?4K#;~aUwjU(4a8kRRh_DDjXyoBo!ToWfKCvK93~% zs@k(c9f=iv#|<=KNAzewe45BHRHnL@Y0$tx+HpcAFEA>?4F85=solsACozIVh{P(6 zGiCE5p2}Oza=wd*2YBaPX1{{-KsbnoIUpkBW;YV!Mr>*cH%n+TZUAB*;OJLkQ2ehN z4csBIU+$t%jrtn|f)xK{McfGPcZtNN>yRtAV0|ew=8zG)0ax>5 zMxC>vOGXcSuFYX!E}@gZu^9~<$RO0%YZxhv03+ZUVJwJ*M}^6bVGw<9z`lor!g%nH_(;0cEn;1+asT+Fg3rK|xE_U{H zdUF#EWN`XIxXpQ3N-G_hZA>G|6|c2JvU?Yj7)GLqLxeK4(ug&E#|#WabQ`ci&e<*0 zR-r*lV%j3KKZqc)$9hyjV*l#VGGxFO6U;!5K`|HJbWQ~ZIMyY?Z3+3o8pS&g85R9- zt&3u8;tjN4O=5k`OcG~^^q!%(7K`ZwVexi?2yZFJ>?H`B4IjDqgvcOr<<@XHkGwSx z@|J5<f#);Q{hs0b!%!NCoAs#|Kq89FHr?$JEKBdB>&@83EQ#i|0A?DI3<5(4T z&Qj|+CKUL?-k5~RJsPxMtA1C;$TUVQcqqU7)O}BwqUlLvq!sT{)sl1@o>SzzZ8X=qI3mPrmSuQRh*z)c6IuX!W_U_67 zV2S1V7xzV=7+Mc(08N(hxpM%hfS=`k4pv)!$r}uGe8Qkk3C4$yOx~jR48oL_H>P%B z@&Q80^6`*{TIM0oCb4?*Y{F`9+|kPC+4$8GC3ekSurju(dwXXOb1w2G~PP zJ6*}gn#!$&T$S`ju>?wwt#_W9SiS|nWs_h!c;Hxo?-A`A<#BC`_ABij?L+O)+TXQy z?O)o{kUbV8k4h>pw_))l%3$dknEsCp{KC*z@#!Q)+J)FB*Tw7_TyEvGS9D#Yc%w_q zao`g;%iQbQq^o_Ymf76!DA^t8+mI!`p-1+?3~C8H)UXER!N(1U{QQIef>UH#I6rn5u@{LepdZw{&E+a0Knj@YADOI(XUQ;sff04M&UA7&bYbnYLsAP45?O|~| zdmtOff%iPSOZ5ht`CcMpjd*DF4%1CyWPLrc%Je1SM3)+SjI36$oeCC{?KYR{BAR*l zp<^Vgr6L?jwM+FulMhaegv|u5Zz1+^0&t^F^cbrl&HN-`XA8E=UQ@3LiT)0$tYDty z(TAz?^kSS4!@t!P%&&APK_R-95a_4ihNs*RDfJZsZNbZH zxDP2C4|SE*jWm-XW#>`5@p22zWJ=itN>_e{X0oO1d=7Qd%w{ROfJ0X}#iVQ^hrZ<` zm$D0o;C-6;Rmv{n&>+n`ld{Piddtn|$)-?q@Nz)~lj+H(Qe?SH1(WT`F5!?@1+&?c zT?*IW?aWIFtKhT}!s4$+(eI3r^A>{$Pb*eLiuJvxYB6i-xRH z^P7rIH&q#61+!bK*T7TMEz@m+Hr-J>RoBo46}!rGj%?(nt4D;zbe7|u8R9BURb&@8 z1b%i1IvgUdm?q%xHu3F;VfX30W-~iqIOIj_bQ5Dr@W?lAl8hq%@iavqIx&Racvg?D zVE&b`%c%(0d}5i&^$@wVg4qwFxM?UwJ~d54$7w371Gr97b_Eq!SRT$bd$KDj4}#cf zlH-PH70d`P$|;|iM9Yg{Z54kgV&+sZvicI}j5r1uVl0FwEwW(&!$_0=Fw!v2%b$#( zG;mX@))Wn&S}ub*u=VS|_&n|mJCQ2+)F&Tq^vPGPkkAV`7zLeA{sjUdX!G-`y@2+< z2_1^@Wb@XMq|S?)2_xF3fqt0X_5_TCi%-o1Phi=paF7N&Pt5?^OWH@dk6)m#zHDOWJ+dYLac$f}OR%&3Z zQd6Em`UH`_K~&-}9bq-i;##~~RTU2PGvSys>%nkXcxGdqXI-bfI!<^r>XVMA#!7nzHNrcx>-txBDgQuhsApSiNCrU}*(XDvTD;tSyTv+Jfu z@6lF@*us~&O0$ewyGPq1W%UH|Ej=~{YJgh^_QCW*y zM-l8jYX{lz%(N=6;h=%ruJNu@HrDs^bSGlskVQXJG?JuGG?N_F%YsgBraavRe4b@j=fQAiM-+_hDd@av&FL&xL)K(Ee<}5Nl$ec&V!kFu0SB{C*mp5it^LQF1 zHEG0y_V^C8x&!U+KnE@$cmcIJ)~UIh2(?>vLMf}bnAwwQ#khdBxxg)iCSTnvdEGF+ z$?V!$`&XF6E|8^lQG23WZ^*J>+x5|L4x@gl6xzLNt$v%Fx@riqzL_4q^=Un}^=V1S zZo?L0QJuBplM6@^ZnfKlPo#Lf^$(j!meqw%w!Y**8uYhK6*_mxI^c5KJny9!dnK=q z&f9tVbKI7a*V`-khwH)G?mCQ))n7DEhPoRgJy&)jzfFfSp6dBhUagfPo49TJNm)DoD4C`xO2q|rypAad)d?ycBpjUj0g8v<#~+~k5S;b_N+f4`<$U~fznq$o zN|)F6_=kC@dQ7`5UH@(5gQalWce2Sokvd{L<>*vgJLgrd&50fVDmR9j;8gsmBd`nL z3D^uZkIg6KVSKx+*&SH-*@n};u zqLs6CR21yI6*>H~OdSzJ5BB%M-RHgRF|-K13b@b*WZYg`SrV_)U^ZJWwnpFCgz%z~kJ z3PBERxf97fl>6_LMvwf|5&uqc)h^<4-|SeK;vsXsiv99xlq~V8Y5Jt+rdf-AB*t}Y zcQ{(IGh2H!)9GlLcLDKFO$I!E*Jo7LM19GE;dE6Q1F-#gb+NlCeZ1aSEW}M%LcC=I~1wBr}5h)<25imnEZxf~1L1eScO!?y_HMMcL-*_{Z7! zNAsz0rrh=_+dKmgrRTz*@0U$Eeck-FQ&Pd3ZSo3`;;j{NLCtYNwzp=2*@ecvw+baT*~d3JZUp0l2iXD^G4t=8|6;Rfyz zU>i%tz_=f700MeG*aFr-{J{a>0N*`W9+-Rc*(6{eH1nPgNp5$8A%;lYG5SSAN*+Ea zbAUNN#{0&GIpJm=4m5h+vOJv&pk|J6!r?x6?Z+VQc5DA}IQKenet0%ab_>}ZdL@5r z@E$Wx1_gD>sddSY`0EB|=S{pa(sNEaBjfeUIwIhUhc%I&08%d+4bh>8rbz{0=Hkjd~^f&xaR4NFPITL z_FFjYv@yo|p4SpfErNF+2Y?9p=i`at6H|+cP%3RX4l{Uq;*;)tBAY9lEcqoKo3&81 z#9%m(-gJi#@72iQ;-3z9WX=4S$o^t?fq(iRi^J6`Ecbu%cXpbZcv$U$9i)};-S3-$ z3sk;HBYjNhFEtavrdSurmi6)2JW!!6&bl0VSOFXQc1!9r#7moqJAhm+hkTb=|p*8Smax z!z>5?d<`a4b)$RK$n&i(i47esTn3rA>diJhewc!tBzL?l#pKJl_{{gz^ai0JH delta 5364 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 q>=Zx2ly7Ye>5S6|F From bbad1d1d8b1020b50453b66b2d88c5fb8712be38 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 11:33:10 -0600 Subject: [PATCH 03/33] Version bump Mystery can apply reduce_flashing --- Main.py | 2 +- Mystery.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index e92bbf61..96235926 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0-dev' +__version__ = '0.4.1-dev' class EnemizerError(RuntimeError): diff --git a/Mystery.py b/Mystery.py index d3e3bddf..1ab4a90b 100644 --- a/Mystery.py +++ b/Mystery.py @@ -226,6 +226,7 @@ def roll_settings(weights): ret.sprite = get_choice('sprite', romweights) ret.disablemusic = get_choice('disablemusic', romweights) == 'on' ret.quickswap = get_choice('quickswap', romweights) == 'on' + ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' ret.fastmenu = get_choice('menuspeed', romweights) ret.heartcolor = get_choice('heartcolor', romweights) ret.heartbeep = get_choice('heartbeep', romweights) From f259e8bdc8f3e248e9a912501a640ffd3024a7c0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Aug 2021 14:32:54 -0600 Subject: [PATCH 04/33] Boss item restriction logic added Reserved location logic started --- BaseClasses.py | 39 ++++++++++--- CLI.py | 3 +- DungeonGenerator.py | 56 +++++++++++++------ Dungeons.py | 8 +-- Fill.py | 15 +++-- KeyDoorShuffle.py | 55 ++++++++---------- Main.py | 4 ++ Mystery.py | 1 + Rules.py | 18 ++++++ resources/app/cli/args.json | 7 +++ resources/app/cli/lang/en.json | 7 +++ resources/app/gui/lang/en.json | 4 ++ resources/app/gui/randomize/item/widgets.json | 9 +++ source/classes/constants.py | 3 +- source/item/FillUtil.py | 20 +++++++ 15 files changed, 181 insertions(+), 68 deletions(-) create mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index b9b3dd1c..3a2dc49b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,6 +114,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) + set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') @@ -324,7 +325,7 @@ class World(object): elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: ret.prog_items[item.name, item.player] += 1 - elif item.advancement or item.smallkey or item.bigkey: + elif item.advancement or item.smallkey or item.bigkey or item.compass or item.map: ret.prog_items[item.name, item.player] += 1 for item in self.itempool: @@ -339,6 +340,8 @@ class World(object): key_list += [dungeon.big_key.name] if len(dungeon.small_keys) > 0: key_list += [x.name for x in dungeon.small_keys] + # map/compass may be required now + key_list += [x.name for x in dungeon.dungeon_items] from Items import ItemFactory for item in ItemFactory(key_list, p): soft_collect(item) @@ -2178,6 +2181,12 @@ class Item(object): item_dungeon = 'Hyrule Castle' return item_dungeon + def is_inside_dungeon_item(self, world): + return ((self.smallkey and not world.keyshuffle[self.player]) + or (self.bigkey and not world.bigkeyshuffle[self.player]) + or (self.compass and not world.compassshuffle[self.player]) + or (self.map and not world.mapshuffle[self.player])) + def __str__(self): return str(self.__unicode__()) @@ -2388,6 +2397,7 @@ class Spoiler(object): 'weapons': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, + 'linkshuffle': self.world.shufflelinks, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'item_pool': self.world.difficulty, @@ -2396,6 +2406,7 @@ class Spoiler(object): 'ganon_crystals': self.world.crystals_needed_for_ganon, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, + 'restricted_boss_items': self.world.restrict_boss_items, 'hints': self.world.hints, 'mapshuffle': self.world.mapshuffle, 'compassshuffle': self.world.compassshuffle, @@ -2411,6 +2422,7 @@ class Spoiler(object): 'experimental': self.world.experimental, 'keydropshuffle': self.world.keydropshuffle, 'shopsanity': self.world.shopsanity, + 'psuedoboots': self.world.pseudoboots, 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} @@ -2438,6 +2450,9 @@ class Spoiler(object): return json.dumps(out) def to_file(self, filename): + def yn(flag): + return 'Yes' if flag else 'No' + self.parse_data() with open(filename, 'w') as outfile: outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) @@ -2462,6 +2477,7 @@ class Spoiler(object): outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) + outfile.write(f"Links House Shuffled: {self.metadata['linkshuffle'][player]}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' @@ -2470,6 +2486,7 @@ class Spoiler(object): outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) @@ -2478,12 +2495,13 @@ class Spoiler(object): outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) - outfile.write('Pot shuffle: %s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) - outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) - outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) - outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) - outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") - outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) + outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n") + outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Key Drops shuffled: {yn(self.metadata['keydropshuffle'][player])}\n") + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['bombbag'][player])}\n") if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2665,6 +2683,12 @@ enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} +# additions +# shuffle links: 1 bit +# restrict_boss_mode: 2 bits +# psuedoboots does not effect code +# sfx_shuffle and other adjust items does not effect settings code + class Settings(object): @staticmethod @@ -2738,7 +2762,6 @@ class Settings(object): args.shufflepots[p] = True if settings[7] & 0x4 else False -@unique class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 diff --git a/CLI.py b/CLI.py index bb9ab0a2..bb0af91b 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -140,6 +140,7 @@ def parse_settings(): "progressive": "on", "accessibility": "items", "algorithm": "balanced", + "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE "openpyramid": False, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 948115f3..4be27692 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -226,7 +226,8 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception) + dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception, + world, player) either_crystal = True # if all hooks from the origin are either, explore all bits with either for hook, crystal in dungeon['Origin'].hooks.items(): if crystal != CrystalBarrier.Either: @@ -247,7 +248,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) o_state_cache[door.name] = o_state - piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception) + piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player) dungeon[door.name] = piece check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors, group_flags, door_map, world, player, exception) @@ -347,7 +348,7 @@ def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_ blue_start.big_key_special = o_state.big_key_special b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception) + dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player) def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name): @@ -639,7 +640,7 @@ def stonewall_valid(stonewall): return True -def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception): +def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player): # todo: info about dungeon events - not sure about that graph_piece = GraphPiece() all_unattached = {} @@ -671,16 +672,14 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio graph_piece.visited_regions.update(o_state.visited_orange) graph_piece.visited_regions.update(b_state.visited_blue) graph_piece.visited_regions.update(b_state.visited_orange) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found)) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player)) graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used return graph_piece -def filter_for_potential_bk_locations(locations): - return [x for x in locations if - '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events - and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']] +def filter_for_potential_bk_locations(locations, world, player): + return count_locations_exclude_big_chest(locations, world, player) type_map = { @@ -1023,12 +1022,8 @@ class ExplorationState(object): return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return True - def count_locations_exclude_specials(self): - cnt = 0 - for loc in self.found_locations: - if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item: - cnt += 1 - return cnt + def count_locations_exclude_specials(self, world, player): + return count_locations_exclude_big_chest(self.found_locations, world, player) def validate(self, door, region, world, player): return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, @@ -1069,6 +1064,32 @@ class ExplorationState(object): return 2 +def count_locations_exclude_big_chest(locations, world, player): + cnt = 0 + for loc in locations: + if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)): + cnt += 1 + return cnt + + +def prize_or_event(loc): + return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + + +def reserved_location(loc, world, player): + return loc.name in world.item_pool_config.reserved_locations[player] + + +def blind_boss_unavail(loc, locations, world, player): + if loc.name == "Thieves' Town - Boss": + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in locations if x.name == 'Attic Cracked Floor')))) + return False + + class ExplorableDoor(object): def __init__(self, door, crystal, flag): @@ -1092,7 +1113,8 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg explorable_door = local_state.next_avail_door() if explorable_door.door.bigKey: if bk_flag: - big_not_found = not special_big_key_found(local_state) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 + big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special + else local_state.count_locations_exclude_specials(world, player) == 0) if big_not_found: continue # we can't open this door if explorable_door.door in proposed_map: diff --git a/Dungeons.py b/Dungeons.py index 73f53794..a37ce3d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -117,6 +117,7 @@ def fill_dungeons(world): def get_dungeon_item_pool(world): return [item for dungeon in world.dungeons for item in dungeon.all_items] + def fill_dungeons_restrictive(world, shuffled_locations): all_state_base = world.get_all_state() @@ -137,10 +138,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True - dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player]) - or (item.map and not world.mapshuffle[item.player]) - or (item.compass and not world.compassshuffle[item.player]))] + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] # sort in the order Big Key, Small Key, Other before placing dungeon items sort_order = {"BigKey": 3, "SmallKey": 2} @@ -414,7 +412,7 @@ dungeon_prize = { 'Palace of Darkness': 'Palace of Darkness - Prize', 'Swamp Palace': 'Swamp Palace - Prize', 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': 'Thieves Town - Prize', + 'Thieves Town': "Thieves' Town - Prize", 'Ice Palace': 'Ice Palace - Prize', 'Misery Mire': 'Misery Mire - Prize', 'Turtle Rock': 'Turtle Rock - Prize', diff --git a/Fill.py b/Fill.py index c42b6251..67476fbf 100644 --- a/Fill.py +++ b/Fill.py @@ -237,7 +237,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = def valid_key_placement(item, location, itempool, world): - if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic': + if not valid_reserved_placement(item, location, world): + return False + if ((not item.smallkey and not item.bigkey) or item.player != location.player + or world.retro[item.player] or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon if dungeon: @@ -247,9 +250,13 @@ def valid_key_placement(item, location, itempool, world): unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) else: - inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player])) - return not inside_dungeon_item + return item.is_inside_dungeon_item(world) + + +def valid_reserved_placement(item, location, world): + if item.player == location.player and item.is_inside_dungeon_item(world): + return location.name not in world.item_pool_config.reserved_locations[location.player] + return True def track_outside_keys(item, location, world): diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index cf18f4f4..c5c666e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,8 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize -from DungeonGenerator import ExplorationState, special_big_key_doors +from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import reserved_location, blind_boss_unavail class KeyLayout(object): @@ -1078,40 +1079,30 @@ def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked -def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] - - -def boss_unavail(loc, world, player): - # todo: ambrosia - # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name - return False - - -def blind_boss_unavail(loc, state, world, player): - if loc.name == "Thieves' Town - Boss": - # todo: check attic - return (loc.parent_region.dungeon.boss.name == 'Blind' and - (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or - (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and - not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) - return False +# todo: verfiy this code is defunct +# def prize_or_event(loc): +# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] +# +# +# def reserved_location(loc, world, player): +# return loc in world.item_pool.config.reserved_locations[player] +# +# +# def blind_boss_unavail(loc, state, world, player): +# if loc.name == "Thieves' Town - Boss": +# return (loc.parent_region.dungeon.boss.name == 'Blind' and +# (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or +# (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and +# not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) +# return False +# counts free locations for keys - hence why reserved locations don't count def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) - and not blind_boss_unavail(loc, state, world, player)): - cnt += 1 - return cnt - - -def count_locations_exclude_big_chest(state, world, player): - cnt = 0 - for loc in state.found_locations: - if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) - and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): + if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player) + and not blind_boss_unavail(loc, state.found_locations, world, player)): cnt += 1 return cnt @@ -1407,7 +1398,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) @@ -1596,7 +1587,7 @@ def can_open_door(door, state, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) if door.smallKey: ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) diff --git a/Main.py b/Main.py index ee9e83fb..f6a94ce9 100644 --- a/Main.py +++ b/Main.py @@ -28,6 +28,8 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names +from source.item.FillUtil import create_item_pool_config + __version__ = '0.5.1.0-u' from source.classes.BabelFish import BabelFish @@ -103,6 +105,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_total = args.triforce_pool.copy() world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() + world.restrict_boss_items = args.restrict_boss_items.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} @@ -146,6 +149,7 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) + create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) diff --git a/Mystery.py b/Mystery.py index 73644500..a53fb514 100644 --- a/Mystery.py +++ b/Mystery.py @@ -132,6 +132,7 @@ def roll_settings(weights): ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] ret.accessibility = get_choice('accessibility') + ret.restrict_boss_items = get_choice('restrict_boss_items') entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' diff --git a/Rules.py b/Rules.py index 28d627da..f06328c5 100644 --- a/Rules.py +++ b/Rules.py @@ -4,6 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType +from Dungeons import dungeon_regions, dungeon_prize from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -553,11 +554,28 @@ def global_rules(world, player): add_key_logic_rules(world, player) # End of door rando rules. + if world.restrict_boss_items[player] != 'none': + def add_mc_rule(l): + boss_location = world.get_location(l, player) + d_name = boss_location.parent_region.dungeon.name + compass_name = f'Compass ({d_name})' + map_name = f'Map ({d_name})' + add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) + + for dungeon in dungeon_prize.keys(): + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: + add_mc_rule(loc) + if world.doorShuffle[player] == 'crossed': + add_mc_rule('Agahnim 1') + add_mc_rule('Agahnim 2') + add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop + def bomb_rules(world, player): bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a0113222..da496104 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -254,6 +254,13 @@ "none" ] }, + "restrict_boss_items": { + "choices": [ + "none", + "mapcompass", + "dungeon" + ] + }, "hints": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 67efab1d..13c99b2a 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -276,6 +276,13 @@ "Locations: You will be able to reach every location in the game.", "None: You will be able to reach enough locations to beat the game." ], + "restrict_boss_items": [ + "Select which dungeon are not allowed on bosses (default: %(default)s)", + "None: All items allowed", + "Mapcompass: Map and Compass are required before you defeat the boss.", + "Dungeon: Same as above and keys too cannot be on the boss. Small key shuffle", + " and big key shuffle override this behavior" + ], "hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ], "shuffleganon": [ "Include the Ganon's Tower and Pyramid Hole in the", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c4cd8a11..ae26b0dd 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -287,6 +287,10 @@ "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.restrict_boss_items": "Forbidden Boss Items", + "randomizer.item.restrict_boss_items.none": "None", + "randomizer.item.restrict_boss_items.mapcompass": "Map & Compass", + "randomizer.item.restrict_boss_items.dungeon": "Map & Compass & Keys", "bottom.content.worlds": "Worlds", "bottom.content.names": "Player names", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 038e668c..89cacb00 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -124,6 +124,15 @@ "vt26", "balanced" ] + }, + "restrict_boss_items": { + "type": "selectbox", + "default": "none", + "options": [ + "none", + "mapcompass", + "dungeon" + ] } } } diff --git a/source/classes/constants.py b/source/classes/constants.py index b184643b..64b01520 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -72,7 +72,8 @@ SETTINGSTOPROCESS = { "progressives": "progressive", "accessibility": "accessibility", "sortingalgo": "algorithm", - "beemizer": "beemizer" + "beemizer": "beemizer", + "restrict_boss_items": "restrict_boss_items" }, "entrance": { "openpyramid": "openpyramid", diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py new file mode 100644 index 00000000..958c6b67 --- /dev/null +++ b/source/item/FillUtil.py @@ -0,0 +1,20 @@ +from collections import defaultdict + +from Dungeons import dungeon_prize + +class ItemPoolConfig(object): + + def __init__(self): + self.reserved_locations = defaultdict(set) + + +def create_item_pool_config(world): + config = ItemPoolConfig() + if world.algorithm in ['balanced']: + for player in range(1, world.players+1): + if world.restrict_boss_items[player]: + for dungeon in dungeon_prize: + if dungeon.startswith('Thieves'): + dungeon = "Thieves' Town" + config.reserved_locations[player].add(f'{dungeon} - Boss') + world.item_pool_config = config From 6f06dbcd04ee1579c88d136d9ed088f0c63afee7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 05/33] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 45 ++++++++++++++++++++++++++++++++------------- KeyDoorShuffle.py | 10 +++++----- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3a2dc49b..b35e53da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -441,8 +441,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index a37ce3d4..2edba0d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -373,6 +373,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -405,19 +437,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': "Thieves' Town - Prize", - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} - dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5c666e4..5c65dbe6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1378,7 +1378,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1541,7 +1541,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1966,8 +1966,8 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 4d776e0fee2c49d7cc208dffbd75fde5f6a2d48c Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:25:29 -0600 Subject: [PATCH 06/33] Compass/Map can be progressive Fixed filter_for_potential_bk_locations Changed rules to use dungeon_table --- BaseClasses.py | 8 ++++---- DungeonGenerator.py | 5 +++-- Rules.py | 11 ++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b35e53da..67f2f93d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,7 +852,7 @@ class CollectionState(object): reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item - if item_player == player and self.check_if_progressive(item_name): + if item_player == player and self.check_if_progressive(item_name, player): if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: @@ -868,8 +868,7 @@ class CollectionState(object): reduced[('Heart Container', player)] = 1 return frozenset(reduced.items()) - @staticmethod - def check_if_progressive(item_name): + def check_if_progressive(self, item_name, player): return (item_name in ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', @@ -881,7 +880,8 @@ class CollectionState(object): 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] - or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) + or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass')))) def can_reach(self, spot, resolution_hint=None, player=None): try: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4be27692..a2c3df04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -679,7 +679,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return count_locations_exclude_big_chest(locations, world, player) + return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] type_map = { @@ -1078,7 +1079,7 @@ def prize_or_event(loc): def reserved_location(loc, world, player): - return loc.name in world.item_pool_config.reserved_locations[player] + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] def blind_boss_unavail(loc, locations, world, player): diff --git a/Rules.py b/Rules.py index f06328c5..dec41c79 100644 --- a/Rules.py +++ b/Rules.py @@ -4,7 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType -from Dungeons import dungeon_regions, dungeon_prize +from Dungeons import dungeon_table from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -562,10 +562,11 @@ def global_rules(world, player): map_name = f'Map ({d_name})' add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) - for dungeon in dungeon_prize.keys(): - d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon - for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: - add_mc_rule(loc) + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [info.prize, f'{d_name} - Boss']: + add_mc_rule(loc) if world.doorShuffle[player] == 'crossed': add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') From 746a73933980057af6c15de7a6b860eaed958874 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:27:05 -0600 Subject: [PATCH 07/33] Main structure for various biased fills Lots of help around correctly reserving locations --- BaseClasses.py | 13 + Bosses.py | 4 +- DoorShuffle.py | 33 +- DungeonGenerator.py | 42 +- Dungeons.py | 114 --- Fill.py | 403 ++++---- ItemList.py | 13 +- KeyDoorShuffle.py | 2 + Main.py | 64 +- Mystery.py | 11 +- Regions.py | 8 + resources/app/cli/args.json | 12 +- resources/app/cli/lang/en.json | 40 +- resources/app/gui/lang/en.json | 11 +- resources/app/gui/randomize/item/widgets.json | 13 +- source/item/BiasedFill.py | 881 ++++++++++++++++++ source/item/FillUtil.py | 20 - 17 files changed, 1239 insertions(+), 445 deletions(-) create mode 100644 source/item/BiasedFill.py delete mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index 67f2f93d..702f5436 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -17,6 +17,7 @@ from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from RoomData import Room + class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, @@ -213,6 +214,11 @@ class World(object): return r_location raise RuntimeError('No such location %s for player %d' % (location, player)) + def get_location_unsafe(self, location, player): + if (location, player) in self._location_cache: + return self._location_cache[(location, player)] + return None + def get_dungeon(self, dungeonname, player): if isinstance(dungeonname, Dungeon): return dungeonname @@ -1452,6 +1458,10 @@ class Dungeon(object): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' +class FillError(RuntimeError): + pass + + @unique class DoorType(Enum): Normal = 1 @@ -1842,6 +1852,8 @@ class Sector(object): self.destination_entrance = False self.equations = None self.item_logic = set() + self.chest_location_set = set() + def region_set(self): if self.r_name_set is None: @@ -2084,6 +2096,7 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False + self.real = not crystal self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.item_rule = lambda item: True diff --git a/Bosses.py b/Bosses.py index 2718431e..5c742015 100644 --- a/Bosses.py +++ b/Bosses.py @@ -1,8 +1,8 @@ import logging import RaceRandom as random -from BaseClasses import Boss -from Fill import FillError +from BaseClasses import Boss, FillError + def BossFactory(boss, player): if boss is None: diff --git a/DoorShuffle.py b/DoorShuffle.py index ca377c65..dc0f18f5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -46,8 +46,7 @@ def link_doors(world, player): world.swamp_patch_required[player] = orig_swamp_patch -def link_doors_main(world, player): - +def link_doors_prep(world, player): # Drop-down connections & push blocks for exitName, regionName in logical_connections: connect_simple_door(world, exitName, regionName, player) @@ -99,6 +98,7 @@ def link_doors_main(world, player): analyze_portals(world, player) for portal in world.dungeon_portals[player]: connect_portal(portal, world, player) + if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) else: @@ -119,11 +119,14 @@ def link_doors_main(world, player): for ent, ext in default_one_way_connections: connect_one_way(world, ent, ext, player) vanilla_key_logic(world, player) - elif world.doorShuffle[player] == 'basic': + + +def link_doors_main(world, player): + if world.doorShuffle[player] == 'basic': within_dungeon(world, player) elif world.doorShuffle[player] == 'crossed': cross_dungeon(world, player) - else: + elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) @@ -214,11 +217,33 @@ def vanilla_key_logic(world, player): world.key_logic[player] = {} analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout log_key_logic(builder.name, key_layout.key_logic) # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) +def validate_vanilla_reservation(dungeon, world, player): + return validate_key_layout(world.key_layout[player][dungeon.name], world, player) + # if not hasattr(world, 'builder_cache'): + # world.builder_cache = {} + # if (dungeon.name, player) not in world.builder_cache: + # sector = Sector() + # sector.name = dungeon.name + # sector.regions.extend(convert_regions(dungeon.regions, world, player)) + # builder = simple_dungeon_builder(sector.name, [sector]) + # builder.master_sector = sector + # + # origin_list = find_accessible_entrances(world, player, builder) + # start_regions = convert_regions(origin_list, world, player) + # doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + # key_layout = build_key_layout(builder, start_regions, doors, world, player) + # world.builder_cache[(dungeon.name, player)] = key_layout + # else: + # key_layout = world.builder_cache[(dungeon.name, player)] + # return validate_key_layout(key_layout, world, player) + + # some useful functions oppositemap = { Direction.South: Direction.North, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index a2c3df04..1b4b3564 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1198,6 +1198,8 @@ class DungeonBuilder(object): self.sectors = [] self.location_cnt = 0 self.key_drop_cnt = 0 + self.dungeon_items = None # during fill how many dungeon items are left + self.free_items = None # during fill how many dungeon items are left self.bk_required = False self.bk_provided = False self.c_switch_required = False @@ -1359,7 +1361,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, polarized_sectors[sector] = None if bow_sectors: assign_bow_sectors(dungeon_map, bow_sectors, global_pole) - assign_location_sectors(dungeon_map, free_location_sectors, global_pole) + assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) for sector in leftover: @@ -1510,6 +1512,7 @@ def define_sector_features(sectors): sector.bk_provided = True elif loc.name not in dungeon_events and not loc.forced_item: sector.chest_locations += 1 + sector.chest_location_set.add(loc.name) if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest", "Thieves' Town - Blind's Cell"]: sector.bk_required = True @@ -1590,19 +1593,26 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): assign_sector(sector_list[i], builder, bow_sectors, global_pole) -def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): +def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player): valid = False choices = None sector_list = list(free_location_sectors) random.shuffle(sector_list) + orig_location_set = build_orig_location_set(dungeon_map) + num_dungeon_items = requested_dungeon_items(world, player) while not valid: choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) + location_set = {x: set(y) for x, y in orig_location_set.items()} for i, sector in enumerate(sector_list): - choice = d_idx[choices[i].name] + d_name = choices[i].name + choice = d_idx[d_name] totals[choice] += sector.chest_locations + location_set[d_name].update(sector.chest_location_set) valid = True for d_name, idx in d_idx.items(): - if totals[idx] < 5: # min locations for dungeons is 5 (bk exception) + free_items = count_reserved_locations(world, player, location_set[d_name]) + target = max(free_items, 2) + num_dungeon_items + if totals[idx] < target: valid = False break for i, choice in enumerate(choices): @@ -1633,6 +1643,30 @@ def weighted_random_locations(dungeon_map, free_location_sectors): return choices, d_idx, totals +def build_orig_location_set(dungeon_map): + orig_locations = {} + for name, builder in dungeon_map.items(): + orig_locations[name] = set().union(*(s.chest_location_set for s in builder.sectors)) + return orig_locations + + +def requested_dungeon_items(world, player): + num = 0 + if not world.bigkeyshuffle[player]: + num += 1 + if not world.compassshuffle[player]: + num += 1 + if not world.mapshuffle[player]: + num += 1 + return num + + +def count_reserved_locations(world, player, proposed_set): + if world.item_pool_config: + return len([x for x in proposed_set if x in world.item_pool_config.reserved_locations[player]]) + return 2 + + def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): population = [] some_c_switches_present = False diff --git a/Dungeons.py b/Dungeons.py index 2edba0d4..ce57e7f8 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -1,8 +1,5 @@ -import RaceRandom as random - from BaseClasses import Dungeon from Bosses import BossFactory -from Fill import fill_restrictive from Items import ItemFactory @@ -36,117 +33,6 @@ def create_dungeons(world, player): world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] -def fill_dungeons(world): - freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest'] - - all_state_base = world.get_all_state() - - for player in range(1, world.players + 1): - pinball_room = world.get_location('Skull Woods - Pinball Room', player) - if world.retro[player]: - world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - else: - world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - pinball_room.event = True - pinball_room.locked = True - - dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons] - - loopcnt = 0 - while dungeons: - loopcnt += 1 - dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0) - # this is what we need to fill - dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions] - random.shuffle(dungeon_locations) - - all_state = all_state_base.copy() - - # first place big key - if big_key is not None: - bk_location = None - for location in dungeon_locations: - if location.item_rule(big_key): - bk_location = location - break - - if bk_location is None: - raise RuntimeError('No suitable location for %s' % big_key) - - world.push_item(bk_location, big_key, False) - bk_location.event = True - bk_location.locked = True - dungeon_locations.remove(bk_location) - big_key = None - - # next place small keys - while small_keys: - small_key = small_keys.pop() - all_state.sweep_for_events() - sk_location = None - for location in dungeon_locations: - if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)): - sk_location = location - break - - if sk_location is None: - # need to retry this later - small_keys.append(small_key) - dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items)) - # infinite regression protection - if loopcnt < (30 * world.players): - break - else: - raise RuntimeError('No suitable location for %s' % small_key) - - world.push_item(sk_location, small_key, False) - sk_location.event = True - sk_location.locked = True - dungeon_locations.remove(sk_location) - - if small_keys: - # key placement not finished, loop again - continue - - # next place dungeon items - for dungeon_item in dungeon_items: - di_location = dungeon_locations.pop() - world.push_item(di_location, dungeon_item, False) - - -def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] - - -def fill_dungeons_restrictive(world, shuffled_locations): - all_state_base = world.get_all_state() - - # for player in range(1, world.players + 1): - # pinball_room = world.get_location('Skull Woods - Pinball Room', player) - # if world.retro[player]: - # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - # else: - # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - # pinball_room.event = True - # pinball_room.locked = True - # shuffled_locations.remove(pinball_room) - - # with shuffled dungeon items they are distributed as part of the normal item pool - for item in world.get_items(): - if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): - item.advancement = True - elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): - item.priority = True - - dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - - # sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order = {"BigKey": 3, "SmallKey": 2} - dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - - fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, - keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, single_player_placement=True) - dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], diff --git a/Fill.py b/Fill.py index 67476fbf..13de2a20 100644 --- a/Fill.py +++ b/Fill.py @@ -3,176 +3,74 @@ import collections import itertools import logging -from BaseClasses import CollectionState +from BaseClasses import CollectionState, FillError from Items import ItemFactory from Regions import shop_to_location_table, retro_shops +from source.item.BiasedFill import filter_locations, classify_major_items, split_pool -class FillError(RuntimeError): - pass - -def distribute_items_cutoff(world, cutoffrate=0.33): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) - - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - - total_advancement_items = len([item for item in itempool if item.advancement]) - placed_advancement_items = 0 - - progress_done = False - advancement_placed = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - placed_advancement_items += 1 - break - - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - placed_advancement_items += 1 - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') - - spot_to_fill = None - for location in fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations): - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) - - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) +def get_dungeon_item_pool(world): + return [item for dungeon in world.dungeons for item in dungeon.all_items] -def distribute_items_staleness(world): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) +def promote_dungeon_items(world): + world.itempool += get_dungeon_item_pool(world) - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool + for item in world.get_items(): + if item.smallkey or item.bigkey: + item.advancement = True + elif item.map or item.compass: + item.priority = True + dungeon_tracking(world) - progress_done = False - advancement_placed = False - # sweep once to pick up preplaced items - world.state.sweep_for_events() +def dungeon_tracking(world): + for dungeon in world.dungeons: + layout = world.dungeon_layouts[dungeon.player][dungeon.name] + layout.dungeon_items = len(dungeon.all_items) + layout.free_items = layout.location_cnt - layout.dungeon_items - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') +def fill_dungeons_restrictive(world, shuffled_locations): + dungeon_tracking(world) + all_state_base = world.get_all_state() - spot_to_fill = None - for location in fill_locations: - # increase likelyhood of skipping a location if it has been found stale - if not progress_done and random.randint(0, location.staleness_count) > 2: - continue + # for player in range(1, world.players + 1): + # pinball_room = world.get_location('Skull Woods - Pinball Room', player) + # if world.retro[player]: + # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) + # else: + # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) + # pinball_room.event = True + # pinball_room.locked = True + # shuffled_locations.remove(pinball_room) - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - else: - location.staleness_count += 1 + # with shuffled dungeon items they are distributed as part of the normal item pool + for item in world.get_items(): + if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): + item.advancement = True + elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): + item.priority = True - # might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate - if spot_to_fill is None: - for location in fill_locations: - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) + # sort in the order Big Key, Small Key, Other before placing dungeon items + sort_order = {"BigKey": 3, "SmallKey": 2} + dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) + fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, + keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, + single_player_placement=True) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) -def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = None, single_player_placement = False): +def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, + reserved_items=None): + if not reserved_items: + reserved_items = [] + def sweep_from_pool(): new_state = base_state.copy() - for item in itempool: + for item in itempool + reserved_items: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -201,41 +99,56 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None - for location in locations: - if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there - location.item = item_to_place - test_state = maximum_exploration_state.copy() - test_state.stale[item_to_place.player] = True - else: - test_state = maximum_exploration_state - if (not single_player_placement or location.player == item_to_place.player)\ - and location.can_fill(test_state, item_to_place, perform_access_check)\ - and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location + item_locations = filter_locations(item_to_place, locations, world) + for location in item_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, + single_player_placement, perform_access_check, itempool, + keys_in_itempool, world) + if spot_to_fill: break - if item_to_place.smallkey or item_to_place.bigkey: - location.item = None - if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) if world.can_beat_game(): if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') continue - spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, - base_state, itempool, keys_in_itempool, single_player_placement) + if world.algorithm in ['balanced', 'equitable']: + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, + single_player_placement) if spot_to_fill is None: raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) track_outside_keys(item_to_place, spot_to_fill, world) + track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) spot_to_fill.event = True itempool.extend(unplaced_items) +def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, + itempool, keys_in_itempool, world): + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + location.item = item_to_place + test_state = max_exp_state.copy() + test_state.stale[item_to_place.player] = True + else: + test_state = max_exp_state + if not single_player_placement or location.player == item_to_place.player: + if location.can_fill(test_state, item_to_place, perform_access_check): + test_pool = itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool + if valid_key_placement(item_to_place, location, test_pool, world): + if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = None + return None + + def valid_key_placement(item, location, itempool, world): if not valid_reserved_placement(item, location, world): return False @@ -259,6 +172,17 @@ def valid_reserved_placement(item, location, world): return True +def valid_dungeon_placement(item, location, world): + if location.parent_region.dungeon: + layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + if not is_dungeon_item(item, world) or item.player != location.player: + return layout.free_items > 0 + else: + # the second half probably doesn't matter much - should always return true + return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0 + return not is_dungeon_item(item, world) + + def track_outside_keys(item, location, world): if not item.smallkey: return @@ -270,6 +194,22 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def track_dungeon_items(item, location, world): + if location.parent_region.dungeon and not item.crystal: + layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + if is_dungeon_item(item, world) and item.player == location.player: + layout.dungeon_items -= 1 + else: + layout.free_items -= 1 + + +def is_dungeon_item(item, world): + return ((item.smallkey and not world.keyshuffle[item.player]) + or (item.bigkey and not world.bigkeyshuffle[item.player]) + or (item.compass and not world.compassshuffle[item.player]) + or (item.map and not world.mapshuffle[item.player])) + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -351,6 +291,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) # get items to distribute + classify_major_items(world) random.shuffle(world.itempool) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] @@ -379,21 +320,53 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) + keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} + if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']: + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + random.shuffle(fill_locations) + if world.algorithm == 'balanced': + fast_fill(world, prioitempool, fill_locations) + elif world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['dungeon_bias', 'entangled']: + filtered_fill(world, prioitempool, fill_locations) + else: # just need to ensure dungeon items still get placed in dungeons + fast_equitable_fill(world, prioitempool, fill_locations) + # placeholder work + if world.algorithm == 'entangled' and world.players > 1: + random.shuffle(fill_locations) + placeholder_locations = filter_locations('Placeholder', fill_locations, world) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + for i in placeholder_items: + restitempool.remove(i) + for l in placeholder_locations: + fill_locations.remove(l) + filtered_fill(world, placeholder_items, placeholder_locations) + else: + primary, secondary = split_pool(progitempool, world) + fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary) + random.shuffle(fill_locations) + tertiary, quaternary = split_pool(prioitempool, world) + prioitempool = [] + filtered_equitable_fill(world, tertiary, fill_locations) + prioitempool += tertiary + random.shuffle(fill_locations) + fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool) + random.shuffle(fill_locations) + fast_equitable_fill(world, quaternary, fill_locations) + prioitempool += quaternary - fill_restrictive(world, world.state, fill_locations, progitempool, - keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)}) - - random.shuffle(fill_locations) - - fast_fill(world, prioitempool, fill_locations) - - fast_fill(world, restitempool, fill_locations) + if world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, restitempool, fill_locations) + else: + fast_fill(world, restitempool, fill_locations) unplaced = [item.name for item in prioitempool + restitempool] unfilled = [location.name for location in fill_locations] if unplaced or unfilled: logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) + def fast_fill(world, item_pool, fill_locations): while item_pool and fill_locations: spot_to_fill = fill_locations.pop() @@ -401,70 +374,48 @@ def fast_fill(world, item_pool, fill_locations): world.push_item(spot_to_fill, item_to_place, False) -def flood_items(world): - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - progress_done = False +def filtered_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(iter(item_locations)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) # sweep once to pick up preplaced items world.state.sweep_for_events() - # fill world from top of itempool while we can - while not progress_done: - location_list = world.get_unfilled_locations() - random.shuffle(location_list) - spot_to_fill = None - for location in location_list: - if location.can_fill(world.state, itempool[0]): - spot_to_fill = location - break +def fast_vanilla_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) - if spot_to_fill: - item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) - continue - # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue +def filtered_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(l for l in item_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) - # need to place a progress item instead of an already placed item, find candidate - item_to_place = None - candidate_item_to_place = None - for item in itempool: - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if item_to_place is None: - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - raise FillError('No more progress items left to place.') - - # find item to replace with progress item - location_list = world.get_reachable_locations() - random.shuffle(location_list) - for location in location_list: - if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey: - # safe to replace - replace_item = location.item - replace_item.location = None - itempool.append(replace_item) - world.push_item(location, item_to_place, True) - itempool.remove(item_to_place) - break +def fast_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + spot_to_fill = next(l for l in fill_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) def lock_shop_locations(world, player): for shop, loc_names in shop_to_location_table.items(): for loc in loc_names: - world.get_location(loc, player).event = True + # world.get_location(loc, player).event = True world.get_location(loc, player).locked = True # I don't believe these locations exist in non-shopsanity # if world.retro[player]: diff --git a/ItemList.py b/ItemList.py index 5f5b5e7e..5721aadc 100644 --- a/ItemList.py +++ b/ItemList.py @@ -5,12 +5,13 @@ import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from Bosses import place_bosses -from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location -from Fill import FillError, fill_restrictive, fast_fill +from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from Items import ItemFactory +from source.item.BiasedFill import trash_items + import source.classes.constants as CONST @@ -262,8 +263,12 @@ def generate_itempool(world, player): if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] if amt < 0: - for _ in range(amt, 0): - pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']]))) + trash_options = [x for x in pool if x in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x], reverse=True) + while amt > 0 and len(trash_options) > 0: + pool.remove(trash_options.pop()) + amt -= 1 elif amt > 0: for _ in range(0, amt): pool.append('Rupees (20)') diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5c65dbe6..67654c8c 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -177,6 +177,8 @@ class PlacementRule(object): return True available_keys = outside_keys empty_chests = 0 + # todo: sometimes we need an extra empty chest to accomodate the big key too + # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk for loc in check_locations: if not loc.item: diff --git a/Main.py b/Main.py index f6a94ce9..912ca72f 100644 --- a/Main.py +++ b/Main.py @@ -19,16 +19,17 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors -from DoorShuffle import link_doors, connect_portal +from DoorShuffle import link_doors, connect_portal, link_doors_prep from RoomData import create_rooms from Rules import set_rules -from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive -from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items +from Dungeons import create_dungeons +from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.FillUtil import create_item_pool_config +from source.item.BiasedFill import create_item_pool_config, massage_item_pool + __version__ = '0.5.1.0-u' @@ -149,7 +150,6 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) - create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) @@ -165,7 +165,13 @@ def main(args, seed=None, fish=None): else: link_inverted_entrances(world, player) - logger.info(world.fish.translate("cli","cli","shuffling.dungeons")) + logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) + for player in range(1, world.players + 1): + link_doors_prep(world, player) + + create_item_pool_config(world) + + logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons")) for player in range(1, world.players + 1): link_doors(world, player) @@ -173,8 +179,7 @@ def main(args, seed=None, fish=None): mark_light_world_regions(world, player) else: mark_dark_world_regions(world, player) - logger.info(world.fish.translate("cli","cli","generating.itempool")) - logger.info(world.fish.translate("cli","cli","generating.itempool")) + logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): generate_itempool(world, player) @@ -192,8 +197,8 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) - - logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) + massage_item_pool(world) + logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) @@ -202,14 +207,14 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - shuffled_locations = None - if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + - list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): + if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']: shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) + elif args.algorithm == 'equitable': + promote_dungeon_items(world) else: - fill_dungeons(world) + promote_dungeon_items(world) for player in range(1, world.players+1): if world.logic[player] != 'nologic': @@ -227,34 +232,22 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","fill.world")) - if args.algorithm == 'flood': - flood_items(world) # different algo, biased towards early game progress items - elif args.algorithm == 'vt21': - distribute_items_cutoff(world, 1) - elif args.algorithm == 'vt22': - distribute_items_cutoff(world, 0.66) - elif args.algorithm == 'freshness': - distribute_items_staleness(world) - elif args.algorithm == 'vt25': - distribute_items_restrictive(world, False) - elif args.algorithm == 'vt26': - - distribute_items_restrictive(world, True, shuffled_locations) - elif args.algorithm == 'balanced': - distribute_items_restrictive(world, True) + distribute_items_restrictive(world, True) if world.players > 1: - logger.info(world.fish.translate("cli","cli","balance.multiworld")) - balance_multiworld_progression(world) + logger.info(world.fish.translate("cli", "cli", "balance.multiworld")) + if args.algorithm in ['balanced', 'equitable']: + balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom if not world.can_beat_game(log_error=True): - raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) + raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game")) for player in range(1, world.players+1): if world.shopsanity[player]: customize_shops(world, player) - balance_money_progression(world) + if args.algorithm in ['balanced', 'equitable']: + balance_money_progression(world) outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -405,6 +398,7 @@ def copy_world(world): ret.keydropshuffle = world.keydropshuffle.copy() ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() + ret.restrict_boss_items = world.restrict_boss_items.copy() ret.exp_cache = world.exp_cache.copy() @@ -579,11 +573,11 @@ def create_playthrough(world): # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) else: # still required, got to keep it around - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end diff --git a/Mystery.py b/Mystery.py index a53fb514..bf5585b5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -71,6 +71,8 @@ def main(): if args.enemizercli: erargs.enemizercli = args.enemizercli + mw_settings = {'algorithm': False} + settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} for player in range(1, args.multi + 1): @@ -79,7 +81,12 @@ def main(): settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) for k, v in vars(settings).items(): if v is not None: - getattr(erargs, k)[player] = v + if k == 'algorithm': # multiworld wide parameters + if not mw_settings[k]: # only use the first roll + setattr(erargs, k, v) + mw_settings[k] = True + else: + getattr(erargs, k)[player] = v else: raise RuntimeError(f'No weights specified for player {player}') @@ -116,6 +123,8 @@ def roll_settings(weights): ret = argparse.Namespace() + ret.algorithm = get_choice('algorithm') + glitches_required = get_choice('glitches_required') if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") diff --git a/Regions.py b/Regions.py index 35a7eda3..f26bbdb4 100644 --- a/Regions.py +++ b/Regions.py @@ -999,6 +999,14 @@ def adjust_locations(world, player): world.get_location(location, player).address = 0x400000 + index # player address? it is in the shop table index += 1 + # unreal events: + for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Frog', 'Missing Smith', 'Floodgate', + 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Attic Cracked Floor', 'Suspicious Maiden', + 'Revealing Light', 'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off']: + location = world.get_location_unsafe(l, player) + if location: + location.real = False + # (type, room_id, shopkeeper, custom, locked, [items]) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index da496104..99ddec22 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -101,12 +101,12 @@ "algorithm": { "choices": [ "balanced", - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26" + "equitable", + "vanilla_bias", + "major_bias", + "dungeon_bias", + "cluster_bias", + "entangled" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 13c99b2a..7b7bb4f7 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -7,6 +7,7 @@ "seed": "Seed", "player": "Player", "shuffling.world": "Shuffling the World about", + "shuffling.prep": "Dungeon and Item prep", "shuffling.dungeons": "Shuffling dungeons", "shuffling.pots": "Shuffling pots", "basic.traversal": "--Basic Traversal", @@ -153,22 +154,29 @@ "balanced: vt26 derivative that aims to strike a balance between", " the overworld heavy vt25 and the dungeon heavy vt26", " algorithm.", - "vt26: Shuffle items and place them in a random location", - " that it is not impossible to be in. This includes", - " dungeon keys and items.", - "vt25: Shuffle items and place them in a random location", - " that it is not impossible to be in.", - "vt21: Unbiased in its selection, but has tendency to put", - " Ice Rod in Turtle Rock.", - "vt22: Drops off stale locations after 1/3 of progress", - " items were placed to try to circumvent vt21\\'s", - " shortcomings.", - "Freshness: Keep track of stale locations (ones that cannot be", - " reached yet) and decrease likeliness of selecting", - " them the more often they were found unreachable.", - "Flood: Push out items starting from Link\\'s House and", - " slightly biased to placing progression items with", - " less restrictions." + "equitable: does not place dungeon items first allowing new potential", + " but mixed with the normal advancement pool", + "biased placements: these consider all major items to be special and attempts", + "to place items from fixed to semi-random locations. For purposes of these shuffles, all", + "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", + "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", + "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", + "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", + "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", + "vanilla_bias Same as above, but attempts to place items in their vanilla", + " location first. Major items that cannot be placed that way", + " will attempt to be placed in other failed locations first.", + " Also attempts to place junk items in vanilla locations", + "major_bias same as above, but uses the major items' location preferentially", + " major item location are defined as the group of location where", + " the items are found in the vanilla game. Backup locations for items", + " not in the vanilla game will be in the documentation", + "dungeon_bias same as above, but major items are preferentially placed", + " in dungeons locations first", + "cluster_bias same as above, but groups of locations are chosen randomly", + " from a pool of fixed locations designed to be interesting", + " and give major clues about the location of other", + " advancement items. These fixed groups will be documented" ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index ae26b0dd..4cfeaafb 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -279,13 +279,12 @@ "randomizer.item.accessibility.none": "Beatable", "randomizer.item.sortingalgo": "Item Sorting", - "randomizer.item.sortingalgo.freshness": "Freshness", - "randomizer.item.sortingalgo.flood": "Flood", - "randomizer.item.sortingalgo.vt21": "VT8.21", - "randomizer.item.sortingalgo.vt22": "VT8.22", - "randomizer.item.sortingalgo.vt25": "VT8.25", - "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.sortingalgo.equitable": "Equitable", + "randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla", + "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", + "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", + "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 89cacb00..7f524a33 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -116,13 +116,12 @@ "type": "selectbox", "default": "balanced", "options": [ - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26", - "balanced" + "balanced", + "equitable", + "vanilla_bias", + "major_bias", + "dungeon_bias", + "cluster_bias" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py new file mode 100644 index 00000000..45301093 --- /dev/null +++ b/source/item/BiasedFill.py @@ -0,0 +1,881 @@ +import RaceRandom as random +import logging +from collections import defaultdict + +from DoorShuffle import validate_vanilla_reservation +from Dungeons import dungeon_table +from Items import item_table, ItemFactory + + +class ItemPoolConfig(object): + + def __init__(self): + self.location_groups = None + self.static_placement = None + self.item_pool = None + self.placeholders = None + self.reserved_locations = defaultdict(set) + + +class LocationGroup(object): + def __init__(self, name): + self.name = name + self.locations = [] + + # flags + self.keyshuffle = False + self.keydropshuffle = False + self.shopsanity = False + self.retro = False + + def locs(self, locs): + self.locations = locs + return self + + def flags(self, k, d=False, s=False, r=False): + self.keyshuffle = k + self.keydropshuffle = d + self.shopsanity = s + self.retro = r + return self + + +def create_item_pool_config(world): + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True + if world.algorithm == 'vanilla_bias': + config.static_placement = {} + config.location_groups = {} + for player in range(1, world.players + 1): + config.static_placement[player] = vanilla_mapping.copy() + if world.keydropshuffle[player]: + for item, locs in keydrop_vanilla_mapping.items(): + if item in config.static_placement[player]: + config.static_placement[player][item].extend(locs) + else: + config.static_placement[player][item] = list(locs) + # todo: shopsanity... + # todo: retro (universal keys...) + # retro + shops + config.location_groups[player] = [ + LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), + LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), + LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + elif world.algorithm == 'major_bias': + config.location_groups = [ + LocationGroup('MajorItems'), + LocationGroup('Backup') + ] + config.item_pool = {} + init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers'] + for player in range(1, world.players + 1): + groups = LocationGroup('Major').locs(init_set) + if world.bigkeyshuffle[player]: + groups.locations.extend(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + groups.locations.append(mode_grouping['Big Key Drops']) + if world.keyshuffle[player]: + groups.locations.extend(mode_grouping['Small Keys']) + if world.keydropshuffle[player]: + groups.locations.extend(mode_grouping['Key Drops']) + if world.compassshuffle[player]: + groups.locations.extend(mode_grouping['Compasses']) + if world.mapshuffle[player]: + groups.locations.extend(mode_grouping['Maps']) + if world.shopsanity[player]: + groups.locations.append('Capacity Upgrade - Left') + groups.locations.append('Capacity Upgrade - Right') + if world.retro[player]: + if world.shopsanity[player]: + pass # todo: 5 locations for single arrow representation? + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(groups.locations) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'dungeon_bias': + config.location_groups = [ + LocationGroup('Dungeons'), + LocationGroup('Backup') + ] + config.item_pool = {} + dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(dungeon_set) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'entangled' and world.players > 1: + config.location_groups = [ + LocationGroup('Entangled'), + ] + item_cnt = 0 + config.item_pool = {} + limits = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + limits[player] = calc_dungeon_limits(world, player) + c_set = {} + for location in world.get_locations(): + if location.real and not location.forced_item: + c_set[location.name] = None + # todo: retroshop locations are created later, so count them here? + ttl_locations, candidates = 0, list(c_set.keys()) + chosen_locations = defaultdict(set) + random.shuffle(candidates) + while ttl_locations < item_cnt: + choice = candidates.pop() + dungeon = world.get_location(choice, 1).parent_region.dungeon + if dungeon: + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + if isinstance(limits[player], int): + if limits[player] > 0: + config.reserved_locations[player].add(choice) + limits[player] -= 1 + chosen_locations[choice].add(player) + else: + previous = previously_reserved(location, world, player) + if limits[player][dungeon.name] > 0 or previous: + if validate_reservation(location, dungeon, world, player): + if not previous: + limits[player][dungeon.name] -= 1 + chosen_locations[choice].add(player) + else: # not dungeon restricted + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + chosen_locations[choice].add(player) + ttl_locations += len(chosen_locations[choice]) + config.placeholders = ttl_locations - item_cnt + config.location_groups[0].locations = chosen_locations + + +def previously_reserved(location, world, player): + if '- Boss' in location.name: + if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] + or not world.mapshuffle[player]): + return True + if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] + or not world.mapshuffle[player] + or not world.bigkeyshuffle[player] + or not (world.keyshuffle[player] or world.retro[player])): + return True + return False + + +def massage_item_pool(world): + player_pool = defaultdict(list) + for item in world.itempool: + player_pool[item.player].append(item) + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item not in player_pool[item.player]: # filters out maps, compasses, etc + player_pool[item.player].append(item) + player_locations = defaultdict(list) + for player in player_pool: + player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name] + discrepancy = len(player_pool[player]) - len(player_locations[player]) + if discrepancy: + trash_options = [x for x in player_pool[player] if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while discrepancy > 0 and len(trash_options) > 0: + deleted = trash_options.pop() + world.itempool.remove(deleted) + discrepancy -= 1 + if discrepancy > 0: + logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random') + if world.item_pool_config.placeholders is not None: + removed = 0 + single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)'] + removed += len(single_rupees) + for x in single_rupees: + world.itempool.remove(x) + if removed < world.item_pool_config.placeholders: + trash_options = [x for x in world.itempool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while removed < world.item_pool_config.placeholders: + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + world.itempool.remove(deleted) + removed += 1 + placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) + world.itempool += placeholders + removed -= len(placeholders) + for _ in range(removed): + world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) + + +def validate_reservation(location, dungeon, world, player): + world.item_pool_config.reserved_locations[player].add(location.name) + if world.doorShuffle[player] != 'vanilla': + return True # we can generate the dungeon somehow most likely + if validate_vanilla_reservation(dungeon, world, player): + return True + world.item_pool_config.reserved_locations[player].remove(location.name) + return False + + +def count_major_items(world, player): + major_item_set = 52 + if world.bigkeyshuffle[player]: + major_item_set += 11 + if world.keydropshuffle[player]: + major_item_set += 1 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.keyshuffle[player]: + major_item_set += 29 + if world.keydropshuffle[player]: + major_item_set += 32 + if world.compassshuffle[player]: + major_item_set += 11 + if world.doorShuffle[player] == 'crossed': + major_item_set += 2 + if world.mapshuffle[player]: + major_item_set += 12 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.shopsanity[player]: + major_item_set += 2 + if world.retro[player]: + major_item_set += 5 # the single arrow quiver + if world.goal == 'triforcehunt': + major_item_set += world.triforce_pool[player] + if world.bombbag[player]: + major_item_set += world.triforce_pool[player] + # todo: vanilla, assured, swordless? + # if world.swords[player] != "random": + # if world.swords[player] == 'assured': + # major_item_set -= 1 + # if world.swords[player] in ['vanilla', 'swordless']: + # major_item_set -= 4 + # todo: starting equipment? + return major_item_set + + +def calc_dungeon_limits(world, player): + b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player], + world.mapshuffle[player], world.keydropshuffle[player], world.retro[player], + world.restrict_boss_items[player]) + if world.doorShuffle[player] in ['vanilla', 'basic']: + limits = {} + for dungeon, info in dungeon_table.items(): + val = info.free_items + if bi != 'none' and info.prize: + if bi == 'mapcompass' and (not c or not m): + val -= 1 + if bi == 'dungeon' and (not c or not m or not (s or r) or not b): + val -= 1 + if b: + val += 1 if info.bk_present else 0 + if k: + val += 1 if info.bk_drops else 0 + if s or r: + val += info.key_num + if k: + val += info.key_drops + if c: + val += 1 if info.compass_present else 0 + if m: + val += 1 if info.map_present else 0 + limits[dungeon] = val + else: + limits = 60 + if world.bigkeyshuffle[player]: + limits += 11 + if world.keydropshuffle[player]: + limits += 1 + if world.keyshuffle[player] or world.retro[player]: + limits += 29 + if world.keydropshuffle[player]: + limits += 32 + if world.compassshuffle[player]: + limits += 11 + if world.mapshuffle[player]: + limits += 12 + return limits + + +def determine_major_items(world, player): + major_item_set = set(major_items) + if world.bigkeyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) + if world.keyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) + if world.compassshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) + if world.mapshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'}) + if world.shopsanity[player]: + major_item_set.add('Bomb Upgrade (+5)') + major_item_set.add('Arrow Upgrade (+5)') + if world.retro[player]: + major_item_set.add('Single Arrow') + major_item_set.add('Small Key (Universal)') + if world.goal == 'triforcehunt': + major_item_set.add('Triforce Piece') + if world.bombbag[player]: + major_item_set.add('Bomb Upgrade (+10)') + return major_item_set + + +def classify_major_items(world): + if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' + and world.players > 1): + config = world.item_pool_config + for item in world.itempool: + if item.name in config.item_pool[item.player]: + if not item.advancement or not item.priority: + if item.smallkey or item.bigkey: + item.advancement = True + else: + item.priority = True + + +def split_pool(pool, world): + # bias or entangled + config = world.item_pool_config + priority, secondary = [], [] + for item in pool: + if item.name in config.item_pool[item.player]: + priority.append(item) + else: + secondary.append(item) + return priority, secondary + + +def filter_locations(item_to_place, locations, world): + if world.algorithm == 'vanilla_bias': + config, filtered = world.item_pool_config, [] + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + if item_name in config.static_placement[item_to_place.player]: + restricted = config.static_placement[item_to_place.player][item_name] + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + i = 0 + while len(filtered) <= 0: + if i >= len(config.location_groups[item_to_place.player]): + return locations + restricted = config.location_groups[item_to_place.player][i].locations + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + i += 1 + return filtered + if world.algorithm in ['major_bias', 'dungeon_bias']: + config = world.item_pool_config + if item_to_place.name in config.item_pool[item_to_place.player]: + restricted = config.location_groups[0].locations + filtered = [l for l in locations if l.name in restricted] + if len(filtered) == 0: + restricted = config.location_groups[1].locations + filtered = [l for l in locations if l.name in restricted] + # bias toward certain location in overflow? (thinking about this for major_bias) + return filtered if len(filtered) > 0 else locations + if world.algorithm == 'entangled' and world.players > 1: + config = world.item_pool_config + if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: + restricted = config.location_groups[0].locations + filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]] + return filtered if len(filtered) > 0 else locations + return locations + + +vanilla_mapping = { + 'Green Pendant': ['Eastern Palace - Prize'], + 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Blue Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Crystal 1': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 2': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 3': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 4': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 7': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Bow': ['Eastern Palace - Big Chest'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Left'], + 'Book of Mudora': ['Library'], + 'Hammer': ['Palace of Darkness - Big Chest'], + 'Hookshot': ['Swamp Palace - Big Chest'], + 'Magic Mirror': ['Old Man'], + 'Ocarina': ['Flute Spot'], + 'Pegasus Boots': ['Sahasrahla'], + 'Power Glove': ['Desert Palace - Big Chest'], + 'Cape': ["King's Tomb"], + 'Mushroom': ['Mushroom'], + 'Shovel': ['Stumpy'], + 'Lamp': ["Link's House"], + 'Magic Powder': ['Potion Shop'], + 'Moon Pearl': ['Tower of Hera - Big Chest'], + 'Cane of Somaria': ['Misery Mire - Big Chest'], + 'Fire Rod': ['Skull Woods - Big Chest'], + 'Flippers': ['King Zora'], + 'Ice Rod': ['Ice Rod Cave'], + 'Titans Mitts': ["Thieves' Town - Big Chest"], + 'Bombos': ['Bombos Tablet'], + 'Ether': ['Ether Tablet'], + 'Quake': ['Catfish'], + 'Bottle': ['Bottle Merchant', 'Kakariko Tavern', 'Purple Chest', 'Hobo'], + 'Master Sword': ['Master Sword Pedestal'], + 'Tempered Sword': ['Blacksmith'], + 'Fighter Sword': ["Link's Uncle"], + 'Golden Sword': ['Pyramid Fairy - Right'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'], + 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], + 'Silver Arrows': ['Pyramid Fairy - Left'], + 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], + 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', + 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left'], + 'Bombs (3)': ['Floodgate Chest', "Sahasrahla's Hut - Middle", 'Kakariko Well - Bottom', 'Superbunny Cave - Top', + 'Mini Moldorm Cave - Far Left', 'Sewers - Secret Room - Left', 'Paradox Cave Upper - Left', + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Maze - Top', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right'], + 'Blue Mail': ['Ice Palace - Big Chest'], + 'Red Mail': ['Ganons Tower - Big Chest'], + 'Progressive Armor': ['Ice Palace - Big Chest', 'Ganons Tower - Big Chest'], + 'Blue Boomerang': ['Hyrule Castle - Boomerang Chest'], + 'Red Boomerang': ['Waterfall Fairy - Left'], + 'Blue Shield': ['Secret Passage'], + 'Red Shield': ['Waterfall Fairy - Right'], + 'Mirror Shield': ['Turtle Rock - Big Chest'], + 'Progressive Shield': ['Secret Passage', 'Waterfall Fairy - Right', 'Turtle Rock - Big Chest'], + 'Bug Catching Net': ['Sick Kid'], + 'Cane of Byrna': ['Spike Cave'], + 'Boss Heart Container': ['Desert Palace - Boss', 'Eastern Palace - Boss', 'Tower of Hera - Boss', + 'Swamp Palace - Boss', "Thieves' Town - Boss", 'Skull Woods - Boss', 'Ice Palace - Boss', + 'Misery Mire - Boss', 'Turtle Rock - Boss', 'Palace of Darkness - Boss'], + 'Sanctuary Heart Container': ['Sanctuary'], + 'Piece of Heart': ['Sunken Treasure', "Blind's Hideout - Top", "Zora's Ledge", "Aginah's Cave", 'Maze Race', + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Lumberjack Tree', 'Cave 45', 'Graveyard Cave', + 'Checkerboard Cave', 'Bonk Rock Cave', 'Lake Hylia Island', 'Desert Ledge', 'Spectacle Rock', + 'Spectacle Rock Cave', 'Pyramid', 'Digging Game', 'Peg Cave', 'Chest Game', 'Bumper Cave Ledge', + 'Mire Shed - Left', 'Floating Island', 'Mimic Cave'], + 'Rupee (1)': ['Turtle Rock - Eye Bridge - Top Right', 'Ganons Tower - Compass Room - Top Right'], + 'Rupees (5)': ["Hyrule Castle - Zelda's Chest", 'Turtle Rock - Eye Bridge - Top Left', + # 'Palace of Darkness - Harmless Hellway', + 'Palace of Darkness - Dark Maze - Bottom', + 'Ganons Tower - Validation Chest'], + 'Rupees (20)': ["Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', + 'Kakariko Well - Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', 'Hype Cave - Top', + 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Swamp Palace - West Chest', 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Waterfall Room', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Turtle Rock - Eye Bridge - Bottom Right', 'Ganons Tower - Compass Room - Bottom Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], + 'Rupees (50)': ["Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", 'Spiral Cave', 'Superbunny Cave - Bottom', + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', + 'Hookshot Cave - Bottom Left'], + 'Rupees (100)': ['Eastern Palace - Cannonball Chest'], + 'Rupees (300)': ['Mini Moldorm Cave - Generous Guy', 'Sewers - Secret Room - Middle', 'Hype Cave - Generous Guy', + 'Brewery', 'C-Shaped House'], + 'Magic Upgrade (1/2)': ['Magic Bat'], + 'Big Key (Eastern Palace)': ['Eastern Palace - Big Key Chest'], + 'Compass (Eastern Palace)': ['Eastern Palace - Compass Chest'], + 'Map (Eastern Palace)': ['Eastern Palace - Map Chest'], + 'Small Key (Desert Palace)': ['Desert Palace - Torch'], + 'Big Key (Desert Palace)': ['Desert Palace - Big Key Chest'], + 'Compass (Desert Palace)': ['Desert Palace - Compass Chest'], + 'Map (Desert Palace)': ['Desert Palace - Map Chest'], + 'Small Key (Tower of Hera)': ['Tower of Hera - Basement Cage'], + 'Big Key (Tower of Hera)': ['Tower of Hera - Big Key Chest'], + 'Compass (Tower of Hera)': ['Tower of Hera - Compass Chest'], + 'Map (Tower of Hera)': ['Tower of Hera - Map Chest'], + 'Small Key (Escape)': ['Sewers - Dark Cross'], + 'Map (Escape)': ['Hyrule Castle - Map Chest'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], + 'Small Key (Palace of Darkness)': ['Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - The Arena - Ledge', + 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Harmless Hellway'], + # 'Palace of Darkness - Dark Maze - Bottom'], + 'Big Key (Palace of Darkness)': ['Palace of Darkness - Big Key Chest'], + 'Compass (Palace of Darkness)': ['Palace of Darkness - Compass Chest'], + 'Map (Palace of Darkness)': ['Palace of Darkness - Map Chest'], + 'Small Key (Thieves Town)': ["Thieves' Town - Blind's Cell"], + 'Big Key (Thieves Town)': ["Thieves' Town - Big Key Chest"], + 'Compass (Thieves Town)': ["Thieves' Town - Compass Chest"], + 'Map (Thieves Town)': ["Thieves' Town - Map Chest"], + 'Small Key (Skull Woods)': ['Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Skull Woods - Bridge Room'], + 'Big Key (Skull Woods)': ['Skull Woods - Big Key Chest'], + 'Compass (Skull Woods)': ['Skull Woods - Compass Chest'], + 'Map (Skull Woods)': ['Skull Woods - Map Chest'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Entrance'], + 'Big Key (Swamp Palace)': ['Swamp Palace - Big Key Chest'], + 'Compass (Swamp Palace)': ['Swamp Palace - Compass Chest'], + 'Map (Swamp Palace)': ['Swamp Palace - Map Chest'], + 'Small Key (Ice Palace)': ['Ice Palace - Iced T Room', 'Ice Palace - Spike Room'], + 'Big Key (Ice Palace)': ['Ice Palace - Big Key Chest'], + 'Compass (Ice Palace)': ['Ice Palace - Compass Chest'], + 'Map (Ice Palace)': ['Ice Palace - Map Chest'], + 'Small Key (Misery Mire)': ['Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], + 'Big Key (Misery Mire)': ['Misery Mire - Big Key Chest'], + 'Compass (Misery Mire)': ['Misery Mire - Compass Chest'], + 'Map (Misery Mire)': ['Misery Mire - Map Chest'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', + 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left'], + 'Big Key (Turtle Rock)': ['Turtle Rock - Big Key Chest'], + 'Compass (Turtle Rock)': ['Turtle Rock - Compass Chest'], + 'Map (Turtle Rock)': ['Turtle Rock - Roller Room - Left'], + 'Small Key (Ganons Tower)': ["Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'Big Key (Ganons Tower)': ['Ganons Tower - Big Key Chest'], + 'Compass (Ganons Tower)': ['Ganons Tower - Compass Room - Top Left'], + 'Map (Ganons Tower)': ['Ganons Tower - Map Chest'] +} + + +keydrop_vanilla_mapping = { + 'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'], + 'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'], + 'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop'], + 'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], + 'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"], + 'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key'], + 'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'], + 'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], +} + +mode_grouping = { + 'Overworld Major': [ + "Link's Uncle", 'King Zora', "Link's House", 'Sahasrahla', 'Ice Rod Cave', 'Library', + 'Master Sword Pedestal', 'Old Man', 'Ether Tablet', 'Catfish', 'Stumpy', 'Bombos Tablet', 'Mushroom', + 'Bottle Merchant', 'Kakariko Tavern', 'Secret Passage', 'Flute Spot', 'Purple Chest', + 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', 'Magic Bat', 'Sick Kid', 'Hobo', + 'Potion Shop', 'Spike Cave', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', "King's Tomb", + ], + 'Big Chests': ['Eastern Palace - Big Chest','Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'], + 'Heart Containers': ['Sanctuary', 'Eastern Palace - Boss','Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', + "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss'], + 'Heart Pieces': [ + 'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island', + 'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree', + 'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45', + 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", + 'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic Cave' + ], + 'Big Keys': [ + 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', + 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', + 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', + 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', + ], + 'Compasses': [ + 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', + 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', + "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', + 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left' + ], + 'Maps': [ + 'Hyrule Castle - Map Chest', 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', + 'Tower of Hera - Map Chest', 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', + 'Skull Woods - Map Chest', "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', + 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest' + ], + 'Small Keys': [ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge', + "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Swamp Palace - Entrance', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Roller Room - Right', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest' + ], + 'Dungeon Trash': [ + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + "Hyrule Castle - Zelda's Chest", 'Eastern Palace - Cannonball Chest', "Thieves' Town - Ambush Chest", + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Basement - Left', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Dark Maze - Top', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterfall Room', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', 'Swamp Palace - West Chest', + ], + 'Overworld Trash': [ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + 'Kakariko Well - Bottom', 'Chicken House', 'Floodgate Chest', 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", + "Sahasrahla's Hut - Middle", 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', + 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House', + 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right', + 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left' + ], + 'GT Trash': [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Bottom Left', "Ganons Tower - Bob's Chest", + 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', + 'Ganons Tower - Validation Chest', + ], + 'Key Drops': [ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', + 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', + 'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key", + "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key', + 'Ganons Tower - Mini Helmasuar Key Drop', + ], + 'Big Key Drops': ['Hyrule Castle - Big Key Drop'], + 'Shops': [ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Dark Lake Hylia Shop - Left', + 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Left', + 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', 'Village of Outcasts Shop - Left', + 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', + 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', + 'Paradox Shop - Right', 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Left', + 'Capacity Upgrade - Right' + ], + 'RetroShops': [ + 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', 'Take-Any #2 Item 1', + 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2','Take-Any #4 Item 1', 'Take-Any #4 Item 2' + ] +} + + +major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', + 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', + 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', + 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', + 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield', + 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', + 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', + 'Progressive Bow', 'Progressive Bow (Alt)'} + + +# todo: re-enter these +clustered_groups = [ + LocationGroup("MajorRoute1").locs([ + 'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot', + 'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern', + 'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right' + ]), + LocationGroup("MajorRoute2").locs([ + 'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish', + 'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', + 'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb" + ]), + LocationGroup("BigChest").locs([ + 'Sanctuary', 'Eastern Palace - Big Chest', + 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', 'Palace of Darkness - Big Chest', + 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', "Thieves' Town - Big Chest", + 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', 'Ice Palace - Big Chest', + 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest' + ]), + LocationGroup("BossUncle").locs([ + "Link's Uncle", "Link's House", 'Secret Passage', 'Eastern Palace - Boss', + 'Desert Palace - Boss', 'Tower of Hera - Boss', 'Palace of Darkness - Boss', 'Swamp Palace - Boss', + 'Skull Woods - Boss', "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', + 'Turtle Rock - Boss']), + LocationGroup("HeartPieces LW").locs([ + 'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure', + 'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid', + 'Lumberjack Tree', "Zora's Ledge"]), + LocationGroup("HeartPieces DW").locs([ + 'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave', + 'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']), + LocationGroup("Minor Trash").locs([ + 'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest', + 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top', + 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + 'Swamp Palace - Waterfall Room', 'Turtle Rock - Eye Bridge - Bottom Right', + 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right']), + LocationGroup("CompassTT").locs([ + "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", + 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', + 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', + "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', + 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left']), + LocationGroup("Early SKs").locs([ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway', + "Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest', + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', + 'Sewers - Secret Room - Middle', 'Floodgate Chest' + ]), + LocationGroup("Late SKs").locs([ + 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest", + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest', + ]), + LocationGroup("Kak-LDM").locs([ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Chicken House', 'Paradox Cave Lower - Far Left', + 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', + 'Paradox Cave Lower - Middle', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', + ]), + LocationGroup("BK-Bunny").locs([ + 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', + 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', + 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', + 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', + 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + ]), + LocationGroup("Early Drops").flags(True, True).locs([ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', + 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop', + ]), + LocationGroup("Late Drops").flags(True, True).locs([ + 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', + 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key']), + LocationGroup("SS-Hype-Voo").locs([ + 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery', + 'C-Shaped House', 'Palace of Darkness - The Arena - Ledge', + ]), + LocationGroup("DDM Hard").flags(True, True).locs([ + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', + 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', + 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Right', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop' + ]), + LocationGroup("Kak Shop").flags(False, False, True).locs([ + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', + 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Capacity Upgrade - Left']), + LocationGroup("Hylia Shop").flags(False, False, True).locs([ + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', + 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', + 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', + 'Capacity Upgrade - Right']), + LocationGroup("Map Validation").locs([ + 'Hyrule Castle - Map Chest', + 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', 'Tower of Hera - Map Chest', + 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', 'Skull Woods - Map Chest', + "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', + 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest', 'Ganons Tower - Validation Chest']), + LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']), + LocationGroup("Tower Pain").flags(True).locs([ + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']), + LocationGroup("Retro Shops").flags(False, False, True, True).locs([ + 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', + 'Take-Any #2 Item 1', 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2', + 'Take-Any #4 Item 1', 'Take-Any #4 Item 2', 'Swamp Palace - Entrance', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right', + 'Ganons Tower - Compass Room - Bottom Right', + ]) + +] + + +trash_items = { + 'Nothing': -1, + 'Bee Trap': 0, + 'Rupee (1)': 1, + 'Rupees (5)': 1, + 'Rupees (20)': 1, + + 'Small Heart': 2, + 'Bee': 2, + + 'Bombs (3)': 3, + 'Arrows (10)': 3, + 'Bombs (10)': 3, + + 'Red Potion': 4, + 'Blue Shield': 4, + 'Rupees (50)': 4, + 'Rupees (100)': 4, + 'Rupees (300)': 5, + + 'Piece of Heart': 17 +} diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py deleted file mode 100644 index 958c6b67..00000000 --- a/source/item/FillUtil.py +++ /dev/null @@ -1,20 +0,0 @@ -from collections import defaultdict - -from Dungeons import dungeon_prize - -class ItemPoolConfig(object): - - def __init__(self): - self.reserved_locations = defaultdict(set) - - -def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config From 5c835dc243ebb9fde6faf9d1efa8424fa165fa13 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 08/33] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 45 ++++++++++++++++++++++++++++++++------------- KeyDoorShuffle.py | 10 +++++----- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3a2dc49b..b35e53da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -441,8 +441,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index a37ce3d4..2edba0d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -373,6 +373,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -405,19 +437,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': "Thieves' Town - Prize", - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} - dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5c666e4..5c65dbe6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1378,7 +1378,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1541,7 +1541,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1966,8 +1966,8 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 4e8a8d28406a1fd59737cd2dcc5b69ad95e27054 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:25:29 -0600 Subject: [PATCH 09/33] Compass/Map can be progressive Fixed filter_for_potential_bk_locations Changed rules to use dungeon_table --- BaseClasses.py | 8 ++++---- DungeonGenerator.py | 5 +++-- Rules.py | 11 ++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b35e53da..67f2f93d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,7 +852,7 @@ class CollectionState(object): reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item - if item_player == player and self.check_if_progressive(item_name): + if item_player == player and self.check_if_progressive(item_name, player): if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: @@ -868,8 +868,7 @@ class CollectionState(object): reduced[('Heart Container', player)] = 1 return frozenset(reduced.items()) - @staticmethod - def check_if_progressive(item_name): + def check_if_progressive(self, item_name, player): return (item_name in ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', @@ -881,7 +880,8 @@ class CollectionState(object): 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] - or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) + or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass')))) def can_reach(self, spot, resolution_hint=None, player=None): try: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4be27692..a2c3df04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -679,7 +679,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return count_locations_exclude_big_chest(locations, world, player) + return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] type_map = { @@ -1078,7 +1079,7 @@ def prize_or_event(loc): def reserved_location(loc, world, player): - return loc.name in world.item_pool_config.reserved_locations[player] + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] def blind_boss_unavail(loc, locations, world, player): diff --git a/Rules.py b/Rules.py index f06328c5..dec41c79 100644 --- a/Rules.py +++ b/Rules.py @@ -4,7 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType -from Dungeons import dungeon_regions, dungeon_prize +from Dungeons import dungeon_table from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -562,10 +562,11 @@ def global_rules(world, player): map_name = f'Map ({d_name})' add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) - for dungeon in dungeon_prize.keys(): - d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon - for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: - add_mc_rule(loc) + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [info.prize, f'{d_name} - Boss']: + add_mc_rule(loc) if world.doorShuffle[player] == 'crossed': add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') From 23352c3bf7ad1098969d4209a41b339349399ed4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:36:12 -0600 Subject: [PATCH 10/33] Correct promotion of map and compass to advancement to add that logic --- source/item/FillUtil.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 958c6b67..6aa2f5cf 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -1,6 +1,6 @@ from collections import defaultdict -from Dungeons import dungeon_prize +from Dungeons import dungeon_table class ItemPoolConfig(object): @@ -9,12 +9,17 @@ class ItemPoolConfig(object): def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True From ebf237cca3b642c1edcbce4693ee7bfe86e2f25a Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:00:55 -0600 Subject: [PATCH 11/33] Ambrosia logic fixes --- DungeonGenerator.py | 2 +- Fill.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 72938538..55a4b9ec 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -680,7 +680,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] diff --git a/Fill.py b/Fill.py index 65aa2f85..339a01ce 100644 --- a/Fill.py +++ b/Fill.py @@ -167,7 +167,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return item.is_inside_dungeon_item(world) + return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this def valid_reserved_placement(item, location, world): From 391db7b5c4ba35c9d51d8be090441ff80566ebe5 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:02:18 -0600 Subject: [PATCH 12/33] Clustered bias algorithm Fixes for various other algorithms --- DoorShuffle.py | 1 + Fill.py | 131 +++++++++---- Main.py | 4 +- source/item/BiasedFill.py | 399 ++++++++++++++++++++++++-------------- 4 files changed, 344 insertions(+), 191 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 8ee16a33..a7e2ee72 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -44,6 +44,7 @@ def link_doors(world, player): reset_rooms(world, player) world.get_door("Skull Pinball WS", player).no_exit() world.swamp_patch_required[player] = orig_swamp_patch + link_doors_prep(world, player) def link_doors_prep(world, player): diff --git a/Fill.py b/Fill.py index 339a01ce..872f09e7 100644 --- a/Fill.py +++ b/Fill.py @@ -6,7 +6,7 @@ import logging from BaseClasses import CollectionState, FillError from Items import ItemFactory from Regions import shop_to_location_table, retro_shops -from source.item.BiasedFill import filter_locations, classify_major_items, split_pool +from source.item.BiasedFill import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback def get_dungeon_item_pool(world): @@ -107,18 +107,17 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - unplaced_items.insert(0, item_to_place) - if world.can_beat_game(): - if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway.' - f' (Could not place {item_to_place})') - continue - if world.algorithm in ['balanced', 'equitable']: - spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, - base_state, itempool, keys_in_itempool, - single_player_placement) + spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, perform_access_check, item_locations, + keys_in_itempool, single_player_placement) if spot_to_fill is None: + # we filled all reachable spots. Maybe the game can be beaten anyway? + unplaced_items.insert(0, item_to_place) + if world.can_beat_game(): + if world.accessibility[item_to_place.player] != 'none': + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') + continue raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) @@ -214,6 +213,55 @@ def is_dungeon_item(item, world): or (item.map and not world.mapshuffle[item.player])) +def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, + keys_in_itempool=None, single_player_placement=False): + if world.algorithm in ['balanced', 'equitable']: + return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, + single_player_placement) + elif world.algorithm == 'vanilla_bias': + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + else: + i, config = 0, world.item_pool_config + tried = set(attempted) + if not item_to_place.is_inside_dungeon_item(world): + while i < len(config.location_groups[item_to_place.player]): + fallback_locations = config.location_groups[item_to_place.player][i].locations + other_locs = [x for x in locations if x.name in fallback_locations] + for location in other_locs: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + i += 1 + tried.update(other_locs) + else: + other_locations = vanilla_fallback(item_to_place, locations, world) + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + tried.update(other_locations) + other_locations = [x for x in locations if x not in tried] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + else: + other_locations = [x for x in locations if x not in attempted] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -232,7 +280,12 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) + return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + +def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): for location in swap_locations: old_item = location.item new_pool = list(itempool) + [old_item] @@ -301,6 +354,9 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] + # dungeon bias may fill up the dungeon... and push items out into the overworld + # fill in gtower locations with trash first for player in range(1, world.players + 1): if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']: @@ -325,40 +381,36 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} - if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']: - fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + + # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia + progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + random.shuffle(fill_locations) + if world.algorithm == 'balanced': + fast_fill(world, prioitempool, fill_locations) + elif world.algorithm == 'vanilla_bias': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias', 'entangled']: + filtered_fill(world, prioitempool, fill_locations) + else: # just need to ensure dungeon items still get placed in dungeons + fast_equitable_fill(world, prioitempool, fill_locations) + # placeholder work + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': random.shuffle(fill_locations) - if world.algorithm == 'balanced': - fast_fill(world, prioitempool, fill_locations) - elif world.algorithm == 'vanilla_bias': - fast_vanilla_fill(world, prioitempool, fill_locations) - elif world.algorithm in ['dungeon_bias', 'entangled']: - filtered_fill(world, prioitempool, fill_locations) - else: # just need to ensure dungeon items still get placed in dungeons - fast_equitable_fill(world, prioitempool, fill_locations) - # placeholder work - if world.algorithm == 'entangled' and world.players > 1: - random.shuffle(fill_locations) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_items = len(placeholder_items) + if num_ph_items > 0: placeholder_locations = filter_locations('Placeholder', fill_locations, world) - placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_locations = len(placeholder_locations) + if num_ph_items < num_ph_locations < len(fill_locations): + for _ in range(num_ph_locations - num_ph_items): + placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)')) + assert len(placeholder_items) == len(placeholder_locations) for i in placeholder_items: restitempool.remove(i) for l in placeholder_locations: fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - else: - primary, secondary = split_pool(progitempool, world) - fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary) - random.shuffle(fill_locations) - tertiary, quaternary = split_pool(prioitempool, world) - prioitempool = [] - filtered_equitable_fill(world, tertiary, fill_locations) - prioitempool += tertiary - random.shuffle(fill_locations) - fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool) - random.shuffle(fill_locations) - fast_equitable_fill(world, quaternary, fill_locations) - prioitempool += quaternary if world.algorithm == 'vanilla_bias': fast_vanilla_fill(world, restitempool, fill_locations) @@ -389,6 +441,7 @@ def filtered_fill(world, item_pool, fill_locations): # sweep once to pick up preplaced items world.state.sweep_for_events() + def fast_vanilla_fill(world, item_pool, fill_locations): while item_pool and fill_locations: item_to_place = item_pool.pop() diff --git a/Main.py b/Main.py index 455cbe41..29e4fe57 100644 --- a/Main.py +++ b/Main.py @@ -207,12 +207,10 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']: + if args.algorithm != 'equitable': shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) - elif args.algorithm == 'equitable': - promote_dungeon_items(world) else: promote_dungeon_items(world) diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 45301093..47aab289 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -1,5 +1,6 @@ import RaceRandom as random import logging +from math import ceil from collections import defaultdict from DoorShuffle import validate_vanilla_reservation @@ -29,7 +30,7 @@ class LocationGroup(object): self.retro = False def locs(self, locs): - self.locations = locs + self.locations = list(locs) return self def flags(self, k, d=False, s=False, r=False): @@ -52,9 +53,10 @@ def create_item_pool_config(world): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') for dungeon in world.dungeons: - for item in dungeon.all_items: - if item.map or item.compass: - item.advancement = True + if world.restrict_boss_items[dungeon.player] != 'none': + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True if world.algorithm == 'vanilla_bias': config.static_placement = {} config.location_groups = {} @@ -70,9 +72,12 @@ def create_item_pool_config(world): # todo: retro (universal keys...) # retro + shops config.location_groups[player] = [ + LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']), LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: + config.reserved_locations[player].add(loc_name) elif world.algorithm == 'major_bias': config.location_groups = [ LocationGroup('MajorItems'), @@ -102,6 +107,7 @@ def create_item_pool_config(world): pass # todo: 5 locations for single arrow representation? config.item_pool[player] = determine_major_items(world, player) config.location_groups[0].locations = set(groups.locations) + config.reserved_locations[player].add(groups.locations) backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) @@ -121,6 +127,36 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) + elif world.algorithm == 'cluster_bias': + config.location_groups = [ + LocationGroup('Clusters'), + ] + item_cnt = defaultdict(int) + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt[player] += count_major_items(world, player) + # set cluster choices + cluster_choices = figure_out_clustered_choices(world) + + chosen_locations = defaultdict(set) + placeholder_cnt = 0 + for player in range(1, world.players + 1): + number_of_clusters = ceil(item_cnt[player] / 13) + location_cnt = 0 + while item_cnt[player] > location_cnt: + chosen_clusters = random.sample(cluster_choices[player], number_of_clusters) + for loc_group in chosen_clusters: + for location in loc_group.locations: + if not location_prefilled(location, world, player): + world.item_pool_config.reserved_locations[player].add(location) + chosen_locations[location].add(player) + location_cnt += 1 + cluster_choices[player] = [x for x in cluster_choices[player] if x not in chosen_clusters] + number_of_clusters = 1 + placeholder_cnt += location_cnt - item_cnt[player] + config.placeholders = placeholder_cnt + config.location_groups[0].locations = chosen_locations elif world.algorithm == 'entangled' and world.players > 1: config.location_groups = [ LocationGroup('Entangled'), @@ -169,6 +205,14 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def location_prefilled(location, world, player): + if world.swords[player] == 'vanilla': + return location in vanilla_swords + if world.goal[player] == 'pedestal': + return location == 'Master Sword Pedestal' + return False + + def previously_reserved(location, world, player): if '- Boss' in location.name: if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] @@ -188,7 +232,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: - if item not in player_pool[item.player]: # filters out maps, compasses, etc + if (not item.compass and not item.map) or item not in player_pool[item.player]: player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: @@ -220,6 +264,9 @@ def massage_item_pool(world): deleted = trash_options.pop() world.itempool.remove(deleted) removed += 1 + if world.item_pool_config.placeholders > len(single_rupees): + for _ in range(world.item_pool_config.placeholders-len(single_rupees)): + single_rupees.append(ItemFactory('Rupee (1)', random.randint(1, world.players))) placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) world.itempool += placeholders removed -= len(placeholders) @@ -227,6 +274,19 @@ def massage_item_pool(world): world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) +def replace_trash_item(item_pool, replacement): + trash_options = [x for x in item_pool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + item_pool.remove(deleted) + replace_item = ItemFactory(replacement, deleted.player) + item_pool.append(replace_item) + return replace_item + + def validate_reservation(location, dungeon, world, player): world.item_pool_config.reserved_locations[player].add(location.name) if world.doorShuffle[player] != 'vanilla': @@ -265,12 +325,22 @@ def count_major_items(world, player): major_item_set += world.triforce_pool[player] if world.bombbag[player]: major_item_set += world.triforce_pool[player] - # todo: vanilla, assured, swordless? - # if world.swords[player] != "random": - # if world.swords[player] == 'assured': - # major_item_set -= 1 - # if world.swords[player] in ['vanilla', 'swordless']: - # major_item_set -= 4 + if world.swords[player] != "random": + if world.swords[player] == 'assured': + major_item_set -= 1 + if world.swords[player] in ['vanilla', 'swordless']: + major_item_set -= 4 + if world.retro[player]: + if world.shopsanity[player]: + major_item_set -= 1 # sword in old man cave + if world.keyshuffle[player]: + major_item_set -= 29 + # universal keys + major_item_set += 19 if world.difficulty[player] == 'normal' else 14 + if world.mode[player] == 'standard' and world.doorShuffle[player] == 'vanilla': + major_item_set -= 1 # a key in escape + if world.doorShuffle[player] != 'vanilla': + major_item_set += 10 # tries to add up to 10 more universal keys for door rando # todo: starting equipment? return major_item_set @@ -354,16 +424,68 @@ def classify_major_items(world): item.priority = True -def split_pool(pool, world): - # bias or entangled - config = world.item_pool_config - priority, secondary = [], [] - for item in pool: - if item.name in config.item_pool[item.player]: - priority.append(item) - else: - secondary.append(item) - return priority, secondary +def figure_out_clustered_choices(world): + cluster_candidates = {} + for player in range(1, world.players + 1): + cluster_candidates[player] = [LocationGroup(x.name).locs(x.locations) for x in clustered_groups] + backups = list(reversed(leftovers)) + if world.bigkeyshuffle[player]: + bk_grp = LocationGroup('BigKeys').locs(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + bk_grp.locations.append(mode_grouping['Big Key Drops']) + for i in range(13-len(bk_grp.locations)): + bk_grp.locations.append(backups.pop()) + cluster_candidates[player].append(bk_grp) + if world.compassshuffle[player]: + cmp_grp = LocationGroup('Compasses').locs(mode_grouping['Compasses']) + if len(cmp_grp.locations) + len(backups) >= 13: + for i in range(13-len(cmp_grp.locations)): + cmp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(cmp_grp) + else: + backups.extend(reversed(cmp_grp.locations)) + if world.mapshuffle[player]: + mp_grp = LocationGroup('Maps').locs(mode_grouping['Maps']) + if len(mp_grp.locations) + len(backups) >= 13: + for i in range(13-len(mp_grp.locations)): + mp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(mp_grp) + else: + backups.extend(reversed(mp_grp.locations)) + if world.shopsanity[player]: + cluster_candidates[player].append(LocationGroup('Shopsanity1').locs(other_clusters['Shopsanity1'])) + cluster_candidates[player].append(LocationGroup('Shopsanity2').locs(other_clusters['Shopsanity2'])) + extras = list(other_clusters['ShopsanityLeft']) + if world.retro[player]: + extras.extend(mode_grouping['RetroShops']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('ShopExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + if world.keyshuffle[player] or world.retro[player]: + cluster_candidates[player].append(LocationGroup('SmallKey1').locs(other_clusters['SmallKey1'])) + cluster_candidates[player].append(LocationGroup('SmallKey2').locs(other_clusters['SmallKey2'])) + extras = list(other_clusters['SmallKeyLeft']) + if world.keydropshuffle[player]: + cluster_candidates[player].append(LocationGroup('KeyDrop1').locs(other_clusters['KeyDrop1'])) + cluster_candidates[player].append(LocationGroup('KeyDrop2').locs(other_clusters['KeyDrop2'])) + extras.extend(other_clusters['KeyDropLeft']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('SmallKeyExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + return cluster_candidates + + +def vanilla_fallback(item_to_place, locations, world): + if item_to_place.is_inside_dungeon_item(world): + return [x for x in locations if x.name in vanilla_fallback_dungeon_set + and x.parent_region.dungeon and x.parent_region.dungeon.name == item_to_place.dungeon] + return [] def filter_locations(item_to_place, locations, world): @@ -391,7 +513,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.name in restricted] # bias toward certain location in overflow? (thinking about this for major_bias) return filtered if len(filtered) > 0 else locations - if world.algorithm == 'entangled' and world.players > 1: + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': config = world.item_pool_config if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -417,7 +539,7 @@ vanilla_mapping = { 'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'], 'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'], 'Bow': ['Eastern Palace - Big Chest'], - 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Left'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Right'], 'Book of Mudora': ['Library'], 'Hammer': ['Palace of Darkness - Big Chest'], 'Hookshot': ['Swamp Palace - Big Chest'], @@ -443,10 +565,10 @@ vanilla_mapping = { 'Master Sword': ['Master Sword Pedestal'], 'Tempered Sword': ['Blacksmith'], 'Fighter Sword': ["Link's Uncle"], - 'Golden Sword': ['Pyramid Fairy - Right'], - 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'], + 'Golden Sword': ['Pyramid Fairy - Left'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Left'], 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], - 'Silver Arrows': ['Pyramid Fairy - Left'], + 'Silver Arrows': ['Pyramid Fairy - Right'], 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', @@ -702,6 +824,11 @@ mode_grouping = { ] } +vanilla_fallback_dungeon_set = set(mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', @@ -714,148 +841,122 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Progressive Bow', 'Progressive Bow (Alt)'} -# todo: re-enter these clustered_groups = [ LocationGroup("MajorRoute1").locs([ - 'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot', - 'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern', - 'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right' + 'Ice Rod Cave', 'Library', 'Old Man', 'Magic Bat', 'Ether Tablet', 'Hobo', 'Purple Chest', 'Spike Cave', + 'Sahasrahla', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right' ]), LocationGroup("MajorRoute2").locs([ - 'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish', - 'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', - 'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb" + 'Mushroom', 'Secret Passage', 'Bottle Merchant', 'Flute Spot', 'Catfish', 'Stumpy', 'Waterfall Fairy - Left', + 'Waterfall Fairy - Right', 'Master Sword Pedestal', "Thieves' Town - Attic", 'Sewers - Secret Room - Right', + 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle' ]), - LocationGroup("BigChest").locs([ - 'Sanctuary', 'Eastern Palace - Big Chest', - 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', 'Palace of Darkness - Big Chest', - 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', "Thieves' Town - Big Chest", - 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', 'Ice Palace - Big Chest', - 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest' + LocationGroup("MajorRoute3").locs([ + 'Kakariko Tavern', 'Sick Kid', 'King Zora', 'Potion Shop', 'Bombos Tablet', "King's Tomb", 'Blacksmith', + 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left' ]), - LocationGroup("BossUncle").locs([ - "Link's Uncle", "Link's House", 'Secret Passage', 'Eastern Palace - Boss', - 'Desert Palace - Boss', 'Tower of Hera - Boss', 'Palace of Darkness - Boss', 'Swamp Palace - Boss', - 'Skull Woods - Boss', "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', - 'Turtle Rock - Boss']), - LocationGroup("HeartPieces LW").locs([ - 'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure', - 'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid', - 'Lumberjack Tree', "Zora's Ledge"]), - LocationGroup("HeartPieces DW").locs([ - 'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave', - 'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island', - 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']), - LocationGroup("Minor Trash").locs([ - 'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest', - 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top', - 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', - 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', - 'Swamp Palace - Waterfall Room', 'Turtle Rock - Eye Bridge - Bottom Right', - 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right']), - LocationGroup("CompassTT").locs([ - "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", - 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', - 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', - "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', - 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left']), - LocationGroup("Early SKs").locs([ - 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', - 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', - 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway', - "Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest', - 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', - 'Sewers - Secret Room - Middle', 'Floodgate Chest' - ]), - LocationGroup("Late SKs").locs([ - 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest", - 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest', - 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', - 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', - 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest', - ]), - LocationGroup("Kak-LDM").locs([ + LocationGroup("Dungeon Major").locs([ + 'Eastern Palace - Big Chest', 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest', "Link's Uncle"]), + LocationGroup("Dungeon Heart").locs([ + 'Sanctuary', 'Eastern Palace - Boss', 'Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', "Thieves' Town - Boss", + 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss', "Link's House", + 'Ganons Tower - Validation Chest']), + LocationGroup("HeartPieces1").locs([ + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Maze Race', 'Lumberjack Tree', 'Bonk Rock Cave', 'Graveyard Cave', + 'Checkerboard Cave', "Zora's Ledge", 'Digging Game', 'Desert Ledge', 'Bumper Cave Ledge', 'Floating Island', + 'Swamp Palace - Waterfall Room']), + LocationGroup("HeartPieces2").locs([ + "Blind's Hideout - Top", 'Sunken Treasure', "Aginah's Cave", 'Mimic Cave', 'Spectacle Rock Cave', 'Cave 45', + 'Spectacle Rock', 'Lake Hylia Island', 'Chest Game', 'Mire Shed - Right', 'Pyramid', 'Peg Cave', + 'Eastern Palace - Cannonball Chest']), + LocationGroup("BlindHope").locs([ "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", - "Blind's Hideout - Far Right", 'Chicken House', 'Paradox Cave Lower - Far Left', - 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', - 'Paradox Cave Lower - Middle', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', + "Blind's Hideout - Far Right", 'Floodgate Chest', 'Spiral Cave', 'Palace of Darkness - Dark Maze - Bottom', + 'Palace of Darkness - Dark Maze - Top', 'Swamp Palace - Flooded Room - Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right']), + LocationGroup('WellHype').locs([ + 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', ]), - LocationGroup("BK-Bunny").locs([ - 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', - 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', - 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', - 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', - 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + LocationGroup('MiniMoldormLasers').locs([ + 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', + 'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Chicken House', 'Brewery', + 'Palace of Darkness - Dark Basement - Left', 'Ice Palace - Freezor Chest', 'Swamp Palace - West Chest', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', ]), - LocationGroup("Early Drops").flags(True, True).locs([ + LocationGroup('ParadoxCloset').locs([ + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', "Hyrule Castle - Zelda's Chest", + 'C-Shaped House', 'Mire Shed - Left', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', + ]) +] + +other_clusters = { + 'SmallKey1': [ + 'Sewers - Dark Cross', 'Tower of Hera - Basement Cage', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - Dark Basement - Right', "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', + 'Misery Mire - Spike Chest', "Ganons Tower - Bob's Torch"], + 'SmallKey2': [ + 'Desert Palace - Torch', 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Harmless Hellway', 'Swamp Palace - Entrance', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Ice Palace - Spike Room', + 'Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left'], + 'SmallKeyLeft': [ + 'Ganons Tower - Tile Room', 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'KeyDrop1': [ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', - 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', - 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', - 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Hyrule Castle - Key Rat Key Drop', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop', + 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'KeyDrop2': [ + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', + 'Desert Palace - Desert Tiles 2 Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Hammer Block Key Drop', + 'Ice Palace - Many Pots Pot Key', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'KeyDropLeft': [ 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', - 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop', - ]), - LocationGroup("Late Drops").flags(True, True).locs([ - 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', - 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', - 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', - 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', - 'Ganons Tower - Double Switch Pot Key']), - LocationGroup("SS-Hype-Voo").locs([ - 'Mini Moldorm Cave - Left', - 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', - 'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', - 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery', - 'C-Shaped House', 'Palace of Darkness - The Arena - Ledge', - ]), - LocationGroup("DDM Hard").flags(True, True).locs([ - 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', - 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', - 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', - 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', - 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Right', - 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop' - ]), - LocationGroup("Kak Shop").flags(False, False, True).locs([ - 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', + 'Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], + 'Shopsanity1': [ 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', - 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', - 'Capacity Upgrade - Left']), - LocationGroup("Hylia Shop").flags(False, False, True).locs([ + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', 'Capacity Upgrade - Left'], + 'Shopsanity2': [ 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', - 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', - 'Capacity Upgrade - Right']), - LocationGroup("Map Validation").locs([ - 'Hyrule Castle - Map Chest', - 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', 'Tower of Hera - Map Chest', - 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', 'Skull Woods - Map Chest', - "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', - 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest', 'Ganons Tower - Validation Chest']), - LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([ - 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', - 'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', - "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", - 'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']), - LocationGroup("Tower Pain").flags(True).locs([ - 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', - 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', - 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', - 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', - 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', - "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']), - LocationGroup("Retro Shops").flags(False, False, True, True).locs([ - 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', - 'Take-Any #2 Item 1', 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2', - 'Take-Any #4 Item 1', 'Take-Any #4 Item 2', 'Swamp Palace - Entrance', - 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right', - 'Ganons Tower - Compass Room - Bottom Right', - ]) + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Right', + ], + 'ShopsanityLeft': ['Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right'] +} +leftovers = [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right',"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right', ] +vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyramid Fairy - Left'} trash_items = { 'Nothing': -1, From a9b872b88d32cde9670a1d579b9da52d10d2add2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:00:55 -0600 Subject: [PATCH 13/33] Ambrosia logic fixes --- DungeonGenerator.py | 2 +- Fill.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 20352b37..cb834877 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -680,7 +680,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] diff --git a/Fill.py b/Fill.py index 447e6912..59a98cea 100644 --- a/Fill.py +++ b/Fill.py @@ -254,7 +254,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return item.is_inside_dungeon_item(world) + return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this def valid_reserved_placement(item, location, world): From c64d499bab652d847990529e0f41cd5aad22dba4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:03:51 -0600 Subject: [PATCH 14/33] Maps/compasses should not be advancement items if the restriction is none --- source/item/FillUtil.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 6aa2f5cf..0a1e54f7 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -20,6 +20,7 @@ def create_item_pool_config(world): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') for dungeon in world.dungeons: - for item in dungeon.all_items: - if item.map or item.compass: - item.advancement = True + if world.restrict_boss_items[dungeon.player] != 'none': + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True From 058b78cff938c905afe7be165da1b8e20603c4bd Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 23 Sep 2021 16:10:07 -0600 Subject: [PATCH 15/33] Massage fix --- source/item/BiasedFill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 47aab289..2eeb8a6b 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -232,7 +232,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: - if (not item.compass and not item.map) or item not in player_pool[item.player]: + if item.is_inside_dungeon_item(world): player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: From 28b87428cc79de844a72aaf0f53c1f48b1484be8 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 5 Oct 2021 13:58:30 -0600 Subject: [PATCH 16/33] Changed bias named. Added districting --- Fill.py | 43 +++-- Main.py | 3 +- RaceRandom.py | 1 + Rom.py | 40 ++++- resources/app/cli/args.json | 9 +- resources/app/cli/lang/en.json | 17 +- resources/app/gui/lang/en.json | 8 +- resources/app/gui/randomize/item/widgets.json | 8 +- source/item/BiasedFill.py | 99 ++++++++-- source/item/District.py | 169 ++++++++++++++++++ 10 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 source/item/District.py diff --git a/Fill.py b/Fill.py index 872f09e7..a770ee71 100644 --- a/Fill.py +++ b/Fill.py @@ -27,7 +27,7 @@ def promote_dungeon_items(world): def dungeon_tracking(world): for dungeon in world.dungeons: layout = world.dungeon_layouts[dungeon.player][dungeon.name] - layout.dungeon_items = len(dungeon.all_items) + layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)]) layout.free_items = layout.location_cnt - layout.dungeon_items @@ -64,13 +64,10 @@ def fill_dungeons_restrictive(world, shuffled_locations): def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, - reserved_items=None): - if not reserved_items: - reserved_items = [] - + vanilla=False): def sweep_from_pool(): new_state = base_state.copy() - for item in itempool + reserved_items: + for item in itempool: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -99,7 +96,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No spot_to_fill = None - item_locations = filter_locations(item_to_place, locations, world) + item_locations = filter_locations(item_to_place, locations, world, vanilla) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, itempool, @@ -107,6 +104,9 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: + if vanilla: + unplaced_items.insert(0, item_to_place) + continue spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, base_state, itempool, perform_access_check, item_locations, keys_in_itempool, single_player_placement) @@ -166,7 +166,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this + return not item.is_inside_dungeon_item(world) def valid_reserved_placement(item, location, world): @@ -215,10 +215,11 @@ def is_dungeon_item(item, world): def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, keys_in_itempool=None, single_player_placement=False): + logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery') if world.algorithm in ['balanced', 'equitable']: return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, single_player_placement) - elif world.algorithm == 'vanilla_bias': + elif world.algorithm == 'vanilla_fill': if item_to_place.type == 'Crystal': possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, @@ -354,8 +355,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] - gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] - # dungeon bias may fill up the dungeon... and push items out into the overworld + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only'] + # dungeon only may fill up the dungeon... and push items out into the overworld # fill in gtower locations with trash first for player in range(1, world.players + 1): @@ -384,18 +385,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + if world.algorithm == 'vanilla_fill': + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True) fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) random.shuffle(fill_locations) if world.algorithm == 'balanced': fast_fill(world, prioitempool, fill_locations) - elif world.algorithm == 'vanilla_bias': + elif world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, prioitempool, fill_locations) - elif world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias', 'entangled']: + elif world.algorithm in ['major_only', 'dungeon_only', 'district']: filtered_fill(world, prioitempool, fill_locations) else: # just need to ensure dungeon items still get placed in dungeons fast_equitable_fill(world, prioitempool, fill_locations) # placeholder work - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if world.algorithm == 'district': random.shuffle(fill_locations) placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] num_ph_items = len(placeholder_items) @@ -412,7 +415,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, restitempool, fill_locations) else: fast_fill(world, restitempool, fill_locations) @@ -443,8 +446,18 @@ def filtered_fill(world, item_pool, fill_locations): def fast_vanilla_fill(world, item_pool, fill_locations): + next_item_pool = [] while item_pool and fill_locations: item_to_place = item_pool.pop() + locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True) + if len(locations): + spot_to_fill = locations.pop() + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + else: + next_item_pool.append(item_to_place) + while next_item_pool and fill_locations: + item_to_place = next_item_pool.pop() spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) fill_locations.remove(spot_to_fill) world.push_item(spot_to_fill, item_to_place, False) diff --git a/Main.py b/Main.py index ca146f5d..09ebb7e5 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.BiasedFill import create_item_pool_config, massage_item_pool +from source.item.BiasedFill import create_item_pool_config, massage_item_pool, district_item_pool_config __version__ = '1.0.0.1-u' @@ -199,6 +199,7 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) + district_item_pool_config(world) massage_item_pool(world) logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) diff --git a/RaceRandom.py b/RaceRandom.py index 127d966d..fa882580 100644 --- a/RaceRandom.py +++ b/RaceRandom.py @@ -22,6 +22,7 @@ def _wrap(name): # These are for intellisense purposes only, and will be overwritten below choice = _prng_inst.choice +choices = _prng_inst.choices gauss = _prng_inst.gauss getrandbits = _prng_inst.getrandbits randint = _prng_inst.randint diff --git a/Rom.py b/Rom.py index 22f017e3..a7ab66ca 100644 --- a/Rom.py +++ b/Rom.py @@ -17,7 +17,8 @@ except ImportError: from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem from DoorShuffle import compass_data, DROptions, boss_indicator -from Dungeons import dungeon_music_addresses +from Dungeons import dungeon_music_addresses, dungeon_table +from DungeonGenerator import dungeon_portals from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -32,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '11f4f494e999a919aafd7d2624e67679' +RANDOMIZERBASEHASH = 'f2791b1fb0776849bd4a0851b75fca26' class JsonRom(object): @@ -2051,6 +2052,7 @@ def write_strings(rom, world, player, team): else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 + hint_count -= 2 if world.algorithm == 'district' and world.shuffle[player] not in ['simple', 'restricted'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: if hint_count > 0: @@ -2142,11 +2144,35 @@ def write_strings(rom, world, player, team): else: tt[hint_locations.pop(0)] = this_hint - # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice. - junk_hints = junk_texts.copy() - random.shuffle(junk_hints) - for location in hint_locations: - tt[location] = junk_hints.pop(0) + if world.shuffle[player] in ['full', 'crossed', 'insanity']: + # 3 hints for dungeons - todo: replace with overworld map code + hint_count = 3 + dungeon_candidates = list(dungeon_table.keys()) + dungeon_choices = random.choices(dungeon_candidates, k=hint_count) + for c in dungeon_choices: + portal_name = random.choice(dungeon_portals[c]) + portal_region = world.get_portal(portal_name, player).door.entrance.connected_region + entrance = next(ent for ent in portal_region.entrances + if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + district =next(d for d in world.districts[player].values() if entrance.name in d.entrances) + this_hint = f'The entrance to {c} can be found in {district.name}' + tt[hint_locations.pop(0)] = this_hint + + if world.algorithm == 'district': + hint_candidates = [] + for name, district in world.districts[player].items(): + if name not in world.item_pool_config.recorded_choices and not district.sphere_one: + hint_candidates.append(f'{name} is a foolish choice') + random.shuffle(hint_candidates) + for location in hint_locations: + tt[location] = hint_candidates.pop(0) + else: + # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint + # isn't selected twice. + junk_hints = junk_texts.copy() + random.shuffle(junk_hints) + for location in hint_locations: + tt[location] = junk_hints.pop(0) # We still need the older hints of course. Those are done here. diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 99ddec22..3a316a69 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -102,11 +102,10 @@ "choices": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias", - "entangled" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 7b7bb4f7..fe536d75 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -156,27 +156,26 @@ " algorithm.", "equitable: does not place dungeon items first allowing new potential", " but mixed with the normal advancement pool", - "biased placements: these consider all major items to be special and attempts", + "restricted placements: these consider all major items to be special and attempts", "to place items from fixed to semi-random locations. For purposes of these shuffles, all", "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", - "vanilla_bias Same as above, but attempts to place items in their vanilla", + "vanilla_fill As above, but attempts to place items in their vanilla", " location first. Major items that cannot be placed that way", " will attempt to be placed in other failed locations first.", - " Also attempts to place junk items in vanilla locations", - "major_bias same as above, but uses the major items' location preferentially", + " Also attempts to place all items in vanilla locations", + "major_only As above, but uses the major items' location preferentially", " major item location are defined as the group of location where", - " the items are found in the vanilla game. Backup locations for items", - " not in the vanilla game will be in the documentation", - "dungeon_bias same as above, but major items are preferentially placed", + " the items are found in the vanilla game.", + "dungeon_only As above, but major items are preferentially placed", " in dungeons locations first", - "cluster_bias same as above, but groups of locations are chosen randomly", + "district As above, but groups of locations are chosen randomly", " from a pool of fixed locations designed to be interesting", " and give major clues about the location of other", - " advancement items. These fixed groups will be documented" + " advancement items. These fixed groups will be documented." ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 4cfeaafb..a2aeb0a2 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -281,10 +281,10 @@ "randomizer.item.sortingalgo": "Item Sorting", "randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.equitable": "Equitable", - "randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla", - "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", - "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", - "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", + "randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill", + "randomizer.item.sortingalgo.major_only": "Major Location Restriction", + "randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction", + "randomizer.item.sortingalgo.district": "District Restriction", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 7f524a33..7b76d8fe 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -118,10 +118,10 @@ "options": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 2eeb8a6b..96e33cfd 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -3,6 +3,7 @@ import logging from math import ceil from collections import defaultdict +from source.item.District import resolve_districts from DoorShuffle import validate_vanilla_reservation from Dungeons import dungeon_table from Items import item_table, ItemFactory @@ -17,6 +18,8 @@ class ItemPoolConfig(object): self.placeholders = None self.reserved_locations = defaultdict(set) + self.recorded_choices = [] + class LocationGroup(object): def __init__(self, name): @@ -57,7 +60,7 @@ def create_item_pool_config(world): for item in dungeon.all_items: if item.map or item.compass: item.advancement = True - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': config.static_placement = {} config.location_groups = {} for player in range(1, world.players + 1): @@ -78,7 +81,7 @@ def create_item_pool_config(world): LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: config.reserved_locations[player].add(loc_name) - elif world.algorithm == 'major_bias': + elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), LocationGroup('Backup') @@ -111,7 +114,7 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) - elif world.algorithm == 'dungeon_bias': + elif world.algorithm == 'dungeon_only': config.location_groups = [ LocationGroup('Dungeons'), LocationGroup('Backup') @@ -205,6 +208,74 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def district_item_pool_config(world): + resolve_districts(world) + if world.algorithm == 'district': + config = world.item_pool_config + config.location_groups = [ + LocationGroup('Districts'), + ] + item_cnt = 0 + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + # set district choices + district_choices = {} + for p in range(1, world.players + 1): + for name, district in world.districts[p].items(): + adjustment = 0 + if district.dungeon: + adjustment = len([i for i in world.get_dungeon(name, p).all_items + if i.is_inside_dungeon_item(world)]) + dist_len = len(district.locations) - adjustment + if name not in district_choices: + district_choices[name] = (district.sphere_one, dist_len) + else: + so, amt = district_choices[name] + district_choices[name] = (so or district.sphere_one, amt + dist_len) + + chosen_locations = defaultdict(set) + location_cnt = 0 + + # choose a sphere one district + sphere_one_choices = [d for d, info in district_choices.items() if info[0]] + sphere_one = random.choice(sphere_one_choices) + so, amt = district_choices[sphere_one] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][sphere_one].locations: + chosen_locations[location].add(player) + del district_choices[sphere_one] + config.recorded_choices.append(sphere_one) + + scale_factors = defaultdict(int) + scale_total = 0 + for p in range(1, world.players + 1): + ent = 'Inverted Ganons Tower' if world.mode[p] == 'inverted' else 'Ganons Tower' + dungeon = world.get_entrance(ent, p).connected_region.dungeon + if dungeon: + scale = world.crystals_needed_for_gt[p] + scale_total += scale + scale_factors[dungeon.name] += scale + scale_total = max(1, scale_total) + scale_divisors = defaultdict(lambda: 1) + scale_divisors.update(scale_factors) + + while location_cnt < item_cnt: + weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] + choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] + so, amt = district_choices[choice] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][choice].locations: + chosen_locations[location].add(player) + del district_choices[choice] + config.recorded_choices.append(choice) + config.placeholders = location_cnt - item_cnt + config.location_groups[0].locations = chosen_locations + + def location_prefilled(location, world, player): if world.swords[player] == 'vanilla': return location in vanilla_swords @@ -390,6 +461,8 @@ def calc_dungeon_limits(world, player): def determine_major_items(world, player): major_item_set = set(major_items) + if world.progressive == 'off': + pass # now what? if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player]: @@ -412,16 +485,18 @@ def determine_major_items(world, player): def classify_major_items(world): - if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' - and world.players > 1): + if world.algorithm in ['major_only', 'dungeon_only', 'district']: config = world.item_pool_config for item in world.itempool: if item.name in config.item_pool[item.player]: - if not item.advancement or not item.priority: + if not item.advancement and not item.priority: if item.smallkey or item.bigkey: item.advancement = True else: item.priority = True + else: + if item.priority: + item.priority = False def figure_out_clustered_choices(world): @@ -488,13 +563,15 @@ def vanilla_fallback(item_to_place, locations, world): return [] -def filter_locations(item_to_place, locations, world): - if world.algorithm == 'vanilla_bias': +def filter_locations(item_to_place, locations, world, vanilla_skip=False): + if world.algorithm == 'vanilla_fill': config, filtered = world.item_pool_config, [] item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name if item_name in config.static_placement[item_to_place.player]: restricted = config.static_placement[item_to_place.player][item_name] filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + if vanilla_skip and len(filtered) == 0: + return filtered i = 0 while len(filtered) <= 0: if i >= len(config.location_groups[item_to_place.player]): @@ -503,7 +580,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] i += 1 return filtered - if world.algorithm in ['major_bias', 'dungeon_bias']: + if world.algorithm in ['major_only', 'dungeon_only']: config = world.item_pool_config if item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -513,7 +590,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.name in restricted] # bias toward certain location in overflow? (thinking about this for major_bias) return filtered if len(filtered) > 0 else locations - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'district': config = world.item_pool_config if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -835,7 +912,7 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', - 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', 'Progressive Bow', 'Progressive Bow (Alt)'} diff --git a/source/item/District.py b/source/item/District.py new file mode 100644 index 00000000..7b48ede8 --- /dev/null +++ b/source/item/District.py @@ -0,0 +1,169 @@ +from collections import deque + +from BaseClasses import CollectionState, RegionType +from Dungeons import dungeon_table + + +class District(object): + + def __init__(self, name, locations, entrances=None, dungeon=None): + self.name = name + self.dungeon = dungeon + self.locations = locations + self.entrances = entrances if entrances else [] + self.sphere_one = False + + +def create_districts(world): + world.districts = {} + for p in range(1, world.players + 1): + create_district_helper(world, p) + + +def create_district_helper(world, player): + inverted = world.mode[player] == 'inverted' + districts = {} + kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'} + nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'} + central_lw_locations = {'Sunken Treasure', 'Flute Spot'} + desert_locations = {'Purple Chest', 'Desert Ledge'} + lake_locations = {'Hobo'} + east_lw_locations = {"Zora's Ledge", 'King Zora'} + lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet'} + east_dw_locations = {'Pyramid', 'Catfish'} + south_dw_locations = {'Stumpy', 'Digging Game', 'Bombos Tablet', 'Lake Hylia Island'} + voo_north_locations = {'Bumper Cave Ledge'} + ddm_locations = {'Floating Island'} + + kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)', + 'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House', + 'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)', + 'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library', + 'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop'] + nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', + 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop', + 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] + central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs', + 'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Cave Shop (Lake Hylia)', + 'Lake Hylia Fortune Teller', 'Hyrule Castle Secret Entrance Drop'] + desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy', + 'Aginahs Cave', '50 Rupee Cave'] + lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave'] + east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut', + 'Long Fairy Cave', 'Potion Shop'] + lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave', + 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', + 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy'] + east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint', + 'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole'] + south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy', + 'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave', + 'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Cave 45'] + voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', + 'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop', 'Graveyard Cave', + 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'Checkerboard Cave'] + ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', + 'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave', + 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave'] + + if inverted: + south_dw_locations.remove('Bombos Tablet') + south_dw_locations.remove('Lake Hylia Island') + voo_north_locations.remove('Bumper Cave Ledge') + ddm_locations.remove('Floating Island') + desert_locations.add('Bombos Tablet') + lake_locations.add('Lake Hylia Island') + nw_lw_locations.add('Bumper Cave Ledge') + lw_dm_locations.add('Floating Island') + + south_dw_entrances.remove('Cave 45') + central_lw_entrances.append('Cave 45') + voo_north_entrances.remove('Graveyard Cave') + nw_lw_entrances.append('Graveyard Cave') + mire_entrances.remove('Checkerboard Cave') + desert_entrances.append('Checkerboard Cave') + ddm_entrances.remove('Mimic Cave') + lw_dm_entrances.append('Mimic Cave') + + south_dw_entrances.remove('Big Bomb Shop') + central_lw_entrances.append('Inverted Big Bomb Shop') + central_lw_entrances.remove('Links House') + south_dw_entrances.append('Inverted Links House') + voo_north_entrances.remove('Dark Sanctuary') + voo_north_entrances.append('Inverted Dark Sanctuary') + ddm_entrances.remove('Ganons Tower') + central_lw_entrances.append('Inverted Ganons Tower') + central_lw_entrances.remove('Agahnims Tower') + ddm_entrances.append('Inverted Agahnims Tower') + east_dw_entrances.remove('Pyramid Entrance') + central_lw_entrances.append('Inverted Pyramid Entrance') + east_dw_entrances.remove('Pyramid Hole') + central_lw_entrances.append('Inverted Pyramid Hole') + + districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances) + districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances) + districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances) + districts['Desert'] = District('Desert', desert_locations, entrances=desert_entrances) + districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances) + districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances) + districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances) + districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances) + districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances) + districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations, + entrances=voo_north_entrances) + districts['The Mire'] = District('The Mire', set(), entrances=mire_entrances) + districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances) + districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()}) + + world.districts[player] = districts + + +def resolve_districts(world): + create_districts(world) + state = CollectionState(world) + state.sweep_for_events() + for player in range(1, world.players + 1): + check_set = find_reachable_locations(state, player) + used_locations = {l for d in world.districts[player].values() for l in d.locations} + for name, district in world.districts[player].items(): + if district.dungeon: + layout = world.dungeon_layouts[player][district.dungeon] + district.locations.update([l.name for r in layout.master_sector.regions + for l in r.locations if not l.item and l.real]) + else: + for entrance in district.entrances: + ent = world.get_entrance(entrance, player) + queue = deque([ent.connected_region]) + visited = set() + while len(queue) > 0: + region = queue.pop() + visited.add(region) + if region.type == RegionType.Cave: + for location in region.locations: + if location.name not in used_locations and not location.item and location.real: + district.locations.add(location.name) + used_locations.add(location.name) + for ext in region.exits: + if ext.connected_region not in visited: + queue.appendleft(ext.connected_region) + district.sphere_one = len(check_set.intersection(district.locations)) > 0 + + +def find_reachable_locations(state, player): + check_set = set() + for region in state.reachable_regions[player]: + for location in region.locations: + if location.can_reach(state) and not location.forced_item and location.real: + check_set.add(location.name) + return check_set From dc7d4940d99a6f1334a1f832020a07d6e9984c0a Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Oct 2021 16:29:09 -0600 Subject: [PATCH 17/33] Overworld map code --- BaseClasses.py | 6 + CLI.py | 3 +- DoorShuffle.py | 1 + Dungeons.py | 32 +++-- EntranceShuffle.py | 136 +++++++++++++++++- Items.py | 2 +- Main.py | 1 + Mystery.py | 2 + Regions.py | 20 +-- Rom.py | 54 +++++-- data/base2current.bps | Bin 136122 -> 136369 bytes mystery_example.yml | 4 + resources/app/cli/args.json | 7 + resources/app/cli/lang/en.json | 3 + resources/app/gui/lang/en.json | 4 + .../app/gui/randomize/entrando/widgets.json | 13 +- source/classes/constants.py | 3 +- 17 files changed, 250 insertions(+), 41 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 58530558..c3c9b59e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1957,6 +1957,12 @@ class Portal(object): self.dependent = None self.deadEnd = False self.light_world = False + self.chosen = False + + def find_portal_entrance(self): + p_region = self.door.entrance.connected_region + return next((x for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]), None) def change_boss_exit(self, exit_idx): self.default = False diff --git a/CLI.py b/CLI.py index bb9ab0a2..168008ae 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'overworld_map', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -146,6 +146,7 @@ def parse_settings(): "shuffleganon": True, "shuffle": "vanilla", "shufflelinks": False, + "overworld_map": "default", "pseudoboots": False, "shufflepots": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 3d3a6eda..3e4bfac9 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1278,6 +1278,7 @@ def refine_boss_exits(world, player): if 0 < len(filtered) < len(reachable_portals): reachable_portals = filtered chosen_one = random.choice(reachable_portals) if len(reachable_portals) > 1 else reachable_portals[0] + chosen_one.chosen = True if chosen_one != current_boss: chosen_one.change_boss_exit(current_boss.boss_exit_idx) current_boss.change_boss_exit(-1) diff --git a/Dungeons.py b/Dungeons.py index 6fe38cfb..fb081155 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -378,8 +378,8 @@ flexible_starts = { class DungeonInfo: - def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): - # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, midx): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): self.free_items = free self.key_num = keys self.bk_present = bk @@ -389,21 +389,23 @@ class DungeonInfo: self.key_drops = drops self.prize = prize + self.map_index = midx + dungeon_table = { - 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), - 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), - 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), - 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), - 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), - 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), - 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), - 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), - 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), - 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), - 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), - 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), - 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None, 0xc), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize', 0x0), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize', 0x2), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize', 0x1), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None, 0xb), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize', 0x3), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize', 0x9), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize', 0x4), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize", 0x6), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize', 0x8), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize', 0x7), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize', 0x5), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None, 0xa), } diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 7e5dcdd3..7a7d3116 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3733,8 +3733,6 @@ indirect_connections = { # | ([addr], None) # holes # exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) -# ToDo somehow merge this with creation of the locations - # ToDo somehow merge this with creation of the locations door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), 'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), @@ -4034,3 +4032,137 @@ exit_ids = {'Links House Exit': (0x01, 0x00), 'Skull Pinball': 0x78, 'Skull Pot Circle': 0x76, 'Pyramid': 0x7B} + +ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Inverted Big Bomb Shop': (0x8b1, 0xb2d), + 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), + 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), + 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), + 'Hyrule Castle Entrance (South)': (0x7b0, 0x730), 'Hyrule Castle Entrance (West)': (0x700, 0x640), + 'Hyrule Castle Entrance (East)': (0x8a0, 0x640), 'Inverted Pyramid Entrance': (0x720, 0x700), + 'Agahnims Tower': (0x7e0, 0x640), 'Inverted Ganons Tower': (0x7e0, 0x640), + 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x240, 0x280), + 'Skull Woods Second Section Door (East)': (0x1a0, 0x240), + 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), 'Skull Woods Final Section': (0x082, 0x0b0), + 'Ice Palace': (0xca0, 0xda0), + 'Misery Mire': (0x100, 0xca0), + 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), + 'Turtle Rock': (0xf11, 0x103), + 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), + 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), + 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), + 'Hyrule Castle Secret Entrance Stairs': (0x850, 0x700), + 'Kakariko Well Cave': (0x060, 0x680), + 'Bat Cave Cave': (0x540, 0x8f0), + 'Elder House (East)': (0x2b0, 0x6a0), + 'Elder House (West)': (0x230, 0x6a0), + 'North Fairy Cave': (0xa80, 0x440), + 'Lost Woods Hideout Stump': (0x240, 0x280), + 'Lumberjack Tree Cave': (0x4e0, 0x004), + 'Two Brothers House (East)': (0x200, 0x0b60), + 'Two Brothers House (West)': (0x180, 0x0b60), + 'Sanctuary': (0x720, 0x4a0), + 'Old Man Cave (West)': (0x580, 0x2c0), + 'Old Man Cave (East)': (0x620, 0x2c0), + 'Old Man House (Bottom)': (0x720, 0x320), + 'Old Man House (Top)': (0x820, 0x220), + 'Death Mountain Return Cave (East)': (0x600, 0x220), + 'Death Mountain Return Cave (West)': (0x500, 0x1c0), + 'Spectacle Rock Cave Peak': (0x720, 0x0a0), + 'Spectacle Rock Cave': (0x790, 0x1a0), + 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), + 'Paradox Cave (Bottom)': (0xd80, 0x180), + 'Paradox Cave (Middle)': (0xd80, 0x380), + 'Paradox Cave (Top)': (0xd80, 0x020), + 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), + 'Fairy Ascension Cave (Top)': (0xc00, 0x240), + 'Spiral Cave': (0xb80, 0x180), + 'Spiral Cave (Bottom)': (0xb80, 0x2c0), + 'Bumper Cave (Bottom)': (0x580, 0x2c0), + 'Bumper Cave (Top)': (0x500, 0x1c0), + 'Superbunny Cave (Top)': (0xd80, 0x020), + 'Superbunny Cave (Bottom)': (0xd00, 0x180), + 'Hookshot Cave': (0xc80, 0x0c0), + 'Hookshot Cave Back Entrance': (0xcf0, 0x004), + 'Ganons Tower': (0x8D0, 0x080), + 'Inverted Agahnims Tower': (0x8D0, 0x080), + 'Pyramid Entrance': (0x640, 0x7c0), + 'Skull Woods First Section Hole (West)': None, + 'Skull Woods First Section Hole (East)': None, + 'Skull Woods First Section Hole (North)': None, + 'Skull Woods Second Section Hole': None, + 'Pyramid Hole': None, + 'Inverted Pyramid Hole': None, + 'Waterfall of Wishing': (0xe80, 0x280), + 'Dam': (0x759, 0xED0), + 'Blinds Hideout': (0x190, 0x6c0), + 'Hyrule Castle Secret Entrance Drop': None, + 'Bonk Fairy (Light)': (0x740, 0xa80), + 'Lake Hylia Fairy': (0xd40, 0x9f0), + 'Light Hype Fairy': (0x940, 0xc80), + 'Desert Fairy': (0x420, 0xe00), + 'Kings Grave': (0x920, 0x520), + 'Tavern North': None, # can't mark this one technically + 'Chicken House': (0x120, 0x880), + 'Aginahs Cave': (0x2e0, 0xd00), + 'Sahasrahlas Hut': (0xcf0, 0x6c0), + 'Cave Shop (Lake Hylia)': (0xbc0, 0xc00), + 'Capacity Upgrade': (0xca0, 0xda0), + 'Kakariko Well Drop': None, + 'Blacksmiths Hut': (0x4a0, 0x880), + 'Bat Cave Drop': None, + 'Sick Kids House': (0x220, 0x880), + 'North Fairy Cave Drop': None, + 'Lost Woods Gamble': (0x240, 0x080), + 'Fortune Teller (Light)': (0x2c0, 0x4c0), + 'Snitch Lady (East)': (0x310, 0x7a0), + 'Snitch Lady (West)': (0x800, 0x7a0), + 'Bush Covered House': (0x2e0, 0x880), + 'Tavern (Front)': (0x270, 0x980), + 'Light World Bomb Hut': (0x070, 0x980), + 'Kakariko Shop': (0x170, 0x980), + 'Lost Woods Hideout Drop': None, + 'Lumberjack Tree Tree': None, + 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), + 'Checkerboard Cave': (0x260, 0xc00), + 'Mini Moldorm Cave': (0xa40, 0xe80), + 'Long Fairy Cave': (0xf60, 0xb00), + 'Good Bee Cave': (0xec0, 0xc00), + '20 Rupee Cave': (0xe80, 0xca0), + '50 Rupee Cave': (0x4d0, 0xed0), + 'Ice Rod Cave': (0xe00, 0xc00), + 'Bonk Rock Cave': (0x5f0, 0x460), + 'Library': (0x270, 0xaa0), + 'Potion Shop': (0xc80, 0x4c0), + 'Sanctuary Grave': None, + 'Hookshot Fairy': (0xd00, 0x180), + 'Pyramid Fairy': (0x740, 0x740), + 'East Dark World Hint': (0xf60, 0xb00), + 'Palace of Darkness Hint': (0xd60, 0x7c0), + 'Dark Lake Hylia Fairy': (0xd40, 0x9f0), + 'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), + 'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), + 'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), + 'Hype Cave': (0x940, 0xc80), + 'Bonk Fairy (Dark)': (0x740, 0xa80), + 'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x800, 0x7a0), + 'Dark World Hammer Peg Cave': (0x4c0, 0x940), + 'Red Shield Shop': (0x500, 0x680), + 'Dark Sanctuary Hint': (0x720, 0x4a0), + 'Inverted Dark Sanctuary': (0x720, 0x4a0), + 'Fortune Teller (Dark)': (0x2c0, 0x4c0), + 'Dark World Shop': (0x2e0, 0x880), + 'Dark World Lumberjack Shop': (0x4e0, 0x0d0), + 'Dark World Potion Shop': (0xc80, 0x4c0), + 'Archery Game': (0x2f0, 0xaf0), + 'Mire Shed': (0x060, 0xc90), + 'Dark Desert Hint': (0x2e0, 0xd00), + 'Dark Desert Fairy': (0x1c0, 0xc90), + 'Spike Cave': (0x860, 0x180), + 'Cave Shop (Dark Death Mountain)': (0xd80, 0x180), + 'Dark Death Mountain Fairy': (0x620, 0x2c0), + 'Mimic Cave': (0xc80, 0x180), + 'Big Bomb Shop': (0x8b1, 0xb2d), 'Inverted Links House': (0x8b1, 0xb2d), + 'Dark Lake Hylia Shop': (0xa40, 0xc40), + 'Lumberjack House': (0x4e0, 0x0d0), + 'Lake Hylia Fortune Teller': (0xa40, 0xc40), + 'Kakariko Gamble Game': (0x2f0, 0xaf0)} diff --git a/Items.py b/Items.py index 808a0740..ba85e51f 100644 --- a/Items.py +++ b/Items.py @@ -22,7 +22,7 @@ def ItemFactory(items, player): return ret -# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) +# Format: Name: (Advancement, Priority, Type, ItemCode, BasePrice, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), diff --git a/Main.py b/Main.py index cfd79062..1f11f3cc 100644 --- a/Main.py +++ b/Main.py @@ -104,6 +104,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_total = args.triforce_pool.copy() world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() + world.overworld_map = args.overworld_map.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} diff --git a/Mystery.py b/Mystery.py index 73644500..3ab32406 100644 --- a/Mystery.py +++ b/Mystery.py @@ -135,6 +135,8 @@ def roll_settings(weights): entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' + overworld_map = get_choice('overworld_map') + ret.overworld_map = overworld_map if overworld_map != 'default' else 'default' door_shuffle = get_choice('door_shuffle') ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') diff --git a/Regions.py b/Regions.py index 546cd49b..c21953f6 100644 --- a/Regions.py +++ b/Regions.py @@ -1335,16 +1335,16 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Ice Block Drop': (None, None, False, None), 'Zelda Pickup': (None, None, False, None), 'Zelda Drop Off': (None, None, False, None), - 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'), - 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'), - 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'), - 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'), - 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'), - 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'), - 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'), - 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'), - 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock'), + 'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE], None, True, 'Eastern Palace'), + 'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF], None, True, 'Desert Palace'), + 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706], None, True, 'Tower of Hera'), + 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702], None, True, 'Palace of Darkness'), + 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701], None, True, 'Swamp Palace'), + 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707], None, True, 'Thieves\' Town'), + 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704], None, True, 'Skull Woods'), + 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705], None, True, 'Ice Palace'), + 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703], None, True, 'Misery Mire'), + 'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708], None, True, 'Turtle Rock'), 'Kakariko Shop - Left': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Middle': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Right': (None, None, False, 'for sale in Kakariko'), diff --git a/Rom.py b/Rom.py index d8514556..ccad9d13 100644 --- a/Rom.py +++ b/Rom.py @@ -16,8 +16,8 @@ except ImportError: raise Exception('Could not load BPS module') from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem -from DoorShuffle import compass_data, DROptions, boss_indicator -from Dungeons import dungeon_music_addresses +from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals +from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -26,13 +26,13 @@ from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc from Items import ItemFactory -from EntranceShuffle import door_addresses, exit_ids +from EntranceShuffle import door_addresses, exit_ids, ow_prize_table from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1c59cec98ba4555db8eed1d2dea76497' +RANDOMIZERBASEHASH = '7ec52e136e8c73a9e093a4baa43fc2d2' class JsonRom(object): @@ -1401,14 +1401,48 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count + compass_mode = 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': - rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location - elif world.dungeon_counters[player] == 'on': - rom.write_byte(0x18003C, 0x02) # always on - elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup': - rom.write_byte(0x18003C, 0x01) # show on pickup - else: + compass_mode = 0x00 # Currently must be off if timer is on, because they use same HUD location rom.write_byte(0x18003C, 0x00) + elif world.dungeon_counters[player] == 'on': + compass_mode = 0x02 # always on + elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup': + compass_mode = 0x01 # show on pickup + if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': + compass_mode |= 0x80 # turn on locating dungeons + x_map_position_generic = [0x3c0, 0xbc0, 0x7c0, 0x1c0, 0x5c0, 0xdc0, 0x7c0, 0xbc0, 0x9c0, 0x3c0] + for idx, x_map in enumerate(x_map_position_generic): + rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) + rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) + if world.compassshuffle[player] and world.overworld_map[player] == 'compass': + compass_mode |= 0x40 # compasses are wild + for dungeon, portal_list in dungeon_portals.items(): + ow_map_index = dungeon_table[dungeon].map_index + if len(portal_list) == 1: + portal_idx = 0 + else: + if world.doorShuffle[player] == 'crossed': + # the random choice excludes sanctuary + portal_idx = next((i for i, elem in enumerate(portal_list) + if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) + else: + portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3}[dungeon] + portal = world.get_portal(portal_list[portal_idx], player) + entrance = portal.find_portal_entrance() + world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00 + coords = ow_prize_table[entrance.name] + # figure out compass entrances and what world (light/dark) + rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0])) + rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) + rom.write_byte(0x53EA6+ow_map_index, world_indicator) + # in crossed doors - flip the compass exists flags + if world.doorShuffle[player] == 'crossed': + exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') + rom.write_byte(0x53E96+ow_map_index, 0x1 if exists_flag else 0x0) + + + rom.write_byte(0x18003C, compass_mode) # Bitfield - enable free items to show up in menu # diff --git a/data/base2current.bps b/data/base2current.bps index fca7a6ecdc7d2b97a0a85bb86dd89a16dfb4749c..0e2a33a60a727bdf791e67f15a7a564fb7eb477b 100644 GIT binary patch delta 9261 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%0DhXFh(4rE7Z_WsuHYfs5#FVDs<`!|ans*s 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 Date: Wed, 27 Oct 2021 15:02:33 -0600 Subject: [PATCH 18/33] Mystery test suite --- .gitignore | 2 +- Mystery.py | 2 + mystery_testsuite.yml | 164 ++++++++++++++++++++++++++++++++ source/test/MysteryTestSuite.py | 124 ++++++++++++++++++++++++ source/test/__init__.py | 0 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 mystery_testsuite.yml create mode 100644 source/test/MysteryTestSuite.py create mode 100644 source/test/__init__.py diff --git a/.gitignore b/.gitignore index bee1283a..b36abdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,6 @@ resources/user/* get-pip.py venv -test +test_games/ data/sprites/official/selan.1.zspr *.zspr diff --git a/Mystery.py b/Mystery.py index 3ab32406..6151693e 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,6 +28,7 @@ def main(): parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -61,6 +62,7 @@ def main(): erargs.seed = seed erargs.names = args.names erargs.create_spoiler = args.create_spoiler + erargs.suppress_rom = args.suppress_rom erargs.race = True erargs.outputname = seedname erargs.outputpath = args.outputpath diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml new file mode 100644 index 00000000..fa99cf91 --- /dev/null +++ b/mystery_testsuite.yml @@ -0,0 +1,164 @@ +description: A test suite for testing various combinations +# Not yet in this branch +#algorithm: +# major_only: 1 +# dungeon_only: 1 +# vanilla_fill: 1 +# balanced: 10 +# district: 1 +door_shuffle: + vanilla: 1 + basic: 2 + crossed: 3 # crossed yield more errors so is preferred +intensity: + 1: 1 + 2: 1 + 3: 2 # intensity 3 usuall yield more errors +keydropshuffle: + on: 1 + off: 1 +shopsanity: + on: 1 + off: 1 +pot_shuffle: + on: 1 + off: 1 +entrance_shuffle: + none: 1 + dungeonssimple: 1 + dungeonsfull: 1 + simple: 1 + restricted: 1 + full: 1 + crossed: 1 + insanity: 1 +shufflelinks: + on: 1 + off: 1 +world_state: + standard: 1 + open: 1 + inverted: 1 + retro: 0 +retro: + on: 1 + off: 1 +goals: + ganon: 1 + fast_ganon: 1 + dungeons: 2 # this yields more errors so is preferred + pedestal: 1 + triforce-hunt: 1 +triforce_goal_min: 20 +triforce_goal_max: 30 +triforce_pool_min: 30 +triforce_pool_max: 40 +triforce_min_difference: 10 +map_shuffle: + on: 1 + off: 1 +compass_shuffle: + on: 1 + off: 1 +smallkey_shuffle: + on: 1 + off: 1 +bigkey_shuffle: + on: 1 + off: 1 +dungeon_counters: + on: 1 + off: 1 + default: 1 +experimental: + on: 1 + off: 1 +glitches_required: + none: 10 # i'm more interest in testing shuffles with more restrictive logic + owg: 1 + no_logic: 1 +accessibility: + items: 1 + locations: 1 + none: 0 # i'm not really interested in this yet +restrict_boss_items: + none: 1 + mapcompass: 1 + dungeon: 1 +tower_open: + "0": 1 + "1": 1 + "2": 1 + "3": 1 + "4": 1 + "5": 1 + "6": 1 + "7": 10 # more restrictions is usually best for testing + random: 1 +ganon_open: + "0": 1 + "1": 1 + "2": 1 + "3": 1 + "4": 1 + "5": 1 + "6": 1 + "7": 10 # more restrictions is usually best for testing + random: 1 +boss_shuffle: + none: 1 + simple: 1 + full: 1 + random: 1 +enemy_shuffle: # shouldn't affect generation + none: 1 + shuffled: 1 + random: 1 + legacy: 0 +hints: + on: 1 + off: 1 +pseudoboots: # shouldn't affect generation + on: 1 + off: 1 +weapons: + randomized: 1 + assured: 1 + vanilla: 1 + swordless: 1 +item_pool: + normal: 1 + hard: 1 + expert: 1 +item_functionality: # shouldn't affect generation + normal: 1 + hard: 0 + expert: 0 +enemy_damage: # shouldn't affect generation + default: 1 + shuffled: 0 + random: 0 +enemy_health: # shouldn't affect generation + default: 1 + easy: 0 + hard: 0 + expert: 0 +rom: + quickswap: # shouldn't affect generation + on: 1 + off: 0 +# reduce_flashing: should affect generation at this point + heartcolor: # shouldn't affect generation + red: 1 + blue: 1 + green: 1 + yellow: 1 + heartbeep: # shouldn't affect generation + double: 0 + normal: 0 + half: 0 + quarter: 1 + off: 0 + shuffle_sfx: + on: 1 + off: 1 diff --git a/source/test/MysteryTestSuite.py b/source/test/MysteryTestSuite.py new file mode 100644 index 00000000..b5143399 --- /dev/null +++ b/source/test/MysteryTestSuite.py @@ -0,0 +1,124 @@ +import subprocess +import sys +import multiprocessing +import concurrent.futures +import argparse +from collections import OrderedDict + +cpu_threads = multiprocessing.cpu_count() +py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + +def main(args=None): + successes = [] + errors = [] + task_mapping = [] + tests = OrderedDict() + + successes.append(f"Testing {args.dr} DR with {args.count} Tests" + (f" (intensity={args.tense})" if args.dr in ['basic', 'crossed'] else "")) + print(successes[0]) + + max_attempts = args.count + pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads) + dead_or_alive = 0 + alive = 0 + + def test(testname: str, command: str): + tests[testname] = [command] + basecommand = f"python3.8 Mystery.py --suppress_rom" + + def gen_seed(): + taskcommand = basecommand + " " + command + return subprocess.run(taskcommand, capture_output=True, shell=True, text=True) + + for x in range(1, max_attempts + 1): + task = pool.submit(gen_seed) + task.success = False + task.name = testname + task.mode = "Mystery" + task.cmd = basecommand + " " + command + task_mapping.append(task) + + for i in range(0, 100): + test("Mystery", "--weights mystery_testsuite.yml") + + from tqdm import tqdm + with tqdm(concurrent.futures.as_completed(task_mapping), + total=len(task_mapping), unit="seed(s)", + desc=f"Success rate: 0.00%") as progressbar: + for task in progressbar: + dead_or_alive += 1 + try: + result = task.result() + if result.returncode: + errors.append([task.name, task.cmd, result.stderr]) + else: + alive += 1 + task.success = True + except Exception as e: + raise e + + progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}") + + def get_results(testname: str): + result = "" + for mode in ['Mystery']: + dead_or_alive = [task.success for task in task_mapping if task.name == testname and task.mode == mode] + alive = [x for x in dead_or_alive if x] + success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%" + successes.append(success) + print(success) + result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t" + return result.strip() + + results = [] + for t in tests.keys(): + results.append(get_results(t)) + + for result in results: + print(result) + successes.append(result) + + return successes, errors + + +if __name__ == "__main__": + successes = [] + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0)) + parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1)) + parser.add_argument('--help', default=False, action='store_true') + + args = parser.parse_args() + + if args.help: + parser.print_help() + exit(0) + + cpu_threads = args.cpu_threads + + for dr in [['mystery', args.count if args.count else 1, 1]]: + + for tense in range(1, dr[2] + 1): + args = argparse.Namespace() + args.dr = dr[0] + args.tense = tense + args.count = dr[1] + s, errors = main(args=args) + if successes: + successes += [""] * 2 + successes += s + print() + + if errors: + with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-errors.txt", 'w') as stream: + for error in errors: + stream.write(error[0] + "\n") + stream.write(error[1] + "\n") + stream.write(error[2] + "\n\n") + + with open("success.txt", "w") as stream: + stream.write(str.join("\n", successes)) + + input("Press enter to continue") diff --git a/source/test/__init__.py b/source/test/__init__.py new file mode 100644 index 00000000..e69de29b From 4131896f93ca5ecfeea0ca0819d569e46d2fd1a7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 2 Nov 2021 15:40:15 -0600 Subject: [PATCH 19/33] Release notes for restricted boss item --- Main.py | 2 +- RELEASENOTES.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 83dc1021..447c3682 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config -__version__ = '1.0.1.0-v' +__version__ = '1.0.2.0-v' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fc5ca90f..e4fd57f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,26 @@ If you do not shuffle the compass or map outside of the dungeon, the non-shuffle The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items. +## Restricted Dungeon Items on Bosses + +You may now restrict the items that can appear on the boss, like the popular ambrosia preset does. + +CLI: ```--restrict_boss_items