Finally… a magic missile

Or two, as it turns out.

Magic missile is one of the most iconic Dungeons & Dragons spells. It’s the first spell every wizard learns because at first level (and all levels) it deals certain damage. It wasn’t until yesterday, however, that it was introduced to Mortal Wayfare.

Why? Honestly, I just never got around to it. The spell is a relatively complex implementation of combat casting with damage and animation. Over the years I’ve thought about how I would build it but now, with a simple prompt, Claude Code spit it out in minutes.

OK, let’s start with a Magic Missile spell. https://www.d20pfsrd.com/magic/all-spells/m/magic-missile/ is the spell. I use that website as a source for ‘core’ Pathfinder rules. The spell will also need an effect which will require an image artifact. I don’t yet have one for magic missile but it could be created. The ‘missile’ will need to travel from the source to the target. That could be a straight line but something that swerves around and did loops, like a paper airplane, would be cool. What else do you need to know to get started? I don’t know if this is a non-trivial feature but should we do this in plan mode?

For the image asset, I fed Claude Code a file with a bunch of icons and requested it grab “the blue one in the middle” and then “make the background transparent.” While Claude Code was “razzmatazzing,” I searched the web for some free audio effects. The whole process took about 15 minutes. Claude Code not only generated the spell logic correctly but implemented the curved trajectory animation without me having to debug path calculations. I’m not sure how long it would have taken me to do it myself but probably an order of magnitude longer.

For your viewing pleasure, here it is. Sound on!

Reviving the ol’ game with AI code generation

It had been… {looks at watch}… over 5 years since my last commit to this project! (Check out a demo video from back then.)

Life got very busy and, frankly, I felt like the coding reached a point where it was more about building on top of existing scaffolding (e.g., more monsters, spells, maps…) rather than creating new functionality. (That being said, Reinforcement Learning to teach combatants to operate optimally is still a high priority.) Many things, however, have changed in the intervening years, including code generation. As such, I’m reviving Mortal Wayfare to see how these new tools might help me push this forward.

  1. Reinstall Android Studio: It had been so long there was no way to automatically update. ChatGPT helped me through this.
  2. Get the project to compile: Here, again, my gradle was so outdated it took a series of steps to get everything up and running. Gemini, built into Android Studio, helped me here.
  3. Get the game running on my Google Pixel Fold: Gemini helped me here, too, and it worked! Although there are lots of bugs, old and new. For example, the game only works when I have the phone unfolded and rotated 90°. Also, when you click the ‘enter command’ bar the soft keyboard covers the entire text area and you can’t see what you’re typing. This was not a problem years ago.
  4. Install Claude Desktop and Code, with the command-line interface (CLI): The former is a lot like the website but the latter is something else entirely.

I started off by asking Claude Code to give me an overview of my project.

 Can you give me an overview of this project?                                                                                                        
● Explore(Explore project structure)
  ⎿ Done (27 tool uses · 50.0k tokens · 53s)
  (ctrl+o to expand)
