diff --git a/Cargo.lock b/Cargo.lock index 233b27c..54ca023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,84 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "gammon" version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml index 92fa348..5d488e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +rand = "0.7.3" diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..7292789 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,553 @@ +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 { + for i in (HOME_MAX)+1..=BOARD_MAX { + 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 { + 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 { + 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, + White, +} + +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 { + 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) => Bin::Black(*n + 1), + Bin::White(n) => Bin::White(*n + 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 { + turn : Color, + board: Board, + roll: Roll, + player : Player, + other : Player, +} + +#[derive(Debug,Clone,Copy)] +pub enum Move { + InBoard { + from: u8, + to : 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, +} + +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 { + 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) -> Self { + // The state is laid out for the white player + let mut state = State { + 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(), + player: Player { born_off: 0, to_place: 0 }, + other: 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.switch_sides_mut(); + } + state + } + + pub fn check_move(&self, mv : Move) -> Result<(), Error> { + match mv { + Move::InBoard{from, to} => { + if to > from { + // can only go lower + 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 - to; + if !self.roll.have_equal(needed) { + return Err(Error::NoMatchingRoll); + } + + Ok(()) + }, + Move::Enter(pos) => { + if self.player.to_place == 0 { + return Err(Error::NothingToPlace); + } + + if pos > BOARD_MAX { + return Err(Error::MalformedMove); + } + + let needed = 24 - pos; + + if !self.roll.have_equal(needed) { + return Err(Error::NoMatchingRoll); + } + + if !self.board.can_place(pos, self.turn) { + return Err(Error::TargetOccupied); + } + + Ok(()) + }, + Move::BearOff(pos) => { + if pos > HOME_MAX { + 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 + } + + Ok(()) + }, + } + } + + pub fn apply_move(&self, turn : Color, mv : Move) -> Result { + if turn != self.turn { + return Err(Error::NotYourTurn); + } + + self.check_move(mv)?; + + print!("{:?} plays move: ", turn); + + let mut next = self.clone(); + + match mv { + Move::InBoard { from, to } => { + println!("In-board {} -> {}", from, to); + + let old_color = next.board.apply_move_mut(from, to); + let needed = from - to; // move is always downward + 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.to_place += 1; + } + } + }, + Move::Enter(pos) => { + println!("Enter -> {}", pos); + + let old_color = next.board.apply_enter_mut(pos, self.turn); + let needed = 24 - pos; + + next.roll.remove_mut(needed); + next.player.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.to_place += 1; + } + } + }, + Move::BearOff(pos) => { + println!("Bear off -> {}", pos); + + let needed = next.roll.get_equal_or_greater(pos + 1).unwrap(); + next.roll.remove_mut(needed); + }, + } + + if next.roll.remaining_moves.is_empty() { + next.switch_sides_mut(); + 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(); + } + + fn switch_sides_mut(&mut self) { + self.board.0.reverse(); + mem::swap(&mut self.player, &mut self.other); + self.turn = self.turn.opposite(); + } +} + +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::>().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 + )?; + + for n in 0..=23 { + write!(f, "{:02} ", n)?; + } + f.write_char('\n')?; + + 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(()) +// } +// } diff --git a/src/main.rs b/src/main.rs index 7c73688..2dbe74f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,134 +1,31 @@ -use std::default::Default; -use std::fmt; -use std::fmt::Display; +use crate::game::{Move, Color, Roll}; -#[derive(Debug,Clone,Copy,Eq,PartialEq)] -pub enum Side { - Black = 0, - White = 1, -} - -#[derive(Debug,Clone,Copy)] -pub enum Phase { - PickSides, - Turn(Side), -} - -#[derive(Debug,Clone,Default)] -pub struct Player { - pub born_off: u32, - pub on_bar: u32, -} - -#[derive(Debug,Clone,Copy)] -pub enum Bin { - Empty, - Black(u32), - White(u32), -} - -#[derive(Debug)] -pub struct Game { - pub phase : Phase, - pub multiplier : u32, - pub roll : [u8; 2], - pub players : [Player; 2], - pub bins : [Bin; 24], -} +mod game; -impl Default for Game { - fn default() -> Self { - Game { - phase: Phase::PickSides, - multiplier: 1, - roll: [0; 2], - players: [ - Player::default(), - Player::default(), - ], - bins: [Bin::Empty; 24], - } - } -} - -impl Display for Game { - fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { - for n in (13..=24) { - write!(f, "{} ", ('D' 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)? //░ - } - } +fn main() { + let g = game::State::start(Some(Color::Black)); - write!(f, "|#{}\n", self.players[Side::Black as usize].born_off)?; - for n in (1..=12).rev() { - write!(f, "{:X} ", n)?; - } - write!(f, "\n")?; + let g = g.spoof_roll(Roll::from_dice((3, 5))); - Ok(()) - } -} + println!("{}", g); -impl Game { - fn clear(&mut self) { - for i in 0..24 { - self.bins[i] = Bin::Empty; - } - } + let g = g.apply_move(Color::Black, Move::InBoard { from: 23, to: 20 }).unwrap(); + let g = g.apply_move(Color::Black, Move::InBoard { from: 12, to: 7 }).unwrap(); - fn set(&mut self, n : u32, val : Bin) { - assert!(n > 0 && n <= 24); - let i = n - 1; - self.bins[i as usize] = val; - } + // White turn + let g = g.spoof_roll(Roll::from_dice((4, 6))); + println!("{}", g); - fn starting_position(&mut self) { - self.clear(); - self.set(1, Bin::White(2)); - self.set(6, Bin::Black(5)); - self.set(8, Bin::Black(3)); - self.set(12, Bin::White(5)); - self.set(13, Bin::Black(5)); - self.set(17, Bin::White(3)); - self.set(19, Bin::White(5)); - self.set(24, Bin::Black(2)); - } -} + let g = g.apply_move(Color::White, Move::InBoard { from: 23, to: 17 }).unwrap(); + let g = g.apply_move(Color::White, Move::InBoard { from: 7, to: 3 }).unwrap(); -fn main() { - let mut board = Game::default(); - - board.starting_position(); + // Black turn, need to place the hit stone + let g = g.spoof_roll(Roll::from_dice((1, 2))); + println!("{}", g); - println!("{}", board); + let g = g.apply_move(Color::Black, Move::Enter(22)).unwrap(); + let g = g.apply_move(Color::Black, Move::InBoard { from: 22, to: 21 }).unwrap(); + println!("{}", g); }