Blundermind — Bot Controls

Technical Brief
Move-selection mechanics, formulas & architecture

This brief explains what happens under the hood every time a Blundermind bot chooses a move: the pipeline it runs, the formulas each control uses, and how the pieces fit together. Everything described here maps to a control you can see and set in the Bot Controls panel.

1 · Architecture — Move Selection Pipeline

Every bot move passes through the sequential stages below. Each stage can veto, modify, or short-circuit to skip later stages. The stages run in strict order so that think time — computed in Stage 2 — feeds every downstream degradation calculation, and so the final engine-verification stages (8–9) see the move the personality actually chose.

StageNameWhat happens
1Opening BookECO table lookup (preferred) or Lichess Masters (mainline). If a matching move is found, it is played immediately after a brief book delay (400–1200 ms). Skipped once the game leaves known lines.
2Think TimeActual think time for this move is computed from the timing mode, the Hustle attractor, and clock pressure. The result feeds all downstream degradation curves.
3Engine QueryA rough think-time estimate picks the Maia ELO (via curve A, if ELO degradation is on). Maia — or, while the Lichess Opening Book is on and the game is still in it, the Lichess explorer — is queried for candidate-move probabilities. A Flounder bot takes its turn through Flounder instead (§2c).
4Distribution FilterAn absolute min-probability floor removes near-zero candidates. Curve B (if the distribution toggle is on) narrows the upper percentile; the Luck attractor shifts the window (§8).
5Attractor ReweightingEach candidate move's probability is multiplied by exp(logBoost), where logBoost is the sum of all active strategic attractor signals. Sets an "applied" flag read by Stage 8.
6Desperation (opt.)If "seek stalemate when losing" is armed and the engine eval confirms a lost position, candidate weights are re-shaped toward low-own-mobility and material-dumping moves (§10).
7Conviction PickThe Conviction share of moves (0% with no personality; choosing a personality sets 100%): the top-ranked move of the reweighted distribution is played outright (argmax). The rest: final temperature applied (base T × phase/pressure modifiers) and one move is sampled.
8CP-Budget AcceptanceMaia personality bots only: if the pick differs from Maia's most-popular move, a shallow Stockfish probe checks that the pick loses ≤ CP Budget vs. the popular move; otherwise it walks down the preference order, up to 15 candidates deep (§9).
9Degradation GuardUnder Bad Day or time pressure only: a pick that evaluates better than Maia's top move is swapped back — degrading never turns up a brilliancy (§9).
10Hard Floor BackstopAny final pick that differs from the most-popular move — whatever produced it — is capped at the Hard Floor (≥ Budget): if it loses more than Floor cp, the popular move is played instead. Skipped when the Floor is Off (§9c).

Flounder bots run Stages 1–3 and 6 as above. Flounder then picks by Stockfish's evaluation (§2c), with the attractors choosing inside a band the CP Budget sets, and Stages 4 and 7–10 do not apply: they exist because Maia's probabilities are popularity rather than quality, and a Flounder move is chosen from the evaluations those stages would check it against.

Principle #1 — think time drives degradation: Think time is computed first (Stage 2), and the resulting value drives Stages 3–4 through the degradation curves. A Coffeehouse Hustler thinking 0.3 s on a move sees heavy ELO and distribution degradation on that specific move — not an average derived from the remaining clock.
Principle #2 — real centipawns, not probability: Maia probability measures how popular a move is at a given rating, which is not the same as how good it is — at low ratings the two can even anticorrelate. So on Maia the CP Budget, the Hard Floor, and all quality guarantees are enforced with actual Stockfish evaluations at Stages 8–10. The probes are fail-open: any timeout leaves the pick untouched, and results are reused between stages so a move never pays for the same evaluation twice. Flounder starts from the other end — every move it considers already carries a Stockfish evaluation — which is why it needs no after-the-fact check.

2 · Move Source Layer

2a · Opening Book

Two independent book modes are available. Either can be active at once; each deactivates permanently the first time it fails to find a candidate move.

ModeSourceBehaviour
PreferredIn-memory ECO table (~3,000 named openings)Matches game history against ECO lines whose ECO code or family prefix is in the bot's slot list. Plays the next move from the highest-priority matching line. Exits the book at maxBookDepth (default 20 half-moves).
MainlineLichess Masters (live)Queries the masters database for the most popular continuation from the current position; falls back to the Lichess community database if there is no masters result. Plays the top-weighted move. Deactivates after the first cache miss or API error.

Book delay: 400–1200 ms random pause to simulate human reading time.

2b · Maia Neural Network

Maia is a series of neural networks (600–2600 ELO) trained to predict human moves at each rating level. The bot queries the appropriate ELO model (determined by degradation curve A) and receives a probability distribution over all legal moves.

2c · Flounder (built on Stockfish)

