lantern

sprout-garden-core

Sprout Garden Core

The Simulation Engine - Hot Potato execution model

"If you've got it, pass it. Everyone passes together."

Overview

The core simulation engine implements a push-based execution model where sprouts flow through plots in discrete ticks. Each plot has defined seeds (input directions) and fruits (output directions).

This is the same execution model as Wanderland production workflows - if a 7-year-old can wire up plots to grow a sprout, the abstraction is correct.

Architecture

┌─────────────────────────────────────────────┐
│              SIMULATION ENGINE              │
├─────────────────────────────────────────────┤
│                                             │
│  Garden                                     │
│  ├── size: {width, height}                  │
│  └── plots: {'A1': {type, config}, ...}     │
│                                             │
│  Sprouts[] (active)                         │
│  ├── id, position, direction                │
│  ├── shape, color (state)                   │
│  ├── status: transit|arrived|lost|blocked   │
│  └── lineage: [{tick, plot, action}, ...]   │
│                                             │
│  Tick Loop                                  │
│  1. emitFromSprings() - Tick 0 only         │
│  2. executeTick() - Process all sprouts     │
│  3. Check completion                        │
│                                             │
└─────────────────────────────────────────────┘

Key Concepts

Position Keys

Grid uses Excel-style addressing: A1, B2, C3...

posKey(x, y) → `${String.fromCharCode(65 + y)}${x + 1}`
// (0,0) → "A1", (1,0) → "A2", (0,1) → "B1"

Seeds and Fruits

Each plot type defines:

  • seeds[]: Directions it can receive from
  • fruits[]: Directions it outputs to (or special markers)

Special fruit markers:

  • 'pass-through': Continue in same direction as incoming
  • 'perpendicular': Split 90° to incoming direction

Hot Potato Execution

executeTick() {
  // Everyone holding something plays
  for (const sprout of this.sprouts) {
    if (sprout.status !== 'transit') continue;
    
    // 1. Get current plot
    // 2. Grow: transform sprout via plot.grow()
    // 3. Find next position(s) via getFruitDirections()
    // 4. Queue movement (clone for splits)
  }
  
  // Execute all movements together
  for (const move of movements) {
    move.sprout.position = move.to;
    move.sprout.direction = move.direction;
  }
}

Splitter Cloning

When a plot has multiple output directions (perpendicular splitter):

let movingSprout = sprout;
if (!isFirstMove) {
  // Clone for additional directions
  movingSprout = this.createSprout(sprout.position, {
    shape: sprout.shape,
    color: sprout.color
  }, sprout.direction);
  movingSprout.lineage = [...sprout.lineage];
  this.sprouts.push(movingSprout);
}

Source Files

File Purpose
src/core/simulation.js Main simulation engine
src/core/constants.js Directions, helper functions
src/plots/index.js Plot registry (auto-wired)

Verification

Live test results from [[sprout-garden-core-test]]:

Test Status Details
simulation.js exists fail file not found
constants.js exists fail file not found

Slots

North

slots:
- sprout-garden
- sprout-garden

South

slots:
- sprout-garden-core-test

East

slots: []

West

slots: []
↑ northsprout-garden