How the personality controls work

Every slider in the bot builder’s Personality panel, what it actually measures, and the formula it uses. Nothing here is a simplification for the sake of the page — these are the expressions the engine evaluates.

The shared machinery

Each slider runs from −5 to +5. Zero means the control has no opinion and costs nothing. Everything else is scored per candidate move and turned into a single number, logBoost, which multiplies that move’s probability:

weight(move) = probability(move) × exp(logBoost(move))

The CP Budget divides itself between whatever is switched on. A control set to ±5 while everything else sits at zero gets the whole budget; two controls at ±5 get half each. That is what scale below means:

scale = CP Budget / ( Σ|slider values| × 150 ) logBoost = slider value × scale × response(position)

The 150 sets the unit: 150 cp of budget spent on one control is worth exp(1) — a 2.7× push on the move it likes best. At the maximum budget of 300 cp that becomes exp(2), or 7.4×.

response(position) is each control’s own measurement, always bounded to −1…+1 by tanh, so no single control can run away. In practice a control delivers about 0.81 of its nominal push to the move it most wants; the rest of the range is used to grade the moves it likes less.

Every divisor below scales with the material left on the board. A two-defender swing in a queenless endgame is enormous and the same swing in a full middlegame is nothing, so a fixed constant would make each personality loud in the middlegame and mute in the endgame — which is backwards, since most players’ style shows up more as the board empties. Where a formula says k, the value actually used is:

phase = clamp( material still on the board / 78 , 0.35 , 1 ) k used = k × phase

Four controls take no share of the budget, because none of them scores a move: Panicky scales the time-pressure curve, Coffeehouse hustler scales think time, Bad day slides the distribution band, and Front-runner nudges temperature once the game is won or lost. Everything else splits the budget in proportion to how far its slider is pushed.

The budget is also a hard limit, not just a volume knob. Whatever the sliders say, the move finally played is checked against the engine: if the personality’s choice would cost more than the CP Budget compared with the move the bot would otherwise have made, it is rejected and the next preference is tried. Style can never spend more than you allowed it to.

Position controls

These score every candidate move individually. v is the slider value.

Attacker · Peacemaker

−5 Peacemaker  ←  0  →  Attacker +5

Counts how many enemy pieces the bot’s pieces attack, before and after the move, and scores the difference. A move that puts two more enemy pieces under fire scores well; one that walks away from threats scores badly. Positive seeks threats, negative avoids them.

threats(board) = Σ over enemy pieces of (number of bot pieces attacking it) logBoost += v × scale × tanh( (threats(after) − threats(before)) / 2 )

Applies to: every move.

Fort Knox · Glass cannon

−5 Glass cannon  ←  0  →  Fort Knox +5

The mirror image of Attacker, pointed at the bot’s own pieces: how many times they defend each other. Every piece counts, attacked or not — a square nobody is attacking yet is still a square you can be driven off — so Fort Knox will not walk a piece onto an undefended one, and Glass cannon is glad to.

defence(board) = Σ over bot pieces of (number of bot pieces defending it) logBoost += v × scale × tanh( (defence(after) − defence(before)) / 3 )

The two directions are the same arithmetic: whatever one pole rewards, the other prefers the opposite of.

Applies to: every move.

Trade seeker · Trade avoider

−5 Trade avoider  ←  0  →  Trade seeker +5

Captures and offers to trade — but weighted by whether trading is a good idea right now. Trading is contextual: you do it when you are ahead, or to kill an attack. A player who simply takes everything is not a trade-seeker, they are a weak player, so the control leans harder into exchanges when the bot is up material and warier when it is down.

context = 1 + 0.6 × tanh( (my material − their material) / 4 ) → 1.0 when level, up to 1.6 well ahead, down to 0.4 well behind capture (including en passant): logBoost += v × scale × tanh( context ) otherwise, with n = enemy pieces the moved piece now attacks: logBoost += v × scale × tanh( context × n / 2 )

