feat: MW progresssion balancing tweaked to be percentage based instead of raw count. Tries to keep each player's locations in each sphere within 80% of the player with the most locations available. (Measured with percentage instead of raw count.) Old algo tried to keep everyone within 20 locations of each other. Difficult if one player has a lot more locations than another.

fix: Potential fix for early Trinexx start
This commit is contained in:
aerinon
2023-12-14 10:13:22 -07:00
parent c4ec28da76
commit 0bd1f90bca
6 changed files with 50 additions and 16 deletions

View File

@@ -512,7 +512,7 @@ class World(object):
if not sphere:
# ran out of places and did not finish yet, quit
if log_error:
missing_locations = ", ".join([x.name for x in prog_locations])
missing_locations = ", ".join([f'{x.name} (#{x.player})' for x in prog_locations])
logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}')
return False
@@ -547,7 +547,7 @@ class CollectionState(object):
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
self.dungeon_limits = None
self.placing_item = None
self.placing_items = None
# self.trace = None
def update_reachable_regions(self, player):
@@ -833,7 +833,7 @@ class CollectionState(object):
return door_candidates
door_candidates, skip = [], set()
if (state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name != 'Universal'
and state.placing_item and state.placing_item.name == small_key_name):
and state.placing_items and any(i.name == small_key_name and i.player == player for i in state.placing_items)):
key_logic = state.world.key_logic[player][dungeon_name]
for door, paired in key_logic.sm_doors.items():
if door.name in key_logic.door_rules:
@@ -878,7 +878,7 @@ class CollectionState(object):
player: defaultdict(dict, {name: copy.copy(checklist)
for name, checklist in self.dungeons_to_check[player].items()})
for player in range(1, self.world.players + 1)}
ret.placing_item = self.placing_item
ret.placing_items = self.placing_items
return ret
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):

46
Fill.py
View File

@@ -3,6 +3,7 @@ import collections
import itertools
import logging
import math
from collections import Counter
from contextlib import suppress
from BaseClasses import CollectionState, FillError, LocationType
@@ -71,13 +72,13 @@ def fill_dungeons_restrictive(world, shuffled_locations):
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
vanilla=False):
def sweep_from_pool(placing_item=None):
def sweep_from_pool(placing_items=None):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.placing_item = placing_item
new_state.placing_items = placing_items
new_state.sweep_for_events()
new_state.placing_item = None
new_state.placing_items = None
return new_state
unplaced_items = []
@@ -94,7 +95,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
while any(player_items.values()) and locations:
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
maximum_exploration_state = sweep_from_pool(placing_item=items_to_place[0])
maximum_exploration_state = sweep_from_pool(placing_items=items_to_place)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
@@ -703,24 +704,44 @@ def balance_multiworld_progression(world):
checked_locations = set()
unchecked_locations = set(world.get_locations())
total_locations_count = Counter(location.player for location in world.get_locations() if not location.locked and not location.forced_item)
reachable_locations_count = {}
for player in range(1, world.players + 1):
reachable_locations_count[player] = 0
sphere_num = 1
moved_item_count = 0
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
def item_percentage(player, num):
return num / total_locations_count[player]
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
if not location.locked and not location.forced_item:
reachable_locations_count[location.player] += 1
logging.debug(f'Sphere {sphere_num}')
logging.debug(f'Reachable locations: {reachable_locations_count}')
debug_percentages = {
player: round(item_percentage(player, num), 2)
for player, num in reachable_locations_count.items()
}
logging.debug(f'Reachable percentages: {debug_percentages}\n')
sphere_num += 1
if checked_locations:
threshold = max(reachable_locations_count.values()) - 20
max_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]), reachable_locations_count))
threshold_percentages = {player: max_percentage * .8 for player in range(1, world.players + 1)}
logging.debug(f'Thresholds: {threshold_percentages}')
balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold}
balancing_players = {player for player, reachables in reachable_locations_count.items()
if item_percentage(player, reachables) < threshold_percentages[player]}
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
@@ -738,7 +759,8 @@ def balance_multiworld_progression(world):
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()):
if world.has_beaten_game(balancing_state) or all(item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
@@ -765,7 +787,8 @@ def balance_multiworld_progression(world):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere))
if p < threshold_percentages[player]:
items_to_replace.append(testing)
replaced_items = False
@@ -790,6 +813,7 @@ def balance_multiworld_progression(world):
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
@@ -797,6 +821,7 @@ def balance_multiworld_progression(world):
logging.warning(f"Could not Progression Balance {old_location.item}")
if replaced_items:
logging.debug(f'Moved {moved_item_count} items so far\n')
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
unchecked_locations.remove(location)
@@ -811,7 +836,8 @@ def balance_multiworld_progression(world):
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
logging.warning('Progression Balancing ran out of paths.')
break
def check_shop_swap(l):

View File

@@ -37,7 +37,7 @@ from source.enemizer.DamageTables import DamageTable
from source.enemizer.Enemizer import randomize_enemies
from source.rom.DataTables import init_data_tables
version_number = '1.3.0.7'
version_number = '1.3.0.8'
version_branch = '-v'
__version__ = f'{version_number}{version_branch}'

View File

@@ -141,6 +141,14 @@ These are now independent of retro mode and have three options: None, Random, an
# Bug Fixes and Notes
* 1.3.0.8v
* Enemizer: Red Mimics correctly banned from challenge rooms in appropriate logic setting
* No Logic Standard ER: Rain doors aren't blocked if no logic is enabled.
* Trinexx: attempt to fix early start
* MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold)
* Take anys: Good Bee cave chosen as take any should no longer prevent generation
* Money balancing: Fixed generation issue
* Enemizer: various enemy bans
* 1.3.0.7v
* Fix for Mimic Cave enemy drops
* Fix for Spectacle Rock Cave enemy drops (the mini-moldorms)

2
Rom.py
View File

@@ -40,7 +40,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '5661a616546e7dc0ee4bdfa9b152bc68'
RANDOMIZERBASEHASH = '4d1f3e36e316077823a3e2eb5359ca17'
class JsonRom(object):

Binary file not shown.