● Mortal Wayfare - Project Overview
  Mortal Wayfare is a custom-built, old-school 2D turn-based RPG for Android, based on the Pathfinder tabletop rule system. It's built entirely from    scratch without a game engine.                                                                                                                                                        ---
  Core Concept
  Dungeon exploration and tactical combat with character creation, spellcasting, feats, NPCs, and quests on tile-based maps.
  ---
  Architecture
  The project uses a custom Android game engine with:
  - MapView — Custom SurfaceView rendering a 100x100 tile world
  - GameLoop — Dedicated render thread
  - GameApp — Singleton application managing all global game state
  - Combat — Turn-based combat with initiative, AoO, and action sequencing
  - Map — Handles pathfinding, line-of-sight, and entity management
  ---
  Package Structure (~175 Java files across 13 packages)
  ┌────────────────┬────────────────────────────────────────────┐
  │     Package      │                   Purpose                      │
  ├────────────────┼────────────────────────────────────────────┤
  │ main/            │ Core engine, rendering, input, UI              │
  ├────────────────┼────────────────────────────────────────────┤
  │ being/           │ Player and creature classes                    │
  ├────────────────┼────────────────────────────────────────────┤
  │ commands/        │ Action system (move, attack, cast)             │
  ├────────────────┼────────────────────────────────────────────┤
  │ effects/         │ Status conditions (dying, stable, poisoned…)   │
  ├────────────────┼────────────────────────────────────────────┤
  │ feats/           │ 60+ character abilities                        │
  ├────────────────┼────────────────────────────────────────────┤
  │ spells/          │ 11 spells implemented                          │
  ├────────────────┼────────────────────────────────────────────┤
  │ equipment/       │ Weapons, armor, items                          │
  ├────────────────┼────────────────────────────────────────────┤
  │ background/      │ Terrain tile types                             │
  ├────────────────┼────────────────────────────────────────────┤
  │ npc/             │ Non-player characters                          │
  ├────────────────┼────────────────────────────────────────────┤
  │ char_generation/ │ Character creation with dice rolling           │
  └────────────────┴────────────────────────────────────────────┘
  ---
  Key Features
  - Dice-based character creation (6 abilities, "bag of dice" method)
  - 30+ commands (movement, combat, spells, social)
  - Fog-of-war / line-of-sight visibility
  - Serialization-based character save/load
  - Text command interface with slash commands (/help, /maps, /load)
  - Pinch-to-zoom and drag-to-pan on the map
  ---
  Tech Stack
  - Language: Java (minimal Kotlin)
  - Min SDK: 21 (Android 5.0) / Target SDK: 34
  - Libraries: AndroidX AppCompat, LocalBroadcastManager
  - Persistence: Java object serialization
  - Graphics: Canvas/Bitmap on SurfaceView
  ---
  Current status: v0.12 alpha, actively developed.

That’s a decent overview, but I then asked Claude if it could analyze my codebase and write detailed observations about the most technically interesting aspects. I’ve tried to blog a little bit about those in the past but haven’t really shared much. The output below is long, and frankly I don’t remember much of it, but here is what Claude had to say with only edits for formatting:

Mortal Wayfare: Technical Deep Dive

In March 2013, armed with an “Introduction to Android” bootcamp and roughly 20 years away from writing serious code, I set out to build an old-school 2D RPG for Android from scratch — no game engine, no Unity, no Unreal. The goal was never the destination; it was the journey. A chance to find out if I still had it after years on the business side of the software world. The answer, ~14,200 lines of Java later, is yes.

What follows is a look at some of the more technically interesting things going on under the hood.


The game interface

The UI is split into two zones. The upper portion is the map — a 100×100 tile world rendered on a custom SurfaceView. You can pinch to zoom in and out and drag to pan. A short tap moves the hero toward the tapped tile; a long press on any visible tile examines it (the game enforces this — you can’t examine what you can’t see). Long-pressing a creature brings up a contextual panel showing its stats and a list of available commands, like Attack, depending on the situation.

Below the map is a scrolling text area that provides a running commentary — damage rolls, initiative order, spell effects, item descriptions. Below that is a command line for slash commands (/help, /maps, /load, /battle and more), giving the game a hybrid touch/text-adventure feel that suits the old-school aesthetic.


Line-of-sight: precomputed visibility with symmetry unfolding

The visibility system is the most algorithmically interesting part of the codebase. Rather than computing line-of-sight at runtime using raycasting (expensive for every entity every frame), the game uses a precomputed visibility matrix stored in a binary asset file that is de-serialized at startup.

The data structure

The pre-computed matrix is a HashMap<Integer, HashMap<Integer, Character>>. The outer key encodes an opaque tile’s position relative to the viewer as tileX * 100 + tileY. The inner HashMap maps each potentially obscured target tile to a char — used as a 16-bit bitmask — where each bit represents one of several possible lines of sight to that target. The bitmask encoding uses nibbles (4-bit groups), one per quadrant.