Applies to: every move.

Rigid · Loose pawn structure

−5 Loose  ←  0  →  Rigid +5

Scores the bot’s own pawn formation with a penalty count — lower is tighter — and rewards moves that reduce it. A pawn counts as connected when a friendly pawn stands on an adjacent file within one rank of it: the side-by-side duo and the diagonal chain. Rigid keeps the pawns holding hands; Loose breaks formation.

penalty(board) = islands + doubled + isolated + unconnected islands groups of occupied files with a gap between them doubled pawns beyond the first on any file isolated pawns with no friendly pawn on either adjacent file unconnected pawns with no friendly pawn on an adjacent file within one rank logBoost += v × scale × tanh( penalty(before) − penalty(after) )

Rigid rewards a move that tightens the formation and dislikes one that loosens it. Loose wants the opposite: it prefers the position broken up.

Applies to: the bot’s own pawn moves only — nothing else can change where its pawns stand, so no other move is scored.

Space cadet · Space waster

−5 Space waster  ←  0  →  Space cadet +5

A weak square is an empty square the bot attacks zero times. Space cadet prefers moves that leave fewer of them — a board it has a say over. Every square counts, including both back ranks — a hole on your own first rank is a mating square, not an irrelevance — with the centre worth half as much again.

weight(square) = 1.5 on d4, d5, e4, e5, f3 and f6 1 everywhere else weak(board) = Σ over empty squares with no bot attacker of weight(square) logBoost += v × scale × tanh( (weak(before) − weak(after)) / 4 )

d4, d5, e4 and e5 because that is where the game is decided; f3 and f6 because they are the soft square in front of a castled king, one for each colour.

Applies to: every move.

Gambito · Gambit shy

−5 Gambit shy  ←  0  →  Gambito +5

Prefers to keep playing a known gambit once the opening has entered one. When the ECO opening data has not loaded, it falls back to the shape of a gambit rather than the name of one: a pawn pushed to a square the opponent attacks and the bot does not defend.

ECO available and this move continues a known gambit line: logBoost += v × scale fallback, for pawn moves only, when the destination square is attacked by the opponent and defended by no bot piece: logBoost += v × scale

Applies to: the first 20 plies (10 moves each) only. After that it is silent.

Tension seeker · Defuser

−5 Defuser  ←  0  →  Tension seeker +5

How much contact there is between the two armies: every piece on the board, counted once for each enemy attacker bearing on it. A quiet position scores near zero; one where captures and recaptures hang over several squares at once scores high. Judged on the position the move leaves behind, so a tension seeker picks the move that tangles the board and a defuser the one that untangles it.

tension(board) = Σ over every piece of (enemy pieces attacking it) logBoost += v × scale × tanh( (tension(after) − tension(before)) / 4 )

Applies to: every move. Costs nothing and gives the same answer every time.

Chaos agent · Clarity

−5 Clarity  ←  0  →  Chaos agent +5

Whether the position after the move is hard to judge, which is a different question from how much wood is in contact. A queen trade can be all tension and no difficulty; a quiet knight move can leave a mess. The measure is how much the engine changes its mind about a move as it searches deeper: a move settled by depth three leads somewhere clear, one whose score is still moving at depth eight does not.

volatility(move) = average |change in the move's score| per extra ply logBoost += v × scale × tanh( (volatility(move) − average volatility) / 25 )

It is scored against the other candidates rather than an absolute scale, because what counts as a hard position is different in an endgame and a middlegame.

Applies to: every move, and it is approximate. The same position measured twice agrees about two thirds of the time — a shallow search does not divide its effort the same way twice. Tension is the steady one; this is the truer one. That is why they are two sliders and not one, and why a bot can hold both opinions at once.

Pawn grabber · Principled

−5 Principled  ←  0  →  Pawn grabber +5

Materialism, weighted by what is actually won — and drawn especially to the capture it probably should not make. The second term fires when the capturing piece lands on a square the opponent still attacks: that is the poisoned pawn, and taking it is the whole personality. Different from Trade, which is about exchanging at all rather than about what is won.

