aboutsummaryrefslogtreecommitdiff
path: root/src/claude-player.rs
blob: c25c5e1aaae999d3c76215bed5d2a84bb25e03ff (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use std::time::Duration;

use crate::{board::{is_jump, GameBoard, Move, PieceType, Turn}, player::{Killers, MinimaxPlayer, SearchReport, TranspositionTable}};

/// Sheep that start the game: on the board plus in hand.
const TOTAL_SHEEP: u32 = 20;

/// A computer player that searches with minimax for `time_limit` per move and scores positions with `evaluate`.
pub struct ClaudePlayer
{
	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 Default for ClaudePlayer
{
	fn default() -> Self
	{
		Self::new(Duration::from_secs(1))
	}
}

impl ClaudePlayer
{
	pub fn new(time_limit: Duration) -> Self
	{
		Self { time_limit, max_depth: 64, cache: TranspositionTable::new(20), killers: Killers::default(), last_search: None }
	}
}

impl MinimaxPlayer for ClaudePlayer
{
	/// Scored from the tigers' side. In order of weight:
	/// - dead sheep, since five of them wins the game for the tigers;
	/// - jumps the tigers could make right now (sheep left hanging);
	/// - tigers boxed in with no move at all, since trapping every tiger wins for the sheep;
	/// - tiger mobility in general, since the sheep win by squeezing it to zero.
	fn evaluate(&mut self, board: &GameBoard, _turn: Turn) -> f32
	{
		let dead = TOTAL_SHEEP - board.sheep_count() - board.sheep_left();

		let tiger_moves = board.get_moves(Turn::Tiger);
		let jumps = tiger_moves.iter().filter(|m| is_jump(**m)).count();

		let mut trapped = 0;
		for y in 0..5
		{
			for x in 0..5
			{
				if matches!(board.get((x, y)), PieceType::Tiger)
					&& !tiger_moves.iter().any(|m| matches!(m, Move::MoveTiger { from, .. } if *from == (x, y)))
				{
					trapped += 1;
				}
			}
		}

		100.0 * dead as f32
			+ 20.0 * jumps as f32
			- 15.0 * trapped as f32
			+ 2.0 * tiger_moves.len() as f32
	}

	fn name(&self) -> String { "Claude".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 crate::player::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 = ClaudePlayer::new(Duration::from_millis(50));
		let m = player.pick_move(GameBoard::default(), Turn::Tiger).unwrap();
		let report = player.last_search.expect("a report");
		assert_eq!(report.mv, m);
		assert!(report.depth >= 1);
		assert_eq!(report.score, player.minimax(GameBoard::default(), Turn::Tiger, report.depth,
			f32::NEG_INFINITY, f32::INFINITY, None).unwrap());

		player.reset();
		assert_eq!(player.last_search, None);
	}
}