Skip to content

Commit 5144147

Browse files
authored
Added solver for Army Ants (#202)
* Added generic movement puzzle handling constraints * Added argument for puzzles with straight constraints * Added solver for armyants * Added and finalized solver for army ants * Fixed issue with empty spaces * Fixed issue with self touching army * Fixed issue with question marks * Simplified constraints * Adressed comments Removed the height and width from the arguments to the add_movement_constraints, explicitly disallowed 2 length loops
1 parent c1f79f0 commit 5144147

5 files changed

Lines changed: 534 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use crate::puzzles::move_common::add_movement_constraints;
2+
use cspuz_rs::graph;
3+
use cspuz_rs::serializer::{
4+
problem_to_url_with_context, url_to_problem, Choice, Combinator, Context, ContextBasedGrid,
5+
Dict, HexInt, Optionalize, Rooms, Size, Spaces, Tuple2,
6+
};
7+
use cspuz_rs::solver::Solver;
8+
9+
pub fn solve_armyants(
10+
borders: &graph::InnerGridEdges<Vec<Vec<bool>>>,
11+
clues: &[Vec<Option<i32>>],
12+
) -> Option<(graph::BoolGridEdgesIrrefutableFacts, Vec<Vec<Option<i32>>>)> {
13+
let (h, w) = borders.base_shape();
14+
let mut solver = Solver::new();
15+
16+
let mut clue_max = 0;
17+
let mut num_qmark = 0;
18+
for y in 0..h {
19+
for x in 0..w {
20+
if let Some(n) = clues[y][x] {
21+
clue_max = clue_max.max(n);
22+
if n < 0 {
23+
num_qmark += 1;
24+
}
25+
}
26+
}
27+
}
28+
29+
let end_state = &solver.int_var_2d((h, w), -1, clue_max + num_qmark);
30+
solver.add_answer_key_int(end_state);
31+
let movement = &graph::BoolGridEdges::new(&mut solver, (h - 1, w - 1));
32+
solver.add_answer_key_bool(&movement.horizontal);
33+
solver.add_answer_key_bool(&movement.vertical);
34+
35+
for y in 0..h {
36+
for x in 0..(w - 1) {
37+
if borders.vertical[y][x] {
38+
solver.add_expr(!movement.horizontal.at((y, x)));
39+
}
40+
}
41+
}
42+
for y in 0..(h - 1) {
43+
for x in 0..w {
44+
if borders.horizontal[y][x] {
45+
solver.add_expr(!movement.vertical.at((y, x)));
46+
}
47+
}
48+
}
49+
50+
add_movement_constraints(
51+
&mut solver,
52+
clue_max + num_qmark,
53+
movement,
54+
clues,
55+
end_state,
56+
false,
57+
);
58+
59+
for y in 0..h {
60+
for x in 0..w {
61+
let connected = &solver.bool_var_2d((h, w));
62+
let is_maximal_ant = end_state
63+
.four_neighbors((y, x))
64+
.ge(end_state.at((y, x)) + 1)
65+
.count_true()
66+
.eq(0)
67+
& end_state.at((y, x)).ge(1);
68+
solver.add_expr(is_maximal_ant.imp(connected.count_true().eq(end_state.at((y, x)))));
69+
70+
graph::active_vertices_connected_2d(&mut solver, connected);
71+
72+
for nb in connected.four_neighbor_indices((y, x)) {
73+
solver.add_expr(is_maximal_ant.imp(end_state.ge(1).at(nb).imp(connected.at(nb))));
74+
}
75+
76+
solver.add_expr(
77+
is_maximal_ant
78+
.imp(end_state.ge(1).slice((1.., ..)) & end_state.ge(1).slice((..(h - 1), ..)))
79+
.imp(
80+
connected
81+
.slice((1.., ..))
82+
.iff(connected.slice((..(h - 1), ..))),
83+
),
84+
);
85+
solver.add_expr(
86+
is_maximal_ant.imp(
87+
(end_state.ge(1).slice((.., 1..)) & end_state.ge(1).slice((.., ..(w - 1))))
88+
.imp(
89+
connected
90+
.slice((.., 1..))
91+
.iff(connected.slice((.., ..(w - 1)))),
92+
),
93+
),
94+
);
95+
96+
solver.add_expr(
97+
end_state.at((y, x)).ge(2).imp(
98+
end_state
99+
.four_neighbors((y, x))
100+
.eq(end_state.at((y, x)) - 1)
101+
.count_true()
102+
.eq(1),
103+
),
104+
);
105+
}
106+
}
107+
108+
solver.add_expr(end_state.ne(0));
109+
110+
solver
111+
.irrefutable_facts()
112+
.map(|f| (f.get(movement), f.get(end_state)))
113+
}
114+
115+
pub type Problem = (graph::InnerGridEdges<Vec<Vec<bool>>>, Vec<Vec<Option<i32>>>);
116+
117+
fn combinator() -> impl Combinator<Problem> {
118+
Size::new(Tuple2::new(
119+
Rooms,
120+
ContextBasedGrid::new(Choice::new(vec![
121+
Box::new(Optionalize::new(HexInt)),
122+
Box::new(Spaces::new(None, 'g')),
123+
Box::new(Dict::new(Some(-1), ".")),
124+
])),
125+
))
126+
}
127+
128+
pub fn serialize_problem(problem: &Problem) -> Option<String> {
129+
let height = problem.0.vertical.len();
130+
let width = problem.0.vertical[0].len() + 1;
131+
problem_to_url_with_context(
132+
combinator(),
133+
"armyants",
134+
problem.clone(),
135+
&Context::sized(height, width),
136+
)
137+
}
138+
139+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
140+
url_to_problem(combinator(), &["armyants"], url)
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::*;
146+
147+
fn problem_for_tests() -> Problem {
148+
let borders = graph::InnerGridEdges {
149+
horizontal: crate::util::tests::to_bool_2d([[0, 1, 1, 0], [1, 1, 1, 1], [0, 1, 1, 0]]),
150+
vertical: crate::util::tests::to_bool_2d([[1, 0, 1], [1, 0, 0], [0, 0, 1], [1, 0, 1]]),
151+
};
152+
153+
let clues = vec![
154+
vec![None, None, Some(1), None],
155+
vec![Some(2), None, None, Some(4)],
156+
vec![Some(3), None, None, Some(1)],
157+
vec![None, Some(2), None, None],
158+
];
159+
160+
(borders, clues)
161+
}
162+
163+
#[test]
164+
fn test_armyants_problem() {
165+
let (borders, clues) = problem_for_tests();
166+
let ans = solve_armyants(&borders, &clues);
167+
assert!(ans.is_some());
168+
let (movement, final_state) = ans.unwrap();
169+
170+
let expected_nums = crate::util::tests::to_option_2d([
171+
[2, 1, -1, -1],
172+
[-1, -1, 4, -1],
173+
[-1, -1, 3, -1],
174+
[-1, -1, 2, 1],
175+
]);
176+
177+
let expected_paths = graph::BoolGridEdgesIrrefutableFacts {
178+
horizontal: crate::util::tests::to_option_bool_2d([
179+
[0, 1, 0],
180+
[0, 0, 1],
181+
[1, 1, 0],
182+
[0, 1, 0],
183+
]),
184+
vertical: crate::util::tests::to_option_bool_2d([
185+
[1, 0, 0, 0],
186+
[0, 0, 0, 0],
187+
[0, 0, 0, 1],
188+
]),
189+
};
190+
assert_eq!(movement, expected_paths);
191+
assert_eq!(final_state, expected_nums);
192+
}
193+
194+
#[test]
195+
fn test_armyants_serializer() {
196+
let problem = problem_for_tests();
197+
let url = "https://puzz.link/p?armyants/4/4/m38dtgh1g2h43h1g2h"; // Example puzzle on puzz.link
198+
crate::util::tests::serializer_test(problem, url, serialize_problem, deserialize_problem);
199+
}
200+
}

cspuz_rs_puzzles/src/puzzles/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod aquapelago;
77
pub mod aquarium;
88
pub mod araf;
99
pub mod archipelago;
10+
pub mod armyants;
1011
pub mod ayeheya;
1112
pub mod balloon;
1213
pub mod barns;
@@ -79,6 +80,7 @@ pub mod minesweeper;
7980
pub mod mintonette;
8081
pub mod moonsun;
8182
pub mod morningwalk;
83+
pub mod move_common;
8284
pub mod multiplication_link;
8385
pub mod n_cells;
8486
pub mod nagenawa;

0 commit comments

Comments
 (0)