At runtime, the map accumulates which lines of sight are blocked by applying bitwise AND across all opaque tiles in the path:

if (obscuredLines != null) {
    numVisibleLines &= obscuredLines; // progressively mask blocked lines
}

If all bits are zeroed out, the target is not visible.

Unfolding 1/8 of the map

The precomputed data only stores visibility information for one octant (1/8 of the map — a triangular slice where x >= 1 and y < x). The remaining 7/8 is derived mathematically at startup using three passes of bitwise symmetry operations:

Pass 1 — Reflect across the diagonal:

for (int x = 2; x <= SCREEN_DRAW_SIZE; x++) {
    for (int y = 1; y < x; y++) {
        for (Entry<Point, Character> entry : visibileLinesMatrix[x][y].entrySet()) {
            Point key = entry.getKey();
            visibileLinesMatrix[y][x].put(new Point(key.y, key.x),
                swap(entry.getValue(), 2, 1));
        }
    }
}

Pass 2 — Flip vertically.

Pass 3 — Mirror left/right. Each transformation requires rearranging not just the coordinates but also the bitmask, since the directional meaning of each bit changes when the geometry is reflected. The swap() function handles this:

static char swap(char x, int i, int j) {
    return (char) swapNibbles(
        swapBits(swapBits(swapBits(swapBits(x, i, j),
            i+4, j+4), i+8, j+8), i+12, j+12), i, j);
}

The result: 8× storage savings, and all visibility lookups at runtime are simple array indexing.

Runtime visibility computation: shadow propagation from blockers

Once the precomputed matrix is loaded, computing what the hero can see at runtime uses an elegant inversion of the usual approach. Rather than asking “can I see tile X?” for each tile (which would require tracing rays to every target), computeVisibleLines asks the opposite question: “given that this opaque tile exists, what does it shadow?”

The algorithm starts by presetting every tile in the visibility window to fully visible (0xffff — all bits set). It then iterates over the rectangle of tiles within range. For each opaque tile it encounters, it retrieves that tile’s entry from the precomputed matrix — a HashMap mapping every potentially shadowed tile to a bitmask of which lines of sight it blocks:

HashMap<Point, Character> obscuredTiles = visibleLinesMatrix[i + offset][j + offset];
for (Entry<Point, Character> innerEntry : obscuredTiles.entrySet()) {
    visibleLinesChar[obscuredTileX][obscuredTileY] &= innerEntry.getValue();
}

Each AND operation progressively narrows the visibility of shadowed tiles. A tile with all bits zeroed is fully obscured. The final step counts the bits remaining in each tile’s visibility value using countNibbleBits() — the nibble with the most set bits determines how visible that tile is, since visibility is measured by the best available line of sight from any corner of the viewer’s square.

The result is that shadow propagates outward from blockers rather than rays being traced inward to targets — a fundamentally different and more efficient approach when the map contains many opaque tiles.


Illumination: inverse-square falloff with torch flicker

Light sources are modeled physically. Each Illumination object has a lumens value; the radius it illuminates is derived as:

lumensRange = (int) Math.sqrt(lumens / 10.0) + 1;

Within that radius, brightness falls off by the inverse square of distance:

lumensFlickerCache[x + lumensRange][y + lumensRange] =
    (lumens + randomLumens) / 4 / ((x*x) + (y*y));

The randomLumens value introduces flicker, throttled to 2 fps to mimic realistic torch behavior without burning unnecessary CPU. Illumination is combined with the visibility matrix — a tile is lit only if the hero can see it and the light source can reach it, meaning walls properly block torchlight.


Initiative and tie-breaking: encoding rules in decimal places

Pathfinder determines combat order by rolling d20 and adding modifiers, with ties broken by the highest DEX modifier. Rather than implementing tie-breaking as a special case in the combat sorting logic, the initiative system encodes the tie-breaker directly into the decimal portion of a float:

float initiative = init_d20 + initModifier;
initiative += (float) (initModifier + 50.0) / 100;
initiative = (float) Math.round(initiative * 100.0) / 100;

