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}