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

ethmarks

@ethmarks

Joined June 13th, 2026

  • 11Devlogs
  • 2Projects
  • 1Ships
  • 7Votes
Ahoy! I'm Ethan Marks. I'm 15 year old and I'm entering my fifth semester at my local community college this coming Fall. I love programming, especially web development. My best language is probably TypeScript.
Ship

This is a theme for the [Lume static site generator](https://lume.land/) based on [Tufte CSS](https://edwardtufte.github.io/tufte-css/).

> **To try the Tufte theme, visit the demo page: **.

* I submitted [this PR](https://github.com/lumeland/themes/pull/10) to the official Lume theme registry to add the Tufte theme, which was accepted. It has its own page on the official Lume site:
* I used several optimization tricks, such as statically analyzing the content of each page at build time to infer which font files to preload, to maximize performance, accessibility, best practices, and SEO. Because of this, the theme [earns perfect 100s on Google Lighthouse](https://pagespeed.web.dev/analysis?url=https%3A%2F%2Fethmarks.github.io%2Flume_tufte%2F), which the official Lighthouse documentation describes as ["extremely challenging to achieve and not expected"](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring#:~:text=To%20provide%20a%20good%20user,90%20to%2094).
* I wrote 5000+ words of documentation across the README and demo articles.
* I wrote [Lume CMS](https://lume.land/cms/) config for every aspect of the theme to allow for no-code content management.

  • 4 devlogs
  • 15h
Try project → See source code →
Open comments for this post

3h 18m 40s logged

Drivers

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.

  • The first driver I made is the dogpile driver. It’s logic is extremely simple: it just selects the player who goes first. It doesn’t even reference the game state at all. If every player is a dogpile driver, this means that everyone just dogpiles onto the first player and ends up stealing basically all of its quarks before it can use them. This is obviously a terrible strategy because it’s not even capable of trying to score, but it was very simple to implement and I use it as a placeholder.
  • The second driver I made is the rng driver. It just makes random decisions. However, because I insist on everything being deterministic, it can’t just use 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.
  • The newest driver that I just finished developing is the expected value driver. It does actual strategic calculations to determine what the expected value of choosing each player would be based on the information it has available, then selects the player that maximizes that value.

Next Steps

I’ve started building the UI, but I’ve still been procrastinating on actually working on it in earnest. I should probably do that.

0
Original post
@ethmarks

Drivers

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.

  • The first driver I made is the dogpile driver. It’s logic is extremely simple: it just selects the player who goes first. It doesn’t even reference the game state at all. If every player is a dogpile driver, this means that everyone just dogpiles onto the first player and ends up stealing basically all of its quarks before it can use them. This is obviously a terrible strategy because it’s not even capable of trying to score, but it was very simple to implement and I use it as a placeholder.
  • The second driver I made is the rng driver. It just makes random decisions. However, because I insist on everything being deterministic, it can’t just use 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.
  • The newest driver that I just finished developing is the expected value driver. It does actual strategic calculations to determine what the expected value of choosing each player would be based on the information it has available, then selects the player that maximizes that value.

Next Steps

I’ve started building the UI, but I’ve still been procrastinating on actually working on it in earnest. I should probably do that.

Replies

Loading replies…

0
5
Open comments for this post

5h 10m 41s logged

Tests and CLI Args

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.

Tests

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.

Next Steps

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.

0
Original post
@ethmarks

Tests and CLI Args

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.

Tests

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.

Next Steps

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.

Replies

Loading replies…

0
3
Open comments for this post

6h 23m 50s logged

CLI Remote Script

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.

How it works

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.

Compatibility

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.

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.

Next Steps

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.

0
Original post
@ethmarks

CLI Remote Script

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.

How it works

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.

Compatibility

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.

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.

Next Steps

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.

Replies

Loading replies…

0
1
Open comments for this post

5h 49m 13s logged

Setup Flow

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:

  • The terminal: at its core, this format is just a normal TUI. It prints input prompts, awaits the user’s input using the abstracted NbrInputFunc I mentioned last time, and repeats this until the user provides an input that passes the validation function. Pretty standard stuff.
  • The browser console: I used the same trick that I described last time to allow users to enter their input via the browser console. It creates a custom 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.
  • The webpage: this uses HTML <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.

Next Steps

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.

0
Original post
@ethmarks

Setup Flow

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:

  • The terminal: at its core, this format is just a normal TUI. It prints input prompts, awaits the user’s input using the abstracted NbrInputFunc I mentioned last time, and repeats this until the user provides an input that passes the validation function. Pretty standard stuff.
  • The browser console: I used the same trick that I described last time to allow users to enter their input via the browser console. It creates a custom 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.
  • The webpage: this uses HTML <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.

Next Steps

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.

Replies

Loading replies…

0
4
Open comments for this post

10h 19m 54s logged

Browser CLI

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

Abstraction

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.

Cross-runtime user input

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.

Beginnings of the UI

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.

Next Steps

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!

0
Original post
@ethmarks

Browser CLI

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

Abstraction

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.

Cross-runtime user input

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.

Beginnings of the UI

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.

Next Steps

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!

Replies

Loading replies…

0
4
Open comments for this post

6h 28m 9s logged

Core Game Logic

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:

Determinism

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.

I hate references

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.

Timeline

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.

Next Steps

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!

0
Original post
@ethmarks

Core Game Logic

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:

Determinism

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.

I hate references

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.

Timeline

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.

Next Steps

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!

Replies

Loading replies…

0
2
Open comments for this post

3h 38m 56s logged

Hadronize

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.

Rules

Hadronize is a 2-6 player game which will probably take about ~10 minutes per game.

  • The goal is to be the first player to hadronize 10 or more quarks.
  • Quarks are basically the cards. There are 6 different flavors of quark: up, down, strange, charm, bottom, and top. By the way, I’m not making that part up.
  • Quarks start out as Superposed Quarks, which are quarks in a state of superposition between three different flavors. Once the superposition collapses (more on that later), it chooses one of the three flavors in its superposition and becomes a normal quark with a single flavor.
  • Each player starts with four collapsed quarks with randomly-chosen flavors in their respective cloud chamber.
  • On each turn, a new superposed quark appears. The players know that the quark will be one of the three flavors in its superposition, but they don’t know which one. The player whose turn it is (aka the active player) will choose any player (including themselves) to be the observing player. The observing player observes the superposed quark, which collapses it and adds it to their cloud chamber.
    • If the observing player has none of that flavor (e.g. the new quark collapsed into charm and the player only has down quarks), then nothing else happens and the turn ends.
    • But if the observing player does have at least one quark of the same flavor, something does happen.
      • If the active player chose themselves to be the observing player, the new quark reacts with all existing quarks of the same flavor, and they all hadronize together.
      • If the active player chose a different player to be the observing player, the new quark reacts with all existing quarks of the same flavor, and they all quantum tunnel from the observing player’s chamber into the active player’s chamber. In other words, the active player steals some of the observing player’s quarks.

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.

Mantis

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.

Next Steps

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.

0
Original post
@ethmarks

Hadronize

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.

Rules

Hadronize is a 2-6 player game which will probably take about ~10 minutes per game.

  • The goal is to be the first player to hadronize 10 or more quarks.
  • Quarks are basically the cards. There are 6 different flavors of quark: up, down, strange, charm, bottom, and top. By the way, I’m not making that part up.
  • Quarks start out as Superposed Quarks, which are quarks in a state of superposition between three different flavors. Once the superposition collapses (more on that later), it chooses one of the three flavors in its superposition and becomes a normal quark with a single flavor.
  • Each player starts with four collapsed quarks with randomly-chosen flavors in their respective cloud chamber.
  • On each turn, a new superposed quark appears. The players know that the quark will be one of the three flavors in its superposition, but they don’t know which one. The player whose turn it is (aka the active player) will choose any player (including themselves) to be the observing player. The observing player observes the superposed quark, which collapses it and adds it to their cloud chamber.
    • If the observing player has none of that flavor (e.g. the new quark collapsed into charm and the player only has down quarks), then nothing else happens and the turn ends.
    • But if the observing player does have at least one quark of the same flavor, something does happen.
      • If the active player chose themselves to be the observing player, the new quark reacts with all existing quarks of the same flavor, and they all hadronize together.
      • If the active player chose a different player to be the observing player, the new quark reacts with all existing quarks of the same flavor, and they all quantum tunnel from the observing player’s chamber into the active player’s chamber. In other words, the active player steals some of the observing player’s quarks.

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.

Mantis

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.

Next Steps

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.

Replies

Loading replies…

0
2
Open comments for this post

3h 15m 29s logged

1.0.0

Unless I think of more stuff to add, this is probably my last devlog before shipping.

Lume Theme Registry

A few hours ago, the PR that I created last devlog was merged into the official Lume theme registry, so you can now install my theme with a single command using Lume init, and it now has its own dedicated page on the official Lume website: https://lume.land/theme/tufte/.

README

  • I added a How it Works section that explains how the Lume theme architecture works behind-the-scenes (spoiler: it’s just remote files) and explains what each of the 18 included plugins do.
  • I updated the quickstart to use the Lume init command.
  • I added a Showcase section that lists the sites that use this theme. It seemed kind of weird to exclusively list the Tufte theme site itself, so I also switched one of my documentation sites to use the Tufte theme.
  • I added a shields.io badge to the top of the README that links to the theme’s page on the Lume website. shields.io relies on simple-icons for its logos, but simple-icons doesn’t have the Lume logo for some reason, so had to Base64-encode an SVG of the Lume logo in order to get the badge to work.

Next steps

I’m not going to ship this project juuuuust yet in case I think of any missing features that I ought to add, but I probably will soon.

0
Original post
@ethmarks

1.0.0

Unless I think of more stuff to add, this is probably my last devlog before shipping.

Lume Theme Registry

A few hours ago, the PR that I created last devlog was merged into the official Lume theme registry, so you can now install my theme with a single command using Lume init, and it now has its own dedicated page on the official Lume website: https://lume.land/theme/tufte/.

README

  • I added a How it Works section that explains how the Lume theme architecture works behind-the-scenes (spoiler: it’s just remote files) and explains what each of the 18 included plugins do.
  • I updated the quickstart to use the Lume init command.
  • I added a Showcase section that lists the sites that use this theme. It seemed kind of weird to exclusively list the Tufte theme site itself, so I also switched one of my documentation sites to use the Tufte theme.
  • I added a shields.io badge to the top of the README that links to the theme’s page on the Lume website. shields.io relies on simple-icons for its logos, but simple-icons doesn’t have the Lume logo for some reason, so had to Base64-encode an SVG of the Lume logo in order to get the badge to work.

Next steps

I’m not going to ship this project juuuuust yet in case I think of any missing features that I ought to add, but I probably will soon.

Replies

Loading replies…

0
1
Open comments for this post

15m 38s logged

Theme Registry PR

I just created a PR to add my theme to the official theme registry for Lume: https://github.com/lumeland/themes/pull/10.

Why

If my theme isn’t in the official registry, the best way for users install it is to clone my entire repo, which is silly, bad for separation of concerns, and just generally worse for UX.

git clone https://github.com/ethmarks/lume_tufte.git
cd lume_tufte
deno task serve

But if I get my theme into the theme registry, users can install it using the official Lume init command, which is much better in every way:

deno run -A https://lume.land/init.ts --theme=tufte

Conclusion

From my interactions with him in the past, the Lume maintainer (Oscar) seems like an extremely nice dude who seems to respond fairly quickly, so I shouldn’t have to wait too long before he either accepts my PR or suggests changes that I can go implement. Meanwhile, I’ll probably continue working on the README.

0
Original post
@ethmarks

Theme Registry PR

I just created a PR to add my theme to the official theme registry for Lume: https://github.com/lumeland/themes/pull/10.

Why

If my theme isn’t in the official registry, the best way for users install it is to clone my entire repo, which is silly, bad for separation of concerns, and just generally worse for UX.

git clone https://github.com/ethmarks/lume_tufte.git
cd lume_tufte
deno task serve

But if I get my theme into the theme registry, users can install it using the official Lume init command, which is much better in every way:

deno run -A https://lume.land/init.ts --theme=tufte

Conclusion

From my interactions with him in the past, the Lume maintainer (Oscar) seems like an extremely nice dude who seems to respond fairly quickly, so I shouldn’t have to wait too long before he either accepts my PR or suggests changes that I can go implement. Meanwhile, I’ll probably continue working on the README.

Replies

Loading replies…

0
14
Open comments for this post

6h 30m 19s logged

README, optimizations, and more

Since my last devlog:

  1. I wrote the README.
  2. I optimized the theme until it earned perfect 100s in every category on Lighthouse on every page.
  3. I added a light mode.
  4. I added support for tables of contents.
  5. I added RSS feed generation.
  6. I rewrote one of the demo blog posts.
  7. I added styles for HTML tables

Link to the diff between last devlog and this one

Optimization

Most of the work I did today is pretty self-explanatory from my little summary, but the optimization work probably merits elaboration.


So while I was writing the README, I had the idea to include the Lighthouse scores. When I checked, they were 100/100/100/92 on mobile and 92/100/100/92 on desktop.

“100s on Lighthouse in some fields” doesn’t sound nearly as nice as “100s on Lighthouse in every field”, so I decided to start optimizing. Lighthouse told me that the SEO problem was a lack of descriptions, and the desktop performance problem was excessive network requests and chained network requests.

SEO problem

I fixed the lack of descriptions by adding descriptions. Simple as that. I wrote descriptions for all of the existing pages, added a description field to the CMS config, and added a mention of it in the usage guide. The network requests and chained requests were a bit trickier to solve.

Performance problems

The cause of the excessive network requests was that, even though the homepage didn’t use the KaTeX or Nueglow styles, they were still being imported. I solved that with a per-page check that runs at build time. Each page only imports the Nueglow stylesheet if it contains code blocks, and it only imports the KaTeX stylesheet if the page contains math blocks. Here’s a link to the relevant section of code. I think that it’s a pretty clever solution because it selectively removes the unnecessary CSS requests without using client-side JS or anything like that.

I used a similar approach to solve the chained requests. What was happening is that in order to download the woff2 files, the browser first had to download the CSS files, so it took twice as long because they couldn’t be downloaded in parallel. Font preloads are the way to solve this. The only problem is that if I preload too many font files, the browser will have download font files even if they aren’t used on the page. But if I preload too few, then the network requests are still chained. My solution was to have it check which font files each page needs and selectively preload only those font files. Here’s the relevant code if you want to see my implementation. Again, this solves the problem of unnecessary requests and chained requests without adding any client-side JS.

I’m glad that Lume and Vento are such powerful tools that I can easily do programmatic optimizations like this.

Next Steps

I think that the next thing I should do is submit my theme to the official Lume theme repo. That way I can simplify the installation process down to one command.

0
Original post
@ethmarks

README, optimizations, and more

Since my last devlog:

  1. I wrote the README.
  2. I optimized the theme until it earned perfect 100s in every category on Lighthouse on every page.
  3. I added a light mode.
  4. I added support for tables of contents.
  5. I added RSS feed generation.
  6. I rewrote one of the demo blog posts.
  7. I added styles for HTML tables

Link to the diff between last devlog and this one

Optimization

Most of the work I did today is pretty self-explanatory from my little summary, but the optimization work probably merits elaboration.


So while I was writing the README, I had the idea to include the Lighthouse scores. When I checked, they were 100/100/100/92 on mobile and 92/100/100/92 on desktop.

“100s on Lighthouse in some fields” doesn’t sound nearly as nice as “100s on Lighthouse in every field”, so I decided to start optimizing. Lighthouse told me that the SEO problem was a lack of descriptions, and the desktop performance problem was excessive network requests and chained network requests.

SEO problem

I fixed the lack of descriptions by adding descriptions. Simple as that. I wrote descriptions for all of the existing pages, added a description field to the CMS config, and added a mention of it in the usage guide. The network requests and chained requests were a bit trickier to solve.

Performance problems

The cause of the excessive network requests was that, even though the homepage didn’t use the KaTeX or Nueglow styles, they were still being imported. I solved that with a per-page check that runs at build time. Each page only imports the Nueglow stylesheet if it contains code blocks, and it only imports the KaTeX stylesheet if the page contains math blocks. Here’s a link to the relevant section of code. I think that it’s a pretty clever solution because it selectively removes the unnecessary CSS requests without using client-side JS or anything like that.

I used a similar approach to solve the chained requests. What was happening is that in order to download the woff2 files, the browser first had to download the CSS files, so it took twice as long because they couldn’t be downloaded in parallel. Font preloads are the way to solve this. The only problem is that if I preload too many font files, the browser will have download font files even if they aren’t used on the page. But if I preload too few, then the network requests are still chained. My solution was to have it check which font files each page needs and selectively preload only those font files. Here’s the relevant code if you want to see my implementation. Again, this solves the problem of unnecessary requests and chained requests without adding any client-side JS.

I’m glad that Lume and Vento are such powerful tools that I can easily do programmatic optimizations like this.

Next Steps

I think that the next thing I should do is submit my theme to the official Lume theme repo. That way I can simplify the installation process down to one command.

Replies

Loading replies…

0
1
Open comments for this post

5h 9m 19s logged

Hi there! This is a project I’ve been working on for the past week. It’s a theme for Lume SSG based on Tufte CSS.

Btw, I did the majority of the work before I started using Hackatime, so I’ve been working on it for a lot longer than 5 hours.

I’m just going to briefly tell you about everything that I did before I started tracking with Hackatime.

  1. I looked through the Lume source code to figure out how the poorly-documented theme APIs work.
  2. I copied the tufte.css file into the project. Then I downloaded the font .tff files, compressed them into .woff2 files, and wrote css font-face rules for them.
  3. I made some Vento layouts.
  4. I created, styled, and wrote the logic for the site header.
  5. I wrote the blog post and blog index layouts and created the BlogList component.
  6. I started installing Lume plugins like KaTeX and Nueglow.
  7. I started writing the “Using this theme” post.
  8. I created the tufte-sections and tufte-notes plugins for markdown-it.
  9. I began heavily tweaking and customizing tufte.css.
  10. I transcribed every word of the TufteCSS homepage from HTML to Markdown.
  11. I started configuring LumeCMS.

Most of what I’ve done since then (what’s reflected in the 5 hours I’ve logged with this devlog) has been a lot of writing, mainly in the Using this theme post. I’ve also been adjusting the LumeCMS config a lot.

I think that the next thing that I’ll do is write the README.

Thanks for reading!

0
Original post
@ethmarks

Hi there! This is a project I’ve been working on for the past week. It’s a theme for Lume SSG based on Tufte CSS.

Btw, I did the majority of the work before I started using Hackatime, so I’ve been working on it for a lot longer than 5 hours.

I’m just going to briefly tell you about everything that I did before I started tracking with Hackatime.

  1. I looked through the Lume source code to figure out how the poorly-documented theme APIs work.
  2. I copied the tufte.css file into the project. Then I downloaded the font .tff files, compressed them into .woff2 files, and wrote css font-face rules for them.
  3. I made some Vento layouts.
  4. I created, styled, and wrote the logic for the site header.
  5. I wrote the blog post and blog index layouts and created the BlogList component.
  6. I started installing Lume plugins like KaTeX and Nueglow.
  7. I started writing the “Using this theme” post.
  8. I created the tufte-sections and tufte-notes plugins for markdown-it.
  9. I began heavily tweaking and customizing tufte.css.
  10. I transcribed every word of the TufteCSS homepage from HTML to Markdown.
  11. I started configuring LumeCMS.

Most of what I’ve done since then (what’s reflected in the 5 hours I’ve logged with this devlog) has been a lot of writing, mainly in the Using this theme post. I’ve also been adjusting the LumeCMS config a lot.

I think that the next thing that I’ll do is write the README.

Thanks for reading!

Replies

Loading replies…

0
1

Followers

Loading…