From 0396644151864687605b946f2adac5ca5c3ae904 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Tue, 26 Apr 2022 08:02:42 -0500 Subject: [PATCH] started replacing sunfish with stockfish --- __main__.py | 10 +- plugin/chess/draw.py | 8 +- plugin/chess/game.py | 94 +++++---- plugin/chess/sunfish.py | 438 ---------------------------------------- plugin/chess/util.py | 41 ---- 5 files changed, 56 insertions(+), 535 deletions(-) delete mode 100644 plugin/chess/sunfish.py delete mode 100644 plugin/chess/util.py diff --git a/__main__.py b/__main__.py index d6911dd..a8b852d 100644 --- a/__main__.py +++ b/__main__.py @@ -1,6 +1,7 @@ import os import signal import time +from traceback import print_exc from importlib import import_module from icecream import ic from menu import Menu, MenuItem, MenuType @@ -55,8 +56,8 @@ try: plugin = import_module("plugin." + item.data["plugin"]) ic(plugin) plugin.execute(cinput, graphics, item.data["arg"]) - except Exception as e: - ic(e) + except: + print_exc() graphics.clear() graphics.text("Plugin error!", 0, 0, 1) graphics.show() @@ -64,7 +65,6 @@ try: elif item.menu_type == MenuType.EXIT_CMD: os.system(item.data["command"]) program_exit() -except Exception as e: - ic(e) +except: + print_exc() program_exit() - diff --git a/plugin/chess/draw.py b/plugin/chess/draw.py index 47e4076..d5aeaf0 100644 --- a/plugin/chess/draw.py +++ b/plugin/chess/draw.py @@ -1,6 +1,6 @@ from icecream import ic from graphics import Graphics -from .sunfish import Position +from chess import Board, Color, WHITE, BLACK class Draw: BOARD_SIZE = 64 @@ -79,9 +79,11 @@ class Draw: self._graphics.fill_rect( self.BOARD_SIZE, 0, self.BOARD_SIZE, self.BOARD_SIZE, 0) - def draw_board(self, pos: Position, player_color: str): + def draw_board(self, board: Board, player_color: Color): + ic(board) self._graphics.fill_rect(0, 0, self.BOARD_SIZE, self.BOARD_SIZE, 0) - nb = "".join(pos.board.split()) if player_color == "w" else "".join(pos.rotate().board.split())[::-1] + nb = "".join(str(board).split()).replace(" ", "") + nb = nb if player_color == WHITE else nb[::-1] c = True for row in range(8): for col in range(8): diff --git a/plugin/chess/game.py b/plugin/chess/game.py index 404745d..52caa82 100644 --- a/plugin/chess/game.py +++ b/plugin/chess/game.py @@ -1,11 +1,12 @@ from os import path from enum import Enum, auto -import time +import shutil +from icecream import ic from cinput import ControlInput, Button from graphics import Graphics from .draw import Draw -from . import sunfish -from . import util +from chess import Board, Move, WHITE, BLACK +from stockfish import Stockfish class GameState(Enum): MAIN_MENU = auto() @@ -19,35 +20,33 @@ class ChessGame: MAX_THINKING_SECONDS = 3 def __init__(self, cinput: ControlInput, graphics: Graphics): + self._sf = Stockfish(shutil.which("stockfish") or "stockfish") self._cinput = cinput self._draw = Draw(graphics) self._state = GameState.MAIN_MENU self._menu_index = 0 - self._searcher = sunfish.Searcher() - def _init_new_game(self, pos: sunfish.Position): - self._hist = [pos] + def _init_new_game(self, fen = None): + self._board = Board() if fen == None else Board(fen) self._last_move = "Begin!" self._all_moves = dict() self._src_idx = 0 self._dst_idx = 0 - self._draw.draw_board(self._hist[-1], self._player_color) + self._player_color = self._board.turn + self._sf.set_fen_position(self._board.fen()) + self._draw.draw_board(self._board, self._player_color) def _load_saved_or_init(self): if path.exists(self.SAVE_FILE): with open(self.SAVE_FILE, "r") as fen: - fenstr = fen.read() - pos = util.parseFEN(fenstr) - self._player_color = util.get_color(pos) - self._init_new_game(pos) + fen = fen.read() + self._init_new_game(fen) else: - self._player_color = "w" - self._init_new_game(sunfish.Position( - sunfish.initial, 0, (True,True), (True,True), 0, 0)) + self._init_new_game() def _save_game(self): with open(self.SAVE_FILE, "w") as fen: - fen.write(util.renderFEN(self._hist[-1])) + fen.write(self._board.fen()) def run(self): # either load the save game or start a new one @@ -55,8 +54,7 @@ class ChessGame: while True: key = None - move = None - src = "" + src = None ################ # HANDLE STATE # @@ -70,25 +68,30 @@ class ChessGame: # computer makes a move elif self._state == GameState.THINKING: self._draw.draw_thinking(self._last_move) - start = time.time() - for _, self._move, self._score in self._searcher.search( - self._hist[-1], self._hist): - if time.time() - start > self.MAX_THINKING_SECONDS: - break - self._hist.append(self._hist[-1].move(self._move)) - self._draw.draw_board(self._hist[-1], self._player_color) - self._last_move = util.move_str(move, self._player_color == "w") - # reset user moves and move state to user's turn - self._all_moves = dict() - self._state = GameState.CHOOSE_SRC + # TODO: check game over + self._move = self._sf.get_best_move() + if self._move != None: + self._board.push(Move.from_uci(self._move)) + # reset user moves and move state to user's turn + self._all_moves = dict() + self._src_idx = 0 + self._dst_idx = 0 + self._state = GameState.CHOOSE_SRC + else: + self._state = GameState.GAME_OVER + self._draw.draw_board(self._board, self._player_color) # user picks source piece elif self._state == GameState.CHOOSE_SRC: if len(list(self._all_moves)) == 0: - self._all_moves = util.get_all_moves(self._hist[-1]) - src = list(self._all_moves)[self._src_idx] - self._draw.draw_select(self._last_move, src) - key = self._cinput.get_one_shot() + for src, dst in map(lambda m: [Move.uci(m)[0:2], Move.uci(m)[2:4]], self._board.legal_moves): + self._all_moves.setdefault(src, []).append(dst) + if len(list(self._all_moves > 0)): + src = list(self._all_moves)[self._src_idx] + self._draw.draw_select(self._last_move, src) + key = self._cinput.get_one_shot() + else: + self._state = GameState.GAME_OVER # user picks dest square elif self._state == GameState.CHOOSE_DST: @@ -126,18 +129,13 @@ class ChessGame: self._save_game() return elif self._menu_index == 2 or self._menu_index == 3: # new game - pos = sunfish.Position( - sunfish.initial, 0, (True,True), (True,True), 0, 0) - if self._menu_index == 3: - self._player_color = "b" - self._state = GameState.THINKING - else: - self._player_color = "w" + self._init_new_game() + if self._menu_index == 2: + self._player_color = WHITE self._state = GameState.CHOOSE_SRC - self._init_new_game(pos) - # TODO: figure out why I need to do this - self._draw.draw_board( - pos if self._player_color == "w" else pos.rotate(), self._player_color) + else: + self._player_color = BLACK + self._state = GameState.THINKING # handle user input when selecting source piece # TODO: some kind of cursor indicator on board @@ -180,15 +178,15 @@ class ChessGame: # make the move elif key == Button.BTN_B: dst = self._all_moves[src][self._dst_idx] - # TODO: fix rotation depending on player color - self._last_move = src + " - " + dst - sfmove = sunfish.parse(src), sunfish.parse(dst) - self._hist.append(self._hist[-1].move(sfmove)) - self._draw.draw_board(self._hist[-1].rotate(), self._player_color) + self._move = src + dst + self._board.append(Move.from_uci(self._move)) + self._draw.draw_board(self._board, self._player_color) + # TODO: check game over self._state = GameState.THINKING # handle user input on game over else: if key == Button.BTN_A or key == Button.BTN_B: + self._state = GameState.MAIN_MENU diff --git a/plugin/chess/sunfish.py b/plugin/chess/sunfish.py deleted file mode 100644 index d786862..0000000 --- a/plugin/chess/sunfish.py +++ /dev/null @@ -1,438 +0,0 @@ -#!/usr/bin/env pypy -# -*- coding: utf-8 -*- - -from __future__ import print_function -import re, sys, 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(): - padrow = lambda row: (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 MATE_UPPER - 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] - put = lambda board, i, p: 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: - 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'): - 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): - 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: - 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: - is_dead = lambda pos: 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 - score = self.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. - self.bound(pos, lower, depth) - # 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 -############################################################################### - - -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 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)) - - # 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])) - hist.append(hist[-1].move(move)) - - -if __name__ == '__main__': - main() - diff --git a/plugin/chess/util.py b/plugin/chess/util.py deleted file mode 100644 index c6bc831..0000000 --- a/plugin/chess/util.py +++ /dev/null @@ -1,41 +0,0 @@ -import re -import itertools -from . import sunfish - -def get_all_moves(pos: sunfish.Position): - all_moves = dict() - for src, dst in pos.gen_moves(): - all_moves.setdefault(sunfish.render(src), []).append(sunfish.render(dst)) - return all_moves - -def move_str(move, shift = False): - s = -119 if shift else 0 - return sunfish.render(s + move[0]) + " - " + sunfish.render(s + move[1]) - -def get_color(pos): - return "wb"[1 if pos.board.startswith("\n") else 0] - -def parseFEN(fen): - board, color, castling, enpas, _, _ = fen.split() - board = re.sub(r"\d", (lambda m: "."*int(m.group(0))), board) - board = list(21*" " + " ".join(board.split("/")) + 21*" ") - board[9::10] = ["\n"]*12 - board = "".join(board) - wc = ("Q" in castling, "K" in castling) - bc = ("k" in castling, "q" in castling) - ep = sunfish.parse(enpas) if enpas != "-" else 0 - score = sum(sunfish.pst[p][i] for i,p in enumerate(board) if p.isupper()) - score -= sum(sunfish.pst[p.upper()][119-i] for i,p in enumerate(board) if p.islower()) - pos = sunfish.Position(board, score, wc, bc, ep, 0) - return pos if color == "w" else pos.rotate() - -def renderFEN(pos, half_move_clock=0, full_move_clock=1): - color = get_color(pos) - if color == "b": - pos = pos.rotate() - board = "/".join(pos.board.split()) - board = re.sub(r"\.+", (lambda m: str(len(m.group(0)))), board) - castling = "".join(itertools.compress("KQkq", pos.wc[::-1]+pos.bc)) or "-" - ep = sunfish.render(pos.ep) if not pos.board[pos.ep].isspace() else "-" - clock = "{} {}".format(half_move_clock, full_move_clock) - return " ".join((board, color, castling, ep, clock))