grabbed = value of the captured piece (P1 N3 B3 R5 Q9), 0 if no capture poisoned = 1 if the opponent still attacks the destination square, else 0 logBoost += v × scale × tanh( (grabbed + 2 × poisoned) / 3 )

Applies to: captures. How badly it can end is still bounded by the CP Budget, so a grabber takes the pawns it can afford and no others.

King safety · Bravado

−5 Bravado  ←  0  →  King safety +5

How much the bot minds its own king being in a draught. Counts enemy attacks on the nine squares around its king, before and after the move. Positive keeps the king tucked up; negative leaves it airy and gets on with its own plans.

danger(board) = Σ over the 9 squares around my king of (number of enemy pieces attacking that square) logBoost += v × scale × tanh( (danger(before) − danger(after)) / 1 )

Applies to: every move. It is naturally quiet while the king is safe and loud once it is not, which is the point.

Prophylaxis · Own plans

−5 Own plans  ←  0  →  Prophylaxis +5

Scores a move by how much room it takes away from the opponent, rather than how much it gains for the bot. It is the one idea here that looks across the board instead of at itself, and it is the difference between a bot playing its own game and one playing against you.

theirMoves(board) = number of LEGAL MOVES the opponent has logBoost += v × scale × tanh( (theirMoves(before) − theirMoves(after)) / 3 )

Legal moves rather than attacked squares, because restricting somebody means taking away moves they could actually have played — a pawn attacks two squares and moves to one. Castling and en passant sit outside both counts, since the position being tested has no castling rights of its own to consult; the control uses the difference, which is unaffected either way.

Applies to: every move. It is the most expensive control here — it generates the opponent's whole move list for every candidate — and only bots that switch it on pay for it.

Piece preferences

−5 avoid  ←  0  →  favour +5, per piece type

A flat like or dislike for moving a given kind of piece — a knight-hopper, a bishop fan, someone who will not move the queen. There is no positional measurement here, so there is no tanh: every move by that piece type gets the same push.

logBoost += pieceValue[type of moving piece] × scale

Applies to: every move, by the type of the piece being moved.

Controls that act on the whole turn

These four do not score individual moves. They still take a share of the CP Budget, because they are still part of the personality.

Front-runner · Swindler

−5 Swindler  ←  0  →  Front-runner +5

The same measurement as Chaos, but consulted only once the game has a direction — and it points opposite ways depending on which direction that is. A front-runner pushes their luck when ahead and folds when behind. A swindler converts cleanly when winning and muddies the water when losing, which is where most saved games come from.

when the bot is better than +50 cp: temperature ×= exp( v × 0.08 × tanh( (complexity − 0.5) × 4 ) ) when the bot is worse than −50 cp: temperature ×= exp( −v × 0.08 × tanh( (complexity − 0.5) × 4 ) ) between the two, it does nothing

Applies to: the whole turn, once the game is decided one way or the other.

Coffeehouse hustler · Overthinker

−5 Overthinker  ←  0  →  Hustler +5

Pure tempo: how long the bot sits before moving. A hustler bangs moves out to put you under time pressure; an overthinker takes its time over everything.

thinkTime ×= ( 1 − v × 0.15 )

Applies to: the clock, not the move. At +5 the bot moves in a quarter of its normal time; at −5 it takes nearly twice as long. Complexity-scaled timing only — Fixed interval, Mirror user and Instantaneous set the pace themselves, and the builder greys this control out under them.

Bad day · Good day

−5 Good day  ←  0  →  Bad day +5

Slides the whole band of moves the bot is willing to consider up or down its own preference list. On a good day it draws from the top of the list; on a bad day it is working from further down, and playing below itself all game.

lower = clamp( dayLower − v × 4 , 0 , 95 ) upper = clamp( dayUpper − v × 4 , lower+5 , 100 ) moves outside the [lower, upper] percentile band are discarded before anything else is scored

