Skip to content

Commit dbf68da

Browse files
authored
Add solver for Pyramid Climbers (#170)
* Add solver for Pyramid Climbers * fix
1 parent 67df189 commit dbf68da

5 files changed

Lines changed: 237 additions & 1 deletion

File tree

cspuz_rs_puzzles/src/puzzles/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub mod pencils;
9393
pub mod polyominous;
9494
pub mod putteria;
9595
pub mod pyramid;
96+
pub mod pyramid_climbers;
9697
pub mod reflect;
9798
pub mod ringring;
9899
pub mod ripple;

cspuz_rs_puzzles/src/puzzles/pyramid.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ mod tests {
170170
}
171171

172172
#[test]
173-
fn test_exercise_serializer() {
173+
fn test_pyramid_serializer() {
174174
let problem = problem_for_tests();
175175
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=";
176176
assert_eq!(deserialize_problem(url).unwrap(), problem);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use crate::penpa_editor::{decode_penpa_editor_url, Item, PenpaEditorPuzzle};
2+
use cspuz_rs::solver::Solver;
3+
4+
pub fn solve_pyramid_climbers(clues: &[Vec<String>]) -> Option<Vec<Vec<Option<bool>>>> {
5+
let n = clues.len();
6+
7+
let mut solver = Solver::new();
8+
let mut ans = vec![];
9+
for i in 0..(n - 1) {
10+
ans.push(solver.bool_var_1d(2 * i + 2));
11+
solver.add_answer_key_bool(&ans[i]);
12+
}
13+
14+
let mut seq = vec![];
15+
for i in 0..n {
16+
seq.push(solver.int_var_1d(i + 1, 0, n as i32 - 1));
17+
}
18+
19+
for j in 0..n {
20+
solver.add_expr(seq[n - 1].at(j).eq(j as i32));
21+
}
22+
23+
for i in 0..(n - 1) {
24+
for j in 0..=i {
25+
solver.add_expr(ans[i].at(j * 2) ^ ans[i].at(j * 2 + 1));
26+
if j > 0 {
27+
solver.add_expr(!(ans[i].at(j * 2) & ans[i].at(j * 2 - 1)));
28+
}
29+
solver.add_expr(ans[i].at(j * 2).imp(seq[i].at(j).eq(seq[i + 1].at(j))));
30+
solver.add_expr(
31+
ans[i]
32+
.at(j * 2 + 1)
33+
.imp(seq[i].at(j).eq(seq[i + 1].at(j + 1))),
34+
);
35+
}
36+
}
37+
38+
for i1 in 0..(n - 1) {
39+
for i2 in (i1 + 1)..n {
40+
for j1 in 0..=i1 {
41+
for j2 in j1..=i2 {
42+
if i2 - i1 < j2 - j1 {
43+
continue;
44+
}
45+
46+
if clues[i1][j1] == clues[i2][j2] {
47+
solver.add_expr(seq[i1].at(j1).ne(seq[i2].at(j2)));
48+
}
49+
}
50+
}
51+
}
52+
}
53+
54+
solver.irrefutable_facts().map(|f| {
55+
let mut result = vec![];
56+
for i in 0..(n - 1) {
57+
result.push(f.get(&ans[i]));
58+
}
59+
result
60+
})
61+
}
62+
63+
type Problem = Vec<Vec<String>>;
64+
65+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
66+
let decoded = decode_penpa_editor_url(url).ok()?;
67+
let decoded = match decoded {
68+
PenpaEditorPuzzle::Pyramid(p) => p,
69+
_ => return None,
70+
};
71+
72+
let size = decoded.size();
73+
74+
let mut clues = vec![];
75+
for i in 0..size {
76+
clues.push(vec![String::new(); i + 1]);
77+
}
78+
79+
for y in 0..size {
80+
for x in 0..=y {
81+
for item in decoded.get_cell(y, x) {
82+
if let Item::Text(text) = item {
83+
clues[y][x] = text.text.clone();
84+
}
85+
}
86+
}
87+
}
88+
Some(clues)
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use crate::util;
94+
95+
use super::*;
96+
97+
fn problem_for_tests() -> Problem {
98+
// https://puzsq.logicpuzzle.app/puzzle/166598
99+
let base = vec![
100+
vec!["A"],
101+
vec!["G", "F"],
102+
vec!["B", "B", "C"],
103+
vec!["B", "A", "C", "D"],
104+
vec!["C", "D", "D", "D", "E"],
105+
vec!["A", "B", "C", "D", "E", "F"],
106+
];
107+
base.iter()
108+
.map(|row| row.iter().map(|&s| s.to_string()).collect())
109+
.collect()
110+
}
111+
112+
#[test]
113+
fn test_pyramid_climbers_problem() {
114+
let clues = problem_for_tests();
115+
let ans = solve_pyramid_climbers(&clues);
116+
assert!(ans.is_some());
117+
let ans = ans.unwrap();
118+
119+
let expected = util::tests::to_option_bool_2d(vec![
120+
vec![0, 1],
121+
vec![1, 0, 1, 0],
122+
vec![0, 1, 0, 1, 0, 1],
123+
vec![1, 0, 0, 1, 0, 1, 0, 1],
124+
vec![1, 0, 1, 0, 1, 0, 0, 1, 0, 1],
125+
]);
126+
assert_eq!(ans, expected);
127+
}
128+
129+
#[test]
130+
fn test_pyramid_climbers_serializer() {
131+
let problem = problem_for_tests();
132+
let url = "https://opt-pan.github.io/penpa-edit/#m=solve&p=vVRtb6pKEP7ur2j2aze5gCJKcj6g1b7c1toejbcSY1BRacHtQbAW0/72zgy2LGib3OTmhuw4PjM7b7vPPr+GTuDNeBU+XeEKV+Erg4ZL1Su0EMev50W+a5500z0nTd8LJm645lYcLUUIBm/1dCHEzHM557ftNp87/trlVw/L66awXs6sfza1aDhUz5X4Uhk8th9P74O/L71yqLY7te5N98bTFtZFs3FXbZ1Wu/G6H7mbu0BtPPaHvXl3sKhrr63OsJIMbxX9ajj/a2P1f5VsZVSymco402CpbPTOpiKYeOzdZr63csWW8fKotEvuzV0yNu3RG0/6mVrL1N/mDmTH3DFNZ6bNLAazwIiclSsInEsAebQzoEIeDQkgDxmoItDMAL1c8NAphpRWpxjyFopxlgFViiF5VCmG7EExZOAghoFAKwMMrVCHUazUoCxSWqOYxaAsclDK8jUxmLRK834g2SapkezBcfCkTPKMpEJSJ3lNPi2SA5JNkhWSVfIx8EBLJVvTeR3y4VK5QTL7Vbn+palwh+COsLXwx+s4nDtTd+xunWnEzPQOy5YctoqRAznIF+IZb96RCJ8mZkZhvMe8xUqEbmYpuLuzxXeR0JQD01ATEc4KJb04vp9v5U/shPnNUy+c+nkoCr3cfycMxUsOCZxomQMmTgSPxHrpPecjuavCLCMnX6Lz5BSyBdk43kpsy2jZZa7hce2SuplYPDmHSyWRnyd3QOgbM+kgn23GeA3uXhD7kTcVvoCUiKl0j2ijBmorUwdkR60JGgRVFdA7qQNuewA1ndT4OkW6pp30OMPcDdqNKgvEBopPa8P/6ZsEAB4yPUnQZDwTT/HeS0VKWFT83vmzA3T9oQMwf3aAatoBakc6wMb+kw7SV7XQQn30lh6U8q+e2//hKdjuuS3CH+idGYvwEZID+gPPJesx/BtOS9YifkBgLPaQw4AeoTGgRSYDdEhmAA/4DNg3lMaoRVZjVUViY6oDbmMqmd72qPQB&a=RY7BDQQhDAN74e3PksQpBtF/G/gw0kk8RpPxatfaWGMWovANjEhkmgppJ1F2EvUcUbxUAYYpQV8V8V0J9iVFPU2J9kJ5+8vK2wvl/Vvov/5vHw==";
133+
assert_eq!(deserialize_problem(url).unwrap(), problem);
134+
}
135+
}

cspuz_solver_backend/src/puzzle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ puzzle_list!(penpa_edit,
233233
(castle_walker, ["castle_walker"], "Castle Walker", "Castle Walker"),
234234
(exercise, ["exercise"], "Exercise", "Exercise"),
235235
(pyramid, ["pyramid"], "Pyramid", "ピラミッド"),
236+
(pyramid_climbers, ["pyramid_climbers"], "Pyramid Climbers", "Pyramid Climbers"),
236237
);
237238

238239
pub mod double_lits;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use crate::board::{Board, BoardKind, Item, ItemKind};
2+
use crate::uniqueness::is_unique;
3+
use cspuz_rs_puzzles::puzzles::pyramid_climbers;
4+
5+
pub fn solve(url: &str) -> Result<Board, &'static str> {
6+
let clues = pyramid_climbers::deserialize_problem(url).ok_or("invalid url")?;
7+
let ans = pyramid_climbers::solve_pyramid_climbers(&clues).ok_or("no answer")?;
8+
let size = ans.len() + 1;
9+
10+
let mut board = Board::new(BoardKind::Empty, size, size * 2, is_unique(&ans.concat()));
11+
12+
// Clues
13+
for y in 0..size {
14+
for x in 0..=y {
15+
board.push(Item {
16+
y: 2 * y + 1,
17+
x: (size - y - 1 + 2 * x + 1) * 2,
18+
color: "black",
19+
kind: ItemKind::TextString(clues[y][x].clone()),
20+
});
21+
}
22+
}
23+
24+
// Borders
25+
for y in 0..=size {
26+
let start = if y == size { 0 } else { size - y - 1 };
27+
let end = if y == size { size * 2 } else { size + y + 1 };
28+
29+
for x in start..end {
30+
board.push(Item {
31+
y: y * 2,
32+
x: x * 2 + 1,
33+
color: "black",
34+
kind: ItemKind::BoldWall,
35+
});
36+
}
37+
}
38+
for y in 0..size {
39+
for x in 0..=(y + 1) {
40+
board.push(Item {
41+
y: y * 2 + 1,
42+
x: (size - y - 1 + 2 * x) * 2,
43+
color: "black",
44+
kind: ItemKind::BoldWall,
45+
});
46+
}
47+
}
48+
49+
// Answers
50+
for y in 0..(size - 1) {
51+
for x in 0..=y {
52+
match ans[y][x * 2] {
53+
Some(true) => {
54+
board.push(Item {
55+
y: 2 * y + 1,
56+
x: (size - y - 1 + 2 * x) * 2 + 2,
57+
color: "green",
58+
kind: ItemKind::LineTo(
59+
(2 * y + 3) as i32,
60+
((size - y - 1 + 2 * x) * 2) as i32,
61+
),
62+
});
63+
}
64+
Some(false) => {
65+
board.push(Item {
66+
y: 2 * y + 2,
67+
x: (size - y - 1 + 2 * x) * 2 + 1,
68+
color: "green",
69+
kind: ItemKind::Cross,
70+
});
71+
}
72+
None => (),
73+
}
74+
match ans[y][x * 2 + 1] {
75+
Some(true) => {
76+
board.push(Item {
77+
y: 2 * y + 1,
78+
x: (size - y - 1 + 2 * x) * 2 + 2,
79+
color: "green",
80+
kind: ItemKind::LineTo(
81+
(2 * y + 3) as i32,
82+
((size - y - 1 + 2 * x + 2) * 2) as i32,
83+
),
84+
});
85+
}
86+
Some(false) => {
87+
board.push(Item {
88+
y: 2 * y + 2,
89+
x: (size - y - 1 + 2 * x + 1) * 2 + 1,
90+
color: "green",
91+
kind: ItemKind::Cross,
92+
});
93+
}
94+
None => (),
95+
}
96+
}
97+
}
98+
Ok(board)
99+
}

0 commit comments

Comments
 (0)