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