Browse Source

Functional part rotations

Christoph Stelz 1 year ago
parent
commit
daef05022f
1 changed files with 186 additions and 28 deletions
  1. 186 28
      src/lib.rs

+ 186 - 28
src/lib.rs

@@ -20,6 +20,8 @@ use std::collections::HashMap;
  *          +--------+                   \/
  *              1
  *
+ *      counter-clockwise            clockwise
+ *
  */
 
 #[derive(Debug,PartialEq,Copy,Clone)]
@@ -39,7 +41,8 @@ pub type Part = Vec<Edge>;
 
 
 /* For example, the brown part (1) could be described by starting at the lower right triangle and
- * traversing edges in the following order: 3, 3, 1, 1, 2, 1, 3
+ * traversing edges in the following order:
+ * 3, 3, 1, 1, 2, 1, 3
  *      ____
  *      \  /\
  *       \/__\
@@ -49,7 +52,85 @@ pub type Part = Vec<Edge>;
  *       \/
  *
  *         (1)
+ *
+ * Rotations look as follows:
+ *
+ * 120 degrees.
+ * Our edges are now 1, 2, 2, 3, 3, 3, 1
+ *
+ *
+ *   --> /\
+ *      /__\___
+ *     /\  /\  /
+ *    /__\/__\/
+ *    \  /
+ *     \/
+ *
+ * 240 degrees.
+ *
+ * 2, 1, 3, 2, 1, 2, 2
+ *      ____
+ *      \  /\
+ *       \/__\____
+ *       /\  /\  /
+ *  --> /__\/__\/
+ *
+ *
+ *  We observe the following principle:
+ *
+ *          0 deg      120deg       240deg
+ *
+ *        3 /\ 2      2 /\ 1       1 /\ 3
+ *         /__\        /__\         /__\
+ *          1           3            2
+ *
+ *          1            2           3
+ *        __v_         __v_        __v_
+ *        \  /         \  /        \  / 
+ *       3 \/ 2       1 \/ 3      2 \/ 1
+ *          
+ *
+ * For upright triangles the edges can be translated as follows: 1 => 2, 2 => 3, 3 => 1.
+ *
+ * For upside-down triangles, we need to take special care, since their order is specified 
+ * clockwise, in order to keep or left-right-convention. Thus, the edge translation looks as
+ * follows:  1 => 3, 2 => 1, 3 => 2
+ *
+ * In other words, for upright triangles we do a left-shift, on upside-down triangles we do
+ * a right-shift.
+ *
  */