Used when the engine card is set to Flounder, and wherever a Maia bot has no model to run (Maia not downloaded): those play Flounder at the same rating. Flounder is not a separate engine and evaluates nothing itself — Stockfish 18 lite, in a Web Worker, does all the chess. Flounder is the selection layer that decides which of Stockfish's evaluated moves to play, so that strength is set in ELO rather than Stockfish's skill levels (a skill level is not a rating: levels −3 through 2 measured as the same bot).

one MultiPV Stockfish search scores every legal move g(v) = sign(v) · ln(1 + |v| / 100) # perceptual scale, applied to evals first cost(m) = g(best) − g(m) # so errors in decided positions cost less target ~ Weibull(shape c, scale s) # Regan & Haworth's human-error curve play = the legal move whose cost is nearest target (overshoot cap: never more than 0.5 scaled units past the target) c(elo) = Regan's fitted shape, closed form # sets how heavy the tail of mistakes is s(elo) = interpolated in ln s between ten measured rungs, 600–2387 ELO each rung measured by playing Flounder against Maia at the same rating dial = ELO 600–2400, step 25

How the other controls reach it:

Flounder is not a Stockfish release and is not endorsed by or affiliated with the Stockfish project; full attribution and the method's sources are on the credits page.

2d · Lichess Opening Book (in front of either engine)

A switch, not an engine: it puts the Lichess explorer in front of Maia or Flounder. While the position is still in the explorer at the book's rating, the bot treats how often real players of that rating chose each move as its probabilities and samples from them (temperature applies); the first time the explorer has nothing, the book is off for the rest of the game and the engine plays. The book's rating is pinned to the Elometer by default, or set on its own. If the engine behind it is Maia and the model is not downloaded, the handover goes to Flounder at the same rating.

3 · Think Time Calculation

Think time is calculated before the engine is queried. The result drives all downstream degradation curves, so attractors that shorten think time (the Hustler) directly increase move-quality degradation for that specific move.

3a · Timing Modes

ModeFormula / behaviour
InstantReturns 0 ms. No delay whatsoever.
FixedthinkMs = fixed delay. Capped at min(fixed, clockMs × 0.08) when the clock is under 30 s. Capped at clockMs − 3 s when flagging is disabled.
Pace (default)baseSec = (5 × 60) / pace. complexity = min(1 + entropy × 0.35, 2.5). thinkMs = baseSec × complexity × 1000. Pace slider: 5–120 (moves per 5-minute equivalent).
Complexitycplx = Stockfish complexity score (spread of its top evaluations) if available, else min(1, entropy / 4). mult rises from cplxMin at cplx 0 through 1× at cplx 0.5 to cplxMax at cplx 1, so a typical position costs the base. thinkMs = base × mult × 1000, where base is either the fixed Base slider or, on the clock-based plan (the panel default for Complexity), (clock + movesLeft × increment) / movesLeft with movesLeft = max(10, 60 or 40 − moves played). On the clock plan, book moves take 0.4–1.2 s up to about 8-minute games and 5–15 s from about 20 minutes, scaled in between.
Mirroravg = rolling average of human move times. thinkMs = avg × jitter(0.8–1.2) × (1 + mirrorOffset%/100). Falls through to Complexity/Pace when no human moves are recorded yet.

3b · Think-Time Modifiers (applied in order)

ModifierConditionEffect
Premove firesA premove is armed and still legal after the human's movePlays instantly — botThinkTime() is never called. See 3c.
Weaponizer shortcutEnabled AND the opponent's clock ≤ botWeaponizerTriggerMs (default 15 s)Return botWeaponizerMinMs (default 0 = instant). Absolute time remaining, not a lead.
Move blinkBlink flag AND entropy < 0.5 (forced/obvious positions)200–500 ms random; skips further calculation — including the timing mode
Clock-pressure mirroringClock-mirror flag AND oppClock < botClock × 0.6thinkMs ×= 0.5
Hustle attractorvalue v ∈ [−5, +5]thinkMs ×= (1 − v × 0.15). At +5: ×0.25 (very fast). At −5: ×1.75 (very slow).
ReconsiderationReconsider flag, 15% random chancethinkMs ×= 1.5 to 2.5 (hesitation pause)
Base jitterAlways appliedthinkMs ×= uniform(0.8, 1.2)
Clock cap (low clock)clockMs < 30,000 msthinkMs = min(thinkMs, clockMs × 0.08)
Flagging guardCan-flag = falsethinkMs = min(thinkMs, max(200, clockMs − 3000))
Global capAlways appliedthinkMs = max(200, min(cap, thinkMs)). Cap = max(6 s, min(45 s, startClock × 0.02)) — scales with time control.

Position entropy is the Shannon entropy over Maia move probabilities: H = −Σ p·log(p). Higher entropy = more complex / branching position.

