this post was submitted on 04 Dec 2024
17 points (94.7% liked)

Advent Of Code

920 readers
2 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2024

Solution Threads

M T W T F S S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
 

Day 4: Ceres Search

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

top 34 comments
sorted by: hot top controversial new old
[–] VegOwOtenks 4 points 1 month ago

I struggled a lot more when doing list slices that I would've liked to

Haskell


import Data.List qualified as List

collectDiagonal :: [String] -> Int -> Int -> String
collectDiagonal c y x
        | length c > y && length (c !! y) > x = c !! y !! x : collectDiagonal c (y+1) (x+1)
        | otherwise = []

part1 c = do
        let forwardXMAS  = map (length . filter (List.isPrefixOf "XMAS") . List.tails) $ c
        let backwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse) $ c
        let downwardXMAS  = map (length . filter (List.isPrefixOf "XMAS") . List.tails ) . List.transpose $ c
        let upwardXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse ) . List.transpose $ c
        let leftSideDiagonals = map (\ y -> collectDiagonal c y 0) [0..length c]
        let leftTopDiagonals = map (\ x -> collectDiagonal c 0 x) [1..(length . List.head $ c)]
        let leftDiagonals = leftSideDiagonals ++ leftTopDiagonals
        let rightSideDiagonals = map (\ y -> collectDiagonal (map List.reverse c) y 0) [0..length c]
        let rightTopDiagonals = map (\ x -> collectDiagonal (map List.reverse c) 0 x) [1..(length . List.head $ c)]
        let rightDiagonals = rightSideDiagonals ++ rightTopDiagonals
        let diagonals = leftDiagonals ++ rightDiagonals

        let diagonalXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails) $ diagonals
        let reverseDiagonalXMAS = map (length . filter (List.isPrefixOf "XMAS") . List.tails . reverse) $ diagonals

        print . sum $ [sum forwardXMAS, sum backwardXMAS, sum downwardXMAS, sum upwardXMAS, sum diagonalXMAS, sum reverseDiagonalXMAS]
        return ()

getBlock h w c y x = map (take w . drop x) . take h . drop y $ c

isXBlock b = do
        let diagonal1 = collectDiagonal b 0 0
        let diagonal2 = collectDiagonal (map List.reverse b) 0 0

        diagonal1 `elem` ["SAM", "MAS"] && diagonal2 `elem` ["SAM", "MAS"]

part2 c = do
        
        let lineBlocks = List.map (getBlock 3 3 c) [0..length c - 1]
        let groupedBlocks = List.map (flip List.map [0..(length . head $ c) - 1]) lineBlocks

        print . sum . map (length . filter isXBlock) $ groupedBlocks

        return ()

main = do
        c <- lines <$> getContents

        part1 c
        part2 c

        return ()
[–] [email protected] 4 points 1 month ago* (last edited 1 month ago)

Nim

Could be done more elegantly, but I haven’t bothered yet.

proc solve(input: string): AOCSolution[int, int] =
  var lines = input.splitLines()

  block p1:
    # horiz
    for line in lines:
      for i in 0..line.high-3:
        if line[i..i+3] in ["XMAS", "SAMX"]:
          inc result.part1

    for y in 0..lines.high-3:
      #vert
      for x in 0..lines[0].high:
        let word = collect(for y in y..y+3: lines[y][x])
        if word in [@"XMAS", @"SAMX"]:
          inc result.part1

      #diag \
      for x in 0..lines[0].high-3:
        let word = collect(for d in 0..3: lines[y+d][x+d])
        if word in [@"XMAS", @"SAMX"]:
          inc result.part1

      #diag /
      for x in 3..lines[0].high:
        let word = collect(for d in 0..3: lines[y+d][x-d])
        if word in [@"XMAS", @"SAMX"]:
          inc result.part1

  block p2:
    for y in 0..lines.high-2:
      for x in 0..lines[0].high-2:
        let diagNW = collect(for d in 0..2: lines[y+d][x+d])
        let diagNE = collect(for d in 0..2: lines[y+d][x+2-d])
        if diagNW in [@"MAS", @"SAM"] and diagNE in [@"MAS", @"SAM"]:
          inc result.part2

Codeberg repo

[–] [email protected] 4 points 1 month ago

Haskell

Popular language this year :)

I got embarrassingly stuck on this one trying to be clever with list operations. Then I realized I should just use an array...

import Data.Array.Unboxed (UArray)
import Data.Array.Unboxed qualified as A
import Data.Bifunctor

readInput :: String -> UArray (Int, Int) Char
readInput s =
  let rows = lines s
      n = length rows
   in A.listArray ((1, 1), (n, n)) $ concat rows

s1 `eq` s2 = s1 == s2 || s1 == reverse s2

part1 arr = length $ filter isXmas $ concatMap lines $ A.indices arr
  where
    isXmas ps = all (A.inRange $ A.bounds arr) ps && map (arr A.!) ps `eq` "XMAS"
    lines p = [take 4 $ iterate (bimap (+ di) (+ dj)) p | (di, dj) <- [(1, 0), (0, 1), (1, 1), (1, -1)]]

part2 arr = length $ filter isXmas innerPoints
  where
    innerPoints =
      let ((i1, j1), (i2, j2)) = A.bounds arr
       in [(i, j) | i <- [i1 + 1 .. i2 - 1], j <- [j1 + 1 .. j2 - 1]]
    isXmas p = up p `eq` "MAS" && down p `eq` "MAS"
    up (i, j) = map (arr A.!) [(i + 1, j - 1), (i, j), (i - 1, j + 1)]
    down (i, j) = map (arr A.!) [(i - 1, j - 1), (i, j), (i + 1, j + 1)]

main = do
  input <- readInput <$> readFile "input04"
  print $ part1 input
  print $ part2 input
[–] mykl 4 points 1 month ago* (last edited 1 month ago) (1 children)

Uiua

Just part1 for now as I need to walk the dog :-)

[edit] Part 2 now added, and a nicer approach than Part 1 in my opinion, if you're able to keep that many dimensions straight in your head :-)

[edit 2] Tightened it up a bit more.

Grid ← ⊜∘⊸≠@\n "MMMSXXMASM\nMSAMXMSMSA\nAMXSXMAAMM\nMSAMASMSMX\nXMASAMXAMM\nXXAMMXXAMA\nSMSMSASXSS\nSAXAMASAAA\nMAMMMXMMMM\nMXMXAXMASX"

≡⍉⍉×⇡4¤[1_0 0_1 1_1 1_¯1]         # Use core dirs to build sets of 4-offsets.
↯∞_2⇡△ Grid                       # Get all possible starting points.
&p/+♭⊞(+∩(≍"XMAS")⇌.⬚@.⊡:Grid≡+¤) # Part 1. Join the two into a table, use to pick 4-elements, check, count.

Diags   ← [[¯. 1_1] [¯. 1_¯1]]
BothMas ← /×≡(+∩(≍"MS")⇌.)⬚@.⊡≡+Diags¤¤ # True if both diags here are MAS.
&p/+≡BothMas⊚="A"⟜¤Grid                 # Part 2. For all "A"s in grid, check diags, count where good.
[–] [email protected] 3 points 1 month ago (1 children)

I'm not even sure how to write most of these characters

[–] mykl 2 points 1 month ago (1 children)

The operators have all got ascii names you can type, and the formatter converts them to the symbols. It's a bit odd but really worthwhile, as you get access to the powerful array handling functionality that made solving today's challenges so much more straightforward than in other languages.

[–] [email protected] 2 points 1 month ago

It looks quite functional indeed

[–] LeixB 3 points 1 month ago* (last edited 1 month ago)

Haskell

import Control.Arrow
import Data.Array.Unboxed
import Data.List

type Pos = (Int, Int)
type Board = Array Pos Char
data Dir = N | NE | E | SE | S | SW | W | NW

target = "XMAS"

parse s = listArray ((1, 1), (n, m)) [l !! i !! j | i <- [0 .. n - 1], j <- [0 .. m - 1]]
  where
    l = lines s
    (n, m) = (length $ head l, length l)

move N = first pred
move S = first succ
move E = second pred
move W = second succ
move NW = move N . move W
move SW = move S . move W
move NE = move N . move E
move SE = move S . move E

check :: Board -> Pos -> Int -> Dir -> Bool
check b p i d =
    i >= length target
        || ( inRange (bounds b) p
                && (b ! p) == (target !! i)
                && check b (move d p) (succ i) d
           )

checkAllDirs :: Board -> Pos -> Int
checkAllDirs b p = length . filter (check b p 0) $ [N, NE, E, SE, S, SW, W, NW]

check2 :: Board -> Pos -> Bool
check2 b p =
    all (inRange (bounds b)) moves && ((b ! p) == 'A') && ("SSMM" `elem` rotations)
  where
    rotations = rots $ (b !) <$> moves
    moves = flip move p <$> [NE, SE, SW, NW]

    rots xs = init $ zipWith (++) (tails xs) (inits xs)

part1 b = sum $ checkAllDirs b <$> indices b
part2 b = length . filter (check2 b) $ indices b

main = getContents >>= print . (part1 &&& part2) . parse
[–] TunaCowboy 2 points 1 month ago* (last edited 1 month ago)

python

solution

import aoc

def setup():
    return (aoc.get_lines(4, padded=(True, '.', 3)), 0)

