This guide builds Poison Chess from scratch, demonstrating the effects system — timed status effects on squares that interact with movement and piece destruction.
In Poison Chess, when a piece is captured, the capture square becomes "poisoned" for 3 turns. Any non-king piece that lands on a poisoned square is destroyed. Kings cannot move to poisoned squares at all. This creates toxic zones that reshape the board over time.
Key engine features used:
MCE.addEffect() — create poison effects on capture squaresMCE.hasEffect() — check if a square is poisonedMCE.tickEffects() — auto-decrement durations each turnMCE.mutateBoard() — destroy pieces that land on poisonmoveFilter — prevent king from entering poisoned squaresevaluate — score positions considering poison proximityEvery game object has an effects array. Each effect is an object:
{ sq: 28, type: 'poison', duration: 3, owner: 'w', data: null }
sq — the board square this effect is ontype — a string identifier (you define these per variant)duration — turns remaining; null for permanent effectsowner — which side created the effectEffects are fully undo-aware. When you call MCE.addEffect(g, undo, effect), the engine snapshots the effects array. On unmakeMove, the snapshot is restored automatically. This means the AI can search through millions of positions with effects without corruption.
Effects with a duration auto-decrement each turn via MCE.tickEffects(). When duration reaches 0, the effect is removed. This happens automatically after turn logic for variants without a custom turnLogic hook.
In afterMove, when a capture occurs we poison the capture square:
afterMove: function(g, move, undo) {
var isCapture = undo.captured || move.flag === 'ep';
var moverSide = undo.turn;
if (isCapture) {
MCE.addEffect(g, undo, {
sq: move.to,
type: 'poison',
duration: 3,
owner: moverSide
});
}
}
MCE.addEffect takes care of undo tracking. The first call in a move snapshots the entire effects array into undo._effectsSnapshot. Subsequent calls in the same move reuse the same snapshot.
We also check in afterMove whether the moving piece landed on an existing poison square. If it did (and it is not a king), destroy it:
afterMove: function(g, move, undo) {
var isCapture = undo.captured || move.flag === 'ep';
var moverSide = undo.turn;
// Create poison on capture
if (isCapture) {
MCE.addEffect(g, undo, {
sq: move.to, type: 'poison', duration: 3, owner: moverSide
});
}
// Destroy non-king pieces that land on poison
if (!isCapture && MCE.hasEffect(g, move.to, 'poison')) {
var landed = g.board[move.to];
if (landed && MCE.pieceType(landed) !== 'k') {
MCE.mutateBoard(g, undo, [{ sq: move.to, piece: null }]);
}
}
}
We skip destruction on capture moves because the capturing piece created the poison — it should not be destroyed by its own poison. Only subsequent pieces that enter the square are affected.
MCE.mutateBoard removes the piece and tracks the change for automatic undo. No restoreState hook needed.
Kings must not be allowed to move onto poisoned squares. We enforce this in moveFilter:
moveFilter: function(g, moves) {
return moves.filter(function(m) {
var piece = g.board[m.from];
if (piece && MCE.pieceType(piece) === 'k') {
if (MCE.hasEffect(g, m.to, 'poison')) return false;
}
return true;
});
}
We filter in moveFilter rather than beforeMove because moveFilter removes moves from the legal list entirely — the player cannot even attempt to move their king to a poisoned square. This is also what the AI sees, so it will never consider illegal king moves.
The AI should avoid moving pieces near poison and value poison near enemy pieces:
evaluate: function(g, defaultEval) {
var score = defaultEval(g);
if (!g.effects || g.effects.length === 0) return score;
for (var i = 0; i < g.effects.length; i++) {
var eff = g.effects[i];
if (eff.type !== 'poison') continue;
var rc = MCE.rc(eff.sq, g);
// Penalize own pieces adjacent to poison
for (var dr = -1; dr <= 1; dr++) {
for (var dc = -1; dc <= 1; dc++) {
if (!MCE.onBoard(rc[0]+dr, rc[1]+dc, g)) continue;
var sq = MCE.sq(rc[0]+dr, rc[1]+dc, g);
var p = g.board[sq];
if (!p) continue;
if (MCE.pieceColor(p) === g.turn) score -= 20;
else score += 20;
}
}
}
return score;
}
Show the player how many poisoned squares are active:
statusText: function(g, helpers) {
if (!g.effects || g.effects.length === 0) return null;
var count = g.effects.filter(function(e) {
return e.type === 'poison';
}).length;
if (count === 0) return null;
return helpers.nameFor(g.turn) + ' to move (' + count + ' poisoned square' + (count > 1 ? 's' : '') + ')';
}
Effects with a numeric duration auto-decrement each turn. Set duration: null for permanent effects (e.g. terrain changes). If your variant has a custom turnLogic hook, call MCE.tickEffects(g, undo) manually at the appropriate point.
Squares can have multiple effects simultaneously. MCE.hasEffect(g, sq, type) checks for a specific type. MCE.getEffects(g, sq) returns all effects on a square — use this when effects interact with each other.
The position key (used for repetition detection and TT lookup) does not include effects. This means two positions with identical pieces but different effects are treated as the same position for caching. For most variants this is acceptable — effects change frequently and the TT entries expire between turns anyway. If your variant relies heavily on effect state for evaluation, consider adding aiTimeMult: 1.5 to give the AI more search time.
When using MCE.addEffect, MCE.removeEffect, and MCE.mutateBoard, the engine handles undo automatically. You only need a restoreState hook for custom state that lives outside the board and effects arrays (like Crazyhouse's hand).
| API | Purpose in Poison Chess |
|---|---|
MCE.addEffect() | Create poison on capture squares |
MCE.hasEffect() | Check if a square is poisoned (moveFilter + afterMove) |
MCE.mutateBoard() | Destroy pieces that land on poison |
moveFilter | Prevent king from entering poison |
evaluate | Penalize pieces near poison |
| Auto-tick | Effects expire after 3 turns automatically |
The effects system handles all the complexity of timed state, undo-awareness, and AI search compatibility. Your variant code only needs to create effects and react to them.
You can also add an openingBook property to give the AI pre-programmed moves for known positions — see Crazyhouse Step 8 for the format.