BINX FORGEALL YOU NEED IS AN IDEA.Support BINX ↗
PRACTICAL GUIDES & COMPARISONS · ORIGINAL SOURCE INCLUDED

Make Readable Enemy AI With a Small State Machine

Build idle, chase, attack, recovery and dead states before adding navigation or complex behaviour trees.

Actual Pulse Meadow gameplay: a playable BINX reference
Pulse Meadow · Actual gameplay · Related playable reference

Thumbnail: an actual related playable BINX demo. It is a gameplay reference, not evidence that a save, inventory, network or native reverse-engineering integration has been demonstrated.

Difficulty: beginner to intermediate. Prerequisites: basic variables, functions and a test copy of your project. Reference: Forge workshop 1.0, browser ES modules and Node 24 checks. Engine-specific translation is a separate integration.

What you will learn

Build idle, chase, attack, recovery and dead states before adding navigation or complex behaviour trees.

Download complete MIT workshop
  1. Extract into a new folder and run node check.mjs from that folder.
  2. For the browser games, run python3 -m http.server 8080 with Python 3 and open http://localhost:8080/index.html. Do not open modules with file://.
  3. Read the full setup instructions and the original code licence. Keep your existing game on a separate test branch.

The JavaScript behaviour checks and native C comparison can run independently of the browser. Ghidra UI, physical-phone and your engine integration are separate checks. This is a small reference, not a certification of your project.

Explore the related game idea · Start with Build a Game · Improve your existing game

Five practical steps

Make Readable Enemy AI With a Small State Machine: Write a state table → Separate the decision from movement → Give attacks a visible commitment
Original implementation diagram · not gameplay or a tool screenshot
  1. Write a state table

    Read enemyMode. Health zero wins over every other condition; recovery wins over attack; nearby targets can attack, farther targets chase, and distant enemies idle. Write the thresholds with units and explain what each state is allowed to do.

    Expected result: A five-state table with explicit priority.

    Check it: At zero health and distance zero, the result is dead, never attack.

    enemyModeObserve the inputs → check the state → compare the result
  2. Separate the decision from movement

    Call the pure decision function from one update owner. Movement and animation react to the returned state. Do not let an animation callback independently choose a different state; that creates contradictory health, movement and attack behaviour.

    Expected result: The same distance, health and cooldown produce the same state.

    Check it: Run cases just below and at 24 and 180; document that the comparisons are strict less-than.

  3. Give attacks a visible commitment

    Before dealing damage, add a wind-up timer and one damage event. Then enter recovery. The example only chooses a state; your game still needs collision, timing and damage ownership. A touching enemy should not damage once per rendered frame.

    Expected result: A player can see the attack coming and a hit occurs once.

    Check it: Stay overlapping for one attack cycle; health drops once, including at different refresh rates.

  4. Deal with boundaries

    Add hysteresis if chase/idle flickers at the detection edge, such as entering chase at 170 and leaving at 190. If walls block direct pursuit, reuse your engine navigation rather than inventing pathfinding first. Reset timers on death and respawn.

    Expected result: A stable transition and a corpse that cannot attack.

    Check it: Oscillate around a detection boundary, then kill the enemy during wind-up and verify no delayed hit.

  5. Tune one readable encounter

    Use one enemy, one obstacle and one player attack. Record detection range, wind-up, recovery and movement speed. Change one value at a time and compare the same encounter; behaviour-tree complexity is not evidence of better combat.

    Expected result: A small encounter with explainable decisions.

    Check it: Test pause, restart, missing target, target death and simultaneous enemy hits before adding a wave.

Read and reuse the actual code

systems.mjs: complete source · Standalone behavioural checks · Shared browser runner and input. Original BINX Forge code, MIT; the package includes every required file and its notice.

