|
| 1 | +use crate::penpa_editor::{decode_penpa_editor_url, Item, PenpaEditorPuzzle}; |
| 2 | +use cspuz_rs::solver::Solver; |
| 3 | + |
| 4 | +pub fn solve_pyramid_climbers(clues: &[Vec<String>]) -> Option<Vec<Vec<Option<bool>>>> { |
| 5 | + let n = clues.len(); |
| 6 | + |
| 7 | + let mut solver = Solver::new(); |
| 8 | + let mut ans = vec![]; |
| 9 | + for i in 0..(n - 1) { |
| 10 | + ans.push(solver.bool_var_1d(2 * i + 2)); |
| 11 | + solver.add_answer_key_bool(&ans[i]); |
| 12 | + } |
| 13 | + |
| 14 | + let mut seq = vec![]; |
| 15 | + for i in 0..n { |
| 16 | + seq.push(solver.int_var_1d(i + 1, 0, n as i32 - 1)); |
| 17 | + } |
| 18 | + |
| 19 | + for j in 0..n { |
| 20 | + solver.add_expr(seq[n - 1].at(j).eq(j as i32)); |
| 21 | + } |
| 22 | + |
| 23 | + for i in 0..(n - 1) { |
| 24 | + for j in 0..=i { |
| 25 | + solver.add_expr(ans[i].at(j * 2) ^ ans[i].at(j * 2 + 1)); |
| 26 | + if j > 0 { |
| 27 | + solver.add_expr(!(ans[i].at(j * 2) & ans[i].at(j * 2 - 1))); |
| 28 | + } |
| 29 | + solver.add_expr(ans[i].at(j * 2).imp(seq[i].at(j).eq(seq[i + 1].at(j)))); |
| 30 | + solver.add_expr( |
| 31 | + ans[i] |
| 32 | + .at(j * 2 + 1) |
| 33 | + .imp(seq[i].at(j).eq(seq[i + 1].at(j + 1))), |
| 34 | + ); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + for i1 in 0..(n - 1) { |
| 39 | + for i2 in (i1 + 1)..n { |
| 40 | + for j1 in 0..=i1 { |
| 41 | + for j2 in j1..=i2 { |
| 42 | + if i2 - i1 < j2 - j1 { |
| 43 | + continue; |
| 44 | + } |
| 45 | + |
| 46 | + if clues[i1][j1] == clues[i2][j2] { |
| 47 | + solver.add_expr(seq[i1].at(j1).ne(seq[i2].at(j2))); |
| 48 | + } |
| 49 | + } |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + solver.irrefutable_facts().map(|f| { |
| 55 | + let mut result = vec![]; |
| 56 | + for i in 0..(n - 1) { |
| 57 | + result.push(f.get(&ans[i])); |
| 58 | + } |
| 59 | + result |
| 60 | + }) |
| 61 | +} |
| 62 | + |
| 63 | +type Problem = Vec<Vec<String>>; |
| 64 | + |
| 65 | +pub fn deserialize_problem(url: &str) -> Option<Problem> { |
| 66 | + let decoded = decode_penpa_editor_url(url).ok()?; |
| 67 | + let decoded = match decoded { |
| 68 | + PenpaEditorPuzzle::Pyramid(p) => p, |
| 69 | + _ => return None, |
| 70 | + }; |
| 71 | + |
| 72 | + let size = decoded.size(); |
| 73 | + |
| 74 | + let mut clues = vec![]; |
| 75 | + for i in 0..size { |
| 76 | + clues.push(vec![String::new(); i + 1]); |
| 77 | + } |
| 78 | + |
| 79 | + for y in 0..size { |
| 80 | + for x in 0..=y { |
| 81 | + for item in decoded.get_cell(y, x) { |
| 82 | + if let Item::Text(text) = item { |
| 83 | + clues[y][x] = text.text.clone(); |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + } |
| 88 | + Some(clues) |
| 89 | +} |
| 90 | + |
| 91 | +#[cfg(test)] |
| 92 | +mod tests { |
| 93 | + use crate::util; |
| 94 | + |
| 95 | + use super::*; |
| 96 | + |
| 97 | + fn problem_for_tests() -> Problem { |
| 98 | + // https://puzsq.logicpuzzle.app/puzzle/166598 |
| 99 | + let base = vec![ |
| 100 | + vec!["A"], |
| 101 | + vec!["G", "F"], |
| 102 | + vec!["B", "B", "C"], |
| 103 | + vec!["B", "A", "C", "D"], |
| 104 | + vec!["C", "D", "D", "D", "E"], |
| 105 | + vec!["A", "B", "C", "D", "E", "F"], |
| 106 | + ]; |
| 107 | + base.iter() |
| 108 | + .map(|row| row.iter().map(|&s| s.to_string()).collect()) |
| 109 | + .collect() |
| 110 | + } |
| 111 | + |
| 112 | + #[test] |
| 113 | + fn test_pyramid_climbers_problem() { |
| 114 | + let clues = problem_for_tests(); |
| 115 | + let ans = solve_pyramid_climbers(&clues); |
| 116 | + assert!(ans.is_some()); |
| 117 | + let ans = ans.unwrap(); |
| 118 | + |
| 119 | + let expected = util::tests::to_option_bool_2d(vec![ |
| 120 | + vec![0, 1], |
| 121 | + vec![1, 0, 1, 0], |
| 122 | + vec![0, 1, 0, 1, 0, 1], |
| 123 | + vec![1, 0, 0, 1, 0, 1, 0, 1], |
| 124 | + vec![1, 0, 1, 0, 1, 0, 0, 1, 0, 1], |
| 125 | + ]); |
| 126 | + assert_eq!(ans, expected); |
| 127 | + } |
| 128 | + |
| 129 | + #[test] |
| 130 | + fn test_pyramid_climbers_serializer() { |
| 131 | + let problem = problem_for_tests(); |
| 132 | + 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=="; |
| 133 | + assert_eq!(deserialize_problem(url).unwrap(), problem); |
| 134 | + } |
| 135 | +} |
0 commit comments