You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
596 lines
16 KiB
596 lines
16 KiB
use std::default::Default;
|
|
use std::{fmt, mem};
|
|
use std::fmt::{Display, Formatter, Write};
|
|
use rand::{Rng, rngs::OsRng};
|
|
use std::borrow::Borrow;
|
|
|
|
#[derive(Debug,Clone)]
|
|
struct Board([Bin; 24]);
|
|
|
|
impl Board {
|
|
fn can_bear_off(&self, color : Color) -> bool {
|
|
// this range covers the non-home area of the board
|
|
let range = if color == Color::White {
|
|
// White's home is low indices
|
|
6..=23
|
|
} else {
|
|
// Black's home is high indices
|
|
0..=17
|
|
};
|
|
|
|
for i in range {
|
|
match (self.0)[i as usize].as_color() {
|
|
Some(c) if c == color => {
|
|
return false;
|
|
},
|
|
_ => {
|
|
// empty or the other color
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn can_place(&self, pos: u8, color : Color) -> bool {
|
|
if pos > BOARD_MAX {
|
|
false
|
|
} else {
|
|
let c = (self.0)[pos as usize].as_color();
|
|
|
|
c.is_none() ||
|
|
c.unwrap() == color ||
|
|
(self.0)[pos as usize].as_count() == 1
|
|
}
|
|
}
|
|
|
|
fn can_move_from(&self, pos: u8, color : Color) -> bool {
|
|
if pos > BOARD_MAX {
|
|
false
|
|
} else {
|
|
let c = (self.0)[pos as usize].as_color();
|
|
|
|
c.is_some() &&
|
|
c.unwrap() == color
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
fn apply_move_mut(&mut self, from : u8, to : u8) -> Option<Color> {
|
|
let old_color = self.0[to as usize].as_color();
|
|
let color = self.0[from as usize].subtract_mut();
|
|
self.0[to as usize].add_mut(color);
|
|
old_color
|
|
}
|
|
|
|
#[must_use]
|
|
fn apply_enter_mut(&mut self, pos : u8, color : Color) -> Option<Color> {
|
|
let old_color = self.0[pos as usize].as_color();
|
|
self.0[pos as usize].add_mut(color);
|
|
old_color
|
|
}
|
|
|
|
fn apply_bearoff_mut(&mut self, pos : u8) {
|
|
self.0[pos as usize].subtract_mut();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
|
|
pub enum Color {
|
|
/// Black, home area 18..=23
|
|
Black,
|
|
/// White, home area 0..=5
|
|
White,
|
|
}
|
|
|
|
impl Display for Color {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:?}", self) // re-use the debug renderer
|
|
}
|
|
}
|
|
|
|
impl Color {
|
|
fn opposite(self) -> Color {
|
|
match self {
|
|
Color::Black => Color::White,
|
|
Color::White => Color::Black,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Copy)]
|
|
pub enum Bin {
|
|
Empty,
|
|
Black(u8),
|
|
White(u8),
|
|
}
|
|
|
|
impl Bin {
|
|
pub fn as_color(self) -> Option<Color> {
|
|
match self {
|
|
Bin::Empty => None,
|
|
Bin::Black(_) => Some(Color::Black),
|
|
Bin::White(_) => Some(Color::White),
|
|
}
|
|
}
|
|
|
|
pub fn as_count(self) -> u8 {
|
|
match self {
|
|
Bin::Empty => 0,
|
|
Bin::Black(n) | Bin::White(n) => n,
|
|
}
|
|
}
|
|
|
|
fn add_mut(&mut self, color : Color) {
|
|
mem::replace(self, match self.borrow() {
|
|
Bin::Empty => {
|
|
match color {
|
|
Color::Black => Bin::Black(1),
|
|
Color::White => Bin::White(1),
|
|
}
|
|
},
|
|
Bin::Black(n) => {
|
|
if color == Color::Black {
|
|
Bin::Black(*n + 1)
|
|
} else {
|
|
Bin::White(1)
|
|
}
|
|
},
|
|
Bin::White(n) => {
|
|
if color == Color::White {
|
|
Bin::White(*n + 1)
|
|
} else {
|
|
Bin::Black(1)
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn subtract_mut(&mut self) -> Color {
|
|
match self {
|
|
Bin::Empty => {
|
|
panic!("Subtract from empty bin");
|
|
},
|
|
Bin::Black(n) => {
|
|
*n -= 1u8;
|
|
if *n == 0 {
|
|
mem::replace(self, Bin::Empty);
|
|
}
|
|
Color::Black
|
|
},
|
|
Bin::White(n) => {
|
|
*n -= 1u8;
|
|
if *n == 0 {
|
|
mem::replace(self, Bin::Empty);
|
|
}
|
|
Color::White
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Player {
|
|
born_off : u8,
|
|
to_place : u8,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct State {
|
|
move_number : usize,
|
|
turn : Color,
|
|
board: Board,
|
|
roll: Roll,
|
|
black : Player,
|
|
white : Player,
|
|
}
|
|
|
|
#[derive(Debug,Clone,Copy)]
|
|
pub enum Move {
|
|
InBoard(u8, u8),
|
|
Enter(u8),
|
|
BearOff(u8)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
Internal,
|
|
MalformedMove,
|
|
NoSourceStone,
|
|
TargetOccupied,
|
|
NotBearingOff,
|
|
NotAllPlaced,
|
|
NothingToPlace,
|
|
NoMatchingRoll,
|
|
NoValidMoves,
|
|
NotYourTurn,
|
|
}
|
|
|
|
impl Display for Error {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
// TODO
|
|
write!(f, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for Error {}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Roll {
|
|
/// The dice as rolled
|
|
dice: (u8, u8),
|
|
/// Remaining moves based on the dice and steps already performed
|
|
remaining_moves: Vec<u8>,
|
|
}
|
|
|
|
impl Roll {
|
|
pub fn new() -> Self {
|
|
Self::from_dice((
|
|
OsRng.gen_range(1, 7),
|
|
OsRng.gen_range(1, 7)
|
|
))
|
|
}
|
|
|
|
pub fn from_dice(dice: (u8,u8)) -> Self {
|
|
let remaining_moves;
|
|
if dice.0 == dice.1 {
|
|
remaining_moves = vec![dice.0, dice.0, dice.0, dice.0];
|
|
} else {
|
|
remaining_moves = vec![dice.0, dice.1];
|
|
}
|
|
|
|
Roll {
|
|
dice,
|
|
remaining_moves
|
|
}
|
|
}
|
|
|
|
pub fn have_equal(&self, eq : u8) -> bool {
|
|
self.remaining_moves.iter()
|
|
.any(|v| *v == eq)
|
|
}
|
|
|
|
pub fn have_equal_or_greater(&self, mineq : u8) -> bool {
|
|
self.remaining_moves.iter()
|
|
.any(|v| *v >= mineq)
|
|
}
|
|
|
|
pub fn get_equal_or_greater(&self, mineq : u8) -> Option<u8> {
|
|
self.remaining_moves.iter()
|
|
.find(|v| *v >= &mineq)
|
|
.cloned()
|
|
}
|
|
|
|
fn remove_mut(&mut self, needed : u8) {
|
|
let nth = self.remaining_moves.iter().position(|v| *v == needed).unwrap();
|
|
self.remaining_moves.remove(nth);
|
|
}
|
|
|
|
/// # Panics
|
|
/// if not available
|
|
pub fn remove(&mut self, needed : u8) -> Roll {
|
|
let mut next = self.clone();
|
|
next.remove_mut(needed);
|
|
next
|
|
}
|
|
}
|
|
|
|
// The board is reversed based on the active player's color to make the algorithm unified.
|
|
// The movement is always to lower positions, with home at 5-0
|
|
const BOARD_MAX : u8 = 23;
|
|
const HOME_MAX : u8 = 5;
|
|
|
|
impl State {
|
|
pub fn start(turn : Option<Color>) -> Self {
|
|
// The state is laid out for the white player
|
|
let mut state = State {
|
|
move_number: 0,
|
|
turn: Color::White,
|
|
board: Board([
|
|
// 1 .. 12 (bottom row, right to left)
|
|
Bin::Black(2), // white player's home side
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::White(5), // 5 in 0-based
|
|
Bin::Empty,
|
|
Bin::White(3),
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Black(5),
|
|
// 13 .. 24 (top row, left to right)
|
|
Bin::White(5), // 12 in 0-based
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Black(3),
|
|
Bin::Empty,
|
|
Bin::Black(5), // 18 in 0-based
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::Empty,
|
|
Bin::White(2), // black player's home side
|
|
]),
|
|
roll: Roll::new(),
|
|
black: Player { born_off: 0, to_place: 0 },
|
|
white: Player { born_off: 0, to_place: 0 }
|
|
};
|
|
|
|
if turn.map(|c| c == Color::Black).unwrap_or_else(|| OsRng.gen()) {
|
|
// flip the board so black starts
|
|
state.turn = Color::Black;
|
|
}
|
|
state
|
|
}
|
|
|
|
pub fn turn(&self) -> (usize, Color) {
|
|
(self.move_number, self.turn)
|
|
}
|
|
|
|
fn player(&self) -> &Player {
|
|
match self.turn {
|
|
Color::Black => &self.black,
|
|
Color::White => &self.white,
|
|
}
|
|
}
|
|
|
|
fn player_mut(&mut self) -> &mut Player {
|
|
match self.turn {
|
|
Color::Black => &mut self.black,
|
|
Color::White => &mut self.white,
|
|
}
|
|
}
|
|
|
|
fn other(&self) -> &Player {
|
|
match self.turn {
|
|
Color::Black => &self.white,
|
|
Color::White => &self.black,
|
|
}
|
|
}
|
|
|
|
fn other_mut(&mut self) -> &mut Player {
|
|
match self.turn {
|
|
Color::Black => &mut self.white,
|
|
Color::White => &mut self.black,
|
|
}
|
|
}
|
|
|
|
|
|
pub fn apply_move(&self, turn : Color, mv : Move) -> Result<Self, Error> {
|
|
if turn != self.turn {
|
|
return Err(Error::NotYourTurn);
|
|
}
|
|
|
|
print!("{} plays move: ", turn);
|
|
|
|
let mut next = self.clone();
|
|
|
|
match mv {
|
|
Move::InBoard(from, to) => {
|
|
println!("In-board {} -> {}", from, to);
|
|
|
|
if (self.turn == Color::White && to >= from) || (self.turn == Color::Black && to <= from) {
|
|
return Err(Error::MalformedMove);
|
|
}
|
|
|
|
if self.player().to_place != 0 {
|
|
return Err(Error::NotAllPlaced);
|
|
}
|
|
|
|
if !self.board.can_move_from(from, self.turn) {
|
|
return Err(Error::NoSourceStone);
|
|
}
|
|
|
|
if !self.board.can_place(to, self.turn) {
|
|
return Err(Error::TargetOccupied);
|
|
}
|
|
|
|
let needed = (from as i8 - to as i8).abs() as u8;
|
|
if !self.roll.have_equal(needed) {
|
|
return Err(Error::NoMatchingRoll);
|
|
}
|
|
|
|
let old_color = next.board.apply_move_mut(from, to);
|
|
next.roll.remove_mut(needed);
|
|
|
|
if let Some(c) = old_color {
|
|
if c != self.turn {
|
|
println!("{} stone at position {} is hit.", c, to);
|
|
|
|
// hit opposite color
|
|
next.other_mut().to_place += 1;
|
|
}
|
|
}
|
|
},
|
|
Move::Enter(pos) => {
|
|
println!("Enter -> {}", pos);
|
|
|
|
if self.player().to_place == 0 {
|
|
return Err(Error::NothingToPlace);
|
|
}
|
|
|
|
if pos > BOARD_MAX {
|
|
return Err(Error::MalformedMove);
|
|
}
|
|
|
|
let needed = if self.turn == Color::White {
|
|
24 - pos
|
|
} else {
|
|
pos + 1
|
|
};
|
|
|
|
if !self.roll.have_equal(needed) {
|
|
return Err(Error::NoMatchingRoll);
|
|
}
|
|
|
|
if !self.board.can_place(pos, self.turn) {
|
|
return Err(Error::TargetOccupied);
|
|
}
|
|
|
|
let old_color = next.board.apply_enter_mut(pos, self.turn);
|
|
|
|
next.roll.remove_mut(needed);
|
|
next.player_mut().to_place -= 1;
|
|
|
|
if let Some(c) = old_color {
|
|
if c != self.turn {
|
|
println!("{} stone at position {} is hit.", c, pos);
|
|
// hit opposite color
|
|
next.other_mut().to_place += 1;
|
|
}
|
|
}
|
|
},
|
|
Move::BearOff(pos) => {
|
|
println!("Bear off -> {}", pos);
|
|
|
|
if (self.turn == Color::White && pos > HOME_MAX) || (self.turn == Color::Black && pos < 18) {
|
|
return Err(Error::MalformedMove);
|
|
}
|
|
|
|
if self.player().to_place != 0 {
|
|
return Err(Error::NotAllPlaced);
|
|
}
|
|
|
|
if !self.board.can_bear_off(self.turn) {
|
|
// still have checkers in higher positions
|
|
return Err(Error::NotBearingOff);
|
|
}
|
|
|
|
if !self.roll.have_equal_or_greater(pos + 1) {
|
|
return Err(Error::NoMatchingRoll);
|
|
// TODO must always bear off the highest possible, if multiple checkers are available
|
|
}
|
|
|
|
let needed = next.roll.get_equal_or_greater(pos + 1).unwrap();
|
|
next.roll.remove_mut(needed);
|
|
},
|
|
}
|
|
|
|
if next.roll.remaining_moves.is_empty() {
|
|
next.turn = self.turn.opposite();
|
|
next.move_number += 1;
|
|
next.roll_mut();
|
|
|
|
// TODO check if any moves are possible with this roll, if not, auto-switch again
|
|
}
|
|
|
|
Ok(next)
|
|
}
|
|
|
|
pub fn spoof_roll(&self, roll : Roll) -> Self {
|
|
let mut next = self.clone();
|
|
next.roll = roll;
|
|
next
|
|
}
|
|
|
|
fn roll_mut(&mut self) {
|
|
self.roll = Roll::new();
|
|
}
|
|
}
|
|
|
|
impl Display for State {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(f, "\n# {} turn, roll ({},{}), moves to play: [{}]\n",
|
|
self.turn,
|
|
self.roll.dice.0,
|
|
self.roll.dice.1,
|
|
self.roll.remaining_moves.iter().map(ToString::to_string).collect::<Vec<_>>().join(",")
|
|
)?;
|
|
|
|
write!(f, "# bearing off? {}, born off: {}, to place: {}\n",
|
|
if self.board.can_bear_off(self.turn) { "YES" } else { "no" },
|
|
self.player().born_off,
|
|
self.player().to_place
|
|
)?;
|
|
|
|
f.write_str("White <- ")?;
|
|
for n in 0..=23 {
|
|
write!(f, "{:02} ", n+1)?;
|
|
}
|
|
f.write_str(" -> Black\n")?;
|
|
f.write_str(" ")?;
|
|
|
|
for n in 0..=23 {
|
|
match self.board.0[n] {
|
|
Bin::Empty => {
|
|
write!(f, "--- ", )?;
|
|
},
|
|
Bin::Black(i) => {
|
|
write!(f, "K{:<3} ", i)?;
|
|
},
|
|
Bin::White(i) => {
|
|
write!(f, "W{:<3} ", i)?;
|
|
},
|
|
}
|
|
}
|
|
f.write_str("\n")?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
|
|
// impl Display for Game {
|
|
// fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// write!(f, "{}'s turn, roll {}+{}", self.turn, self.roll.0, self.roll.1)?;
|
|
//
|
|
// let pl = self.cur_player();
|
|
//
|
|
// if pl.bearing_off {
|
|
// write!(f, ", born off {}/15", pl.born_off)?;
|
|
// }
|
|
//
|
|
// if pl.on_bar > 0 {
|
|
// write!(f, ", {} on bar!", pl.on_bar)?;
|
|
// }
|
|
// else {
|
|
// write!(f, ", normal move")?;
|
|
// }
|
|
//
|
|
// f.write_str("\n")?;
|
|
//
|
|
// for n in (13..=24) {
|
|
// write!(f, "{} ", ('m' as u8 + (n - 13) as u8) as char)?;
|
|
// }
|
|
// write!(f, "\n")?;
|
|
//
|
|
// for (i, b) in (&self.bins[12..=23]).iter().enumerate() {
|
|
// match *b {
|
|
// Bin::Empty => {
|
|
// if i%2==0 {
|
|
// write!(f, "░░")?
|
|
// } else {
|
|
// write!(f, "▒▒")?
|
|
// }
|
|
// },
|
|
// Bin::Black(n) => write!(f, "{:X}#", n)?,
|
|
// Bin::White(n) => write!(f, "{:X}:", n)?
|
|
// }
|
|
// }
|
|
// write!(f, "|:{}\n", self.players[Side::Black as usize].born_off)?;
|
|
//
|
|
// for (i, b) in (&self.bins[0..=11]).iter().rev().enumerate() {
|
|
// match *b {
|
|
// Bin::Empty => {
|
|
// if i%2==0 {
|
|
// write!(f, "░░")?
|
|
// } else {
|
|
// write!(f, "▒▒")?
|
|
// }
|
|
// },
|
|
// Bin::Black(n) => write!(f, "{:X}#", n)?, //█
|
|
// Bin::White(n) => write!(f, "{:X}:", n)? //░
|
|
// }
|
|
// }
|
|
//
|
|
// write!(f, "|#{}\n", self.players[Side::Black as usize].born_off)?;
|
|
// for n in (1..=12).rev() {
|
|
// write!(f, "{} ", ('a' as u8 + (n-1) as u8) as char)?;
|
|
// }
|
|
// write!(f, "\n")?;
|
|
//
|
|
// Ok(())
|
|
// }
|
|
// }
|
|
|