pencil_puzzles_core/view.rs
1//! A rectangular view for browser clients (I4, P10): cells, outside labels
2//! and links between cells.
3/// A cell or link mark offered by a genre.
4#[derive(Clone, Debug)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize))]
6#[non_exhaustive]
7pub struct MarkOption {
8 /// Stable value used when submitting answers.
9 pub value: String,
10 /// Human-readable accessible name.
11 pub name: String,
12 /// Whether the cell is filled rather than displaying a text symbol.
13 pub shaded: bool,
14 /// Symbol drawn in the cell; empty for an unmarked cell.
15 pub symbol: String,
16 /// For a link mark, how many parallel lines to draw (0 for none).
17 pub lines: u32,
18}
19impl MarkOption {
20 /// A shaded cell mark.
21 pub fn shaded(value: &str, name: &str) -> Self {
22 let mut mark = Self::new(value, name, "");
23 mark.shaded = true;
24 mark
25 }
26
27 /// A mark with its accessible name and displayed symbol.
28 pub fn new(value: &str, name: &str, symbol: &str) -> Self {
29 Self {
30 shaded: false,
31 value: value.into(),
32 name: name.into(),
33 symbol: symbol.into(),
34 lines: 0,
35 }
36 }
37
38 /// A link mark drawn as `lines` parallel lines.
39 pub fn link(value: &str, name: &str, lines: u32) -> Self {
40 let mut mark = Self::new(value, name, "");
41 mark.lines = lines;
42 mark
43 }
44}
45/// One cell in row-major order.
46#[derive(Clone, Debug)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize))]
48#[non_exhaustive]
49pub struct ViewCell {
50 /// Current mark or clue value.
51 pub value: String,
52 /// Optional region index for highlighting rule violations.
53 pub region: Option<u32>,
54 /// Whether a player may change the cell.
55 pub editable: bool,
56 /// Whether the value is drawn in a circle, such as a Hashi island.
57 pub circled: bool,
58 /// Text printed in the cell whatever its mark, such as a Hitori number;
59 /// empty for none. Marks are drawn around it.
60 pub label: String,
61 /// Drawn as a block instead of a mark, as Kakuro clue cells are.
62 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
63 pub block: Option<BlockCell>,
64}
65impl ViewCell {
66 /// An editable cell with the given mark.
67 pub fn editable(value: String) -> Self {
68 Self {
69 value,
70 editable: true,
71 region: None,
72 circled: false,
73 label: String::new(),
74 block: None,
75 }
76 }
77
78 /// A block cell that players cannot change, holding `value` in answers.
79 pub fn block(value: String, block: BlockCell) -> Self {
80 Self {
81 editable: false,
82 block: Some(block),
83 ..Self::editable(value)
84 }
85 }
86}
87/// A shaded cell that holds no mark. When either label is non-empty, a
88/// diagonal from the top-left to the bottom-right corner splits it, with
89/// `upper` in the top-right half and `lower` in the bottom-left half.
90#[derive(Clone, Debug)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize))]
92#[non_exhaustive]
93pub struct BlockCell {
94 /// Label in the top-right half; empty for none.
95 pub upper: String,
96 /// Label in the bottom-left half; empty for none.
97 pub lower: String,
98 /// Accessible name, such as `down 4, across 6`.
99 pub name: String,
100}
101impl BlockCell {
102 /// A block with two labels and an accessible name.
103 pub fn new(upper: String, lower: String, name: String) -> Self {
104 Self { upper, lower, name }
105 }
106}
107/// A straight link between the centres of two cells in the same row or
108/// column, such as a Hashi bridge.
109#[derive(Clone, Debug)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize))]
111#[non_exhaustive]
112pub struct ViewLink {
113 /// The link's two cells; the first comes first in row-major order.
114 pub place: crate::Place,
115 /// Current mark, one of [`View::link_marks`].
116 pub value: String,
117 /// Whether a player may change the link.
118 pub editable: bool,
119}
120impl ViewLink {
121 /// An editable link between two cells, given as (row, column).
122 pub fn editable(from: (u32, u32), to: (u32, u32), value: String) -> Self {
123 Self {
124 place: crate::Place::Link {
125 row: from.0,
126 col: from.1,
127 row2: to.0,
128 col2: to.1,
129 },
130 value,
131 editable: true,
132 }
133 }
134}
135/// A rectangular board with labels outside its four sides and links
136/// between cells (I4).
137///
138/// Clients send marks back as the cell values in row-major order followed
139/// by the link values in order. Further layers, such as cell borders, can be
140/// added when a genre needs them.
141#[derive(Clone, Debug, Default)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143#[non_exhaustive]
144pub struct View {
145 /// Number of rows.
146 pub rows: u32,
147 /// Number of columns.
148 pub cols: u32,
149 /// Cells in row-major order.
150 pub cells: Vec<ViewCell>,
151 /// Column labels above the board.
152 pub top: Vec<String>,
153 /// Row clues to the right.
154 pub right: Vec<String>,
155 /// Column clues below the board.
156 pub bottom: Vec<String>,
157 /// Row labels to the left.
158 pub left: Vec<String>,
159 /// Available marks in cycling order, unmarked first.
160 pub marks: Vec<MarkOption>,
161 /// Links between cells, in the genre's order.
162 pub links: Vec<ViewLink>,
163 /// Available link marks in cycling order, unmarked first.
164 pub link_marks: Vec<MarkOption>,
165 /// Row indices with a strong boundary before them.
166 pub row_separators: Vec<u32>,
167 /// Column indices with a strong boundary before them.
168 pub col_separators: Vec<u32>,
169 /// What happens to unmarked cells when checking a finished answer.
170 pub finish_note: String,
171 /// Short instructions for the displayed rules, one rule per line.
172 pub instructions: String,
173}
174
175/// A numeric clue input in a visual puzzle editor.
176#[derive(Clone, Debug)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize))]
178#[non_exhaustive]
179pub struct ClueInput {
180 /// The cell, row or column carrying the clue.
181 pub place: crate::Place,
182 /// Current text, empty when no clue has been entered.
183 pub value: String,
184 /// Smallest accepted clue.
185 pub min: u32,
186 /// Largest accepted clue.
187 pub max: u32,
188 /// Whether an empty field is a valid absent clue.
189 pub optional: bool,
190 /// For a text field instead of a number: the pattern a value must match
191 /// (as in HTML), with `min` and `max` unused.
192 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
193 pub pattern: Option<String>,
194}
195impl ClueInput {
196 /// A bounded numeric clue input.
197 pub fn new(place: crate::Place, value: String, min: u32, max: u32, optional: bool) -> Self {
198 Self {
199 place,
200 value,
201 min,
202 max,
203 optional,
204 pattern: None,
205 }
206 }
207
208 /// A text field whose value must match `pattern` (as in HTML).
209 pub fn text(place: crate::Place, value: String, pattern: &str, optional: bool) -> Self {
210 Self {
211 place,
212 value,
213 min: 0,
214 max: 0,
215 optional,
216 pattern: Some(pattern.into()),
217 }
218 }
219}
220/// A visual clue editor supplied by a genre, independent of browser code.
221#[derive(Clone, Debug)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223#[non_exhaustive]
224pub struct PuzzleEditor {
225 /// Stable shape encoding used to submit the fields.
226 pub shape: String,
227 /// Background grid, labels and region boundaries.
228 pub view: View,
229 /// Inputs in the genre's canonical order.
230 pub fields: Vec<ClueInput>,
231}
232impl PuzzleEditor {
233 /// A visual clue editor for a validated shape.
234 pub fn new(shape: String, view: View, fields: Vec<ClueInput>) -> Self {
235 Self {
236 shape,
237 view,
238 fields,
239 }
240 }
241}