diff --git a/README.md b/README.md
index b3c8d61..4bf1dbf 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ This repository contains solutions and a local development environment for the [
- Day 12: Garden Groups [[link]](/src/2024/2024-12-12/README.md)
- Day 13: Claw Contraption [[link]](/src/2024/2024-12-13/README.md)
- Day 14: Restroom Redoubt [[link]](/src/2024/2024-12-14/README.md)
+- Day 15: Warehouse Woes [[link]](/src/2024/2024-12-15/README.md)
diff --git a/src/2024/2024-12-06/lib/guard.types.ts b/src/2024/2024-12-06/lib/guard.types.ts
index 4106c64..d2efd31 100644
--- a/src/2024/2024-12-06/lib/guard.types.ts
+++ b/src/2024/2024-12-06/lib/guard.types.ts
@@ -3,9 +3,9 @@
*
* @enum {string}
* @property {string} UP - Upward direction `"^"`
- * @property {string} RIGHT - Upward direction `">"`
- * @property {string} DOWN - Upward direction `"v"`
- * @property {string} LEFT - Upward direction `"<"`
+ * @property {string} RIGHT - Right direction `">"`
+ * @property {string} DOWN - Downward direction `"v"`
+ * @property {string} LEFT - Left direction `"<"`
*/
export enum GuardDirection {
UP = '^',
diff --git a/src/2024/2024-12-15/README.md b/src/2024/2024-12-15/README.md
new file mode 100644
index 0000000..d2522b1
--- /dev/null
+++ b/src/2024/2024-12-15/README.md
@@ -0,0 +1,95 @@
+## Day 15: Warehouse Woes
+
+Visit the Advent of Code website for more information on this puzzle at:
+
+**Source:** https://adventofcode.com/2024/day/15
+**Status:** Complete ⭐⭐
+
+
+(Part2 animation from AoC online sample)
+
+## Code
+
+### `robot.ts`
+
+**Robot** class
+
+- Manages the `Robot` object that runs across the grid and moves boxes
+- **`findInitialPosition()`** - Finds the robot's initial `Point` position in the 2D grid and stores them in the `this.pos` object
+- **`readInstruction()`** - Reads the next instruction and sets the (y,x) direction
+- **`walk()`** - Increments the robot's (y,x) coordinate by direction
+- **`next()`** - Finds the **next** (y,x) coordinate of the robot or a given `Point` parameter using the robot's current direction.
+- **`prev()`** - Finds the robot's **previous** (y,x) coordinate or a given `Point` parameter using the robot's current direction.
+
+### `calculateGPS.ts`
+
+- **`moveBoxes()`** - Moves the robot and boxes across the grid
+- **`calculateGPS()`** - Calculates the GPS sum of all boxes in the grid
+
+### `calculateExpandedGPS.ts`
+
+- **`moveExpandedBoxes()`** - Moves the robot and the expanded (2x size) boxes across the grid.
+- **`calculateExpandedGPS()`** - Calculates the GPS sum of all expanded boxes in the grid.
+
+### `fileReader.ts`
+
+- **`fileReader()`** - Reads and formats the day 15 quiz input file.
+
+### `fileReaderExpanded.ts`
+
+- **`fileReader()`** - Reads and formats the day 15 - part 2 quiz input file. Expands the tile symbols by 2.
+
+## Notes
+
+### Main Objects
+
+1. GRID
+ - 2d board array
+ - contains
+ - blank space: `.`
+ - walls: `#`
+ - box: `O`
+ - amok robot: = `@`
+
+2. WALLS
+ - symbol `#`
+ - surrounds grid edges
+ - found inside grid
+ - impassable by: `O`, `@`
+ - passable: GPS
+
+3. BOX
+ - symbol `O`
+ - impassable: `@`, other `O`
+ - passable: GPS
+ - can be moved up/down/left/right by `@`
+
+4. ROBOT
+ - symbol `@`
+ - has move instructions
+ - can move up/down/left/rigt
+ - can push `O`
+ - cannot move if blocked by unpushable `O`
+ - cannot move if blocked by `#`
+
+### Algo
+
+#### Phase 1 - Move Boxes
+
+- initialize all pos in grid
+- read robot move instruction
+- turn + move according to instruction
+- if symbol ahead is `O`
+ - chain pushing preceeding `O`s
+ - if `O` unmovable, stop
+- if symbol ahead is `.`
+ - move
+- if symbol ahead is `#`
+ - stop
+- repeat until end of move instructions
+
+#### Phase 2 - Calculate Box GPS
+
+- for each box,
+ - GPS = 100 * distance from top + distance from left
+- sum all box GPS
diff --git a/src/2024/2024-12-15/assets/part2.gif b/src/2024/2024-12-15/assets/part2.gif
new file mode 100644
index 0000000..b9ce532
Binary files /dev/null and b/src/2024/2024-12-15/assets/part2.gif differ
diff --git a/src/2024/2024-12-15/input.txt b/src/2024/2024-12-15/input.txt
new file mode 100644
index 0000000..8d35d95
--- /dev/null
+++ b/src/2024/2024-12-15/input.txt
@@ -0,0 +1,10 @@
+########
+#..O.O.#
+##@.O..#
+#...O..#
+#.#.O..#
+#...O..#
+#......#
+########
+
+<^>>v>>^v
\ No newline at end of file
diff --git a/src/2024/2024-12-15/input2.txt b/src/2024/2024-12-15/input2.txt
new file mode 100644
index 0000000..621c19c
--- /dev/null
+++ b/src/2024/2024-12-15/input2.txt
@@ -0,0 +1,15 @@
+##########
+#..O..O.O#
+#......O.#
+#.OO..O.O#
+#..O@..O.#
+#O#..O...#
+#O..O..O.#
+#.OO.O.OO#
+#....O...#
+##########
+
+^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^
+vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<
+<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
\ No newline at end of file
diff --git a/src/2024/2024-12-15/lib/calculateExpandedGPS.ts b/src/2024/2024-12-15/lib/calculateExpandedGPS.ts
new file mode 100644
index 0000000..06f0066
--- /dev/null
+++ b/src/2024/2024-12-15/lib/calculateExpandedGPS.ts
@@ -0,0 +1,156 @@
+import type { Point, PointSymbol } from '@/aoc/point/types.js'
+
+import { getCoordinateSymbol, getGridDimensions, isOutOfBounds } from '@/aoc/grid/utils.js'
+
+import {
+ getBoxStartCoordinates,
+ getReverseSymbol,
+ getSymbolDirection,
+ isExpandedBoxSymbol
+} from './utils.js'
+
+import { Robot } from './robot.js'
+
+/**
+ * Moves the robot and the expanded (2x size) boxes across the grid
+ * @param {string[][]} grid - 2D string array containing walls, boxes and space data
+ * @param {string[]} instructions - String array containing the robot's pushBox instructions
+ * @returns {void}
+ */
+export const moveExpandedBoxes = (grid: string[][], instructions: string[]): void => {
+ const dimensions = getGridDimensions(grid)
+ const robot = new Robot(grid, instructions)
+
+ while (robot.instructions.length > 0) {
+ robot.readInstruction()
+
+ let nextPos = robot.next()
+ let nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol as string
+
+ // Move robot to the next blank space
+ if (nextSymbol === '.') {
+ grid[robot.pos.y]![robot.pos.x] = '.'
+ grid[nextPos.y]![nextPos.x] = robot.symbol
+ robot.walk()
+ }
+
+ if (isExpandedBoxSymbol(nextSymbol)) {
+ // Move boxes along the horizontal x-axis
+ if (robot.direction.x !== 0) {
+ const boxes: Point[] = [nextPos]
+
+ while (!isOutOfBounds(nextPos, dimensions) && isExpandedBoxSymbol(nextSymbol)) {
+ nextPos = robot.next(nextPos)
+ nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol as string
+
+ if (isExpandedBoxSymbol(nextSymbol)) {
+ boxes.push(nextPos)
+ }
+ }
+
+ // Move groups of boxes if there's a space at their end
+ if (nextSymbol === '.' && boxes.length > 0) {
+ for (let i = boxes.length - 1; i >= 0; i -= 1) {
+ const next = robot.next(boxes[i])
+ const temp = grid[boxes[i]!.y]![boxes[i]!.x] as string
+
+ grid[boxes[i]!.y]![boxes[i]!.x] = '.'
+ grid[next.y]![next.x] = temp
+ }
+
+ // Move the robot
+ grid[robot.pos.y]![robot.pos.x] = '.'
+ robot.walk()
+ grid[robot.pos.y]![robot.pos.x] = robot.symbol
+ }
+ }
+
+ // Move boxes along the vertical y-axis
+ if (robot.direction.y !== 0) {
+ // 1st box after the robot
+ const { side1, side2 } = getBoxStartCoordinates(nextPos, grid)
+ let start = { ...side1 }
+
+ const visited: PointSymbol[] = []
+ const retrace: PointSymbol[] = [side2]
+ let canMove = true
+
+ while (start && isExpandedBoxSymbol(start.symbol)) {
+ const next = robot.next(start)
+ const nextSym = getCoordinateSymbol(next, grid)!.symbol
+
+ // Store the visited box coordinates
+ visited.push(start)
+
+ if (['.', '#'].includes(nextSym as string)) {
+ // End of line reached (space or wall): Retrace unvisited connected coordinates
+ start = retrace.pop() as PointSymbol
+
+ // Connected boxes will not move at anytime a wall `#` is encountered
+ if (nextSym === '#') {
+ canMove = false
+ }
+ } else if (nextSym === start.symbol) {
+ // Found stack of boxes:
+ // Move to the next (up/down) coordinate if its symbol is similar to the current symbol
+ start = { ...next } as PointSymbol
+ start.symbol = getCoordinateSymbol(start, grid)!.symbol
+ } else if (nextSym === getReverseSymbol(start.symbol)) {
+ // Found half-stacked boxes:
+ // Stack the next coordinate if it's symbol is different from the current symbol
+ retrace.push({ ...next, symbol: nextSym })
+
+ // Move to the next diagonally-aligned half-box symbol coordinate
+ const xDirection = getSymbolDirection(nextSym)
+
+ if (xDirection) {
+ start = { ...next, x: next.x + xDirection } as PointSymbol
+ start.symbol = getCoordinateSymbol(start, grid)!.symbol
+ }
+ }
+ }
+
+ // Move the boxes
+ if (canMove) {
+ visited
+ .sort((a, b) => {
+ if (robot.direction.y < 0) {
+ return (a.y < b.y ? -1 : 1)
+ } else {
+ return (a.y > b.y ? -1 : 1)
+ }
+ })
+ .forEach(item => {
+ const next = robot.next(item)
+ grid[item.y]![item.x] = '.'
+ grid[next.y]![next.x] = item.symbol
+ })
+
+ grid[robot.pos.y]![robot.pos.x] = '.'
+ grid[nextPos.y]![nextPos.x] = robot.symbol
+ robot.walk()
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Calculates the GPS sum of all expanded boxes in the grid
+ * @param {string[][]} grid - 2D string array containing walls, boxes and space data
+ * @returns {number} GPS sum of all boxes in the grid
+ */
+export const calculateExpandedGPS = (grid: string[][]): number => {
+ const dimensions = getGridDimensions(grid)
+ let sumGPS = 0
+
+ for (let y = 0; y < dimensions.length; y += 1) {
+ for (let x = 0; x < dimensions.width; x += 1) {
+ if (grid[y]![x] === '[') {
+ sumGPS += y * 100 + x
+ }
+ }
+ }
+
+ return sumGPS
+}
diff --git a/src/2024/2024-12-15/lib/calculateGPS.ts b/src/2024/2024-12-15/lib/calculateGPS.ts
new file mode 100644
index 0000000..ba7fd5d
--- /dev/null
+++ b/src/2024/2024-12-15/lib/calculateGPS.ts
@@ -0,0 +1,79 @@
+import type { Point } from '@/aoc/point/types.js'
+
+import { getCoordinateSymbol, isOutOfBounds } from '@/aoc/grid/utils.js'
+import { Robot } from './robot.js'
+
+/**
+ * Moves the robot and boxes across the grid
+ * @param {string[][]} grid - 2D string array containing walls, boxes and space data
+ * @param {string[]} instructions - String array containing the robot's move instructions
+ * @returns {void}
+ */
+export const moveBoxes = (grid: string[][], instructions: string[]): void => {
+ const dimensions = { length: grid.length, width: grid[0]!.length }
+ const robot = new Robot(grid, instructions)
+
+ while (robot.instructions.length > 0) {
+ robot.readInstruction()
+
+ let nextPos = robot.next()
+ let nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol
+
+ // Move robot to the next blank space
+ if (nextSymbol === '.') {
+ grid[robot.pos.y]![robot.pos.x] = '.'
+ grid[nextPos.y]![nextPos.x] = robot.symbol
+ robot.walk()
+ }
+
+ // Find connected boxes until a space symbol
+ if (nextSymbol === 'O') {
+ const boxes: Point[] = [nextPos]
+
+ while (!isOutOfBounds(nextPos, dimensions) && nextSymbol === 'O') {
+ nextPos = robot.next(nextPos)
+ nextSymbol = getCoordinateSymbol(nextPos, grid)?.symbol
+
+ if (nextSymbol === 'O') {
+ boxes.push(nextPos)
+ }
+ }
+
+ // Move groups of boxes if there's a space at their end
+ if (nextSymbol === '.' && boxes.length > 0) {
+ for (let i = boxes.length - 1; i >= 0; i -= 1) {
+ const next = robot.next(boxes[i])
+
+ grid[boxes[i]!.y]![boxes[i]!.x] = '.'
+ grid[next.y]![next.x] = 'O'
+ }
+
+ // Move the robot
+ grid[robot.pos.y]![robot.pos.x] = '.'
+ robot.walk()
+
+ grid[robot.pos.y]![robot.pos.x] = robot.symbol
+ }
+ }
+ }
+}
+
+/**
+ * Calculates the GPS sum of all boxes in the grid
+ * @param {string[][]} grid - 2D string array containing walls, boxes and space data
+ * @returns {number} GPS sum of all boxes in the grid
+ */
+export const calculateGPS = (grid: string[][]): number => {
+ const dimensions = { length: grid.length, width: grid[0]!.length }
+ let sumGPS = 0
+
+ for (let y = 0; y < dimensions.length; y += 1) {
+ for (let x = 0; x < dimensions.width; x += 1) {
+ if (grid[y]![x] === 'O') {
+ sumGPS += y * 100 + x
+ }
+ }
+ }
+
+ return sumGPS
+}
diff --git a/src/2024/2024-12-15/lib/fileReader.ts b/src/2024/2024-12-15/lib/fileReader.ts
new file mode 100644
index 0000000..68099c6
--- /dev/null
+++ b/src/2024/2024-12-15/lib/fileReader.ts
@@ -0,0 +1,39 @@
+import type { RobotWarehouseData } from './types.js'
+
+import { AOCOutputType, readAOCInputFile } from '@/aoc/file/aocfile.js'
+import { file } from '@/aoc/file/utils.js'
+
+/**
+ * Reads and formats the day 15 quiz input file.
+ * @param {string} fileName - Input text filename path relative to this script
+ * @returns {RobotWarehouseData} Quiz input data
+ */
+export const fileReader = (fileName: string): RobotWarehouseData => {
+ const input = readAOCInputFile({
+ filePath: file(import.meta.url, fileName),
+ type: AOCOutputType.STRING
+ }) as string
+
+ return input
+ .split('\n\n')
+ .reduce((data, item, index) => {
+ // Grid
+ if (index === 0) {
+ return {
+ ...data,
+ grid: item
+ .split('\n')
+ .map(line => line.split(''))
+ }
+ }
+
+ // Instructions
+ return {
+ ...data,
+ instructions: item
+ .split('')
+ .reverse()
+ .filter(direction => direction !== '\n')
+ }
+ }, {}) as RobotWarehouseData
+}
diff --git a/src/2024/2024-12-15/lib/fileReaderExpanded.ts b/src/2024/2024-12-15/lib/fileReaderExpanded.ts
new file mode 100644
index 0000000..09eda51
--- /dev/null
+++ b/src/2024/2024-12-15/lib/fileReaderExpanded.ts
@@ -0,0 +1,54 @@
+import type { RobotWarehouseData } from './types.js'
+
+import { AOCOutputType, readAOCInputFile } from '@/aoc/file/aocfile.js'
+import { file } from '@/aoc/file/utils.js'
+
+/**
+ * Reads and formats the day 15 - part 2 quiz input file. Expands the tile symbols by 2.
+ * @param {string} fileName - Input text filename path relative to this script
+ * @returns {RobotWarehouseData} Quiz input data
+ */
+export const fileReaderExpand = (fileName: string): RobotWarehouseData => {
+ const input = readAOCInputFile({
+ filePath: file(import.meta.url, fileName),
+ type: AOCOutputType.STRING
+ }) as string
+
+ return input
+ .split('\n\n')
+ .reduce((data, item, index) => {
+ // Grid
+ if (index === 0) {
+ return {
+ ...data,
+ grid: item
+ .split('\n')
+ .map(line => line.split(''))
+ .reduce((list: string[][], tiles) => {
+ const processed = tiles.reduce((subList: string[], elem) => {
+ if (['.', '#'].includes(elem)) {
+ subList.push(elem, elem)
+ } else if (elem === 'O') {
+ subList.push('[', ']')
+ } else if (elem === '@') {
+ subList.push('@', '.')
+ }
+
+ return subList
+ }, [])
+
+ return [...list, processed]
+ }, [])
+ }
+ }
+
+ // Instructions
+ return {
+ ...data,
+ instructions: item
+ .split('')
+ .reverse()
+ .filter(direction => direction !== '\n')
+ }
+ }, {}) as RobotWarehouseData
+}
diff --git a/src/2024/2024-12-15/lib/robot.ts b/src/2024/2024-12-15/lib/robot.ts
new file mode 100644
index 0000000..e4cb898
--- /dev/null
+++ b/src/2024/2024-12-15/lib/robot.ts
@@ -0,0 +1,106 @@
+import { GuardDirection } from '@/2024/2024-12-06/lib/guard.types.js'
+import { getGridCoordinate } from '@/aoc/grid/utils.js'
+import type { Point } from '@/aoc/point/types.js'
+
+/**
+ * @class Robot
+ * @description A set of methods, objects and variables for managing a `Robot`
+ */
+export class Robot {
+ /** Symbol representation in the grid */
+ symbol: string = '@'
+
+ /** Current (y,x) grid coordinate */
+ pos: Point = { x: -1, y: -1 }
+
+ /** Direction indicator up/down/left/right */
+ direction: Point = { x: 0, y: 0 }
+
+ /** Current active move instruction symbol */
+ instruction: GuardDirection | '-' = '-'
+
+ /** Moves instructions set */
+ instructions: string[]
+
+ /**
+ * @constructor
+ * @param {string[][]} grid - 2D string array grid
+ */
+ constructor (grid: string[][], instructions: string[]) {
+ this.findInitialPosition(grid)
+ this.instructions = [...instructions]
+ }
+
+ /**
+ * Finds the robot's initial `Point` position in the 2D grid, storing them in the `this.pos` object
+ * @param {string[][]} grid - 2D string array grid
+ */
+ findInitialPosition (grid: string[][]): void {
+ const start = getGridCoordinate(grid, this.symbol)
+
+ this.pos.x = start.x
+ this.pos.y = start.y
+ }
+
+ /** Reads the next instruction and sets the (y,x) direction */
+ readInstruction (): void {
+ this.instruction = this.instructions.pop() as GuardDirection || '-'
+
+ switch(this.instruction) {
+ case GuardDirection.UP:
+ this.direction = { x: 0, y: -1 }
+ break
+ case GuardDirection.DOWN:
+ this.direction = { x: 0, y: 1 }
+ break
+ case GuardDirection.LEFT:
+ this.direction = { x: -1, y: 0 }
+ break
+ case GuardDirection.RIGHT:
+ this.direction = { x: 1, y: 0 }
+ break
+ default:
+ break
+ }
+ }
+
+ /** Increments the robot's (y,x) coordinate by direction */
+ walk (): void {
+ this.pos.x += this.direction.x
+ this.pos.y += this.direction.y
+ }
+
+ /**
+ * Finds the next (y,x) coordinate of the robot or a given `Point` parameter using the robot's current direction.
+ * @param {Point} [point] - (Optional) (y,x) coordinate to find the next step coorndinate from
+ * @returns {Point} Next (y,x) coordinate after 1 step
+ */
+ next (point?: Point): Point {
+ const x = point?.x || this.pos.x
+ const y = point?.y || this.pos.y
+
+ const nextX = x + this.direction.x
+ const nextY = y + this.direction.y
+
+ return {
+ x: nextX, y: nextY
+ }
+ }
+
+ /**
+ * Finds the robot's previous (y,x) coordinate or a given `Point` parameter using the robot's current direction.
+ * @param {Point} [point] - (Optional) (y,x) coordinate to find the previous step coorndinate from
+ * @returns {Point} Next (y,x) coordinate before the provided coordinate
+ */
+ prev (point?: Point): Point {
+ const x = point?.x || this.pos.x
+ const y = point?.y || this.pos.y
+
+ const nextX = x - this.direction.x
+ const nextY = y - this.direction.y
+
+ return {
+ x: nextX, y: nextY
+ }
+ }
+}
diff --git a/src/2024/2024-12-15/lib/types.ts b/src/2024/2024-12-15/lib/types.ts
new file mode 100644
index 0000000..c873f25
--- /dev/null
+++ b/src/2024/2024-12-15/lib/types.ts
@@ -0,0 +1,9 @@
+/**
+ * @type {Object} RobotWarehouseData
+ * @property {string[][]} grid - 2D string array containng walls, boxes, spaces and robot symbols
+ * @property {string[]} instructions - String array contaning the robot's move instructions
+ */
+export type RobotWarehouseData = {
+ grid: string[][];
+ instructions: string[];
+}
diff --git a/src/2024/2024-12-15/lib/utils.ts b/src/2024/2024-12-15/lib/utils.ts
new file mode 100644
index 0000000..48cad4e
--- /dev/null
+++ b/src/2024/2024-12-15/lib/utils.ts
@@ -0,0 +1,66 @@
+import type { Point, PointSymbol } from '@/aoc/point/types.js'
+import { getCoordinateSymbol } from '@/aoc/grid/utils.js'
+
+/**
+ * Checks if the `symbol` parameter is an expanded box symbol
+ * @param {string} symbol - String character representing an expanded box symbol
+ * @returns {boolean} Flag indicating if the `symbol` character is a box symbol
+ */
+export const isExpandedBoxSymbol = (symbol: string) => ['[', ']'].includes(symbol as string)
+
+/**
+ * Retrieves the 2 half-box (y,x) coordinates of a full expanded box - left and right `PointSymbol`
+ * @param {Point} point - (x,y) coordinate of one side of the box
+ */
+export const getBoxStartCoordinates = (point: Point, grid: string[][]):
+{ side1: PointSymbol, side2: PointSymbol } => {
+
+ const rightCoord = { ...point, x: point.x + 1 }
+ const leftCoord = { ...point, x: point.x - 1 }
+ let side2: PointSymbol = { x: -1, y: -1, symbol: '-' }
+
+ const symbol = getCoordinateSymbol(point, grid)?.symbol as string
+ const right = getCoordinateSymbol(rightCoord, grid)?.symbol as string
+ const left = getCoordinateSymbol(leftCoord, grid)?.symbol as string
+
+ if (`${symbol}${right}` === '[]') {
+ side2 = { ...side2, ...rightCoord, symbol: right }
+ }
+
+ if (`${left}${symbol}` === '[]') {
+ side2 = { ...side2, ...leftCoord, symbol: left }
+ }
+
+ const side1 = { ...point, symbol }
+
+ return {
+ side1,
+ side2
+ }
+}
+
+/**
+ * Retrieves the reverse matching symbol of an expanded box symbol
+ * @param {string} symbol - String character representing an expanded box symbol
+ * @returns {string | undefined} Reverse matching symbol of an expanded box symbol or `undefined`
+ */
+export const getReverseSymbol = (symbol: string): string | undefined => {
+ if (!['[', ']'].includes(symbol)) return
+ return symbol === '[' ? ']' : '['
+}
+
+/**
+ * Retrieves the `x` direction associated with an expanded box symbol
+ * @param symbol - String character representing an expanded box symbol
+ * @returns {number | undefined}
+ */
+export const getSymbolDirection = (symbol: string): number | undefined => {
+ if (!['[', ']'].includes(symbol)) return
+
+ let xDirection = 0
+
+ if (symbol === '[') xDirection = 1
+ if (symbol === ']') xDirection = -1
+
+ return xDirection
+}
diff --git a/src/2024/2024-12-15/main.ts b/src/2024/2024-12-15/main.ts
new file mode 100644
index 0000000..c461b28
--- /dev/null
+++ b/src/2024/2024-12-15/main.ts
@@ -0,0 +1,34 @@
+import { fileReader } from './lib/fileReader.js'
+import { fileReaderExpand } from './lib/fileReaderExpanded.js'
+
+import { moveBoxes, calculateGPS } from './lib/calculateGPS.js'
+import { moveExpandedBoxes, calculateExpandedGPS } from './lib/calculateExpandedGPS.js'
+
+/**
+ * Part 1/2 of the 2024-12-15 quiz
+ * Moves the robot, boxes and calculates all boxes' GPS
+ */
+const quiz20241215_01 = () => {
+ const { grid, instructions } = fileReader('../input.txt')
+
+ moveBoxes(grid, instructions)
+ const gps = calculateGPS(grid)
+
+ console.log('Part 1: Regular boxes GPS:', gps)
+}
+
+/**
+ * Part 2/2 of the 2024-12-15 quiz
+ * Moves the robot, expanded boxes and calculates all expanded boxes' GPS
+ */
+const quiz20241215_02 = () => {
+ const { grid, instructions } = fileReaderExpand('../input.txt')
+
+ moveExpandedBoxes(grid, instructions)
+ const gps = calculateExpandedGPS(grid)
+
+ console.log('Part 2: Expanded boxes GPS:', gps)
+}
+
+quiz20241215_01()
+quiz20241215_02()
diff --git a/src/2024/2024-12-15/sample.test.ts b/src/2024/2024-12-15/sample.test.ts
new file mode 100644
index 0000000..a8495d5
--- /dev/null
+++ b/src/2024/2024-12-15/sample.test.ts
@@ -0,0 +1,29 @@
+import { test, expect } from 'vitest'
+
+import { fileReader } from './lib/fileReader.js'
+import { fileReaderExpand } from './lib/fileReaderExpanded.js'
+
+import { moveBoxes, calculateGPS } from './lib/calculateGPS.js'
+import { moveExpandedBoxes, calculateExpandedGPS } from './lib/calculateExpandedGPS.js'
+
+test('Part 1: Move boxes and calculate GPS', () => {
+ const data = fileReader('../input.txt')
+ const data2 = fileReader('../input2.txt')
+
+ moveBoxes(data.grid, data.instructions)
+ expect(calculateGPS(data.grid)).toBe(2027)
+
+ moveBoxes(data2.grid, data2.instructions)
+ expect(calculateGPS(data2.grid)).toBe(9897)
+})
+
+test('Part 2: Move expanded boxes and calculate GPS', () => {
+ const data = fileReaderExpand('../input.txt')
+ const data2 = fileReaderExpand('../input2.txt')
+
+ moveExpandedBoxes(data.grid, data.instructions)
+ expect(calculateExpandedGPS(data.grid)).toBe(1950)
+
+ moveExpandedBoxes(data2.grid, data2.instructions)
+ expect(calculateExpandedGPS(data2.grid)).toBe(9125)
+})
diff --git a/src/aoc/README.md b/src/aoc/README.md
index f0bfc83..bc29099 100644
--- a/src/aoc/README.md
+++ b/src/aoc/README.md
@@ -25,6 +25,7 @@ A collection handler functions for manipulating regular arrays.
A collection of convenience handler functions for AoC 2D input arrays.
- **`getCoordinateSymbol()`** - Converts a 2D `Point` point object to a string and returns its value from the 2D `string` or `number` array
+- **`getGridCoordinate()`** - Retrieves the first `Point` (y,x) coordinate of a `symbol` that appears in a 2D array.
- **`getGridDimensions()`** - Retrieves the length and width of a generic 2D array
- **`isIllegalCoordinate()`** - Checks if a point at coordinate (y,x) in a grid is illegal: if it's out of bounds of the grid area or if its symbol differs from the symbol parameter.
- **`isOutOfBounds()`** - Checks if a (y,x) coordinate is out of the grid area
diff --git a/src/aoc/grid/utils.ts b/src/aoc/grid/utils.ts
index 4f55540..c9a4841 100644
--- a/src/aoc/grid/utils.ts
+++ b/src/aoc/grid/utils.ts
@@ -10,7 +10,7 @@ import { findNeighbors } from '../point/utils.js'
/**
* Converts a 2D `Point` point object to a string and returns its value from the 2D `string` or `number` array
- * @template T Extends `string` or `number`, representing the type of elements in the 2D array.
+ * @template T Extends `string` or `number`, representing the type of elements in the 2D `data` array.
* @param {Point} point - (y,x) coordinatate in the 2D array
* @param {T[][]} data - 2D array containing `number` or `string` elements
* @returns {GridCoordinateSymbol | undefined} Returns the `GridCoordinateSymbol` (x,y) coordinate expressed in string and its value
@@ -27,9 +27,34 @@ export const getCoordinateSymbol =
}
}
+/**
+ * Retrieves the first `Point` (y,x) coordinate of a `symbol` that appears in a 2D array.
+ * @param {T[][]} grid - 2D `string` or `number` array
+ * @param {string} symbol - Character or number to look for in the `grid`
+ * @returns {Point} Object containing the (y,x) coordinate of the `symbol`
+ */
+export const getGridCoordinate = (grid: T[][], symbol: T): Point => {
+ const point: Point = { x: -1, y: -1 }
+
+ for (let y = 0; y < grid.length; y += 1) {
+ for (let x = 0; x < grid[0]!.length; x += 1) {
+ if (grid[y]![x] === symbol) {
+ point.x = x
+ point.y = y
+ break
+ }
+
+ if (point.x !== -1) break
+ }
+ }
+
+ return point
+}
+
/**
* Retrieves the length and width of a generic 2D array
- * @param {T[][] | T[][]} data - 2D array, usually `string[][]`, `number[][]` or others.
+ * @template T Type of elements in the 2D `data` array.
+ * @param {T[][]} data - 2D array, usually `string[][]`, `number[][]` or others.
* @returns {GridDimensions} Object containig the length and width of the 2D array
*/
export const getGridDimensions = (data: T[][]): GridDimensions => {