Skip to Content
🌾 Simulation FeaturesField Tile System

Last Updated: 4/7/2026


Field Tile System

Agricultural Microworlds uses a binary-packed array structure to represent the field efficiently. Instead of storing each tile as a JavaScript object, the BitmapFieldState class packs multiple properties into typed arrays, providing significant memory and performance benefits for large fields.

Why Binary Packing?

A 300×300 field contains 90,000 tiles. Storing each tile as a JavaScript object would create 90,000 objects with property lookup overhead and scattered memory allocation. The binary approach offers:

  • Memory efficiency: Typed arrays use contiguous memory with predictable size
  • Cache locality: Sequential tile access benefits from CPU cache prefetching
  • Performance: Direct array indexing is faster than object property access
  • Predictable memory: Each tile’s memory footprint is known at initialization

For a field with 6 properties per tile (stage, type, currentGDD, requiredGDD, waterLevel, minerals), the binary structure uses approximately 1.6 MB of contiguous memory instead of potentially 10+ MB of fragmented object allocations.

BitmapFieldState Structure

The BitmapFieldState class is defined in agricultural-microworlds.client/src/BinaryArrayAbstractionMethods/BitmapFieldState.js.

Constructor

constructor(rows, columns, fieldTileKey)

Parameters:

  • rows: Number of rows in the field (typically 300)
  • columns: Number of columns in the field (typically 300)
  • fieldTileKey: Object defining tile properties and their types

Example initialization (from simulationEngine.js):

const tileState = { ["stage"]: { size: 1, type: "uint8", }, ["type"]: { size: 1, type: "uint8", }, ["currentGDD"]: { size: 4, type: "float32", }, ["requiredGDD"]: { size: 4, type: "float32", }, ["waterLevel"]: { size: 4, type: "float32", }, ["minerals"]: { size: 4, type: "float32", }, }; const field = new BitmapFieldState(300, 300, tileState);

Supported Data Types

The typeMap defines available typed array types:

Type StringJavaScript TypeSize (bytes)Range
"int8"Int8Array1-128 to 127
"uint8"Uint8Array10 to 255
"int16"Int16Array2-32,768 to 32,767
"uint16"Uint16Array20 to 65,535
"int32"Int32Array4-2,147,483,648 to 2,147,483,647
"uint32"Uint32Array40 to 4,294,967,295
"float32"Float32Array4~±3.4e38 (7 decimal digits)
"float64"Float64Array8~±1.8e308 (15 decimal digits)

Choose the smallest type that fits your data range. For example, crop stages (0-2) use uint8, while GDD values use float32 for decimal precision.

Core API

Reading Tile Data

Get entire tile (returns object with all properties):

const tile = field.getTileAt(x, y); // Returns: { stage: 1, type: 2, currentGDD: 450.5, requiredGDD: 1300, ... }

Get single property:

const stage = field.GetVariableAt(x, y, "stage"); // Returns: 1 (SEEDED)

Writing Tile Data

Set entire tile (updates all properties):

const updatedTile = { stage: 2, // MATURE type: 2, // CORN currentGDD: 1300, requiredGDD: 1300, waterLevel: 800, minerals: 950, }; field.setTile(x, y, updatedTile);

Set single property:

field.setVariable("currentGDD", 500.0, x, y);

Initialization and Cloning

Initialize all tiles to default values:

const startingValues = { stage: CROP_STAGES.MATURE, type: CROP_TYPES.CORN, currentGDD: 0, requiredGDD: CROP_GDDS[CROP_TYPES.CORN], waterLevel: 1000, minerals: 1000, }; field.InitializeField(startingValues);

Clone the field (for state management):

const fieldCopy = field.clone();

The clone operation creates a new BitmapFieldState with independent typed arrays, copying all values. This is used in the simulation loop to create the nextStates object without mutating oldStates.

Internal Structure

Each property is stored in a separate typed array:

this.fieldProps = { "stage": { arr: Uint8Array(90000), type: "uint8", size: 1 }, "currentGDD": { arr: Float32Array(90000), type: "float32", size: 4 }, // ... other properties };

Index Calculation

Tiles are stored in row-major order. The index for tile (x, y) is:

#GetTileIndex(x, y) { return this.width * y + x; }

For a 300×300 field:

  • Tile (0, 0) → index 0
  • Tile (1, 0) → index 1
  • Tile (0, 1) → index 300
  • Tile (5, 10) → index 3005

This layout provides good cache locality when iterating row-by-row (the common case in rendering and simulation updates).

Usage in Simulation Managers

CropSimManager Example

The CropSimManager iterates over all tiles to apply GDD:

update(deltaTime, oldState, newState) { const currentField = oldState.field; const nextField = newState.field; const gddToAdd = weather.gddToApplyThisFrame; 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; } nextField.setTile(j, i, fieldTile); } } } }