The integer part of the float is the standard initiative score. The decimal part encodes the DEX modifier shifted up by 50 (to handle negative modifiers) and scaled to the hundredths place — so a +3 DEX modifier adds 0.53, while a -2 adds 0.48. Higher modifier always wins a tie, exactly per the rules.

If two combatants still collide after that — identical roll and identical DEX — the code nudges one of them by a random ±0.01 until the collision is resolved:

while (combatants.containsKey(newInitiative)) {
    newInitiative += ((float) GameApp.d(1, 3) - 2f) / 100;
}

The payoff: combat order is just a descending sort of floats. No special tie-breaking logic anywhere else in the codebase. The rules are baked into the number itself.


Attacks of opportunity

AoO is modeled faithfully to the Pathfinder rules. Each entity gets exactly one attack of opportunity per turn. The AttackOfOpportunity command enforces a strict set of preconditions — flat-footed entities can’t take AoOs, unarmed humanoids can’t, and you can’t AoO an ally:

public boolean isPossibleCommand(Entities source, Entities target) {
    return source instanceof Beings
        && source.canTakeActions()
        && combat.isCombatant(source)
        && combat.attackOfOpportunityAvailable(source)
        && ((Beings) source).getHostility() != targetHostility
        && (!(source instanceof Humanoids) || !((Humanoids) source).isUnarmed())
        && !hasEffect(source, FlatFooted.class)
        && Attack.isMeleePossible(source, target)
        && super.isPossibleCommand(source, target);
}

A* pathfinding with horizon clipping

The Path class implements A* with a Euclidean distance heuristic, restricted to a bounding rectangle around the direct line between source and destination, padded by 10 tiles:

int left   = max(min(start.x, dest.x) - EXTRA_SEARCH_DISTANCE, 0);
int top    = max(min(start.y, dest.y) - EXTRA_SEARCH_DISTANCE, 0);
int right  = min(max(start.x, dest.x) + EXTRA_SEARCH_DISTANCE, MAP_SIZE);
int bottom = min(max(start.y, dest.y) + EXTRA_SEARCH_DISTANCE, MAP_SIZE);

Movement costs are differentiated — axial moves cost 2, diagonal moves cost 3, occupied squares cost 100, and impossible moves cost 1000 — which steers paths around obstacles naturally.


Tile variation: filename-encoded randomization

Rather than needing a unique asset for every possible tile orientation, the game encodes transformation rules directly in the asset filename. At load time, the filename is parsed and transformations applied:

  • rot4 — randomly rotate 0°, 90°, 180° or 270°
  • rot2 — randomly rotate 0° or 180°
  • fliph — randomly mirror horizontally
  • flipv — randomly mirror vertically

So _wall_stone_brick_mediumdark_rot2_fliphv tells the engine this wall tile can appear in two rotations and two mirror states. _blood_splatter_red_3dots_rot4_fliphv can appear in any of 8 orientations. One asset, many distinct placements — a significant reduction in required art.


Decor: composable tile layers

The game supports a decor system that allows any number of bitmaps to be stacked on top of a background tile. A fence bitmap placed on a grass tile creates a fenced meadow. A torch placed on a stone wall creates a lit dungeon corridor. Trees, rubble, bones, streams — all implemented as decor layered onto backgrounds. This multiplies the visual variety of maps enormously without requiring composite art assets.


The Level Editor

Mortal Wayfare ships with a companion Java desktop application — the level editor — that makes map creation visual and interactive. The editor presents a 100×100 tile canvas on the left and a full sprite palette on the right. The palette auto-detects the most likely class for each tile based on its filename (an orc sprite defaults to the Orc class; everything else defaults to Decor).

Maps are saved as simple pipe-delimited text files:

x|y|Class|_tile_filename|rotation

That file is copied into the Android project’s assets directory and parsed at runtime to instantiate all map entities. Right-clicking a tile in the palette lets you rename it; the editor then automatically updates all map files and Android source files that reference the old name — a handy refactoring tool baked right in.