def one():
    lines, acc = setup()
    for row, l in enumerate(lines):
        for col, c in enumerate(l):
            if c == 'X':
                w = l[col - 3:col + 1]
                e = l[col:col + 4]
                n = c + lines[row - 1][col] + \
                    lines[row - 2][col] + lines[row - 3][col]
                s = c + lines[row + 1][col] + \
                    lines[row + 2][col] + lines[row + 3][col]
                nw = c + lines[row - 1][col - 1] + \
                    lines[row - 2][col - 2] + lines[row - 3][col - 3]
                ne = c + lines[row - 1][col + 1] + \
                    lines[row - 2][col + 2] + lines[row - 3][col + 3]
                sw = c + lines[row + 1][col - 1] + \
                    lines[row + 2][col - 2] + lines[row + 3][col - 3]
                se = c + lines[row + 1][col + 1] + \
                    lines[row + 2][col + 2] + lines[row + 3][col + 3]
                for word in [w, e, n, s, nw, ne, sw, se]:
                    if word in ['XMAS', 'SAMX']:
                        acc += 1
    print(acc)

def two():
    lines, acc = setup()
    for row, l in enumerate(lines):
        for col, c in enumerate(l):
            if c == 'A':
                l = lines[row - 1][col - 1] + c + lines[row + 1][col + 1]
                r = lines[row + 1][col - 1] + c + lines[row - 1][col + 1]
                if l in ['MAS', 'SAM'] and r in ['MAS', 'SAM']:
                    acc += 1
    print(acc)

one()
two()

[–] [email protected] 2 points 1 month ago

Uiua

This one was nice. The second part seemed quite daunting at first but wasn't actually that hard in the end.

Run with example input here

Row    ← ⌕ "XMAS"
RevRow ← ⌕"SAMX"
Sum    ← /+/+
Count  ← +∩Sum⊃Row RevRow

PartOne ← (
  &rs ∞ &fo "input-4.txt"
  ⊜∘≠@\n.
  ⊙+⟜∩Count⟜⍉ # horizontal and vertical search
  ⟜(/+⧈(Count⍉≡⬚@ ↻⇡⧻.)4)
  /+⧈(Count⍉≡⬚@ ↻¯⇡⧻.)4
  ++
)

Mask ← °⊚×2⇡5
# Create variations of X-MAS
Vars ← (
  ["M S"
   " A "
   "M S"]
  ≡♭[∩⟜⍉]≡⇌.
  Mask
  ⊏0⊞▽¤
)

PartTwo ← (
  &rs ∞ &fo "input-4.txt"
  ⊜∘≠@\n.
  ⧈(/+♭⊞≍⊙¤Vars▽Mask♭)3_3
  Sum
)

&p "Day 4:"
&pf "Part 1: "
&p PartOne
&pf "Part 2: "
&p PartTwo
[–] [email protected] 2 points 1 month ago* (last edited 1 month ago)

Factor

spoiler

: get-input ( -- rows )
  "vocab:aoc-2024/04/input.txt" utf8 file-lines ;

: verticals ( rows -- lines )
  [ dimension last [0..b) ] keep cols ;

: slash-origins ( rows -- coords )
  dimension
  [ first [0..b) [ 0 2array ] map ] [
    first2 [ 1 - ] [ 1 (a..b] ] bi*
    [ 2array ] with map
  ] bi append ;

: backslash-origins ( rows -- coords )
  dimension first2
  [ [0..b) [ 0 2array ] map ]
  [ 1 (a..b] [ 0 swap 2array ] map ] bi* append ;

: slash ( rows origin -- line )
  first2
  [ 0 [a..b] ]
  [ pick dimension last [a..b) ] bi* zip
  swap matrix-nths ;

: backslash ( rows origin -- line )
  [ dup dimension ] dip first2
  [ over first [a..b) ]
  [ pick last [a..b) ] bi* zip nip
  swap matrix-nths ;

: slashes ( rows -- lines )
  dup slash-origins
  [ slash ] with map ;

: backslashes ( rows -- lines )
  dup backslash-origins
  [ backslash ] with map ;

: word-count ( line word -- n )
  dupd [ reverse ] dip
  '[ _ subseq-indices length ] bi@ + ;

: part1 ( -- n )
  get-input
  { [ ] [ verticals ] [ slashes ] [ backslashes ] } cleave-array concat
  [ "XMAS" word-count ] map-sum ;

: origin-adistances ( rows origins line-quot: ( rows origin -- line ) -- origin-adistances-assoc )
  with zip-with
  "MAS" "SAM" [ '[ [ _ subseq-indices ] map-values ] ] bi@ bi append
  harvest-values
  [ [ 1 + ] map ] map-values ; inline

: a-coords ( origin-adistances coord-quot: ( adistance -- row-delta col-delta ) -- coords )
  '[ first2 [ @ 2array v+ ] with map ] map-concat ; inline

: slash-a-coords ( rows -- coords )
  dup slash-origins [ slash ] origin-adistances
  [ [ 0 swap - ] keep ] a-coords ;

: backslash-a-coords ( rows -- coords )
  dup backslash-origins [ backslash ] origin-adistances
  [ dup ] a-coords ;

: part2 ( -- n )
  get-input [ slash-a-coords ] [ backslash-a-coords ] bi
  intersect length ;

Better viewed on GitHub.

[–] [email protected] 2 points 1 month ago

C#

public class Day04 : Solver
{
  private int width, height;
  private char[,] data;

  public void Presolve(string input) {
    var lines = input.Trim().Split("\n").ToList();
    height = lines.Count;
    width = lines[0].Length;
    data = new char[height, width];
    for (int i = 0; i < height; i++) {
      for (int j = 0; j < width; j++) {
        data[i, j] = lines[i][j];
      }
    }
  }

  private static readonly string word = "XMAS";

  public string SolveFirst()
  {
    int counter = 0;
    for (int start_i = 0; start_i < height; start_i++) {
      for (int start_j = 0; start_j < width; start_j++) {
        if (data[start_i, start_j] != word[0]) continue;
        for (int di = -1; di <= 1; di++) {
          for (int dj = -1; dj <= 1; dj++) {
            if (di == 0 && dj == 0) continue;
            int end_i = start_i + di * (word.Length - 1);
            int end_j = start_j + dj * (word.Length - 1);
            if (end_i < 0 || end_j < 0 || end_i >= height || end_j >= width) continue;
            for (int k = 1; k < word.Length; k++) {
              if (data[start_i + di * k, start_j + dj * k] != word[k]) break;
              if (k == word.Length - 1) counter++;
            }
          }
        }
      }
    }
    return counter.ToString();
  }

  public string SolveSecond()
  {
    int counter = 0;
    for (int start_i = 1; start_i < height - 1; start_i++) {
      for (int start_j = 1; start_j < width - 1; start_j++) {
        if (data[start_i, start_j] != 'A') continue;
        int even_mas_starts = 0;
        for (int di = -1; di <= 1; di++) {
          for (int dj = -1; dj <= 1; dj++) {
            if (di == 0 && dj == 0) continue;
            if ((di + dj) % 2 != 0) continue;
            if (data[start_i + di, start_j + dj] != 'M') continue;
            if (data[start_i - di, start_j - dj] != 'S') continue;
            even_mas_starts++;
          }
        }
        if (even_mas_starts == 2) counter++;
      }
    }
    return counter.ToString();
  }
}
[–] zarlin 2 points 1 month ago

Nim

import ../aoc, strutils

type
  Cell* = tuple[x,y:int]

#the 8 grid direction
const directions : array[8, Cell] = [
  (1, 0), (-1, 0),
  (0, 1), ( 0,-1),
  (1, 1), (-1,-1),
  (1,-1), (-1, 1)
]

const xmas = "XMAS"

#part 1
proc searchXMAS*(grid:seq[string], x,y:int):int =
  #search in all 8 directions (provided we can find a full match in that direction)
  let w = grid[0].len
  let h = grid.len
  
  for dir in directions:
    # check if XMAS can even fit
    let xEnd = x + dir.x * 3
    let yEnd = y + dir.y * 3
    if xEnd < 0 or xEnd >= w or
       yEnd < 0 or yEnd >= h:
      continue;
    
    #step along direction
    var matches = 0
    for s in 0..3:
      if grid[y + dir.y * s][x + dir.x * s] == xmas[s]:
        inc matches
        
    if matches == xmas.len:
      inc result

#part 2
proc isMAS(grid:seq[string], c, o:Cell):bool=
  let ca : Cell = (c.x+o.x, c.y+o.y)
  let cb : Cell = (c.x-o.x, c.y-o.y)
  let a = grid[ca.y][ca.x]
  let b = grid[cb.y][cb.x]
  (a == 'M' and b == 'S') or (a == 'S' and b == 'M')

proc searchCrossMAS*(grid:seq[string], x,y:int):bool =
  grid[y][x] == 'A' and
  grid.isMAS((x,y), (1,1)) and
  grid.isMAS((x,y), (1,-1))

proc solve*(input:string): array[2,int] =
  let grid = input.splitLines
  let w = grid[0].len
  let h = grid.len
  
  #part 1
  for y in 0..<h:
    for x in 0..<w:
      result[0] += grid.searchXMAS(x, y)
  
  #part 2, skipping borders
  for y in 1..<h-1:
    for x in 1..<w-1:
      result[1] += (int)grid.searchCrossMAS(x, y)

Part 1 was done really quickly. Part 2 as well, but the result was not accepted...

Turns out +MAS isn't actually a thing :P

[–] [email protected] 2 points 1 month ago

Rust

Ugh. Spent way too long on today's. Should have just used my own grid structure from last year. I will likely refactor to use that. Even though it's likely a super slow implementation, the convenience of dealing with it is better than shoehorning in the grid::Grid<T> from that crate.

solution (no supporting code)

use grid::Grid;

use crate::shared::{
    grid2d::{iter_diag_nesw, iter_diag_nwse, Point},
    util::read_lines,
};

fn parse_grid(input: &[String]) -> Grid<u8> {
    let cols = input.first().unwrap().len();
    Grid::from_vec(
        input
            .iter()
            .flat_map(|row| row.chars().map(|c| c as u8).collect::<Vec<u8>>())
            .collect(),
        cols,
    )
}

fn part1(grid: &Grid<u8>) -> usize {
    let mut xmas_count = 0;
    let rows = grid
        .iter_rows()
        .map(|d| String::from_utf8(d.copied().collect()).unwrap());
    let cols = grid
        .iter_cols()
        .map(|d| String::from_utf8(d.copied().collect()).unwrap());
    for diag in iter_diag_nesw(grid)
        .chain(iter_diag_nwse(grid))
        .filter_map(|d| {
            if d.len() >= 4 {
                Some(String::from_utf8(d.clone()).unwrap())
            } else {
                None
            }
        })
        .chain(rows)
        .chain(cols)
    {
        xmas_count += diag.matches("XMAS").count() + diag.matches("SAMX").count()
    }
    xmas_count
}

fn part2(grid: &Grid<u8>) -> usize {
    let mut xmas_count = 0;
    let valid = [
        [b'M', b'M', b'S', b'S'],
        [b'M', b'S', b'S', b'M'],
        [b'S', b'M', b'M', b'S'],
        [b'S', b'S', b'M', b'M'],
    ];
    for x in 1..grid.cols() - 1 {
        for y in 1..grid.rows() - 1 {
            if grid.get(y, x) == Some(&b'A')
                && valid.contains(
                    &(Point::new(x as isize, y as isize)
                        .diagonal_neighbors(grid)
                        .map(|i| i.unwrap_or(0))),
                )
            {
                xmas_count += 1;
            }
        }
    }
    xmas_count
}

pub fn solve() {
    let input = read_lines("inputs/day04.txt");
    let grid = parse_grid(&input);
    println!("Part 1: {}", part1(&grid));
    println!("Part 2: {}", part2(&grid));
}

And here's a link to the Github if you care to see the gross supporting code :D

[–] [email protected] 2 points 1 month ago* (last edited 1 month ago)

Rust

I had a hunch about part two that didn't pay off, so I over-coded this instead of just using an array of arrays.

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 {
    fn all() -> &'static [Direction] {
        &[
            Direction::N,
            Direction::NE,
            Direction::E,
            Direction::SE,
            Direction::S,
            Direction::SW,
            Direction::W,
            Direction::NW,
        ]
    }
}

