Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspuz_rs_puzzles/src/puzzles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub mod pencils;
pub mod polyominous;
pub mod putteria;
pub mod pyramid;
pub mod pyramid_climbers;
pub mod reflect;
pub mod ringring;
pub mod ripple;
Expand Down
2 changes: 1 addition & 1 deletion cspuz_rs_puzzles/src/puzzles/pyramid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ mod tests {
}

#[test]
fn test_exercise_serializer() {
fn test_pyramid_serializer() {
let problem = problem_for_tests();
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=";
assert_eq!(deserialize_problem(url).unwrap(), problem);
Expand Down
135 changes: 135 additions & 0 deletions cspuz_rs_puzzles/src/puzzles/pyramid_climbers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::penpa_editor::{decode_penpa_editor_url, Item, PenpaEditorPuzzle};
use cspuz_rs::solver::Solver;

pub fn solve_pyramid_climbers(clues: &[Vec<String>]) -> Option<Vec<Vec<Option<bool>>>> {
let n = clues.len();

let mut solver = Solver::new();
let mut ans = vec![];
for i in 0..(n - 1) {
ans.push(solver.bool_var_1d(2 * i + 2));
solver.add_answer_key_bool(&ans[i]);
}

let mut seq = vec![];
for i in 0..n {
seq.push(solver.int_var_1d(i + 1, 0, n as i32 - 1));
}

for j in 0..n {
solver.add_expr(seq[n - 1].at(j).eq(j as i32));
}

for i in 0..(n - 1) {
for j in 0..=i {
solver.add_expr(ans[i].at(j * 2) ^ ans[i].at(j * 2 + 1));
if j > 0 {
solver.add_expr(!(ans[i].at(j * 2) & ans[i].at(j * 2 - 1)));
}
solver.add_expr(ans[i].at(j * 2).imp(seq[i].at(j).eq(seq[i + 1].at(j))));
solver.add_expr(
ans[i]
.at(j * 2 + 1)
.imp(seq[i].at(j).eq(seq[i + 1].at(j + 1))),
);
}
}

for i1 in 0..(n - 1) {
for i2 in (i1 + 1)..n {
for j1 in 0..=i1 {
for j2 in j1..=i2 {
if i2 - i1 < j2 - j1 {
continue;
}

if clues[i1][j1] == clues[i2][j2] {
solver.add_expr(seq[i1].at(j1).ne(seq[i2].at(j2)));
}
}
}
}
}

solver.irrefutable_facts().map(|f| {
let mut result = vec![];
for i in 0..(n - 1) {
result.push(f.get(&ans[i]));
}
result
})
}

type Problem = Vec<Vec<String>>;

pub fn deserialize_problem(url: &str) -> Option<Problem> {
let decoded = decode_penpa_editor_url(url).ok()?;
let decoded = match decoded {
PenpaEditorPuzzle::Pyramid(p) => p,
_ => return None,
};

let size = decoded.size();

let mut clues = vec![];
for i in 0..size {
clues.push(vec![String::new(); i + 1]);
}

for y in 0..size {
for x in 0..=y {
for item in decoded.get_cell(y, x) {
if let Item::Text(text) = item {
clues[y][x] = text.text.clone();
}
}
}
}
Some(clues)
}

#[cfg(test)]
mod tests {
use crate::util;

use super::*;

fn problem_for_tests() -> Problem {
// https://puzsq.logicpuzzle.app/puzzle/166598
let base = vec![
vec!["A"],
vec!["G", "F"],
vec!["B", "B", "C"],
vec!["B", "A", "C", "D"],
vec!["C", "D", "D", "D", "E"],
vec!["A", "B", "C", "D", "E", "F"],
];
base.iter()
.map(|row| row.iter().map(|&s| s.to_string()).collect())
.collect()
}

#[test]
fn test_pyramid_climbers_problem() {
let clues = problem_for_tests();
let ans = solve_pyramid_climbers(&clues);
assert!(ans.is_some());
let ans = ans.unwrap();

let expected = util::tests::to_option_bool_2d(vec![
vec![0, 1],
vec![1, 0, 1, 0],
vec![0, 1, 0, 1, 0, 1],
vec![1, 0, 0, 1, 0, 1, 0, 1],
vec![1, 0, 1, 0, 1, 0, 0, 1, 0, 1],
]);
assert_eq!(ans, expected);
}

#[test]
fn test_pyramid_climbers_serializer() {
let problem = problem_for_tests();
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==";
assert_eq!(deserialize_problem(url).unwrap(), problem);
}
}
1 change: 1 addition & 0 deletions cspuz_solver_backend/src/puzzle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ puzzle_list!(penpa_edit,
(castle_walker, ["castle_walker"], "Castle Walker", "Castle Walker"),
(exercise, ["exercise"], "Exercise", "Exercise"),
(pyramid, ["pyramid"], "Pyramid", "ピラミッド"),
(pyramid_climbers, ["pyramid_climbers"], "Pyramid Climbers", "Pyramid Climbers"),
);

pub mod double_lits;
Expand Down
99 changes: 99 additions & 0 deletions cspuz_solver_backend/src/puzzle/pyramid_climbers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::board::{Board, BoardKind, Item, ItemKind};
use crate::uniqueness::is_unique;
use cspuz_rs_puzzles::puzzles::pyramid_climbers;

pub fn solve(url: &str) -> Result<Board, &'static str> {
let clues = pyramid_climbers::deserialize_problem(url).ok_or("invalid url")?;
let ans = pyramid_climbers::solve_pyramid_climbers(&clues).ok_or("no answer")?;
let size = ans.len() + 1;

let mut board = Board::new(BoardKind::Empty, size, size * 2, is_unique(&ans.concat()));

// Clues
for y in 0..size {
for x in 0..=y {
board.push(Item {
y: 2 * y + 1,
x: (size - y - 1 + 2 * x + 1) * 2,
color: "black",
kind: ItemKind::TextString(clues[y][x].clone()),
});
}
}

// Borders
for y in 0..=size {
let start = if y == size { 0 } else { size - y - 1 };
let end = if y == size { size * 2 } else { size + y + 1 };

for x in start..end {
board.push(Item {
y: y * 2,
x: x * 2 + 1,
color: "black",
kind: ItemKind::BoldWall,
});
}
}
for y in 0..size {
for x in 0..=(y + 1) {
board.push(Item {
y: y * 2 + 1,
x: (size - y - 1 + 2 * x) * 2,
color: "black",
kind: ItemKind::BoldWall,
});
}
}

// Answers
for y in 0..(size - 1) {
for x in 0..=y {
match ans[y][x * 2] {
Some(true) => {
board.push(Item {
y: 2 * y + 1,
x: (size - y - 1 + 2 * x) * 2 + 2,
color: "green",
kind: ItemKind::LineTo(
(2 * y + 3) as i32,
((size - y - 1 + 2 * x) * 2) as i32,
),
});
}
Some(false) => {
board.push(Item {
y: 2 * y + 2,
x: (size - y - 1 + 2 * x) * 2 + 1,
color: "green",
kind: ItemKind::Cross,
});
}
None => (),
}
match ans[y][x * 2 + 1] {
Some(true) => {
board.push(Item {
y: 2 * y + 1,
x: (size - y - 1 + 2 * x) * 2 + 2,
color: "green",
kind: ItemKind::LineTo(
(2 * y + 3) as i32,
((size - y - 1 + 2 * x + 2) * 2) as i32,
),
});
}
Some(false) => {
board.push(Item {
y: 2 * y + 2,
x: (size - y - 1 + 2 * x + 1) * 2 + 1,
color: "green",
kind: ItemKind::Cross,
});
}
None => (),
}
}
}
Ok(board)
}