def parse_items(items_str): items = [i.strip() for i in items_str.split(",")] s = ItemSet() for i in items: if len(i) > 0: s = s.add(i) return s
def parse_items_singleton(items_str): items = [i.strip() for i in items_str.split(",")] item_list = [] for i in items: if i != "": s = ItemSet() s = s.add(i) item_list.append(s) return item_list
def parse_starting_items(items): """Parses the CLI starting items into an ItemSet""" if items is None: return ItemSet() items = items.split() item_set = ItemSet() for item in items: item_def = item.rstrip("1234567890") item_set.add(item_def) return item_set
def remove_loops(path, starting_items, item_nodes): """ Simplify a path with cycles to create a minimal spoiler path """ # Add bosses to item_nodes item_nodes.update(boss_items) # Node name -> node neighbors nodes = defaultdict(list) item_set = starting_items # Build a mini-graph of states for i, node in enumerate(path[:-1]): if node in item_nodes: item_set |= ItemSet([item_nodes[node]]) state = (node, item_set) next_node = path[i + 1] if next_node in item_nodes: next_item_set = item_set | ItemSet([item_nodes[next_node]]) else: next_item_set = item_set next_state = (next_node, next_item_set) nodes[state].append(next_state) print(item_set) print([n for n in nodes if n[0] == path[-1]]) # Now BFS start = (path[0], starting_items) end = (path[-1], item_set) offers = {start: start} # Use a dict to avoid set hashing RNG (now that dicts order is determinisitic) finished = {start: None} queue = deque([start]) while len(queue) > 0: node = queue.popleft() if node == end: break for neighbor in nodes[node]: if neighbor not in finished: queue.append(neighbor) finished[neighbor] = None offers[neighbor] = node # Now decode offers to get the path assert end in finished out_path = [] current_node = end while current_node != start: out_path.append(current_node) current_node = offers[current_node] out_path.append(current_node) return out_path[::-1]
def mk_plm_to_item(item_defs): plm_to_item = {} for item in item_defs: for presentation in item_defs[item]: plm_id = int.from_bytes(item_defs[item][presentation], byteorder="little") iset = ItemSet([item]) plm_to_item[plm_id] = iset return plm_to_item
def __init__(self, node_, wildcards_=OrderedSet(), items_=ItemSet(), assignments_={}): self.node = node_ self.items = items_ self.wildcards = wildcards_ self.assignments = assignments_
def BFS_optimized(self, start_state, end_state=None, edge_pred=lambda x: True): """I don't care about every possible way to get everywhere - just BFS until you find end, noting that picking up items is always beneficial.""" n = 0 # key - node name # key - item set # value - state predecessor offers = collections.defaultdict(lambda: {}) # key - node_name # value - set of item sets finished = collections.defaultdict(set) final_state = None # queue to hold node, item pairs queue = [start_state] while len(queue) > 0: n += 1 state = queue.pop(0).copy() #if n % 10000 == 0: # print(n) # print(state) node = state.node items = state.items # we've reached the goal with at least the right items if end_state is not None and state >= end_state: final_state = state break # make an offer to pick up an item or defeat a boss node_data = self.name_node[node].data if isinstance(node_data, Item) or isinstance(node_data, Boss): new_items = items | ItemSet([node_data.type]) # if we haven't already visited this node with the new item set... if new_items not in finished[node]: offers[node][new_items] = state.copy() finished[node].add(new_items) # don't have to make a new queue item - pick up the item/boss is the only option # the following for-loop handles creating the new queue items... items = new_items # make an offer to every adjacent node reachable with this item set for edge in self.node_edges[node]: if edge.items.matches(items) and edge_pred( (node, edge.terminal)): # if we haven't already visited terminal with those items... if items not in finished[edge.terminal]: offers[edge.terminal][items] = state.copy() finished[edge.terminal].add(items) queue.append(BFSState(edge.terminal, items)) return offers, finished, final_state is not None, final_state
def network_flow(self, edge_weights, source, sink, items=ItemSet()): assert source in self.name_node assert sink in self.name_node # Edge weights is (node1, node2) -> non-negative float current_edge_weights = edge_weights.copy() start_state = BFSState(source, items) # TODO: can you pick up new items during the search? # TODO: is it correct to use BFS_optimized when we wish not to collect items? end_state = BFSState(sink, items) # Both refer to the (mutable) current edge weights edge_inf = lambda x: current_edge_weights[x] == infinity edge_nonzero = lambda x: current_edge_weights[x] > 0 # Check if there is a path of weight infinity from source to sink _, _, inf_path, _ = self.BFS_optimized(start_state, end_state, edge_pred=edge_inf) if inf_path: # No cut is possible since every cut has infinite weight return infinity, None # If there is no inf_path, then there is a finite-weight cut iteration = 0 while True: iteration += 1 o, f, p, _ = self.BFS_optimized(start_state, end_state, edge_pred=edge_nonzero) # No nonzero path means a cut has been found if not p: break # If there is a nonzero path, create regret along it path = bfs_backtrack(start_state, end_state, o) # List because we are going to re-use it path_pairs = list(zip(path, path[1:])) # The smallest edge limits the amount of possible flow regret = min([current_edge_weights[(u, v)] for u, v in path_pairs]) # Update the edge weights with the regret: #TODO: assumes current_edge_weights is complete for u, v in path_pairs: current_edge_weights[(u, v)] -= regret current_edge_weights[(v, u)] += regret # Find the cut by first finding the nodes reachable from the source _, f, _, _ = self.BFS_optimized(start_state, edge_pred=edge_nonzero) source_nodes = set(f.keys()) # All the edges which cross the boundary cut_edges = [(u, v.terminal) for u in source_nodes for v in self.node_edges[u] if v.terminal not in source_nodes] total_cut_weight = sum([edge_weights[e] for e in cut_edges]) return total_cut_weight, cut_edges
def parse_constraint(constraint): # BASE CASE # if it's not a symbol, then it's a variable if constraint[0] != "|" and constraint[0] != "&": # special case - bombs, power bombs, and springball all require morph ball if constraint == "B" or constraint == "PB" or constraint == "SPB": return MinSetSet(set([ItemSet(["MB", constraint])])) # special case - super missiles are sufficient for all missile requirements if constraint == "M": #TODO - does gravity suit stop environmental damage or not? return MinSetSet(set([ItemSet([constraint]), ItemSet(["S"])])) return MinSetSet(set([ItemSet([constraint])])) # RECURSIVE CASE else: symbol = constraint[0] # remove the symbol and the space, then find the top-level expressions constraint_list = find_expressions(constraint[2:]) # now that constraint_list has every top-level expression, parse it recursively! minset_list = [parse_constraint(i) for i in constraint_list] # now combine them with either AND or OR if symbol == "|": return reduce(lambda x, y: x + y, minset_list) elif symbol == "&": return reduce(lambda x, y: x * y, minset_list)
def BFS_target(self, start_state, end_state=None): #TODO: review this - does it really process every combo only once? # key - node name # key - item set # value - state predecessor offers = collections.defaultdict(lambda: {}) # key - node name # value - item set finished = collections.defaultdict(set) final_state = None # queue to hold node, item pairs queue = [start_state] while len(queue) > 0: state = queue.pop(0).copy() # we've reached the goal with at least the right items if end_state is not None and start_state >= end_state: final_state = state break node = state.node items = state.items # make an offer to every adjacent node reachable with this item set for edge in self.node_edges[node]: if edge.items.matches(items): # if we haven't already visited terminal with those items... if items not in finished[edge.terminal]: offers[edge.terminal][items] = state finished[edge.terminal].add(items) queue.append(BFSState(edge.terminal, items)) # make an offer to pick up an item or defeat a boss node_data = self.name_node[node].data if isinstance(node_data, Item) or isinstance(node_data, Boss): new_items = items | ItemSet([node_data.type]) # if we haven't already visited this node with the new item set... if new_items not in finished[node]: offers[node][new_items] = state finished[node].add(new_items) queue.append(BFSState(node, new_items)) return offers, finished, final_state is not None, final_state
def get_fixed_items(): """get the set of items whose locations cannot be wildcarded""" return ItemSet(sm_global.boss_types) | ItemSet(sm_global.special_types)
def BFS_items(self, start_state, end_state=None, fixed_items=ItemSet()): """Finds a satsifying assignment of items to reach end from start. finished[end] will wind up with a list of (unassigned but reached items, item set needed, and possible item assignments). Each assignment is a dictionary, where key = item node name, and value = string value for item assigned there. Currently does not allow items to be fixed, but an already-assigned items dictionary can be passed.""" #TODO: I think there's a way to make finished store less stuff - after all, we are only interested in keeping the # elements with a maximal NUMBER of wildcards for each item set. # Set of BFSItemsState # Ordered so that you can randomly choose from it without relying # on the internal hash function which is randomly salted. finished = OrderedSet() # Key - BFSItemsState (antecessor) # Value - BFSItemsState (predecessor) offers = {} # what items we actually needed to reach the end... final_state = None queue = [start_state] # queue search terms are: # - node name # - wildcard set # - item set # - assignment dictionary - key: item node, value: type assigned there # however two search terms are equal if the number of wildcards and the # obtained items are the same - that's why finished just includes the number # - add start node to the finished list #finished[start_state.node][start_state.items].append((start_state.wildcards.copy(), start_state.assignments.copy())) finished.add(start_state) while len(queue) > 0: state = queue.pop(0) #print("State: {}".format(state)) node = state.node wildcards = state.wildcards items = state.items assignments = state.assignments if end_state is not None and state >= end_state: final_state = state break node_data = self.name_node[node].data # In addition to fixed items, pass an assigments list and check it if isinstance(node_data, Item): # If we don't already have this item, pick it up as a wildcard if node not in wildcards and node not in assignments: new_state = BFSItemsState(node, wildcards | set([state.node]), items, assignments) if new_state not in finished: finished.add(new_state) offers[new_state] = state queue.append(new_state) # There's no need to process edges - picking up that item won't prevent you from crossing an edge continue # If we don't have the item but it was already assigned, pick it up as a fixed item elif node in assignments and assignments[node] not in items: new_state = BFSItemsState( node, wildcards, items | ItemSet([assignments[node]]), assignments) if new_state not in finished: finished.add(new_state) offers[new_state] = state queue.append(new_state) continue elif isinstance(node_data, Boss): # If we haven't defeated this boss yet, do so (as a fixed item) if node_data.type not in items: new_state = BFSItemsState( node, wildcards, items | ItemSet([node_data.type]), assignments) if new_state not in finished: finished.add(new_state) offers[new_state] = state queue.append(new_state) # There's no need to process edges - defeating that boss will allow you to cross strictly more edges continue # Now cross edges for edge in self.node_edges[state.node]: # For each set, use some wildcards to cross it, then add that node with those assignments to the queue for item_set in edge.items.sets: # Items in item set that we don't already have need_items = item_set - items #print("Need items: {}".format(need_items)) # Can cross the edge if we have enough wildcards to satisfy need_items and there are no fixed items that we do not already have (i.e. bosses) if len(need_items) <= len(wildcards) and len( need_items & fixed_items) == 0: #print("Can cross") wildcards_copy = wildcards.copy() items_copy = state.items.copy() assignments_copy = assignments.copy() # Make an assignment that allows crossing that edge for item in need_items: # Get the last available wildcard -> the latest one the player got. wildcard = wildcards_copy.pop() assignments_copy[wildcard] = item items_copy = items_copy.add(item) new_state = BFSItemsState(edge.terminal, wildcards_copy, items_copy, assignments_copy) #print("Items: {}, wildcards: {}".format(items_copy, wildcards_copy)) # If there's not already an entry for this item set with at least as many wildcards, then add it if new_state not in finished: finished.add(new_state) offers[new_state] = state queue.append(new_state) return offers, finished, final_state is not None, final_state
def main(arg_list): args = get_args(arg_list) # Hijack stdout for output if args.logfile is not None: sys.stdout = open(args.logfile, "w") seed = rng.seed_rng(args.seed) spoiler_file = open(args.create + ".spoiler.txt", "w") # Update the settings from JSON files if args.settings is not None: settings_parse.get_settings(settings.setting_paths, args.settings) # Setup # Copy it to remove Bombs # TODO: GET RID OF Bombs all_items = sm_global.items[:] all_items = ItemSet(all_items) escape_timer = 0 starting_items = parse_starting_items(args.starting_items) items_to_place = settings.items_to_item_list(settings.items) completable = False while not completable: #TODO: re-parsing rooms is quick and dirty... if args.hard_mode: rooms = parse_rooms.parse_rooms("encoding/dsl/rooms_hard.txt") else: rooms = parse_rooms.parse_rooms("encoding/dsl/rooms.txt") # Phantoon means an extra L door - mercilessly destroy the maridia map station if args.doubleboss: del rooms["Maridia_Map"] # Remove the double boss rooms else: second_boss_rooms = ["Kraid2", "Phantoon2", "Draygon2", "Ridley2"] for boss_room in second_boss_rooms: if boss_room in rooms: del rooms[boss_room] door_changes, item_changes, graph, state, path = item_quota_rando( rooms, args.debug, starting_items, items_to_place[:]) # Check completability - can reach statues? start_state = BFSState(state.node, state.items) # This takes too long #start_state = BFSState("Landing_Site_R2", ItemSet()) end_state = BFSState("Statues_ET", ItemSet()) path_to_statues = graph.check_completability(start_state, end_state) final_path = path_to_statues escape_path = None completable = path_to_statues is not None if completable: final_path = path + path_to_statues final_path = remove_loops(final_path, starting_items, {k: v for k, v in item_changes}) print(final_path[-1]) # Check completability - can escape? items = all_items | ItemSet( ["Kraid", "Phantoon", "Draygon", "Ridley"]) prepare_for_escape(graph) escape_start = BFSState("Escape_4_R", items) escape_end = BFSState("Landing_Site_L2", items) escape_path = graph.check_completability(escape_start, escape_end) if escape_path is None: completable = False else: # One minute to get out of tourian, then 30 seconds per room #TODO: Is this fair? the player might need to farm and explore... #TODO: Simple node-length means intermediate nodes / etc. will cause problems # give the player time to defeat minibosses, or go through long cutscenes for node in escape_path: if node in settings.escape: escape_timer += (settings.escape[node] - 2 * settings.escape["per_node"]) escape_timer += settings.escape[ "tourian"] + settings.escape["per_node"] * len(escape_path) # Accept the seed regardless if we don't care about completability if not args.completable: break # Re-seed the rng for a new map (if we need to) if not completable and args.completable: print("Not Completable") seed = rng.seed_rng(None) print("Completable: " + str(completable)) print("RNG SEED - " + str(seed)) # Write the seed spoiler_file.write("RNG Seed: {}\n".format(str(seed))) spoiler_file.write("Items Placed: {}\n".format(str(items_to_place))) # Write the escape path if escape_path is not None: spoiler_file.write("Path to Escape:\n") spoiler_file.write(str(escape_path)) spoiler_file.write("\n") spoiler_file.write("Esape Timer: {} seconds\n".format(escape_timer)) # Write the path to the statues (including every boss) spoiler_file.write("Path to Statues:\n") if final_path is not None: pretty_print_out_path(spoiler_file, final_path) #spoiler_file.write(str(final_path)) spoiler_file.write("\n") # Write the items, doors etc. spoiler_file.write("ITEMS:\n") write_item_assignments(item_changes, spoiler_file) spoiler_file.write("DOORS:\n") write_door_changes(door_changes, spoiler_file) spoiler_file.close() # Make the spoiler graph if args.graph: from door_rando import spoiler_graph spoiler_graph.make_spoiler_graph(door_changes, args.create) # Now that we have the door changes and the item changes, implement them! # First, make the new rom file: rom = RomManager(args.clean, args.create) # Make the rest of the necessary changes rom.set_escape_timer(escape_timer) if args.starting_items is not None: make_starting_items(args.starting_items, rom) # Apply teleportation patch if args.noescape: rom.apply_ips("patches/teleport_refill.ips") else: rom.apply_ips("patches/teleport.ips") # Then make the necessary changes make_items(item_changes, rom) extra_from, extra_to = logic_improvements(rom, args.g8, args.doubleboss) make_doors(door_changes, rom, extra_from, extra_to) make_saves(door_changes, rom, extra_from) fix_skyscroll(door_changes, rom, extra_from) # Logic improvements must happen last since they may # copy PLMs, which can be edited via prior changes # Save out the rom rom.save_and_close() # Collect output info out = {} out["seed"] = str(seed) return out
def __init__(self, set_=None): if set_ is None: self.sets = set([ItemSet()]) else: self.sets = set_
#TODO: how to handle super block in a tunnel? You don't necessarily need stand to destroy a super block... #TODO: a "plant power bomb 'action' as part of the BFS? -> a fire super missile action... # Gets very complex very quickly though... any_velocity_type = set([VType.RUN, VType.SPEED, VType.WATER]) any_pose = set( [SamusPose.STAND, SamusPose.MORPH, SamusPose.JUMP, SamusPose.SPIN]) any_interval = Interval(-Infinity, Infinity) any_velocity = VelocitySet(any_interval, HVelocitySet(any_velocity_type, any_interval)) # The requirements to treat blocks as solid # Cannot treat a tile as solid if either it is not in this data structure, or samus does not meet one of # the necessary requirements block_solid_requirements = { AbstractTile.SOLID: [(any_velocity, ItemSet([]), any_pose)], AbstractTile.BLOCK_CRUMBLE: "Reciprocal", # Can treat a shot block as either solid or air depending on the situation AbstractTile.BLOCK_SHOT: [(any_velocity, ItemSet([]), any_pose)], } speed_hv = HVelocitySet(set([VType.SPEED]), Interval(30, Infinity)) # Exposing a weakness of velocity sets as single-sided intervals speed_velocity_l = VelocitySet(any_interval, speed_hv) speed_velocity_r = VelocitySet(any_interval, speed_hv.horizontal_flip()) # Negative vertical speed and no horizontal speed crumble_velocity = VelocitySet(Interval(0, Infinity), HVelocitySet(any_velocity_type, Interval(0, 0))) # The requirements to treat blocks as air
def __init__(self, node_, items_=ItemSet()): self.node = node_ self.items = items_
def get_exit_items(current_state, exits): exit_items = ItemSet() for exit in exits: new_items = exit.items - current_state.items exit_items |= new_items return exit_items