package fr.iut_fbleau.HexGame; import fr.iut_fbleau.GameAPI.*; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.EnumMap; import java.util.Random; /** * Lance un grand nombre de parties Hex entre 2 bots aléatoires et affiche des stats. * * Exemples : * java fr.iut_fbleau.HexGame.HexSimMain * java fr.iut_fbleau.HexGame.HexSimMain --games 10000 --size 7 --seed 123 * java fr.iut_fbleau.HexGame.HexSimMain --games 5000 --size 11 --csv results.csv */ public class HexSimMain { private static class Stats { long win = 0; long draw = 0; long loss = 0; long totalMoves = 0; long minMoves = Long.MAX_VALUE; long maxMoves = Long.MIN_VALUE; void record(Result r, int moves) { if (r == Result.WIN) win++; else if (r == Result.DRAW) draw++; else if (r == Result.LOSS) loss++; totalMoves += moves; minMoves = Math.min(minMoves, moves); maxMoves = Math.max(maxMoves, moves); } long games() { return win + draw + loss; } double winRate() { return games() == 0 ? 0.0 : (double) win / games(); } double drawRate() { return games() == 0 ? 0.0 : (double) draw / games(); } double lossRate() { return games() == 0 ? 0.0 : (double) loss / games(); } double avgMoves() { return games() == 0 ? 0.0 : (double) totalMoves / games(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Games: ").append(games()).append("\n"); sb.append("WIN: ").append(win).append(String.format(" (%.2f%%)\n", 100.0 * winRate())); sb.append("DRAW: ").append(draw).append(String.format(" (%.2f%%)\n", 100.0 * drawRate())); sb.append("LOSS: ").append(loss).append(String.format(" (%.2f%%)\n", 100.0 * lossRate())); sb.append(String.format("Moves: avg=%.2f, min=%d, max=%d\n", avgMoves(), minMoves, maxMoves)); return sb.toString(); } } private static class Args { int size = 7; int games = 1000; long seed = System.currentTimeMillis(); int progressEvery = 0; // 0 = pas de progress String csvPath = null; // si non null, export par partie } public static void main(String[] args) { Args a = parseArgs(args); System.out.println("Hex random-vs-random simulation"); System.out.println(" size=" + a.size + " games=" + a.games + " seed=" + a.seed + (a.csvPath != null ? " csv=" + a.csvPath : "")); Random master = new Random(a.seed); Stats stats = new Stats(); BufferedWriter csv = null; try { if (a.csvPath != null) { csv = new BufferedWriter(new FileWriter(a.csvPath)); csv.write("game_index,result_p1,moves\n"); } for (int i = 0; i < a.games; i++) { // Nouveau plateau, nouveaux bots (seeds dérivés du seed principal) HexBoard board = new HexBoard(a.size); EnumMap players = new EnumMap<>(Player.class); players.put(Player.PLAYER1, new RandomBot(Player.PLAYER1, new Random(master.nextLong()))); players.put(Player.PLAYER2, new RandomBot(Player.PLAYER2, new Random(master.nextLong()))); int moves = runOneGame(board, players); Result res = board.getResult(); stats.record(res, moves); if (csv != null) { csv.write(i + "," + res + "," + moves + "\n"); } if (a.progressEvery > 0 && (i + 1) % a.progressEvery == 0) { System.out.println("Progress: " + (i + 1) + "/" + a.games); } } System.out.println("\n=== SUMMARY (Result is from PLAYER1 perspective) ==="); System.out.println(stats); } catch (IOException e) { System.err.println("I/O error: " + e.getMessage()); e.printStackTrace(); } finally { if (csv != null) { try { csv.close(); } catch (IOException ignored) {} } } } /** * Boucle de jeu (même logique que AbstractGame.run, mais on compte les coups). * On ne modifie pas GameAPI. */ private static int runOneGame(IBoard board, EnumMap players) { int moves = 0; int guardMaxMoves = ((HexBoard) board).getSize() * ((HexBoard) board).getSize(); // au pire : plateau rempli while (!board.isGameOver()) { AbstractGamePlayer p = players.get(board.getCurrentPlayer()); IBoard safe = board.safeCopy(); AbstractPly ply = p.giveYourMove(safe); if (!board.isLegal(ply)) { throw new IllegalStateException("Illegal move: " + ply + " by " + board.getCurrentPlayer()); } board.doPly(ply); moves++; if (moves > guardMaxMoves) { throw new IllegalStateException("Too many moves (" + moves + "), something is wrong."); } } return moves; } private static Args parseArgs(String[] args) { Args a = new Args(); for (int i = 0; i < args.length; i++) { String s = args[i]; switch (s) { case "--size": a.size = Integer.parseInt(nextArg(args, ++i, "--size requires a value")); break; case "--games": a.games = Integer.parseInt(nextArg(args, ++i, "--games requires a value")); break; case "--seed": a.seed = Long.parseLong(nextArg(args, ++i, "--seed requires a value")); break; case "--progress": a.progressEvery = Integer.parseInt(nextArg(args, ++i, "--progress requires a value")); break; case "--csv": a.csvPath = nextArg(args, ++i, "--csv requires a value"); break; case "--help": case "-h": printHelpAndExit(); break; default: // compat: si l'utilisateur donne juste un nombre, on l'interprète comme size ou games // ex: "7 10000" if (isInt(s)) { int v = Integer.parseInt(s); if (a.size == 7) a.size = v; else a.games = v; } else { System.err.println("Unknown arg: " + s); printHelpAndExit(); } } } return a; } private static String nextArg(String[] args, int idx, String errMsg) { if (idx < 0 || idx >= args.length) throw new IllegalArgumentException(errMsg); return args[idx]; } private static boolean isInt(String s) { try { Integer.parseInt(s); return true; } catch (NumberFormatException e) { return false; } } private static void printHelpAndExit() { System.out.println("Usage: java fr.iut_fbleau.HexGame.HexSimMain [options]\n" + "Options:\n" + " --size N Board size (default 7)\n" + " --games N Number of games (default 1000)\n" + " --seed N Random seed (default current time)\n" + " --progress N Print progress every N games (default 0)\n" + " --csv FILE Write per-game results to CSV\n" + " -h, --help Show this help\n"); System.exit(0); } }