|  | 
|  | 1 | +import type { Point, PointSymbol } from '@/aoc/point/types.js' | 
|  | 2 | + | 
|  | 3 | +import { getCoordinateSymbol, getGridDimensions, isOutOfBounds } from '@/aoc/grid/utils.js' | 
|  | 4 | + | 
|  | 5 | +import { | 
|  | 6 | +  getBoxStartCoordinates, | 
|  | 7 | +  getReverseSymbol, | 
|  | 8 | +  getSymbolDirection, | 
|  | 9 | +  isExpandedBoxSymbol | 
|  | 10 | +} from './utils.js' | 
|  | 11 | + | 
|  | 12 | +import { Robot } from './robot.js' | 
|  | 13 | + | 
|  | 14 | +/** | 
|  | 15 | + * Moves the robot and the expanded (2x size) boxes across the grid | 
|  | 16 | + * @param {string[][]} grid - 2D string array containing walls, boxes and space data | 
|  | 17 | + * @param {string[]} instructions - String array containing the robot's pushBox instructions | 
|  | 18 | + * @returns {void} | 
|  | 19 | + */ | 
|  | 20 | +export const moveExpandedBoxes = (grid: string[][], instructions: string[]): void => { | 
|  | 21 | +  const dimensions = getGridDimensions(grid) | 
|  | 22 | +  const robot = new Robot(grid, instructions) | 
|  | 23 | + | 
|  | 24 | +  while (robot.instructions.length > 0) { | 
|  | 25 | +    robot.readInstruction() | 
|  | 26 | + | 
|  | 27 | +    let nextPos = robot.next() | 
|  | 28 | +    let nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol as string | 
|  | 29 | + | 
|  | 30 | +    // Move robot to the next blank space | 
|  | 31 | +    if (nextSymbol === '.') { | 
|  | 32 | +      grid[robot.pos.y]![robot.pos.x] = '.' | 
|  | 33 | +      grid[nextPos.y]![nextPos.x] = robot.symbol | 
|  | 34 | +      robot.walk() | 
|  | 35 | +    } | 
|  | 36 | + | 
|  | 37 | +    if (isExpandedBoxSymbol(nextSymbol)) { | 
|  | 38 | +      // Move boxes along the horizontal x-axis | 
|  | 39 | +      if (robot.direction.x !== 0) { | 
|  | 40 | +        const boxes: Point[] = [nextPos] | 
|  | 41 | + | 
|  | 42 | +        while (!isOutOfBounds(nextPos, dimensions) && isExpandedBoxSymbol(nextSymbol)) { | 
|  | 43 | +          nextPos = robot.next(nextPos) | 
|  | 44 | +          nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol as string | 
|  | 45 | + | 
|  | 46 | +          if (isExpandedBoxSymbol(nextSymbol)) { | 
|  | 47 | +            boxes.push(nextPos) | 
|  | 48 | +          } | 
|  | 49 | +        } | 
|  | 50 | + | 
|  | 51 | +        // Move groups of boxes if there's a space at their end | 
|  | 52 | +        if (nextSymbol === '.' && boxes.length > 0) { | 
|  | 53 | +          for (let i = boxes.length - 1; i >= 0; i -= 1) { | 
|  | 54 | +            const next = robot.next(boxes[i]) | 
|  | 55 | +            const temp = grid[boxes[i]!.y]![boxes[i]!.x] as string | 
|  | 56 | + | 
|  | 57 | +            grid[boxes[i]!.y]![boxes[i]!.x] = '.' | 
|  | 58 | +            grid[next.y]![next.x] = temp | 
|  | 59 | +          } | 
|  | 60 | + | 
|  | 61 | +          // Move the robot | 
|  | 62 | +          grid[robot.pos.y]![robot.pos.x] = '.' | 
|  | 63 | +          robot.walk() | 
|  | 64 | +          grid[robot.pos.y]![robot.pos.x] = robot.symbol | 
|  | 65 | +        } | 
|  | 66 | +      } | 
|  | 67 | + | 
|  | 68 | +      // Move boxes along the vertical y-axis | 
|  | 69 | +      if (robot.direction.y !== 0) { | 
|  | 70 | +        // 1st box after the robot | 
|  | 71 | +        const { side1, side2 } = getBoxStartCoordinates(nextPos, grid) | 
|  | 72 | +        let start = { ...side1 } | 
|  | 73 | + | 
|  | 74 | +        const visited: PointSymbol[] = [] | 
|  | 75 | +        const retrace: PointSymbol[] = [side2] | 
|  | 76 | +        let canMove = true | 
|  | 77 | + | 
|  | 78 | +        while (start && isExpandedBoxSymbol(start.symbol)) { | 
|  | 79 | +          const next = robot.next(start) | 
|  | 80 | +          const nextSym = getCoordinateSymbol(next, grid)!.symbol | 
|  | 81 | + | 
|  | 82 | +          // Store the visited box coordinates | 
|  | 83 | +          visited.push(start) | 
|  | 84 | + | 
|  | 85 | +          if (['.', '#'].includes(nextSym as string)) { | 
|  | 86 | +            // End of line reached (space or wall): Retrace unvisited connected coordinates | 
|  | 87 | +            start = retrace.pop() as PointSymbol | 
|  | 88 | + | 
|  | 89 | +            // Connected boxes will not move at anytime a wall `#` is encountered | 
|  | 90 | +            if (nextSym === '#') { | 
|  | 91 | +              canMove = false | 
|  | 92 | +            } | 
|  | 93 | +          } else if (nextSym === start.symbol) { | 
|  | 94 | +            // Found stack of boxes: | 
|  | 95 | +            // Move to the next (up/down) coordinate if its symbol is similar to the current symbol | 
|  | 96 | +            start = { ...next } as PointSymbol | 
|  | 97 | +            start.symbol = getCoordinateSymbol(start, grid)!.symbol | 
|  | 98 | +          } else if (nextSym === getReverseSymbol(start.symbol)) { | 
|  | 99 | +            // Found half-stacked boxes: | 
|  | 100 | +            // Stack the next coordinate if it's symbol is different from the current symbol | 
|  | 101 | +            retrace.push({ ...next, symbol: nextSym }) | 
|  | 102 | + | 
|  | 103 | +            // Move to the next diagonally-aligned half-box symbol coordinate | 
|  | 104 | +            const xDirection = getSymbolDirection(nextSym) | 
|  | 105 | + | 
|  | 106 | +            if (xDirection) { | 
|  | 107 | +              start = { ...next, x: next.x + xDirection } as PointSymbol | 
|  | 108 | +              start.symbol = getCoordinateSymbol(start, grid)!.symbol | 
|  | 109 | +            } | 
|  | 110 | +          } | 
|  | 111 | +        } | 
|  | 112 | + | 
|  | 113 | +        // Move the boxes | 
|  | 114 | +        if (canMove) { | 
|  | 115 | +          visited | 
|  | 116 | +            .sort((a, b) => { | 
|  | 117 | +              if (robot.direction.y < 0) { | 
|  | 118 | +                return (a.y < b.y ? -1 : 1) | 
|  | 119 | +              } else { | 
|  | 120 | +                return (a.y > b.y ? -1 : 1) | 
|  | 121 | +              } | 
|  | 122 | +            }) | 
|  | 123 | +            .forEach(item => { | 
|  | 124 | +              const next = robot.next(item) | 
|  | 125 | +              grid[item.y]![item.x] = '.' | 
|  | 126 | +              grid[next.y]![next.x] = item.symbol | 
|  | 127 | +            }) | 
|  | 128 | + | 
|  | 129 | +          grid[robot.pos.y]![robot.pos.x] = '.' | 
|  | 130 | +          grid[nextPos.y]![nextPos.x] = robot.symbol | 
|  | 131 | +          robot.walk() | 
|  | 132 | +        } | 
|  | 133 | +      } | 
|  | 134 | +    } | 
|  | 135 | +  } | 
|  | 136 | +} | 
|  | 137 | + | 
|  | 138 | +/** | 
|  | 139 | + * Calculates the GPS sum of all expanded boxes in the grid | 
|  | 140 | + * @param {string[][]} grid - 2D string array containing walls, boxes and space data | 
|  | 141 | + * @returns {number} GPS sum of all boxes in the grid | 
|  | 142 | + */ | 
|  | 143 | +export const calculateExpandedGPS = (grid: string[][]): number => { | 
|  | 144 | +  const dimensions = getGridDimensions(grid) | 
|  | 145 | +  let sumGPS = 0 | 
|  | 146 | + | 
|  | 147 | +  for (let y = 0; y < dimensions.length; y += 1) { | 
|  | 148 | +    for (let x = 0; x < dimensions.width; x += 1) { | 
|  | 149 | +      if (grid[y]![x] === '[') { | 
|  | 150 | +        sumGPS += y * 100 + x | 
|  | 151 | +      } | 
|  | 152 | +    } | 
|  | 153 | +  } | 
|  | 154 | + | 
|  | 155 | +  return sumGPS | 
|  | 156 | +} | 
0 commit comments