7DRL 2025 - Crypt of Sokoban fully functional. Minor C++ changes.

This commit is contained in:
John McCardle 2025-03-05 20:18:01 -05:00
parent c975599251
commit b920a51736
7 changed files with 723 additions and 609 deletions

View File

@ -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");
}
}

203
src/scripts/cos_entities.py Normal file
View File

@ -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"<COSEntity ({self.draw_pos}) on {self.grid}>"
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"<PlayerEntity {self.draw_pos}, {self.grid}>"
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)

195
src/scripts/cos_level.py Normal file
View File

@ -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"<RoomNode {self.data}>"
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"<RoomGraph, root={self.root}, {len(self.walk())} rooms>"
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

View File

@ -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

223
src/scripts/cos_tiles.py Normal file
View File

@ -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"<TileInfo {self._values}>"
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

View File

@ -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()

View File

@ -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