Split singe file into set of src files

This commit is contained in:
DMK
2021-08-01 21:13:25 +03:00
parent 8d51d69ab3
commit 186870e770
7 changed files with 312 additions and 306 deletions

View File

@@ -1,4 +1,4 @@
import 'package:dartsunfish/dartsunfish.dart' as dartsunfish;
import 'package:dartsunfish/index.dart' as dartsunfish;
void main(List<String> arguments) {
dartsunfish.GameBot().playGame();

5
lib/index.dart Normal file
View File

@@ -0,0 +1,5 @@
export 'src/dartsunfish.dart';
export 'src/extentions.dart';
export 'src/globals.dart';
export 'src/game_play.dart';
export 'src/ui.dart';

87
lib/src/dartsunfish.dart Normal file
View File

@@ -0,0 +1,87 @@
import 'dart:io';
import 'dart:developer' as dv;
import 'package:dartsunfish/index.dart';
class GameBot {
GameBot();
void gameMain() {
var hist = <Position>[
Position(initial, 0, [true, true], [true, true], 0, 0),
];
var searcher = Searcher();
while (true) {
UI.printBoard(hist.last);
if (hist.last.score <= -MATE_LOWER) {
print('You lost');
break;
}
// We query the user until she enters a (pseudo) legal move.
var move = <int>[];
final possibleMoves = hist.last.generateMoves().toList();
while (!possibleMoves.doContains(move)) {
var pattern = RegExp(
r'([a-h][1-8])' * 2,
caseSensitive: false,
multiLine: false,
);
print('Your move:');
var input = stdin.readLineSync();
var match = pattern.firstMatch(input.toString());
if (match != null) {
move = [UI.parse(match.group(1)), UI.parse(match.group(2))];
if (!possibleMoves.doContains(move)) {
print('Not valid move typed in. Try again');
}
} else {
// Inform the user when invalid input (e.g. "help") is entered
print('Please enter a move like g8f6');
}
}
try {
hist.add(hist.last.movePiece(move));
} catch (e) {
// print(e.toString());
}
// After our move we rotate the board and print it again.
// This allows us to see the effect of our move.
UI.printBoard(hist.last.rotate());
// print('${hist.last.score}');
if (hist.last.score <= -MATE_LOWER) {
print('You won');
break;
}
// Fire up the engine to look for a move.
var _depth = 0;
var 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() {
setPst();
gameMain();
}
}

16
lib/src/extentions.dart Normal file
View File

@@ -0,0 +1,16 @@
///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;
}

View File

@@ -1,169 +1,6 @@
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;
import 'package:dartsunfish/index.dart';
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Chess logic
@@ -194,7 +31,7 @@ class Position {
kp - the king passant square
""";
*/
Iterable<List<int>> gen_moves() sync* {
Iterable<List<int>> generateMoves() sync* {
// For each of our pieces, iterate through each possible 'ray' of moves,
// as defined in the 'directions' map. The rays are broken e.g. by
// captures or immediately in case of pieces such as knights.
@@ -391,13 +228,13 @@ class Searcher {
late Map<List, Entry> tpScore = {};
late Map<Position, List<int>?> tpMove = {};
late Set<Position> history;
int nodes = 0;
// 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;
// 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
@@ -465,12 +302,12 @@ class Searcher {
yield ([killer, -1 * bound(pos.movePiece(killer), 1 - gamma, depth - 1, root: false)]);
}
// Then all the other moves
var possibleMoves = pos.gen_moves().toList()
var possibleMoves = pos.generateMoves().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):
//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) {
@@ -525,8 +362,8 @@ class Searcher {
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)))) {
// 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;
}
@@ -547,11 +384,11 @@ class Searcher {
Iterable<dynamic> search(Position pos, Set<Position> _history) sync* {
''' Iterative deepening MTD-bi search ''';
nodes = 0;
// nodes = 0;
if (DRAW_TEST) {
history = _history;
// print('// Clearing table due to new history')
// Clearing table due to new history
tpScore.clear();
}
// In finished games, we could potentially go far enough to cause a recursion
@@ -582,134 +419,3 @@ class Searcher {
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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();
}
}

145
lib/src/globals.dart Normal file
View File

@@ -0,0 +1,145 @@
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// // Piece-Square tables. Tune these to change sunfish's behaviour
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// TODO: add wizard
final piece = {'P': 100, 'N': 280, 'B': 320, 'R': 479, 'Q': 929, 'K': 60000};
///TODO: add 2 raws and 2 colums to each set AND Wizard pst
var pst = {
'P': [
0, 0, 0, 0, 0, 0, 0, 0, //
78, 83, 86, 73, 102, 82, 85, 90, //
7, 29, 21, 44, 40, 31, 44, 7, //
-17, 16, -2, 15, 14, 0, 15, -13, //
-26, 3, 10, 9, 6, 1, 0, -23, //
-22, 9, 5, -11, -10, -2, 3, -19, //
-31, 8, -7, -37, -36, -14, 3, -31, //
0, 0, 0, 0, 0, 0, 0, 0
], //
'N': [
-66, -53, -75, -75, -10, -55, -58, -70, //
-3, -6, 100, -36, 4, 62, -4, -14, //
10, 67, 1, 74, 73, 27, 62, -2, //
24, 24, 45, 37, 33, 41, 25, 17, //
-1, 5, 31, 21, 22, 35, 2, 0, //
-18, 10, 13, 22, 18, 15, 11, -14, //
-23, -15, 2, 0, 2, 0, -23, -20, //
-74, -23, -26, -24, -19, -35, -22, -69
], //
'B': [
-59, -78, -82, -76, -23, -107, -37, -50, //
-11, 20, 35, -42, -39, 31, 2, -22, //
-9, 39, -32, 41, 52, -10, 28, -14, //
25, 17, 20, 34, 26, 25, 15, 10, //
13, 10, 17, 23, 17, 16, 0, 7, //
14, 25, 24, 15, 8, 25, 20, 15, //
19, 20, 11, 6, 7, 6, 20, 16, //
-7, 2, -15, -12, -14, -15, -10, -10
], //
'R': [
35, 29, 33, 4, 37, 33, 56, 50, //
55, 29, 56, 67, 55, 62, 34, 60, //
19, 35, 28, 33, 45, 27, 25, 15, //
0, 5, 16, 13, 18, -4, -9, -6, //
-28, -35, -16, -21, -13, -29, -46, -30, //
-42, -28, -42, -25, -25, -35, -26, -46, //
-53, -38, -31, -26, -29, -43, -44, -53, //
-30, -24, -18, 5, -2, -18, -31, -32
], //
'Q': [
6, 1, -8, -104, 69, 24, 88, 26, //
14, 32, 60, -10, 20, 76, 57, 24, //
-2, 43, 32, 60, 72, 63, 43, 2, //
1, -16, 22, 17, 25, 20, -13, -6, //
-14, -15, -2, -5, -1, -10, -20, -22, //
-30, -6, -13, -11, -16, -11, -16, -27, //
-36, -18, 0, -19, -15, -15, -21, -38, //
-39, -30, -31, -13, -31, -36, -34, -42
], //
'K': [
4, 54, 47, -99, -99, 60, 83, -62, //
-32, 10, 55, 56, 56, 55, 10, 3, //
-62, 12, -57, 44, -67, 28, 37, -31, //
-55, 50, 11, -4, -19, 13, 0, -49, //
-55, -43, -52, -28, -51, -47, -8, -50, //
-47, -42, -43, -79, -64, -32, -29, -32, //
-4, 3, -14, -50, -57, -18, 13, 4, //
17, 30, -3, -14, 6, -1, 40, 18
], //
};
// recalculating Piece-Square raw into desk surrounded by 0-s
List<int> padrow(List<int> row, String k) {
var rowBody = <int>[for (int x in row) x + piece[k]!];
return [
0,
...rowBody,
0,
];
}
void setPst() {
pst.forEach((key, item) {
final innerItem = [...List.filled(20, 0)];
for (var i = 0; i < 8; i++) {
innerItem.addAll(padrow(item.getRange(i * 8, i * 8 + 8).toList(), key));
}
innerItem.addAll(List.filled(20, 0));
pst[key] = innerItem;
});
}
int getSeconds() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000.toInt();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Global constants
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Our board is represented as a 120 character string. The padding allows for
// fast detection of moves that don't stay within the board.
//TODO: Tune globals for 10x10 field
final A1 = 91, H1 = 98, A8 = 21, H8 = 28;
final initial = (' \n' // 0 - 9
' \n' // 10 - 19
' rnbqkbnr\n' // 20 - 29
' pppppppp\n' // 30 - 39
' ........\n' // 40 - 49
' ........\n' // 50 - 59
' ........\n' // 60 - 69
' ........\n' // 70 - 79
' PPPPPPPP\n' // 80 - 89
' RNBQKBNR\n' // 90 - 99
' \n' // 100 -109
' \n' // 110 -119
);
// Lists of possible moves for each piece type.
int N = -10, E = 1, S = 10, W = -1;
//TODO: add directions for Wizard
final Map<String?, List<int>> directions = {
'P': [N, N + N, N + W, N + E],
'N': [N + N + E, E + N + E, E + S + E, S + S + E, S + S + W, W + S + W, W + N + W, N + N + W],
'B': [N + E, S + E, S + W, N + W],
'R': [N, E, S, W],
'Q': [N, E, S, W, N + E, S + E, S + W, N + W],
'K': [N, E, S, W, N + E, S + E, S + W, N + W],
'.': []
};
// Mate value must be greater than 8*queen + 2*(rook+knight+bishop)
// King value is set to twice this value such that if the opponent is
// 8 queens up, but we got the king, we still exceed MATE_VALUE.
// When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there
// E.g. Mate in 3 will be MATE_UPPER - 6
// TOD: replace mate with King capture event
final MATE_LOWER = piece['K']! - 10 * piece['Q']!;
final MATE_UPPER = piece['K']! + 10 * piece['Q']!;
// The table size is the maximum number of elements in the transposition table.
final TABLE_SIZE = 1e7;
// Constants for tuning search
final QS_LIMIT = 219, EVAL_ROUGHNESS = 13, DRAW_TEST = true;

47
lib/src/ui.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'package:dartsunfish/index.dart';
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// User interface
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class UI {
static int parse(String? c) {
var fil = c![0].codeUnits.first - 'a'.codeUnits.first;
var rank = int.parse(c[1]) - 1;
return A1 + fil - 10 * rank;
}
static String render(int i) {
var rank = (i - A1) ~/ 10;
var fil = (i - A1) % 10;
return String.fromCharCodes([fil + 'a'.codeUnits.first]) + (-rank + 1).toString();
}
static void printBoard(Position pos) {
final uni_pieces = {
'R': '',
'N': '',
'B': '',
'Q': '',
'K': '',
'P': '',
'r': '',
'n': '',
'b': '',
'q': '',
'k': '',
'p': '',
'.': '·',
// ' ': ' '
};
print('');
final 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');
}
}