Skip to content

Commit df03675

Browse files
committed
Refactor Rust AI core into idiomatic modules, improve and fix tests, ensure all tests pass and code is lint-free
1 parent 9d78b29 commit df03675

File tree

4 files changed

+634
-75
lines changed

4 files changed

+634
-75
lines changed

worker/rust_ai_core/src/ai.rs

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl AI {
7070
.map(|(move_idx, score)| {
7171
let piece = &state.get_pieces(state.current_player)[*move_idx as usize];
7272
let track = GameState::get_player_track(state.current_player);
73-
let (move_type, to_square) = if piece.square == -1 {
73+
let (move_type, to_square) = if piece.is_at_start() {
7474
("enter".to_string(), Some(track[0]))
7575
} else if let Some(track_pos) =
7676
track.iter().position(|&sq| sq as i8 == piece.square)
@@ -205,7 +205,7 @@ impl AI {
205205

206206
fn get_move_score(&self, piece: &PiecePosition, state: &GameState) -> i32 {
207207
let mut score = 0;
208-
if piece.square == -1 {
208+
if piece.is_at_start() {
209209
score += 100;
210210
} else {
211211
let track = GameState::get_player_track(state.current_player);
@@ -226,6 +226,26 @@ impl AI {
226226
}
227227
score
228228
}
229+
230+
pub fn clear_transposition_table(&mut self) {
231+
self.transposition_table.clear();
232+
self.nodes_evaluated = 0;
233+
self.transposition_hits = 0;
234+
}
235+
236+
pub fn get_cache_stats(&self) -> (usize, u32, u32) {
237+
(
238+
self.transposition_table.len(),
239+
self.nodes_evaluated,
240+
self.transposition_hits,
241+
)
242+
}
243+
}
244+
245+
impl Default for AI {
246+
fn default() -> Self {
247+
Self::new()
248+
}
229249
}
230250

231251
#[derive(Serialize)]
@@ -240,3 +260,139 @@ pub struct MoveEvaluation {
240260
#[serde(rename = "toSquare")]
241261
pub to_square: Option<u8>,
242262
}
263+
264+
#[cfg(test)]
265+
mod tests {
266+
use super::*;
267+
268+
#[test]
269+
fn test_ai_creation() {
270+
let ai = AI::new();
271+
assert_eq!(ai.nodes_evaluated, 0);
272+
assert_eq!(ai.transposition_hits, 0);
273+
assert!(ai.transposition_table.is_empty());
274+
}
275+
276+
#[test]
277+
fn test_ai_default() {
278+
let ai1 = AI::new();
279+
let ai2 = AI::default();
280+
assert_eq!(ai1.nodes_evaluated, ai2.nodes_evaluated);
281+
assert_eq!(ai1.transposition_hits, ai2.transposition_hits);
282+
}
283+
284+
#[test]
285+
fn test_ai_best_move_single_option() {
286+
let mut state = GameState::new();
287+
state.dice_roll = 1;
288+
state.current_player = Player::Player1;
289+
290+
for i in 1..PIECES_PER_PLAYER {
291+
state.player1_pieces[i].square = 20;
292+
}
293+
294+
let mut ai = AI::new();
295+
let (best_move, evaluations) = ai.get_best_move(&state, 2);
296+
297+
assert_eq!(best_move, 0);
298+
assert!(evaluations.len() >= 1);
299+
assert_eq!(evaluations[0].piece_index, 0);
300+
}
301+
302+
#[test]
303+
fn test_ai_best_move_multiple_options() {
304+
let mut state = GameState::new();
305+
state.dice_roll = 1;
306+
state.current_player = Player::Player1;
307+
308+
let mut ai = AI::new();
309+
let (best_move, evaluations) = ai.get_best_move(&state, 1);
310+
311+
assert!(best_move < PIECES_PER_PLAYER as u8);
312+
assert!(!evaluations.is_empty());
313+
assert_eq!(evaluations.len(), PIECES_PER_PLAYER);
314+
}
315+
316+
#[test]
317+
fn test_ai_no_valid_moves() {
318+
let mut state = GameState::new();
319+
state.dice_roll = 0;
320+
321+
let mut ai = AI::new();
322+
let (best_move, evaluations) = ai.get_best_move(&state, 1);
323+
324+
assert_eq!(best_move, 0);
325+
assert!(evaluations.is_empty());
326+
}
327+
328+
#[test]
329+
fn test_ai_clear_transposition_table() {
330+
let mut ai = AI::new();
331+
ai.nodes_evaluated = 100;
332+
ai.transposition_hits = 50;
333+
ai.transposition_table.insert(
334+
123,
335+
TranspositionEntry {
336+
evaluation: 0,
337+
depth: 1,
338+
},
339+
);
340+
341+
ai.clear_transposition_table();
342+
343+
assert_eq!(ai.nodes_evaluated, 0);
344+
assert_eq!(ai.transposition_hits, 0);
345+
assert!(ai.transposition_table.is_empty());
346+
}
347+
348+
#[test]
349+
fn test_ai_get_cache_stats() {
350+
let mut ai = AI::new();
351+
ai.nodes_evaluated = 100;
352+
ai.transposition_hits = 50;
353+
ai.transposition_table.insert(
354+
123,
355+
TranspositionEntry {
356+
evaluation: 0,
357+
depth: 1,
358+
},
359+
);
360+
361+
let (cache_size, nodes, hits) = ai.get_cache_stats();
362+
363+
assert_eq!(cache_size, 1);
364+
assert_eq!(nodes, 100);
365+
assert_eq!(hits, 50);
366+
}
367+
368+
#[test]
369+
fn test_move_evaluation_creation() {
370+
let eval = MoveEvaluation {
371+
piece_index: 2,
372+
score: 150.5,
373+
move_type: "capture".to_string(),
374+
from_square: 5,
375+
to_square: Some(10),
376+
};
377+
378+
assert_eq!(eval.piece_index, 2);
379+
assert_eq!(eval.score, 150.5);
380+
assert_eq!(eval.move_type, "capture");
381+
assert_eq!(eval.from_square, 5);
382+
assert_eq!(eval.to_square, Some(10));
383+
}
384+
385+
#[test]
386+
fn test_get_move_score() {
387+
let ai = AI::new();
388+
let state = GameState::new();
389+
390+
let start_piece = PiecePosition::new(-1, Player::Player1);
391+
let score = ai.get_move_score(&start_piece, &state);
392+
assert!(score > 0);
393+
394+
let board_piece = PiecePosition::new(5, Player::Player1);
395+
let score = ai.get_move_score(&board_piece, &state);
396+
assert!(score >= 0);
397+
}
398+
}

0 commit comments

Comments
 (0)