+
+impl Edge {
+    fn from_int(x: i8) -> Edge {
+        match x {
+            0 => Edge::BOTTOMTOP,
+            1 => Edge::RIGHT,
+            2 => Edge::LEFT,
+            _ => panic!("Invalid argument"),
+        }
+    }
+
+    fn to_int(&self) -> i8 {
+        match self {
+            Edge::BOTTOMTOP => 0,
+            Edge::RIGHT => 1,
+            Edge::LEFT => 2,
+        }
+    }
+
+    fn rotate(&self, other: &Edge, upright: bool) -> Edge {
+        let shift_amount = other.to_int();
+        let shift_direction = if upright { -1 } else { 1 };
+
+        let shifted = (self.to_int() + shift_direction * shift_amount).rem_euclid(3);
+        println!("Shifted: {}", shifted);
+
+        Edge::from_int(shifted)
+    }
+
+}
+
 pub fn generate_parts() -> HashMap<PartID, Part> {
     HashMap::from([
         (1, vec![Edge::LEFT, Edge::LEFT, Edge::BOTTOMTOP, Edge::BOTTOMTOP, Edge::RIGHT, Edge::BOTTOMTOP, Edge::LEFT]),
@@ -110,8 +191,10 @@ impl Cell {
  * size of 16x16.
  * Since there are two sides, we store both of them in a tuple.
  */
-pub type MapSide = [[Cell; 16]; 16];
-pub type Map = (MapSide, MapSide);
+pub const MAP_WIDTH : u8 = 16;
+pub const MAP_HEIGHT : u8 = 5;
+pub type Map = [[Cell; MAP_WIDTH as usize]; MAP_HEIGHT as usize];
+
 
 /**
  * Helper function that tells us whether a given coordinate is an upright or upside-down triangle
@@ -130,7 +213,7 @@ pub fn is_upright(x: u8, y: u8) -> bool {
  * Helper function that returns true if the coordinates are within map bounds
  */
 pub fn in_bound(x: u8, y: u8) -> bool {
-    x < 16 && y < 16
+    x < MAP_WIDTH && y < MAP_HEIGHT
 }
 
 /**
@@ -147,14 +230,14 @@ pub fn move_along_edge(x: u8, y: u8, edge: Edge) -> Option<(u8, u8)> {
             return Some((x - 1, y));
         },
         Edge::RIGHT => {
-            if x == 15 {
+            if x == MAP_WIDTH - 1 {
                 return None;
             }
 
             return Some((x + 1, y));
         },
         Edge::BOTTOMTOP => {
-            if (is_upright(x, y) && y == 15) 
+            if (is_upright(x, y) && y == MAP_HEIGHT - 1) 
                 || (!is_upright(x,y) && y == 0) {
                 return None;
             }
@@ -169,6 +252,7 @@ pub fn move_along_edge(x: u8, y: u8, edge: Edge) -> Option<(u8, u8)> {
     }
 }
 
+
 /*
  * Parts can be inverted (flipped on the other side) and also rotated.
  * On a triangular grid, three rotations are possible.
@@ -179,7 +263,7 @@ pub fn move_along_edge(x: u8, y: u8, edge: Edge) -> Option<(u8, u8)> {
  *
  * Returns true if the part can fit at the specified location on the map.
  */
-pub fn check_part(map: &MapSide, part: &Part, x: u8, y: u8, rotation: Edge) -> bool {
+pub fn check_part(map: &Map, part: &Part, x: u8, y: u8, rotation: Edge) -> bool {
     /* Make sure the start triangle can actually be placed */
     assert!(in_bound(x, y));
 
@@ -190,9 +274,9 @@ pub fn check_part(map: &MapSide, part: &Part, x: u8, y: u8, rotation: Edge) -> b
     let mut mx : u8 = x;
     let mut my : u8 = y;
 
-    println!("Starting at {}, {}", x, y);
     for movement in part {
-        let result = move_along_edge(mx, my, *movement);
+        let rotated_movement = movement.rotate(&rotation, is_upright(x, y));
+        let result = move_along_edge(mx, my, rotated_movement);
 
         if result.is_none() {
             return false;
@@ -203,13 +287,79 @@ pub fn check_part(map: &MapSide, part: &Part, x: u8, y: u8, rotation: Edge) -> b
         if !map[my as usize][mx as usize].is_empty() || !in_bound(mx, my) {
             return false;
         }
-        println!("Went to {}, {}", mx, my);
     }
     println!("");
 
     return true;
 }
 
+/* Once we have found a valid position with `check_part`, we can place the part on the map.
+ */
+fn place_part(map: &mut Map, id: PartID, part: &Part, x: u8, y: u8, rotation: Edge) {
+    map[y as usize][x as usize] = Cell::Occupied(id);
+
+    let mut mx : u8 = x;
+    let mut my : u8 = y;
+
+    for movement in part {
+        let rotated_movement = movement.rotate(&rotation, is_upright(x, y));
+        (mx, my) = move_along_edge(mx, my, rotated_movement).unwrap();
+        map[my as usize][mx as usize] = Cell::Occupied(id);
+    }
+
+}
+
+/* If a part is not placed right, we might need to remove it from the map again
+ */
+fn erase_part(map: &mut Map, part: &Part, x: u8, y: u8, rotation: Edge) {
+    map[y as usize][x as usize] = Cell::Empty;
+
+    let mut mx : u8 = x;
+    let mut my : u8 = y;
+
+    for movement in part {
+        let rotated_movement = movement.rotate(&rotation, is_upright(x, y));
+        (mx, my) = move_along_edge(mx, my, rotated_movement).unwrap();
+        map[my as usize][mx as usize] = Cell::Empty;
+    }
+
+}
+
+pub fn solve(map: &mut Map, parts: &[(PartID, &Part)]) -> Option<()>{
+    if parts.len() == 0 {
+        return Some(());
+    }
+
+    let (id, part) = parts[0];
+
+
+    // Todo: limit to non-barrier positions
+    for x in 0..MAP_WIDTH {
+        for y in 0..MAP_HEIGHT {
+            if !map[y as usize][x as usize].is_empty() {
+                continue;
+            }
+
+            for rotation in [Edge::BOTTOMTOP, Edge::LEFT, Edge::RIGHT] {
+                let valid_position = check_part(&map, &part, x, y, rotation);
+
+                if valid_position {
+                    // Place our part and try the others
+                    place_part(map, id, &part, x, y, rotation);
+                    if let Some(()) = solve(map, &parts[1..]) {
+                        // We have found a valid solution we can pass back!
+                        return Some(());
+                    } else {
+                        // Remove the part from our position and try again
+                        erase_part(map, &part, x, y, rotation);
+                    }
+                }
+            }
+        }
+    }
+
+    None
+}
 
 #[cfg(test)]
 mod test {
@@ -239,48 +389,56 @@ mod test {
         
     }
 
+    #[test]
+    fn test_rotation() {
+        // 120 deg counter-clockwise rotation
+        for (expected, original, upright) in [
+            (3, 1, true), (1, 2, true), (2, 3, true),
+            (2, 1, false), (1, 3, false), (3, 2, false)] {
+            assert_eq!(Edge::from_int(expected - 1), Edge::from_int(original - 1)
+                       .rotate(&Edge::RIGHT, upright));
+        }
+
+        // 240 deg counter-clockwise rotation
+        for (expected, original, upright) in [
+            (3, 2, true), (1, 3, true), (2, 1, true),
+            (2, 3, false), (1, 2, false), (3, 1, false)] {
+            assert_eq!(Edge::from_int(expected - 1), Edge::from_int(original - 1)
+                       .rotate(&Edge::LEFT, upright));
+        }
+    }
+
     #[test]
     fn test_part_placement() {
         let B = Cell::Barrier;
         let E = Cell::Empty;
 
-        let left : MapSide = [
+        let map : Map = [
             [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
             [B, B, E, E, B, B, B, B, B, B, B, B, B, B, B, B],
             [B, B, E, E, E, B, B, B, B, B, B, B, B, B, B, B],
             [B, B, E, B, B, B, B, B, B, B, B, B, B, B, B, B],
             [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
-            [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
         ];
 
         let parts = generate_parts();
         let brown_part = parts.get(&1).unwrap();
 
 
-        assert!(check_part(&left, &brown_part, 4, 2, Edge::BOTTOMTOP));
+        assert!(check_part(&map, &brown_part, 4, 2, Edge::BOTTOMTOP));
         let mut called : usize = 0;
-        for x in 0..15 {
-            for y in 0..15 {
+        for x in 0..MAP_WIDTH {
+            for y in 0..MAP_HEIGHT {
                 for rotation in [Edge::LEFT, Edge::RIGHT, Edge::BOTTOMTOP] {
-                    if check_part(&left, &brown_part, x, y, Edge::BOTTOMTOP) {
-                        //assert_eq!(Edge::BOTTOMTOP, rotation);
+                    if check_part(&map, &brown_part, x, y, rotation) {
+                        assert_eq!(Edge::BOTTOMTOP, rotation);
                         assert_eq!((4, 2), (x, y));
                         called += 1;
                     }
                 }
             }
         }
-        assert_eq!(3, called);
+        assert_eq!(1, called);
 
     }