Projects
Problem
Planning a trip involves three distinct pain points that none of the existing tools solve together.
First, discovery: finding relevant content means spending hours across Reddit threads, travel blogs, YouTube videos, and review sites — most of it noise, none of it organized. The good stuff gets buried or lost in browser tabs.
Second, organization: once you have information, there's no good place to put it that ties together the what, the where, and the when. Spreadsheets are tedious. Notes apps lose structure. Travel apps are opinionated about itinerary format and don't accommodate the way you actually think about a trip.
Third, adaptation: even a well-researched plan falls apart on the ground. You hear about a place from a local, stumble onto something unexpected, or realize the original plan doesn't work. Re-planning mid-trip with scattered notes is friction at exactly the moment you don't want it.
I wanted a single AI-powered assistant that addressed all three: help me research, help me organize, and help me re-plan without losing context.
Design Decisions
Claude API as the core intelligence. Claude handles the heavy lifting — synthesizing travel research into recommendations, structuring itinerary drafts from unstructured notes, and responding to re-planning requests mid-trip with full context of what's already been decided. The conversational interface means the planning process stays natural: you describe what you're looking for and Claude shapes it, rather than forcing input into rigid form fields.
Neon Postgres for persistent conversation and trip state. Trip planning isn't a single session. You come back to it over days or weeks, and the assistant needs to remember what was already discussed — destinations locked in, preferences stated, things ruled out. Neon's serverless Postgres stores conversation history and trip data persistently, so every session picks up where the last one left off without re-establishing context.
React + Tailwind for the frontend. The UI is intentionally minimal — a chat interface plus a structured trip view. Tailwind kept styling fast without a design system overhead. React managed the real-time feel of the assistant interaction and kept the conversation state in sync with the persisted trip data.
Trade-offs
This is a personal tool first. There's no multi-user auth, no sharing features, no mobile app. Building those would have shifted the project from "solve my own problem" to "build a product" — a meaningful scope jump that wasn't the goal. The constraint of building only for myself kept decisions fast and the surface area small.
Using the Claude API also means ongoing cost per session. For a personal tool with low usage volume, this is negligible. At product scale it would require thinking about caching, prompt compression, and cost controls — trade-offs that aren't relevant here but would be the first things to address before any broader release.
Challenges + Debugging
The hardest design problem was context management. A travel planning conversation can get long — multiple destinations, many preferences, evolving constraints. Naively passing the full conversation history to Claude on every message works until the context window becomes expensive or hits limits. The solution was to store full history in Postgres but summarize older segments before passing them back to Claude, keeping the active context window focused on recent decisions while preserving the ability to reference earlier ones on demand.
A trickier problem was prompt design for re-planning. Asking Claude to revise an existing itinerary while respecting already-committed plans (flights booked, accommodation reserved) required careful prompting. Early versions would confidently suggest changes that conflicted with fixed constraints. Building a structured representation of "locked" vs "flexible" items in the trip state, and injecting that into the system prompt explicitly, made Claude's suggestions reliable and constraint-aware.
Impact
- Eliminated the doom-scrolling research phase — a single conversation can surface, evaluate, and organize destination research faster than manual browsing.
- Trip context persists across sessions, so returning to planning after a week away doesn't mean starting over.
- Mid-trip re-planning works: describe what changed, get an updated itinerary that respects what's already fixed.
- Built end-to-end solo across a stack I hadn't fully used before — TypeScript, Neon Postgres, and the Claude API all in one project.
Problem
I was born and raised as a Chinese American. My parents only speak Cantonese — no English. Growing up, Cantonese was just the language of the house. But as I got older, the gap widened. My Cantonese stayed functional while theirs stayed fully native. I could talk about surface things — food, what I did today — but not deeply. The idioms, the metaphors, the colloquial expressions that carry real meaning in conversation: those I didn't have.
I wanted to close that gap. Not just to communicate better, but to actually connect. The kind of conversations that require you to sound like you belong in the language, not like a heritage speaker filling in vocabulary holes.
The problem with existing language tools is that they teach textbook language. Duolingo gives you correct Cantonese. It doesn't give you the way your parents' generation actually talks — the idioms a local uses, the metaphors that don't translate literally, the phrasing that signals you're comfortable in the language rather than performing it.
Design Decisions
AI as conversation partner and grader. The core loop is practice through dialogue. Claude plays the role of a native Cantonese speaker in a realistic conversational scenario. After each exchange, it grades the response — not just for grammatical correctness but for naturalness: would a local say it this way, and if not, how would they actually say it? The feedback is specific and instructive, not just a score.
Focus on colloquial speech and idioms. The content is deliberately informal. Scenarios are modeled on the kinds of conversations that actually happen in daily Cantonese life — not "please direct me to the train station" but the small talk, the family conversation, the way you express frustration or affection or humor in the language. Claude's breadth of knowledge about regional Cantonese usage made it the right fit for generating and evaluating this kind of content.
Deno + Deno Fresh. This project was also an opportunity to learn outside of my established stack. Deno Fresh uses islands architecture — minimal JavaScript shipped to the client, with interactive components hydrated selectively. For a tool that's mostly text input and response rendering, that model fit well. Preact kept the component layer lightweight.
Trade-offs
The grading is only as good as Claude's knowledge of colloquial Cantonese, which has limits — especially for highly regional expressions or very contemporary slang. The tool is more reliable for the kind of Cantonese my parents' generation speaks than for whatever is current on Hong Kong social media. That's actually fine for my use case, but it's a meaningful constraint if the tool were to serve a broader audience.
There's also no speech component. Real Cantonese fluency is deeply tonal and the written romanization (Jyutping) doesn't fully capture it. A text-based practice tool improves vocabulary and phrasing but can't train the ear or the mouth. That's a significant gap for a complete language learning product — and a real engineering challenge (speech recognition for tonal languages is harder) that I scoped out deliberately.
Challenges + Debugging
The hardest prompt engineering problem was getting Claude to grade responses in a way that was genuinely instructive rather than either overly forgiving or pedantically corrective. Early prompts produced feedback that was either "great job, here's a minor tweak" (too soft to be useful) or a rewrite of the entire response with no explanation of why (too opaque to learn from). The right prompt structure explicitly asked Claude to identify the specific phrase or construction that felt unnatural, explain why a native speaker would phrase it differently, and provide one concrete alternative — keeping the feedback actionable and learnable.
The other challenge was personal: building a tool for a deeply personal reason is motivating but also harder to stay objective about. There were features I wanted to add because they felt meaningful — voice input, character practice, extended scenario packs — that would have made the project sprawl without making the core loop better. Shipping the practice-and-grade loop first, and leaving the rest for later, required the same discipline as any other scope decision.
Impact
- A working practice tool I actually use for Cantonese conversation and idiom learning.
- The grading loop surfaces patterns in my own speech I hadn't noticed — recurring phrasing that sounds foreign, specific constructions to replace.
- Learned Deno and Fresh end-to-end on a real project, expanding my web stack beyond React/Node.
- Built something that matters personally: closing the language gap with my parents.
Problem
After spending my professional time in C# and Unity, I wanted to understand the web development world from the ground up. I had been working through The Odin Project curriculum and needed a real project to apply what I was learning — something small enough to finish but complex enough to actually test the skills.
One of my first Python projects was a Pokémon text adventure, so the next stage was adding visuals and turning what I had into a visual-novel-esque game. The project has branching narrative logic, stateful UI (player choices, battle state, a Pokémon party), and a clear enough domain that I wouldn't spend time figuring out what to build instead of how to build it.
Design Decisions
React for state-driven UI. This app is fundamentally a state machine — the current scene, the player's choices, the Pokémon on their team, battle outcomes. React's component model and state management mapped cleanly onto that structure. Each scene is a rendered state, and user decisions trigger state transitions. This made React a natural fit even as a beginner; the problem shape and the tool shape matched.
Narrative-first structure. The game opens at Professor Oak's Lab, where the player picks a starter Pokémon — Charmander, Squirtle, or Bulbasaur, each with distinct stats and traits. From there, the player moves through tall grass encounters, wild Pokémon battles, and branching decisions. The structure prioritizes the player's sense of agency: every fork in the story changes what happens next.
No backend. The entire game runs in the browser. Game state lives in React component state. There's no server, no database, no auth. For a first web project, eliminating the backend entirely was the right call — it let me focus entirely on learning JavaScript, React, and the DOM without splitting attention across a stack I hadn't touched yet.
Trade-offs
Keeping the scope tight meant some Pokémon mechanics are simplified. Battle logic doesn't implement the full damage formula, type matchups are present but not exhaustive, and there's no persistent save state between sessions. A returning player starts fresh every time. These were intentional constraints — not design failures — to keep the project shippable as a learning exercise rather than a full game.
The choice of vanilla React (no routing library, no global state manager) also meant some prop-drilling as the component tree grew. At the scale of this project it was manageable, but it gave me direct experience with the exact pain that libraries like React Router and Zustand exist to solve.
Challenges + Debugging
This was my first project in JavaScript and React, which meant every challenge was also a first encounter with a class of problem I'd never seen before.
The steepest learning curve was thinking in components. In Unity, a game object owns its own behavior — scripts attach to objects and run independently. In React, the component tree is declarative and data flows in one direction. Unlearning the Unity mental model and replacing it with React's took the first half of the project.
Event handling tripped me up early. Understanding how synthetic events work in React, why you don't call event handlers the same way you do in vanilla JS, and how to correctly wire up user interactions to state updates required working through a lot of small bugs before it clicked.
The Pokémon battle logic also surfaced a category of bug I hadn't anticipated: stale state. Because React state updates are asynchronous, reading state immediately after setting it gives you the old value. Early battle sequences were producing wrong results — a hit that should have reduced HP wasn't reflected in the next action's check — because I was reading from the state I thought I'd just updated. Learning to use the functional update form of setState to derive next state from the previous value fixed the class of bug entirely.
Impact
- Shipped a complete, playable browser game as a first web project.
- Moved from zero JavaScript/React knowledge to a functional React application with stateful UI, game logic, and branching narrative.
- Established the foundation for everything that came after in web development — the hard lessons here (component thinking, async state, event handling) paid dividends on every subsequent project.
Problem
Jampack is a subsidiary of Games for Love, a non-profit that supports children fighting for their lives in hospitals by delivering gaming content and community — giving kids a way to cope, connect, and find joy during some of the hardest experiences of their lives. The platform delivers that content digitally, and like any community platform, it generates questions: from users, volunteers, donors, and partners.
The challenge was answering those questions at scale without a full support team — and without the budget to use AI carelessly. Games for Love is a non-profit. Every dollar spent on API calls is a dollar not spent on getting gaming equipment into a hospital. The solution had to be smart about cost from the start, not as an optimization added later.
Design Decisions
Question mapping as the first line of defense. Before a message ever reaches the AI, it passes through a pattern-matching layer that maps common questions to pre-written answers. FAQs, event information, volunteer onboarding questions, platform how-tos — a significant portion of inbound questions are variations of things the organization answers repeatedly. Catching those early and returning a direct answer costs nothing. The AI is reserved for questions the mapping layer can't confidently resolve.
This two-tier architecture — deterministic answers first, AI fallback second — was the core cost-control mechanism. The goal was to minimize AI invocations without degrading the experience for users asking questions that genuinely needed generative responses.
PHP + WordPress + Bricks UI. Jampack's existing stack was WordPress-based, so the chatbot had to integrate without a platform migration. The logic runs server-side in PHP, embedded into the WordPress environment. Bricks UI handled the frontend component integration. Working within the existing stack rather than introducing new infrastructure kept deployment simple and maintenance accessible to a team without dedicated engineering resources.
Trade-offs
A mapping-first approach requires maintaining the mapping layer. As the organization evolves — new programs, new events, policy changes — the pre-written answers go stale if no one updates them. For an engineering team this is a minor overhead; for a small non-profit team where the people maintaining the chatbot aren't engineers, it's real friction. The design choice favored cost predictability over maintenance simplicity, which was the right call for the budget constraint but would need tooling to make the mapping layer editable by non-technical staff at scale.
The mapping layer also can't handle ambiguity well. A question that half-matches two different patterns either routes incorrectly or falls through to AI. Getting the matching logic right required iterating on the pattern definitions after seeing real user questions, not just hypothetical ones.
Challenges + Debugging
This was my first real experience with context management in an AI integration. The chatbot needed to maintain conversational coherence — a follow-up question should be understood in the context of what was asked before, not treated as a new conversation from scratch. Managing that context window in a stateless PHP environment meant explicitly storing and passing conversation history with each request, rather than relying on any built-in session handling.
The early implementation had a subtle bug: context was being accumulated without any pruning. Long conversations would eventually pass so much history in the request that responses slowed down, costs crept up, and in edge cases the context window limit was hit. Adding a rolling window — keeping only the most recent N turns of history — fixed the performance and cost issue while preserving enough context for follow-up questions to make sense.
Working in PHP was also a new environment. My background was in C# and JavaScript; PHP's approach to web server integration, request handling, and WordPress's plugin/hook architecture all had their own learning curve. Getting the chatbot logic to work reliably within WordPress's lifecycle without conflicting with the existing site behavior required more careful integration work than building something greenfield would have.
Impact
- Delivered an AI chatbot for a non-profit children's hospital gaming platform with cost-conscious architecture built in from day one.
- The question-mapping layer handles the high-frequency, low-complexity queries that would otherwise consume the majority of API budget for minimal added value.
- First hands-on experience with context management in an AI product — a lesson that directly informed how I approached the problem on every AI project that followed.
- Supported an organization whose mission is giving kids in hospitals something to look forward to.
Problem
Class 2 gaming machines (EGMs) require a regulatory-compliant middleware layer between the physical machine and the Central Determination Server (CDS) — handling things like CD-approved random number generation, Bingo ball and card generation, progressive jackpot broadcasting, and game configuration. The existing stack had no clean abstraction for this. Customer trials were coming up, and the team needed a proper SaaS platform — now called Hyperion — that could sit on each EGM as a standalone service and speak the right protocols.
The scope was broad: RESTful API for the EGM client, a WPF Bingo UI overlay meeting Class 2 regulations, a configuration store for dynamic game settings, and real-time progressive broadcasting to the CDS. None of this existed yet.
The added constraint: I was handed this project as a game developer. My background was C# and Unity. I had never built a web API, designed a system architecture, or touched ASP.NET. I had to build the product and develop the engineering skills simultaneously.
Design Decisions
Monolithic over microservices. The RFC for Hyperion explored a microservices architecture, but we rejected it. We were a small team, didn't have independent features blocking each other, and didn't need the operational overhead. A monolith let us move fast without the coordination cost. The design keeps the door open — if a customer engagement grows the team and the feature surface, the boundaries are clean enough to split later.
ASP.NET Core for the API layer. The existing Platform Services and CDS used the MK2SVC framework, so building on top of ASP.NET Core with the same foundation kept the integration surface familiar and reduced reinvention. About 30 endpoints cover full EGM game state — start, pause, reset, configuration reads and writes, progressive reporting.
Protobuf for inter-process communication. The Bingo UI runs as a separate WPF process and communicates with the Hyperion backend over a dedicated TCP port using Protocol Buffers. This mirrors the messaging pattern already used in Platform Services and CDS, keeps the message format compact, and keeps the UI process decoupled from direct business logic.
Per-EGM deployment as a standalone Windows executable. Rather than centralizing Hyperion on a single server, each EGM runs its own instance. This eliminates a single point of failure across a property's floor, matches how the existing MK2/Titan EGMs operated, and makes the deployment model simple — standard Windows executable management, no cluster orchestration required.
The Windows executable is the current shipping form. The planned next step is containerizing Hyperion as a Docker image, which would make the service OS-agnostic and open the door to deploying on Linux-based EGM hardware — removing the Windows dependency without changing the service contract for any EGM client.
Existing Titan RNG. Rather than building a new random number generator, Hyperion integrates the existing Titan RNG engine. Class 2 regulations require a CD-approved RNG, and Titan was already approved. Using it directly avoided a costly re-certification process.
Trade-offs
Skipping authentication was a deliberate, reasoned call. Hyperion runs inside a closed LAN on the EGM floor — external access doesn't exist by design. Adding auth mechanisms would have added development time without meaningfully reducing the real threat surface in this deployment context. The document explicitly flags what would need to change if a customer required it: CDS-registered auth and network monitoring. It's deferred, not forgotten.
The configuration store supports multiple paytables but doesn't yet have a mechanism to validate which paytable is active and confirm that state is reflected on the CDS. That gap — along with cashout handling and proper health monitoring — represents the next layer of production-hardening work before a wider rollout.
Challenges + Debugging
The hardest part of this project wasn't a technical problem — it was the skill gap. I came in knowing C# and Unity game development. Building Hyperion meant learning REST API design, HTTP, ASP.NET Core, system architecture tradeoffs, Protobuf, WPF inter-process communication, and CI/CD pipeline configuration, mostly in parallel, under real delivery pressure.
The mental shift from game developer to fullstack engineer was steep. In game dev, the "backend" is a game loop — deterministic, self-contained, frame-by-frame. In a web API context, requests arrive concurrently, state is distributed across processes, and failures are often silent until something downstream breaks. Learning to reason about that model while simultaneously shipping features took deliberate effort.
One concrete debugging challenge was the TCP channel between the Bingo UI process and the Hyperion backend. Early in development, the UI would occasionally stall — the WPF process had sent a Protobuf message over TCP but received no response, leaving the UI in a hanging state. The root cause was a message framing issue: TCP is a stream protocol, not a message protocol, and without explicit length-prefixing on the Protobuf messages, the reader on the backend side was sometimes blocking waiting for bytes that had already arrived in a prior read. Adding proper length-prefix framing to both sides resolved it completely.
Impact
- Delivered a functioning Class 2 gaming middleware platform — API, Bingo UI, RNG, progressive broadcasting, config store — from zero.
- Load testing showed no performance bottlenecks compared to standard MK2/Titan EGMs on CDS.
- GitHub Actions CI pipeline catches build failures and runs unit tests on every PR and push to main.
- Positioned the team to onboard a pilot customer and iterate quickly on their feedback, with a CI/CD expansion plan in place for fast turnaround during the trial period.
- Personally grew from game developer to fullstack engineer — system design, REST APIs, inter-process communication, and infrastructure — in the span of this single project.
Problem
Our game math SDK had accumulated years of tooling debt. Different team members had built different utilities independently, so the toolchain was a sprawl of overlapping tools — some widely used, some touched by one person years ago, most with no clear ownership. The result was a confusing SDK surface where onboarding a new developer meant a scavenger hunt just to figure out which tool to actually run.
Underneath that surface problem was a harder one: the math simulation pipeline was entirely synchronous. Every simulation step blocked the thread it ran on. On large game configurations, a full simulation run could take weeks, which meant developers were context-switching constantly, scheduling simulations as overnight or weekend jobs, and frequently discovering failures only after multi-day waits.
Design Decisions
The core decision was to separate the overhaul into two distinct efforts: surface cleanup (consolidating the tool inventory) and pipeline re-architecture (async processing).
For surface cleanup, I audited usage across the SDK and interviewed team members to map which functions and modes were actively relied on versus legacy dead weight. Unused modes were removed rather than deprecated-in-place — deprecation notices tend to linger forever; hard removal forces the conversation. The consolidated toolset was reorganized around actual team workflows instead of how the original authors had thought about the problem.
For the pipeline, the re-architecture enabled parallel simulations across all bets and percentages — allowing automatic flow between each simulation stage, making it possible to run the entire process in a single operation. That meant a developer could kick off a full simulation run in the background during the workday or before leaving the office, and come back to completed results rather than scheduling it as a multi-day overnight job. Less waiting, more building.
Trade-offs
The async refactor introduced non-trivial complexity around shared state. The original synchronous design made state management simple — one thing ran at a time, so there were no races. Moving to concurrent task execution meant auditing every piece of mutable state the simulation engine touched and either making it thread-safe or restructuring ownership so tasks didn't share mutable data.
We chose not to parallelize everything. Some simulation stages have strict ordering dependencies. Over-parallelizing would have required introducing synchronization primitives throughout, trading one kind of complexity for another. The goal was to eliminate unnecessary blocking, not to maximize raw concurrency.
Challenges + Debugging
The hardest debugging session involved a class of intermittent failures in simulation output — results that were wrong but not obviously so. The culprit turned out to be a shared random number generator being accessed concurrently from multiple tasks. In the synchronous world this was invisible; under async execution, tasks were racing to read and advance the RNG state, producing non-deterministic output. The fix was to give each task its own seeded RNG instance rather than sharing one, but finding the root cause required adding deterministic replay logging to the simulation so we could isolate which task sequence produced the divergent output.
Impact
- Simulation time dropped from weeks to days — a reduction that materially changed how the team could iterate on game math, moving it from a scheduled batch job to something fast enough for a development feedback loop.
- Tool sprawl was eliminated. The consolidated SDK surface made onboarding straightforward and reduced the "which tool do I actually use?" confusion to zero.
- Removed years of dead code and unused modes, shrinking the maintenance surface the team had to reason about going forward.
Problem
HuffNMorePuff is a Class 2 slot game built around the story of the Three Little Pigs — players earn through a base game, bonus rounds, and jackpots, with the wolf's "huff and puff" mechanic driving the game's identity. The starting point was an existing Class 3 game: one where outcomes are determined locally by the machine's own RNG. The goal was to convert it to Class 2 — where game outcomes are determined by a Central Determination Server (CDS) using a CD-approved Bingo-based RNG — and get it certified and into the field.
This was my first project with full end-to-end ownership. No senior developer to hand off the hard parts. I was the sole developer and the main technical point of contact across product management, the art team, and QA throughout the entire lifecycle.
Design Decisions
Class 3 → Class 2 conversion as the architectural spine. Class 2 compliance isn't a feature you bolt on — it changes how the game fundamentally resolves outcomes. The local RNG calls had to be replaced with CDS-mediated requests, and the game logic had to be restructured around receiving externally-determined outcomes rather than generating them internally. Every place in the codebase that produced a game result was audited and re-routed.
Class 2 math generation from scratch. Class 2 games require a separate math model that maps Bingo outcomes from the CDS to game results (reel stops, bonus triggers, jackpot awards). Generating this math meant building paytables and probability distributions that satisfied both the regulatory requirements and the game's target return-to-player. The math had to be submitted for approval as part of certification.
Keeping the Class 3 game as a reference. Rather than rebuilding from nothing, the existing Class 3 game served as the visual and gameplay reference. This let the art and product teams stay aligned on what the game should look and feel like, while the engineering work focused on the underlying mechanism change. It also gave QA a direct comparison target for regression testing.
Trade-offs
Because this project was a port, the trade-off wasn't deciding which features to invent; it was deciding implementation order. As the sole developer, I prioritized the highest-risk systems first so we could get stable builds into QA early and validate the Class 2 flow before polishing everything else. I worked closely with product to confirm the correct paytables were implemented and that payouts matched customer requirements, then sequenced remaining work behind that baseline. The success criterion was getting a stable, certifiable build into field testing as early as possible.
Cross-team communication also required time that would otherwise go into code. As the main point of contact for art, QA, and product, I was the integration layer between disciplines that didn't always speak the same language. That was non-negotiable — no one else could resolve an art asset spec question or a QA repro ambiguity at the technical level — but it meant context-switching constantly during development.
Challenges + Debugging
The steepest challenge was the sheer scope of ownership. In prior work I had always had a layer of support — a senior developer who had seen the problem before, or a narrower slice of the system to own. Here, every category of problem was mine: bugs the art team introduced through asset updates, QA-reported edge cases in bonus logic, math discrepancies caught during review, build issues in the CI pipeline. Learning to triage all of that simultaneously, without losing forward momentum on the core development work, was the real skill built on this project.
One recurring class of bugs came from the Class 2 outcome mapping. The CDS returns a Bingo result, and the game has to translate that into a specific reel outcome deterministically. Early in development, certain CDS result patterns would map to reel states that the paytable didn't account for — either producing incorrect payouts or crashing the outcome resolver. Fixing these required going back to the math model, identifying the gap in the mapping table, regenerating the affected math, and re-validating end-to-end. It was slow, methodical work, and it taught me to think about the full input space of a system before declaring it done.
Impact
- Shipped a complete Class 2 game — base game, bonuses, and jackpots — from conversion through certification to field deployment.
- Sole developer and cross-functional point of contact across product, art, and QA for the full project lifecycle.
- Developed a working understanding of the full software development lifecycle: requirements, design, implementation, testing, certification, and deployment.
- The project established a personal baseline: what it actually takes to own something from idea to field, and how to problem-solve without a safety net.