Skip to content

Mobility and Passed Pawn Contracts

The playground evaluation model ablation (fortemate/dicechess-training#12, #14) tests position-evaluation models against the baseline KcpFeatures (kcp-13). Strategy research highlights three position properties that no previous feature schema exposed: expected wasted rolls (dice faces whose piece type has no move), pawn blockage, and tempo as separate own and opponent mobility counts rather than a single signed difference.

In addition, the endgame motivates a passed-pawn block: pawn promotion in Dice Chess depends on rolling a pawn face, and a passed pawn’s value increases sharply as it nears the back rank.

Following the principle of train == serve, training enrichment and serving inference share the exact engine implementation to prevent drift.

Two primitives in dicechess.engine.search compute the underlying positional features:

Exposes pseudo-legal move counts per moving piece type in dice order (Pawn, Knight, Bishop, Rook, Queen, King):

package dicechess.engine.search
object PieceMobility:
def counts(state: GameState, color: Color): Array[Int]
def count(state: GameState, color: Color, pieceType: PieceType): Int
def total(state: GameState, color: Color): Int

Moves are computed on state.withActiveColor(color) with castling dice present (King and Rook dice in pool), exactly matching how RichFeatures.mobility_diff computes its operands. For every position and color:

own_moves_*opp_moves_*=mobility_diff\sum \text{own\_moves\_*} - \sum \text{opp\_moves\_*} = \text{mobility\_diff}

Exposes passed-pawn detection and rank advancement:

package dicechess.engine.search
object PassedPawns:
def count(state: GameState, color: Color): Int
def maxRank(state: GameState, color: Color): Int
def countAndMaxRank(state: GameState, color: Color): (Int, Int)
def bitboard(state: GameState, color: Color): Bitboard
  • Definition: Uses the standard chess definition: a pawn is passed if there are no opposing pawns on the same file or on adjacent files ahead of the pawn along its direction of advance.
  • Max rank advancement: Counted from that side’s own back rank as 0:
    • White pawn on rank r[2,7]r \in [2, 7]: advancement is r1[1,6]r - 1 \in [1, 6].
    • Black pawn on rank r[2,7]r \in [2, 7]: advancement is 8r[1,6]8 - r \in [1, 6].
    • When a side has no passed pawn, the value is 00.
  • Perspective: Evaluated from the explicit color argument, independent of active color and dice pool.

Extends KcpFeatures (13 columns) with per-piece move counts and normalized PDI:

IndexColumn NameSource / DefinitionRange
0–12p_diffqueen_capture_dangerIdentical to KcpFeatures.extractfloat32
13own_moves_pOwn pseudo-legal pawn moves0\ge 0
14own_moves_nOwn pseudo-legal knight moves0\ge 0
15own_moves_bOwn pseudo-legal bishop moves0\ge 0
16own_moves_rOwn pseudo-legal rook moves0\ge 0
17own_moves_qOwn pseudo-legal queen moves0\ge 0
18own_moves_kOwn pseudo-legal king moves0\ge 0
19–24opp_moves_popp_moves_kOpponent pseudo-legal move counts0\ge 0
25own_pdiPieceDiversity.count(state, color) / 5f0.0 to 1.0
26opp_pdiPieceDiversity.count(state, opponent) / 5f0.0 to 1.0

Extends kcp-mobility-27-v1 with the passed-pawn block:

IndexColumn NameSource / DefinitionRange
0–26Prefix (27 columns)Identical to KcpMobilityFeatures.extractfloat32
27own_passed_pawnsNumber of passed pawns for color0 to 8
28opp_passed_pawnsNumber of passed pawns for opponent0 to 8
29own_passed_max_rankMost advanced passed pawn rank from back rank0 to 6
30opp_passed_max_rankOpponent most advanced passed pawn rank0 to 6

Both contracts are mover-canonical: evaluating state from color perspective produces the identical vector as evaluating Symmetry.colorFlip(state) from color.opponent perspective. When evaluated for the side to move (state.activeColor), the vector matches that of the color-flipped state evaluated for its own mover.

In Dice Chess, a turn rolls 3 dice from a uniform 6-sided die {P,N,B,R,Q,K}\{P, N, B, R, Q, K\}. If a side has zero pseudo-legal moves for a piece type TT (and therefore zero legal moves), any die face showing TT cannot be used for that type.

For a single die roll, the probability that the rolled piece type has no move is:

P(unusable)=immobile_types6P(\text{unusable}) = \frac{\text{immobile\_types}}{6}

Across the 3 dice rolled in a turn, by linearity of expectation, the expected number of wasted dice is:

E[wasted rolls]=3×immobile_types6=12×immobile_types\mathbb{E}[\text{wasted rolls}] = 3 \times \frac{\text{immobile\_types}}{6} = \frac{1}{2} \times \text{immobile\_types}

The per-piece move counts own_moves_* and opp_moves_* directly expose whether each piece type is mobile (>0>0) or blocked (00).

  • Pawn Blockage: When own_moves_p == 0, friendly pawns are completely locked or absent. In Dice Chess, rolling pawn dice when pawns are blocked forces the player to burn turns or forgo actions.
  • Separate Tempo: Rather than collapsing mobility into a signed scalar (mobility_diff = own - opp), exposing own_moves_* and opp_moves_* separately allows non-linear models (neural nets) to evaluate asymmetries—for example, high mobility for both sides (tactical volatility) versus low mobility for both sides (closed deadlock).

[!IMPORTANT] Any change in column order, normalization, perspective, or semantic definition requires a new schema identifier (e.g. kcp-mobility-27-v2) and a corresponding newly trained model. Existing schemas must remain immutable to preserve reproducibility and avoid silent evaluation failures.

Microbenchmarks on OpenJDK 25 (Temurin 25.0.4) compare extraction overhead across the 5 standard benchmark positions (initial, kiwipete, endgame, castling, promotion):

BenchmarkPositionAverage Time (μs\mu s)
passedPawnsSummaryinitial / kiwipete / endgame / castling / promotion0.004 – 0.012 μs\mu s
pieceMobilityCountsinitial / kiwipete / endgame / castling / promotion0.101 – 0.436 μs\mu s
kcp (13 cols)promotion / endgame / castling / initial / kiwipete79 – 17,664 μs\mu s
kcpMobility27promotion / endgame / castling / initial / kiwipete82 – 17,466 μs\mu s
kcpMobilityPawns31promotion / endgame / castling / initial / kiwipete78 – 17,149 μs\mu s

The 216-outcome DFS search for king/queen capture probabilities in kcp dominates total extraction time (hundreds of microseconds to tens of milliseconds). The additional per-type move counting (0.100.44μs\sim 0.10\text{–}0.44\,\mu s) and passed-pawn bitboard probe (0.01μs\sim 0.01\,\mu s) add under 0.01%0.01\% overhead on complex positions (e.g. 0.44μs0.44\,\mu s on kiwipete’s 17.6ms17.6\,\text{ms}) and under 0.55%0.55\% on the fastest sparse positions (0.44μs0.44\,\mu s on promotion’s 79μs79\,\mu s).