DEV Community

Cover image for Technical Deep Dive: Building "Botanical Battle"
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Technical Deep Dive: Building "Botanical Battle"

In this technical blog, we'll explore the architecture and implementation of Botanical Battle, a strategic AI-driven garden simulation built with React 19, TypeScript, and the Canvas API.


The Architecture: A Deterministic Game Engine

The core of Botanical Battle is a deterministic game engine that processes actions and returns the next game state. This approach ensures that the simulation is predictable and easy to debug.

The Game State Machine

The game state is defined by the GameState interface, which includes the grid, water levels, scores, stats, and current turn information.

// From /src/types.ts
export interface GameState {
  grid: Cell[][];
  water: { A: number; B: number };
  scores: { A: number; B: number };
  stats: GameStats;
  turn: number;
  maxTurns: number;
  currentAgent: 'A' | 'B';
  logs: string[];
  isGameOver: boolean;
  agentConfigs: { A: AgentConfig; B: AgentConfig };
}
Enter fullscreen mode Exit fullscreen mode

The processAction function in engine.ts acts as the state transition function. It takes the current state and an action (Plant, Water, Sabotage, Save) and returns the new state.


AI Decision-Making: Heuristic-Based Logic

The AI agents, Elder Bloom and Nightshade, use heuristic-based logic to make decisions. Their behavior is driven by their "personality" strings, which weight different actions.

Elder Bloom: Defensive Growth

Elder Bloom focuses on high-scoring trees and long-term growth. It also employs "defensive planting" by placing low-cost flowers around its trees to block the opponent from planting weeds.

// Defensive planting logic in /src/ai.ts
if (prefersTrees && water >= COSTS.plant + COSTS.water) {
  const myTrees = findCells(agentId, ['tree'], ['seed', 'sprout', 'mature', 'bloom']);
  for (const [tx, ty] of myTrees) {
    const neighbors = findEmptyNeighbors(tx, ty);
    if (neighbors.length > 0) {
      return { agentId, type: 'plant', target: neighbors[0], plantType: 'flower' };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Nightshade: Aggressive Sabotage

Nightshade is the opposite. It prioritizes sabotaging high-value opponent plants (Trees and Vines) by destroying them and replacing them with weeds.


High-Performance Rendering with Canvas

To ensure smooth performance, especially with a 10x10 grid and various animations, we used the HTML5 Canvas API for the GameGrid component.

The "Drought" Visual Effect

One of the most interesting visual features is the "Drought" effect. When both agents' water levels drop below 10 units, the grid subtly dims and a fog effect appears.

// From /src/components/GameGrid.tsx
if (isDrought) {
  // Subtle dimming
  ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Fog effect using a radial gradient
  const gradient = ctx.createRadialGradient(
    canvas.width / 2, canvas.height / 2, 50,
    canvas.width / 2, canvas.height / 2, canvas.width * 0.8
  );
  gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
  gradient.addColorStop(1, 'rgba(100, 100, 100, 0.2)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}
Enter fullscreen mode Exit fullscreen mode

State Management and Persistence

We used React's useState and useEffect hooks for local state management. For persistence, we implemented a Leaderboard component that stores battle results in LocalStorage.

Saving Results

// From /src/App.tsx
const saveResult = (finalState: GameState) => {
  const winner = finalState.scores.A > finalState.scores.B ? 'A' : 'B';
  const newResult: BattleResult = {
    id: Math.random().toString(36).substr(2, 9),
    date: new Date().toISOString(),
    scoreA: finalState.scores.A,
    scoreB: finalState.scores.B,
    nameA: agentConfigs.A.name,
    nameB: agentConfigs.B.name,
    winner,
    stats: finalState.stats
  };
  setResults([newResult, ...results]);
};
Enter fullscreen mode Exit fullscreen mode

Future Roadmap

  • Multiplayer Support: Enabling human vs. AI or human vs. human battles.
  • Dynamic Weather: Rain, heatwaves, and storms that affect water levels and plant growth.
  • Advanced AI: Integrating LLMs (like Gemini) to drive more complex and unpredictable agent behavior.

Botanical Battle demonstrates how simple game mechanics can lead to complex emergent behavior when combined with distinct AI personalities. By leveraging React and the Canvas API, we've created a performant and visually engaging simulation that's easy to extend and experiment with.

Github: https://github.com/harishkotra/botanical-battle

Top comments (0)