commit 8d51d69ab3f17da9f2818365665844630ce60a43 Author: DMK Date: Sun Aug 1 15:36:03 2021 +0300 Problems with search loop diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c8a157 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d99d7e1 --- /dev/null +++ b/.vscode/launch.json @@ -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" + // } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3816eca --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..18b40b8 --- /dev/null +++ b/analysis_options.yaml @@ -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/** diff --git a/bin/dartsunfish.dart b/bin/dartsunfish.dart new file mode 100644 index 0000000..a6d7a8d --- /dev/null +++ b/bin/dartsunfish.dart @@ -0,0 +1,5 @@ +import 'package:dartsunfish/dartsunfish.dart' as dartsunfish; + +void main(List arguments) { + dartsunfish.GameBot().playGame(); +} diff --git a/lib/dartsunfish.dart b/lib/dartsunfish.dart new file mode 100644 index 0000000..41a7b89 --- /dev/null +++ b/lib/dartsunfish.dart @@ -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 padrow(List row, String k) { + var rowBody = [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> 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 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> 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 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.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? 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 search(Position pos, Set _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(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 = []; + 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(); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..dd1cac0 --- /dev/null +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..1e910af --- /dev/null +++ b/pubspec.yaml @@ -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 diff --git a/test/dartsunfish_test.dart b/test/dartsunfish_test.dart new file mode 100644 index 0000000..6c4bd15 --- /dev/null +++ b/test/dartsunfish_test.dart @@ -0,0 +1,8 @@ +// import 'package:dartsunfish/dartsunfish.dart'; +import 'package:test/test.dart'; + +void main() { + test('calculate', () { + // expect(calculate(), 42); + }); +}