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
- Data Loading: Weather data is fetched from the Kansas Mesonet API with daily average temperature (
TEMP2MAVG) and precipitation (PRECIP) - Day Advancement: When
timeAccumulatorreaches 23.0 hours, the simulation advances to the next day - GDD Accumulation: Each new day adds its calculated GDD to
cumulativeGDDin the WeatherState - Frame Distribution: The daily GDD is stored in
gddToApplyThisFramefor 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:
| Property | Type | Purpose |
|---|---|---|
timeAccumulator | number | Current hour of day (0-24), starts at 5.0 (6 AM) |
currentDayIndex | number | Index into the CSV weather data array |
cumulativeGDD | number | Total GDD accumulated since simulation start |
cumulativeRain | number | Total precipitation (inches) since simulation start |
startDate | Date | Real-world date corresponding to day 0 |
speedMultiplier | number | Time acceleration factor (default: 1) |
isWaiting | boolean | Whether 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
| Crop | Required GDD | Approximate Days (15°C avg) |
|---|---|---|
| Soybean | 900 | ~180 days |
| Wheat | 1000 | ~200 days |
| Corn | 1300 | ~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:
- Weather Data Fetch: Kansas Mesonet API →
WeatherSimManager.loadWeatherData() - Day Advancement:
timeAccumulator >= 23.0→advanceDay() - GDD Calculation:
max(0, temp - 10)→weather.gddToApplyThisFrame - Crop Growth:
CropSimManager.update()→ adds GDD to all SEEDED tiles - Maturity Check:
currentGDD >= requiredGDD→ stage becomes MATURE - Harvesting: Tractor with harvesting on → collects yield score, resets tile
- Score Display:
yieldScoreshown in UI stats panel
Tile Properties
Each field tile stores these properties (via BitmapFieldState):
| Property | Type | Purpose |
|---|---|---|
stage | uint8 | Current growth stage (0=UNPLANTED, 1=SEEDED, 2=MATURE) |
type | uint8 | Crop type (0=EMPTY, 1=WHEAT, 2=CORN, 3=SOY) |
currentGDD | float32 | GDD accumulated since planting |
requiredGDD | float32 | GDD needed to reach maturity |
waterLevel | float32 | Water content (currently unused) |
minerals | float32 | Soil 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:
- Day 0 (April 1): Plant corn (requires 1300 GDD)
- Day 1-180: Accumulate ~5 GDD per day (spring/summer average)
- Day 180 (late September): Corn reaches ~900 GDD, still SEEDED
- Day 260 (mid-November): Corn reaches 1300 GDD, becomes MATURE
- 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.csvLinesis populated (weather data loaded) - Confirm tiles are in SEEDED stage, not UNPLANTED
Crops growing too fast/slow:
- Check
weather.speedMultipliervalue - Verify
isWaitingstate (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
- Field Tile System: Understand the binary field storage that underpins crop tiles
- Rendering Pipeline: Learn how crop stages are visualized on screen
- Simulation Engine Architecture: Explore the complete simulation loop