Core: migrate item links out of main (#2914)
* Core: move item linking out of main * add a test that item link option correctly validates * remove unused fluff --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
parent
1d19da0c76
commit
83521e99d9
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import functools
|
import functools
|
||||||
|
@ -288,6 +289,84 @@ class MultiWorld():
|
||||||
group["non_local_items"] = item_link["non_local_items"]
|
group["non_local_items"] = item_link["non_local_items"]
|
||||||
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
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):
|
def secure(self):
|
||||||
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
||||||
self.is_race = True
|
self.is_race = True
|
||||||
|
|
77
Main.py
77
Main.py
|
@ -184,82 +184,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."
|
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||||
multiworld.itempool[:] = new_items
|
multiworld.itempool[:] = new_items
|
||||||
|
|
||||||
# temporary home for item links, should be moved out of Main
|
multiworld.link_items()
|
||||||
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)])
|
|
||||||
|
|
||||||
if any(multiworld.item_links.values()):
|
if any(multiworld.item_links.values()):
|
||||||
multiworld._all_state = None
|
multiworld._all_state = None
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import MultiWorld, PlandoOptions
|
||||||
from Options import ItemLinks
|
from Options import ItemLinks
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
@ -47,3 +47,15 @@ class TestOptions(unittest.TestCase):
|
||||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||||
|
|
||||||
# TODO test that the group created using these options has the items
|
# TODO test that the group created using these options has the items
|
||||||
|
|
||||||
|
def test_item_links_resolve(self):
|
||||||
|
"""Test item link option resolves correctly."""
|
||||||
|
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])
|
||||||
|
|
Loading…
Reference in New Issue