I added an "auto-play" mode to my browser farming game — an AI agent that plants seeds, waters crops, harvests when ripe, and sells produce at the shop, all without the player touching the keyboard. You don't need machine learning for this. A well-designed finite state machine combined with utility scoring produces behavior that looks genuinely smart. Here is the design.
What "Game AI" Usually Means
Game AI is rarely neural networks. It's mostly decision-making logic that picks a sensible action given the current world state, cheaply enough to run every frame. The two workhorse patterns are finite state machines and utility systems, and they compose beautifully.
The Finite State Machine
At the top level the agent is always in exactly one state. Each state knows what to do and when to hand off to another:
const States = {
IDLE: 'idle',
PLANT: 'plant',
WATER: 'water',
HARVEST: 'harvest',
SELL: 'sell',
};
class FarmAgent {
constructor(game) {
this.game = game;
this.state = States.IDLE;
}
tick() {
switch (this.state) {
case States.IDLE: return this.decide();
case States.PLANT: return this.doPlant();
case States.WATER: return this.doWater();
case States.HARVEST: return this.doHarvest();
case States.SELL: return this.doSell();
}
}
}
The FSM keeps behavior readable: at any moment you know precisely what the agent is doing and which transitions are legal. No tangled web of booleans.
The Decision Function: Utility Scoring
A pure FSM gets rigid if transitions are hardcoded. The trick is to make the IDLE state score each possible action and pick the highest. Each action gets a number based on the world state:
decide() {
const scores = {
[States.HARVEST]: this.scoreHarvest(),
[States.SELL]: this.scoreSell(),
[States.WATER]: this.scoreWater(),
[States.PLANT]: this.scorePlant(),
};
// pick the action with the highest utility
const best = Object.entries(scores)
.sort((a, b) => b[1] - a[1])[0];
this.state = best[1] > 0 ? best[0] : States.IDLE;
}
Now priorities emerge from the numbers instead of from rigid if/else chains. Tuning the agent means tuning the scoring functions.
Designing Good Utility Functions
This is where the craft is. Each score should reflect both how valuable and how urgent an action is:
scoreHarvest() {
const ripe = this.game.crops.filter(c => c.isRipe).length;
return ripe * 100; // ripe crops are top priority
}
scoreWater() {
const dry = this.game.crops.filter(c => c.isDry).length;
return dry * 40; // important, but less than harvest
}
scoreSell() {
// only worth it once inventory is worth the trip
return this.game.inventoryValue > 50
? this.game.inventoryValue * 0.5 : 0;
}
scorePlant() {
const empty = this.game.emptyPlots.length;
// don't plant if we can't afford seeds
return this.game.coins >= this.game.seedCost
? empty * 20 : 0;
}
The constants (100, 40, 20) encode the agent's "personality." Bump harvesting higher and it becomes greedy about ripe crops; raise planting and it expands aggressively. A few well-chosen multipliers replace dozens of conditional rules.
Acting Over Time, Not Instantly
A real agent shouldn't teleport through tasks — it should move and act at a human-ish pace so the player can watch. I drive the agent from the game loop with a small cooldown so each action takes a beat:
let cooldown = 0;
function gameLoop(dt) {
cooldown -= dt;
if (cooldown <= 0 && agent.enabled) {
agent.tick();
cooldown = 0.4; // act ~2.5 times per second
}
// ...render
}
This single line transformed it from a flickering glitch-machine into something that looks deliberate and alive.
Avoiding Common Pitfalls
- Oscillation. If two actions have nearly equal scores the agent can flip-flop between them. Add a small "stickiness" bonus to the current state so it commits.
- Deadlock. Always make sure some action can score above zero, or include a safe fallback to
IDLE, so the agent never freezes. - Unreadable tuning. Keep all the magic numbers in one config object, not scattered through the scoring functions, so balancing is a single screen.
Why This Approach Wins
A utility-driven FSM is debuggable (log the scores and you see exactly why a choice was made), tunable (designers adjust numbers, not code), and cheap (a few comparisons per tick). It scales from a farming bot to enemy AI, companion NPCs, or city-builder citizens. You reach for learning-based AI only when the action space is too large to score by hand — which, for most browser games, it never is.
You can watch the agent run in the farming game on the games page — flip on auto-play and let it tend the farm.