Precedence matters. The table above is evaluated top-down and the first match wins, so Move blink is checked before the timing mode. Leaving blink enabled alongside Fixed interval therefore lets any near-forced position ignore the duration the user set — a bot configured for 22 s still answers obvious recaptures in ~0.3 s. Selecting Fixed interval in the panel therefore switches blink off and marks the row with the reason; re-enabling it by hand is respected.

3c · Premove Simulation

Human speed-chess players premove: they queue a reply before the opponent has moved, and it fires the instant the opponent's move lands. It buys seconds but commits blind — which is what makes it both realistic and exploitable. The bot reproduces this, so players can practise baiting a premove and punishing it.

Arming runs in botPremoveArm() immediately after the bot's own move, while the human is on move. It is fully asynchronous and never blocks input; a human move made mid-computation discards the result through a generation guard (_botPremoveGen). Two Maia passes:

PassQuestionGate
1What will the human play from the current position?Top-move confidence must be ≥ botPremoveMinPct, else no commitment
2Apply that predicted move; what should the bot play in the resulting position?That answer is locked in as the premove

When the human moves, botPremoveTryFire() tests the locked move against the real board:

SettingDefaultBehaviour
botPremoveEnabledfalseMaster switch. Requires the Maia 3 model — the prediction runs on it.
botPremoveMinPct85Minimum Maia confidence in the predicted human reply. The setting that decides whether the bot reads as fast or as broken. 85% ≈ near-forced replies only, matching real play. Far below ~60% it premoves into positions no human would, and play looks erratic.
botPremoveRatePct80Percentage of the moves that already cleared the confidence bar — not a percentage of all moves.
botPremoveOnlyLowClockfalseOptional master gate restricting premoving to time scrambles.
botPremoveOppClockSecs30Trigger: opponent at or below this. The aggressive reason humans premove — keep their clock burning and play for the flag. 0 = off.
botPremoveClockSecs30Trigger: the bot itself at or below this, out of its own desperation. 0 = off.
botPremoveBustDelayMs2000The "didn't see that coming" stall after a bust.

The two clock triggers are independent — either one alone arms a premove. Both clocks read null in untimed games, so the gate simply never opens there. Per-game telemetry (armed / fired / busted) is surfaced live in the panel, and premove state is shown in the collapsed Move Timing header, because a bot replying instantly looks like a malfunction if you don't know the setting is on.

The predictor uses the bot's own Maia rating band to guess the human's move, since that is the only band available to it. This is realistic — a human premoving is also guessing from their own understanding — but it does mean a 2400 bot anticipates a 900's replies as though they were a 2400's.

4 · Time-Pressure Play Degradation

Two independent, user-editable spline curves control how the bot's play quality degrades as think time per move decreases. Curve A's control points are seeded on a closed-form time–rating model (below); curve B's on a sigmoid. Both use log-scale interpolation on the X axis (think time in seconds), so visually equal spacing on the panel matches the mathematical interpolation.

Each curve has its own ON/OFF toggle beside its chart, so the two mechanisms are fully independent: ELO degradation (curve A) reduces the Maia ELO the engine is queried at, while distribution restriction (curve B) keeps the ELO fixed but narrows which slice of Maia's move distribution is available. You can run either alone, both together, or switch both off for constant strength. When a curve is off it is flat at its reference value (curve A at the configured ELO, curve B at 100%), and its slider no longer re-anchors the other curve.

Curve interpolation (log-x space): pts sorted by x. For xSec in [pts[i].x, pts[i+1].x]: t = (ln(xSec) − ln(pts[i].x)) / (ln(pts[i+1].x) − ln(pts[i].x)) y = pts[i].y + t × (pts[i+1].y − pts[i].y)

Curve A — ELO Degradation

Maps think time (s) → effective Maia ELO. At the time control's nominal pace the bot queries its configured ELO. As think time shrinks, the ELO drops, producing weaker move distributions.

Default: the Regan time–rating model — a power-law fit of IM Kenneth Regan's rating-loss vs time-control data, from his Perpetual Chess Podcast interview with Ben Johnson. Regan's x-axis (total minutes for a 60-move game) is numerically identical to seconds per move, so the anchor uses EXPECTED_MOVES = 60:

D(s) = 339 − 1442 · s^(−0.283) // rating loss, ≈0 at 165 s/move anchorSec = baseSec / 60 + incrementSec // 15+0 → 15, 5+3 → 8; untimed → flat curve floor = max(600, E₀ − maxDrop) // Max ELO drop slider caps the fall seed(s) = clamp(E₀ + D(s) − D(anchorSec), floor, min(E₀, 2600))

The rough think-time estimate (computed before the Maia query) is used for ELO selection. The precise think time (after the engine responds) is used for curve B and temperature.

