Skip to content

Commit 51aef8a

Browse files
committed
feat: day 15 warehouse woes 2/2 soln, #24
1 parent 4271061 commit 51aef8a

File tree

8 files changed

+344
-5
lines changed

8 files changed

+344
-5
lines changed

src/2024/2024-12-15/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Visit the Advent of Code website for more information on this puzzle at:
44

55
**Source:** https://adventofcode.com/2024/day/15<br>
6-
**Status:** On-going
6+
**Status:** Complete ⭐
77

88
## Code
99

@@ -15,13 +15,27 @@ Visit the Advent of Code website for more information on this puzzle at:
1515
- **`findInitialPosition()`** - Finds the robot's initial `Point` position in the 2D grid and stores them in the `this.pos` object
1616
- **`readInstruction()`** - Reads the next instruction and sets the (y,x) direction
1717
- **`walk()`** - Increments the robot's (y,x) coordinate by direction
18-
- **`next()`** - Finds the next (y,x) coordinate of the robot or a given `Point` parameter.
18+
- **`next()`** - Finds the **next** (y,x) coordinate of the robot or a given `Point` parameter using the robot's current direction.
19+
- **`prev()`** - Finds the robot's **previous** (y,x) coordinate or a given `Point` parameter using the robot's current direction.
1920

2021
### `calculateGPS.ts`
2122

2223
- **`moveBoxes()`** - Moves the robot and boxes across the grid
2324
- **`calculateGPS()`** - Calculates the GPS sum of all boxes in the grid
2425

26+
### `calculateExpandedGPS.ts`
27+
28+
- **`moveExpandedBoxes()`** - Moves the robot and the expanded (2x size) boxes across the grid.
29+
- **`calculateExpandedGPS()`** - Calculates the GPS sum of all expanded boxes in the grid.
30+
31+
### `fileReader.ts`
32+
33+
- **`fileReader()`** - Reads and formats the day 15 quiz input file.
34+
35+
### `fileReaderExpanded.ts`
36+
37+
- **`fileReader()`** - Reads and formats the day 15 - part 2 quiz input file. Expands the tile symbols by 2.
38+
2539
## Notes
2640

2741
### Main Objects
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { RobotWarehouseData } from './types.js'
2+
3+
import { AOCOutputType, readAOCInputFile } from '@/aoc/file/aocfile.js'
4+
import { file } from '@/aoc/file/utils.js'
5+
6+
/**
7+
* Reads and formats the day 15 - part 2 quiz input file. Expands the tile symbols by 2.
8+
* @param {string} fileName - Input text filename path relative to this script
9+
* @returns {RobotWarehouseData} Quiz input data
10+
*/
11+
export const fileReaderExpand = (fileName: string): RobotWarehouseData => {
12+
const input = readAOCInputFile({
13+
filePath: file(import.meta.url, fileName),
14+
type: AOCOutputType.STRING
15+
}) as string
16+
17+
return input
18+
.split('\n\n')
19+
.reduce((data, item, index) => {
20+
// Grid
21+
if (index === 0) {
22+
return {
23+
...data,
24+
grid: item
25+
.split('\n')
26+
.map(line => line.split(''))
27+
.reduce((list: string[][], tiles) => {
28+
const processed = tiles.reduce((subList: string[], elem) => {
29+
if (['.', '#'].includes(elem)) {
30+
subList.push(elem, elem)
31+
} else if (elem === 'O') {
32+
subList.push('[', ']')
33+
} else if (elem === '@') {
34+
subList.push('@', '.')
35+
}
36+
37+
return subList
38+
}, [])
39+
40+
return [...list, processed]
41+
}, [])
42+
}
43+
}
44+
45+
// Instructions
46+
return {
47+
...data,
48+
instructions: item
49+
.split('')
50+
.reverse()
51+
.filter(direction => direction !== '\n')
52+
}
53+
}, {}) as RobotWarehouseData
54+
}

src/2024/2024-12-15/lib/robot.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class Robot {
7171
}
7272

7373
/**
74-
* Finds the next (y,x) coordinate of the robot or a given `Point` parameter.
74+
* Finds the next (y,x) coordinate of the robot or a given `Point` parameter using the robot's current direction.
7575
* @param {Point} [point] - (Optional) (y,x) coordinate to find the next step coorndinate from
7676
* @returns {Point} Next (y,x) coordinate after 1 step
7777
*/
@@ -86,4 +86,21 @@ export class Robot {
8686
x: nextX, y: nextY
8787
}
8888
}
89+
90+
/**
91+
* Finds the robot's previous (y,x) coordinate or a given `Point` parameter using the robot's current direction.
92+
* @param {Point} [point] - (Optional) (y,x) coordinate to find the previous step coorndinate from
93+
* @returns {Point} Next (y,x) coordinate before the provided coordinate
94+
*/
95+
prev (point?: Point): Point {
96+
const x = point?.x || this.pos.x
97+
const y = point?.y || this.pos.y
98+
99+
const nextX = x - this.direction.x
100+
const nextY = y - this.direction.y
101+
102+
return {
103+
x: nextX, y: nextY
104+
}
105+
}
89106
}