Inspect complete systems.mjs
// Original BINX Forge practice code. MIT; see LICENSE.txt.
export function readSave(raw) {
  const s = JSON.parse(raw);
  if (!s || s.version !== 1 || !Number.isInteger(s.coins) || s.coins < 0 || s.coins > 9999 || !['room-a','room-b'].includes(s.room)) throw Error('Invalid save');
  return {version:1, coins:s.coins, room:s.room};
}
export function transfer(from, to, id, count, capacity=10) {
  if (!Number.isInteger(count) || count < 1 || !Number.isInteger(from[id]) || from[id] < count || Object.values(to).reduce((a,b)=>a+b,0)+count > capacity) return false;
  from[id]-=count; to[id]=(to[id]||0)+count; return true;
}
export function pointerInput() {
  const owners=new Map();
  return {press:(id,action)=>owners.set(id,action),release:id=>owners.delete(id),clear:()=>owners.clear(),held:action=>[...owners.values()].includes(action)};
}
export function enemyMode(distance, hp, cooldown) {
  if (hp<=0) return 'dead';
  if (cooldown>0) return 'recover';
  if (distance<24) return 'attack';
  return distance<180?'chase':'idle';
}
export function frameSummary(samples) {
  const s=samples.filter(Number.isFinite).filter(x=>x>=0).sort((a,b)=>a-b);
  if (!s.length) throw Error('No samples');
  return {median:s[Math.floor((s.length-1)*.5)],p95:s[Math.ceil(s.length*.95)-1],count:s.length};
}
export function fixedStep(clock, elapsed, update) {
  clock.carry+=Math.max(0,Math.min(.1,elapsed));
  while(clock.carry>=1/60){update(1/60);clock.carry-=1/60;}
}
// Owned protocol specimen: version:u8, coins:u16 little-endian, room:u8.
export function decodeRecord(bytes) {
  if(bytes.length!==4)throw Error('Expected four bytes');
  const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
  if(v.getUint8(0)!==1||v.getUint8(3)>1)throw Error('Unknown record');
  return {version:1,coins:v.getUint16(1,true),room:v.getUint8(3)};
}
export function applyInput(player, packet) {
  if(!packet||!Number.isInteger(packet.seq)||packet.seq<=player.seq||![-1,0,1].includes(packet.dx))return false;
  player.seq=packet.seq;player.x=Math.max(0,Math.min(100,player.x+packet.dx*2));return true;
}
export function jumpTrace(speed=300,gravity=900,dt=1/120) {
  let y=0,vy=-speed,t=0,peak=0;
  const rows=[{t,y,vy}];
  while(t<3){vy+=gravity*dt;y+=vy*dt;t+=dt;peak=Math.min(peak,y);rows.push({t,y,vy});if(y>=0)break;}
  return {rows,height:-peak,airtime:t};
}

Code rights: the included MIT notice permits use, modification and distribution, including commercial games and source products, with the copyright and licence notice. Added third-party files have their own terms. Read the official licence & usage terms.

Common failures and fixes

The enemy attacks every frame

Own damage in a one-shot transition and start recovery after it fires.

Idle/chase flickers

Use different enter and exit distances or a bounded state duration.

Dead enemies still damage

Death has highest priority; cancel pending wind-up and damage events.

Make Your Game Better

Change one system after the baseline works. Keep the free reference and compare the same inputs before and after.

Optional building blocks from original creators

Inspect exact formats and editions on each resource page. Pay for a resource only when it fills an identified gap; the free code exercise needs no paid engine or pack.

Licensing summary, not legal advice. Before publishing or selling, check exact terms for finished-game use, reselling files and including files in a source/template product separately.

Learn at the original sources

Sources checked 11 October 2026. The numbered plan is original Forge instruction; linked documentation does not imply every engine or device has been tested.

Take this guide to your AI

Review and copy into your assistant. Nothing is sent automatically. These prompts include the complete focus source, full-package links, checks and compatibility limits.

Review a prompt or select its text manually.

Review build prompt
Review debug prompt
Review upgrade prompt

Read the complete guide and prompts as Markdown →

Keep learning

Browse every guide and recipe →