started replacing sunfish with stockfish
This commit is contained in:
parent
ee1ca2f653
commit
0396644151
10
__main__.py
10
__main__.py
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
from traceback import print_exc
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from icecream import ic
|
from icecream import ic
|
||||||
from menu import Menu, MenuItem, MenuType
|
from menu import Menu, MenuItem, MenuType
|
||||||
|
@ -55,8 +56,8 @@ try:
|
||||||
plugin = import_module("plugin." + item.data["plugin"])
|
plugin = import_module("plugin." + item.data["plugin"])
|
||||||
ic(plugin)
|
ic(plugin)
|
||||||
plugin.execute(cinput, graphics, item.data["arg"])
|
plugin.execute(cinput, graphics, item.data["arg"])
|
||||||
except Exception as e:
|
except:
|
||||||
ic(e)
|
print_exc()
|
||||||
graphics.clear()
|
graphics.clear()
|
||||||
graphics.text("Plugin error!", 0, 0, 1)
|
graphics.text("Plugin error!", 0, 0, 1)
|
||||||
graphics.show()
|
graphics.show()
|
||||||
|
@ -64,7 +65,6 @@ try:
|
||||||
elif item.menu_type == MenuType.EXIT_CMD:
|
elif item.menu_type == MenuType.EXIT_CMD:
|
||||||
os.system(item.data["command"])
|
os.system(item.data["command"])
|
||||||
program_exit()
|
program_exit()
|
||||||
except Exception as e:
|
except:
|
||||||
ic(e)
|
print_exc()
|
||||||
program_exit()
|
program_exit()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from icecream import ic
|
from icecream import ic
|
||||||
from graphics import Graphics
|
from graphics import Graphics
|
||||||
from .sunfish import Position
|
from chess import Board, Color, WHITE, BLACK
|
||||||
|
|
||||||
class Draw:
|
class Draw:
|
||||||
BOARD_SIZE = 64
|
BOARD_SIZE = 64
|
||||||
|
@ -79,9 +79,11 @@ class Draw:
|
||||||
self._graphics.fill_rect(
|
self._graphics.fill_rect(
|
||||||
self.BOARD_SIZE, 0, self.BOARD_SIZE, self.BOARD_SIZE, 0)
|
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)
|
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
|
c = True
|
||||||
for row in range(8):
|
for row in range(8):
|
||||||
for col in range(8):
|
for col in range(8):
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from os import path
|
from os import path
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
import time
|
import shutil
|
||||||
|
from icecream import ic
|
||||||
from cinput import ControlInput, Button
|
from cinput import ControlInput, Button
|
||||||
from graphics import Graphics
|
from graphics import Graphics
|
||||||
from .draw import Draw
|
from .draw import Draw
|
||||||
from . import sunfish
|
from chess import Board, Move, WHITE, BLACK
|
||||||
from . import util
|
from stockfish import Stockfish
|
||||||
|
|
||||||
class GameState(Enum):
|
class GameState(Enum):
|
||||||
MAIN_MENU = auto()
|
MAIN_MENU = auto()
|
||||||
|
@ -19,35 +20,33 @@ class ChessGame:
|
||||||
MAX_THINKING_SECONDS = 3
|
MAX_THINKING_SECONDS = 3
|
||||||
|
|
||||||
def __init__(self, cinput: ControlInput, graphics: Graphics):
|
def __init__(self, cinput: ControlInput, graphics: Graphics):
|
||||||
|
self._sf = Stockfish(shutil.which("stockfish") or "stockfish")
|
||||||
self._cinput = cinput
|
self._cinput = cinput
|
||||||
self._draw = Draw(graphics)
|
self._draw = Draw(graphics)
|
||||||
self._state = GameState.MAIN_MENU
|
self._state = GameState.MAIN_MENU
|
||||||
self._menu_index = 0
|
self._menu_index = 0
|
||||||
self._searcher = sunfish.Searcher()
|
|
||||||
|
|
||||||
def _init_new_game(self, pos: sunfish.Position):
|
def _init_new_game(self, fen = None):
|
||||||
self._hist = [pos]
|
self._board = Board() if fen == None else Board(fen)
|
||||||
self._last_move = "Begin!"
|
self._last_move = "Begin!"
|
||||||
self._all_moves = dict()
|
self._all_moves = dict()
|
||||||
self._src_idx = 0
|
self._src_idx = 0
|
||||||
self._dst_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):
|
def _load_saved_or_init(self):
|
||||||
if path.exists(self.SAVE_FILE):
|
if path.exists(self.SAVE_FILE):
|
||||||
with open(self.SAVE_FILE, "r") as fen:
|
with open(self.SAVE_FILE, "r") as fen:
|
||||||
fenstr = fen.read()
|
fen = fen.read()
|
||||||
pos = util.parseFEN(fenstr)
|
self._init_new_game(fen)
|
||||||
self._player_color = util.get_color(pos)
|
|
||||||
self._init_new_game(pos)
|
|
||||||
else:
|
else:
|
||||||
self._player_color = "w"
|
self._init_new_game()
|
||||||
self._init_new_game(sunfish.Position(
|
|
||||||
sunfish.initial, 0, (True,True), (True,True), 0, 0))
|
|
||||||
|
|
||||||
def _save_game(self):
|
def _save_game(self):
|
||||||
with open(self.SAVE_FILE, "w") as fen:
|
with open(self.SAVE_FILE, "w") as fen:
|
||||||
fen.write(util.renderFEN(self._hist[-1]))
|
fen.write(self._board.fen())
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# either load the save game or start a new one
|
# either load the save game or start a new one
|
||||||
|
@ -55,8 +54,7 @@ class ChessGame:
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
key = None
|
key = None
|
||||||
move = None
|
src = None
|
||||||
src = ""
|
|
||||||
|
|
||||||
################
|
################
|
||||||
# HANDLE STATE #
|
# HANDLE STATE #
|
||||||
|
@ -70,25 +68,30 @@ class ChessGame:
|
||||||
# computer makes a move
|
# computer makes a move
|
||||||
elif self._state == GameState.THINKING:
|
elif self._state == GameState.THINKING:
|
||||||
self._draw.draw_thinking(self._last_move)
|
self._draw.draw_thinking(self._last_move)
|
||||||
start = time.time()
|
# TODO: check game over
|
||||||
for _, self._move, self._score in self._searcher.search(
|
self._move = self._sf.get_best_move()
|
||||||
self._hist[-1], self._hist):
|
if self._move != None:
|
||||||
if time.time() - start > self.MAX_THINKING_SECONDS:
|
self._board.push(Move.from_uci(self._move))
|
||||||
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
|
# reset user moves and move state to user's turn
|
||||||
self._all_moves = dict()
|
self._all_moves = dict()
|
||||||
|
self._src_idx = 0
|
||||||
|
self._dst_idx = 0
|
||||||
self._state = GameState.CHOOSE_SRC
|
self._state = GameState.CHOOSE_SRC
|
||||||
|
else:
|
||||||
|
self._state = GameState.GAME_OVER
|
||||||
|
self._draw.draw_board(self._board, self._player_color)
|
||||||
|
|
||||||
# user picks source piece
|
# user picks source piece
|
||||||
elif self._state == GameState.CHOOSE_SRC:
|
elif self._state == GameState.CHOOSE_SRC:
|
||||||
if len(list(self._all_moves)) == 0:
|
if len(list(self._all_moves)) == 0:
|
||||||
self._all_moves = util.get_all_moves(self._hist[-1])
|
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]
|
src = list(self._all_moves)[self._src_idx]
|
||||||
self._draw.draw_select(self._last_move, src)
|
self._draw.draw_select(self._last_move, src)
|
||||||
key = self._cinput.get_one_shot()
|
key = self._cinput.get_one_shot()
|
||||||
|
else:
|
||||||
|
self._state = GameState.GAME_OVER
|
||||||
|
|
||||||
# user picks dest square
|
# user picks dest square
|
||||||
elif self._state == GameState.CHOOSE_DST:
|
elif self._state == GameState.CHOOSE_DST:
|
||||||
|
@ -126,18 +129,13 @@ class ChessGame:
|
||||||
self._save_game()
|
self._save_game()
|
||||||
return
|
return
|
||||||
elif self._menu_index == 2 or self._menu_index == 3: # new game
|
elif self._menu_index == 2 or self._menu_index == 3: # new game
|
||||||
pos = sunfish.Position(
|
self._init_new_game()
|
||||||
sunfish.initial, 0, (True,True), (True,True), 0, 0)
|
if self._menu_index == 2:
|
||||||
if self._menu_index == 3:
|
self._player_color = WHITE
|
||||||
self._player_color = "b"
|
|
||||||
self._state = GameState.THINKING
|
|
||||||
else:
|
|
||||||
self._player_color = "w"
|
|
||||||
self._state = GameState.CHOOSE_SRC
|
self._state = GameState.CHOOSE_SRC
|
||||||
self._init_new_game(pos)
|
else:
|
||||||
# TODO: figure out why I need to do this
|
self._player_color = BLACK
|
||||||
self._draw.draw_board(
|
self._state = GameState.THINKING
|
||||||
pos if self._player_color == "w" else pos.rotate(), self._player_color)
|
|
||||||
|
|
||||||
# handle user input when selecting source piece
|
# handle user input when selecting source piece
|
||||||
# TODO: some kind of cursor indicator on board
|
# TODO: some kind of cursor indicator on board
|
||||||
|
@ -180,15 +178,15 @@ class ChessGame:
|
||||||
# make the move
|
# make the move
|
||||||
elif key == Button.BTN_B:
|
elif key == Button.BTN_B:
|
||||||
dst = self._all_moves[src][self._dst_idx]
|
dst = self._all_moves[src][self._dst_idx]
|
||||||
# TODO: fix rotation depending on player color
|
self._move = src + dst
|
||||||
self._last_move = src + " - " + dst
|
self._board.append(Move.from_uci(self._move))
|
||||||
sfmove = sunfish.parse(src), sunfish.parse(dst)
|
self._draw.draw_board(self._board, self._player_color)
|
||||||
self._hist.append(self._hist[-1].move(sfmove))
|
# TODO: check game over
|
||||||
self._draw.draw_board(self._hist[-1].rotate(), self._player_color)
|
|
||||||
self._state = GameState.THINKING
|
self._state = GameState.THINKING
|
||||||
|
|
||||||
# handle user input on game over
|
# handle user input on game over
|
||||||
else:
|
else:
|
||||||
if key == Button.BTN_A or key == Button.BTN_B:
|
if key == Button.BTN_A or key == Button.BTN_B:
|
||||||
|
|
||||||
self._state = GameState.MAIN_MENU
|
self._state = GameState.MAIN_MENU
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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))
|
|
Reference in New Issue