Stack at searcher
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
export 'src/dartsunfish.dart';
|
||||
export 'src/extentions.dart';
|
||||
export 'src/globals.dart';
|
||||
export 'src/game_play.dart';
|
||||
export 'src/helpers.dart';
|
||||
export 'src/position.dart';
|
||||
export 'src/searcher.dart';
|
||||
export 'src/ui.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:developer' as dv;
|
||||
import 'package:dartsunfish/index.dart';
|
||||
|
||||
class GameBot {
|
||||
@@ -20,6 +19,7 @@ class GameBot {
|
||||
|
||||
// We query the user until she enters a (pseudo) legal move.
|
||||
var move = <int>[];
|
||||
var score = 0;
|
||||
final possibleMoves = hist.last.generateMoves().toList();
|
||||
while (!possibleMoves.doContains(move)) {
|
||||
var pattern = RegExp(
|
||||
@@ -55,16 +55,15 @@ class GameBot {
|
||||
print('You won');
|
||||
break;
|
||||
}
|
||||
// Fire up the engine to look for a move.
|
||||
var _depth = 0;
|
||||
var score = 0;
|
||||
var start = getSeconds();
|
||||
/*
|
||||
*/
|
||||
// Fire up the engine to look for a move.
|
||||
// var _depth = 0;
|
||||
var start = getSeconds();
|
||||
for (var s in searcher.search(hist.last, hist.toSet())) {
|
||||
_depth = s[0];
|
||||
move = s[1];
|
||||
score = s[2] ?? 0;
|
||||
// _depth = s[0];
|
||||
move = s[0];
|
||||
score = s[1] ?? 0;
|
||||
if (getSeconds() - start > 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,68 @@ extension CheckString on String {
|
||||
extension CheckListOfTuples on List {
|
||||
bool doContains(newList) => newList.length == 2 && indexWhere((l) => l[0] == newList[0] && l[1] == newList[1]) > -1;
|
||||
}
|
||||
|
||||
extension CompareIretables<E> on Iterable<E> {
|
||||
bool isEqual(Iterable newList) {
|
||||
if (newList is! Iterable) return false;
|
||||
if (newList.length != length || runtimeType != newList.runtimeType) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (elementAt(i) is Iterable) {
|
||||
print('In iterrable ${(elementAt(i) as Iterable).isEqual(newList.elementAt(i) as Iterable)}');
|
||||
if ((elementAt(i) as Iterable).isEqual(newList.elementAt(i) as Iterable) == false) return false;
|
||||
} else if (elementAt(i).runtimeType != newList.elementAt(i).runtimeType || elementAt(i) != newList.elementAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Iterable<T> mapIndexed<T>(T Function(E e, int i) f) {
|
||||
var i = 0;
|
||||
return map((e) => f(e, i++));
|
||||
}
|
||||
|
||||
void forEachIndexed(void Function(E e, int i) f) {
|
||||
var i = 0;
|
||||
forEach((e) => f(e, i++));
|
||||
}
|
||||
}
|
||||
|
||||
extension XDartMap on Map {
|
||||
T? get<T>(Object? key, {Object? or}) {
|
||||
var tempMap = this;
|
||||
var result = or;
|
||||
if (key == null) {
|
||||
return null;
|
||||
} else {
|
||||
for (final item in tempMap.keys) {
|
||||
if (item.toString() == key.toString()) {
|
||||
result = tempMap[item];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result != null) {
|
||||
return result as T;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isEqual(Map newMap) {
|
||||
if (newMap is! Map) return false;
|
||||
if (!keys.isEqual(newMap.keys)) return false;
|
||||
for (var key in keys) {
|
||||
if (this[key] is Iterable) {
|
||||
if ((this[key] as Iterable).isEqual(newMap[key] as Iterable) == false) {
|
||||
return false;
|
||||
}
|
||||
} else if (this[key] != newMap[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dartsunfish/index.dart';
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Chess logic
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class Position {
|
||||
Position(
|
||||
this.board,
|
||||
this.score,
|
||||
this.wc,
|
||||
this.bc,
|
||||
this.ep,
|
||||
this.kp,
|
||||
);
|
||||
String board;
|
||||
int score;
|
||||
List<bool> wc;
|
||||
List<bool> bc;
|
||||
int ep;
|
||||
int kp;
|
||||
/*
|
||||
""" A state of a chess game
|
||||
board -- a 120 char representation of the board
|
||||
score -- the board evaluation
|
||||
wc -- the castling rights, [west/queen side, east/king side]
|
||||
bc -- the opponent castling rights, [west/king side, east/queen side]
|
||||
ep - the en passant square
|
||||
kp - the king passant square
|
||||
""";
|
||||
*/
|
||||
Iterable<List<int>> generateMoves() sync* {
|
||||
// For each of our pieces, iterate through each possible 'ray' of moves,
|
||||
// as defined in the 'directions' map. The rays are broken e.g. by
|
||||
// captures or immediately in case of pieces such as knights.
|
||||
//TODO: add limitation of 3 cells for Wizard
|
||||
final charsList = board.split('');
|
||||
for (var i = 0; i < board.length; i++) {
|
||||
final p = charsList[i];
|
||||
// yield ([i, 12]);
|
||||
// }
|
||||
// board.split('').asMap().forEach((i, p) sync* {
|
||||
if (!p.isUpper) {
|
||||
continue;
|
||||
}
|
||||
for (var d in directions[p]!) {
|
||||
for (var j in count(i + d, d)) {
|
||||
final q = board[j];
|
||||
// Stay inside the board, and off friendly pieces
|
||||
if (q.isSpace || q.isUpper) {
|
||||
break;
|
||||
}
|
||||
// Pawn move, double move and capture
|
||||
if (p == 'P' && [N, N + N].contains(d) && q != '.') {
|
||||
break;
|
||||
}
|
||||
if (p == 'P' && d == (N + N) && (i < A1 + N || board[i + N] != '.')) {
|
||||
break;
|
||||
}
|
||||
if (p == 'P' && [N + W, N + E].contains(d) && q == '.' && ![ep, kp, kp - 1, kp + 1].contains(j)) {
|
||||
break;
|
||||
}
|
||||
// Move it
|
||||
final res = [i, j];
|
||||
yield (res);
|
||||
|
||||
// Stop crawlers from sliding, and sliding after captures
|
||||
if ('PNK'.contains(p) || q.isLower) {
|
||||
break;
|
||||
}
|
||||
// Castling, by sliding the rook next to the king
|
||||
if (i == A1 && board[j + E] == 'K' && wc[0]) {
|
||||
yield ([j + E, j + W]);
|
||||
}
|
||||
|
||||
if (i == H1 && board[j + W] == 'K' && wc[1]) {
|
||||
yield ([j + W, j + E]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String reversed(String subject) {
|
||||
// return subject.split('').reversed.join();
|
||||
// }
|
||||
|
||||
String swapCase(String subject) {
|
||||
if (subject is! String || subject.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
String _swap(String l) {
|
||||
if (l.toUpperCase() == l) {
|
||||
return l.toLowerCase();
|
||||
} else {
|
||||
return l.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
var subjectChars = subject.split('');
|
||||
|
||||
return subjectChars.map((x) => _swap(x)).join();
|
||||
}
|
||||
|
||||
Position rotate() {
|
||||
''' Rotates the board, preserving enpassant ''';
|
||||
return Position(board.reversedAndSwapCased, -score, bc, wc, ep > 0 ? 119 - ep : 0, kp > 0 ? 119 - kp : 0);
|
||||
}
|
||||
|
||||
Position nullmove() {
|
||||
''' Like rotate, but clears ep and kp ''';
|
||||
return Position(board.reversedAndSwapCased, -score, bc, wc, 0, 0);
|
||||
}
|
||||
|
||||
String put(String _str, int i, String p) {
|
||||
final newStr = _str.substring(0, i) + p + _str.substring(i + 1);
|
||||
return newStr;
|
||||
}
|
||||
|
||||
// move piece due to the move scecification
|
||||
Position movePiece(List<int> move) {
|
||||
final i = move[0], j = move[1];
|
||||
final p = board[i];
|
||||
final q = board[j];
|
||||
// Copy variables and reset ep and kp
|
||||
var _board = board;
|
||||
var _wc = wc, _bc = bc, _ep = 0, _kp = 0;
|
||||
score = score + value(move);
|
||||
// Actual move
|
||||
board = put(_board, j, p);
|
||||
board = put(board, i, '.');
|
||||
|
||||
// Castling rights, we move the rook or capture the opponent's
|
||||
if (i == A1) {
|
||||
_wc = [false, wc[1]];
|
||||
}
|
||||
if (i == H1) {
|
||||
_wc = [wc[0], false];
|
||||
}
|
||||
if (j == A8) {
|
||||
bc = [bc[0], false];
|
||||
}
|
||||
if (j == H8) {
|
||||
bc = [false, bc[1]];
|
||||
}
|
||||
// Castling
|
||||
if (p == 'K') {
|
||||
_wc = [false, false];
|
||||
if ((j - i).abs() == 2) {
|
||||
kp = (i + j); //2
|
||||
board = put(board, (j < i) ? A1 : H1, '.');
|
||||
board = put(board, kp, 'R');
|
||||
}
|
||||
}
|
||||
// Pawn promotion, double move and en passant capture
|
||||
if (p == 'P') {
|
||||
if (A8 <= j && j <= H8) {
|
||||
board = put(board, j, 'Q');
|
||||
}
|
||||
if (j - i == 2 * N) {
|
||||
ep = i + N;
|
||||
}
|
||||
if (j == ep) {
|
||||
board = put(board, j + S, '.');
|
||||
}
|
||||
}
|
||||
// We rotate the returned position, so it's ready for the next player
|
||||
return Position(board, score, _wc, _bc, _ep, _kp).rotate();
|
||||
}
|
||||
|
||||
// calculate score of the board position
|
||||
int value(List<int> move) {
|
||||
final i = move[0], j = move[1];
|
||||
final p = board[i], q = board[j];
|
||||
// Actual move
|
||||
score = pst[p] != null ? pst[p]![j] - pst[p]![i] : 0;
|
||||
// Capture
|
||||
if (!q.isUpper) {
|
||||
score += pst[q.toUpperCase()] != null ? pst[q.toUpperCase()]![119 - j] : 0;
|
||||
}
|
||||
// Castling check detection
|
||||
if ((j - kp).abs() < 2) {
|
||||
score += pst['K']![119 - j];
|
||||
}
|
||||
// Castling
|
||||
if (p == 'K' && (i - j).abs() == 2) {
|
||||
score += pst['R']![(i + j)]; //2]
|
||||
score -= pst['R']![j < i ? A1 : H1];
|
||||
}
|
||||
// Special pawn stuff
|
||||
if (p == 'P') {
|
||||
if (A8 <= j && j <= H8) {
|
||||
score += pst['Q']![j] - pst['P']![j];
|
||||
}
|
||||
if (j == ep) {
|
||||
score += pst['P']![119 - (j + S)];
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
/// python counter immulation
|
||||
Iterable<int> count(int firstval, int step) sync* {
|
||||
var x = firstval;
|
||||
while (true) {
|
||||
yield (x);
|
||||
x += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Search logic
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// lower <= s(pos) <= upper
|
||||
class Entry {
|
||||
Entry(this.lower, this.upper);
|
||||
int lower;
|
||||
int upper;
|
||||
}
|
||||
|
||||
class Searcher {
|
||||
Searcher();
|
||||
|
||||
late Map<List, Entry> tpScore = {};
|
||||
late Map<Position, List<int>?> tpMove = {};
|
||||
late Set<Position> history;
|
||||
// int nodes = 0;
|
||||
|
||||
int bound(Position pos, int gamma, int depth, {bool root = true}) {
|
||||
''' returns r where
|
||||
s(pos) <= r < gamma if gamma > s(pos)
|
||||
gamma <= r <= s(pos) if gamma <= s(pos)''';
|
||||
// nodes += 1;
|
||||
|
||||
// Depth <= 0 is QSearch. Here any position is searched as deeply as is needed for
|
||||
// calmness, and from this point on there is no difference in behaviour depending on
|
||||
// depth, so so there is no reason to keep different depths in the transposition table.
|
||||
depth = max(depth, 0);
|
||||
|
||||
// Sunfish is a king-capture engine, so we should always check if we
|
||||
// still have a king. Notice since this is the only termination check,
|
||||
// the remaining code has to be comfortable with being mated, stalemated
|
||||
// or able to capture the opponent king.
|
||||
if (pos.score <= -MATE_LOWER) {
|
||||
return -MATE_UPPER;
|
||||
}
|
||||
|
||||
// We detect 3-fold captures by comparing against previously
|
||||
// _actually played_ positions.
|
||||
// Note that we need to do this before we look in the table, as the
|
||||
// position may have been previously reached with a different score.
|
||||
// This is what prevents a search instability.
|
||||
// FIXME: This is not true, since other positions will be affected by
|
||||
// the new values for all the drawn positions.
|
||||
if (DRAW_TEST) {
|
||||
if (!root && history.contains(pos)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Look in the table if we have already searched this position before.
|
||||
// We also need to be sure, that the stored search was over the same
|
||||
// nodes as the current search.
|
||||
var entry = tpScore[[pos, depth, root]] ?? Entry(-MATE_UPPER, MATE_UPPER);
|
||||
if (entry.lower >= gamma && (!root || tpMove[pos] != null)) {
|
||||
return entry.lower;
|
||||
}
|
||||
if (entry.upper < gamma) {
|
||||
return entry.upper;
|
||||
}
|
||||
// Here extensions may be added
|
||||
// Such as 'if in_check: depth += 1'
|
||||
|
||||
// Generator of moves to search in order.
|
||||
// This allows us to define the moves, but only calculate them if needed.
|
||||
Iterable<List<dynamic>> moves() sync* {
|
||||
// First try not moving at all. We only do this if there is at least one major
|
||||
// piece left on the board, since otherwise zugzwangs are too dangerous.
|
||||
// if (depth > 0 && !root && any(c in pos.board for c in 'RBNQ')){
|
||||
if (depth > 0 && !root && pos.board.split('').any((c) => 'RBNQ'.contains(c))) {
|
||||
// print('post 1st condition');
|
||||
yield ([null, -1 * bound(pos.nullmove(), 1 - gamma, depth - 3, root: false)]);
|
||||
}
|
||||
|
||||
// For QSearch we have a different kind of null-move, namely we can just stop
|
||||
// and not capture anything else.
|
||||
if (depth == 0) {
|
||||
// print('post 2nd condition');
|
||||
yield ([null, pos.score]);
|
||||
}
|
||||
// Then killer move. We search it twice, but the tp will fix things for us.
|
||||
// Note, we don't have to check for legality, since we've already done it
|
||||
// before. Also note that in QS the killer must be a capture, otherwise we
|
||||
// will be non deterministic.
|
||||
var killer = tpMove[pos];
|
||||
if (killer != null && (depth > 0 || pos.value(killer) >= QS_LIMIT)) {
|
||||
// print('post killer');
|
||||
yield ([killer, -1 * bound(pos.movePiece(killer), 1 - gamma, depth - 1, root: false)]);
|
||||
}
|
||||
// Then all the other moves
|
||||
var possibleMoves = pos.generateMoves().toList()
|
||||
..sort((a, b) => pos.value(a).compareTo(pos.value(b)))
|
||||
..toList(); //.reversed;
|
||||
for (var move in possibleMoves.reversed) {
|
||||
//pos.generateMoves().toList().sort((a, b) => a.value.compareto(b.value)).toList().reversed)) {
|
||||
// for val, move in sorted(((pos.value(move), move) for move in pos.generateMoves()), reverse=true):
|
||||
// If depth == 0 we only try moves with high intrinsic score (captures and
|
||||
// promotions). Otherwise we do all moves.
|
||||
if (depth > 0 || pos.value(move) >= QS_LIMIT) {
|
||||
yield ([move, -1 * bound(pos.movePiece(move), 1 - gamma, depth - 1, root: false)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run through the moves, shortcutting when possible
|
||||
var best = -MATE_UPPER;
|
||||
for (var m in moves()) {
|
||||
List<int>? move = m[0];
|
||||
int score = m[1];
|
||||
best = max(best, score);
|
||||
// if (depth == 0){
|
||||
// print([
|
||||
// 'Depth',
|
||||
// depth,
|
||||
// 'Move',
|
||||
// move != null ? UI.render(119 - move[0]) + UI.render(119 - move[1]) : 'No move',
|
||||
// 'Score',
|
||||
// score,
|
||||
// 'Best',
|
||||
// best,
|
||||
// 'Gamma',
|
||||
// gamma,
|
||||
// 'STOP',
|
||||
// best >= gamma
|
||||
// ]);}
|
||||
if (best >= gamma) {
|
||||
// Clear before setting, so we always have a value
|
||||
if (tpMove.length > TABLE_SIZE) {
|
||||
tpMove.clear();
|
||||
}
|
||||
// Save the move for pv construction and killer heuristic
|
||||
tpMove[pos] = move;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Stalemate checking is a bit tricky: Say we failed low, because
|
||||
// we can't (legally) move and so the (real) score is -infty.
|
||||
// At the next depth we are allowed to just return r, -infty <= r < gamma,
|
||||
// which is normally fine.
|
||||
// However, what if gamma = -10 and we don't have any legal moves?
|
||||
// Then the score is actaully a draw and we should fail high!
|
||||
// Thus, if best < gamma and best < 0 we need to double check what we are doing.
|
||||
// This doesn't prevent sunfish from making a move that results in stalemate,
|
||||
// but only if depth == 1, so that's probably fair enough.
|
||||
// (Btw, at depth 1 we can also mate without realizing.)
|
||||
if (best < gamma && best < 0 && depth > 0) {
|
||||
bool is_dead(pos) {
|
||||
return pos.value.any((m) => m >= MATE_LOWER);
|
||||
}
|
||||
|
||||
// if (all(is_dead(pos.move(m)) for m in pos.generateMoves())){
|
||||
if (pos.generateMoves().every((m) => is_dead(pos.movePiece(m)))) {
|
||||
var in_check = is_dead(pos.nullmove());
|
||||
best = in_check ? -MATE_UPPER : 0;
|
||||
}
|
||||
}
|
||||
// Clear before setting, so we always have a value
|
||||
if (tpScore.length > TABLE_SIZE) {
|
||||
tpScore.clear();
|
||||
}
|
||||
// Table part 2
|
||||
if (best >= gamma) {
|
||||
tpScore[[pos, depth, root]] = Entry(best, entry.upper);
|
||||
}
|
||||
if (best < gamma) {
|
||||
tpScore[[pos, depth, root]] = Entry(entry.lower, best);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
Iterable<dynamic> search(Position pos, Set<Position> _history) sync* {
|
||||
''' Iterative deepening MTD-bi search ''';
|
||||
// nodes = 0;
|
||||
|
||||
if (DRAW_TEST) {
|
||||
history = _history;
|
||||
// Clearing table due to new history
|
||||
tpScore.clear();
|
||||
}
|
||||
// In finished games, we could potentially go far enough to cause a recursion
|
||||
// limit exception. Hence we bound the ply.
|
||||
for (var depth = 1; depth < 1000; depth++) {
|
||||
// The inner loop is a binary search on the score of the position.
|
||||
// Inv: lower <= score <= upper
|
||||
// 'while lower != upper' would work, but play tests show a margin of 20 plays
|
||||
// better.
|
||||
var lower = -MATE_UPPER;
|
||||
var upper = MATE_UPPER;
|
||||
while (lower < upper - EVAL_ROUGHNESS) {
|
||||
var gamma = (lower + upper + 1) ~/ 2;
|
||||
var score = bound(pos, gamma, depth);
|
||||
if (score >= gamma) lower = score;
|
||||
if (score < gamma) upper = score;
|
||||
}
|
||||
// We want to make sure the move to play hasn't been kicked out of the table,
|
||||
// So we make another call that must always fail high and thus produce a move.
|
||||
// bound(pos, lower, depth);
|
||||
// If the game hasn't finished we can retrieve our move from the
|
||||
// transposition table.
|
||||
yield ([
|
||||
depth,
|
||||
tpMove[pos],
|
||||
tpScore[[pos, depth, true]]?.lower
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/src/helpers.dart
Normal file
29
lib/src/helpers.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
/// python counter immulation
|
||||
Iterable<int> count(int firstval, int step) sync* {
|
||||
var x = firstval;
|
||||
while (true) {
|
||||
yield (x);
|
||||
x += step;
|
||||
}
|
||||
}
|
||||
|
||||
String reversed(String subject) {
|
||||
return subject.split('').reversed.join();
|
||||
}
|
||||
|
||||
String swapCase(String subject) {
|
||||
if (subject is! String || subject.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
String _swap(String l) {
|
||||
if (l.toUpperCase() == l) {
|
||||
return l.toLowerCase();
|
||||
} else {
|
||||
return l.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
var subjectChars = subject.split('');
|
||||
|
||||
return subjectChars.map((x) => _swap(x)).join();
|
||||
}
|
||||
178
lib/src/position.dart
Normal file
178
lib/src/position.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dartsunfish/index.dart';
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Chess logic
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class Position {
|
||||
Position(
|
||||
this.board,
|
||||
this.score,
|
||||
this.wc,
|
||||
this.bc,
|
||||
this.ep,
|
||||
this.kp,
|
||||
);
|
||||
String board;
|
||||
int score, ep, kp;
|
||||
List<bool> wc, bc;
|
||||
/*
|
||||
""" A state of a chess game
|
||||
board -- a 120 char representation of the board
|
||||
score -- the board evaluation
|
||||
wc -- the castling rights, [west/queen side, east/king side]
|
||||
bc -- the opponent castling rights, [west/king side, east/queen side]
|
||||
ep - the en passant square
|
||||
kp - the king passant square
|
||||
""";
|
||||
*/
|
||||
Iterable<List<int>> generateMoves() sync* {
|
||||
// For each of our pieces, iterate through each possible 'ray' of moves,
|
||||
// as defined in the 'directions' map. The rays are broken e.g. by
|
||||
// captures or immediately in case of pieces such as knights.
|
||||
/// TODO: add limitation of 3 cells for Wizard
|
||||
// Split boadr string into list of chars
|
||||
final charsList = board.split('');
|
||||
// Iterate over list of chars
|
||||
for (var i = 0; i < charsList.length; i++) {
|
||||
final p = charsList[i];
|
||||
if (!p.isUpper) {
|
||||
continue;
|
||||
}
|
||||
for (var d in directions[p]!) {
|
||||
for (var j in count(i + d, d)) {
|
||||
final q = board[j];
|
||||
// Stay inside the board, and off friendly pieces
|
||||
if (q.isSpace || q.isUpper) {
|
||||
break;
|
||||
}
|
||||
// Pawn move, double move and capture
|
||||
if (p == 'P' && [N, N + N].contains(d) && q != '.') {
|
||||
break;
|
||||
}
|
||||
if (p == 'P' && d == (N + N) && (i < A1 + N || board[i + N] != '.')) {
|
||||
break;
|
||||
}
|
||||
if (p == 'P' && [N + W, N + E].contains(d) && q == '.' && ![ep, kp, kp - 1, kp + 1].contains(j)) {
|
||||
break;
|
||||
}
|
||||
// Move it
|
||||
final res = [i, j];
|
||||
yield (res);
|
||||
|
||||
// Stop crawlers from sliding, and sliding after captures
|
||||
if ('PNK'.contains(p) || q.isLower) {
|
||||
break;
|
||||
}
|
||||
// Castling, by sliding the rook next to the king
|
||||
if (i == A1 && board[j + E] == 'K' && wc[0]) {
|
||||
yield ([j + E, j + W]);
|
||||
}
|
||||
|
||||
if (i == H1 && board[j + W] == 'K' && wc[1]) {
|
||||
yield ([j + W, j + E]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Position rotate() {
|
||||
''' Rotates the board, preserving enpassant ''';
|
||||
return Position(board.reversedAndSwapCased, -score, bc, wc, ep > 0 ? 119 - ep : 0, kp > 0 ? 119 - kp : 0);
|
||||
}
|
||||
|
||||
Position nullmove() {
|
||||
''' Like rotate, but clears ep and kp ''';
|
||||
return Position(board.reversedAndSwapCased, -score, bc, wc, 0, 0);
|
||||
}
|
||||
|
||||
String put(String _str, int i, String p) {
|
||||
final newStr = _str.substring(0, i) + p + _str.substring(i + 1);
|
||||
return newStr;
|
||||
}
|
||||
|
||||
// move piece due to the move scecification
|
||||
Position movePiece(List<int> move) {
|
||||
final i = move[0], j = move[1];
|
||||
final p = board[i];
|
||||
final q = board[j];
|
||||
// Copy variables and reset ep and kp
|
||||
var _board = board;
|
||||
var _wc = wc, _bc = bc, _ep = 0, _kp = 0;
|
||||
score = score + value(move);
|
||||
// Actual move
|
||||
board = put(_board, j, p);
|
||||
board = put(board, i, '.');
|
||||
|
||||
// Castling rights, we move the rook or capture the opponent's
|
||||
if (i == A1) {
|
||||
_wc = [false, wc[1]];
|
||||
}
|
||||
if (i == H1) {
|
||||
_wc = [wc[0], false];
|
||||
}
|
||||
if (j == A8) {
|
||||
bc = [bc[0], false];
|
||||
}
|
||||
if (j == H8) {
|
||||
bc = [false, bc[1]];
|
||||
}
|
||||
// Castling
|
||||
if (p == 'K') {
|
||||
_wc = [false, false];
|
||||
if ((j - i).abs() == 2) {
|
||||
kp = (i + j); //2
|
||||
board = put(board, (j < i) ? A1 : H1, '.');
|
||||
board = put(board, kp, 'R');
|
||||
}
|
||||
}
|
||||
// Pawn promotion, double move and en passant capture
|
||||
if (p == 'P') {
|
||||
if (A8 <= j && j <= H8) {
|
||||
board = put(board, j, 'Q');
|
||||
}
|
||||
if (j - i == 2 * N) {
|
||||
ep = i + N;
|
||||
}
|
||||
if (j == ep) {
|
||||
board = put(board, j + S, '.');
|
||||
}
|
||||
}
|
||||
// We rotate the returned position, so it's ready for the next player
|
||||
return Position(board, score, _wc, _bc, _ep, _kp).rotate();
|
||||
}
|
||||
|
||||
// calculate score of the board position
|
||||
int value(List<int> move) {
|
||||
final i = move[0], j = move[1];
|
||||
final p = board[i], q = board[j];
|
||||
// Actual move
|
||||
score = pst[p] != null ? pst[p]![j] - pst[p]![i] : 0;
|
||||
// Capture
|
||||
if (!q.isUpper) {
|
||||
score += pst[q.toUpperCase()] != null ? pst[q.toUpperCase()]![119 - j] : 0;
|
||||
}
|
||||
// Castling check detection
|
||||
if ((j - kp).abs() < 2) {
|
||||
score += pst['K']![119 - j];
|
||||
}
|
||||
// Castling
|
||||
if (p == 'K' && (i - j).abs() == 2) {
|
||||
score += pst['R']![(i + j)]; //2]
|
||||
score -= pst['R']![j < i ? A1 : H1];
|
||||
}
|
||||
// Special pawn stuff
|
||||
if (p == 'P') {
|
||||
if (A8 <= j && j <= H8) {
|
||||
score += pst['Q']![j] - pst['P']![j];
|
||||
}
|
||||
if (j == ep) {
|
||||
score += pst['P']![119 - (j + S)];
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
}
|
||||
220
lib/src/searcher.dart
Normal file
220
lib/src/searcher.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dartsunfish/index.dart';
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Search logic
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// lower <= s(pos) <= upper
|
||||
class Entry {
|
||||
Entry(this.lower, this.upper);
|
||||
int lower;
|
||||
int upper;
|
||||
}
|
||||
|
||||
class Searcher {
|
||||
Searcher();
|
||||
|
||||
late Map<Set, Entry> tpScore = {};
|
||||
late Map<Position, List<int>?> tpMove = {};
|
||||
late Set<Position> history = <Position>{};
|
||||
int nodes = 0;
|
||||
|
||||
int bound(Position pos, int gamma, int depth, {bool root = true}) {
|
||||
''' returns r where
|
||||
s(pos) <= r < gamma if gamma > s(pos)
|
||||
gamma <= r <= s(pos) if gamma <= s(pos)''';
|
||||
nodes += 1;
|
||||
|
||||
// Depth <= 0 is QSearch. Here any position is searched as deeply as is needed for
|
||||
// calmness, and from this point on there is no difference in behaviour depending on
|
||||
// depth, so so there is no reason to keep different depths in the transposition table.
|
||||
depth = max(depth, 0);
|
||||
|
||||
// Sunfish is a king-capture engine, so we should always check if we
|
||||
// still have a king. Notice since this is the only termination check,
|
||||
// the remaining code has to be comfortable with being mated, stalemated
|
||||
// or able to capture the opponent king.
|
||||
if (pos.score <= -MATE_LOWER) {
|
||||
return -MATE_UPPER;
|
||||
}
|
||||
|
||||
// We detect 3-fold captures by comparing against previously
|
||||
// _actually played_ positions.
|
||||
// Note that we need to do this before we look in the table, as the
|
||||
// position may have been previously reached with a different score.
|
||||
// This is what prevents a search instability.
|
||||
// FIXME: This is not true, since other positions will be affected by
|
||||
// the new values for all the drawn positions.
|
||||
if (DRAW_TEST) {
|
||||
// print('Nit root ${!root} * ${history.contains(pos)} ${pos.score}');
|
||||
if (history.contains(pos) && !root) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Look in the table if we have already searched this position before.
|
||||
// We also need to be sure, that the stored search was over the same
|
||||
// nodes as the current search.
|
||||
var entry = tpScore.get<Entry>({pos, depth, root}, or: Entry(-MATE_UPPER, MATE_UPPER)); // as Entry;
|
||||
if (entry!.lower >= gamma && (!root || tpMove.get<List<int>>(pos, or: <int>[])!.isNotEmpty)) {
|
||||
return entry.lower;
|
||||
}
|
||||
if (entry.upper < gamma) {
|
||||
return entry.upper;
|
||||
}
|
||||
// Here extensions may be added
|
||||
// Such as 'if in_check: depth += 1'
|
||||
|
||||
// Generator of moves to search in order.
|
||||
// This allows us to define the moves, but only calculate them if needed.
|
||||
Iterable<List<dynamic>> moves() sync* {
|
||||
// First try not moving at all. We only do this if there is at least one major
|
||||
// piece left on the board, since otherwise zugzwangs are too dangerous.
|
||||
// if (depth > 0 && !root && any(c in pos.board for c in 'RBNQ')){
|
||||
if (depth > 0 && !root && pos.board.split('').any((c) => 'RBNQ'.contains(c))) {
|
||||
// print('Bound in moves1');
|
||||
yield ([null, -bound(pos.nullmove(), 1 - gamma, depth - 3, root: false)]);
|
||||
}
|
||||
|
||||
// For QSearch we have a different kind of null-move, namely we can just stop
|
||||
// and not capture anything else.
|
||||
if (depth == 0) {
|
||||
yield ([null, pos.score]);
|
||||
}
|
||||
// Then killer move. We search it twice, but the tp will fix things for us.
|
||||
// Note, we don't have to check for legality, since we've already done it
|
||||
// before. Also note that in QS the killer must be a capture, otherwise we
|
||||
// will be non deterministic.
|
||||
var killer = tpMove.get<List<int>>(pos, or: <int>[]);
|
||||
if (killer!.isNotEmpty && (depth > 0 || pos.value(killer) >= QS_LIMIT)) {
|
||||
// print('Bound in moves2 killer');
|
||||
yield ([killer, -bound(pos.movePiece(killer), 1 - gamma, depth - 1, root: false)]);
|
||||
}
|
||||
// Then all the other moves
|
||||
var possibleMoves = pos.generateMoves().toList()
|
||||
..sort((a, b) => pos.value(a).compareTo(pos.value(b)))
|
||||
..toList(); //.reversed;
|
||||
for (var move in possibleMoves.reversed) {
|
||||
//pos.generateMoves().toList().sort((a, b) => a.value.compareto(b.value)).toList().reversed)) {
|
||||
// for val, move in sorted(((pos.value(move), move) for move in pos.generateMoves()), reverse=true):
|
||||
// If depth == 0 we only try moves with high intrinsic score (captures and
|
||||
// promotions). Otherwise we do all moves.
|
||||
if (depth > 0 || pos.value(move) >= QS_LIMIT) {
|
||||
// print('Bound in moves3');
|
||||
yield ([move, -bound(pos.movePiece(move), 1 - gamma, depth - 1, root: false)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run through the moves, shortcutting when possible
|
||||
var best = -MATE_UPPER;
|
||||
for (var m in moves()) {
|
||||
List<int>? move = m[0];
|
||||
int score = m[1];
|
||||
best = max(best, score);
|
||||
// if (depth == 0){
|
||||
// }
|
||||
if (best >= gamma) {
|
||||
// Clear before setting, so we always have a value
|
||||
if (tpMove.length > TABLE_SIZE) {
|
||||
tpMove.clear();
|
||||
}
|
||||
// Save the move for pv construction and killer heuristic
|
||||
tpMove[pos] = move;
|
||||
// print([
|
||||
// 'Depth',
|
||||
// depth,
|
||||
// 'Move',
|
||||
// move != null ? UI.render(119 - move[0]) + UI.render(119 - move[1]) : 'No move',
|
||||
// 'Score',
|
||||
// score,
|
||||
// 'Best',
|
||||
// best,
|
||||
// 'Gamma',
|
||||
// gamma,
|
||||
// 'STOP',
|
||||
// best >= gamma
|
||||
// ]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Stalemate checking is a bit tricky: Say we failed low, because
|
||||
// we can't (legally) move and so the (real) score is -infty.
|
||||
// At the next depth we are allowed to just return r, -infty <= r < gamma,
|
||||
// which is normally fine.
|
||||
// However, what if gamma = -10 and we don't have any legal moves?
|
||||
// Then the score is actaully a draw and we should fail high!
|
||||
// Thus, if best < gamma and best < 0 we need to double check what we are doing.
|
||||
// This doesn't prevent sunfish from making a move that results in stalemate,
|
||||
// but only if depth == 1, so that's probably fair enough.
|
||||
// (Btw, at depth 1 we can also mate without realizing.)
|
||||
if (best < gamma && best < 0 && depth > 0) {
|
||||
bool is_dead(Position pos) {
|
||||
return pos.generateMoves().any((m) => pos.value(m) >= MATE_LOWER);
|
||||
}
|
||||
|
||||
// if (all(is_dead(pos.move(m)) for m in pos.generateMoves())){
|
||||
if (pos.generateMoves().every((m) => is_dead(pos.movePiece(m)))) {
|
||||
var in_check = is_dead(pos.nullmove());
|
||||
best = in_check ? -MATE_UPPER : 0;
|
||||
}
|
||||
}
|
||||
// Clear before setting, so we always have a value
|
||||
if (tpScore.length > TABLE_SIZE) {
|
||||
tpScore.clear();
|
||||
}
|
||||
// Table part 2
|
||||
if (best >= gamma) {
|
||||
tpScore[{pos, depth, root}] = Entry(best, entry.upper);
|
||||
}
|
||||
if (best < gamma) {
|
||||
tpScore[{pos, depth, root}] = Entry(entry.lower, best);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
Iterable<dynamic> search(Position pos, Set<Position> _history) sync* {
|
||||
''' Iterative deepening MTD-bi search ''';
|
||||
nodes = 0;
|
||||
|
||||
if (DRAW_TEST) {
|
||||
history = _history;
|
||||
// Clearing table due to new history
|
||||
tpScore.clear();
|
||||
}
|
||||
// In finished games, we could potentially go far enough to cause a recursion
|
||||
// limit exception. Hence we bound the ply.
|
||||
for (var depth = 1; depth < 100; depth++) {
|
||||
// The inner loop is a binary search on the score of the position.
|
||||
// Inv: lower <= score <= upper
|
||||
// 'while lower != upper' would work, but play tests show a margin of 20 plays
|
||||
// better.
|
||||
var lower = -MATE_UPPER;
|
||||
var upper = MATE_UPPER;
|
||||
/*
|
||||
*/
|
||||
while (lower < upper - EVAL_ROUGHNESS) {
|
||||
var gamma = (lower + upper + 1) ~/ 2;
|
||||
// print([lower, upper, gamma, lower < upper - EVAL_ROUGHNESS]);
|
||||
var score = bound(pos, gamma, depth);
|
||||
print('Call in while');
|
||||
if (score >= gamma) lower = score;
|
||||
if (score < gamma) upper = score;
|
||||
}
|
||||
|
||||
// We want to make sure the move to play hasn't been kicked out of the table,
|
||||
// So we make another call that must always fail high and thus produce a move.
|
||||
bound(pos, lower, depth);
|
||||
print('Call finally');
|
||||
// If the game hasn't finished we can retrieve our move from the
|
||||
// transposition table.
|
||||
yield ([
|
||||
// depth,
|
||||
tpMove.get<List<int>>(pos),
|
||||
tpScore.get<Entry>({pos, depth, true})?.lower
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,20 @@ class UI {
|
||||
'k': '♔',
|
||||
'p': '♙',
|
||||
'.': '·',
|
||||
// ' ': ' '
|
||||
};
|
||||
print('');
|
||||
final bordChars = pos.board.split('\n');
|
||||
for (var i = 0; i < bordChars.length; i++) {
|
||||
var row = bordChars[i];
|
||||
if (!row.isSpace) {
|
||||
var pieces = row.split('').map((e) => e = e.isSpace ? '' : uni_pieces[e]!);
|
||||
final bordRows = pos.board.split('\n');
|
||||
for (var i = 0; i < bordRows.length; i++) {
|
||||
var row = bordRows[i].split('');
|
||||
var pieces = row.map((e) => e.isSpace ? '' : uni_pieces.get(e, or: e));
|
||||
if (!row.every((element) => element.isSpace)) {
|
||||
print('${(10 - i)}${pieces.join(' ')}');
|
||||
}
|
||||
}
|
||||
print(' a b c d e f g h \n\n');
|
||||
print(' a b c d e f g h');
|
||||
print(' \n\n');
|
||||
}
|
||||
|
||||
// for i, row in enumerate(pos.board.split()):
|
||||
// print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row))
|
||||
}
|
||||
|
||||
132
lib/tester.dart
Normal file
132
lib/tester.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dartsunfish/src/extentions.dart';
|
||||
|
||||
class MyItem {
|
||||
int id;
|
||||
String name;
|
||||
MyItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
MyItem copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
}) {
|
||||
return MyItem(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
|
||||
factory MyItem.fromMap(Map<String, dynamic> map) {
|
||||
return MyItem(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory MyItem.fromJson(String source) => MyItem.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() => 'MyItem(id: $id, name: $name)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is MyItem && other.id == id && other.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ name.hashCode;
|
||||
}
|
||||
|
||||
void main(List<String> args) {
|
||||
final someList = List.generate(
|
||||
10,
|
||||
(i) => {
|
||||
{i, 's'}: 'See item $i'
|
||||
});
|
||||
|
||||
bool root = true;
|
||||
|
||||
final someSet = Set.from(List<MyItem>.generate(10, (index) => MyItem(id: index, name: 'name $index')).toSet());
|
||||
|
||||
print(someSet.contains(MyItem(id: 2, name: 'name 2')) && !root);
|
||||
|
||||
print(someList);
|
||||
final first = someList[0].get({0, 's'});
|
||||
print('I found $first');
|
||||
|
||||
final someMap = {
|
||||
{1, 'S1'}: 'See item 1',
|
||||
{2, 'S2'}: 'See item 1'
|
||||
};
|
||||
|
||||
var s1 = {
|
||||
1,
|
||||
'S1',
|
||||
22.23,
|
||||
['q', 12]
|
||||
};
|
||||
var s2 = {
|
||||
1,
|
||||
'S1',
|
||||
22.23,
|
||||
['q', 12]
|
||||
};
|
||||
|
||||
var s3 = {
|
||||
1: 'S1',
|
||||
22.23: ['q', 12]
|
||||
};
|
||||
var s4 = {
|
||||
1: 'S1',
|
||||
22.23: ['q', 12]
|
||||
};
|
||||
|
||||
print('Equality test gives ${s1.isEqual(s2)}');
|
||||
print('Equality test gives ${s3.isEqual(s4)}');
|
||||
|
||||
print(someMap.get({1, 'S2'}, or: 'Oops'));
|
||||
|
||||
var ff = someList.firstWhere((element) => element.get({1, 's'}) != null, orElse: () => someList[0]);
|
||||
// print('In map ${someMap.entries.toList()}');
|
||||
print('In map $ff');
|
||||
|
||||
for (var i in List.generate(10, (index) => index + 1)) {
|
||||
print('III = $i');
|
||||
}
|
||||
|
||||
Iterator<String> getStringIterator(String s) => s.runes.map((r) => String.fromCharCode(r)).iterator;
|
||||
|
||||
final str = 'Hello, World';
|
||||
|
||||
final strList = str.split('');
|
||||
|
||||
strList.forEachIndexed((e, i) {
|
||||
if (e != 'o') {
|
||||
print('See $e');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
final strIter = getStringIterator(str);
|
||||
for (var i = 0; i < str.runes.length; i++) {
|
||||
strIter.moveNext();
|
||||
print('In map ${strIter.current}');
|
||||
print('And ${String.fromCharCode(str.runes.elementAt(i))}');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user