Add flag to only check for beatable, not fully clearable configurations (only works with restrictive algorithm for now).

This commit is contained in:
LLCoolDave 2017-06-23 22:15:29 +02:00
parent 350c688d93
commit 28a2e3cf2d
3 changed files with 49 additions and 23 deletions

View File

@ -4,7 +4,7 @@ import logging
class World(object):
def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items):
def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items, check_beatable_only):
self.shuffle = shuffle
self.logic = logic
self.mode = mode
@ -37,6 +37,7 @@ class World(object):
self.fix_door_frames = self.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull']
self.fix_trock_doors = self.shuffle != 'vanilla'
self.save_and_quite_from_boss = False
self.check_beatable_only = check_beatable_only
def get_region(self, regionname):
if isinstance(regionname, Region):
@ -160,10 +161,13 @@ class World(object):
return False
def can_beat_game(self):
def can_beat_game(self, starting_state=None):
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event)]
state = CollectionState(self)
if starting_state:
state = starting_state.copy()
else:
state = CollectionState(self)
treasure_pieces_collected = 0
while prog_locations:
sphere = []

60
Main.py
View File

@ -11,23 +11,23 @@ import logging
import argparse
import os
__version__ = '0.3-dev'
__version__ = '0.4-dev'
logic_hash = [169, 242, 143, 206, 16, 22, 49, 159, 94, 18, 202, 249, 155, 198, 75, 55, 122, 166, 239, 175, 62, 4, 118, 13, 149, 70, 26, 11, 141, 173, 168, 252,
100, 152, 221, 248, 112, 58, 80, 158, 87, 162, 190, 99, 219, 184, 178, 101, 43, 73, 164, 226, 63, 185, 54, 107, 38, 17, 68, 32, 148, 209, 181, 146,
85, 156, 127, 7, 182, 37, 113, 66, 59, 41, 78, 189, 20, 180, 144, 9, 231, 161, 88, 46, 1, 24, 53, 167, 213, 220, 115, 81, 194, 205, 163, 14,
42, 64, 183, 104, 79, 71, 50, 98, 138, 233, 240, 96, 23, 31, 67, 251, 217, 232, 236, 250, 238, 218, 201, 151, 200, 28, 150, 65, 2, 103, 223, 5,
72, 93, 176, 243, 177, 40, 197, 52, 132, 56, 212, 227, 136, 147, 135, 188, 29, 19, 51, 142, 120, 225, 8, 137, 92, 154, 196, 241, 215, 171, 133, 131,
186, 117, 130, 210, 69, 106, 145, 110, 214, 15, 124, 157, 57, 191, 121, 255, 170, 237, 229, 105, 30, 134, 235, 102, 119, 139, 83, 153, 47, 82, 114, 160,
211, 108, 216, 10, 203, 39, 77, 123, 207, 140, 230, 90, 27, 244, 116, 21, 179, 165, 245, 95, 12, 253, 6, 60, 25, 74, 76, 91, 126, 195, 224, 246,
125, 61, 33, 44, 187, 222, 0, 45, 86, 34, 129, 174, 111, 35, 84, 128, 208, 247, 234, 48, 97, 199, 204, 192, 228, 89, 172, 109, 36, 254, 3, 193]
logic_hash = [215, 18, 94, 177, 161, 252, 45, 4, 29, 231, 99, 158, 70, 55, 74, 39, 12, 134, 142, 189, 61, 105, 10, 254, 137, 44, 72, 154, 145, 167, 98, 225,
100, 217, 126, 187, 13, 255, 138, 51, 64, 130, 139, 233, 168, 69, 175, 25, 58, 160, 1, 27, 206, 169, 223, 210, 188, 111, 186, 240, 133, 26, 41, 241,
204, 89, 78, 63, 96, 218, 198, 224, 219, 35, 82, 181, 121, 243, 0, 155, 91, 120, 221, 178, 162, 80, 66, 97, 118, 103, 86, 191, 135, 122, 104, 40,
183, 9, 230, 110, 14, 87, 143, 249, 90, 75, 232, 157, 238, 196, 23, 248, 2, 101, 159, 108, 201, 73, 34, 15, 179, 92, 226, 60, 222, 32, 109, 119,
49, 56, 16, 6, 22, 209, 190, 21, 136, 113, 205, 192, 146, 30, 212, 43, 200, 193, 185, 242, 71, 163, 102, 239, 24, 220, 166, 228, 208, 47, 3, 112,
203, 50, 216, 214, 107, 106, 57, 67, 88, 42, 176, 129, 144, 54, 237, 165, 116, 141, 125, 128, 172, 171, 152, 83, 38, 93, 148, 151, 207, 236, 131, 85,
170, 124, 28, 251, 194, 250, 8, 164, 65, 20, 150, 182, 77, 17, 202, 253, 173, 229, 46, 140, 76, 95, 117, 174, 79, 84, 36, 244, 199, 37, 211, 7,
247, 213, 31, 62, 59, 153, 197, 19, 48, 114, 53, 115, 149, 81, 5, 184, 147, 68, 227, 234, 52, 156, 132, 127, 235, 245, 11, 33, 123, 180, 246, 195]
def main(args, seed=None):
start = time.clock()
# initialize the world
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems)
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly)
logger = logging.getLogger('')
if seed is None:
@ -71,7 +71,7 @@ def main(args, seed=None):
elif args.algorithm == 'freshness':
distribute_items_staleness(world)
elif args.algorithm == 'restrictive':
distribute_items_restrictive(world)
distribute_items_restrictive(world, 10 if world.goal is not 'starhunt' else 0)
world.spoiler += print_location_spoiler(world)
@ -253,7 +253,7 @@ def distribute_items_staleness(world):
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations]))
def distribute_items_restrictive(world, gftower_trash_count=10):
def distribute_items_restrictive(world, gftower_trash_count=0):
# get list of locations to fill in
fill_locations = world.get_unfilled_locations()
@ -289,14 +289,28 @@ def distribute_items_restrictive(world, gftower_trash_count=10):
spot_to_fill = None
for location in fill_locations:
if maximum_exploration_state.can_reach(location) and location.item_rule(item_to_place):
spot_to_fill = location
break
if location.item_rule(item_to_place):
if world.check_beatable_only:
starting_state = world.state.copy()
for item in progitempool:
starting_state.collect(item, True)
if maximum_exploration_state.can_reach(location):
if world.check_beatable_only:
starting_state.collect(item_to_place, True)
else:
spot_to_fill = location
break
if world.check_beatable_only and world.can_beat_game(starting_state):
spot_to_fill = location
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.')
if not world.check_beatable_only:
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
break
raise RuntimeError('No more spots to place %s' % item_to_place)
@ -468,7 +482,7 @@ def generate_itempool(world):
def copy_world(world):
# ToDo: Not good yet
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items)
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only)
ret.required_medallions = list(world.required_medallions)
ret.swamp_patch_required = world.swamp_patch_required
ret.treasure_hunt_count = world.treasure_hunt_count
@ -514,6 +528,10 @@ def create_playthrough(world):
if world.goal in ['pedestal', 'starhunt', 'triforcehunt']:
world.get_location('Ganon').item = None
# if we only check for beatable, we can do this sanity check first before writing down spheres
if world.check_beatable_only and not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
# get locations containing progress items
prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement]
@ -539,8 +557,11 @@ def create_playthrough(world):
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.' % (len(collection_spheres), len(sphere), len(prog_locations)))
if not sphere:
logging.getLogger('').debug('The following items could not be placed: %s' % ['%s at %s' % (location.item.name, location.name) for location in sphere_candidates])
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
logging.getLogger('').debug('The following items could not be reached: %s' % ['%s at %s' % (location.item.name, location.name) for location in sphere_candidates])
if not world.check_beatable_only:
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
else:
break
# 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):
@ -613,6 +634,7 @@ if __name__ == '__main__':
parser.add_argument('--count', help='Use to batch generate multiple seeds with same settings. If --seed is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with --seed given will produce the same 10 (different) roms each time).', type=int)
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--nodungeonitems', help='Remove Maps and Compasses from Itempool, replacing them by empty slots.', action='store_true')
parser.add_argument('--beatableonly', help='Only check if the game is beatable with placement. Do not ensure all locations are reachable. This only has an effect on the restrictive algorithm currently.', action='store_true')
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['normal', 'half', 'quarter', 'off'],
help='Select the rate at which the heart beep sound is played at low health.')
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')

2
Rom.py
View File

@ -292,7 +292,7 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None
# set rom name
# 21 bytes
write_bytes(rom, 0x7FC0, bytearray('ER_030_%09d_' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big'))
write_bytes(rom, 0x7FC0, bytearray('ER_040_%09d_' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big'))
# set heart beep rate
write_byte(rom, 0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20}[beep])