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 String | JavaScript Type | Size (bytes) | Range |
|---|---|---|---|
"int8" | Int8Array | 1 | -128 to 127 |
"uint8" | Uint8Array | 1 | 0 to 255 |
"int16" | Int16Array | 2 | -32,768 to 32,767 |
"uint16" | Uint16Array | 2 | 0 to 65,535 |
"int32" | Int32Array | 4 | -2,147,483,648 to 2,147,483,647 |
"uint32" | Uint32Array | 4 | 0 to 4,294,967,295 |
"float32" | Float32Array | 4 | ~±3.4e38 (7 decimal digits) |
"float64" | Float64Array | 8 | ~±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 KBtype: 1 byte × 90,000 = 90 KBcurrentGDD: 4 bytes × 90,000 = 360 KBrequiredGDD: 4 bytes × 90,000 = 360 KBwaterLevel: 4 bytes × 90,000 = 360 KBminerals: 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:
waterLevelandmineralsare 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
- Rendering Pipeline: See how tiles are rendered on the canvas
- Crop Growth & Weather Mechanics: Understand how crop growth uses this tile system
- State Management System: Learn about the state management pattern that wraps this field