about summary refs log tree commit diff stats
path: root/apworld/rules.py
blob: a84cfbba337aebf3e5bd073dce341aa4ce4a047e (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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
from collections.abc import Callable
from enum import IntFlag, auto
from typing import TYPE_CHECKING

from BaseClasses import CollectionState, Region, MultiWorld, Entrance
from .player_logic import AccessRequirements
from ..AutoWorld import LogicMixin

if TYPE_CHECKING:
    from . import Lingo2World


class ControlCenterColor(IntFlag):
    RED = auto()
    BLUE = auto()
    ORANGE = auto()
    MAGENTA = auto()
    PURPLE = auto()
    GREEN = auto()
    BROWN = auto()
    WHITE = auto()

    NONE = 0
    ALL = RED + BLUE + ORANGE + MAGENTA + PURPLE + GREEN + BROWN + WHITE


CONTROL_CENTER_COLOR_NAMES: dict[str, ControlCenterColor] = {
    "red": ControlCenterColor.RED,
    "blue": ControlCenterColor.BLUE,
    "orange": ControlCenterColor.ORANGE,
    "magenta": ControlCenterColor.MAGENTA,
    "purple": ControlCenterColor.PURPLE,
    "green": ControlCenterColor.GREEN,
    "brown": ControlCenterColor.BROWN,
    "white": ControlCenterColor.WHITE,
}


class Lingo2Entrance(Entrance):
    world: "Lingo2World"
    reqs: AccessRequirements | None
    required_regions: list[Region]
    control_center_colors: ControlCenterColor

    def set_lingo2_rule(self, world: "Lingo2World", regions: dict[str, Region] | None, reqs: AccessRequirements,
                        control_center_colors: ControlCenterColor):
        self.world = world

        if reqs.is_empty():
            self.reqs = None
        else:
            self.reqs = reqs.copy()
            self.reqs.rooms.clear()

            if regions is None:
                self.required_regions = [world.multiworld.get_region(room_name, world.player)
                                         for room_name in reqs.rooms]
            else:
                self.required_regions = [regions[room_name] for room_name in reqs.rooms]

        self.control_center_colors = control_center_colors

        self.access_rule = self._lingo2_rule

    def _lingo2_rule(self, state: CollectionState) -> bool:
        if self.reqs is not None and not lingo2_can_satisfy_requirements(state, self.reqs, self.required_regions,
                                                                         self.world):
            return False

        if self.control_center_colors != ControlCenterColor.ALL\
                and not self.world.options.shuffle_control_center_colors:
            from_region_state = state.lingo2_get_region_state(self.parent_region)

            if from_region_state.control_center_colors & self.control_center_colors == ControlCenterColor.NONE:
                return False

        state.lingo2_use_entrance(self)
        return True


class Lingo2Region(Region):
    def can_reach(self, state) -> bool:
        if self in state.reachable_regions[self.player]:
            return True

        if not state.stale[self.player] and not state.lingo2_state[self.player].stale:
            # if the cache is updated we can use the cache
            return super().can_reach(state)

        if state.lingo2_state[self.player].stale:
            state.lingo2_sweep(self.player)

        return super().can_reach(state)


class PerPlayerRegionState:
    control_center_colors: ControlCenterColor

    def __init__(self):
        self.control_center_colors = ControlCenterColor.NONE

    def copy(self) -> "PerPlayerRegionState":
        result = PerPlayerRegionState()
        result.control_center_colors = self.control_center_colors
        return result


class PerPlayerState:
    world: "Lingo2World"
    regions: dict[str, PerPlayerRegionState]
    stale: bool
    sweeping: bool
    sweepable_entrances: set[Entrance]
    check_colors: bool

    def __init__(self, world: "Lingo2World"):
        self.world = world
        self.regions = {}
        self.stale = True
        self.sweeping = False
        self.sweepable_entrances = set()
        self.check_colors = not world.options.shuffle_control_center_colors

        self.regions["Menu"] = PerPlayerRegionState()
        self.regions["Menu"].control_center_colors = ControlCenterColor.ALL

    def copy(self) -> "PerPlayerState":
        result = PerPlayerState(self.world)
        result.regions = {region_name: region_state.copy() for region_name, region_state in self.regions.items()}
        result.stale = self.stale
        result.sweeping = self.sweeping
        result.sweepable_entrances = self.sweepable_entrances.copy()
        result.check_colors = self.check_colors
        return result

    def get_region_state(self, region: Region):
        return self.regions.setdefault(region.name, PerPlayerRegionState())


class Lingo2LogicMixin(LogicMixin):
    multiworld: MultiWorld
    reachable_regions: dict[int, set[Region]]

    lingo2_state: dict[int, PerPlayerState]

    def init_mixin(self, multiworld: MultiWorld):
        self.lingo2_state = {player: PerPlayerState(multiworld.worlds[player])
                             for player in multiworld.get_game_players("Lingo 2")}

    def copy_mixin(self, new_state: CollectionState) -> CollectionState:
        new_state.lingo2_state = {player: pps.copy() for player, pps in self.lingo2_state.items()}

        return new_state

    def lingo2_get_region_state(self, region: Region):
        return self.lingo2_state[region.player].get_region_state(region)

    def lingo2_use_entrance(self, entrance: Lingo2Entrance):
        player_state = self.lingo2_state[entrance.player]

        from_region_state = player_state.get_region_state(entrance.parent_region)
        to_region_state = player_state.get_region_state(entrance.connected_region)

        should_update = False

        if player_state.check_colors:
            avail_colors = from_region_state.control_center_colors & entrance.control_center_colors
            if avail_colors & ~to_region_state.control_center_colors != ControlCenterColor.NONE:
                if to_region_state.control_center_colors != ControlCenterColor.NONE or avail_colors != ControlCenterColor.ALL:
                    #print(f"entrance {entrance.name} takes region from {to_region_state.control_center_colors} to {avail_colors}")
                    pass

                should_update = True
                to_region_state.control_center_colors = to_region_state.control_center_colors | avail_colors

        if should_update:
            player_state.stale = True

            for adjacent in entrance.parent_region.exits:
                player_state.sweepable_entrances.add(adjacent)

    def lingo2_sweep(self, player: int):
        player_state = self.lingo2_state[player]
        if player_state.sweeping:
            return
        player_state.sweeping = True

        while player_state.sweepable_entrances:
            next_entrance = player_state.sweepable_entrances.pop()
            if next_entrance.parent_region in self.reachable_regions[player]:
                next_entrance.can_reach(self)

        player_state.stale = False
        player_state.sweeping = False


def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region],
                                    world: "Lingo2World") -> bool:
    if not all(state.has(item, world.player) for item in reqs.items):
        return False

    if not all(state.has(item, world.player, amount) for item, amount in reqs.progressives.items()):
        return False

    if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
        return False

    if not all(state.can_reach(region) for region in regions):
        return False

    for letter_key, letter_level in reqs.letters.items():
        if not state.has(letter_key, world.player, letter_level):
            return False

    if reqs.cyans:
        if not any(state.has(letter, world.player, amount)
                   for letter, amount in world.player_logic.double_letter_amount.items()):
            return False

    if len(reqs.or_logic) > 0:
        if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction)
                   for subjunction in reqs.or_logic):
            return False

    if reqs.complete_at is not None:
        completed = 0
        checked = 0
        for possibility in reqs.possibilities:
            checked += 1
            if lingo2_can_satisfy_requirements(state, possibility, [], world):
                completed += 1
                if completed >= reqs.complete_at:
                    break
            elif len(reqs.possibilities) - checked + completed < reqs.complete_at:
                # There aren't enough remaining possibilities for the check to pass.
                return False
        if completed < reqs.complete_at:
            return False

    return True


def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
                         regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]:
    # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule
    # checking.
    if regions is not None:
        required_regions = [regions[room_name] for room_name in reqs.rooms]
    else:
        required_regions = [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms]
    new_reqs = reqs.copy()
    new_reqs.rooms.clear()
    return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)