feat(web client): local heuristic bot
This commit is contained in:
parent
20134ce468
commit
e61448b627
1 changed files with 56 additions and 7 deletions
|
|
@ -1,5 +1,4 @@
|
|||
use rand::prelude::IndexedRandom;
|
||||
use trictrac_store::{CheckerMove, Color, GameState, MoveRules, Stage, TurnStage};
|
||||
use trictrac_store::{Board, CheckerMove, Color, GameState, MoveRules, Stage, TurnStage};
|
||||
|
||||
use super::types::{PlayerAction, PreGameRollState};
|
||||
|
||||
|
|
@ -29,15 +28,65 @@ pub fn bot_decide(game: &GameState, pgr: Option<&PreGameRollState>) -> Option<Pl
|
|||
TurnStage::Move | TurnStage::HoldOrGoChoice => {
|
||||
let rules = MoveRules::new(&Color::Black, &game.board, game.dice);
|
||||
let sequences = rules.get_possible_moves_sequences(true, vec![]);
|
||||
let mut rng = rand::rng();
|
||||
let (m1, m2) = sequences
|
||||
.choose(&mut rng)
|
||||
.cloned()
|
||||
.unwrap_or((CheckerMove::default(), CheckerMove::default()));
|
||||
// MoveRules with Color::Black mirrors the board internally, so
|
||||
// returned move coordinates are in mirrored (White) space — mirror back.
|
||||
let (m1, m2) = sequences
|
||||
.iter()
|
||||
.max_by(|(m1a, m2a), (m1b, m2b)| {
|
||||
score_seq(&game.board, m1a, m2a)
|
||||
.partial_cmp(&score_seq(&game.board, m1b, m2b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or((CheckerMove::default(), CheckerMove::default()));
|
||||
Some(PlayerAction::Move(m1.mirror(), m2.mirror()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Score a candidate move sequence from the bot's (Black) perspective.
|
||||
/// `m1` and `m2` are in mirrored (White) space, as returned by MoveRules for Color::Black.
|
||||
fn score_seq(board: &Board, m1: &CheckerMove, m2: &CheckerMove) -> f32 {
|
||||
let mut b = board.mirror();
|
||||
let _ = b.move_checker(&Color::White, *m1);
|
||||
let _ = b.move_checker(&Color::White, *m2);
|
||||
evaluate(&b)
|
||||
}
|
||||
|
||||
/// Evaluate a board position from White's perspective (call after mirroring for Black).
|
||||
fn evaluate(board: &Board) -> f32 {
|
||||
let mut score = 0.0f32;
|
||||
|
||||
let white_fields = board.get_color_fields(Color::White);
|
||||
let black_fields = board.get_color_fields(Color::Black);
|
||||
|
||||
// Quarter fill progress — quarters 1-6, 7-12, 19-24.
|
||||
// Quarter 13-18 is skipped: field 13 is the opponent's rest corner so White can never fill it.
|
||||
for &q in &[1usize, 7, 19] {
|
||||
if board.is_quarter_filled(Color::White, q) {
|
||||
score += 8.0;
|
||||
} else {
|
||||
let missing = board.get_quarter_filling_candidate(Color::White);
|
||||
score += (6 - missing.len().min(6)) as f32 * 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton exposure: penalise a White singleton at field f only when there is at least
|
||||
// one Black checker at a field g > f (opponent can potentially threaten it).
|
||||
let max_black_field = black_fields.iter().map(|(f, _)| *f).max().unwrap_or(0);
|
||||
for (f, count) in &white_fields {
|
||||
if *count == 1 && *f < max_black_field {
|
||||
score -= 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit zone progress: reward checkers already in fields 19-24.
|
||||
for (field, count) in &white_fields {
|
||||
if *field >= 19 {
|
||||
score += count.abs() as f32 * 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue