def make_level(image): level_array = np.zeros(image.size, dtype="int") player_before_pos = None player_after_pos = None lowest_air = -float("inf") highest_liquid = float("inf") liquid_type = LiquidType.NONE item_locations = [] for xy in Rect(Coord(0,0), Coord(image.size[0], image.size[1])): p = image.getpixel((xy.x, xy.y)) m = color_to_abstract[p] if p in player_before_colors and player_before_pos is None: player_before_pos = xy if p in player_after_colors and player_after_pos is None: player_after_pos = xy if p in air_colors and xy.y > lowest_air: lowest_air = xy.y if p in water_colors and xy.y < highest_liquid: liquid_type = LiquidType.WATER highest_liquid = xy.y if p == item_color: item_locations.append(xy) level_array[(xy.x, xy.y)] = m #TODO: other liquids # Water level has to be /strictly/ below the lowest air, # and at least as high as the highest liquid liquid_interval = Interval(lowest_air + 1, highest_liquid) # Put the liquid as low as possible level = LevelState(Coord(0,0), level_array, LiquidType.WATER, highest_liquid, {}, {}) if player_after_pos is None: player_after_pos = player_before_pos.copy() return player_before_pos, player_after_pos, level, liquid_interval, liquid_type, item_locations
def collide(self, ds, debug=False): s = self.copy() v = self.velocity for d in ds: # Landing on the ground if d == Coord(0, 1): vh = HVelocity(VType.RUN, 0) vv = 0 v = Velocity(vv, vh) s.velocity = v # Any non-morph pose leaves you standing #TODO: what if you're in spin in a 2-high gap -- this will clip you into the floor!! if s.pose == SamusPose.SPIN: s.pose = SamusPose.STAND s.position += Coord(0, -1) elif s.pose != SamusPose.MORPH: s.pose = SamusPose.STAND # Colliding with the ceiling elif d == Coord(0, -1): vv = 0 v = Velocity(vv, s.velocity.vh.copy()) s.velocity = v # Colliding with a wall (kill horizontal velocity) else: vh = HVelocity(VType.RUN, 0) v = Velocity(s.velocity.vv, vh) s.velocity = v if debug: print("Collided") print(s) return s
def horizontal_flip(self): vnew = self.vfunction.horizontal_flip() # Flip the after position horizontally too pnew = Coord(-self.after_position.x, self.after_position.y) # Flip the set of items obtained new_items = [Coord(-g.x, g.y) for g in self.gain_items] return SamusFunction(vnew, self.required_items, new_items, self.before_pose, self.after_pose, pnew)
def extent(coords): """Returns the smallest rectangle that contains each of the coords""" if len(coords) == 0: return None minx = min(coords, key=lambda item: item.x).x miny = min(coords, key=lambda item: item.y).y maxx = max(coords, key=lambda item: item.x).x maxy = max(coords, key=lambda item: item.y).y return Rect(Coord(minx, miny), Coord(maxx, maxy) + Coord(1, 1))
def find_all_rects(space): all_rects = set() for p in space: p_rects = find_all_rects_at(space, p, directions=[Coord(1, 0), Coord(0, 1)]) all_rects |= p_rects return all_rects
def horizontal_flip(self): pos = Coord(-self.pos.x, self.pos.y) walls = [Coord(-w.x, w.y) for w in self.walls] airs = [(Coord(-d.x, d.y), [Coord(-t.x, t.y) for t in tiles]) for d, tiles in self.airs] if self.samusfunction is None: sf = None else: sf = self.samusfunction.horizontal_flip() return IntermediateState(pos, walls, airs, sf)
def get_cross(p): """ Cross-shaped group of coords centered on p """ directions = [ Coord(0, 0), Coord(1, 0), Coord(-1, 0), Coord(0, 1), Coord(0, -1) ] return [p + d for d in directions]
def pretty_print(self, samus_states, image_folder): tile_size = 16 scale_factor = Coord(tile_size, tile_size) center = Coord(tile_size // 2, tile_size // 2) image_dict = parse_image_folder(image_folder) i = Image.new("RGB", (self.shape[0] * tile_size, self.shape[1] * tile_size)) pixels = i.load() # Draw the level for xy in self.rect: pos = (xy - self.origin) * scale_factor # Have to convert pos from coord to tuple for some reason i.paste(image_dict[abstract_to_image[self[xy]]], (pos[0], pos[1])) # Draw the samusstates # Draw s0 arbitrarily first if len(samus_states) > 0: s0_i = image_dict[samus_state_to_image(samus_states[0], None)] pos = (samus_states[0].position - self.origin) * scale_factor i.paste(s0_i, (pos[0], pos[1])) # Samus facing direction depends on context of previous state for s1, s2 in pairwise(samus_states): s_i = image_dict[samus_state_to_image(s2, s1)] pos = (s2.position - self.origin) * scale_factor i.paste(s_i, (pos[0], pos[1])) # Draw the items (over the samuses, to make them visible, under the arrows) for pos, item in self.items.items(): if item in item_to_image: item_image = image_dict[item_to_image[item]] else: # Handles bosses, etc. item_image = image_dict["item_unknown.png"] real_pos = pos * scale_factor i.paste(item_image, (real_pos[0], real_pos[1])) # Convert to numpy to draw arrows using CV2 numpy_i = np.array(i) # Draw the arrows connecting the samusstates for index, (s1, s2) in enumerate(pairwise(samus_states)): p1 = s1.position * scale_factor + center p2 = s2.position * scale_factor + center # cv2 does not have fixed size tiplengths length = p2.euclidean(p1) if length == 0: tiplength = 0 else: tiplength = 10 / length percent = index / len(samus_states) c_index = int(255 * percent) color = (c_index, 0, 255 - c_index) cv2.arrowedLine(numpy_i, p1, p2, color, 2, tipLength=tiplength) # Convert back to PIL i = Image.fromarray(numpy_i) return i
def get_extra_fixed_tiles(rect, fixed_tiles, extra_similarity): assert extra_similarity >= 0 assert extra_similarity <= 1 border_tiles = set() for x in range(rect.start.x, rect.end.x): border_tiles.add(Coord(x, rect.start.y)) border_tiles.add(Coord(x, rect.end.y - 1)) for y in range(rect.start.y, rect.end.y): border_tiles.add(Coord(rect.start.x, y)) border_tiles.add(Coord(rect.end.x - 1, y)) free_tiles = rect.as_set() - (fixed_tiles | border_tiles) n_fixed = int(len(free_tiles) * extra_similarity) extra_fixed_tiles = set(random.sample(free_tiles, n_fixed)) return extra_fixed_tiles | border_tiles
def wfc_level_data(room_header, auto_rect=False, rects=None, extra_similarity=0, seed=None): if seed is not None: random.seed(seed) # Get fns before messing with the level data fns = get_state_functions(room_header) fixed_tiles, screen_map = get_fixed_tiles(room_header) default_level_data = room_header.state_chooser.default.level_data level_shape = Coord(*default_level_data.level_array.layer1.shape) level_bits = bit_array_from_bytes(default_level_data.level_bytes, level_shape) # Get the rectangularization either automatically, by input, or just the whole level at once if auto_rect: assert rects is None # Scale them back up to size 16x16 (rects found are rects of screens) #TODO: choose tiles based on ratio of tiles / pattern (higher is better) rects = [ rect.scale(16) for rect in wfc_rectangularize(screen_map, max_area=9) ] print("Found rectangles: {}".format(rects)) # Use one big rect if none is satisfied elif rects is None: offset = Coord(0, 0) size = Coord(*level_shape) rects = [Rect(offset, size)] # Use WFC to reconstruct each rect using tiles from within that rect #TODO: how to use tiles from the entire room? for rect in rects: print("For rectangle {}:".format(rect)) # Add borders etc. extra_fixed_tiles = get_extra_fixed_tiles(rect, fixed_tiles, extra_similarity) all_fixed_tiles = fixed_tiles | extra_fixed_tiles rect_bits = level_bits[rect.start[0]:rect.end[0], rect.start[1]:rect.end[1]] rect_fixed_tiles = rel_fixed_tiles(rect, all_fixed_tiles) #print(rect_fixed_tiles) print("Fixed tile ratio: {}".format(len(rect_fixed_tiles) / rect.area)) wfc_bits = bit_wfc(rect_bits, rect_fixed_tiles) # Overwrite level_bits with the new data level_bits[rect.start[0]:rect.end[0], rect.start[1]:rect.end[1]] = wfc_bits level = level_from_bits(level_bits) return level, fns
def random_node_place(graph, dimensions, up_es, down_es): """ Returns a dictionary of node -> location by choosing locations for the nodes at random. """ r = Rect(Coord(0, 0), dimensions) # Use as_list since set() is forbidden for ordering xys = r.as_list() locs = random.sample(xys, graph.nnodes) node_list = graph.nodes.keys() node_locs = {} # choose elevator locations: down elevators are the lowest locs, and up are the highest locs #TODO: seems like it doesn't always return the lowest n or highest n points sorted_locs = sorted(locs, key=lambda n: n.y) for node in node_list: if node in up_es: node_locs[node] = sorted_locs.pop( 0) # highest y coordinate is further down elif node in down_es: node_locs[node] = sorted_locs.pop() node_list = [ node for node in node_list if node not in up_es and node not in down_es ] random.shuffle(node_list) #TODO: this is where we can make sure there is an item behind Draygon or some such?? # -> pick an item node later in the order than Draygon and put it past Draygon? # -> default to supers for i in range(len(node_list)): if node_list[i] not in node_locs: node_locs[node_list[i]] = sorted_locs[i] return node_locs
def compose(self, other, offset=Coord(0,0), collision_policy="error"): """Returns a new cmap which is a composition of self and other. If self and other share a maptile, then the collision policy decides.""" new_tiles = {} for c, t in self.items(): # Use a copy to avoid duplicates new_tiles[c] = t.copy() for c, t in other.items(): c_o = c + offset if not self.in_bounds(c_o) or c_o in new_tiles: # 'defer' means tiles from self are preferred over tiles from other if collision_policy == "defer": continue # 'error' means bomb out when there's a conflict elif collision_policy == "error": assert False, "Collision in compose: " + str(c) # 'none' means to not produce a composed cmap if there is a conflict elif collision_policy == "none": return None else: assert False, "Bad collision policy: " + collision_policy else: new_tiles[c_o] = t.copy() #TODO: which dimensions does it get? return ConcreteMap(self.dimensions, _tiles=new_tiles)
def parse_statefunction(name, level_image, d): #print("Parsing rule: {}".format(name)) b_pos, a_pos, level, liquid_interval, liquid_type, item_locations = make_level(level_image) rel_pos = a_pos - b_pos scan_direction = (a_pos - b_pos).sign() r = Rect(Coord(0,0), Coord(level.shape[0], level.shape[1])) vf = parse_vfunction(d) p_initial = Coord(0,0) v_final = vf.domain pose_initial = pose_str[d["b_pose"]] pose_final = pose_str[d["a_pose"]] items_initial = parse_items(d["items"]) sfunction = SamusFunction(vf, items_initial, item_locations, pose_initial, pose_final, rel_pos) #TODO: certain (i.e. the first of each rule) IntermediateStates hold the sfunction so that samus' state # can be inferred within the rule #TODO: verify by checking that the inferred end state is the same as the end state achieved through function # composition i_states = [] for xy in r.iter_direction(scan_direction): s_t = samus_tiles(xy, pose_final) level_t = [index_level(level, t) for t in s_t] #If samus is here through the rule if all([t == AbstractTile.AIR for t in level_t]): # Note the position, nearby airs, and nearby walls all_adj = get_all_adj(xy, pose_final) walls = get_all(level, all_adj, AbstractTile.SOLID) # Normalized walls = normalize_list(walls, xy) airs = [] # Collect the necessary airs for each direction for direction in [Coord(scan_direction.x, 0), Coord(0, scan_direction.y)]: airs_in_d = get_all(level, get_adj(xy, pose_final, direction), AbstractTile.AIR) airs_in_d = normalize_list(airs_in_d, xy) airs.append((direction, airs_in_d)) # The position of this intermediate state relative to the origin rel_pos = xy - b_pos i_states.append(IntermediateState(rel_pos, walls, airs, None)) if len(i_states) > 0: # Applies the function at the beginning of the rule i_states[0].samusfunction = sfunction lfunction = LevelFunction(liquid_type, liquid_interval, i_states) cost = int(d["cost"]) s = StateFunction(name, sfunction, lfunction, cost) if "Symmetric" not in d: return [s, s.horizontal_flip()] else: return [s]
def get_tile(level, origin, tile): index = tile - origin try: # Do not allow negative indexing if Coord(0, 0) > index: return None return level[index] # Do not allow oob indexing except IndexError: return None
def get_item_locations(plms): item_locations = {} plm_to_item = mk_plm_to_item(item_definitions.make_item_definitions()) for plm in plms.l: print(hex(plm.plm_id)) if plm.plm_id in plm_to_item: iset = plm_to_item[plm.plm_id] c = Coord(plm.xpos, plm.ypos) item_locations[c] = iset return item_locations
def sub(self, rect, relative=False): """Returns a subcmap which is the cmap defined by the [start, end) rectangle.""" new_tiles = {} if relative: offset = rect.start else: offset = Coord(0, 0) for c in rect.as_list(): if c in self: new_tiles[c - offset] = self[c] # new cmap's dimensions are end-start return ConcreteMap(rect.size_coord(), _tiles=new_tiles), rect.start
def find_all_rects_at(positions, pos, directions=coord_directions): assert pos in positions i_rect = Rect(pos, pos + Coord(1, 1)) finished = set([i_rect]) q = [i_rect] while len(q) > 0: r = q.pop() expands = expand_rect(positions, r, directions) for e in expands: if e not in finished: finished.add(e) q.append(e) return finished
def parse_pattern(pattern_filename): f = open(pattern_filename, "r") tiles = {} max_tile = Coord(0, 0) max_area = 0 for y, line in enumerate(f.readlines()): for x, entry in enumerate(line.split()): assert entry[0] == "[" assert entry[-1] == "]" entry = entry[1:-1] es = entry.split(",") # Assume BTS if len(es) == 2: bts = 0 elif len(es) == 3: bts = int(es[2], 16) else: assert False, "Bad entry: " + entry # Find the texture if es[0] == "_": texture = Texture(0, 0, is_any=True) else: tex_index, flips = find_flips(es[0]) texture = Texture(tex_index, flips) # Find the type if es[1] == "_": tile_type = Type(0, 0, is_any=True) else: ty_index = int(es[1], 16) ty = Type(ty_index, bts) c = Coord(x, y) a = c.area() tiles[c] = Tile(texture, ty) if a >= max_area: max_tile = c max_area = a f.close() level = Level(max_tile + Coord(1, 1), tiles=tiles) return level
def spring_model(node_locs, graph, n_iterations, spring_constant, spring_equilibrium, dt, damping): """Changes node placement based on a simple spring model. Node locs is the dictionary of initial node placements and is edited by the function. graph is the graph of how the nodes are connected to each other (i.e. where to place springs). n_iterations is how many time steps to run the model for. A few should suffice for my purposes. spring_constant is the k in -kx. spring_equilibrium is the distance where the spring is at rest. dt is the time step. Returns a lower potential-energy node_locs.""" # node locs is node_name -> node_position # node_name -> node_velocity # using mcoords as a vector node_v = {n: Coord(0, 0) for n in node_locs} # node_name -> node_acceleration node_a = {n: Coord(0, 0) for n in node_locs} iteration = 0 while iteration < n_iterations: for n in node_locs: # Update node_v for e in graph.nodes[n].edges: t = e.terminal # The amount of stretching, possibly negative x = euclidean(node_locs[n], node_locs[t]) - spring_equilibrium # Direction n_to_t = (node_locs[t] - node_locs[n]).to_unit() #-kx node_v[n] = node_v[n] + n_to_t.scale(spring_constant * x * dt) node_v[t] = node_v[t] + n_to_t.scale(-spring_constant * x * dt) # update node_locs node_locs[n] = node_locs[n] + node_v[n].scale(dt) iteration = iteration + 1 # resolve to an int # TODO: is there a dict map function? node_locs = {n: node_locs[n].resolve_int() for n in node_locs} return node_locs
def load_patterns(path): # Get all the filenames in path fnames = [ f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) ] patterns = {} for f in fnames: name, ext = f.split(".") if ext == "txt": p = parse_pattern(os.path.join(path, f)) #TODO: create a flag that can be written in the file for whether to include a flip patterns[name + "_l"] = p patterns[name + "_r"] = p.reflect(Coord(1, 0)) return patterns
def expand_rect(positions, rect, directions=coord_directions): """ Helper for find_rect """ #TODO: randomize order? rects = [] for d in directions: if d < Coord(0, 0): r = Rect(rect.start + d, rect.end) else: r = Rect(rect.start, rect.end + d) # Need the resulting expanded rect to be a subset of the space if r.as_set() <= positions: rects.append(r) return rects
def node_place(graph, dimensions, up_es, down_es, settings): """ Find a placement for nodes where their respective areas do not violate each other. Random initially, with elevators guaranteed to be at the top and bottom (initially) Subjected to a spring model which reduces the total potential energy (moves far nodes closer) Subject to a search so that nodes can be placed without violating each other's areas. """ initial = random_node_place(graph, dimensions, up_es, down_es) spring = spring_model(initial, graph, settings["n_iterations"], settings["spring_constant"], settings["equilibrium"], settings["spring_dt"], settings["spring_damping"]) trunc_spring = { n: xy.truncate(Coord(0, 0), dimensions) for (n, xy) in spring.items() } # Now do a search for a good placement for each nod. cmap = ConcreteMap(dimensions) return find_placement(trunc_spring, cmap, up_es, down_es)
def level_from_bytes(levelbytes, dimensions): # First two bytes are the amount of level1 data levelsize = int.from_bytes(levelbytes[0:2], byteorder='little') # Cut off the size levelbytes = levelbytes[2:] # Make sure everything matches assert levelsize % 2 == 0, "Purported level size {} is not even!".format( levelsize) assert levelsize == dimensions.x * dimensions.y * 2, "Level data length {} does not match specified room dimensions {}".format( levelsize, dimensions.x * dimensions.y * 2) # The level might not include level2 data if len(levelbytes) == int(2.5 * levelsize): has_level2 = True elif len(levelbytes) == int(1.5 * levelsize): has_level2 = False else: assert False, "Purported level size does not match actual level size" level = Level(dimensions) for y in range(dimensions.y): for x in range(dimensions.x): index = y * dimensions.x + x level1index = index * 2 level1 = int.from_bytes(levelbytes[level1index:level1index + 2], byteorder='little') btsindex = index + levelsize bts = int.from_bytes(levelbytes[btsindex:btsindex + 1], byteorder='little') if has_level2: level2index = index + (3 * levelsize // 2) level2 = int.from_bytes(levelbytes[level2index:level2index + 2], byteorder='little') else: level2 = 0 #TODO: level2 info dropped on the floor ttype = level1 >> 12 hflip = (level1 >> 11) & 1 vflip = (level1 >> 10) & 1 tindex = level1 & 0b1111111111 texture = Texture(tindex, (hflip, vflip)) tiletype = Type(ttype, bts) level[Coord(x, y)] = Tile(texture, tiletype) return level
def make_level_from_room(room_header): #TODO: is there a way to know which state is currently in use? state = room_header.state_chooser.default # Get layer1: larray = state.level_data.level_array layer1 = larray.layer1 bts = larray.bts level_array = np.zeros(layer1.shape, dtype="int") it = np.nditer(layer1, flags=["multi_index", "refs_ok"]) # Abstractify level data for tile in it: tile = tile.item() i = it.multi_index # Get the up and left tiles for v and h copy x = i[0] y = i[1] up = None left = None if y > 0: up = level_array[x, y-1] if x > 0: left = level_array[x-1, y] tile_bts = bts[i] level_array[i] = tile_to_abstract(tile, tile_bts, up, left, i) #TODO: May not use default FX depending on door #TODO: Door cap PLMs change level data! fx = state.fx.default_fx if fx is not None and fx.layer3_type in layer3_to_liquid: liquid_type = layer3_to_liquid[fx.layer3_type] #TODO - tile or pixel? liquid_level = fx.liquid_target_y // 16 else: liquid_type = LiquidType.NONE liquid_level = None #TODO item_locations = get_item_locations(state.plms) level = LevelState(Coord(0,0), level_array, liquid_type, liquid_level, item_locations, {}) return level
def rectangularize(subroom_state, cmap): """ Finds a set of initial rectangular subrooms for the given concrete map """ position_order = list(cmap.keys()) random.shuffle(position_order) positions = set(cmap.keys()) # Gross way of getting the main set of tiles assert len(subroom_state.g.nodes.keys()) == 1 # Main subroom is always initially subroom 0 main_subroom = subroom_state[0] # Find rectangles until the entire cmap is covered rects = [] print("RECT: Finding rectangles") while len(positions) > 0: pos = position_order[0] rect = find_rect_at(positions, pos) # Only need to create a subroom if the remaining cmap component that # contains it also contains other cells cmap_components = find_components(positions) r_set = rect.as_set() r_components = [c for c in cmap_components if r_set <= c] assert len(r_components) == 1 r_component = r_components[0] #TODO: rects are not relative to the room position, but they should be... print(rect) rects.append(rect) if r_set < r_component: # Find the subroom id for the component in question t = next(iter(r_component)).scale(16) + Coord(8, 8) component_parent_sid = subroom_state.tile_to_subroom(t) rect_room = rect.scale( 16).as_set() & subroom_state[component_parent_sid].tiles subroom_state.place_subroom(rect_room) # Update positions for c in rect: positions.remove(c) position_order.remove(c)
def paste(self, level_origin, level): assert self.origin + self.shape <= Coord(level.shape[0], level.shape[1]) o = self.origin - level_origin assert o >= Coord(0, 0), o level[o.x:o.x + self.shape.x, o.y:o.y + self.shape.y] = self.level
def get_fixed_tiles(room_header): """Compute the set of tiles that should be fixed for WFC for a given room header""" fixed_tiles = set() screen_map = set() all_states = room_header.all_states() #all_states = [s.state for s in room_header.state_chooser.conditions] + [room_header.state_chooser.default] shapes = [ state.level_data.level_array.layer1.shape for state in all_states ] # Hopefully all level datas have the same shape... assert len(set(shapes)) == 1 for state in all_states: # Add doors layer1 = state.level_data.level_array.layer1 def inbounds(c): if c[0] < 0 or c[1] < 0: return False if c[0] >= layer1.shape[0] or c[1] >= layer1.shape[1]: return False return True it = np.nditer(layer1, flags=["multi_index", "refs_ok"]) for tile in it: tile = tile.item() if tile.tile_type == 9: fixed_tiles.add(Coord(*it.multi_index)) # Add all enemy positions #TODO: Some enemies may take up more than one tile... #TODO: PLMs may want to have more than one tile allocated too for enemy in state.enemy_list.enemies: e_pos = Coord(enemy.x_pos // 16, enemy.y_pos // 16) enemy_locale = get_cross(e_pos) for p in enemy_locale: if inbounds(p): fixed_tiles.add(p) # Add PLM positions for plm in state.plms.l: plm_pos = Coord(plm.x_pos, plm.y_pos) plm_locale = get_cross(plm_pos) for p in plm_locale: if inbounds(p): fixed_tiles.add(p) # Add screens that are either fully solid or fully air for x in range(layer1.shape[0] // 16): for y in range(layer1.shape[1] // 16): screen_x = x * 16 screen_y = y * 16 screen = layer1[screen_x:screen_x + 16, screen_y:screen_y + 16] test_solid = np.vectorize(lambda x: x.tile_type == 8) test_air = np.vectorize(lambda x: x.tile_type == 0) if np.all(test_solid(screen)) or np.all(test_air(screen)): # Add this screen and its borders for x_sub in range(-1, 17): for y_sub in range(-1, 17): c = (screen_x + x_sub, screen_y + y_sub) if inbounds(c): fixed_tiles.add(Coord(*c)) # Add all "valid" screens to the screen map else: screen_map.add(Coord(x, y)) # Add level borders for x in range(layer1.shape[0]): fixed_tiles.add(Coord(x, 0)) fixed_tiles.add(Coord(x, layer1.shape[1] - 1)) for y in range(layer1.shape[1]): fixed_tiles.add(Coord(0, y)) fixed_tiles.add(Coord(layer1.shape[0] - 1, y)) #TODO: constrain scroll boundaries? return fixed_tiles, screen_map
def level_from_bits(bits): new_bytes = bytes_from_bit_array(bits) new_arrays = level_array_from_bytes(new_bytes, Coord(*bits.shape[:-1])) return new_bytes, new_arrays
def __getitem__(self, index): internal_index = index - self.origin # Do not allow negative indexing if Coord(0, 0) > internal_index: raise IndexError(internal_index) return self.level[internal_index]
def horizontal_flip(self, level): position = self.position.flip_in_rect(level.rect, Coord(1, 0)) v = self.velocity.horizontal_flip() i = self.items.copy() p = self.pose return SamusState(position, v, i, p)