about summary refs log tree commit diff stats
path: root/apworld/player_logic.py
blob: dc1bdf0f3574c7e43567f52be53c35306d6b5652 (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
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#include "connection_dialog.h"

#include "tracker_config.h"

ConnectionDialog::ConnectionDialog()
    : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
  server_box_ = new wxTextCtrl(
      this, -1,
      wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server),
      wxDefaultPosition, FromDIP(wxSize{300, -1}));
  player_box_ = new wxTextCtrl(
      this, -1,
      wxString::FromUTF8pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
from .generated import data_pb2 as data_pb2
from typing import TYPE_CHECKING, NamedTuple

from .options import VictoryCondition

if TYPE_CHECKING:
    from . import Lingo2World


def calculate_letter_histogram(solution: str) -> dict[str, int]:
    histogram = dict()
    for l in solution:
        if l.isalpha():
            real_l = l.upper()
            histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2)

    for free_letter in "HINT":
        if histogram.get(free_letter, 0) == 1:
            del histogram[free_letter]

    return histogram


class AccessRequirements:
    items: set[str]
    progressives: dict[str, int]
    rooms: set[str]
    symbols: set[str]
    letters: dict[str, int]

    # This is an AND of ORs.
    or_logic: list[list["AccessRequirements"]]

    def __init__(self):
        self.items = set()
        self.progressives = dict()
        self.rooms = set()
        self.symbols = set()
        self.letters = dict()
        self.or_logic = list()

    def add_solution(self, solution: str):
        histogram = calculate_letter_histogram(solution)

        for l, a in histogram.items():
            self.letters[l] = max(self.letters.get(l, 0), histogram.get(l))

    def merge(self, other: "AccessRequirements"):
        for item in other.items:
            self.items.add(item)

        for item, amount in other.progressives.items():
            self.progressives[item] = max(amount, self.progressives.get(item, 0))

        for room in other.rooms:
            self.rooms.add(room)

        for symbol in other.symbols:
            self.symbols.add(symbol)

        for letter, level in other.letters.items():
            self.letters[letter] = max(self.letters.get(letter, 0), level)

        for disjunction in other.or_logic:
            self.or_logic.append(disjunction)

    def __repr__(self):
        parts = []
        if len(self.items) > 0:
            parts.append(f"items={self.items}")
        if len(self.progressives) > 0:
            parts.append(f"progressives={self.progressives}")
        if len(self.rooms) > 0:
            parts.append(f"rooms={self.rooms}")
        if len(self.symbols) > 0:
            parts.append(f"symbols={self.symbols}")
        if len(self.letters) > 0:
            parts.append(f"letters={self.letters}")
        if len(self.or_logic) > 0:
            parts.append(f"or_logic={self.or_logic}")
        return f"AccessRequirements({", ".join(parts)})"


class PlayerLocation(NamedTuple):
    code: int | None
    reqs: AccessRequirements


