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

  1. Box coordinates always refer to leftmost position
  2. Robot position remains single-width aligned
  3. Box pushing requires checking both positions
  4. 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

Author: Jason Walsh

jwalsh@nexus

Last Updated: 2025-07-30 13:45:27

build: 2025-12-23 09:12 | sha: e32f33e