parabox/world/algorithm/algorithm.rs
1use super::cycle::Cycle;
2use super::movement::{
3 Direction, EatInfo, EnterInfo, ExitInfo, IntoMoveResult, MoveProcessor, MoveResult, Movement,
4 SourceArrow, TargetArrow,
5};
6use super::rational::Rational;
7use crate::world::query::PositionState;
8use crate::{BlockKey, Position, World};
9use parabox_macros::trace_func;
10use std::collections::HashSet;
11use tracing::{debug, instrument};
12
13pub(crate) struct Algorithm {
14 trace: Cycle<BlockKey>,
15 movements: Vec<Movement>,
16 positioned: HashSet<Position>,
17}
18
19impl Algorithm {
20 pub fn new() -> Self {
21 Self {
22 trace: Cycle::new(),
23 movements: Vec::new(),
24 positioned: HashSet::new(),
25 }
26 }
27}
28
29impl Algorithm {
30 /// Returns `Ok(TargetArrow)` if the target is inside the container, or
31 /// `Err(ExitInfo)` if the target is out of bounds.
32 #[trace_func]
33 #[instrument(skip(self, world))]
34 fn source_to_target(
35 &self,
36 world: &World,
37 source: SourceArrow,
38 ) -> Result<TargetArrow, ExitInfo> {
39 // Compute the target pos according to the source pos and direction.
40 let (x, y) = source.position.pos;
41 let (x, y) = (x as isize, y as isize);
42 let (dx, dy) = source.direction.delta();
43 let (x, y) = (x + dx, y + dy);
44
45 let container = source.position.container.unwrap();
46 let (width, height) = container.get(world).proto.size();
47
48 // Check if the target pos is out of bounds.
49 if x < 0 || y < 0 || x >= width as isize || y >= height as isize {
50 // Compute and return the exit info.
51 let (offset, total) = match source.direction {
52 Direction::North | Direction::South => (x as usize, width),
53 Direction::East | Direction::West => (y as usize, height),
54 };
55
56 let precise = (source.precise + offset) / total;
57
58 Err(ExitInfo {
59 from: container,
60 direction: source.direction,
61 precise,
62 })
63 } else {
64 // Return the target arrow.
65 Ok(TargetArrow::new(
66 Position::inside(container, (x as usize, y as usize)),
67 source.direction,
68 source.precise,
69 ))
70 }
71 }
72
73 /// If the target position is empty, which means the movement can be
74 /// directly executed, then `Ok(Movement)` will be returned.
75 ///
76 /// If the target position is taken by a block, then `Err((Movement,
77 /// EnterInfo))` is returned, where `EnterInfo` is the information about the
78 /// possible block entering.
79 fn target_to_movement(
80 &self,
81 world: &World,
82 key: BlockKey,
83 target: TargetArrow,
84 ) -> Result<Movement, (Movement, EnterInfo)> {
85 let movement = Movement::new(key, target.position);
86
87 match world.position_state(target.position) {
88 PositionState::Present(key) => Err((
89 movement,
90 EnterInfo {
91 into: key,
92 direction: target.direction,
93 precise: target.precise,
94 },
95 )),
96 PositionState::Empty => Ok(movement),
97 _ => unreachable!("Target position is not valid"),
98 }
99 }
100
101 /// Confirms the movement, also returning `Ok(true)` for convenience.
102 ///
103 /// If `cycling` is `true`, then the movement will be checked before being
104 /// saved. This is because if the movement occurs in a cycle, a block
105 /// that triggers the cycle does not necessarily move.
106 #[trace_func]
107 #[instrument(skip(self))]
108 fn confirm(&mut self, movement: Movement, cycling: bool) -> MoveResult<bool> {
109 // Check if the movement is blocked.
110 //
111 // When `cycling` is `true`, the target block is always moving, leaving an empty
112 // space there. It will be blocked only if another block in the cycle is trying
113 // to move there.
114 //
115 // Since this function is called in the recursion of pushing movements, in the
116 // reverse order of the cycle, later movements will always be confirmed first,
117 // so finally the confirmed movements will be exactly the loop in the pushing
118 // cycle.
119 let blocked = cycling && self.positioned.contains(&movement.target);
120 debug!("whether blocked: {}", blocked);
121
122 if !blocked {
123 // Save the movement.
124 self.movements.push(movement);
125 self.positioned.insert(movement.target);
126 }
127
128 Ok(true)
129 }
130}
131
132impl Algorithm {
133 /// Tries to push the blocks from the source position.
134 ///
135 /// This function will evaluate the exit movements. Then it will call
136 /// [Algorithm::push_into] for the rest evaluation.
137 ///
138 /// Exits from a [crate::ProtoType::Void] block will be forbidden.
139 #[trace_func]
140 #[instrument(skip(self, world))]
141 fn push_from(&mut self, world: &World, key: BlockKey, source: SourceArrow) -> MoveResult<bool> {
142 let mut cycle: Cycle<BlockKey, Rational> = Cycle::new();
143 let mut current: SourceArrow = source;
144
145 loop {
146 // Try to push the block into the target position.
147 match self.source_to_target(world, current) {
148 Ok(target) => {
149 // The target is inside the container.
150 // Push the block into the target position.
151 break self.push_into(world, key, target, false);
152 }
153 Err(mut info) => {
154 // The target is out of bounds.
155 // Try to resolve the exit info.
156 while let Some(original) = cycle.push(info.from, info.precise) {
157 // The exit info is in a cycle.
158 // Use infinity to resolve the cycle.
159 info = MoveProcessor.infinity(
160 world,
161 ExitInfo {
162 from: info.from,
163 direction: info.direction,
164 precise: *original,
165 },
166 )?;
167 }
168
169 // Forbid exits from a void block.
170 if info.from.is_void(world) {
171 break Ok(false);
172 }
173
174 current = MoveProcessor.exit(world, info)?;
175 }
176 }
177 }
178 }
179
180 /// Tries to push the blocks into the target position.
181 ///
182 /// This function will evaluate the pushing, entering and eating movements.
183 /// If the target is empty, the movement will be directly confirmed.
184 /// Otherwise, the source block will first try to enter the target block,
185 /// and then try to eat the target block. When multiple entering movements
186 /// happen, eating movements will be tried in the reverse order.
187 ///
188 /// `eating` is `true` if the function is called during an eating movement.
189 /// When `A` eats `B`, it is equivalent to `B` entering `A`, which is almost
190 /// equivalent to pushing `B` into the position of `A`, except that `A`
191 /// should not try to push `B` or eat `B`.
192 ///
193 /// Movements directly inside a [crate::ProtoType::Void] block will be
194 /// forbidden.
195 #[trace_func]
196 #[instrument(skip(self, world))]
197 fn push_into(
198 &mut self,
199 world: &World,
200 key: BlockKey,
201 target: TargetArrow,
202 eating: bool,
203 ) -> MoveResult<bool> {
204 if self.trace.push(key, ()).is_some() {
205 // Found a pushing cycle.
206 debug!("cycle: {:?}", self.trace);
207
208 // Resolve the cycle by confirming the movements in the cycle.
209 //
210 // To achieve this, we start by returning `Ok(true)` for the last block in the
211 // cycle. Then every block in the cycle will confirm its movement by recursion.
212 return Ok(true);
213 }
214
215 let mut cycle: Cycle<EnterInfo> = Cycle::new();
216 let mut current: TargetArrow = target;
217
218 // Stop the first pushing try when eating.
219 let mut can_push = !eating;
220
221 loop {
222 // Try to push the block into the target position.
223 match self.target_to_movement(world, key, current) {
224 Ok(movement) => {
225 // The target is empty.
226 // Confirm the movement.
227 return self.confirm(movement, false);
228 }
229 Err((movement, mut info)) => {
230 // The target is taken by a block.
231 // Try to push the target block.
232 // But do not push when eating.
233 if can_push && self.push(world, info.into, info.direction)? {
234 return self.confirm(movement, true);
235 }
236
237 can_push = true;
238
239 // Forbid entering movements directly inside a void block.
240 if movement.target.is_in_void(world) {
241 break;
242 }
243
244 // Resolve the transferred enter info when entering an alias block.
245 info = MoveProcessor.alias(world, info);
246
247 // Try to resolve the enter info.
248 while cycle.push(info, ()).is_some() {
249 // The enter info is in a cycle.
250 // Use epsilon to resolve the cycle.
251 info = MoveProcessor.epsilon(world, info)?;
252 }
253
254 match MoveProcessor.enter(world, info) {
255 // Successfully entered the target block.
256 // Go to the next push-enter loop.
257 Some(next) => current = next,
258 // Failed to enter the target block.
259 None => break,
260 }
261 }
262 }
263 }
264
265 // Try to eat the target block, in the reverse order of entering movements.
266 while let Some((info, _)) = cycle.pop() {
267 // Stop the last eating try when eating.
268 // The last eating try corresponds to the first entering movement.
269 if eating && cycle.is_empty() {
270 break;
271 }
272
273 let eat_info = EatInfo {
274 eat: key,
275 ate: info.into,
276 direction: info.direction,
277 };
278
279 // Cannot eat a static block.
280 if eat_info.ate.get(world).proto.is_static() {
281 continue;
282 }
283
284 // Convert the eating movement into an entering movement.
285 let enter_info = MoveProcessor.eat(world, eat_info);
286
287 let enter_target = TargetArrow::new(
288 enter_info.into.get(world).state.position,
289 enter_info.direction,
290 enter_info.precise,
291 );
292
293 if self.push_into(world, eat_info.ate, enter_target, true)? {
294 return self.confirm(
295 Movement::new(key, info.into.get(world).state.position),
296 true,
297 );
298 }
299 }
300
301 self.trace.pop();
302
303 Ok(false)
304 }
305
306 /// Pushes the block in the specified direction, returns `Ok(true)` if the
307 /// block is successfully pushed, or `Ok(false)` if the block is
308 /// blocked.
309 #[trace_func]
310 #[instrument(skip(self, world))]
311 pub fn push(&mut self, world: &World, key: BlockKey, direction: Direction) -> MoveResult<bool> {
312 let block = key.get(world);
313 if block.proto.is_static() {
314 return Ok(false);
315 }
316 let position = block.state.position;
317 position.container.orphan(key)?;
318
319 let source = SourceArrow::new(position, direction, Rational::HALF);
320 self.push_from(world, key, source)
321 }
322}
323
324impl Algorithm {
325 pub fn commit(&self, world: &mut World) {
326 for movement in &self.movements {
327 world.place(movement.key, movement.target);
328 }
329 }
330}
331
332impl Position {
333 /// Whether the position is directly inside a void block.
334 fn is_in_void(&self, world: &World) -> bool {
335 self.container
336 .map(|key| key.get(world).proto.is_void())
337 .unwrap_or(false)
338 }
339}
340
341impl BlockKey {
342 /// Whether the block is void.
343 fn is_void(&self, world: &World) -> bool {
344 self.get(world).proto.is_void()
345 }
346}