class Lingo2PlayerLogic:
    world: "Lingo2World"

    locations_by_room: dict[int, list[PlayerLocation]]
    event_loc_item_by_room: dict[int, dict[str, str]]

    item_by_door: dict[int, tuple[str, int]]

    panel_reqs: dict[int, AccessRequirements]
    proxy_reqs: dict[int, dict[str, AccessRequirements]]
    door_reqs: dict[int, AccessRequirements]

    real_items: list[str]

    def __init__(self, world: "Lingo2World"):
        self.world = world
        self.locations_by_room = {}
        self.event_loc_item_by_room = {}
        self.item_by_door = {}
        self.panel_reqs = dict()
        self.proxy_reqs = dict()
        self.door_reqs = dict()
        self.real_items = list()

        if self.world.options.shuffle_doors:
            for progressive in world.static_logic.objects.progressives:
                for i in range(0, len(progressive.doors)):
                    self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
                    self.real_items.append(progressive.name)

        # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
        # before we calculate any access requirements.
        for door in world.static_logic.objects.doors:
            if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors:
                if door.id in self.item_by_door:
                    continue

                door_item_name = self.world.static_logic.get_door_item_name(door)
                self.item_by_door[door.id] = (door_item_name, 1)
                self.real_items.append(door_item_name)

        for door in world.static_logic.objects.doors:
            if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
                self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id,
                                                                                          self.get_door_reqs(door.id)))

        for letter in world.static_logic.objects.letters:
            self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
                                                                                        AccessRequirements()))

            letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
            event_name = f"{letter_name} (Collected)"
            self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()

            if letter.level2:
                event_name = f"{letter_name} (Double Collected)"
                self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()

        for mastery in world.static_logic.objects.masteries:
            self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
                                                                                         AccessRequirements()))

        for ending in world.static_logic.objects.endings:
            # Don't ever create a location for White Ending. Don't even make an event for it if it's not the victory
            # condition, since it is necessarily going to be in the postgame.
            if ending.name == "WHITE":
                if self.world.options.victory_condition != VictoryCondition.option_white_ending:
                    continue
            else:
                self.locations_by_room.setdefault(ending.room_id, []).append(PlayerLocation(ending.ap_id,
                                                                                            AccessRequirements()))

            event_name = f"{ending.name.capitalize()} Ending (Achieved)"
            item_name = event_name

            if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
                item_name = "Victory"

            self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = item_name

        if self.world.options.keyholder_sanity:
            for keyholder in world.static_logic.objects.keyholders:
                if keyholder.HasField("key"):
                    reqs = AccessRequirements()
                    reqs.letters[keyholder.key.upper()] = 1

                    self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id,
                                                                                                   reqs))

    def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements:
        if answer is None:
            if panel_id not in self.panel_reqs:
                self.panel_reqs[panel_id] = self.calculate_panel_reqs(panel_id, answer)

            return self.panel_reqs.get(panel_id)
        else:
            if panel_id not in self.proxy_reqs or answer not in self.proxy_reqs.get(panel_id):
                self.proxy_reqs.setdefault(panel_id, {})[answer] = self.calculate_panel_reqs(panel_id, answer)

            return self.proxy_reqs.get(panel_id).get(answer)

    def calculate_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements:
        panel = self.world.static_logic.objects.panels[panel_id]
        reqs = AccessRequirements()

        reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id))

        if answer is not None:
            reqs.add_solution(answer)
        elif len(panel.proxies) > 0:
            possibilities = []

            for proxy in panel.proxies:
                proxy_reqs = AccessRequirements()
                proxy_reqs.add_solution(proxy.answer)

                possibilities.append(proxy_reqs)

            if not any(proxy.answer == panel.answer for proxy in panel.proxies):
                proxy_reqs = AccessRequirements()
                proxy_reqs.add_solution(panel.answer)

                possibilities.append(proxy_reqs)

            reqs.or_logic.append(possibilities)
        else:
            reqs.add_solution(panel.answer)

        for symbol in panel.symbols:
            reqs.symbols.add(symbol)

        if panel.HasField("required_door"):
            door_reqs = self.get_door_open_reqs(panel.required_door)
            reqs.merge(door_reqs)

        if panel.HasField("required_room"):
            reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.required_room))

        return reqs

    # This gets/calculates the requirements described by the door object. This is most notably used as the requirements
    # for clearing a location, or opening a door when the door is not shuffled.
    def get_door_reqs(self, door_id: int) -> AccessRequirements:
        if door_id not in self.door_reqs:
            self.door_reqs[door_id] = self.calculate_door_reqs(door_id)

        return self.door_reqs.get(door_id)

    def calculate_door_reqs(self, door_id: int) -> AccessRequirements:
        door = self.world.static_logic.objects.doors[door_id]
        reqs = AccessRequirements()

        # TODO: lavender_cubes, endings
        if not door.HasField("complete_at") or door.complete_at == 0:
            for proxy in door.panels:
                panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
                reqs.merge(panel_reqs)
        elif door.complete_at == 1:
            reqs.or_logic.append([self.get_panel_reqs(proxy.panel,
                                                      proxy.answer if proxy.HasField("answer") else None)
                                  for proxy in door.panels])
        else:
            # TODO: Handle complete_at > 1
            pass

        if door.HasField("control_center_color"):
            # TODO: Logic for ensuring two CC states aren't needed at once.
            reqs.rooms.add("Control Center - Main Area")
            reqs.add_solution(door.control_center_color)

        if door.double_letters:
            # TODO: When letter shuffle is on, change this to require any double letter instead.
            reqs.rooms.add("The Repetitive - Main Room")

        for keyholder_uses in door.keyholders:
            key_name = keyholder_uses.key.upper()
            if key_name not in reqs.letters:
                reqs.letters[key_name] = 1

            keyholder = self.world.static_logic.objects.keyholders[keyholder_uses.keyholder]
            reqs.rooms.add(self.world.static_logic.get_room_region_name(keyholder.room_id))

        for room in door.rooms:
            reqs.rooms.add(self.world.static_logic.get_room_region_name(room))

        for ending_id in door.endings:
            ending = self.world.static_logic.objects.endings[ending_id]
            reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")

        for sub_door_id in door.doors:
            sub_reqs = self.get_door_open_reqs(sub_door_id)
            reqs.merge(sub_reqs)

        return reqs

    # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item
    # that acts as the door's key.
    def get_door_open_reqs(self, door_id: int) -> AccessRequirements:
        if door_id in self.item_by_door:
            reqs = AccessRequirements()

            item_name, amount = self.item_by_door.get(door_id)
            if amount == 1:
                reqs.items.add(item_name)
            else:
                reqs.progressives[item_name] = amount

            return reqs
        else:
            return self.get_door_reqs(door_id)