Fixed remaining key logic, correctly implemented sewers. Added support for other game modes, create playthrough.

This commit is contained in:
LLCoolDave
2017-05-16 21:23:47 +02:00
parent 57483052e4
commit f374c637c3
5 changed files with 313 additions and 61 deletions

193
Main.py
View File

@@ -1,4 +1,4 @@
from BaseClasses import World
from BaseClasses import World, CollectionState
from Regions import create_regions
from EntranceShuffle import link_entrances
from Rules import set_rules
@@ -7,19 +7,21 @@ from Items import *
import random
import cProfile
import time
import logging
def main(seed=None, shuffle='Default', logic='no-glitches', mode='standard', difficulty='normal', goal='defeat ganon'):
# initialize the world
world = World()
world = World(shuffle, logic, mode, difficulty, goal)
create_regions(world)
random.seed(seed)
link_entrances(world, shuffle)
set_rules(world, logic, mode)
generate_itempool(world, difficulty, goal)
link_entrances(world)
set_rules(world)
generate_itempool(world)
distribute_items(world)
# flood_items(world) # different algo, biased towards early game progress items
return world
@@ -56,6 +58,10 @@ def distribute_items(world):
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
# we placed all available progress items. Maybe the game can be beaten anyway?
if world.can_beat_game():
logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.')
break
raise RuntimeError('No more progress items left to place.')
spot_to_fill = None
@@ -65,21 +71,80 @@ def distribute_items(world):
break
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
if world.can_beat_game():
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
break
raise RuntimeError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, True)
itempool.remove(item_to_place)
fill_locations.remove(spot_to_fill)
print('Unplaced items: %s - Unfilled Locations: %s' % (itempool, fill_locations))
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % (itempool, fill_locations))
def generate_itempool(world, difficulty, goal):
if difficulty != 'normal' or goal != 'defeat ganon':
def flood_items(world):
# get items to distribute
random.shuffle(world.itempool)
itempool = world.itempool
progress_done = False
# fill world from top of itempool while we can
while not progress_done:
location_list = world.get_unfilled_locations()
random.shuffle(location_list)
spot_to_fill = None
for location in location_list:
if world.state.can_reach(location):
spot_to_fill = location
break
if spot_to_fill:
item = itempool.pop(0)
world.push_item(spot_to_fill, item, True)
continue
# ran out of spots, check if we need to step in and correct things
if len(world.get_reachable_locations()) == len(world.get_locations()):
progress_done = True
continue
# need to place a progress item instead of an already placed item, find candidate
item_to_place = None
candidate_item_to_place = None
for item in itempool:
if item.advancement:
candidate_item_to_place = item
if world.unlocks_new_location(item):
item_to_place = item
break
# we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying
if item_to_place is None:
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
raise RuntimeError('No more progress items left to place.')
# find item to replace with progress item
location_list = world.get_reachable_locations()
random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.key and 'Map' not in location.item.name and 'Compass' not in location.item.name:
# safe to replace
replace_item = location.item
replace_item.location = None
itempool.append(replace_item)
world.push_item(location, item_to_place, True)
itempool.remove(item_to_place)
break
def generate_itempool(world):
if world.difficulty != 'normal' or world.goal not in ['defeat ganon', 'pedestal', 'all dungeons'] or world.mode not in ['open', 'standard']:
raise NotImplementedError('Not supported yet')
# Push the two fixed items
world.push_item('Uncle', ProgressiveSword())
world.push_item('Ganon', Triforce(), False)
# set up item pool
@@ -134,6 +199,22 @@ def generate_itempool(world, difficulty, goal):
Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20()
]
if world.mode == 'standard':
world.push_item('Uncle', ProgressiveSword())
else:
world.itempool.append(ProgressiveSword())
if world.goal == 'pedestal':
world.push_item('Altar', Triforce())
items = list(world.itempool)
random.shuffle(items)
for item in items:
if not item.advancement:
# save to remove
world.itempool.remove(item)
break
# ToDo what to do if EVERYTHING is a progress item?
if random.randint(0, 3) == 0:
world.itempool.append(QuarterMagic())
else:
@@ -156,15 +237,95 @@ def generate_itempool(world, difficulty, goal):
# push dungeon items
fill_dungeons(world)
#profiler = cProfile.Profile()
#profiler.enable()
def copy_world(world):
# ToDo: Not good yet
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal)
ret.required_medallions = list(world.required_medallions)
create_regions(ret)
set_rules(ret)
# connect copied world
for region in world.regions:
for entrance in region.entrances:
ret.get_entrance(entrance.name).connect(ret.get_region(region.name))
# fill locations
for location in world.get_locations():
if location.item is not None:
item = Item(location.item.name, location.item.advancement, location.item.key)
ret.get_location(location.name).item = item
item.location = ret.get_location(location.name)
# copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool:
ret.itempool.append(Item(item.name, item.advancement, item.key))
# copy progress items in state
ret.state.prog_items = list(world.state.prog_items)
return ret
def create_playthrough(world):
# create a copy as we will modify it
world = copy_world(world)
# get locations containing progress items
prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement]
collection_spheres = []
state = CollectionState(world)
sphere_candidates = list(prog_locations)
while sphere_candidates:
sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in sphere_candidates:
if state.can_reach(location):
sphere.append(location)
for location in sphere:
sphere_candidates.remove(location)
state.collect(location.item)
collection_spheres.append(sphere)
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
for sphere in reversed(collection_spheres):
to_delete = []
for location in sphere:
# we remove the item at location and check if game is still beatable
old_item = location.item
location.item = None
state.remove(old_item)
world._item_cache = {} # need to invalidate
if world.can_beat_game():
to_delete.append(location)
else:
# still required, got to keep it around
location.item = old_item
# cull entries in spheres for spoiler walkthrough at end
for location in to_delete:
sphere.remove(location)
# we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces)
collection_spheres = [sphere for sphere in collection_spheres if sphere]
# we can finally output our playthrough
return ''.join(['%s: {\n%s}\n' % (i + 1, ''.join([' %s: %s\n' % (location, location.item) for location in sphere])) for i, sphere in enumerate(collection_spheres)])
profiler = cProfile.Profile()
profiler.enable()
tally = {}
iterations = 300
iterations = 10
start = time.clock()
for i in range(iterations):
print('Seed %s\n\n' % i)
w = main()
w = main(mode='open')
print(create_playthrough(w))
for location in w.get_locations():
if location.item is not None:
old_sk, old_bk, old_prog = tally.get(location.name, (0, 0, 0))
@@ -183,6 +344,6 @@ print('\n\n\n')
for location, stats in tally.items():
print('%s, %s, %s, %s, %s, %s, %s, %s' % (location, stats[0], stats[0]/float(iterations), stats[1], stats[1]/float(iterations), stats[2], stats[2]/float(iterations), 0 if iterations - stats[0] - stats[1] == 0 else stats[2]/float(iterations - stats[0] - stats[1])))
#profiler.disable()
#profiler.print_stats()
profiler.disable()
profiler.print_stats()