psenger/random-path-generator
Procedural horizontal path generation that actually works written in GoLang.
Random Path Generator
Procedural horizontal path generation that actually works.
..................................................
......┌──────┐....................................
......│......│....................................
......└────┐.│....................................
...........│.│..┌●─┐.┌─●─┐.┌─●─┐.....┌─●┐.........
●───●──────┘.└─●┘..│.│...│.│...└┐...┌┘..└●─┐.┌─●──
...................│.│...└─┘....└┐.┌┘......│.│....
...................└─┘...........└─┘.......│.│....
...........................................└─┘....
..................................................
Left to right. Right to left. Every run different. Always valid.
The Problem
You need procedurally generated horizontal paths for your game, visualization, or creative project. Random walks hit dead ends. Maze algorithms feel too structured. You want something organic that guarantees a valid path from one side to the other - every single time.
The Solution
Random Path Generator builds horizontal paths using tile-based composition with backtracking. Define ASCII art tiles with entry and exit points. The algorithm chains them together randomly from edge to edge, and when it hits a dead end, it backtracks and tries another route.
Key constraint: Paths flow horizontally only - either left-to-right or right-to-left. Tiles connect at the left and right edges, never top or bottom.
Result: Infinite variety. Zero failures. Full control over visual style.
Why This Approach?
Alternative methods exist for procedural path generation:
| Approach | Trade-off |
|---|---|
| Wave Function Collapse | Powerful but complex to implement, debug, and tune constraints |
| L-systems | Great for organic growth, harder to guarantee endpoint connectivity |
| A* / Dijkstra variants | Optimal paths but require pre-built graphs, less visual variety |
| Perlin noise + carving | Natural-looking but unpredictable connectivity, requires post-validation |
This tile-based approach prioritizes simplicity and reliability. The algorithm is straightforward to understand, test, and extend. Complexity grows linearly with the tile set, not exponentially with edge cases. The cost of more sophisticated methods is often hidden in debugging time, edge case handling, and test coverage requirements.
Quick Start
git clone https://github.com/psenger/random-path-generator.git
cd random-path-generator
# Generate path with defaults (20x10 grid, left-to-right)
go run main.go
# Generate path from RIGHT edge to LEFT edge
go run main.go -rtl
# Custom grid size and start position
go run main.go -width 30 -height 15 -start-y 7
# Use custom tiles directory
go run main.go -tiles ./my-tilesExample Output (Left to Right)
Configuration:
Tiles path: ./tiles
Tiles loaded: 18
Min tile size: 3 x 3
Grid size: 20 x 10
Start Y: 5
Direction: left-to-right
Solution found!
Tiles used:
1. medium-line.md at (0,4)
2. small-jag-mirrored.md at (3,4)
3. small-jag.md at (5,4)
4. medium-wave.md at (7,1)
5. medium-line.md at (13,4)
6. small-s-curve.md at (16,0)
Path waypoints:
(0,5) ENTRY [medium-line.md]
(1,5) STRAIGHT_H [medium-line.md]
(2,5) STRAIGHT_H [medium-line.md]
(3,5) EXIT [medium-line.md]
(3,5) ENTRY [small-jag-mirrored.md]
(4,5) CORNER_SW [small-jag-mirrored.md]
(4,6) CORNER_NE [small-jag-mirrored.md]
(5,6) EXIT [small-jag-mirrored.md]
(5,6) ENTRY [small-jag.md]
(6,6) CORNER_NW [small-jag.md]
(6,5) CORNER_SE [small-jag.md]
(7,5) EXIT [small-jag.md]
(7,5) ENTRY [medium-wave.md]
(8,5) STRAIGHT_H [medium-wave.md]
(9,5) CORNER_NW [medium-wave.md]
(9,4) STRAIGHT_V [medium-wave.md]
(9,3) STRAIGHT_V [medium-wave.md]
(9,2) CORNER_SE [medium-wave.md]
(10,2) STRAIGHT_H [medium-wave.md]
(11,2) CORNER_SW [medium-wave.md]
(11,3) STRAIGHT_V [medium-wave.md]
(11,4) STRAIGHT_V [medium-wave.md]
(11,5) CORNER_NE [medium-wave.md]
(12,5) STRAIGHT_H [medium-wave.md]
(13,5) EXIT [medium-wave.md]
(13,5) ENTRY [medium-line.md]
(14,5) STRAIGHT_H [medium-line.md]
(15,5) STRAIGHT_H [medium-line.md]
(16,5) EXIT [medium-line.md]
(16,5) ENTRY [small-s-curve.md]
(17,5) STRAIGHT_H [small-s-curve.md]
(18,5) STRAIGHT_H [small-s-curve.md]
(19,5) CORNER_NW [small-s-curve.md]
(19,4) STRAIGHT_V [small-s-curve.md]
(19,3) CORNER_SW [small-s-curve.md]
(18,3) STRAIGHT_H [small-s-curve.md]
(17,3) CORNER_NE [small-s-curve.md]
(17,2) STRAIGHT_V [small-s-curve.md]
(17,1) CORNER_SE [small-s-curve.md]
(18,1) STRAIGHT_H [small-s-curve.md]
(19,1) STRAIGHT_H [small-s-curve.md]
(20,1) EXIT [small-s-curve.md]
Direction Modes
This generator supports two horizontal directions only:
| Flag | Direction | Start | End | Use Case |
|---|---|---|---|---|
| (default) | Left to Right | Left edge (x=0) | Right edge (x=width) | Standard flow, reading direction |
-rtl |
Right to Left | Right edge (x=width) | Left edge (x=0) | Reverse flow, mirrored layouts |
Note: Vertical path generation (top-to-bottom, bottom-to-top) is not supported. Tiles must have ENTRY on the left edge and EXIT on the right edge.
CLI Options
| Flag | Default | Description |
|---|---|---|
-width |
20 |
Grid width (minimum: smallest tile width, default tiles: 3) |
-height |
10 |
Grid height (minimum: smallest tile height, default tiles: 3) |
-start-y |
5 |
Vertical start position (0 to height-1) |
-tiles |
./tiles |
Path to tiles directory |
-rtl |
false |
Generate path from right to left |
Note: Minimum dimensions are determined by the smallest tile in the loaded tile set. With the default tiles, the smallest tile is small-line.md at 3x3, so width and height must each be at least 3.
Examples
# Default: 20x10 grid, start at y=5, left-to-right
go run main.go
# Larger grid
go run main.go -width 40 -height 20 -start-y 10
# Right-to-left with custom dimensions
go run main.go -rtl -width 30 -height 12 -start-y 6
# Custom tiles directory
go run main.go -tiles /path/to/custom/tilesWhy This Exists
| Feature | Description |
|---|---|
| Horizontal paths | Generates paths that flow left-to-right or right-to-left across a grid. Not mazes - connected paths. |
| Bidirectional | Single flag (-rtl) reverses direction. Same tiles work both ways. |
| Guaranteed success | Backtracking algorithm exhaustively tries all combinations. If a solution exists, it finds it. |
| Tile-based design | Define your own visual style with ASCII art tiles. Swap tile sets without touching code. |
| Path metadata | Every waypoint includes type info (CORNER_NW, STRAIGHT_H, etc.) for rendering engines. |
| Production ready | 49 tests, 78% coverage, typed YAML parsing, proper error handling. |
How It Works
Left to Right (Default)
+-------------------------------------------------------------+
| 1. Start at left edge (0, startY) |
| 2. Pick random tile from available set |
| 3. Align tile ENTRY (left side) with current position |
| 4. Validate tile fits within grid bounds |
| 5. Place tile, snapshot grid region for backtracking |
| 6. Tile EXIT reaches right edge? --> SUCCESS |
| 7. Move current position to tile EXIT |
| 8. Repeat from step 2 |
| |
| Dead end? (no tiles fit) |
| --> Pop last tile from stack |
| --> Restore grid from snapshot |
| --> Mark that tile as rejected at this position |
| --> Try next available tile |
+-------------------------------------------------------------+
Right to Left (-rtl flag)
+-------------------------------------------------------------+
| 1. Start at right edge (width, startY) |
| 2. Pick random tile from available set |
| 3. Align tile EXIT (right side) with current position |
| 4. Validate tile fits within grid bounds |
| 5. Place tile, snapshot grid region for backtracking |
| 6. Tile ENTRY reaches left edge? --> SUCCESS |
| 7. Move current position to tile ENTRY |
| 8. Repeat from step 2 |
+-------------------------------------------------------------+
The algorithm is randomized tile placement with exhaustive backtracking. Randomness creates variety; backtracking guarantees success.
Tile Format
Tiles are Markdown files with YAML frontmatter defining the path geometry.
---
title: small-wave
path:
- x: 0
y: 3
type: ENTRY
- x: 1
y: 3
type: STRAIGHT_H
- x: 2
y: 3
type: CORNER_NW
- x: 2
y: 2
type: STRAIGHT_V
- x: 2
y: 1
type: CORNER_SE
- x: 3
y: 1
type: STRAIGHT_H
- x: 4
y: 1
type: CORNER_SW
- x: 4
y: 2
type: STRAIGHT_V
- x: 4
y: 3
type: CORNER_NE
- x: 5
y: 3
type: STRAIGHT_H
- x: 6
y: 3
type: EXIT
size:
- x: 7
y: 5
---
.......
..+-+..
..|.|..
o-+.+-o
.......Tile Requirements
| Requirement | Description |
|---|---|
| ENTRY at x=0 | Entry point must be on the LEFT edge of the tile |
| EXIT at x=size.x-1 | Exit point must be on the RIGHT edge of the tile |
| Horizontal flow | Path flows from left to right within each tile |
| Size matches art | The size field must match the ASCII art dimensions |
Path Types
Use these types to tell your rendering engine what each cell represents:
| Type | Description |
|---|---|
ENTRY |
Path entry point (left edge) |
EXIT |
Path exit point (right edge) |
STRAIGHT_H |
Horizontal line segment |
STRAIGHT_V |
Vertical line segment |
CORNER_NW |
Corner connecting south and east (└) |
CORNER_NE |
Corner connecting south and west (┘) |
CORNER_SW |
Corner connecting north and east (┌) |
CORNER_SE |
Corner connecting north and west (┐) |
| custom | Add your own types for special tiles |
Tile Library
18 pre-built tiles included in ./tiles:
| Category | Tiles | Description |
|---|---|---|
| Lines | small-line, medium-line, large-line |
Straight horizontal paths |
| Waves | small-wave, medium-wave, large-wave |
Undulating paths with vertical movement |
| Mirrored | *-mirrored variants |
Vertically flipped versions |
| Jags | small-jag, small-jag-mirrored |
Diagonal step patterns |
| Complex | aztec, aztec-mirrored, crazy-ivan |
Multi-segment decorative paths |
Mix and match. Remove tiles you don't want. Add your own.
API
Load and Build
// Load tiles from directory
tiles, err := loadAllTiles("./tiles")
if err != nil {
log.Printf("Warning: %v", err)
}
// Build path left to right
solution, err := buildSolution(tiles, 20, 10, 5, LeftToRight)
if err != nil {
log.Fatal(err)
}
// Build path right to left
solution, err = buildSolution(tiles, 20, 10, 5, RightToLeft)
if err != nil {
log.Fatal(err)
}
// Render the grid
for _, row := range solution.Grid {
fmt.Println(string(row))
}Function Signature
func buildSolution(
tiles map[string]*TileInfo, // Loaded tile definitions
width int, // Grid width (path spans this)
height int, // Grid height
startY int, // Vertical start position
direction Direction, // LeftToRight or RightToLeft
) (*Solution, error)Access Path Data
// Tiles used in solution
for _, entry := range solution.Path {
fmt.Printf("%s at (%d,%d)\n",
entry.Tile.Name,
entry.OffsetX,
entry.OffsetY)
}
// Full path with cell types (for rendering engines)
for _, wp := range solution.Waypoints {
fmt.Printf("(%d,%d) %s [%s]\n",
wp.GridX, wp.GridY, wp.Type, wp.TileName)
}Types
// Direction controls horizontal path flow
type Direction int
const (
LeftToRight Direction = iota // ENTRY at x=0, EXIT at x=width-1
RightToLeft // EXIT at x=width-1, ENTRY at x=0
)
// Solution contains the generated path
type Solution struct {
Grid [][]rune // Rendered ASCII grid
Path []StackEntry // Tiles used and their positions
Waypoints []PathSegment // Full path with cell types
Width int
Height int
}
// PathSegment represents one cell in the path
type PathSegment struct {
GridX int // X position on solution grid
GridY int // Y position on solution grid
Type string // ENTRY, EXIT, CORNER_NW, STRAIGHT_H, etc.
TileName string // Source tile filename
}
// TileInfo contains parsed tile data
type TileInfo struct {
Name string
Entry Point // Entry point (must be at x=0)
Exit Point // Exit point (must be at x=width-1)
Width int
Height int
Grid [][]rune // ASCII art
Path []PathPoint // Full path metadata
}Testing
# Run all tests
go test -v ./...
# Coverage report
go test -cover ./...
# coverage: 78% of statements
# Benchmarks
go test -bench=. ./...Test Coverage
| Component | Tests |
|---|---|
| File parsing | 5 |
| Tile extraction | 10 |
| Directory loading | 4 |
| Grid operations | 12 |
| Path translation | 6 |
| Left-to-right generation | 8 |
| Right-to-left generation | 3 |
| Integration | 1 |
| Total | 49 |
Project Structure
.
├── main.go # All source code (~580 lines)
├── main_test.go # Test suite (~1200 lines)
├── tiles/ # 18 pre-built tile definitions
│ ├── small-line.md
│ ├── small-wave.md
│ ├── aztec.md
│ ├── crazy-ivan.md
│ └── ...
├── go.mod
├── go.sum
├── CONTRIBUTING.md
└── README.md
Requirements
| Dependency | Version | Purpose |
|---|---|---|
| Go | 1.21+ | Uses maps.Clone from stdlib |
| gopkg.in/yaml.v3 | latest | YAML frontmatter parsing |
Use Cases
| Domain | Application |
|---|---|
| Game Development | Procedural dungeon corridors, pipe puzzles, circuit boards, side-scrolling level generation |
| Data Visualization | Flow diagrams, network paths, process flows, animated connection traces |
| Generative Art | ASCII art, plotter drawings, textile patterns, decorative borders |
| Education | Teaching backtracking algorithms, constraint satisfaction, procedural generation |
Limitations
| Limitation | Reason |
|---|---|
| Horizontal only | Paths flow left-to-right or right-to-left. No vertical (top-to-bottom) support. |
| Edge-to-edge | Paths must span the full grid width. No partial paths. |
| Fixed tile orientation | Tiles cannot be rotated or flipped at runtime. Create mirrored variants instead. |
Contributing
See CONTRIBUTING.md for development setup, guidelines, and how to submit pull requests.
Future Work
Vertical Path Generation
Extend the generator to support vertical paths in addition to the current horizontal-only implementation.
| Direction | Start | End | Tile Requirements |
|---|---|---|---|
| Top to Bottom | Top edge (y=0) | Bottom edge (y=height) | ENTRY on top edge, EXIT on bottom edge |
| Bottom to Top | Bottom edge (y=height) | Top edge (y=0) | ENTRY on bottom edge, EXIT on top edge |
Implementation requirements:
- New tile set with entry/exit points on top and bottom edges
- Modified algorithm to traverse vertically instead of horizontally
- New CLI flags (
-ttb,-btt) for direction selection
Path Crossing
Enable paths to intersect without requiring crossings to be pre-built into individual tiles.
Current behavior: Paths can only cross where a tile explicitly includes a crossing pattern baked into its ASCII art.
Proposed approaches:
| Approach | Description | Complexity |
|---|---|---|
| Crossing tiles | Dedicated tiles with pre-built intersections (┼, ╬, ╳) |
Low |
| Dynamic crossing detection | Algorithm detects intersections and inserts appropriate crossing tiles | Medium |
| Multi-layer compositing | Generate paths on separate layers, composite with crossing markers | High |
Use cases:
- Multiple simultaneous routes across the same grid
- Paths that weave over/under each other
- Complex network topologies (circuit boards, pipe systems, dungeon layouts)
License
MIT License - Use it however you want.
Horizontal paths. Guaranteed delivery. Zero dead ends.
Built with backtracking and zero patience for failures.