#[derive(Debug, PartialEq, Eq)]
struct WordSearch {
    grid: Vec<char>,
    width: usize,
    height: usize,
}

impl FromStr for WordSearch {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let grid: Vec<_> = s.chars().filter(|&ch| ch != '\n').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 WordSearch {
    fn neighbour(&self, i: usize, dir: Direction) -> Option<usize> {
        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 % width != 0 => Some(i + width - 1),
            W if i % width != 0 => Some(i - 1),
            NW if i >= width && i % width != 0 => Some(i - width - 1),
            _ => None,
        }
    }

    fn word_count(&self, word: &str) -> Result<usize> {
        let mut found = 0;
        for i in 0..self.grid.len() {
            for dir in Direction::all() {
                if self.word_present(word, i, *dir) {
                    found += 1;
                }
            }
        }
        Ok(found)
    }

    fn x_count(&self) -> Result<usize> {
        let mut found = 0;
        for i in 0..self.grid.len() {
            if self.x_present(i) {
                found += 1;
            }
        }
        Ok(found)
    }

    fn word_present(&self, word: &str, location: usize, dir: Direction) -> bool {
        let mut next = Some(location);
        for ch in word.chars() {
            let i = if let Some(i) = next {
                i
            } else {
                // Off the edge
                return false;
            };

            if self.grid[i] != ch {
                return false;
            }
            next = self.neighbour(i, dir);
        }
        true
    }

    fn x_present(&self, location: usize) -> bool {
        if self.grid.get(location) != Some(&'A') {
            return false;
        }
        let diags = [
            (Direction::NE, Direction::SW),
            (Direction::NW, Direction::SE),
        ];
        diags.iter().all(|(dir_a, dir_b)| {
            let Some(a_idx) = self.neighbour(location, *dir_a) else {
                return false;
            };
            let Some(b_idx) = self.neighbour(location, *dir_b) else {
                return false;
            };
            let a = self.grid[a_idx];
            let b = self.grid[b_idx];
            (a == 'M' && b == 'S') || (b == 'M' && a == 'S')
        })
    }
}

fn part1(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let grid = WordSearch::from_str(&input)?;
    grid.word_count("XMAS")
}

fn part2(filepath: &str) -> Result<usize> {
    let input = fs::read_to_string(filepath)?;
    let grid = WordSearch::from_str(&input)?;
    grid.x_count()
}

fn main() -> Result<()> {
    color_eyre::install()?;

    println!("Part 1: {}", part1("d04/input.txt")?);
    println!("Part 2: {}", part2("d04/input.txt")?);
    Ok(())
}
[–] [email protected] 2 points 1 month ago

C

What can I say, bunch of for loops! I add a 3 cell border to avoid having to do bounds checking in the inner loops.

Code

#include "common.h"
#define GZ 146

int main(int argc, char **argv) {
	static char g[GZ][GZ];
	static const char w[] = "XMAS";
	int p1=0,p2=0, x,y, m,i;

	if (argc > 1) DISCARD(freopen(argv[1], "r", stdin));
	for (y=3; y<GZ && fgets(g[y]+3, GZ-3, stdin); y++) ;

	for (y=3; y<GZ-3; y++)
	for (x=3; x<GZ-3; x++) {
		for (m=1,i=0; i<4; i++) {m &= g[y+i][x]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y-i][x]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y][x+i]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y][x-i]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y+i][x+i]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y-i][x-i]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y+i][x-i]==w[i];} p1+=m;
		for (m=1,i=0; i<4; i++) {m &= g[y-i][x+i]==w[i];} p1+=m;

		p2 += g[y+1][x+1]=='A' &&
		      ((g[y][x]  =='M' && g[y+2][x+2]=='S')  ||
		       (g[y][x]  =='S' && g[y+2][x+2]=='M')) &&
		      ((g[y+2][x]=='M' && g[y][x+2]  =='S')  ||
		       (g[y+2][x]=='S' && g[y][x+2]  =='M'));
	}

	printf("04: %d %d\n", p1, p2);
}

https://github.com/sjmulder/aoc/blob/master/2024/c/day04.c

[–] [email protected] 1 points 1 month ago

Lisp

Not super happy with the code, but it got the job done.

Part 1 and 2

(defun p1-process-line (line)
  (to-symbols line))

(defun found-word-h (word data i j)
  "checks for a word existing from the point horizontally to the right"
  (loop for j2 from j 
        for w in word
        when (not (eql w (aref data i j2)))
          return nil
        finally (return t)))

(defun found-word-v (word data i j)
  "checks for a word existing from the point vertically down"
  (loop for i2 from i 
        for w in word
        when (not (eql w (aref data i2 j)))
          return nil
        finally (return t)))

(defun found-word-d-l (word data i j)
  "checks for a word existsing from the point diagonally to the left and down"
  (destructuring-bind (n m) (array-dimensions data)
    (declare (ignorable n))
    
    (and (>= (- i (length word)) -1)
         (>= m (+ j  (length word)))
         (loop for i2 from i downto 0
               for j2 from j
               for w in word
               when (not (eql w (aref data i2 j2)))
                 return nil
               finally  (return t)))))

(defun found-word-d-r (word data i j)
  "checks for a word existing from the point diagonally to the right and down"
  (destructuring-bind (n m) (array-dimensions data)
    (and (>= n (+ i (length word)))
         (>= m (+ j  (length word)))
         (loop for i2 from i
               for j2 from j
               for w in word
               when (not (eql w (aref data i2 j2)))
                 return nil
               finally  (return t)))
    ))

(defun count-word-h (data word)
  "Counts horizontal matches of the word"
  (let ((word-r (reverse word))
        (word-l (length word)))
    (destructuring-bind (n m) (array-dimensions data)
      (loop for i from 0 below n 
            sum (loop for j from 0 upto (- m word-l)
                      count (found-word-h word data i j)
                      count (found-word-h word-r data i j))))))

(defun count-word-v (data word)
  "Counts vertical matches of the word"
  (let ((word-r (reverse word))
        (word-l (length word)))
    (destructuring-bind (n m) (array-dimensions data)
      (loop for j from 0 below m 
            sum (loop for i from 0 upto (- n word-l)
                      count (found-word-v word data i j)
                      count (found-word-v word-r data i j))))))

(defun count-word-d (data word)
  "Counts diagonal matches of the word"
  (let ((word-r (reverse word)))
    (destructuring-bind (n m) (array-dimensions data)
      (loop for i from 0 below n
            sum (loop for j from 0 below m
                      count (found-word-d-l word data i j)
                      count (found-word-d-l word-r data i j)
                      count (found-word-d-r word data i j)
                      count (found-word-d-r word-r data i j)
                      )))))


