I don’t usually do year-in-review posts. They tend to read like LinkedIn updates: vague claims of growth, carefully curated wins, no mention of the three weeks you spent debugging a shader that turned out to have a misplaced semicolon. But 2025 was weird enough that it deserves documentation, if only so I can look back in a year and confirm that yes, I really did write my own SPA router from scratch. On purpose.

Here’s what actually happened.


The Site

The biggest project of the year was ellyseum.me itself. I nuked my old portfolio site sometime around March. It was a React app that had accumulated enough cruft to qualify as a brownfield site, and the thought of updating it one more time made me want to close my laptop and take up woodworking.

So I started from zero. Vanilla TypeScript. Vite for bundling. No framework, no component library, no CSS-in-JS solution with a clever name. Just HTML, CSS, TypeScript, and the browser APIs I’d been ignoring for years while React abstracted them away. It felt like moving to the countryside after years in a loud apartment.

The centerpiece is the WebGL background. A cosmic particle field rendered with custom GLSL shaders: stars drifting, nebula-like color gradients, depth-of-field blur, the works. I wanted the site to feel like looking out a viewport into deep space. I spent more time tweaking the fragment shader’s color mixing than I did writing the actual page content, and I’m not even a little ashamed of that.

What worked: the shader pipeline is clean, the performance tiers handle everything from a desktop 4090 down to a phone GPU gracefully, and the visual result is exactly what I wanted. I wrote a tier system that detects GPU capability and scales particle counts and post-processing effects accordingly. Full mode runs 30,000 particles with bloom. Reduced mode drops to 10,000 with no post-processing. Static mode gives you a CSS gradient and gets out of the way.

What didn’t work: Safari. Safari’s WebGL implementation is a special kind of torture. Half the extensions I relied on either weren’t supported or behaved differently than every other browser. EXT_shader_texture_lod? Not in Safari. Certain precision qualifiers that work everywhere else? Silent failures in Safari. I spent an entire weekend building a Safari-specific shader fallback path, which is a sentence I never want to type again. 😤

Mobile was its own adventure. Turns out “mobile GPU” covers a range from “surprisingly capable” to “will thermal throttle if you look at it wrong.” I ended up implementing a runtime FPS monitor that dynamically downgrades the render tier if frame times start spiking. It’s inelegant, but it works, and nobody’s phone has caught fire yet. That I know of.


EllyMUD

I started building a MUD. In 2025. On purpose.

EllyMUD is a TypeScript MUD server with Telnet and WebSocket support. If you don’t know what a MUD is, imagine a text-based MMO from the early ’90s, because that’s exactly what it is. I grew up on these things, and the idea of building one with modern tooling was too appealing to resist.

The Telnet implementation was a trip down memory lane. RFC 854 hasn’t changed since 1983, and there’s something deeply satisfying about implementing a protocol that’s older than me. Telnet negotiation is fiddly, the IAC byte escaping is annoying, and I loved every minute of it.

WebSocket support was the concession to modernity. Not everyone has a Telnet client lying around (most people, in fact, do not), so the web client connects via WebSocket and renders the MUD output in a terminal-style interface in the browser.

The part I’m most proud of is the MCP API. MCP is Anthropic’s Model Context Protocol, and I built an MCP server into EllyMUD so AI agents can connect and interact with the game world. An AI can explore rooms, examine objects, talk to NPCs, and generally do everything a human player can do, but through a structured API instead of parsing text. It’s an experiment in giving LLMs a sandbox to play in, and watching Claude navigate a dungeon is both entertaining and occasionally unsettling. It keeps trying to befriend the goblins.

EllyMUD is further along than I expected. The architecture is solid: 54 commands, a real combat system, NPC AI, crafting, a visual world builder, and a test suite that would make my day-job self jealous (157 tests and counting). The MCP API is fully operational, meaning AI agents can connect and play alongside humans right now. It’s not done, because a MUD is never done, but it’s a real project with real systems, not a weekend hack that got out of hand. Okay, it started as a weekend hack that got out of hand. But it grew up.


Going Frameworkless

I dropped React this year, and I wrote a whole post about it already, but the short version: I got tired of the ecosystem. Not React itself; React is fine. The problem is that “React” in practice means React plus a state management library plus a routing library plus a form library plus a CSS solution plus a meta-framework plus whatever new thing dropped on Twitter this week. The dependency graph for a simple project had become absurd.

So I went vanilla. TypeScript, Vite, raw DOM APIs. I built my own SPA navigation from scratch. History.pushState, route matching, dynamic imports for code splitting. It’s maybe 200 lines of code. It does exactly what I need and nothing else.

