A route planner can compute one good path and still make a bad bot if it treats every read of the same target position as a fresh command. The difference between “the enemy is at this tile” and “the enemy just moved to this tile” is the beginning of an event-driven design.
Section: Polling is not the same as reacting.
An observer might read the same fact twenty times per second:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
A responsive system should usually produce one event:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The event says what changed, while a snapshot says what is true now. Keeping both ideas separate prevents a bot from performing the same action on every poll.
A responsive loop should turn noisy polls into one deliberate action in stages, rather than connecting every poll directly to a click:
Validation protects the model, debouncing protects it from flicker, and the cooldown prevents a valid event from producing frantic repeated input.
Section: Level facts and edge facts answer different questions.
A snapshot contains level facts: “the menu is open” or “gold equals 145.” An event describes an edge between two accepted snapshots: “the menu opened” or “gold increased by 25.”
That difference controls behavior. A level-triggered check such as if menu_open { click() } may click on every poll. A transition-triggered handler acts only on the transition from closed to open. Neither form is always better:
use a level fact to keep a safety condition true, such as “stop while the target is absent”;
use an edge event for one-time work, such as recording that a turn began;
use both when an action needs a starting event and an ongoing confirmation.
Here are six polls of menu_open and what each kind of check does with them:
A held button has the same distinction. Suppose each input check toggles a lamp when it fires. Checking the pressed state fires on every held sample; detecting a new press fires only when released becomes pressed:

Events are interpretations, not raw truth. If the observer misses snapshots, 120 to 145 proves the endpoints but not whether the game passed through 130. Name an event GoldChanged, not GoldEarned, unless something else shows why it changed.
Code note: After reporting a change, replace the saved baseline with the newly accepted snapshot. Comparing the next identical snapshot with that baseline emits no second event.
In brief: A snapshot says what is true now, and an event says what changed. Acting on the change, not on every poll, stops repeated actions.
Section: Diff two validated snapshots.
Compare two snapshots only after each has passed its own read and identity checks. If gold changes from 120 to 145 while menu_open changes from false to true, diff returns both GoldChanged { before: 120, after: 145 } and MenuOpened. Repeated identical snapshots return an empty vector, so the bot has no new event to act on.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Why use an enum? Each variant defines exactly which data belongs to that event. A typo in a string such as "turn_chagned" cannot silently create a new event type.
Follow the two accepted snapshots through diff, then compare an unchanged poll. The endpoints reveal changes, not the reason gold changed:
Section: Debouncing asks for stability.
Some values flicker while a game changes scenes. Debouncing waits for the same candidate value to appear several times before accepting it.
It helps to know why a value flickers, because that tells you how long to wait. During a scene change the game is often rebuilding the very object you are reading. The pointer is briefly null, or it points at a freshly allocated record whose fields have not been filled in yet. Your read succeeds and hands back a real number that was never a real game state:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The strange value on poll 4 is the byte hexadecimal C D, which Microsoft’s debug heap writes into every fresh allocation, repeated four times. One hexadecimal C D is 12 times 16 + 13 equals 205, so the four bytes read as 205 times 256 to the power of 3 + 205 times 256 to the power of 2 + 205 times 256 + 205 equals the large number shown.
A debouncer keeps two things: the last accepted value, and a candidate value with a count. Each poll does one of three things. A poll equal to the accepted value clears the candidate. A poll equal to the candidate adds one to its count. Any other value becomes a new candidate with a count of 1. When the candidate’s count reaches the required number, it becomes the accepted value and the debouncer reports one change.
Alternative: Think of a lift button that must be pressed steadily before it counts: one flicker of contact is ignored. Debouncing asks for the same steady agreement.
With three required polls, here is the trace above, followed by 20 points of real damage that take health from 100 to 100 minus 20 equals 80:
The code follows those three cases:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Debouncing buys correctness with latency, and the cost is easy to work out. At twenty polls per second, polls are 1000 divided by 20 equals 50 milliseconds apart. The change is accepted on the third poll that shows it, which comes two gaps, 2 times 50 equals 100 milliseconds, after the first. That first poll can itself arrive up to 50 milliseconds after the real change, so every genuine change reaches the bot 100 to 150 milliseconds late. “Three polls” means nothing without the polling rate: at 100 polls per second the gaps are 10 milliseconds, and the same three-poll debounce adds only 20 to 30 milliseconds.
That delay is cheap for “the match ended” and far too slow for “an enemy appeared.” Choose the requirement per value rather than once for the whole bot, and use debouncing for noisy or safety-critical transitions, not for every coordinate in a smooth animation.
Section: Add cooldowns after actions.
After requesting “end turn,” wait until a snapshot shows that the turn changed. Do not send the same input again merely because the next 50 milliseconds poll still shows the old turn.
Narration: A game takes a moment to react, so polls just after a click may still show the old state. Without a pending record, the bot would click again and again.
A useful action record contains:
the requested action;
the time it was requested;
the state change that will confirm success;
a timeout;
a cancellation reason.
The pending action is a small state machine of its own. While it exists, the bot sends nothing, and each poll either confirms it, counts it down, or times it out:
The timeout is a number of polls, so it becomes a time through the polling rate: starting from polls_left: 20 at one poll every 50 milliseconds gives the game 20 times 50 equals 1000 milliseconds, one second, to show the new turn.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
In a full program, distinguish confirmed success from timeout in the result. The short sample focuses on the stop condition.
Section: A safe responsive loop.
Copy and validate a snapshot.
Diff it against the previous accepted snapshot.
Debounce noisy state.
Feed events to the state machine.
Propose at most one bounded action.
Wait for its confirmation or timeout.
Stop immediately if the target disappears or the user cancels.
This design is useful far beyond game automation. File watchers, user interfaces, network clients, and monitoring tools all turn repeated polls into meaningful events while controlling duplicate work.
Make repeated requests idempotent when you can: sending the same request twice should have the same final effect as sending it once. “Select entity 7” can be idempotent; “move selection to the next entity” is not. Idempotence does not replace cooldowns, but it limits damage when a timeout hides a successful first action.