diff --git a/web/contact-service/src/ui.ts b/web/contact-service/src/ui.ts index e034291..3c5dc9a 100644 --- a/web/contact-service/src/ui.ts +++ b/web/contact-service/src/ui.ts @@ -8,7 +8,6 @@ import { } from "./model.ts"; import type { ApiResponses, ShareRequest } from "./types.ts"; - type Elements = { status: HTMLDivElement; result: HTMLDivElement; diff --git a/web/contact-service/src/worker.ts b/web/contact-service/src/worker.ts index b3e1e8d..e023e09 100644 --- a/web/contact-service/src/worker.ts +++ b/web/contact-service/src/worker.ts @@ -21,7 +21,7 @@ const json = (value: unknown, status = 200) => "Content-Type": "application/json", }); - function qr(text: string) { +function qr(text: string) { const { modules } = QRCode.create(text, { errorCorrectionLevel: "M" }); let path = ""; for (let y = 0; y < modules.size; y++) diff --git a/web/media/blog/mind-sweeping/avoidant-polyhedra-test.mp4 b/web/media/blog/mind-sweeping/avoidant-spheroid-test.mp4 similarity index 100% rename from web/media/blog/mind-sweeping/avoidant-polyhedra-test.mp4 rename to web/media/blog/mind-sweeping/avoidant-spheroid-test.mp4 diff --git a/web/media/blog/mind-sweeping/avoidant-spheroid-test.webm b/web/media/blog/mind-sweeping/avoidant-spheroid-test.webm new file mode 100644 index 0000000..de05c15 Binary files /dev/null and b/web/media/blog/mind-sweeping/avoidant-spheroid-test.webm differ diff --git a/web/media/blog/mind-sweeping/avoidant-spheroid-test_av1.mp4 b/web/media/blog/mind-sweeping/avoidant-spheroid-test_av1.mp4 new file mode 100644 index 0000000..4916d9f Binary files /dev/null and b/web/media/blog/mind-sweeping/avoidant-spheroid-test_av1.mp4 differ diff --git a/web/media/blog/mind-sweeping/avoidant-spheroid-test_h264.mp4 b/web/media/blog/mind-sweeping/avoidant-spheroid-test_h264.mp4 new file mode 100644 index 0000000..bf8883a Binary files /dev/null and b/web/media/blog/mind-sweeping/avoidant-spheroid-test_h264.mp4 differ diff --git a/web/media/blog/mind-sweeping/avoidant-spheroid-test_h265.mp4 b/web/media/blog/mind-sweeping/avoidant-spheroid-test_h265.mp4 new file mode 100644 index 0000000..3fa693a Binary files /dev/null and b/web/media/blog/mind-sweeping/avoidant-spheroid-test_h265.mp4 differ diff --git a/web/src/content/blog/mind-sweeping.mdx b/web/src/content/blog/mind-sweeping.mdx index 818a814..2771d28 100644 --- a/web/src/content/blog/mind-sweeping.mdx +++ b/web/src/content/blog/mind-sweeping.mdx @@ -1,7 +1,7 @@ --- description: "My journey to make a Minesweeper-inspired game on varied maps with multiplayer support." favicon: "/blog/mind-sweeping/favicon.svg" -heroAlt: "Lower Manhattan, repeating radially in different styles." +heroAlt: "A Minesweeper-inspired board with numbered cells, buoys, and waves." heroComponent: "mine-game-hero.astro" heroImage: "../../assets/blog/mind-sweeping/hero.svg" pubDate: 0 @@ -16,7 +16,7 @@ import Video from "../../components/Video.astro"; ## The Initial Sweep -As a kid, I didn't pay [Microsoft Minesweeper](https://en.wikipedia.org/wiki/Microsoft_Minesweeper) much mind on my Windows 95 computer, so I hadn't figured out the rules through my random clicking. As I browsed job listings in 2013 soon after being laid off from my first fulltime job, I came across a [programming challenge](https://web.archive.org/web/20130123165552/http://www.thumbtack.com/challenges#minesweeper) from [Thumbtack]() that ignited my interest in Minesweeper. +As a kid, I didn't pay [Microsoft Minesweeper](https://en.wikipedia.org/wiki/Microsoft_Minesweeper) much mind on my Windows 95 computer, so I hadn't figured out the rules through my random clicking. As I browsed job listings in 2013 soon after being laid off from my first full-time job, I came across a [programming challenge](https://web.archive.org/web/20130123165552/http://www.thumbtack.com/challenges#minesweeper) from [Thumbtack]() that ignited my interest in Minesweeper.
> ### Rules of Minesweeper @@ -34,10 +34,11 @@ Eager to try [Backbone.js](https://backbonejs.org/) and having been a fan of the loading="lazy" frameborder="0" style="height: 630px;border:none;overflow:hidden;" + title="My original Minesweeper clone" > -
[My subpar submission](https://aakside.github.io/minesweeper/) for the Thumbtack Minesweeper challenge, which is might be impossible to play on a touchscreen. I didn't realize then that I preferred a [dimetric projection](https://en.wikipedia.org/wiki/Axonometric_projection#Three_types) to an isometric one.
+
Baby's first Minesweeper clone: [my subpar submission](https://aakside.github.io/minesweeper/) for the Thumbtack Minesweeper challenge. It might be impossible to play on a touchscreen. I didn't realize then that I preferred a [dimetric projection](https://en.wikipedia.org/wiki/Axonometric_projection#Three_types) to an isometric one.
## Swept Away @@ -51,16 +52,17 @@ I became addicted to playing [Mines](https://gitlab.gnome.org/GNOME/gnome-mines)
My record for finding 99 mines: 1 minute and 15 seconds.
-Eventually I felt a pretty bad pain in my right hand, and was diagnosed with [De Quervain tendinopathy](https://en.wikipedia.org/wiki/De_Quervain_tendinopathy). I started wearing a brace and opting usually to play [Klondike]() instead of Mines when I needed a quick distraction, but I never stopped thinking about it because... +Eventually I felt a pretty bad pain in my right hand and was diagnosed with [De Quervain tendinopathy](https://en.wikipedia.org/wiki/De_Quervain_tendinopathy). I started wearing a brace and usually opting to play [Klondike]() instead of Mines when I needed a quick distraction, but I never stopped thinking about it because... ## Filling the Void I'm back at it. I'm trying to control myself now, and I'm trying to engage with the concept of the game more than the game itself. -I decided to remake the game again, but with a few twists: +I decided to remake the game, but with a few twists: - No square grid - No mines, just "voids" + - And thus the name "Avoidant" - No immediate losing (getting rid of the momentary panic of pressing a wrong tile) - No secondary clicking, to help players on touchscreens - Multiplayer @@ -81,7 +83,7 @@ Also, I wanted it to work in a web browser, so players won't need to download an While I chose to keep game logic in Rust to make state synchronization through Iroh easier, I decided to utilize Svelte for the UI due to my familiarity and reluctance to adopt yet another new UI framework. If I ever make a faster-paced game, I might consider something like [egui](https://www.egui.rs/). -To maintain shared reactive state, I initially passed Svelte state from JS to Rust, but it was messy. I knew declaring reactive state in Rust was preferable so I considered implementing the [store contract](https://svelte.dev/docs/svelte/stores) in Rust, but thankfully I found that [jquesada2016](https://crates.io/users/jquesada2016) already implemented [Readable](https://docs.rs/svelte-store) to resolve my exact issue. +To maintain shared reactive state, I initially passed Svelte state from JS to Rust, but it was messy. I knew declaring reactive state in Rust was preferable, so I considered implementing the [store contract](https://svelte.dev/docs/svelte/stores) in Rust, but thankfully I found that [jquesada2016](https://crates.io/users/jquesada2016) had already implemented [Readable](https://docs.rs/svelte-store) to resolve my exact issue.
-
-### Map Generation - -[Inspired by Red Blob Games](https://www.redblobgames.com/x/1830-jittered-grid/), I used Poisson disk sampling (with [bluenoise](https://github.com/arlyon/bluenoise-rs)) to generate evenly-spaced random points to feed into [voronator](https://github.com/fesoliveira014/voronator-rs) for voronoi diagram generation. - ### Networking -While I could have used [matchbox](https://github.com/johanhelsing/matchbox/) (I might one day), I drool at the idea of decentralized peer resolution. I had used [libp2p](https://libp2p.io/) in the context of distributed LLM inference before, and while that was fun to play with, I hesitated to it for this game because I had experienced a lot of latency with peer discovery and a high connection failure rate. +While I could have used [matchbox](https://github.com/johanhelsing/matchbox/) (I might one day), I drool at the idea of decentralized peer resolution. I had used [libp2p](https://libp2p.io/) in the context of distributed LLM inference before, and while that was fun to play with, I hesitated to use it for this game because I had experienced a lot of latency with peer discovery and a high connection failure rate.
> Critically, we empirically refute the long-held ’tribal knowledge’ of UDP’s superiority for NAT traversal, demonstrating that DCUtR’s high-precision, RTT-based synchronization yields statistically indistinguishable success rates for both TCP and QUIC (∼70%). -
My fears of libp2p having low hole punching rates reaffirmed by [Challenging Tribal Knowledge – Large Scale Measurement Campaign on Decentralized NAT Traversal](https://arxiv.org/html/2510.27500v1).
+
Poor hole punching rates reported in [a large-scale study of decentralized NAT traversal](https://arxiv.org/abs/2510.27500v1) dissuaded me from using libp2p.
I decided to try Iroh, hoping that one day soon it will [support WebRTC](https://docs.iroh.computer/languages/wasm-browser#no-direct-connections) so players on web browsers won't devour bandwidth on my self-hosted [Iroh Relay](https://github.com/n0-computer/iroh/tree/main/iroh-relay) server. @@ -148,7 +145,9 @@ I decided to try Iroh, hoping that one day soon it will [support WebRTC](https:/ ### Texels, DataTextures, and Shaders -TODO +As I played with larger maps, I realized that rendering each cell as a separate Threlte component was becoming a bottleneck that made the game lag significantly. I didn't know how I should get around storing cell state without having a Threlte component for each cell until I discovered using a [`DataTexture`](https://threejs.org/docs/#DataTexture) to store cell metadata. + +A `DataTexture`, it turned out, does not need to contain an actual image, and can simply store arbitrary data for the shader to read. I used one texel (the smallest unit of a texture) per cell with 4 1-byte channels to store cell exploration, void, fall distance and reveal state, instead of the typical RGBA values. Exploring a cell updates its metadata without rebuilding the terrain mesh.
-### Bangla +### Map Generation -I added a tutorial mode and Bangla translations to help my parents and extended family learn to play the game. +[Inspired by Red Blob Games](https://www.redblobgames.com/x/1830-jittered-grid/), I used Poisson disk sampling (with [bluenoise](https://github.com/arlyon/bluenoise-rs)) to generate evenly-spaced random points to feed into [voronator](https://github.com/fesoliveira014/voronator-rs) for Voronoi diagram generation. -## Play the Game +The result was fun, but playing on a flattish map got boring. I had been wanting to play Minesweeper on 3D surfaces for a while, so I started experimenting...
-
+### Bangla + +I added a tutorial mode and Bangla translations to help my parents and extended family learn to play the game. As I should have expected, people don't like reading instructions, and making a tutorial that can suggest a good next logical move is hard without implementing a solver first. + +## Play the Game + While making this game, I dreamt of making many more. I resurrected a long-held domain for my amateur game development "studio." Click the link below to play the game: -[Avoidant](https://games.kuncat.com/avoidant/). +[Play Avoidant](https://games.kuncat.com/avoidant/), or browse the [source code](https://github.com/kuncat/avoidant). + +Start with a small flat map if you want to get used to the rules, or try a polyhedron in multiplayer mode and watch your friends sweep around edges.