deep-pink icon indicating copy to clipboard operation
deep-pink copied to clipboard

Sunfish search error

Open Your3xcelency opened this issue 7 years ago • 19 comments

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?

Your3xcelency avatar Sep 29 '16 14:09 Your3xcelency

sorry, not sure what's going on :(

erikbern avatar Sep 29 '16 15:09 erikbern

What should I show you to help you understand my error?

Your3xcelency avatar Sep 29 '16 17:09 Your3xcelency

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'

Your3xcelency avatar Sep 29 '16 17:09 Your3xcelency

sorry – i don't have time to look into this. you are on your own :)

erikbern avatar Sep 29 '16 18:09 erikbern

Okay thank you anyway - any place you would recommend I start?

Your3xcelency avatar Sep 29 '16 18:09 Your3xcelency

sorry i don't have any pointers (haven't looked at the code for like 2 years)

erikbern avatar Sep 29 '16 19:09 erikbern

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 avatar Oct 01 '16 02:10 thomasahle

@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.

Your3xcelency avatar Oct 02 '16 20:10 Your3xcelency

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

mightyhorst avatar Oct 13 '16 06:10 mightyhorst

great if you can send a pull request if you get it working! thanks

erikbern avatar Oct 13 '16 10:10 erikbern

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?

Your3xcelency avatar Oct 13 '16 13:10 Your3xcelency

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)

thomasahle avatar Oct 13 '16 15:10 thomasahle

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?

Your3xcelency avatar Oct 13 '16 16:10 Your3xcelency

If I can get all this to work, I'll happily send a pull request @erikbern

Your3xcelency avatar Oct 13 '16 16:10 Your3xcelency

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.

thomasahle avatar Oct 13 '16 19:10 thomasahle

Did anyone end up getting this to work?

bbarney7 avatar Feb 08 '17 15:02 bbarney7

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

FF27 avatar Feb 18 '17 15:02 FF27

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()

FF27 avatar Feb 19 '17 23:02 FF27

I made a pull-request with basically FF27's changes. Everything seems to work now.

thomasahle avatar Feb 20 '17 01:02 thomasahle