HaileyStorm commited on
Commit
161498a
1 Parent(s): 4bdef83

Upload chess-gpt-eval/mainvs.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. chess-gpt-eval/mainvs.py +708 -0
chess-gpt-eval/mainvs.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+ import chess
3
+ import chess.engine
4
+ import os
5
+ import csv
6
+ import random
7
+ import time
8
+ import platform
9
+
10
+ # NOTE: LLAMA AND NANOGPT ARE EXPERIMENTAL PLAYERS, if not using them, comment them out
11
+ # from llama_module import BaseLlamaPlayer, LocalLlamaPlayer, LocalLoraLlamaPlayer
12
+ from nanogpt.nanogpt_module import NanoGptPlayer
13
+ from mamba_module import MambaPlayer
14
+ import gpt_query
15
+ from lczero.backends import Weights, Backend, GameState
16
+ import numpy as np
17
+
18
+ from typing import Optional, Tuple
19
+ from dataclasses import dataclass
20
+
21
+
22
+ @dataclass
23
+ class LegalMoveResponse:
24
+ move_san: Optional[str] = None
25
+ move_uci: Optional[chess.Move] = None
26
+ attempts: int = 0
27
+ is_resignation: bool = False
28
+ is_illegal_move: bool = False
29
+
30
+
31
+ # Define base Player class
32
+ class Player:
33
+ def get_move(self, board: chess.Board, game_state: str, temperature: float) -> str:
34
+ raise NotImplementedError
35
+
36
+ def get_config(self) -> dict:
37
+ raise NotImplementedError
38
+
39
+
40
+ class GPTPlayer(Player):
41
+ def __init__(self, model: str):
42
+ with open("gpt_inputs/api_key.txt", "r") as f:
43
+ openai.api_key = f.read().strip()
44
+ self.model = model
45
+
46
+ def get_move(
47
+ self, board: chess.Board, game_state: str, temperature: float
48
+ ) -> Optional[str]:
49
+ response = get_gpt_response(game_state, self.model, temperature)
50
+ return get_move_from_gpt_response(response)
51
+
52
+ def get_config(self) -> dict:
53
+ return {"model": self.model}
54
+
55
+
56
+ class LC0PLayer(Player):
57
+ # "11258-32x4-se.pb.gz" = stockfish level 0- = skill 0
58
+ # "11258-48x5-se.pb.gz" = stockfish level 0+ = skill 1
59
+ # "11258-80x7-se.pb.gz" = stockfish level 1 = skill 2
60
+ # "11258-104x9-se.pb.gz" = stockfish level 2 = skill 3
61
+ # "TK-6430 aka 128x10-BPR-64M-6430000.pb.gz" = stockfish level 3 = skill 4
62
+ # "00af53b081e80147172e6f281c01daf5ca19ada173321438914c730370aa4267" = stockfish level 4 = skill 5
63
+ # "b2ec465d0fb5b5eb39d2e1e3f74041a5d2fc92d413b71aa7ea0b6fb082ccba9c" = stockfish level 5+ = skill 6
64
+ def __init__(self, skill):
65
+ self.skill = skill
66
+ network_paths = ["./lc0/build/release/11258-32x4-se.pb.gz", "./lc0/build/release/11258-48x5-se.pb.gz", "./lc0/build/release/11258-80x7-se.pb.gz", "./lc0/build/release/11258-104x9-se.pb.gz", "./lc0/build/release/TK-6430 aka 128x10-BPR-64M-6430000.pb.gz", "./lc0/build/release/00af53b081e80147172e6f281c01daf5ca19ada173321438914c730370aa4267", "./lc0/build/release/b2ec465d0fb5b5eb39d2e1e3f74041a5d2fc92d413b71aa7ea0b6fb082ccba9c"]
67
+ print(f"\n\nLoading lc0 network: {network_paths[skill]}\n\n")
68
+ self.weights = Weights(network_paths[skill])
69
+ self.backend = Backend(weights=self.weights)
70
+ self.gamestate = GameState()
71
+
72
+ def get_move(self, board: chess.Board, game_state: str, temperature: float):
73
+ self.gamestate = GameState(fen=board.fen())
74
+ input_planes = self.gamestate.as_input(self.backend)
75
+ result = self.backend.evaluate(input_planes)[0]
76
+ moves = self.gamestate.moves()
77
+ policy_indices = self.gamestate.policy_indices()
78
+ move_probs = np.array(result.p_softmax(*policy_indices))
79
+ best_move_idx = move_probs.argmax()
80
+ best_move = moves[best_move_idx]
81
+ return board.san(chess.Move.from_uci(best_move))
82
+
83
+ def get_config(self) -> dict:
84
+ return {"network": self.weights, "skill_level": self.skill, "play_time": 0}
85
+
86
+
87
+ class StockfishPlayer(Player):
88
+
89
+ @staticmethod
90
+ def get_stockfish_path() -> str:
91
+ """
92
+ Determines the operating system and returns the appropriate path for Stockfish.
93
+
94
+ Returns:
95
+ str: Path to the Stockfish executable based on the operating system.
96
+ """
97
+ if platform.system() == 'Linux':
98
+ return "/usr/games/stockfish"
99
+ elif platform.system() == 'Darwin': # Darwin is the system name for macOS
100
+ return "stockfish"
101
+ elif platform.system() == 'Windows':
102
+ return r"C:\Users\Haile\Downloads\stockfish\stockfish-windows-x86-64-avx2.exe"
103
+ else:
104
+ raise OSError("Unsupported operating system")
105
+
106
+ def __init__(self, skill_level: int, play_time: float):
107
+ self._skill_level = skill_level
108
+ self._play_time = play_time
109
+ # If getting started, you need to run brew install stockfish
110
+ stockfish_path = StockfishPlayer.get_stockfish_path()
111
+ self._engine = chess.engine.SimpleEngine.popen_uci(stockfish_path)
112
+
113
+ def get_move(
114
+ self, board: chess.Board, game_state: str, temperature: float
115
+ ) -> Optional[str]:
116
+ if self._skill_level == -2:
117
+ legal_moves = list(board.legal_moves)
118
+ random_move = random.choice(legal_moves)
119
+ return board.san(random_move)
120
+ elif self._skill_level < 0:
121
+ self._engine.configure({"Skill Level": 0})
122
+ result = self._engine.play(
123
+ board, chess.engine.Limit(time=1e-8, depth=1, nodes=1)
124
+ )
125
+
126
+ else:
127
+ self._engine.configure({"Skill Level": self._skill_level})
128
+ result = self._engine.play(board, chess.engine.Limit(time=self._play_time))
129
+ if result.move is None:
130
+ return None
131
+ return board.san(result.move)
132
+
133
+ def get_config(self) -> dict:
134
+ return {"skill_level": self._skill_level, "play_time": self._play_time}
135
+
136
+ def close(self):
137
+ self._engine.quit()
138
+
139
+
140
+ class HumanPlayer(Player):
141
+ def get_move(self, board: chess.Board, game_state: str, temperature: float) -> str:
142
+ # Print board for human player
143
+ print(board)
144
+ while True:
145
+ move = input("Enter your move (SAN format): ")
146
+ try:
147
+ move_uci = board.parse_san(move)
148
+ if move_uci in board.legal_moves:
149
+ return move
150
+ except:
151
+ print("Illegal move, try again.")
152
+
153
+ def get_config(self) -> dict:
154
+ return {"player": "human"}
155
+
156
+
157
+ def get_gpt_response(game_state: str, model: str, temperature: float) -> Optional[str]:
158
+ # trying to prevent what I believe to be rate limit issues
159
+ if model == "gpt-4":
160
+ time.sleep(0.4)
161
+ response = gpt_query.get_gpt_response(game_state, model, temperature)
162
+ return response
163
+
164
+
165
+ def get_move_from_gpt_response(response: Optional[str]) -> Optional[str]:
166
+ if response is None:
167
+ return None
168
+
169
+ # Parse the response to get only the first move
170
+ moves = response.split()
171
+ first_move = moves[0] if moves else None
172
+
173
+ return first_move
174
+
175
+
176
+ def calculate_stats(csv_file_path):
177
+ data = []
178
+ with open(csv_file_path, "r") as csv_file:
179
+ reader = csv.DictReader(csv_file)
180
+ data = list(reader)
181
+
182
+ if not data:
183
+ return None
184
+
185
+ for i, v in enumerate(data):
186
+ data[i]["player_one_score"] = 0.5 if data[i]["player_one_score"] == "1/2" else float(data[i]["player_one_score"])
187
+ data[i]["player_two_score"] = 0.5 if data[i]["player_two_score"] == "1/2" else float(data[i]["player_two_score"])
188
+
189
+ stats = {
190
+ "wins": sum(row["player_one_score"] for row in data if row["player_one_score"] > 0.6),
191
+ "draws": len(data) - sum(row["player_two_score"] for row in data if row["player_two_score"] > 0.6) - sum(row["player_one_score"] for row in data if row["player_one_score"] > 0.6),
192
+ "illegal_attempts_ratio": sum(float(row["p1_illegal_attempts"]) for row in data) / (sum(float(row["p1_illegal_attempts"]) for row in data) + sum(float(row["player_one_legal_moves"]) for row in data)),
193
+ "illegal_moves_ratio": sum(float(row["player_one_illegal_moves"]) for row in data) / sum(float(row["player_one_illegal_moves"]) + float(row["player_one_legal_moves"]) for row in data),
194
+ "avg_attempts_per_illegal": sum(float(row["p1_avg_attempts_per_illegal"]) for row in data) / len(data),
195
+ "avg_first_illegal_move": sum(float(row["p1_first_illegal_move_num"]) for row in data if float(row["p1_first_illegal_move_num"]) > 0) / len([row for row in data if float(row["p1_first_illegal_move_num"]) > 0]),
196
+ "avg_illegal_move_num": sum(float(row["p1_avg_illegal_move_num"]) for row in data if float(row["p1_avg_illegal_move_num"]) > 0) / len([row for row in data if float(row["p1_avg_illegal_move_num"]) > 0]),
197
+ "lost_to_illegal_ratio": len([row for row in data if row["player_one_failed_to_find_legal_move"] == "True"]) / len([row for row in data if float(row["number_of_moves"]) > 0]),
198
+ "avg_game_length": sum(float(row["number_of_moves"]) for row in data) / len(data),
199
+ "max_game_length": max(float(row["number_of_moves"]) for row in data),
200
+ }
201
+
202
+ return stats
203
+
204
+
205
+ def record_results(
206
+ board: chess.Board,
207
+ player_one: Player,
208
+ player_two: Player,
209
+ game_state: str,
210
+ player_one_illegal_moves: int,
211
+ player_one_illegal_attempts: int,
212
+ player_two_illegal_moves: int,
213
+ player_two_illegal_attempts: int,
214
+ player_one_legal_moves: int,
215
+ player_two_legal_moves: int,
216
+ total_time: float,
217
+ player_one_resignation: bool,
218
+ player_two_resignation: bool,
219
+ player_one_failed_to_find_legal_move: bool,
220
+ player_two_failed_to_find_legal_move: bool,
221
+ total_moves: int,
222
+ illegal_moves: int,
223
+ opening_moves: int,
224
+ illegal_move_numbers: list[int],
225
+ illegal_move_numbers2: list[int],
226
+ ):
227
+ unique_game_id = generate_unique_game_id()
228
+
229
+ (
230
+ player_one_title,
231
+ player_two_title,
232
+ player_one_time,
233
+ player_two_time,
234
+ ) = get_player_titles_and_time(player_one, player_two)
235
+
236
+ if player_one_resignation or player_one_failed_to_find_legal_move:
237
+ result = "0-1"
238
+ player_one_score = 0
239
+ player_two_score = 1
240
+ elif player_two_resignation or player_two_failed_to_find_legal_move:
241
+ result = "1-0"
242
+ player_one_score = 1
243
+ player_two_score = 0
244
+ else:
245
+ result = board.result()
246
+ # Hmmm.... debating this one. Annoying if I leave it running and it fails here for some reason, probably involving some
247
+ # resignation / failed move situation I didn't think of
248
+ # -1e10 at least ensures it doesn't fail silently
249
+ if "-" in result:
250
+ player_one_score = result.split("-")[0]
251
+ player_one_score = 0.5 if player_one_score == "1/2" else player_one_score
252
+ player_two_score = result.split("-")[1]
253
+ player_two_score = 0.5 if player_two_score == "1/2" else player_two_score
254
+ elif result == "*": # Loss due to hitting max moves
255
+ player_one_score = 0
256
+ player_two_score = 1
257
+ else:
258
+ player_one_score = -1e10
259
+ player_two_score = -1e10
260
+
261
+ played_moves = player_one_illegal_moves + player_one_legal_moves
262
+ info_dict = {
263
+ "game_id": unique_game_id,
264
+ "transcript": game_state,
265
+ "result": result,
266
+ "player_one": player_one_title,
267
+ "player_two": player_two_title,
268
+ "player_one_time": player_one_time,
269
+ "player_two_time": player_two_time,
270
+ "player_one_score": player_one_score,
271
+ "player_two_score": player_two_score,
272
+ "player_one_illegal_moves": player_one_illegal_moves,
273
+ "player_two_illegal_moves": player_two_illegal_moves,
274
+ "player_one_legal_moves": player_one_legal_moves,
275
+ "player_two_legal_moves": player_two_legal_moves,
276
+ "player_one_resignation": player_one_resignation,
277
+ "player_two_resignation": player_two_resignation,
278
+ "player_one_failed_to_find_legal_move": player_one_failed_to_find_legal_move,
279
+ "player_two_failed_to_find_legal_move": player_two_failed_to_find_legal_move,
280
+ "game_title": f"{player_one_title} vs. {player_two_title}",
281
+ "number_of_moves": board.fullmove_number,
282
+ "p1_illegal_attempts": player_one_illegal_attempts,
283
+ "p1_avg_attempts_per_illegal": 0 if player_one_illegal_moves == 0 else player_one_illegal_attempts / float(player_one_illegal_moves),
284
+ "p1_illegal_attemtps_pct": 1.0 if played_moves == 0 else player_one_illegal_attempts / float(player_one_illegal_attempts + player_one_legal_moves),
285
+ "p1_illegal_moves_pct": 1.0 if played_moves == 0 else player_one_illegal_moves / float(played_moves),
286
+ "p1_first_illegal_move_num": illegal_move_numbers[0] if illegal_move_numbers else 0,
287
+ "p1_avg_illegal_move_num": np.average(illegal_move_numbers) if illegal_move_numbers else 0,
288
+ "p2_illegal_attempts": player_two_illegal_attempts,
289
+ "p2_avg_attempts_per_illegal": 0 if player_two_illegal_moves == 0 else player_two_illegal_attempts / float(player_two_illegal_moves),
290
+ "p2_illegal_attemtps_pct": 1.0 if played_moves == 0 else player_two_illegal_attempts / float(player_two_illegal_attempts + player_two_legal_moves),
291
+ "p2_illegal_moves_pct": 1.0 if played_moves == 0 else player_two_illegal_moves / float(played_moves),
292
+ "p2_first_illegal_move_num": illegal_move_numbers2[0] if illegal_move_numbers2 else 0,
293
+ "p2_avg_illegal_move_num": np.average(illegal_move_numbers2) if illegal_move_numbers2 else 0,
294
+
295
+
296
+ "time_taken": total_time,
297
+ "total_moves": total_moves,
298
+ "illegal_moves": illegal_moves,
299
+ }
300
+
301
+ if RUN_FOR_ANALYSIS:
302
+ csv_file_path = f"logs/{player_one_recording_name}_vs_{player_two_recording_name}"
303
+ csv_file_path = csv_file_path.replace(".", "_") # Because I'm using ckpt filenames for nanogpt models
304
+ csv_file_path += ".csv"
305
+ else:
306
+ csv_file_path = recording_file
307
+
308
+
309
+
310
+ # Determine if we need to write headers (in case the file doesn't exist yet)
311
+ write_headers = not os.path.exists(csv_file_path)
312
+
313
+ # Append the results to the CSV file
314
+ os.makedirs(os.path.dirname(csv_file_path), exist_ok=True)
315
+ with open(csv_file_path, "a", newline="") as csv_file:
316
+ writer = csv.DictWriter(csv_file, fieldnames=info_dict.keys())
317
+ if write_headers:
318
+ writer.writeheader()
319
+ writer.writerow(info_dict)
320
+
321
+ with open("game.txt", "w") as f:
322
+ f.write(game_state)
323
+
324
+
325
+ def generate_unique_game_id() -> str:
326
+ timestamp = int(time.time())
327
+ random_num = random.randint(1000, 9999) # 4-digit random number
328
+ return f"{timestamp}-{random_num}"
329
+
330
+
331
+ def get_player_titles_and_time(
332
+ player_one: Player, player_two: Player
333
+ ) -> Tuple[str, str, Optional[float], Optional[float]]:
334
+ player_one_config = player_one.get_config()
335
+ player_two_config = player_two.get_config()
336
+
337
+ # For player one
338
+ if "model" in player_one_config:
339
+ player_one_title = player_one_config["model"]
340
+ player_one_time = None
341
+ else:
342
+ player_one_title = f"Stockfish {player_one_config['skill_level']}"
343
+ player_one_time = player_one_config["play_time"]
344
+
345
+ # For player two
346
+ if "model" in player_two_config:
347
+ player_two_title = player_two_config["model"]
348
+ player_two_time = None
349
+ else:
350
+ player_two_title = f"Stockfish {player_two_config['skill_level']}"
351
+ player_two_time = player_two_config["play_time"]
352
+
353
+ return (player_one_title, player_two_title, player_one_time, player_two_time)
354
+
355
+
356
+ used_openings = []
357
+ def random_book_opening(
358
+ game_state: str, board: chess.Board
359
+ ) -> Tuple[str, chess.Board]:
360
+ global used_openings
361
+ with open("openings.csv", "r") as file:
362
+ lines = file.readlines()[1:] # Skip header
363
+ moves_string = random.choice(lines)
364
+ while moves_string in used_openings:
365
+ moves_string = random.choice(lines)
366
+ used_openings.append(moves_string)
367
+ if move_num_in_gamestate:
368
+ game_state = moves_string.rstrip() + " "
369
+ else:
370
+ game_state = ' '.join(['.' + m.split(".")[-1] if "." in m else m for m in moves_string.split()])
371
+ game_state = game_state.rstrip() + " "
372
+ # Splitting the moves string on spaces
373
+ tokens = moves_string.split()
374
+
375
+ for token in tokens:
376
+ # If the token contains a period, it's a move number + move combination
377
+ if "." in token:
378
+ move = token.split(".")[-1] # Take the move part after the period
379
+ else:
380
+ move = token
381
+
382
+ board.push_san(move)
383
+ return game_state.rstrip(), board, len(tokens) // 2
384
+
385
+
386
+ def add_random_moves(
387
+ game_state: str, board: chess.Board, num_moves: int = 20
388
+ ) -> Tuple[str, chess.Board, int]:
389
+ for i in range(num_moves * 2): # Full moves to half moves
390
+ legal_moves = list(board.legal_moves)
391
+ if not legal_moves:
392
+ return None, None, 0 # Game over, discard the game
393
+
394
+ move = board.san(random.choice(legal_moves))
395
+ board.push(board.parse_san(move))
396
+
397
+ if board.turn == chess.BLACK:
398
+ game_state += f" {i//2 + 1}.{move}" if move_num_in_gamestate else f" .{move}"
399
+ else:
400
+ game_state += f" {move}"
401
+
402
+ if board.is_game_over():
403
+ return None, None, 0 # Game over, discard the game
404
+
405
+ game_state = game_state.strip()
406
+ return game_state, board, num_moves
407
+
408
+
409
+ # Return is (move_san, move_uci, attempts, is_resignation, is_illegal_move)
410
+ def get_legal_move(
411
+ player: Player,
412
+ board: chess.Board,
413
+ game_state: str,
414
+ player_one: bool,
415
+ max_attempts: int = 5,
416
+ ) -> LegalMoveResponse:
417
+ """Request a move from the player and ensure it's legal."""
418
+ move_san = None
419
+ move_uci = None
420
+
421
+ for attempt in range(max_attempts):
422
+ #print(f"get_legal_move: |{game_state}|")
423
+ move_san = player.get_move(
424
+ board, game_state, min(((attempt / max_attempts) * 1) + 0.001, 0.75)
425
+ )
426
+
427
+ # Sometimes when GPT thinks it's the end of the game, it will just output the result
428
+ # Like "1-0". If so, this really isn't an illegal move, so we'll add a check for that.
429
+ if move_san is not None:
430
+ if move_san == "1-0" or move_san == "0-1" or move_san == "1/2-1/2":
431
+ print(f"{move_san}, player has resigned")
432
+ return LegalMoveResponse(
433
+ move_san=None,
434
+ move_uci=None,
435
+ attempts=attempt,
436
+ is_resignation=True,
437
+ )
438
+
439
+ try:
440
+ move_uci = board.parse_san(move_san)
441
+ except Exception as e:
442
+ print(f"Error parsing move {move_san}: {e}")
443
+ # check if player is gpt-3.5-turbo-instruct
444
+ # only recording errors for gpt-3.5-turbo-instruct because it's errors are so rare
445
+ if player.get_config()["model"] == "gpt-3.5-turbo-instruct":
446
+ with open("gpt-3.5-turbo-instruct-illegal-moves.txt", "a") as f:
447
+ f.write(f"{game_state}\n{move_san}\n")
448
+ continue
449
+
450
+ if move_uci in board.legal_moves:
451
+ if player_one == False:
452
+ if not move_san.startswith(" "):
453
+ move_san = " " + move_san
454
+ else:
455
+ if move_san.startswith(" "):
456
+ move_san = move_san[1:]
457
+ return LegalMoveResponse(move_san, move_uci, attempt)
458
+ print(f"Illegal move: {move_san}")
459
+
460
+ # If we reach here, the player has made illegal moves for all attempts.
461
+ print(f"{player} provided illegal moves for {max_attempts} attempts.")
462
+ return LegalMoveResponse(
463
+ move_san=None, move_uci=None, attempts=max_attempts, is_illegal_move=True
464
+ )
465
+
466
+
467
+ def play_turn(
468
+ player: Player, board: chess.Board, game_state: str, player_one: bool
469
+ ) -> Tuple[str, bool, bool, int]:
470
+ result = get_legal_move(player, board, game_state, player_one, 5)
471
+ illegal_moves = result.attempts
472
+ move_san = result.move_san
473
+ move_uci = result.move_uci
474
+ resignation = result.is_resignation
475
+ failed_to_find_legal_move = result.is_illegal_move
476
+
477
+ if resignation:
478
+ print(f"{player} resigned with result: {board.result()}")
479
+ elif failed_to_find_legal_move:
480
+ print(f"Game over: 5 consecutive illegal moves from {player}")
481
+ elif move_san is None or move_uci is None:
482
+ print(f"Game over: {player} failed to find a legal move")
483
+ else:
484
+ board.push(move_uci)
485
+ game_state += move_san
486
+ print(move_san, end=" ")
487
+
488
+ return game_state, resignation, failed_to_find_legal_move, illegal_moves
489
+
490
+
491
+ def play_games(
492
+ player_one: Player,
493
+ player_two: Player,
494
+ max_games: int = 10,
495
+ book_opening: bool = False,
496
+ random_opening: bool = False,
497
+ random_opening_moves: int = 20,
498
+ ):
499
+ unique_games = set()
500
+ games_saved = 0
501
+ while games_saved < max_games:
502
+ print(f"\nGame {games_saved} of {max_games}\n")
503
+
504
+ with open("gpt_inputs/prompt.txt", "r") as f:
505
+ game_state = f.read()
506
+ board = chess.Board()
507
+
508
+ if book_opening:
509
+ game_state, board, opening_moves = random_book_opening(game_state, board)
510
+ elif random_opening:
511
+ while True:
512
+ game_state, board, opening_moves = add_random_moves(game_state, board, random_opening_moves)
513
+ if game_state is not None:
514
+ break
515
+ else:
516
+ opening_moves = 0
517
+ player_one_illegal_moves = 0
518
+ player_one_illegal_attempts = 0
519
+ player_two_illegal_moves = 0
520
+ player_two_illegal_attempts = 0
521
+ player_one_legal_moves = 0
522
+ player_two_legal_moves = 0
523
+ player_one_resignation = False
524
+ player_two_resignation = False
525
+ player_one_failed_to_find_legal_move = False
526
+ player_two_failed_to_find_legal_move = False
527
+ start_time = time.time()
528
+
529
+ total_moves = 0
530
+ illegal_moves = 0
531
+ illegal_move_numbers = []
532
+ illegal_move_numbers2 = []
533
+ print_for_human = isinstance(player_one, HumanPlayer) or isinstance(player_two, HumanPlayer)
534
+
535
+ while not board.is_game_over():
536
+ if print_for_human:
537
+ print(board)
538
+
539
+ with open("game.txt", "w") as f:
540
+ f.write(game_state)
541
+ current_move_num = f"{board.fullmove_number if move_num_in_gamestate else ''}."
542
+ total_moves += 1
543
+ # I increment legal moves here so player_two isn't penalized for the game ending before its turn
544
+ player_one_legal_moves += 1
545
+ player_two_legal_moves += 1
546
+
547
+ # this if statement may be overkill, just trying to get format to exactly match PGN notation
548
+ if board.fullmove_number != 1:
549
+ game_state += " "
550
+ game_state += current_move_num
551
+ #print(f"|{game_state}|")
552
+ #print(f"{current_move_num}", end=" ")
553
+
554
+ (
555
+ game_state,
556
+ player_one_resignation,
557
+ player_one_failed_to_find_legal_move,
558
+ illegal_moves_one,
559
+ ) = play_turn(player_one, board, game_state, player_one=True)
560
+ player_one_illegal_moves += 1 if illegal_moves_one > 0 else 0
561
+ player_one_illegal_attempts += illegal_moves_one
562
+ if illegal_moves_one != 0:
563
+ player_one_legal_moves -= 1
564
+ illegal_move_numbers.append(board.fullmove_number)
565
+ if (
566
+ board.is_game_over()
567
+ or player_one_resignation
568
+ or player_one_failed_to_find_legal_move
569
+ ):
570
+ break
571
+
572
+ (
573
+ game_state,
574
+ player_two_resignation,
575
+ player_two_failed_to_find_legal_move,
576
+ illegal_moves_two,
577
+ ) = play_turn(player_two, board, game_state, player_one=False)
578
+ player_two_illegal_moves += 1 if illegal_moves_two > 0 else 0
579
+ player_two_illegal_attempts += illegal_moves_two
580
+ if illegal_moves_two != 0:
581
+ player_two_legal_moves -= 1
582
+ illegal_move_numbers2.append(board.fullmove_number)
583
+ if (
584
+ board.is_game_over()
585
+ or player_two_resignation
586
+ or player_two_failed_to_find_legal_move
587
+ ):
588
+ break
589
+
590
+ print("\n", end="")
591
+
592
+ if total_moves > MAX_MOVES:
593
+ break
594
+
595
+ end_time = time.time()
596
+ total_time = end_time - start_time
597
+ print(f"\nGame over. Total time: {total_time} seconds")
598
+ print(f"Result: {board.result()}")
599
+ print(board)
600
+ print()
601
+ game_transcript = game_state.strip()
602
+ if game_transcript not in unique_games:
603
+ unique_games.add(game_transcript)
604
+ record_results(
605
+ board,
606
+ player_one,
607
+ player_two,
608
+ game_state,
609
+ player_one_illegal_moves,
610
+ player_one_illegal_attempts,
611
+ player_two_illegal_moves,
612
+ player_two_illegal_attempts,
613
+ player_one_legal_moves,
614
+ player_two_legal_moves,
615
+ total_time,
616
+ player_one_resignation,
617
+ player_two_resignation,
618
+ player_one_failed_to_find_legal_move,
619
+ player_two_failed_to_find_legal_move,
620
+ total_moves,
621
+ illegal_moves,
622
+ opening_moves,
623
+ illegal_move_numbers,
624
+ illegal_move_numbers2
625
+ )
626
+ games_saved += 1
627
+ else:
628
+ print("Duplicate game; not saved.")
629
+ if isinstance(player_one, StockfishPlayer):
630
+ player_one.close()
631
+ if isinstance(player_two, StockfishPlayer):
632
+ player_two.close()
633
+
634
+ if RUN_FOR_ANALYSIS:
635
+ csv_file_path = f"logs/{player_one_recording_name}_vs_{player_two_recording_name}"
636
+ csv_file_path = csv_file_path.replace(".", "_") # Because I'm using ckpt filenames for nanogpt models
637
+ csv_file_path += ".csv"
638
+ else:
639
+ csv_file_path = recording_file
640
+ stats = calculate_stats(csv_file_path)
641
+ if stats:
642
+ print("\nStatistics:")
643
+ for key, value in stats.items():
644
+ print(f"{key}: {value}")
645
+
646
+ with open(csv_file_path, "a") as csv_file:
647
+ writer = csv.writer(csv_file)
648
+ writer.writerow([""] * 19) # Add empty cells for existing columns
649
+ writer.writerow(list(stats.keys()))
650
+ writer.writerow(list(stats.values()))
651
+
652
+
653
+ RUN_FOR_ANALYSIS = True
654
+ MAX_MOVES = 999 # Due to nanogpt max input length of 1024
655
+ recording_file = "logs/determine.csv" # default recording file. Because we are using list [player_ones], recording_file is overwritten
656
+ player_ones = ["50M/adams.pt"]
657
+ player_two_recording_name = "lc0_sweep" #"stockfish_sweep"
658
+ move_num_in_gamestate = False
659
+ book_opening = True
660
+ random_opening = False
661
+ random_opening_moves = 20
662
+ if __name__ == "__main__":
663
+ for nanogpt_player in player_ones:
664
+ for i in [0]: # [3] #range(11):
665
+ num_games = 500
666
+ # player_one = GPTPlayer(model="gpt-3.5-turbo-instruct")
667
+ # player_one = LocalLlamaPlayer(model_name="meta-llama/Llama-2-7b-hf")
668
+ # player_one = LocalLoraLlamaPlayer("meta-llama/Llama-2-7b-hf", "/workspace/axolotl/lora2-out")
669
+ # player_one = GPTPlayer(model="gpt-4")
670
+ # player_one = StockfishPlayer(skill_level=-1, play_time=0.1)
671
+
672
+ player_one_recording_name = nanogpt_player
673
+ # player_one = NanoGptPlayer(model_name=player_one_recording_name, move_num_in_gamestate=move_num_in_gamestate)
674
+ # player_one_recording_name = "xformer_" + nanogpt_player
675
+ player_one_recording_name = "mamba"
676
+ player_one = MambaPlayer(model_name="50M/anneal/anneal_complete_round3.pt", move_num_in_gamestate=move_num_in_gamestate)
677
+ #player_two = StockfishPlayer(skill_level=i, play_time=0.1)
678
+ # player_two = LC0PLayer(skill=i)
679
+
680
+ player_two = NanoGptPlayer(model_name="50M/stockfish_16layers_ckpt_with_optimizer.pt", move_num_in_gamestate=True)
681
+ player_two_recording_name = "adam"
682
+ # player_two = GPTPlayer(model="gpt-4")
683
+ # player_two = GPTPlayer(model="gpt-3.5-turbo-instruct")
684
+
685
+ #print(f"\n\nSTARTING GAMES AGAINST STOCKFISH LEVEL {i}\n\n")
686
+ print(f"\n\nSTARTING GAMES AGAINST LC0 LEVEL {i}\n\n")
687
+
688
+ play_games(player_one, player_two, num_games, book_opening=book_opening, random_opening=random_opening, random_opening_moves=random_opening_moves)
689
+
690
+ print("\n\n\n********\nFinal Statistics:\n********\n")
691
+ for nanogpt_player in player_ones:
692
+ #player_one_recording_name = "xformer_" + nanogpt_player
693
+ if RUN_FOR_ANALYSIS:
694
+ csv_file_path = f"logs/{player_one_recording_name}_vs_{player_two_recording_name}"
695
+ csv_file_path = csv_file_path.replace(".", "_") # Because I'm using ckpt filenames for nanogpt models
696
+ csv_file_path += ".csv"
697
+ else:
698
+ csv_file_path = recording_file
699
+
700
+ try:
701
+ stats = calculate_stats(csv_file_path)
702
+ if stats:
703
+ print(f"\nStatistics for {nanogpt_player}:")
704
+ for key, value in stats.items():
705
+ print(f"{key}: {value}")
706
+ except:
707
+ print(f"Couldn't get stats for {csv_file_path}")
708
+ print("\n\n\n********\nDONE!\n********\n\n\n")