The spell system: reflection-based registration

Spells are discovered at startup by scanning the DEX file for any class in the spells package:

DexFile df = new DexFile(getPackageCodePath());
for (Enumeration<String> iter = df.entries(); iter.hasMoreElements(); ) {
    String className = iter.nextElement();
    if (className.contains("spells"))
        Spells.addSpellToList(className);
}

Adding a new spell requires only writing the class — no registration code elsewhere.


Battle simulation mode

The /battle slash command puts the game into headless automated combat. The hero is handed to the monster AI, and results go to battle_log.txt. A battleRenderMod parameter controls how often the screen actually renders — set it to 10 and only every 10th battle is drawn, making it practical to run hundreds of combats for balance testing or, eventually, reinforcement learning data collection.


Other notable details

Outdoor flood fill and daylight. Maps support a mix of outdoor and indoor areas. At load time, the level editor’s ~outside markers seed a scanline flood fill that propagates outward across non-opaque tiles, stopping at walls and ~inside transition markers (doorways, thresholds). Any indoor tile bordering an outside tile is automatically flagged and assigned an OutdoorIllumination effect — a special variant of the torch illumination system that uses the same inverse-square falloff and visibility checks to simulate daylight bleeding through openings, without flicker. Outside tiles themselves simply return full brightness, bypassing the illumination pipeline entirely.

Command stack with deferred execution. Commands are pushed per entity and popped each game loop tick, so combat animations play out frame by frame. All entity collections use ConcurrentLinkedQueue for thread safety between the game loop and UI threads. Outdoor border tiles automatically receive the same inverse-square illumination treatment as torchlight, unifying indoor and outdoor lighting under one model.

Character class progression as data. The four character classes — Fighter, Cleric, Rogue and Wizard — are implemented as Java enums, each carrying its complete Pathfinder progression tables directly in the enum constructor: hit die, skill points per level, base attack bonus and saving throw arrays. Multiclassing falls out naturally by summing across a HashMap<Classes, Integer>. No lookup tables, no switch statements — the rules live in the type itself.

The effects system. Status conditions like FlatFooted, Stunned, Poisoned and Dying are first-class objects that attach to and detach from entities cleanly. This is what makes the AoO precondition check so readable — hasEffect(source, FlatFooted.class) is exactly what it looks like. The same pattern handles everything from poison ticks to the dying/stable/dead progression.

Threading. The game runs a dedicated render thread (GameLoop) alongside the Android UI thread, with both touching entity state. Rather than locking, entity effect and special-effect collections use ConcurrentLinkedQueue throughout — a small architectural decision that quietly prevents a whole class of race conditions.


Total: approximately 14,200 lines of Java across 175 files, plus the level editor.

No cs234 project

It was with a mix of disappointment and relief that I learned cs234, the final course for my Artificial Intelligence Graduate Certificate from Stanford, cancelled the project for this quarter. With the course going fully online due to the pandemic, the rational was to give everyone a bit of a break. That is perfectly reasonable given the work projects often entail, but it would have been pretty cool.

As such, development is once again suspended. (These Stanford courses are a lot of work, and there is a family and a job.) During the December sprint, however, I am happy to say that quite a bit of progress was made:

2020 GitHub Contributions
2020 GitHub Contributions
  1. A line of code was written – Just doing anything after 4 years is an accomplishment.
  2. Automated combat with enhanced messaging and logging – When I do get around to building a deep RL model to train the agents, this will be handy.
  3. Created the wizard class with wizard spells, including touch spell attacks – This one rounds out the fighters, clerics and rouges. I won’t be adding any new classes for a while.
  4. Group combat – Previously it was the ‘hero’ versus the monsters, but now two groups of combatants can fight each other.
  5. Charging, flanking and cover – These add a little more dynamic to the combat.
  6. Ferocity – Orcs just became much more challenging.
  7. Fixed a bunch of bugs – Always a decent idea.

