From 685ff49711a40575470b23c61f086fb7915b717e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 20 Aug 2020 20:13:00 +0200 Subject: [PATCH] make "universal" small key shuffle a thing and split it out of retro also make retro usable independently from the other world modes in mystery --- BaseClasses.py | 25 ++++++++++++------------- Dungeons.py | 3 ++- EntranceRandomizer.py | 28 +++++++++++++++++++++------- Fill.py | 8 +++++++- Gui.py | 21 ++++++++++++++++----- ItemPool.py | 40 +++++++++++++++++++++++----------------- Mystery.py | 8 ++++++-- Rom.py | 2 +- Rules.py | 10 ++++++---- easy.yaml | 9 +++++++-- 10 files changed, 101 insertions(+), 53 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ce943cc3..e1284ec8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -516,17 +516,13 @@ class CollectionState(object): def has_key(self, item, player, count: int = 1): if self.world.logic[player] == 'nologic': return True - if self.world.retro[player]: + if self.world.keyshuffle[player] == "universal": return self.can_buy_unlimited('Small Key (Universal)', player) - if count == 1: - return (item, player) in self.prog_items return self.prog_items[item, player] >= count def can_buy_unlimited(self, item: str, player: int) -> bool: - for shop in self.world.shops: - if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self): - return True - return False + return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for + shop in self.world.shops) def item_count(self, item, player: int) -> int: return self.prog_items[item, player] @@ -619,9 +615,10 @@ class CollectionState(object): ) def has_sword(self, player: int) -> bool: - return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword', - player) or self.has( - 'Golden Sword', player) + return self.has('Fighter Sword', player) \ + or self.has('Master Sword', player) \ + or self.has('Tempered Sword', player) \ + or self.has('Golden Sword', player) def has_beam_sword(self, player: int) -> bool: return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player) @@ -1267,13 +1264,15 @@ class Spoiler(object): def to_file(self, filename): self.parse_data() - def bool_to_text(variable: bool) -> str: + def bool_to_text(variable: Union[bool, str]) -> str: + if type(variable) == str: + return variable return 'Yes' if variable else 'No' with open(filename, 'w', encoding="utf-8-sig") as outfile: outfile.write( 'ALttP Berserker\'s Multiworld Version %s - Seed: %s\n\n' % ( - self.metadata['version'], self.world.seed)) + self.metadata['version'], self.world.seed)) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %d\n' % self.world.teams) @@ -1312,7 +1311,7 @@ class Spoiler(object): 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')) + 'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player]))) outfile.write('Big Key shuffle: %s\n' % ( 'Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) diff --git a/Dungeons.py b/Dungeons.py index 25d34fee..87c304ad 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -6,7 +6,8 @@ from Items import ItemFactory def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): - dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, dungeon_items, player) + dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys, + dungeon_items, player) dungeon.boss = BossFactory(default_boss, player) for region in dungeon.regions: world.get_region(region, player).dungeon = dungeon diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index dc4e5132..e6fc67dc 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -231,17 +231,27 @@ def parse_arguments(argv, no_defaults=False): --seed given will produce the same 10 (different) roms each time). ''', type=int) - parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], + parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', + choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], help='''\ Select the rate at which the menu opens and closes. (default: %(default)s) ''') parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') - parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') + parser.add_argument('--mapshuffle', default=defval(False), + help='Maps are no longer restricted to their dungeons, but can be anywhere', + action='store_true') + parser.add_argument('--compassshuffle', default=defval(False), + help='Compasses are no longer restricted to their dungeons, but can be anywhere', + action='store_true') + parser.add_argument('--keyshuffle', default=defval("off"), help='\ + on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\ + universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.', + choices=["on", "universal", "off"]) + parser.add_argument('--bigkeyshuffle', default=defval(False), + help='Big Keys are no longer restricted to their dungeons, but can be anywhere', + action='store_true') parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true') parser.add_argument('--retro', default=defval(False), help='''\ Keys are universal, shooting arrows costs rupees, @@ -326,9 +336,13 @@ def parse_arguments(argv, no_defaults=False): ret.dungeon_counters = True elif ret.dungeon_counters == 'off': ret.dungeon_counters = False - if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 + if ret.keysanity: + ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True + elif ret.keyshuffle == "on": + ret.keyshuffle = True + elif ret.keyshuffle == "off": + ret.keyshuffle = False if multiargs.multi: defaults = copy.deepcopy(ret) for player in range(1, multiargs.multi + 1): diff --git a/Fill.py b/Fill.py index d8ce1a3b..279a9c37 100644 --- a/Fill.py +++ b/Fill.py @@ -203,7 +203,13 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si logging.warning( f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})') continue - raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid') + placements = [] + for region in world.regions: + for location in region.locations: + if location.item and not location.event: + placements.append(location) + raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. ' + f'\nAlready placed {len(placements)}: {", ".join(placements)}') world.push_item(spot_to_fill, item_to_place, False) locations.remove(spot_to_fill) diff --git a/Gui.py b/Gui.py index 219d0d06..29a623f0 100755 --- a/Gui.py +++ b/Gui.py @@ -67,16 +67,27 @@ def guiMain(args=None): openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) mcsbshuffleFrame = Frame(checkBoxFrame) mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ") + mapshuffleVar = IntVar() mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar) + compassshuffleVar = IntVar() compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar) - keyshuffleVar = IntVar() - keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar) + bigkeyshuffleVar = IntVar() bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar) + + keyshuffleFrame = Frame(checkBoxFrame) + keyshuffleVar = StringVar() + keyshuffleVar.set('off') + modeOptionMenu = OptionMenu(keyshuffleFrame, keyshuffleVar, 'off', 'universal', 'on') + modeOptionMenu.pack(side=LEFT) + modeLabel = Label(keyshuffleFrame, text='Key Shuffle') + modeLabel.pack(side=LEFT) + retroVar = IntVar() - retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) + retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode", variable=retroVar) + shuffleGanonVar = IntVar() shuffleGanonVar.set(1) # set default shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", @@ -99,8 +110,8 @@ def guiMain(args=None): mcsbLabel.grid(row=0, column=0) mapshuffleCheckbutton.grid(row=0, column=1) compassshuffleCheckbutton.grid(row=0, column=2) - keyshuffleCheckbutton.grid(row=0, column=3) bigkeyshuffleCheckbutton.grid(row=0, column=4) + keyshuffleFrame.pack(expand=True, anchor=W) retroCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W) @@ -476,7 +487,7 @@ def guiMain(args=None): guiargs.openpyramid = bool(openpyramidVar.get()) guiargs.mapshuffle = bool(mapshuffleVar.get()) guiargs.compassshuffle = bool(compassshuffleVar.get()) - guiargs.keyshuffle = bool(keyshuffleVar.get()) + guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()] guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get()) guiargs.retro = bool(retroVar.get()) guiargs.quickswap = bool(quickSwapVar.get()) diff --git a/ItemPool.py b/ItemPool.py index e25d2175..f4f9b3a1 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -43,7 +43,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivemagic', 'basicmagic', 'progressivesword', 'basicsword', 'progressivebow', 'basicbow', 'timedohko', 'timedother', - 'triforcehunt', 'retro', + 'triforcehunt', 'universal_keys', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) @@ -70,7 +70,7 @@ difficulties = { timedohko=['Green Clock'] * 25, timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, triforcehunt=['Triforce Piece'] * 30, - retro=['Small Key (Universal)'] * 28, + universal_keys=['Small Key (Universal)'] * 28, extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra], progressive_sword_limit=8, progressive_shield_limit=6, @@ -99,15 +99,15 @@ difficulties = { timedohko=['Green Clock'] * 25, timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, triforcehunt=['Triforce Piece'] * 30, - retro=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, + universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=4, progressive_shield_limit=3, progressive_armor_limit=2, progressive_bow_limit=2, - progressive_bottle_limit = 4, - boss_heart_container_limit = 10, - heart_piece_limit = 24, + progressive_bottle_limit=4, + boss_heart_container_limit=10, + heart_piece_limit=24, ), 'hard': Difficulty( baseitems = normalbaseitems, @@ -128,7 +128,7 @@ difficulties = { timedohko=['Green Clock'] * 25, timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, triforcehunt=['Triforce Piece'] * 30, - retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, + universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=3, progressive_shield_limit=2, @@ -158,7 +158,7 @@ difficulties = { timedohko=['Green Clock'] * 20 + ['Red Clock'] * 5, timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, triforcehunt=['Triforce Piece'] * 30, - retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, + universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=2, progressive_shield_limit=1, @@ -425,21 +425,26 @@ def fill_prizes(world, attempts=15): raise FillError('Unable to place dungeon prizes') -def set_up_shops(world, player): - # TODO: move hard+ mode changes for sheilds here, utilizing the new shops +def set_up_shops(world, player: int): + # TODO: move hard+ mode changes for shields here, utilizing the new shops if world.retro[player]: rss = world.get_region('Red Shield Shop', player).shop if not rss.locked: rss.add_inventory(2, 'Single Arrow', 80) + rss.locked = True + + if world.keyshuffle[player] == "universal": for shop in world.random.sample([s for s in world.shops if s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player], 5): shop.locked = True - shop.add_inventory(0, 'Single Arrow', 80) + if world.retro[player]: + shop.add_inventory(0, 'Single Arrow', 80) + else: + shop.add_inventory(0, "Red Potion", 150) shop.add_inventory(1, 'Small Key (Universal)', 100) shop.add_inventory(2, 'Bombs (10)', 50) - rss.locked = True def get_pool_core(world, player: int): @@ -592,7 +597,8 @@ def get_pool_core(world, player: int): pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool] pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool] pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool] - pool.extend(diff.retro) + if world.keyshuffle[player] == "universal": + pool.extend(diff.universal_keys) if mode == 'standard': key_location = world.random.choice( ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', @@ -609,7 +615,6 @@ def make_custom_item_pool(world, player): timer = world.timer[player] goal = world.goal[player] mode = world.mode[player] - retro = world.retro[player] customitemarray = world.customitemarray[player] pool = [] @@ -726,7 +731,7 @@ def make_custom_item_pool(world, player): itemtotal = itemtotal + 1 if mode == 'standard': - if retro: + if world.keyshuffle == "universal": key_location = world.random.choice( ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) @@ -749,9 +754,10 @@ def make_custom_item_pool(world, player): pool.extend(['Magic Mirror'] * customitemarray[22]) pool.extend(['Moon Pearl'] * customitemarray[28]) - if retro: - itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode + if world.keyshuffle == "universal": + itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode if itemtotal < total_items_to_place: pool.extend(['Nothing'] * (total_items_to_place - itemtotal)) + logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}") return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon) diff --git a/Mystery.py b/Mystery.py index 92d8895b..9bbe90c8 100644 --- a/Mystery.py +++ b/Mystery.py @@ -278,7 +278,8 @@ def roll_settings(weights): ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items) ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items) - ret.keyshuffle = get_choice('smallkey_shuffle', weights, 's' in dungeon_items) + ret.keyshuffle = get_choice('smallkey_shuffle', weights, + 'universal' if 'u' in dungeon_items else 's' in dungeon_items) ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items) ret.accessibility = get_choice('accessibility', weights) @@ -309,10 +310,13 @@ def roll_settings(weights): ret.triforce_pieces_required = get_choice('triforce_pieces_required', weights, 20) ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90) - ret.mode = get_choice('world_state', weights) + ret.mode = get_choice('world_state', weights, None) # legacy support if ret.mode == 'retro': ret.mode = 'open' ret.retro = True + elif ret.mode is None: + ret.mode = get_choice("mode", weights) + ret.retro = get_choice("retro", weights) ret.hints = get_choice('hints', weights) diff --git a/Rom.py b/Rom.py index 47ce4a56..c14659db 100644 --- a/Rom.py +++ b/Rom.py @@ -1237,7 +1237,7 @@ def patch_rom(world, rom, player, team, enemized): write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal - rom.write_byte(0x180172, 0x01 if world.retro[player] else 0x00) # universal keys + rom.write_byte(0x180172, int(world.keyshuffle == "universal")) # universal keys rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost diff --git a/Rules.py b/Rules.py index 8fdf6864..d1f3c4fe 100644 --- a/Rules.py +++ b/Rules.py @@ -206,8 +206,9 @@ def global_rules(world, player): set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Sewers Door', player), - lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (world.retro[player] and world.mode[ - player] == 'standard')) # standard retro cannot access the shop + lambda state: state.has_key('Small Key (Hyrule Castle)', player) or ( + world.keyshuffle[player] == "universal" and world.mode[ + player] == 'standard')) # standard universal small keys cannot access the shop set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state.has_key('Small Key (Hyrule Castle)', player)) set_rule(world.get_entrance('Agahnim 1', player), @@ -896,7 +897,7 @@ def set_trock_key_rules(world, player): return 4 # If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential - if not can_reach_front and not world.keyshuffle[player] and not world.retro[player]: + if not can_reach_front and not world.keyshuffle[player]: # Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player) if not can_reach_big_chest: @@ -905,7 +906,8 @@ def set_trock_key_rules(world, player): if world.accessibility[player] == 'locations': if world.bigkeyshuffle[player] and can_reach_big_chest: # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first - for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']: + for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', + 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']: forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player) else: # A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works diff --git a/easy.yaml b/easy.yaml index 417b8497..cb189f47 100644 --- a/easy.yaml +++ b/easy.yaml @@ -38,6 +38,7 @@ compass_shuffle: # Shuffle compasses into the world and other dungeons, includin off: 1 smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds on: 0 + universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more off: 1 bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds on: 0 @@ -50,6 +51,8 @@ dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does n none: 1 # Shuffle none of the 4 mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing + ub: 0 # universal small keys and shuffled big keys + # you can add more combos of these letters here dungeon_counters: on: 0 # Always display amount of items checked in a dungeon pickup: 1 # Show when compass is picked up @@ -119,11 +122,13 @@ ganon_open: # Crystals required to hurt Ganon '6': 2 '7': 1 random: 0 -world_state: +mode: standard: 1 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary open: 1 # Begin the game from your choice of Link's House or the Sanctuary inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered - retro: 0 # Small keys are universal, you must buy a quiver, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave +retro: + on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave + off: 1 hints: 'on': 1 # Hint tiles sometimes give item location hints 'off': 0 # Hint tiles provide gameplay tips