src/2024/2024-12-15/lib/utils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Point, PointSymbol } from '@/aoc/point/types.js'
2+
import { getCoordinateSymbol } from '@/aoc/grid/utils.js'
3+
4+
/**
5+
* Checks if the `symbol` parameter is an expanded box symbol
6+
* @param {string} symbol - String character representing an expanded box symbol
7+
* @returns {boolean} Flag indicating if the `symbol` character is a box symbol
8+
*/
9+
export const isExpandedBoxSymbol = (symbol: string) => ['[', ']'].includes(symbol as string)
10+
11+
/**
12+
* Retrieves the 2 half-box (y,x) coordinates of a full expanded box - left and right `PointSymbol`
13+
* @param {Point} point - (x,y) coordinate of one side of the box
14+
*/
15+
export const getBoxStartCoordinates = (point: Point, grid: string[][]):
16+
{ side1: PointSymbol, side2: PointSymbol } => {
17+
18+
const rightCoord = { ...point, x: point.x + 1 }
19+
const leftCoord = { ...point, x: point.x - 1 }
20+
let side2: PointSymbol = { x: -1, y: -1, symbol: '-' }
21+
22+
const symbol = getCoordinateSymbol(point, grid)?.symbol as string
23+
const right = getCoordinateSymbol(rightCoord, grid)?.symbol as string
24+
const left = getCoordinateSymbol(leftCoord, grid)?.symbol as string
25+
26+
if (`${symbol}${right}` === '[]') {
27+
side2 = { ...side2, ...rightCoord, symbol: right }
28+
}
29+
30+
if (`${left}${symbol}` === '[]') {
31+
side2 = { ...side2, ...leftCoord, symbol: left }
32+
}
33+
34+
const side1 = { ...point, symbol }
35+
36+
return {
37+
side1,
38+
side2
39+
}
40+
}
41+
42+
/**
43+
* Retrieves the reverse matching symbol of an expanded box symbol
44+
* @param {string} symbol - String character representing an expanded box symbol
45+
* @returns {string | undefined} Reverse matching symbol of an expanded box symbol or `undefined`
46+
*/
47+
export const getReverseSymbol = (symbol: string): string | undefined => {
48+
if (!['[', ']'].includes(symbol)) return
49+
return symbol === '[' ? ']' : '['
50+
}
51+
52+
/**
53+
* Retrieves the `x` direction associated with an expanded box symbol
54+
* @param symbol - String character representing an expanded box symbol
55+
* @returns {number | undefined}
56+
*/
57+
export const getSymbolDirection = (symbol: string): number | undefined => {
58+
if (!['[', ']'].includes(symbol)) return
59+
60+
let xDirection = 0
61+
62+
if (symbol === '[') xDirection = 1
63+
if (symbol === ']') xDirection = -1
64+
65+
return xDirection
66+
}

src/2024/2024-12-15/main.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { fileReader } from './lib/fileReader.js'
2+
import { fileReaderExpand } from './lib/fileReaderExpanded.js'
3+
24
import { moveBoxes, calculateGPS } from './lib/calculateGPS.js'
5+
import { moveExpandedBoxes, calculateExpandedGPS } from './lib/calculateExpandedGPS.js'
36

47
/**
58
* Part 1/2 of the 2024-12-15 quiz
@@ -11,7 +14,21 @@ const quiz20241215_01 = () => {
1114
moveBoxes(grid, instructions)
1215
const gps = calculateGPS(grid)
1316

14-
console.log('---GPS', gps)
17+
console.log('Part 1: Regular boxes GPS:', gps)
18+
}
19+
20+
/**
21+
* Part 2/2 of the 2024-12-15 quiz
22+
* Moves the robot, expanded boxes and calculates all expanded boxes' GPS
23+
*/
24+
const quiz20241215_02 = () => {
25+
const { grid, instructions } = fileReaderExpand('../input.txt')
26+
27+
moveExpandedBoxes(grid, instructions)
28+
const gps = calculateExpandedGPS(grid)
29+
30+
console.log('Part 2: Expanded boxes GPS:', gps)
1531
}
1632

1733
quiz20241215_01()
34+
quiz20241215_02()

0 commit comments

Comments
 (0)