I’ve been working on the broughlike pretty steadily since my last update. The gameplay loop is pretty much unchanged, but I’ve added a fair bit of polish, and fixed a lot of bugs. I think it is honestly sort of boring to play, but I am excited to have this as the starting point for future projects…can you smell the roguelike!? I can!
The major fixes and improvements that I’ve made since my last update include:
- better way finding used during level generation helps to make sure that items and enemies are always reachable by the player and not trapped in little rooms all on their own
- 4 different wall generators that are used at random! I was previously only using a naïve flood fill algorithm, now, alongside that I also have RSP Tree, Cellular Automata, and Drunkard’s Walk algorithms for generating walls
- enhanced enemy logic means the enemies are a lot more clever — they’re set up to move straight to the exit unless the player is within 4 grid cells of their current location, at which point they switch to pursuing the player, this combo means that they often end up swarming the player as they get closer to the exit
- bosses now appear every 10 levels…if you defeat one your health is boosted a little bit
- a whole bunch of optimizations and code clean up
An interesting side-effect of working on a 6x6 grid is that it is about as small as any of my algorithms can support — if I go smaller to 4x4 they get wicked slow or totally crash. If I go bigger they stay about as useful, without any noticeable lag (though I haven’t tested for an upper limit to this).
I’ve also been experimenting with an auto-play algorithm, that, similar to the logic which controls the enemies, would have the player automatically navigate toward the exit, able to move around walls, engage enemies and collect items. So far that isn’t really working, though, so I’ll save that for the code-spelunkers among you dear readers.
Here’s the code I’m using to make sure that the player can always reach the exit, other enemies, and items.
function isReachable(startX, startY, targetX, targetY) {
const visited = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(false));
function dfs(x, y) {
if (x < 0 || x >= CONFIG.GRID_SIZE || y < 0 || y >= CONFIG.GRID_SIZE) return false;
if (visited[x][y]) return false;
if (walls.some(wall => wall.x === x && wall.y === y)) return false;
visited[x][y] = true;
if (x === targetX && y === targetY) return true;
return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1);
}
return dfs(startX, startY);
}
Beyond little bits of game development here and there I’ve switched projects at work, been reading a bunch and generally enjoying the fall-flavored actives that happen this time of year…which is a little bit to say “I’ve been playing ostrich.”
The Roguelike Celebration happened this weekend. Every year I think about participating, and every year I let it slip me by. In honor of it, though, this weekend I made a Broughlike…which I’ve creatively named “Eli’s Broughlike.”
It runs in the browser. It should work on most anything with a keyboard, or with a touchscreen — the about page has some more details about how to play, though I’d suggest trying to play 1 or 2 rounds at least without reading the instructions. I think a subtle confusion about what is happening is a key part to the broughlike genre.
The enemy logic isn’t very sophisticated, and is likely where I’ll spend more time if I do return to this. I learned a lot about flood fill making this, which was rad.
If you play, I hope you have fun!
This afternoon I put the garden to sleep for the fall; in the past we’ve had some fall and winter vegetables going, but this year that didn’t happen, so, I emptied out the rain barrels, cleaned them out, trundled them to a place where they wouldn’t get blown around by any winds, mulched some of the beds, weeded and generally plotzed around like a garden goblin.
I’ve fallen into the habit of making a big thing of rice over the weekend — I always intend to do something with this rice, but instead I use it for serving upon serving of steamed veg in some sort of sauce or for making fried rice with. Neither is bad, to be totally honest. Perhaps better than what I would plan on, even. I’d also like to say that I’m pretty darn tootin’ good at making fried rice.
Since my last post, I feel like I’ve been trying to make up for spending so much time slogging through that Brandon Sanderson book. I’ve read a handful of really enjoyable books. Mercedes Lackey is perhaps the antithesis of Brandon Sanderson, so was a good starting point and The Amazing Adventures of Kavalier & Clay by Michael Chabon was delicious. I hadn’t read it before…but most delicious (and not only because of all the descriptions of food) has been The Light from Uncommon Stars by Ryka Aoki.
I’ve been noodling on a technical-ish blog post, or maybe wiki page about using the browser’s console to automate boring tasks, like having to manually click “delete” 800 times in a terrible web application…I guess keep a look out for that one!? Similarly, I’ve started to scheme and plan for this year’s December Adventure.
Dinosaur golf before it shuts for the season.
I’ve found myself in possession of a guitar. Actually, the guitar that I had in middle school has come back to me after a decade’s long jaunt with someone else. I don’t really play guitar, but, I figured I should restring it and tune it.
I’m really very bad at tuning, so, rather than get good at that, or use any of the existing tools within reach of the internet to help me with that I made a thing. Tuner is a little web app that does 2 things: using a device’s microphone it listens for a primary frequency and displays what note that is, and it can play some reference tones, starting from middle C.
The most interesting bit of tuner is the code that detects the dominate frequency being input, and then maps that to a note. I script-monkeyed most of this together.
function detectNote() {
const freqArray = new Float32Array(analyzer.frequencyBinCount);
analyzer.getFloatFrequencyData(freqArray);
let maxAmp = -Infinity;
let maxIndex = 0;
for (let i = 0; i < freqArray.length; i++) {
if (freqArray[i] > maxAmp) {
maxAmp = freqArray[i];
maxIndex = i;
}
}
const nyquist = audioContext.sampleRate / 2;
const frequency = maxIndex * nyquist / freqArray.length;
const note = getNoteFromFrequency(frequency);
noteDisplay.textContent = `Note: ${note}`;
}
function getNoteFromFrequency(frequency) {
const notes = [
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
];
const A4 = 440;
const semitoneRatio = Math.pow(2, 1 / 12);
const noteIndex = Math.round(12 * Math.log2(frequency / A4));
const note = notes[(noteIndex % 12 + 12) % 12];
return frequency ? note : 'N/A';
}
Once the guitar was tuned, I figured I may as well make a metronome because while I am really very bad at tuning I’m even worse at keeping time. BP-sand is a metronome — you can enter a beats per minute count and it’ll beep at an appropriate interval. With each beep a grain of sand rains from the top of the screen. The sand piles up into sticky, branching stacks.
Next, I have to relearn how to play the guitar, I guess…
Other than these musically inclined adventures, autumn has come to Maine — we’ve been enjoying fall vibes, and soaking it all in before the Winter Drearies set in. Recent reading includes The Way of Kings by Brandon Sanderson (an author that I’m honestly ready to call it quits on…page turners for sure, but not for me these days) and The Algebraist by Iain M. Banks. Since finishing Arranger I haven’t picked up another video game, but I have fallen back in love with Puzzmo — pile up poker is 10/10. I think this winter me and Caves of Qud are gonna spend some quality time together. I’m really weirdly excited for it’s 1.0 release. I’m especially excited to explore Qud’s tutorial…I’ve played it for years, but haven’t ever really felt that I’ve actually known how to play it.
Solar powered.
I had time to take a walk before running some errands and getting a flu shot and a Covid booster this morning.
It is starting to smell like fall!
I shared some snippets of JavaScript in a recent blog post and was wicked irked that I didn’t have an easy way to share interactive code on my own thing…so…I made a totally static JavaScript playground for running little experiments and sharing scrappy fiddles!
It is pretty simple — it allows folks to enter and run JavaScript, includes a console so you can easily log things out without folks needing to open developer tools, and lets you share your code in two different ways. First, you can append the contents of the code editor (just a textarea
) as a hash to the URL. If you share a URL that includes hashed contents those will automatically load when anyone visits that hash. Because that makes some wickedly long URLs, though, I also added the capability to download the entire website (a single html page) plus the contents of the textarea
— this way you can either directly host your own version or share the entire html file with someone else.
I’m
really pleased with how this turned out, and I’m excited to use this little playground a lot more. The piece I’m most pleased with is how I handle console.log
; I didn’t want anyone to have to write a custom logging function, I wanted folks to be able to reliably use whatever the browser offers, so, I hijacked the in-built console.log
function that we all know and love and then return it to you after the code has been run!
To learn about the playground’s secrets run the help
function from the editor.
After the fact updates!
I updated the playground a bit since the initial post!
It now includes a few pre-made functions that make creating things a bit easier, including clear
and mount
functions. For more info, check out the newly improved help
command. Here is a demo that shows off how to use the mount function
If you use this playground for anything, I’d love to see what you make!
After reading my last post, a friend asked an interesting question that I thought would also be fun to write about!
They noted that in the reshape
function I declared the variable result
as a constant. They asked if this was a mistake, because I was resigning the value iteratively, shouldn’t it be declared using let
?
What is happening there is that the constant is being declared as an array, so the reference to the array is constant…meaning that you can’t resign that variable to a new array or object. You can, however, fiddle with the contents of that array. Here, const
makes the reference immutable, but the contents of the referenced array can be modified.
This is different than, say, C++, where if you declare an array as a constant with the keyword const
, you cannot modify the contents.
const int arr[] = {1, 2, 3};
arr[0] = 5;
In C++, the const
keyword sorta goes “deeper,” and applies to the contents of the array, too, not only the reference, so the entire kit-and-caboodle is immutable. To achieve something like what I did in JavaScript in C++ with const
you’d want a pointer to an array. This will allow you to modify the contents of the array, but keep the pointer immutable.
int arr[] = {1, 2, 3};
int* const p = arr;
p[0] = 5;
In this example, the pointer, p
, is a constant, and immutable, but the contents it points to can change. Sorta like having a box bolted to the floor — you can’t move or change the box, but you can put all kinds of different stuff into the box as long as they fit!
In APL the rho, ⍴
, called reshape is used to both construct arrays of a given shape (dimensionality), and to reconfigure arrays into new shapes.
Sometimes I wish I had reshape in JavaScript…so I wrote it!
Here are two functions that, when combined, a la Captain Planet, can stand in for APL’s reshape in JavaScript.
Ravel is the simpler of the two, it takes an array of any dimension and returns a new, one-dimensional array of the same data. Sorta like flatten from functional libraries like Ramda.js.
function ravel(array) {
if (!Array.isArray(array)) return [array];
return array.reduce((acc, val) => acc.concat(ravel(val)), []);
}
Reshape takes a vector describing the desired shape of a new array and an array to reformulate into that shape. The function will produce a new array with the specified shape and fill it by cycling through the elements of the input array. I think that this mimics APL’s reshape, allowing you to reshape arrays with fill and cycling behavior.
function reshape(shape, array) {
const totalSize = shape.reduce((acc, val) => acc * val, 1);
const ravelledArray = ravel(array);
const filledArray = [];
for (let i = 0; i < totalSize; i++) {
filledArray.push(ravelledArray[i % ravelledArray.length]);
}
function constructArray(shape, data, offset = 0) {
if (shape.length === 1) {
return data.slice(offset, offset + shape[0]);
}
const size = shape[0];
const subShape = shape.slice(1);
const subArraySize = subShape.reduce((acc, val) => acc * val, 1);
const result = [];
for (let i = 0; i < size; i++) {
result.push(constructArray(subShape, data, offset + i * subArraySize));
}
return result;
}
return constructArray(shape, filledArray);
}
Here are some side-by-side tests of my JS and the same maneuvers in APL.
First, we reshape an array into an array of two nested arrays:
const array = [1, 2, 3, 4, 5];
const shape = [2, 3];
const reshapedArray = reshape(shape, array);
console.log(reshapedArray);
In APL:
2 3 ⍴ 1 2 3 4 5
Returns
1 2 3
4 5 1
Next a test case where we need to fill in a larger shape with repeating:
const array = [1, 2, 3];
const shape = [3, 3];
const reshapedArray = reshape(shape, array);
console.log(reshapedArray);
In APL:
3 3 ⍴ 1 2 3
Returns
1 2 3
1 2 3
1 2 3
I finished reading Robin Sloan’s Moonbound today. It was fun, and light. The blurb likens it to Narnia, and, while a bold claim, I think that was a correct assertion, but more about the intended audience than the book’s subject matter. If a sequel is ever written I’d most certainly give it a look. It seems like a great gift book for a kid between like 8 and 15…or you know, perhaps, anyone who likes fun stories that aren’t scared of being joyful.
The book did do a thing that I don’t often like, which is write a story about the power of a story…it didn’t really lean too hard into that, so I will forgive that…this time.
Next up, following the theme of talking animals…Redwall? Or maybe The Mountain in the Sea, by Ray Nayler! I haven’t read that yet, so don’t really know what it is about, but lets assume a giant talking octopus for the time being!
After some biking about, and playground time with the kids today, I played some more Arranger. This game is becoming the star of this blog, it seems. The game recently started to signal that we’re nearing the final boss!? I may actually finish this game! I’m shocked and excited. Being able to play in bite-sized chunks has a huge breath of fresh air. I rarely have the attention span, nor eyeball powers to play a video game much longer than 45 - 60 minutes in a go.
I’ve also been playing a narrative game in the evenings before bed when I’d typically read a book or stare, listless, at the ceiling. Specifically, I’ve been playing The Ghost and the Golem. I’m still pretty early in, I think, but I’m reminded now of how much I enjoy playing narrative games, and this one seems especially tailored for me. If you have any recommendations for narrative games, I’d be really interested to learn about them.
A new page, still in progress, has appeared on the wiki. This one is being used to catalogue the parts of lil that I find particularly exciting.
Here are some drawings I made with the pixel art thing I mentioned in my last post.
As we start to round out the summer I haven’t been reading as much, so I don’t have much to report on that front, but I have been keeping busy!
I made yet another pixel art drawing tool, pixel pixel pixel pixel pixel pixel allows folks to draw chonky pixel art creations on pretty much any sized canvas. This was fun to make. I’ve spent so much time with the HTML5 canvas lately that I’m really starting to feel like I get it, which is a fun realization! If you draw anything with pixel I’d love to see what you make!
Having made a fancy little menagerie of web things lately, I’ve been wondering if I can do anything more to unify their aesthetic — I have some subtle and secret rules that I use when designing them to help keep things consistent, but that is more to save me from having to make choices than it is to make them all feel related.
…but also, why would I need them to feel related when they almost all already live at the same URL?
My family recently started to play a game called SCHiM — I’ve been watching, mostly, and I’m really struck by it. A few things I am appreciating about the game:
- it includes an accessibility setting to increase the contrast
- the sound design is gorgeous, and supports the visual aesthetic. A lot of the world building and wayfinding is audio-driven
- there’s a totally subtle, yet effective mechanism for helping you to find your next waypoint, like a nonverbal/textual hint system
That sort of quest log stuff has really been on my mind; after noticing how good the quest log is in Arranger in a previous post, being forever struck by how bad the Pokémon games handle this, and picking up Dragon Quest VI on the Gameboy after like…an eon? a millennium? a full epoch? away and being able to slip right back in I decided to make my own quest log…which isn’t in any way a todo list. I’ve been using it at work.
Home beach. Last day of vacation for me.