Applies to: the candidate list, before scoring. Maia, and the Lichess opening book on any engine — wherever moves come ranked by popularity.

Why this control belongs to Maia and has no Flounder equivalent. Maia orders its moves by how often humans play them, which is popularity, not quality — so sliding the band down reaches for less-obvious moves, and a less-obvious move may turn out to be good or bad. That is what an off day actually feels like.

Flounder orders its moves by what they cost. Sliding a band down that list can only ever reach worse moves, every time, which is not a bad day — it is simply a lower rating, and the rating dial already does that with measurements behind it. The control has no effect on Flounder’s own moves, and the Move Distribution Range it lives on is hidden for a bot that only ever plays Flounder. With the opening book on, it still shapes the book’s moves.

Panicky · Calm under pressure

−5 Calm  ←  0  →  Panicky +5

How hard the time-pressure curve bites. It does not change any move on its own — it multiplies how far along the temperature-escalation curve a low clock pushes the bot. At 0 the bot follows the curve as drawn, which is mild by default (a ceiling of T 2). Toward Calm it climbs less, and fully Calm under pressure leaves the temperature untouched; toward Panicky it climbs faster and reaches the ceiling early.

multiplier = 1 + v / 5 for v ≤ 0 (−5 → 0, untouched) multiplier = 1 + 0.3 × v for v > 0 (+5 → 2.5) temperature = base + fraction × multiplier × (ceiling − base)

Applies to: timed games where think time can really run short — Complexity-scaled timing, which budgets from the clock, or Mirror user, which plays at your pace and so speeds up (and cracks) when you do — with the temperature curve switched on. Under a Fixed or Instantaneous pace there is no time pressure and it does nothing. It takes no share of the centipawn budget in any game — it scores no moves.

Custom controls

The named sliders are the ones worth having a name. Beyond them a bot can carry any number of custom controls, built from the measurements below and scored in exactly the same way — the change the move makes to that measurement, bounded by tanh:

logBoost += v × scale × tanh( (metric(after) − metric(before)) / k )

k sets what counts as a big change for that measurement and is listed with each one. A positive slider prefers moves that raise the metric, a negative one moves that lower it. Everything is measured for the bot’s side unless it says otherwise. In the formulas, me is the bot and them the opponent.

Pawns

MetrickWhat it counts
Passed pawns1Own pawns with no enemy pawn ahead of them on their own or either adjacent file. count of my pawns with no their-pawn ahead on files f−1, f, f+1
Pawn advancement4How far the pawns have come, measured as the distance they still have to travel. Any pawn move raises it, from any square, and so does promoting. − Σmy pawns (squares short of promotion)
Doubled pawns1Pawns beyond the first on any file. Σfiles max(0, myPawnsOnFile − 1)
Isolated pawns1Pawns on a file with no own pawn on either adjacent file. Σfiles f with my pawns, none on f−1 or f+1 myPawnsOnFile
Pawn islands1Groups of occupied files separated by at least one empty file. number of unbroken runs of files holding my pawns
Connected pawns2Pawns with an own pawn on an adjacent file within one rank — the duo and the chain. count of my pawns with a my-pawn at |Δfile| = 1 and |Δrank| ≤ 1
Pawn structure health1The Rigid/Loose penalty, negated, so higher is tidier. −(islands + doubled + isolated + unconnected)

King safety

MetrickWhat it counts
Enemy pressure on my king3Enemy attacks on the squares around the own king. Σ9 squares around my king (their attackers on that square)
King-zone pressure3The same sum around their king, counting own attacks. Σ9 squares around their king (my attackers on that square)
Pawn shield on my king1Own pawns on the six squares one and two ranks in front of the own king. count of my pawns at |Δfile| ≤ 1 and 1 ≤ Δrank ≤ 2 ahead of my king
Gives check1Whether the move leaves the opponent in check. 1 if they are in check, else 0

