Stack at searcher

This commit is contained in:
DMK
2021-08-20 10:55:48 +03:00
parent 186870e770
commit c1188922c8
12 changed files with 1133 additions and 439 deletions

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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
View 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
View 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
View 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
]);
}
}
}

View File

@@ -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
View 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))}');
}
}