//! Snort is played on undirected graph. In each turn Left colors a vertex blue and Right colors //! a vertex red. Players can only choose a vertex that is adjecent to only empty vertices or to //! vertices in their own color. use crate::{ graph::undirected::Graph, numeric::{dyadic_rational_number::DyadicRationalNumber, nimber::Nimber}, short::partizan::{canonical_form::CanonicalForm, partizan_game::PartizanGame}, }; use std::{collections::VecDeque, fmt::Write, num::NonZeroU32}; /// Color of Snort vertex. Note that we are taking tinting apporach rather than direct tracking /// of adjacent colors. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[cfg_attr( feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) )] #[repr(u8)] pub enum VertexColor { /// Vertex without color, not connected to any taken vertices Empty = 0, /// Vertex that is adjecent to left TintLeft = 1, /// Vertex that is adjecent to right TintRight = 2, /// Vertex that is either taken or connected to both colors Taken = 3, } impl TryFrom for VertexColor { type Error = (); #[inline] fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::Empty), 1 => Ok(Self::TintLeft), 2 => Ok(Self::TintRight), 3 => Ok(Self::Taken), _ => Err(()), } } } /// Type of vertex (or group of them) in the graph. We abstract over vertices to support efficient /// calculations of positions with star-like structure #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum VertexKind { /// Single graph vertex Single(VertexColor), /// Cluster of vertices that are not connected to each other, but may be connected to other /// vertices in the graph. Cluster(VertexColor, NonZeroU32), } impl VertexKind { #[inline] const fn color(self) -> VertexColor { match self { Self::Single(color) | Self::Cluster(color, _) => color, } } #[inline] fn color_mut(&mut self) -> &mut VertexColor { match self { Self::Single(color) | Self::Cluster(color, _) => color, } } #[inline] fn degree_factor(self) -> usize { match self { VertexKind::Single(_) => 1, VertexKind::Cluster(_, cluster_size) => cluster_size.get() as usize, } } } /// Position of a [snort](self) game #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Snort { /// Vertices colors of the game graph pub vertices: Vec, /// Get graph of the game. This includes only edges pub graph: Graph, } impl Snort { /// Create new Snort position with all vertices empty. pub fn new(graph: Graph) -> Self { Self { vertices: vec![VertexKind::Single(VertexColor::Empty); graph.size()], graph, } } // TODO: Perform that check /// Create a Snort position with initial colors. It's up to the user to ensure that no conflicting /// colors are connected in the graph. /// Returns `None` if `vertices` and `graph` have conflicting sizes. pub fn with_colors(vertices: Vec, graph: Graph) -> Option { if vertices.len() != graph.size() { return None; } Some(Self { vertices, graph }) } /// Construct new position on caterpillar `C(n+1, n, n+1)` /// /// The caterpillar `C(n+1, n, n+1)` consists of a main path of length 3, whose central vertex /// has `n` leaves added, and `n+1` leaves are added to the side vertices. pub fn new_three_caterpillar(n: NonZeroU32) -> Self { let on_edges = n.checked_add(1).unwrap(); let in_center = n; Self::with_colors( vec![ VertexKind::Single(VertexColor::Empty), VertexKind::Single(VertexColor::Empty), VertexKind::Single(VertexColor::Empty), VertexKind::Cluster(VertexColor::Empty, on_edges), VertexKind::Cluster(VertexColor::Empty, in_center), VertexKind::Cluster(VertexColor::Empty, on_edges), ], Graph::from_edges(6, &[(0, 1), (0, 2), (0, 4), (1, 3), (2, 5)]), ) .unwrap() } fn vertex_degree(&self, this_vertex: usize) -> usize { let mut res = 0; for one_away in self.graph.vertices() { if one_away != this_vertex && self.graph.are_adjacent(this_vertex, one_away) { res += self.vertices[one_away].degree_factor(); } } res } fn vertex_second_degree(&self, this_vertex: usize) -> usize { let mut res = 0; let mut seen = vec![false; self.graph.size()]; for one_away in self.graph.vertices() { if one_away != this_vertex && self.graph.are_adjacent(this_vertex, one_away) { for two_away in self.graph.vertices() { if two_away != one_away && two_away != this_vertex && self.graph.are_adjacent(one_away, two_away) && !seen[two_away] { seen[two_away] = true; res += self.vertices[two_away].degree_factor(); } } } } res } /// Get degree of the underlying game graph, correctly counting clusters of vertices /// /// Note that using [`Graph::degree`] will yield incorrect results pub fn degree(&self) -> usize { self.graph .vertices() .into_iter() .map(|v| self.vertex_degree(v)) .max() .expect("graph to have at least 1 vertex") } /// Get second degree of the underlying game graph /// /// Second degree of a vertex is the number of all vertices two away from a given vertex /// and just like in first degree, second degree of a graph is the maximum value among vertices pub fn second_degree(&self) -> usize { self.graph .vertices() .into_iter() .map(|v| self.vertex_second_degree(v)) .max() .expect("graph to have at least 1 vertex") } /// Get moves for a given player. Works only for `TintLeft` and `TintRight`. /// Any other input is undefined. fn moves_for(&self) -> Vec { // const ADT generics are unsable, so here we go let own_tint_color: VertexColor = VertexColor::try_from(COLOR).unwrap(); let mut moves = Vec::with_capacity(self.graph.size()); // Vertices where player can move let move_vertices = self .vertices .iter() .enumerate() .filter(|(_, vertex)| { let vertex_color = vertex.color(); vertex_color == own_tint_color || vertex_color == VertexColor::Empty }) .map(|(idx, _)| idx); // Go through list of vertices with legal move for move_vertex_idx in move_vertices { let mut position: Self = self.clone(); // Take vertex let move_vertex = &mut position.vertices[move_vertex_idx]; match move_vertex { VertexKind::Single(move_vertex_color) => *move_vertex_color = VertexColor::Taken, VertexKind::Cluster(_, cluster_size) => { if *cluster_size == NonZeroU32::new(1).unwrap() { *move_vertex = VertexKind::Single(VertexColor::Taken); } else { // Vertices in cluster are disconnected so nothing changes color *cluster_size = NonZeroU32::new(cluster_size.get() - 1).unwrap(); } } } // Disconnect `move_vertex` from adjecent vertices and tint them for adjacent_vertex_idx in self.graph.adjacent_to(move_vertex_idx) { // Disconnect move vertex from adjecent, we disconnect only single vertices // because clusters are still alive. If cluster is dead it's turned into single // before (See: 'take vertex' above), so it still works. if let VertexKind::Single(_) = position.vertices[move_vertex_idx] { position .graph .connect(move_vertex_idx, adjacent_vertex_idx, false); } // No loops in snort graphs if adjacent_vertex_idx != move_vertex_idx { let adjacent_vertex = &mut position.vertices[adjacent_vertex_idx]; let adjacent_vertex_color = adjacent_vertex.color_mut(); // Tint adjacent vertex if *adjacent_vertex_color == own_tint_color || *adjacent_vertex_color == VertexColor::Empty { // If adjacent vertex is empty or tinted in own color, tint it in own *adjacent_vertex_color = own_tint_color; } else { // Otherwise the vertex is tinted in opponents color, so no one can longer // move there, thus we mark is as taken and disconnect from the graph *adjacent_vertex_color = VertexColor::Taken; for v in position.graph.vertices() { position.graph.connect(v, adjacent_vertex_idx, false); } } } } moves.push(position); } moves } /// BFS search to get the decompisitons, should be used only as a helper for [`Self::decompositions`] fn bfs(&self, visited: &mut [bool], v: usize) -> Self { let mut vertices_to_take: Vec = Vec::new(); let mut q: VecDeque = VecDeque::new(); q.push_back(v); visited[v] = true; while let Some(v) = q.pop_front() { vertices_to_take.push(v); for u in self.graph.adjacent_to(v) { if !visited[u] { visited[u] = true; q.push_back(u); } } } let mut new_graph = Graph::empty(vertices_to_take.len()); for (new_v, old_v) in vertices_to_take.iter().enumerate() { for old_u in self.graph.adjacent_to(*old_v) { if let Some(new_u) = vertices_to_take.iter().position(|x| *x == old_u) { new_graph.connect(new_v, new_u, true); } } } let mut new_vertices = Vec::with_capacity(vertices_to_take.len()); for v in &vertices_to_take { new_vertices.push(self.vertices[*v]); } Self { vertices: new_vertices, graph: new_graph, } } /// Render to a [graphviz](https://graphviz.org/) format, that can be later rendered to an /// image with external engine. pub fn to_graphviz(&self) -> String { let mut buf = String::new(); write!(buf, "graph G {{").unwrap(); for (vertex_idx, vertex) in self.vertices.iter().enumerate() { let color = match vertex.color() { VertexColor::Empty => "white", VertexColor::TintLeft => "blue", VertexColor::TintRight => "red", VertexColor::Taken => continue, }; let shape = match vertex { VertexKind::Single(_) => "circle", VertexKind::Cluster(_, _) => "square", }; let label = match vertex { VertexKind::Single(_) => format!("\"{}\"", vertex_idx), VertexKind::Cluster(_, cluster_size) => { format!("\"{}\\n<{}>\"", vertex_idx, cluster_size.get()) } }; write!(buf, "{} [label={}, fillcolor={}, style=filled, shape={}, fixedsize=true, width=1, height=1, fontsize=24];", vertex_idx, label, color, shape).unwrap(); } for v in self.graph.vertices() { for u in self.graph.vertices() { if v < u && self.graph.are_adjacent(v, u) { write!(buf, "{v} -- {u};").unwrap(); } } } write!(buf, "}}").unwrap(); buf } } #[test] fn degree_works() { let snort = Snort::new_three_caterpillar(NonZeroU32::new(8).unwrap()); assert_eq!(snort.degree(), 10); let snort = Snort::new_three_caterpillar(NonZeroU32::new(10).unwrap()); assert_eq!(snort.degree(), 12); } impl PartizanGame for Snort { fn left_moves(&self) -> Vec { self.moves_for::<{ VertexColor::TintLeft as u8 }>() } fn right_moves(&self) -> Vec { self.moves_for::<{ VertexColor::TintRight as u8 }>() } /// Decompose the game graph into disconnected components /// /// # Examples /// /// ``` /// use cgt::graph::undirected::Graph; /// use cgt::short::partizan::games::snort::Snort; /// use cgt::short::partizan::partizan_game::PartizanGame; /// /// assert_eq!( /// Snort::new(Graph::from_edges(5, &[(0, 1), (0, 2), (1, 2), (3, 4)])).decompositions(), /// vec![ /// Snort::new(Graph::from_edges(3, &[(0, 1), (0, 2), (1, 2)])), /// Snort::new(Graph::from_edges(2, &[(0, 1)])) /// ] /// ); /// ``` fn decompositions(&self) -> Vec { let mut visited = vec![false; self.vertices.len()]; let mut res = Vec::new(); for v in self.graph.vertices() { if !matches!(self.vertices[v].color(), VertexColor::Taken) && !visited[v] { res.push(self.bfs(&mut visited, v)); } } res } fn reductions(&self) -> Option { if let &[vertex] = &self.vertices[..] { let cf = match vertex { VertexKind::Single(VertexColor::Empty) => { CanonicalForm::new_nimber(DyadicRationalNumber::from(0), Nimber::new(1)) } VertexKind::Single(VertexColor::TintLeft) => CanonicalForm::new_integer(1), VertexKind::Single(VertexColor::TintRight) => CanonicalForm::new_integer(-1), VertexKind::Cluster(VertexColor::Empty, cluster_size) => { let nimber = Nimber::new(cluster_size.get() % 2); CanonicalForm::new_nimber(DyadicRationalNumber::from(0), nimber) } VertexKind::Cluster(VertexColor::TintLeft, cluster_size) => { CanonicalForm::new_integer(cluster_size.get() as i64) } VertexKind::Cluster(VertexColor::TintRight, cluster_size) => { CanonicalForm::new_integer(-(cluster_size.get() as i64)) } VertexKind::Single(VertexColor::Taken) | VertexKind::Cluster(VertexColor::Taken, _) => CanonicalForm::new_integer(0), }; return Some(cf); } None } } #[test] fn no_moves() { let position = Snort::new(Graph::empty(0)); assert_eq!(position.left_moves(), vec![]); assert_eq!(position.right_moves(), vec![]); } #[test] fn correct_canonical_forms() { use crate::short::partizan::transposition_table::ParallelTranspositionTable; let transposition_table = ParallelTranspositionTable::new(); let snort = Snort::with_colors( vec![VertexKind::Cluster( VertexColor::Empty, NonZeroU32::new(10).unwrap(), )], Graph::empty(1), ) .unwrap(); let canonical_form = snort.canonical_form(&transposition_table); assert_eq!(canonical_form.to_string(), "0"); let snort = Snort::with_colors( vec![VertexKind::Cluster( VertexColor::Empty, NonZeroU32::new(11).unwrap(), )], Graph::empty(1), ) .unwrap(); let canonical_form = snort.canonical_form(&transposition_table); assert_eq!(canonical_form.to_string(), "*"); } #[test] fn correct_sensible() { use crate::short::partizan::transposition_table::ParallelTranspositionTable; let position = Snort::with_colors( vec![ VertexKind::Single(VertexColor::Empty), VertexKind::Single(VertexColor::TintLeft), ], Graph::empty(2), ) .unwrap(); let transposition_table = ParallelTranspositionTable::new(); assert_eq!( position.sensible_left_moves(&transposition_table), vec![Snort::with_colors( vec![ VertexKind::Single(VertexColor::Taken), VertexKind::Single(VertexColor::TintLeft) ], Graph::empty(2), ) .unwrap()] ); }