Material and development

MetrickWhat it counts
Material balance3Own material minus the opponent’s. Σ my values − Σ their values, at P1 N3 B3 R5 Q9
Total pieces on board3Every piece of either colour. Lower means a simplified position. count of occupied squares
Queens on board1Queens of either colour, 0 to 2. count of queens, either colour
Bishop pair1Whether the bot still holds two bishops. 1 if my bishops ≥ 2, else 0
Developed minor pieces2Own knights and bishops off the back rank. count of my N and B not on my first rank
Piece centralization3How close the own pieces stand to the middle of the board. Pawns and the king are not counted. Σmy N B R Q (3.5 − |file − 3.5|) + (3.5 − |rank − 3.5|)
Knight/bishop outposts1Own knights and bishops past the middle of the board on a square no enemy pawn can ever attack: every pawn that might have challenged it has gone past or been captured. count of my N and B in their half with no their-pawn on file ±1 still behind the square
My piece mobility5The number of legal moves available. The same measurement Prophylaxis points at the opponent. count of my legal moves

Threats and defence

MetrickWhat it counts
Attacks on enemy pieces3Attackers summed over every enemy piece. Counting attacks rather than pieces means a second attacker on an already-attacked piece registers, which is usually the move worth finding. Σtheir pieces (my attackers on it)
My hanging pieces2Own pieces, king aside, that are attacked and defended zero times. count of my pieces with their-attackers > 0 and my-defenders = 0
Loose enemy pieces1The same test on the other side. count of their pieces with my-attackers > 0 and their-defenders = 0
My defended pieces3Own pieces, king aside, with at least one own defender. Counts pieces. count of my pieces with my-defenders ≥ 1
Piece defense (Fort Knox)3Total defensive cover. Counts defences rather than pieces — this is the measurement the Fort Knox slider uses. Σmy pieces (my defenders on it)

Squares

MetrickWhat it counts
My weak squares5Empty squares the bot does not attack, over the whole board — the Space cadet measurement without the centre weighting. count of empty squares with my-attackers = 0
Central square control3Own attacks on the four centre squares. Counts attacks, so two attackers on one square score twice. Σd4 d5 e4 e5 (my attackers on that square)
Space in enemy half5Empty squares in the opponent’s half that the bot attacks. count of empty squares in their half with my-attackers ≥ 1

Rooks

MetrickWhat it counts
Rooks on open files1Own rooks on a file with no pawn of either colour. count of my rooks on files holding no pawns
Rooks on the 7th rank1Own rooks on the opponent’s second rank. count of my rooks on their 2nd rank

A custom control can also be gated on game phase, on whether the bot is winning or losing, and on time pressure — in which case it only takes a share of the budget on the moves where its condition actually holds.

Where personality sits in the move pipeline

The two engines are different shapes, so the personality attaches at a different point in each. Both are bounded by the same CP Budget.

MaiaFlounder
Maia returns a probability for every move — how often humans of that rating play it. Stockfish scores every legal move, and the rating draws a target for how much the turn should cost.
Personality reweights those probabilities by exp(logBoost). Personality chooses within a band of moves lying either side of the cost the rating asked for.
Temperature then samples the reshaped distribution. Temperature has already acted, by reshaping the error distribution the target was drawn from.

One consequence worth knowing on the Maia side: because personality is applied before temperature, turning temperature down does not switch personality off. It makes it decisive — the bot plays the top of the distribution personality just reshaped, every time. If a trait has lifted its favourite move above the crowd, a cold bot plays that move and means it.

On the Flounder side the band is deliberately symmetric: it reaches moves that cost a little more than the rating asked for and moves that cost a little less. A real trait makes you play below your rating on some moves and above it on others, so a one-sided band would turn every personality into a handicap.

Blundermind is free software under the GNU GPL v3 or later, so none of the above has to be taken on trust — the expressions on this page live in src/50-bot-engine.js in the public repository. See also the credits and licences.