# Part 5: From Terminal Cells to Sprite Maps: Font Fallbacks and Auto-Tiling

> **Source:** [https://lorbic.com/autotiling-sprites-font-fallbacks-raylib/](https://lorbic.com/autotiling-sprites-font-fallbacks-raylib/)
> **Author:** [Vikash Patel](https://vikashpatel.net)
> **Published:** August 09, 2026
> **Reading Time:** 4 min
> 
> *This is the raw Markdown source of the article from the [Lorbic Technical Journal](https://lorbic.com/).*

---


Building a terminal-based engine in pure ASCII looks cool for about five minutes. Then you try rendering a complex facility map with corners, T-junctions, and status icons, and raw character cells start feeling incredibly limiting.

Two specific problems hit me immediately when I tried switching to Raylib for graphics. First, drawing wall tiles manually by hand in level files meant placing 16 different corner variations by hand. Second, when I tried rendering a 🚨 warning emoji alongside FiraCode monospace text, Raylib just rendered a missing glyph box (`?` or `□`).

To solve this, *Derelict Facility* uses a 4-bit cardinal autotiling algorithm alongside a cascading font fallback resolver in the Raylib display pipeline (`internal/display/raylib.go`).

---

## 4-Bit Cardinal Autotiling Mathematics

Drawing walls as uniform square blocks (`#`) breaks visual continuity in corridors. We want corners, T-junctions, and straight hallways to select their matching sprite tile automatically.

Instead of manual texture assignment, we evaluate each wall tile's 4 cardinal neighbors (North, East, South, West) and compile a 4-bit integer bitmask.

```mermaid
graph TD
    subgraph NeighborBitmask ["4-Bit Cardinal Bitmask Rules"]
        N["North: Bit 0 (Value 1)"]
        E["East: Bit 1 (Value 2)"]
        S["South: Bit 2 (Value 4)"]
        W["West: Bit 3 (Value 8)"]
    end

    NeighborBitmask --> Sum["Bitwise OR Sum (0 to 15)"]
    Sum --> SpriteMap["Tileset Source Rect (Bitmask * 16px, 0)"]
```

### Cardinal Neighbor Values
Each direction is mapped to a power-of-two bit index:
- **North (N)**: `1 << 0 = 1`
- **East (E)**: `1 << 1 = 2`
- **South (S)**: `1 << 2 = 4`
- **West (W)**: `1 << 3 = 8`

During level loading (`internal/world/json_loader.go`), we inspect adjacent tiles and construct the bitmask:

{{< code lang="go" file="internal/world/json_loader.go" >}}
func (l *JSONMapLoader) calculateWallBitmasks(m *Map) {
	for y := 0; y < m.Height; y++ {
		for x := 0; x < m.Width; x++ {
			tile := m.GetTile(x, y)
			if tile == nil || tile.Type != TileTypeWall {
				continue
			}

			var mask uint8 = 0
			if tN := m.GetTile(x, y-1); tN != nil && tN.Type == TileTypeWall { mask |= 1 }
			if tE := m.GetTile(x+1, y); tE != nil && tE.Type == TileTypeWall { mask |= 2 }
			if tS := m.GetTile(x, y+1); tS != nil && tS.Type == TileTypeWall { mask |= 4 }
			if tW := m.GetTile(x-1, y); tW != nil && tW.Type == TileTypeWall { mask |= 8 }

			tile.Bitmask = mask
		}
	}
}
{{< /code >}}

This produces a single byte value between `0` (an isolated wall column) and `15` (a 4-way intersection).

### Tileset Coordinate Mapping
The computed `Bitmask` maps directly to sprite sheet source X offsets:

```
Bitmask Value -> Visual Connection -> Sprite Sheet Offset X
0             -> Isolated Pillar    -> 0px
1             -> North Cap          -> 16px
3 (1+2)       -> N-E Corner         -> 48px
15 (1+2+4+8)  -> 4-Way Cross        -> 240px
```

### Autotiling Trade-offs (4-Bit vs. 8-Bit)
4-bit cardinal autotiling requires only 16 sprite variations, keeping tileset textures small. However, cardinal autotiling does not evaluate diagonal neighbors (NW, NE, SE, SW). If your game requires outer vs. inner corner transitions on thick 2D terrain, an 8-bit (47-tile Blob autotiling) lookup table is required instead.

---

## Cascading Font Fallback Chain

Standard font loading functions in graphics libraries bind a single TTF/OTF font file. When rendering terminal UI text mixed with unicode indicators or status emojis, missing glyphs trigger placeholder box rendering.

To fix this, we implement a fallback resolver (`internal/display/raylib.go`) that queries a prioritized chain of loaded font instances:

```mermaid
graph LR
    Rune["Target Unicode Rune"] --> CheckPrimary{"IsGlyphAvailable(PrimaryFont)?"}
    CheckPrimary -- Yes --> RenderPrimary["Render with FiraCode"]
    CheckPrimary -- No --> CheckEmoji{"IsGlyphAvailable(EmojiFont)?"}
    CheckEmoji -- Yes --> RenderEmoji["Render with Noto Emoji"]
    CheckEmoji -- No --> RenderSymbol["Render with Symbola"]
```

{{< code lang="go" file="internal/display/raylib.go" >}}
func (r *RaylibDisplay) getFontForGlyph(char rune) rl.Font {
    if rl.IsGlyphAvailable(r.PrimaryFont, char) {
        return r.PrimaryFont
    }
    
    if rl.IsGlyphAvailable(r.EmojiFont, char) {
        return r.EmojiFont
    }
    
    return r.SymbolaFont
}
{{< /code >}}

### Render Loop Integration
During draw passes, the display driver uses the calculated bitmask for sprite sheet clipping while routing text glyphs through the font fallback chain:

{{< code lang="go" file="internal/display/raylib.go" >}}
func (r *RaylibDisplay) DrawMapTile(gridX, gridY int, tile world.Tile) {
    pixelX := int32(gridX) * r.CellWidth
    pixelY := int32(gridY) * r.CellHeight
    
    if tile.Type == world.TileTypeWall {
        srcRect := rl.NewRectangle(float32(tile.Bitmask)*16, 0, 16, 16)
        destRect := rl.NewRectangle(float32(pixelX), float32(pixelY), float32(r.CellWidth), float32(r.CellHeight))
        rl.DrawTexturePro(r.Tileset, srcRect, destRect, rl.NewVector2(0,0), 0, rl.White)
    } else {
        rl.DrawRectangle(pixelX, pixelY, r.CellWidth, r.CellHeight, rl.GetColor(0x222222FF))
    }
}
{{< /code >}}

---

## Summary

Combining 4-bit cardinal autotiling bitmasks with cascading font fallback chains resolves two core 2D rendering bottlenecks:

- **Automated Autotiling**: Eliminates manual wall placement errors by compiling neighbor connections into a single `0 to 15` byte index.
- **Robust Text & Emoji Rendering**: Prevents missing glyph artifacts when mixing vector fonts with unicode emojis in game UI layers.

