Skip to Content
🌾 Simulation FeaturesCrop Growth Weather Mechanics

Last Updated: 4/7/2026


Crop Growth & Weather Mechanics

Agricultural Microworlds simulates realistic crop growth driven by weather data. The system uses Growing Degree Days (GDD) as the core mechanism linking temperature to plant development. Understanding this relationship is essential for creating accurate educational scenarios and debugging simulation behavior.

Growing Degree Days (GDD)

GDD is an agricultural metric that measures heat accumulation over time. Plants require a specific amount of accumulated heat energy to reach maturity, making GDD a more accurate predictor of crop development than calendar days alone.

GDD Calculation

The WeatherSimManager calculates daily GDD using the formula:

dailyGDD = max(0, dailyAverageTemp - baseTemp)

The base temperature for wheat is 10°C (defined as WHEAT_BASE_TEMP in WeatherSimManager.js). Days with average temperatures below this threshold contribute zero GDD.

Weather Data Flow

  1. Data Loading: Weather data is fetched from the Kansas Mesonet API with daily average temperature (TEMP2MAVG) and precipitation (PRECIP)
  2. Day Advancement: When timeAccumulator reaches 23.0 hours, the simulation advances to the next day
  3. GDD Accumulation: Each new day adds its calculated GDD to cumulativeGDD in the WeatherState
  4. Frame Distribution: The daily GDD is stored in gddToApplyThisFrame for CropSimManager to consume

Speed Multiplier

The speedMultiplier property in WeatherState controls simulation time:

getSpeedMultiplier() { if (this.isWaiting) return this.speedMultiplier * 6.0; return this.speedMultiplier; }

When the tractor is waiting (via the wait_x_weeks block), the simulation runs 6× faster to skip ahead through time. The base speedMultiplier defaults to 1 but can be adjusted via setSpeedMultiplier().

Time Tracking

WeatherState tracks time using these properties:

PropertyTypePurpose
timeAccumulatornumberCurrent hour of day (0-24), starts at 5.0 (6 AM)
currentDayIndexnumberIndex into the CSV weather data array
cumulativeGDDnumberTotal GDD accumulated since simulation start
cumulativeRainnumberTotal precipitation (inches) since simulation start
startDateDateReal-world date corresponding to day 0
speedMultipliernumberTime acceleration factor (default: 1)
isWaitingbooleanWhether tractor is in wait mode (6× speed boost)

Crop Growth Stages

Each field tile progresses through three distinct stages defined in CropState.js:

export const CROP_STAGES = { UNPLANTED: 0, SEEDED: 1, MATURE: 2, };

Stage Transitions

UNPLANTED → SEEDED: Occurs when the seeder passes over an unplanted tile with seeding enabled. The tile’s currentGDD is reset to 0, and requiredGDD is set based on crop type.

SEEDED → MATURE: Triggered automatically when currentGDD >= requiredGDD. The CropSimManager performs this check every frame when new GDD is available.

CropSimManager Update Logic

The CropSimManager applies accumulated GDD to all seeded crops:

update(deltaTime, oldState, newState) { const gddToAdd = weather.gddToApplyThisFrame; if (gddToAdd <= 0) return; for (let i = 0; i < currentField.rows; i++) { for (let j = 0; j < currentField.columns; j++) { const fieldTile = currentField.getTileAt(j, i); if (fieldTile["stage"] == CROP_STAGES.SEEDED) { fieldTile["currentGDD"] += gddToAdd; if (fieldTile["currentGDD"] >= fieldTile["requiredGDD"]) { fieldTile["stage"] = CROP_STAGES.MATURE; fieldTile["currentGDD"] = fieldTile["requiredGDD"]; } nextField.setTile(j, i, fieldTile); } } } }

This loop runs only when gddToApplyThisFrame > 0, which happens once per simulated day at 11 PM (when timeAccumulator crosses 23.0).

Crop Types and GDD Requirements

Three crop types are available, each with different heat requirements:

export const CROP_TYPES = { EMPTY: 0, WHEAT: 1, CORN: 2, SOY: 3, }; export const CROP_GDDS = { [CROP_TYPES.UNPLANTED]: 0, [CROP_TYPES.WHEAT]: 1000.0, [CROP_TYPES.CORN]: 1300.0, [CROP_TYPES.SOY]: 900.0, };

GDD Thresholds

