Wide Box (Part 2) Warehouse Analysis
Table of Contents
1. Part 2: Wide Box Mechanics
1.1. Key Differences
- Original tiles are doubled in width
- Boxes represented as '[]' taking two spaces
- Robot remains single width '@'
- Robot can potentially push two boxes simultaneously
1.2. State Management
from dataclasses import dataclass
from typing import Set, Tuple, List
import gps
@dataclass(frozen=True)
class WideState:
robot_x: int
robot_y: int
# We store only the leftmost coordinate of each wide box
box_lefts: frozenset[Tuple[int, int]]
def is_box_at(self, x: int, y: int) -> bool:
"""Check if either part of a wide box occupies this position"""
return (x, y) in self.box_lefts or (x, y-1) in self.box_lefts
def try_move(self, dx: int, dy: int) -> Optional['WideState']:
new_x = self.robot_x + dx
new_y = self.robot_y + dy
# Simple wall collision
if is_wall(new_x, new_y):
return None
# Check for box pushing cases
if self.is_box_at(new_x, new_y):
# Need to handle both single and double box pushes
return self._try_box_push(new_x, new_y, dx, dy)
# Just moving into empty space
return WideState(new_x, new_y, self.box_lefts)
def _try_box_push(self, x: int, y: int, dx: int, dy: int) -> Optional['WideState']:
"""Handle the complex box pushing logic for wide boxes"""
# Find all affected boxes (could be 1 or 2)
affected_boxes = set()
if (x, y) in self.box_lefts:
affected_boxes.add((x, y))
if (x, y-1) in self.box_lefts:
affected_boxes.add((x, y-1))
# Check if we can push all affected boxes
new_boxes = self.box_lefts
for box_x, box_y in affected_boxes:
# Both spots the box would move to must be clear
if (is_wall(box_x + dx, box_y) or
is_wall(box_x + dx, box_y + 1)):
return None
# Remove old box position
new_boxes = new_boxes - {(box_x, box_y)}
# Add new box position
new_boxes = new_boxes | {(box_x + dx, box_y)}
return WideState(x, y, frozenset(new_boxes))
1.3. State Space Characteristics
- Each wide box has fewer valid positions than Part 1
- Complex push mechanics when robot encounters box edge
- Need to track box alignment for valid moves
4. Solution Driver
def solve_wide_warehouse(initial_map: List[str], moves: str) -> int:
# Parse initial state, doubling width
width = len(initial_map[0]) * 2
height = len(initial_map)
# Find robot and boxes
robot_pos = None
boxes = set()
for i, row in enumerate(initial_map):
for j, cell in enumerate(row):
wide_j = j * 2 # Convert to wide coordinates
if cell == '@':
robot_pos = (i, wide_j)
elif cell == 'O':
boxes.add((i, wide_j))
state = WideState(robot_pos[0], robot_pos[1], frozenset(boxes))
# Process moves
for move in moves:
dx, dy = MOVES[move]
new_state = state.try_move(dx, dy)
if new_state:
state = new_state
# Calculate final GPS coordinates
return gps.calculate_gps(state.box_lefts, width, height)
5. Key Invariants
- Box coordinates always refer to leftmost position
- Robot position remains single-width aligned
- Box pushing requires checking both positions
- GPS calculations must account for wide format
6. Game
;;; warehouse-game.el --- Interactive warehouse robot game -*- lexical-binding: t -*-
;; Author: Jason Walsh
;; Keywords: games
;; Version: 0.1
;; Package-Requires: ((emacs "27.1"))
;;; Commentary:
;; A game mode implementing the Advent of Code 2024 Day 15 warehouse robot puzzle
;; Use arrow keys to move the robot and push boxes around the warehouse
;;; Code:
(require 'cl-lib)
(defgroup warehouse-game nil
"Warehouse robot pushing game."
:group 'games)
(defface warehouse-wall-face
'((t :foreground "gray30" :weight bold))
"Face for warehouse walls.")
(defface warehouse-box-face
'((t :foreground "orange" :weight bold))
"Face for pushable boxes.")
(defface warehouse-robot-face
'((t :foreground "green" :weight bold))
"Face for the robot.")
(defvar-local warehouse-grid nil
"The current game grid.")
(defvar-local warehouse-robot-pos nil
"Current robot position (row . col).")
(defvar-local warehouse-boxes nil
"Set of box positions.")
(defvar-local warehouse-moves '()
"List of moves made.")
(defvar warehouse-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [left] 'warehouse-move-left)
(define-key map [right] 'warehouse-move-right)
(define-key map [up] 'warehouse-move-up)
(define-key map [down] 'warehouse-move-down)
(define-key map "r" 'warehouse-reset-level)
(define-key map "u" 'warehouse-undo-move)
map)
"Keymap for `warehouse-game-mode'.")
(defun warehouse-create-buffer ()
"Create and switch to the game buffer."
(switch-to-buffer (get-buffer-create "*Warehouse Game*"))
(warehouse-game-mode))
(defun warehouse-init-level (level-data)
"Initialize game with LEVEL-DATA."
(setq warehouse-grid (split-string level-data "\n" t)
warehouse-moves '()
warehouse-boxes '())
;; Find robot and boxes
(cl-loop for row from 0 to (1- (length warehouse-grid))
do (let ((line (nth row warehouse-grid)))
(cl-loop for col from 0 to (1- (length line))
do (let ((char (aref line col)))
(cond
((char-equal char ?@)
(setq warehouse-robot-pos (cons row col)))
((char-equal char ?O)
(push (cons row col) warehouse-boxes)))))))
(warehouse-draw-game))
(defun warehouse-draw-game ()
"Draw the current game state."
(let ((inhibit-read-only t))
(erase-buffer)
(cl-loop for row from 0 to (1- (length warehouse-grid))
do (let ((line (nth row warehouse-grid)))
(cl-loop for col from 0 to (1- (length line))
do (let ((char (aref line col)))
(insert
(cond
((equal (cons row col) warehouse-robot-pos)
(propertize "@" 'face 'warehouse-robot-face))
((member (cons row col) warehouse-boxes)
(propertize "O" 'face 'warehouse-box-face))
((char-equal char ?#)
(propertize "#" 'face 'warehouse-wall-face))
(t "."))))
finally (insert "\n")))))
(goto-char (point-min)))
(defun warehouse-move (dx dy)
"Try to move the robot by DX and DY."
(let* ((new-row (+ (car warehouse-robot-pos) dy))
(new-col (+ (cdr warehouse-robot-pos) dx))
(new-pos (cons new-row new-col)))
;; Check wall collision
(unless (char-equal (aref (nth new-row warehouse-grid) new-col) ?#)
;; Check box collision
(if (member new-pos warehouse-boxes)
(let* ((box-new-row (+ new-row dy))
(box-new-col (+ new-col dx))
(box-new-pos (cons box-new-row box-new-col)))
;; Try to push box
(unless (or (char-equal (aref (nth box-new-row warehouse-grid) box-new-col) ?#)
(member box-new-pos warehouse-boxes))
;; Move box and robot
(setq warehouse-boxes (cons box-new-pos
(remove new-pos warehouse-boxes))
warehouse-robot-pos new-pos
warehouse-moves (cons (cons dx dy) warehouse-moves))))
;; Move just robot
(setq warehouse-robot-pos new-pos
warehouse-moves (cons (cons dx dy) warehouse-moves))))
(warehouse-draw-game)))
(defun warehouse-move-left ()
"Move robot left."
(interactive)
(warehouse-move -1 0))
(defun warehouse-move-right ()
"Move robot right."
(interactive)
(warehouse-move 1 0))
(defun warehouse-move-up ()
"Move robot up."
(interactive)
(warehouse-move 0 -1))
(defun warehouse-move-down ()
"Move robot down."
(interactive)
(warehouse-move 0 1))
(defun warehouse-undo-move ()
"Undo the last move if possible."
(interactive)
(when warehouse-moves
;; TODO: Implement proper undo with box state
(let* ((last-move (car warehouse-moves))
(dx (- (car last-move)))
(dy (- (cdr last-move))))
(setq warehouse-robot-pos (cons (+ (car warehouse-robot-pos) dy)
(+ (cdr warehouse-robot-pos) dx))
warehouse-moves (cdr warehouse-moves)))
(warehouse-draw-game)))
(defun warehouse-reset-level ()
"Reset the current level."
(interactive)
(warehouse-init-level (mapconcat 'identity warehouse-grid "\n")))
(define-derived-mode warehouse-game-mode special-mode "Warehouse"
"Major mode for playing the warehouse robot game."
(buffer-disable-undo)
(setq-local cursor-type nil))
;;;###autoload
(defun warehouse-game ()
"Start playing warehouse game."
(interactive)
(warehouse-create-buffer)
(warehouse-init-level "########\n#..O.O.#\n##@.O..#\n#...O..#\n#.#.O..#\n#...O..#\n#......#\n########"))
(provide 'warehouse-game)
;;; warehouse-game.el ends here