Variant material diff (#2655)

This commit is contained in:
Tom Praschan
2026-02-20 23:15:50 +01:00
committed by GitHub
parent 0bd251ba3d
commit 89fef76550
13 changed files with 93 additions and 25 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ ExportedGame _archivedGameFromPick(RequiredPick pick, {bool withBookmarked = fal
GameStep(
sanMove: SanMove(san, move),
position: position,
diff: MaterialDiff.fromBoard(position.board),
diff: MaterialDiff.fromPosition(position),
archivedWhiteClock: index.isOdd ? stepClock : clock,
archivedBlackClock: index.isEven ? stepClock : clock,
),
+1 -1
View File
@@ -412,7 +412,7 @@ IList<GameStep> stepsFromJson(String json) {
GameStep(
position: position,
sanMove: SanMove(san, move),
diff: MaterialDiff.fromBoard(position.board),
diff: MaterialDiff.fromPosition(position),
),
);
}
+3 -3
View File
@@ -202,7 +202,7 @@ class GameController extends AsyncNotifier<GameState> {
final newStep = GameStep(
position: newPos,
sanMove: sanMove,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
state = AsyncValue.data(
@@ -286,7 +286,7 @@ class GameController extends AsyncNotifier<GameState> {
final newStep = GameStep(
position: newPos,
sanMove: sanMove,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
state = AsyncValue.data(
@@ -671,7 +671,7 @@ class GameController extends AsyncNotifier<GameState> {
final newStep = GameStep(
sanMove: sanMove,
position: newPos,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
newState = newState.copyWith(
+26 -8
View File
@@ -10,7 +10,13 @@ sealed class MaterialDiffSide with _$MaterialDiffSide {
required IMap<Role, int> pieces,
required int score,
required IMap<Role, int> capturedPieces,
/// Number of checks given by this side. Only relevant in the 3-check variant, null otherwise.
required int? checksGiven,
}) = _MaterialDiffSide;
factory MaterialDiffSide.empty() =>
MaterialDiffSide(pieces: IMap(), score: 0, capturedPieces: IMap(), checksGiven: null);
}
const IMap<Role, int> pieceScores = IMapConst({
@@ -29,15 +35,19 @@ sealed class MaterialDiff with _$MaterialDiff {
const factory MaterialDiff({required MaterialDiffSide black, required MaterialDiffSide white}) =
_MaterialDiff;
factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) {
int score = 0;
final IMap<Role, int> blackCount = board.materialCount(Side.black);
final IMap<Role, int> whiteCount = board.materialCount(Side.white);
factory MaterialDiff.fromPosition(Position position) {
if (position.rule == Rule.crazyhouse || position.rule == Rule.horde) {
return MaterialDiff(black: MaterialDiffSide.empty(), white: MaterialDiffSide.empty());
}
final IMap<Role, int> blackStartingCount =
startingPosition?.materialCount(Side.black) ?? Board.standard.materialCount(Side.black);
final IMap<Role, int> whiteStartingCount =
startingPosition?.materialCount(Side.white) ?? Board.standard.materialCount(Side.white);
int score = 0;
final IMap<Role, int> blackCount = position.board.materialCount(Side.black);
final IMap<Role, int> whiteCount = position.board.materialCount(Side.white);
final startingPosition = Position.initialPosition(position.rule);
final IMap<Role, int> blackStartingCount = startingPosition.board.materialCount(Side.black);
final IMap<Role, int> whiteStartingCount = startingPosition.board.materialCount(Side.white);
IMap<Role, int> subtractPieceCounts(
IMap<Role, int> startingCount,
@@ -95,16 +105,24 @@ sealed class MaterialDiff with _$MaterialDiff {
}
});
int? checksGiven(Side side) => switch (position) {
ThreeCheck(remainingChecks: (final white, final black)) =>
3 - (side == Side.white ? white : black),
_ => null,
};
return MaterialDiff(
black: MaterialDiffSide(
pieces: black.toIMap(),
score: -score,
capturedPieces: blackCapturedPieces,
checksGiven: checksGiven(Side.black),
),
white: MaterialDiffSide(
pieces: white.toIMap(),
score: score,
capturedPieces: whiteCapturedPieces,
checksGiven: checksGiven(Side.white),
),
);
}
+1 -1
View File
@@ -207,7 +207,7 @@ PlayableGame _playableGameFromPick(RequiredPick pick) {
GameStep(
sanMove: SanMove(san, move),
position: position,
diff: MaterialDiff.fromBoard(position.board),
diff: MaterialDiff.fromPosition(position),
),
);
}
@@ -168,7 +168,7 @@ class OfflineComputerGameController extends Notifier<OfflineComputerGameState> {
final newStep = GameStep(
position: newPos,
sanMove: sanMove,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
_clearHints();
@@ -61,7 +61,7 @@ class OverTheBoardGameController extends Notifier<OverTheBoardGameState> {
final newStep = GameStep(
position: newPos,
sanMove: sanMove,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
// In an over-the-board game, we support "implicit takebacks":
+1 -1
View File
@@ -213,7 +213,7 @@ class TvController extends AsyncNotifier<TvState> {
final newStep = GameStep(
sanMove: sanMove,
position: newPos,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
TvState newState = curState.copyWith(
@@ -332,7 +332,7 @@ class _BodyState extends ConsumerState<_Body> {
final newStep = GameStep(
position: newPos,
sanMove: sanMove,
diff: MaterialDiff.fromBoard(newPos.board),
diff: MaterialDiff.fromPosition(newPos),
);
setState(() {
+4 -2
View File
@@ -32,13 +32,15 @@ class MaterialDifferenceDisplay extends StatelessWidget {
: materialDiff!.pieces)
: IMap();
Icon roleIcon(Role role) => Icon(_iconByRole[role], size: 13, color: textShade(context, 0.5));
return materialDifferenceFormat?.visible ?? true
? Row(
mainAxisSize: MainAxisSize.min,
children: [
for (final role in Role.values)
for (int i = 0; i < (piecesToRender.get(role) ?? 0); i++)
Icon(_iconByRole[role], size: 13, color: textShade(context, 0.5)),
for (int i = 0; i < (piecesToRender.get(role) ?? 0); i++) roleIcon(role),
...Iterable.generate(materialDiff?.checksGiven ?? 0, (_) => roleIcon(Role.king)),
const SizedBox(width: 3),
Text(
// a text font size of 14 is used to ensure that the text will take more vertical space
+1 -1
View File
@@ -77,7 +77,7 @@ IList<GameStep> makeSteps(String pgn) {
GameStep(
position: position,
sanMove: SanMove(san, move),
diff: MaterialDiff.fromBoard(position.board),
diff: MaterialDiff.fromPosition(position),
),
);
}
+1 -1
View File
@@ -58,7 +58,7 @@ IList<GameStep> _makeSteps(String pgn) {
GameStep(
position: position,
sanMove: SanMove(san, move),
diff: MaterialDiff.fromBoard(position.board),
diff: MaterialDiff.fromPosition(position),
),
);
}
+51 -3
View File
@@ -5,9 +5,12 @@ import 'package:lichess_mobile/src/model/game/material_diff.dart';
void main() {
group('GameMaterialDiff', () {
test('generation from board', () {
final Board board = Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1');
final MaterialDiff diff = MaterialDiff.fromBoard(board);
test('generation from position', () {
final Position position = Position.setupPosition(
Rule.chess,
Setup.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'),
);
final MaterialDiff diff = MaterialDiff.fromPosition(position);
expect(diff.bySide(Side.black).score, equals(-10));
expect(diff.bySide(Side.white).score, equals(10));
@@ -63,6 +66,51 @@ void main() {
}),
),
);
expect(diff.bySide(Side.white).checksGiven, null);
expect(diff.bySide(Side.black).checksGiven, null);
});
test('three-check', () {
final Position position = Position.setupPosition(
Rule.threecheck,
Setup.parseFen('rnbqkbnr/ppp1pppp/3p4/1B6/4P3/8/PPPP1PPP/RNBQK1NR b KQkq - 2+3 1 2'),
);
final MaterialDiff diff = MaterialDiff.fromPosition(position);
expect(diff.bySide(Side.white).checksGiven, 1);
expect(diff.bySide(Side.black).checksGiven, 0);
});
test('horde returns empty material diff', () {
final Position position = Position.setupPosition(
Rule.horde,
Setup.parseFen('rnbqkbnr/pppppppp/8/8/pppppppp/pppppppp/PPPPPPPP/PPPPPPPP w kq - 0 1'),
);
final MaterialDiff diff = MaterialDiff.fromPosition(position);
for (final side in Side.values) {
final sideDiff = diff.bySide(side);
expect(sideDiff.score, 0);
expect(sideDiff.pieces.isEmpty, isTrue);
expect(sideDiff.capturedPieces.isEmpty, isTrue);
expect(sideDiff.checksGiven, null);
}
});
test('crazyhouse returns empty material diff', () {
final Position position = Position.setupPosition(
Rule.crazyhouse,
Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'),
);
final MaterialDiff diff = MaterialDiff.fromPosition(position);
for (final side in Side.values) {
final sideDiff = diff.bySide(side);
expect(sideDiff.score, 0);
expect(sideDiff.pieces.isEmpty, isTrue);
expect(sideDiff.capturedPieces.isEmpty, isTrue);
expect(sideDiff.checksGiven, null);
}
});
});
}