From dc2c3a6da59d3188f652d773d4faf98366181135 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 6 Mar 2024 09:08:02 -0600 Subject: [PATCH 1/3] Core: move item linking out of main --- BaseClasses.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ Main.py | 77 +----------------------------------------------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 2be9a9820d07..0c9e55042812 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import itertools import functools @@ -341,6 +342,84 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] + def link_items(self) -> None: + """Called to link together items in the itempool related to the registered item link groups.""" + for group_id, group in self.groups.items(): + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in self.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + classifications[item.name] |= item.classification + + for player in players.copy(): + if all([counters[player][item] == 0 for item in shared_pool]): + players.remove(player) + del (counters[player]) + + if not players: + return None, None + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del (counters[player][item]) + return counters, classifications + + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) + if not common_item_count: + continue + + new_itempool: List[Item] = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_item = group["world"].create_item(item_name) + # mangle together all original classification bits + new_item.classification |= classifications[item_name] + new_itempool.append(new_item) + + region = Region("Menu", group_id, self, "ItemLink") + self.regions.append(region) + locations = region.locations + for item in self.itempool: + count = common_item_count.get(item.player, {}).get(item.name, 0) + if count: + loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ + state.has(item_name, group_id_, count_) + + locations.append(loc) + loc.place_locked_item(item) + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(self.itempool) + self.itempool = new_itempool + + while itemcount > len(self.itempool): + items_to_add = [] + for player in group["players"]: + if group["link_replacement"]: + item_player = group_id + else: + item_player = player + if group["replacement_items"][player]: + items_to_add.append(AutoWorld.call_single(self, "create_item", item_player, + group["replacement_items"][player])) + else: + items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player)) + self.random.shuffle(items_to_add) + self.itempool.extend(items_to_add[:itemcount - len(self.itempool)]) + def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True diff --git a/Main.py b/Main.py index f1d2f63692d6..77b6c7db3439 100644 --- a/Main.py +++ b/Main.py @@ -204,82 +204,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." multiworld.itempool[:] = new_items - # temporary home for item links, should be moved out of Main - for group_id, group in multiworld.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ - Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] - ]: - classifications: Dict[str, int] = collections.defaultdict(int) - counters = {player: {name: 0 for name in shared_pool} for player in players} - for item in multiworld.itempool: - if item.player in counters and item.name in shared_pool: - counters[item.player][item.name] += 1 - classifications[item.name] |= item.classification - - for player in players.copy(): - if all([counters[player][item] == 0 for item in shared_pool]): - players.remove(player) - del (counters[player]) - - if not players: - return None, None - - for item in shared_pool: - count = min(counters[player][item] for player in players) - if count: - for player in players: - counters[player][item] = count - else: - for player in players: - del (counters[player][item]) - return counters, classifications - - common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) - if not common_item_count: - continue - - new_itempool: List[Item] = [] - for item_name, item_count in next(iter(common_item_count.values())).items(): - for _ in range(item_count): - new_item = group["world"].create_item(item_name) - # mangle together all original classification bits - new_item.classification |= classifications[item_name] - new_itempool.append(new_item) - - region = Region("Menu", group_id, multiworld, "ItemLink") - multiworld.regions.append(region) - locations = region.locations - for item in multiworld.itempool: - count = common_item_count.get(item.player, {}).get(item.name, 0) - if count: - loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", - None, region) - loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ - state.has(item_name, group_id_, count_) - - locations.append(loc) - loc.place_locked_item(item) - common_item_count[item.player][item.name] -= 1 - else: - new_itempool.append(item) - - itemcount = len(multiworld.itempool) - multiworld.itempool = new_itempool - - while itemcount > len(multiworld.itempool): - items_to_add = [] - for player in group["players"]: - if group["link_replacement"]: - item_player = group_id - else: - item_player = player - if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, - group["replacement_items"][player])) - else: - items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) - multiworld.random.shuffle(items_to_add) - multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) + multiworld.link_items() if any(multiworld.item_links.values()): multiworld._all_state = None From c5485062bfd6b5b2b21338590778053225e1983c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 6 Mar 2024 09:44:40 -0600 Subject: [PATCH 2/3] add a test that item link option correctly validates --- test/general/test_options.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/general/test_options.py b/test/general/test_options.py index 211704dfe6ba..fe9593ab30bc 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -1,4 +1,7 @@ import unittest + +from BaseClasses import MultiWorld +from Options import ItemLinks from worlds.AutoWorld import AutoWorldRegister @@ -17,3 +20,19 @@ def test_options_are_not_set_by_world(self): with self.subTest(game=gamename): self.assertFalse(hasattr(world_type, "options"), f"Unexpected assignment to {world_type.__name__}.options!") + + def test_item_links_resolve(self): + """Test item link option resolves correctly.""" + multiworld = MultiWorld(2) + multiworld.game = {1: "Game 1", 2: "Game 2"} + multiworld.player_name = {1: "Player 1", 2: "Player 2"} + multiworld.set_seed() + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Everything"], + "link_replacement": False, + "replacement_item": None, + }] + item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} + for link in item_links.values(): + self.assertEqual(link.value[0], item_link_group[0]) From c5c2b824cfecc2b07b9840d56bb667b9de9d5224 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 6 Mar 2024 09:51:34 -0600 Subject: [PATCH 3/3] remove unused fluff --- test/general/test_options.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/general/test_options.py b/test/general/test_options.py index fe9593ab30bc..3a34228d68bd 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -23,10 +23,6 @@ def test_options_are_not_set_by_world(self): def test_item_links_resolve(self): """Test item link option resolves correctly.""" - multiworld = MultiWorld(2) - multiworld.game = {1: "Game 1", 2: "Game 2"} - multiworld.player_name = {1: "Player 1", 2: "Player 2"} - multiworld.set_seed() item_link_group = [{ "name": "ItemLinkTest", "item_pool": ["Everything"],