Wide Box (Part 2) Warehouse Analysis
Table of Contents
Part 2: Wide Box Mechanics
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
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))
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
Implementation Flow
graph TD A[Process Move] --> B{Wall Collision?} B -- Yes --> C[Invalid Move] B -- No --> D{Box Contact?} D -- Yes --> E[Check Box Type] E --> F{Single Box?} F -- Yes --> G[Try Single Push] F -- No --> H[Try Double Push] G --> I{Push Success?} H --> I I -- Yes --> J[Update State] I -- No --> C D -- No --> K[Move Robot] J --> L[Next Move] K --> L
Box Push Scenarios
stateDiagram-v2 [*] --> Approach Approach --> SingleBox: Hit Box Center Approach --> DoubleBox: Hit Box Edge SingleBox --> Success: Clear Path SingleBox --> Blocked: Wall/Box DoubleBox --> Success: Both Clear DoubleBox --> Blocked: Any Blocked Success --> [*] Blocked --> [*]
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)
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
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