You are browsing as a guest. Sign up (or log in) to start making projects!

ethmarks

@ethmarks

Joined June 13th, 2026

  • 44Devlogs
  • 4Projects
  • 3Ships
  • 33Votes
Ahoy! I'm Ethan Marks. I'm 15 years old and I'm entering my third year at my local community college. I love programming, especially web development.
Ship Pending review

Hadronize: a quark-themed set collection game.

It has a web UI with a custom SvelteKit virtual layout engine, a polyglot CLI that runs across Node, Deno, Bun, and the browser console, a QuickJS bot sandbox, a comprehensive test suite, and more.

I'm really proud of Hadronize and hope that you enjoy it.

  • 26 devlogs
  • 118h
Try project → See source code →
Open comments for this post

26m 14s logged

Shipping

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.

1
0
5
Open comments for this post

1h 7m 46s logged

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.

0
0
12
Open comments for this post

2h 12m 17s logged

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.

0
0
6
Open comments for this post

1h 8m 38s logged

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.

0
0
6
Open comments for this post

2h 19m 10s logged

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:

  • “inserted ‘a’ at 4:2”
  • “split line 4 to line 5”
  • “merged line 5 with 4”
  • …et cetera

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.

1
0
45
Open comments for this post

1h 45m 42s logged

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:

  • the cursor is now the actual terminal cursor rather than a fake drawn-on cursor.
  • if you use left arrow at the start of a line, the cursor moves to the end of the line above.
  • if you use right arrow at the end of a line, the cursor moves to the start of the line below.
  • delete! It’s exactly like backspace but in the opposite direction.
  • tab! As mentioned, 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.

1
0
9
Open comments for this post

3h 20m 22s logged

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:

  • backspace! If you press backspace, it’ll either delete the character immediately before your cursor, or if you’re at the beginning of a line, it’ll merge current line with the previous one and then shift all other lines upwards.
  • enter! If you press enter, it’ll bifurcate the current line and split the right half onto a new line.
  • the cursor will now stop going out of bounds. It automatically clamps the mouse X position to the max length of the current line.
  • the top row is now reserved for a nano-style header that currently says deci 0.0.1 on the left and by @ethmarks on the right.
  • the cursor now highlights the character under it rather than replacing it with a solid white square.
  • plus lots of bugfixes, mainly fixing bugs that I added while adding the other features mentioned in this list.

Features I want to add next:

  • highlighting the active line by lightening the background color
  • a status bar
  • line numbers
  • a keybind info bar

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.

0
0
7
Open comments for this post

4h 2m 55s logged

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:

  • reading files
  • creating files if they don’t exist
  • transforming the lines of a file into a slice (Go’s version of dynamic-length arrays) of strings
  • displaying the lines on screen
  • moving the cursor around
  • displaying the cursor
  • preventing the cursor from going out of bounds
  • inserting characters onto the current line at the cursor position
  • displaying a line of status text
  • writing lines to the file

Things that might seem like they would work based on the list above, but actually don’t:

  • backspaces
  • creating new lines (aka enter)
  • scrolling through a file
  • displaying the cursor when it isn’t directly over an existing character
  • preventing the cursor from going out of bounds when it goes above or below a line that’s longer than its neighbors
  • other subtle cursor behaviors that are difficult to describe that we take for granted in editors but are surprisingly difficult to implement

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.

2
0
20
Open comments for this post

46m 4s logged

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 deci using deci itself.

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:

  • The Deci prefix is far larger than Nano, and I expect that the 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 is not a very commonly used prefix. There’s basically nothing that uses Deci other than decibels and deciliters, and the “deci” in both of those terms is easy to miss (did you know that decibels are based on Bels? Also, the wikipedia page for Bel redirects to decibel). Nano, on the other hand is extremely common; everybody knows about nanobots and that “nano” means “very small”. Likewise, nobody except you, me, and a handful of other people know about 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.

0
0
9
Open comments for this post

5h 8m 13s logged

Revised Readme (again)

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.

0
0
13
Open comments for this post

2h 44m 15s logged

Revised README

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.

0
0
11
Open comments for this post

6h 27m 50s logged

How to play page

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

0
0
14
Open comments for this post

3h 36m 50s logged

Discontinuing the ranking arena

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.

  • The first is called Kingslayer, and it just tries to steal from the player who’s currently winning. It’s not very good at actually winning, but it does make things more challenging for the other players.
  • The second is called Gentleman (aka 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.

0
0
8
Open comments for this post

1h 48m 34s logged

Benchmark

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.

0
0
7
Open comments for this post

6h 44m 53s logged

Bot editor

I integrated the QuickJS driver code that I made last devlog into the rest of the site. Things I did:

  • I made the QuickJS VM override the default 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.
  • I gave the drivers names and descriptions. The prng driver is named “Bogo” (after Bogosort) and the expected value driver is named “EVan” (because it’s a normal name that starts with the abbreviation of “Expected Value”).
  • I moved the code for the existing drivers into a centralized array, rather than in multiple different files that had to be imported individually. I’m calling this array the “stock drivers”.
  • I added a bot editor page. You can check it out at https://ethmarks.github.io/hadronize/bots. I used 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.
  • I made the play page fetch the list of drivers from localstorage on page load, defaulting to the stock drivers. I also made the bot editor automatically sync the drivers to localstorage.
  • I added a new driver strategy: tit for tat. As expected, it plays pretty terribly because that strategy just isn’t very good in Hadronize. I mostly made it so that users looking at the stock drivers can learn how to reference the timeline from the state.

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.

0
0
9
Open comments for this post

1h 19m 50s logged

QuickJS drivers

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.

0
0
8
Open comments for this post

1h 5m 16s logged

New quark style

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:

  • up: vertical lines
  • down: horizontal lines
  • charm: checkerboard
  • strange: square grid with irregular shading
  • top: light dot grid
  • bottom: dark rhombus grid
  • hadron: solid (unchanged)

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.

0
0
8
Open comments for this post

2h 21m 28s logged

Minor graphics improvements

This is a smaller devlog. Stuff I did since last devlog:

  • Fixed the winning animation to work with the new layout planner
  • Added transitions to the quark text. Whenever a quark changes its display text (e.g. from a ‘?’ to a flavor abbreviation like ‘s’), rather than instantly snapping to the new text, it’ll do a quick crossfade.
  • Changing the font used for the quark text from the default to a font called Degheest that I think looks nice.
  • Merging the layout refactor into prod because I’ve decided that it seems stable enough.

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.

0
0
4
Open comments for this post

29m 3s logged

Touchscreen support

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.

0
0
7
Loading more…

Followers

Loading…