There are no doors. Tread carefully or the creature in the next room might spot you. Light helps you explore more easily, but also reveals you to your foes. Fires can spread wildly through the environment, while glowing mushroom spores can mend your wounds.
There are no classes or character levels. Expand your abilities by finding magical items on your descent, and bring them to their full potential with Orbs of Power.
I figured some of you might be interested in some of the technical aspects of the game. This was my first foray into PICO-8 and Lua, and I learned a lot along the way -- from discovering with shock that variables are global by default after a week and a half of strange bugs, to trying to cram as much as possible into strings, and _ENV abuse to reduce token count.
Initially I thought I could fit everything into 1 cart, but partway through I realized that even if I could fit all the features I had planned in while respecting the token limit, the compressed character limit would be a problem.
The final version of the game is in 2 carts -- one for the title screen and text intro, and another for the main game. The title screen cart also puts a huge string containing most of the game's data (creature stat blocks, animation data, item stats, text descriptions) into the upper memory region, which is then read and parsed by the main cart. Because the title screen still displays the environment and character, there's a lot of similar code between the two carts, though a lot of things that were refactored to save tokens in the main cart are still in the title cart in their original form.
As far as tokens go, making almost everything in the game be the same type of object went a long way. Each object has a type (which is just its sprite number), and from that we can initialize its properties with a combination of default values and ones read from the huge string mentioned earlier. Having everything be intialized and accessed the same way means that each object has a lot of data it doesn't actually need, but we aren't strapped for Lua memory so that's okay! The upside is we can reuse code where appropriate, and make things interact more easily. To save tokens on accessing properties, I also made heavy use of the _ENV trick outlined by slainte here, and seleb's list of token optimizations was indispensible as well.
The levels are generated using a couple different algorithms. Levels can contain natural caves, manmade dungeon rooms, or both. In either case, there is a variable called Entropy which ticks down as more of the level is generated.
Caves are made recursively using a type of random walk. From our current position, we try to generate another tile in a random neighbouring space. We then decide whether to recurse again from this position or backtrack -- this probability is based on the current entropy, so at the start we always continue, and by the end backtracking is more likely. Playing with the starting entropy and the amount it goes down by with each recursion allows us to achieve the right density and balance between narrow tunnels and wide open areas.
When choosing which tile to put in a new space, we consider the tile we are generating from. Each tile type has a corresponding section of the map data which defines 16 possibilities for the next tile, so we choose one at random from these. In a way this is like a Markov Chain, where the state is just based on the tile we are coming from. Holes, grass and other environmental features are all generated this way. The tile definition can also include an entity to generate, like a mushroom, brazier, chair or barrel.
For rooms, we choose a random point on the map which will be contained in the room, and then random values for the room's width and height. The boundaries of the room are clamped to multiples of 2 or 4 to get them to lock together well and play nice with the rendering. We then create the floor and walls of the room, with one dimension being staggered to fit properly into the hex grid. Each room gets a random crumble value, which we use to decide if we should replace parts of the wall with different tiles (selected in the same way as in the cave generation).
After generating each room, there's a chance we switch to cave generation. Otherwise, we keep generating rooms at random positions until the entropy reaches 0. The rooms can overlap each other, and each time we generate one, a random openplan variable sets whether it should have walls on all sides, or try to merge with any overlapping rooms. In this way, we can get all kinds of interesting dungeon shapes just by combining rectangles.
Once the initial generation is complete, we analyze the level to find parts that are inaccessible, and then find a nearby position the player can get to and connect them in a straight line, either by building bridges or tunneling through walls. Then we recurse randomly through the level and fill out the contents of the manmade rooms, in the same way we did with the caves earlier.
There are a few more steps, like creating cave walls, ensuring there are exit holes, and running the code to connect areas again at various points. One interesting step is to replace cave walls which have walkable tiles both below and above them with a random tile, again sampled from the map data. This is actually the only way that stalagmites and mushrooms can be generated, and I think it makes their placement a bit more natural looking.
Creature spawn groups are again defined in the map, with each line corresponding to a different depth. We attempt 6 times to spawn a group of creatures, either choosing a group from the current depth or sometimes randomly from a greater one for a nasty surprise. We pick a random position, and try to spawn a creature there, and then move on to a to a neighbouring tile for the next one in the group. If it turns out the spawn point is invalid, we don't try again, just move on. This means some levels, especially smaller ones, will tend to have fewer creatures but that's okay -- it just adds variety!
Items are spawned in a similar way, with the depth just affecting how many we try to spawn (we attempt to spawn slightly more items on earlier depths than on later ones). For certain important orbs, we make extra spawn attempts if there haven't been enough of them generated yet over the course of the game based on the current depth.
There's a lot of other things that I think could be interesting to write about, like the lighting, FOV algorithm, or various aspects of the AI, but for now I think this post is long enough. Let me know if there's anything you'd like to hear more about!
@joealarson yeah, some of the tiles aren't the most readable, especially when you're in the dark. Some sacrifices were made in the name of aesthetics, and prioritizing the enemies/player being readable against the tileset. I think this is something you get used to as you play the game more and familiarize yourself with the tiles, and at least walking up to it and trying to move/interact doesn't cost too much.
I realize "amazing" has already been used several times to describe this, but I'll add another to the pile. The attention to detail, depth of gameplay, and overall presentation is indeed amazing. I've been playing roguelikes for about 25 years now, and this isn't only a great roguelike for PICO-8, it's a great roguelike period.
Brogue is easily my favorite roguelike from a gameplay perspective, and I love seeing its influence here. Every element (enemies, items, and terrain) is essentially unique and integral to the whole, and there are so many opportunities for emergent behavior. The bats that catch fire in light and start massive infernos are great. The braziers that can be knocked over to start a fire create interesting tactical situations. The weapons differing in how best to move in relation to enemies. My favorite bit along these lines is small, but I love it. Instead of healing directly, the goblin mystic heals allies with a healing cloud. I don't know if this was a conscious design decision or a byproduct of streamlining code, but it's great because it creates meaningful decisions. Kill the more dangerous enemy, or kill the one that was just healed? If I do the latter quickly enough, I can benefit from the healing cloud. So good.
Also, the little details are fantastic. The difference in the sound of footsteps depending on terrain (bones, grass, ice). The glow of orbs. Stepping on bones alerting nearby enemies. Staves of lightning briefly illuminating dark areas. It all combines to make the game feel like a cohesive, consistent whole.
And, of course, the graphics and animations are great. The terrain feels suitably dark and ruined, creating a great atmosphere. Each enemy already has its own character due to its behavior and sounds, but the animations reinforce this further.
I'm honestly not sure if I have any real criticism of the game. I do think the initial readability of some tiles is a valid criticism. For me, this went away after about a half-dozen plays, but I can understand that it might discourage some. I like the twist on cursed items, though the cloak is quite brutal. I like that each can be turned into an advantage of sorts, but the drawback of the cloak is such that I don't know how it could be used beneficially (I tried several times). I think the difficulty curve is close to perfect. It can be rough at the beginning (e.g. goblin archers and no mushroom on the first level) but seems fair more often than not. I've gotten the impression of a sharp increase in difficulty at level 14, but I've been that deep less than 10 times.
My first victory was melee-focused with some utility staves. +4 axe, cloak of wisdom +3, staff of blink +1, and staff of ice +1. The cloak recharging the staves every level was a huge contributor, but didn't feel overpowered. Becoming cornered was always a concern, because I couldn't escape via blink, and e.g. ogres could potentially kill me in one turn. I had one such very close call on D14, where an orb of teleport saved me. D16 was tricky, with a dragon in the way, but the staff of ice and a couple blinks made victory possible.
b1e95dc632