//! The game is played on a rectengular grid. Left places vertical dominoes, Right places //! horizontal dominoes. extern crate alloc; use crate::{ drawing::svg::{self, ImmSvg, Svg}, grid::{decompositions, move_top_left, small_bit_grid::SmallBitGrid, FiniteGrid, Grid}, short::partizan::partizan_game::PartizanGame, }; use cgt_derive::Tile; use core::{fmt, hash::Hash}; use std::{fmt::Display, str::FromStr}; /// Tile on a Domineering grid #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Tile)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Tile { /// Tile where domino can be placed #[tile(char('.'), bool(false), default)] Empty, /// Tile occupied by domino #[tile(char('#'), bool(true))] Taken, } impl Tile { #[inline] fn is_non_blocking(self) -> bool { self == Self::Empty } } /// A Domineering position on a rectengular grid. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Domineering> { grid: G, } impl Display for Domineering where G: Grid + FiniteGrid, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.grid.display(f, '|') } } impl FromStr for Domineering where G: Grid + FiniteGrid, { type Err = (); fn from_str(s: &str) -> Result { Ok(Self::new(G::parse(s).ok_or(())?)) } } impl Domineering where G: Grid + FiniteGrid, { /// Create a domineering position from a grid. pub const fn new(grid: G) -> Self { Self { grid } } /// Get underlying grid pub const fn grid(&self) -> &G { &self.grid } /// Get underlying grid mutably pub fn grid_mut(&mut self) -> &mut G { &mut self.grid } /// Output positions as LaTeX `TikZ` picture where empty tiles are 1x1 tiles pub fn to_latex(&self) -> String { self.to_latex_with_scale(1.) } /// Like [`Self::to_latex`] but allows to specify image scale. Scale must be positive /// /// # Panics /// - `scale` is negative pub fn to_latex_with_scale(&self, scale: f32) -> String { use std::fmt::Write; assert!(scale >= 0., "Scale must be positive"); let scale = scale.to_string(); let mut buf = String::new(); write!(buf, "\\begin{{tikzpicture}}[scale={}] ", scale).unwrap(); for y in 0..self.grid.height() { for x in 0..self.grid.width() { if self.grid.get(x, y) == Tile::Taken { write!( buf, "\\fill[fill=gray] ({},{}) rectangle ({},{}); ", x, y, x + 1, y + 1, ) .unwrap(); } } } write!( buf, "\\draw[step=1cm,black] (0,0) grid ({}, {}); \\end{{tikzpicture}}", self.grid.width(), self.grid.height() ) .unwrap(); buf } /// Get number of empty tiles on a grid pub fn free_places(&self) -> usize { let mut res = 0; for y in 0..self.grid.height() { for x in 0..self.grid.width() { if self.grid.get(x, y) == Tile::Empty { res += 1; } } } res } fn moves_for(&self) -> Vec where G: Ord + Clone, { let mut moves = Vec::new(); if self.grid.height() == 0 || self.grid.width() == 0 { return moves; } for y in 0..(self.grid.height() - DIR_Y) { for x in 0..(self.grid.width() - DIR_X) { let next_x = x + DIR_X; let next_y = y + DIR_Y; if self.grid.get(x, y) == Tile::Empty && self.grid.get(next_x, next_y) == Tile::Empty { let mut new_grid = self.clone(); new_grid.grid.set(x, y, Tile::Taken); new_grid.grid.set(next_x, next_y, Tile::Taken); moves.push(new_grid.move_top_left()); } } } moves.sort_unstable(); moves.dedup(); moves } /// Remove filled rows and columns from the edges /// /// # Examples /// ``` /// use cgt::short::partizan::games::domineering::Domineering; /// use std::str::FromStr; /// /// let position: Domineering = Domineering::from_str("###|.#.|##.").unwrap(); /// assert_eq!(&format!("{}", position.move_top_left()), ".#.|##."); /// ``` // Panic at `Self::empty(minimized_width, minimized_height).unwrap();` is unreachable #[must_use] #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_panics_doc))] pub fn move_top_left(&self) -> Self { Self::new(move_top_left(&self.grid, Tile::is_non_blocking)) } } #[cfg(not(tarpaulin_include))] impl Svg for Domineering where G: Grid + FiniteGrid, { fn to_svg(&self, buf: &mut W) -> fmt::Result where W: fmt::Write, { // Chosen arbitrarily let tile_size = 48; let grid_width = 4; let offset = grid_width / 2; let svg_width = self.grid.width() as u32 * tile_size + grid_width; let svg_height = self.grid.height() as u32 * tile_size + grid_width; ImmSvg::new(buf, svg_width, svg_height, |buf| { for y in 0..self.grid.height() { for x in 0..self.grid.width() { let fill = match self.grid.get(x, y) { Tile::Empty => "white", Tile::Taken => "gray", }; ImmSvg::rect( buf, (x as u32 * tile_size + offset) as i32, (y as u32 * tile_size + offset) as i32, tile_size, tile_size, fill, )?; } } let grid = svg::Grid { x1: 0, y1: 0, x2: svg_width as i32, y2: svg_height as i32, grid_width, tile_size, }; ImmSvg::grid(buf, &grid) }) } } impl PartizanGame for Domineering where G: Grid + FiniteGrid + Clone + Hash + Send + Sync + Ord, { /// Get moves for the Left player as positions she can move to. /// /// # Examples /// /// ``` /// // ..# .. .# | /// // .#. = { .# , #. | <...> } /// // ##. #. | /// /// use cgt::short::partizan::games::domineering::Domineering; /// use crate::cgt::short::partizan::partizan_game::PartizanGame; /// use std::str::FromStr; /// /// let position: Domineering = Domineering::from_str("..#|.#.|##.").unwrap(); /// assert_eq!( /// position.left_moves(), /// vec![ /// Domineering::from_str("..|.#").unwrap(), /// Domineering::from_str(".#|#.|#.").unwrap(), /// ] /// ); /// ``` fn left_moves(&self) -> Vec { self.moves_for::<0, 1>() } /// Get moves for the Right player as positions he can move to. /// /// # Examples /// /// ``` /// // ..# | /// // .#. = { <...> | .#. , /// // ##. | ##. /// /// use cgt::short::partizan::{partizan_game::PartizanGame, games::domineering::Domineering}; /// use std::str::FromStr; /// /// let position: Domineering = Domineering::from_str("..#|.#.|##.").unwrap(); /// assert_eq!( /// position.right_moves(), /// vec![Domineering::from_str(".#.|##.").unwrap(),] /// ); /// ``` fn right_moves(&self) -> Vec { self.moves_for::<1, 0>() } /// Get decompisitons of given position /// /// # Examples /// ``` /// // ..# ..# ### /// // .#. = .## + ##. /// // ##. ### ##. /// /// use cgt::short::partizan::{partizan_game::PartizanGame, games::domineering::Domineering}; /// use std::str::FromStr; /// /// let position: Domineering = Domineering::from_str("..#|.#.|##.").unwrap(); /// assert_eq!( /// position.decompositions(), /// vec![ /// Domineering::from_str("..|.#").unwrap(), /// Domineering::from_str(".|.").unwrap(), /// ] /// ); /// ``` fn decompositions(&self) -> Vec { let directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]; decompositions(&self.grid, Tile::is_non_blocking, Tile::Taken, &directions) .into_iter() .map(Self::new) .collect::>() } } #[cfg(test)] mod tests { use super::*; use crate::{ numeric::dyadic_rational_number::DyadicRationalNumber, short::partizan::transposition_table::ParallelTranspositionTable, }; use std::str::FromStr; #[test] #[should_panic] fn grid_max_size_is_respected() { Domineering::new(SmallBitGrid::empty(10, 10).unwrap()); } #[test] fn parse_display_roundtrip() { let inp = "...|#.#|##.|###"; let pos: Domineering = Domineering::from_str(inp).unwrap(); assert_eq!(&format!("{}", pos), inp,); } // Values confirmed with gcsuite #[cfg(test)] fn test_grid_canonical_form(grid: Domineering, canonical_form: &str) { let transposition_table = ParallelTranspositionTable::new(); let game_id = grid.canonical_form(&transposition_table); assert_eq!(&game_id.to_string(), canonical_form); } #[test] fn finds_canonical_form_of_one() { test_grid_canonical_form(Domineering::from_str(".|.").unwrap(), "1"); } #[test] fn finds_canonical_form_of_minus_one() { test_grid_canonical_form(Domineering::from_str("..").unwrap(), "-1"); } #[test] fn finds_canonical_form_of_two_by_two() { test_grid_canonical_form(Domineering::from_str("..|..").unwrap(), "{1|-1}"); } #[test] fn finds_canonical_form_of_two_by_two_with_noise() { test_grid_canonical_form(Domineering::from_str("..#|..#|##.").unwrap(), "{1|-1}"); } #[test] fn finds_canonical_form_of_minus_two() { test_grid_canonical_form(Domineering::from_str("....").unwrap(), "-2"); } #[test] fn finds_canonical_form_of_l_shape() { test_grid_canonical_form(Domineering::from_str(".#|..").unwrap(), "*"); } #[test] fn finds_canonical_form_of_long_l_shape() { test_grid_canonical_form(Domineering::from_str(".##|.##|...").unwrap(), "0"); } #[test] fn finds_canonical_form_of_weird_l_shape() { test_grid_canonical_form(Domineering::from_str("..#|..#|...").unwrap(), "{1/2|-2}"); } #[test] fn finds_canonical_form_of_three_by_three() { test_grid_canonical_form(Domineering::from_str("...|...|...").unwrap(), "{1|-1}"); } #[test] fn finds_canonical_form_of_num_nim_sum() { test_grid_canonical_form(Domineering::from_str(".#.#|.#..").unwrap(), "1*"); } #[test] #[cfg(not(miri))] fn finds_temperature_of_four_by_four_grid() { let transposition_table = ParallelTranspositionTable::new(); let grid: Domineering = Domineering::from_str("#...|....|....|....").unwrap(); let game_id = grid.canonical_form(&transposition_table); let temp = game_id.temperature(); dbg!(transposition_table.len()); assert_eq!(&game_id.to_string(), "{1*|-1*}"); assert_eq!(temp, DyadicRationalNumber::from(1)); } #[test] fn latex_works() { let position: Domineering = Domineering::from_str("##..|....|#...|..##").unwrap(); let latex = position.to_latex(); assert_eq!( &latex, r#"\begin{tikzpicture}[scale=1] \fill[fill=gray] (0,0) rectangle (1,1); \fill[fill=gray] (1,0) rectangle (2,1); \fill[fill=gray] (0,2) rectangle (1,3); \fill[fill=gray] (2,3) rectangle (3,4); \fill[fill=gray] (3,3) rectangle (4,4); \draw[step=1cm,black] (0,0) grid (4, 4); \end{tikzpicture}"# ); } /// Assert temperature value without going through canonical form /// Using macro yields better error location on assertion failure #[cfg(test)] macro_rules! assert_temperature { ($grid:expr, $temp:expr) => { let grid: Domineering = $grid.unwrap(); let thermograph = grid.thermograph_direct(); let expected_temperature = DyadicRationalNumber::from($temp); assert_eq!(thermograph.temperature(), expected_temperature); }; } #[test] fn temperature_without_game_works() { assert_temperature!(Domineering::from_str(""), -1); assert_temperature!(Domineering::from_str(".."), -1); assert_temperature!(Domineering::from_str("..|.#"), 0); // FIXME: takes too long // assert_temperature!(Domineering::from_str("#...|....|....|...."), 1); } }