Skip to main content

problemreductions/models/misc/
square_tiling.rs

1//! Square Tiling (Wang Tiling) problem implementation.
2//!
3//! Given a set C of colors, a collection T of tiles (each with four colored edges:
4//! top, right, bottom, left), and a positive integer N, determine whether there
5//! exists a tiling of an N x N grid using tiles from T such that adjacent tiles
6//! have matching edge colors. Tiles may be reused but not rotated or reflected.
7
8use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
9use crate::traits::Problem;
10use crate::types::Or;
11use serde::de::Error as _;
12use serde::{Deserialize, Deserializer, Serialize};
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "SquareTiling",
17        display_name: "Square Tiling",
18        aliases: &["WangTiling"],
19        dimensions: &[],
20        module_path: module_path!(),
21        description: "Place colored square tiles on an N x N grid with matching edge colors",
22        fields: &[
23            FieldInfo { name: "num_colors", type_name: "usize", description: "Number of colors" },
24            FieldInfo { name: "tiles", type_name: "Vec<(usize, usize, usize, usize)>", description: "Collection of tile types (top, right, bottom, left)" },
25            FieldInfo { name: "grid_size", type_name: "usize", description: "Grid dimension N for N x N tiling" },
26        ],
27    }
28}
29
30inventory::submit! {
31    ProblemSizeFieldEntry {
32        name: "SquareTiling",
33        fields: &["num_colors", "num_tiles", "grid_size"],
34    }
35}
36
37/// A tile with four colored edges: (top, right, bottom, left).
38/// Each color is an index in `0..num_colors`.
39pub type Tile = (usize, usize, usize, usize);
40
41#[derive(Debug, Clone, Serialize)]
42pub struct SquareTiling {
43    num_colors: usize,
44    tiles: Vec<Tile>,
45    grid_size: usize,
46}
47
48impl SquareTiling {
49    fn validate_inputs(num_colors: usize, tiles: &[Tile], grid_size: usize) -> Result<(), String> {
50        if num_colors == 0 {
51            return Err("SquareTiling requires at least one color".to_string());
52        }
53        if tiles.is_empty() {
54            return Err("SquareTiling requires at least one tile".to_string());
55        }
56        if grid_size == 0 {
57            return Err("SquareTiling requires grid_size >= 1".to_string());
58        }
59        for (i, &(top, right, bottom, left)) in tiles.iter().enumerate() {
60            if top >= num_colors
61                || right >= num_colors
62                || bottom >= num_colors
63                || left >= num_colors
64            {
65                return Err(format!(
66                    "Tile {} has color(s) out of range 0..{}",
67                    i, num_colors
68                ));
69            }
70        }
71        Ok(())
72    }
73
74    /// Create a new `SquareTiling` instance, returning an error if inputs are invalid.
75    pub fn try_new(num_colors: usize, tiles: Vec<Tile>, grid_size: usize) -> Result<Self, String> {
76        Self::validate_inputs(num_colors, &tiles, grid_size)?;
77        Ok(Self {
78            num_colors,
79            tiles,
80            grid_size,
81        })
82    }
83
84    /// Create a new `SquareTiling` instance.
85    ///
86    /// # Panics
87    ///
88    /// Panics if `num_colors` is 0, `tiles` is empty, `grid_size` is 0,
89    /// or any tile color is out of range.
90    pub fn new(num_colors: usize, tiles: Vec<Tile>, grid_size: usize) -> Self {
91        Self::try_new(num_colors, tiles, grid_size).unwrap_or_else(|message| panic!("{message}"))
92    }
93
94    /// Number of colors.
95    pub fn num_colors(&self) -> usize {
96        self.num_colors
97    }
98
99    /// Number of tile types.
100    pub fn num_tiles(&self) -> usize {
101        self.tiles.len()
102    }
103
104    /// Grid dimension N (for N x N grid).
105    pub fn grid_size(&self) -> usize {
106        self.grid_size
107    }
108
109    /// The collection of tile types.
110    pub fn tiles(&self) -> &[Tile] {
111        &self.tiles
112    }
113
114    /// Check whether a configuration represents a valid tiling.
115    ///
116    /// The configuration is a flat array of tile indices of length `grid_size^2`,
117    /// laid out in row-major order: position `i * grid_size + j` corresponds
118    /// to grid cell `(i, j)` (row i, column j).
119    fn is_valid_tiling(&self, config: &[usize]) -> bool {
120        let n = self.grid_size;
121        if config.len() != n * n {
122            return false;
123        }
124
125        // Check all tile indices are valid
126        for &tile_idx in config {
127            if tile_idx >= self.tiles.len() {
128                return false;
129            }
130        }
131
132        // Check horizontal adjacency: right of (i,j) must match left of (i,j+1)
133        for i in 0..n {
134            for j in 0..n - 1 {
135                let left_tile = self.tiles[config[i * n + j]];
136                let right_tile = self.tiles[config[i * n + j + 1]];
137                if left_tile.1 != right_tile.3 {
138                    return false;
139                }
140            }
141        }
142
143        // Check vertical adjacency: bottom of (i,j) must match top of (i+1,j)
144        for i in 0..n - 1 {
145            for j in 0..n {
146                let upper_tile = self.tiles[config[i * n + j]];
147                let lower_tile = self.tiles[config[(i + 1) * n + j]];
148                if upper_tile.2 != lower_tile.0 {
149                    return false;
150                }
151            }
152        }
153
154        true
155    }
156}
157
158#[derive(Deserialize)]
159struct SquareTilingData {
160    num_colors: usize,
161    tiles: Vec<Tile>,
162    grid_size: usize,
163}
164
165impl<'de> Deserialize<'de> for SquareTiling {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        let data = SquareTilingData::deserialize(deserializer)?;
171        Self::try_new(data.num_colors, data.tiles, data.grid_size).map_err(D::Error::custom)
172    }
173}
174
175impl Problem for SquareTiling {
176    const NAME: &'static str = "SquareTiling";
177    type Value = Or;
178
179    fn variant() -> Vec<(&'static str, &'static str)> {
180        crate::variant_params![]
181    }
182
183    fn dims(&self) -> Vec<usize> {
184        vec![self.tiles.len(); self.grid_size * self.grid_size]
185    }
186
187    fn evaluate(&self, config: &[usize]) -> Or {
188        Or(self.is_valid_tiling(config))
189    }
190}
191
192crate::declare_variants! {
193    default SquareTiling => "num_tiles^(grid_size^2)",
194}
195
196#[cfg(feature = "example-db")]
197pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
198    vec![crate::example_db::specs::ModelExampleSpec {
199        id: "square_tiling",
200        instance: Box::new(SquareTiling::new(
201            3,
202            vec![(0, 1, 2, 0), (0, 0, 2, 1), (2, 1, 0, 0), (2, 0, 0, 1)],
203            2,
204        )),
205        optimal_config: vec![0, 1, 2, 3],
206        optimal_value: serde_json::json!(true),
207    }]
208}
209
210#[cfg(test)]
211#[path = "../../unit_tests/models/misc/square_tiling.rs"]
212mod tests;