Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sunfish search error #10

Open
Your3xcelency opened this issue Sep 29, 2016 · 19 comments
Open

Sunfish search error #10

Your3xcelency opened this issue Sep 29, 2016 · 19 comments

Comments

@Your3xcelency
Copy link

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?

@erikbern
Copy link
Owner

sorry, not sure what's going on :(

@Your3xcelency
Copy link
Author

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

@Your3xcelency
Copy link
Author

Your3xcelency commented Sep 29, 2016

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'

@erikbern
Copy link
Owner

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

@Your3xcelency
Copy link
Author

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

@erikbern
Copy link
Owner

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

@thomasahle
Copy link
Contributor

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

@Your3xcelency
Copy link
Author

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

@mightyhorst
Copy link

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

@erikbern
Copy link
Owner

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

@Your3xcelency
Copy link
Author

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?

@thomasahle
Copy link
Contributor

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)

@Your3xcelency
Copy link
Author

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
Copy link
Author

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

@thomasahle
Copy link
Contributor

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.

@bbarney7
Copy link

bbarney7 commented Feb 8, 2017

Did anyone end up getting this to work?

@FF27
Copy link

FF27 commented Feb 18, 2017

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
Copy link

FF27 commented Feb 19, 2017

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

thomasahle added a commit to thomasahle/deep-pink that referenced this issue Feb 20, 2017
@thomasahle
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants