fromFen static method

Tensor fromFen(
  1. String fen
)

Parse a FEN into a [1, 112, 8, 8] CPU tensor ready to feed into Lc0Net.

Implementation

static Tensor fromFen(String fen) {
  final parts = fen.trim().split(RegExp(r'\s+'));
  if (parts.isEmpty) {
    throw ArgumentError('Lc0Input: empty FEN');
  }
  final placement = parts[0];
  final stm = parts.length > 1 ? parts[1] : 'w';
  final castling = parts.length > 2 ? parts[2] : '-';
  final rule50 = parts.length > 4 ? int.tryParse(parts[4]) ?? 0 : 0;
  final fullMove = parts.length > 5 ? int.tryParse(parts[5]) ?? 1 : 1;

  if (stm != 'w' && stm != 'b') {
    throw ArgumentError('Lc0Input: side-to-move must be w or b, got "$stm"');
  }
  final blackToMove = stm == 'b';

  final data = Float32List(112 * 8 * 8);

  // Piece placement is given top-down (rank 8 first). Store into
  // whitePOV[rank][file] with rank 0 = white's back rank (a1),
  // rank 7 = black's back rank (a8).
  final ranks = placement.split('/');
  if (ranks.length != 8) {
    throw ArgumentError(
      'Lc0Input: expected 8 ranks in placement, got ${ranks.length}',
    );
  }
  // Fill piece planes 0..11 in white POV first, then mirror below
  // if it's black to move.
  for (int fenRank = 0; fenRank < 8; fenRank++) {
    final rank = 7 - fenRank; // rank index in white-POV coords
    final row = ranks[fenRank];
    int file = 0;
    for (int k = 0; k < row.length; k++) {
      final ch = row[k];
      final digit = int.tryParse(ch);
      if (digit != null) {
        file += digit;
        continue;
      }
      final idx = _pieceIndex[ch];
      if (idx == null) {
        throw ArgumentError('Lc0Input: bad FEN char "$ch"');
      }
      _set(data, idx, rank, file, 1.0);
      file++;
    }
  }

  // If black to move, mirror board vertically and swap piece colours
  // so STM's back rank sits at rank 0.
  if (blackToMove) {
    _mirrorForBlack(data);
  }

  // Castling rights, INPUT_CLASSICAL_112_PLANE layout (see
  // lc0/src/neural/encoder.cc EncodePositionForNN):
  //   plane 104 = our queenside (000)
  //   plane 105 = our kingside  (00)
  //   plane 106 = their queenside
  //   plane 107 = their kingside
  final wK = castling.contains('K');
  final wQ = castling.contains('Q');
  final bK = castling.contains('k');
  final bQ = castling.contains('q');
  final stmK = blackToMove ? bK : wK;
  final stmQ = blackToMove ? bQ : wQ;
  final oppK = blackToMove ? wK : bK;
  final oppQ = blackToMove ? wQ : bQ;
  if (stmQ) _fillPlane(data, 104, 1.0);
  if (stmK) _fillPlane(data, 105, 1.0);
  if (oppQ) _fillPlane(data, 106, 1.0);
  if (oppK) _fillPlane(data, 107, 1.0);

  // Plane 108 = "we_are_black" (1.0 iff STM is black).
  _fillPlane(data, 108, blackToMove ? 1.0 : 0.0);

  // Plane 109 = rule-50 half-move counter, RAW (not normalized) for
  // the classical INPUT_CLASSICAL_112_PLANE format. LC0 only
  // divides by 100 in the "hectoplies" input formats.
  _fillPlane(data, 109, rule50.toDouble());

  // Plane 110 used to be a movecount plane; the modern encoder
  // leaves it zero for the classical format.
  _fillPlane(data, 110, 0.0);

  // Plane 111 = constant ones (helps the network find board edges).
  _fillPlane(data, 111, 1.0);

  // fullMove parameter is intentionally unused for the classical
  // format — LC0's own encoder does not put it anywhere.
  // ignore: unused_local_variable
  final _ = fullMove;

  return Tensor.fromList([1, 112, 8, 8], data.toList(), device: Device.CPU);
}