Hybrid Maia slots carry their own ratings, so the curve's absolute ELO (anchored to the main Elometer) doesn't apply directly. Instead each slot takes the curve's relative drop — the spline's relaxed top minus its value at this think time — subtracted from the slot's own ELO. Relaxed think = no drop; under pressure all slots degrade by the same amount, preserving their identity gap.

Curve B — Distribution Cutoff

Maps think time (s) → upper-percentile cutoff of the candidate list. 100% = full distribution (top candidate down to last). As think time falls the cutoff drops, forcing the bot to pick from a narrower top slice of the distribution (higher-probability, safer-looking moves).

effectiveUpperPct = min(botDayUpper, evalPressureCurve(curveB, thinkSec))

Time-Pressure Temperature Boost

In addition to the ELO and distribution effects, low think time raises the sampling temperature, making move selection more random under pressure:

boost = { steady: 0.0, normal: 1.0, panicky: 2.5 }[timePressureMode] if curveB present: fraction = 1 − evalPressureCurve(curveB, thinkSec) / 100 else: fraction = max(0, 1 − thinkSec / 30) # linear fallback effectiveTemp = baseTemp + fraction × boost

Time-pressure modes: "steady" (no boost), "normal" (+1.0 max), "panicky" (+2.5 max).

5 · Temperature & Move Sampling

