Learning 25 programming languages in 25 days with Advent of Code

Published:
Updated:

The excitement of waking up every morning with a new puzzle to solve is one of the many reasons I love December. I’ve never actually completed Advent of Code, a daily advent calendar-style programming challenge created by @ericwastl, but I think this is my year.

I’ve made attempts in the past, but usually gave up because it was too hard, or I didn’t have time to go all the way through 25 different challenges. But this year, I figured out a way to make it easier, and I decided to learn 25 new programming languages in the process.

Jump to day:


Day 1: Javascript

My original plan was to complete all of the challenges in Golang first, and then I was going to attempt to learn Rust and do each challenge in Rust. This turned out to be a lot more work than I thought; it took significantly more code to solve the challenges in Golang & Rust and I wanted to have success faster.

I needed to start with a language that I felt comfortable with and had a lot of experience with. Enter our trusty, old friend from long ago: Javascript.

I started the challenge a few days late (December 3rd), so I wanted to lurk on Github and see how others were solving the puzzles. Some would call this cheating; I call it learning. I had an ephiphany when I was saw how simple @colourgarden’s JS solution for Day 1 was:

const path = require("path");
const fs = require("fs");

// Get input data of groups of numbers separated by blank lines.
const data = fs.readFileSync(`./input.txt`, "utf-8");

// Convert the individual lines of each group to Numbers and tally.
const elves = data.split("\n\n").map((group) => {
  return group
    .split("\n")
    .map((group) => Number(group))
    .reduce((total, calories) => total + calories);
});

// Find the biggest number in the resultant array.
const most = Math.max(...elves);

// Get the index of the biggest number.
const who = elves.indexOf(most);

// Print answer to part one.
console.log(
  `Elf ${who} is carrying the most calories. They are carrying ${most} calories.`
);

This code is concise, well-commented, and super easy to understand. I ran it locally with node 01/main.js and got the answer for part 1. Their solution for part 2 was just as simple:

// Sort values by descending.
const sorted = [...elves].sort((a, b) => b - a);

// Get three biggest numbers and tally.
const topThree = sorted
  .slice(0, 3)
  .reduce((total, calories) => total + calories);

// Print answer to part two.
console.log(topThree);

I peeked at their solution for day 2 and did the same thing. He hadn’t started the challenge for day 3 yet but I was determined to solve it. With a little help from Github Copilot, I was able to solve day 3 in just a few minutes.

I had my new plan: each day, complete all of the challenges in Javascript first, then try to complete the challenge in a new language I’m not familiar with.

Solution in JS


Day 2: Rust

Whenever I learn a new language, I immediately visit https://learnxinyminutes.com. I love this site because it gives you a quick overview of the syntax of a language, with tons of comments and example code. I skimmed the Rust page there and also discovered this useful, step-by-step resource for getting started with Rust.

One of the challenges of learning all these new languages is that I’ll have to install the build tools locally and get familiar with the developer experience.

Luckily, installing Rust & Cargo (Rust’s package manager) is a one-liner:

curl https://sh.rustup.rs -sSf | sh

When I initially attempted the challenge in Rust, I set out to find some solutions from skilled Rust developers on Github. Luckily, I found this awesome repo with AOC solutions in many languages (wow, someone did it in Excel). I jumped to the Rust section and tried to find a good example.

I discovered ZAZPRO’s AOC repo and noticed a 00 folder with a Hello World script. I cloned the repo, and ran it locally with cargo run --bin part_one:

