From bd86a07115a6dc0a817820206c25547617e2dc83 Mon Sep 17 00:00:00 2001
From: Fabian Dill <fabian.dill@web.de>
Date: Mon, 4 Jan 2021 15:14:20 +0100
Subject: [PATCH] make random world targeting smarter, in only considering
 possible unfilled locations

---
 BaseClasses.py |  8 ++++++-
 Fill.py        | 58 +++++++++++++++++++++++++++++++++++++++++++++++++-
 Main.py        | 39 ++-------------------------------
 3 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/BaseClasses.py b/BaseClasses.py
index 9cc25bcb..4729e4fd 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -5,7 +5,7 @@ from enum import Enum, unique
 import logging
 import json
 from collections import OrderedDict, Counter, deque
-from typing import Union, Optional, List, Set, Dict, NamedTuple
+from typing import Union, Optional, List, Set, Dict, NamedTuple, Iterable
 import secrets
 import random
 
@@ -393,6 +393,12 @@ class World(object):
         return [location for location in self.get_locations() if
                 (player is None or location.player == player) and location.item is None and location.can_reach(state)]
 
+    def get_unfilled_locations_for_players(self, location_name: str, players: Iterable[int]):
+        for player in players:
+            location = self.get_location(location_name, player)
+            if location.item is None:
+                yield location
+
     def unlocks_new_location(self, item) -> bool:
         temp_state = self.state.copy()
         temp_state.collect(item, True)
diff --git a/Fill.py b/Fill.py
index a4ef61d9..0ad4332f 100644
--- a/Fill.py
+++ b/Fill.py
@@ -1,12 +1,14 @@
 import logging
 import typing
 
-from BaseClasses import CollectionState
+from BaseClasses import CollectionState, PlandoItem
+from Items import ItemFactory
 
 
 class FillError(RuntimeError):
     pass
 
+
 def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False):
     def sweep_from_pool():
         new_state = base_state.copy()
@@ -339,3 +341,57 @@ def balance_multiworld_progression(world):
                 break
             elif not sphere_locations:
                 raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
+
+
+def distribute_planned(world):
+    world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
+
+    for player in world.player_ids:
+        placement: PlandoItem
+        for placement in world.plando_items[player]:
+            item = ItemFactory(placement.item, player)
+            target_world: int = placement.world
+            if target_world is False or world.players == 1:
+                target_world = player  # in own world
+            elif target_world is True:  # in any other world
+                unfilled = list(location for location in world.get_unfilled_locations_for_players(
+                    placement.location,
+                    set(world.player_ids) - {player}) if location.item_rule(item)
+                                )
+                if not unfilled:
+                    raise FillError(f"Could not find a world with an unfilled location {placement.location}")
+
+                target_world = world.random.choice(unfilled).player
+
+            elif target_world is None:  # any random world
+                unfilled = list(location for location in world.get_unfilled_locations_for_players(
+                    placement.location,
+                    set(world.player_ids)) if location.item_rule(item)
+                                )
+                if not unfilled:
+                    raise FillError(f"Could not find a world with an unfilled location {placement.location}")
+
+                target_world = world.random.choice(unfilled).player
+
+            elif type(target_world) == int:  # target world by player id
+                pass
+            else:  # find world by name
+                target_world = world_name_lookup[target_world]
+
+            location = world.get_location(placement.location, target_world)
+            if location.item:
+                raise Exception(f"Cannot place item into already filled location {location}.")
+
+            if placement.from_pool:
+                try:
+                    world.itempool.remove(item)
+                except ValueError:
+                    logging.warning(f"Could not remove {item} from pool as it's already missing from it.")
+
+            if location.can_fill(world.state, item, False):
+                world.push_item(location, item, collect=False)
+                location.event = True  # flag location to be checked during fill
+                location.locked = True
+                logging.debug(f"Plando placed {item} at {location}")
+            else:
+                raise Exception(f"Can't place {item} at {location} due to fill condition not met.")
diff --git a/Main.py b/Main.py
index 007178a5..ffa6670c 100644
--- a/Main.py
+++ b/Main.py
@@ -17,7 +17,7 @@ from EntranceShuffle import link_entrances, link_inverted_entrances, plando_conn
 from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
 from Rules import set_rules
 from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
-from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression
+from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
 from ItemPool import generate_itempool, difficulties, fill_prizes
 from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
 import Patch
@@ -179,42 +179,7 @@ def main(args, seed=None):
 
     logger.info("Running Item Plando")
 
-    world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
-
-    for player in world.player_ids:
-        placement: PlandoItem
-        for placement in world.plando_items[player]:
-            target_world: int = placement.world
-            if target_world is False or world.players == 1:
-                target_world = player  # in own world
-            elif target_world is True:  # in any other world
-                target_world = player
-                while target_world == player:
-                    target_world = world.random.randint(1, world.players + 1)
-            elif target_world is None:  # any random world
-                target_world = world.random.randint(1, world.players + 1)
-            elif type(target_world) == int:  # target world by player id
-                pass
-            else:  # find world by name
-                target_world = world_name_lookup[target_world]
-
-            location = world.get_location(placement.location, target_world)
-            if location.item:
-                raise Exception(f"Cannot place item into already filled location {location}.")
-            item = ItemFactory(placement.item, player)
-            if placement.from_pool:
-                try:
-                    world.itempool.remove(item)
-                except ValueError:
-                    logger.warning(f"Could not remove {item} from pool as it's already missing from it.")
-
-            if location.can_fill(world.state, item, False):
-                world.push_item(location, item, collect=False)
-                location.event = True  # flag location to be checked during fill
-                location.locked = True
-                logger.debug(f"Plando placed {item} at {location}")
-            else:
-                raise Exception(f"Can't place {item} at {location} due to fill condition not met.")
+    distribute_planned(world)
 
     logger.info('Placing Dungeon Items.')