diff --git a/README.md b/README.md index 76ca216..032c815 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,219 @@ # Breakthrough -Experiments with Monte Carlo Tree Search (MCTS) and its modifications applied to the game [Breakthrough](https://en.wikipedia.org/wiki/Breakthrough_(board_game)). +

+ Rust + Macroquad + Clap CLI + Python + License: MIT +

+ +A high-performance implementation of the **Breakthrough** board game featuring advanced AI agents based on **Monte Carlo Tree Search (MCTS)** and **Minimax**. This project explores various MCTS modifications, including **RAVE** (*Rapid Action Value Estimation*) and **Heavy Playouts**, evaluating their performance against heuristic-based baselines. ## Game Rules -Breakthrough is a two-player abstract strategy game played on a rectangular board (most commonly 8×8). Each player begins with two full rows of pieces positioned on their side of the board. +Breakthrough is a two-player abstract strategy game played on a rectangular board (most commonly 8×8). +- **Objective**: Be the first to reach the opponent's back row with any piece, or capture all opponent pieces. +- **Movement**: Pieces move one square forward (straight or diagonally) into empty squares. +- **Capture**: Captured by moving one square diagonally forward onto an opponent's piece. +- **No Draws**: The game's forward-only nature ensures a decisive outcome. + +## Key Features + +- **Advanced Search Algorithms**: + - **MCTS (UCT)**: Standard Monte Carlo Tree Search using the UCB1 formula. + - **RAVE**: Accelerated convergence using the AMAF (*All-Moves-As-First*) heuristic. + - **Heavy Playouts**: Domain-aware simulations using $\epsilon$-greedy heuristic guidance. + - **Minimax**: Tactical baseline with Alpha-Beta pruning and expert evaluation. +- **Interactive GUI**: Real-time gameplay with a built-in parameter adjustment panel. +- **Tournament Mode**: Automated head-to-head evaluation of different agent configurations. +- **Reproducibility**: Deterministic seeding for all probabilistic components. + +## Graphical Interface + +| Main Menu | Human Gameplay | +|:---:|:---:| +| ![Main Menu](docs/overleaf/images/main_menu.png) | ![Human Turn](docs/overleaf/images/human_turn.png) | +| **MCTS Agent Thinking** | **Victory Screen** | +| ![MCTS Turn](docs/overleaf/images/mcts_turn.png) | ![Player Won](docs/overleaf/images/player_won.png) | + +## Evaluation + +The performance of various MCTS configurations was evaluated against a **Minimax baseline** with varying search depths ($d \in [1, 7]$). + +### Win Rates + +The charts below compare the win rates of different MCTS variants against Minimax of increasing strength. Notably, MCTS variants dominate at $d=1$, struggle in the middle range ($d=3, 4$), but regain efficiency at larger depths ($d=7$) due to the long-term strategic value of full simulations. + +| MCTS | MCTS + RAVE | MCTS + Heavy Playouts | +|:---:|:---:|:---:| +| ![MCTS Win Rate](docs/overleaf/images/mcts_win_rate.png) | ![RAVE Win Rate](docs/overleaf/images/rave_win_rate.png) | ![Heavy Playouts Win Rate](docs/overleaf/images/heavyplayouts_win_rate.png) | + +### Thinking Time + +Thinking times reflect the computational overhead of each modification. While RAVE adds minimal cost, **Heavy Playouts** significantly increase the average move time due to domain-specific heuristic evaluations during every simulation step. + +| MCTS | MCTS + RAVE | MCTS + Heavy Playouts | +|:---:|:---:|:---:| +| ![MCTS Time](docs/overleaf/images/mcts_thinking_time.png) | ![RAVE Time](docs/overleaf/images/rave_thinking_time.png) | ![Heavy Playouts Time](docs/overleaf/images/heavyplayouts_thinking_time.png) | + +### Game Length Distribution +The total number of moves per game indicates the aggressiveness and tactical depth of the agents. Boxplots illustrate the distribution of game lengths across different experiments. + +| MCTS | MCTS + RAVE | MCTS + Heavy Playouts | +|:---:|:---:|:---:| +| ![MCTS Total Moves](docs/overleaf/images/mcts_total_moves.png) | ![RAVE Total Moves](docs/overleaf/images/rave_total_moves.png) | ![Heavy Playouts Total Moves](docs/overleaf/images/heavyplayouts_total_moves.png) | + +## User Manual + +### Build Process + +The computational layer and graphical interface of the project are implemented entirely in **Rust**. To build and run the executables, you must have the **Rust toolchain** installed. + +Verify your installation by checking the versions of `rustc` and `cargo`: + +```bash +rustc --version +cargo --version +``` + +To build the application in **release mode** (optimized), run the following command from the project root: + +```bash +cd src/compute && cargo build --release +``` + +After a successful compilation, two independent executables will be generated in the `src/compute/target/release` directory: `breakthrough` and `tournament`. Both support command-line arguments (CLI). You can access the built-in help system by adding the `-h` or `--help` flag: -On each turn, a player moves a single piece one square forward. Moves can be made straight ahead or diagonally into an empty square, while captures are performed by moving one square diagonally forward onto an opponent’s piece. Pieces cannot move backward, and captures are optional. +```bash +./breakthrough --help +``` + +### Graphical Environment +The `breakthrough.exe` executable launches a graphical testing environment for single games between humans or a human and an AI agent. It allows defining board dimensions, setting random seeds, and saving detailed game logs for each player. -The objective is to either: -- move one of your pieces to the opponent's back row, or -- capture all of your opponent's pieces. +**Usage:** +```bash +./breakthrough [OPTIONS] +``` -Because all pieces move only forward and there is constant pressure toward the goal, games are decisive and do not result in draws. +| Option / Argument | Description | +|:--- |:--- | +| `` | Path to the TOML configuration file. Optional (default: `config.toml`). | +| `--white-output ` | (Optional) Path to the JSONL output file for the white player. Default: `white_output.jsonl`. | +| `--black-output ` | (Optional) Path to the JSONL output file for the black player. Default: `black_output.jsonl`. | +| `--board-width ` | (Required) Board width (number of columns). | +| `--board-height ` | (Required) Board height (number of rows). | +| `--seed ` | (Optional) Seed for the random number generator, crucial for reproducibility. | +| `-h, --help` | Displays the help screen with all available parameters. | +| `-V, --version` | Displays the program version. | -## Running the project +### Tournament Environment +The `tournament.exe` executable allows running games between two AI agents. The CLI is consistent with the graphical environment. -```sh -cargo run --bin breakthrough -- .\src\resources\configs\minimax_vs_human.toml +**Usage:** +```bash +./tournament [OPTIONS] ``` -## Project Structure - -The repository is organized into two main components: a high-performance game engine written in Rust and a Python-based analysis layer for experimentation and evaluation. - -```txt -Breakthrough/ -├── docs/ -│ └── ... -├── src/ -│ ├── compute/ -│ │ ├── src/ -│ │ │ ├── main.rs -│ │ │ └── ... -│ │ ├── Cargo.toml -│ │ └── ... -│ └── analysis/ -│ ├── main.py -│ └── ... -└── README.md -``` \ No newline at end of file +### Input File Format + +Agent parameters are configured using a TOML text file. You can define independent settings for both sides (white and black). Each player must have a specified `type`, which determines the available optional parameters. If a field is omitted, predefined defaults are used. + +**Example:** +```toml +board_width = 6 +board_height = 7 +seed = 100 + +[white_player] +type = "Minimax" +max_depth = 5 +material_weight = 250 +advancement_weight = 10 + +[black_player] +type = "Mcts" +max_iterations = 100000 +exploration_constant = 1.41 +use_heavy_playouts = true +``` + +#### Configuration Details + +**Minimax Agent** +| Field | Default | Description | +|:--- |:--- |:--- | +| `max_depth` | `4` | Maximum search depth in the game tree. | +| `material_weight` | `200` | Weight for material advantage (number of pieces). | +| `advancement_weight` | `5` | Weight for piece advancement toward the opponent's back row. | +| `defended_weight` | `5` | Weight for safe positions where pieces defend each other. | +| `edge_penalty_weight`| `-2` | Penalty for placing pieces on the edges of the board. | + +**MCTS Agent** +| Field | Default | Description | +|:--- |:--- |:--- | +| `max_iterations` | `75000` | Maximum MCTS iterations (tree expansions). | +| `max_time_ms` | `None` | Optional hard time limit per move in milliseconds. Overrides iterations if set. | +| `exploration_constant`| `1.41` | Exploration constant ($C$) in the UCT formula. | +| `use_rave` | `false` | Boolean to activate the RAVE modification. | +| `rave_k` | `1000.0` | Parameter $k$ controlling RAVE heuristic weight. | +| `use_heavy_playouts` | `false` | Boolean to enable heuristic-guided rollout simulations. | +| `heavy_playouts_epsilon`| `0.1` | Probability of a random move ($\varepsilon$) in $\epsilon$-greedy heavy playouts. | +| `material_weight` | `200` | Material advantage weight for heavy playouts evaluation. | +| `advancement_weight` | `5` | Advancement weight for heavy playouts. | + +**Human Player** + +Used for manual testing. Set `type = "Human"`. No additional parameters required. + +### Output File Format + +Results are saved in JSONL (JSON Lines) files. Each line is a valid JSON object aggregating statistics from a single player's perspective. + +**Common Output Fields:** + +| Field | Description | +|---|---| +| `board_width` | Board width dimension. | +| `board_height` | Board height dimension. | +| `seed` | Random seed used for the game. | +| `agent_type` | Type of the agent (`Minimax`, `Mcts`, or `Human`). | +| `agent_color` | Assigned color (`White` or `Black`). | +| `agent_won` | Boolean flag indicating if the agent won. | +| `pieces_remaining` | Number of the agent's pieces at the end of the game. | +| `total_moves` | Total number of moves made by the agent. | +| `moves` | List of moves performed during the game (e.g. "a6a5").| +| `move_times_ms` | List of thinking times for each move. | +| `opponent_type` | Type of the opponent agent. | +| `opponent_pieces_remaining` | Number of opponent’s pieces remaining at the end of the game. | + +**Algorithm-Specific Fields:** + +**Minimax:** + +| Field | Description | +|---|---| +| `max_depth` | Maximum depth of the search tree. | +| `total_nodes_evaluated` | Total number of nodes evaluated during search. | +| `total_cutoffs` | Number of alpha-beta pruning cutoffs. | + +**MCTS:** + +| Field | Description | +|---|---| +| `max_iterations` | Maximum number of iterations allowed. | +| `exploration_constant` | Constant controlling the exploration vs exploitation balance. | +| `use_rave` | Whether the RAVE heuristic is enabled. | +| `use_heavy_playouts` | Whether full (heavy) playout simulations are used. | +| `total_iterations` | Actual number of iterations performed. | +| `total_nodes_created` | Number of nodes created in the search tree. | + +## Authors + +The project was implemented as part of the *Methods of Artificial Intelligence* academic course in the summer semester of the 2025–2026 academic year by: +- [Adam Grącikowski](https://github.com/adamgracikowski) +- [Piotr Iśtok](https://github.com/p10tr13) + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/docs/.gitkeep b/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/CompileTex.ps1 b/docs/CompileTex.ps1 new file mode 100644 index 0000000..1dd16c7 --- /dev/null +++ b/docs/CompileTex.ps1 @@ -0,0 +1,62 @@ +param ( + [string]$InputFile = "report.tex", + [string]$OutputDir = ".\output_pdf" +) + +$AbsoluteOutputDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputDir) +$File = Get-Item -Path $InputFile -ErrorAction SilentlyContinue + +if (-not $File) { + Write-Host "Error: Cannot find file '$InputFile'." -ForegroundColor Red + exit +} + +$TexDir = $File.DirectoryName +$BaseName = $File.BaseName +$PdfFile = "$BaseName.pdf" + +Push-Location +Set-Location -Path $TexDir + +Write-Host "Working directory temporarily changed to: $TexDir" -ForegroundColor DarkGray +Write-Host "Starting compilation of $($File.Name)..." -ForegroundColor DarkGray + +pdflatex -interaction=nonstopmode $File.Name *> $null +pdflatex -interaction=nonstopmode $File.Name *> $null + +$ErrorsAndWarnings = $log2 | Select-String -Pattern "(!|Warning:|Error|Undefined|No file)" +if ($ErrorsAndWarnings) { + Write-Host "`nLaTeX reported some issues:" -ForegroundColor Yellow + $ErrorsAndWarnings | ForEach-Object { Write-Host " $_" -ForegroundColor Red } + Write-Host "Please check your .tex file or the $BaseName.log file for details.`n" -ForegroundColor Yellow +} + +if (Test-Path $PdfFile) { + if (-not (Test-Path $AbsoluteOutputDir)) { + New-Item -ItemType Directory -Path $AbsoluteOutputDir -Force | Out-Null + } + + Move-Item -Path $PdfFile -Destination "$AbsoluteOutputDir\$PdfFile" -Force + Write-Host "Created: $AbsoluteOutputDir\$PdfFile" -ForegroundColor Green +} +else { + Write-Host "`nError: Compilation failed. PDF file was not created." -ForegroundColor Red +} + +Write-Host "Cleaning up temporary files..." -ForegroundColor DarkGray + +$Extensions = @(".aux", ".out", ".toc", ".fls", ".fdb_latexmk", ".synctex.gz", ".bbl", ".blg") + +foreach ($Ext in $Extensions) { + $TempFile = "$BaseName$Ext" + if (Test-Path $TempFile) { + Remove-Item -Path $TempFile -Force + } +} + +if (Test-Path "texput.log") { + Remove-Item "texput.log" -Force +} + +Pop-Location +Write-Host "Script execution finished." -ForegroundColor Cyan \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ff176b6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# Tex Compilation + +To compile the documents and clean up temporary files automatically, use the `CompileTex.ps1` script: + +```ps1 +# Compile the main report +.\CompileTex.ps1 -InputFile .\overleaf\report.tex -OutputDir . + +# Compile the outline +.\CompileTex.ps1 -InputFile .\overleaf\outline.tex -OutputDir . +``` \ No newline at end of file diff --git a/docs/overleaf/images/heavyplayouts_board_size_win_rate.png b/docs/overleaf/images/heavyplayouts_board_size_win_rate.png new file mode 100644 index 0000000..a0394e2 Binary files /dev/null and b/docs/overleaf/images/heavyplayouts_board_size_win_rate.png differ diff --git a/docs/overleaf/images/heavyplayouts_thinking_time.png b/docs/overleaf/images/heavyplayouts_thinking_time.png new file mode 100644 index 0000000..e60a0c5 Binary files /dev/null and b/docs/overleaf/images/heavyplayouts_thinking_time.png differ diff --git a/docs/overleaf/images/heavyplayouts_total_moves.png b/docs/overleaf/images/heavyplayouts_total_moves.png new file mode 100644 index 0000000..9e5e7e7 Binary files /dev/null and b/docs/overleaf/images/heavyplayouts_total_moves.png differ diff --git a/docs/overleaf/images/heavyplayouts_win_rate.png b/docs/overleaf/images/heavyplayouts_win_rate.png new file mode 100644 index 0000000..5f5f47b Binary files /dev/null and b/docs/overleaf/images/heavyplayouts_win_rate.png differ diff --git a/docs/overleaf/images/human_turn.png b/docs/overleaf/images/human_turn.png new file mode 100644 index 0000000..3da4f54 Binary files /dev/null and b/docs/overleaf/images/human_turn.png differ diff --git a/docs/overleaf/images/main_menu.png b/docs/overleaf/images/main_menu.png new file mode 100644 index 0000000..e982d2e Binary files /dev/null and b/docs/overleaf/images/main_menu.png differ diff --git a/docs/overleaf/images/mcts_board_size_win_rate.png b/docs/overleaf/images/mcts_board_size_win_rate.png new file mode 100644 index 0000000..9701576 Binary files /dev/null and b/docs/overleaf/images/mcts_board_size_win_rate.png differ diff --git a/docs/overleaf/images/mcts_thinking_time.png b/docs/overleaf/images/mcts_thinking_time.png new file mode 100644 index 0000000..9877f3c Binary files /dev/null and b/docs/overleaf/images/mcts_thinking_time.png differ diff --git a/docs/overleaf/images/mcts_total_moves.png b/docs/overleaf/images/mcts_total_moves.png new file mode 100644 index 0000000..8d4eefe Binary files /dev/null and b/docs/overleaf/images/mcts_total_moves.png differ diff --git a/docs/overleaf/images/mcts_turn.png b/docs/overleaf/images/mcts_turn.png new file mode 100644 index 0000000..1f93d55 Binary files /dev/null and b/docs/overleaf/images/mcts_turn.png differ diff --git a/docs/overleaf/images/mcts_win_rate.png b/docs/overleaf/images/mcts_win_rate.png new file mode 100644 index 0000000..083715d Binary files /dev/null and b/docs/overleaf/images/mcts_win_rate.png differ diff --git a/docs/overleaf/images/minimax_turn.png b/docs/overleaf/images/minimax_turn.png new file mode 100644 index 0000000..61e72aa Binary files /dev/null and b/docs/overleaf/images/minimax_turn.png differ diff --git a/docs/overleaf/images/player_won.png b/docs/overleaf/images/player_won.png new file mode 100644 index 0000000..c39a299 Binary files /dev/null and b/docs/overleaf/images/player_won.png differ diff --git a/docs/overleaf/images/rave_board_size_win_rate.png b/docs/overleaf/images/rave_board_size_win_rate.png new file mode 100644 index 0000000..c23426f Binary files /dev/null and b/docs/overleaf/images/rave_board_size_win_rate.png differ diff --git a/docs/overleaf/images/rave_thinking_time.png b/docs/overleaf/images/rave_thinking_time.png new file mode 100644 index 0000000..68f48cf Binary files /dev/null and b/docs/overleaf/images/rave_thinking_time.png differ diff --git a/docs/overleaf/images/rave_total_moves.png b/docs/overleaf/images/rave_total_moves.png new file mode 100644 index 0000000..dfc7aee Binary files /dev/null and b/docs/overleaf/images/rave_total_moves.png differ diff --git a/docs/overleaf/images/rave_win_rate.png b/docs/overleaf/images/rave_win_rate.png new file mode 100644 index 0000000..58654ec Binary files /dev/null and b/docs/overleaf/images/rave_win_rate.png differ diff --git a/docs/overleaf/report.tex b/docs/overleaf/report.tex new file mode 100644 index 0000000..0dfbfba --- /dev/null +++ b/docs/overleaf/report.tex @@ -0,0 +1,758 @@ +\documentclass{article} +%---------------------------------------------------------------------- +\usepackage[polish]{babel} +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage[a4paper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} +\usepackage{amsmath} +\usepackage{graphicx} +\usepackage{blindtext} +\usepackage{nomencl} +\usepackage{listings} +\usepackage{etoolbox} +\usepackage{sudoku} +\usepackage{algorithm} +\usepackage{algpseudocode} +\usepackage{tabularx} +\usepackage{array} +\usepackage{enumitem} +\usepackage[colorlinks=true, linkcolor=black, urlcolor=black, citecolor=black]{hyperref} +\usepackage{subcaption} +\usepackage{xcolor} +\usepackage{float} +\usepackage{newfloat} +\usepackage{fancyvrb} +\usepackage{multirow} + +\definecolor{consoleGreen}{RGB}{22, 198, 12} +\definecolor{consoleRed}{RGB}{197, 15, 31} +\definecolor{consoleYellow}{RGB}{193, 156, 0} +\definecolor{consoleBlue}{RGB}{0, 55, 218} +\definecolor{consoleGray}{RGB}{100, 100, 100} +\definecolor{consolePurple}{RGB}{136, 23, 152} + +\newcommand{\txtGreen}[1]{\textcolor{consoleGreen}{#1}} +\newcommand{\txtRed}[1]{\textcolor{consoleRed}{#1}} +\newcommand{\txtYellow}[1]{\textcolor{consoleYellow}{#1}} +\newcommand{\txtBlue}[1]{\textcolor{consoleBlue}{#1}} +\newcommand{\txtGray}[1]{\textcolor{consoleGray}{#1}} +\newcommand{\txtPurple}[1]{\textcolor{consolePurple}{#1}} + +\newfloat{code}{H}{loc}[section] +\floatname{code}{Listing} + +\lstset{ + basicstyle=\ttfamily, +} +%---------------------------------------------------------------------- +\begin{document} + +\begin{titlepage} + \centering + \vspace{1.5cm} + \includegraphics[width=0.9\textwidth]{university-logo.png} \\ + \vspace{2.5cm} + {\fontsize{30}{20}\selectfont \textbf{Metody Sztucznej Inteligencji 2} \par} + \vspace{1.5cm} + {\fontsize{25}{20}\selectfont Zastosowanie Monte-Carlo Tree Search do implementacji sztucznej inteligencji w grze Breakthrough\par} + \vspace{1.5cm} + {\fontsize{18}{20}\selectfont Raport końcowy\par} + \vspace{1.5cm} + {\fontsize{12}{20}\selectfont Zespół: \\ inż. Adam Grącikowski \\ inż. Piotr Iśtok \par} + \vspace{0.5cm} + {\fontsize{12}{20}\selectfont Prowadzący: \\ mgr inż. Bartłomiej Olechno \\ prof. dr hab. inż. Jacek Mańdziuk \par} + + \vfill + Oświadczamy, że niniejsza praca, stanowiąca podstawę do uznania osiągnięcia efektów uczenia się z przedmiotu \textit{Metody Sztucznej Inteligencji 2}, została wykonana samodzielnie. + + \vspace{1cm} + + Warszawa, \today + +\end{titlepage} + +%---------------------------------------------------------------------- +\tableofcontents +\newpage + +%---------------------------------------------------------------------- + +\newpage +\section{Instrukcja użytkownika} +\label{sec:instrukcja} + +\subsection{Środowisko i proces budowania} + +Warstwa obliczeniowa oraz interfejs graficzny projektu zostały w całości zrealizowane w języku Rust~\cite{rust-install}. Do poprawnego zbudowania i uruchomienia plików wykonywalnych niezbędne jest posiadanie dedykowanych dla tego języka narzędzi (ang. \textit{Rust toolchain}). Zalecana wersja kompilatora to \texttt{1.88.0} lub nowsza. + +Aby zweryfikować poprawność instalacji środowiska, należy wywołać w wierszu poleceń polecenia sprawdzające wersje kompilatora \texttt{rustc}~\cite{rustc-man} oraz menedżera pakietów \texttt{Cargo}~\cite{cargo-book} (Rysinek~\ref{fig:rust-check}): + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\{\}] +> |txtGreen{rustc} |txtGray{--version} +rustc 1.88.0 (6b00bc388 2025-06-23) +> |txtGreen{cargo} |txtGray{--version} +cargo 1.88.0 (873a06493 2025-05-10) +\end{Verbatim} +\caption{Weryfikacja instalacji środowiska Rust.} +\label{fig:rust-check} +\end{figure} + +\noindent +Aplikację należy budować w trybie optymalizacji kompilatora (ang. \textit{release}) korzystając z terminala w głównym katalogu projektu (Rysunek~\ref{fig:cargo-build}): + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\{\}] +> |txtGreen{cd} src/compute && |txtGreen{cargo} build |txtGray{--release} +\end{Verbatim} +\caption{Budowanie aplikacji w trybie optymalizacji kompilatora.} +\label{fig:cargo-build} +\end{figure} + +Po udanej kompilacji, w katalogu \path{src/compute/target/release} wygenerowane zostaną dwa niezależne pliki wykonywalne: \texttt{breakthrough} oraz \texttt{tournament}. Każdy z programów umożliwia obsługę argumentów linii poleceń (CLI). Wbudowany system pomocy dla każdego z algorytmów można wywołać dodając flagę \texttt{-h} lub \texttt{---help} podczas uruchamiania z terminala (np. \texttt{./as ---help}). + +Wszystkie programy przyjmują na wejściu ścieżkę do pliku konfiguracyjnego w formacie TOML. + +\subsection{Opis plików wykonywalnych} + +W następnych podrozdziałach przedstawiono szczegółowy opis argumentów pozycyjnych, flag oraz opcjonalnych parametrów sterujących dla każdego z plików wykonywalnych. + +\subsubsection{Środowisko graficzne} + +Plik wykonywalny \texttt{breakthrough.exe} uruchamia graficzne środowisko testowe przeznaczone do pojedynczej rozgrywki pomiędzy ludźmi lub człowiekiem i agentem (jednym z zaimplementowanych algorytmów). Pozwala na zdefiniowanie wymiarów planszy, ustawienie ziarna losowości oraz zapis szczegółowych informacji z przebiegu partii osobno dla każdego z graczy (Rysunek~\ref{fig:breakthrough-run}). + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\{\}] +> |txtGreen{breakthrough.exe} |txtGray{[OPTIONS]} |txtGray{} +\end{Verbatim} +\caption{Podstawowe użycie graficznego środowiska testowego.} +\label{fig:breakthrough-run} +\end{figure} + +\begin{table}[H] + \centering + \caption{Argumenty i opcje dla programu breakthrough.} + \label{tab:breakthrough-args} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|X|} + \hline + \textbf{Opcja / Argument} & \textbf{Opis} \\ + \hline + \texttt{} & Ścieżka do pliku konfiguracyjnego TOML. Pozycja opcjonalna, domyślnie przyjmuje wartość \texttt{config.toml}. \\ + \texttt{---white-output } & (Opcjonalne) Ścieżka do pliku wynikowego JSONL z zapisem ruchów gracza białego. Domyślnie: \texttt{white\_output.jsonl}. \\ + \texttt{---black-output } & (Opcjonalne) Ścieżka do pliku wynikowego JSONL z zapisem ruchów gracza czarnego. Domyślnie: \texttt{black\_output.jsonl}. \\ + \texttt{---board-width } & (Wymagane) Szerokość planszy do gry (liczba kolumn). \\ + \texttt{---board-height } & (Wymagane) Wysokość planszy do gry (liczba wierszy). \\ + \texttt{---seed } & (Opcjonalne) Ziarno dla generatora liczb losowych, kluczowe dla zapewnienia pełnej powtarzalności eksperymentów. \\ + \texttt{-h, ---help} & Wyświetla ekran pomocy z opisem wszystkich dostępnych parametrów. \\ + \texttt{-V, ---version} & Wyświetla wersję kompilacji programu. \\ + \hline + \end{tabularx} +\end{table} + +\subsubsection{Środowisko turniejowe} + +Plik wykonywalny \texttt{tournament.exe} pozwala na przeprowadzenie rozgrywki pomiędzy dwoma agentami (algorytmami). Interfejs wiersza poleceń zachowuje pełną spójność z graficznym środowiskiem testowym (Rysunek~\ref{fig:tournament-run}). + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\{\}] +> |txtGreen{tournament.exe} |txtGray{[OPTIONS]} |txtGray{} +\end{Verbatim} +\caption{Użycie modułu turniejowego.} +\label{fig:tournament-run} +\end{figure} + +\begin{table}[H] + \centering + \caption{Argumenty i opcje dla programu tournament.} + \label{tab:tournament-args} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|X|} + \hline + \textbf{Opcja / Argument} & \textbf{Opis} \\ + \hline + \texttt{} & Ścieżka do pliku konfiguracyjnego TOML. Pozycja opcjonalna, domyślnie przyjmuje wartość \texttt{config.toml}. \\ + \texttt{---white-output } & (Opcjonalne) Ścieżka do pliku wynikowego JSONL dla gracza białego. Domyślnie: \texttt{white\_output.jsonl}. \\ + \texttt{---black-output } & (Opcjonalne) Ścieżka do pliku wynikowego JSONL dla gracza czarnego. Domyślnie: \texttt{black\_output.jsonl}. \\ + \texttt{---board-width } & (Wymagane) Szerokość planszy turniejowej. \\ + \texttt{---board-height } & (Wymagane) Wysokość planszy turniejowej. \\ + \texttt{---seed } & (Opcjonalne) Ziarno generatora liczb losowych używane do deterministycznej inicjalizacji partii w ramach turnieju. \\ + \texttt{-h, ---help} & Wyświetla ekran pomocy programu. \\ + \texttt{-V, ---version} & Wyświetla wersję kompilacji programu. \\ + \hline + \end{tabularx} +\end{table} + +\subsection{Format pliku wejściowego} + +Konfiguracja parametrów agentów biorących udział w rozgrywce odbywa się za pomocą pliku tekstowego w formacie TOML. W pliku tym definiuje się niezależne ustawienia dla obu stron (np. dla gracza białego i czarnego). Każdy z graczy musi mieć jawnie określony typ algorytmu sterującego (pole \texttt{type}), co determinuje listę dostępnych parametrów opcjonalnych. + +W przypadku pominięcia któregokolwiek z opcjonalnych pól, środowisko przyjmie predefiniowane wartości domyślne. Przykład struktury takiego pliku przedstawiono na Rysunku~\ref{fig:config-example}. + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\{\}] +board_width = |txtBlue{6} +board_height = |txtBlue{7} +seed = |txtBlue{100} + +|txtPurple{[white_player]} +type = |txtYellow{"Minimax"} +max_depth = |txtBlue{5} +material_weight = |txtBlue{250} +advancement_weight = |txtBlue{10} + +|txtPurple{[black_player]} +type = |txtYellow{"Mcts"} +max_iterations = |txtBlue{100000} +exploration_constant = |txtBlue{1.41} +use_heavy_playouts = |txtRed{true} +\end{Verbatim} +\caption{Przykładowy plik konfiguracyjny \texttt{config.toml}.} +\label{fig:config-example} +\end{figure} + +\noindent +W następnych podrozdziałach opisano szczegółowo dostępne parametry dla każdego z trzech zaimplementowanych rodzajów graczy. + +\subsubsection{Agent Minimax} + +Wymaga podania parametrów sterujących przeszukiwaniem drzewa gry oraz wag poszczególnych komponentów funkcji heurystycznej oceniającej stan planszy. + +\begin{table}[H] + \centering + \caption{Pola konfiguracyjne dla agenta typu Minimax.} + \label{tab:config-minimax} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|c|X|} + \hline + \textbf{Pole} & \textbf{Domyślnie} & \textbf{Opis} \\ + \hline + \texttt{max\_depth} & \texttt{4} & Maksymalna głębokość przeszukiwania drzewa gry. \\ + \texttt{material\_weight} & \texttt{200} & Waga heurystyki odpowiadająca za przewagę materialną (liczbę bierek). \\ + \texttt{advancement\_weight} & \texttt{5} & Waga heurystyki premiująca zaawansowanie pionów w kierunku linii docelowej przeciwnika. \\ + \texttt{defended\_weight} & \texttt{5} & Waga heurystyki promująca bezpieczne ustawienia, w których piony wzajemnie się bronią. \\ + \texttt{edge\_penalty\_weight} & \texttt{-2} & Waga (kara) nakładana za umieszczenie pionów na krawędziach planszy. \\ + \hline + \end{tabularx} +\end{table} + +\subsubsection{Agent MCTS} + +Agent udostępnia on szereg zaawansowanych parametrów pozwalających na włączenie dodatkowych rozszerzeń, takich jak heurystyka RAVE czy Heavy Playouts. + +\begin{table}[H] + \centering + \caption{Pola konfiguracyjne dla agenta typu MCTS.} + \label{tab:config-mcts} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|c|X|} + \hline + \textbf{Pole} & \textbf{Domyślnie} & \textbf{Opis} \\ + \hline + \texttt{max\_iterations} & \texttt{75000} & Maksymalna liczba iteracji algorytmu MCTS (liczba rozbudów drzewa). \\ + \texttt{max\_time\_ms} & \texttt{None} & Opcjonalny, twardy limit czasu obliczeń dla jednego ruchu w milisekundach. Jeśli jest zdefiniowany, nadpisuje warunek liczby iteracji. \\ + \texttt{exploration\_constant} & \texttt{1.41} & Stała eksploracji ($C$) w formule UCT, balansująca proces poszukiwań. \\ + \texttt{use\_rave} & \texttt{false} & Wartość logiczna aktywująca modyfikację RAVE. \\ + \texttt{rave\_k} & \texttt{1000.0} & Parametr $k$ sterujący wagą heurystyki RAVE w stosunku do standardowych wartości UCT. \\ + \texttt{use\_heavy\_playouts} & \texttt{false} & Wartość logiczna włączająca ulepszone symulacje w fazie \textit{rollout}, kierowane funkcją oceny planszy. \\ + \texttt{heavy\_playouts\_epsilon} & \texttt{0.1} & Prawdopodobieństwo wykonania ruchu losowego ($\varepsilon$) w ulepszonych symulacjach (strategia $\epsilon$-greedy). \\ + \texttt{material\_weight} & \texttt{200} & Waga przewagi materialnej, wykorzystywana przez funkcję oceny w fazie \textit{heavy playouts}. \\ + \texttt{advancement\_weight} & \texttt{5} & Waga zaawansowania pionów dla symulacji \textit{heavy playouts}. \\ + \texttt{defended\_weight} & \texttt{5} & Waga bronionych pionów dla symulacji \textit{heavy playouts}. \\ + \texttt{edge\_penalty\_weight} & \texttt{-2} & Waga (kara) za piony na krawędzi dla symulacji \textit{heavy playouts}. \\ + \hline + \end{tabularx} +\end{table} + +\subsubsection{Gracz ludzki} + +Opcja dedykowana do manualnego testowania środowiska. Pozwala na zdefiniowanie interaktywnego gracza ludzkiego (\texttt{type = "Human"}), który wprowadza swoje ruchy poprzez interfejs graficzny GUI. Ten wariant agenta nie wymaga i nie przyjmuje żadnych dodatkowych parametrów konfiguracyjnych. + +\subsection{Format pliku wyjściowego} + +Wyniki działania środowiska testowego oraz turniejowego zapisywane są w plikach tekstowych o strukturze JSONL (JSON Lines). Każda linia w takim pliku stanowi niezależny, poprawny obiekt JSON, który agreguje statystyki i parametry z perspektywy pojedynczego gracza w rozegranej partii. + +Struktura generowanego logu zawiera zestaw parametrów wspólnych (rejestrowanych dla wszystkich typów graczy, w tym człowieka) oraz sekcję pól specyficznych, które zależą od użytego algorytmu. Przykładowy zrzut danych wyjściowych dla agenta wykorzystującego algorytm Minimax przedstawiono na Rysunku~\ref{fig:output-example}. + +\begin{figure}[H] +\small +\centering +\begin{Verbatim}[commandchars=\|\(\)] +{ + "board_width": |txtBlue(6), + "board_height": |txtBlue(7), + "seed": |txtBlue(100), + "agent_type": |txtYellow("Minimax"), + "agent_color": |txtYellow("Black"), + "agent_won": |txtRed(true), + "pieces_remaining": |txtBlue(10), + "move_times_ms": [|txtBlue(32), |txtBlue(74), |txtBlue(93), ...], + "total_moves": |txtBlue(16), + "moves": [|txtYellow("a6a5"), |txtYellow("e6d5"), |txtYellow("c6d5"), ...], + "opponent_type": |txtYellow("Mcts"), + "opponent_pieces_remaining": |txtBlue(6), + "max_depth": |txtBlue(4), + "total_nodes_evaluated": |txtBlue(33229), + "total_cutoffs": |txtBlue(10248), + "material_weight": |txtBlue(200), + "advancement_weight": |txtBlue(5), + "defended_weight": |txtBlue(5), + "edge_penalty_weight": |txtBlue(-2) +} +\end{Verbatim} +\caption{Przykładowy rekord w pliku wyjściowym dla agenta typu Minimax.} +\label{fig:output-example} +\end{figure} + +\noindent +W Tabeli~\ref{tab:out-common} zestawiono atrybuty bazowe, które są generowane dla każdego uczestnika rozgrywki (w tym dla agenta typu \texttt{Human}). + +\begin{table}[H] + \centering + \caption{Wspólne pola wyjściowe logowane dla każdego gracza.} + \label{tab:out-common} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|X|} + \hline + \textbf{Pole} & \textbf{Opis} \\ + \hline + \texttt{board\_width}, \texttt{board\_height} & Wymiary planszy, na której rozegrano partię. \\ + \texttt{seed} & Ziarno losowości wykorzystane w danej grze. \\ + \texttt{agent\_type} & Typ agenta generującego log (\texttt{Minimax}, \texttt{Mcts} lub \texttt{Human}). \\ + \texttt{agent\_color} & Kolor przypisany agentowi w partii (\texttt{White} lub \texttt{Black}). \\ + \texttt{agent\_won} & Flaga logiczna informująca, czy dany agent wygrał rozgrywkę. \\ + \texttt{pieces\_remaining} & Liczba pionów gracza, które pozostały na planszy w momencie zakończenia gry. \\ + \texttt{move\_times\_ms} & Lista zawierająca czas obliczania każdego kolejnego ruchu przez agenta (w milisekundach). \\ + \texttt{total\_moves} & Całkowita liczba ruchów wykonana przez agenta. \\ + \texttt{moves} & Sekwencja wszystkich wykonanych ruchów (np. \texttt{"a6a5"}). \\ + \texttt{opponent\_type} & Typ agenta przeciwnika. \\ + \texttt{opponent\_pieces\_remaining} & Liczba pionów przeciwnika na koniec gry. \\ + \hline + \end{tabularx} +\end{table} + +\noindent +W przypadku agentów zautomatyzowanych, bazowy zestaw danych jest rozszerzany o metryki charakterystyczne dla danego modułu heurystycznego. Tabela~\ref{tab:out-minimax} przedstawia logowane parametry dla algorytmu Minimax, natomiast Tabela~\ref{tab:out-mcts} dla algorytmu MCTS. + +\begin{table}[H] + \centering + \caption{Pola specyficzne dla agenta Minimax.} + \label{tab:out-minimax} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|X|} + \hline + \textbf{Pole} & \textbf{Opis} \\ + \hline + \texttt{max\_depth} & Skonfigurowana maksymalna głębokość przeszukiwania. \\ + \texttt{total\_nodes\_evaluated} & Łączna liczba wywołań funkcji statycznej oceny stanu gry (odwiedzonych liści) w trakcie całej partii. \\ + \texttt{total\_cutoffs} & Łączna liczba odcięć uzyskanych w całym drzewie przeszukiwań. \\ + \texttt{material\_weight} \dots & Zestaw wag wykorzystanych w funkcji oceny (\texttt{material}, \texttt{advancement}, \texttt{defended}, \texttt{edge\_penalty}). \\ + \hline + \end{tabularx} +\end{table} + +\begin{table}[H] + \centering + \caption{Pola specyficzne dla agenta MCTS.} + \label{tab:out-mcts} + \renewcommand{\arraystretch}{1.2} + \begin{tabularx}{\textwidth}{|l|X|} + \hline + \textbf{Pole} & \textbf{Opis} \\ + \hline + \texttt{max\_iterations} & Limit liczby iteracji algorytmu MCTS. \\ + \texttt{max\_time\_ms} & Limit czasu obliczeń dla jednego ruchu (lub \texttt{null}, jeśli brak). \\ + \texttt{exploration\_constant} & Wartość współczynnika eksploracji. \\ + \texttt{use\_rave}, \texttt{rave\_k} & Informacja o wykorzystaniu heurystyki RAVE i jej parametrze $k$. \\ + \texttt{use\_heavy\_playouts} \dots & Flaga logiczna i parametry (\texttt{epsilon} oraz wagi heurystyki) wykorzystywane do kierowania fazą symulacji (rollout). \\ + \texttt{total\_iterations} & Zsumowana liczba wszystkich iteracji MCTS wykonanych przez agenta we wszystkich ruchach danej partii. \\ + \texttt{total\_nodes\_created} & Sumaryczna liczba wygenerowanych w pamięci węzłów drzewa MCTS podczas gry. \\ + \hline + \end{tabularx} +\end{table} + +\subsection{Interfejs graficzny} + +Aplikacja została wyposażona w prosty, czytelny interfejs graficzny (GUI), umożliwiający rozgrywkę człowieka z agentem. Zestawienie poszczególnych widoków aplikacji przedstawiono na Rysunku~\ref{fig:gui-overview}. + +\begin{figure}[H] + \centering + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/main_menu.png} + \caption{Ekran startowy.} + \label{fig:gui-main} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/human_turn.png} + \caption{Oczekiwanie na ruch gracza.} + \label{fig:gui-human} + \end{subfigure} + + \vspace{0.5cm} + + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/player_won.png} + \caption{Ekran końcowy z ogłoszeniem zwycięzcy.} + \label{fig:gui-won} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/mcts_turn.png} + \caption{Oczekiwanie na ruch agenta.} + \label{fig:gui-mcts} + \end{subfigure} + + \vspace{0.5cm} + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/minimax_turn.png} + \caption{Gra z agentem Minimax.} + \label{fig:gui-minimax} + \end{subfigure} + + \caption{Zrzuty ekranu interfejsu graficznego dla gry Breakthrough.} + \label{fig:gui-overview} +\end{figure} + +\newpage +\section{Weryfikacja hipotez badawczych} + +\subsection{Hipoteza 1: Wpływ algorytmu RAVE} + +\subsection{Hipoteza 2: Wpływ algorytmu Heavy Playouts} + +\newpage +\subsection{Hipoteza 3: Przewaga nad ewaluacją statyczną} + +\subsubsection{Metodologia i przebieg eksperymentu} + +Weryfikacja hipotezy wymagała przeprowadzenia serii rozgrywek turniejowych, w których zautomatyzowani agenci bazujący na algorytmie MCTS zostali skonfrontowani z agentem heurystycznym, wykorzystującym algorytm Minimax ze statyczną funkcją oceny pozycji. Celem eksperymentu było zbadanie, czy probabilistyczne podejście do przeszukiwania przestrzeni stanów pozwala na uzyskanie przewagi nad deterministycznym oponentem o ograniczonym horyzoncie planowania. + +Eksperyment podzielono na trzy główne serie pomiarowe, odpowiadające testowanym wariantom algorytmu sterującego: MCTS, MCTS + RAVE oraz MCTS + Heavy Playouts. W każdej z serii agent testowy (korzystający ze stałego budżetu iteracyjnego 10000) rozgrywał partie przeciwko agentowi Minimax. Siła oponenta była stopniowo zwiększana poprzez inkrementację limitu maksymalnej głębokości przeszukiwania drzewa gry (parametr $d$) w przedziale od $d=1$ do $d=7$. Pozwoliło to na weryfikację zachowania wariantów stochastycznych w starciu z przeciwnikiem o rosnących zdolnościach taktycznych. + +Dla każdej testowanej głębokości oponenta rozegrano 20 niezależnych partii. Aby zneutralizować wpływ przewagi pierwszego ruchu, zrezygnowano z przypisywania stałych ról –- w 10 partiach agent kontrolował piony białe, a w pozostałych 10 piony czarne. Tym samym każda z trzech serii pomiarowych składała się ze 140 partii, co przełożyło się na 420 rozegranych gier w całym eksperymencie. + +Zgodnie z przyjętymi założeniami, główną metryką decydującą o weryfikacji hipotezy H3 był sumaryczny odsetek wygranych partii (wymagany próg powyżej 70\%). Dodatkowo, w celu pogłębienia analizy, z zebranych logów wyodrębniono informacje na temat średniej długości trwania rozgrywki (liczby ruchów) oraz czasów namysłu poszczególnych algorytmów. + +\subsubsection{Analiza wyników} + +Zestawienie odsetka wygranych partii dla wszystkich trzech wariantów algorytmu MCTS (MCTS, MCTS + RAVE oraz MCTS + Heavy Playouts) w zależności od głębokości przeszukiwania agenta Minimax przedstawiono na Rysunku~\ref{fig:comparison-winrate} oraz w Tabeli \ref{tab:comparison-win-rate}. \\ + +\begin{figure}[H] + \centering + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/mcts_win_rate.png} + \caption{Klasyczny MCTS} + \label{fig:winrate-mcts} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/rave_win_rate.png} + \caption{MCTS + RAVE} + \label{fig:winrate-rave} + \end{subfigure} + + \vspace{0.5cm} + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/heavyplayouts_win_rate.png} + \caption{MCTS + Heavy Playouts} + \label{fig:winrate-heavy} + \end{subfigure} + + \caption{Porównanie odsetka wygranych partii w zależności od głębokości przeszukiwania agenta Minimax dla różnych wariantów algorytmu MCTS.} + \label{fig:comparison-winrate} +\end{figure} + +\noindent +Bezpośrednie porównanie tych wykresów pozwala na sformułowanie kilku obserwacji dotyczących stabilności i siły gry poszczególnych modyfikacji: + +\begin{enumerate} + \item Niezależnie od zastosowanego wariantu MCTS, można zaobserwować wyraźny trend spadku skuteczności w początkowej fazie wykresu. Dla najmniejszej głębokości ($d=1$), wszystkie modyfikacje oparte na symulacjach całkowicie dominują agenta Minimax, osiągając wskaźniki zwycięstw bliskie lub równe 100\% ($\geq 70\%)$. Jednakże, wraz ze wzrostem limitu głębokości do $d=3$ i $d=4$, siła gry Minimaxa znacząco rośnie, co przekłada się na gwałtowny spadek skuteczności agentów probabilistycznych. W tym przedziale klasyczny MCTS oraz MCTS z modyfikacją RAVE przegrywają niemal wszystkie partie, co obnaża podatność losowych oraz pół-losowych symulacji na szerszy horyzont planowania agenta Minimax + \item Szczególnie interesujące jest zachowanie algorytmów dla dużych głębokości przeszukiwania oponenta ($d \geq 5$). Na wykresach zauważalne jest swoiste odwrócenie trendu –- skuteczność wariantów MCTS ponownie zaczyna rosnąć, a dla $d=7$ agent Minimax zaczyna regularnie przegrywać z ulepszonymi wariantami (Heavy Playouts i RAVE osiągają lepsze wyniki w tym rejonie w porównaniu do klasycznego MCTS, choć i on odnotowuje wzrost do poziomu około 50\%). Przy bardzo dużych głębokościach (np. $d=7$), agent Minimax staje się ofiarą własnego determinizmu -- ,,ślepo'' ufa zaprogramowanym wagom heurystycznym. Warianty MCTS, dzięki pełnym symulacjom do końca partii, potrafią elastyczniej identyfikować rzeczywiste szanse na wygraną. + \item Spośród testowanych modyfikacji, wariant MCTS + Heavy Playouts (Rysunek~\ref{fig:winrate-heavy}) wykazał się największą odpornością na siłę taktyczną Minimaxa w środkowym przedziale głębokości ($d=3$, $d=4$). + \item RAVE (Rysunek~\ref{fig:winrate-rave}) zachowywało się w sposób zbliżony do klasycznego MCTS. +\end{enumerate} + +\begin{table}[H] + \centering + \caption{Odsetek wygranych partii przez różne warianty algorytmu MCTS w starciu z algorytmami Minimax o zmiennej głębokości przeszukiwania ($d$).} + \label{tab:comparison-win-rate} + \renewcommand{\arraystretch}{1.2} + + \begin{tabular}{|c|c|p{1.6cm}|p{3cm}|} + \hline + \textbf{Głębokość} & \textbf{MCTS} & \textbf{MCTS + RAVE} & \textbf{MCTS + Heavy Playouts} \\ + \hline + 1 & \hfill 95.0\% & \hfill 70.0\% & \hfill 100.0\% \\ + 2 & \hfill 25.0\% & \hfill 15.0\% & \hfill 60.0\% \\ + 3 & \hfill 0.0\% & \hfill 10.0\% & \hfill 40.0\% \\ + 4 & \hfill 0.0\% & \hfill 0.0\% & \hfill 40.0\% \\ + 5 & \hfill 15.0\% & \hfill 15.0\% & \hfill 60.0\% \\ + 6 & \hfill 5.0\% & \hfill 0.0\% & \hfill 50.0\% \\ + 7 & \hfill 50.0\% & \hfill 50.0\% & \hfill 60.0\% \\ + \hline + \end{tabular} +\end{table} + +\begin{figure}[H] + \centering + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/mcts_total_moves.png} + \caption{Klasyczny MCTS} + \label{fig:moves-mcts} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/rave_total_moves.png} + \caption{MCTS + RAVE} + \label{fig:moves-rave} + \end{subfigure} + + \vspace{0.5cm} + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/heavyplayouts_total_moves.png} + \caption{MCTS + Heavy Playouts} + \label{fig:moves-heavy} + \end{subfigure} + + \caption{Zestawienie dystrybucji całkowitej liczby ruchów rozegranych w partiach przeciwko agentowi Minimax dla różnych wariantów algorytmu MCTS.} + \label{fig:comparison-moves} +\end{figure} + +\noindent +Kolejnym elementem poddanym analizie była średnia długość rozegranych partii, wyrażona jako całkowita liczba ruchów wykonana przez obu graczy przed osiągnięciem stanu końcowego (Rysunek~\ref{fig:comparison-moves}, Tabela \ref{tab:comparison-game-length}). Wykresy uwzględniają odrębne ,,pudełka'' dla sytuacji, w której agent MCTS steruje bierkami białymi oraz czarnymi. + +\begin{table}[H] + \centering + \caption{Średnia długość partii (wyrażona jako całkowita liczba ruchów obu graczy) wraz z odchyleniem standardowym, w starciach wariantów MCTS z algorytmem Minimax.} + \label{tab:comparison-game-length} + \renewcommand{\arraystretch}{1.2} + + \begin{tabular}{|c|c|p{1.6cm}|p{3cm}|} + \hline + \textbf{Głębokość} & \textbf{MCTS} & \textbf{MCTS + RAVE} & \textbf{MCTS + Heavy Playouts} \\ + \hline + 1 & \hfill 21.1 $\pm$ 4.3 & \hfill 21.5 $\pm$ 5.5 & \hfill 23.4 $\pm$ 3.5 \\ + 2 & \hfill 17.0 $\pm$ 3.2 & \hfill 15.8 $\pm$ 2.6 & \hfill 19.1 $\pm$ 3.9 \\ + 3 & \hfill 19.6 $\pm$ 4.7 & \hfill 18.6 $\pm$ 4.5 & \hfill 21.8 $\pm$ 3.4 \\ + 4 & \hfill 16.9 $\pm$ 2.9 & \hfill 16.1 $\pm$ 4.1 & \hfill 21.8 $\pm$ 3.3 \\ + 5 & \hfill 17.4 $\pm$ 3.5 & \hfill 17.4 $\pm$ 3.9 & \hfill 22.1 $\pm$ 4.2 \\ + 6 & \hfill 17.9 $\pm$ 3.1 & \hfill 16.3 $\pm$ 3.9 & \hfill 21.0 $\pm$ 3.8 \\ + 7 & \hfill 17.1 $\pm$ 4.7 & \hfill 15.8 $\pm$ 4.9 & \hfill 20.2 $\pm$ 4.4 \\ + \hline + \end{tabular} +\end{table} + +\begin{figure}[H] + \centering + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/mcts_thinking_time.png} + \caption{Klasyczny MCTS} + \label{fig:time-mcts} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/rave_thinking_time.png} + \caption{MCTS + RAVE} + \label{fig:time-rave} + \end{subfigure} + + \vspace{0.5cm} + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/heavyplayouts_thinking_time.png} + \caption{MCTS + Heavy Playouts} + \label{fig:time-heavy} + \end{subfigure} + + \caption{Średni czas namysłu (w milisekundach) dla różnych wariantów algorytmu MCTS oraz ich przeciwników (agenta Minimax o rosnącej głębokości przeszukiwania).} + \label{fig:comparison-time} +\end{figure} + +\noindent +Ostatnim, w ramach tego eksperymentu, badanym elementem była analiza średniego czasu namysłu wymaganego do wygenerowania pojedynczego ruchu. Profile wydajnościowe, zestawione na układzie wykresów (Rysunek~\ref{fig:comparison-time}), obrazują różnice w naturze obu podejść do przeszukiwania przestrzeni stanów. + +Przewidywalną obserwacją, jest wykładniczy wzrost czasu namysłu algorytmu Minimax wraz ze wzrostem limitu głębokości przeszukiwania ($d$). Dla płytkich wartości ($d \le 4$), decyzje podejmowane są niemal natychmiastowo, jednakże przekroczenie progu $d=5$ powoduje drastyczne wzrost wielkości drzewa gry (wynikające z wysokiego współczynnika rozgałęzienia w gry). Dla maksymalnej testowanej głębokości ($d=7$), pojedynczy ruch oponenta statycznego zajmuje znacząco więcej czasu niż iteracyjne rozwijanie drzewa przez warianty MCTS oraz MCTS + RAVE, co pokazuje ograniczenia skalowalności naiwnego (bądź wyposażonego jedynie w cięcia alfa-beta) przeglądu zupełnego. + +Ciekawszym zjawiskiem jest zachowanie czasu namysłu dla algorytmów opartych na MCTS. Mimo iż operowały one na stałym, odgórnie narzuconym limicie całkowitej liczby iteracji w każdym ruchu (niezależnie od przeciwnika), czas potrzebny na realizację tego budżetu skraca się (w przybliżeniu liniowo) wraz z rozwojem gry. Losowe lub pół-losowe rozgrywki algorytmów MCTS trwają coraz krócej ponieważ pozostało niewiele ruchów do zakończenia rozgrywki. + +Analizując warianty MCTS między sobą, można zauważyć, że zastosowanie heurystyki RAVE nie wprowadziło znaczącego narzutu obliczeniowego, natomiast wielokrotne wywoływanie funkcji oceny pozycji w wariancie Heavy Playouts, dosyć znacząco wydłuża czas namysłu algorytmu, którego średni czas namysłu do 11 ruchu przekracza 200 ms (przy stałym limicie 10000 iteracji). + +\subsubsection{Wnioski końcowe} + +Analiza zebranych danych pozwala stwierdzić, że dla najbardziej podstawowego wariantu płytkiego przeszukiwania, czyli dla głębokości $d=1$, wszystkie testowane modyfikacje algorytmu stochastycznego z sukcesem osiągnęły lub przekroczyły wymagany próg skuteczności. Klasyczny wariant MCTS uzyskał wynik 95.0\%, MCTS z heurystyką RAVE osiągnął barierę 70.0\%, natomiast wariant kierowany (Heavy Playouts) zdominował przeciwnika całkowicie, wygrywając 100.0\% rozegranych partii. + +\textbf{Hipoteza H3 zostaje przyjęta}. Należy jednak zaznaczyć, że założony próg 70\% wygranych jest spełniony wyłącznie dla głębokości równej 1. Zwiększenie horyzontu planowania agenta Minimax zaledwie o jeden lub dwa półruchy ($d \ge 2$) powodowało gwałtowne załamanie skuteczności metod opartych o algorytm MCTS. + +\newpage +\subsection{Hipoteza 4: Wpływ rozmiaru planszy na asymetrię szans} + +\subsubsection{Metodologia i przebieg eksperymentu} + +Weryfikacja czwartej hipotezy badawczej (H4) koncentrowała się na ocenie wpływu geometrii planszy na asymetrię szans w grze. Ze względu na specyfikę mechaniki Breakthrough, w której celem jest dotarcie do linii końcowej przeciwnika, gracz wykonujący pierwszy ruch (białe) dysponuje naturalną przewagą tempa. Hipoteza zakładała, że wraz ze wzrostem wymiarów planszy przewaga koloru białego ulegnie marginalizacji na rzecz strategii. Zgodnie z hipotezą, zmodyfikowane algorytmy MCTS będą w stanie odnosić relatywnie częstsze zwycięstwa z pozycji czarnych na większych planszach. + +Rozgrywki przeprowadzono dla asymetrycznych wymiarów planszy. Szerokość ustalono jako stałą wartość $W = 8$, natomiast wysokość ($H$) stanowiła zmienną niezależną przyjmującą wartości z rosnącego zbioru: $H \in \{5, 6, 7, 8, 9, 10, 11\}$. + +Eksperyment podzielono na trzy symetryczne serie pomiarowe, w których po obu stronach planszy stawali identyczni agenci: +\begin{enumerate} + \item \textbf{Seria 1:} Klasyczny algorytm MCTS (UCT) grający przeciwko sobie. + \item \textbf{Seria 2:} Wariant MCTS + RAVE. + \item \textbf{Seria 3:} Wariant MCTS + Heavy Playouts. +\end{enumerate} + +\noindent +Aby zapewnić agentom budżet adekwatny do rozmiaru gry, maksymalna liczba iteracji zmieniała się liniowo w zależności od wysokości planszy zgodnie ze wzorem: + +$$I = 2000 \cdot H$$ + +\noindent +Tym samym dla planszy $8 \times 5$ limit wynosił 10000 iteracji, a dla $8 \times 11$ już 22000. \\ + +\noindent +Dla każdego z testowanych wymiarów planszy i dla każdej z trzech serii rozegrano dokładnie 50 partii turniejowych. Ponieważ w każdej serii agenci reprezentowali identyczną siłę gry, zrezygnowano z rotacji kolorów na rzecz jednorodnego monitorowania współczynnika zwycięstw z perspektywy białego gracza. + +\subsubsection{Analiza wyników} + +Zgromadzone dane pozwoliły na prześledzenie zachowania wskaźnika zwycięstw dla gracza rozpoczynającego (Białe) w zależności od wysokości planszy $H$. Zestawienie wyników dla wszystkich trzech symetrycznych par algorytmów (Klasyczny MCTS, MCTS + RAVE, MCTS + Heavy Playouts) przedstawiono w formie wykresów na Rysunku~\ref{fig:h4-winrate-comparison}. Szczegółowe dane liczbowe zostały zgromadzone w Tabeli~\ref{tab:h4-summary}. + +\begin{figure}[H] + \centering + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/mcts_board_size_win_rate.png} + \caption{Klasyczny MCTS} + \label{fig:h4-mcts} + \end{subfigure} + \hfill + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/rave_board_size_win_rate.png} + \caption{MCTS + RAVE} + \label{fig:h4-rave} + \end{subfigure} + + \vspace{0.5cm} + + \begin{subfigure}[b]{0.49\textwidth} + \centering + \includegraphics[width=\textwidth]{images/heavyplayouts_board_size_win_rate.png} + \caption{MCTS + Heavy Playouts} + \label{fig:h4-heavy} + \end{subfigure} + + \caption{Odsetek zwycięstw gracza rozpoczynającego (Białe) w starciach symetrycznych w zależności od wysokości planszy ($H$) przy stałej szerokości $W=8$.} + \label{fig:h4-winrate-comparison} +\end{figure} + +\noindent +Wstępna analiza wyników wskazuje na dużą zmienność (wariancję) wskaźnika wygranych w poszczególnych wariantach na szerokim przedziale wysokości planszy. W celu ułatwienia interpretacji tych odchyleń, na wykresach naniesiono linię trendu obrazującą ogólną tendencję zmian skuteczności (modelowaną przez wielomian stopnia 2), a także poziomą linię wyznaczającą próg 50\% (tzw. linię \textit{fair play}), która reprezentuje stan idealnej równowagi szans między stroną rozpoczynającą a broniącą. + +Analiza zgromadzonych danych pozwala na pozytywne zweryfikowanie postawionych warunków. Rozpatrując kluczowy przedział wysokości od $H=7$ do $H=9$, zaobserwowano spadek odsetka wygranych Białego. Dla klasycznego MCTS skuteczność Białych spadła z poziomu 54.0\% ($H=7$) do 42.0\% ($H=8$) oraz 44.0\% ($H=9$), co oznacza utratę inicjatywy startowej i przejście inicjatywy (choć z niewielką przewagą) na stronę Czarnych. + +Wprowadzenie wiedzy domenowej poprzez modyfikację Heavy Playouts ujawniło wyraźny trend malejący: od dominacji Białych wynoszącej 80.0\% na planszy $8 \times 7$, poprzez spadek do 40.0\% na planszy $8 \times 8$, aż do zaledwie 20.0\% na planszy $8 \times 9$. + +Należy zaznaczyć, że trend ten nie zachowuje idealnej liniowości w nieskończoność (co pokazują anomalie dla dużej planszy $H=11$), a wariant RAVE charakteryzował się większą zachowawczością wyników wokół linii remisu (50\%). Nie zmienia to jednak faktu, że w zakładanym w kryteriach przedziale badawczym (odpowiednik plansz średnich i dużych) zjawisko marginalizacji przewagi Białych jest zauważalne. + +\begin{table}[H] + \centering + \caption{Zestawienie skuteczności i średniej długości partii dla wariantów MCTS w zależności od wysokości planszy.} + \label{tab:h4-summary} + \renewcommand{\arraystretch}{1.2} + + \begin{tabular}{|p{1.5cm}|p{1.8cm}|p{2.7cm}|p{2.1cm}|} + \hline + \textbf{Wariant} & \textbf{Wysokość planszy} & \textbf{Współczynnik zwycięstw (Białe/Czarne)} & \textbf{Średnia długość partii} \\ + \hline + + \multirow{7}{1.5cm}{Klasyczny MCTS} + & \hfill 5 & \hfill 50/50\% & \hfill 15.8 $\pm$ 3.5 \\ + & \hfill 6 & \hfill 52/48\% & \hfill 20.4 $\pm$ 4.1 \\ + & \hfill 7 & \hfill 54/46\% & \hfill 25.4 $\pm$ 6.2 \\ + & \hfill 8 & \hfill 42/58\% & \hfill 29.1 $\pm$ 7.7 \\ + & \hfill 9 & \hfill 44/56\% & \hfill 31.3 $\pm$ 7.2 \\ + & \hfill 10 & \hfill 56/44\% & \hfill 39.2 $\pm$ 8.4 \\ + & \hfill 11 & \hfill 56/44\% & \hfill 38.5 $\pm$ 10.7 \\ + \hline + + \multirow{7}{1.5cm}{MCTS + Heavy Playouts} + & \hfill 5 & \hfill 50/50\% & \hfill 16.3 $\pm$ 1.7 \\ + & \hfill 6 & \hfill 70/30\% & \hfill 19.8 $\pm$ 4.7 \\ + & \hfill 7 & \hfill 80/20\% & \hfill 27.5 $\pm$ 2.6 \\ + & \hfill 8 & \hfill 40/60\% & \hfill 34.9 $\pm$ 5.1 \\ + & \hfill 9 & \hfill 20/80\% & \hfill 40.7 $\pm$ 4.0 \\ + & \hfill 10 & \hfill 30/70\% & \hfill 52.7 $\pm$ 6.4 \\ + & \hfill 11 & \hfill 80/20\% & \hfill 56.5 $\pm$ 3.8 \\ + \hline + + + \multirow{7}{1.5cm}{MCTS + RAVE} + & \hfill 5 & \hfill 36/64\% & \hfill 15.2 $\pm$ 3.2 \\ + & \hfill 6 & \hfill 52/48\% & \hfill 18.4 $\pm$ 4.1 \\ + & \hfill 7 & \hfill 44/56\% & \hfill 19.7 $\pm$ 4.3 \\ + & \hfill 8 & \hfill 50/50\% & \hfill 23.0 $\pm$ 5.2 \\ + & \hfill 9 & \hfill 48/52\% & \hfill 25.6 $\pm$ 5.9 \\ + & \hfill 10 & \hfill 38/62\% & \hfill 28.6 $\pm$ 7.0 \\ + & \hfill 11 & \hfill 50/50\% & \hfill 30.9 $\pm$ 8.5 \\ + \hline + + \end{tabular} +\end{table} + +\subsubsection{Wnioski końcowe} + +\textbf{Hipoteza 4 zostaje przyjęta}. Rozmiar planszy w grze Breakthrough w istotne znaczenie na balans rozgrywki, a na rozszerzonych planszach przemyślana strategia (reprezentowana przez ukierunkowane symulacje) pozwala na statystycznie istotne przejęcie kontroli nad partią przez gracza broniącego (Czarne). + +\newpage +\section{Testy z udziałem ludzi} + +\newpage +\section{Podsumowanie} + +%---------------------------------------------------------------------- +\newpage +\begin{thebibliography}{9} + +\bibitem{rust-install} +\emph{The Rust Programming Language -- Installation}, \url{https://doc.rust-lang.org/book/ch01-01-installation.html\#installation} (dostęp 28.01.2026r.). + +\bibitem{rustc-man} +\emph{What is rustc?}, \url{https://doc.rust-lang.org/stable/rustc/index.html\#what-is-rustc} (dostęp 28.01.2026r.). + +\bibitem{cargo-book} +\emph{The Cargo Book -- Installation}, \url{https://cargo-book.irust.net/en-us/getting-started/installation.html} (dostęp 28.01.2026r.) + +\end{thebibliography} + +\end{document} \ No newline at end of file diff --git a/docs/report.pdf b/docs/report.pdf new file mode 100644 index 0000000..e673ce7 Binary files /dev/null and b/docs/report.pdf differ diff --git a/src/analysis/.gitignore b/src/analysis/.gitignore index 6e23b8e..e4c0ba6 100644 --- a/src/analysis/.gitignore +++ b/src/analysis/.gitignore @@ -1,3 +1,4 @@ *.jsonl *.toml -*.png \ No newline at end of file +*.png +*.txt \ No newline at end of file diff --git a/src/analysis/experiments/h3/configs_generator.py b/src/analysis/experiments/h3/configs_generator.py new file mode 100644 index 0000000..1e98628 --- /dev/null +++ b/src/analysis/experiments/h3/configs_generator.py @@ -0,0 +1,60 @@ +import copy +import tomli_w # type: ignore +import os +import shutil +from pathlib import Path +from itertools import product + +BOARD_WIDTH = 6 +BOARD_HEIGHT = 7 +MCTS_ITERATIONS = 10000 +DEPTHS = [1, 2, 3, 4, 5, 6, 7] +COLORS = ["black", "white"] + +def generate_configs(output_dir: str, mcts_config_base: dict, name_prefix: str): + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + os.makedirs(output_dir, exist_ok=True) + + for depth, minimax_color in product(DEPTHS, COLORS): + minimax_config = {"type": "Minimax", "max_depth": depth} + mcts_config = copy.deepcopy(mcts_config_base) + + if minimax_color == "black": + config = { + "board_width": BOARD_WIDTH, + "board_height": BOARD_HEIGHT, + "white_player": mcts_config, + "black_player": minimax_config + } + else: + config = { + "board_width": BOARD_WIDTH, + "board_height": BOARD_HEIGHT, + "white_player": minimax_config, + "black_player": mcts_config + } + + filename = os.path.join(output_dir, f"{name_prefix}_{minimax_color}_depth_{depth}.toml") + with open(filename, "wb") as f: + tomli_w.dump(config, f) + +if __name__ == "__main__": + SCRIPT_DIR = Path(__file__).parent.resolve() + + mcts_base = {"type": "Mcts", "max_iterations": MCTS_ITERATIONS} + generate_configs(str(SCRIPT_DIR / "mcts"), mcts_base, "minimax") + + rave_base = { + "type": "Mcts", + "max_iterations": MCTS_ITERATIONS, + "use_rave": True, + } + generate_configs(str(SCRIPT_DIR / "rave"), rave_base, "minimax") + + heavy_base = { + "type": "Mcts", + "max_iterations": MCTS_ITERATIONS, + "use_heavy_playouts": True, + } + generate_configs(str(SCRIPT_DIR / "heavyplayouts"), heavy_base, "minimax") \ No newline at end of file diff --git a/src/analysis/experiments/h3/plots_generator.py b/src/analysis/experiments/h3/plots_generator.py new file mode 100644 index 0000000..39bece3 --- /dev/null +++ b/src/analysis/experiments/h3/plots_generator.py @@ -0,0 +1,226 @@ +import json +import argparse +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +from pathlib import Path + +def load_all_data(white_filepath: Path, black_filepath: Path): + game_records = [] + move_records = [] + + mcts_name = "Mcts" + minimax_name = "Minimax" + + with open(white_filepath, 'r') as fw, open(black_filepath, 'r') as fb: + for _, (line_white, line_black) in enumerate(zip(fw, fb), start=1): + if not line_white.strip() or not line_black.strip(): + continue + + data_white = json.loads(line_white) + data_black = json.loads(line_black) + + if data_white['agent_type'] == 'Minimax': + minimax_data, minimax_color = data_white, 'White' + mcts_data, mcts_color = data_black, 'Black' + else: + minimax_data, minimax_color = data_black, 'Black' + mcts_data, mcts_color = data_white, 'White' + + minimax_name = minimax_data['agent_type'] + mcts_name = mcts_data['agent_type'] + depth = minimax_data.get('max_depth') + + game_records.append({ + 'minimax_depth': depth, + 'mcts_color': mcts_color, + 'minimax_color': minimax_color, + 'mcts_won': int(mcts_data['agent_won']), + 'minimax_won': int(minimax_data['agent_won']), + 'total_moves': mcts_data['total_moves'] + }) + + for i, time_ms in enumerate(mcts_data['move_times_ms']): + move_records.append({ + 'Category': mcts_name, + 'Agent Move Number': i + 1, + 'Thinking Time (ms)': time_ms, + 'Sort_Key': 0 + }) + + for i, time_ms in enumerate(minimax_data['move_times_ms']): + move_records.append({ + 'Category': f'{minimax_name} (głębokość = {depth})', + 'Agent Move Number': i + 1, + 'Thinking Time (ms)': time_ms, + 'Sort_Key': depth + }) + + df_games = pd.DataFrame(game_records) + df_moves = pd.DataFrame(move_records) + + return df_games, df_moves, mcts_name, minimax_name + +def print_statistics(df: pd.DataFrame, mcts_name: str, minimax_name: str): + print(f"\n{'-'*75}") + print(f" TABLE 1: WIN RATE ({mcts_name} vs {minimax_name})") + print(f"{'-'*75}") + + overall_win_stats = df.groupby('minimax_depth')['mcts_won'].mean() * 100 + color_win_stats = df.groupby(['minimax_depth', 'mcts_color'])['mcts_won'].mean() * 100 + + col_w = f"{mcts_name} as White" + col_b = f"{mcts_name} as Black" + + print(f"{'Depth':<8} | {col_w:<20} | {col_b:<20} | {'Overall Average':<18}") + print("-" * 75) + + for depth in sorted(df['minimax_depth'].unique()): + win_white = color_win_stats.get((depth, 'White'), 0.0) + win_black = color_win_stats.get((depth, 'Black'), 0.0) + win_overall = overall_win_stats.get(depth, 0.0) + + print(f"d={depth:<6} | {win_white:>19.1f}% | {win_black:>19.1f}% | {win_overall:>17.1f}%") + + print(f"\n{'-'*80}") + print(f" TABLE 2: GAME LENGTH: Mean ± SD ({mcts_name} vs {minimax_name})") + print(f"{'-'*80}") + + overall_moves = df.groupby('minimax_depth')['total_moves'].agg(['mean', 'std']) + color_moves = df.groupby(['minimax_depth', 'mcts_color'])['total_moves'].agg(['mean', 'std']) + + print(f"{'Depth':<8} | {col_w:<22} | {col_b:<22} | {'Overall Average':<20}") + print("-" * 80) + + def format_moves(stats_df, key): + try: + row = stats_df.loc[key] + std_val = row['std'] + if pd.isna(std_val): + std_val = 0.0 + return f"{row['mean']:.1f} ± {std_val:.1f}" + except KeyError: + return "N/A" + + for depth in sorted(df['minimax_depth'].unique()): + moves_white = format_moves(color_moves, (depth, 'White')) + moves_black = format_moves(color_moves, (depth, 'Black')) + moves_overall = format_moves(overall_moves, depth) + + print(f"d={depth:<6} | {moves_white:>22} | {moves_black:>22} | {moves_overall:>19}") + +def generate_move_time_plots(df_moves: pd.DataFrame, mcts_name: str, minimax_name: str): + if df_moves.empty: + print("Error: No move data found.") + return + + sns.set_theme(style="whitegrid") + df_moves = df_moves.sort_values(by=['Sort_Key']) + + plt.figure(figsize=(12, 7)) + + depths = sorted([d for d in df_moves['Sort_Key'].unique() if d > 0]) + minimax_colors = sns.color_palette("Reds", n_colors=len(depths) + 2).as_hex()[2:] + + palette = {f'{mcts_name}': '#1f77b4'} + for depth, color in zip(depths, minimax_colors): + palette[f'{minimax_name} (głębokość = {depth})'] = color + + sns.lineplot( + data=df_moves, + x='Agent Move Number', + y='Thinking Time (ms)', + hue='Category', + palette=palette, + marker='o', + markersize=5, + linewidth=2, + errorbar=('ci', 95) + ) + + plt.xlabel('Numer ruchu', fontsize=12) + plt.ylabel('Czas namysłu (ms)', fontsize=12) + + plt.xticks(sorted(df_moves['Agent Move Number'].unique())) + plt.legend(title='Algorytm') + plt.tight_layout() + + time_filename = f"{mcts_name.lower()}_thinking_time.png" + plt.savefig(time_filename, dpi=300) + plt.close() + + print(f" -> {time_filename}") + +def generate_plots(df_games: pd.DataFrame, mcts_name: str): + sns.set_theme(style="whitegrid") + + mcts_stats = df_games.groupby(['minimax_depth', 'mcts_color'])['mcts_won'].mean().reset_index() + mcts_stats.rename(columns={'mcts_won': 'win_rate'}, inplace=True) + + mcts_stats['max_win'] = 1.0 + + plt.figure(figsize=(8, 6)) + + sns.barplot( + data=mcts_stats, x='minimax_depth', y='max_win', hue='mcts_color', + palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, alpha=0.3, + legend=False, hue_order=['White', 'Black'] + ) + + sns.barplot( + data=mcts_stats, x='minimax_depth', y='win_rate', hue='mcts_color', + palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, + hue_order=['White', 'Black'] + ) + + plt.xlabel('Głębokość przeszukiwania drzewa gry', fontsize=12) + plt.ylabel('Współczynnik zwycięstw', fontsize=12) + plt.ylim(0, 1.05) + plt.legend(title=f'Kolor {mcts_name}') + plt.tight_layout() + mcts_filename = f"{mcts_name.lower()}_win_rate.png" + plt.savefig(mcts_filename, dpi=300) + plt.close() + + plt.figure(figsize=(8, 6)) + + sns.boxplot( + data=df_games, x='minimax_depth', y='total_moves', hue='mcts_color', + palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, + linewidth=1.5, fliersize=4, hue_order=['White', 'Black'] + ) + + plt.xlabel('Głębokość przeszukiwania drzewa gry', fontsize=12) + plt.ylabel('Całkowita liczba ruchów', fontsize=12) + plt.legend(title=f'Kolor {mcts_name}') + plt.tight_layout() + + moves_filename = f"{mcts_name.lower()}_total_moves.png" + plt.savefig(moves_filename, dpi=300) + plt.close() + + print("Plots saved:") + print(f" -> {mcts_filename}") + print(f" -> {moves_filename}") + +def main(): + parser = argparse.ArgumentParser(description="Generate plots from game results.") + parser.add_argument("--white", type=Path, required=True, help="Path to the white player JSONL file") + parser.add_argument("--black", type=Path, required=True, help="Path to the black player JSONL file") + + args = parser.parse_args() + + if not args.white.exists() or not args.black.exists(): + print("Error: Input file(s) not found.") + return + + df_games, df_moves, mcts_name, minimax_name = load_all_data(args.white, args.black) + + print(f"Processed {len(df_games)} games.") + + print_statistics(df_games, mcts_name, minimax_name) + generate_plots(df_games, mcts_name) + generate_move_time_plots(df_moves, mcts_name, minimax_name) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/analysis/experiments/h4/configs_generator.py b/src/analysis/experiments/h4/configs_generator.py index f375eb3..3ff534f 100644 --- a/src/analysis/experiments/h4/configs_generator.py +++ b/src/analysis/experiments/h4/configs_generator.py @@ -3,45 +3,49 @@ import os import shutil from pathlib import Path -from itertools import product -CONFIG_MCTS_VS_MINIMAX_BLACK = { - "board_width": 6, - "board_height": 7, - "white_player": {"type": "Mcts", "max_iterations": 10000}, - "black_player": {"type": "Minimax"} # max_depth added in a loop -} +BOARD_WIDTH = 8 +HEIGHTS = [5, 6, 7, 8, 9, 10, 11] +MAX_ITERATIONS_SCALE_FACTOR = 2000 -CONFIG_MCTS_VS_MINIMAX_WHITE = { - "board_width": 6, - "board_height": 7, - "white_player": {"type": "Minimax"}, # max_depth added in a loop - "black_player": {"type": "Mcts", "max_iterations": 10000} -} - -def generate_mcts_vs_minimax_configs(output_dir: str): +def generate_board_size_configs(output_dir: str, agent_config_base: dict, name_prefix: str): if os.path.exists(output_dir): shutil.rmtree(output_dir) os.makedirs(output_dir, exist_ok=True) - depths = [1, 2, 3, 4, 5, 6, 7] - colors = ["black", "white"] - - for depth, minimax_color in product(depths, colors): + for height in HEIGHTS: + iterations = height * MAX_ITERATIONS_SCALE_FACTOR - if minimax_color == "black": - config = copy.deepcopy(CONFIG_MCTS_VS_MINIMAX_BLACK) - config["black_player"]["max_depth"] = depth - else: - config = copy.deepcopy(CONFIG_MCTS_VS_MINIMAX_WHITE) - config["white_player"]["max_depth"] = depth + agent_config = copy.deepcopy(agent_config_base) + if agent_config["type"] == "Mcts": + agent_config["max_iterations"] = iterations + + config = { + "board_width": BOARD_WIDTH, + "board_height": height, + "white_player": agent_config, + "black_player": agent_config + } - filename = os.path.join(output_dir, f"minimax_{minimax_color}_depth_{depth}.toml") + filename = os.path.join(output_dir, f"{name_prefix}_8x{height}.toml") with open(filename, "wb") as f: tomli_w.dump(config, f) - + if __name__ == "__main__": SCRIPT_DIR = Path(__file__).parent.resolve() - CONFIG_DIR = SCRIPT_DIR / "mcts" - generate_mcts_vs_minimax_configs(str(CONFIG_DIR)) \ No newline at end of file + mcts_base = {"type": "Mcts"} + generate_board_size_configs(str(SCRIPT_DIR / "mcts"), mcts_base, "mcts") + + rave_base = { + "type": "Mcts", + "use_rave": True, + } + generate_board_size_configs(str(SCRIPT_DIR / "rave"), rave_base, "rave") + + heavy_base = { + "type": "Mcts", + "use_heavy_playouts": True, + "heavy_playouts_epsilon": 0.1 + } + generate_board_size_configs(str(SCRIPT_DIR / "heavyplayouts"), heavy_base, "heavyplayouts") diff --git a/src/analysis/experiments/h4/plots_generator.py b/src/analysis/experiments/h4/plots_generator.py index 3686cdb..0e89d35 100644 --- a/src/analysis/experiments/h4/plots_generator.py +++ b/src/analysis/experiments/h4/plots_generator.py @@ -5,222 +5,150 @@ import seaborn as sns from pathlib import Path -def load_all_data(white_filepath: Path, black_filepath: Path): +def load_data(white_file: Path, black_file: Path): + """ + Loads data from specified white and black result files. + """ game_records = [] - move_records = [] - mcts_name = None - minimax_name = None + print(f"Loading data from:\n White: {white_file}\n Black: {black_file}") - with open(white_filepath, 'r') as fw, open(black_filepath, 'r') as fb: - for line_num, (line_white, line_black) in enumerate(zip(fw, fb), start=1): - if not line_white.strip() or not line_black.strip(): + with open(white_file, 'r') as fw, open(black_file, 'r') as fb: + for lw, lb in zip(fw, fb): + if not lw.strip() or not lb.strip(): continue - - data_white = json.loads(line_white) - data_black = json.loads(line_black) + dw = json.loads(lw) - if data_white['agent_type'] == 'Minimax': - minimax_data, minimax_color = data_white, 'White' - mcts_data, mcts_color = data_black, 'Black' - else: - minimax_data, minimax_color = data_black, 'Black' - mcts_data, mcts_color = data_white, 'White' - - minimax_name = minimax_data['agent_type'] - mcts_name = mcts_data['agent_type'] - depth = minimax_data.get('max_depth') + height = dw.get('board_height') + agent_name = dw.get('agent_type', 'Mcts') + + category = agent_name + if dw.get('use_rave'): + category = "Rave" + elif dw.get('use_heavy_playouts'): + category = "HeavyPlayouts" + + if height is None: + continue + + white_won = int(dw['agent_won']) game_records.append({ - 'minimax_depth': depth, - 'mcts_color': mcts_color, - 'minimax_color': minimax_color, - 'mcts_won': int(mcts_data['agent_won']), - 'minimax_won': int(minimax_data['agent_won']), - 'total_moves': mcts_data['total_moves'] + 'height': height, + 'category': category, + 'white_won': white_won, + 'black_won': 1 - white_won, + 'total_moves': dw['total_moves'] }) - - for i, time_ms in enumerate(mcts_data['move_times_ms']): - move_records.append({ - 'Category': mcts_name, - 'Agent Move Number': i + 1, - 'Thinking Time (ms)': time_ms, - 'Sort_Key': 0 - }) - for i, time_ms in enumerate(minimax_data['move_times_ms']): - move_records.append({ - 'Category': f'{minimax_name} (głębokość = {depth})', - 'Agent Move Number': i + 1, - 'Thinking Time (ms)': time_ms, - 'Sort_Key': depth - }) - - df_games = pd.DataFrame(game_records) - df_moves = pd.DataFrame(move_records) - - return df_games, df_moves, mcts_name, minimax_name + return pd.DataFrame(game_records) -def print_statistics(df: pd.DataFrame, mcts_name: str, minimax_name: str): - print(f"\n{'-'*75}") - print(f" TABLE 1: WIN RATE ({mcts_name} vs {minimax_name})") - print(f"{'-'*75}") - - overall_win_stats = df.groupby('minimax_depth')['mcts_won'].mean() * 100 - color_win_stats = df.groupby(['minimax_depth', 'mcts_color'])['mcts_won'].mean() * 100 - - col_w = f"{mcts_name} as White" - col_b = f"{mcts_name} as Black" - - print(f"{'Depth':<8} | {col_w:<20} | {col_b:<20} | {'Overall Average':<18}") - print("-" * 75) - - for depth in sorted(df['minimax_depth'].unique()): - win_white = color_win_stats.get((depth, 'White'), 0.0) - win_black = color_win_stats.get((depth, 'Black'), 0.0) - win_overall = overall_win_stats.get(depth, 0.0) - - print(f"d={depth:<6} | {win_white:>19.1f}% | {win_black:>19.1f}% | {win_overall:>17.1f}%") +def print_statistics(df: pd.DataFrame): + print(f"\n{'-'*95}") + print(" TABLE: BOARD SIZE IMPACT (Width = 8)") + print(f"{'-'*95}") + + stats = df.groupby(['category', 'height']).agg( + white_wr=('white_won', 'mean'), + black_wr=('black_won', 'mean'), + moves_mean=('total_moves', 'mean'), + moves_std=('total_moves', 'std') + ).reset_index() + + print(f"{'Category':<10} | {'Height':<8} | {'White Win Rate':<16} | {'Black Win Rate':<16} | {'Game Length (Mean ± SD)':<22}") + print("-" * 95) + + for _, row in stats.sort_values(['category', 'height']).iterrows(): + w_wr = f"{row['white_wr']*100:.1f}%" + b_wr = f"{row['black_wr']*100:.1f}%" + moves = f"{row['moves_mean']:.1f} ± {row['moves_std']:.1f}" + print(f"{row['category']:<10} | {int(row['height']):<8} | {w_wr:<16} | {b_wr:<16} | {moves:<22}") - print(f"\n{'-'*80}") - print(f" TABLE 2: GAME LENGTH: Mean ± SD ({mcts_name} vs {minimax_name})") - print(f"{'-'*80}") - - overall_moves = df.groupby('minimax_depth')['total_moves'].agg(['mean', 'std']) - color_moves = df.groupby(['minimax_depth', 'mcts_color'])['total_moves'].agg(['mean', 'std']) - - print(f"{'Depth':<8} | {col_w:<22} | {col_b:<22} | {'Overall Average':<20}") - print("-" * 80) - - def format_moves(stats_df, key): - try: - row = stats_df.loc[key] - std_val = row['std'] - if pd.isna(std_val): - std_val = 0.0 - return f"{row['mean']:.1f} ± {std_val:.1f}" - except KeyError: - return "N/A" - - for depth in sorted(df['minimax_depth'].unique()): - moves_white = format_moves(color_moves, (depth, 'White')) - moves_black = format_moves(color_moves, (depth, 'Black')) - moves_overall = format_moves(overall_moves, depth) - - print(f"d={depth:<6} | {moves_white:>22} | {moves_black:>22} | {moves_overall:>19}") - -def generate_move_time_plots(df_moves: pd.DataFrame, mcts_name: str, minimax_name: str): - if df_moves.empty: - print("Error: No move data found.") +def generate_win_rate_plot(df: pd.DataFrame, output_dir: Path): + if df.empty: + print("No data to plot.") return sns.set_theme(style="whitegrid") - df_moves = df_moves.sort_values(by=['Sort_Key']) - - plt.figure(figsize=(12, 7)) - - depths = sorted([d for d in df_moves['Sort_Key'].unique() if d > 0]) - minimax_colors = sns.color_palette("Reds", n_colors=len(depths) + 2).as_hex()[2:] - - palette = {f'{mcts_name}': '#1f77b4'} - for depth, color in zip(depths, minimax_colors): - palette[f'{minimax_name} (głębokość = {depth})'] = color - - sns.lineplot( - data=df_moves, - x='Agent Move Number', - y='Thinking Time (ms)', - hue='Category', - palette=palette, - marker='o', - markersize=5, - linewidth=2, - errorbar=('ci', 95) - ) - - plt.xlabel('Numer ruchu', fontsize=12) - plt.ylabel('Czas namysłu (ms)', fontsize=12) - - plt.xticks(sorted(df_moves['Agent Move Number'].unique())) - plt.legend(title='Algorytm') - plt.tight_layout() - - time_filename = f"{mcts_name.lower()}_thinking_time.png" - plt.savefig(time_filename, dpi=300) - plt.close() - - print(f" -> {time_filename}") - -def generate_plots(df_games: pd.DataFrame, mcts_name: str): - sns.set_theme(style="whitegrid") - - mcts_stats = df_games.groupby(['minimax_depth', 'mcts_color'])['mcts_won'].mean().reset_index() - mcts_stats.rename(columns={'mcts_won': 'win_rate'}, inplace=True) - - mcts_stats['max_win'] = 1.0 - - plt.figure(figsize=(8, 6)) - - sns.barplot( - data=mcts_stats, x='minimax_depth', y='max_win', hue='mcts_color', - palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, alpha=0.3, - legend=False, hue_order=['White', 'Black'] - ) + categories = sorted(df['category'].unique()) + palette = {'White': '#d62728', 'Black': '#1f77b4'} - sns.barplot( - data=mcts_stats, x='minimax_depth', y='win_rate', hue='mcts_color', - palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, - hue_order=['White', 'Black'] - ) - - plt.xlabel('Głębokość przeszukiwania drzewa gry', fontsize=12) - plt.ylabel('Współczynnik zwycięstw', fontsize=12) - plt.ylim(0, 1.05) - plt.legend(title=f'Kolor {mcts_name}') - plt.tight_layout() - mcts_filename = f"{mcts_name.lower()}_win_rate.png" - plt.savefig(mcts_filename, dpi=300) - plt.close() - - plt.figure(figsize=(8, 6)) - - sns.boxplot( - data=df_games, x='minimax_depth', y='total_moves', hue='mcts_color', - palette={'White': '#d62728', 'Black': '#1f77b4'}, dodge=True, - linewidth=1.5, fliersize=4, hue_order=['White', 'Black'] - ) - - plt.xlabel('Głębokość przeszukiwania drzewa gry', fontsize=12) - plt.ylabel('Całkowita liczba ruchów', fontsize=12) - plt.legend(title=f'Kolor {mcts_name}') - plt.tight_layout() - - moves_filename = f"{mcts_name.lower()}_total_moves.png" - plt.savefig(moves_filename, dpi=300) - plt.close() + for cat in categories: + cat_df = df[df['category'] == cat] + + plt.figure(figsize=(10, 6)) + ax = plt.gca() + + plot_data = [] + for height in sorted(cat_df['height'].unique()): + h_df = cat_df[cat_df['height'] == height] + w_rate = h_df['white_won'].mean() + b_rate = 1.0 - w_rate + + plot_data.append({'height': height, 'color': 'White', 'win_rate': w_rate, 'max_win': 1.0}) + plot_data.append({'height': height, 'color': 'Black', 'win_rate': b_rate, 'max_win': 1.0}) + + pdf = pd.DataFrame(plot_data) + + sns.barplot( + data=pdf, x='height', y='max_win', hue='color', + palette=palette, dodge=True, alpha=0.3, ax=ax, legend=False, + hue_order=['White', 'Black'] + ) + sns.barplot( + data=pdf, x='height', y='win_rate', hue='color', + palette=palette, dodge=True, ax=ax, + hue_order=['White', 'Black'] + ) + + white_stats = pdf[pdf['color'] == 'White'].sort_values('height') + import numpy as np + z = np.polyfit(white_stats['height'], white_stats['win_rate'], 2) + p = np.poly1d(z) + x_new = np.linspace(white_stats['height'].min(), white_stats['height'].max(), 100) + ax.plot(x_new - white_stats['height'].min(), p(x_new), color='#d62728', linestyle='-', linewidth=2, label='Trend (White)', alpha=0.8) - print("Plots saved:") - print(f" -> {mcts_filename}") - print(f" -> {moves_filename}") + ax.set_ylabel('Współczynnik zwycięstw', fontsize=12) + ax.set_ylim(0, 1.05) + + ax.axhline(0.5, ls='--', color='black', alpha=0.3, label='Fair play (0.5)') + + handles, labels = ax.get_legend_handles_labels() + ax.legend(handles=handles, labels=labels, title=cat, loc='upper right') + + ax.set_xlabel('Wysokość planszy', fontsize=12) + + plt.tight_layout() + + filename = f"{cat.lower()}_board_size_win_rate.png" + final_path = output_dir / filename + plt.savefig(final_path, dpi=300) + plt.close() + print(f"Plot saved to: {final_path}") def main(): - parser = argparse.ArgumentParser(description="Generate plots from game results.") + parser = argparse.ArgumentParser(description="Generate win rate plots for board size experiments.") parser.add_argument("--white", type=Path, required=True, help="Path to the white player JSONL file") parser.add_argument("--black", type=Path, required=True, help="Path to the black player JSONL file") + parser.add_argument("-o", "--output-dir", type=Path, default=Path("."), help="Output directory for the plot") args = parser.parse_args() + args.output_dir.mkdir(parents=True, exist_ok=True) + if not args.white.exists() or not args.black.exists(): print("Error: Input file(s) not found.") return - df_games, df_moves, mcts_name, minimax_name = load_all_data(args.white, args.black) + df = load_data(args.white, args.black) - print(f"Processed {len(df_games)} games.") - - print_statistics(df_games, mcts_name, minimax_name) - generate_plots(df_games, mcts_name) - generate_move_time_plots(df_moves, mcts_name, minimax_name) + if df.empty: + print("Error: No valid data found in the provided files.") + return + + print(f"Successfully loaded {len(df)} games.") + print_statistics(df) + generate_win_rate_plot(df, args.output_dir) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index e85a7aa..10e5084 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -3,6 +3,7 @@ mod metrics; mod minimax; mod stats; +use crate::core::{Board, BoardConfig, Ply}; pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator, PlayoutStrategy, SelectionStrategy}; pub use metrics::*; pub use minimax::{ @@ -10,61 +11,9 @@ pub use minimax::{ }; pub use stats::{AgentRuntimeStats, AgentStatsAccumulator}; -use serde::{Deserialize, Serialize}; - -use crate::core::{Board, BoardConfig, Ply}; - -pub const DEFAULT_MINIMAX_MAX_DEPTH: u8 = 4; -pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 200; -pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 5; -pub const DEFAULT_HEURISTIC_DEFENDED_WEIGHT: i32 = 5; -pub const DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT: i32 = -2; -pub const DEFAULT_MCTS_MAX_ITERATIONS: u32 = 75000; -pub const DEFAULT_MCTS_MAX_TIME_MS: Option = None; -pub const DEFAULT_MCTS_EXPLORATION_CONSTANT: f64 = 1.41; -pub const DEFAULT_MCTS_USE_HEAVY_PLAYOUTS: bool = false; -pub const DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON: f64 = 0.1; -pub const DEFAULT_MCTS_USE_RAVE: bool = false; -pub const DEFAULT_MCTS_RAVE_K: f64 = 1000.0; - -fn default_max_depth() -> u8 { - DEFAULT_MINIMAX_MAX_DEPTH -} +use super::defaults::*; -fn default_material() -> i32 { - DEFAULT_HEURISTIC_MATERIAL_WEIGHT -} -fn default_advancement() -> i32 { - DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT -} -fn default_defended() -> i32 { - DEFAULT_HEURISTIC_DEFENDED_WEIGHT -} -fn default_edge_penalty() -> i32 { - DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT -} - -fn default_max_iterations() -> u32 { - DEFAULT_MCTS_MAX_ITERATIONS -} -fn default_max_time_ms() -> Option { - DEFAULT_MCTS_MAX_TIME_MS -} -fn default_exploration_constant() -> f64 { - DEFAULT_MCTS_EXPLORATION_CONSTANT -} -fn default_use_heavy_playouts() -> bool { - DEFAULT_MCTS_USE_HEAVY_PLAYOUTS -} -fn default_heavy_playouts_epsilon() -> f64 { - DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON -} -fn default_use_rave() -> bool { - DEFAULT_MCTS_USE_RAVE -} -fn default_rave_k() -> f64 { - DEFAULT_MCTS_RAVE_K -} +use serde::{Deserialize, Serialize}; pub fn build_mcts_selection_strategy(use_rave: bool, rave_k: f64) -> SelectionStrategy { if use_rave { diff --git a/src/compute/src/defaults.rs b/src/compute/src/defaults.rs new file mode 100644 index 0000000..faa5047 --- /dev/null +++ b/src/compute/src/defaults.rs @@ -0,0 +1,60 @@ +pub const DEFAULT_MINIMAX_MAX_DEPTH: u8 = 4; +pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 200; +pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 5; +pub const DEFAULT_HEURISTIC_DEFENDED_WEIGHT: i32 = 5; +pub const DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT: i32 = -2; +pub const DEFAULT_MCTS_MAX_ITERATIONS: u32 = 75000; +pub const DEFAULT_MCTS_MAX_TIME_MS: Option = None; +pub const DEFAULT_MCTS_EXPLORATION_CONSTANT: f64 = 1.41; +pub const DEFAULT_MCTS_USE_HEAVY_PLAYOUTS: bool = false; +pub const DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON: f64 = 0.1; +pub const DEFAULT_MCTS_USE_RAVE: bool = false; +pub const DEFAULT_MCTS_RAVE_K: f64 = 1000.0; + +pub fn default_max_depth() -> u8 { + DEFAULT_MINIMAX_MAX_DEPTH +} + +pub fn default_material() -> i32 { + DEFAULT_HEURISTIC_MATERIAL_WEIGHT +} + +pub fn default_advancement() -> i32 { + DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT +} + +pub fn default_defended() -> i32 { + DEFAULT_HEURISTIC_DEFENDED_WEIGHT +} + +pub fn default_edge_penalty() -> i32 { + DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT +} + +pub fn default_max_iterations() -> u32 { + DEFAULT_MCTS_MAX_ITERATIONS +} + +pub fn default_max_time_ms() -> Option { + DEFAULT_MCTS_MAX_TIME_MS +} + +pub fn default_exploration_constant() -> f64 { + DEFAULT_MCTS_EXPLORATION_CONSTANT +} + +pub fn default_use_heavy_playouts() -> bool { + DEFAULT_MCTS_USE_HEAVY_PLAYOUTS +} + +pub fn default_heavy_playouts_epsilon() -> f64 { + DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON +} + +pub fn default_use_rave() -> bool { + DEFAULT_MCTS_USE_RAVE +} + +pub fn default_rave_k() -> f64 { + DEFAULT_MCTS_RAVE_K +} diff --git a/src/compute/src/gui/views/gameplay.rs b/src/compute/src/gui/views/gameplay.rs index d19cd1d..0f119ec 100644 --- a/src/compute/src/gui/views/gameplay.rs +++ b/src/compute/src/gui/views/gameplay.rs @@ -11,6 +11,7 @@ use crate::{ append_record_to_jsonl, }, core::{Board, BoardConfig, Player, Status}, + defaults, gui::themes::BoardTheme, }; @@ -224,18 +225,16 @@ impl GameplayView { { *max_time_ms = Some(*max_time); } - } else { - if ui - .add( - egui::Slider::new(&mut agent.max_iterations, 1000..=100000) - .text("iters") - .logarithmic(true), - ) - .changed() - && let AgentConfig::Mcts { max_iterations, .. } = config - { - *max_iterations = agent.max_iterations; - } + } else if ui + .add( + egui::Slider::new(&mut agent.max_iterations, 1000..=100000) + .text("iters") + .logarithmic(true), + ) + .changed() + && let AgentConfig::Mcts { max_iterations, .. } = config + { + *max_iterations = agent.max_iterations; } if ui @@ -259,7 +258,7 @@ impl GameplayView { if let AgentConfig::Mcts { rave_k, .. } = config { *rave_k } else { - agents::DEFAULT_MCTS_RAVE_K + defaults::DEFAULT_MCTS_RAVE_K } } }; @@ -313,11 +312,11 @@ impl GameplayView { ) } else { ( - agents::DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, - agents::DEFAULT_HEURISTIC_MATERIAL_WEIGHT, - agents::DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, - agents::DEFAULT_HEURISTIC_DEFENDED_WEIGHT, - agents::DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, + defaults::DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, + defaults::DEFAULT_HEURISTIC_MATERIAL_WEIGHT, + defaults::DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, + defaults::DEFAULT_HEURISTIC_DEFENDED_WEIGHT, + defaults::DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, ) }; diff --git a/src/compute/src/lib.rs b/src/compute/src/lib.rs index b59232f..86db4e2 100644 --- a/src/compute/src/lib.rs +++ b/src/compute/src/lib.rs @@ -3,6 +3,7 @@ mod error; pub mod agents; pub mod cli; pub mod core; +pub mod defaults; pub mod gui; pub use error::{ComputeError, ComputeResult}; diff --git a/src/compute/src/resources/configs/with_human/human_vs_human.toml b/src/compute/src/resources/configs/with_human/human_vs_human.toml index 428e61d..f9dec37 100644 --- a/src/compute/src/resources/configs/with_human/human_vs_human.toml +++ b/src/compute/src/resources/configs/with_human/human_vs_human.toml @@ -1,5 +1,5 @@ -board_width = 11 -board_height = 11 +board_width = 8 +board_height = 8 seed = 42 [white_player]