deep-pink
deep-pink copied to clipboard
Sunfish search error
When I run play.py, i get the following error:
Traceback (most recent call last): File "play.py", line 226, in game gn_current = player.move(gn_current) File "play.py", line 196, in move move, score = sunfish.search(self._pos, maxn=self._maxn) AttributeError: 'module' object has no attribute 'search'
What does this error mean?
sorry, not sure what's going on :(
What should I show you to help you understand my error?
When i run the command: export PYTHONPATH=/home/username/.sunfish/, it runs fine. I then run: python play.py, and i get the above error.
I tried running python in the terminal, and it seems that sunfish does not even have a search module? This was my result: Python 2.7.12 (default, Jul 1 2016, 15:12:24) [GCC 5.4.0 20160609] on linux2 Type "help", "copyright", "credits" or "license" for more information.
import sunfish sunfish.search() Traceback (most recent call last): File "
", line 1, in AttributeError: 'module' object has no attribute 'search'
sorry – i don't have time to look into this. you are on your own :)
Okay thank you anyway - any place you would recommend I start?
sorry i don't have any pointers (haven't looked at the code for like 2 years)
I think I can shed some light on this. Recent versions of sunfish no longer has a global search function, but a Searcher class of which you should make an instance. Basically this is just to encapsulate things like "nodes searcehd".
@thomasahle Unfortunately I am quite the beginner at this, so would you mind explaining to me what changes I would make to the code? I'm not very familiar with sunfish and python, and don't know how to implement the Searcher class.
Same error here.
If I'm not mistaken, @thomasahle is saying that the 'search' function has been refactored as an instance method of the Searcher class see https://github.com/thomasahle/sunfish/blob/master/sunfish.py line 353
to fix this, you will need to replace the line 196, which reads sunfish.search()
with something to the effect of
searcher = sunfish.Searcher()
# at the top of the play.py file
searcher.search(...)
Let me know if that works
great if you can send a pull request if you get it working! thanks
Thank you @mitni455! Your searcher class implementation worked! However, I'm getting a new error.
Traceback (most recent call last): File "play.py", line 227, in game gn_current = player.move(gn_current) File "play.py", line 197, in move move, score = searcher.search(self._pos, maxn=self._maxn) TypeError: search() got an unexpected keyword argument 'maxn'
Does this have anything to do with lines 350-352 in sunfish?
# secs over maxn is a breaking change. Can we do this? # I guess I could send a pull request to deep pink # Why include secs at all?
Here is how it's used in main() of sunfish
# Fire up the engine to look for a move.
move, score = searcher.search(pos, secs=2)
Thank you for the explanation. Does this mean that sunfish has migrated completely away from maxn? In other words, does this mean in order to make deep pink work, I need to modify it to use secs instead of maxn?
If I can get all this to work, I'll happily send a pull request @erikbern
Yes. Though if it's easier you can also use the _search method, which returns an iterator that can be stopped when enough nodes are searched. See the implementation of the search method.
Did anyone end up getting this to work?
after the
`searcher = sunfish.Searcher() # at the top of the play.py file
searcher.search(...)`
line 182
def __init__(self, secs=2):
line 184
self._secs = secs
line 197
move, score = searcher.search(self._pos, secs=self._secs)
after line 214 add
secs = 1
(or secs = n )
line 221
player_b = Sunfish(secs=secs)
here is working now
or..if you want you can use this verision of sunfish wich have maxn instead of secs
#!/usr/bin/env pypy
# -*- coding: utf-8 -*-
from __future__ import print_function
import re
import sys
from itertools import count
from collections import OrderedDict, namedtuple
# The table size is the maximum number of elements in the transposition table.
TABLE_SIZE = 1e6
# This constant controls how much time we spend on looking for optimal moves.
NODES_SEARCHED = 1e4
# 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.
MATE_VALUE = 30000
# 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
' ' # 110 -119
)
###############################################################################
# Move and evaluation tables
###############################################################################
N, E, S, W = -10, 1, 10, -1
directions = {
'P': (N, 2*N, N+W, N+E),
'N': (2*N+E, N+2*E, S+2*E, 2*S+E, 2*S+W, S+2*W, N+2*W, 2*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)
}
pst = {
'P': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 198, 198, 198, 198, 198, 198, 198, 198, 0,
0, 178, 198, 198, 198, 198, 198, 198, 178, 0,
0, 178, 198, 198, 198, 198, 198, 198, 178, 0,
0, 178, 198, 208, 218, 218, 208, 198, 178, 0,
0, 178, 198, 218, 238, 238, 218, 198, 178, 0,
0, 178, 198, 208, 218, 218, 208, 198, 178, 0,
0, 178, 198, 198, 198, 198, 198, 198, 178, 0,
0, 198, 198, 198, 198, 198, 198, 198, 198, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
'B': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 797, 824, 817, 808, 808, 817, 824, 797, 0,
0, 814, 841, 834, 825, 825, 834, 841, 814, 0,
0, 818, 845, 838, 829, 829, 838, 845, 818, 0,
0, 824, 851, 844, 835, 835, 844, 851, 824, 0,
0, 827, 854, 847, 838, 838, 847, 854, 827, 0,
0, 826, 853, 846, 837, 837, 846, 853, 826, 0,
0, 817, 844, 837, 828, 828, 837, 844, 817, 0,
0, 792, 819, 812, 803, 803, 812, 819, 792, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
'N': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 627, 762, 786, 798, 798, 786, 762, 627, 0,
0, 763, 798, 822, 834, 834, 822, 798, 763, 0,
0, 817, 852, 876, 888, 888, 876, 852, 817, 0,
0, 797, 832, 856, 868, 868, 856, 832, 797, 0,
0, 799, 834, 858, 870, 870, 858, 834, 799, 0,
0, 758, 793, 817, 829, 829, 817, 793, 758, 0,
0, 739, 774, 798, 810, 810, 798, 774, 739, 0,
0, 683, 718, 742, 754, 754, 742, 718, 683, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
'R': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
'Q': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
'K': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 60098, 60132, 60073, 60025, 60025, 60073, 60132, 60098, 0,
0, 60119, 60153, 60094, 60046, 60046, 60094, 60153, 60119, 0,
0, 60146, 60180, 60121, 60073, 60073, 60121, 60180, 60146, 0,
0, 60173, 60207, 60148, 60100, 60100, 60148, 60207, 60173, 0,
0, 60196, 60230, 60171, 60123, 60123, 60171, 60230, 60196, 0,
0, 60224, 60258, 60199, 60151, 60151, 60199, 60258, 60224, 0,
0, 60287, 60321, 60262, 60214, 60214, 60262, 60321, 60287, 0,
0, 60298, 60332, 60273, 60225, 60225, 60273, 60332, 60298, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
}
###############################################################################
# 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
bc -- the opponent castling rights
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
if self.board[j].isspace(): break
# Castling
if i == A1 and q == 'K' and self.wc[0]: yield (j, j-2)
if i == H1 and q == 'K' and self.wc[1]: yield (j, j+2)
# No friendly captures
if q.isupper(): break
# Special pawn stuff
if p == 'P' and d in (N+W, N+E) and q == '.' and j not in (self.ep, self.kp): break
if p == 'P' and d in (N, 2*N) and q != '.': break
if p == 'P' and d == 2*N and (i < A1+N or self.board[i+N] != '.'): break
# Move it
yield (i, j)
# Stop crawlers from sliding
if p in ('P', 'N', 'K'): break
# No sliding after captures
if q.islower(): break
def rotate(self):
return Position(
self.board[::-1].swapcase(), -self.score,
self.bc, self.wc, 119-self.ep, 119-self.kp)
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
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')
# Special pawn stuff
if p == 'P':
if A8 <= j <= H8:
board = put(board, j, 'Q')
if j - i == 2*N:
ep = i + N
if j - i in (N+W, N+E) and q == '.':
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()][j]
# Castling check detection
if abs(j-self.kp) < 2:
score += pst['K'][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'][j+S]
return score
Entry = namedtuple('Entry', 'depth score gamma move')
tp = OrderedDict()
###############################################################################
# Search logic
###############################################################################
nodes = 0
def bound(pos, gamma, depth):
""" returns s(pos) <= r < gamma if s(pos) < gamma
returns s(pos) >= r >= gamma if s(pos) >= gamma """
global nodes; nodes += 1
# Look in the table if we have already searched this position before.
# We use the table value if it was done with at least as deep a search
# as ours, and the gamma value is compatible.
entry = tp.get(pos)
if entry is not None and entry.depth >= depth and (
entry.score < entry.gamma and entry.score < gamma or
entry.score >= entry.gamma and entry.score >= gamma):
return entry.score
# Stop searching if we have won/lost.
if abs(pos.score) >= MATE_VALUE:
return pos.score
# Null move. Is also used for stalemate checking
nullscore = -bound(pos.rotate(), 1-gamma, depth-3) if depth > 0 else pos.score
#nullscore = -MATE_VALUE*3 if depth > 0 else pos.score
if nullscore >= gamma:
return nullscore
# We generate all possible, pseudo legal moves and order them to provoke
# cuts. At the next level of the tree we are going to minimize the score.
# This can be shown equal to maximizing the negative score, with a slightly
# adjusted gamma value.
best, bmove = -3*MATE_VALUE, None
for move in sorted(pos.gen_moves(), key=pos.value, reverse=True):
# We check captures with the value function, as it also contains ep and kp
if depth <= 0 and pos.value(move) < 150:
break
score = -bound(pos.move(move), 1-gamma, depth-1)
if score > best:
best = score
bmove = move
if score >= gamma:
break
# If there are no captures, or just not any good ones, stand pat
if depth <= 0 and best < nullscore:
return nullscore
# Check for stalemate. If best move loses king, but not doing anything
# would save us. Not at all a perfect check.
if depth > 0 and best <= -MATE_VALUE and nullscore > -MATE_VALUE:
best = 0
# We save the found move together with the score, so we can retrieve it in
# the play loop. We also trim the transposition table in FILO order.
# We prefer fail-high moves, as they are the ones we can build our pv from.
if entry is None or depth >= entry.depth and best >= gamma:
tp[pos] = Entry(depth, best, gamma, bmove)
if len(tp) > TABLE_SIZE:
tp.popitem()
return best
def search(pos, maxn=NODES_SEARCHED):
""" Iterative deepening MTD-bi search """
global nodes; nodes = 0
# We limit the depth to some constant, so we don't get a stack overflow in
# the end game.
for depth in range(1, 99):
# The inner loop is a binary search on the score of the position.
# Inv: lower <= score <= upper
# However this may be broken by values from the transposition table,
# as they don't have the same concept of p(score). Hence we just use
# 'lower < upper - margin' as the loop condition.
lower, upper = -3*MATE_VALUE, 3*MATE_VALUE
while lower < upper - 3:
gamma = (lower+upper+1)//2
score = bound(pos, gamma, depth)
if score >= gamma:
lower = score
if score < gamma:
upper = score
# We stop deepening if the global N counter shows we have spent too
# long, or if we have already won the game.
if nodes >= maxn or abs(score) >= MATE_VALUE:
break
# If the game hasn't finished we can retrieve our move from the
# transposition table.
entry = tp.get(pos)
if entry is not None:
return entry.move, score
return None, score
###############################################################################
# User interface
###############################################################################
# Python 2 compatability
if sys.version_info[0] == 2:
input = raw_input
def parse(c):
fil, rank = ord(c[0]) - ord('a'), int(c[1]) - 1
return A1 + fil - 10*rank
def render(i):
rank, fil = divmod(i - A1, 10)
return chr(fil + ord('a')) + str(-rank + 1)
def print_pos(pos):
print()
uni_pieces = {'R':'♜', 'N':'♞', 'B':'♝', 'Q':'♛', 'K':'♚', 'P':'♟',
'r':'♖', 'n':'♘', 'b':'♗', 'q':'♕', 'k':'♔', 'p':'♙', '.':'·'}
for i, row in enumerate(pos.board.strip().split('\n ')):
print(' ', 8-i, ' '.join(uni_pieces.get(p, p) for p in row))
print(' a b c d e f g h \n\n')
def main():
pos = Position(initial, 0, (True,True), (True,True), 0, 0)
while True:
print_pos(pos)
# We query the user until she enters a legal move.
move = None
while move not in pos.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")
pos = pos.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(pos.rotate())
# Fire up the engine to look for a move.
move, score = search(pos)
if score <= -MATE_VALUE:
print("You won")
break
if score >= MATE_VALUE:
print("You lost")
break
# 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]))
pos = pos.move(move)
if __name__ == '__main__':
main()
I made a pull-request with basically FF27's changes. Everything seems to work now.