(defun run-p1 (file)
  "cares about the word xmas in any direction"
  (let ((word '(X M A S))
        (data (list-to-2d-array (read-file file #'p1-process-line))))
    (+
     (count-word-v data word)
     (count-word-h data word)
     (count-word-d data word))))


(defun run-p2 (file) 
  "cares about an x of mas crossed with mas"
  (let ((word '(M A S))
        (word-r '(S A M))
        (data (list-to-2d-array (read-file file #'p1-process-line))))
    (destructuring-bind (n m) (array-dimensions data)
      (loop for i from 0 below (- n 2)
            sum (loop for j from 0 below (- m 2)
                      count (and (found-word-d-r word data i j)
                                 (found-word-d-l word data (+ i 2) j))
                      count (and (found-word-d-r word-r data i j)
                                 (found-word-d-l word data (+ i 2) j))
                      count (and (found-word-d-r word data i j)
                                 (found-word-d-l word-r data (+ i 2) j))
                      count (and (found-word-d-r word-r data i j)
                                 (found-word-d-l word-r data (+ i 2) j))
                        )))))

[–] landreville 1 points 1 month ago
[–] [email protected] 1 points 2 weeks ago* (last edited 2 weeks ago)

Part 1:

with open('input') as data:
    lines = [l.strip() for l in data.readlines()]
# Remove empty line
class Result():
    def __init__(self):
        self.count = 0


def analyze_lines(lines: list[str]):
    ans.count += get_rights(lines)
    ans.count += get_ups(lines)
    ans.count += get_downs(lines)
    ans.count += get_down_rights(lines)
    ans.count += get_down_lefts(lines)
    ans.count += get_up_lefts(lines)
    ans.count += get_up_rights(lines)
    for line in lines:
        ans.count += get_lefts(line)




def get_ups(lines: list[str]) -> int:
    up_count = 0
    for i_l, line in enumerate(lines):
        result = ""
        if i_l < 3:
            continue
        for i_c, char in enumerate(line):
            if char == "X":
                result = char
                result += "".join([lines[i_l - n][i_c] for n in range(1, 4)])
                if result == "XMAS":
                    up_count += 1
                else:
                    result = ""
    return up_count


def get_downs(lines: list[str]) -> int:
    down_count = 0
    for i_l, l in enumerate(lines):
        result = ""
        for i_c, c in enumerate(l):
            if c == "X":
                result += c
                try:
                    result += "".join([lines[i_l + n][i_c] for n in range(1, 4)])
                except IndexError:
                    result = ""
                    continue
                finally:
                    if result == "XMAS":
                        down_count += 1
                    result = ""
    return down_count


        
def get_lefts(line: str) -> int:
    left_count = 0
    for i, char in enumerate(line):
        if i < 3:
            continue
        elif char == "X" and line[i-1] == "M" and line[i-2] == "A" and line[i-3] == "S":
            left_count += 1
    return left_count


def get_rights(lines: list[str]) -> int:
    right_counts = 0
    for l in lines:
        right_counts += l.count("XMAS")
    return right_counts

def get_down_rights(lines: list[str]) -> int:
    down_right_count = 0
    for i_l, l in enumerate(lines):
        result = ""
        for i_c, c in enumerate(l):
            if c == "X":
                result += c
                try:
                    result += "".join(
                            [lines[i_l + n][i_c + n] for n in range(1,4)]
                            )
                except IndexError:
                    result = ""
                    continue
                finally:
                    if result == "XMAS":
                        down_right_count += 1
                    result = ""
    return down_right_count

def get_down_lefts(lines: list[str]) -> int:
    down_left_count = 0
    for i_l, l in enumerate(lines):
        result = ""
        for i_c, c in enumerate(l):
            if i_c < 3:
                continue
            if c == "X":
                result += c
                try:
                    result += "".join(
                            [lines[i_l + n][i_c - n] for n in range(1,4)]
                            )
                except IndexError:
                    result = ""
                    continue
                finally:
                    if result == "XMAS":
                        down_left_count += 1
                    result = ""
    return down_left_count

def get_up_rights(lines: list[str]) -> int:
    up_right_count = 0
    for i_l, l in enumerate(lines):
        result = ""
        if i_l < 3:
            continue
        for i_c, c in enumerate(l):
            if c == "X":
                result += c
                try:
                    result += "".join(
                            [lines[i_l - n][i_c + n] for n in range(1,4)]
                            )
                except IndexError:
                    result = ""
                    continue
                finally:
                    if result == "XMAS":
                        up_right_count += 1
                    result = ""
    return up_right_count


def get_up_lefts(lines: list[str]) -> int:
    up_left_count = 0
    for i_l, l in enumerate(lines):
        result = ""
        if i_l < 3:
            continue
        for i_c, c in enumerate(l):
            if i_c < 3:
                continue
            if c == "X":
                result = c
                try:
                    result += "".join(
                            [lines[i_l - n][i_c - n] for n in range(1,4)]
                            )
                except IndexError as e:
                    result = ""
                    continue
                finally:
                    if result == "XMAS":
                        up_left_count += 1
                    result = ""
    return up_left_count

ans = Result()
analyze_lines(lines)
print(ans.count)

Part 2:

with open('input') as data:
    lines = list(filter(lambda x: x != '', [l.strip() for l in data.readlines()]))
    
xmases = 0
for i in range(1, len(lines)):
    for j in range(1, len(lines[i])):
        if lines[i][j] == "A":
            try:
                up_back = lines[i-1][j-1]
                down_over = lines[i+1][j+1]
                up_over = lines[i-1][j+1]
                down_back = lines[i+1][j-1]
            except IndexError:
                continue
            else:
                if {up_back, down_over} == set("MS") and {up_over, down_back} == set("MS"):
                    xmases += 1

print(xmases)

I actually found part two A LOT easier than part 1.

[–] [email protected] 1 points 1 month ago (1 children)

I tried to think of some clever LINQ to do this one, but was blanking entirely.

So naïve search it is.

C#

string wordsearch = "";
int width;
int height;

public void Input(IEnumerable<string> lines)
{
  wordsearch = string.Join("", lines);
  height = lines.Count();
  width = lines.First().Length;
}

public void Part1()
{
  int words = 0;
  for (int y = 0; y < height; y++)
    for (int x = 0; x < width; x++)
      words += SearchFrom(x, y);

  Console.WriteLine($"Words: {words}");
}
public void Part2()
{
  int words = 0;
  for (int y = 1; y < height - 1; y++)
    for (int x = 1; x < width - 1; x++)
      words += SearchCross(x, y);

  Console.WriteLine($"Crosses: {words}");
}

public int SearchFrom(int x, int y)
{
  char at = wordsearch[y * width + x];
  if (at != 'X')
    return 0;

  int words = 0;
  for (int ydir = -1; ydir <= 1; ++ydir)
    for (int xdir = -1; xdir <= 1; ++xdir)
    {
      if (xdir == 0 && ydir == 0)
        continue;

      if (SearchWord(x, y, xdir, ydir))
        words++;
    }

  return words;
}

private readonly string word = "XMAS";
public bool SearchWord(int x, int y, int xdir, int ydir)
{
  int wordit = 0;
  while (true)
  {
    char at = wordsearch[y * width + x];
    if (at != word[wordit])
      return false;

    if (wordit == word.Length - 1)
      return true;

    wordit++;

    x += xdir;
    y += ydir;

    if (x < 0 || y < 0 || x >= width || y >= height)
      return false;
  }
}

public int SearchCross(int x, int y)
{
  if (x == 0 || y == 0 || x == width - 1 || y == width - 1)
    return 0;

  char at = wordsearch[y * width + x];
  if (at != 'A')
    return 0;

  int found = 0;
  for (int ydir = -1; ydir <= 1; ++ydir)
    for (int xdir = -1; xdir <= 1; ++xdir)
    {
      if (xdir == 0 || ydir == 0)
        continue;

      if (wordsearch[(y + ydir) * width + (x + xdir)] != 'M')
        continue;
      if (wordsearch[(y - ydir) * width + (x - xdir)] != 'S')
        continue;

      found++;
    }

  if (found == 2)
    return 1;

  return 0;
}

[–] [email protected] 1 points 1 month ago

I haven't quite started yet, and this one does feel like a busy work kinda problem. I was wondering if I could write something to rotate the board and do the search, but I think that might be not worth the effort

[–] [email protected] 1 points 1 month ago

Rust

One of those with running through tricky grid indices. The vector types from the euclid crate helped in dealing with positions.

Code

use euclid::{vec2, default::*};

fn count_xmas(grid: &[&[u8]], pos: (usize, usize)) -> u32 {
    if grid[pos.1][pos.0] != b'X' {
        return 0
    }

    let bounds = Rect::new(Point2D::origin(), Size2D::new(grid[0].len() as i32, grid.len() as i32));
    const DIRS: [Vector2D<i32>; 8] = [
        vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1),
        vec2(1, 1), vec2(1, -1), vec2(-1, 1), vec2(-1, -1),
    ];
    let mut count = 0;
    for dir in DIRS {
        let mut cur = Point2D::from(pos).to_i32();
        let mut found = true;
        for letter in [b'M', b'A', b'S'] {
            cur += dir;
            if !bounds.contains(cur) || grid[cur.y as usize][cur.x as usize] != letter {
                found = false;
                break
            }
        }
        if found {
            count += 1;
        }
    }
    count
}

fn part1(input: String) {
    let grid = input.lines().map(|l| l.as_bytes()).collect::<Vec<_>>();    
    let count = (0..grid.len()).map(|y| {
            (0..grid[y].len()).map(|x| count_xmas(&grid, (x, y))).sum::<u32>()
        })
        .sum::<u32>();
    println!("{count}");
}

fn is_x_mas(grid: &[&[u8]], pos: (usize, usize)) -> bool {
    if grid[pos.1][pos.0] != b'A' {
        return false
    }

    const DIRS: [Vector2D<i32>; 4] = [vec2(1, -1), vec2(1, 1), vec2(-1, 1), vec2(-1, -1)];
    let pos = Point2D::from(pos).to_i32();
    (0..4).any(|d| {
        let m_pos = [pos + DIRS[d], pos + DIRS[(d + 1) % 4]]; // 2 adjacent positions of M
        let s_pos = [pos + DIRS[(d + 2) % 4], pos + DIRS[(d + 3) % 4]]; // others S
        m_pos.iter().all(|p| grid[p.y as usize][p.x as usize] == b'M') &&
        s_pos.iter().all(|p| grid[p.y as usize][p.x as usize] == b'S')
    })
}

fn part2(input: String) {
    let grid = input.lines().map(|l| l.as_bytes()).collect::<Vec<_>>();    
    let count = (1..grid.len() - 1).map(|y| {
            (1..grid[y].len() - 1).filter(|&x| is_x_mas(&grid, (x, y))).count()
        })
        .sum::<usize>();
    println!("{count}");
}

util::aoc_main!();

(also on github)

[–] [email protected] 1 points 1 month ago

Kotlin

fun part1(input: String): Int {
    return countWordOccurrences(input.lines())
}

fun part2(input: String): Int {
    val grid = input.lines().map(String::toList)
    var count = 0
    for (row in 1..grid.size - 2) {
        for (col in 1..grid[row].size - 2) {
            if (grid[row][col] == 'A') {
                count += countCrossMatch(grid, row, col)
            }
        }
    }
    return count
}

private fun countCrossMatch(grid: List<List<Char>>, row: Int, col: Int): Int {
    val surroundingCorners = listOf(
        grid[row - 1][col - 1], // upper left
        grid[row - 1][col + 1], // upper right
        grid[row + 1][col - 1], // lower left
        grid[row + 1][col + 1], // lower right
    )
    // no matches:
    //   M S   S M
    //    A     A
    //   S M   M S
    return if (surroundingCorners.count { it == 'M' } == 2
        && surroundingCorners.count { it == 'S' } == 2
        && surroundingCorners[0] != surroundingCorners[3]
    ) 1 else 0
}

private fun countWordOccurrences(matrix: List<String>): Int {
    val rows = matrix.size
    val cols = if (rows > 0) matrix[0].length else 0
    val directions = listOf(
        Pair(0, 1),   // Horizontal right
        Pair(1, 0),   // Vertical down
        Pair(1, 1),   // Diagonal down-right
        Pair(1, -1),  // Diagonal down-left
        Pair(0, -1),  // Horizontal left
        Pair(-1, 0),  // Vertical up
        Pair(-1, -1), // Diagonal up-left
        Pair(-1, 1)   // Diagonal up-right
    )

    fun isWordAt(row: Int, col: Int, word: String, direction: Pair<Int, Int>): Boolean {
        val (dx, dy) = direction
        for (i in word.indices) {
            val x = row + i * dx
            val y = col + i * dy
            if (x !in 0 until rows || y !in 0 until cols || matrix[x][y] != word[i]) {
                return false
            }
        }
        return true
    }

    var count = 0

    for (row in 0 until rows) {
        for (col in 0 until cols) {
            for (direction in directions) {
                if (isWordAt(row, col, "XMAS", direction)) {
                    count++
                }
            }
        }
    }

    return count
}
[–] [email protected] 1 points 1 month ago

C#

namespace Day04;

static class Program
{
    public record struct Point(int Row, int Col);

    static void Main(string[] args)
    {
        var sample = File.ReadAllLines("sample.txt");
        var data = File.ReadAllLines("data.txt");

        Console.WriteLine($"Part 1 (sample): {SolvePart1(sample)}");
        Console.WriteLine($"Part 1 (data): {SolvePart1(data)}");

        Console.WriteLine($"Part 2 (sample): {SolvePart2(sample)}");
        Console.WriteLine($"Part 2 (data): {SolvePart2(data)}");
    }

    private static readonly string Search = "XMAS";

    private static readonly Func<Point, Point>[] DirectionalMoves =
    {
        p => new Point(p.Row + 1, p.Col),
        p => new Point(p.Row + 1, p.Col + 1),
        p => new Point(p.Row, p.Col + 1),
        p => new Point(p.Row - 1, p.Col + 1),
        p => new Point(p.Row - 1, p.Col),
        p => new Point(p.Row - 1, p.Col - 1),
        p => new Point(p.Row, p.Col - 1),
        p => new Point(p.Row + 1, p.Col - 1),
    };

    private static readonly Func<Point, Point>[] ForwardSlashMoves =
    {
        p => new Point(p.Row - 1, p.Col - 1),
        p => new Point(p.Row + 1, p.Col + 1),
    };
    
    private static readonly Func<Point, Point>[] BackSlashMoves =
    {
        p => new Point(p.Row + 1, p.Col - 1),
        p => new Point(p.Row - 1, p.Col + 1),
    };

    static long SolvePart1(string[] data)
    {
        return Enumerable
            .Range(0, data.Length)
            .SelectMany(row => Enumerable.Range(0, data[row].Length)
                .Select(col => new Point(row, col)))
            .Where(p => IsMatch(data, p, Search[0]))
            .Sum(p => DirectionalMoves
                .Count(move => DeepMatch(data, move(p), move, Search, 1)));
    }

    static long SolvePart2(string[] data)
    {
        return Enumerable
            .Range(0, data.Length)
            .SelectMany(row => Enumerable.Range(0, data[row].Length)
                .Select(col => new Point(row, col)))
            .Where(p => IsMatch(data, p, 'A'))
            .Count(p => CheckDiagonalMoves(data, p, ForwardSlashMoves)
                        && CheckDiagonalMoves(data, p, BackSlashMoves));
    }

    static bool CheckDiagonalMoves(string[] data, Point p, Func<Point, Point>[] moves)
        => (IsMatch(data, moves[0](p), 'S') && IsMatch(data, moves[1](p), 'M'))
           || (IsMatch(data, moves[0](p), 'M') && IsMatch(data, moves[1](p), 'S'));

    static bool DeepMatch(string[] data, Point p, Func<Point, Point> move, string search, int searchIndex) =>
        (searchIndex >= search.Length) ? true :
        (!IsMatch(data, p, search[searchIndex])) ? false :
        DeepMatch(data, move(p), move, search, searchIndex + 1);

    static bool IsMatch(string[] data, Point p, char searchChar)
        => IsInBounds(data, p) && (data[p.Row][p.Col] == searchChar);

    static bool IsInBounds(string[] data, Point p) =>
        (p.Row >= 0) && (p.Col >= 0) && (p.Row < data.Length) && (p.Col < data[0].Length);
}
[–] [email protected] 1 points 1 month ago

Go

Just a bunch of ifs and bounds checking. Part 2 was actually simpler.

Code

func part1(W [][]rune) {
	m := len(W)
	n := len(W[0])
	xmasCount := 0

	for i := 0; i < m; i++ {
		for j := 0; j < n; j++ {
			if W[i][j] != 'X' {
				continue
			}
			if j < n-3 && W[i][j+1] == 'M' && W[i][j+2] == 'A' && W[i][j+3] == 'S' {
				// Horizontal left to right
				xmasCount++
			}
			if j >= 3 && W[i][j-1] == 'M' && W[i][j-2] == 'A' && W[i][j-3] == 'S' {
				// Horizontal right to left
				xmasCount++
			}
			if i < m-3 && W[i+1][j] == 'M' && W[i+2][j] == 'A' && W[i+3][j] == 'S' {
				// Vertical up to down
				xmasCount++
			}
			if i >= 3 && W[i-1][j] == 'M' && W[i-2][j] == 'A' && W[i-3][j] == 'S' {
				// Vertical down to up
				xmasCount++
			}
			if j < n-3 && i < m-3 && W[i+1][j+1] == 'M' && W[i+2][j+2] == 'A' && W[i+3][j+3] == 'S' {
				// Diagonal left to right and up to down
				xmasCount++
			}
			if j >= 3 && i < m-3 && W[i+1][j-1] == 'M' && W[i+2][j-2] == 'A' && W[i+3][j-3] == 'S' {
				// Diagonal right to left and up to down
				xmasCount++
			}
			if j < n-3 && i >= 3 && W[i-1][j+1] == 'M' && W[i-2][j+2] == 'A' && W[i-3][j+3] == 'S' {
				// Diagonal left to right and down to up
				xmasCount++
			}
			if j >= 3 && i >= 3 && W[i-1][j-1] == 'M' && W[i-2][j-2] == 'A' && W[i-3][j-3] == 'S' {
				// Diagonal right to left and down to up
				xmasCount++
			}
		}
	}

	fmt.Println(xmasCount)
}

func part2(W [][]rune) {
	m := len(W)
	n := len(W[0])
	xmasCount := 0

	for i := 0; i <= m-3; i++ {
		for j := 0; j <= n-3; j++ {
			if W[i+1][j+1] != 'A' {
				continue
			}
			if W[i][j] == 'M' && W[i][j+2] == 'M' && W[i+2][j] == 'S' && W[i+2][j+2] == 'S' {
				xmasCount++
			} else if W[i][j] == 'M' && W[i][j+2] == 'S' && W[i+2][j] == 'M' && W[i+2][j+2] == 'S' {
				xmasCount++
			} else if W[i][j] == 'S' && W[i][j+2] == 'S' && W[i+2][j] == 'M' && W[i+2][j+2] == 'M' {
				xmasCount++
			} else if W[i][j] == 'S' && W[i][j+2] == 'M' && W[i+2][j] == 'S' && W[i+2][j+2] == 'M' {
				xmasCount++
			}
		}
	}

	fmt.Println(xmasCount)
}

func main() {
	file, _ := os.Open("input.txt")
	defer file.Close()
	scanner := bufio.NewScanner(file)

	var W [][]rune
	for scanner.Scan() {
		line := scanner.Text()
		W = append(W, []rune(line))
	}

	part1(W)
	part2(W)
}

[–] vole 1 points 1 month ago

Raku

Oof, my struggle to make custom index walking paths for part 1 did not pay off for part 2.

Solution

sub MAIN($input) {
    my $file = (open $input).slurp;
    my @grid is List = $file.lines».comb».list;
    my @transposedGrid is List = [Z] @grid;
    my @reversedGrid is List = @grid».reverse;
    my @transposedReversedGrid is List = @transposedGrid».reverse;

    my @horizontalScanRows is List = generateScanHorizontal(@grid);
    my @transposedHorizontalScanRows is List = generateScanHorizontal(@transposedGrid);

    my @part-one-counts = [];
    @part-one-counts.push(count-xmas(@grid, @horizontalScanRows)); # Right
    @part-one-counts.push(count-xmas(@transposedGrid, @transposedHorizontalScanRows)); # Down
    @part-one-counts.push(count-xmas(@reversedGrid, @horizontalScanRows)); # Left
    @part-one-counts.push(count-xmas(@transposedReversedGrid, @transposedHorizontalScanRows)); # Up

    my @diagonalScanRows is List = generateScanDiagonal(@grid);
    my @transposedDiagonalScanRows is List = generateScanDiagonal(@transposedGrid);
    @part-one-counts.push(count-xmas(@grid, @diagonalScanRows)); # Down Right
    @part-one-counts.push(count-xmas(@grid, @diagonalScanRows».reverse)); # Up Left
    @part-one-counts.push(count-xmas(@reversedGrid, @diagonalScanRows)); # Down Left
    @part-one-counts.push(count-xmas(@reversedGrid, @diagonalScanRows».reverse)); # Up Right

    my $part-one-solution = @part-one-counts.sum;
    say "part 1: $part-one-solution";


    my @part-two-counts = [];
    @part-two-counts.push(countGridMatches(@grid, (<M . S>,<. A .>,<M . S>)));
    @part-two-counts.push(countGridMatches(@grid, (<S . S>,<. A .>,<M . M>)));
    @part-two-counts.push(countGridMatches(@grid, (<S . M>,<. A .>,<S . M>)));
    @part-two-counts.push(countGridMatches(@grid, (<M . M>,<. A .>,<S . S>)));

    my $part-two-solution = @part-two-counts.sum;
    say "part 2: $part-two-solution";

}

sub count-xmas(@grid, @scanRows) {
    my $xmas-count = 0;
    for @scanRows -> @scanRow {
        my $xmas-pos = 0;
        for @scanRow -> @pos {
            my $char = @grid[@pos[0]][@pos[1]];
            if "X" eq $char {
                $xmas-pos = 1;
            }elsif <X M A S>[$xmas-pos] eq $char {
                if $xmas-pos == 3 {
                    $xmas-pos = 0;
                    $xmas-count += 1;
                } else {
                    $xmas-pos += 1;
                }
            } else {
                $xmas-pos = 0;
            }
        }
    }
    return $xmas-count;
}

sub generateScanHorizontal(@grid) {
    # Horizontal
    my $rows = @grid.elems;
    my $cols = @grid[0].elems;
    my @scanRows = ();
    for 0..^$rows -> $row {
        my @scanRow = ();
        for 0..^$cols -> $col {
            @scanRow.push(($row, $col));
        }
        @scanRows.push(@scanRow);
    }
    return @scanRows.List».List;
}

sub generateScanDiagonal(@grid) {
    # Down-right diagonal
    my $rows = @grid.elems;
    my $cols = @grid[0].elems;
    my @scanRows = ();
    for 0..^($rows + $cols - 1) -> $diag {
        my @scanRow = ();
        my $starting-row = max(-$cols + $diag + 1, 0);
        my $starting-col = max($rows - $diag - 1, 0);
        my $diag-len = min($rows - $starting-row, $cols - $starting-col);
        for 0..^$diag-len -> $diag-pos {
            @scanRow.push(($starting-row + $diag-pos, $starting-col + $diag-pos));
        }
        @scanRows.push(@scanRow);
    }
    return @scanRows.List».List;
}

sub countGridMatches(@grid, @needle) {
    my $count = 0;
    for 0..(@grid.elems - @needle.elems) -> $top {
        TOP-LEFT:
        for 0..(@grid[$top].elems - @needle[0].elems) -> $left {
            for 0..^@needle.elems -> $row-offset {
                for 0..^@needle[$row-offset].elems -> $col-offset {
                    my $needle-char = @needle[$row-offset][$col-offset];
                    next if $needle-char eq ".";
                    next TOP-LEFT if $needle-char ne @grid[$top+$row-offset][$left+$col-offset];
                }
            }
            $count += 1;
        }
    }
    return $count;
}

github

[–] [email protected] 1 points 1 month ago

This one was a little bit of a pain. I loved it.

TypeScript

Solution

import { AdventOfCodeSolutionFunction } from "./solutions";

enum Direction {
    UP,
    UP_RIGHT,
    RIGHT,
    BOTTOM_RIGHT,
    BOTTOM,
    BOTTOM_LEFT,
    LEFT,
    UP_LEFT,
};

const ALL_DIRECTIONS = [
    Direction.RIGHT,
    Direction.BOTTOM_RIGHT,
    Direction.BOTTOM,
    Direction.BOTTOM_LEFT,
    Direction.LEFT,
    Direction.UP_LEFT,
    Direction.UP,
    Direction.UP_RIGHT,
];

const check_coords = (grid: Array<Array<string>>, x: number, y: number) => {
    return y >= grid.length ||
        y < 0 ||
        x >= grid[y].length ||
        x < 0
}

const search_direction = (grid: Array<Array<string>>, x: number, y: number, direction: Direction, find: Array<string>) => {
    // exit conditions
    // no more to find
    if (find.length == 0)
        return 1; // found the end

    // invalid coords
    if (check_coords(grid, x, y))
        return 0;

    // make new mutable list
    const newFind = [...find];
    const searchChar = newFind.shift();

    // wrong character
    if (grid[y][x] !== searchChar)
        return 0;

    switch (direction) {
        case Direction.UP:
            return search_direction(grid, x, y + 1, direction, newFind);

        case Direction.UP_RIGHT:
            return search_direction(grid, x + 1, y + 1, direction, newFind);

        case Direction.RIGHT:
            return search_direction(grid, x + 1, y, direction, newFind);

        case Direction.BOTTOM_RIGHT:
            return search_direction(grid, x + 1, y - 1, direction, newFind);

        case Direction.BOTTOM:
            return search_direction(grid, x, y - 1, direction, newFind);

        case Direction.BOTTOM_LEFT:
            return search_direction(grid, x - 1, y - 1, direction, newFind);

        case Direction.LEFT:
            return search_direction(grid, x - 1, y, direction, newFind);

        case Direction.UP_LEFT:
            return search_direction(grid, x - 1, y + 1, direction, newFind);

        default:
            return 0;
    }
}

const part_1_search = (grid: Array<Array<string>>, x: number, y: number, find: Array<string>) => {
    return ALL_DIRECTIONS.reduce<number>(
        (instances, direction) =>
            instances + search_direction(grid, x, y, direction, find),
        0
    );
}

const part_2_search = (grid: Array<Array<string>>, x: number, y: number, find: Array<string>) => {
    return (
        search_direction(grid, x - 1, y + 1, Direction.BOTTOM_RIGHT, find) +
        search_direction(grid, x + 1, y + 1, Direction.BOTTOM_LEFT, find) +
        search_direction(grid, x - 1, y - 1, Direction.UP_RIGHT, find) +
        search_direction(grid, x + 1, y - 1, Direction.UP_LEFT, find)
    ) == 2 ? 1 : 0;
}

export const solution_4: AdventOfCodeSolutionFunction = (input) => {
    const grid = input.split("\n").map(st => st.trim()).map(v => v.split(""));

    let part_1 = 0;
    let part_2 = 0;

    const find_1 = "XMAS".split("");
    const find_2 = "MAS".split("");

    for (let y = 0; y < grid.length; y++) {
        for (let x = 0; x < grid[y].length; x++) {
            part_1 += part_1_search(grid, x, y, find_1);
            part_2 += part_2_search(grid, x, y, find_2);
        }
    }

    return {
        part_1,
        part_2,
    };
}

Felt like this code quality is better than what I usually output :)

[–] [email protected] 1 points 1 month ago

Python

def read_input(path):
    with open(path) as f:
        lines = f.readlines()
        for i, line in enumerate(lines):
            ln = line.replace("\n","")
            lines[i] = ln
    return lines

def find_X(lines):
    Xes = []
    for j, line in enumerate(lines):
        ind = [i for i, ltr in enumerate(line) if ltr == "X"]
        for i in ind:
            Xes.append((j,i))
    return Xes

def find_M(lines, x, dim):
    # Check for Ms
    M_dirs = []
    for i in [-1, 0, 1]:
        x_ind = x[0] + i
        if x_ind>=0 and x_ind<dim:
            for j in [-1, 0, 1]:
                y_ind = x[1]+j
                if y_ind>=0 and y_ind<dim:
                    if lines[x_ind][y_ind] == "M":
                        M = [(x_ind, y_ind), (i,j)]
                        M_dirs.append(M)
    return M_dirs

def check_surroundings(loc, lines, check_char, direction):
    max = len(lines)-1
    check_lock = [loc[i]+direction[i] for i in range(len(loc))]
    if all(i>=0 and i<=max for i in check_lock) and check_char in str(lines[check_lock[0]][check_lock[1]]):
        return True
    else:
        return False

def part_one(lines):
    ans = 0 

    X = find_X(lines)
    dim = len(lines[0])
    for x in X:
        M = find_M(lines, x, dim)
        for m in M:
            loc = m[0]
            dir = m[1]
            
            if not check_surroundings(loc, lines, 'A', dir):
                continue
            
            loc = [loc[0]+dir[0], loc[1]+dir[1]]
            if not all(i>=0 and i<=dim-1 for i in loc):
                continue
            if not check_surroundings(loc, lines, 'S', dir):
                continue
            
            ans+=1
    return ans

def extract_square(lines, loc):
    str = ""
    for i in range(-1,2,1):
        for j in range(-1,2,1):
            x_ind = loc[0]+i
            y_ind = loc[1]+j
            if not all(p>=0 and p<=len(lines[0])-1 for p in [x_ind, y_ind]):
                raise ValueError("The given lock is at the edge of the grid and therefore will not produce a square")
            str += lines[x_ind][y_ind]
    return str

def check_square(square):
    if not square[4]=="A":
        return False
    elif not ((square[0]=="M" and square[8]=="S") or (square[0]=="S" and square[8]=="M")):
        return False
    elif not ((square[2]=="M" and square[6]=="S") or (square[2]=="S" and square[6]=="M")):
        return False
    else: return True

def part_two(lines):
    ans = 0
    dim = len(lines[0])
    for i in range(1,dim-1):
        for j in range(1,dim-1):
            square = extract_square(lines, (i,j))
            if check_square(square):
                ans += 1
    return ans

path = r'Day_4\input.txt'
lines = read_input(path)
print("Answer part 1: ", part_one(lines))
print("Answer part 2: ", part_two(lines))

[–] Karmmah 1 points 1 month ago* (last edited 1 month ago)

Julia

Had some time to clean up the code today since the solution was quite straight forward after making a plan on how to approach it.

spoiler

function readWordSearch(inputFile::String)::Matrix{Char}
	f = open(inputFile,"r")
	lines::Vector{String} = readlines(f)
	close(f)
	wordSearch = Matrix{Char}(undef,length(lines),length(lines))
	for (i,line) in enumerate(lines)
		wordSearch[i,:] = collect(line)
	end
	return wordSearch
end

function countXMASAppearances(wS::Matrix{Char})::Int
	appearanceCount::Int = 0
	for i=1 : size(wS)[1] #lines
		for j=1 : size(wS)[2] #columns
			wS[i,j]!='X' ? continue : nothing #continue if char is not X
			#if char is X, check surrounding area
			# check horizontals
			#left
			j>=4 ? (wS[i,j-1]*wS[i,j-2]*wS[i,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			#right
			j<=size(wS)[2]-3 ? (wS[i,j+1]*wS[i,j+2]*wS[i,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			# check verticals
			#up
			i>=4 ? (wS[i-1,j]*wS[i-2,j]*wS[i-3,j]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			#down
			i<=size(wS)[1]-3 ? (wS[i+1,j]*wS[i+2,j]*wS[i+3,j]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			# check diagonals
			#left up
			i>=4 && j>=4 ? (wS[i-1,j-1]*wS[i-2,j-2]*wS[i-3,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			#right up
			i>=4 && j<=size(wS)[2]-3 ? (wS[i-1,j+1]*wS[i-2,j+2]*wS[i-3,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			#left down
			i<=size(wS)[1]-3 && j>=4 ? (wS[i+1,j-1]*wS[i+2,j-2]*wS[i+3,j-3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
			#right down
			i<=size(wS)[1]-3 && j<=size(wS)[2]-3 ? (wS[i+1,j+1]*wS[i+2,j+2]*wS[i+3,j+3]=="MAS" ? appearanceCount+=1 : nothing) : nothing
		end
	end
	return appearanceCount
end

function countX_MASAppearances(wordSearch::Matrix{Char})::Int
	appearances::Int = 0
	for l=2 : size(wordSearch)[1]-1
		for c=2 : size(wordSearch)[2]-1
			wordSearch[l,c]!='A' ? continue : nothing
			checkArr = [wordSearch[l-1,c-1],wordSearch[l-1,c+1],wordSearch[l+1,c-1],wordSearch[l+1,c+1]]
			if checkArr in [['M','M','S','S'],['M','S','M','S'],['S','S','M','M'],['S','M','S','M']]
				appearances += 1
			end
		end
	end
	return appearances
end

wordSearch::Matrix{Char} = readWordSearch(inputFile
prinltn("part 1 appearances: $(countXMASAppearances(wordSearch))")
prinltn("part 2 appearances: $(countX_MASAppearances(wordSearch))")

[–] [email protected] 1 points 1 month ago* (last edited 1 month ago)

Python

Essentially I'm extracting strings from the word search and compare them to the desired value. For part one that means extracting from an X in eight directions. Because I'm reading from the central X outwards, I don't need to reverse any of them.
Part two reads two strings in an X-shape around the coordinates of each X. The resulting strings are filtered down to include only "MAS" and "SAM". If there are exactly two strings we found an X-MAS.

from pathlib import Path


def parse_input(input: str) -> list[str]:
    return input.strip().splitlines()


def extract_strings_one(m: int, n: int, haystack: list[str], l: int = 4) -> list[str]:
    result = []
    # Right
    if m + l <= len(haystack[n]):
        result.append(haystack[n][m : m + l])
    # Up-Right
    if m + l <= len(haystack[n]) and n > l - 2:
        result.append("".join([haystack[n - i][m + i] for i in range(l)]))
    # Up
    if n > l - 2:
        result.append("".join([haystack[n - i][m] for i in range(l)]))
    # Up-Left
    if m > l - 2 and n > l - 2:
        result.append("".join([haystack[n - i][m - i] for i in range(l)]))
    # Left
    if m > l - 2:
        result.append("".join([haystack[n][m - i] for i in range(l)]))
    # Down-Left
    if m > l - 2 and n + l <= len(haystack):
        result.append("".join([haystack[n + i][m - i] for i in range(l)]))
    # Down
    if n + l <= len(haystack):
        result.append("".join([haystack[n + i][m] for i in range(l)]))
    # Down-Right
    if m + l <= len(haystack[n]) and n + l <= len(haystack):
        result.append("".join([haystack[n + i][m + i] for i in range(l)]))
    return result


def extract_strings_two(m: int, n: int, haystack: list[str], d: int = 1) -> list[str]:
    result = []
    if 0 <= m - d and m + d < len(haystack[n]) and 0 <= n - d and n + d < len(haystack):
        result.append("".join([haystack[n + i][m + i] for i in range(-d, d + 1)]))
        result.append("".join([haystack[n - i][m + i] for i in range(-d, d + 1)]))
    return result


def part_one(input: str) -> int:
    lines = parse_input(input)
    xmas_count = 0
    for i, line in enumerate(lines):
        x = line.find("X", 0)
        while x != -1:
            xmas_count += len(
                list(filter(lambda s: s == "XMAS", extract_strings_one(x, i, lines)))
            )
            x = line.find("X", x + 1)
    return xmas_count


def part_two(input: str) -> int:
    lines = parse_input(input)
    x_mas_count = 0
    for i, line in enumerate(lines[1:-1], 1):
        a = line.find("A", 0)
        while a != -1:
            if (
                len(
                    list(
                        filter(
                            lambda s: s in ("MAS", "SAM"),
                            extract_strings_two(a, i, lines),
                        )
                    )
                )
                == 2
            ):
                x_mas_count += 1
            a = line.find("A", a + 1)
    return x_mas_count


if __name__ == "__main__":
    input = Path("input").read_text("utf-8")
    print(part_one(input))
    print(part_two(input))
[–] [email protected] 1 points 1 month ago

Rust

I'm a day behind on this one due to a lot of work with my job and school.

use std::iter::zip;

use crate::utils::read_lines;

pub fn solution1() {
    let puzzle = read_puzzle();

    let horizontal_sum = puzzle
        .iter()
        .map(|line| {
            line.windows(4)
                .filter(|window| {
                    matches!(window, [b'X', b'M', b'A', b'S'] | [b'S', b'A', b'M', b'X'])
                })
                .count() as u32
        })
        .sum::<u32>();
    let vertical_and_diagonal_sum = puzzle
        .windows(4)
        .map(|window| {
            count_xmas(window, (0, 0, 0, 0))
                + count_xmas(window, (0, 1, 2, 3))
                + count_xmas(window, (3, 2, 1, 0))
        })
        .sum::<u32>();

    println!(
        "XMAS count = {}",
        horizontal_sum + vertical_and_diagonal_sum
    );
}

pub fn solution2() {
    let puzzle = read_puzzle();

    let sum = puzzle
        .windows(3)
        .map(|window| {
            zip(
                window[0].windows(3),
                zip(window[1].windows(3), window[2].windows(3)),
            )
            .map(|(a, (b, c))| (a, b, c))
            .filter(|tuple| {
                matches!(
                    tuple,
                    ([b'M', _, b'M'], [_, b'A', _], [b'S', _, b'S'])
                        | ([b'S', _, b'M'], [_, b'A', _], [b'S', _, b'M'])
                        | ([b'M', _, b'S'], [_, b'A', _], [b'M', _, b'S'])
                        | ([b'S', _, b'S'], [_, b'A', _], [b'M', _, b'M'])
                )
            })
            .count() as u32
        })
        .sum::<u32>();

    println!("X-MAS count = {sum}");
}

fn count_xmas(
    window: &[Vec<u8>],
    (skip0, skip1, skip2, skip3): (usize, usize, usize, usize),
) -> u32 {
    zip(
        window[0].iter().skip(skip0),
        zip(
            window[1].iter().skip(skip1),
            zip(window[2].iter().skip(skip2), window[3].iter().skip(skip3)),
        ),
    )
    .map(|(a, (b, (c, d)))| (a, b, c, d))
    .filter(|tup| matches!(tup, (b'X', b'M', b'A', b'S') | (b'S', b'A', b'M', b'X')))
    .count() as u32
}

fn read_puzzle() -> Vec<Vec<u8>> {
    read_lines("src/day4/input.txt")
        .map(|line| line.into_bytes())
        .collect()
}

The standard library windows method and pattern matching have been carrying me this year so far.

[–] [email protected] 1 points 1 month ago

Smalltalk

I could have done it in 2 fns if I made them more generic, but couldn't be bothered

day4p1: input
    | lines sum w h|
    
    sum := ('XMAS' asRegex matchesIn: input) size. "forward"
    sum := sum + ('SAMX' asRegex matchesIn: input) size. "backwards sep cause overlapping"
    
    lines := input lines.
    h := lines size.
    w := (lines at: 1) size.
    
    1 to: h-3 do: [ :p1 |
        1 to: w do: [ :p2 |
            sum := sum + (self d4diag: lines p1: p1 p2: p2 dir: -1).
            sum := sum + (self d4diag: lines p1: p1 p2: p2 dir: 0).
            sum := sum + (self d4diag: lines p1: p1 p2: p2 dir: 1).    
        ]
    ].
    
    ^ sum.
d4diag: input p1: p1 p2: p2 dir: dir
    | reverse xm ii |
    
    xm := 'XMAS'.
    
    reverse := ((input at: p1) at: p2) = $S.
    
    0 to: 3 do: [ :i |
        ii := reverse ifTrue: [ 4 - i ] ifFalse: [ i + 1 ].
                                                    "if out of bounds, obv not possible"
        ((xm at: ii) = ((input at: p1 + i) at: i * dir + p2 ifAbsent: [^ 0])) ifFalse: [^ 0]
    ].

    ^ 1.

Part 2

day4p2: input
    | lines sum w h pos |
    "Find all diag mas, then check of 1 . -1 (we can look back on every -1"
    
    sum := 0.
    lines := input lines.
    h := lines size.
    w := (lines at: 1) size.
    
    1 to: h-2 do: [ :p1 |
        pos := Array new: w withAll: false.
        1 to: w do: [ :p2 |
            (self d42: lines p1: p1 p2: p2 dir: -1)
                ifTrue: [
                    sum := sum + ((pos at: p2-2) ifTrue:[1] ifFalse:[0]).
                ].
            
            (self d42: lines p1: p1 p2: p2 dir: 1) ifTrue: [pos at: p2 put: true].
        ]
    ].
    
    ^ sum.
d42: input p1: p1 p2: p2 dir: dir
    | reverse xm ii |
    
    xm := 'MAS'.
    
    reverse := ((input at: p1) at: p2) = $S.
    
    0 to: 2 do: [ :i |
        ii := reverse ifTrue: [ 3 - i ] ifFalse: [ i + 1 ].
                                                    "if out of bounds, obv not possible"
        ((xm at: ii) = ((input at: p1 + i) at: i * dir + p2 ifAbsent: [^ false])) ifFalse: [^ false]
    ].

    ^ true.
[–] [email protected] 1 points 1 month ago

Elixir

defmodule AdventOfCode.Solution.Year2024.Day04 do
  use AdventOfCode.Solution.SharedParse

  defmodule Map do
    defstruct [:chars, :width, :height]
  end

  @impl true
  def parse(input) do
    chars = String.split(input, "\n", trim: true) |> Enum.map(&String.codepoints/1)
    %Map{chars: chars, width: length(Enum.at(chars, 0)), height: length(chars)}
  end

  def at(%Map{} = map, x, y) do
    cond do
      x < 0 or x >= map.width or y < 0 or y >= map.height -> ""
      true -> map.chars |> Enum.at(y, []) |> Enum.at(x, "")
    end
  end

  def part1(map) do
    dirs = for dx <- -1..1, dy <- -1..1, {dx, dy} != {0, 0}, do: {dx, dy}
    xmas = String.codepoints("XMAS") |> Enum.with_index() |> Enum.drop(1)

    for x <- 0..(map.width - 1),
        y <- 0..(map.height - 1),
        "X" == at(map, x, y),
        {dx, dy} <- dirs,
        xmas
        |> Enum.all?(fn {c, n} -> at(map, x + dx * n, y + dy * n) == c end),
        reduce: 0 do
      t -> t + 1
    end
  end

  def part2(map) do
    for x <- 0..(map.width - 1),
        y <- 0..(map.height - 1),
        "A" == at(map, x, y),
        (at(map, x - 1, y - 1) <> at(map, x + 1, y + 1)) in ["MS", "SM"],
        (at(map, x - 1, y + 1) <> at(map, x + 1, y - 1)) in ["MS", "SM"],
        reduce: 0 do
      t -> t + 1
    end
  end
end
[–] [email protected] 1 points 1 month ago

Zig

const std = @import("std");
const List = std.ArrayList;

const tokenizeScalar = std.mem.tokenizeScalar;
const parseInt = std.fmt.parseInt;
const print = std.debug.print;
const eql = std.mem.eql;

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();

const Point = struct {
    x: isize,
    y: isize,
    fn add(self: *const Point, point: *const Point) Point {
        return Point{ .x = self.x + point.x, .y = self.y + point.y };
    }
};

// note: i have no idea how to use this or if it's even possible
// const DirectionType = enum(u8) { Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight };
// const Direction = union(DirectionType) {
//     up: Point = .{ .x = 0, .y = 0 },
// };

const AllDirections = [_]Point{
    .{ .x = 0, .y = -1 }, // up
    .{ .x = 0, .y = 1 }, // down
    .{ .x = -1, .y = 0 }, // left
    .{ .x = 1, .y = 0 }, // right
    .{ .x = -1, .y = -1 }, // up left
    .{ .x = 1, .y = -1 }, // up right
    .{ .x = -1, .y = 1 }, // down left
    .{ .x = 1, .y = 1 }, // down right
};

const Answer = struct {
    xmas: u32,
    mas: u32,
};

pub fn searchXmas(letters: List([]const u8), search_char: u8, position: Point, direction: Point) u32 {
    const current_char = getChar(letters, position);
    if (current_char == search_char) {
        const next = position.add(&direction);
        if (current_char == 'M') {
            return searchXmas(letters, 'A', next, direction);
        } else if (current_char == 'A') {
            return searchXmas(letters, 'S', next, direction);
        } else if (current_char == 'S') {
            return 1; // found all letters
        }
    }
    return 0;
}

pub fn countXmas(letters: List([]const u8), starts: List(Point)) u32 {
    var counter: u32 = 0;
    for (starts.items) |start| {
        for (AllDirections) |direction| {
            const next = start.add(&direction);
            counter += searchXmas(letters, 'M', next, direction);
        }
    }
    return counter;
}

pub fn countMas(letters: List([]const u8), starts: List(Point)) u32 {
    var counter: u32 = 0;
    for (starts.items) |start| {
        const a_char = getChar(letters, start) orelse continue;
        const top_left_char = getChar(letters, start.add(&AllDirections[4])) orelse continue;
        const down_right_char = getChar(letters, start.add(&AllDirections[7])) orelse continue;
        const top_right_char = getChar(letters, start.add(&AllDirections[5])) orelse continue;
        const down_left_char = getChar(letters, start.add(&AllDirections[6])) orelse continue;

        const tldr = [3]u8{ top_left_char, a_char, down_right_char };
        const trdl = [3]u8{ top_right_char, a_char, down_left_char };
        if ((eql(u8, &tldr, "MAS") or eql(u8, &tldr, "SAM")) and (eql(u8, &trdl, "MAS") or eql(u8, &trdl, "SAM"))) {
            counter += 1;
        }
    }
    return counter;
}

pub fn getChar(letters: List([]const u8), point: Point) ?u8 {
    if (0 > point.x or point.x >= letters.items.len) {
        return null;
    }
    const row = @as(usize, @intCast(point.x));

    if (0 > point.y or point.y >= letters.items[row].len) {
        return null;
    }
    const col = @as(usize, @intCast(point.y));
    return letters.items[row][col];
}

pub fn solve(input: []const u8) !Answer {
    var rows = tokenizeScalar(u8, input, '\n');

    var letters = List([]const u8).init(alloc);
    defer letters.deinit();
    var x_starts = List(Point).init(alloc);
    defer x_starts.deinit();
    var a_starts = List(Point).init(alloc);
    defer a_starts.deinit();

    var x: usize = 0;
    while (rows.next()) |row| {
        try letters.append(row);
        for (row, 0..) |letter, y| {
            if (letter == 'X') {
                try x_starts.append(.{ .x = @intCast(x), .y = @intCast(y) });
            } else if (letter == 'A') {
                try a_starts.append(.{ .x = @intCast(x), .y = @intCast(y) });
            }
        }
        x += 1;
    }

    // PART 1
    const xmas = countXmas(letters, x_starts);

    // PART 2
    const mas = countMas(letters, a_starts);

    return Answer{ .xmas = xmas, .mas = mas };
}

pub fn main() !void {
    const answer = try solve(@embedFile("input.txt"));
    print("Part 1: {d}\n", .{answer.xmas});
    print("Part 2: {d}\n", .{answer.mas});
}

test "test input" {
    const answer = try solve(@embedFile("test.txt"));
    try std.testing.expectEqual(18, answer.xmas);
}