After attractor reweighting, the conviction pick decides how the move is chosen: on the Conviction share of moves (one value with two linked sliders — under the Centipawmeter's Budget and under the Engine section's distribution chart — 0–100%; 0% with no personality, set to 100% when a personality is chosen) the bot plays the top-ranked move of the reweighted distribution outright (argmax — its personality's honest first choice); the rest are sampled using temperature-scaled probabilities. Raising it makes a personality more assertive; 100% makes it perfectly predictable. Conviction 0% with Temperature 1.0 and no personality samples Maia's distribution unaltered. The chart's last bar ("Played") draws the mix. Bots saved before the slider existed load at 30%, the fixed share they were built with.

if rand() < conviction → play argmax of reweighted distribution else: scaled[m] = prob[m] ^ (1/T) total = Σ scaled[m] sample r ~ Uniform(0, total) return first m where cumulative sum ≥ r

At T = 1.0 the original Maia probabilities are used directly. T < 1 sharpens the distribution (top move more likely); T > 1 flattens it (unlikely moves become relatively more likely). T is clamped to ≥ 0.1 to prevent division issues.

5a · Base Temperature Presets

PresetT valueBehaviour
Focused0.4Strongly favours the top 1–2 Maia moves
Neutral1.0Samples Maia's full distribution as-is
Varied1.8Wider sample — more surprise choices
Wild3.0Even low-probability moves get meaningful weight

5b · Hustler Phase Temperature

When the Coffeehouse Hustler personality is active, the base temperature is replaced by a phase-sensitive curve:

piecesLeft = count of non-pawn, non-king pieces on board fraction = clamp(1 − (piecesLeft − 4) / 10, 0, 1) # 0 at 14 pieces (opening), 1 at ≤4 pieces (endgame) T = 5.0 + fraction × (0.6 − 5.0) = 5.0 in opening → 0.6 in endgame

14 non-pawn/non-king pieces = full material. At 4 or fewer such pieces, the endgame temperature floor of 0.6 is reached.

5c · Complexity-Adjusted Temperature

The base temperature can optionally be scaled by position complexity (the spread of Stockfish's top evaluations). On Maia it scales the exponent directly; on Flounder the adjusted temperature then becomes the Weibull shape (§2c):

if complexityScore is null → return baseT cplxFactor = 0.8 + complexityScore × 0.4 # 0.8 at simple, 1.2 at complex return baseT × cplxFactor

The complexity score is the normalized standard deviation of the top-N Stockfish evaluations, ranging 0 (all moves equal) to 1 (widely spread evaluations).

6 · Strategic Attractors (Centipawmeter)

Strategic attractors reweight each candidate move's probability by multiplying it by exp(logBoost). The total logBoost for a move is the sum of all active attractor contributions. Attractors are zero at centre and scale linearly with slider value (−5 to +5).

6a · CP Budget, Hard Floor and Scale Factor

The CP (centipawn) budget (0–300), shown on the Centipawmeter dial, is the personality's allowance and does double duty. First, it is a master gain knob: it controls how strongly attractors push the distribution, regardless of the individual slider positions (the 150-cp-per-log-unit scale below). Second, it is a real, engine-calculated centipawn ceiling on how far a personality pick may fall below Maia's most-popular choice — enforced at Stage 8 (§9a).

The Hard Floor (Budget–1000 cp, or Off) is a separate slider directly beneath the Budget, always ≥ Budget: it is the absolute backstop on every other selection mechanism — temperature sampling, the Luck window shift, Bad Day, curve-B time-pressure restriction — enforced at Stage 10 (§9c). At Off, nothing is capped. The Engine panel shows both values beside the Elometer with a one-click ELO-based recommendation. Budget and Floor have separate curves: Budget ≈ (2700 − ELO)/15 clamped to 10–120 cp (persistent style spend — a fraction of the rating's typical centipawn loss), Floor ≈ (2900 − ELO)/4.5 clamped to 80–520 cp (the worst single move still in character for the rating).

totalAbs = Σ |v| for all attractor and piece sliders scale = cpBudget / (totalAbs × 150) if cpBudget > 0 and totalAbs > 0 = 0 otherwise For each move: prob_new = prob × exp(logBoost) where logBoost = Σ (attractor_v × scale × signal) At cpBudget = 150 with one slider at 1 and all others at 0: scale = 1/150, signal = 1 → logBoost = 1 → exp(1) ≈ 2.7× boost

Allocating 150 cp of budget on a single maximally-set attractor produces roughly a 2.7× probability boost on moves that fully satisfy that attractor.

6b · Attractor Formulas

AttractorLeft (−) / Right (+)Signal formulaNotes
Chaos Agent / Simplifier− Simplifier → low-σ moves
+ Chaos Agent → high-σ moves
logBoost += chaosVal × scale × signal (signal from MultiPV σ)Requires a MultiPV Stockfish probe. σ = std. dev. of top-N eval scores.
Complicate / Simplify winning− Simplify when winning
+ Complicate when winning
Active when eval ≥ +100 cp. Boosts moves that increase or decrease MultiPV variance.Only fires in clearly winning positions. Combines with Chaos Agent for full effect.
Space Cadet / Space Waster− Space Waster → cede space
+ Space Cadet → reduce weak sq.
delta = currentWeakSq − simWeakSq
logBoost += v × scale × tanh(delta/5)
Full-board attack map on the simulated position. Captures discovered attacks and blocking moves. delta > 0 = fewer weak squares after the move.
Fort Knox / Glass Cannon− Glass Cannon → exposed pieces
+ Fort Knox → more defenders
delta = simTotalDefs − currentDefs
logBoost += v × scale × tanh(delta/3)
Sum of defensive coverage across all bot pieces, computed on the simulated position.
Gambito / Gambit Shy− Gambit Shy → avoid sacrifices
+ Gambito → follow gambit lines
Opening only (< 20 half-moves). ECO: logBoost += v × scale if the move is an ECO gambit continuation. Fallback: pawn to an attacked, undefended square.ECO gambit-line match takes priority; structural fallback if ECO data is not yet loaded.
Trade Seeker / Trade Avoider− Trade Avoider → no captures
+ Trade Seeker → captures
Capture: logBoost += v × scale × tanh(ctx)
Threat: logBoost += v × scale × min(tanh(ctx × newThreats / 2), tanh(ctx))
Non-captures scored by the number of new opponent-piece threats created from the destination square, capped at the capture score — offering a trade never outscores making one.
Rigid / Loose Pawn Structure− Loose → open mobile structure
+ Rigid → tighter structure
Own pawn moves only. delta = currentPenalty − simPenalty
logBoost += v × scale × tanh(delta)
Penalty = pawn islands + doubled pawns + isolated pawns. delta > 0 = move tightens structure.
Coffeehouse Hustler / Overthinker− Overthinker → longer think time
+ Hustler → shorter think time
Applied to think time (not prob): thinkMs ×= (1 − v × 0.15)At +5: ×0.25 (4× faster). At −5: ×1.75 (75% slower). Feeds into the pressure curves as actual think time.
Luck / Bad Day− Good day → top of distribution
+ Bad day → bottom of distribution
Shifts the quality-range window:
lo = botDayLower − v × 4
hi = pressureUpper − v × 4
See §8 Move Quality Range for the full description.
Panicky / Calm under pressure− Calm → no temperature boost
+ Panicky → larger T boost under pressure
Modifies the time-pressure boost mode (steady / normal / panicky).See §4 Pressure Temperature Boost.
Attacker / Peacemaker− Peacemaker → avoids captures, then threats
+ Attacker → captures first, then threats
Δ = Σ attacks on opp. pieces after − before (+ attacks on the captured piece)
capture: tier = 0.75 + 0.25 × tanh(Δ / 2)
quiet: tier = 0.5 × tanh(Δ / 2)
logBoost += v × scale × tier
Full-board attack map on the simulated position. Every capture outranks every quiet move; a capture keeps credit for the attacks it cashed in. Presets wanting calm that still trades use Tension → Defuser.

All attractor signals that use tanh map their input to a smooth −1..+1 range, preventing any single move from receiving an unbounded boost.

7 · Piece-Type Attractors

Six independent sliders (−5 to +5) bias the bot toward or away from moving each piece type. The formula is the same for all six:

logBoost += pieceVals[pieceType] × scale

The piece type is determined from the moving piece before the move. The signal is 1 (or 0 for no boost) — there is no continuous signal function as with most strategic attractors. Scale is shared with the strategic attractors, so the CP budget controls piece-attractor strength alongside everything else.

PieceLeft (−) behaviourRight (+) behaviourBest combined with
♙ PawnSuppress pawn movesPrioritise pawn advancesRigid structure (connected chains)
♘ KnightDe-prioritise knight hopsKnight manoeuvres firstRigid structure (closed positions)
♗ BishopKeep bishops passiveDiagonal play, bishop pairLoose structure (open diagonals)
♖ RookPassive rooks, avoid early tradesFile seizure, rook liftsOpen files, trade-seeker
♕ QueenConservative queen, avoid exposureActive queen, queen-led attacksChaos Agent (keeps complications)
♔ KingKing safety, stay castledKing activity, endgame aggressionEndgame positions, low piece count

8 · Move Distribution Filter & Luck

The Move Distribution Range dual slider selects which segment of the Maia probability-ranked candidate list the bot samples from. Candidates outside the [lower, upper] percentile window are excluded before temperature sampling and attractor reweighting.

Popularity is not engine quality — Maia probability is how often players at the selected rating choose a move. A strong move few players see can sit low in the distribution, which is why the rare tail is where creative, unconventional play lives; real move-quality enforcement is the engine checking in §9.

8a · Percentile Band Filtering

sorted candidates by probability (highest first) cumulative probability sum = total band.lo = (lo/100) × total band.hi = (hi/100) × total Include move m if: cumulative_end(m) > band.lo AND cumulative_start(m) < band.hi

Default: lo = 0%, hi = 100% (full distribution). Setting lo = 20%, hi = 60% samples only the middle tier — avoiding the top moves (too accurate) and the worst moves (random blunders).

8b · Luck Attractor Shift

The Luck attractor shifts both bounds of the quality window simultaneously:

lo_effective = botDayLower − luckVal × 4 hi_effective = pressureUpper − luckVal × 4 luckVal > 0 (Bad day): window shifts down → samples lower-ranked moves → worse play luckVal < 0 (Good day): window shifts up → samples top-ranked moves → sharper play

pressureUpper is the curve-B degraded upper bound, so the luck shift compounds with time-pressure effects.

8c · Min-Probability Filter

A single absolute-popularity filter prunes the candidate list before the quality range is applied. It is an honest distribution control expressed as a percentage — it removes near-zero candidates and makes no claim about move quality (centipawn enforcement is the engine-calculated check in §9).

absFloor = minProbPct / 100 keep only moves with prob ≥ absFloor (if that empties the list, keep the single most-popular move)

9 · CP-Budget Acceptance, Degradation Guard & Hard Floor

Maia probability is popularity, not quality. Three post-pick stages use a shallow Stockfish probe to hold move selection to a real centipawn standard. They apply to Maia moves; Flounder moves are chosen from Stockfish's evaluations in the first place and skip them (§2c). All are fail-open — any probe failure or timeout leaves the pick unchanged — and probe results are shared between stages so no move pays for the same evaluation twice. The probe reuses the MultiPV complexity machinery with a searchmoves restriction (depth ~10), evaluating one wide batch in a single search rather than one call per move.

9a · CP-Budget Acceptance

Turns the Centipawmeter into a real centipawn ceiling. When personality reweighting produced a pick different from the most-popular move, the pick, the popular move, and up to 15 of the personality's next favourites are evaluated together in one probe. The pick is accepted only if it loses no more than the CP Budget versus the popular move; otherwise the bot walks down its own preference order until one fits, falling back to the popular move itself (0 cp by definition). The wide walk means a rare move from deep in Maia's tail still gets a genuine chance to be played — it just has to prove it can afford it.

topMove = argmax Maia probability if pick == topMove or reweighting not applied → keep pick evals = Stockfish probe over [topMove, pick, next favourites] for m in [pick, ...favourites]: if evals[topMove] − evals[m] ≤ cpBudget → play m else → play topMove

Stalemate-seeking picks (§10) are deliberately exempt — throwing material is the whole point, so the budget must not veto them.

9b · Degradation Guard

Runs only under the two features whose job is to make the bot play worse: Grandmaster Bad Day, and time pressure when curve B is raising this move's temperature above the base (premoves are judged at their 1-second think time). There, a pick that evaluates better than Maia's top move is swapped back to the top move, so degradation never turns up a brilliancy. Personality picks may still beat it by up to the CP Budget. Ordinary sampling at the bot's own temperature is not checked: Maia sometimes plays a strong rare move, and so does the bot. There is no switch; a noUpgradeGuard field in an older saved bot is ignored.

if not (Bad Day or time pressure raising T by > 0.05) or pick == topMove → keep evals = Stockfish probe over [pick, topMove] allowance = Budget if the personality reshaped this move, else 0 if eval(pick) − eval(topMove) > allowance → play topMove

Grandmaster Bad Day picks the lowest-probability move above its configured floor: if that move happens to be strong (a shot few players see), the guard swaps it back to the mainstream move — the bot is never accidentally brilliant.

9c · Hard Floor Backstop

The last stage in the pick flow. Whatever produced the final pick — temperature sampling, the Luck shift, Bad Day, curve-B restriction, or plain sampling variance — if it differs from the most-popular move, it must not lose more than the Hard Floor's centipawns against it; otherwise the popular move is played instead. Picks already verified within the Budget skip this check (Budget ≤ Floor by construction), stalemate-seeking picks are exempt, and a Floor set to Off disables the stage entirely — the honest setting for beginner bots that should be able to hang pieces.

active if Floor < Off AND pick ≠ topMove AND not budget-verified AND not stale-seek evals = reuse guard's probe if available, else Stockfish probe over [pick, topMove] if evals[topMove] − evals[pick] > floor → play topMove else → keep pick

10 · Draw & Desperation Behaviours

Four opt-in behaviours (Personality section → "Draws & Desperation" tab) let a bot handle draw offers, offer draws, and swindle when lost — all judged the way a human of the bot's strength would, not at raw engine accuracy.

10a · Perceived Evaluation

Every draw decision uses the bot's perceived advantage, not Stockfish's. A shallow eval probe gives the true centipawn advantage from the bot's side; that value is then blurred by rating-scaled Gaussian noise plus a mild self-optimism bias. A novice genuinely misjudges; a master barely does.

sigma(elo) = 15 + 700 × exp(−elo / 500) ≈ 225 cp at 600 ELO, ≈ 50 cp at 1500, ≈ 19 cp at 2600 perceived = trueAdv + gauss() × sigma + 0.25 × sigma

So a 700-rated bot can believe a lost position is fine (and decline your draw, or offer one from a losing position it thinks is level); a 2400 is almost never fooled.

10b · Accepting & Offering Draws

BehaviourControlRule
Accepts draw offerstoggle + accept-margin (0–300 cp); on at 200 cp by defaultOn your ½ offer the bot accepts iff its perceived advantage ≤ margin — by default, unless it is more than 200 cp ahead. Declines are wordless (a human just plays on — no eval is announced), and a bot that knows it is worse may still decline, hoping for a blunder.
Offers drawstoggle + level-band (±cp) + from-moveAfter its move, in a position that feels level (|perceived| ≤ band) and past the move threshold, it occasionally offers — never twice within 12 plies.

10c · Clock Awareness

Draw decisions read the clock (only when the increment is under 10 s, since nobody flags with a healthy increment):

Untimed games and games with a ≥ 10 s increment ignore the clock entirely.

10d · Seek Stalemate When Losing

A desperation swindle mode. Once the engine eval says the bot is worse than the threshold and the game has passed the configured move number, candidate weights are re-shaped toward the classic stalemate-trap recipe:

active if staleSeek AND fullmove ≥ fromMove AND eval ≤ −thresholdCp weight ×= exp( mobilityBoost + desperadoBoost )

These picks are exempt from the §9 CP-budget check. Arming this behaviour also forces the complexity/eval probe every move so the trigger stays current.

11 · Preset Personalities

Each preset personality is a named collection of attractor values; loading one sets all sliders to those values (−5 to +5 scale). Some presets also reconfigure the engine, opening repertoire, or draw behaviour and confirm first via a dialog. The table below shows each personality's notable settings.

PersonalityTagNotable attractor valuesCharacter
Captain EntropyChaoschaos +4, compwin +4, gambito +3, trade +2, spacecadet +2, hustle −3, structure −3Seeks complications, gambit lines, and tactical chaos. Thinks longer per move (hustle −3).
NormOrderstructure +4, fortkx +3, spacecadet 0, chaos −4, compwin −4, gambito −2, hustle +2, tension −2Closes positions, defends solidly. Simplifies when winning. Slightly faster pace.
Attacky McTackersonCapturesattacker +5, trade +3, hustle −2 (nothing else shares the budget)Takes whatever it can within the cp ceiling; with nothing to take, piles attacks onto your pieces. Slightly deliberate (hustle −2).
OverthinkerMethodicalstructure +3, spacecadet +3, fortkx +2, hustle −4, pressure −3, chaos −3, compwin −2Maximises board control and structure. Thinks long on every move (hustle −4). Likely to accumulate time pressure in classical games.
Coffeehouse HustlerFasthustle +5, pressure +2, luck +2, gambito +2, structure −2Plays fast and loose. Phase temperature: T = 5 opening → T = 0.6 endgame. At hustle +5: think time ×0.25 of baseline.
The BlundererHumanluck +3, pressure +5Plays normally but cracks unpredictably under pressure. Luck shifts toward lower-ranked moves.
The HoarderMaterialgambito −3, trade −3, fortkx +2, chaos −2, hustle +2, luck −2, tension −2Never sacrifices material. Avoids trades and gambits. Slightly faster pace.
Pawn Chain GangPawnsstructure +5, fortkx +1, hustle +1, chaos −1, compwin −2, gambito −1Maximises pawn structure. Slightly faster pace.
Spite CheckCheckschaos +3, compwin +2, attacker +3, trade +2, hustle −2Always prefers checking moves. Builds up attacks. Slower, deliberate pace (hustle −2).
Clock WatcherClockcompwin −2, trade −1, pressure −3Conservative when ahead on time, reckless when behind. Calm under pressure (pressure −3).
Grandmaster Bad DayHumanluck +5, pressure +2 (Maia @ 2400, picks lowest-prob move above 4%)A strong player having an off day. The §9 degradation guard runs on every Bad Day pick, which prevents it from ever accidentally choosing a strong move — never brilliant by mistake.
Drunken MasterHumanattacker +4, chaos +3, gambito +3 (Hybrid: 50% Maia 2400 + 50% Maia 1000 per move)Each move rolls one of two Maia strengths — brilliant one move, blundering the next. A plain hybrid config, so the two slots are freely re-tunable.
The DrawmeisterHumantrade +4, fortkx +3, structure +2, chaos −4, gambito −3, tension −3 (accepts + offers draws)Plays for the half point: simplifies, fortresses up, offers/accepts draws (§10), and installs solid drawish repertoires for both colours (London / Exchange Slav / Four Knights; Petroff / Berlin / Slav).

Hustle attractor sign convention: +5 = Coffeehouse Hustler (fast, think time ×0.25). −5 = Overthinker (slow, think time ×1.75).

12 · Time Controls

Time controls are selected from the bot panel grid, divided into Blitz (≤ 5 min), Rapid (10–30 min), Classical/Correspondence (60 min+), Infinite (untimed), and a special 90+30 column for the FIDE/Candidates format.

FormatBase timeIncrementBonusBonus atInc from
Bullet 1+01 min0 s———
Blitz 3+23 min2 s———
Blitz 5+05 min0 s———
Rapid 10+010 min0 s———
Rapid 15+1015 min10 s———
Classical 30+030 min0 s———
Classical 60+060 min0 s———
Classical 90+090 min0 s———
FIDE 90+30 *90 min30 s+30 minMove 40Move 41
Untimed ∞∞0 s———

* 90+30 (FIDE / Candidates): 90 minutes for the first 40 moves, then +30 minutes added to both clocks at move 40. A 30-second increment applies from move 41 onward. A guard flag prevents the bonus from being awarded twice.

Bonus Time Implementation

fullMoveNum = ceil(gameMovesAlgebraic.length / 2) if tc.bonusSecs AND fullMoveNum ≥ tc.bonusAtMove AND NOT bonusApplied: clockTimeW += bonusSecs (capped at 59940 s) clockTimeB += bonusSecs bonusApplied = true if clockInc > 0 AND fullMoveNum ≥ tc.incFromMove: add increment to the side that just moved

13 · Human Behaviour Flags

Human behaviour flags add realistic timing irregularities and clock-pressure responses. Each flag is independently toggleable, and they modify think time or move selection in specific situations.

FlagWhen activeEffect
Move BlinkPosition entropy < 0.5 (forced or obvious move) — on Maia from its probabilities, on Flounder from its own selection distributionReturns a 200–500 ms random pause immediately, skipping the full think-time calculation. Simulates a human instantly playing the obvious recapture or forced reply.
Reconsider15% random chance per moveMultiplies computed think time by 1.5–2.5×. Simulates the human hesitation of starting to play a move then reconsidering.
Clock MirrorOpponent clock < bot clock × 0.6Halves think time when the opponent is significantly lower on time. Simulates a human speeding up to maintain a clock advantage.
Clock WeaponizerOpponent's clock at or below the trigger (default 15 s)Returns the minimum move time (default 0 ms) AND, while the ELO curve is on, drops the bot to the curve's floor rating — Maia's model at that rating, or Flounder's. Maximises pressure when the opponent is close to flagging. The trigger is absolute time remaining, not a lead — 15 s is flaggable in any format — and is set by the Opponent Clock slider, with a separate Min Move Time slider for the floor.
Can FlagAlways on or offWhen OFF: think time capped at max(200 ms, clockMs − 3000 ms) to stop the bot flagging itself. When ON: no such safeguard.

14 · Appendix — Play From Any Position

Beyond bot configuration, games are not locked to the standard start. Any finished game, or any loaded PGN, can be reviewed move-by-move and resumed as a new game — against a bot or a friend — from any position along the way.

14a · Review & Replay

14b · Resume Play From Here

All ELO values are relative playing strengths within the Blundermind engine; they are not official FIDE ratings.  ·  Back to top ↑