From 4f7790d1d54314aa222a3864be4ae14f5014c9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= Date: Mon, 1 Sep 2025 18:50:36 +0200 Subject: [PATCH] solver --- src/main.rs | 266 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 244 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index db5d9c5..ae6eb2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ use palette::{IntoColor, OklabHue, Oklch, Srgb}; use std::{ - collections::{HashMap, HashSet}, + collections::{BinaryHeap, HashMap, HashSet, VecDeque}, fmt::Display, - io::{self, Read, Write, stdin, stdout}, - num::NonZero, + hash::{DefaultHasher, Hash, Hasher}, + io::{self, Write, stdin, stdout}, + num::{self, NonZero}, ops::ControlFlow, }; @@ -36,15 +37,24 @@ impl Display for Ring { let color: Srgb = color.into_color(); let color: Srgb = color.into_format(); - write!( - f, - "{}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{RESET}", - crate_cli_color((color.red, color.green, color.blue)) - ) + if f.alternate() { + write!( + f, + "{}{BLOCK}{:^5}{BLOCK}{RESET}", + crate_cli_color((color.red, color.green, color.blue)), + self.0 + ) + } else { + write!( + f, + "{}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{BLOCK}{RESET}", + crate_cli_color((color.red, color.green, color.blue)) + ) + } } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Tower { pub rings: [Option; TOWER_HEIGHT], } @@ -70,6 +80,34 @@ impl Tower { tower } + fn burried_score(&self) -> usize { + let mut ring_type = None; + let mut same_ring_type_score = 0; + let mut max_same_ring_type_score = 0; + let mut burried_score = 0; + + for i in self.rings.iter().rev() { + if let Some(ring) = i { + if let Some(existing_ring_type) = ring_type { + if existing_ring_type == *ring { + same_ring_type_score += 2; + } else { + max_same_ring_type_score = + max_same_ring_type_score.max(same_ring_type_score); + burried_score += 2usize.pow(max_same_ring_type_score); + same_ring_type_score = 0; + ring_type = Some(*ring); + } + } else { + same_ring_type_score = 0; + ring_type = Some(*ring); + } + } + } + + burried_score + } + fn get_movable_top(&self) -> Result<(Ring, usize), MoveError> { let mut topmost_ring = None; let mut count = 0; @@ -112,7 +150,11 @@ impl Tower { assert_eq!(num, 0); } - fn add_movable_top(&mut self, (ring, num): (Ring, usize)) -> Result { + fn add_movable_top( + &mut self, + (ring, num): (Ring, usize), + dummy: bool, + ) -> Result { if self.top_ring().is_some_and(|top| top != ring) { return Err(MoveError::DestinationWrongTop); } @@ -120,6 +162,10 @@ impl Tower { let height_left = TOWER_HEIGHT - self.height(); let num_rings_moved = num.min(height_left); + if dummy { + return Ok(num_rings_moved); + } + self.rings .iter_mut() .filter(|i| i.is_none()) @@ -189,6 +235,7 @@ pub enum MoveError { SourceEmpty, } +#[derive(Debug, Clone, Copy)] pub struct Move { pub from_tower: usize, pub to_tower: usize, @@ -233,30 +280,180 @@ impl Game { Self { ring_sets, towers } } - pub fn make_move(&mut self, m: Move) -> Result { + pub fn possible_moves(&mut self) -> Vec { + let mut res = Vec::new(); + + for from_tower in 0..self.towers.len() { + for to_tower in 0..self.towers.len() { + if from_tower == to_tower { + continue; + } + + let m = Move::new(from_tower, to_tower); + if self.try_make_move(m, true).is_ok() { + res.push(m); + } + } + } + + res + } + + fn burried_ring_score(&self) -> usize { + self.towers.iter().map(|i| i.burried_score()).sum() + } + + pub fn solve(&self) -> Option> { + let mut working_instance = Self { + towers: Vec::new(), + ring_sets: self.ring_sets.clone(), + }; + + #[derive(Clone)] + struct State { + towers: Vec>, + m: Move, + parent_index: usize, + } + impl Hash for State { + fn hash(&self, state: &mut H) { + self.towers.hash(state); + } + } + + let hash_state = |s: &[Tower]| { + let mut hasher = DefaultHasher::default(); + + for x in s.iter().filter(|i| i.height() != 0).enumerate() { + x.hash(&mut hasher); + } + + hasher.finish() + }; + + #[derive(PartialEq, PartialOrd)] + struct Ordf32(f32); + impl Eq for Ordf32 {} + impl Ord for Ordf32 { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.total_cmp(&other.0) + } + } + + let mut states = Vec::new(); + let mut todo = BinaryHeap::<(Ordf32, usize, usize)>::new(); + let mut had = HashSet::new(); + let mut ctr = 0; + + states.push(State { + towers: self.towers.clone(), + parent_index: 0, + m: Move::new(0, 0), + }); + todo.push((Ordf32(0.0), 1000000000, 0)); + + while let Some((score, depth, state_index)) = todo.pop() { + ctr += 1; + let state = states[state_index].clone(); + + let hash = hash_state(&state.towers); + + // already had + if had.contains(&hash) { + continue; + } + had.insert(hash); + + working_instance.towers = state.towers.clone(); + + if ctr % 100000 == 0 { + println!("{working_instance}"); + println!("{} {depth}", score.0); + stdout().flush().unwrap(); + stdin().read_line(&mut String::new()).unwrap(); + } + if working_instance.check_done() { + println!("done!"); + let mut res = Vec::new(); + + let mut curr = state_index; + while curr != 0 { + res.push(states[curr].m); + curr = states[curr].parent_index; + } + + res.reverse(); + + return Some(res); + } + + for m in working_instance.possible_moves() { + working_instance.towers = state.towers.clone(); + working_instance.make_move(m).unwrap(); + + let num_solved = working_instance.num_solved(); + let brs = working_instance.burried_ring_score(); + + let new_state_hash = hash_state(&working_instance.towers); + if had.contains(&new_state_hash) { + continue; + } + + let new_state_index = states.len(); + states.push(State { + towers: working_instance.towers.clone(), + parent_index: state_index, + m, + }); + + let score = (num_solved as f32 * 10.0) + (-(brs as f32) * 100.0); + + todo.push((Ordf32(score), depth - 1, new_state_index)); + } + } + + None + } + + pub fn try_make_move(&mut self, m: Move, dummy: bool) -> Result { let Move { from_tower, to_tower, } = m; let (ring, mut num) = self.towers[from_tower].get_movable_top()?; - num = self.towers[to_tower].add_movable_top((ring, num))?; + num = self.towers[to_tower].add_movable_top((ring, num), dummy)?; + + if dummy { + return Ok(num); + } + self.towers[from_tower].remove_movable_top((ring, num)); Ok(num) } + pub fn make_move(&mut self, m: Move) -> Result { + self.try_make_move(m, false) + } + pub fn check_done(&self) -> bool { + self.num_solved() == self.towers.len() + } + + pub fn num_solved(&self) -> usize { + let mut num_solved = 0; for t in &self.towers { t.check(); // If the tower itself didn't have matching rings, return false let Some((ring_type, c)) = t.count_if_single_ring_type() else { - return false; + continue; }; - // If the tower had no rings, continue + // If the tower had no rings, trivially solved if c == 0 { + num_solved += 1; continue; } @@ -265,11 +462,13 @@ impl Game { // if this tower has only a subset of the total, we're not done. let ring_type = ring_type.unwrap(); if *self.ring_sets.get(&ring_type).unwrap() != c { - return false; + continue; } + + num_solved += 1; } - true + num_solved } fn ask_tower(&self, msg: &str) -> io::Result { @@ -477,10 +676,14 @@ impl GameGenerator { impl Display for Game { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for i in (0..TOWER_HEIGHT).rev() { - for _ in 0..3 { + for m in 0..3 { for t in &self.towers { if let Some(r) = t.rings[i] { - write!(f, " {r:} ")?; + if m == 1 { + write!(f, " {r:#} ")?; + } else { + write!(f, " {r:} ")?; + } } else { write!(f, " ")?; } @@ -499,6 +702,11 @@ impl Display for Game { let num = format!(" {} ", t + 1); write!(f, "{num:━^13}")?; } + writeln!(f)?; + for t in 0..self.towers.len() { + let num = format!(" {} ", self.towers[t].burried_score()); + write!(f, "{num:^13}")?; + } Ok(()) } @@ -534,13 +742,27 @@ impl UndoStack { fn main() -> io::Result<()> { let mut g = GameGenerator { num_extra_towers: 2, - num_ring_types: 8, + num_ring_types: 12, } .generate::<4>(); - println!("{CLEAR_SCREEN}"); - let mut u = UndoStack::new(); - while let ControlFlow::Continue(()) = g.cli_move(&mut u)? {} + if let Some(solution) = g.solve() { + let mut g = Game { + towers: g.towers.clone(), + ring_sets: g.ring_sets.clone(), + }; + for i in solution { + println!("{CLEAR_SCREEN}"); + g.make_move(i).unwrap(); + println!("{g}"); + stdout().flush().unwrap(); + stdin().read_line(&mut String::new()).unwrap(); + } + } + + // println!("{CLEAR_SCREEN}"); + // let mut u = UndoStack::new(); + // while let ControlFlow::Continue(()) = g.cli_move(&mut u)? {} Ok(()) }