Skip to content

Commit 67df189

Browse files
authored
Add solver for Pyramid (#169)
1 parent 20a1069 commit 67df189

4 files changed

Lines changed: 259 additions & 0 deletions

File tree

cspuz_rs_puzzles/src/puzzles/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub mod parrot_loop;
9292
pub mod pencils;
9393
pub mod polyominous;
9494
pub mod putteria;
95+
pub mod pyramid;
9596
pub mod reflect;
9697
pub mod ringring;
9798
pub mod ripple;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use crate::penpa_editor::{decode_penpa_editor_url, Item, PenpaEditorPuzzle};
2+
use cspuz_rs::solver::{any, Solver};
3+
4+
pub fn solve_pyramid(
5+
is_shaded: &[bool],
6+
clues: &[Vec<Option<i32>>],
7+
min_value: i32,
8+
max_value: i32,
9+
) -> Option<Vec<Vec<Option<i32>>>> {
10+
let n = clues.len();
11+
12+
let mut solver = Solver::new();
13+
let mut ans = vec![];
14+
for i in 0..n {
15+
ans.push(solver.int_var_1d(i + 1, min_value, max_value));
16+
solver.add_answer_key_int(&ans[i]);
17+
}
18+
19+
for i in 0..n {
20+
assert_eq!(clues[i].len(), i + 1);
21+
for j in 0..=i {
22+
if let Some(n) = clues[i][j] {
23+
solver.add_expr(ans[i].at(j).eq(n));
24+
}
25+
}
26+
}
27+
28+
for i in 0..n {
29+
if is_shaded[i] {
30+
for x1 in 0..=i {
31+
for x2 in 0..x1 {
32+
solver.add_expr(ans[i].at(x1).ne(ans[i].at(x2)));
33+
}
34+
}
35+
} else {
36+
let mut cond = vec![];
37+
for x1 in 0..=i {
38+
for x2 in 0..x1 {
39+
cond.push(ans[i].at(x1).eq(ans[i].at(x2)));
40+
}
41+
}
42+
solver.add_expr(any(cond));
43+
}
44+
}
45+
46+
for i in 0..(n - 1) {
47+
for j in 0..=i {
48+
solver.add_expr(
49+
ans[i].at(j).eq(ans[i + 1].at(j) + ans[i + 1].at(j + 1))
50+
| ans[i].at(j).eq(ans[i + 1].at(j) - ans[i + 1].at(j + 1))
51+
| ans[i].at(j).eq(ans[i + 1].at(j + 1) - ans[i + 1].at(j)),
52+
);
53+
}
54+
}
55+
56+
solver.irrefutable_facts().map(|f| {
57+
let mut result = vec![];
58+
for i in 0..n {
59+
result.push(f.get(&ans[i]));
60+
}
61+
result
62+
})
63+
}
64+
65+
type Problem = (Vec<bool>, Vec<Vec<Option<i32>>>, i32, i32);
66+
67+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
68+
let decoded = decode_penpa_editor_url(url).ok()?;
69+
let decoded = match decoded {
70+
PenpaEditorPuzzle::Pyramid(p) => p,
71+
_ => return None,
72+
};
73+
74+
let size = decoded.size();
75+
let mut n_shaded = vec![0; size];
76+
let mut clues = vec![];
77+
for i in 0..size {
78+
clues.push(vec![None; i + 1]);
79+
}
80+
let mut min_value = 1;
81+
let mut max_value = 9;
82+
83+
for item in decoded.get_outside() {
84+
if let Item::Text(text) = item {
85+
// Parses min-max value like "[1-9]" or "[2~8]"
86+
if let Some(s) = text
87+
.text
88+
.strip_prefix('[')
89+
.and_then(|s| s.strip_suffix(']'))
90+
{
91+
let parts: Vec<&str> = s.split(|c| c == '-' || c == '~' || c == '~').collect();
92+
if parts.len() == 2 {
93+
if let Ok(min_v) = parts[0].parse::<i32>() {
94+
if let Ok(max_v) = parts[1].parse::<i32>() {
95+
min_value = min_v;
96+
max_value = max_v;
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
103+
104+
for y in 0..size {
105+
for x in 0..=y {
106+
let mut is_shaded = false;
107+
108+
for item in decoded.get_cell(y, x) {
109+
if let &Item::Fill(fill) = item {
110+
if fill == 1 || fill == 3 || fill == 8 {
111+
is_shaded = true;
112+
}
113+
} else if let Item::Text(text) = item {
114+
if let Ok(n) = text.text.parse::<i32>() {
115+
clues[y][x] = Some(n);
116+
}
117+
}
118+
}
119+
120+
n_shaded[y] += if is_shaded { 1 } else { 0 };
121+
}
122+
}
123+
124+
let mut is_shaded = vec![false; size];
125+
for i in 0..size {
126+
if n_shaded[i] == 0 {
127+
is_shaded[i] = false;
128+
} else if n_shaded[i] == i + 1 {
129+
is_shaded[i] = true;
130+
} else {
131+
return None;
132+
}
133+
}
134+
135+
Some((is_shaded, clues, min_value, max_value))
136+
}
137+
138+
#[cfg(test)]
139+
mod tests {
140+
use super::*;
141+
142+
fn problem_for_tests() -> Problem {
143+
// https://puzsq.logicpuzzle.app/puzzle/166721
144+
let is_shaded = vec![true, false, true, false];
145+
let clues = vec![
146+
vec![None],
147+
vec![None, None],
148+
vec![None, None, None],
149+
vec![Some(5), Some(3), Some(1), None],
150+
];
151+
let min_value = 1;
152+
let max_value = 7;
153+
(is_shaded, clues, min_value, max_value)
154+
}
155+
156+
#[test]
157+
fn test_pyramid_problem() {
158+
let (is_shaded, clues, min_value, max_value) = problem_for_tests();
159+
let ans = solve_pyramid(&is_shaded, &clues, min_value, max_value);
160+
assert!(ans.is_some());
161+
let ans = ans.unwrap();
162+
163+
let expected = vec![
164+
vec![Some(4)],
165+
vec![Some(2), Some(2)],
166+
vec![Some(2), Some(4), Some(6)],
167+
vec![Some(5), Some(3), Some(1), Some(5)],
168+
];
169+
assert_eq!(ans, expected);
170+
}
171+
172+
#[test]
173+
fn test_exercise_serializer() {
174+
let problem = problem_for_tests();
175+
let url = "https://opt-pan.github.io/penpa-edit/?m=solve&p=vVNNb5tAEL3zK6o5byUDBjd7c9O6l9T9chVFK2StbRKjgHEXaFws57dnZsC1F1ypl1ZoH4/HwDyWedtfRmfJSoR4BAMxEC4ePjJaXjDkRTods6RMY/lKjKtynRskQnyaTMS9TovYUW7kKHBBgIfLhei5nj6z4EXOvv4q9/Vcqugg6u8n+uZEv8k9DAOQvoBw2JzaqxBPWDDFgpEPUkEAaJJaCBhhqQL/TMCHuOtR8H0uUe7rUcRqCNTOlXvEO8YJo8c4Qzei9hnfMQ4YA8YbrnnPeMt4zThkDLlmRN/jOGoYiCvsR8sVI0baItwMKPJ0XlTmXi9jkLx9grVNlS1iA7I0Vaukeb5Nk41dljxschNfvEVivHq4VL/IzYpefnbjSaepJRQ/Km3sh5eJWaa2VJrEutbG5E+WkulybQkLXeLsFOtka78p3pS2gVLbFvWj7nTLTt98cGAHvJQnvFB4uMH7+krWY1F/aMbgOI2i/oKz9lHWUxo1BSB8/p/thNJP/U1v+T6x63aWBsinyEPkSO+QNvsyv2mUz1LVMwHU5y0/TRSy/CdabXzQ9TLPFvgxCui/7VqxqFb5Y3WcWprNcccpNWidkunWKdHGKbGu0/ZT/p3Tq+jQbP/gL9PdRPg/RG/XJiw3F0OG8jFntnoxUK3eyxTqvfRQw36AUL2QIVS7MUKpnyQUe2FC7Q95ord2I0WuuqmiVr1gUavzbKnIeQE=&a=RcvBCUAxDALQXTx7+kQ7TMj+a6T5ORSEh4iZxReEGCAU/Ab9eJt3s+nLMYV5VQM=";
176+
assert_eq!(deserialize_problem(url).unwrap(), problem);
177+
}
178+
}

cspuz_solver_backend/src/puzzle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ puzzle_list!(kudamono,
232232
puzzle_list!(penpa_edit,
233233
(castle_walker, ["castle_walker"], "Castle Walker", "Castle Walker"),
234234
(exercise, ["exercise"], "Exercise", "Exercise"),
235+
(pyramid, ["pyramid"], "Pyramid", "ピラミッド"),
235236
);
236237

237238
pub mod double_lits;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use crate::board::{Board, BoardKind, Item, ItemKind};
2+
use crate::uniqueness::is_unique;
3+
use cspuz_rs_puzzles::puzzles::pyramid;
4+
5+
pub fn solve(url: &str) -> Result<Board, &'static str> {
6+
let (is_shaded, clues, min_value, max_value) =
7+
pyramid::deserialize_problem(url).ok_or("invalid url")?;
8+
let ans =
9+
pyramid::solve_pyramid(&is_shaded, &clues, min_value, max_value).ok_or("no answer")?;
10+
let size = ans.len();
11+
12+
let mut board = Board::new(BoardKind::Empty, size, size * 2, is_unique(&ans.concat()));
13+
14+
// Fills
15+
for y in 0..size {
16+
if is_shaded[y] {
17+
for x in 0..=y {
18+
board.push(Item::cell(
19+
y,
20+
size - y - 1 + 2 * x,
21+
"#cccccc",
22+
ItemKind::Fill,
23+
));
24+
board.push(Item::cell(
25+
y,
26+
size - y - 1 + 2 * x + 1,
27+
"#cccccc",
28+
ItemKind::Fill,
29+
));
30+
}
31+
}
32+
}
33+
34+
// Borders
35+
for y in 0..=size {
36+
let start = if y == size { 0 } else { size - y - 1 };
37+
let end = if y == size { size * 2 } else { size + y + 1 };
38+
39+
for x in start..end {
40+
board.push(Item {
41+
y: y * 2,
42+
x: x * 2 + 1,
43+
color: "black",
44+
kind: ItemKind::BoldWall,
45+
});
46+
}
47+
}
48+
for y in 0..size {
49+
for x in 0..=(y + 1) {
50+
board.push(Item {
51+
y: y * 2 + 1,
52+
x: (size - y - 1 + 2 * x) * 2,
53+
color: "black",
54+
kind: ItemKind::BoldWall,
55+
});
56+
}
57+
}
58+
59+
for y in 0..size {
60+
for x in 0..=y {
61+
if let Some(n) = clues[y][x] {
62+
board.push(Item {
63+
y: 2 * y + 1,
64+
x: (size - y - 1 + 2 * x + 1) * 2,
65+
color: "black",
66+
kind: ItemKind::Num(n),
67+
});
68+
} else if let Some(n) = ans[y][x] {
69+
board.push(Item {
70+
y: 2 * y + 1,
71+
x: (size - y - 1 + 2 * x + 1) * 2,
72+
color: "green",
73+
kind: ItemKind::Num(n),
74+
});
75+
}
76+
}
77+
}
78+
Ok(board)
79+
}

0 commit comments

Comments
 (0)