diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index fdf0d84..f548709 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -9,10 +9,10 @@ GameEngine::GameEngine() { Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::game = this; - window_title = "McRogueFace - 7DRL 2024 Engine Demo"; + window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close); visible = window.getDefaultView(); - window.setFramerateLimit(30); + window.setFramerateLimit(60); scene = "uitest"; scenes["uitest"] = new UITestScene(this); @@ -63,7 +63,10 @@ void GameEngine::run() currentFrame++; frameTime = clock.restart().asSeconds(); fps = 1 / frameTime; - window.setTitle(window_title + " " + std::to_string(fps) + " FPS"); + int whole_fps = (int)fps; + int tenth_fps = int(fps * 100) % 10; + //window.setTitle(window_title + " " + std::to_string(fps) + " FPS"); + window.setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); } } diff --git a/src/scripts/cos_entities.py b/src/scripts/cos_entities.py new file mode 100644 index 0000000..ed916f7 --- /dev/null +++ b/src/scripts/cos_entities.py @@ -0,0 +1,203 @@ +import mcrfpy +#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) +t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) +#def iterable_entities(grid): +# """Workaround for UIEntityCollection bug; see issue #72""" +# entities = [] +# for i in range(len(grid.entities)): +# entities.append(grid.entities[i]) +# return entities + +class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bugs workarounds + def __init__(self, g:mcrfpy.Grid, x=0, y=0, sprite_num=86, *, game): + #self.e = mcrfpy.Entity((x, y), t, sprite_num) + #super().__init__((x, y), t, sprite_num) + self._entity = mcrfpy.Entity((x, y), t, sprite_num) + #grid.entities.append(self.e) + self.grid = g + #g.entities.append(self._entity) + self.game = game + self.game.add_entity(self) + + ## Wrapping mcfrpy.Entity properties to emulate derived class... see issue #76 + @property + def draw_pos(self): + return self._entity.draw_pos + + @draw_pos.setter + def draw_pos(self, value): + self._entity.draw_pos = value + + @property + def sprite_number(self): + return self._entity.sprite_number + + @sprite_number.setter + def sprite_number(self, value): + self._entity.sprite_number = value + + def __repr__(self): + return f"" + + def die(self): + # ugly workaround! grid.entities isn't really iterable (segfaults) + for i in range(len(self.grid.entities)): + e = self.grid.entities[i] + if e == self._entity: + #if e == self: + self.grid.entities.remove(i) + break + else: + print(f"!!! {self!r} wasn't removed from grid on call to die()") + + def bump(self, other, dx, dy, test=False): + raise NotImplementedError + + def do_move(self, tx, ty): + """Base class method to move this entity + Assumes try_move succeeded, for everyone! + from: self._entity.draw_pos + to: (tx, ty) + calls ev_exit for every entity at (draw_pos) + calls ev_enter for every entity at (tx, ty) + """ + old_pos = self.draw_pos + self.draw_pos = (tx, ty) + for e in self.game.entities: + if e is self: continue + if e.draw_pos == old_pos: e.ev_exit(self) + for e in self.game.entities: + if e is self: continue + if e.draw_pos == (tx, ty): e.ev_enter(self) + + + def ev_enter(self, other): + pass + + def ev_exit(self, other): + pass + + def try_move(self, dx, dy, test=False): + x_max, y_max = self.grid.grid_size + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + #for e in iterable_entities(self.grid): + + # sorting entities to test against the boulder instead of the button when they overlap. + for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True): + if e.draw_pos == (tx, ty): + #print(f"bumping {e}") + return e.bump(self, dx, dy) + + if tx < 0 or tx >= x_max: + return False + if ty < 0 or ty >= y_max: + return False + if self.grid.at((tx, ty)).walkable == True: + if not test: + #self.draw_pos = (tx, ty) + self.do_move(tx, ty) + return True + else: + #print("Bonk") + return False + + def _relative_move(self, dx, dy): + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + #self.draw_pos = (tx, ty) + self.do_move(tx, ty) + +class PlayerEntity(COSEntity): + def __init__(self, *, game): + #print(f"spawn at origin") + self.draw_order = 10 + super().__init__(game.grid, 0, 0, sprite_num=84, game=game) + + def respawn(self, avoid=None): + # find spawn point + x_max, y_max = g.size + spawn_points = [] + for x in range(x_max): + for y in range(y_max): + if g.at((x, y)).walkable: + spawn_points.append((x, y)) + random.shuffle(spawn_points) + ## TODO - find other entities to avoid spawning on top of + for spawn in spawn_points: + for e in avoid or []: + if e.draw_pos == spawn: break + else: + break + self.draw_pos = spawn + + def __repr__(self): + return f"" + + +class BoulderEntity(COSEntity): + def __init__(self, x, y, *, game): + self.draw_order = 8 + super().__init__(game.grid, x, y, 66, game=game) + + def bump(self, other, dx, dy, test=False): + if type(other) == BoulderEntity: + #print("Boulders can't push boulders") + return False + #tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + # Is the boulder blocked the same direction as the bumper? If not, let's both move + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + if self.try_move(dx, dy, test=test): + if not test: + other.do_move(*old_pos) + #other.draw_pos = old_pos + return True + +class ButtonEntity(COSEntity): + def __init__(self, x, y, exit_entity, *, game): + self.draw_order = 1 + super().__init__(game.grid, x, y, 42, game=game) + self.exit = exit_entity + + def ev_enter(self, other): + print("Button makes a satisfying click!") + self.exit.unlock() + + def ev_exit(self, other): + print("Button makes a disappointing click.") + self.exit.lock() + + def bump(self, other, dx, dy, test=False): + #if type(other) == BoulderEntity: + # self.exit.unlock() + # TODO: unlock, and then lock again, when player steps on/off + if not test: + other._relative_move(dx, dy) + return True + +class ExitEntity(COSEntity): + def __init__(self, x, y, bx, by, *, game): + self.draw_order = 2 + super().__init__(game.grid, x, y, 45, game=game) + self.my_button = ButtonEntity(bx, by, self, game=game) + self.unlocked = False + #global cos_entities + #cos_entities.append(self.my_button) + + def unlock(self): + self.sprite_number = 21 + self.unlocked = True + + def lock(self): + self.sprite_number = 45 + self.unlocked = False + + def bump(self, other, dx, dy, test=False): + if type(other) == BoulderEntity: + return False + if self.unlocked: + if not test: + other._relative_move(dx, dy) + #TODO - player go down a level logic + if type(other) == PlayerEntity: + self.game.create_level(self.game.depth + 1) + self.game.swap_level(self.game.level, self.game.spawn_point) diff --git a/src/scripts/cos_level.py b/src/scripts/cos_level.py new file mode 100644 index 0000000..32392b0 --- /dev/null +++ b/src/scripts/cos_level.py @@ -0,0 +1,195 @@ +import random +import mcrfpy +import cos_tiles as ct + +t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) + +def binary_space_partition(x, y, w, h): + d = random.choices(["vert", "horiz"], weights=[w/(w+h), h/(w+h)])[0] + split = random.randint(30, 70) / 100.0 + if d == "vert": + coord = int(w * split) + return (x, y, coord, h), (x+coord, y, w-coord, h) + else: # horizontal + coord = int(h * split) + return (x, y, w, coord), (x, y+coord, w, h-coord) + +room_area = lambda x, y, w, h: w * h + +class BinaryRoomNode: + def __init__(self, xywh): + self.data = xywh + self.left = None + self.right = None + + def __repr__(self): + return f"" + + def split(self): + new_data = binary_space_partition(*self.data) + self.left = BinaryRoomNode(new_data[0]) + self.right = BinaryRoomNode(new_data[1]) + + def walk(self): + if self.left and self.right: + return self.left.walk() + self.right.walk() + return [self] + +class RoomGraph: + def __init__(self, xywh): + self.root = BinaryRoomNode(xywh) + + def __repr__(self): + return f"" + + def walk(self): + w = self.root.walk() if self.root else [] + #print(w) + return w + +def room_coord(room, margin=0): + x, y, w, h = room.data + w -= 1 + h -= 1 + margin += 1 + x += margin + y += margin + w -= margin + h -= margin + if w < 0: w = 0 + if h < 0: h = 0 + tx = x if w==0 else random.randint(x, x+w) + ty = y if h==0 else random.randint(y, y+h) + return (tx, ty) + +class Level: + def __init__(self, width, height): + self.width = width + self.height = height + #self.graph = [(0, 0, width, height)] + self.graph = RoomGraph( (0, 0, width, height) ) + self.grid = mcrfpy.Grid(width, height, t, (10, 10), (1014, 758)) + self.highlighted = -1 #debug view feature + + def fill(self, xywh, highlight = False): + if highlight: + ts = 0 + else: + ts = room_area(*xywh) % 131 + X, Y, W, H = xywh + for x in range(X, X+W): + for y in range(Y, Y+H): + self.grid.at((x, y)).tilesprite = ts + + def highlight(self, delta): + rooms = self.graph.walk() + if self.highlighted < len(rooms): + print(f"reset {self.highlighted}") + self.fill(rooms[self.highlighted].data) # reset + self.highlighted += delta + print(f"highlight {self.highlighted}") + self.highlighted = self.highlighted % len(rooms) + self.fill(rooms[self.highlighted].data, highlight = True) + + def reset(self): + self.graph = RoomGraph( (0, 0, self.width, self.height) ) + for x in range(self.width): + for y in range(self.height): + self.grid.at((x, y)).walkable = True + self.grid.at((x, y)).transparent = True + self.grid.at((x, y)).tilesprite = 0 #random.choice([40, 28]) + + def split(self, single=False): + if single: + areas = {g.data: room_area(*g.data) for g in self.graph.walk()} + largest = sorted(self.graph.walk(), key=lambda g: areas[g.data])[-1] + largest.split() + else: + for room in self.graph.walk(): room.split() + self.fill_rooms() + + def fill_rooms(self, features=None): + rooms = self.graph.walk() + print(f"rooms: {len(rooms)}") + for i, g in enumerate(rooms): + X, Y, W, H = g.data + #c = [random.randint(0, 255) for _ in range(3)] + ts = room_area(*g.data) % 131 + i # modulo - consistent tile pick + for x in range(X, X+W): + for y in range(Y, Y+H): + self.grid.at((x, y)).tilesprite = ts + + def wall_rooms(self): + rooms = self.graph.walk() + for g in rooms: + #if random.random() > 0.66: continue + X, Y, W, H = g.data + for x in range(X, X+W): + self.grid.at((x, Y)).walkable = False + #self.grid.at((x, Y+H-1)).walkable = False + for y in range(Y, Y+H): + self.grid.at((X, y)).walkable = False + #self.grid.at((X+W-1, y)).walkable = False + # boundary of entire level + for x in range(0, self.width): + # self.grid.at((x, 0)).walkable = False + self.grid.at((x, self.height-1)).walkable = False + for y in range(0, self.height): + # self.grid.at((0, y)).walkable = False + self.grid.at((self.width-1, y)).walkable = False + + def generate(self, target_rooms = 5, features=None): + if features is None: + shuffled = ["boulder", "button"] + random.shuffle(shuffled) + features = ["spawn"] + shuffled + ["exit", "treasure"] + # Binary space partition to reach given # of rooms + self.reset() + while len(self.graph.walk()) < target_rooms: + self.split(single=len(self.graph.walk()) > target_rooms * .5) + + # Player path planning + #self.fill_rooms() + self.wall_rooms() + rooms = self.graph.walk() + feature_coords = {} + prev_room = None + for room in rooms: + if not features: break + f = features.pop(0) + feature_coords[f] = room_coord(room, margin=4 if f in ("boulder",) else 1) + + ## Hallway generation + # plow an inelegant path + if prev_room: + start = room_coord(prev_room, margin=2) + end = room_coord(room, margin=2) + # get x1,y1 and x2,y2 coordinates: top left and bottom right points on the rect formed by two random points, one from each of the 2 rooms + x1 = min([start[0], end[0]]) + x2 = max([start[0], end[0]]) + dw = x2 - x1 + y1 = min([start[1], end[1]]) + y2 = max([start[1], end[1]]) + dh = y2 - y1 + #print(x1, y1, x2, y2, dw, dh) + + # random: top left or bottom right as the corner between the paths + tx, ty = (x1, y1) if random.random() >= 0.5 else (x2, y2) + + for x in range(x1, x1+dw): + self.grid.at((x, ty)).walkable = True + #self.grid.at((x, ty)).color = (255, 0, 0) + #self.grid.at((x, ty)).tilesprite = -1 + for y in range(y1, y1+dh): + self.grid.at((tx, y)).walkable = True + #self.grid.at((tx, y)).color = (0, 255, 0) + #self.grid.at((tx, y)).tilesprite = -1 + prev_room = room + + + # Tile painting + possibilities = None + while possibilities or possibilities is None: + possibilities = ct.wfc_pass(self.grid, possibilities) + + return feature_coords diff --git a/src/scripts/cos_play.py b/src/scripts/cos_play.py deleted file mode 100644 index 5cc9ffd..0000000 --- a/src/scripts/cos_play.py +++ /dev/null @@ -1,300 +0,0 @@ -import mcrfpy -mcrfpy.createScene("play") -ui = mcrfpy.sceneUI("play") -t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) -font = mcrfpy.Font("assets/JetbrainsMono.ttf") - -frame_color = (64, 64, 128) - -grid = mcrfpy.Grid(20, 15, t, 10, 10, 800, 595) -grid.zoom = 2.0 -entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color) -inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color) -stats_frame = mcrfpy.Frame(815, 610, 194, 143, fill_color = frame_color) - -begin_btn = mcrfpy.Frame(350,250,100,100, fill_color = (255,0,0)) -begin_btn.children.append(mcrfpy.Caption(5, 5, "Begin", font)) -def cos_keys(key, state): - if key == 'M' and state == 'start': - mapgen() - elif state == "end": return - elif key == "W": - player.move("N") - elif key == "A": - player.move("W") - elif key == "S": - player.move("S") - elif key == "D": - player.move("E") - - -def cos_init(*args): - if args[3] != "start": return - mcrfpy.keypressScene(cos_keys) - ui.remove(4) - -begin_btn.click = cos_init - -[ui.append(e) for e in (grid, entity_frame, inventory_frame, stats_frame, begin_btn)] - -import random -def rcolor(): - return tuple([random.randint(0, 255) for i in range(3)]) # TODO list won't work with GridPoint.color, so had to cast to tuple - -x_max, y_max = grid.grid_size -for x in range(x_max): - for y in range(y_max): - grid.at((x,y)).color = rcolor() - -from math import pi, cos, sin -def mapgen(room_size_max = 7, room_size_min = 3, room_count = 4): - # reset map - for x in range(x_max): - for y in range(y_max): - grid.at((x, y)).walkable = False - grid.at((x, y)).transparent= False - grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0] - global cos_entities - for e in cos_entities: - e.e.position = (999,999) # TODO - e.die() - cos_entities = [] - - #Dungeon generation - centers = [] - attempts = 0 - while len(centers) < room_count: - # Leaving this attempt here for later comparison. These rooms sucked. - # overlapping, uninteresting hallways, crowded into the corners sometimes, etc. - attempts += 1 - if attempts > room_count * 15: break - # room_left = random.randint(1, x_max) - # room_top = random.randint(1, y_max) - - # Take 2 - circular distribution of rooms - angle_mid = (len(centers) / room_count) * 2 * pi + 0.785 - angle = random.uniform(angle_mid - 0.25, angle_mid + 0.25) - radius = random.uniform(3, 14) - room_left = int(radius * cos(angle)) + int(x_max/2) - if room_left <= 1: room_left = 1 - if room_left > x_max - 1: room_left = x_max - 2 - room_top = int(radius * sin(angle)) + int(y_max/2) - if room_top <= 1: room_top = 1 - if room_top > y_max - 1: room_top = y_max - 2 - room_w = random.randint(room_size_min, room_size_max) - if room_w + room_left >= x_max: room_w = x_max - room_left - 2 - room_h = random.randint(room_size_min, room_size_max) - if room_h + room_top >= y_max: room_h = y_max - room_top - 2 - #print(room_left, room_top, room_left + room_w, room_top + room_h) - if any( # centers contained in this randomly generated room - [c[0] >= room_left and c[0] <= room_left + room_w and c[1] >= room_top and c[1] <= room_top + room_h for c in centers] - ): - continue # re-randomize the room position - centers.append( - (int(room_left + (room_w/2)), int(room_top + (room_h/2))) - ) - - for x in range(room_w): - for y in range(room_h): - grid.at((room_left+x, room_top+y)).walkable=True - grid.at((room_left+x, room_top+y)).transparent=True - grid.at((room_left+x, room_top+y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53]) - - # generate a boulder - if (room_w > 2 and room_h > 2): - room_boulder_x, room_boulder_y = random.randint(room_left+1, room_left+room_w-1), random.randint(room_top+1, room_top+room_h-1) - cos_entities.append(BoulderEntity(room_boulder_x, room_boulder_y)) - - print(f"{room_count} rooms generated after {attempts} attempts.") - #print(centers) - # hallways - pairs = [] - for c1 in centers: - for c2 in centers: - if c1 == c2: continue - if (c2, c1) in pairs or (c1, c2) in pairs: continue - left = min(c1[0], c2[0]) - right = max(c1[0], c2[0]) - top = min(c1[1], c2[1]) - bottom = max(c1[1], c2[1]) - - corners = [(left, top), (left, bottom), (right, top), (right, bottom)] - corners.remove(c1) - corners.remove(c2) - random.shuffle(corners) - target, other = corners - for x in range(target[0], other[0], -1 if target[0] > other[0] else 1): - was_walkable = grid.at((x, target[1])).walkable - grid.at((x, target[1])).walkable=True - grid.at((x, target[1])).transparent=True - if not was_walkable: - grid.at((x, target[1])).tilesprite = random.choices([0, 12, 24], weights=[.6, .3, .1])[0] - for y in range(target[1], other[1], -1 if target[1] > other[1] else 1): - was_walkable = grid.at((target[0], y)).walkable - grid.at((target[0], y)).walkable=True - grid.at((target[0], y)).transparent=True - if not was_walkable: - grid.at((target[0], y)).tilesprite = random.choices([0, 12, 24], weights=[0.4, 0.3, 0.3])[0] - pairs.append((c1, c2)) - - - # spawn exit and button - spawn_points = [] - for x in range(x_max): - for y in range(y_max): - if grid.at((x, y)).walkable: - spawn_points.append((x, y)) - random.shuffle(spawn_points) - door_spawn, button_spawn = spawn_points[:2] - cos_entities.append(ExitEntity(*door_spawn, *button_spawn)) - - # respawn player - global player - if player: - player.position = (999,999) # TODO - die() is broken and I don't know why - player = PlayerEntity() - - - #for x in range(x_max): - # for y in range(y_max): - # if grid.at((x, y)).walkable: - # #grid.at((x,y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53]) - # pass - # else: - # #grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0] - -#131 - last sprite -#123, 124 - brown, grey rats -#121 - ghost -#114, 115, 116 - green, red, blue potion -#102 - shield -#98 - low armor guy, #97 - high armor guy -#89 - chest, #91 - empty chest -#84 - wizard -#82 - barrel -#66 - boulder -#64, 65 - graves -#48 - 53: ground (not going to figure out how they fit together tonight) -#42 - button-looking ground -#40 - basic solid wall -#36, 37, 38 - wall (left, middle, right) -#28 solid wall but with a grate -#21 - wide open door, 33 medium open, 45 closed door -#0 - basic dirt -class MyEntity: - def __init__(self, x=0, y=0, sprite_num=86): - self.e = mcrfpy.Entity(x, y, t, sprite_num) - grid.entities.append(self.e) - def die(self): - for i in range(len(grid.entities)): - e = grid.entities[i] - if e == self.e: - grid.entities.remove(i) - break - def bump(self, other, dx, dy): - raise NotImplementedError - - def try_move(self, dx, dy): - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - for e in cos_entities: - if e.e.position == (tx, ty): - #print(f"bumping {e}") - return e.bump(self, dx, dy) - if tx < 0 or tx >= x_max: - #print("out of bounds horizontally") - return False - if ty < 0 or ty >= y_max: - #print("out of bounds vertically") - return False - if grid.at((tx, ty)).walkable == True: - #print("Motion!") - self.e.position = (tx, ty) - return True - else: - #print("Bonk") - return False - - def _relative_move(self, dx, dy): - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - self.e.position = (tx, ty) - - - def move(self, direction): - if direction == "N": - self.try_move(0, -1) - elif direction == "E": - self.try_move(1, 0) - elif direction == "S": - self.try_move(0, 1) - elif direction == "W": - self.try_move(-1, 0) - -cos_entities = [] - -class PlayerEntity(MyEntity): - def __init__(self): - # find spawn point - spawn_points = [] - for x in range(x_max): - for y in range(y_max): - if grid.at((x, y)).walkable: - spawn_points.append((x, y)) - random.shuffle(spawn_points) - for spawn in spawn_points: - for e in cos_entities: - if e.e.position == spawn: break - else: - break - - #print(f"spawn at {spawn}") - super().__init__(spawn[0], spawn[1], sprite_num=84) - -class BoulderEntity(MyEntity): - def __init__(self, x, y): - super().__init__(x, y, 66) - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - #print("Boulders can't push boulders") - return False - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - # Is the boulder blocked the same direction as the bumper? If not, let's both move - old_pos = int(self.e.position[0]), int(self.e.position[1]) - if self.try_move(dx, dy): - other.e.position = old_pos - return True - -class ButtonEntity(MyEntity): - def __init__(self, x, y, exit): - super().__init__(x, y, 42) - self.exit = exit - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - self.exit.unlock() - other._relative_move(dx, dy) - return True - -class ExitEntity(MyEntity): - def __init__(self, x, y, bx, by): - super().__init__(x, y, 45) - self.my_button = ButtonEntity(bx, by, self) - self.unlocked = False - global cos_entities - cos_entities.append(self.my_button) - - def unlock(self): - self.e.sprite_number = 21 - self.unlocked = True - - def lock(self): - self.e.sprite_number = 45 - self.unlocked = True - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - return False - if self.unlocked: - other._relative_move(dx, dy) - -player = None diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py new file mode 100644 index 0000000..d7cb57b --- /dev/null +++ b/src/scripts/cos_tiles.py @@ -0,0 +1,223 @@ +tiles = {} +deltas = [ + (-1, -1), ( 0, -1), (+1, -1), + (-1, 0), ( 0, 0), (+1, 0), + (-1, +1), ( 0, +1), (+1, +1) + ] + +class TileInfo: + GROUND, WALL, DONTCARE = True, False, None + chars = { + "X": WALL, + "_": GROUND, + "?": DONTCARE + } + symbols = {v: k for k, v in chars.items()} + + def __init__(self, values:dict): + self._values = values + self.rules = [] + self.chance = 1.0 + + @staticmethod + def from_grid(grid, xy:tuple): + values = {} + for d in deltas: + tx, ty = d[0] + xy[0], d[1] + xy[1] + try: + values[d] = grid.at((tx, ty)).walkable + except ValueError: + values[d] = True + return TileInfo(values) + + @staticmethod + def from_string(s): + values = {} + for d, c in zip(deltas, s): + values[d] = TileInfo.chars[c] + return TileInfo(values) + + def __hash__(self): + """for use as a dictionary key""" + return hash(tuple(self._values.items())) + + def match(self, other:"TileInfo"): + for d, rule in self._values.items(): + if rule is TileInfo.DONTCARE: continue + if other._values[d] is TileInfo.DONTCARE: continue + if rule != other._values[d]: return False + return True + + def show(self): + nine = ['', '', '\n'] * 3 + for k, end in zip(deltas, nine): + c = TileInfo.symbols[self._values[k]] + print(c, end=end) + + def __repr__(self): + return f"" + +cardinal_directions = { + "N": ( 0, -1), + "S": ( 0, +1), + "E": (-1, 0), + "W": (+1, 0) +} + +def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False): + cardinal, allowed_tile = rule + dxy = cardinal_directions[cardinal.upper()] + tx, ty = xy[0] + dxy[0], xy[1] + dxy[1] + #print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}") + if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified + try: + return grid.at((tx, ty)).tilesprite == allowed_tile + except ValueError: + return False + +import random +tile_of_last_resort = 431 + +def find_possible_tiles(grid, x, y, unverified_tiles=None, pass_unverified=False): + ti = TileInfo.from_grid(grid, (x, y)) + if unverified_tiles is None: unverified_tiles = [] + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + if not matches: + return [] + possible = [] + if not any([tileinfo.rules for tileinfo, _ in matches]): + # make weighted choice, as the tile does not depend on verification + wts = [k.chance for k, v in matches] + tileinfo, tile = random.choices(matches, weights=wts)[0] + return [tile] + + for tileinfo, tile in matches: + if not tileinfo.rules: + possible.append(tile) + continue + for r in tileinfo.rules: #for r in ...: if ... continue == more readable than an "any" 1-liner + p = special_rule_verify(r, grid, (x,y), + unverified_tiles=unverified_tiles, + pass_unverified = pass_unverified + ) + if p: + possible.append(tile) + continue + return list(set(list(possible))) + +def wfc_first_pass(grid): + w, h = grid.grid_size + possibilities = {} + for x in range(0, w): + for y in range(0, h): + matches = find_possible_tiles(grid, x, y, pass_unverified=True) + if len(matches) == 0: + grid.at((x, y)).tilesprite = tile_of_last_resort + possibilities[(x,y)] = matches + elif len(matches) == 1: + grid.at((x, y)).tilesprite = matches[0] + else: + possibilities[(x,y)] = matches + return possibilities + +def wfc_pass(grid, possibilities=None): + w, h = grid.grid_size + if possibilities is None: + #print("first pass results:") + possibilities = wfc_first_pass(grid) + counts = {} + for v in possibilities.values(): + if len(v) in counts: counts[len(v)] += 1 + else: counts[len(v)] = 1 + print(counts) + return possibilities + elif len(possibilities) == 0: + print("We're done!") + return + old_possibilities = possibilities + possibilities = {} + for (x, y) in old_possibilities.keys(): + matches = find_possible_tiles(grid, x, y, unverified_tiles=old_possibilities.keys(), pass_unverified = True) + if len(matches) == 0: + print((x,y), matches) + grid.at((x, y)).tilesprite = tile_of_last_resort + possibilities[(x,y)] = matches + elif len(matches) == 1: + grid.at((x, y)).tilesprite = matches[0] + else: + grid.at((x, y)).tilesprite = -1 + grid.at((x, y)).color = (32 * len(matches), 32 * len(matches), 32 * len(matches)) + possibilities[(x,y)] = matches + + if len(possibilities) == len(old_possibilities): + #print("No more tiles could be solved without collapse") + counts = {} + for v in possibilities.values(): + if len(v) in counts: counts[len(v)] += 1 + else: counts[len(v)] = 1 + #print(counts) + if 0 in counts: del counts[0] + if len(counts) == 0: + print("Contrats! You broke it! (insufficient tile defs to solve remaining tiles)") + return [] + target = min(list(counts.keys())) + while possibilities: + for (x, y) in possibilities.keys(): + if len(possibilities[(x, y)]) != target: + continue + ti = TileInfo.from_grid(grid, (x, y)) + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + verifiable_matches = find_possible_tiles(grid, x, y, unverified_tiles=possibilities.keys()) + if not verifiable_matches: continue + #print(f"collapsing {(x, y)} ({target} choices)") + matches = [(k, v) for k, v in matches if v in verifiable_matches] + wts = [k.chance for k, v in matches] + tileinfo, tile = random.choices(matches, weights=wts)[0] + grid.at((x, y)).tilesprite = tile + del possibilities[(x, y)] + break + else: + selected = random.choice(list(possibilities.keys())) + #print(f"No tiles have verifable solutions: QUANTUM -> {selected}") + # sprinkle some quantumness on it + ti = TileInfo.from_grid(grid, (x, y)) + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + wts = [k.chance for k, v in matches] + if not wts: + print(f"This one: {(x,y)} {matches}\n{wts}") + del possibilities[(x, y)] + return possibilities + tileinfo, tile = random.choices(matches, weights=wts)[0] + grid.at((x, y)).tilesprite = tile + del possibilities[(x, y)] + + return possibilities + +#with open("scripts/tile_def.txt", "r") as f: +with open("scripts/simple_tiles.txt", "r") as f: + for block in f.read().split('\n\n'): + info, constraints = block.split('\n', 1) + if '#' in info: + info, comment = info.split('#', 1) + rules = [] + if '@' in info: + info, *block_rules = info.split('@') + #print(block_rules) + for r in block_rules: + rules.append((r[0], int(r[1:]))) + #cardinal_dir = block_rules[0] + #partner + if ':' not in info: + tile_id = int(info) + chance = 1.0 + else: + tile_id, chance = info.split(':') + tile_id = int(tile_id) + chance = float(chance.strip()) + constraints = constraints.replace('\n', '') + k = TileInfo.from_string(constraints) + k.rules = rules + k.chance = chance + tiles[k] = tile_id + + diff --git a/src/scripts/game.py b/src/scripts/game.py index 43989b3..b6ce058 100644 --- a/src/scripts/game.py +++ b/src/scripts/game.py @@ -1,98 +1,109 @@ import mcrfpy +#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) +#t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) # 12, 11) font = mcrfpy.Font("assets/JetbrainsMono.ttf") -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) -print("[game.py] Default texture:") -print(mcrfpy.default_texture) -print(type(mcrfpy.default_texture)) +frame_color = (64, 64, 128) -# build test widgets - -mcrfpy.createScene("pytest") -mcrfpy.setScene("pytest") -ui = mcrfpy.sceneUI("pytest") - -# Frame -f = mcrfpy.Frame(25, 19, 462, 346, fill_color=(255, 92, 92)) -print("Frame alive") -# fill (LinkedColor / Color): f.fill_color -# outline (LinkedColor / Color): f.outline_color -# pos (LinkedVector / Vector): f.pos -# size (LinkedVector / Vector): f.size - -# Caption -print("Caption attempt w/ fill_color:") -#c = mcrfpy.Caption(512+25, 19, "Hi.", font) -#c = mcrfpy.Caption(512+25, 19, "Hi.", font, fill_color=(255, 128, 128)) -c = mcrfpy.Caption(512+25, 19, "Hi.", font, fill_color=mcrfpy.Color(255, 128, 128), outline_color=(128, 255, 128)) -print("Caption alive") -# fill (LinkedColor / Color): c.fill_color -#color_val = c.fill_color -print(c.fill_color) -#print("Set a fill color") -#c.fill_color = (255, 255, 255) -print("Lol, did it segfault?") -# outline (LinkedColor / Color): c.outline_color -# font (Font): c.font -# pos (LinkedVector / Vector): c.pos - -# Sprite -s = mcrfpy.Sprite(25, 384+19, texture, 86, 9.0) -# pos (LinkedVector / Vector): s.pos -# texture (Texture): s.texture -s.click = lambda *args, **kwargs: print("clicky", args, kwargs) - -# Grid -g = mcrfpy.Grid(10, 10, texture, 512+25, 384+19, 462, 346) -# texture (Texture): g.texture -# pos (LinkedVector / Vector): g.pos -# size (LinkedVector / Vector): g.size - -for _x in range(10): - for _y in range(10): - g.at((_x, _y)).color = (255 - _x*25, 255 - _y*25, 255) -g.zoom = 2.0 - -[ui.append(d) for d in (f, c, s, g)] - -# Entity -e = mcrfpy.Entity(5, 5, mcrfpy.default_texture, 86) -e.pos = e.draw_pos # TODO - sync draw/collision positions on init -g.entities.append(e) import random -def wander(*args, **kwargs): - p = e.pos - new_p = (p[0] + random.randint(-1, 1), p[1] + random.randint(-1, 1)) - if g.grid_size[0] >= new_p[0] >= 0 and g.grid_size[1] >= new_p[1] >= 0: - e.pos = new_p - #print(e.pos) +import cos_entities as ce +import cos_level as cl +#import cos_tiles as ct -mcrfpy.setTimer("wander", wander, 400) +class Crypt: + def __init__(self): + mcrfpy.createScene("play") + self.ui = mcrfpy.sceneUI("play") + mcrfpy.setScene("play") + mcrfpy.keypressScene(self.cos_keys) -last_anim = None -def anim(t, *args, **kwargs): - global last_anim - if last_anim is None: - last_anim = t - return - duration = t - last_anim + entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color) + inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color) + stats_frame = mcrfpy.Frame(815, 610, 194, 143, fill_color = frame_color) - entity_speed = 1 / 250 # 250 milliseconds to move one square - if e.pos == e.draw_pos: - return - tx, ty = e.pos #"target" position - entity is already occupying that spot, animate them moving there. - dx, dy = e.draw_pos #"draw" position - newx = tx if (abs(dx - tx) < entity_speed * duration) else entity_speed * duration - if tx < dx: newx *= -1 - newy = ty if (abs(dy - ty) < entity_speed * duration) else entity_speed * duration - if ty < dy: newx *= -1 + #self.level = cl.Level(30, 23) + self.entities = [] + self.depth=1 + self.create_level(self.depth) + #self.grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758)) + self.player = ce.PlayerEntity(game=self) + self.swap_level(self.level, self.spawn_point) - print(f"({dx}, {dy}) -> ({tx}, {ty}) = ({newx}, {newy}) ; @{entity_speed} * {duration} = {entity_speed * duration}") - e.draw_pos = (newx, newy) + # Test Entities + #ce.BoulderEntity(9, 7, game=self) + #ce.BoulderEntity(9, 8, game=self) + #ce.ExitEntity(12, 6, 14, 4, game=self) + # scene setup -mcrfpy.setTimer("anim", anim, 67) -print("built!") + [self.ui.append(e) for e in (self.grid,)] # entity_frame, inventory_frame, stats_frame)] -# tests + self.possibilities = None # track WFC possibilities between rounds + def add_entity(self, e:ce.COSEntity): + self.entities.append(e) + self.entities.sort(key = lambda e: e.draw_order, reverse=False) + # hack / workaround for grid.entities not interable + while len(self.grid.entities): # while there are entities on the grid, + self.grid.entities.remove(0) # remove the 1st ("0th") + for e in self.entities: + self.grid.entities.append(e._entity) + + def create_level(self, depth): + #if depth < 3: + # features = None + self.level = cl.Level(30, 23) + self.grid = self.level.grid + coords = self.level.generate() + self.entities = [] + for k, v in coords.items(): + if k == "spawn": + self.spawn_point = v + elif k == "boulder": + ce.BoulderEntity(v[0], v[1], game=self) + elif k == "button": + pass + elif k == "exit": + ce.ExitEntity(v[0], v[1], coords["button"][0], coords["button"][1], game=self) + + def cos_keys(self, key, state): + d = None + if state == "end": return + elif key == "W": d = (0, -1) + elif key == "A": d = (-1, 0) + elif key == "S": d = (0, 1) + elif key == "D": d = (1, 0) + elif key == "M": self.level.generate() + elif key == "R": + self.level.reset() + self.possibilities = None + elif key == "T": + self.level.split() + self.possibilities = None + elif key == "Y": self.level.split(single=True) + elif key == "U": self.level.highlight(+1) + elif key == "I": self.level.highlight(-1) + elif key == "O": + self.level.wall_rooms() + self.possibilities = None + #elif key == "P": ct.format_tiles(self.grid) + elif key == "P": + self.possibilities = ct.wfc_pass(self.grid, self.possibilities) + if d: self.player.try_move(*d) + + def swap_level(self, new_level, spawn_point): + self.level = new_level + self.grid = self.level.grid + self.grid.zoom = 2.0 + # TODO, make an entity mover function + self.add_entity(self.player) + self.player.grid = self.grid + self.player.draw_pos = spawn_point + self.grid.entities.append(self.player._entity) + try: + self.ui.remove(0) + except: + pass + self.ui.append(self.grid) + +crypt = Crypt() diff --git a/src/scripts/game_old.py b/src/scripts/game_old.py deleted file mode 100644 index 2975588..0000000 --- a/src/scripts/game_old.py +++ /dev/null @@ -1,221 +0,0 @@ -#print("Hello mcrogueface") -import mcrfpy -import cos_play -# Universal stuff -font = mcrfpy.Font("assets/JetbrainsMono.ttf") -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) #12, 11) -texture_cold = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) #12, 11) -texture_hot = mcrfpy.Texture("assets/kenney_lava.png", 16, 16) #12, 11) - -# Test stuff -mcrfpy.createScene("boom") -mcrfpy.setScene("boom") -ui = mcrfpy.sceneUI("boom") -box = mcrfpy.Frame(40, 60, 200, 300, fill_color=(255,128,0), outline=4.0, outline_color=(64,64,255,96)) -ui.append(box) - -#caption = mcrfpy.Caption(10, 10, "Clicky", font, (255, 255, 255, 255), (0, 0, 0, 255)) -#box.click = lambda x, y, btn, type: print("Hello callback: ", x, y, btn, type) -#box.children.append(caption) - -test_sprite_number = 86 -sprite = mcrfpy.Sprite(20, 60, texture, test_sprite_number, 4.0) -spritecap = mcrfpy.Caption(5, 5, "60", font) -def click_sprite(x, y, btn, action): - global test_sprite_number - if action != "start": return - if btn in ("left", "wheel_up"): - test_sprite_number -= 1 - elif btn in ("right", "wheel_down"): - test_sprite_number += 1 - sprite.sprite_number = test_sprite_number # TODO - inconsistent naming for __init__, __repr__ and getsetter: sprite_number vs sprite_index - spritecap.text = test_sprite_number - -sprite.click = click_sprite # TODO - sprites don't seem to correct for screen position or scale when clicking -box.children.append(sprite) -box.children.append(spritecap) -box.click = click_sprite - -f_a = mcrfpy.Frame(250, 60, 80, 80, fill_color=(255, 92, 92)) -f_a_txt = mcrfpy.Caption(5, 5, "0", font) - -f_b = mcrfpy.Frame(340, 60, 80, 80, fill_color=(92, 255, 92)) -f_b_txt = mcrfpy.Caption(5, 5, "0", font) - -f_c = mcrfpy.Frame(430, 60, 80, 80, fill_color=(92, 92, 255)) -f_c_txt = mcrfpy.Caption(5, 5, "0", font) - - -ui.append(f_a) -f_a.children.append(f_a_txt) -ui.append(f_b) -f_b.children.append(f_b_txt) -ui.append(f_c) -f_c.children.append(f_c_txt) - -import sys -def ding(*args): - f_a_txt.text = str(sys.getrefcount(ding)) + " refs" - f_b_txt.text = sys.getrefcount(dong) - f_c_txt.text = sys.getrefcount(stress_test) - -def dong(*args): - f_a_txt.text = str(sys.getrefcount(ding)) + " refs" - f_b_txt.text = sys.getrefcount(dong) - f_c_txt.text = sys.getrefcount(stress_test) - -running = False -timers = [] - -def add_ding(): - global timers - n = len(timers) - mcrfpy.setTimer(f"timer{n}", ding, 100) - print("+1 ding:", timers) - -def add_dong(): - global timers - n = len(timers) - mcrfpy.setTimer(f"timer{n}", dong, 100) - print("+1 dong:", timers) - -def remove_random(): - global timers - target = random.choice(timers) - print("-1 timer:", target) - print("remove from list") - timers.remove(target) - print("delTimer") - mcrfpy.delTimer(target) - print("done") - -import random -import time -def stress_test(*args): - global running - global timers - if not running: - print("stress test initial") - running = True - timers.append("recurse") - add_ding() - add_dong() - mcrfpy.setTimer("recurse", stress_test, 1000) - mcrfpy.setTimer("terminate", lambda *args: mcrfpy.delTimer("recurse"), 30000) - ding(); dong() - else: - #print("stress test random activity") - #random.choice([ - # add_ding, - # add_dong, - # remove_random - # ])() - #print(timers) - print("Segfaultin' time") - mcrfpy.delTimer("recurse") - print("Does this still work?") - time.sleep(0.5) - print("How about now?") - - -stress_test() - - -# Loading Screen -mcrfpy.createScene("loading") -ui = mcrfpy.sceneUI("loading") -#mcrfpy.setScene("loading") -logo_texture = mcrfpy.Texture("assets/temp_logo.png", 1024, 1024)#1, 1) -logo_sprite = mcrfpy.Sprite(50, 50, logo_texture, 0, 0.5) -ui.append(logo_sprite) -logo_sprite.click = lambda *args: mcrfpy.setScene("menu") -logo_caption = mcrfpy.Caption(70, 600, "Click to Proceed", font, (255, 0, 0, 255), (0, 0, 0, 255)) -#logo_caption.fill_color =(255, 0, 0, 255) -ui.append(logo_caption) - - -# menu screen -mcrfpy.createScene("menu") - -for e in [ - mcrfpy.Caption(10, 10, "Crypt of Sokoban", font, (255, 255, 255), (0, 0, 0)), - mcrfpy.Caption(20, 55, "a McRogueFace demo project", font, (192, 192, 192), (0, 0, 0)), - mcrfpy.Frame(15, 70, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 145, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 220, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 295, 150, 60, fill_color=(64, 64, 128)), - #mcrfpy.Frame(900, 10, 100, 100, fill_color=(255, 0, 0)), - ]: - mcrfpy.sceneUI("menu").append(e) - -def click_once(fn): - def wraps(*args, **kwargs): - #print(args) - action = args[3] - if action != "start": return - return fn(*args, **kwargs) - return wraps - -@click_once -def asdf(x, y, btn, action): - print(f"clicky @({x},{y}) {action}->{btn}") - -@click_once -def clicked_exit(*args): - mcrfpy.exit() - -menu_btns = [ - ("Boom", lambda *args: 1 / 0), - ("Exit", clicked_exit), - ("About", lambda *args: mcrfpy.setScene("about")), - ("Settings", lambda *args: mcrfpy.setScene("settings")), - ("Start", lambda *args: mcrfpy.setScene("play")) - ] -for i in range(len(mcrfpy.sceneUI("menu"))): - e = mcrfpy.sceneUI("menu")[i] # TODO - fix iterator - #print(e, type(e)) - if type(e) is not mcrfpy.Frame: continue - label, fn = menu_btns.pop() - #print(label) - e.children.append(mcrfpy.Caption(5, 5, label, font, (192, 192, 255), (0,0,0))) - e.click = fn - - -# settings screen -mcrfpy.createScene("settings") -window_scaling = 1.0 - -scale_caption = mcrfpy.Caption(180, 70, "1.0x", font, (255, 255, 255), (0, 0, 0)) -#scale_caption.fill_color = (255, 255, 255) # TODO - mcrfpy.Caption.__init__ is not setting colors -for e in [ - mcrfpy.Caption(10, 10, "Settings", font, (255, 255, 255), (0, 0, 0)), - mcrfpy.Frame(15, 70, 150, 60, fill_color=(64, 64, 128)), # + - mcrfpy.Frame(300, 70, 150, 60, fill_color=(64, 64, 128)), # - - mcrfpy.Frame(15, 295, 150, 60, fill_color=(64, 64, 128)), - scale_caption, - ]: - mcrfpy.sceneUI("settings").append(e) - -@click_once -def game_scale(x, y, btn, action, delta): - global window_scaling - print(f"WIP - scale the window from {window_scaling:.1f} to {window_scaling+delta:.1f}") - window_scaling += delta - scale_caption.text = f"{window_scaling:.1f}x" - mcrfpy.setScale(window_scaling) - #mcrfpy.setScale(2) - -settings_btns = [ - ("back", lambda *args: mcrfpy.setScene("menu")), - ("-", lambda x, y, btn, action: game_scale(x, y, btn, action, -0.1)), - ("+", lambda x, y, btn, action: game_scale(x, y, btn, action, +0.1)) - ] - -for i in range(len(mcrfpy.sceneUI("settings"))): - e = mcrfpy.sceneUI("settings")[i] # TODO - fix iterator - #print(e, type(e)) - if type(e) is not mcrfpy.Frame: continue - label, fn = settings_btns.pop() - #print(label, fn) - e.children.append(mcrfpy.Caption(5, 5, label, font, (192, 192, 255), (0,0,0))) - e.click = fn