Key pattern: Read from oldState.field, modify the tile object, write to nextState.field. This preserves immutability at the state level while allowing efficient batch updates.

TractorSimManager Example

The TractorSimManager reads and writes individual tiles during harvesting:

getTileAtLocation(x, y, field) { const tileX = Math.floor(x / this.TILE_WIDTH); const tileY = Math.floor(y / this.TILE_HEIGHT); if (tileY >= 0 && tileY < 300 && tileX >= 0 && tileX < 300) { const targetCrop = field.getTileAt(tileX, tileY); return [targetCrop, tileX, tileY]; } return null; }

After modifying the tile (e.g., resetting after harvest), it writes back:

field.setTile(tileX, tileY, modifiedTile);

Complete Example: Reading and Modifying a Tile

Here’s a complete example of checking a tile’s stage and advancing it:

// Get the field from state const field = stateManager.getState("field"); // Read tile at position (50, 100) const x = 50; const y = 100; const tile = field.getTileAt(x, y); console.log(`Tile at (${x}, ${y}):`); console.log(` Stage: ${tile.stage}`); console.log(` Type: ${tile.type}`); console.log(` Current GDD: ${tile.currentGDD}`); console.log(` Required GDD: ${tile.requiredGDD}`); // Modify the tile if (tile.stage === CROP_STAGES.SEEDED) { tile.currentGDD += 50; // Add 50 GDD if (tile.currentGDD >= tile.requiredGDD) { tile.stage = CROP_STAGES.MATURE; console.log("Crop matured!"); } // Write back to field field.setTile(x, y, tile); }

Performance Considerations

Efficient Iteration

Good (row-major order, cache-friendly):

for (let row = 0; row < field.rows; row++) { for (let col = 0; col < field.columns; col++) { const tile = field.getTileAt(col, row); // Process tile } }

Bad (column-major order, cache-unfriendly):

for (let col = 0; col < field.columns; col++) { for (let row = 0; row < field.rows; row++) { const tile = field.getTileAt(col, row); // Process tile } }

Batch Updates

When updating multiple properties, use setTile() once instead of multiple setVariable() calls:

Good:

const tile = field.getTileAt(x, y); tile.currentGDD += gdd; tile.waterLevel -= 10; tile.minerals -= 5; field.setTile(x, y, tile);

Bad:

const currentGDD = field.GetVariableAt(x, y, "currentGDD"); field.setVariable("currentGDD", currentGDD + gdd, x, y); const water = field.GetVariableAt(x, y, "waterLevel"); field.setVariable("waterLevel", water - 10, x, y); const minerals = field.GetVariableAt(x, y, "minerals"); field.setVariable("minerals", minerals - 5, x, y);

Memory Footprint

For a 300×300 field with the standard 6 properties:

  • stage: 1 byte × 90,000 = 90 KB
  • type: 1 byte × 90,000 = 90 KB
  • currentGDD: 4 bytes × 90,000 = 360 KB
  • requiredGDD: 4 bytes × 90,000 = 360 KB
  • waterLevel: 4 bytes × 90,000 = 360 KB
  • minerals: 4 bytes × 90,000 = 360 KB

Total: ~1.6 MB of contiguous memory

Adding New Properties

To add a new property to all tiles:

const newProps = { "soilPH": { size: 4, type: "float32" } }; const count = field.AddVariables(newProps); console.log(`Added ${count} new properties`);

This allocates a new typed array for the property across all tiles. The initial values will be 0 (or 0.0 for floats).

Limitations and Future Work

Current limitations:

  • waterLevel and minerals are stored but not used in growth calculations
  • No built-in serialization (would need custom save/load logic)
  • Fixed field size after creation (no dynamic resizing)

FieldTileSimManager exists but is currently empty (FieldTileSimManager.js). This is reserved for future water and mineral simulation logic.

What’s Next