From 62d3bee922be05b852cda33619203667371b1b65 Mon Sep 17 00:00:00 2001 From: semiexp Date: Sat, 20 Dec 2025 11:03:32 +0000 Subject: [PATCH 1/2] Add solver for Pyramid Climbers --- cspuz_rs_puzzles/src/puzzles/mod.rs | 1 + cspuz_rs_puzzles/src/puzzles/pyramid.rs | 2 +- .../src/puzzles/pyramid_climbers.rs | 135 ++++++++++++++++++ cspuz_solver_backend/src/puzzle/mod.rs | 1 + .../src/puzzle/pyramid_climbers.rs | 99 +++++++++++++ 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 cspuz_rs_puzzles/src/puzzles/pyramid_climbers.rs create mode 100644 cspuz_solver_backend/src/puzzle/pyramid_climbers.rs diff --git a/cspuz_rs_puzzles/src/puzzles/mod.rs b/cspuz_rs_puzzles/src/puzzles/mod.rs index 95cbd8fc..32acbb85 100644 --- a/cspuz_rs_puzzles/src/puzzles/mod.rs +++ b/cspuz_rs_puzzles/src/puzzles/mod.rs @@ -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; diff --git a/cspuz_rs_puzzles/src/puzzles/pyramid.rs b/cspuz_rs_puzzles/src/puzzles/pyramid.rs index 32a0dcca..16f3dcf1 100644 --- a/cspuz_rs_puzzles/src/puzzles/pyramid.rs +++ b/cspuz_rs_puzzles/src/puzzles/pyramid.rs @@ -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); diff --git a/cspuz_rs_puzzles/src/puzzles/pyramid_climbers.rs b/cspuz_rs_puzzles/src/puzzles/pyramid_climbers.rs new file mode 100644 index 00000000..654dbb4f --- /dev/null +++ b/cspuz_rs_puzzles/src/puzzles/pyramid_climbers.rs @@ -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]) -> Option>>> { + 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>; + +pub fn deserialize_problem(url: &str) -> Option { + 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); + } +} diff --git a/cspuz_solver_backend/src/puzzle/mod.rs b/cspuz_solver_backend/src/puzzle/mod.rs index ec382fac..74443421 100644 --- a/cspuz_solver_backend/src/puzzle/mod.rs +++ b/cspuz_solver_backend/src/puzzle/mod.rs @@ -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; diff --git a/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs b/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs new file mode 100644 index 00000000..6b049352 --- /dev/null +++ b/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs @@ -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 { + 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(); + + 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) +} From 51489246935a198d26117ff0280a2568a7b3f85b Mon Sep 17 00:00:00 2001 From: semiexp Date: Sat, 20 Dec 2025 11:05:00 +0000 Subject: [PATCH 2/2] fix --- cspuz_solver_backend/src/puzzle/pyramid_climbers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs b/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs index 6b049352..a4639255 100644 --- a/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs +++ b/cspuz_solver_backend/src/puzzle/pyramid_climbers.rs @@ -5,7 +5,7 @@ use cspuz_rs_puzzles::puzzles::pyramid_climbers; pub fn solve(url: &str) -> Result { 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(); + let size = ans.len() + 1; let mut board = Board::new(BoardKind::Empty, size, size * 2, is_unique(&ans.concat()));