CropRequired GDDApproximate Days (15°C avg)
Soybean900~180 days
Wheat1000~200 days
Corn1300~260 days

Note: Actual days vary based on weather data. At 15°C average temperature, daily GDD = 15 - 10 = 5, so 1000 GDD ÷ 5 = 200 days.

Yield Scoring

Each crop type has a different yield score value:

const CROP_YIELDSCORES = { [CROP_TYPES.UNPLANTED]: 0, [CROP_TYPES.WHEAT]: 1, [CROP_TYPES.CORN]: 3, [CROP_TYPES.SOY]: 2, };

Corn provides the highest yield score (3 points per tile), making it the most valuable crop if successfully matured and harvested.

Harvesting Mechanics

The TractorSimManager handles harvesting when isHarvestingOn is true:

handleHarvesting(tractor, field) { this.applyToolAction(tractor, field, (tile) => { if (isMature(tile)) { tractor.yieldScore += getYieldScore(tile); reset(tile); return true; } else if (isGrowing(tile)) { reset(tile); return true; } }, this.HEADER_OFFSET); }

Harvesting Outcomes

Mature Crop (MATURE stage):

  • Adds the crop’s yield score to tractor.yieldScore
  • Resets the tile to UNPLANTED state
  • Success: Player earns points

Growing Crop (SEEDED stage):

  • Resets the tile to UNPLANTED state
  • No points awarded
  • Damage: Harvesting immature crops destroys them without yield

Unplanted Tile:

  • No action taken
  • Harvester passes over harmlessly

Seeding Mechanics

Seeding works in reverse — it only affects unplanted tiles:

handleSeeding(tractor, field) { this.applyToolAction(tractor, field, (tile) => { if (isUnplanted(tile)) { plant(tractor.cropBeingPlanted, tile); return true; } }, -this.HEADER_OFFSET); }

The seeder uses a negative header offset (-this.HEADER_OFFSET), placing seeds behind the tractor rather than in front.

Data Flow Summary

Here’s the complete flow from weather to harvest:

  1. Weather Data Fetch: Kansas Mesonet API → WeatherSimManager.loadWeatherData()
  2. Day Advancement: timeAccumulator >= 23.0advanceDay()
  3. GDD Calculation: max(0, temp - 10)weather.gddToApplyThisFrame
  4. Crop Growth: CropSimManager.update() → adds GDD to all SEEDED tiles
  5. Maturity Check: currentGDD >= requiredGDD → stage becomes MATURE
  6. Harvesting: Tractor with harvesting on → collects yield score, resets tile
  7. Score Display: yieldScore shown in UI stats panel

Tile Properties

Each field tile stores these properties (via BitmapFieldState):

PropertyTypePurpose
stageuint8Current growth stage (0=UNPLANTED, 1=SEEDED, 2=MATURE)
typeuint8Crop type (0=EMPTY, 1=WHEAT, 2=CORN, 3=SOY)
currentGDDfloat32GDD accumulated since planting
requiredGDDfloat32GDD needed to reach maturity
waterLevelfloat32Water content (currently unused)
mineralsfloat32Soil nutrients (currently unused)

The waterLevel and minerals properties are stored but not currently used in growth calculations. They are reserved for future expansion of the simulation model.

Example Growth Timeline

Using Kansas weather data starting April 1, 2021:

  1. Day 0 (April 1): Plant corn (requires 1300 GDD)
  2. Day 1-180: Accumulate ~5 GDD per day (spring/summer average)
  3. Day 180 (late September): Corn reaches ~900 GDD, still SEEDED
  4. Day 260 (mid-November): Corn reaches 1300 GDD, becomes MATURE
  5. Harvest: Collect 3 yield points per tile

If harvested too early (e.g., day 100 at 500 GDD), the crop is destroyed with zero yield.

Debugging Growth Issues

Common issues and their causes:

Crops not growing:

  • Check weather.gddToApplyThisFrame — should be > 0 once per day
  • Verify weather.csvLines is populated (weather data loaded)
  • Confirm tiles are in SEEDED stage, not UNPLANTED

Crops growing too fast/slow:

  • Check weather.speedMultiplier value
  • Verify isWaiting state (causes 6× acceleration)
  • Inspect actual temperature values in CSV data

Harvest gives zero points:

  • Tile must be MATURE stage (check with debugger)
  • Verify getYieldScore() returns correct value for crop type
  • Ensure harvesting is enabled (isHarvestingOn = true)

What’s Next