diff --git a/lib/index.dart b/lib/index.dart index 887b039..2837750 100644 --- a/lib/index.dart +++ b/lib/index.dart @@ -1,5 +1,7 @@ export 'src/dartsunfish.dart'; export 'src/extentions.dart'; export 'src/globals.dart'; -export 'src/game_play.dart'; +export 'src/helpers.dart'; +export 'src/position.dart'; +export 'src/searcher.dart'; export 'src/ui.dart'; diff --git a/lib/src/dartsunfish.dart b/lib/src/dartsunfish.dart index 6125914..b2bb0c2 100644 --- a/lib/src/dartsunfish.dart +++ b/lib/src/dartsunfish.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:developer' as dv; import 'package:dartsunfish/index.dart'; class GameBot { @@ -20,6 +19,7 @@ class GameBot { // We query the user until she enters a (pseudo) legal move. var move = []; + var score = 0; final possibleMoves = hist.last.generateMoves().toList(); while (!possibleMoves.doContains(move)) { var pattern = RegExp( @@ -55,16 +55,15 @@ class GameBot { print('You won'); break; } - // Fire up the engine to look for a move. - var _depth = 0; - var score = 0; - var start = getSeconds(); /* */ + // Fire up the engine to look for a move. + // var _depth = 0; + var start = getSeconds(); for (var s in searcher.search(hist.last, hist.toSet())) { - _depth = s[0]; - move = s[1]; - score = s[2] ?? 0; + // _depth = s[0]; + move = s[0]; + score = s[1] ?? 0; if (getSeconds() - start > 1) { break; } diff --git a/lib/src/extentions.dart b/lib/src/extentions.dart index f1731da..3631ca3 100644 --- a/lib/src/extentions.dart +++ b/lib/src/extentions.dart @@ -14,3 +14,68 @@ extension CheckString on String { extension CheckListOfTuples on List { bool doContains(newList) => newList.length == 2 && indexWhere((l) => l[0] == newList[0] && l[1] == newList[1]) > -1; } + +extension CompareIretables on Iterable { + 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 mapIndexed(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(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; + } +} diff --git a/lib/src/game_play.dart b/lib/src/game_play.dart deleted file mode 100644 index eb0b009..0000000 --- a/lib/src/game_play.dart +++ /dev/null @@ -1,421 +0,0 @@ -import 'dart:math'; - -import 'package:dartsunfish/index.dart'; - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Chess logic -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class Position { - Position( - this.board, - this.score, - this.wc, - this.bc, - this.ep, - this.kp, - ); - String board; - int score; - List wc; - List 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> generateMoves() sync* { - // For each of our pieces, iterate through each possible 'ray' of moves, - // as defined in the 'directions' map. The rays are broken e.g. by - // captures or immediately in case of pieces such as knights. - //TODO: add limitation of 3 cells for Wizard - final charsList = board.split(''); - for (var i = 0; i < board.length; i++) { - final p = charsList[i]; - // yield ([i, 12]); - // } - // board.split('').asMap().forEach((i, p) sync* { - if (!p.isUpper) { - continue; - } - for (var d in directions[p]!) { - for (var j in count(i + d, d)) { - final q = board[j]; - // Stay inside the board, and off friendly pieces - if (q.isSpace || q.isUpper) { - break; - } - // Pawn move, double move and capture - if (p == 'P' && [N, N + N].contains(d) && q != '.') { - break; - } - if (p == 'P' && d == (N + N) && (i < A1 + N || board[i + N] != '.')) { - break; - } - if (p == 'P' && [N + W, N + E].contains(d) && q == '.' && ![ep, kp, kp - 1, kp + 1].contains(j)) { - break; - } - // Move it - final res = [i, j]; - yield (res); - - // Stop crawlers from sliding, and sliding after captures - if ('PNK'.contains(p) || q.isLower) { - break; - } - // Castling, by sliding the rook next to the king - if (i == A1 && board[j + E] == 'K' && wc[0]) { - yield ([j + E, j + W]); - } - - if (i == H1 && board[j + W] == 'K' && wc[1]) { - yield ([j + W, j + E]); - } - } - } - } - } - - // String reversed(String subject) { - // return subject.split('').reversed.join(); - // } - - String swapCase(String subject) { - if (subject is! String || subject.isEmpty) { - return ''; - } - String _swap(String l) { - if (l.toUpperCase() == l) { - return l.toLowerCase(); - } else { - return l.toUpperCase(); - } - } - - var subjectChars = subject.split(''); - - return subjectChars.map((x) => _swap(x)).join(); - } - - Position rotate() { - ''' Rotates the board, preserving enpassant '''; - return Position(board.reversedAndSwapCased, -score, bc, wc, ep > 0 ? 119 - ep : 0, kp > 0 ? 119 - kp : 0); - } - - Position nullmove() { - ''' Like rotate, but clears ep and kp '''; - return Position(board.reversedAndSwapCased, -score, bc, wc, 0, 0); - } - - String put(String _str, int i, String p) { - final newStr = _str.substring(0, i) + p + _str.substring(i + 1); - return newStr; - } - -// move piece due to the move scecification - Position movePiece(List 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 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 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 tpScore = {}; - late Map?> tpMove = {}; - late Set 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> moves() sync* { - // First try not moving at all. We only do this if there is at least one major - // piece left on the board, since otherwise zugzwangs are too dangerous. - // if (depth > 0 && !root && any(c in pos.board for c in 'RBNQ')){ - if (depth > 0 && !root && pos.board.split('').any((c) => 'RBNQ'.contains(c))) { - // print('post 1st condition'); - yield ([null, -1 * bound(pos.nullmove(), 1 - gamma, depth - 3, root: false)]); - } - - // For QSearch we have a different kind of null-move, namely we can just stop - // and not capture anything else. - if (depth == 0) { - // print('post 2nd condition'); - yield ([null, pos.score]); - } - // Then killer move. We search it twice, but the tp will fix things for us. - // Note, we don't have to check for legality, since we've already done it - // before. Also note that in QS the killer must be a capture, otherwise we - // will be non deterministic. - var killer = tpMove[pos]; - if (killer != null && (depth > 0 || pos.value(killer) >= QS_LIMIT)) { - // print('post killer'); - yield ([killer, -1 * bound(pos.movePiece(killer), 1 - gamma, depth - 1, root: false)]); - } - // Then all the other moves - var possibleMoves = pos.generateMoves().toList() - ..sort((a, b) => pos.value(a).compareTo(pos.value(b))) - ..toList(); //.reversed; - for (var move in possibleMoves.reversed) { - //pos.generateMoves().toList().sort((a, b) => a.value.compareto(b.value)).toList().reversed)) { - // for val, move in sorted(((pos.value(move), move) for move in pos.generateMoves()), reverse=true): - // If depth == 0 we only try moves with high intrinsic score (captures and - // promotions). Otherwise we do all moves. - if (depth > 0 || pos.value(move) >= QS_LIMIT) { - yield ([move, -1 * bound(pos.movePiece(move), 1 - gamma, depth - 1, root: false)]); - } - } - } - - // Run through the moves, shortcutting when possible - var best = -MATE_UPPER; - for (var m in moves()) { - List? move = m[0]; - int score = m[1]; - best = max(best, score); - // if (depth == 0){ - // print([ - // 'Depth', - // depth, - // 'Move', - // move != null ? UI.render(119 - move[0]) + UI.render(119 - move[1]) : 'No move', - // 'Score', - // score, - // 'Best', - // best, - // 'Gamma', - // gamma, - // 'STOP', - // best >= gamma - // ]);} - if (best >= gamma) { - // Clear before setting, so we always have a value - if (tpMove.length > TABLE_SIZE) { - tpMove.clear(); - } - // Save the move for pv construction and killer heuristic - tpMove[pos] = move; - break; - } - } - // Stalemate checking is a bit tricky: Say we failed low, because - // we can't (legally) move and so the (real) score is -infty. - // At the next depth we are allowed to just return r, -infty <= r < gamma, - // which is normally fine. - // However, what if gamma = -10 and we don't have any legal moves? - // Then the score is actaully a draw and we should fail high! - // Thus, if best < gamma and best < 0 we need to double check what we are doing. - // This doesn't prevent sunfish from making a move that results in stalemate, - // but only if depth == 1, so that's probably fair enough. - // (Btw, at depth 1 we can also mate without realizing.) - if (best < gamma && best < 0 && depth > 0) { - bool is_dead(pos) { - return pos.value.any((m) => m >= MATE_LOWER); - } - - // if (all(is_dead(pos.move(m)) for m in pos.generateMoves())){ - if (pos.generateMoves().every((m) => is_dead(pos.movePiece(m)))) { - var in_check = is_dead(pos.nullmove()); - best = in_check ? -MATE_UPPER : 0; - } - } - // Clear before setting, so we always have a value - if (tpScore.length > TABLE_SIZE) { - tpScore.clear(); - } - // Table part 2 - if (best >= gamma) { - tpScore[[pos, depth, root]] = Entry(best, entry.upper); - } - if (best < gamma) { - tpScore[[pos, depth, root]] = Entry(entry.lower, best); - } - return best; - } - - Iterable search(Position pos, Set _history) sync* { - ''' Iterative deepening MTD-bi search '''; - // nodes = 0; - - if (DRAW_TEST) { - history = _history; - // Clearing table due to new history - tpScore.clear(); - } - // In finished games, we could potentially go far enough to cause a recursion - // limit exception. Hence we bound the ply. - for (var depth = 1; depth < 1000; depth++) { - // The inner loop is a binary search on the score of the position. - // Inv: lower <= score <= upper - // 'while lower != upper' would work, but play tests show a margin of 20 plays - // better. - var lower = -MATE_UPPER; - var upper = MATE_UPPER; - while (lower < upper - EVAL_ROUGHNESS) { - var gamma = (lower + upper + 1) ~/ 2; - var score = bound(pos, gamma, depth); - if (score >= gamma) lower = score; - if (score < gamma) upper = score; - } - // We want to make sure the move to play hasn't been kicked out of the table, - // So we make another call that must always fail high and thus produce a move. - // bound(pos, lower, depth); - // If the game hasn't finished we can retrieve our move from the - // transposition table. - yield ([ - depth, - tpMove[pos], - tpScore[[pos, depth, true]]?.lower - ]); - } - } -} diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart new file mode 100644 index 0000000..de50809 --- /dev/null +++ b/lib/src/helpers.dart @@ -0,0 +1,29 @@ +/// python counter immulation +Iterable 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(); +} diff --git a/lib/src/position.dart b/lib/src/position.dart new file mode 100644 index 0000000..23651c1 --- /dev/null +++ b/lib/src/position.dart @@ -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 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> 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 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 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; + } +} diff --git a/lib/src/searcher.dart b/lib/src/searcher.dart new file mode 100644 index 0000000..356f12c --- /dev/null +++ b/lib/src/searcher.dart @@ -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 tpScore = {}; + late Map?> tpMove = {}; + late Set 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) { + // 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({pos, depth, root}, or: Entry(-MATE_UPPER, MATE_UPPER)); // as Entry; + if (entry!.lower >= gamma && (!root || tpMove.get>(pos, or: [])!.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> 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>(pos, or: []); + 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? 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 search(Position pos, Set _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>(pos), + tpScore.get({pos, depth, true})?.lower + ]); + } + } +} diff --git a/lib/src/ui.dart b/lib/src/ui.dart index 19371b4..d935d38 100644 --- a/lib/src/ui.dart +++ b/lib/src/ui.dart @@ -31,17 +31,20 @@ class UI { 'k': '♔', 'p': '♙', '.': '·', - // ' ': ' ' }; print(''); - final bordChars = pos.board.split('\n'); - for (var i = 0; i < bordChars.length; i++) { - var row = bordChars[i]; - if (!row.isSpace) { - var pieces = row.split('').map((e) => e = e.isSpace ? '' : uni_pieces[e]!); + final bordRows = pos.board.split('\n'); + for (var i = 0; i < bordRows.length; i++) { + var row = bordRows[i].split(''); + var pieces = row.map((e) => e.isSpace ? '' : uni_pieces.get(e, or: e)); + if (!row.every((element) => element.isSpace)) { print('${(10 - i)}${pieces.join(' ')}'); } } - print(' a b c d e f g h \n\n'); + print(' a b c d e f g h'); + print(' \n\n'); } + + // for i, row in enumerate(pos.board.split()): + // print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row)) } diff --git a/lib/tester.dart b/lib/tester.dart new file mode 100644 index 0000000..416ab98 --- /dev/null +++ b/lib/tester.dart @@ -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 toMap() { + return { + 'id': id, + 'name': name, + }; + } + + factory MyItem.fromMap(Map 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 args) { + final someList = List.generate( + 10, + (i) => { + {i, 's'}: 'See item $i' + }); + + bool root = true; + + final someSet = Set.from(List.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 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))}'); + } +} diff --git a/pubspec.lock b/pubspec.lock index dd1cac0..129b96b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" file: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e910af..1b91055 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,9 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: -# path: ^1.8.0 # flutter: # sdk: flutter - + equatable: 2.0.3 dev_dependencies: pedantic: ^1.10.0 test: ^1.16.0 diff --git a/python/sunfish.py b/python/sunfish.py new file mode 100644 index 0000000..1ab917d --- /dev/null +++ b/python/sunfish.py @@ -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()