Game AI
Behavior trees, finite state machines, pathfinding, and steering behaviors. You'll hit this when enemies get stuck on corners, AI decisions feel robotic, or adding a new behavior means rewriting the entire decision tree.
function updateEnemy(enemy: Enemy, player: Player) {
if (enemy.health < 20) {
if (distanceTo(player) < 100) {
flee(enemy, player);
} else {
heal(enemy);
}
} else if (distanceTo(player) < 50) {
if (enemy.ammo > 0) {
shoot(enemy, player);
} else {
melee(enemy, player);
}
} else if (distanceTo(player) < 200) {
chase(enemy, player);
} else {
patrol(enemy);
}
}function updateEnemy(enemy: Enemy, player: Player) {
if (enemy.health < 20) {
if (distanceTo(player) < 100) {
flee(enemy, player);
} else {
heal(enemy);
}
} else if (distanceTo(player) < 50) {
if (enemy.ammo > 0) {
shoot(enemy, player);
} else {
melee(enemy, player);
}
} else if (distanceTo(player) < 200) {
chase(enemy, player);
} else {
patrol(enemy);
}
}const enemyBehavior = selector([
sequence([
condition((e) => e.health < 20),
selector([
sequence([
condition((e, p) => distanceTo(e, p) < 100),
action(flee),
]),
action(heal),
]),
]),
sequence([
condition((e, p) => distanceTo(e, p) < 50),
selector([
sequence([
condition((e) => e.ammo > 0),
action(shoot),
]),
action(melee),
]),
]),
sequence([
condition((e, p) => distanceTo(e, p) < 200),
action(chase),
]),
action(patrol),
]);const enemyBehavior = selector([
sequence([
condition((e) => e.health < 20),
selector([
sequence([
condition((e, p) => distanceTo(e, p) < 100),
action(flee),
]),
action(heal),
]),
]),
sequence([
condition((e, p) => distanceTo(e, p) < 50),
selector([
sequence([
condition((e) => e.ammo > 0),
action(shoot),
]),
action(melee),
]),
]),
sequence([
condition((e, p) => distanceTo(e, p) < 200),
action(chase),
]),
action(patrol),
]);Nested if/else chains encode the decision tree implicitly in control flow. Adding a new behavior means finding the right nesting level and carefully inserting conditions without breaking existing branches. The structure is invisible to debugging tools, and two developers editing the same function will almost certainly cause merge conflicts.
A behavior tree makes the decision hierarchy explicit and composable. Each node is a reusable building block: selectors try children until one succeeds, sequences run children in order. Adding 'take cover' behavior means inserting a node, not restructuring nested if/else. Trees can be serialized, visualized in debug tools, and edited by designers.
Seek behavior (no deceleration)
Arrive behavior (slowing radius)
Pure seek always applies maximum steering force toward the target regardless of distance. When the agent reaches the target it is still at full speed, so it overshoots, turns around, overshoots again, and oscillates indefinitely. Clamping speed near the target is a hack that produces abrupt stops. The arrive behavior solves this naturally.
Arrive behavior scales the steering force based on distance to the target. Outside the slowing radius it behaves like seek, but inside it reduces desired speed proportionally, causing the agent to decelerate smoothly to a stop. This is the standard approach for any AI that needs to reach a specific position, from RTS unit movement to NPC navigation.
Manhattan distance heuristic
Octile distance heuristic
Manhattan distance assumes only 4-directional movement. On an 8-directional grid, it overestimates the cost of diagonal paths, which can cause A* to explore unnecessary nodes and return suboptimal paths if the implementation does not handle inadmissible heuristics. It works, but it is not the right tool for the grid.
Octile distance is the correct heuristic for 8-directional grids because it accounts for diagonal moves costing sqrt(2) instead of 1. It never overestimates, so A* remains optimal while exploring fewer nodes than a looser heuristic. The formula takes min(dx, dy) diagonal steps and the remainder as cardinal steps.