Compare commits
2 Commits
master
...
splited_st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1188922c8 | ||
|
|
186870e770 |
@@ -1,4 +1,4 @@
|
|||||||
import 'package:dartsunfish/dartsunfish.dart' as dartsunfish;
|
import 'package:dartsunfish/index.dart' as dartsunfish;
|
||||||
|
|
||||||
void main(List<String> arguments) {
|
void main(List<String> arguments) {
|
||||||
dartsunfish.GameBot().playGame();
|
dartsunfish.GameBot().playGame();
|
||||||
|
|||||||
@@ -1,715 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:developer' as dv;
|
|
||||||
|
|
||||||
///String extentions
|
|
||||||
extension CheckString on String {
|
|
||||||
bool get isSpace => trim().isEmpty || RegExp(r'/\s/').hasMatch(this);
|
|
||||||
bool get isUpper => !isSpace && toUpperCase() == this && this != '.';
|
|
||||||
bool get isLower => !isSpace && toLowerCase() == this && this != '.';
|
|
||||||
String get reversed => split('').reversed.join('');
|
|
||||||
String get swapCase =>
|
|
||||||
split('').map((x) => x.toUpperCase() == x ? x.toLowerCase() : x.toUpperCase()).toList().join('');
|
|
||||||
String get reversedAndSwapCased =>
|
|
||||||
split('').map((x) => x.toUpperCase() == x ? x.toLowerCase() : x.toUpperCase()).toList().reversed.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of list extention
|
|
||||||
extension CheckListOfTuples on List {
|
|
||||||
bool doContains(newList) => newList.length == 2 && indexWhere((l) => l[0] == newList[0] && l[1] == newList[1]) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getSeconds() {
|
|
||||||
return DateTime.now().millisecondsSinceEpoch ~/ 1000.toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// // Piece-Square tables. Tune these to change sunfish's behaviour
|
|
||||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
/// TODO: add wizard
|
|
||||||
final piece = {'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000};
|
|
||||||
|
|
||||||
///TODO: add 2 raws and 2 colums to each set AND Wizard pst
|
|
||||||
var pst = {
|
|
||||||
'P': [
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, //
|
|
||||||
78, 83, 86, 73, 102, 82, 85, 90, //
|
|
||||||
7, 29, 21, 44, 40, 31, 44, 7, //
|
|
||||||
-17, 16, -2, 15, 14, 0, 15, -13, //
|
|
||||||
-26, 3, 10, 9, 6, 1, 0, -23, //
|
|
||||||
-22, 9, 5, -11, -10, -2, 3, -19, //
|
|
||||||
-31, 8, -7, -37, -36, -14, 3, -31, //
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0
|
|
||||||
], //
|
|
||||||
'N': [
|
|
||||||
-66, -53, -75, -75, -10, -55, -58, -70, //
|
|
||||||
-3, -6, 100, -36, 4, 62, -4, -14, //
|
|
||||||
10, 67, 1, 74, 73, 27, 62, -2, //
|
|
||||||
24, 24, 45, 37, 33, 41, 25, 17, //
|
|
||||||
-1, 5, 31, 21, 22, 35, 2, 0, //
|
|
||||||
-18, 10, 13, 22, 18, 15, 11, -14, //
|
|
||||||
-23, -15, 2, 0, 2, 0, -23, -20, //
|
|
||||||
-74, -23, -26, -24, -19, -35, -22, -69
|
|
||||||
], //
|
|
||||||
'B': [
|
|
||||||
-59, -78, -82, -76, -23, -107, -37, -50, //
|
|
||||||
-11, 20, 35, -42, -39, 31, 2, -22, //
|
|
||||||
-9, 39, -32, 41, 52, -10, 28, -14, //
|
|
||||||
25, 17, 20, 34, 26, 25, 15, 10, //
|
|
||||||
13, 10, 17, 23, 17, 16, 0, 7, //
|
|
||||||
14, 25, 24, 15, 8, 25, 20, 15, //
|
|
||||||
19, 20, 11, 6, 7, 6, 20, 16, //
|
|
||||||
-7, 2, -15, -12, -14, -15, -10, -10
|
|
||||||
], //
|
|
||||||
'R': [
|
|
||||||
35, 29, 33, 4, 37, 33, 56, 50, //
|
|
||||||
55, 29, 56, 67, 55, 62, 34, 60, //
|
|
||||||
19, 35, 28, 33, 45, 27, 25, 15, //
|
|
||||||
0, 5, 16, 13, 18, -4, -9, -6, //
|
|
||||||
-28, -35, -16, -21, -13, -29, -46, -30, //
|
|
||||||
-42, -28, -42, -25, -25, -35, -26, -46, //
|
|
||||||
-53, -38, -31, -26, -29, -43, -44, -53, //
|
|
||||||
-30, -24, -18, 5, -2, -18, -31, -32
|
|
||||||
], //
|
|
||||||
'Q': [
|
|
||||||
6, 1, -8, -104, 69, 24, 88, 26, //
|
|
||||||
14, 32, 60, -10, 20, 76, 57, 24, //
|
|
||||||
-2, 43, 32, 60, 72, 63, 43, 2, //
|
|
||||||
1, -16, 22, 17, 25, 20, -13, -6, //
|
|
||||||
-14, -15, -2, -5, -1, -10, -20, -22, //
|
|
||||||
-30, -6, -13, -11, -16, -11, -16, -27, //
|
|
||||||
-36, -18, 0, -19, -15, -15, -21, -38, //
|
|
||||||
-39, -30, -31, -13, -31, -36, -34, -42
|
|
||||||
], //
|
|
||||||
'K': [
|
|
||||||
4, 54, 47, -99, -99, 60, 83, -62, //
|
|
||||||
-32, 10, 55, 56, 56, 55, 10, 3, //
|
|
||||||
-62, 12, -57, 44, -67, 28, 37, -31, //
|
|
||||||
-55, 50, 11, -4, -19, 13, 0, -49, //
|
|
||||||
-55, -43, -52, -28, -51, -47, -8, -50, //
|
|
||||||
-47, -42, -43, -79, -64, -32, -29, -32, //
|
|
||||||
-4, 3, -14, -50, -57, -18, 13, 4, //
|
|
||||||
17, 30, -3, -14, 6, -1, 40, 18
|
|
||||||
], //
|
|
||||||
};
|
|
||||||
|
|
||||||
// recalculating Piece-Square raw into desk surrounded by 0-s
|
|
||||||
List<int> padrow(List<int> row, String k) {
|
|
||||||
var rowBody = <int>[for (int x in row) x + piece[k]!];
|
|
||||||
return [
|
|
||||||
0,
|
|
||||||
...rowBody,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPst() {
|
|
||||||
pst.forEach((key, item) {
|
|
||||||
final innerItem = [...List.filled(20, 0)];
|
|
||||||
for (var i = 0; i < 8; i++) {
|
|
||||||
innerItem.addAll(padrow(item.getRange(i * 8, i * 8 + 8).toList(), key));
|
|
||||||
}
|
|
||||||
innerItem.addAll(List.filled(20, 0));
|
|
||||||
pst[key] = innerItem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Global constants
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Our board is represented as a 120 character string. The padding allows for
|
|
||||||
// fast detection of moves that don't stay within the board.
|
|
||||||
|
|
||||||
//TODO: Tune globals for 10x10 field
|
|
||||||
final A1 = 91, H1 = 98, A8 = 21, H8 = 28;
|
|
||||||
final initial = (' \n' // 0 - 9
|
|
||||||
' \n' // 10 - 19
|
|
||||||
' rnbqkbnr\n' // 20 - 29
|
|
||||||
' pppppppp\n' // 30 - 39
|
|
||||||
' ........\n' // 40 - 49
|
|
||||||
' ........\n' // 50 - 59
|
|
||||||
' ........\n' // 60 - 69
|
|
||||||
' ........\n' // 70 - 79
|
|
||||||
' PPPPPPPP\n' // 80 - 89
|
|
||||||
' RNBQKBNR\n' // 90 - 99
|
|
||||||
' \n' // 100 -109
|
|
||||||
' \n' // 110 -119
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lists of possible moves for each piece type.
|
|
||||||
int N = -10, E = 1, S = 10, W = -1;
|
|
||||||
//TODO: add directions for Wizard
|
|
||||||
final Map<String?, List<int>> directions = {
|
|
||||||
'P': [N, N + N, N + W, N + E],
|
|
||||||
'N': [N + N + E, E + N + E, E + S + E, S + S + E, S + S + W, W + S + W, W + N + W, N + N + W],
|
|
||||||
'B': [N + E, S + E, S + W, N + W],
|
|
||||||
'R': [N, E, S, W],
|
|
||||||
'Q': [N, E, S, W, N + E, S + E, S + W, N + W],
|
|
||||||
'K': [N, E, S, W, N + E, S + E, S + W, N + W],
|
|
||||||
'.': []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mate value must be greater than 8*queen + 2*(rook+knight+bishop)
|
|
||||||
// King value is set to twice this value such that if the opponent is
|
|
||||||
// 8 queens up, but we got the king, we still exceed MATE_VALUE.
|
|
||||||
// When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there
|
|
||||||
// E.g. Mate in 3 will be MATE_UPPER - 6
|
|
||||||
// TOD: replace mate with King capture event
|
|
||||||
final MATE_LOWER = piece['K']! - 10 * piece['Q']!;
|
|
||||||
final MATE_UPPER = piece['K']! + 10 * piece['Q']!;
|
|
||||||
|
|
||||||
// The table size is the maximum number of elements in the transposition table.
|
|
||||||
final TABLE_SIZE = 1e7;
|
|
||||||
|
|
||||||
// Constants for tuning search
|
|
||||||
final QS_LIMIT = 219, EVAL_ROUGHNESS = 13, DRAW_TEST = true;
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// 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>> gen_moves() 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.gen_moves().toList()
|
|
||||||
..sort((a, b) => pos.value(a).compareTo(pos.value(b)))
|
|
||||||
..toList(); //.reversed;
|
|
||||||
for (var move in possibleMoves.reversed) {
|
|
||||||
//pos.gen_moves().toList().sort((a, b) => a.value.compareto(b.value)).toList().reversed)) {
|
|
||||||
// for val, move in sorted(((pos.value(move), move) for move in pos.gen_moves()), 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.gen_moves())){
|
|
||||||
if (pos.gen_moves().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;
|
|
||||||
// print('// 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
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// User interface
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
class UI {
|
|
||||||
static int parse(String? c) {
|
|
||||||
var fil = c![0].codeUnits.first - 'a'.codeUnits.first;
|
|
||||||
var rank = int.parse(c[1]) - 1;
|
|
||||||
return A1 + fil - 10 * rank;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String render(int i) {
|
|
||||||
var rank = (i - A1) ~/ 10;
|
|
||||||
var fil = (i - A1) % 10;
|
|
||||||
return String.fromCharCodes([fil + 'a'.codeUnits.first]) + (-rank + 1).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void printBoard(Position pos) {
|
|
||||||
final uni_pieces = {
|
|
||||||
'R': '♜',
|
|
||||||
'N': '♞',
|
|
||||||
'B': '♝',
|
|
||||||
'Q': '♛',
|
|
||||||
'K': '♚',
|
|
||||||
'P': '♟',
|
|
||||||
'r': '♖',
|
|
||||||
'n': '♘',
|
|
||||||
'b': '♗',
|
|
||||||
'q': '♕',
|
|
||||||
'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]!);
|
|
||||||
print('${(10 - i)}${pieces.join(' ')}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
print(' a b c d e f g h \n\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GameBot {
|
|
||||||
GameBot();
|
|
||||||
|
|
||||||
void gameMain() {
|
|
||||||
var hist = <Position>[
|
|
||||||
Position(initial, 0, [true, true], [true, true], 0, 0),
|
|
||||||
];
|
|
||||||
var searcher = Searcher();
|
|
||||||
while (true) {
|
|
||||||
UI.printBoard(hist.last);
|
|
||||||
|
|
||||||
if (hist.last.score <= -MATE_LOWER) {
|
|
||||||
print('You lost');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We query the user until she enters a (pseudo) legal move.
|
|
||||||
var move = <int>[];
|
|
||||||
final possibleMoves = hist.last.gen_moves().toList();
|
|
||||||
while (!possibleMoves.doContains(move)) {
|
|
||||||
var pattern = RegExp(
|
|
||||||
r'([a-h][1-8])' * 2,
|
|
||||||
caseSensitive: false,
|
|
||||||
multiLine: false,
|
|
||||||
);
|
|
||||||
print('Your move:');
|
|
||||||
var input = stdin.readLineSync();
|
|
||||||
var match = pattern.firstMatch(input.toString());
|
|
||||||
if (match != null) {
|
|
||||||
move = [UI.parse(match.group(1)), UI.parse(match.group(2))];
|
|
||||||
if (!possibleMoves.doContains(move)) {
|
|
||||||
print('Not valid move typed in. Try again');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Inform the user when invalid input (e.g. "help") is entered
|
|
||||||
print('Please enter a move like g8f6');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
hist.add(hist.last.movePiece(move));
|
|
||||||
} catch (e) {
|
|
||||||
// print(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// After our move we rotate the board and print it again.
|
|
||||||
// This allows us to see the effect of our move.
|
|
||||||
UI.printBoard(hist.last.rotate());
|
|
||||||
// print('${hist.last.score}');
|
|
||||||
|
|
||||||
if (hist.last.score <= -MATE_LOWER) {
|
|
||||||
print('You won');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Fire up the engine to look for a move.
|
|
||||||
var _depth = 0;
|
|
||||||
var score = 0;
|
|
||||||
var start = getSeconds();
|
|
||||||
/*
|
|
||||||
*/
|
|
||||||
for (var s in searcher.search(hist.last, hist.toSet())) {
|
|
||||||
_depth = s[0];
|
|
||||||
move = s[1];
|
|
||||||
score = s[2] ?? 0;
|
|
||||||
if (getSeconds() - start > 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score == MATE_UPPER) {
|
|
||||||
print('Checkmate!');
|
|
||||||
}
|
|
||||||
// The black player moves from a rotated position, so we have to
|
|
||||||
// 'back rotate' the move before printing it.
|
|
||||||
print('My move: ${UI.render(119 - move[0]) + UI.render(119 - move[1])}');
|
|
||||||
hist.add(hist.last.movePiece(move));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void playGame() {
|
|
||||||
dv.log('message');
|
|
||||||
setPst();
|
|
||||||
gameMain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
lib/index.dart
Normal file
7
lib/index.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export 'src/dartsunfish.dart';
|
||||||
|
export 'src/extentions.dart';
|
||||||
|
export 'src/globals.dart';
|
||||||
|
export 'src/helpers.dart';
|
||||||
|
export 'src/position.dart';
|
||||||
|
export 'src/searcher.dart';
|
||||||
|
export 'src/ui.dart';
|
||||||
86
lib/src/dartsunfish.dart
Normal file
86
lib/src/dartsunfish.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:dartsunfish/index.dart';
|
||||||
|
|
||||||
|
class GameBot {
|
||||||
|
GameBot();
|
||||||
|
|
||||||
|
void gameMain() {
|
||||||
|
var hist = <Position>[
|
||||||
|
Position(initial, 0, [true, true], [true, true], 0, 0),
|
||||||
|
];
|
||||||
|
var searcher = Searcher();
|
||||||
|
while (true) {
|
||||||
|
UI.printBoard(hist.last);
|
||||||
|
|
||||||
|
if (hist.last.score <= -MATE_LOWER) {
|
||||||
|
print('You lost');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
r'([a-h][1-8])' * 2,
|
||||||
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
);
|
||||||
|
print('Your move:');
|
||||||
|
var input = stdin.readLineSync();
|
||||||
|
var match = pattern.firstMatch(input.toString());
|
||||||
|
if (match != null) {
|
||||||
|
move = [UI.parse(match.group(1)), UI.parse(match.group(2))];
|
||||||
|
if (!possibleMoves.doContains(move)) {
|
||||||
|
print('Not valid move typed in. Try again');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Inform the user when invalid input (e.g. "help") is entered
|
||||||
|
print('Please enter a move like g8f6');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
hist.add(hist.last.movePiece(move));
|
||||||
|
} catch (e) {
|
||||||
|
// print(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// After our move we rotate the board and print it again.
|
||||||
|
// This allows us to see the effect of our move.
|
||||||
|
UI.printBoard(hist.last.rotate());
|
||||||
|
// print('${hist.last.score}');
|
||||||
|
|
||||||
|
if (hist.last.score <= -MATE_LOWER) {
|
||||||
|
print('You won');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
// 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[0];
|
||||||
|
score = s[1] ?? 0;
|
||||||
|
if (getSeconds() - start > 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score == MATE_UPPER) {
|
||||||
|
print('Checkmate!');
|
||||||
|
}
|
||||||
|
// The black player moves from a rotated position, so we have to
|
||||||
|
// 'back rotate' the move before printing it.
|
||||||
|
print('My move: ${UI.render(119 - move[0]) + UI.render(119 - move[1])}');
|
||||||
|
hist.add(hist.last.movePiece(move));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void playGame() {
|
||||||
|
setPst();
|
||||||
|
gameMain();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
lib/src/extentions.dart
Normal file
81
lib/src/extentions.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
///String extentions
|
||||||
|
extension CheckString on String {
|
||||||
|
bool get isSpace => trim().isEmpty || RegExp(r'/\s/').hasMatch(this);
|
||||||
|
bool get isUpper => !isSpace && toUpperCase() == this && this != '.';
|
||||||
|
bool get isLower => !isSpace && toLowerCase() == this && this != '.';
|
||||||
|
String get reversed => split('').reversed.join('');
|
||||||
|
String get swapCase =>
|
||||||
|
split('').map((x) => x.toUpperCase() == x ? x.toLowerCase() : x.toUpperCase()).toList().join('');
|
||||||
|
String get reversedAndSwapCased =>
|
||||||
|
split('').map((x) => x.toUpperCase() == x ? x.toLowerCase() : x.toUpperCase()).toList().reversed.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of list extention
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
lib/src/globals.dart
Normal file
145
lib/src/globals.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// // Piece-Square tables. Tune these to change sunfish's behaviour
|
||||||
|
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/// TODO: add wizard
|
||||||
|
final piece = {'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000};
|
||||||
|
|
||||||
|
///TODO: add 2 raws and 2 colums to each set AND Wizard pst
|
||||||
|
var pst = {
|
||||||
|
'P': [
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, //
|
||||||
|
78, 83, 86, 73, 102, 82, 85, 90, //
|
||||||
|
7, 29, 21, 44, 40, 31, 44, 7, //
|
||||||
|
-17, 16, -2, 15, 14, 0, 15, -13, //
|
||||||
|
-26, 3, 10, 9, 6, 1, 0, -23, //
|
||||||
|
-22, 9, 5, -11, -10, -2, 3, -19, //
|
||||||
|
-31, 8, -7, -37, -36, -14, 3, -31, //
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
], //
|
||||||
|
'N': [
|
||||||
|
-66, -53, -75, -75, -10, -55, -58, -70, //
|
||||||
|
-3, -6, 100, -36, 4, 62, -4, -14, //
|
||||||
|
10, 67, 1, 74, 73, 27, 62, -2, //
|
||||||
|
24, 24, 45, 37, 33, 41, 25, 17, //
|
||||||
|
-1, 5, 31, 21, 22, 35, 2, 0, //
|
||||||
|
-18, 10, 13, 22, 18, 15, 11, -14, //
|
||||||
|
-23, -15, 2, 0, 2, 0, -23, -20, //
|
||||||
|
-74, -23, -26, -24, -19, -35, -22, -69
|
||||||
|
], //
|
||||||
|
'B': [
|
||||||
|
-59, -78, -82, -76, -23, -107, -37, -50, //
|
||||||
|
-11, 20, 35, -42, -39, 31, 2, -22, //
|
||||||
|
-9, 39, -32, 41, 52, -10, 28, -14, //
|
||||||
|
25, 17, 20, 34, 26, 25, 15, 10, //
|
||||||
|
13, 10, 17, 23, 17, 16, 0, 7, //
|
||||||
|
14, 25, 24, 15, 8, 25, 20, 15, //
|
||||||
|
19, 20, 11, 6, 7, 6, 20, 16, //
|
||||||
|
-7, 2, -15, -12, -14, -15, -10, -10
|
||||||
|
], //
|
||||||
|
'R': [
|
||||||
|
35, 29, 33, 4, 37, 33, 56, 50, //
|
||||||
|
55, 29, 56, 67, 55, 62, 34, 60, //
|
||||||
|
19, 35, 28, 33, 45, 27, 25, 15, //
|
||||||
|
0, 5, 16, 13, 18, -4, -9, -6, //
|
||||||
|
-28, -35, -16, -21, -13, -29, -46, -30, //
|
||||||
|
-42, -28, -42, -25, -25, -35, -26, -46, //
|
||||||
|
-53, -38, -31, -26, -29, -43, -44, -53, //
|
||||||
|
-30, -24, -18, 5, -2, -18, -31, -32
|
||||||
|
], //
|
||||||
|
'Q': [
|
||||||
|
6, 1, -8, -104, 69, 24, 88, 26, //
|
||||||
|
14, 32, 60, -10, 20, 76, 57, 24, //
|
||||||
|
-2, 43, 32, 60, 72, 63, 43, 2, //
|
||||||
|
1, -16, 22, 17, 25, 20, -13, -6, //
|
||||||
|
-14, -15, -2, -5, -1, -10, -20, -22, //
|
||||||
|
-30, -6, -13, -11, -16, -11, -16, -27, //
|
||||||
|
-36, -18, 0, -19, -15, -15, -21, -38, //
|
||||||
|
-39, -30, -31, -13, -31, -36, -34, -42
|
||||||
|
], //
|
||||||
|
'K': [
|
||||||
|
4, 54, 47, -99, -99, 60, 83, -62, //
|
||||||
|
-32, 10, 55, 56, 56, 55, 10, 3, //
|
||||||
|
-62, 12, -57, 44, -67, 28, 37, -31, //
|
||||||
|
-55, 50, 11, -4, -19, 13, 0, -49, //
|
||||||
|
-55, -43, -52, -28, -51, -47, -8, -50, //
|
||||||
|
-47, -42, -43, -79, -64, -32, -29, -32, //
|
||||||
|
-4, 3, -14, -50, -57, -18, 13, 4, //
|
||||||
|
17, 30, -3, -14, 6, -1, 40, 18
|
||||||
|
], //
|
||||||
|
};
|
||||||
|
|
||||||
|
// recalculating Piece-Square raw into desk surrounded by 0-s
|
||||||
|
List<int> padrow(List<int> row, String k) {
|
||||||
|
var rowBody = <int>[for (int x in row) x + piece[k]!];
|
||||||
|
return [
|
||||||
|
0,
|
||||||
|
...rowBody,
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPst() {
|
||||||
|
pst.forEach((key, item) {
|
||||||
|
final innerItem = [...List.filled(20, 0)];
|
||||||
|
for (var i = 0; i < 8; i++) {
|
||||||
|
innerItem.addAll(padrow(item.getRange(i * 8, i * 8 + 8).toList(), key));
|
||||||
|
}
|
||||||
|
innerItem.addAll(List.filled(20, 0));
|
||||||
|
pst[key] = innerItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int getSeconds() {
|
||||||
|
return DateTime.now().millisecondsSinceEpoch ~/ 1000.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Global constants
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Our board is represented as a 120 character string. The padding allows for
|
||||||
|
// fast detection of moves that don't stay within the board.
|
||||||
|
|
||||||
|
//TODO: Tune globals for 10x10 field
|
||||||
|
final A1 = 91, H1 = 98, A8 = 21, H8 = 28;
|
||||||
|
final initial = (' \n' // 0 - 9
|
||||||
|
' \n' // 10 - 19
|
||||||
|
' rnbqkbnr\n' // 20 - 29
|
||||||
|
' pppppppp\n' // 30 - 39
|
||||||
|
' ........\n' // 40 - 49
|
||||||
|
' ........\n' // 50 - 59
|
||||||
|
' ........\n' // 60 - 69
|
||||||
|
' ........\n' // 70 - 79
|
||||||
|
' PPPPPPPP\n' // 80 - 89
|
||||||
|
' RNBQKBNR\n' // 90 - 99
|
||||||
|
' \n' // 100 -109
|
||||||
|
' \n' // 110 -119
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lists of possible moves for each piece type.
|
||||||
|
int N = -10, E = 1, S = 10, W = -1;
|
||||||
|
//TODO: add directions for Wizard
|
||||||
|
final Map<String?, List<int>> directions = {
|
||||||
|
'P': [N, N + N, N + W, N + E],
|
||||||
|
'N': [N + N + E, E + N + E, E + S + E, S + S + E, S + S + W, W + S + W, W + N + W, N + N + W],
|
||||||
|
'B': [N + E, S + E, S + W, N + W],
|
||||||
|
'R': [N, E, S, W],
|
||||||
|
'Q': [N, E, S, W, N + E, S + E, S + W, N + W],
|
||||||
|
'K': [N, E, S, W, N + E, S + E, S + W, N + W],
|
||||||
|
'.': []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mate value must be greater than 8*queen + 2*(rook+knight+bishop)
|
||||||
|
// King value is set to twice this value such that if the opponent is
|
||||||
|
// 8 queens up, but we got the king, we still exceed MATE_VALUE.
|
||||||
|
// When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there
|
||||||
|
// E.g. Mate in 3 will be MATE_UPPER - 6
|
||||||
|
// TOD: replace mate with King capture event
|
||||||
|
final MATE_LOWER = piece['K']! - 10 * piece['Q']!;
|
||||||
|
final MATE_UPPER = piece['K']! + 10 * piece['Q']!;
|
||||||
|
|
||||||
|
// The table size is the maximum number of elements in the transposition table.
|
||||||
|
final TABLE_SIZE = 1e7;
|
||||||
|
|
||||||
|
// Constants for tuning search
|
||||||
|
final QS_LIMIT = 219, EVAL_ROUGHNESS = 13, DRAW_TEST = true;
|
||||||
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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lib/src/ui.dart
Normal file
50
lib/src/ui.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:dartsunfish/index.dart';
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// User interface
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class UI {
|
||||||
|
static int parse(String? c) {
|
||||||
|
var fil = c![0].codeUnits.first - 'a'.codeUnits.first;
|
||||||
|
var rank = int.parse(c[1]) - 1;
|
||||||
|
return A1 + fil - 10 * rank;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String render(int i) {
|
||||||
|
var rank = (i - A1) ~/ 10;
|
||||||
|
var fil = (i - A1) % 10;
|
||||||
|
return String.fromCharCodes([fil + 'a'.codeUnits.first]) + (-rank + 1).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void printBoard(Position pos) {
|
||||||
|
final uni_pieces = {
|
||||||
|
'R': '♜',
|
||||||
|
'N': '♞',
|
||||||
|
'B': '♝',
|
||||||
|
'Q': '♛',
|
||||||
|
'K': '♚',
|
||||||
|
'P': '♟',
|
||||||
|
'r': '♖',
|
||||||
|
'n': '♘',
|
||||||
|
'b': '♗',
|
||||||
|
'q': '♕',
|
||||||
|
'k': '♔',
|
||||||
|
'p': '♙',
|
||||||
|
'.': '·',
|
||||||
|
};
|
||||||
|
print('');
|
||||||
|
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');
|
||||||
|
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))}');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
equatable:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ environment:
|
|||||||
sdk: '>=2.12.0 <3.0.0'
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
# path: ^1.8.0
|
|
||||||
# flutter:
|
# flutter:
|
||||||
# sdk: flutter
|
# sdk: flutter
|
||||||
|
equatable: 2.0.3
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
pedantic: ^1.10.0
|
pedantic: ^1.10.0
|
||||||
test: ^1.16.0
|
test: ^1.16.0
|
||||||
|
|||||||
481
python/sunfish.py
Normal file
481
python/sunfish.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
#!/usr/bin/env pypy
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from itertools import count
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Piece-Square tables. Tune these to change sunfish's behaviour
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
piece = {'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000}
|
||||||
|
pst = {
|
||||||
|
'P': (0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
78, 83, 86, 73, 102, 82, 85, 90,
|
||||||
|
7, 29, 21, 44, 40, 31, 44, 7,
|
||||||
|
-17, 16, -2, 15, 14, 0, 15, -13,
|
||||||
|
-26, 3, 10, 9, 6, 1, 0, -23,
|
||||||
|
-22, 9, 5, -11, -10, -2, 3, -19,
|
||||||
|
-31, 8, -7, -37, -36, -14, 3, -31,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
|
'N': (-66, -53, -75, -75, -10, -55, -58, -70,
|
||||||
|
-3, -6, 100, -36, 4, 62, -4, -14,
|
||||||
|
10, 67, 1, 74, 73, 27, 62, -2,
|
||||||
|
24, 24, 45, 37, 33, 41, 25, 17,
|
||||||
|
-1, 5, 31, 21, 22, 35, 2, 0,
|
||||||
|
-18, 10, 13, 22, 18, 15, 11, -14,
|
||||||
|
-23, -15, 2, 0, 2, 0, -23, -20,
|
||||||
|
-74, -23, -26, -24, -19, -35, -22, -69),
|
||||||
|
'B': (-59, -78, -82, -76, -23, -107, -37, -50,
|
||||||
|
-11, 20, 35, -42, -39, 31, 2, -22,
|
||||||
|
-9, 39, -32, 41, 52, -10, 28, -14,
|
||||||
|
25, 17, 20, 34, 26, 25, 15, 10,
|
||||||
|
13, 10, 17, 23, 17, 16, 0, 7,
|
||||||
|
14, 25, 24, 15, 8, 25, 20, 15,
|
||||||
|
19, 20, 11, 6, 7, 6, 20, 16,
|
||||||
|
-7, 2, -15, -12, -14, -15, -10, -10),
|
||||||
|
'R': (35, 29, 33, 4, 37, 33, 56, 50,
|
||||||
|
55, 29, 56, 67, 55, 62, 34, 60,
|
||||||
|
19, 35, 28, 33, 45, 27, 25, 15,
|
||||||
|
0, 5, 16, 13, 18, -4, -9, -6,
|
||||||
|
-28, -35, -16, -21, -13, -29, -46, -30,
|
||||||
|
-42, -28, -42, -25, -25, -35, -26, -46,
|
||||||
|
-53, -38, -31, -26, -29, -43, -44, -53,
|
||||||
|
-30, -24, -18, 5, -2, -18, -31, -32),
|
||||||
|
'Q': (6, 1, -8, -104, 69, 24, 88, 26,
|
||||||
|
14, 32, 60, -10, 20, 76, 57, 24,
|
||||||
|
-2, 43, 32, 60, 72, 63, 43, 2,
|
||||||
|
1, -16, 22, 17, 25, 20, -13, -6,
|
||||||
|
-14, -15, -2, -5, -1, -10, -20, -22,
|
||||||
|
-30, -6, -13, -11, -16, -11, -16, -27,
|
||||||
|
-36, -18, 0, -19, -15, -15, -21, -38,
|
||||||
|
-39, -30, -31, -13, -31, -36, -34, -42),
|
||||||
|
'K': (4, 54, 47, -99, -99, 60, 83, -62,
|
||||||
|
-32, 10, 55, 56, 56, 55, 10, 3,
|
||||||
|
-62, 12, -57, 44, -67, 28, 37, -31,
|
||||||
|
-55, 50, 11, -4, -19, 13, 0, -49,
|
||||||
|
-55, -43, -52, -28, -51, -47, -8, -50,
|
||||||
|
-47, -42, -43, -79, -64, -32, -29, -32,
|
||||||
|
-4, 3, -14, -50, -57, -18, 13, 4,
|
||||||
|
17, 30, -3, -14, 6, -1, 40, 18),
|
||||||
|
}
|
||||||
|
# Pad tables and join piece and pst dictionaries
|
||||||
|
for k, table in pst.items():
|
||||||
|
def padrow(row): return (0,) + tuple(x+piece[k] for x in row) + (0,)
|
||||||
|
pst[k] = sum((padrow(table[i*8:i*8+8]) for i in range(8)), ())
|
||||||
|
pst[k] = (0,)*20 + pst[k] + (0,)*20
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Global constants
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Our board is represented as a 120 character string. The padding allows for
|
||||||
|
# fast detection of moves that don't stay within the board.
|
||||||
|
A1, H1, A8, H8 = 91, 98, 21, 28
|
||||||
|
initial = (
|
||||||
|
' \n' # 0 - 9
|
||||||
|
' \n' # 10 - 19
|
||||||
|
' rnbqkbnr\n' # 20 - 29
|
||||||
|
' pppppppp\n' # 30 - 39
|
||||||
|
' ........\n' # 40 - 49
|
||||||
|
' ........\n' # 50 - 59
|
||||||
|
' ........\n' # 60 - 69
|
||||||
|
' ........\n' # 70 - 79
|
||||||
|
' PPPPPPPP\n' # 80 - 89
|
||||||
|
' RNBQKBNR\n' # 90 - 99
|
||||||
|
' \n' # 100 -109
|
||||||
|
' \n' # 110 -119
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lists of possible moves for each piece type.
|
||||||
|
N, E, S, W = -10, 1, 10, -1
|
||||||
|
directions = {
|
||||||
|
'P': (N, N+N, N+W, N+E),
|
||||||
|
'N': (N+N+E, E+N+E, E+S+E, S+S+E, S+S+W, W+S+W, W+N+W, N+N+W),
|
||||||
|
'B': (N+E, S+E, S+W, N+W),
|
||||||
|
'R': (N, E, S, W),
|
||||||
|
'Q': (N, E, S, W, N+E, S+E, S+W, N+W),
|
||||||
|
'K': (N, E, S, W, N+E, S+E, S+W, N+W)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mate value must be greater than 8*queen + 2*(rook+knight+bishop)
|
||||||
|
# King value is set to twice this value such that if the opponent is
|
||||||
|
# 8 queens up, but we got the king, we still exceed MATE_VALUE.
|
||||||
|
# When a MATE is detected, we'll set the score to - plies to get there
|
||||||
|
# E.g. Mate in 3 will be MATE_UPPER - 6
|
||||||
|
MATE_LOWER = piece['K'] - 10*piece['Q']
|
||||||
|
MATE_UPPER = piece['K'] + 10*piece['Q']
|
||||||
|
|
||||||
|
# The table size is the maximum number of elements in the transposition table.
|
||||||
|
TABLE_SIZE = 1e7
|
||||||
|
|
||||||
|
# Constants for tuning search
|
||||||
|
QS_LIMIT = 219
|
||||||
|
EVAL_ROUGHNESS = 13
|
||||||
|
DRAW_TEST = True
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Chess logic
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
class Position(namedtuple('Position', 'board score wc bc ep 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
|
||||||
|
"""
|
||||||
|
|
||||||
|
def gen_moves(self):
|
||||||
|
# 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.
|
||||||
|
for i, p in enumerate(self.board):
|
||||||
|
if not p.isupper():
|
||||||
|
continue
|
||||||
|
for d in directions[p]:
|
||||||
|
for j in count(i+d, d):
|
||||||
|
q = self.board[j]
|
||||||
|
# Stay inside the board, and off friendly pieces
|
||||||
|
if q.isspace() or q.isupper():
|
||||||
|
break
|
||||||
|
# Pawn move, double move and capture
|
||||||
|
if p == 'P' and d in (N, N+N) and q != '.':
|
||||||
|
break
|
||||||
|
if p == 'P' and d == N+N and (i < A1+N or self.board[i+N] != '.'):
|
||||||
|
break
|
||||||
|
if p == 'P' and d in (N+W, N+E) and q == '.' \
|
||||||
|
and j not in (self.ep, self.kp, self.kp-1, self.kp+1):
|
||||||
|
break
|
||||||
|
# Move it
|
||||||
|
yield (i, j)
|
||||||
|
# Stop crawlers from sliding, and sliding after captures
|
||||||
|
if p in 'PNK' or q.islower():
|
||||||
|
break
|
||||||
|
# Castling, by sliding the rook next to the king
|
||||||
|
if i == A1 and self.board[j+E] == 'K' and self.wc[0]:
|
||||||
|
yield (j+E, j+W)
|
||||||
|
if i == H1 and self.board[j+W] == 'K' and self.wc[1]:
|
||||||
|
yield (j+W, j+E)
|
||||||
|
|
||||||
|
def rotate(self):
|
||||||
|
''' Rotates the board, preserving enpassant '''
|
||||||
|
return Position(
|
||||||
|
self.board[::-1].swapcase(), -self.score, self.bc, self.wc,
|
||||||
|
119-self.ep if self.ep else 0,
|
||||||
|
119-self.kp if self.kp else 0)
|
||||||
|
|
||||||
|
def nullmove(self):
|
||||||
|
''' Like rotate, but clears ep and kp '''
|
||||||
|
return Position(
|
||||||
|
self.board[::-1].swapcase(), -self.score,
|
||||||
|
self.bc, self.wc, 0, 0)
|
||||||
|
|
||||||
|
def move(self, move):
|
||||||
|
i, j = move
|
||||||
|
p, q = self.board[i], self.board[j]
|
||||||
|
def put(board, i, p): return board[:i] + p + board[i+1:]
|
||||||
|
# Copy variables and reset ep and kp
|
||||||
|
board = self.board
|
||||||
|
wc, bc, ep, kp = self.wc, self.bc, 0, 0
|
||||||
|
score = self.score + self.value(move)
|
||||||
|
# Actual move
|
||||||
|
board = put(board, j, board[i])
|
||||||
|
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 abs(j-i) == 2:
|
||||||
|
kp = (i+j)//2
|
||||||
|
board = put(board, A1 if j < i else H1, '.')
|
||||||
|
board = put(board, kp, 'R')
|
||||||
|
# Pawn promotion, double move and en passant capture
|
||||||
|
if p == 'P':
|
||||||
|
if A8 <= j <= H8:
|
||||||
|
board = put(board, j, 'Q')
|
||||||
|
if j - i == 2*N:
|
||||||
|
ep = i + N
|
||||||
|
if j == self.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()
|
||||||
|
|
||||||
|
def value(self, move):
|
||||||
|
i, j = move
|
||||||
|
p, q = self.board[i], self.board[j]
|
||||||
|
# Actual move
|
||||||
|
score = pst[p][j] - pst[p][i]
|
||||||
|
# Capture
|
||||||
|
if q.islower():
|
||||||
|
score += pst[q.upper()][119-j]
|
||||||
|
# Castling check detection
|
||||||
|
if abs(j-self.kp) < 2:
|
||||||
|
score += pst['K'][119-j]
|
||||||
|
# Castling
|
||||||
|
if p == 'K' and abs(i-j) == 2:
|
||||||
|
score += pst['R'][(i+j)//2]
|
||||||
|
score -= pst['R'][A1 if j < i else H1]
|
||||||
|
# Special pawn stuff
|
||||||
|
if p == 'P':
|
||||||
|
if A8 <= j <= H8:
|
||||||
|
score += pst['Q'][j] - pst['P'][j]
|
||||||
|
if j == self.ep:
|
||||||
|
score += pst['P'][119-(j+S)]
|
||||||
|
return score
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Search logic
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
# lower <= s(pos) <= upper
|
||||||
|
Entry = namedtuple('Entry', 'lower upper')
|
||||||
|
|
||||||
|
|
||||||
|
class Searcher:
|
||||||
|
def __init__(self):
|
||||||
|
self.tp_score = {}
|
||||||
|
self.tp_move = {}
|
||||||
|
self.history = set()
|
||||||
|
self.nodes = 0
|
||||||
|
|
||||||
|
def bound(self, pos, gamma, depth, root=True):
|
||||||
|
""" returns r where
|
||||||
|
s(pos) <= r < gamma if gamma > s(pos)
|
||||||
|
gamma <= r <= s(pos) if gamma <= s(pos)"""
|
||||||
|
self.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 not root and pos in self.history:
|
||||||
|
print('Nit root', not root, 'or in history',
|
||||||
|
pos in self.history, pos.score)
|
||||||
|
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.
|
||||||
|
entry = self.tp_score.get(
|
||||||
|
(pos, depth, root), Entry(-MATE_UPPER, MATE_UPPER))
|
||||||
|
if entry.lower >= gamma and (not root or self.tp_move.get(pos) is not None):
|
||||||
|
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.
|
||||||
|
def moves():
|
||||||
|
# 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 and not root and any(c in pos.board for c in 'RBNQ'):
|
||||||
|
# print('Bound in moves1 RBNQ')
|
||||||
|
|
||||||
|
yield None, -self.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 None, 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.
|
||||||
|
killer = self.tp_move.get(pos)
|
||||||
|
if killer and (depth > 0 or pos.value(killer) >= QS_LIMIT):
|
||||||
|
# print('Bound in moves2 killer')
|
||||||
|
|
||||||
|
yield killer, -self.bound(pos.move(killer), 1-gamma, depth-1, root=False)
|
||||||
|
# Then all the other moves
|
||||||
|
for move in sorted(pos.gen_moves(), key=pos.value, reverse=True):
|
||||||
|
# for val, move in sorted(((pos.value(move), move) for move in pos.gen_moves()), reverse=True):
|
||||||
|
# If depth == 0 we only try moves with high intrinsic score (captures and
|
||||||
|
# promotions). Otherwise we do all moves.
|
||||||
|
if depth > 0 or pos.value(move) >= QS_LIMIT:
|
||||||
|
# print('Bound in moves3')
|
||||||
|
|
||||||
|
yield move, -self.bound(pos.move(move), 1-gamma, depth-1, root=False)
|
||||||
|
|
||||||
|
# Run through the moves, shortcutting when possible
|
||||||
|
best = -MATE_UPPER
|
||||||
|
for move, score in moves():
|
||||||
|
best = max(best, score)
|
||||||
|
if best >= gamma:
|
||||||
|
# Clear before setting, so we always have a value
|
||||||
|
if len(self.tp_move) > TABLE_SIZE:
|
||||||
|
self.tp_move.clear()
|
||||||
|
# Save the move for pv construction and killer heuristic
|
||||||
|
self.tp_move[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 and best < 0 and depth > 0:
|
||||||
|
def is_dead(pos): return any(pos.value(m) >=
|
||||||
|
MATE_LOWER for m in pos.gen_moves())
|
||||||
|
if all(is_dead(pos.move(m)) for m in pos.gen_moves()):
|
||||||
|
in_check = is_dead(pos.nullmove())
|
||||||
|
best = -MATE_UPPER if in_check else 0
|
||||||
|
|
||||||
|
# Clear before setting, so we always have a value
|
||||||
|
if len(self.tp_score) > TABLE_SIZE:
|
||||||
|
self.tp_score.clear()
|
||||||
|
# Table part 2
|
||||||
|
if best >= gamma:
|
||||||
|
self.tp_score[pos, depth, root] = Entry(best, entry.upper)
|
||||||
|
if best < gamma:
|
||||||
|
self.tp_score[pos, depth, root] = Entry(entry.lower, best)
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def search(self, pos, history=()):
|
||||||
|
""" Iterative deepening MTD-bi search """
|
||||||
|
self.nodes = 0
|
||||||
|
if DRAW_TEST:
|
||||||
|
self.history = set(history)
|
||||||
|
# print('# Clearing table due to new history')
|
||||||
|
self.tp_score.clear()
|
||||||
|
|
||||||
|
# In finished games, we could potentially go far enough to cause a recursion
|
||||||
|
# limit exception. Hence we bound the ply.
|
||||||
|
for depth in range(1, 1000):
|
||||||
|
# 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.
|
||||||
|
lower, upper = -MATE_UPPER, MATE_UPPER
|
||||||
|
while lower < upper - EVAL_ROUGHNESS:
|
||||||
|
gamma = (lower+upper+1)//2
|
||||||
|
# print(lower, upper, gamma, lower < upper - EVAL_ROUGHNESS)
|
||||||
|
score = self.bound(pos, gamma, depth)
|
||||||
|
print('Call bounder in while', score)
|
||||||
|
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.
|
||||||
|
self.bound(pos, lower, depth)
|
||||||
|
print('Call bounder final', score)
|
||||||
|
# If the game hasn't finished we can retrieve our move from the
|
||||||
|
# transposition table.
|
||||||
|
yield depth, self.tp_move.get(pos), self.tp_score.get((pos, depth, True)).lower
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# User interface
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Python 2 compatability
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
input = raw_input
|
||||||
|
|
||||||
|
|
||||||
|
def parse(c):
|
||||||
|
fil, rank = ord(c[0]) - ord('a'), int(c[1]) - 1
|
||||||
|
return A1 + fil - 10*rank
|
||||||
|
|
||||||
|
|
||||||
|
def render(i):
|
||||||
|
rank, fil = divmod(i - A1, 10)
|
||||||
|
return chr(fil + ord('a')) + str(-rank + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def print_pos(pos):
|
||||||
|
print()
|
||||||
|
uni_pieces = {'R': '♜', 'N': '♞', 'B': '♝', 'Q': '♛', 'K': '♚', 'P': '♟',
|
||||||
|
'r': '♖', 'n': '♘', 'b': '♗', 'q': '♕', 'k': '♔', 'p': '♙', '.': '·'}
|
||||||
|
for i, row in enumerate(pos.board.split()):
|
||||||
|
print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row))
|
||||||
|
print(' a b c d e f g h \n\n')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
hist = [Position(initial, 0, (True, True), (True, True), 0, 0)]
|
||||||
|
searcher = Searcher()
|
||||||
|
while True:
|
||||||
|
print_pos(hist[-1])
|
||||||
|
|
||||||
|
if hist[-1].score <= -MATE_LOWER:
|
||||||
|
print("You lost")
|
||||||
|
break
|
||||||
|
|
||||||
|
# We query the user until she enters a (pseudo) legal move.
|
||||||
|
move = None
|
||||||
|
while move not in hist[-1].gen_moves():
|
||||||
|
match = re.match('([a-h][1-8])'*2, input('Your move: '))
|
||||||
|
if match:
|
||||||
|
move = parse(match.group(1)), parse(match.group(2))
|
||||||
|
else:
|
||||||
|
# Inform the user when invalid input (e.g. "help") is entered
|
||||||
|
print("Please enter a move like g8f6")
|
||||||
|
hist.append(hist[-1].move(move))
|
||||||
|
print(hist[-1].score)
|
||||||
|
# After our move we rotate the board and print it again.
|
||||||
|
# This allows us to see the effect of our move.
|
||||||
|
print_pos(hist[-1].rotate())
|
||||||
|
|
||||||
|
if hist[-1].score <= -MATE_LOWER:
|
||||||
|
print("You won")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fire up the engine to look for a move.
|
||||||
|
start = time.time()
|
||||||
|
for _depth, move, score in searcher.search(hist[-1], hist):
|
||||||
|
if time.time() - start > 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
if score == MATE_UPPER:
|
||||||
|
print("Checkmate!")
|
||||||
|
|
||||||
|
# The black player moves from a rotated position, so we have to
|
||||||
|
# 'back rotate' the move before printing it.
|
||||||
|
print("My move:", render(119-move[0]) + render(119-move[1]), score)
|
||||||
|
hist.append(hist[-1].move(move))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user