test1978 commited on
Commit
89accdb
·
verified ·
1 Parent(s): 52a2708

Add mcvs_vs_uct.py

Browse files
Files changed (1) hide show
  1. mcvs_vs_uct.py +541 -0
mcvs_vs_uct.py ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BREAKTHROUGH MCVS vs UCT TOURNAMENT - 200 GAMES WITH DETAILED LOGGING
3
+
4
+ Exact analogue of the chess tournament script.
5
+ - Zone database saved AFTER EVERY GAME
6
+ - Detailed per-game training logging (examples, time taken)
7
+ - Shows progressive learning in console and learning_log.txt
8
+ """
9
+
10
+ import torch
11
+ import numpy as np
12
+ import os
13
+ import time
14
+ import threading
15
+ import queue
16
+ import random
17
+ from datetime import datetime
18
+ from breakthrough_mcvs import (
19
+ Breakthrough,
20
+ PolicyNetworkCNN,
21
+ ValueNetworkCNN,
22
+ HilbertOrderedZoneDatabase,
23
+ MCVSSearcher,
24
+ UCTSearcher,
25
+ train_networks
26
+ )
27
+
28
+ class DetailedMoveLogger:
29
+ """Logs detailed move generation statistics to move_log.txt"""
30
+
31
+ def __init__(self, filepath="move_log.txt"):
32
+ self.filepath = filepath
33
+ self.file_handle = None
34
+
35
+ def open(self):
36
+ """Open log file for writing"""
37
+ self.file_handle = open(self.filepath, 'w', encoding='utf-8')
38
+ self._write_header()
39
+
40
+ def _write_header(self):
41
+ """Write log file header"""
42
+ self.file_handle.write("=" * 100 + "\n")
43
+ self.file_handle.write("BREAKTHROUGH TOURNAMENT - MOVE GENERATION LOG\n")
44
+ self.file_handle.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
45
+ self.file_handle.write("=" * 100 + "\n\n")
46
+ self.file_handle.flush()
47
+
48
+ def log_move(self, game_num, move_num, player_name, agent_type,
49
+ simulations, time_taken, move, board_state=""):
50
+ """Log a single move with detailed statistics"""
51
+ msg = (
52
+ f"[Game {game_num:3d}] [Move {move_num:3d}] "
53
+ f"Player: {player_name:4s} ({agent_type:4s}) | "
54
+ f"Sims: {simulations:5d} | "
55
+ f"Time: {time_taken:6.3f}s | "
56
+ f"Move: {move}\n"
57
+ )
58
+ self.file_handle.write(msg)
59
+ self.file_handle.flush()
60
+
61
+ def log_game_summary(self, game_num, winner, total_moves, mcvs_stats, uct_stats):
62
+ """Log summary statistics for a completed game"""
63
+ self.file_handle.write("\n" + "-" * 100 + "\n")
64
+ self.file_handle.write(f"GAME {game_num} SUMMARY - Winner: {winner}\n")
65
+ self.file_handle.write(f"Total moves: {total_moves}\n\n")
66
+
67
+ self.file_handle.write(f"MCVS Statistics:\n")
68
+ self.file_handle.write(f" Total simulations: {mcvs_stats['total_sims']:,}\n")
69
+ self.file_handle.write(f" Avg sims/move: {mcvs_stats['avg_sims']:.1f}\n")
70
+ self.file_handle.write(f" Total time: {mcvs_stats['total_time']:.2f}s\n")
71
+ self.file_handle.write(f" Avg time/move: {mcvs_stats['avg_time']:.3f}s\n\n")
72
+
73
+ self.file_handle.write(f"UCT Statistics:\n")
74
+ self.file_handle.write(f" Total simulations: {uct_stats['total_sims']:,}\n")
75
+ self.file_handle.write(f" Avg sims/move: {uct_stats['avg_sims']:.1f}\n")
76
+ self.file_handle.write(f" Total time: {uct_stats['total_time']:.2f}s\n")
77
+ self.file_handle.write(f" Avg time/move: {uct_stats['avg_time']:.3f}s\n")
78
+ self.file_handle.write("-" * 100 + "\n\n")
79
+ self.file_handle.flush()
80
+
81
+ def close(self):
82
+ """Close log file"""
83
+ if self.file_handle:
84
+ self.file_handle.write("\n" + "=" * 100 + "\n")
85
+ self.file_handle.write(f"Completed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
86
+ self.file_handle.write("=" * 100 + "\n")
87
+ self.file_handle.close()
88
+
89
+ class LearningLogger:
90
+ """Logs training phase timing and statistics to learning_log.txt"""
91
+
92
+ def __init__(self, filepath="learning_log.txt"):
93
+ self.filepath = filepath
94
+ self.file_handle = None
95
+
96
+ def open(self):
97
+ """Open log file for writing"""
98
+ self.file_handle = open(self.filepath, 'w', encoding='utf-8')
99
+ self._write_header()
100
+
101
+ def _write_header(self):
102
+ """Write log file header"""
103
+ self.file_handle.write("=" * 100 + "\n")
104
+ self.file_handle.write("BREAKTHROUGH TOURNAMENT - LEARNING PHASE LOG\n")
105
+ self.file_handle.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
106
+ self.file_handle.write("=" * 100 + "\n\n")
107
+ self.file_handle.flush()
108
+
109
+ def log_training_queued(self, game_num, num_examples):
110
+ msg = f"→ Queued training data from game {game_num} ({num_examples} examples)\n"
111
+ self.file_handle.write(msg)
112
+ self.file_handle.flush()
113
+
114
+ def log_training_completed(self, game_num, num_examples, time_taken):
115
+ msg = f"✓ Completed training on game {game_num} data ({num_examples} examples) in {time_taken:.2f}s\n"
116
+ self.file_handle.write(msg)
117
+ self.file_handle.flush()
118
+
119
+ def log_initial_training(self, game_num, num_examples, time_taken, epochs):
120
+ """Log initial synchronous training after game 1"""
121
+ msg = (
122
+ f"\n{'=' * 100}\n"
123
+ f"INITIAL TRAINING (Game {game_num})\n"
124
+ f"{'=' * 100}\n"
125
+ f"Training examples: {num_examples}\n"
126
+ f"Epochs: {epochs}\n"
127
+ f"Training time: {time_taken:.2f}s\n"
128
+ f"Zone guidance enabled: λ = 1.0\n"
129
+ f"{'=' * 100}\n\n"
130
+ )
131
+ self.file_handle.write(msg)
132
+ self.file_handle.flush()
133
+
134
+ def log_tournament_summary(self, total_games_learned, total_training_time, avg_time_per_game):
135
+ msg = (
136
+ f"\n{'=' * 100}\n"
137
+ f"FINAL TRAINING SUMMARY\n"
138
+ f"{'=' * 100}\n"
139
+ f"Games learned from: {total_games_learned}\n"
140
+ f"Total training time: {total_training_time/60:.1f} minutes\n"
141
+ f"Avg time per game: {avg_time_per_game:.1f}s\n"
142
+ f"{'=' * 100}\n"
143
+ )
144
+ self.file_handle.write(msg)
145
+ self.file_handle.flush()
146
+
147
+ def close(self):
148
+ if self.file_handle:
149
+ self.file_handle.close()
150
+
151
+ class TrainingManager:
152
+ def __init__(self, policy_net, value_net, device, learning_logger):
153
+ self.policy_net = policy_net
154
+ self.value_net = value_net
155
+ self.device = device
156
+ self.learning_logger = learning_logger
157
+ self.queue = queue.Queue()
158
+ self.thread = threading.Thread(target=self._worker, daemon=True)
159
+ self.stop_event = threading.Event()
160
+ self.total_training_time = 0.0
161
+ self.games_processed = 0
162
+ self.thread.start()
163
+
164
+ def queue_training(self, W_states, policies, values, game_num):
165
+ self.queue.put((W_states, policies, values, game_num))
166
+ self.learning_logger.log_training_queued(game_num, len(W_states))
167
+
168
+ def _worker(self):
169
+ while not self.stop_event.is_set():
170
+ try:
171
+ W_states, policies, values, game_num = self.queue.get(timeout=1.0)
172
+ start = time.time()
173
+ train_networks(self.policy_net, self.value_net, W_states, policies, values, epochs=10, device=self.device)
174
+ elapsed = time.time() - start
175
+ self.total_training_time += elapsed
176
+ self.games_processed += 1
177
+ self.learning_logger.log_training_completed(game_num, len(W_states), elapsed)
178
+ self.queue.task_done()
179
+ except queue.Empty:
180
+ pass
181
+
182
+ def stop(self):
183
+ self.stop_event.set()
184
+ self.thread.join()
185
+
186
+ class BreakthroughTournament:
187
+ def __init__(self, num_games=200, time_per_move=1.0, max_moves_per_game=200, device='cpu',
188
+ output_file="breakthrough_tournament_results.txt", ablation_no_nets=False):
189
+ self.num_games = num_games
190
+ self.time_per_move = time_per_move
191
+ self.max_moves_per_game = max_moves_per_game
192
+ self.device = device
193
+ self.output_file = output_file
194
+
195
+ self.move_logger = DetailedMoveLogger()
196
+ self.learning_logger = LearningLogger()
197
+ self.report_lines = []
198
+
199
+ self.move_logger.open()
200
+ self.learning_logger.open()
201
+
202
+ self.ablation_no_nets = ablation_no_nets
203
+
204
+ self.zone_db = HilbertOrderedZoneDatabase("breakthrough_zone_db.npz")
205
+ self.policy_net = PolicyNetworkCNN().to(device)
206
+ self.value_net = ValueNetworkCNN().to(device)
207
+
208
+ self.agent_mcvs = MCVSSearcher(self.policy_net, self.value_net, self.zone_db, device=device, lambda_zone=0.0)
209
+ self.agent_uct = UCTSearcher()
210
+
211
+ self.training_manager = None
212
+ self.networks_initialized = False
213
+
214
+ self.results = {
215
+ "MCVS": {"wins": 0, "as_white": 0, "as_black": 0, "moves": []},
216
+ "UCT": {"wins": 0, "as_white": 0, "as_black": 0, "moves": []},
217
+ "Draw": {"count": 0, "moves": []}
218
+ }
219
+
220
+ if self.ablation_no_nets:
221
+ print("ABLATION MODE: Zone guidance only (NO neural networks)")
222
+ self.agent_mcvs = MCVSSearcher(
223
+ policy_net=None, value_net=None, zone_db=self.zone_db,
224
+ device=device, lambda_zone=1.0, use_nets=False
225
+ )
226
+ self.networks_initialized = False
227
+ else:
228
+ self.policy_net = PolicyNetworkCNN().to(device)
229
+ self.value_net = ValueNetworkCNN().to(device)
230
+ self.agent_mcvs = MCVSSearcher(
231
+ self.policy_net, self.value_net, self.zone_db,
232
+ device=device, lambda_zone=0.0, use_nets=True
233
+ )
234
+
235
+ def log_line(self, text):
236
+ print(text)
237
+ self.report_lines.append(text)
238
+
239
+ def extract_training_and_zone_data(self, moves_made, pi_records, outcome):
240
+ game = Breakthrough()
241
+ W_states = []
242
+ policies = []
243
+ values = []
244
+
245
+ for step, move in enumerate(moves_made):
246
+ # Current state matrix
247
+ W = game.get_weighted_adjacency_matrix().copy()
248
+ W_states.append(W)
249
+
250
+ # Policy target
251
+ pi = np.zeros(4096, dtype=np.float32) # 64 from-squares * 64 to-squares
252
+
253
+ visits = pi_records[step] # dict move_tuple -> count, or None if UCT move
254
+
255
+ legal_moves = game.get_legal_moves()
256
+
257
+ if visits is not None and sum(visits.values()) > 0:
258
+ total = sum(visits.values())
259
+ for m, count in visits.items():
260
+ fr, fc, tr, tc = m
261
+ from_sq = fr * 8 + fc
262
+ to_sq = tr * 8 + tc
263
+ idx = from_sq * 64 + to_sq
264
+ pi[idx] = count / total
265
+ else:
266
+ # Uniform over legal moves
267
+ if legal_moves:
268
+ p = 1.0 / len(legal_moves)
269
+ for m in legal_moves:
270
+ fr, fc, tr, tc = m
271
+ from_sq = fr * 8 + fc
272
+ to_sq = tr * 8 + tc
273
+ idx = from_sq * 64 + to_sq
274
+ pi[idx] = p
275
+
276
+ policies.append(pi)
277
+
278
+ # Value target (from perspective of player to move at this position)
279
+ if outcome is None: # Draw
280
+ v = 0.0
281
+ elif outcome == Breakthrough.PLAYER1:
282
+ # PLAYER1 wins → +1 if PLAYER1 to move here, -1 if PLAYER2 to move
283
+ v = 1.0 if (step % 2 == 0) else -1.0
284
+ else: # PLAYER2 wins
285
+ v = 1.0 if (step % 2 == 1) else -1.0
286
+
287
+ values.append(v)
288
+
289
+ # Apply the move to advance the replay
290
+ game.apply_move(move)
291
+
292
+ return W_states, policies, values
293
+
294
+ def play_game(self, agent_white, agent_black, game_idx):
295
+ game = Breakthrough()
296
+ moves = 0
297
+ moves_made = []
298
+ pi_records = []
299
+ mcvs_stats = {"total_sims": 0, "total_time": 0.0, "moves": 0}
300
+ uct_stats = {"total_sims": 0, "total_time": 0.0, "moves": 0}
301
+
302
+ while not game.is_terminal() and moves < self.max_moves_per_game:
303
+ moves += 1
304
+ current_player = Breakthrough.PLAYER1 if moves % 2 == 1 else Breakthrough.PLAYER2
305
+ searcher = agent_white if current_player == Breakthrough.PLAYER1 else agent_black
306
+ agent_type = "MCVS" if searcher is self.agent_mcvs else "UCT"
307
+
308
+ if agent_type == "MCVS":
309
+ mcvs_stats["moves"] += 1
310
+ else:
311
+ uct_stats["moves"] += 1
312
+
313
+ start_t = time.time()
314
+ output = searcher.search_with_time_budget(game.copy(), self.time_per_move)
315
+ # Robust handling for different return types
316
+ if isinstance(output, tuple) or isinstance(output, list):
317
+ visits_dict = output[0]
318
+ sims = output[1] if len(output) > 1 else sum(output[0].values()) if output[0] else 0
319
+ elif isinstance(output, dict):
320
+ visits_dict = output
321
+ sims = sum(output.values()) if output else 0
322
+ else:
323
+ raise ValueError(f"Unexpected search return: {type(output)}")
324
+ t_taken = time.time() - start_t
325
+
326
+ if agent_type == "MCVS":
327
+ mcvs_stats["total_sims"] += sims
328
+ mcvs_stats["total_time"] += t_taken
329
+ else:
330
+ uct_stats["total_sims"] += sims
331
+ uct_stats["total_time"] += t_taken
332
+
333
+ chosen_move = max(visits_dict, key=visits_dict.get) if visits_dict else random.choice(game.get_legal_moves())
334
+
335
+ player_name = "Player1" if current_player == Breakthrough.PLAYER1 else "Player2"
336
+ self.move_logger.log_move(game_idx, moves, player_name, agent_type, sims, t_taken, str(chosen_move))
337
+
338
+ game.apply_move(chosen_move)
339
+ moves_made.append(chosen_move)
340
+ pi_records.append(visits_dict if agent_type == "MCVS" else None)
341
+
342
+ # Averages
343
+ if mcvs_stats["moves"] > 0:
344
+ mcvs_stats["avg_sims"] = mcvs_stats["total_sims"] / mcvs_stats["moves"]
345
+ mcvs_stats["avg_time"] = mcvs_stats["total_time"] / mcvs_stats["moves"]
346
+ else:
347
+ mcvs_stats["avg_sims"] = 0.0
348
+ mcvs_stats["avg_time"] = 0.0
349
+
350
+ if uct_stats["moves"] > 0:
351
+ uct_stats["avg_sims"] = uct_stats["total_sims"] / uct_stats["moves"]
352
+ uct_stats["avg_time"] = uct_stats["total_time"] / uct_stats["moves"]
353
+ else:
354
+ uct_stats["avg_sims"] = 0.0
355
+ uct_stats["avg_time"] = 0.0
356
+
357
+ # Winner
358
+ if moves >= self.max_moves_per_game:
359
+ winner = "Draw"
360
+ winner_val = 0
361
+ self.move_logger.log_game_summary(game_idx, "Draw (move limit)", moves, mcvs_stats, uct_stats)
362
+ else:
363
+ winner_val = game.check_winner()
364
+ if winner_val == 0:
365
+ winner = "Draw"
366
+ elif winner_val == Breakthrough.PLAYER1:
367
+ winner = "MCVS" if agent_white is self.agent_mcvs else "UCT"
368
+ else:
369
+ winner = "MCVS" if agent_black is self.agent_mcvs else "UCT"
370
+
371
+ self.move_logger.log_game_summary(game_idx, winner, moves, mcvs_stats, uct_stats)
372
+
373
+ return winner, moves, moves_made, pi_records, winner_val
374
+
375
+ def run_tournament(self):
376
+ start_time = time.time()
377
+ self.log_line("Starting 200-game Breakthrough MCVS vs UCT tournament")
378
+
379
+ for game_idx in range(1, self.num_games + 1):
380
+ if game_idx % 2 == 1:
381
+ winner, moves, moves_made, pi_records, outcome = self.play_game(self.agent_mcvs, self.agent_uct, game_idx)
382
+ white_name = "MCVS"
383
+ else:
384
+ winner, moves, moves_made, pi_records, outcome = self.play_game(self.agent_uct, self.agent_mcvs, game_idx)
385
+ white_name = "UCT"
386
+
387
+ if winner == "Draw":
388
+ self.results["Draw"]["count"] += 1
389
+ self.results["Draw"]["moves"].append(moves)
390
+ else:
391
+ self.results[winner]["wins"] += 1
392
+ self.results[winner]["moves"].append(moves)
393
+ if winner == white_name:
394
+ self.results[winner]["as_white"] += 1
395
+ else:
396
+ self.results[winner]["as_black"] += 1
397
+
398
+ W_states, policies, values = self.extract_training_and_zone_data(moves_made, pi_records, outcome)
399
+
400
+ # Sample positions for zone DB (skip if no states were recorded)
401
+ if not W_states:
402
+ selected = []
403
+ else:
404
+ selected = [0]
405
+ if len(W_states) > 1:
406
+ selected.append(len(W_states)-1)
407
+ for i in range(1, len(W_states)-1):
408
+ if random.random() < 0.3:
409
+ selected.append(i)
410
+
411
+ # Label stored matrices according to the game's global outcome (not per-position training value)
412
+ if outcome == Breakthrough.PLAYER1:
413
+ label = 1
414
+ elif outcome == Breakthrough.PLAYER2:
415
+ label = -1
416
+ else:
417
+ label = 0
418
+
419
+ for i in selected:
420
+ W = W_states[i]
421
+ if label > 0:
422
+ self.zone_db.add_winning_matrix(W)
423
+ elif label < 0:
424
+ self.zone_db.add_losing_matrix(W)
425
+ else:
426
+ self.zone_db.add_draw_matrix(W)
427
+
428
+ # SAVE ZONE DB AFTER EVERY GAME
429
+ self.zone_db.save()
430
+
431
+ if game_idx == 1 and not self.ablation_no_nets:
432
+ # Initial synchronous training after game 1 (full system only)
433
+ train_start = time.time()
434
+ train_networks(
435
+ self.policy_net, self.value_net,
436
+ W_states, policies, values,
437
+ epochs=10, # More epochs for bootstrap
438
+ device=self.device
439
+ )
440
+ train_time = time.time() - train_start
441
+ self.learning_logger.log_initial_training(game_idx, len(W_states), train_time, 10)
442
+ self.agent_mcvs.lambda_zone = 1.0 # Turn on zone guidance after initial training
443
+ self.networks_initialized = True
444
+ self.training_manager = TrainingManager(
445
+ self.policy_net, self.value_net, self.device, self.learning_logger
446
+ )
447
+ print(f"Initial training completed in {train_time:.2f}s. Zone guidance enabled.")
448
+
449
+ if not self.ablation_no_nets and game_idx > 1:
450
+ # Queue asynchronous training for subsequent games (full system only)
451
+ self.training_manager.queue_training(W_states, policies, values, game_idx)
452
+
453
+ # Progress logging
454
+ elapsed = time.time() - start_time
455
+ remaining = elapsed / game_idx * (self.num_games - game_idx)
456
+ queue_size = self.training_manager.queue.qsize() if self.networks_initialized else 0
457
+ learned_games = self.training_manager.games_processed if self.networks_initialized else 0
458
+ db_size = len(self.zone_db.winning_matrices) + len(self.zone_db.losing_matrices) + len(self.zone_db.draw_matrices)
459
+ self.log_line(
460
+ f"Game {game_idx:3d}/{self.num_games} | {winner:10s} (moves={moves:3d}) | "
461
+ f"MCVS wins: {self.results['MCVS']['wins']:3d} | Draws: {self.results['Draw']['count']:3d} | "
462
+ f"Queue: {queue_size} | Learned: {learned_games} | DB: {db_size} | ETA: {remaining/60:.1f} min"
463
+ )
464
+
465
+ if self.networks_initialized:
466
+ self.log_line("\nWaiting for remaining training to finish...")
467
+ self.training_manager.stop()
468
+
469
+ avg_training_time = (self.training_manager.total_training_time /
470
+ self.training_manager.games_processed
471
+ if self.training_manager.games_processed > 0 else 0)
472
+ self.learning_logger.log_tournament_summary(
473
+ self.training_manager.games_processed,
474
+ self.training_manager.total_training_time,
475
+ avg_training_time
476
+ )
477
+ self.log_line("All training completed.")
478
+
479
+ # Final summary
480
+ self.log_line("\n" + "=" * 80)
481
+ self.log_line("TOURNAMENT SUMMARY")
482
+ self.log_line("=" * 80)
483
+
484
+ mcvs_wins = self.results["MCVS"]["wins"]
485
+ uct_wins = self.results["UCT"]["wins"]
486
+ draws = self.results["Draw"]["count"]
487
+
488
+ self.log_line(f"\nMCVS (λ-PUCT): {mcvs_wins} wins")
489
+ self.log_line(f"UCT (Standard): {uct_wins} wins")
490
+ self.log_line(f"Draws: {draws}")
491
+ self.log_line(f"Total: {self.num_games}")
492
+
493
+ if mcvs_wins + uct_wins > 0:
494
+ win_rate = (mcvs_wins / (mcvs_wins + uct_wins)) * 100
495
+ self.log_line(f"\nMCVS Win Rate (excluding draws): {win_rate:.1f}%")
496
+
497
+ if self.results["MCVS"]["moves"]:
498
+ avg_moves_mcvs = np.mean(self.results["MCVS"]["moves"])
499
+ self.log_line(f"Avg moves (MCVS wins): {avg_moves_mcvs:.1f}")
500
+
501
+ if self.results["UCT"]["moves"]:
502
+ avg_moves_uct = np.mean(self.results["UCT"]["moves"])
503
+ self.log_line(f"Avg moves (UCT wins): {avg_moves_uct:.1f}")
504
+
505
+ self.log_line(f"\nMCVS White: {self.results['MCVS']['as_white']}, Black: {self.results['MCVS']['as_black']}")
506
+ self.log_line(f"UCT White: {self.results['UCT']['as_white']}, Black: {self.results['UCT']['as_black']}")
507
+
508
+ if self.networks_initialized:
509
+ total_db = len(self.zone_db.winning_matrices) + len(self.zone_db.losing_matrices) + len(self.zone_db.draw_matrices)
510
+ self.log_line(f"\nZone Database Statistics:")
511
+ self.log_line(f" Winning positions: {len(self.zone_db.winning_matrices)}")
512
+ self.log_line(f" Losing positions: {len(self.zone_db.losing_matrices)}")
513
+ self.log_line(f" Draw positions: {len(self.zone_db.draw_matrices)}")
514
+ self.log_line(f" Total positions: {total_db:,}")
515
+ self.log_line(f" Games learned from: {self.training_manager.games_processed}")
516
+ self.log_line(f" Total training time: {self.training_manager.total_training_time/60:.1f} min")
517
+
518
+ total_time = time.time() - start_time
519
+ self.log_line("\n" + "=" * 80)
520
+ self.log_line(f"Tournament Duration: {total_time/60:.1f} minutes")
521
+ self.log_line(f"Avg time per game: {total_time/self.num_games:.1f}s")
522
+ self.log_line("=" * 80 + "\n")
523
+
524
+ with open(self.output_file, 'w', encoding='utf-8') as f:
525
+ f.write('\n'.join(self.report_lines))
526
+
527
+ self.move_logger.close()
528
+ self.learning_logger.close()
529
+
530
+ if __name__ == "__main__":
531
+ DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
532
+ print(f"Using device: {DEVICE}")
533
+
534
+ # 2. Ablation: Zone-only (no nets)
535
+ print("\n=== RUNNING ABLATION: Zone guidance ONLY (no neural networks) ===")
536
+ tournament_ablation = BreakthroughTournament(
537
+ num_games=100, time_per_move=0.5, max_moves_per_game=100,
538
+ device=DEVICE, output_file="breakthrough_zone_only_results.txt",
539
+ ablation_no_nets=True # Set to True to disable neural networks entirely
540
+ )
541
+ tournament_ablation.run_tournament()