From 7dff6572462bdc59011f8ff8d1c6dc3a0494e44a Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 22 Oct 2026 17:17:02 +1300 Subject: feat: Improvements to UI Now uses ratatui --- src/player.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 8 deletions(-) (limited to 'src/player.rs') diff --git a/src/player.rs b/src/player.rs index f46e36b..5f2bafb 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,10 +1,63 @@ use std::{collections::HashMap, time::{Duration, Instant}}; -use crate::board::{GameBoard, Move, Turn}; +use ratatui::{buffer::Buffer, crossterm::event::KeyEvent, layout::Rect, widgets::{Paragraph, Widget}}; +use crate::board::{GameBoard, Move, Pos, Turn}; + +/// Input the game coordinator passes on to the player whose turn it is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Input +{ + /// A click on a board point. + Click(Pos), + Key(KeyEvent), +} + +/// Points for the coordinator to highlight on the board. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Highlights +{ + pub selected: Option, + pub targets: Vec, +} + +/// A side in the game. The coordinator calls `pick_move` once per loop while it's this player's +/// turn, and passes on input with `handle_input`; whichever returns a move first plays it. pub trait Player { - fn pick_move(&mut self, board: GameBoard, turn: Turn) -> Move; + fn name(&self) -> String; + + /// Called before each draw while it's this player's turn, with the position to play. + fn update(&mut self, _board: GameBoard, _turn: Turn) {} + + /// Computer players search here and return Some. Interactive players return None and + /// wait for `handle_input`. + fn pick_move(&mut self, _board: GameBoard, _turn: Turn) -> Option { None } + + /// A click on a board point, or a key press, while it's this player's turn. + fn handle_input(&mut self, _input: Input, _board: GameBoard, _turn: Turn) -> Option { None } + + /// Points for the coordinator to highlight on the board. + fn highlights(&self) -> Highlights { Highlights::default() } + + /// Drop any half-made selection, e.g. after a move or a revert. + fn reset(&mut self) {} + + /// Draw into the player-output panel. By default this shows the player's name. + fn render(&self, area: Rect, buf: &mut Buffer) + { + Paragraph::new(self.name()).render(area, buf); + } +} + +/// Every player is a ratatui widget through `Player::render`. +/// (A blanket `impl Widget for &P` would break the orphan rule.) +impl Widget for &dyn Player +{ + fn render(self, area: Rect, buf: &mut Buffer) + { + Player::render(self, area, buf); + } } pub type ScoreType = f32; @@ -175,14 +228,17 @@ pub trait MinimaxPlayer /// How long `pick_move` may spend searching. fn time_limit(&self) -> Duration { Duration::from_secs(1) } + + fn name(&self) -> String { "Computer".to_string() } } impl Player for P { - + fn name(&self) -> String { MinimaxPlayer::name(self) } + /// Iterative deepening: searches depth 1, 2, … until time runs out, each depth trying the /// previous one's best moves first, and plays the best move from the deepest finished search. - fn pick_move(&mut self, board: GameBoard, turn: Turn) -> Move { + fn pick_move(&mut self, board: GameBoard, turn: Turn) -> Option { let start = Instant::now(); let time_limit = self.time_limit(); let deadline = start + time_limit; @@ -210,8 +266,13 @@ impl Player for P break; } } - best.expect("pick_move called with no legal moves") + Some(best.expect("pick_move called with no legal moves")) } + + fn render(&self, area: Rect, buf: &mut Buffer) + { + Paragraph::new(format!("{}\n\nThinking…", MinimaxPlayer::name(self))).render(area, buf); + } } @@ -366,7 +427,7 @@ mod tests player.time_limit = Duration::from_millis(50); let board = GameBoard::default(); let start = Instant::now(); - let m = player.pick_move(board, Turn::Sheep); + let m = player.pick_move(board, Turn::Sheep).unwrap(); assert!(start.elapsed() < Duration::from_millis(500), "took {:?}", start.elapsed()); assert_eq!(board.check_move(m), Ok(())); } @@ -380,7 +441,7 @@ mod tests { let mut player = TestPlayer::new(4, cached); let best = reference(&mut player, board, turn, 4); - let m = player.pick_move(board, turn); + let m = player.pick_move(board, turn).unwrap(); let score = reference(&mut player, board.after_move_unchecked(m), turn.next_turn(), 3); assert_eq!(score, best, "{turn:?} picked {m:?} (cached: {cached})"); } @@ -413,7 +474,7 @@ mod tests for cached in [false, true] { - let m = TestPlayer::new(3, cached).pick_move(board, Turn::Tiger); + let m = TestPlayer::new(3, cached).pick_move(board, Turn::Tiger).unwrap(); assert!(board.after_move_unchecked(m).check_win(Turn::Sheep), "picked {m:?} (cached: {cached})"); } } -- cgit v1.3.1