1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: MIT |
3 | |
4 | use slint::Model; |
5 | use std::cell::RefCell; |
6 | use std::rc::Rc; |
7 | |
8 | #[cfg (target_arch = "wasm32" )] |
9 | use wasm_bindgen::prelude::*; |
10 | |
11 | slint::include_modules!(); |
12 | |
13 | fn shuffle() -> Vec<i8> { |
14 | fn is_solvable(positions: &[i8]) -> bool { |
15 | // Same source as the flutter's slide_puzzle: |
16 | // https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html |
17 | // This page seems to be no longer available, a copy can be found here: |
18 | // https://horatiuvlad.com/unitbv/inteligenta_artificiala/2015/TilesSolvability.html |
19 | |
20 | let mut inversions = 0; |
21 | for x in 0..positions.len() - 1 { |
22 | let v = positions[x]; |
23 | inversions += positions[x + 1..].iter().filter(|x| **x >= 0 && **x < v).count(); |
24 | } |
25 | //((blank on odd row from bottom) == (#inversions even)) |
26 | let blank_row = positions.iter().position(|x| *x == -1).unwrap() / 4; |
27 | inversions % 2 != blank_row % 2 |
28 | } |
29 | |
30 | let mut vec = ((-1)..15).collect::<Vec<i8>>(); |
31 | use rand::seq::SliceRandom; |
32 | let mut rng = rand::thread_rng(); |
33 | vec.shuffle(&mut rng); |
34 | while !is_solvable(&vec) { |
35 | vec.shuffle(&mut rng); |
36 | } |
37 | vec |
38 | } |
39 | |
40 | struct AppState { |
41 | pieces: Rc<slint::VecModel<Piece>>, |
42 | main_window: slint::Weak<MainWindow>, |
43 | /// An array of 16 values which represent a 4x4 matrix containing the piece number in that |
44 | /// position. -1 is no piece. |
45 | positions: Vec<i8>, |
46 | auto_play_timer: slint::Timer, |
47 | kick_animation_timer: slint::Timer, |
48 | /// The speed in the x and y direction for the associated tile |
49 | speed_for_kick_animation: [(f32, f32); 15], |
50 | finished: bool, |
51 | } |
52 | |
53 | impl AppState { |
54 | fn set_pieces_pos(&self, p: i8, pos: i8) { |
55 | if p >= 0 { |
56 | self.pieces.set_row_data( |
57 | p as usize, |
58 | Piece { pos_y: (pos % 4) as _, pos_x: (pos / 4) as _, offset_x: 0., offset_y: 0. }, |
59 | ); |
60 | } |
61 | } |
62 | |
63 | fn randomize(&mut self) { |
64 | self.positions = shuffle(); |
65 | for (i, p) in self.positions.iter().enumerate() { |
66 | self.set_pieces_pos(*p, i as _); |
67 | } |
68 | self.main_window.unwrap().set_moves(0); |
69 | self.apply_tiles_left(); |
70 | } |
71 | |
72 | fn apply_tiles_left(&mut self) { |
73 | let left = 15 - self.positions.iter().enumerate().filter(|(i, x)| *i as i8 == **x).count(); |
74 | self.main_window.unwrap().set_tiles_left(left as _); |
75 | self.finished = left == 0; |
76 | } |
77 | |
78 | fn piece_clicked(&mut self, p: i8) -> bool { |
79 | let piece = self.pieces.row_data(p as usize).unwrap_or_default(); |
80 | assert_eq!(self.positions[(piece.pos_x * 4 + piece.pos_y) as usize], p); |
81 | |
82 | // find the coordinate of the hole. |
83 | let hole = self.positions.iter().position(|x| *x == -1).unwrap() as i8; |
84 | let pos = (piece.pos_x * 4 + piece.pos_y) as i8; |
85 | let sign = if pos > hole { -1 } else { 1 }; |
86 | if hole % 4 == piece.pos_y as i8 { |
87 | self.slide(pos, sign * 4) |
88 | } else if hole / 4 == piece.pos_x as i8 { |
89 | self.slide(pos, sign) |
90 | } else { |
91 | self.speed_for_kick_animation[p as usize] = ( |
92 | if hole % 4 > piece.pos_y as i8 { 10. } else { -10. }, |
93 | if hole / 4 > piece.pos_x as i8 { 10. } else { -10. }, |
94 | ); |
95 | return false; |
96 | }; |
97 | self.apply_tiles_left(); |
98 | if let Some(x) = self.main_window.upgrade() { |
99 | x.set_moves(x.get_moves() + 1); |
100 | } |
101 | true |
102 | } |
103 | |
104 | fn slide(&mut self, pos: i8, offset: i8) { |
105 | let mut swap = pos; |
106 | while self.positions[pos as usize] != -1 { |
107 | swap += offset; |
108 | self.positions.swap(pos as usize, swap as usize); |
109 | self.set_pieces_pos(self.positions[swap as usize] as _, swap); |
110 | } |
111 | } |
112 | |
113 | fn random_move(&mut self) { |
114 | let mut rng = rand::thread_rng(); |
115 | let hole = self.positions.iter().position(|x| *x == -1).unwrap() as i8; |
116 | let mut p; |
117 | loop { |
118 | p = rand::Rng::gen_range(&mut rng, 0..16); |
119 | if hole == p { |
120 | continue; |
121 | } else if (hole % 4 == p % 4) || (hole / 4 == p / 4) { |
122 | break; |
123 | } |
124 | } |
125 | let p = self.positions[p as usize]; |
126 | self.piece_clicked(p); |
127 | } |
128 | |
129 | /// Advance the kick animation |
130 | fn kick_animation(&mut self) { |
131 | /// update offset and speed, returns true if the animation is still running |
132 | fn spring_animation(offset: &mut f32, speed: &mut f32) -> bool { |
133 | const C: f32 = 0.3; // Constant = k/m |
134 | const DAMP: f32 = 0.7; |
135 | const EPS: f32 = 0.3; |
136 | let acceleration = -*offset * C; |
137 | *speed += acceleration; |
138 | *speed *= DAMP; |
139 | if *speed != 0. || *offset != 0. { |
140 | *offset += *speed; |
141 | if speed.abs() < EPS && offset.abs() < EPS { |
142 | *speed = 0.; |
143 | *offset = 0.; |
144 | } |
145 | true |
146 | } else { |
147 | false |
148 | } |
149 | } |
150 | |
151 | let mut has_animation = false; |
152 | for idx in 0..15 { |
153 | let mut p = self.pieces.row_data(idx).unwrap_or_default(); |
154 | let ax = spring_animation(&mut p.offset_x, &mut self.speed_for_kick_animation[idx].0); |
155 | let ay = spring_animation(&mut p.offset_y, &mut self.speed_for_kick_animation[idx].1); |
156 | if ax || ay { |
157 | self.pieces.set_row_data(idx, p); |
158 | has_animation = true; |
159 | } |
160 | } |
161 | if !has_animation { |
162 | self.kick_animation_timer.stop(); |
163 | } |
164 | } |
165 | } |
166 | |
167 | #[cfg_attr (target_arch = "wasm32" , wasm_bindgen(start))] |
168 | pub fn main() { |
169 | // This provides better error messages in debug mode. |
170 | // It's disabled in release mode so it doesn't bloat up the file size. |
171 | #[cfg (all(debug_assertions, target_arch = "wasm32" ))] |
172 | console_error_panic_hook::set_once(); |
173 | |
174 | let main_window = MainWindow::new().unwrap(); |
175 | |
176 | let state = Rc::new(RefCell::new(AppState { |
177 | pieces: Rc::new(slint::VecModel::<Piece>::from(vec![Piece::default(); 15])), |
178 | main_window: main_window.as_weak(), |
179 | positions: vec![], |
180 | auto_play_timer: Default::default(), |
181 | kick_animation_timer: Default::default(), |
182 | speed_for_kick_animation: Default::default(), |
183 | finished: false, |
184 | })); |
185 | state.borrow_mut().randomize(); |
186 | main_window.set_pieces(state.borrow().pieces.clone().into()); |
187 | |
188 | let state_copy = state.clone(); |
189 | main_window.on_piece_clicked(move |p| { |
190 | state_copy.borrow().auto_play_timer.stop(); |
191 | state_copy.borrow().main_window.unwrap().set_auto_play(false); |
192 | if state_copy.borrow().finished { |
193 | return; |
194 | } |
195 | if !state_copy.borrow_mut().piece_clicked(p as i8) { |
196 | let state_weak = Rc::downgrade(&state_copy); |
197 | state_copy.borrow().kick_animation_timer.start( |
198 | slint::TimerMode::Repeated, |
199 | std::time::Duration::from_millis(16), |
200 | move || { |
201 | if let Some(state) = state_weak.upgrade() { |
202 | state.borrow_mut().kick_animation(); |
203 | } |
204 | }, |
205 | ); |
206 | } |
207 | }); |
208 | |
209 | let state_copy = state.clone(); |
210 | main_window.on_reset(move || { |
211 | state_copy.borrow().auto_play_timer.stop(); |
212 | state_copy.borrow().main_window.unwrap().set_auto_play(false); |
213 | state_copy.borrow_mut().randomize(); |
214 | }); |
215 | |
216 | let state_copy = state; |
217 | main_window.on_enable_auto_mode(move |enabled| { |
218 | if enabled { |
219 | let state_weak = Rc::downgrade(&state_copy); |
220 | state_copy.borrow().auto_play_timer.start( |
221 | slint::TimerMode::Repeated, |
222 | std::time::Duration::from_millis(200), |
223 | move || { |
224 | if let Some(state) = state_weak.upgrade() { |
225 | state.borrow_mut().random_move(); |
226 | } |
227 | }, |
228 | ); |
229 | } else { |
230 | state_copy.borrow().auto_play_timer.stop(); |
231 | } |
232 | }); |
233 | main_window.run().unwrap(); |
234 | } |
235 | |