Day 4: Printing Department
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Rust
I pulled out some code from last year to make representing 2D grids as a vector easier, so this was quite straightforward. 2.5ms runtime (including reading/parsing the input twice cos of TDD).
impl Grid { fn neighbour(&self, i: usize, dir: Direction) -> Option<bool> { let width = self.width; let length = self.grid.len(); use Direction::*; match dir { N if i >= width => Some(i - width), NE if i >= width && i % width != width - 1 => Some(i - width + 1), E if i % width != width - 1 => Some(i + 1), SE if i + width + 1 < length && i % width != width - 1 => Some(i + width + 1), S if i + width < length => Some(i + width), SW if i + width - 1 < length && !i.is_multiple_of(width) => Some(i + width - 1), W if !i.is_multiple_of(width) => Some(i - 1), NW if i >= width && !i.is_multiple_of(width) => Some(i - width - 1), _ => None, }; .map(|i| self.grid[i]) } #[rustfmt::skip] fn cell_accessible(&self, i: usize) -> bool { Direction::ALL .iter() .filter(|&&dir| self.neighbour(i, dir).unwrap_or(false)) .count() < 4 } fn num_accessible(&self) -> usize { self.grid .iter() .enumerate() .filter(|&(i, &is_occupied)| is_occupied && self.cell_accessible(i)) .count() } fn remove_accessible(&mut self) -> Option<usize> { let removables = self .grid .iter() .enumerate() .filter_map(|(i, &is_occupied)| (is_occupied && self.cell_accessible(i)).then_some(i)) .collect::<Vec<_>>(); let count = removables.len(); if count > 0 { for idx in removables { self.grid[idx] = false; } Some(count) } else { None } } fn remove_recursive(&mut self) -> usize { let mut total_removed = 0; while let Some(removed) = self.remove_accessible() { total_removed += removed; } total_removed } }Full code
use std::{fs, str::FromStr}; use color_eyre::eyre::{Report, Result}; #[derive(Debug, Copy, Clone)] enum Direction { N, NE, E, SE, S, SW, W, NW, } impl Direction { const ALL: [Direction; 8] = [ Direction::N, Direction::NE, Direction::E, Direction::SE, Direction::S, Direction::SW, Direction::W, Direction::NW, ]; } #[derive(Debug, PartialEq, Eq, Clone)] struct Grid { grid: Vec<bool>, width: usize, height: usize, } impl FromStr for Grid { type Err = Report; fn from_str(s: &str) -> Result<Self, Self::Err> { let grid: Vec<_> = s .chars() .filter_map(|ch| match ch { '.' => Some(false), '@' => Some(true), '\n' => None, _ => panic!("Invalid input"), }) .collect(); let width = s .chars() .position(|ch| ch == '\n') .ok_or_else(|| Report::msg("grid width cannot be zero, or one line"))?; let height = grid.len() / width; Ok(Self { grid, width, height, }) } } impl Grid { fn neighbour(&self, i: usize, dir: Direction) -> Option<bool> { let width = self.width; let length = self.grid.len(); use Direction::*; match dir { N if i >= width => Some(i - width), NE if i >= width && i % width != width - 1 => Some(i - width + 1), E if i % width != width - 1 => Some(i + 1), SE if i + width + 1 < length && i % width != width - 1 => Some(i + width + 1), S if i + width < length => Some(i + width), SW if i + width - 1 < length && !i.is_multiple_of(width) => Some(i + width - 1), W if !i.is_multiple_of(width) => Some(i - 1), NW if i >= width && !i.is_multiple_of(width) => Some(i - width - 1), _ => None, }; .map(|i| self.grid[i]) } #[rustfmt::skip] fn cell_accessible(&self, i: usize) -> bool { Direction::ALL .iter() .filter(|&&dir| self.neighbour(i, dir).unwrap_or(false)) .count() < 4 } fn num_accessible(&self) -> usize { self.grid .iter() .enumerate() .filter(|&(i, &is_occupied)| is_occupied && self.cell_accessible(i)) .count() } fn remove_accessible(&mut self) -> Option<usize> { let removables = self .grid .iter() .enumerate() .filter_map(|(i, &is_occupied)| (is_occupied && self.cell_accessible(i)).then_some(i)) .collect::<Vec<_>>(); let count = removables.len(); if count > 0 { for idx in removables { self.grid[idx] = false; } Some(count) } else { None } } fn remove_recursive(&mut self) -> usize { let mut total_removed = 0; while let Some(removed) = self.remove_accessible() { total_removed += removed; } total_removed } } fn part1(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let grid = Grid::from_str(&input)?; Ok(grid.num_accessible()) } fn part2(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let mut grid = Grid::from_str(&input)?; Ok(grid.remove_recursive()) } fn main() -> Result<()> { color_eyre::install()?; println!("Part 1: {}", part1("d04/input.txt")?); println!("Part 2: {}", part2("d04/input.txt")?); Ok(()) }This is the first day I’ve wished I were working in J, like last year, instead of Lisp. Common Lisp arrays show the age of the language design. They’re basically C arrays with less convenient syntax; if you want higher-order array iteration you have to write it yourself or use a package that provides it. This solution is also less efficient than it could be for part 2, because I recompute the full neighbors array on each iteration rather than updating it incrementally.
(defun read-inputs (filename) (let* ((input-lines (uiop:read-file-lines filename)) (rows (length input-lines)) (cols (length (car input-lines))) (result (make-array (list rows cols) :initial-element 0))) (loop for line in input-lines for y from 0 to (1- rows) do (loop for x from 0 to (1- cols) if (eql (char line x) #\@) do (setf (aref result y x) 1))) result)) (defun neighbors (grid) (let* ((dimensions (array-dimensions grid)) (rows (car dimensions)) (cols (cadr dimensions)) (result (make-array dimensions :initial-element 0))) (flet ((neighbors-at (y x) (loop for dy from -1 to 1 sum (loop for dx from -1 to 1 sum (let ((yy (+ y dy)) (xx (+ x dx))) (if (and (>= yy 0) (< yy rows) (>= xx 0) (< xx cols) (not (and (= dx 0) (= dy 0))) (> (aref grid yy xx) 0)) 1 0)))))) (loop for y from 0 to (1- rows) do (loop for x from 0 to (1- cols) do (setf (aref result y x) (neighbors-at y x)))) result))) (defun main-1 (filename) (let* ((grid (read-inputs filename)) (dimensions (array-dimensions grid)) (neighbors-grid (neighbors grid))) (loop for y from 0 to (1- (car dimensions)) sum (loop for x from 0 to (1- (cadr dimensions)) sum (if (and (> (aref grid y x) 0) (< (aref neighbors-grid y x) 4)) 1 0))))) (defun remove-accessible (grid) (let* ((dimensions (array-dimensions grid)) (neighbors-grid (neighbors grid)) (removed 0)) (loop for y from 0 to (1- (car dimensions)) do (loop for x from 0 to (1- (cadr dimensions)) do (if (and (> (aref grid y x) 0) (< (aref neighbors-grid y x) 4)) (progn (setf (aref grid y x) 0) (incf removed))))) removed)) (defun main-2 (filename) (let ((grid (read-inputs filename)) (removed 0)) (loop for this-removed = (remove-accessible grid) until (zerop this-removed) do (incf removed this-removed)) removed))Nim
type AOCSolution[T,U] = tuple[part1: T, part2: U] Vec2 = tuple[x,y: int] proc removePaper(rolls: var seq[string]): int = var toRemove: seq[Vec2] for y, line in rolls: for x, c in line: if c != '@': continue var adjacent = 0 for (dx, dy) in [(-1,-1),(0,-1),(1,-1), (-1, 0), (1, 0), (-1, 1),(0, 1),(1, 1)]: let pos: Vec2 = (x+dx, y+dy) if pos.x < 0 or pos.x >= rolls[0].len or pos.y < 0 or pos.y >= rolls.len: continue if rolls[pos.y][pos.x] == '@': inc adjacent if adjacent < 4: inc result toRemove.add (x, y) for (x, y) in toRemove: rolls[y][x] = '.' proc solve(input: string): AOCSolution[int, int] = var rolls = input.splitLines() result.part1 = rolls.removePaper() result.part2 = result.part1 while (let cnt = rolls.removePaper(); result.part2 += cnt; cnt) > 0: discardToday was so easy, that I decided to solve it twice, just for fun. First is a 2D traversal (see above). And then I did a node graph solution in a few minutes (in repo below). Both run in ~27 ms.
It’s a bit concerning, because a simple puzzle can only mean that tomorrow will be a nightmare. Good Luck everyone, we will need it.
Full solution is at Codeberg: solution.nim
Javascript
After smashing out a functional version in 20 minutes, I converted it into a OOP approach for a more appealing solution.
Solution
const input = require('fs').readFileSync('input-day4.txt', 'utf-8'); class PrintingDepartmentMap { /** @param {string} input */ constructor(input) { /** @type {number[][]} */ this.map = input.split("\n").map(r => r.split('').map(v => v === '@' ? 1 : 0)); } /** * @param {number} x * @param {number} y * @returns {number} the value of that tile, or -1 if invalid */ getCoord(x, y) { if (x < 0 || y < 0 || x >= this.map[0].length || y >= this.map.length) { return -1; } return this.map[y][x]; } /** * @param {number} x * @param {number} y * @returns {number} the number of adjacent tiles that are >= 1 */ countAdjacent(x, y) { return [ // top-left this.getCoord(x - 1, y - 1) >= 1, // top this.getCoord(x, y - 1) >= 1, // top-right this.getCoord(x + 1, y - 1) >= 1, // right this.getCoord(x + 1, y) >= 1, // bottom-right this.getCoord(x + 1, y + 1) >= 1, // bottom this.getCoord(x, y + 1) >= 1, // bottom-left this.getCoord(x - 1, y + 1) >= 1, // left this.getCoord(x - 1, y) >= 1 ].reduce((acc, v) => !!v ? acc + 1 : acc, 0); } /** * transform in place the map replacing any rolls (1) with (2) if they are accessible * @returns {PrintingDepartmentMap} */ markAccessable() { for (let y = 0; y < this.map.length; y += 1) { for (let x = 0; x < this.map[0].length; x += 1) { if (this.map[y][x] < 1) continue; if (this.countAdjacent(x, y) < 4) { this.map[y][x] = 2; } } } return this; } /** @returns {number} the number of removed rolls */ removeAccessable() { let removed = 0; for (let y = 0; y < this.map.length; y += 1) { for (let x = 0; x < this.map[0].length; x += 1) { if (this.map[y][x] === 2) { removed += 1; this.map[y][x] = 0; } } } return removed; } } const map = new PrintingDepartmentMap(input); let removed = 0; while (true) { const justRemoved = map.markAccessable().removeAccessable(); if (removed === 0) { console.log(`Part 1 Answer: ${justRemoved}`); } if (justRemoved === 0) break; removed += justRemoved; } console.log(`Part 2 Answer: ${removed}`);Rust
fn parse_input(input: &str) -> Vec<Vec<bool>> { input .lines() .map(|l| l.chars().map(|c| c == '@').collect()) .collect() } fn count_adj(grid: &[Vec<bool>], (x, y): (usize, usize)) -> usize { let width = grid[0].len(); let height = grid.len(); grid.iter() .take((y + 2).min(height)) .skip(y.saturating_sub(1)) .map(|r| { r.iter() .take((x + 2).min(width)) .skip(x.saturating_sub(1)) .take(3) .filter(|e| **e) .count() }) .sum::<usize>() } fn part1(input: String) { let grid = parse_input(&input); let mut count = 0u32; for (y, row) in grid.iter().enumerate() { for (x, _) in row.iter().enumerate().filter(|(_, r)| **r) { let n_adj = count_adj(&grid, (x, y)); // Center roll is counted too if n_adj < 5 { count += 1; } } } println!("{count}"); } fn part2(input: String) { let mut grid = parse_input(&input); let mut removed = 0u32; loop { let mut next_grid = grid.clone(); let prev_removed = removed; for (y, row) in grid.iter().enumerate() { for (x, _) in row.iter().enumerate().filter(|(_, r)| **r) { let n_adj = count_adj(&grid, (x, y)); // Center roll is counted too if n_adj < 5 { next_grid[y][x] = false; removed += 1; } } } if removed == prev_removed { break; } grid = next_grid; } println!("{}", removed); } util::aoc_main!();Haskell
I tried rewriting part 2 to use a MutableArray, but it only made everything slower. So I left it at this. I saw somebody do a 1-second-challenge last year and I feel like that will be very hard unless I up my performance game.
Solution, Both Parts
{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wall #-} module Main (main) where import qualified Data.Text as Text import Data.Array.Unboxed (UArray) import qualified Data.Array.IArray as Array import qualified Data.List as List import Control.Monad ((<$!>), guard) import qualified Data.Text.IO as TextIO import Data.Maybe (fromMaybe) import Control.Arrow ((&&&)) parse :: Text.Text -> UArray (Int, Int) Bool parse t = let gridLines = init $ Text.lines t lineSize = maybe 0 pred $ Text.findIndex (== '\n') t lineCount = Text.count "\n" t - 2 in Array.listArray ((0, 0), (lineCount, lineSize)) $ List.concatMap (fmap (== '@') . Text.unpack) gridLines neighbors8 :: (Int, Int) -> [(Int, Int)] neighbors8 p@(x, y) = do x' <- [pred x .. succ x] y' <- [pred y .. succ y] let p' = (x', y') guard (p /= p') pure p' main :: IO () main = do grid <- parse <$!> TextIO.getContents print $ part1 grid print $ part2 grid part2 :: UArray (Int, Int) Bool -> Int part2 grid = case accessiblePositions grid of [] -> 0 xs -> List.length xs + part2 (grid Array.// fmap (id &&& const False) xs) part1 :: UArray (Int, Int) Bool -> Int part1 = List.length . accessiblePositions accessiblePositions :: UArray (Int, Int) Bool -> [(Int, Int)] accessiblePositions grid = let lookupPosition = fromMaybe False . (grid Array.!?) positions = Array.indices grid paperRollPositions = List.filter lookupPosition positions isPositionAccessible = (< 4) . List.length . List.filter lookupPosition . neighbors8 in List.filter isPositionAccessible paperRollPositionsTurns out on part 2 you can remove on access rather than after a full sweep of the grid, which cuts down the number of iterations you need to do about 1/2 sometimes 1/3 (depending on input).
import itertools as it from pathlib import Path import numpy as np cwd = Path(__file__).parent.resolve() def parse_input(file_path): with file_path.open("r") as fp: grid = np.array(list(map(list, list(map(str.strip, fp.readlines())))), dtype=str) return grid def solve_problem(file_name, single_attempt=True): grid = parse_input(Path(cwd, file_name)) nr, nc = grid.shape nacc_total = 0 stop = False while not stop: nacc = 0 for i,j in it.product(range(nr), range(nc)): if grid[i,j] != '@': continue if np.count_nonzero(grid[max(i-1, 0):min(i+2, nr), max(j-1, 0):min(j+2, nc)] == '@')<5: nacc += 1 if not single_attempt: grid[i,j] = '.' nacc_total += nacc if nacc==0 or single_attempt: stop = True return nacc_totalGo
package main import ( "aoc/utils" "fmt" ) type rollMap [][]bool func (rm *rollMap) addRow(line string) { runes := []rune(line) row := make([]bool, len(runes)) for idx, r := range runes { row[idx] = r == '@' } *rm = append(*rm, row) } func (rm rollMap) getAdjacentContents(row, col int) []bool { contents := []bool{} lenrow := len(rm[0]) lenrm := len(rm) for _, i := range []int{-1, 0, 1} { for _, j := range []int{-1, 0, 1} { if i == 0 && j == 0 { continue } r := row + i c := col + j if r >= 0 && r < lenrm && c >= 0 && c < lenrow { contents = append(contents, rm[r][c]) } } } return contents } func countTrue(array []bool) int { count := 0 for _, val := range array { if val { count++ } } return count } func createMap(input chan string) rollMap { rm := rollMap{} for line := range input { rm.addRow(line) } return rm } func stepOne(input chan string) (int, error) { rm := createMap(input) lenrow := len(rm[0]) accessibleRolls := 0 for row := range rm { for col := range lenrow { if rm[row][col] { adj := rm.getAdjacentContents(row, col) numRolls := countTrue(adj) if numRolls < 4 { accessibleRolls++ } } } } return accessibleRolls, nil } type position struct { row, col int } func stepTwo(input chan string) (int, error) { rm := createMap(input) lenrow := len(rm[0]) accessibleRolls := 0 for true { accessedRolls := []position{} for row := range rm { for col := range lenrow { if rm[row][col] { adj := rm.getAdjacentContents(row, col) numRolls := countTrue(adj) if numRolls < 4 { accessibleRolls++ accessedRolls = append(accessedRolls, position{ row: row, col: col, }) } } } } if len(accessedRolls) == 0 { break } else { for _, pos := range accessedRolls { rm[pos.row][pos.col] = false } } } return accessibleRolls, nil } func main() { input := utils.FilePath("day04.txt") ch1, err1 := input.GetLineChannel() if err1 != nil { fmt.Errorf("step one error: %s\n", err1) return } val1, errStep1 := stepOne(ch1) if errStep1 != nil { fmt.Errorf("step one error: %s\n", errStep1) return } fmt.Printf("Step one result: %d\n", val1) ch2, err2 := input.GetLineChannel() if err2 != nil { fmt.Errorf("step two error: %s\n", err2) return } val2, errStep2 := stepTwo(ch2) if errStep2 != nil { fmt.Errorf("step two error: %s\n", errStep2) return } fmt.Printf("Step two result: %d\n", val2) }Rust
fn count_sides(grid: &[Vec<char>], x: usize, y: usize) -> usize { let mut count = 0; for i in y.saturating_sub(1)..=y + 1 { for j in x.saturating_sub(1)..=x + 1 { if i == y && j == x { continue; } if let Some(row) = grid.get(i) { if let Some(col) = row.get(j) { if *col == '@' { count += 1; } } } } } count } #[test] fn test_y2025_day4_part1() { let input = std::fs::read_to_string("input/2025/day_4.txt").unwrap(); let grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut total = 0; let width = grid[0].len(); let height = grid.len(); for y in 0..height { for x in 0..width { if grid[y][x] != '@' { continue; } if count_sides(&grid, x, y) < 4 { total += 1 } } } println!("Total = {total}") } #[test] fn test_y2025_day4_part2() { let input = std::fs::read_to_string("input/2025/day_4.txt").unwrap(); let grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut grid = input .lines() .map(|l| l.chars().collect::<Vec<_>>()) .collect::<Vec<_>>(); let mut total = 0; let width = grid[0].len(); let height = grid.len(); loop { let mut iter_total = 0; for y in 0..height { for x in 0..width { if grid[y][x] != '@' { continue; } if count_sides(&grid, x, y) < 4 { grid[y][x] = '*'; iter_total += 1 } } } println!("Moved = {iter_total}"); if iter_total == 0 { break; } total += iter_total; } println!("Total = {total}"); }Nothing really exciting here, was pretty straightforward
Haskell
import Data.Array.Unboxed import Control.Arrow import Data.Foldable type Coord = (Int, Int) type Diagram = UArray Coord Char moves :: Coord -> [Coord] moves pos = (.+. pos) <$> deltas where deltas = [(x, y) | x <- [-1, 0, 1], y <- [-1, 0, 1], not (x == 0 && y == 0)] (ax, ay) .+. (bx, by) = (ax + bx, ay + by) parse :: String -> Diagram parse s = listArray ((1, 1), (n, m)) $ concat l where l = lines s n = length l m = length $ head l isRoll = (== '@') numRolls = length . filter isRoll neighbors d p = (d !) <$> filter (inRange (bounds d)) (moves p) removable d = filter ((<4) . numRolls . neighbors d . fst) . filter (isRoll . snd) $ assocs d part1 :: Diagram -> Int part1 = length . removable part2 d = fmap ((initial -) . fst) . find (uncurry (==)) $ zip stages (tail stages) where initial = numRolls $ elems d stages = numRolls . elems <$> iterate (\x -> x // toX (removable x)) d toX = fmap (second (const 'x')) main = getContents >>= print . (part1 &&& part2) . parseHaskell
Very simple, this one.
import Data.List import Data.Set qualified as Set readInput s = Set.fromDistinctAscList [ (i, j) :: (Int, Int) | (i, l) <- zip [0 ..] (lines s), (j, c) <- zip [0 ..] l, c == '@' ] accessible ps = Set.filter ((< 4) . adjacent) ps where adjacent (i, j) = length . filter (`Set.member` ps) $ [ (i + di, j + dj) | di <- [-1 .. 1], dj <- [-1 .. 1], (di, dj) /= (0, 0) ] main = do input <- readInput <$> readFile "input04" let removed = (`unfoldr` input) $ \ps -> case accessible ps of d | Set.null d -> Nothing | otherwise -> Just (Set.size d, ps Set.\\ d) print $ head removed print $ sum removedKotlin
Pretty simple solution, just plain count / remove the rolls until none can be removed anymore. I would’ve liked to try using imaginary numbers this year (due to this article), but sadly Kotlin doesn’t natively support them and I was too lazy to use a library.
Solution
class Day04 : Puzzle { val grid = mutableListOf<MutableList<Char>>() override fun readFile() { val input = readInputFromFile("src/main/resources/a2025/day04.txt") for ((r, line) in input.lines().filter(String::isNotBlank).withIndex()) { grid.add(mutableListOf()) for (char in line) { grid[r].add(char) } } } override fun solvePartOne(): String { return countRolls(grid).toString() } override fun solvePartTwo(): String { var sum = 0 var removed = -1 while (removed != 0) { // main grid is modified here, not the best but doesn't really matter // also no idea how to clone 2D list in Kotlin removed = countRolls(grid, true) sum += removed } return sum.toString() } private fun countRolls(grid: MutableList<MutableList<Char>>, removeRolls: Boolean = false): Int { val dr = listOf(-1, -1, -1, 0, 0, 1, 1, 1) val dc = listOf(-1, 0, 1, -1, 1, -1, 0, 1) var sum = 0 for (r in grid.indices) { for (c in grid[r].indices) { if (grid[r][c] != '@') continue var neighborCount = 0 for (i in dr.indices) { if (gridGet(grid, r + dr[i], c + dc[i]) == '@') neighborCount++ } if (neighborCount < 4) { sum++ if (removeRolls) grid[r][c] = '.' } } } return sum } private fun gridGet(grid: List<List<Char>>, r: Int, c: Int, default: Char = '.'): Char { return if (r in grid.indices && c in grid[r].indices) { grid[r][c] } else { default } } }full code on Codeberg
Ha, I’ve got that article half-read in a tab somewhere. Same problem here though - they’re not in the standard library for anything I plan to use for AoC.
C
For loops!
Code
#include <stdio.h> #define GZ 144 static char g[GZ][GZ]; int main() { int p1=0,p2=0, nc=0, x,y; for (y=1; fgets(g[y]+1, GZ-2, stdin); y++) ; for (y=1; y<GZ-1; y++) for (x=1; x<GZ-1; x++) p1 += g[y][x] == '@' && (g[y-1][x-1] == '@') + (g[y-1][x ] == '@') + (g[y-1][x+1] == '@') + (g[y ][x-1] == '@') + (g[y ][x+1] == '@') + (g[y+1][x-1] == '@') + (g[y+1][x ] == '@') + (g[y+1][x+1] == '@') < 4; do { nc = 0; for (y=1; y<GZ-1; y++) for (x=1; x<GZ-1; x++) if (g[y][x] == '@' && (g[y-1][x-1] == '@') + (g[y-1][x ] == '@') + (g[y-1][x+1] == '@') + (g[y ][x-1] == '@') + (g[y ][x+1] == '@') + (g[y+1][x-1] == '@') + (g[y+1][x ] == '@') + (g[y+1][x+1] == '@') < 4) { nc++; p2++; g[y][x] = '.'; } } while (nc); printf("04: %d %d\n", p1, p2); return 0; }For my x86-16 version, the 20K input is pushing it over the 64K .COM limit, so I’ll need to implement some better compression first.
Python
Simple brute-force is enough.
DIRECTIONS = [ (0, 1), (1, 0), (0, -1), (-1, 0), # cardinal (1, 1), (1, -1), (-1, 1), (-1, -1) # diagonal ] # yield all valid neighbor coordinates def yield_neighbors(i, j, m, n): for di, dj in DIRECTIONS: ni, nj = i+di, j+dj if 0 <= ni < m and 0 <= nj < n: yield ni, nj # build char grid from input data def build_grid(data: str): return [list(row) for row in data.splitlines()] def part1(data: str): grid = build_grid(data) m, n = len(grid), len(grid[0]) # count accessible rolls accessible = 0 for i in range(m): for j in range(n): if grid[i][j] != '@': continue neighbor_rolls = sum(grid[ni][nj] == '@' for ni, nj in yield_neighbors(i, j, m, n)) if neighbor_rolls < 4: accessible += 1 return accessible def part2(data: str): grid = build_grid(data) m, n = len(grid), len(grid[0]) total_accessible = 0 # total accessible rolls over all cycles accessible = None # rolls accessible this cycle # repeat until no more rolls can be accessed while accessible != 0: accessible = 0 for i in range(m): for j in range(n): if grid[i][j] != '@': continue neighbor_rolls = sum(grid[ni][nj] == '@' for ni, nj in yield_neighbors(i, j, m, n)) if neighbor_rolls < 4: accessible += 1 # we can immediately remove this roll, no need to wait for next cycle grid[i][j] = '.' # accumulate accessible rolls this cycle total_accessible += accessible return total_accessible sample = """<paste sample here>""" assert part1(sample) == 13 assert part2(sample) == 43








