{
  "path": "/posts/crossing-the-wasm",
  "site": "at://did:plc:pans3xjam4khj7y54dx7gtfg/site.standard.publication/3mdqevmg6w32c",
  "tags": [
    "rust",
    "game dev",
    "roguelike",
    "WASM"
  ],
  "$type": "site.standard.document",
  "title": "Crossing the WASM",
  "description": "Architecture of Flashlight, a browser roguelike with vanilla JS, Rust/WASM, recursive shadowcasting, and BFS pathfinding.",
  "publishedAt": "2025-04-28T02:28:31.000Z",
  "textContent": "{{< video src=\"/flashlight.webm\" type=\"video/webm\" >}}\n\n<a href=\"/flashlight\" target=\"_blank\" rel=\"noopener noreferrer\">Flashlight</a> | <a href=\"/path\" target=\"_blank\" rel=\"noopener noreferrer\">Perf comparison</a>\n\nI recently talked to a group of engineers about integrating WASM into Javascript projects. I've integrated some Rust compiled to WASM into my projects a few times for various reasons ranging from performance to FOMO. I wanted to write down the architecture of a tiny game I wrote recently (with vanilla JS, HTML, and WASM), to demonstrate how to integrate very small parts of Rust using WASM into Javascript/TS projects. And eventually let it consume your entire life.\n\nI've been interested in roguelikes since I first saw Gridbugs' Rain Forest and started reading the blog posts. The rabbit hole led me down recursive shadowcasting and the work Roguelike Celebration conference speakers have been creating. I wanted to make something for the web without an engine or external libraries, just for shits and giggles.\n\nThis post highlights some aspects of the architecture that I think are most crucial to recreating something like Flashlight without excessive handholding.\n\nThe architecture has undergone several small refactors but the following constraints have always guided the changes:\n\n- run in the browser\n- fast\n- LGTM on mobile\n- no libraries\n- avoid exploits\n\nCurrent Architecture\n\nRex\n\nUsually when I write a new web app I just use React as it's straightforward and I can focus on the app logic. This time I didn't want to bloat the bundle size for something this simple.\n\nRex is an incredibly thin library for wrangling observable streams. All of the event handling, data manipulation, and visual side effects are handled by it.\n\nIt can do a handful of things:\n\n1. create a stream\n\n   1.1. from an event\n\n   1.2. from a timer\n\n2. apply filtering over a stream\n\n3. map over a stream\n\n4. merge two streams\n\n5. cause side effects via stream subscriptions\n\nThese are sufficient to build a tiny game like Flashlight.\n\nEntrypoint - Flashlight (crate)\n\nThis is the entrypoint into the gameplay logic all of which is compiled to WASM and runs in the browser. The game uses recursive shadowcasting to calculate the visible tiles.\nInitially I wrote shadowcaster to use on a game I wrote with Bevy (a Rust game engine), so instead of re-implementing shadowcasting in Javascript I build the game around it and used WASM to run it on the browser.\nLater on in the post you'll see that running the core game logic in WASM provides a gameplay benefit that aligns with the core mechanic.\n\nFlashlight takes care of the following:\n\n- process character movement\n- process enemy movement\n- get clipped map state from camera\n- update map state\n- enforce gameplay rules like walkable tiles, consumables, combat etc\n\nShadowcaster\n\nFlashlight calls into Shadowcaster to calculate the tiles visible to the character at any given time. I won't go much into the details of the implementation other than that it was implemented without looking at existing implementations and was pretty incorrect for a long time. Here are some references that I've used while building it.\n\n- Visible Area Detection with Recursive Shadowcast - Gridbugs\n- Roguelike Vision Algorithms - Adam Milazzo\n\nThe compute_visible_tiles function does the heavy lifting of finding the visible tiles and returning their positions, and Flashlight then decides how to best use that to update map state.\n\nPathfinder\n\nThis is just a naive breadth-first search implementation that allows the enemy to find the character. I wanted to keep pathfinding in a separate create as I've been using it on other projects that don't share the core gameplay of Flashlight.\nIt merely takes a vector of glyphs, grid width, a starting glyph and an ending glyph, and returns the shortest set of moves between those glyphs.\nThe rules engine then determines what to do with all this data.\n\nFlashlight (crate) contd.\n\nOnce the crate has all the information it updates the state and converts it to a JS UInt8Array to communicate the updated map state back to the JS for rendering.\nAs the only way to communicate between the JS context and the WASM boundary is via character move commands and a map state in the form of a UInt8Array there's little room for leaking extra information.\nThis ensures that the player can't just inspect element on the hidden glyphs to find the locations of the enemy and the health pack.\nOnly returning a single array of glyphs that represent each cell on the map has it's drawbacks. The state can only represent full moves, and no sub move data can be represented using the map state. Eg. animation direction of the character when it bumps into an obstacle is kept firmly in the JS land. Visual properties of how each cell is rendered have to be mapped from a single UInt8 (glyph).\nOn one hand it's very limiting but it also means that the frontend can be completely independent. I can write it using canvas, a native gui library, as a terminal UI etc.\n\nThis brings me back to Rex and how the glyphs are rendered to the screen (it's just HTML divs).\n\nUI and Ephemeral State Management\n\nRendering the UI elements and the game is handled via JS functions all of which are orchestrated using Rex.\n\nAny state that doesn't impact the core gameplay stays firmly in the JS land.\nThe state of the UI is stored in a class which keeps track of things like character animation states, in-game dialog, dialog boxes, button states, etc.\nAll of this isn't part of the core gameplay logic (inside flashlight crate) as it can either be derived from the map state or it's too tightly coupled to the visual representation.\nParticle effect states like position and intensity of rain, damage numbers over the player character and the monster, etc are part of the closure state of their update functions. This data doesn't have gameplay consequences and that's why colocating it inside the core engine has no benefits.\n\nI want to focus on a small section of the game that triggers a state update and renders the map to demonstrate the end to end flow.\nAt the start of the game the user is expected to turn the flashlight on by tapping the button three consecutive times.\n\nLocal state update\n\nThis is an ever so slightly simplified version of the code that connects events to state updates. The code comments point to the capabilities of Rex mentioned earlier.\n\nRendering\n\nThis is a version of the render function, simplified for illustration.\n\nOnce the filter is satisfied the state for flashlight (in-game item) is toggled which results in the compute_visibility function getting invoked in the next render loop.\nFlashlight uses the shadowcaster crate to get a hashmap of tiles that are visible.",
  "canonicalUrl": "https://afloat.boats/posts/crossing-the-wasm"
}