Problems with search loop
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build output.
|
||||
build/
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dart",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "bin/dartsunfish.dart",
|
||||
"console": "terminal",
|
||||
// "flutterMode": "debug"
|
||||
// "dartMode": "debug",
|
||||
},
|
||||
// {
|
||||
// "name": "dartsunfish",
|
||||
// "request": "launch",
|
||||
// "type": "dart"
|
||||
// }
|
||||
]
|
||||
}
|
||||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
A sample command-line application with an entrypoint in `bin/`, library code
|
||||
in `lib/`, and example unit test in `test/`.
|
||||
16
analysis_options.yaml
Normal file
16
analysis_options.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Defines a default set of lint rules enforced for projects at Google. For
|
||||
# details and rationale, see
|
||||
# https://github.com/dart-lang/pedantic#enabled-lints.
|
||||
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
|
||||
# Uncomment to specify additional rules.
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
5
bin/dartsunfish.dart
Normal file
5
bin/dartsunfish.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
import 'package:dartsunfish/dartsunfish.dart' as dartsunfish;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
dartsunfish.GameBot().playGame();
|
||||
}
|
||||
715
lib/dartsunfish.dart
Normal file
715
lib/dartsunfish.dart
Normal file
@@ -0,0 +1,715 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
341
pubspec.lock
Normal file
341
pubspec.lock
Normal file
@@ -0,0 +1,341 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "23.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.10"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
pedantic:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.10"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.17.10"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.12.0 <3.0.0"
|
||||
16
pubspec.yaml
Normal file
16
pubspec.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
name: dartsunfish
|
||||
description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
# homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
# path: ^1.8.0
|
||||
# flutter:
|
||||
# sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.10.0
|
||||
test: ^1.16.0
|
||||
8
test/dartsunfish_test.dart
Normal file
8
test/dartsunfish_test.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
// import 'package:dartsunfish/dartsunfish.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('calculate', () {
|
||||
// expect(calculate(), 42);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user