Skip to content

Commit 1c9037a

Browse files
authored
Added nuriloop (#235)
* Added nuriloop * Forgot some constraints * Parity optimization
1 parent 0d76a01 commit 1c9037a

4 files changed

Lines changed: 228 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
@@ -102,6 +102,7 @@ pub mod nothing;
102102
pub mod nothree;
103103
pub mod numcity;
104104
pub mod nurikabe;
105+
pub mod nuriloop;
105106
pub mod nurimaze;
106107
pub mod nurimisaki;
107108
pub mod nuritwin;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use crate::util;
2+
use cspuz_rs::graph;
3+
use cspuz_rs::serializer::{
4+
problem_to_url_pzprxs, url_to_problem, Choice, Combinator, Dict, Grid, HexInt, Optionalize,
5+
Spaces,
6+
};
7+
use cspuz_rs::solver::Solver;
8+
9+
pub fn solve_nuriloop(clues: &[Vec<Option<i32>>]) -> Option<graph::BoolGridEdgesIrrefutableFacts> {
10+
let (h, w) = util::infer_shape(clues);
11+
12+
let mut solver = Solver::new();
13+
let is_line = &graph::BoolGridEdges::new(&mut solver, (h - 1, w - 1));
14+
solver.add_answer_key_bool(&is_line.horizontal);
15+
solver.add_answer_key_bool(&is_line.vertical);
16+
let is_passed = &graph::single_cycle_grid_edges(&mut solver, is_line);
17+
18+
let mut has_question_mark = false;
19+
let mut sum = 0;
20+
21+
let mut clue_pos = vec![];
22+
for y in 0..h {
23+
for x in 0..w {
24+
if let Some(n) = clues[y][x] {
25+
if n < 0 {
26+
has_question_mark = true;
27+
}
28+
sum += n;
29+
clue_pos.push((y, x, n));
30+
}
31+
}
32+
}
33+
34+
// Parity check
35+
if !has_question_mark && (h as i32 * w as i32 - sum) % 2 != 0 {
36+
return None;
37+
}
38+
39+
let group_id = solver.int_var_2d((h, w), 0, clue_pos.len() as i32);
40+
41+
solver.add_expr(is_passed.iff(group_id.eq(0)));
42+
43+
for i in 1..=clue_pos.len() {
44+
graph::active_vertices_connected_2d(&mut solver, group_id.eq(i as i32));
45+
}
46+
47+
solver.add_expr(
48+
(!is_passed.conv2d_or((2, 1))).imp(
49+
group_id
50+
.slice((..(h - 1), ..))
51+
.eq(group_id.slice((1.., ..))),
52+
),
53+
);
54+
solver.add_expr(
55+
(!is_passed.conv2d_or((1, 2))).imp(
56+
group_id
57+
.slice((.., ..(w - 1)))
58+
.eq(group_id.slice((.., 1..))),
59+
),
60+
);
61+
62+
for (i, &(y, x, n)) in clue_pos.iter().enumerate() {
63+
solver.add_expr(group_id.at((y, x)).eq((i + 1) as i32));
64+
if n > 0 {
65+
solver.add_expr(group_id.eq((i + 1) as i32).count_true().eq(n));
66+
}
67+
}
68+
69+
solver.irrefutable_facts().map(|f| f.get(is_line))
70+
}
71+
72+
type Problem = Vec<Vec<Option<i32>>>;
73+
74+
fn combinator() -> impl Combinator<Problem> {
75+
Grid::new(Choice::new(vec![
76+
Box::new(Optionalize::new(HexInt)),
77+
Box::new(Spaces::new(None, 'g')),
78+
Box::new(Dict::new(Some(-1), ".")),
79+
]))
80+
}
81+
82+
pub fn serialize_problem(problem: &Problem) -> Option<String> {
83+
problem_to_url_pzprxs(combinator(), "nuriloop", problem.clone())
84+
}
85+
86+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
87+
url_to_problem(combinator(), &["nuriloop"], url)
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
94+
fn problem_for_tests() -> Problem {
95+
vec![
96+
vec![Some(4), None, None, None, None],
97+
vec![None, None, None, Some(2), None],
98+
vec![None, None, None, None, None],
99+
vec![None, Some(1), None, None, None],
100+
vec![None, None, None, None, None],
101+
]
102+
}
103+
104+
#[test]
105+
fn test_nuriloop_problem() {
106+
let problem = problem_for_tests();
107+
let ans = solve_nuriloop(&problem);
108+
assert!(ans.is_some());
109+
let ans = ans.unwrap();
110+
111+
let expected_edges = graph::GridEdges {
112+
horizontal: crate::util::tests::to_option_bool_2d([
113+
[0, 0, 1, 1],
114+
[0, 0, 0, 0],
115+
[1, 1, 0, 0],
116+
[0, 0, 1, 0],
117+
[1, 1, 0, 1],
118+
]),
119+
vertical: crate::util::tests::to_option_bool_2d([
120+
[0, 0, 1, 0, 1],
121+
[0, 0, 1, 0, 1],
122+
[1, 0, 0, 0, 1],
123+
[1, 0, 1, 1, 1],
124+
]),
125+
};
126+
assert_eq!(ans, expected_edges);
127+
}
128+
129+
#[test]
130+
fn test_nurilooop_serializer() {
131+
{
132+
let problem = problem_for_tests();
133+
let url = "https://pzprxs.vercel.app/p?nuriloop/5/5/4m2m1n";
134+
util::tests::serializer_test(problem, url, serialize_problem, deserialize_problem);
135+
}
136+
}
137+
}

