-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathhomemade.py
More file actions
231 lines (191 loc) · 9.21 KB
/
homemade.py
File metadata and controls
231 lines (191 loc) · 9.21 KB
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
"""
Some example classes for people who want to create a homemade bot.
With these classes, bot makers will not have to implement the UCI or XBoard interfaces themselves.
"""
import chess
from chess.engine import PlayResult, Limit
import random
from lib.engine_wrapper import MinimalEngine
from lib.lichess_types import MOVE, HOMEMADE_ARGS_TYPE
import logging
# Use this logger variable to print messages to the console or log files.
# logger.info("message") will always print "message" to the console or log file.
# logger.debug("message") will only print "message" if verbose logging is enabled.
logger = logging.getLogger(__name__)
class ExampleEngine(MinimalEngine):
"""An example engine that all homemade engines inherit."""
# Bot names and ideas from tom7's excellent eloWorld video
class RandomMove(ExampleEngine):
"""Get a random move."""
def search(self, board: chess.Board, *args: HOMEMADE_ARGS_TYPE) -> PlayResult: # noqa: ARG002
"""Choose a random move."""
return PlayResult(random.choice(list(board.legal_moves)), None)
class Alphabetical(ExampleEngine):
"""Get the first move when sorted by san representation."""
def search(self, board: chess.Board, *args: HOMEMADE_ARGS_TYPE) -> PlayResult: # noqa: ARG002
"""Choose the first move alphabetically."""
moves = list(board.legal_moves)
moves.sort(key=board.san)
return PlayResult(moves[0], None)
class FirstMove(ExampleEngine):
"""Get the first move when sorted by uci representation."""
def search(self, board: chess.Board, *args: HOMEMADE_ARGS_TYPE) -> PlayResult: # noqa: ARG002
"""Choose the first move alphabetically in uci representation."""
moves = list(board.legal_moves)
moves.sort(key=str)
return PlayResult(moves[0], None)
class ComboEngine(ExampleEngine):
"""
Get a move using multiple different methods.
This engine demonstrates how one can use `time_limit`, `draw_offered`, and `root_moves`.
"""
def search(self,
board: chess.Board,
time_limit: Limit,
ponder: bool, # noqa: ARG002
draw_offered: bool,
root_moves: MOVE) -> PlayResult:
"""
Choose a move using multiple different methods.
:param board: The current position.
:param time_limit: Conditions for how long the engine can search (e.g. we have 10 seconds and search up to depth 10).
:param ponder: Whether the engine can ponder after playing a move.
:param draw_offered: Whether the bot was offered a draw.
:param root_moves: If it is a list, the engine should only play a move that is in `root_moves`.
:return: The move to play.
"""
if isinstance(time_limit.time, int):
my_time = time_limit.time
my_inc = 0
elif board.turn == chess.WHITE:
my_time = time_limit.white_clock if isinstance(time_limit.white_clock, int) else 0
my_inc = time_limit.white_inc if isinstance(time_limit.white_inc, int) else 0
else:
my_time = time_limit.black_clock if isinstance(time_limit.black_clock, int) else 0
my_inc = time_limit.black_inc if isinstance(time_limit.black_inc, int) else 0
possible_moves = root_moves if isinstance(root_moves, list) else list(board.legal_moves)
if my_time / 60 + my_inc > 10:
# Choose a random move.
move = random.choice(possible_moves)
else:
# Choose the first move alphabetically in uci representation.
possible_moves.sort(key=str)
move = possible_moves[0]
return PlayResult(move, None, draw_offered=draw_offered)
class MyBot(ExampleEngine):
"""Template code for hackathon participants to modify.
This is intentionally a very small, simple, and weak example engine
meant for learning and quick prototyping only.
Key limitations:
- Fixed-depth search with only a very naive time-to-depth mapping (no true time management).
- Plain minimax: no alpha-beta pruning, so the search is much slower than it
could be for the same depth.
- No iterative deepening: the engine does not progressively deepen and use PV-based ordering.
- No move ordering or capture heuristics: moves are searched in arbitrary order.
- No transposition table or caching: repeated positions are re-searched.
- Evaluation is material-only and very simplistic; positional factors are ignored.
Use this as a starting point: replace minimax with alpha-beta, add
iterative deepening, quiescence search, move ordering (MVV/LVA, history),
transposition table, and a richer evaluator to make it competitive.
"""
def search(self, board: chess.Board, *args: HOMEMADE_ARGS_TYPE) -> PlayResult:
# NOTE: The sections below are intentionally simple to keep the example short.
# They demonstrate the structure of a search but also highlight the engine's
# weaknesses (fixed depth, naive time handling, no pruning, no quiescence, etc.).
# --- very simple time-based depth selection (naive) ---
# Expect args to be (time_limit: Limit, ponder: bool, draw_offered: bool, root_moves: MOVE)
time_limit = args[0] if (args and isinstance(args[0], Limit)) else None
my_time = my_inc = None
if time_limit is not None:
if isinstance(time_limit.time, (int, float)):
my_time = time_limit.time
my_inc = 0
elif board.turn == chess.WHITE:
my_time = time_limit.white_clock if isinstance(time_limit.white_clock, (int, float)) else 0
my_inc = time_limit.white_inc if isinstance(time_limit.white_inc, (int, float)) else 0
else:
my_time = time_limit.black_clock if isinstance(time_limit.black_clock, (int, float)) else 0
my_inc = time_limit.black_inc if isinstance(time_limit.black_inc, (int, float)) else 0
# Map a rough time budget to a coarse fixed depth.
# Examples:
# - >= 60s: depth 4
# - >= 20s: depth 3
# - >= 5s: depth 2
# - else: depth 1
remaining = my_time if isinstance(my_time, (int, float)) else None
inc = my_inc if isinstance(my_inc, (int, float)) else 0
budget = (remaining or 0) + 2 * inc # crude increment bonus
if remaining is None:
total_depth = 4
elif budget >= 60:
total_depth = 4
elif budget >= 20:
total_depth = 3
elif budget >= 5:
total_depth = 2
else:
total_depth = 1
total_depth = max(1, int(total_depth))
# --- simple material evaluator (White-positive score) ---
def evaluate(b: chess.Board) -> int:
# Large score for terminal outcomes
if b.is_game_over():
outcome = b.outcome()
if outcome is None or outcome.winner is None:
return 0 # draw
return 10_000_000 if outcome.winner is chess.WHITE else -10_000_000
values = {
chess.PAWN: 100,
chess.KNIGHT: 320,
chess.BISHOP: 330,
chess.ROOK: 500,
chess.QUEEN: 900,
chess.KING: 0, # king material ignored (checkmates handled above)
}
score = 0
for pt, v in values.items():
score += v * (len(b.pieces(pt, chess.WHITE)) - len(b.pieces(pt, chess.BLACK)))
return score
# --- plain minimax (no alpha-beta) ---
def minimax(b: chess.Board, depth: int, maximizing: bool) -> int:
if depth == 0 or b.is_game_over():
return evaluate(b)
if maximizing:
best = -10**12
for m in b.legal_moves:
b.push(m)
val = minimax(b, depth - 1, False)
b.pop()
if val > best:
best = val
return best
else:
best = 10**12
for m in b.legal_moves:
b.push(m)
val = minimax(b, depth - 1, True)
b.pop()
if val < best:
best = val
return best
# --- root move selection ---
legal = list(board.legal_moves)
if not legal:
# Should not happen during normal play; fall back defensively
return PlayResult(random.choice(list(board.legal_moves)), None)
maximizing = board.turn == chess.WHITE
best_move = None
best_eval = -10**12 if maximizing else 10**12
# Lookahead depth chosen by the simple time heuristic; subtract one for the root move
for m in legal:
board.push(m)
val = minimax(board, total_depth - 1, not maximizing)
board.pop()
if maximizing and val > best_eval:
best_eval, best_move = val, m
elif not maximizing and val < best_eval:
best_eval, best_move = val, m
# Fallback in rare cases (shouldn't trigger)
if best_move is None:
best_move = legal[0]
return PlayResult(best_move, None)