The first week was uncomfortable. Muscle memory kept reaching for useState and useEffect. I kept wanting to write JSX. But once I pushed through that, something clicked. I was writing less code, understanding more of what it did, and shipping faster. The entire bundle for ellyseum.me is smaller than the React runtime alone. Let that sink in!

I don’t think everyone should drop frameworks. Large teams need shared conventions. Complex apps need architecture that scales with headcount. But for personal projects, for creative experiments, for things that should be fun? The browser is a perfectly good application platform, and it’s been one for a while now. We just forgot because we buried it under twelve layers of abstraction.


AI Tools

I started using Claude Code around the middle of the year, and I’ll be honest: I was skeptical. I’d tried Copilot when it launched, found it decent for boilerplate and annoying for everything else, and mostly wrote it off as a parlor trick with good marketing.

Claude Code changed my mind. Not because it writes perfect code; it doesn’t. But because it’s genuinely useful as a collaborator for the kind of work I do. I can describe a shader effect I want, and it’ll give me GLSL that’s 80% of the way there. I can paste in a TypeScript type error and get an explanation that’s actually helpful instead of a Stack Overflow link from 2019. I can ask it to refactor a module and get something back that’s at least structurally reasonable.

The key insight for me was treating it like a junior developer who’s read the entire internet. It’s fast, it knows a lot, it sometimes confidently produces nonsense, and it needs review. But the feedback loop is tight enough that the net effect is positive. I ship more, and the code isn’t worse. For a solo developer, that matters a lot.

I also built a RAG chatbot for the site using WebGPU for in-browser inference. That’s a whole separate post. But the process of building it taught me more about embeddings, vector search, and prompt engineering than any tutorial could have. Sometimes the best way to learn something is to build it wrong three times and then build it right once. I’m on attempt four. Don’t ask.


What Didn’t Work

Let’s talk about the failures, because they’re funnier.

The Great Dependency Incident of 2025. I had a build pipeline that worked perfectly for months. One day it didn’t. An npm package I’d never heard of, four levels deep in the dependency tree, released a minor version that broke everything. Not my code. Not my direct dependency. A dependency of a dependency of a dependency decided to change its export format. I spent a full day tracking this down, fixed it by pinning a version, and then spent the next hour writing a strongly-worded comment in a GitHub issue that I deleted before posting because it wasn’t the maintainer’s fault and I needed to calm down.

Lesson learned: package-lock.json exists for a reason, and “it works on my machine” is not a deployment strategy.

The Sleep Thing. There were several stretches this year where I was coding until 4 AM, sleeping until noon, and wondering why I felt terrible. I’m a night owl by nature, but there’s a difference between “I do my best work at night” and “I have not seen sunlight in six days.” 💀 I got better about this toward the end of the year. Not good. Better. Progress is progress.

The WebGPU Shader That Ate Three Weeks. I decided I was going to rewrite the site’s entire rendering pipeline in WebGPU because it’s the future and I wanted to be ahead of the curve. Technically, it worked! The shaders compiled, the compute passes ran, the output was correct. But WebGPU support is still patchy enough that I would have been cutting off half my visitors. Probably dozens of them!! I shelved it. Those three weeks live on a branch that I’ll revisit when browser support catches up, which based on current trajectories should be sometime around 2027.


2026 Plans

I’m keeping this short because plans are just predictions that haven’t been embarrassed yet.

Ship EllyMUD. Not “finish” it, because a MUD is never finished, but get it to a state where other people can actually play it. Real content, real systems, real world. The architecture is there. Now I need to fill it with things worth exploring.

More AI experiments. The RAG chatbot was a starting point. I want to build more tools that use local inference, more things that run on-device without phoning home to an API. WebGPU is going to make this increasingly viable, and I want to be building on it as it matures.

Make a game. A real one. Not a MUD, not a tech demo, not a “game engine” that never gets a game built on top of it. I’ve been sketching ideas for years. 2026 is the year I either ship one or admit I’m a tools programmer who just likes building engines. Both outcomes are acceptable as long as I stop lying to myself about it. 😅

Write more. This blog has been inconsistent. I have a dozen drafts that never got finished because I convinced myself they weren’t good enough, or technical enough, or interesting enough. In 2026 I’m going to post more and worry less. The internet doesn’t need another perfectionist with an empty blog!


2025 was the year I stopped building with other people’s tools and started building my own. Some of them worked. Some of them didn’t. All of them taught me something, even if the lesson was just “don’t do that again.”

Not a bad year. Let’s see what the next one breaks. 🚀