cspuz_solver_backend/src/puzzle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ puzzle_list!(puzz_link,
176176
(nothree, ["nothree"], "No Three", "ノースリー"),
177177
(numcity, ["numcity"], "Number City", "ナンバーシティー"),
178178
(nurikabe, ["nurikabe"], "Nurikabe", "ぬりかべ", enumerable),
179+
(nuriloop, ["nuriloop"], "Nurikabe Loop", "面積リンク"),
179180
(nurimaze, ["nurimaze"], "Nurimaze", "ぬりめいず"),
180181
(nurimisaki, ["nurimisaki"], "Nurimisaki", "ぬりみさき"),
181182
(nuritwin, ["nuritwin"], "Nuritwin", "ぬりツイン"),
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use crate::board::{Board, BoardKind, Item, ItemKind};
2+
use crate::uniqueness::check_uniqueness;
3+
use cspuz_rs_puzzles::puzzles::nuriloop;
4+
5+
pub fn solve(url: &str) -> Result<Board, &'static str> {
6+
let problem = nuriloop::deserialize_problem(url).ok_or("invalid url")?;
7+
let ans = nuriloop::solve_nuriloop(&problem);
8+
9+
let height = problem.len();
10+
let width = problem[0].len();
11+
let mut board = Board::new(BoardKind::Grid, height, width, check_uniqueness(&ans));
12+
13+
if let Some(is_line) = &ans {
14+
for y in 0..height {
15+
for x in 0..width {
16+
if let Some(clue) = problem[y][x] {
17+
board.push(Item::cell(
18+
y,
19+
x,
20+
"black",
21+
if clue >= 0 {
22+
ItemKind::Num(clue)
23+
} else {
24+
ItemKind::Text("?")
25+
},
26+
));
27+
}
28+
}
29+
}
30+
31+
board.add_lines_irrefutable_facts(is_line, "green", None);
32+
} else {
33+
for y in 0..height {
34+
for x in 0..width {
35+
if let Some(clue) = problem[y][x] {
36+
board.push(Item::cell(
37+
y,
38+
x,
39+
"black",
40+
if clue >= 0 {
41+
ItemKind::Num(clue)
42+
} else {
43+
ItemKind::Text("?")
44+
},
45+
));
46+
}
47+
}
48+
}
49+
}
50+
51+
Ok(board)
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::solve;
57+
use crate::board::*;
58+
use crate::compare_board_and_check_no_solution_case;
59+
use crate::uniqueness::Uniqueness;
60+
61+
#[test]
62+
#[rustfmt::skip]
63+
fn test_solve() {
64+
compare_board_and_check_no_solution_case!(
65+
solve("https://pzprxs.vercel.app/p?nuriloop/3/3/l1h"),
66+
Board {
67+
kind: BoardKind::Grid,
68+
height: 3,
69+
width: 3,
70+
data: vec![
71+
Item { y: 5, x: 1, color: "black", kind: ItemKind::Num(1) },
72+
Item { y: 2, x: 1, color: "green", kind: ItemKind::Line },
73+
Item { y: 2, x: 3, color: "green", kind: ItemKind::Cross },
74+
Item { y: 2, x: 5, color: "green", kind: ItemKind::Line },
75+
Item { y: 4, x: 1, color: "green", kind: ItemKind::Cross },
76+
Item { y: 4, x: 3, color: "green", kind: ItemKind::Line },
77+
Item { y: 4, x: 5, color: "green", kind: ItemKind::Line },
78+
Item { y: 1, x: 2, color: "green", kind: ItemKind::Line },
79+
Item { y: 1, x: 4, color: "green", kind: ItemKind::Line },
80+
Item { y: 3, x: 2, color: "green", kind: ItemKind::Line },
81+
Item { y: 3, x: 4, color: "green", kind: ItemKind::Cross },
82+
Item { y: 5, x: 2, color: "green", kind: ItemKind::Cross },
83+
Item { y: 5, x: 4, color: "green", kind: ItemKind::Line },
84+
],
85+
uniqueness: Uniqueness::Unique,
86+
},
87+
);
88+
}
89+
}

0 commit comments

Comments
 (0)