diff options
Diffstat (limited to 'apworld/tracker.py')
| -rw-r--r-- | apworld/tracker.py | 146 |
1 files changed, 146 insertions, 0 deletions
| diff --git a/apworld/tracker.py b/apworld/tracker.py new file mode 100644 index 0000000..d473af4 --- /dev/null +++ b/apworld/tracker.py | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | from typing import TYPE_CHECKING, Iterator | ||
| 2 | |||
| 3 | from BaseClasses import MultiWorld, CollectionState, ItemClassification, Region, Entrance | ||
| 4 | from NetUtils import NetworkItem | ||
| 5 | from . import Lingo2World, Lingo2Item | ||
| 6 | from .regions import connect_ports_from_ut | ||
| 7 | from .options import Lingo2Options, ShuffleLetters | ||
| 8 | |||
| 9 | if TYPE_CHECKING: | ||
| 10 | from .context import Lingo2Manager | ||
| 11 | |||
| 12 | PLAYER_NUM = 1 | ||
| 13 | |||
| 14 | |||
| 15 | class Tracker: | ||
| 16 | manager: "Lingo2Manager" | ||
| 17 | |||
| 18 | multiworld: MultiWorld | ||
| 19 | world: Lingo2World | ||
| 20 | |||
| 21 | collected_items: dict[int, int] | ||
| 22 | checked_locations: set[int] | ||
| 23 | accessible_locations: set[int] | ||
| 24 | accessible_worldports: set[int] | ||
| 25 | goal_accessible: bool | ||
| 26 | |||
| 27 | state: CollectionState | ||
| 28 | |||
| 29 | def __init__(self, manager: "Lingo2Manager"): | ||
| 30 | self.manager = manager | ||
| 31 | self.collected_items = {} | ||
| 32 | self.checked_locations = set() | ||
| 33 | self.accessible_locations = set() | ||
| 34 | self.accessible_worldports = set() | ||
| 35 | self.goal_accessible = False | ||
| 36 | |||
| 37 | def setup_slot(self, slot_data): | ||
| 38 | Lingo2World.for_tracker = True | ||
| 39 | |||
| 40 | self.multiworld = MultiWorld(players=PLAYER_NUM) | ||
| 41 | self.world = Lingo2World(self.multiworld, PLAYER_NUM) | ||
| 42 | self.multiworld.worlds[1] = self.world | ||
| 43 | self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) | ||
| 44 | for k, t in Lingo2Options.type_hints.items()}) | ||
| 45 | |||
| 46 | self.world.generate_early() | ||
| 47 | self.world.create_regions() | ||
| 48 | |||
| 49 | if self.world.options.shuffle_worldports: | ||
| 50 | port_pairings = { | ||
| 51 | self.world.static_logic.port_id_by_ap_id[int(fp)]: self.world.static_logic.port_id_by_ap_id[int(tp)] | ||
| 52 | for fp, tp in slot_data["port_pairings"].items() | ||
| 53 | } | ||
| 54 | connect_ports_from_ut(port_pairings, self.world) | ||
| 55 | |||
| 56 | self.refresh_state() | ||
| 57 | |||
| 58 | def set_checked_locations(self, checked_locations: set[int]): | ||
| 59 | self.checked_locations = checked_locations.copy() | ||
| 60 | |||
| 61 | def set_collected_items(self, network_items: list[NetworkItem]): | ||
| 62 | self.collected_items = {} | ||
| 63 | |||
| 64 | for item in network_items: | ||
| 65 | self.collected_items[item.item] = self.collected_items.get(item.item, 0) + 1 | ||
| 66 | |||
| 67 | self.refresh_state() | ||
| 68 | |||
| 69 | def refresh_state(self): | ||
| 70 | self.state = CollectionState(self.multiworld) | ||
| 71 | |||
| 72 | for item_id, item_amount in self.collected_items.items(): | ||
| 73 | for i in range(item_amount): | ||
| 74 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), | ||
| 75 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) | ||
| 76 | |||
| 77 | for k, v in self.manager.keyboard.items(): | ||
| 78 | # Unless all level 1 letters are pre-unlocked, H1 I1 N1 and T1 act differently between the generator and | ||
| 79 | # game. The generator considers them to be unlocked, which means they are not included in logic | ||
| 80 | # requirements, and only one item/event is needed to unlock their level 2 forms. The game considers them to | ||
| 81 | # be vanilla, which means you still have to pick them up in the Starting Room in order for them to appear on | ||
| 82 | # your keyboard. This also means that whether or not you have the level 1 forms should be synced to the | ||
| 83 | # multiworld. The tracker specifically should collect one fewer item for these letters in this scenario. | ||
| 84 | tv = v | ||
| 85 | if k in "hint" and self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla, | ||
| 86 | ShuffleLetters.option_progressive]: | ||
| 87 | tv = max(0, v - 1) | ||
| 88 | |||
| 89 | if tv > 0: | ||
| 90 | for i in range(tv): | ||
| 91 | self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM), | ||
| 92 | prevent_sweep=True) | ||
| 93 | |||
| 94 | for port_id in self.manager.worldports: | ||
| 95 | self.state.collect(Lingo2Item(f"Worldport {port_id} Entered", ItemClassification.progression, None, | ||
| 96 | PLAYER_NUM), prevent_sweep=True) | ||
| 97 | |||
| 98 | self.state.sweep_for_advancements() | ||
| 99 | |||
| 100 | self.accessible_locations = set() | ||
| 101 | self.accessible_worldports = set() | ||
| 102 | self.goal_accessible = False | ||
| 103 | |||
| 104 | for region in self.state.reachable_regions[PLAYER_NUM]: | ||
| 105 | for location in region.locations: | ||
| 106 | if location.access_rule(self.state): | ||
| 107 | if location.address is not None: | ||
| 108 | if location.address not in self.checked_locations: | ||
| 109 | self.accessible_locations.add(location.address) | ||
| 110 | elif hasattr(location, "port_id"): | ||
| 111 | if location.port_id not in self.manager.worldports: | ||
| 112 | self.accessible_worldports.add(location.port_id) | ||
| 113 | elif hasattr(location, "goal") and location.goal: | ||
| 114 | if not self.manager.goaled: | ||
| 115 | self.goal_accessible = True | ||
| 116 | |||
| 117 | def get_path_to_location(self, location_id: int) -> list[str] | None: | ||
| 118 | location_name = self.world.location_id_to_name.get(location_id) | ||
| 119 | location = self.multiworld.get_location(location_name, PLAYER_NUM) | ||
| 120 | return self.get_logical_path(location.parent_region) | ||
| 121 | |||
| 122 | def get_path_to_port(self, port_id: int) -> list[str] | None: | ||
| 123 | port = self.world.static_logic.objects.ports[port_id] | ||
| 124 | region_name = self.world.static_logic.get_room_region_name(port.room_id) | ||
| 125 | region = self.multiworld.get_region(region_name, PLAYER_NUM) | ||
| 126 | return self.get_logical_path(region) | ||
| 127 | |||
| 128 | def get_path_to_goal(self): | ||
| 129 | room_id = self.world.player_logic.goal_room_id | ||
| 130 | region_name = self.world.static_logic.get_room_region_name(room_id) | ||
| 131 | region = self.multiworld.get_region(region_name, PLAYER_NUM) | ||
| 132 | return self.get_logical_path(region) | ||
| 133 | |||
| 134 | def get_logical_path(self, region: Region) -> list[str] | None: | ||
| 135 | if region not in self.state.path: | ||
| 136 | return None | ||
| 137 | |||
| 138 | def flist_to_iter(path_value) -> Iterator[str]: | ||
| 139 | while path_value: | ||
| 140 | region_or_entrance, path_value = path_value | ||
| 141 | yield region_or_entrance | ||
| 142 | |||
| 143 | reversed_path = self.state.path.get(region) | ||
| 144 | flat_path = reversed(list(map(str, flist_to_iter(reversed_path)))) | ||
| 145 | |||
| 146 | return list(flat_path)[1::2] | ||