➜ cargo run --bin part_one
   Compiling rust v0.1.0 (/Users/mager/Code/adventofcode-2022/00/rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.90s
     Running `target/debug/part_one`
Hello, world!

Easy enough. Next, I wanted to copy their solution locally and try to run it. In my 02 folder, I ran cargo new rust which created a few new files:

├── Cargo.toml
└── src
    └── main.rs

I followed @ZAZPRO’s lead and created a bin folder in src and added the code to part_one.rs and ran it locally:

➜ cargo run --bin part_one
   Compiling rust v0.1.0 (/Users/mager/Code/aoc/2022/02/rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/part_one`
Final Score: 8392

It works! Now, let’s check out the solution for part 1 in Rust:

use std::error::Error;

// Enum that describes match result.
enum Outcome {
    Win,
    Draw,
    Lose,
}

// Trait for outcome to calculate result score.
impl Outcome {
    pub fn get_score(&self) -> u32 {
        match &self {
            Outcome::Win => 6,
            Outcome::Draw => 3,
            Outcome::Lose => 0,
        }
    }
}

// Possible Items that players can throw.
enum Item {
    Rock,
    Paper,
    Scissors,
}

// Value of each Item.
impl Item {
    pub fn get_score(&self) -> u32 {
        match &self {
            Item::Rock => 1,
            Item::Paper => 2,
            Item::Scissors => 3,
        }
    }
}

// Struct that holds each game Round.
struct Round {
    opponent: Item,
    player: Item,
}

impl Round {
    // Common Rust way to create structs.
    pub fn new(opponent: Item, player: Item) -> Self {
        Self { opponent, player }
    }

    // Calculate outcome of the Round.
    pub fn get_outcome(&self) -> Outcome {
        match (&self.player, &self.opponent) {
            (Item::Rock, Item::Scissors) => Outcome::Win,
            (Item::Rock, Item::Paper) => Outcome::Lose,
            (Item::Paper, Item::Rock) => Outcome::Win,
            (Item::Paper, Item::Scissors) => Outcome::Lose,
            (Item::Scissors, Item::Paper) => Outcome::Win,
            (Item::Scissors, Item::Rock) => Outcome::Lose,
            _ => Outcome::Draw,
        }
    }

    // Calculate score of the round.
    pub fn calc_score(&self) -> u32 {
        &self.player.get_score() + &self.get_outcome().get_score()
    }
}

// Program entry point.
fn main() -> Result<(), Box<dyn Error>> {
    // List of game rounds.
    let mut rounds: Vec<Round> = Vec::new();

    // Read input file into String.
    let file = std::fs::read_to_string("../input.txt")?;
    // For each line in the file.
    for line in file.lines() {
        // Split a line in two values.
        let mut iter = line.split_whitespace();

        // First value is what current opponent is throwing.
        let current_opponent = match iter.next() {
            Some(v) => match v {
                "A" => Item::Rock,
                "B" => Item::Paper,
                "C" => Item::Scissors,
                _ => todo!(),
            },
            None => todo!(),
        };

        // Second value is what current player is throwing.
        let current_player = match iter.next() {
            Some(v) => match v {
                "X" => Item::Rock,
                "Y" => Item::Paper,
                "Z" => Item::Scissors,
                _ => todo!(),
            },
            None => todo!(),
        };

        // Create a new Round struct with opponent and player values.
        rounds.push(Round::new(current_opponent, current_player));
    }

    // Calculate a final sum by calling a score calculation method of Round struct and summing those up.
    let final_sum: u32 = rounds.into_iter().map(|r| r.calc_score()).sum();
    println!("Final Score: {final_sum}");

    Ok(())
}

It’s a lot of code, but it makes sense. At first glance it reminds me of Ruby or Elixir, but more elegant. I’d like to play around more with Rust, but there’s no time; I need to learn another new language.

Solution in JS


Day 3: Crystal

I heard about this one briefly on Hacker News a while back and always wanted to use it. While brew install crystal ran, I determined that Crystal is like a mix between Ruby and C. From their Github README:

We love Ruby’s efficiency for writing code. We love C’s efficiency for running code. We want the best of both worlds.

Let’s take a look at the solution for day 1 before we try to run it:

puts STDIN.each_line.reduce(0) { |acc, line|
  s = line.size() // 2
  a,b = line[0..s-1],line[s..]
  common = (a.chars.to_set & b.chars.to_set).to_a[0]
  priority = common.ord - (common.uppercase? ? 38 : 'a'.ord - 1)
  acc + priority
}

I love how simple this is, way less code than my JS implementation. Shoutout to @DestyNova on the solution & detailed blog post about the experience (and how he used ChatGPT to find a solution in Python).

Getting this running locally was pretty easy as well:

➜  crystal git:(main) ✗ cat ../input.txt | crystal run part1.cr
... Lots of "ld: warning: object file (somefile.o) was built for newer macOS version than being linked" warnings ...
7785

Here’s part 2:

puts STDIN.each_line.to_a.in_groups_of(3, "").reduce(0) { |acc,lines|
  common = lines.map {|s| s.chars.to_set}.reduce {|a,s| a & s}.to_a[0]
  priority = common.ord - (common.uppercase? ? 38 : 'a'.ord - 1)
  acc + priority
}

Crystal looks fun and I want to spend more time with it. Fun fact: The last time I worked on anything that resembled Ruby was one of my first hack day projects (almost 10 years ago!): Sendwiki.

Solution in JS


Day 4: Elm

I’ve seen Elm before on Hacker News, but never double clicked further. A quick glance at Learn X, and the syntax looks… pretty? No brackets, pipe functions, indenting matters, support for pattern matching. I approve. It only took a few seconds to install, and they even suggested a tutorial to get started.

Let’s check out @ostcar’s solution:

module Days.Day4 exposing (solution, testSolution1, testSolution2)

import Expect exposing (equal)
import Parser exposing ((|.), (|=), int, symbol)
import Test exposing (Test, test)


solution : ( () -> String, () -> String )
solution =
    ( \_ -> solution1 puzzleInput
    , \_ -> solution2 puzzleInput
    )


solution1 : String -> String
solution1 =
    String.lines
        >> List.filter (String.isEmpty >> not)
        >> List.map parseLine
        >> countFunc isFullIntersect
        >> String.fromInt


parseLine : String -> ( ( Int, Int ), ( Int, Int ) )
parseLine input =
    let
        inputParser =
            Parser.succeed (\a b c d -> ( ( a, b ), ( c, d ) ))
                |= int
                |. symbol "-"
                |= int
                |. symbol ","
                |= int
                |. symbol "-"
                |= int
    in
    Parser.run inputParser input
        |> Result.withDefault ( ( 0, 0 ), ( 0, 0 ) )


isFullIntersect : ( ( Int, Int ), ( Int, Int ) ) -> Bool
isFullIntersect ( first, second ) =
    let
        firstIsFullIntersect ( a, b ) ( c, d ) =
            a >= c && b <= d
    in
    firstIsFullIntersect first second || firstIsFullIntersect second first


countFunc : (a -> Bool) -> List a -> Int
countFunc func list =
    List.foldl
        (\e count ->
            if func e then
                count + 1

            else
                count
        )
        0
        list


testSolution1 : Test
testSolution1 =
    test "test input1" <|
        \_ -> equal (solution1 testInput) "2"


solution2 : String -> String
solution2 =
    String.lines
        >> List.filter (String.isEmpty >> not)
        >> List.map parseLine
        >> countFunc isPartlyIntersect
        >> String.fromInt


isPartlyIntersect : ( ( Int, Int ), ( Int, Int ) ) -> Bool
isPartlyIntersect ( first, second ) =
    let
        firstInSecond ( a, b ) ( c, d ) =
            a <= c && b >= c
    in
    firstInSecond first second || firstInSecond second first


testSolution2 : Test
testSolution2 =
    test "test input2" <|
        \_ -> equal (solution2 testInput) "4"


testInput : String
testInput =
    """
2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8"""


puzzleInput : String
puzzleInput =
    """
75-76,18-75
2-54,1-50
82-83,78-82
...
"""

Definitely some weird syntax going on in there, but the code is readable and it makes sense. I was surpsied you could import these parser operators like |. and |= (great docs on the Parser by the way).

I was really curious to get this up and running, and it turns out that @ostcar built a website along with the code.

➜  elm make src/Main.elm
Starting downloads...
Dependencies ready!
Success! Compiled 8 modules.

    Main ───> index.html

The web page shows a list of days and a link to their solution. Here’s the source if you’re interested.

Elm looks pretty powerful. I also starred this repo from @rtfeldman for later to learn how to build a single page application using Elm.

Solution in JS


Day 5: Java

While I haven’t actually built anything myself in Java, I’ve been familiar with it for a while and been exposed to it in code reviews and interviews a bunch. I hope to play with Kotlin before Xmas.

I found an elegant solution (with tests!) from @vuryss:

package com.vuryss.aoc.solutions.event2022;

import com.vuryss.aoc.DayInterface;

import java.util.*;

public class Day5 implements DayInterface {
    @Override
    public Map<String, String> part1Tests() {
        return Map.of(
            """
                [D]
            [N] [C]
            [Z] [M] [P]
             1   2   3

            move 1 from 2 to 1
            move 3 from 1 to 3
            move 2 from 2 to 1
            move 1 from 1 to 2
            """,
            "CMZ"
        );
    }

    @Override
    public Map<String, String> part2Tests() {
        return Map.of(
            """
                [D]
            [N] [C]
            [Z] [M] [P]
             1   2   3

            move 1 from 2 to 1
            move 3 from 1 to 3
            move 2 from 2 to 1
            move 1 from 1 to 2
            """,
            "MCD"
        );
    }

    @Override
    public String part1Solution(String input) {
        var parts = input.split("\n\n");
        var stacks = createCurrentState(parts[0]);

        for (var line: parts[1].split("\n")) {
            var moveParts = line.trim().split(" ");
            var originStack = stacks.get(Integer.parseInt(moveParts[3]));
            var targetStack = stacks.get(Integer.parseInt(moveParts[5]));

            for (var i = 0; i < Integer.parseInt(moveParts[1]); i++) {
                targetStack.addFirst(originStack.pollFirst());
            }
        }

        return getTopCrates(stacks);
    }

    @Override
    public String part2Solution(String input) {
        var parts = input.split("\n\n");
        var stacks = createCurrentState(parts[0]);

        for (var line: parts[1].split("\n")) {
            var moveParts = line.trim().split(" ");
            var originStack = stacks.get(Integer.parseInt(moveParts[3]));
            var targetStack = stacks.get(Integer.parseInt(moveParts[5]));
            var tempList = new LinkedList<Character>();

            for (var i = 0; i < Integer.parseInt(moveParts[1]); i++) {
                tempList.add(originStack.pollFirst());
            }

            while (!tempList.isEmpty()) {
                targetStack.addFirst(tempList.pollLast());
            }
        }

        return getTopCrates(stacks);
    }

    private HashMap<Integer, LinkedList<Character>> createCurrentState(String state) {
        var stacks = new HashMap<Integer, LinkedList<Character>>();

        for (var line: state.split("\n")) {
            var charIndex = 1;
            var stackId = 1;

            while (line.length() > charIndex) {
                var c = line.charAt(charIndex);
                var stack = stacks.getOrDefault(stackId, new LinkedList<>());
                stacks.putIfAbsent(stackId, stack);

                if (c >= 'A' && c <= 'Z') {
                    stack.add(c);
                }

                charIndex += 4;
                stackId++;
            }
        }

        return stacks;
    }

    private String getTopCrates(HashMap<Integer, LinkedList<Character>> stacks) {
        StringBuilder result = new StringBuilder();

        for (var entry: stacks.entrySet()) {
            result.append(entry.getValue().peek());
        }

        return result.toString();
    }
}

This puzzle was trickier because you had to do more work to parse the input:

    [D]
[N] [C]
[Z] [M] [P]
 1   2   3

We have to strip out the spaces and [] characters and rotate it so we can work with the stacks better. We need something like: [[Z, N], [M, C, D], [P]].

In Javascript, we can use a helper transpose function and pipe some map and filter functions:

Array.prototype.transpose = function () {
  return this[0].map((_, i) => this.map((y) => y[i]));
};

const stacks = input_stacks
  .map((row) => [...row])
  .transpose()
  .map((row) => row.join("").replace(/\[|\]/g, "").trim())
  .filter((row) => row !== "")
  .map((row) => [...row.slice(0, -1)].reverse());

In Java, we iterate through each character and check if it’s a letter and add it to a LinkedList:

private HashMap<Integer, LinkedList<Character>> createCurrentState(String state) {
    var stacks = new HashMap<Integer, LinkedList<Character>>();

    for (var line: state.split("\n")) {
        var charIndex = 1;
        var stackId = 1;

        while (line.length() > charIndex) {
            var c = line.charAt(charIndex);
            var stack = stacks.getOrDefault(stackId, new LinkedList<>());
            stacks.putIfAbsent(stackId, stack);

            if (c >= 'A' && c <= 'Z') {
                stack.add(c);
            }

            charIndex += 4;
            stackId++;
        }
    }

    return stacks;
}

The docs for HashMap are detailed, and function names like getOrDefault and putIfAbsent are clear and intuitive.

Let’s see how easy it is to get running locally. One reason I chose @vuryss’s solution is because his README had instructions on how to run it and included a straightforward command for running a specific day’s solution: ./gradlew bootRun --args="--day=1".

It’s as easy as running brew install java to get Java installed these days (previously you had to download an executable and install the JVM separately). While I waited, I asked ChatGPT why Java is so popular:

Java is a popular programming language because it is versatile and easy to learn. It is widely used for building a variety of applications, including web, mobile, and desktop applications. Additionally, Java has a large and active community of developers, which makes it easy to find support and assistance when needed. Because of its popularity, there is a wealth of documentation and learning resources available, making it a good choice for people who are new to programming.

So, I actually had trouble running this locally, but I’ll circle back later and try again when I have more time.

I’m a few days behind so, on to the next one!

Solution in JS


Day 6: Excel

That’s right, you didn’t misread. We’re going to solve this one with Excel. While Microsoft’s spreadsheet app isn’t technically considered a programming language, it’s been used for decades for manipulating data, and its table-first, dependable interface seems like a perfect tool for this challenge. In fact, the underlying programming language that is used in that little input box above the table is VBA, which is a dialect of Visual Basic.

Luckily someone was brave enough to try these challenges in Excel (shoutout @askholme). I downloaded the xlsx file and uploaded it to Google Drive.

AOC Day 6 using spreadsheets

In cell A1, we have the 4000 plus-character string. And then in each row below, we grab a 14-character chunk using the MID function which lets you select a substring given a starting point and character length. Columns C-P is a single character, and then in column Q, we’re using a combination of COUNTA and UNIQUE to determine if the first four characters are all unique. So the first row looks like this:

=COUNTA(UNIQUE(C4:F4,1,TRUE))=4

Then you can filter for TRUE and the first row returned is the answer.

AOC Day 6 using spreadsheets

Column R is the same thing for part 2 (14 characters instead of 4). Pretty clever, I must say. I’m going to lurk on this repo to see how other days are solved.

Solution in JS


Day 7: Python

The challenges are getting tougher. Today, we are told that our computer is out of space, and are given a list of input that shows someone navigating around directories running ls to see which directories have large files in them:

$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
...

We have to determine the directory structure from the above input, and then sum up the largest files to determine which ones to delete.

After spending a few minutes grokking the excellent solution from @colourgarden, I decided to try check the public leaderboard to see how the all-stars are doing it.

I picked a random user in the top 10 and it was @jonathanpaulson, and the solution was written in Python; a language I’m very familiar with from my days of hacking Django at Postmates.

#!/usr/bin/python3
import sys
from collections import defaultdict
infile = sys.argv[1] if len(sys.argv)>1 else '7.in'
data = open(infile).read().strip()
lines = [x for x in data.split('\n')]

# directory path -> total size of that directory (including subdirectories)
SZ = defaultdict(int)
path = []
for line in lines:
    words = line.strip().split()
    if words[1] == 'cd':
        if words[2] == '..':
            path.pop()
        else:
            path.append(words[2])
    elif words[1] == 'ls':
        continue
    elif words[0] == 'dir':
        continue
    else:
        sz = int(words[0])
        # Add this file's size to the current directory size *and* the size of all parents
        for i in range(1, len(path)+1):
            SZ['/'.join(path[:i])] += sz

max_used = 70000000 - 30000000
total_used = SZ['/']
need_to_free = total_used - max_used

p1 = 0
p2 = 1e9
for k,v in SZ.items():
    #print(k,v)
    if v <= 100000:
        p1 += v
    if v>=need_to_free:
        p2 = min(p2, v)
print(p1)
print(p2)

This is pretty clean; it uses a defaultdict to keep track of the size of each directory (key = dir name, value = size).

defaultdict(<class 'int'>, {
    '/': 44376732,
    '//lhrfs': 240152,
    '//nwh': 834387,
    '//nwh/pmdj': 514336,
    '//pjsd': 22040913,
    '//pjsd/czzcslm': 266197,
    '//pjsd/dgwpl': 492311,
    '//pjsd/dgwpl/ljzrwpv': 468764,
    // ...
})

Then you can just loop through and tally up the total size of each directory, and then find the largest files that needs to be deleted.

I’m definitely going to check back in on how @jonathanpaulson is doing throughout the month.

Solution in JS


Day 8: Deno

Today’s puzzle input is a huge grid of numbers that represents tree heights in a forest. And we have to determine which trees are visibile when looking from any angle (part 1), and then we have to give each tree a “scenic score” to find the tree with area with the best view for building a treehouse (part 2).

Tree grid screenshot

Deno is a Javascript runtime environment, similar to Node.js, but there are a few key differences:

I was really impressed with the Deno getting started guide, which walks you from Hello, World to creating a web server that returns an API response from Github.

Let’s checkout the incredible solution for Day 8 from @ismtabo:

function preprocess(text: string) {
  return text
    .trim()
    .split("\n")
    .map((line) => Array.from(line.trim()).map((char) => parseInt(char)));
}

function partOne(input: number[][]) {
  return input.flat().filter((height, position) => {
    const x = Math.floor(position % input[0].length);
    const y = Math.floor(position / input[0].length);
    return (
      input
        .at(y)!
        .slice(0, x)
        .every((other) => other < height) ||
      input
        .at(y)!
        .slice(x + 1)
        .every((other) => other < height) ||
      input.slice(0, y).every((row) => row.at(x)! < height) ||
      input.slice(y + 1).every((row) => row.at(x)! < height)
    );
  }).length;
}

function lookUp(y: number, input: number[][], x: number, height: number) {
  if (y === 0) {
    return 1;
  }
  let closesTaller = input
    .slice(0, y)
    .reverse()
    .findIndex((row) => row.at(x)! >= height);
  if (closesTaller === -1) {
    closesTaller = y - 1;
  }
  return closesTaller + 1;
}

function lookLeft(x: number, y: number, input: number[][], height: number) {
  if (x === 0) {
    return 1;
  }
  let closestTaller = input
    .at(y)!
    .slice(0, x)
    .reverse()
    .findIndex((other) => other >= height);
  if (closestTaller === -1) {
    closestTaller = x - 1;
  }
  return closestTaller + 1;
}

function lookRight(x: number, input: number[][], y: number, height: number) {
  if (x === input[0].length - 1) {
    return 1;
  }
  let closestTaller = input
    .at(y)!
    .slice(x + 1)
    .findIndex((other) => other >= height);
  if (closestTaller === -1) {
    closestTaller = input[0].length - x - 2;
  }
  return closestTaller + 1;
}

function lookDown(y: number, input: number[][], x: number, height: number) {
  if (y === input.length - 1) {
    return 1;
  }
  let closesTaller = input
    .slice(y + 1)
    .findIndex((row) => row.at(x)! >= height);
  if (closesTaller === -1) {
    closesTaller = input.length - y - 2;
  }
  return closesTaller + 1;
}

function partTwo(input: number[][]) {
  const values = input.flat().map((height, position) => {
    const x = Math.floor(position % input[0].length);
    const y = Math.floor(position / input[0].length);
    if (
      x === 0 ||
      x === input[0].length - 1 ||
      y === 0 ||
      y === input.length - 1
    ) {
      return -Infinity;
    }
    const up = lookUp(y, input, x, height);
    const left = lookLeft(x, y, input, height);
    const right = lookRight(x, input, y, height);
    const down = lookDown(y, input, x, height);
    const visibility = left * right * up * down;
    return visibility;
  });
  return Math.max(...values);
}

export function main(text: string) {
  const input = preprocess(text);

  console.log(partOne(input));
  console.log(partTwo(input));
}

The docs from the README were great, and the author created a very nice (and modular) cli for running the code:

➜ deno run -A --unstable src/cli/mod.ts run -d 8 -p 1
1798
➜ deno run -A --unstable src/cli/mod.ts run -d 8 -p 2
259308

Additionally, there was an amazing 8-bit style website that accompanied the code, which let you paste your input and validate the expected result.

Deno 8-bit website

Deno 8-bit website

Here’s another useful resource for Deno examples if you’re interested in learning more.

Let’s keep moving…

Solution in JS


Day 9: jq

The puzzle today is all about tracing a path on a grid, and the input is a list of instructions like: L 4, U 2, R 1, D 3, which means “go left 4, up 2, right 1, down 3”. In part 1, we need to follow the tail and count how many cells it’s been to. In part 2, the tail is 10 units long, so it will touch more cells.

jq is a usually meant to process JSON but it can handle any kind of input. Let’s check out the solutions from @christianberg.

Part 1:

#!/usr/bin/env jq -rRsf
split("\n")[:-1] |
map(split(" ") | .[0]*(.[1]|tonumber)) |
join("") | split("") |
map(
  {"R": [1,0], "L": [-1,0], "U": [0,1], "D": [0,-1]}[.]
) |
[foreach .[] as $step ([0,0]; [.[0]+$step[0],.[1]+$step[1]]; .)] |
[foreach .[] as $head (
  [0,0];
  if (.[0]-$head[0])>1 or (.[0]-$head[0])<-1 or (.[1]-$head[1])>1 or (.[1]-$head[1])<-1 then
    .[0] |= . + ([([($head[0]-.),1] | min),-1] | max) |
    .[1] |= . + ([([($head[1]-.),1] | min),-1] | max)
  else
    .
  end;
  .
)] |
unique | length

Part 2:

#!/usr/bin/env jq -rRsf
split("\n")[:-1] |
map(split(" ") | .[0]*(.[1]|tonumber)) |
join("") | split("") |
map(
  {"R": [1,0], "L": [-1,0], "U": [0,1], "D": [0,-1]}[.]
) |
[foreach .[] as $step ([0,0]; [.[0]+$step[0],.[1]+$step[1]]; .)] |
reduce range(9) as $_ (.;
[foreach .[] as $head (
  [0,0];
  if (.[0]-$head[0])>1 or (.[0]-$head[0])<-1 or (.[1]-$head[1])>1 or (.[1]-$head[1])<-1 then
    .[0] |= . + ([([($head[0]-.),1] | min),-1] | max) |
    .[1] |= . + ([([($head[1]-.),1] | min),-1] | max)
  else
    .
  end;
  .
)]) |
unique | length

You can run these but using cat and piping the input to the script:

➜  cat input.txt | part1.jq
6366
➜  cat input.txt | part2.jq
2601

If you want to understand it line by line, you can step through each line the command line:

➜ cat input.txt | jq -rRs 'split("\n")[:-1] | map(split(" ") | .[0]*(.[1]|tonumber)) | join("") | split("")'
[
  "L",
  "U",
  "U",
  "L",
  "U",
  "U",
  "L",
  "L",
  "R",
  "L",
  "U",
  ...
]

Another tool for visualizing jq results is jqplay.org:

Day 9 debugging with jqplay.org

Solution in JS


Day 10: Clojure

Clojure is a dialect of Lisp built on the JVM. A quick glance at Learn X in Y Minutes and you’ll see how simple and minimal the language is.

Sure enough, someone has a similarly minimal solution for today’s puzzle, which requires us to construct a cathode ray tube (CRT) from a list of instructions.

I found an awesome solution from @coutego:


;; Day 10
(defn exec [state [op arg]]
  (let [[[_ _ x] cycle] (or (last state) [[1 1 1] 0])]
    (cond
      (= op "noop") (conj state [[x x x] (inc cycle) "noop"])
      (= op "addx") (-> state
                        (conj [[x x x] (inc cycle)])
                        (conj [[x x (+ x arg)] (+ 2 cycle)])))))

(defn sum-idxs [sts]
  (let [f (fn [sts id] (* id (->> id (nth sts) first second)))]
    (+ (f sts 20)
       (f sts 60)
       (f sts 100)
       (f sts 140)
       (f sts 180)
       (f sts 220))))

(defn sprite-in-draw-position? [idx cycle]
  (let [col (mod idx 40)
        pos (-> cycle first second)]
    (< (abs (- pos col)) 2)))

(defn parse-d10 [s]
  (let [[op arg] (st/split s #" ")]
    [op (and arg (parse-int arg))]))

(defn d10p1 [& [filename]]
  (->> (read-input-day (or filename "d10") parse-d10)
       (reduce exec [[[1 1 1] 0]])
       sum-idxs))

(defn d10p2 [& [filename]]
  (->> (read-input-day (or filename "d10") parse-d10)
       (reduce exec [[[1 1 1] 0]])
       (drop 1)
       (map-indexed sprite-in-draw-position?)
       (map (fn [b] (if b "#" " ")))
       (partition 40)
       (map (fn [ss] (apply str ss)))))

(deftest d10
  (is (= (d10p1 "d10-test") 13140))
  (is (= (d10p1) 17380))
  (is (= (-> (d10p2) first (nth 5)) \space))
  (is (= (-> (d10p2) second (nth 2)) \space))
  (is (= (-> (d10p2) second (nth 5)) \#)))

I checked out the docs for map-indexed and partition and was impressed that the community can share examples in the official docs. These were really helpful!

After a few tries, I was able to use lein to run tests locally in @coutego’s repo.

Solution in JS


Day 11: Dart

The last time I worked with Dart was Stephen Grider’s amazing Dart and Flutter course on Udemy. (I also really love the official Flutter channel on YouTube).

Let’s check out a solution from @hieptk:

import 'dart:collection';
import 'dart:io';

final bool part1 = false;
final int N_ROUNDS = part1 ? 20 : 10000;
final int mod = 9699690;

class Monkey {
  final Queue<int> items;
  final String op;
  final int opVal;
  final int testVal;
  final int trueThrow, falseThrow;

  Monkey(this.items, this.op, this.opVal, this.testVal, this.trueThrow,
      this.falseThrow);

  void doMonkeyThing(List<Monkey> monkeys) {
    while (items.isNotEmpty) {
      int x = items.removeFirst();
      if (op == '+') {
        x = (x + opVal) % mod;
      } else if (op == '*') {
        x = (x * opVal) % mod;
      } else {
        x = (x * x) % mod;
      }
      if (part1) {
        x ~/= 3;
      }
      if (x % testVal == 0) {
        monkeys[trueThrow].items.addLast(x);
      } else {
        monkeys[falseThrow].items.addLast(x);
      }
    }
  }
}

void main(List<String> arguments) {
  File file = File('input.txt');
  List<String> lines = file.readAsLinesSync();
  List<Monkey> monkeys = [];
  for (int i = 0; i + 5 < lines.length; i += 7) {
    Queue<int> items = Queue.from(lines[i + 1]
        .trimLeft()
        .split(' ')
        .sublist(2)
        .map((e) => int.parse(e.replaceAll(',', ''))));
    List<String> tmp = lines[i + 2].trimLeft().split(' ');
    String op = '';
    int opVal = 1;
    if (tmp.last != 'old') {
      op = tmp[4];
      opVal = int.parse(tmp.last);
    } else {
      // old * old
      op = '^';
      opVal = 2;
    }
    int testVal = int.parse(lines[i + 3].split(' ').last);
    int trueThrow = int.parse(lines[i + 4].split(' ').last);
    int falseThrow = int.parse(lines[i + 5].split(' ').last);
    monkeys.add(Monkey(items, op, opVal, testVal, trueThrow, falseThrow));
  }

  List<int> cnt = List.filled(monkeys.length, 0);
  for (int i = 1; i <= N_ROUNDS; ++i) {
    for (int j = 0; j < monkeys.length; ++j) {
      cnt[j] += monkeys[j].items.length;
      monkeys[j].doMonkeyThing(monkeys);
    }
  }

  print(cnt);
  cnt.sort();
  print(cnt.last * cnt[cnt.length - 2]);
}

Part 1 Solution in JS | Part 2 Solution in JS


Day 12: Smalltalk

I’ve heard of Smalltalk, but I haven’t really been exposed to it much. ChatGPT gives a pretty good summary of what Smalltalk is all about:

Smalltalk is an object-oriented, dynamically typed programming language that was developed in the 1970s. It was one of the first programming languages to use the concept of “objects” to represent data and methods, and it was also one of the first to use just-in-time compilation. Smalltalk is known for its simplicity and its use of the “programming by messaging” paradigm, which allows objects to communicate with each other by sending and receiving messages. Despite its innovations, Smalltalk has not been widely adopted and is not commonly used today.

When I first read the code, I was impressed how readable it is. One thing I immediately notice: lines end with a period, kinda like a sentence. And comments are surrounded by double quotes. Check out the Learn X in Y Minutes page for more.

Shoutout to @musifter on Reddit for this solution:

#!/usr/local/bin/gst -q

Collection extend [
    apply: method  [ ^self collect: [:x | x perform: method] ]
]

Object subclass: HeightMap [
    | grid start end |

    dirs := { 0 @ 1. 0 @ -1. 1 @ 0. -1 @ 0 }.

    HeightMap class >> new: arrayStrings [
        ^super new init: arrayStrings
    ]

    init: mapArray [
        | width |
        width := (mapArray at: 1) size + 2.

        grid := OrderedCollection new.
        mapArray keysAndValuesDo: [ :y :line |
            | row x |
            row := line asOrderedCollection apply: #asInteger.
            row addFirst: 128; addLast: 128.    " Add sentinels left/right "

            " Find start "
            (x := row indexOf: $S asInteger) ~= 0 ifTrue: [
                start := x @ (y + 1).
                row at: x put: $a asInteger.
            ].

            " Find end "
            (x := row indexOf: $E asInteger) ~= 0 ifTrue: [
                end := x @ (y + 1).
                row at: x put: $z asInteger.
            ].

            grid add: row asArray.
        ].

        " Add sentinel rows to top and bottom "
        grid addFirst: (Array new: width withAll: 128).
        grid addLast:  (Array new: width withAll: 128).
        ^self
    ]

    at: pt  [ ^(grid at: pt y) at: pt x ]

    start   [ ^start ]
    end     [ ^end   ]

    " Find path from startPos until endBlock is true, with heightAllow rule "
    pathFrom: startPos endWhen: endBlock moving: heightAllow [
        | visit queue state time pos height move |

        " Initialise visit array "
        visit := (1 to: grid size) collect: [ :i |
            Array new: (grid at: 1) size withAll: false
        ].

        " Queue starts with startPos at time 0 "
        queue := OrderedCollection with: { 0. startPos }.

        " Basic BFS loop: "
        [ queue notEmpty ] whileTrue: [
            state := queue removeFirst.
            time  := state first.
            pos   := state second.

            (endBlock value: pos) ifTrue: [ ^time ].

            ((visit at: pos y) at: pos x) ifFalse: [
                (visit at: pos y) at: pos x put: true.

                height := self at: pos.
                dirs do: [ :dir |
                    move := pos + dir.
                    (heightAllow value: height value: (self at: move)) ifTrue: [
                        queue addLast: { time + 1. move }.
                    ]
                ]
            ]
        ]
    ]
]

"
| Mainline
"
map := HeightMap new: stdin lines contents.

" Start to end, going up: "
part1 := map pathFrom: map start
              endWhen: [:pos | pos = map end]
               moving: [:height :targ | (height + 1) >= targ].

('Part 1: %1' % {part1}) displayNl.

" End to level $a, going down: "
part2 := map pathFrom: map end
              endWhen: [:pos | (map at: pos) = $a asInteger]
               moving: [:height :targ | targ between: (height - 1) and: $z asInteger].

('Part 2: %1' % {part2}) displayNl.

I didn’t actually get this up and running locally. It’s not straightforward to setup on a Mac, but I might try it on a Linux machine soon.

Part 1 Solution in JS | Part 2 Solution in JS


Day 13: nim

I went straight to Learn X in Y Minutes for this one:

Nim (formerly Nimrod) is a statically typed, imperative programming language that gives the programmer power without compromises on runtime efficiency. Nim is efficient, expressive, and elegant.

The syntax is very Pythonesque (colons and indents) and looks easy to use. Let’s check out a solution from @MichalMarsalek:

include aoc
import json

day 13:
    func cmp(a, b: JsonNode): int =
        if a.kind == JInt and b.kind == JInt:
            return cmp(a.getInt, b.getInt)
        let aChildren = if a.kind == JArray: a.elems else: @[a]
        let bChildren = if b.kind == JArray: b.elems else: @[b]

        for (a, b) in zip(aChildren, bChildren):
            let c = cmp(a, b)
            if c != 0: return c
        return cmp(aChildren.len, bChildren.len)

    part 1:
        let pairs = input.split("\n\n").mapIt(it.splitLines.mapIt it.parseJson).mapIt (it[0], it[1])
        sum(
            for i, (a, b) in pairs:
                if cmp(a, b) < 0:
                    i+1
        )

    part 2:
        var data = (lines & "[[2]]" & "[[6]]").filterIt(it != "").mapIt it.parseJson
        sort(data, cmp)
        prod:
            collect:
                for i, x in data:
                    if $x in ["[[2]]", "[[6]]"]:
                        i+1

Really clean! The docs are a joy to read as well. I’ll definitely be trying this out another time.

TODO: Run locally.

Part 1 Solution in JS | Part 2 Solution in JS


Day 14: C#

Today’s challenge is tough: you have to calculate sand falling through rock crevices.

I have never used C, C++, or C# but I’ve always wanted to try it out. I was curious how C# is different than other C variants, so I asked our favorite AI of the moment. The main differences:

Let’s check out the solution from @ShootMe:

using AdventOfCode.Common;
using AdventOfCode.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace AdventOfCode.Y2022 {
    [Description("Regolith Reservoir")]
    public class Puzzle14 : ASolver {
        private BlockType[] cave;
        private int width, height, minX, minY;

        public override void Setup() {
            string[] lines = Input.Split('\n');
            List<Line> rockFormations = new();

            minX = int.MaxValue;
            int maxX = int.MinValue;
            minY = 0;
            int maxY = int.MinValue;
            for (int i = 0; i < lines.Length; i++) {
                string line = lines[i];
                string[] points = line.Split(" -> ");

                Line formation = null;
                for (int j = 0; j < points.Length; j++) {
                    string[] point = points[j].Split(',');
                    if (formation == null) {
                        formation = new Line();
                        formation.StartPosition = new Point() { X = point[0].ToInt(), Y = point[1].ToInt() };
                    } else {
                        formation.EndPosition = new Point() { X = point[0].ToInt(), Y = point[1].ToInt() };
                        rockFormations.Add(formation);
                        Line nextFormation = new Line();
                        nextFormation.StartPosition = formation.EndPosition;
                        formation = nextFormation;
                    }

                    if (formation.StartPosition.X < minX) { minX = formation.StartPosition.X; }
                    if (formation.StartPosition.X > maxX) { maxX = formation.StartPosition.X; }
                    if (formation.StartPosition.Y < minY) { minY = formation.StartPosition.Y; }
                    if (formation.StartPosition.Y > maxY) { maxY = formation.StartPosition.Y; }
                }
            }

            minX = 500 - minX < maxY + 2 - minY ? 500 - (maxY + 3 - minY) : minX;
            maxX = maxX - 500 < maxY + 2 - minY ? 500 + maxY + 3 - minY : maxX;
            maxY += 2;

            width = maxX - minX + 1;
            height = maxY - minY + 1;
            cave = new BlockType[width * height];

            for (int i = 0; i < rockFormations.Count; i++) {
                Line formation = rockFormations[i];
                if (formation.StartPosition.X > formation.EndPosition.X) {
                    Point temp = formation.EndPosition;
                    formation.EndPosition = formation.StartPosition;
                    formation.StartPosition = temp;
                } else if (formation.StartPosition.Y > formation.EndPosition.Y) {
                    Point temp = formation.EndPosition;
                    formation.EndPosition = formation.StartPosition;
                    formation.StartPosition = temp;
                }
            }

            AddRock(rockFormations);
        }

        [Description("How many units of sand come to rest before sand starts flowing into the abyss below?")]
        public override string SolvePart1() {
            return $"{FillSand()}";
        }

        [Description("How many units of sand come to rest?")]
        public override string SolvePart2() {
            for (int i = 0; i < cave.Length; i++) {
                BlockType block = cave[i];
                if (block == BlockType.Sand) { cave[i] = BlockType.Empty; }
            }
            List<Line> lines = new();
            lines.Add(new Line() { StartPosition = new Point() { X = minX, Y = minY + height - 1 }, EndPosition = new Point() { X = minX + width - 1, Y = minY + height - 1 } });
            AddRock(lines);
            return $"{FillSand()}";
        }

        private void AddRock(List<Line> lines) {
            for (int i = 0; i < lines.Count; i++) {
                Line line = lines[i];
                int index = line.StartPosition.X - minX + (line.StartPosition.Y - minY) * width;
                if (line.StartPosition.X == line.EndPosition.X) {
                    for (int j = line.StartPosition.Y; j <= line.EndPosition.Y; j++) {
                        cave[index] = BlockType.Rock;
                        index += width;
                    }
                } else {
                    for (int j = line.StartPosition.X; j <= line.EndPosition.X; j++) {
                        cave[index++] = BlockType.Rock;
                    }
                }
            }
        }
        private int FillSand() {
            int totalSand = 0;
            Queue<Sand> sand = new();
            sand.Enqueue(new Sand() { Position = 500 - minX });

            while (sand.Count > 0) {
                Sand current = sand.Dequeue();
                Sand next = new Sand() { Position = current.Position + width, Parent = current };
                if (next.Position >= cave.Length) { continue; }

                BlockType block = cave[next.Position];
                if (block == BlockType.Empty) {
                    sand.Enqueue(next);
                    continue;
                }

                if (cave[next.Position - 1] == BlockType.Empty) {
                    next.Position--;
                    sand.Enqueue(next);
                } else if (cave[next.Position + 1] == BlockType.Empty) {
                    next.Position++;
                    sand.Enqueue(next);
                } else {
                    cave[current.Position] = BlockType.Sand;
                    if (current.Parent != null) {
                        sand.Enqueue(current.Parent);
                    }
                    totalSand++;
                    //Display(current.Position);
                }
            }
            //Display(position);
            return totalSand;
        }
        private void Display(int position) {
            Console.WriteLine();
            for (int i = 0; i < cave.Length; i++) {
                BlockType block = cave[i];
                if (i == position) {
                    Console.Write('*');
                } else {
                    Console.Write(block == BlockType.Sand ? 'O' : block == BlockType.Rock ? '#' : '.');
                }
                if (((i + 1) % width) == 0) {
                    Console.WriteLine();
                }
            }
        }
        private enum BlockType : byte {
            Empty,
            Sand,
            Rock
        }
        private class Sand {
            public int Position;
            public Sand Parent;
            public override string ToString() {
                return $"{Position}";
            }
        }
    }
}

The syntax isn’t bad, and the code makes a lot of sense. The docs for Queue have plenty of examples.

I don’t know if I’d choose this as my first language when building a new project, but I do understand why it’s in the top 5 programming languages by popularity.

TODO: Run locally.

Part 1 Solution in JS | Part 2 Solution in JS


Day 15: C

Let’s go straight to the original now. Check out the clean solution from @piscilus:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define MAX_LINE_SIZE (100U)
#define MAX_SENSORS   (50U)
#define ROW_EXAMPLE   (10)
#define ROW_CONTEST   (2000000)

#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define ABS(n)  (((n) < 0) ? -(n) : (n))

enum
{
    YES,
    NO
};

typedef struct
{
    int64_t x;
    int64_t y;
} coords_t;

typedef struct
{
    coords_t self;   /* coords of sensor itself... */
    coords_t beacon; /* ... and its beacon*/
} sensor_t;

typedef struct
{
    coords_t max;
    coords_t min;
} grid_t;

/* https://en.wikipedia.org/wiki/Taxicab_geometry */
static inline int64_t manhattan_distance(int64_t x1, int64_t y1, int64_t x2, int64_t y2)
{
    return ABS((x2) - (x1)) + ABS((y2) - (y1));
}

int main(int argc, char *argv[])
{
    printf("Advent of Code 2022 - Day 15: Beacon Exclusion Zone\n\n");

    if (argc != 2)
    {
        fprintf(stderr, "Please provide data record file name.");
        exit(EXIT_FAILURE);
    }

    FILE* fp = fopen(argv[1], "r");

    if (!fp)
    {
        fprintf(stderr, "Could not open file!");
        exit(EXIT_FAILURE);
    }

    size_t sc = 0U;
    sensor_t sensors[MAX_SENSORS] = {0};

    char line_buf[MAX_LINE_SIZE] = {0};
    while (fgets(line_buf, MAX_LINE_SIZE, fp) != NULL)
    {
        if (sscanf(
                line_buf,
                "Sensor at x=%lld, y=%lld: closest beacon is at x=%lld, y=%lld",
                &sensors[sc].self.x,
                &sensors[sc].self.y,
                &sensors[sc].beacon.x,
                &sensors[sc].beacon.y) == 4)
        {
            sc++;
        }
        else
        {
            fprintf(stderr, "Could not parse input!");
            exit(EXIT_FAILURE);
        }
    }

    fclose(fp);

    grid_t grid = {0};
    grid.min.x = INT64_MAX;
    grid.min.y = INT64_MAX;

    printf("Number of sensors: %llu\n\n", sc);

    /* determine size of grid */
    for (size_t i = 0U; i < sc; i++)
    {
        grid.min.x = MIN(sensors[i].self.x, grid.min.x);
        grid.min.x = MIN(sensors[i].beacon.x, grid.min.x);

        grid.max.x = MAX(sensors[i].self.x, grid.max.x);
        grid.max.x = MAX(sensors[i].beacon.x, grid.max.x);

        grid.min.y = MIN(sensors[i].self.y, grid.min.y);
        grid.min.y = MIN(sensors[i].beacon.y, grid.min.y);

        grid.max.y = MAX(sensors[i].self.y, grid.max.y);
        grid.max.y = MAX(sensors[i].beacon.y, grid.max.y);
    }

    printf("min x = %lld\n", grid.min.x);
    printf("max x = %lld\n", grid.max.x);
    int64_t dx = grid.max.x - grid.min.x;
    printf("   dx = %lld\n\n", dx);
    printf("min y = %lld\n", grid.min.y);
    printf("max y = %lld\n", grid.max.y);
    int64_t dy = grid.max.y - grid.min.y;
    printf("   dy = %lld\n\n", dy);

    int64_t rows_wo_beacon = 0;

    /* for every point on the row ... */
    for (int64_t x = grid.min.x - (dy / 2); x <= grid.max.x + (dy / 2); x++)
    {
        /* ... check all sensors and their beacons */
        int has_beacon = YES;
        for (size_t s = 0U; s < sc; s++)
        {
            /* if beacon on row */
            if ((x == sensors[s].beacon.x) && (ROW_CONTEST == sensors[s].beacon.y))
            {
                has_beacon = YES;
                break;
            }
            /* if sensor is closer to point than to the beacon -> no beacon possible */
            else if (  manhattan_distance(sensors[s].self.x, sensors[s].self.y, x, ROW_CONTEST)
                    <= manhattan_distance(sensors[s].self.x, sensors[s].self.y, sensors[s].beacon.x, sensors[s].beacon.y))
            {
                has_beacon = NO;
                break;
            }
        }
        if (has_beacon == NO)
            rows_wo_beacon++;
    }

    printf("Part 1: position without beacons on row %lld = %lld", ROW_CONTEST, rows_wo_beacon);

    return EXIT_SUCCESS;
}

I see why people like using C.

These programs are also really easy to run on a Mac:

➜  g++ -o ./main ./main.c
➜  ./main ../input.txt
Advent of Code 2022 - Day 15: Beacon Exclusion Zone

Number of sensors: 23

min x = -518661
max x = 4305648
   dx = 4824309

min y = -294577
max y = 3984884
   dy = 4279461

Part 1: position without beacons on row 2000000 = 4951427%

Part 1 Solution in JS | Part 2 Solution in JS


Day 16: C++

I actually had trouble solving finding a solution for Day 16 in JS so I went straight to see if @jonathanpaulson (Day 7) had solved it in Python, and I was surprised to see he used C++ in this one:

#include <vector>
#include <set>
#include <sstream>
#include <cassert>
#include <tuple>
#include <unordered_map>
#include <iostream>
#include <map>
#include <utility>

using namespace std;
using ll = int64_t;
using pll = pair<ll, ll>;

vector<ll> R;
vector<vector<ll>> E;

ll best = 0;
vector<ll> DP;
ll f(ll p1, ll U, ll time, ll other_players)
{
    if (time == 0)
    {
        return other_players > 0 ? f(0, U, 26, other_players - 1) : 0LL;
    }

    auto key = U * R.size() * 31 * 2 + p1 * 31 * 2 + time * 2 + other_players;
    if (DP[key] >= 0)
    {
        return DP[key];
    }

    ll ans = 0;
    bool no_p1 = ((U & (1LL << p1)) == 0);
    if (no_p1 && R[p1] > 0)
    {
        ll newU = U | (1LL << p1);
        assert(newU > U);
        ans = max(ans, (time - 1) * R[p1] + f(p1, newU, time - 1, other_players));
    }
    for (auto &y : E[p1])
    {
        ans = max(ans, f(y, U, time - 1, other_players));
    }
    DP[key] = ans;
    /*if(DP.size() % 100000 == 0) {
      //cerr << DP.size() << " best=" << best << endl;
    }*/
    return ans;
}

int main()
{
    map<string, pair<ll, vector<string>>> INPUT;
    while (!cin.eof())
    {
        string S;
        getline(cin, S);
        std::istringstream iss(S);
        std::string word;

        ll idx = 0;
        string id;
        ll rate = 0;
        vector<string> NBR;
        while (std::getline(iss, word, ' '))
        {
            if (idx == 1)
            {
                id = word;
            }
            else if (idx == 4)
            {
                rate = stoll(word.substr(5, word.size() - 6));
            }
            else if (idx >= 9)
            {
                if (word[word.size() - 1] == ',')
                {
                    word = word.substr(0, word.size() - 1);
                }
                NBR.push_back(word);
            }
            idx++;
        }
        INPUT[id] = make_pair(rate, NBR);
    }

    ll n = INPUT.size();
    map<string, int> INDEX_OF;
    vector<string> ORDER;
    ll nonzero = 0;
    // Convenient to have the start position have index 0
    for (auto &p : INPUT)
    {
        if (p.first == "AA")
        {
            INDEX_OF[p.first] = ORDER.size();
            ORDER.push_back(p.first);
            nonzero++;
        }
    }
    // put valves with non-zero flow rate first
    for (auto &p : INPUT)
    {
        if (p.second.first > 0)
        {
            INDEX_OF[p.first] = ORDER.size();
            ORDER.push_back(p.first);
            nonzero++;
        }
    }
    for (auto &p : INPUT)
    {
        if (INDEX_OF.count(p.first) == 0)
        {
            INDEX_OF[p.first] = ORDER.size();
            ORDER.push_back(p.first);
        }
    }

    R = vector<ll>(n, 0);
    for (ll i = 0; i < n; i++)
    {
        R[i] = INPUT[ORDER[i]].first;
    }
    E = vector<vector<ll>>(n, vector<ll>{});
    for (ll i = 0; i < n; i++)
    {
        for (auto &y : INPUT[ORDER[i]].second)
        {
            E[i].push_back(INDEX_OF[y]);
        }
    }

    DP = vector<ll>((1L << nonzero) * n * 31 * 2, -1);
    // cerr << "DP size=" << DP.size() << endl;
    ll p1 = f(0, 0, 30, false);
    ll p2 = f(0, 0, 26, true);
    cout << p1 << endl;
    cout << p2 << endl;
}

Same as before, we can compile it into a main executable and run it. This one ran in about 5 seconds, while someone else’s JS solution I found took 12 minutes to run.

Day 17: C3

Wait, another C variant? Why not?

Check out this perfect solution from @lerno:

module day17;
import std::io;
import std::math;
import std::map;
import std::array::list;

define StateCacheMap = HashMap<StateCache, int>;

struct StateCache
{
	int shape_index;
	int wind_index;
	GameRow[128] rows;
	int height;
}

fn uint StateCache.hash(StateCache* cache)
{
	uint hash = cache.wind_index * 5 + cache.shape_index;
	return hash * 31 + cache.height;
}

macro bool StateCache.equals(StateCache* cache, StateCache other)
{
	if (cache.shape_index != other.shape_index) return false;
	if (cache.wind_index != other.wind_index) return false;
	if (cache.height != other.height) return false;
	for (int i = 0; i < cache.height; i++)
	{
		if (cache.rows[i] != other.rows[i]) return false;
	}
	return true;
}

fn String load_jets()
{
	File f;
	f.open("jets.txt", "rb")!!;
	defer catch(f.close());
	return f.getline();
}

struct Shape
{
	int width;
	int height;
	int[4][4] shape;
}

Shape[5] shapes = {
	{
		4, 1,
		{ { 1, 1, 1, 1 }, {}, {}, {} }
	},
	{
		3, 3,
		{
			{ 0, 1, 0, 0 },
			{ 1, 1, 1, 0 },
			{ 0, 1, 0, 0 }, {}
		}
	},
	{
		3, 3,
		{
			{ 1, 1, 1, 0 },
            { 0, 0, 1, 0 },
			{ 0, 0, 1, 0 },
			{} }
	},
	{
		1, 4,
		{
			{ 1, 0, 0, 0 },
			{ 1, 0, 0, 0 },
			{ 1, 0, 0, 0 },
			{ 1, 0, 0, 0 }
		}
	},
	{
		2, 2,
		{
			{ 1, 1, 0, 0 },
			{ 1, 1, 0, 0 }, {}, {}
		}
	}
};

define GameRow = distinct char;
GameRow[1000] game;

macro bool GameRow.is_full(GameRow row)
{
	return row == 127;
}

macro bool GameRow.bit(GameRow row, int bit)
{
	return ((GameRow)(1 << bit) & row) != 0;
}

macro bool GameRow.set_bit(GameRow* row, int bit)
{
	return *row |= (GameRow)1 << bit;
}

fn int find_height()
{
	int size = game.len;
	for (int i = 0; i < size; i++)
	{
		if (game[i] == 0) return i;
	}
	unreachable("Max exceeded");
}

fn int find_lowest_bound(int height)
{
	GameRow ok;
	for (int i = height; i >= 0; i--)
	{
		ok |= game[i];
		if (ok.is_full()) return i;
	}
	return 0;
}

fn bool check_collision(Shape* shape, int[<2>] location)
{
	if (location[1] < 0) return true;
	if (location[0] < 0) return true;
	if (location[0] + shape.width > 7) return true;
	int y_offset = shape.height;
	for (int y = 0; y < shape.height; y++)
	{
		for (int x = 0; x < shape.width; x++)
		{
			if (!shape.shape[y][x]) continue;
			if (game[location[1] + y].bit(location[0] + x)) return true;
		}
	}
	return false;
}

fn void land_shape(Shape* shape, int[<2>] location)
{
	for (int y = 0; y < shape.height; y++)
	{
		for (int x = 0; x < shape.width; x++)
		{
			if (!shape.shape[y][x]) continue;
			game[location[1] + y].set_bit(location[0] + x);
		}
	}
}

fn void draw_field()
{
	io::println("+-----+");
	for (int i = find_height(); i >= 0; i--)
	{
		GameRow row = game[i];
		for (int j = 0; j < 7; j++)
		{
			io::putchar(row.bit(j) ? '#' : '.');
		}
		io::println();
	}
	io::println("+-----+");
}

macro long adjust_lowest_bounds()
{
	int height = find_height();
	int bound = find_lowest_bound(height);
	if (bound == 0) return 0;
	for (int i = bound; i < height; i++)
	{
		game[i - bound] = game[i];
	}
	for (int i = height - bound; i < height; i++)
	{
		game[i] = 0;
	}
	return bound;
}

macro @drop_rock(char[] winds, int &shape_index, isz &wind_index)
{
	int start = find_height() + 3;
	Shape* current_shape = &shapes[shape_index];
	shape_index = (shape_index + 1) % 5;
	int[<2>] location = { 2, start };
	while (true)
	{
		char jet = winds[wind_index];
		wind_index = (wind_index + 1) % winds.len;
		int[<2>] new_loc = location + int[<2>] { jet == '<' ? -1 : 1, 0 };
		if (check_collision(current_shape, new_loc))
		{
			new_loc = location;
		}
		location = new_loc;
		new_loc[1]--;
		if (check_collision(current_shape, new_loc))
		{
			land_shape(current_shape, location);
			break;
		}
		location = new_loc;
	}
}

fn void solve(char[] winds, long rocks)
{
	game = {};
	int shape_index = 0;
	isz wind_index = 0;
	long offset = 0;
	StateCacheMap map;
	map.init(1024);
	long max = rocks;
	bool check_cache = true;
	long desired_drops = rocks;
	long first_cycle_hit = -1;
	long cycle_start_height = 0;
	long cycle_start_rock = 0;
	for (long i = 0; i < max; i++)
	{
		if (check_cache)
		{
			int height = find_height();
            int bottom = find_lowest_bound(height);
            StateCache cache = { .shape_index = shape_index, .wind_index = (int)wind_index, .height = height - bottom - 1 };
            assert(height - bottom < cache.rows.len);
            for (int k = bottom; k < height; k++)
            {
            	cache.rows[k - bottom] = game[k];
            }
            int! index = map.get(cache);
            if (try index)
            {
                {|
                    if (first_cycle_hit < 0)
                    {
                        first_cycle_hit = index;
                        cycle_start_rock = i;
                        cycle_start_height = find_height() + offset;
                        return;
					}
                    if (index != first_cycle_hit) return;
                    long cycle_height = find_height() + offset - cycle_start_height;
                    long cycle_len = (long)i - cycle_start_rock;
	                desired_drops -= i;
                    long cycles = desired_drops / cycle_len;
                    offset += cycles * cycle_height;
                    max = desired_drops % cycle_len;
                    i = 0;
                    check_cache = false;
                |};
            }
            else
            {
				map.set(cache, (int)i);
            }
        }
		@drop_rock(winds, shape_index, wind_index);
		offset += adjust_lowest_bounds();
	}
	io::printfln("Height was: %d", find_height() + offset);
}

fn void main()
{
	String s = load_jets();
	defer s.destroy();
	solve(s.str(), 2022);
	solve(s.str(), 1000000000000i64);
}

This looks really nice. I like the syntax of this variant the most; it’s very readable.

TODO: Run locally.

Part 1 Solution in JS | Part 2 Solution in JS


Day 18: Go

Ahhh finally! Undoubedly my favorite language, I have been waiting for a good puzzle for Go.

Here’s a really clean solution from @nikolay:

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

type Coord struct {
	x, y, z int
}

func (coord Coord) Move(dx, dy, dz int) Coord {
	return Coord{coord.x + dx, coord.y + dy, coord.z + dz}
}

func (coord Coord) IsInside(min, max Coord) bool {
	return coord.x >= min.x && coord.x <= max.x &&
		coord.y >= min.y && coord.y <= max.y &&
		coord.z >= min.z && coord.z <= max.z
}

func (coord Coord) Adjacent() []Coord {
	return []Coord{
		coord.Move(-1, 0, 0),
		coord.Move(+1, 0, 0),
		coord.Move(0, -1, 0),
		coord.Move(0, +1, 0),
		coord.Move(0, 0, -1),
		coord.Move(0, 0, +1),
	}
}

func Min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func Max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func (coord Coord) Min(min Coord) Coord {
	return Coord{Min(coord.x, min.x), Min(coord.y, min.y), Min(coord.z, min.z)}
}

func (coord Coord) Max(min Coord) Coord {
	return Coord{Max(coord.x, min.x), Max(coord.y, min.y), Max(coord.z, min.z)}
}

func Solve1(cubes []Coord) int {
	space := map[Coord]bool{}
	for _, coord := range cubes {
		space[coord] = true
	}
	surface := 0
	for _, coord := range cubes {
		sides := 0
		for _, adjacent := range coord.Adjacent() {
			if _, ok := space[adjacent]; !ok {
				sides++
			}
		}
		surface += sides
	}
	return surface
}

func Solve2(cubes []Coord) int {
	space := map[Coord]bool{}
	min := cubes[0]
	max := min
	for _, coord := range cubes {
		space[coord] = true
		min = coord.Min(min)
		max = coord.Max(max)
	}
	min = min.Move(-1, -1, -1)
	max = max.Move(+1, +1, +1)

	step := 1
	water := map[Coord]int{{min.x, min.y, min.z}: step}
	for {
		found := false
		batch := map[Coord]int{}
		for coord, v := range water {
			if v == step {
				for _, adjacent := range coord.Adjacent() {
					if adjacent.IsInside(min, max) {
						if _, ok := space[adjacent]; ok {
							continue
						}
						if _, ok := batch[adjacent]; ok {
							continue
						}
						if _, ok := water[adjacent]; !ok {
							batch[adjacent] = step + 1
							found = true
						}
					}
				}
			}
		}
		if !found {
			break
		}
		for coord, v := range batch {
			water[coord] = v
		}
		step++
	}

	surface := 0
	for _, coord := range cubes {
		sides := 0
		for _, adjacent := range coord.Adjacent() {
			if _, ok := water[adjacent]; ok {
				sides++
			}
		}
		surface += sides
	}
	return surface
}

func main() {
	file, err := os.Open("input.txt")
	if err != nil {
		log.Fatal(err)
	}

	var cubes []Coord

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if len(line) == 0 {
			continue
		}
		fields := strings.Split(line, ",")
		x, _ := strconv.Atoi(strings.TrimSpace(fields[0]))
		y, _ := strconv.Atoi(strings.TrimSpace(fields[1]))
		z, _ := strconv.Atoi(strings.TrimSpace(fields[2]))
		cubes = append(cubes, Coord{x, y, z})
	}
	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}

	fmt.Println(Solve1(cubes))
	fmt.Println(Solve2(cubes))
}

It’s really easy to run a Go program locally, just create a main.go in the same folder as the input file and:

➜ go run main.go
4332
2524

No solution in JS today.


Day 19: Haskell

Another language I’ve been wanting to try for a while. It’s functional, strongly typed, and has a huge developer community. I used ChatGPT to get familiar with how to parse a file:

Haskell learnings

Look at this elegant solution from @ephemient:

{-|
Module:         Day19
Description:    <https://adventofcode.com/2022/day/19 Day 19: Not Enough Minerals>
-}
{-# LANGUAGE OverloadedStrings, TypeFamilies #-}
module Day19 (day19a, day19b) where

import Control.Arrow (second)
import Control.Parallel.Strategies (parMap, rseq)
import Data.Char (isAlphaNum)
import qualified Data.Heap as Heap (FstMaxPolicy, insert, singleton, view)
import Data.List (foldl', scanl', transpose)
import Data.Map (Map)
import qualified Data.Map as Map ((!), (!?), elems, fromDistinctAscList, fromList, keys, insert, insertWith, toAscList, toList, unionWith, unionsWith)
import Data.Maybe (fromMaybe)
import Data.String (IsString)
import Data.Text (Text)
import Data.Void (Void)
import Debug.Trace (traceShowId)
import Text.Megaparsec (MonadParsec, ParseErrorBundle, Token, Tokens, between, eof, many, parse, sepBy1, sepEndBy, takeWhile1P)
import Text.Megaparsec.Char (char, space, string)
import qualified Text.Megaparsec.Char.Lexer as L (decimal)

parser :: (Integral a, Integral b, MonadParsec e s m, IsString (Tokens s), Token s ~ Char) => m [(a, Map (Tokens s) (Map (Tokens s) b))]
parser = many blueprint where
    blueprint = (,) <$> between (string "Blueprint ") (char ':' >> space) L.decimal <*>
        (Map.fromList <$> (robot <* char '.') `sepEndBy` space)
    robot = (,) <$> between (string "Each ") (string " robot costs ") name <*>
        (Map.fromList <$> ore `sepBy1` string " and ")
    ore = flip (,) <$> L.decimal <*> (char ' ' >> name)
    name = takeWhile1P Nothing isAlphaNum

geodes :: (IsString k, Ord k, Num v, Ord v) => Int -> Map k (Map k v) -> v
geodes n blueprint = go 0 (initialRobots, initialResources, n) where
    maxValues = Map.unionsWith max $ Map.elems blueprint
    initialRobots = Map.insert "ore" 1 $ const 0 <$> blueprint
    initialResources = const 0 <$> blueprint
    potential robots resources m = potentialResources !! m Map.! "geode" where
        potentialRobots = Map.fromDistinctAscList . zip (Map.keys blueprint) <$> transpose
          [ (robots Map.! robot +) <$> scanl' f 0 potentialResources
          | (robot, costs) <- Map.toAscList blueprint
          , let f new resources'
                  | and [resources' Map.! key >= new * cost | (key, cost) <- Map.toList costs]
                  = new + 1
                  | otherwise = new
          ]
        potentialResources = scanl' (Map.unionWith (+)) resources potentialRobots
    go k (_, _, 0) = k
    go k (robots, resources, m)
      | k > potential robots resources m = k
      | otherwise = foldl' go
          (max k $ resources Map.! "geode" + fromIntegral m * robots Map.! "geode")
          [ (robots', resources'', m - d - 1)
          | (robot, costs) <- Map.toList blueprint
          , maybe True (robots Map.! robot <) $ maxValues Map.!? robot
          , (d, resources') <- take 1 $ dropWhile (any (< 0) . snd) $
                zip [0..m - 1] $ iterate (Map.unionWith (+) robots) $
                Map.unionWith (+) resources $ negate <$> costs
          , let robots' = Map.insertWith (+) robot 1 robots
                resources'' = Map.unionWith (+) robots resources'
          ]

day19a :: Text -> Either (ParseErrorBundle Text Void) Int
day19a input = sum . fmap (uncurry (*)) . parMap rseq (traceShowId . second (geodes 24)) <$>
    parse (parser @Int @Int @Void <* eof) "day19.txt" input

day19b :: Text -> Either (ParseErrorBundle Text Void) Int
day19b input = product . parMap rseq (snd . traceShowId . second (geodes 32)) . take 3 <$>
    parse (parser @Int @Int @Void <* eof) "day19.txt" input

TODO: Run locally.

Solution in Python


Day 20: Elixir

Another familiar friend: Elixir! It’s been a few years since I used it, but I haven’t forgotten it. I was big on Elixir ever since @hayesgm used it for a big project at Postmates back in 2017.

Let’s check out @gabrielhora’s solution (slightly modified so I can run it locally):

defmodule Ex do
  def part1(input) do
    input
    |> Enum.map(&String.to_integer/1)
    |> Enum.with_index()
    |> then(&mix({&1, &1}))
    |> then(fn {_, mixed} -> solve(mixed) end)
  end

  def part2(input) do
    input
    |> Enum.map(&String.to_integer/1)
    |> Enum.map(&(&1 * 811_589_153))
    |> Enum.with_index()
    |> then(fn data ->
      Enum.reduce(0..9, {data, data}, fn _, acc -> mix(acc) end)
    end)
    |> then(fn {_, mixed} -> solve(mixed) end)
  end

  defp solve(data) do
    size = length(data)
    zero = Enum.find_index(data, fn {val, _} -> val == 0 end)
    a = data |> Enum.at(mod(zero + 1000, size)) |> elem(0)
    b = data |> Enum.at(mod(zero + 2000, size)) |> elem(0)
    c = data |> Enum.at(mod(zero + 3000, size)) |> elem(0)
    a + b + c
  end

  defp mix({original, mixed}) do
    size = length(original)

    mixed =
      original
      |> Enum.reduce(mixed, fn d = {val, _}, acc ->
        cur_idx = Enum.find_index(acc, &(&1 == d))
        new_idx = mod(cur_idx + val, size - 1)
        new_idx = if(new_idx == 0, do: -1, else: new_idx)
        {_, acc} = List.pop_at(acc, cur_idx)
        List.insert_at(acc, new_idx, d)
      end)

    {original, mixed}
  end

  defp mod(x, y) when x > 0, do: rem(x, y)
  defp mod(x, y) when x < 0, do: rem(x, y) + y
  defp mod(0, _), do: 0
end

I love the use of the |> pipe operator to take the output from one function and send it as the first argument to the next function.

It’s pretty easy to run locally with iex, the Elixir REPL:

➜ iex -S mix
iex(1)> File.read!("../input.txt") |> String.split("\n") |> Ex.part1
4066
iex(2)> File.read!("../input.txt") |> String.split("\n") |> Ex.part2
6704537992933

No solution in JS today.


Day 21: Scala

Another familiar friend; I used Scala towards the final years at Postmates to build some data pipelines for the restaurant feed. This was the first time I used Learn X in Y Minutes, and I made sure to stop there again this time.

I found a beautiful solution by @dev-andrey:

val input = scala.io.Source.fromResource(s"advent2022/day21.txt").getLines().toList

sealed trait Monkey

object Monkey {
  final case class Yelling(num: Long)                       extends Monkey
  final case class Adding(left: String, right: String)      extends Monkey
  final case class Subtracting(left: String, right: String) extends Monkey
  final case class Multiplying(left: String, right: String) extends Monkey
  final case class Dividing(left: String, right: String)    extends Monkey
}

val monkeys = input.map {
  case s"$name: $left + $right" => name -> Monkey.Adding(left, right)
  case s"$name: $left - $right" => name -> Monkey.Subtracting(left, right)
  case s"$name: $left * $right" => name -> Monkey.Multiplying(left, right)
  case s"$name: $left / $right" => name -> Monkey.Dividing(left, right)
  case s"$name: $num"           => name -> Monkey.Yelling(num.toLong)
}.toMap

def memo[K, V](fn: K => V): K => V = {
  val cache = collection.mutable.Map.empty[K, V]
  key => cache.getOrElseUpdate(key, fn(key))
}

lazy val answer: String => Long = memo { monkey =>
  monkeys(monkey) match {
    case Monkey.Yelling(num)             => num
    case Monkey.Adding(left, right)      => answer(left) + answer(right)
    case Monkey.Subtracting(left, right) => answer(left) - answer(right)
    case Monkey.Multiplying(left, right) => answer(left) * answer(right)
    case Monkey.Dividing(left, right)    => answer(left) / answer(right)
  }
}

answer("root")

val humans = monkeys
  .removed("humn")
  .updatedWith("root") { case Some(Monkey.Adding(left, right)) =>
    Option(Monkey.Subtracting(left, right))
  }

lazy val searching: String => Option[Long] = memo { monkey =>
  if (monkey == "humn") None
  else
    humans(monkey) match {
      case Monkey.Yelling(num)             => Some(num)
      case Monkey.Adding(left, right)      => (searching(left) zip searching(right)).map { case (l, r) => l + r }
      case Monkey.Subtracting(left, right) => (searching(left) zip searching(right)).map { case (l, r) => l - r }
      case Monkey.Multiplying(left, right) => (searching(left) zip searching(right)).map { case (l, r) => l * r }
      case Monkey.Dividing(left, right)    => (searching(left) zip searching(right)).map { case (l, r) => l / r }
    }
}

def makeMonkey(name: String)(goal: Long): Option[Long] = {
  def invertMonkey(monkey: Monkey, goal: Long): Option[Long] =
    monkey match {
      case Monkey.Yelling(_) => None
      case Monkey.Adding(left, right) =>
        (searching(left), searching(right)) match {
          case (Some(l), None) => makeMonkey(right)(goal - l)
          case (None, Some(r)) => makeMonkey(left)(goal - r)
        }
      case Monkey.Subtracting(left, right) =>
        (searching(left), searching(right)) match {
          case (Some(l), None) => makeMonkey(right)(l - goal)
          case (None, Some(r)) => makeMonkey(left)(r + goal)
        }
      case Monkey.Multiplying(left, right) =>
        (searching(left), searching(right)) match {
          case (Some(l), None) => makeMonkey(right)(goal / l)
          case (None, Some(r)) => makeMonkey(left)(goal / r)
        }
      case Monkey.Dividing(left, right) =>
        (searching(left), searching(right)) match {
          case (Some(l), None) => makeMonkey(right)(goal * l)
          case (None, Some(r)) => makeMonkey(left)(goal * r)
        }
    }

  if (name == "humn") Option(goal)
  else invertMonkey(humans(name), goal)
}

makeMonkey("root")(0L)

TODO: Run locally.

Part 1 Solution in JS | Part 2 Solution in JS


Day 22: R

A lot of these AoC puzzles are visual, so it would make sense to try a visual programming language like R. I asked ChatGPT what R is typically used for:

R is commonly used in a variety of fields, including biology, finance, and social sciences, and it is widely used in data science and machine learning.

Check out this solution from @plannapus:

input <- readLines("input22.txt")
s<-strsplit(input[1:200],"")
map <- do.call(rbind,lapply(s,function(x) if(length(x)!=150){
  return(c(x,rep(" ",150-length(x))))
  }else{return(x)}))
inst <- el(strsplit(input[202],"(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)",perl=TRUE))
loc <- c(1,which(map[1,]==".")[1])
d <- ">"
correct <- function(new){
  if(new[2]>ncol(map)) new[2] <- 1
  if(new[2]<1) new[2] <- ncol(map)
  if(new[1]>nrow(map)) new[1] <- 1
  if(new[1]<1) new[1] <- nrow(map)
  new
}
path <- cbind(rep(NA,4002),rep(NA,4002))
path[1,]<-loc
options(warn=-1)
for(i in seq_along(inst)){
  if(!is.na(as.integer(inst[i]))){
    n <- as.integer(inst[i])
    incr <- switch(d,"<"=c(0,-1),"v"=c(1,0),">"=c(0,1),"^"=c(-1,0))
    for(k in 1:n){
      new <- loc+incr
      new <- correct(new)
      while(map[new[1],new[2]]==" "){
        new <- new+incr
        new <- correct(new)
      }
      if(map[new[1],new[2]]=="#") break
      loc <- new
    }
  }else{
    if(inst[i]=="R"){
      dirs <- c("<","^",">","v","<")
      d <- dirs[which(dirs==d)[1]+1]
    }else{
      dirs <- c("<","v",">","^","<")
      d <- dirs[which(dirs==d)[1]+1]
    }
  }
  path[i+1,]<-loc
  cat(i,":",loc[1],"-",loc[2],"\n")
}

loc[1]*1000+loc[2]*4+switch(d,"<"=2,"^"=3,">"=0,"v"=1)
#149138

### Part 2
map_dice <- matrix(0,ncol=150,nrow=200)
map_dice[1:50,51:100]<-1
map_dice[1:50,101:150]<-2
map_dice[51:100,51:100]<-3
map_dice[101:150,1:50]<-5
map_dice[101:150,51:100]<-4
map_dice[151:200,1:50]<-6

loc <- c(1,which(map[1,]==".")[1])
d <- ">"
correct_dice <- function(loc,new,d){
  md1 <- map_dice[loc[1],loc[2]]
  if(new[2]>ncol(map)&md1==2){
    #Go to Side 4
    d <- "<"
    mappingXX <- cbind(1:50,150:101)
    newC <- c(mappingXX[mappingXX[,1]==new[1],2],100)
    new <- newC
  }
  if(new[2]<1){
    if(md1==5){
      #Go to Side 1
      d <- ">"
      mappingXX <- cbind(150:101,1:50)
      newC <- c(mappingXX[mappingXX[,1]==new[1],2],51)
      new <- newC
    }
    if(md1==6){
      #Go to Side 1
      d <- "v"
      mappingXY <- cbind(151:200,51:100)
      newC <- c(1,mappingXY[mappingXY[,1]==new[1],2])
      new <- newC
    }
  }
  if(new[1]<1){
    if(md1==1){
      #Go to Side 6
      d <- ">"
      mappingYX <- cbind(51:100,151:200)
      newC <- c(mappingYX[mappingYX[,1]==new[2],2],1)
      new <- newC
    }
    if(md1==2){
      #Go to Side 6
      d <- "^"
      mappingYY <- cbind(101:150,1:50)
      newC <- c(200,mappingYY[mappingYY[,1]==new[2],2])
      new <- newC
    }
  }
  if(new[1]>nrow(map)&md1==6){
    # Go to Side 2
    dir <- "v"
    mappingYY <- cbind(1:50,101:150)
    newC <- c(1,mappingYY[mappingYY[,1]==new[2],2])
    new <- newC
  }
  if(map[new[1],new[2]]==" "){
    if(md1==1){
      #Go to Side 5
      d <- ">"
      mappingXX <- cbind(1:50,150:101)
      newC <- c(mappingXX[mappingXX[,1]==new[1],2],1)
      new <- newC
    }
    if(md1==2){
      #Go to Side 3
      d <- "<"
      mappingYX <- cbind(101:150,51:100)
      newC <- c(mappingYX[mappingYX[,1]==new[2],2],100)
      new <- newC
    }
    if(md1==3){
      w<-which(map_dice==3,arr.ind=T)
      if(new[2]<min(w[,2])){
        #Go to Side 5
        d <- "v"
        mappingXY <- cbind(51:100,1:50)
        newC <- c(101,mappingXY[mappingXY[,1]==new[1],2])
        new <- newC
      }else{
        #Go to Side 2
        d <- "^"
        mappingXY <- cbind(51:100,101:150)
        newC <- c(50,mappingXY[mappingXY[,1]==new[1],2])
        new <- newC
      }
    }
    if(md1==4){
      w<-which(map_dice==4,arr.ind=T)
      if(new[2]>max(w[,2])){
        #Go to Side 2
        d <- "<"
        mappingXX <- cbind(101:150,50:1)
        newC <- c(mappingXX[mappingXX[,1]==new[1],2],150)
        new <- newC
      }else{
        #Go to Side 6
        d <- "<"
        mappingYX <- cbind(51:100,151:200)
        newC <- c(mappingYX[mappingYX[,1]==new[2],2],50)
        new <- newC
      }
    }
    if(md1==5){
     #Go to Side 3
      d<-">"
      mappingYX <- cbind(1:50,51:100)
      newC <- c(mappingYX[mappingYX[,1]==new[2],2],51)
      new <- newC
    }
    if(md1==6){
      #Go to side 4
      d <- "^"
      mappingXY <- cbind(151:200,51:100)
      newC <- c(150,mappingXY[mappingXY[,1]==new[1],2])
      new <- newC
    }
  }
  if(map[new[1],new[2]]==" ") stop()
  return(list(new=new,d=d))
}
path <- loc
options(warn=-1)
for(i in seq_along(inst)){
  if(!is.na(as.integer(inst[i]))){
    n <- as.integer(inst[i])
    for(k in 1:n){
      incr <- switch(d,"<"=c(0,-1),"v"=c(1,0),">"=c(0,1),"^"=c(-1,0))
      new <- loc+incr
      NEW <- correct_dice(loc,new,d)
      new <- NEW$new
      if(map[new[1],new[2]]=="."){
        d <- NEW$d
        loc <- new
        path <- rbind(path,loc)
      }
    }
  }else{
    if(inst[i]=="R"){
      dirs <- c("<","^",">","v","<")
      d <- dirs[which(dirs==d)[1]+1]
    }else{
      dirs <- c("<","v",">","^","<")
      d <- dirs[which(dirs==d)[1]+1]
    }
  }
  #path[i+1,]<-loc
  cat(i,":",loc[1],"-",loc[2],"\n")
}

loc[1]*1000+loc[2]*4+switch(d,"<"=2,"^"=3,">"=0,"v"=1)
#153203

Btw, the Learn X in Y Minutes for R is really fun.

Part 1 Solution in JS | Part 2 Solution in JS


Day 23: Kotlin

We’re almost done and we haven’t done any native mobile development. Let’s try some Kotlin and see what the docs say about it:

Kotlin is a modern but already mature programming language aimed to make developers happier. It’s concise, safe, interoperable with Java and other languages, and provides many ways to reuse code between multiple platforms for productive programming.

Making developers happy is a good goal. Let’s check out @ArpitShukla’s clean solution:

fun main() {
    val input = readInputLines("Day23")
    var elves = input.indices.flatMap { i -> input[0].indices.map { j -> i to j } }
        .filter { (i, j) -> input[i][j] == '#' }.toSet()
    val directions = listOf(
        listOf(-1 to 0, -1 to 1, -1 to -1), // NORTH
        listOf(1 to 0, 1 to 1, 1 to -1),    // SOUTH
        listOf(0 to -1, 1 to -1, -1 to -1), // WEST
        listOf(0 to 1, 1 to 1, -1 to 1),    // EAST
    )
    for (i in 0..Int.MAX_VALUE) {
        val moves = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>>()
        for (elf in elves) {
            if (directions.flatten().any { elf + it in elves }) {
                val d = (0..3).find { d -> directions[(i + d) % 4].none { elf + it in elves } } ?: continue
                moves[elf] = elf + directions[(i + d) % 4][0]
            }
        }
        val posCountMap = mutableMapOf<Pair<Int, Int>, Int>()
        moves.forEach { (_, pos) -> posCountMap[pos] = (posCountMap[pos] ?: 0) + 1 }
        val canMove = moves.filter { (_, pos) -> posCountMap[pos] == 1 }
        if (canMove.isEmpty()) {
            println("Part 2: ${i + 1}")
            break
        }
        elves = (elves - canMove.keys + canMove.values).toMutableSet()
        if (i == 9) {
            val height = elves.maxOf { it.first } - elves.minOf { it.first } + 1
            val width = elves.maxOf { it.second } - elves.minOf { it.second } + 1
            println("Part 1: ${height * width - elves.size}")
        }
    }
}

operator fun Pair<Int, Int>.plus(other: Pair<Int, Int>) = first + other.first to second + other.second

I was really curious about this to operator. The docs say it’s mostly for creating map literals.

By the looks of this solution, Kotlin is inviting and if I ever decide to build an Android app, I’ll definitely dive deeper!

Part 1 Solution in JS | Part 2 Solution in JS


Day 24: Swift

I’ve always wanted to spent more time on Swift. It’s a joy to learn; the docs are thorough and have tons of code examples. I have to shoutout Learn X in Y Minutes again here, which is a great way to learn. Just create a Learn.swift file and run swift Learn.swift if using a Mac.

Here’s a solution from @gereons:

import AoCTools

private enum Ground: Character, Drawable {
    case wall = "#"
    case open = "."
}

struct Blizzard: Hashable, Equatable {
    let direction: Direction

    init?(dir: Character) {
        switch (dir) {
        case "<": direction = .left
        case ">": direction = .right
        case "^": direction = .up
        case "v": direction = .down
        default: return nil
        }
    }
}

private struct Valley {
    let grid: [Point: Ground]
    let blizzards: [Point: [Blizzard]]
    let maxX: Int
    let maxY: Int
    let start: Point
    let destination: Point
    let blizzardStates: [[Point: [Blizzard]]]

    init(_ str: String) {
        var start = Point.zero
        var destination = Point.zero
        var grid = [Point: Ground]()

        var blizzards = [Point: [Blizzard]]()
        let lines = str.lines
        for (y, line) in lines.enumerated() {
            for (x, ch) in line.enumerated() {
                let point = Point(x, y)
                if let b = Blizzard(dir: ch) {
                    blizzards[point, default: []].append(b)
                }
                if y == 0 && ch == "." { start = point }
                if y == lines.count - 1 && ch == "." { destination = point }
                grid[point] = ch == "#" ? .wall : .open
            }
        }

        maxX = grid.keys.max(of: \.x)!
        maxY = grid.keys.max(of: \.y)!
        self.start = start
        self.destination = destination
        self.grid = grid
        self.blizzards = blizzards

        // precompute all possible blizzard states
        var bStates = [[Point: [Blizzard]]]()
        var seen = Set<[Point: [Blizzard]]>()
        bStates.append(blizzards)
        seen.insert(blizzards)
        for _ in 1..<lcm(maxX - 1, maxY - 1) {
            let moved = Self.move(blizzards, maxX: maxX, maxY: maxY)
            if seen.contains(moved) {
                break
            }
            bStates.append(moved)
            seen.insert(moved)
            blizzards = moved
        }

        self.blizzardStates = bStates
    }

    private static func move(_ blizzards: [Point: [Blizzard]], maxX: Int, maxY: Int) -> [Point: [Blizzard]] {
        var moved = [Point: [Blizzard]]()

        for (point, blizzards) in blizzards {
            for b in blizzards {
                var p = point.moved(to: b.direction)
                if p.x == 0 {
                    p = Point(maxX - 1, p.y)
                } else if p.x == maxX {
                    p = Point(1, p.y)
                } else if p.y == 0 {
                    p = Point(p.x, maxY - 1)
                } else if p.y == maxY {
                    p = Point(p.x, 1)
                }

                moved[p, default: []].append(b)
            }
        }
        return moved
    }
}

final class Day24: AOCDay {
    private let valley: Valley


    init(rawInput: String? = nil) {
        let input = rawInput ?? Self.rawInput

        valley = Valley(input)
    }

    func part1() -> Int {
        let pathfinder = AStarPathfinder(map: valley)
        let start = PathNode(valley.start)
        let destination = PathNode(valley.destination)
        let path = pathfinder.shortestPath(from: start, to: destination)
        return path.count
    }

    func part2() -> Int {
        let pathfinder = AStarPathfinder(map: valley)

        let path1 = pathfinder.shortestPath(from: PathNode(valley.start),
                                            to: PathNode(valley.destination))
        let path2 = pathfinder.shortestPath(from: PathNode(valley.destination, time: path1.count),
                                            to: PathNode(valley.start))
        let path3 = pathfinder.shortestPath(from: PathNode(valley.start, time: path1.count + path2.count),
                                            to: PathNode(valley.destination))

        return path1.count + path2.count + path3.count
    }
}

private struct PathNode: Hashable {
    let point: Point
    let time: Int

    init(_ point: Point, time: Int = 0) {
        self.point = point
        self.time = time
    }
}

extension Valley: Pathfinding {
    typealias Coordinate = PathNode

    func neighbors(for node: PathNode) -> [PathNode] {
        let moved = blizzardStates[(node.time + 1) % blizzardStates.count]
        let targets = node.point.neighbors() + [node.point] // move or stay
        let neighbors = targets
            .filter { grid[$0] == .open }
            .filter { moved[$0] == nil }

        return neighbors.map { PathNode($0, time: node.time + 1) }
    }

    func costToMove(from: PathNode, to: PathNode) -> Int {
        distance(from: from, to: to) + to.time
    }

    func distance(from: PathNode, to: PathNode) -> Int {
        from.point.distance(to: to.point)
    }

    func goalReached(at node: PathNode, goal: PathNode) -> Bool {
        node.point == goal.point
    }
}

_Solution in Python

Day 25: Noulith

We made it.

Noulith is a new language created by @betaveros, the winner of Advent of Code 2022. This language is very basic but super powerful. A quick read through the Github README will help you understand how it works, and the CLI pretty easy to get up and running locally.

Playing with Noulith

Here’s @betaveros solution:

day := 25;
import "advent-prelude.noul";

puzzle_input := advent_input();

to_digs := {"0": 0, "1": 1, "2": 2, "-": -1, "=": -2};
from_digs := to_digs.items map reverse then dict;

from_snafu := \s -> s fold (\acc, d -> acc * 5 + to_digs[d]) from 0;
to_snafu := \n ->
	if (not n) ""
	else switch (n %% 5)
		case 0 or 1 or 2 -> to_snafu(n // 5) $ from_digs[n %% 5]
		case 3 or 4 -> to_snafu(n // 5 + 1) $ from_digs[n %% 5 - 5];

submit! 1, puzzle_input.lines map from_snafu then sum then to_snafu;

It was fun trying to run the solution using @betaveros’s repo too; you copy your session cookie from adventofcode.com and it actually submits the answer for you. I also got to play around with Fish shell in the process.

More noulith

I’m really inspired by this project, and I’m going to check out the source code (in Rust) later.

Conclusion

While I actually didn’t fully learn 25 new languages, I did get a taste of each one, and ran most of them locally in the process.

We did it

Learn X in Y Minutes is the single best resource to get started with a new programming language. ChatGPT and Github Copilot helped me deep-dive into learning the syntax and flow of a language.

I see why people use languages like Java, C, Go, Swift, Kotlin, and Rust. All of these are super powerful, and they get the job done.

At the end of the day, you’re going to use the tool you know best to solve the problem. I’m glad I had the chance to explore so many new langugages. I hope to circle back and play around with some of these again.