Skip to content

Commit 826ddb9

Browse files
committed
wip
1 parent f3b2ceb commit 826ddb9

File tree

14 files changed

+1042
-103
lines changed

14 files changed

+1042
-103
lines changed

lib/src/model/common/game.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'dart:math';
2+
3+
import 'package:dartchess/dartchess.dart';
14
import 'package:lichess_mobile/l10n/l10n.dart';
25

36
/// Represents the choice of a side as a player: white, black or random.
@@ -6,6 +9,15 @@ enum SideChoice {
69
random,
710
black;
811

12+
/// Generate a random side
13+
Side _randomSide() => Side.values[Random().nextInt(Side.values.length)];
14+
15+
Side get generateSide => switch (this) {
16+
SideChoice.white => Side.white,
17+
SideChoice.black => Side.black,
18+
SideChoice.random => _randomSide(),
19+
};
20+
921
String label(AppLocalizations l10n) => switch (this) {
1022
SideChoice.white => l10n.white,
1123
SideChoice.random => l10n.randomColor,
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import 'package:dartchess/dartchess.dart';
2+
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
3+
import 'package:flutter/foundation.dart';
4+
import 'package:freezed_annotation/freezed_annotation.dart';
5+
import 'package:lichess_mobile/src/model/common/chess.dart';
6+
import 'package:lichess_mobile/src/model/common/chess960.dart';
7+
import 'package:lichess_mobile/src/model/common/game.dart';
8+
import 'package:lichess_mobile/src/model/common/perf.dart';
9+
import 'package:lichess_mobile/src/model/common/service/move_feedback.dart';
10+
import 'package:lichess_mobile/src/model/common/speed.dart';
11+
import 'package:lichess_mobile/src/model/computer/computer_game.dart';
12+
import 'package:lichess_mobile/src/model/engine/evaluation_service.dart';
13+
import 'package:lichess_mobile/src/model/engine/work.dart';
14+
import 'package:lichess_mobile/src/model/game/game.dart';
15+
import 'package:lichess_mobile/src/model/game/game_status.dart';
16+
import 'package:lichess_mobile/src/model/game/material_diff.dart';
17+
import 'package:riverpod_annotation/riverpod_annotation.dart';
18+
19+
part 'computer_controller.freezed.dart';
20+
part 'computer_controller.g.dart';
21+
22+
@riverpod
23+
class ComputerGameController extends _$ComputerGameController {
24+
@override
25+
ComputerGameState build() {
26+
ref.onDispose(() {
27+
ref.read(evaluationServiceProvider).disposeEngine();
28+
});
29+
30+
state = ComputerGameState.fromUserChoice(
31+
StockfishLevel.one,
32+
SideChoice.white,
33+
Variant.standard,
34+
);
35+
36+
// start initializing the engine
37+
ref.read(evaluationServiceProvider).ensureEngineInitialized(state.evaluationContext);
38+
39+
if (state.game.youAre == Side.black) {
40+
_playComputerMove();
41+
}
42+
43+
return state;
44+
}
45+
46+
void _onNewGame() {
47+
ref.read(evaluationServiceProvider).newGame();
48+
if (state.game.youAre == Side.black) {
49+
_playComputerMove();
50+
}
51+
}
52+
53+
void _playMove(Move move, {bool isFromComputer = false}) {
54+
final (newPos, newSan) = state.currentPosition.makeSan(move);
55+
final sanMove = SanMove(newSan, move);
56+
final newStep = GameStep(
57+
position: newPos,
58+
sanMove: sanMove,
59+
diff: MaterialDiff.fromBoard(newPos.board),
60+
);
61+
62+
// to fix: if user goes through move list while computer is thinking, state is updated which breaks computer move
63+
if (isFromComputer) {
64+
state = state.copyWith(
65+
game: state.game.copyWith(steps: state.game.steps.add(newStep)),
66+
stepCursor: state.stepCursor + 1,
67+
);
68+
} else {
69+
// In an offline computer game, we support "implicit takebacks":
70+
// When going back one or more steps (i.e. stepCursor < game.steps.length - 1),
71+
// a new move can be made, removing all steps after the current stepCursor.
72+
state = state.copyWith(
73+
game: state.game.copyWith(
74+
steps: state.game.steps
75+
.removeRange(state.stepCursor + 1, state.game.steps.length)
76+
.add(newStep),
77+
),
78+
stepCursor: state.stepCursor + 1,
79+
);
80+
}
81+
82+
_checkGameResults();
83+
_moveFeedback(sanMove);
84+
}
85+
86+
Future<void> _playComputerMove() async {
87+
await ref.read(evaluationServiceProvider).ensureEngineInitialized(state.evaluationContext);
88+
89+
final move = await ref
90+
.read(evaluationServiceProvider)
91+
.startMoveWork(
92+
MoveWork(fen: state.game.lastPosition.fen, level: state.game.level, clock: null),
93+
);
94+
95+
// maybe should show that variant is not supported ?
96+
if (move == null) return;
97+
98+
_playMove(move, isFromComputer: true);
99+
}
100+
101+
void _checkGameResults() {
102+
// check for threefold repetition
103+
if (state.game.steps.count((p) => p.position.board == state.game.lastPosition.board) == 3) {
104+
state = state.copyWith(game: state.game.copyWith(isThreefoldRepetition: true));
105+
} else {
106+
state = state.copyWith(game: state.game.copyWith(isThreefoldRepetition: false));
107+
}
108+
109+
if (state.currentPosition.isCheckmate) {
110+
state = state.copyWith(
111+
game: state.game.copyWith(status: GameStatus.mate, winner: state.turn.opposite),
112+
);
113+
} else if (state.currentPosition.isStalemate) {
114+
state = state.copyWith(game: state.game.copyWith(status: GameStatus.stalemate));
115+
}
116+
}
117+
118+
void startNewGame(StockfishLevel level, SideChoice side, Variant variant) {
119+
state = ComputerGameState.fromUserChoice(level, side, variant);
120+
_onNewGame();
121+
}
122+
123+
void rematch() {
124+
state = ComputerGameState.fromUserChoice(
125+
state.game.level,
126+
state.game.sideChoice,
127+
state.game.meta.variant,
128+
);
129+
_onNewGame();
130+
}
131+
132+
void resign() {
133+
state = state.copyWith(
134+
game: state.game.copyWith(status: GameStatus.resign, winner: state.turn.opposite),
135+
);
136+
}
137+
138+
void draw() {
139+
state = state.copyWith(game: state.game.copyWith(status: GameStatus.draw));
140+
}
141+
142+
Future<void> onUserMove(NormalMove move) async {
143+
if (isPromotionPawnMove(state.currentPosition, move)) {
144+
state = state.copyWith(promotionMove: move);
145+
return;
146+
}
147+
148+
_playMove(move);
149+
150+
if (!state.finished) {
151+
return _playComputerMove();
152+
}
153+
}
154+
155+
void onPromotionSelection(Role? role) {
156+
if (role == null) {
157+
state = state.copyWith(promotionMove: null);
158+
return;
159+
}
160+
final promotionMove = state.promotionMove;
161+
if (promotionMove != null) {
162+
final move = promotionMove.withPromotion(role);
163+
onUserMove(move);
164+
state = state.copyWith(promotionMove: null);
165+
}
166+
}
167+
168+
void onFlag(Side side) {
169+
state = state.copyWith(
170+
game: state.game.copyWith(status: GameStatus.outoftime, winner: side.opposite),
171+
);
172+
}
173+
174+
void goForward() {
175+
if (state.canGoForward) {
176+
state = state.copyWith(stepCursor: state.stepCursor + 1, promotionMove: null);
177+
}
178+
}
179+
180+
void goBack() {
181+
if (state.canGoBack) {
182+
state = state.copyWith(stepCursor: state.stepCursor - 1, promotionMove: null);
183+
}
184+
}
185+
186+
void _moveFeedback(SanMove sanMove) {
187+
final isCheck = sanMove.san.contains('+');
188+
if (sanMove.san.contains('x')) {
189+
ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck);
190+
} else {
191+
ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck);
192+
}
193+
}
194+
}
195+
196+
@freezed
197+
sealed class ComputerGameState with _$ComputerGameState {
198+
const ComputerGameState._();
199+
200+
const factory ComputerGameState({
201+
required ComputerGame game,
202+
required EvaluationContext evaluationContext,
203+
@Default(0) int stepCursor,
204+
@Default(null) NormalMove? promotionMove,
205+
}) = _ComputerGameState;
206+
207+
factory ComputerGameState.fromUserChoice(
208+
StockfishLevel level,
209+
SideChoice sideChoice,
210+
Variant variant,
211+
) {
212+
// check if we can get rid of speed and perf
213+
// this is not an online game and time is unlimited
214+
const speed = Speed.correspondence;
215+
final position = variant == Variant.chess960
216+
? randomChess960Position()
217+
: variant.initialPosition;
218+
return ComputerGameState(
219+
evaluationContext: EvaluationContext(variant: variant, initialPosition: position),
220+
game: ComputerGame(
221+
steps: [GameStep(position: position)].lock,
222+
status: GameStatus.started,
223+
initialFen: position.fen,
224+
level: level,
225+
sideChoice: sideChoice,
226+
youAre: sideChoice.generateSide,
227+
meta: GameMeta(
228+
createdAt: DateTime.now(),
229+
rated: false,
230+
variant: variant,
231+
speed: speed,
232+
perf: Perf.fromVariantAndSpeed(variant, speed),
233+
),
234+
),
235+
);
236+
}
237+
238+
Position get currentPosition => game.stepAt(stepCursor).position;
239+
Side get turn => currentPosition.turn;
240+
bool get finished => game.finished;
241+
NormalMove? get lastMove =>
242+
stepCursor > 0 ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) : null;
243+
244+
MaterialDiffSide? currentMaterialDiff(Side side) {
245+
return game.steps[stepCursor].diff?.bySide(side);
246+
}
247+
248+
List<String> get moves => game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false);
249+
250+
bool get canGoForward => stepCursor < game.steps.length - 1;
251+
bool get canGoBack => stepCursor > 0;
252+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:dartchess/dartchess.dart';
2+
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
3+
import 'package:freezed_annotation/freezed_annotation.dart';
4+
import 'package:lichess_mobile/l10n/l10n.dart';
5+
import 'package:lichess_mobile/src/model/common/eval.dart';
6+
import 'package:lichess_mobile/src/model/common/game.dart';
7+
import 'package:lichess_mobile/src/model/common/id.dart';
8+
import 'package:lichess_mobile/src/model/game/game.dart';
9+
import 'package:lichess_mobile/src/model/game/game_status.dart';
10+
import 'package:lichess_mobile/src/model/game/player.dart';
11+
import 'package:lichess_mobile/src/utils/string.dart';
12+
13+
part 'computer_game.freezed.dart';
14+
part 'computer_game.g.dart';
15+
16+
enum StockfishLevel {
17+
// Values taken from lichobile https://github.com/lichess-org/lichobile/blob/663e69fab10e4267a9b3369febe85d1363816ba2/src/ui/ai/engine.ts#L71-L108
18+
one(1350, 5),
19+
two(1500, 5),
20+
three(1600, 5),
21+
four(1700, 5),
22+
five(2000, 5),
23+
six(2300, 8),
24+
seven(2700, 13),
25+
eight(2850, 22);
26+
27+
const StockfishLevel(this.elo, this.depth);
28+
29+
final int elo;
30+
final int depth;
31+
32+
static const _maxMoveTime = Duration(milliseconds: 5000);
33+
34+
int get value => index + 1;
35+
36+
Duration get moveTime => (_maxMoveTime * value) ~/ 8;
37+
38+
String label(AppLocalizations l10n) => l10n.aiNameLevelAiLevel('Stockfish', value.toString());
39+
}
40+
41+
/// An offline game played against Sockfish.
42+
@Freezed(fromJson: true, toJson: true)
43+
abstract class ComputerGame with _$ComputerGame, BaseGame, IndexableSteps {
44+
const ComputerGame._();
45+
46+
@Assert('steps.isNotEmpty')
47+
factory ComputerGame({
48+
@JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList<GameStep> steps,
49+
required GameMeta meta,
50+
required String? initialFen,
51+
required GameStatus status,
52+
required StockfishLevel level,
53+
required SideChoice sideChoice,
54+
// can't be null for a computer game but I don't know how to make the type not nullable
55+
Side? youAre,
56+
Side? winner,
57+
bool? isThreefoldRepetition,
58+
}) = _ComputerGame;
59+
60+
@override
61+
Player get white => _playerFromSide(Side.white);
62+
63+
@override
64+
Player get black => _playerFromSide(Side.black);
65+
66+
Player _playerFromSide(Side side) =>
67+
(youAre == side) ? Player(name: side.name.capitalize()) : Player(aiLevel: level.value);
68+
69+
@override
70+
IList<ExternalEval>? get evals => null;
71+
72+
@override
73+
IList<Duration>? get clocks => null;
74+
75+
@override
76+
GameId get id => const GameId('--------');
77+
78+
bool get abortable => playable && lastPosition.fullmoves <= 1;
79+
80+
bool get resignable => playable && !abortable;
81+
bool get drawable => playable && lastPosition.fullmoves >= 2;
82+
}

0 commit comments

Comments
 (0)