-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathengine.py
148 lines (125 loc) · 4.86 KB
/
engine.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
from time import perf_counter
from math import trunc
import sys
import chess
import clock
INFINITY = 32768
START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
MATE_LIMIT = 1000
def mate_distancing(num):
if num >= INFINITY - MATE_LIMIT:
return num - 1
if num <= -(INFINITY - MATE_LIMIT):
return num + 1
return num
class Minsikfish:
value = {"P": 100, "N": 300, "B": 300, "R": 500, "Q": 900, "K": INFINITY}
def __init__(self, fen=START_FEN):
self.board = chess.Board(fen)
self.nodes = 0
self.start_millis = 0
self.depth = 0
def push(self, move: chess.Move):
self.board.push(move)
def is_stm_white(self):
return self.board.turn == chess.WHITE
def hit_blunt(self):
# actually evaluation function
score = 0
for _, piece in self.board.piece_map().items():
if piece is None:
continue
elif piece.color is self.board.turn:
score += self.value[piece.symbol().upper()]
else:
score -= self.value[piece.symbol().upper()]
return score
def should_runsik(self):
# called in IDDFS root
if clock.state == clock.State.IDLE:
return True
match clock.lim.mode:
case clock.TimingMode.DEPTH:
return self.depth > clock.lim.depth
case clock.TimingMode.NODES:
return self.nodes > clock.lim.nodes
case mode if mode in [clock.TimingMode.MOVETIME, clock.TimingMode.TC]:
# branching factor: dictates whether next ply's search can fully pass
# if BF = 3, 3 / 1/(1-1/3) = x2 of used time is needed again
bf = 20
cur_millis = perf_counter() * 1000
return (cur_millis - self.start_millis) * (bf - 1) > clock.lim.movetime
case _:
return False
def should_runsik_nodes(self):
# 'run every few thousand nodes or so': called at depth 3 left
# (affected by general nodes per depth)
if clock.state == clock.State.IDLE:
return True
match clock.lim.mode:
case clock.TimingMode.DEPTH:
return self.depth > clock.lim.depth
case clock.TimingMode.NODES:
return self.nodes > clock.lim.nodes
case mode if mode in [clock.TimingMode.MOVETIME, clock.TimingMode.TC]:
# quit only if movetime is passed (different from root fx)
cur_millis = perf_counter() * 1000
return (cur_millis - self.start_millis) > clock.lim.movetime
case _:
return False
def awake(self):
# IDDFS function
self.nodes = 0
self.start_millis = perf_counter() * 1000
self.depth = 1
while True:
(pv, score) = self.struggle(depth=self.depth)
if pv is None:
break
end_millis = perf_counter() * 1000
millis_time = trunc(end_millis - self.start_millis)
nps = trunc(self.nodes * 1000 / (end_millis - self.start_millis))
pv_uci = list(map(lambda move: move.uci(), pv))
print(
f"info depth {self.depth} score cp {score} "
f"time {millis_time} nodes {self.nodes} nps {nps} "
f"pv {' '.join(pv_uci)}"
)
sys.stdout.flush()
self.depth += 1
if self.should_runsik():
break
return pv_uci[0]
def struggle(
self, alpha=-INFINITY, beta=INFINITY, depth=1
) -> tuple[list[chess.Move], int]:
# actually search function
if depth == 3 and self.should_runsik_nodes():
return (None, None)
self.nodes += 1
if self.board.is_checkmate():
return ([], -INFINITY)
if self.board.is_stalemate():
return ([], 0)
if depth == 0:
return ([], self.hit_blunt())
moves = self.board.generate_legal_moves()
pv: list[chess.Move] = []
best_score = -INFINITY
for move in moves:
self.board.push(move)
(following, score) = self.struggle(-beta, -alpha, depth - 1)
if score is None:
return (None, None)
score *= -1
score = mate_distancing(score)
self.board.pop()
# fail-soft alpha-beta
if score > best_score:
best_score = score
if score > alpha:
pv = [move, *following]
alpha = score
if score >= beta:
return ([], best_score) # can also exceed beta
return (pv, best_score)