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