Hadronize
- 26 Devlogs
- 118 Total hours
Quark-themed set collection game you can play in your browser
Quark-themed set collection game you can play in your browser
As a prize for your great work, look out for a bonus prize in the mail :)
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.
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.
It took an entire day, but I did it. Hadronize will now automatically adjust 5 different layout variables in order of priority to make everything fit on screen.
I think that the algorithm is probably the most complicated part of this entire project. Iām going to try to explain it really simply in this devlog, but if you want to see how it actually works, I wrote a bunch of comments that explain the priority pipeline in this file.
So basically, every time a new layout update is triggered, the layout manager calculates a brand new layout plan from scratch. Once itās finished calculating, it applies the layout plan onto the actual layout variables that are used to move the quarks on the screen. If I modified the layout variables directly, the rapid-fire modifications while calculating would trigger a bajillion reactive updates from Svelte which would slow the page down.
Anyways, the way that the layout plan is calculated is that it starts by assuming ideal conditions: there is more than enough room for everything to fit comfortably. So it does the calculations using the preferred ideal variables, it modifies the plan, and then it actually checks to see if anything is overlapping. If there is any overlap, it tries to resolve it using the least-disruptive adjustment it can make. If that doesnāt work, it tries the next least-disruptive adjustment. And so on. Also, each time it moves up the chain of disruptiveness, it tries to use the ideal variables for all of the less-disruptive adjustments. If that makes sense.
Here are the adjustments that it can make, in ascending order of disruptiveness:
Iām pleased to report that when I tested it on my phone, everything worked perfectly. I still havenāt implemented mobile input yet, but I started a 6-player game of all bots, and the layout planner managed to keep all of the quarks visible all of the time, even when a single chamber had more than 20 quarks. Never once did I see two chambers overlap with each other or go partially offscreen, which used to happen 100% of the time on small mobile screens. It just changed layout modes and decreased the quark size whenever it started to run out of room.
Anyways, Iām not going to merge the new layout planner just yet because Iām not 100% sure how stable it is, but Iāll do that soon. Next up is mobile input.
So the first step to mobile support is making the all of the quarks in all of the chambers actually fit on a phone screen. Right now they just overlap a bunch and you canāt tell which quarks belong to who. I have some ideas for an algorithm that can automatically adjust layout variables until everything fits, but first I need to give the algorithm things that it can adjust.
The crux of the problem is that thereās simply no way to fit 6 chambers in a circular ring around the center of the screen on mobile. Thereās just not enough screen space. So I made a new layout that places chambers in a grid instead of a ring. It works on desktop in addition to mobile. I donāt think it looks quite as nice, but itās much more space efficient.
I attached some side-by-side comparison screenshots. As you can see, the quarks still donāt quite fit on mobile even with the grid layout, but itās much closer.
Next step is either writing the automatic layout algorithm, or making input work on mobile.
I added sound effects to the game UI. A unique sound plays for each of these:
I sourced them all from https://freesound.org/, and I left attribution for each one in sounds.svelte.ts. I also modified a few in Audacity to trim them or apply simple effects.
Iām not sure that Iām 100% satisfied with the sounds. I sourced almost all of the effects from different freesound sounds because I couldnāt find a sound pack that I liked, so the sounds donāt quite match tonally. Some of them are generic UI sounds while others are clearly scifi-ey. I may change some of the sounds later if I find more cohesive ones.
Iād never done audio in HTML/JS before, so this was new for me. I originally just used the Audio() constructor because that was the most obvious way to play, but when I realized the many issues with it (though it did work!), I rewrote my code to use AudioContext. I also made a multi-step preloading and decoding pipeline that fetches all sound effects on page load, but doesnāt decode them until the user presses a button because otherwise JS throws a warning in the console and suspends the audio context.
I think that the sound effects make Hadronize feel a lot more polished and nicer to play.
Next up on my list is mobile support. Iām not looking forward to it.
(Make sure to turn on sound for the video below)
https://github.com/ethmarks/hadronize/commit/3a5adcb
I started working on writing audio code and looking through sound effect libraries, but when I tried to hook my WIP sound-playing function into the mouse handler, I realized what an awful mess my mouse handler code was.
So I spent over an hour refactoring it. While I was at it, I optimized it a tiny bit and fixed the hover thrashing bug that had been bugging me for nearly 2 weeks now.
So, funny story about the site styles. When I first made the CLI page 3 weeks ago (has it really been that long?! wow), I added holiday.css to make it look nice as a stopgap measure until I made the real styles. However, as the old saying goes, thereās nothing more permanent than a temporary solution. Iāve decided to just embrace using holiday.css for the entire site because it looks much nicer than anything I could make. The styles in the actual game component are entirely my own, of course, but the homepage, rule page, and rewritten CLI page all use holiday.css.
āwhat rule page and rewritten CLI page?ā, you ask? Thatās the other thing that I did this devlog. I created a dedicated page on the site for the rules, and I rewrote the CLI page to only contain instructions on how to use the CLI rather than a playable browser CLI. Donāt worry, you can still play Hadronize via the browser console. The main UI has had that functionality since the beginning. I just didnāt see a reason to maintain two different game UIs, especially because the main UI can do everything that the playable CLI page could (and much more).
I think that next Iāll start polishing the game UI. Itās long overdue. Iāll add sound effects, add mobile support, and maybe (donāt hold me to this) improve the graphics.
diff from last devlog to this one
I added a setup flow to the main UI page, and did a lot of refactoring.
I also removed support for setting up the game via browser console. I didnāt make this choice lightly because it was a fun feature, but the tradeoffs were just too much and I decided that it wasnāt worth it, especially because the console setup is much clunkier than just using the HTML form, so I doubt anyone would even use it.
But anyways, the main focus of this devlog is the setup flow on the main UI page (aka the play page). I also added some pretty sleek animations when activating and deactivating the game overlay.
diff from last devlog to this one
I refactored the Hadronize UI code. Rather than one giant 570-line file that handled the quark positioning, state tracking, mouse movement, and game loop, I split the code into four different files, each dedicated to one specific thing, with an average line count of ~154 (191, 184, 111, and 132, respectively). This makes the codebase way more maintainable and easy to work in.
I also deduplicated most of the main game loop code. Before, I had just copied the entire content of the executeTurn() function, pasted it into the UI code, and customized it until the logic code correctly interfaced with the UI. Maintaining two separate functions that do basically the same thing is bad practice, so today I came up with an alternative solution: adding a bunch of āhooksā as optional parameters, which are just functions that the executeTurn() function runs at specific stages. In a CLI game, the hooks are never provided to the function, so theyāre never run and nothing changes. But the UI code does provide hooks, to do things like trigger re-renders. This way, the same code can be reused for two different use cases (CLI and UI).
Thereās still more refactoring to do, but I donāt think that Iāll do any more major restructuring.
diff from last devlog to this one
Iāve been working a lot on the Hadronize UI. Last devlog, all that it could do was render all of the quarks into a big overlapping heap and you could drag around one specific quark. Now, you can play a full game in the UI from start to finish, and itās reasonably polished.
I also conducted the first playtest with a few friends. Nothing broke during the playtest and they all liked it and gave positive feedback, which was really encouraging. We all got absolutely crushed by the expected value driver I built last devlog.
It sounds hyperbolic to say that this UI is the messiest code Iāve ever written, but I legitimately canāt think of any other contenders. Of the 800+ lines of code in the UI, about 570 of them are in one massive Game.svelte component. This component handles state tracking, mouse movement, the main game loop, and most of the geometry to position the quarks. The very next thing that I need to do is massively refactor the UI code.
After that, I have a few ideas:
Each player in Hadronize is controlled by a Driver, which is basically just a function that takes the current game state as an input and outputs a player. I implemented drivers on like day #2, but I donāt think Iāve talked about them in a devlog yet.
Math.random. Instead, it stringifies the game state, converts that string into a 32-bit integer via a djb2 hash, then uses that integer to seed a mulberry32 pseudorandom number generator, then uses the output of the mulberry to make its player selection.Iāve started building the UI, but Iāve still been procrastinating on actually working on it in earnest. I should probably do that.
You can now add arguments to the Hadronize CLI in the terminal, which lets you avoid having to use the setup flow. Hereās an example.
pnpm run cli --seed 12345 --player Alice:human --player Bob:bot
It also works if you run the CLI as a remote script.
deno run -A http://ethmarks.github.io/hadronize/cli.ts -s 8 -p a:bot -p b:bot -p c:bot
This was pretty easy to implement. I just used Nodeās parseArgs utility for the main arg parsing and wrote a bunch of validation logic that converts the raw arguments (which are always strings) into the data types that my code uses.
Over the past couple days, Iāve made a little test suite for the game logic using Vitest. Iāve written 19 individual tests, but some of them repeat multiple times with different game seeds and whatnot to ensure that it works with a variety of inputs, so Vitest actually runs 101 tests in total.
All of the tests pass, and I didnāt discover any bugs, which either means that I managed to get all the game logic correct on the first try, or that thereās something wrong with my tests.
I think that Iāve probably spent too much time on the CLI. I donāt think that it was time wasted, but I probably ought to get started on the main game UI.
You can now run Hadronize CLI in a terminal without cloning the repo. If you run the command below, Deno will automatically fetch all the dependencies.
deno run -A http://ethmarks.github.io/hadronize/cli.ts
Node doesnāt support remote scripts because it lacks Denoās awesomeness, but because of the Deno binary on npm, you can run Deno (and therefore Hadronize) through any of the other package managers!
npx deno run -A http://ethmarks.github.io/hadronize/cli.ts
pnpm dlx deno run -A http://ethmarks.github.io/hadronize/cli.ts
bunx deno run -A http://ethmarks.github.io/hadronize/cli.ts
For example, in the attached screenshot, I SSH-ed into one of my home servers that hadnāt cloned the Hadronize repo nor installed Deno, and tried to run Hadronize as a remote script via pnpm. After resolving dependencies for a few seconds, it ran flawlessly.
I first encountered Deno remote scripts in Lumeās init command, and my implementation is heavily inspired by it. Basically, all that the cli.ts file does is fetch the CLI code from JSDelivr, which fetches it directly from the GitHub repo. From there, Deno is smart enough to resolve all of the CLI codeās dependencies and run it.
Before todayās changes, running Hadronize via Deno worked perfectly fine. deno task cli behaved identically to pnpm run cli. However, thatās because Deno has an automatic Node compatibility layer, not because Deno can actually run the code natively. That compatibility layer doesnāt really work with remote scripts (because Deno canāt find the package.json), so when I first tried to run the remote script while developing it, it crashed and burned immediately. This was because, without the compatibility layer, Deno and Node handle things fundamentally differently. There were several differences, some of which I expected and some of which I didnāt, but the easiest one to understand is NPM imports.
My CLI code relies on the picocolors library for getting ANSI escape codes. When I was first writing the code, I added the picocolors package via NPM and imported it in the normal Node way:
import pc from "picocolors";
The problem is that Deno canāt parse bare imports like that. Deno expects this format, which Node cannot parse:
import pc from "npm:picocolors";
I tried configuring an import map in deno.json, but it didnāt work. I considered just hardcoding the ANSI escape sequences, but I decided that I would need to figure out how to polyglottally import NPM packages in both Node and Deno at some point, so I might as well figure it out now.
My solution was to create a deps/picocolors.ts file to proxy the picocolors import by dynamically importing it based on whether itās being run in Deno or not:
const { default: pc } =
"Deno" in globalThis
? await import("npm:picocolors")
: await import("picocolors");
export default pc;
NPM imports were only one of the issues that sprung up. Frustratingly, the only way to test whether or not a solution worked was to commit, push to main, and test on production. The whole Deno compatibility saga required 12 commits in total.
Iām still working on writing unit tests for the game logic. I already have a few tests, but theyāre definitely not comprehensive. Iāll write a devlog about the tests once Iām finished with them.
diff from last devlog to this one
Since my last devlog, I added a flow that lets users set up each game before starting it. The setup includes specifying the seed, player count, the names of each player, and whether each player should be a human or a bot. I made it work in three different formats, each of which had to be implemented individually:
NbrInputFunc I mentioned last time, and repeats this until the user provides an input that passes the validation function. Pretty standard stuff.window property that resumes execution, then halts execution with a Promise<void> until the window property is called. Unfortunately, it wasnāt possible to use the anonymous getter trick that I used last time. If I did that, I would have to create custom window properties for every possible 32-bit number to allow user to enter the seed, and create custom properties for every possible string to allow users to enter player names. That obviously wasnāt possible, so instead I just created a window.r function that takes any input type. The ārā stands for ārespondā. To set the seed to 42, you type r(42) into the console, which is a reasonably clean UX, I think, though not as clean as just typing 42 like you can in the terminal.<input>s and SvelteKitās reactivity. The trickiest part was navigating the fact that the value of one of the inputs (player count) controls the structure of some of the other inputs (player configs). At first I tried writing code to dynamically construct a variable-length Array, but that was a nightmare of complexity. Then I tried making all 6 possible player configs visible all the time and just adding the āDisabledā type, but that made the UI look ugly and was also kind of complicated to implement. The solution I settled on was to internally use an Array with a fixed length, automatically hide the player config UI elements with an index less than the player count value, and slice the player config Array before passing it into the main game function. I think that this solution is the best of both worlds while also being fairly simple to implement.I know that Iāve been saying this for the past 4 days, but I need to write some internal docs for my game logic code and write tests. I havenāt noticed any bugs during my playtests while building the CLI, but Iād like the peace of mind to be fairly confident that Iāve implemented my game rules correctly.
Since my last devlog, I implemented a CLI for Hadronize that works across Node, Deno, Bun, and browsers. You can try it out by visiting https://ethmarks.github.io/hadronize/cli or running the following commands (you only have to run one of the Node/Deno/Bun commands):
# Clone the repo
git clone https://github.com/ethmarks/hadronize.git
cd hadronize
# Run with Node (via npm or pnpm)
npm install
npm run cli
# Run with Deno
deno task cli
# Run with Bun
bun install
bun run cli
Link to the diff between last devlog and this one
In case youāre not familiar with the JS ecosystem, a cross-runtime CLI like this is not normal. My CLI uses styled text, but the different runtimes have different ways of logging styled text. Node, Deno, and Bun all use ANSI escape characters, but browsers use %c placeholders (Deno also supports %c because Deno is awesome, but none of the others do). Node uses the readline API to get user input, but Deno and Bun both use prompt(), and browsers donāt even have a way to get user input without clever tricks (as youāll see later).
My solution to the styled text problem was to create a custom micro-library which I call styledLog.ts (or sl for short) which invents an easy-to-use syntax for rich-text logging called slChunk, then automatically detects if itās running in a browser and renders slChunks with either ANSI escapes or %c placeholders accordingly.
My solution to the user input problem was a little less elegant, but in my opinion itās much cleverer. Node, Deno, and Bun were the low-hanging fruit. All I had to do was create an abstracted NbrInputFunc() function that automatically detects which runtime is running the code, then switches out the method it internally uses to fetch input accordingly. Easy.
But implementing a CLI in the browser was a lot more tricky. I could have just used the window.prompt() method, but that opens a disruptive pop-up window that looks terrible. Instead, my code dynamically adds custom properties to the window object, then halts the script execution via a Promise<void>. Each custom property is named after a player name and has an anonymous getter function. When you type a playerās name (e.g. alice) in the console, it automatically evaluates window.alice, which triggers the getter. The getter sets the userInput variable then resolves the frozen Promise to resume execution.
I hope that made sense. Itās a really hacky solution, but it does work, and it provides an elegant input syntax that resembles that of terminal CLIs. My first idea (which is still used as the fallback) was to just define a window.turn() function, so you would enter your input with turn("alice"). The current solution is much more elegant, in my opinion, because the user doesnāt need to type parenthesis or quotes.
Hereās a link to the code if you want to check it out for yourself.
I also started working on the UI the tiniest bit. Not anything thatāll make it into the final version, mind you, but I added a page that demos the Hadronize CLI. It seemed wrong to make the demo page blank and not include instructions, so I wrote some. It seemed wrong to use unstyled bare HTML, so I added holidaycss.
I think that thereās still some work I need to do in the game logic and CLI before I start working on the UI. The CLI demo currently doesnāt have any way to customize the game settings (e.g. how many players there are and what deterministic seed to use), so Iāll probably work on that next.
Thanks for reading!
Today I implemented most of the core game logic in TypeScript. My code is pretty messy, probably has some bugs, and definitely has some idiosyncrasies that I should add docs for, but it does work correctly. Mostly. Here are a few highlights from today:
The rules specify that quarks are created on-the-fly at the start of each turn, and that itās impossible to predict what flavor a superposition will collapse into. Nondeterministic code makes me uncomfortable, so I decided to cheat a bit behind the scenes to make the whole thing pseudorandom and fully deterministic. In the game constructor, every quark that will ever be used during the game is pre-generated based on values from a mulberry32 function, then the collapsed flavor is predetermined ahead of time. Those two things: the list of future quarks that will be ācreatedā on subsequent turns, and the flavor that each superposition will collapse into, are secret information that the players arenāt allowed to know, which means I have to be careful to sanitize the game state before itās given to players.
So, one of the first things that I did was create the Quark class. It has a few methods, but mainly itās just a DTO for a flavor and a superposition. So my original plan was involved doing normal OOP and having each quark be owned by a Player class or the Game class itself. But once I actually started coding it, I remembered that I hate having to remember whatās stored as a value and whatās stored as a reference in JS. This is especially annoying when moving quarks between containers. So instead, I decided to just store an array of every extant quark, including not only a flavor and superposition but also an index. When a player class āownsā a quark, it really just owns that quarkās index number which corresponds to a quark in the game classās SSoT. Crucially, the index number is a simple integer that doesnāt have any of JSās object reference silliness.
I decided that the game class should save every past state that itās been in for some reason. I might use it in the UI to let users scroll back in time to see how the game played out, or maybe use it to make neat statistics at the end of the game, or something like that. It might also be useful for bots (more on that in a future devlog). But anyways, actually implementing the timeline was extremely frustrating, mainly due to Typescript. The final implementation that I settled on is pretty clean, but it was not my first thought, and I went through maybe a dozen different iterations over the course of an hour.
Iām probably going to spent a bit more time in the game logic before starting work on the UI. As I mentioned, I need to write docs for the idiosyncrasies of my code because otherwise Iāll forget how it works and thatās how bugs happen. Speaking of bugs, Iām sure that there are a few in my code, so Iāll probably write some tests to try to find them.
Thanks for reading!
This is my first devlog for this project. Itās a multiplayer strategy game that you can play in your browser.
https://github.com/ethmarks/hadronize
Basically the only things that Iāve done so far are write the rules in the README and brainstorm a bunch. The rules are pretty important, so Iāll write them in this devlog. Because thereās a 4000-character limit to Stardance devlogs, Iām going to try to keep the rules shorter and less thorough than I did in the README.
Hadronize is a 2-6 player game which will probably take about ~10 minutes per game.
If my explanation is unclear, check out the README because I probably explained it better there. If the README is unclear too, feel free to let me know by commenting on this devlog or opening an issue or whatever.
If the rules sound familiar, itās because I mostly just stole them from the card game Mantis by Exploding Kittens. My original idea was to just create a digital version of Mantis, but I was concerned about running into copyright trouble if I called it āMantisā and used the same terminology and whatnot. Game mechanics canāt be copyrighted, but names and terminology absolutely can. I considered just making my game about abstract colors, but then I suddenly realized that making it about quarks would fit really well, so I did that instead. I did make a few changes to the core mechanics (e.g. using 6 flavors instead of 7 colors, and combining āscoringā and āstealingā into āobservingā), but itās still basically just a reskin of Mantis.
The next thing Iām going to work on is actually implementing the rules in TypeScript. After that Iāll probably work on the UI.
P.S. I also spent like an hour making a terrible-looking banner for Hadronize to use as the Stardance project image.