-
Notifications
You must be signed in to change notification settings - Fork 0
/
chess_logic.py
368 lines (313 loc) · 13.2 KB
/
chess_logic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
from __future__ import print_function
import re
from collections import namedtuple
from itertools import count
# TODO: mobility, opp/end piece value/pst, BNR pairs, King safety
WHITE, BLACK = range(2)
FEN_INITIAL = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
opp_piece_value = {'P': 100, 'N': 305, 'B': 333, 'R': 563, 'Q': 950, 'K': 60000}
end_piece_value = {'P': 125, 'N': 280, 'B': 360, 'R': 600, 'Q': 1000, 'K': 60000}
ope_pst = {
'P': ( 0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, -5,-10, 0, 0,-10, -5, 5,
5, 10, 10,-20,-20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0),
'N': (-50,-40,-30,-30,-30,-30,-40,-50,
-40,-20, 0, 0, 0, 0,-20,-40,
-30, 0, 10, 15, 15, 10, 0,-30,
-30, 5, 15, 20, 20, 15, 5,-30,
-30, 0, 15, 20, 20, 15, 0,-30,
-30, 5, 10, 15, 15, 10, 5,-30,
-40,-20, 0, 5, 5, 0,-20,-40,
-50,-40,-30,-30,-30,-30,-40,-50),
'B': (-20,-10,-10,-10,-10,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5, 10, 10, 5, 0,-10,
-10, 5, 5, 10, 10, 5, 5,-10,
-10, 0, 10, 10, 10, 10, 0,-10,
-10, 10, 10, 10, 10, 10, 10,-10,
-10, 5, 0, 0, 0, 0, 5,-10,
-20,-10,-10,-10,-10,-10,-10,-20),
'R': ( 0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0),
'Q': (-20,-10,-10, -5, -5,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5, 5, 5, 5, 0,-10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0,-10,
-10, 0, 5, 0, 0, 0, 0,-10,
-20,-10,-10, -5, -5,-10,-10,-20),
'K': (-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-20,-30,-30,-40,-40,-30,-30,-20,
-10,-20,-20,-20,-20,-20,-20,-10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 40, 10, 0, 0, 10, 40, 20)
}
end_pst = ope_pst.copy()
end_pst['K'] = (-50,-40,-30,-20,-20,-30,-40,-50,
-30,-20,-10, 0, 0,-10,-20,-30,
-30,-10, 20, 30, 30, 20,-10,-30,
-30,-10, 30, 40, 40, 30,-10,-30,
-30,-10, 30, 40, 40, 30,-10,-30,
-30,-10, 20, 30, 30, 20,-10,-30,
-30,-30, 0, 0, 0, 0,-30,-30,
-50,-30,-30,-30,-30,-30,-30,-50)
end_pst['P'] = ( 0, 0, 0, 0, 0, 0, 0, 0,
80, 80, 80, 80, 80, 80, 80, 80,
20, 20, 30, 40, 40, 30, 20, 20,
10, 10, 15, 30, 30, 15, 10, 10,
5, 5, 10, 20, 20, 10, 5, 5,
0,-10,-15, -5, -5,-15,-10, 0,
-5, -5, -5,-20,-20, -5, -5, -5,
0, 0, 0, 0, 0, 0, 0, 0)
def pad_n_join(piece_value, pst):
for k, table in pst.items():
padrow = lambda row: (0,) + tuple(x + piece_value[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
return pst
# Pad and join both piece square tables
ope_pst = pad_n_join(opp_piece_value, ope_pst)
end_pst = pad_n_join(end_piece_value, end_pst)
# Transposition table Entry
Entry = namedtuple('Entry', 'flag value')
# Transposition table max len
TABLE_SIZE = 1e7
# 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)}
# 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 = opp_piece_value['K'] - 10 * opp_piece_value['Q']
MATE_UPPER = opp_piece_value['K'] + 10 * opp_piece_value['Q']
# Delta pruning
QS_LIMIT = 200
def get_color(pos):
''' A slightly hacky way to to get the color from a sunfish position '''
return BLACK if pos.board.startswith('\n') else WHITE
#####################################################
# The Board
#####################################################
# 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
)
class Position(namedtuple('Position', 'board score wc bc ep kp qu')):
""" 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
qu - the number of queen
"""
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, self.qu)
def nullmove(self):
''' Like rotate, but clears ep and kp '''
return Position(
self.board[::-1].swapcase(), -self.score,
self.bc, self.wc, 0, 0, self.qu)
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, qu = self.wc, self.bc, 0, 0, self.qu
score = self.score + self.value(move)
if q in 'qQ':
qu -= 1
# 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, qu).rotate()
def pst(self):
eg = {'Q': 26,
'R': 6,
'B': 3,
'N': 3}
ope = sum(eg.get(p, 0) for p in self.board.upper()) / 100
return ope
def value(self, move):
# end-game ?
pst = ope_pst if self.qu else end_pst
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
def gen_legal_moves(self):
''' pos.gen_moves(), but without those that leaves us in check.
Also the position after moving is included. '''
for move in self.gen_moves():
pos1 = self.move(move)
if not can_kill_king(pos1):
yield move, pos1
def can_kill_king(pos):
# If we just checked for opponent moves capturing the king, we would miss
# captures in case of illegal castling.
return any(pos.value(m) >= MATE_LOWER for m in pos.gen_moves())
#####################################################
# Parse and render
#####################################################
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 mrender(pos, m):
"""sunfish to uci"""
# Sunfish always assumes promotion to queen
p = 'q' if A8 <= m[1] <= H8 and pos.board[m[0]] == 'P' else ''
m = m if get_color(pos) == WHITE else (119 - m[0], 119 - m[1])
return render(m[0]) + render(m[1]) + p
def mparse(color, move):
"""uci to sunfish"""
m = (parse(move[0:2]), parse(move[2:4]))
return m if color == WHITE else (119 - m[0], 119 - m[1])
def parseFEN(fen):
""" Parses a string in Forsyth-Edwards Notation into a Position """
board, color, castling, enpas, _hclock, _fclock = 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
# if color == 'w': board[::10] = ['\n']*12
# if color == 'b': board[9::10] = ['\n']*12
board = ''.join(board)
wc = ('Q' in castling, 'K' in castling)
bc = ('k' in castling, 'q' in castling)
ep = parse(enpas) if enpas != '-' else 0
score = sum(ope_pst[p][i] for i, p in enumerate(board) if p.isupper())
score -= sum(ope_pst[p.upper()][119 - i] for i, p in enumerate(board) if p.islower())
qu = sum(1 for q in board if q in 'qQ')
pos = Position(board, score, wc, bc, ep, 0, qu)
return pos if color == 'w' else pos.rotate()
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.split()):
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 pv(searcher, pos, include_scores=True, include_loop=False):
res = []
seen_pos = set()
color = get_color(pos)
origc = color
if include_scores:
res.append(str(pos.score))
while True:
move = searcher.tt_move.get(pos)
# The tp may have illegal moves, given lower depths don't detect king killing
if move is None or can_kill_king(pos.move(move)):
break
res.append(mrender(pos, move))
pos, color = pos.move(move), 1 - color
if pos in seen_pos:
if include_loop:
res.append('loop')
break
seen_pos.add(pos)
if include_scores:
res.append(str(pos.score if color == origc else -pos.score))
return ' '.join(res)