So, what does all this progress look like? Here’s a little demo video set to O Fortuna from Carmina Burana by the MIT Concert Choir.

I’ll try to pick it up again in April, but we have to see how that goes.

A line of code has been written

The title’s deliberately passive voice is intended to comically understate the fact that it has been four years since I last worked on this project. Four years! I previously wrote about the delay, but the long story short is that life got busy and I got distracted with, arguably, much more important things. That being said, I am giving it another go. The renewed motivation comes from an unlikely source: Stanford University.

Over the past several years I have become quite passionate about AI, taking a number if different MOOCs, reading various book and pursuing some personal projects to explore the domain and learn the technology. One day in late 2018, gazing out of my office window at PARC, I spied the spire at the top of Hoover Tower and thought to myself, “I bet they have got some AI classes over there.”

Turns out they do. More specifically, turns out they have a Graduate Certificate in AI which offers a 4-course, 16-unit program of master’s-level instruction in AI. What could be better than that? A statement of motivation and a couple transcripts later, I was in. With a family and demanding full-time job I knew that pursuing graduate coursework would be challenging, but if I took only one course at a time, how hard could it be?

I quickly discovered that it could be very, very hard. Deep Learning (cs230) with Prof. Andrew Ng was the first course I took, which was probably a good choice. It was not a ton of work, until the final project, but the midterm was shockingly difficult. AI Principals and Techniques (cs221) is the only required course for the Certificate, so I took that next. It wasn’t too difficult, and I was more prepared for the midterm, but it was a lot of work and our final project was challenging. Then I took Machine Learning (cs229), again with Prof. Ng. It is notoriously one of the most difficult courses in the department and it lived up to its reputation. It was also an amazing amount of work and really tested my family life, but I survived that, too. In fact, we all did.

So what is this all about? For my last class I am signed up to take Reinforcement Learning (cs234) with Prof. Emma Brunskill. What does that have to do with Mortal Wayfare? Well, like all the courses, there is a final project. I therefore thought it could be really cool, for the final project, to implement RL within the game to ‘teach’ the actors (i.e. monsters and NPCs) to fight optimally. You can tell from game play that most RPGs use a simple rules-based decision engines to control enemies during combat. With a complex D&D-based system, however, constructing those rules can be complicated and the end result is often flat. I am curious to know what sort of dynamic and exciting combat I might be able to get by leveraging the materials from the course. To be able to do that I need to get the game back in working order as well as build a mechanism for fully-automated, repeating combat. After only a week, which I’m chronicling on Twitter, I am getting closer, but we will see how it goes. The course starts in January, so I am running out of time. Whatever happens, however, I will report back here.

In any event, unwilling to stop after course number four, I have already submitted my application for the MSCS program. Wish me luck!

Holy hiatus

I’m sure nobody noticed because nobody is actually following this blog, which is fine. That being said, I just looked up my last commit, which was Oct 31th, 2016 – 671 days ago! During this time I have been busy with full-time employment, taking classes in ML and AI, playing around with deep learning, volunteering for a fantastic AI conference, running the books and making a presentation for an amazing non-profit and otherwise life.

In the back of my mind, however, there has always been Mortal Wayfare. I’ve fantasized about finding time to do everything from creating the wizard class (there are already fighters, clerics and rogues) to using reinforcement learning to ‘teach’ the enemies and NPCs policies for optimal decision-making. So why this post now?

Yesterday, in a burst of get-it-done-idness, I fixed the MW email and upgraded the phpBB to v3.2.2. It still needs some tweaks, but I’m now going to try to figure out a system to deflect the relentless spammers, who, for the moment, are the only visitors to this site. Then I’m going to download the repo and try to figure out a plan to, painful inch by inch, move this project forward until, one day, it’s the most amazing retro 2D Android turn-based RPG on planet Earth. If not, I hope to at least have something that is playable. That sounds like a lot, but actually, I don’t think I’m too far off.

Wish me luck, nobody.