Skip to content

Commit d8f3913

Browse files
authored
Add solver for Rail Pool (#244)
1 parent 869ead2 commit d8f3913

4 files changed

Lines changed: 402 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
@@ -111,6 +111,7 @@ pub mod polyominous;
111111
pub mod putteria;
112112
pub mod pyramid;
113113
pub mod pyramid_climbers;
114+
pub mod railpool;
114115
pub mod reflect;
115116
pub mod ringring;
116117
pub mod ripple;
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
use cspuz_rs::graph;
2+
use cspuz_rs::serializer::{
3+
url_to_problem, Choice2, ContextBasedGrid, Dict, Map, MultiDigit, Optionalize, Rooms, Size,
4+
Tuple3,
5+
};
6+
use cspuz_rs::serializer::{Combinator, Context};
7+
use cspuz_rs::solver::{count_true, Solver};
8+
9+
pub fn solve_railpool(
10+
borders: &graph::InnerGridEdges<Vec<Vec<bool>>>,
11+
clues: &[Vec<Vec<i32>>],
12+
holes: &Option<Vec<Vec<bool>>>,
13+
) -> Option<graph::BoolGridEdgesIrrefutableFacts> {
14+
let h = borders.vertical.len();
15+
assert!(h > 0);
16+
let w = borders.vertical[0].len() + 1;
17+
18+
let mut solver = Solver::new();
19+
let is_line = &graph::BoolGridEdges::new(&mut solver, (h - 1, w - 1));
20+
solver.add_answer_key_bool(&is_line.horizontal);
21+
solver.add_answer_key_bool(&is_line.vertical);
22+
23+
let visited = &graph::single_cycle_grid_edges(&mut solver, is_line);
24+
let mut borders = borders.clone();
25+
if let Some(holes) = holes {
26+
for y in 0..h {
27+
for x in 0..w {
28+
if holes[y][x] {
29+
solver.add_expr(!visited.at((y, x)));
30+
if y > 0 {
31+
borders.horizontal[y - 1][x] = true;
32+
}
33+
if y < h - 1 {
34+
borders.horizontal[y][x] = true;
35+
}
36+
if x > 0 {
37+
borders.vertical[y][x - 1] = true;
38+
}
39+
if x < w - 1 {
40+
borders.vertical[y][x] = true;
41+
}
42+
} else {
43+
solver.add_expr(visited.at((y, x)));
44+
}
45+
}
46+
}
47+
} else {
48+
solver.add_expr(visited);
49+
}
50+
51+
let horizontal_len = &solver.int_var_2d((h, w), 0, (w - 1) as i32);
52+
let vertical_len = &solver.int_var_2d((h, w), 0, (h - 1) as i32);
53+
for y in 0..h {
54+
for x in 0..w {
55+
solver.add_expr(
56+
horizontal_len.at((y, x)).eq(is_line
57+
.horizontal
58+
.slice_fixed_y((y, x..))
59+
.consecutive_prefix_true()
60+
+ is_line
61+
.horizontal
62+
.slice_fixed_y((y, ..x))
63+
.reverse()
64+
.consecutive_prefix_true()),
65+
);
66+
solver.add_expr(
67+
vertical_len.at((y, x)).eq(is_line
68+
.vertical
69+
.slice_fixed_x((y.., x))
70+
.consecutive_prefix_true()
71+
+ is_line
72+
.vertical
73+
.slice_fixed_x((..y, x))
74+
.reverse()
75+
.consecutive_prefix_true()),
76+
);
77+
}
78+
}
79+
80+
let max_length = (h.max(w) as i32 - 1) as usize;
81+
let rooms = graph::borders_to_rooms(&borders);
82+
for room in &rooms {
83+
for &(y, x) in room {
84+
let clue = &clues[y][x];
85+
if clue.is_empty() {
86+
continue;
87+
}
88+
let lengths = &solver.bool_var_1d(max_length + 1);
89+
for i in 1..=max_length {
90+
solver.add_expr(lengths.at(i).iff(
91+
horizontal_len.select(room).eq(i as i32).any()
92+
| vertical_len.select(room).eq(i as i32).any(),
93+
));
94+
}
95+
96+
let mut n_question = 0;
97+
for &c in clue {
98+
if c == 0 {
99+
n_question += 1;
100+
}
101+
}
102+
103+
if n_question == 0 {
104+
for i in 1..=max_length {
105+
solver.add_expr(lengths.at(i).iff(clue.contains(&(i as i32))));
106+
}
107+
} else {
108+
let mut other = vec![];
109+
for i in 1..=max_length {
110+
if clue.contains(&(i as i32)) {
111+
solver.add_expr(lengths.at(i));
112+
} else {
113+
other.push(lengths.at(i));
114+
}
115+
}
116+
solver.add_expr(count_true(other).eq(n_question));
117+
}
118+
}
119+
}
120+
121+
solver.irrefutable_facts().map(|f| f.get(is_line))
122+
}
123+
124+
pub struct RailpoolClueCombinator;
125+
126+
impl Combinator<Vec<Vec<Vec<i32>>>> for RailpoolClueCombinator {
127+
fn serialize(&self, _ctx: &Context, _input: &[Vec<Vec<Vec<i32>>>]) -> Option<(usize, Vec<u8>)> {
128+
unimplemented!()
129+
}
130+
131+
fn deserialize(&self, ctx: &Context, input: &[u8]) -> Option<(usize, Vec<Vec<Vec<Vec<i32>>>>)> {
132+
let height = ctx.height.unwrap();
133+
let width = ctx.width.unwrap();
134+
135+
let mut result = vec![vec![vec![]; width]; height];
136+
let mut pos: usize = 0;
137+
let mut idx: usize = 0;
138+
139+
while idx < input.len() {
140+
if input[idx] == b'/' || pos >= height * width {
141+
break;
142+
}
143+
144+
let c = input[idx];
145+
if b'0' <= c && c <= b'9' {
146+
let y = pos / width;
147+
let x = pos % width;
148+
result[y][x].push((c - b'0') as i32);
149+
idx += 1;
150+
} else if c >= b'k' && c <= b'z' {
151+
let advance = (c - b'k' + 1) as usize;
152+
pos += advance;
153+
idx += 1;
154+
} else {
155+
break;
156+
}
157+
}
158+
159+
Some((idx, vec![result]))
160+
}
161+
}
162+
163+
type Problem = (
164+
Vec<Vec<Vec<i32>>>,
165+
graph::InnerGridEdges<Vec<Vec<bool>>>,
166+
Option<Vec<Vec<bool>>>,
167+
);
168+
169+
fn combinator() -> impl Combinator<Problem> {
170+
Size::new(Tuple3::new(
171+
RailpoolClueCombinator,
172+
Rooms,
173+
Choice2::new(
174+
Optionalize::new(ContextBasedGrid::new(Map::new(
175+
MultiDigit::new(2, 5),
176+
|x: bool| Some(if x { 1 } else { 0 }),
177+
|n: i32| Some(n == 1),
178+
))),
179+
Dict::new(None, ""),
180+
),
181+
))
182+
}
183+
184+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
185+
url_to_problem(combinator(), &["railpool"], url)
186+
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::*;
191+
192+
fn problem_for_tests() -> Problem {
193+
let mut clues = vec![vec![vec![]; 7]; 5];
194+
clues[0][0] = vec![0];
195+
clues[3][1] = vec![0, 0];
196+
clues[3][6] = vec![2, 0];
197+
198+
let borders = graph::InnerGridEdges {
199+
horizontal: crate::util::tests::to_bool_2d([
200+
[0, 0, 0, 0, 0, 0, 0],
201+
[1, 0, 0, 0, 0, 0, 0],
202+
[0, 1, 0, 0, 0, 0, 1],
203+
[0, 0, 0, 0, 0, 0, 0],
204+
]),
205+
vertical: crate::util::tests::to_bool_2d([
206+
[1, 0, 0, 0, 0, 0],
207+
[1, 0, 0, 0, 0, 0],
208+
[0, 0, 0, 0, 0, 0],
209+
[1, 1, 0, 0, 0, 1],
210+
[0, 1, 0, 0, 0, 1],
211+
]),
212+
};
213+
214+
let holes = Some(crate::util::tests::to_bool_2d([
215+
[0, 0, 0, 0, 0, 0, 0],
216+
[0, 0, 0, 0, 0, 0, 0],
217+
[0, 0, 0, 0, 0, 0, 0],
218+
[0, 0, 0, 0, 0, 0, 0],
219+
[1, 0, 0, 0, 0, 0, 0],
220+
]));
221+
222+
(clues, borders, holes)
223+
}
224+
225+
#[test]
226+
fn test_railpool_problem() {
227+
let (clues, borders, holes) = problem_for_tests();
228+
let ans = solve_railpool(&borders, &clues, &holes);
229+
assert!(ans.is_some());
230+
let ans = ans.unwrap();
231+
232+
let expected = graph::BoolGridEdgesIrrefutableFacts {
233+
horizontal: crate::util::tests::to_option_bool_2d([
234+
[1, 1, 1, 0, 1, 1],
235+
[0, 1, 1, 0, 0, 1],
236+
[0, 1, 0, 1, 0, 1],
237+
[1, 0, 1, 0, 1, 0],
238+
[0, 1, 1, 1, 0, 1],
239+
]),
240+
vertical: crate::util::tests::to_option_bool_2d([
241+
[1, 0, 0, 1, 1, 0, 1],
242+
[1, 1, 0, 0, 1, 1, 0],
243+
[1, 0, 1, 1, 0, 0, 1],
244+
[0, 1, 0, 0, 1, 1, 1],
245+
]),
246+
};
247+
assert_eq!(ans, expected);
248+
}
249+
250+
#[test]
251+
fn test_railpool_serializer() {
252+
let problem = problem_for_tests();
253+
let url = "https://puzz.link/p?railpool/7/5/0zp00o20rg8032h040gg00000020";
254+
assert_eq!(deserialize_problem(url).unwrap(), problem);
255+
}
256+
}

cspuz_solver_backend/src/puzzle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ puzzle_list!(puzz_link,
182182
(pencils, ["pencils"], "Pencils", "ペンシルズ"),
183183
(pentominous, ["pentominous"], "Pentominous", "Pentominous"),
184184
(putteria, ["putteria"], "Putteria", "プッテリア"),
185+
(railpool, ["railpool"], "Rail Pool", "Rail Pool"),
185186
(reflect, ["reflect"], "Reflect Link", "リフレクトリンク"),
186187
(ringring, ["ringring"], "Ring-Ring", "リングリング"),
187188
(ripple, ["ripple"], "Ripple Effect", "波及効果"),

0 commit comments

Comments
 (0)