diff --git a/rs/ai/_example/handlers/event_handler.py b/rs/ai/_example/handlers/event_handler.py index 9ba9194a..cc5e94f1 100644 --- a/rs/ai/_example/handlers/event_handler.py +++ b/rs/ai/_example/handlers/event_handler.py @@ -1,7 +1,6 @@ -from typing import List - from presentation_config import presentation_mode, p_delay, p_delay_s, slow_events from rs.ai._example.config import CARD_REMOVAL_PRIORITY_LIST, DESIRED_CARDS_FOR_DECK +from rs.game.event import Event from rs.game.screen_type import ScreenType from rs.helper.logger import log_missing_event from rs.machine.command import Command @@ -22,25 +21,26 @@ def handle(self, state: GameState) -> HandlerAction: return HandlerAction(commands=[p_delay, "choose 0", "wait 30"]) return HandlerAction(commands=["choose 0", "wait 30"]) - if self.find_event_choice(state): # Otherwise figure out what to do below! + if find_event_choice(state): # Otherwise figure out what to do below! if presentation_mode or slow_events: - return HandlerAction(commands=[p_delay, self.find_event_choice(state), p_delay_s]) - return HandlerAction(commands=[self.find_event_choice(state), "wait 30"]) + return HandlerAction(commands=[p_delay, find_event_choice(state), p_delay_s]) + return HandlerAction(commands=[find_event_choice(state), "wait 30"]) + - def find_event_choice(self, state: GameState) -> str: - hp_per = state.get_player_health_percentage() * 100 - event_name = state.game_state()['screen_state']['event_name'] +def find_event_choice(state: GameState) -> str: + hp_per = state.get_player_health_percentage() * 100 + event = state.get_event() - # ACT 1 + match event: - if event_name == "Big Fish": + case Event.BIG_FISH: if hp_per <= 30: return "choose 0" # heal if state.get_relic_counter("Omamori") >= 1: return "choose 2" # relic and curse return "choose 1" # max health up - if event_name == "The Cleric": + case Event.THE_CLERIC: if hp_per <= 65 and 'heal' in state.get_choice_list(): return "choose heal" if 'purify' in state.get_choice_list(): @@ -49,10 +49,10 @@ def find_event_choice(self, state: GameState) -> str: return "choose leave" # Heal not worth the money and can't purify apparently return "choose 0" - if event_name == "Dead Adventurer": + case Event.DEAD_ADVENTURER: return "choose 1" # Escape. Could do: Add logic for sometimes taking the fight. - if event_name == "Golden Idol": + case Event.GOLDEN_IDOL: if state.has_relic("Ectoplasm"): return "choose 1" # Leave! if len(state.get_choice_list()) == 2: @@ -64,141 +64,144 @@ def find_event_choice(self, state: GameState) -> str: return "choose 1" # 25% (35%) damage return "choose 2" # max hp loss - if event_name == "Mushrooms": + case Event.HYPNOTIZING_MUSHROOMS: if hp_per >= 40: return "choose 0" # Get 'em! return "choose 1" # Take the heal and curse - if event_name == "Living Wall": + case Event.LIVING_WALL: return "choose 2" # Upgrade - if event_name == "Scrap Ooze": + case Event.SCRAP_OOZE: return "choose 0" # Yolo. We'll probably get it after a few tries? If not, we don't deserve to live!! - if event_name == "Shining Light": + case Event.SHINING_LIGHT: if hp_per >= 70: return "choose 0" # Take the 2 random upgrades. return "choose 1" # Leave. - if event_name == "The Ssssserpent": + case Event.THE_SSSSSERPENT: if state.get_relic_counter("Omamori") >= 1 and not state.has_relic("Ectoplasm"): return "choose 0" # Money in exchange for a curse return "choose 1" # Leave - if event_name == "World of Goop": + case Event.WORLD_OF_GOOP: if hp_per >= 80 and not state.has_relic("Ectoplasm"): return "choose 0" # Take the money and lose a little HP. return "choose 1" # Leave - if event_name == "Wing Statue": + case Event.WING_STATUE: if hp_per >= 70: return "choose 0" # Purge at cost of 7 HP return "choose 1" # Money or leave # ACT 1, 2 - if event_name == "Face Trader": + case Event.FACE_TRADER: if hp_per >= 75 and not state.has_relic("Ectoplasm"): return "choose 0" return "choose 2" # Leave. # ACT 1, 2, 3 - if event_name == "A Note For Yourself": + case Event.A_NOTE_FOR_YOURSELF: return "choose 1" # Ignore. - if event_name == "Bonfire Spirits": + case Event.BONFIRE_SPIRITS: return "choose 0" # Purge - if event_name == "The Divine Fountain": + case Event.THE_DIVINE_FOUNTAIN: return "choose 0" # Remove curses! - if event_name == "Duplicator": + case Event.DUPLICATOR: return "choose 1" # Needs some duplication logic, would be better, but for now leave. - if event_name == "Golden Shrine": + case Event.GOLDEN_SHRINE: if state.get_relic_counter("Omamori") >= 1 and not state.has_relic("Ectoplasm"): return "choose 1" # More free money! return "choose 0" # Free money - if event_name == "Lab": + case Event.LAB: return "choose 0" # Free potions - if event_name == "Match and Keep": + case Event.MATCH_AND_KEEP: return "choose 0" # Just keep clicking - if event_name == "Ominous Forge": + case Event.OMINOUS_FORGE: if state.get_relic_counter("Omamori") >= 1: return "choose 1" # Warped tongs! if state.floor() >= 30: return "choose 0" # Might not be able to reasonably get rid of the curse anymore return "choose 1" # I love the Warped Tongs relic. - if event_name == "Purifier": + case Event.PURIFIER: return "choose 0" # Purge - if event_name == "Transmogrifier": + case Event.TRANSMOGRIFIER: return "choose 1" # Ignore the transform since we don't know all cards (depending on character) - if event_name == "Upgrade Shrine": + case Event.UPGRADE_SHRINE: return "choose 0" # Free upgrade - # if event_name == "We Meet Again!" + case Event.WE_MEET_AGAIN: + return "choose 0" # Todo - if event_name == "The Woman in Blue": + case Event.THE_WOMAN_IN_BLUE: return "choose 0" # Grab 1 potion, or if we don't have enough money, leave. # ACT 2 - if event_name == "Ancient Writing": + case Event.ANCIENT_WRITING: return "choose 1" # Upgrade all strikes and defends always because we currently can't tell the difference between card selection and purging in a grid event. - if event_name == "Augmenter": + case Event.AUGMENTER: return "choose 2" # Take the Mutagenic Strength relic. - # if event_name == "The Colosseum" + case Event.THE_COLOSSEUM: + return "choose 0" # todo - if event_name == "Council of Ghosts": + case Event.COUNCIL_OF_GHOSTS: if state.has_relic("Snecko Eye") or state.deck.contains_cards(["Bite"]): # Not amazing combos: return "choose refuse" return "choose accept" # Become a spooky ghost! - if event_name == "Cursed Tome": + case Event.CURSED_TOME: return "choose 1" # Leave, we don't currently make good use of the possible relics. - # if event_name == "Forgotten Altar" + case Event.FORGOTTEN_ALTAR: + return "choose 0" # todo - if event_name == "The Joust": + case Event.THE_JOUST: return "choose 0" # Be conservative - if event_name == "Knowing Skull": + case Event.KNOWING_SKULL: return "choose 3" # Leave - if event_name == "The Library": + case Event.THE_LIBRARY: return "choose sleep" # Heal, because we currently can't tell the difference between card selection and purging in a grid event. - if event_name == "Masked Bandits": + case Event.MASKED_BANDITS: if hp_per >= 65: return "choose 1" # Get 'em! return "choose 0" # Give up all money and leave. - if event_name == "The Mausoleum": + case Event.THE_MAUSOLEUM: if state.get_relic_counter("Omamori") >= 1: return "choose 0" return "choose 1" # Leave, we don't like curses. - if event_name == "The Nest": + case Event.THE_NEST: if hp_per >= 50: return "choose 1" # Ritual Dagger and a little damage. return "choose 0" # Free money - if event_name == "N'loth": + case Event.NLOTH: return "choose 2" # Leave, hard to statically make a good choice here. - if event_name == "Old Beggar": + case Event.OLD_BEGGAR: return "choose 0" # Cheap purge. - if event_name == "Pleading Vagrant": + case Event.PLEADING_VAGRANT: if state.get_relic_counter("Omamori") >= 1: return "choose rob" # Get curse and relic elif "offer gold" in state.get_choice_list(): @@ -206,7 +209,7 @@ def find_event_choice(self, state: GameState) -> str: else: return "choose leave" - if event_name == "Vampires(?)": + case Event.VAMPIRES: if state.deck.contains_cards(["Apparition"]): return "choose refuse" if state.has_relic("Strike Dummy"): @@ -220,12 +223,12 @@ def find_event_choice(self, state: GameState) -> str: # Act 2, 3 - if event_name == "Designer In-Spire": + case Event.DESIGNER_IN_SPIRE: return "choose 0" # It'll do reasonable things like upgrading and removing. # Act 3 - if event_name == "Falling": + case Event.FALLING: options = state.get_falling_event_options() # check for stuff we want to purge @@ -250,36 +253,37 @@ def find_event_choice(self, state: GameState) -> str: if card == least_desired: return "choose " + str(idx) - if event_name == "Mind Bloom": + case Event.MIND_BLOOM: return "choose 0" # Fight an Act 1 boss for a relic. - if event_name == "The Moai Head": + case Event.MIND_BLOOM: return "choose 1" # Get a bunch of money or leave - if event_name == "Mysterious Sphere": + case Event.MYSTERIOUS_SPHERE: if hp_per >= 70: return "choose 0" # Get 'em! else: return "choose 1" # Leave - if event_name == "Secret Portal": + case Event.SECRET_PORTAL: return "choose 1" # Nope - we want the rest of Act 3 to not screw up our stats. - if event_name == "Sensory Stone": + case Event.SENSORY_STONE: return "choose 0" # Get a free colorless obtain - if event_name == "Tomb of Lord Red Mask": + case Event.TOMB_OF_LORD_RED_MASK: if state.has_relic("Red Mask") or state.game_state()['gold'] <= 130: return "choose 0" # Either free money or cheap. else: return "choose 1" # Leave - if event_name == "Winding Halls": + case Event.WINDING_HALLS: if state.get_relic_counter("Omamori") >= 1 and hp_per < 75: return "choose 1" # Take the curse and heal if hp_per <= 10: return "choose 1" # Take the curse and heal return "choose 2" # Lose Max HP - log_missing_event(event_name) - return "choose 0" \ No newline at end of file + case _: + log_missing_event(str(event)) + return "choose 0" diff --git a/rs/game/event.py b/rs/game/event.py new file mode 100644 index 00000000..761da8b9 --- /dev/null +++ b/rs/game/event.py @@ -0,0 +1,58 @@ +from enum import Enum + + +class Event(Enum): + UNKNOWN = "unknown" + + ANCIENT_WRITING = "Ancient Writing" + AUGMENTER = "Augmenter" + A_NOTE_FOR_YOURSELF = "A Note For Yourself" + BIG_FISH = "Big Fish" + BONFIRE_SPIRITS = "Bonfire Spirits" + COUNCIL_OF_GHOSTS = "Council of Ghosts" + CURSED_TOME = "Cursed Tome" + DEAD_ADVENTURER = "Dead Adventurer" + DESIGNER_IN_SPIRE = "Designer In-Spire" + DUPLICATOR = "Duplicator" + FACE_TRADER = "Face Trader" + FALLING = "Falling" + FORGOTTEN_ALTAR = "Forgotten Altar" + GOLDEN_IDOL = "Golden Idol" + GOLDEN_SHRINE = "Golden Shrine" + HYPNOTIZING_MUSHROOMS = "Mushrooms" # Adding 'hypnotizing' because that word is included on the wiki + KNOWING_SKULL = "Knowing Skull" + LAB = "Lab" + LIVING_WALL = "Living Wall" + MASKED_BANDITS = "Masked Bandits" + MATCH_AND_KEEP = "Match and Keep" + MIND_BLOOM = "Mind Bloom" + MYSTERIOUS_SPHERE = "Mysterious Sphere" + NLOTH = "N'loth" + OLD_BEGGAR = "Old Beggar" + OMINOUS_FORGE = "Ominous Forge" + PLEADING_VAGRANT = "Pleading Vagrant" + PURIFIER = "Purifier" + SCRAP_OOZE = "Scrap Ooze" + SECRET_PORTAL = "Secret Portal" + SENSORY_STONE = "Sensory Stone" + SHINING_LIGHT = "Shining Light" + THE_CLERIC = "The Cleric" + THE_COLOSSEUM = "The Colosseum" + THE_DIVINE_FOUNTAIN = "The Divine Fountain" + THE_JOUST = "The Joust" + THE_LIBRARY = "The Library" + THE_MAUSOLEUM = "The Mausoleum" + THE_MOAI_HEAD = "The Moai Head" + THE_NEST = "The Nest" + THE_SSSSSERPENT = "The Ssssserpent" + THE_WOMAN_IN_BLUE = "The Woman in Blue" + TOMB_OF_LORD_RED_MASK = "Tomb of Lord Red Mask" + TRANSMOGRIFIER = "Transmogrifier" + UPGRADE_SHRINE = "Upgrade Shrine" + VAMPIRES = "Vampires(?)" + WE_MEET_AGAIN = "We Meet Again!" + WHEEL_OF_CHANGE = "Wheel of Change" + WINDING_HALLS = "Winding Halls" + WING_STATUE = "Wing Statue" + WORLD_OF_GOOP = "World of Goop" + diff --git a/rs/machine/state.py b/rs/machine/state.py index bb70e241..7bdd0a7d 100644 --- a/rs/machine/state.py +++ b/rs/machine/state.py @@ -3,6 +3,7 @@ from rs.calculator.interfaces.memory_items import MemoryItem from rs.game.deck import Deck +from rs.game.event import Event from rs.machine.command import Command from rs.machine.orb import Orb from rs.machine.the_bots_memory_book import TheBotsMemoryBook @@ -200,3 +201,11 @@ def extract_card_from_text(text): for idx, choice in enumerate(options): options[idx] = choice.replace("+", "") return options + + def get_event(self) -> Event: + event_name = self.game_state()['screen_state']['event_name'] + possible_events = set(item.value for item in Event) + + if event_name not in possible_events: + return event_name + return Event(event_name) \ No newline at end of file diff --git a/tests/game_state_converter/test_game_state_converter.py b/tests/game_state_converter/test_game_state_converter.py index 6309c4df..45e6ee92 100644 --- a/tests/game_state_converter/test_game_state_converter.py +++ b/tests/game_state_converter/test_game_state_converter.py @@ -1,5 +1,6 @@ import unittest +from rs.game.event import Event from test_helpers.resources import load_resource_state @@ -64,3 +65,10 @@ def test_get_falling_event_2_options(self): state = load_resource_state("event/event_falling_2_options.json") self.assertEqual(['tranquility', 'crush joints'], state.get_falling_event_options()) + def test_get_event(self): + state = load_resource_state("event/event_falling.json") + self.assertEqual(Event.FALLING, state.get_event()) + + def test_get_unknown_event(self): + state = load_resource_state("event/event_unknown.json") + self.assertEqual("Garble garble", state.get_event()) diff --git a/tests/res/event/event_unknown.json b/tests/res/event/event_unknown.json new file mode 100644 index 00000000..5d9a4206 --- /dev/null +++ b/tests/res/event/event_unknown.json @@ -0,0 +1,1208 @@ +{ + "available_commands": [ + "choose", + "potion", + "key", + "click", + "wait", + "state" + ], + "ready_for_command": true, + "in_game": true, + "game_state": { + "choice_list": [ + "land", + "channel", + "strike" + ], + "screen_type": "EVENT", + "screen_state": { + "event_id": "Garble garble", + "body_text": "While in free fall you consider your options: Land safely with your greatest techniques. Channel a Power to survive the fall. Strike at the wall to hang on to it.", + "options": [ + { + "choice_index": 0, + "disabled": false, + "text": "[Land] Lose Tranquility+", + "label": "Land" + }, + { + "choice_index": 1, + "disabled": false, + "text": "[Channel] Lose Strike", + "label": "Channel" + }, + { + "choice_index": 2, + "disabled": false, + "text": "[Strike] Lose Halt+", + "label": "Strike" + } + ], + "event_name": "Garble garble" + }, + "seed": 1020360029503034068, + "deck": [ + { + "exhausts": false, + "cost": 1, + "name": "Strike", + "id": "Strike_P", + "type": "ATTACK", + "ethereal": false, + "uuid": "60c3998f-c4bd-4e8a-865f-e4139ad3407b", + "upgrades": 0, + "rarity": "BASIC", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Strike", + "id": "Strike_P", + "type": "ATTACK", + "ethereal": false, + "uuid": "ff3e114b-b9ff-4256-8b35-e25f9c10a753", + "upgrades": 0, + "rarity": "BASIC", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Strike+", + "id": "Strike_P", + "type": "ATTACK", + "ethereal": false, + "uuid": "7aece7e1-38a5-4e08-a2a9-8e3a31c216d6", + "upgrades": 1, + "rarity": "BASIC", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Strike", + "id": "Strike_P", + "type": "ATTACK", + "ethereal": false, + "uuid": "99425340-1ec8-4606-a1af-91e3d8fe589e", + "upgrades": 0, + "rarity": "BASIC", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Defend+", + "id": "Defend_P", + "type": "SKILL", + "ethereal": false, + "uuid": "624db820-f5fc-4ea6-8d02-4f2672018157", + "upgrades": 1, + "rarity": "BASIC", + "has_target": false + }, + { + "exhausts": false, + "cost": 1, + "name": "Eruption+", + "id": "Eruption", + "type": "ATTACK", + "ethereal": false, + "uuid": "5dd1ae31-46a0-458c-aa03-2e4812b1ae10", + "upgrades": 1, + "rarity": "BASIC", + "has_target": true + }, + { + "exhausts": false, + "cost": 2, + "name": "Vigilance+", + "id": "Vigilance", + "type": "SKILL", + "ethereal": false, + "uuid": "b8bd6375-53b7-497e-ace1-ec7f9510cffc", + "upgrades": 1, + "rarity": "BASIC", + "has_target": false + }, + { + "exhausts": false, + "cost": 1, + "name": "Cut Through Fate", + "id": "CutThroughFate", + "type": "ATTACK", + "ethereal": false, + "uuid": "0afdd8df-02f7-4b93-9775-95136de0365d", + "upgrades": 0, + "rarity": "COMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 0, + "name": "Flurry of Blows+", + "id": "FlurryOfBlows", + "type": "ATTACK", + "ethereal": false, + "uuid": "0f347682-c7c0-4689-b0a4-8bdbe10a6913", + "upgrades": 1, + "rarity": "COMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Empty Body", + "id": "EmptyBody", + "type": "SKILL", + "ethereal": false, + "uuid": "991681a8-9584-4676-9657-e13a9176fdea", + "upgrades": 0, + "rarity": "COMMON", + "has_target": false + }, + { + "exhausts": true, + "cost": 1, + "name": "Talk to the Hand+", + "id": "TalkToTheHand", + "type": "ATTACK", + "ethereal": false, + "uuid": "1fd35c2c-9b2c-455e-bf35-a6315be85144", + "upgrades": 1, + "rarity": "UNCOMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Crush Joints+", + "id": "CrushJoints", + "type": "ATTACK", + "ethereal": false, + "uuid": "8001c1d5-4e05-4e1b-be5a-110ed4557b37", + "upgrades": 1, + "rarity": "COMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 2, + "name": "Spirit Shield", + "id": "SpiritShield", + "type": "SKILL", + "ethereal": false, + "uuid": "a23467d3-1e5f-46e4-99c6-b200678b2627", + "upgrades": 0, + "rarity": "RARE", + "has_target": false + }, + { + "exhausts": false, + "cost": 0, + "name": "Flurry of Blows+", + "id": "FlurryOfBlows", + "type": "ATTACK", + "ethereal": false, + "uuid": "6de2ad08-1f8b-4753-ab79-3601c81a94d3", + "upgrades": 1, + "rarity": "COMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 2, + "name": "Wheel Kick", + "id": "WheelKick", + "type": "ATTACK", + "ethereal": false, + "uuid": "8d18c0eb-b13d-4b42-a637-e278eb0e2352", + "upgrades": 0, + "rarity": "UNCOMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Empty Fist", + "id": "EmptyFist", + "type": "ATTACK", + "ethereal": false, + "uuid": "58558629-f550-48f4-adfa-d577d4b18535", + "upgrades": 0, + "rarity": "COMMON", + "has_target": true + }, + { + "exhausts": false, + "cost": 1, + "name": "Battle Hymn+", + "id": "BattleHymn", + "type": "POWER", + "ethereal": false, + "uuid": "d2f9edeb-a43a-4c6f-9b44-3f0e7d6b9aa4", + "upgrades": 1, + "rarity": "UNCOMMON", + "has_target": false + }, + { + "exhausts": false, + "cost": 1, + "name": "Empty Body", + "id": "EmptyBody", + "type": "SKILL", + "ethereal": false, + "uuid": "c9482d64-947d-425b-9569-5998b4b59e9f", + "upgrades": 0, + "rarity": "COMMON", + "has_target": false + }, + { + "exhausts": false, + "cost": 3, + "name": "Ragnarok", + "id": "Ragnarok", + "type": "ATTACK", + "ethereal": false, + "uuid": "4aae9bc1-d43a-40e5-8a3f-d9731377e0b1", + "upgrades": 0, + "rarity": "RARE", + "has_target": false + }, + { + "exhausts": true, + "cost": 0, + "name": "Tranquility+", + "id": "ClearTheMind", + "type": "SKILL", + "ethereal": false, + "uuid": "2dc9361a-399c-4fa4-b51f-cf1c28f2effd", + "upgrades": 1, + "rarity": "COMMON", + "has_target": false + }, + { + "exhausts": true, + "cost": 0, + "name": "Crescendo+", + "id": "Crescendo", + "type": "SKILL", + "ethereal": false, + "uuid": "97c53e09-f758-4904-818c-28aa251c97fd", + "upgrades": 1, + "rarity": "COMMON", + "has_target": false + } + ], + "relics": [ + { + "name": "Pure Water", + "id": "PureWater", + "counter": -1 + }, + { + "name": "Neow\u0027s Lament", + "id": "NeowsBlessing", + "counter": -2 + }, + { + "name": "Teardrop Locket", + "id": "TeardropLocket", + "counter": -1 + }, + { + "name": "Bag of Preparation", + "id": "Bag of Preparation", + "counter": -1 + }, + { + "name": "Bottled Lightning", + "id": "Bottled Lightning", + "counter": -1 + }, + { + "name": "Philosopher\u0027s Stone", + "id": "Philosopher\u0027s Stone", + "counter": -1 + }, + { + "name": "Fossilized Helix", + "id": "FossilizedHelix", + "counter": -1 + }, + { + "name": "Mutagenic Strength", + "id": "MutagenicStrength", + "counter": -1 + }, + { + "name": "Frozen Egg", + "id": "Frozen Egg 2", + "counter": -1 + }, + { + "name": "Violet Lotus", + "id": "VioletLotus", + "counter": -1 + }, + { + "name": "Shovel", + "id": "Shovel", + "counter": -1 + }, + { + "name": "Ginger", + "id": "Ginger", + "counter": -1 + } + ], + "max_hp": 77, + "act_boss": "Donu and Deca", + "gold": 230, + "action_phase": "WAITING_ON_USER", + "act": 3, + "screen_name": "NONE", + "room_phase": "EVENT", + "is_screen_up": false, + "potions": [ + { + "requires_target": false, + "can_use": false, + "can_discard": true, + "name": "Distilled Chaos", + "id": "DistilledChaos" + }, + { + "requires_target": false, + "can_use": false, + "can_discard": true, + "name": "Liquid Bronze", + "id": "LiquidBronze" + }, + { + "requires_target": false, + "can_use": false, + "can_discard": true, + "name": "Distilled Chaos", + "id": "DistilledChaos" + } + ], + "current_hp": 62, + "floor": 41, + "ascension_level": 0, + "class": "WATCHER", + "map": [ + { + "symbol": "M", + "children": [ + { + "x": 1, + "y": 1 + } + ], + "x": 0, + "y": 0, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 1 + } + ], + "x": 2, + "y": 0, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 4, + "y": 1 + } + ], + "x": 3, + "y": 0, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 1 + } + ], + "x": 4, + "y": 0, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 1 + } + ], + "x": 5, + "y": 0, + "parents": [] + }, + { + "symbol": "$", + "children": [ + { + "x": 0, + "y": 2 + } + ], + "x": 1, + "y": 1, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 3, + "y": 2 + }, + { + "x": 4, + "y": 2 + } + ], + "x": 3, + "y": 1, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 4, + "y": 2 + } + ], + "x": 4, + "y": 1, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 2 + } + ], + "x": 5, + "y": 1, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 2 + } + ], + "x": 6, + "y": 1, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 1, + "y": 3 + } + ], + "x": 0, + "y": 2, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 3 + } + ], + "x": 3, + "y": 2, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 4, + "y": 3 + }, + { + "x": 5, + "y": 3 + } + ], + "x": 4, + "y": 2, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 3 + } + ], + "x": 5, + "y": 2, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 2, + "y": 4 + } + ], + "x": 1, + "y": 3, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 4, + "y": 4 + } + ], + "x": 3, + "y": 3, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 4, + "y": 4 + } + ], + "x": 4, + "y": 3, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 4, + "y": 4 + } + ], + "x": 5, + "y": 3, + "parents": [] + }, + { + "symbol": "$", + "children": [ + { + "x": 6, + "y": 4 + } + ], + "x": 6, + "y": 3, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 3, + "y": 5 + } + ], + "x": 2, + "y": 4, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 5 + }, + { + "x": 4, + "y": 5 + }, + { + "x": 5, + "y": 5 + } + ], + "x": 4, + "y": 4, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 5 + } + ], + "x": 6, + "y": 4, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 2, + "y": 6 + }, + { + "x": 3, + "y": 6 + } + ], + "x": 3, + "y": 5, + "parents": [] + }, + { + "symbol": "E", + "children": [ + { + "x": 4, + "y": 6 + } + ], + "x": 4, + "y": 5, + "parents": [] + }, + { + "symbol": "$", + "children": [ + { + "x": 5, + "y": 6 + } + ], + "x": 5, + "y": 5, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 5, + "y": 6 + }, + { + "x": 6, + "y": 6 + } + ], + "x": 6, + "y": 5, + "parents": [] + }, + { + "symbol": "E", + "children": [ + { + "x": 3, + "y": 7 + } + ], + "x": 2, + "y": 6, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 4, + "y": 7 + } + ], + "x": 3, + "y": 6, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 4, + "y": 7 + } + ], + "x": 4, + "y": 6, + "parents": [] + }, + { + "symbol": "E", + "children": [ + { + "x": 5, + "y": 7 + } + ], + "x": 5, + "y": 6, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 6, + "y": 7 + } + ], + "x": 6, + "y": 6, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 3, + "y": 8 + } + ], + "x": 3, + "y": 7, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 8 + }, + { + "x": 4, + "y": 8 + } + ], + "x": 4, + "y": 7, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 4, + "y": 8 + }, + { + "x": 6, + "y": 8 + } + ], + "x": 5, + "y": 7, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 8 + } + ], + "x": 6, + "y": 7, + "parents": [] + }, + { + "symbol": "T", + "children": [ + { + "x": 2, + "y": 9 + }, + { + "x": 3, + "y": 9 + } + ], + "x": 3, + "y": 8, + "parents": [] + }, + { + "symbol": "T", + "children": [ + { + "x": 3, + "y": 9 + }, + { + "x": 4, + "y": 9 + } + ], + "x": 4, + "y": 8, + "parents": [] + }, + { + "symbol": "T", + "children": [ + { + "x": 5, + "y": 9 + }, + { + "x": 6, + "y": 9 + } + ], + "x": 6, + "y": 8, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 1, + "y": 10 + } + ], + "x": 2, + "y": 9, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 3, + "y": 10 + } + ], + "x": 3, + "y": 9, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 10 + } + ], + "x": 4, + "y": 9, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 5, + "y": 10 + } + ], + "x": 5, + "y": 9, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 10 + } + ], + "x": 6, + "y": 9, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 2, + "y": 11 + } + ], + "x": 1, + "y": 10, + "parents": [] + }, + { + "symbol": "E", + "children": [ + { + "x": 2, + "y": 11 + }, + { + "x": 4, + "y": 11 + } + ], + "x": 3, + "y": 10, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 6, + "y": 11 + } + ], + "x": 5, + "y": 10, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 6, + "y": 11 + } + ], + "x": 6, + "y": 10, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 1, + "y": 12 + }, + { + "x": 3, + "y": 12 + } + ], + "x": 2, + "y": 11, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 4, + "y": 12 + }, + { + "x": 5, + "y": 12 + } + ], + "x": 4, + "y": 11, + "parents": [] + }, + { + "symbol": "E", + "children": [ + { + "x": 5, + "y": 12 + } + ], + "x": 6, + "y": 11, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 1, + "y": 13 + } + ], + "x": 1, + "y": 12, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 3, + "y": 13 + } + ], + "x": 3, + "y": 12, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 13 + } + ], + "x": 4, + "y": 12, + "parents": [] + }, + { + "symbol": "?", + "children": [ + { + "x": 5, + "y": 13 + }, + { + "x": 6, + "y": 13 + } + ], + "x": 5, + "y": 12, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 0, + "y": 14 + } + ], + "x": 1, + "y": 13, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 2, + "y": 14 + } + ], + "x": 3, + "y": 13, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 14 + } + ], + "x": 5, + "y": 13, + "parents": [] + }, + { + "symbol": "M", + "children": [ + { + "x": 5, + "y": 14 + }, + { + "x": 6, + "y": 14 + } + ], + "x": 6, + "y": 13, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 3, + "y": 16 + } + ], + "x": 0, + "y": 14, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 3, + "y": 16 + } + ], + "x": 2, + "y": 14, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 3, + "y": 16 + } + ], + "x": 5, + "y": 14, + "parents": [] + }, + { + "symbol": "R", + "children": [ + { + "x": 3, + "y": 16 + } + ], + "x": 6, + "y": 14, + "parents": [] + } + ], + "room_type": "EventRoom" + } +}