diff options
| author | Dylan <boss@tehbox.org> | 2026-11-05 13:46:28 +1300 |
|---|---|---|
| committer | Dylan <boss@tehbox.org> | 2026-11-05 13:46:28 +1300 |
| commit | 94f3df190a91898eb344b9214a57eb31792bcb1c (patch) | |
| tree | db123ed498e84453d89da4df99253e56e6ded259 /src/neural-player.rs | |
| parent | 8b6323efba32da9e3651ce882c02a6e7a6671f23 (diff) | |
| download | tigers-and-sheep-94f3df190a91898eb344b9214a57eb31792bcb1c.tar.gz tigers-and-sheep-94f3df190a91898eb344b9214a57eb31792bcb1c.zip | |
feat: Neural network opponent trained by self-play
- NeuralPlayer: a MinimaxPlayer that scores positions with a small MLP
(network.rs, hand-written backprop and Adam, no new dependencies)
- train subcommand: self-play with TD(lambda) targets, cut at random
exploration moves so sheep blunders don't bias the net towards tigers
- neural subcommand: play against a trained network
- evaluate now gets the side to move
- Board symmetries; the search cache keys by the canonical board, which
halves the depth-9 bench time with identical moves and scores
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/neural-player.rs')
| -rw-r--r-- | src/neural-player.rs | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/src/neural-player.rs b/src/neural-player.rs new file mode 100644 index 0000000..3a9c4a0 --- /dev/null +++ b/src/neural-player.rs @@ -0,0 +1,68 @@ +use std::time::Duration; + +use crate::{board::{GameBoard, Turn}, network::Network, player::{Killers, MinimaxPlayer, ScoreCache, SearchReport, TranspositionTable}}; + +/// A computer player that searches like `ClaudePlayer` but scores positions with a neural network +/// trained by self-play (see `train.rs`), so it has no human idea of what a good position is. +pub struct NeuralPlayer +{ + pub net: Network, + pub time_limit: Duration, + /// The most plies to search, counting the move itself; normally `time_limit` stops the search first. + pub max_depth: usize, + cache: TranspositionTable, + killers: Killers, + last_search: Option<SearchReport>, +} + +impl NeuralPlayer +{ + /// A player with a `1 << table_bits`-slot transposition table. + pub fn new(net: Network, time_limit: Duration, table_bits: u32) -> Self + { + Self { net, time_limit, max_depth: 64, cache: TranspositionTable::new(table_bits), killers: Killers::default(), last_search: None } + } +} + +impl MinimaxPlayer for NeuralPlayer +{ + /// The network's output, in -1..1; forced wins are still ±`WIN` from the search itself. + fn evaluate(&mut self, board: &GameBoard, turn: Turn) -> f32 + { + self.net.forward(board, turn) + } + + fn name(&self) -> String { "Neural".to_string() } + + fn set_last_search(&mut self, report: Option<SearchReport>) { self.last_search = report; } + + fn last_search(&self) -> Option<SearchReport> { self.last_search } + + fn time_limit(&self) -> Duration { self.time_limit } + + fn depth(&self) -> usize { self.max_depth } + + fn cache(&mut self) -> Option<&mut dyn ScoreCache> { Some(&mut self.cache) } + + fn killers(&mut self) -> Option<&mut Killers> { Some(&mut self.killers) } +} + +#[cfg(test)] +mod tests +{ + use super::*; + use crate::player::Player; + + #[test] + fn pick_move_reports_its_search() + { + let mut player = NeuralPlayer::new(Network::random(2), Duration::from_secs(3600), 12); + player.max_depth = 3; + let board = GameBoard::default(); + let m = player.pick_move(board, Turn::Sheep).unwrap(); + assert_eq!(board.check_move(m), Ok(())); + let report = player.last_search.expect("a report"); + assert_eq!((report.mv, report.depth), (m, 3)); + assert_eq!(report.score, player.minimax(board, Turn::Sheep, 3, f32::NEG_INFINITY, f32::INFINITY, None).unwrap()); + } +} |
