I did a few miscellaneous things like make the remote script endpoint production-ready, fix some typos across the project, and create a favicon in Inkscape.
After I write this devlog, I’m going to press the big “Ship your project!” button. If you’re a reviewer reading this: I hope you enjoy Hadronize. It’s one of my biggest projects ever, and I’m really proud of how it’s turned out.
I published some binaries on GitHub releases and also made a webpage.
The webpage is live at https://ethmarks.github.io/deci/. I made it using the Lume Tufte theme that I made on Stardance back in June: https://stardance.hackclub.com/projects/18206. It’s still a WIP, but it doesn’t need to be complicated. I don’t think that I really need to put anything on the website other than installation instructions and some screenshots.
I just used the GOOS and GOARCH flags of go build to make the binaries. For the v0.1.0 release, I published binaries for x86 Linux, ARM MacOS, and x86 Windows. I might add support for more platforms later. Meanwhile, if I didn’t make a binary for your platform, you can always just build it from source:
git clone https://github.com/ethmarks/deci.git
cd deci
go build
I do all of my developing work on Linux, so I’ve tested deci pretty thoroughly on Linux. I do have a Windows laptop, but I’ve misplaced the charging cable for it and it’s out of battery, so I can’t test it on Windows right now. I don’t even own an Apple device, so there’s no way I can test on MacOS.
There’s a pretty good chance that you’re reading this devlog on Windows or MacOS right now. If you are, I’d really appreciate it if you could try out deci and let me know if it works.
Scrolling! If you move the cursor all the way to the bottom of the screen, it’ll shift the viewport down. And the opposite if you move the cursor to the top.
This took forever to get right. I kept breaking other things because I had juggle three different “perspectives”: the grid of characters output to the screen, the slice of the grid that’s the viewport, and the actual lines of the file. But it all works now.
I think next I might work on getting a demo set up. Most of the CLI binaries that I’ve downloaded before have used GitHub Releases, so I guess I should figure out how to make those work.
Line numbers! Yay! It took a few attempts to get the Lipgloss structure right. For a while, it either wouldn’t highlight the line number of the active line, or it would only highlight the line number of the active line. But I got it working in the end.
So Lipgloss is like powerful. Wow.
I was using Lipgloss to style the status bar (more on that in a bit), and I took some time to actually read through the docs.
I’d used the picocolors before, which is a JS package that makes working with ANSI codes easier. I had assumed that Lipgloss was kind of like that: you just type lipgloss.Blue rather than \x1b[34m. But it can do all sorts of stuff like borders and padding and centering and compositing and enumeration and like a bajillion other things. I don’t understand how the Lipgloss codebase is only ~12,000 lines; based on how powerful it is, it feels like it should be much bigger.
Anyways, I added active line highlighting and a status bar. Right now, I’ve just made the status bar barf out info about the most recent keypress. For example:
I might decide to make the status bar a bit less talkative later, but for now it’s useful for debugging.
Next, I’m going to work on adding line numbers. I also might decide to refactor the text rendering to use Lipgloss layers.
I’ve found something that I really like about Go compared to TypeScript: you don’t need to import identifiers (e.g. functions) from other files if they’re all part of the some package.
I did a big refactor to split the 400-line behemoth main.go into a handful of smaller files. I was fully expecting to have to import each and every constant, type, and function between files. But I didn’t have to! Because every file is part of the main package, they all use the same namespace or something. I defined the statusMsg struct in update.go, but I can just instantiate a statusMsg literal in file.go without any imports or anything! This is a really cool language feature. 10/10.
Anyways, apart from the big refactor, I did the following:
deci operates on a strict “1 character = 1 byte = 1 column’ basis because I don’t wanna deal with Unicode, so properly implementing tabs was out of the question. Instead, I just made the tab key instantly insert 4 spaces.None of this stuff was on last devlog’s list of features to implement. I just kept noticing that these behaviors were missing while playtesting, so I implemented them. Next up is the aesthetic stuff like the status bar that I promised last devlog.
Hi again! The past three hours have been a manic frenzy of adding features. I’m starting to get the hang of Go. I’m not used to working so quickly, but working on this project is surprisingly exciting.
New features:
nano-style header that currently says deci 0.0.1 on the left and by @ethmarks on the right.Features I want to add next:
Oh, and I’ve decided that deci is ASCII-only. Go is weird about counting characters if they occupy more than one byte, so it would be a huge pain to add support for Unicode and would probably lead to a bajillion bugs.
To make sure you understand what I mean about the ASCII thing, I’ll provide an example: the hamster emoji is 1 character, it occupies 2 columns in the terminal, and it’s made of 4 bytes. Because of the last part, len("🐹") returns 4 in Go, so any string with a hamster emoji in it will appear to have 3 more characters and occupy 2 more columns than it actually does.
Making sure that the code reliably distinguishes between those three very similar concepts would be really challenging, and the only benefit would be that you could use emojis and non-Latin characters in deci. I don’t really think that it’s worth it.
deci is shaping up shockingly quickly. I almost could have written this devlog in deci, though the lack of horizontal or vertical scrolling would probably prevent me from actually writing the full thing.
I stayed up late into the night yesterday reading docs for Go and Bubble Tea. Here are the ones I read:
So far, my overall impressions are “wow, Go kinda sucks compared to TypeScript”. Maybe it’s just that I’m only scratching the surface and I’ve yet to see the true advantages of Go, but so far I am unimpressed. It’s just much more restrictive and much less expressive than TypeScript while simultaneously somehow being more prone to runtime panics, at least from my small amount of experience.
Anyways, I’ve also gotten a decent amount of work done. deci can now kind of edit files. Watch the video below for a demonstration.
Things that work:
Things that might seem like they would work based on the list above, but actually don’t:
There’s still a lot to do. It might seem like I’ve made a lot of progress but I doubt that I’m even 10% of the way done.
Are you tired of using actual editors, but nano is still too feature-rich and stable for you?
Introducing
deci, your second-to-last next editor
I’m making this because I want to learn the Go lang. I’ve heard that Bubble Tea is the best of the best for making TUIs, but I’ve never used it because it requires using Go. Since I want to learn Go anyways, I figured that I’d give Bubble Tea a shot. I was trying to think of what kind of TUI to make, and a nano/vim/helix style text editor was the first thing that sprang to my mind.
Obviously, my editor won’t have as many features as even nano (which is quite a low bar). I intend to have text rendering, file reading and writing, character insertion and deletion, an undo buffer, and maybe some other stuff.
Don’t hold me to this, but I might even implement markdown previewing via wrapping the Glamour utility.
The goal that I’m setting for myself is to write my final devlog for
deciusingdeciitself.
Anyways, the name deci comes from the Metric prefix Deci, meaning 10^-1. nano comes from Nano, meaning 10^-9. It’s a bit of wordplay:
deci binary will be much heavier than the nano binary because it has to bundle the Go runtime which is like 3-10 MB.deci editor, but tons of people know about nano.Anyways, all that I’ve done so far is come up with a plan, write the stub README, and make the terrible banner that you see below.
Next, I’m going to install Go and start reading the Bubble Tea docs.
Still working on the README. I think that Hadronize is more or less ready to ship now, but there’s probably still a bit more polishing I can do.
In my other Stardance projects, I’ve gotten really positive feedback on my READMEs, so apparently my writing style and README format of choice is a winning combination. I’ve been referencing my past READMEs and the Stardance README guide a lot while writing this one.
The bulk of what I added was the How it Works section, which explains some of the technical details and design choices that I made. It’s a dense topic so it’s obviously a long section. At first it was just a wall of text, even when I tried to break it up with lists and subheadings. So I spent about half an hour taking screenshots and editing them in GIMP until I had decorative images that I was satisfied with for each subsection. I think that drop shadows on transparent backgrounds look really cool in README images, so I used GIMP’s drop shadow filter to add them to my screenshots (in addition to cropping, rounding corners, et cetera). I kept fiddling with the drop shadow settings and I didn’t want to go back and redo the images that I’d already done, so the shadows are kind of inconsistent, which really irks me but I don’t think is very noticeable.
I also added a Running Locally section and fixed a few typos throughout the README.
Lastly, I did a bit of work on the home page. I added a big screenshot below the intro paragraph, thanks to feedback from the awesome @Luksus, whose project you should totally check out.
I worked on updating and basically completely rewriting the README to something more polished. I still need to add more sections and revise what I have, but I think that it’s alright. I’m trying to strike a balance between not having enough content and being a wall of text. It’s also surprisingly difficult to come up with bullet points for the features list.
After I finish up the README, I’m going to redesign the home page a bit. After that, I think I might be ready to ship.
I wrote a page that explains how to play, assuming zero knowledge. I think that this’ll be helpful for onboarding, because this way they learn the rules at the same time as they learn how to use the UI, rather than reading the abstract and theoretical rules and then trial-and-erroring their way through the UI.
The actual writing part took a few hours, and most of it was revision and trying to find better ways to phrase things. It’s nearly 1000 words, meaning I could turn it in for an essay assignment and not fail the word count, which is funny.
But what also took a lot of time was making the visual aid. I had to record myself performing the same action in the UI many times to get a perfect take, and then I had to craft an ffmpeg command to crop and trim it because I refuse to use graphical video editors. Also my recording software kept stuffing H.264 video data in a webm container, which made ffmpeg freak out because that file is technically corrupted. I also had to figure out how to screen record on my phone to get a tap indicator, which I’d never done before.
You can read the page here: https://ethmarks.github.io/hadronize/how
I’ve decided to stop working on the driver ranking arena. I already have an MVP that works well enough to kinda sorta determine which drivers are good and which are bad, but it’s not robust and it just sums up rankings rather than implementing an actual Elo algorithm. The reason I’m not going to continue working on it is that it seems like a lot of work, and I don’t think that it’ll provide any value. Like, at all.
The root of the problem is that Hadronize just isn’t a strategic-enough game. There’s certainly some strategy, of course, and probabilistic modeling, but luck plays just as much of a factor. The cleverest Hadronize driver can still routinely lose to the dumbest one just due to slightly bad luck. So it doesn’t really make sense to build tooling for strategic bots if the strategy is not a very big aspect of the game.
Thinking back, I’m not really sure why I decided to start working on the driver arena in the first place. It just seemed like the right thing to do at the time even though I now realize that it would be kind of useless.
Anyways, I also created two new drivers.
charm). It’s obsessed with trying to collect charm quarks, because its a charming gentleman. I made it as a joke because I thought it was funny, but to my complete surprise it turns out to be a pretty good strategy? Not charm quarks in particular, but trying to collect a specific flavor of quark at all costs turns out to be surprisingly effective. It consistently wins more often than EVan, the driver that I specifically built to be a good strategy and that actually does probabilistic expected value calculations.I think that next I’ll start working on the README and documentation, because I haven’t updated the README in like 3 weeks and it’s extremely outdated.
Before I started working on the bot ranking arena, I did a quick sidequest to figure out how many games per second I could simulate. So I wrote this script to execute a bunch of games sequentially, then print the elapsed time and calculate the games per second rate.
I ran the benchmark in Node 24.15.0 on my 4.40 GHz Intel i5-12450H using Bogo drivers:
Simulated 82157 turns across 1000 games
Took 109048ms (109.048s). Speed is 9.170273640965446 games per second
So that was surprising. I was expecting a speed in the low thousands, but instead it was less than 10.
I had a suspicion what was causing the slowdowns. To make sure, I swapped out the Bogo driver (which uses QuickJS) with a non-QuickJS driver that I made for testing, and reran the same benchmark:
Simulated 53854 turns across 1000 games.
Took 130ms (0.13s). Speed is 7692.307692307692 games per second
(the turn count is smaller because the drivers used a different strategy which made games end faster)
So, obviously, the slowdown is caused entirely by QuickJS. Which makes sense. Every time a QuickJS driver is invoked, it spins up a brand new QuickJS VM, configures it, executes the actual code, extracts the return value from the VM, disposes of the VM to avoid memory leaks, and then finally returns the value. Non-QuickJS drivers can just execute the code directly, and the code can be optimized very well by the JIT compiler that Node uses.
So the solution seems simple, right? Just don’t use QuickJS for the drivers. Whatever the advantages of using QuickJS are, surely they aren’t worth an 839x decrease in performance. Well, unfortunately that’s not an option. More specifically, dynamically running strings of JS (like the driver code that users write at runtime) as native code is not possible, so I have to use a JS VM. And QuickJS is by far the best embeddable JS VM that I know of. So I just have to accept the slowness.
This means that I’ll have to be clever about the bot arena, though. I can’t just brute-force a ranking with thousands of games if I can only run 10 games per second.
btw, if you’d like to see how your PC does on my benchmark, just run these commands:
git clone https://github.com/ethmarks/hadronize.git
cd hadronize
pnpm install
pnpm bench --count 1000 --driver prng
If your time is faster than mine, leave a comment to gloat.
I integrated the QuickJS driver code that I made last devlog into the rest of the site. Things I did:
Math.random function and replace it with a mulberry32 prng seeded by stringifying the game state and hashing it through djbs2. So now drivers can still use Math.random without making the rng predictable or sacrificing perfect determinism.contenteditable and Svelte’s bind:textContent to let users edit the title, ID, and description. I used Prism code editor as the embedded IDE for editing the actual driver code.localstorage on page load, defaulting to the stock drivers. I also made the bot editor automatically sync the drivers to localstorage.I think that next I’ll work on a driver ranking system so that when I make more drivers, I’ll know whether they’re better or worse than the others. I might research how chess ranking systems work and copy concepts from those.
I can now store driver strategy code as strings, execute the strings as JS code using QuickJS, read the quickjs output, and use it as driver output.
This might not sound like much of a difference, but it completely changes how I can run drivers. Before, all driver strategies had to be present in the actual source code. Now, they just have to be strings. Because we can manipulate strings at runtime, this means that we can create new drivers while the site is running without modifying the source code. In other words: this lays the groundwork for letting you create and play against your own drivers without making a PR to the repo.
To make sure that my implementation works, I converted the expected value driver to use quickjs. I enclosed all of the actual logic in backticks, making it a giant string, and then passed the string through my quickjs driver factory function. I ran the expected value driver test suite, and it failed immediately because I forgot to remove the type annotations (quickjs can only execute plain JS, not TS). But once I fixed that, all 19 tests passed, and it seems to work perfectly from my playtests.
I eventually plan on adding a page that uses Prism code editor to let users edit driver strategies and save them to localStorage. But first I’m going to add more features to the quickjs runtime. For example, I want to have it swap all Math.random() calls with a deterministic psuedorandom generator, so that way developers can use randomness without making it non-deterministic.
I made new quark graphics, like I promised last devlog. I’m not really sure if it fits the aesthetic I’m going for, but it’s something, and I tried to make it extensible so that I can experiment with more styles later.
I also added a dropdown menu in the setup flow so that you can select which style you want to use, though I might remove that in the future if I decide on one style to be the definitive one.
The new style is just patterns masked onto the same flat color circles as before. Each flavor has a different pattern:
I don’t know if this makes the quarks look nicer or not, but it definitely makes them more accessible. If the abbreviation text isn’t visible for whatever reason (e.g. because the chamber is in count layout mode), quark flavors used to be distinguished solely by color. But the new patterns make them distinguishable without color, which is important for colorblind accessibility.
This is a smaller devlog. Stuff I did since last devlog:
Next, I’m going to work on improving the quark graphics. I’m trying to go for an aesthetic that looks like it’s from a physics textbook (hence the dot grid backdrop I’ve had for a while), so it’s already pretty close, but imo it looks a bit too plain right now. I don’t have a precise vision for exactly what I want the quarks to look like, so I’ll need to figure that out.
Idk why I was worried that this would be difficult. Less than 20 lines of code later, Hadronize now automatically detects whether you’re using a fine pointer (e.g. a mouse) or a coarse pointer (e.g. a touchscreen), and changes its behavior accordingly.
So now you can select chambers on touchscreen just by tapping on them. You don’t have to drag the superposed quark, you just tap.
I recorded a video showcasing this in action using Firefox’s mobile device simulator (aka RDM). As you can see, when I disable touch simulation, the chambers respond to hovering and I have to drag the quark around. But when I enable touch, the chambers don’t react at all until I click one, and then the superposed quark gets added to that chamber like normal.
I’ve also tested this on an actual phone and it works perfectly, but it’s more difficult to record on there which is why I used Firefox’s simulator for the video.