about summary refs log tree commit diff stats
path: root/apworld/tracker.py
blob: c65317c1fad5ed4756362f2d9153946b74b8cd7e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from typing import TYPE_CHECKING, Iterator

from BaseClasses import MultiWorld, CollectionState, ItemClassification, Region, Entrance
from NetUtils import NetworkItem
from . import Lingo2World, Lingo2Item
from .regions import connect_ports_from_ut
from .options import Lingo2Options, ShuffleLetters

if TYPE_CHECKING:
    from .context import Lingo2Manager

PLAYER_NUM = 1


class Tracker:
    manager: "Lingo2Manager"

    multiworld: MultiWorld
    world: Lingo2World

    collected_items: dict[int, int]
    checked_locations: set[int]
    accessible_locations: set[int]
    accessible_worldports: set[int]
    goal_accessible: bool

    state: CollectionState

    def __init__(self, manager: "Lingo2Manager"):
        self.manager = manager
        self.collected_items = {}
        self.checked_locations = set()
        self.accessible_locations = set()
        self.accessible_worldports = set()
        self.goal_accessible = False

    def setup_slot(self, slot_data):
        Lingo2World.for_tracker = True

        self.multiworld = MultiWorld(players=PLAYER_NUM)
        self.world = Lingo2World(self.multiworld, PLAYER_NUM)
        self.multiworld.worlds[1] = self.world
        self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default))
                                              for k, t in Lingo2Options.type_hints.items()})

        self.world.generate_early()
        self.world.create_regions()

        if self.world.options.shuffle_worldports:
            port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()}
            connect_ports_from_ut(port_pairings, self.world)

        self.refresh_state()

    def set_checked_locations(self, checked_locations: set[int]):
        self.checked_locations = checked_locations.copy()

    def set_collected_items(self, network_items: list[NetworkItem]):
        self.collected_items = {}

        for item in network_items:
            self.collected_items[item.item] = self.collected_items.get(item.item, 0) + 1

        self.refresh_state()

    def refresh_state(self):
        self.state = CollectionState(self.multiworld)

        for item_id, item_amount in self.collected_items.items():
            for i in range(item_amount):
                self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id),
                                              ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True)

        for k, v in self.manager.keyboard.items():
            # Unless all level 1 letters are pre-unlocked, H1 I1 N1 and T1 act differently between the generator and
            # game. The generator considers them to be unlocked, which means they are not included in logic
            # requirements, and only one item/event is needed to unlock their level 2 forms. The game considers them to
            # be vanilla, which means you still have to pick them up in the Starting Room in order for them to appear on
            # your keyboard. This also means that whether or not you have the level 1 forms should be synced to the
            # multiworld. The tracker specifically should collect one fewer item for these letters in this scenario.
            tv = v
            if k in "hint" and self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla,
                                                                      ShuffleLetters.option_progressive]:
                tv = max(0, v - 1)

            if tv > 0:
                for i in range(tv):
                    self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM),
                                       prevent_sweep=True)

        for port_id in self.manager.worldports:
            self.state.collect(Lingo2Item(f"Worldport {port_id} Entered", ItemClassification.progression, None,
                                          PLAYER_NUM), prevent_sweep=True)

        self.state.sweep_for_advancements()

        self.accessible_locations = set()
        self.accessible_worldports = set()
        self.goal_accessible = False

        for region in self.state.reachable_regions[PLAYER_NUM]:
            for location in region.locations:
                if location.access_rule(self.state):
                    if location.address is not None:
                        if location.address not in self.checked_locations:
                            self.accessible_locations.add(location.address)
                    elif hasattr(location, "port_id"):
                        if location.port_id not in self.manager.worldports:
                            self.accessible_worldports.add(location.port_id)
                    elif hasattr(location, "goal") and location.goal:
                        if not self.manager.goaled:
                            self.goal_accessible = True

    def get_path_to_location(self, location_id: int) -> list[str] | None:
        location_name = self.world.location_id_to_name.get(location_id)
        location = self.multiworld.get_location(location_name, PLAYER_NUM)
        return self.get_logical_path(location.parent_region)

    def get_path_to_port(self, port_id: int) -> list[str] | None:
        port = self.world.static_logic.objects.ports[port_id]
        region_name = self.world.static_logic.get_room_region_name(port.room_id)
        region = self.multiworld.get_region(region_name, PLAYER_NUM)
        return self.get_logical_path(region)

    def get_path_to_goal(self):
        room_id = self.world.player_logic.goal_room_id
        region_name = self.world.static_logic.get_room_region_name(room_id)
        region = self.multiworld.get_region(region_name, PLAYER_NUM)
        return self.get_logical_path(region)

    def get_logical_path(self, region: Region) -> list[str] | None:
        if region not in self.state.path:
            return None

        def flist_to_iter(path_value) -> Iterator[str]:
            while path_value:
                region_or_entrance, path_value = path_value
                yield region_or_entrance

        reversed_path = self.state.path.get(region)
        flat_path = reversed(list(map(str, flist_to_iter(reversed_path))))

        return list(flat_path)[1::2]