lily's recent activity

  1. Comment on UFO50AndroidUnofficial: A tool to build your own Android version of UFO 50 in ~games

    lily
    Link Parent
    We're lucky in this case that the UFO 50 developers chose to compile the game the way they did. GameMaker has two ways to compile your game: VM and YYC. VM is how the engine initially worked...

    We're lucky in this case that the UFO 50 developers chose to compile the game the way they did.

    GameMaker has two ways to compile your game: VM and YYC. VM is how the engine initially worked (after the initial release of GameMaker: Studio, at least - in the pre-GMS versions, games' source code was stored as plaintext and interpreted directly); source code is compiled to bytecode and packed together with all the game's assets into a data file. It's called data.win on Windows, which is probably the most common name it's referred to as, but it has a different extension on every platform for some reason. At this point, this data format has been completely reverse-engineered, and there is a high-quality decompiler for the bytecode (as a part of UndertaleModTool, a full modding suite for the engine) that can turn it back into readable GML code. This data file is paired with a runner that's the same for every game made with a given GameMaker version, which is what makes UFO 50 relatively easy to port to Android in this case. You just need to get an Android runner for the correct GameMaker version, which is easy to obtain by just compiling a dummy project, and you're good to go.

    YYC still uses a data file with the same format, but source code is no longer stored in it. Instead, it's transpiled to C++, then compiled to native machine code as part of the runner executable. This makes the game not especially moddable, and would make it way harder to port to other platforms.

    I've always used VM for my own GameMaker games to allow mods and ports like this to be possible. I always find it a shame when a game is locked down enough to be difficult to mod. This kind of thing is what gives games longevity, and I think it's important to encourage it.

    Unfortunately, things may change in the future. YoYo Games has been working for a while on a new GameMaker runtime (tentatively called GMRT), which will completely change the format games are stored in and possibly lock modders out. At the very least, it would be a whole lot of work to build back to the point we're at now, where GameMaker games can be completely taken apart and changed at will. I'm sure there will be improvements along with the new runtime, but it's a real shame we might have all that progress reset soon.

    2 votes
  2. Comment on Stimulation Clicker in ~games

    lily
    (edited )
    Link
    Got a decent amount into this, but it is incredibly poorly optimized. It's still running in the background - I can hear it - but the tab won't open anymore. Either the developer has a particularly...

    Got a decent amount into this, but it is incredibly poorly optimized. It's still running in the background - I can hear it - but the tab won't open anymore. Either the developer has a particularly powerful machine, or this wasn't tested properly. I'm not convinced it's even possible to complete on my laptop; I think I was only about halfway through.

    EDIT: With Firefox, I was able to complete the game! Still incredibly laggy, but it was playable.

  3. Comment on Half-Life 3 playtests begin and 2025 reveal “quite possible,” says Valve insider in ~games

    lily
    Link Parent
    I was a teenager not very long ago, and Half-Life wasn't particularly unknown within my social circles... I think the age range for stuff like Skibidi Toilet is more what they're calling...

    I was a teenager not very long ago, and Half-Life wasn't particularly unknown within my social circles... I think the age range for stuff like Skibidi Toilet is more what they're calling Generation Alpha - so, younger children. I'd say for people around my age, if they didn't know about Half-Life specifically, they were probably exposed to it through Garry's Mod, or just from the general influence Half-Life 2 has had on internet/meme culture over the years. I don't think most teenagers today would be caught dead watching Skibidi Toilet; "cringe" content like that is looked down upon.

    4 votes
  4. Comment on Half-Life 3 playtests begin and 2025 reveal “quite possible,” says Valve insider in ~games

    lily
    Link Parent
    I mean, I'm what you'd probably call a "zoomer", and Half-Life 2 is one of my favorite games. I have friends who have played and enjoyed the game as well. I don't know anybody personally who plays...

    I mean, I'm what you'd probably call a "zoomer", and Half-Life 2 is one of my favorite games. I have friends who have played and enjoyed the game as well. I don't know anybody personally who plays either Fortnite or Helldivers (and I'm hardly familiar with them at all, myself). So, it doesn't necessarily disprove your point, but just keep in mind demographics are very broad, and trying to generalize across an entire generation is probably not going to be accurate.

    7 votes
  5. Comment on Day 25: Code Chronicle in ~comp.advent_of_code

    lily
    Link Parent
    It's very procedural / C-like and quite opinionated, so it probably is only appealing to specific types of programmers. I've found some things about it I like, and some I don't - though, maybe...

    It's very procedural / C-like and quite opinionated, so it probably is only appealing to specific types of programmers. I've found some things about it I like, and some I don't - though, maybe some of the latter will be improved by the time the language enters open beta. The compile-time features are probably the language's "big idea", though I didn't find much use for them in Advent of Code.

    1 vote
  6. Comment on Day 25: Code Chronicle in ~comp.advent_of_code

    lily
    Link
    Glad for the easier one today. I wasn't able to solve yesterday's part 2 as I'm not familiar with adders and learning about them didn't feel like a fun way to spend my Christmas Eve. If anyone has...

    Glad for the easier one today. I wasn't able to solve yesterday's part 2 as I'm not familiar with adders and learning about them didn't feel like a fun way to spend my Christmas Eve. If anyone has any hints for that that don't entirely give the solution away, I would appreciate it. Until then, I'll have to leave this year's Advent of Code unfinished.

    As for today's puzzle, it was one of the easiest this year. I wasn't especially fast at solving it, unfortunately, but there really wasn't anything to solve about it - it was immediately clear what I was supposed to do. Most of my time was spent writing input parsing code.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 25: Code Chronicle
     */
    
    #import "Basic";
    #import "File";
    #import "String";
    
    main :: () {
        input, success := read_entire_file("inputs/day_25.txt");
        assert(success);
    
        groups := split(input, "\n\n");
        defer array_free(groups);
    
        keys: [..][5] int;
        locks: [..][5] int;
        defer array_free(keys);
        defer array_free(locks);
    
        for groups {
            lines := split(it, "\n");
            defer array_free(lines);
    
            if lines[0] == "....." {
                key: [5] int;
                for x: 0..4 {
                    height := 0;
                    for #v2 < y: 1..5 {
                        if lines[y][x] == #char "#" {
                            height += 1;
                        } else {
                            break;
                        }
                    }
    
                    key[x] = height;
                }
    
                array_add(*keys, key);
            } else {
                lock: [5] int;
                for x: 0..4 {
                    height := 0;
                    for y: 1..5 {
                        if lines[y][x] == #char "#" {
                            height += 1;
                        } else {
                            break;
                        }
                    }
    
                    lock[x] = height;
                }
    
                array_add(*locks, lock);
            }
        }
    
        fitting_pairs := 0;
        for key: keys {
            for lock: locks {
                fits := true;
                for 0..4 {
                    if key[it] + lock[it] > 5 {
                        fits = false;
                        break;
                    }
                }
    
                if fits {
                    fitting_pairs += 1;
                }
            }
        }
    
        print("Part 1: %\n", fitting_pairs);
    }
    
    1 vote
  7. Comment on Tildes Game Giveaway: Holiday 2024 in ~games

    lily
    Link Parent
    I've heard nothing but effusive praise for this game, so I figure I should give it a try eventually! One of my favorite childhood games was Super Mario Galaxy. I still don't think Nintendo has...

    I've heard nothing but effusive praise for this game, so I figure I should give it a try eventually!

    One of my favorite childhood games was Super Mario Galaxy. I still don't think Nintendo has surpassed it since (maybe in pure gameplay - but not in style or heart), so I would definitely still choose to gift it to a kid today.

    2 votes
  8. Comment on Day 23: LAN Party in ~comp.advent_of_code

    lily
    Link
    I do not know graph theory especially well, and of course Jai has no available libraries to do it for me, so I had to implement everything myself. Part 1 was easy enough, at least, though my...

    I do not know graph theory especially well, and of course Jai has no available libraries to do it for me, so I had to implement everything myself. Part 1 was easy enough, at least, though my original method for finding all 3-length cliques was pretty slow, and I had to go back afterwards to speed it up. I cannot claim to understand everything that's going on in part 2 all that well - I just searched online for algorithms and picked what seemed to be the most common one. Thankfully, implementing it wasn't too bad. The performance of part 2 could definitely stand to be improved a little, but I'm not sure how I would go about doing that, so I'll just leave it as-is.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 23: LAN Party
     */
    
    #import "Basic";
    #import "File";
    #import "Hash_Table";
    #import "Sort";
    #import "String";
    
    Connection_Table :: Table(string, [..] string);
    
    // Finds all maximal cliques using the Bron-Kerbosch algorithm.
    bron_kerbosch :: (
        r: [] string,
        p: [] string,
        x: [] string,
        connections: Connection_Table,
        cliques: *[..][..] string
    ) {
        if p.count == 0 && x.count == 0 {
            // We've found a maximal clique.
            clique: [..] string;
            array_copy(*clique, r);
            array_add(cliques, clique);
        }
    
        // p and x are passed as array views, but we need to modify them.
        mutable_p: [..] string;
        array_copy(*mutable_p, p);
        defer array_free(mutable_p);
    
        mutable_x: [..] string;
        array_copy(*mutable_x, x);
        defer array_free(mutable_x);
    
        for mutable_p {
            new_r: [..] string;
            new_p: [..] string;
            new_x: [..] string;
    
            defer array_free(new_r);
            defer array_free(new_p);
            defer array_free(new_x);
    
            array_copy(*new_r, r);
            array_add(*new_r, it);
    
            // The connections table is guaranteed to contain all computers.
            individual_connections, _ := table_find(*connections, it);
    
            for mutable_p {
                if array_find(individual_connections, it) {
                    array_add(*new_p, it);
                }
            }
    
            for mutable_x {
                if array_find(individual_connections, it) {
                    array_add(*new_x, it);
                }
            }
    
            bron_kerbosch(new_r, new_p, new_x, connections, cliques);
    
            array_add(*mutable_x, it);
            remove it;
        }
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_23.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        connections: Connection_Table;
        defer {
            for connections {
                array_free(it);
            }
    
            deinit(*connections);
        }
    
        for lines {
            if it == "" {
                continue;
            }
    
            computers := split(it, "-");
            defer array_free(computers);
    
            for computers {
                individual_connections := table_find_pointer(*connections, it);
                if individual_connections == null {
                    individual_connections: [..] string;
                    array_add(*individual_connections, computers[xx !it_index]);
                    table_add(*connections, it, individual_connections);
                } else {
                    array_add(individual_connections, computers[xx !it_index]);
                }
            }
        }
    
        computers: [..] string;
        defer array_free(computers);
    
        for connections {
            array_add(*computers, it_index);
        }
    
        triangles_with_t := 0;
        for computer_a, index_a: computers {
            connections_a, _ := table_find(*connections, computer_a);
            for computer_b: connections_a {
                _, index_b := array_find(computers, computer_b);
                if index_b <= index_a {
                    continue;
                }
    
                connections_b, _ := table_find(*connections, computer_b);
                for computer_c: connections_b {
                    _, index_c := array_find(computers, computer_c);
                    if index_c <= index_b {
                        continue;
                    }
    
                    if array_find(connections_a, computer_c) && (
                        computer_a[0] == #char "t"
                        || computer_b[0] == #char "t"
                        || computer_c[0] == #char "t"
                    ) {
                        triangles_with_t += 1;
                    }
                }
            }
        }
    
        print("Part 1: %\n", triangles_with_t);
    
        cliques: [..][..] string;
        defer {
            for cliques {
                array_free(it);
            }
    
            array_free(cliques);
        }
    
        bron_kerbosch(.[], computers, .[], connections, *cliques);
    
        lan_party: [..] string;
        largest_count := 0;
    
        for cliques {
            if it.count > largest_count {
                lan_party = it;
                largest_count = it.count;
            }
        }
    
        print("Part 2: %\n", join(.. quick_sort(lan_party, compare), ","));
    }
    
    1 vote
  9. Comment on Tildes Game Giveaway: Holiday 2024 in ~games

    lily
    Link Parent
    I'd be interested in TIS-100, provided the code works. I've been meaning to try some of the Zachtronics stuff.

    I'd be interested in TIS-100, provided the code works. I've been meaning to try some of the Zachtronics stuff.

    1 vote
  10. Comment on Tildes Game Giveaway: Holiday 2024 in ~games

    lily
    Link Parent
    Games (in order of preference): The Talos Principle 2, Windblown, Jusant Words: warmth, family, coziness, lights, calm Thank you for doing this!

    Games (in order of preference): The Talos Principle 2, Windblown, Jusant
    Words: warmth, family, coziness, lights, calm

    Thank you for doing this!

  11. Comment on Day 22: Monkey Market in ~comp.advent_of_code

    lily
    Link
    Part 2 took me a bit longer than I'd like, but I just barely made top 1000 for part 1, which was nice. The trick to solving part 2 in a reasonable amount of time was just to do everything in a...

    Part 2 took me a bit longer than I'd like, but I just barely made top 1000 for part 1, which was nice. The trick to solving part 2 in a reasonable amount of time was just to do everything in a single iteration, and store the price sums in a table to find the maximum of afterwards. I was glad for the slightly easier puzzle, after yesterday - though I'll admit I'm worried for what the last few days will bring.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 22: Monkey Market
     */
    
    #import "Basic";
    #import "File";
    #import "Hash_Table";
    #import "String";
    
    // There's no point making this an actual == operator; it's never used as such,
    // and doing so would make it harder to use in tables.
    change_sequences_are_equal :: inline (a: [4] int, b: [4] int) -> bool {
        return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3];
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_22.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        secrets: [..] int;
        defer array_free(secrets);
    
        for lines {
            if it != "" {
                array_add(*secrets, string_to_int(it));
            }
        }
    
        price_sums: Table(
            [4] int,
            int,
            given_compare_function = change_sequences_are_equal
        );
        defer deinit(*price_sums);
    
        secret_sum := 0;
        for secrets {
            prices: [2001] int;
            prices[0] = it % 10;
    
            for 1..2000 {
                secrets[it_index] = (
                    (secrets[it_index] * 64) ^ secrets[it_index]
                ) % 16777216;
    
                secrets[it_index] = (
                    (secrets[it_index] / 32) ^ secrets[it_index]
                ) % 16777216;
    
                secrets[it_index] = (
                    (secrets[it_index] * 2048) ^ secrets[it_index]
                ) % 16777216;
    
                prices[it] = secrets[it_index] % 10;
            }
    
            secret_sum += secrets[it_index];
    
            changes: [2000] int;
            for prices {
                if it_index > 0 {
                    changes[it_index - 1] = it - prices[it_index - 1];
                }
            }
    
            seen_sequences: Table(
                [4] int,
                void,
                given_compare_function = change_sequences_are_equal
            );
    
            for 1..1997 {
                sequence: [4] int;
                for array_view(changes, it - 1, 4) {
                    sequence[it_index] = it;
                }
    
                if !table_contains(*seen_sequences, sequence) {
                    current_price_sum, success := table_find(*price_sums, sequence);
                    if success {
                        table_set(
                            *price_sums,
                            sequence,
                            current_price_sum + prices[it + 3]
                        );
                    } else {
                        table_add(*price_sums, sequence, prices[it + 3]);
                    }
    
                    table_add(*seen_sequences, sequence, #run {});
                }
            }
        }
    
        print("Part 1: %\n", secret_sum);
    
        max_price_sum := 0;
        for price_sums {
            if it > max_price_sum {
                max_price_sum = it;
            }
        }
    
        print("Part 2: %\n", max_price_sum);
    }
    
    2 votes
  12. Comment on Day 21: Keypad Conundrum in ~comp.advent_of_code

    lily
    Link
    Well, it took me a good while, but I finally got this working. Correctly generating the most efficient paths between buttons was one of the sticking points for me - the priorities of each...

    Well, it took me a good while, but I finally got this working. Correctly generating the most efficient paths between buttons was one of the sticking points for me - the priorities of each direction were difficult to wrap my head around. Part 2 wasn't too bad once I realized there was no feasible way to work with the strings directly, luckily. A good majority of my time was spent on part 1. I'm not especially happy with my final solution (I think it's quite messy and could have been structured much better), but I do not want to work on this puzzle any further.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 21: Keypad Conundrum
     */
    
    #import "Basic";
    #import "File";
    #import "Hash";
    #import "Hash_Table";
    #import "Math";
    #import "String";
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    State :: struct {
        code: string;
        depth: int;
    }
    
    Button_Table :: Table(u8, Vector2_Int);
    
    Path_Table :: Table(
        [2] u8,
        [..] u8,
        given_compare_function = (a: [2] u8, b: [2] u8) -> bool {
            return a[0] == b[0] && a[1] == b[1];
        }
    );
    
    Length_Table :: Table(
        State,
        int,
        (state: State) -> u32 {
            return inline sdbm_hash(xx *state, size_of(State));
        },
        (a: State, b: State) -> bool {
            return a.code == b.code && a.depth == b.depth;
        }
    );
    
    operator == :: inline (a: Vector2_Int, b: Vector2_Int) -> bool {
        return a.x == b.x && a.y == b.y;
    }
    
    sign :: inline (n: int) -> int {
        if n < 0 return -1;
        if n > 0 return 1;
        return 0;
    }
    
    get_path :: (
        start: Vector2_Int,
        end: Vector2_Int,
        gap: Vector2_Int
    ) -> [..] u8 {
        path: [..] u8;
    
        if start != end {
            char_x: u8 = xx (
                ifx sign(end.x - start.x) == -1 then #char "<" else #char ">"
            );
    
            char_y: u8 = xx (
                ifx sign(end.y - start.y) == -1 then #char "^" else #char "v"
            );
    
            // This is a little convoluted, but makes sure we're using the correct
            // direction priorities.
            if char_x == #char "<" {
                if Vector2_Int.{end.x, start.y} == gap {
                    for 1..abs(end.y - start.y) array_add(*path, char_y);
                    for 1..abs(end.x - start.x) array_add(*path, char_x);
                } else {
                    for 1..abs(end.x - start.x) array_add(*path, char_x);
                    for 1..abs(end.y - start.y) array_add(*path, char_y);
                }
            } else if Vector2_Int.{start.x, end.y} == gap {
                for 1..abs(end.x - start.x) array_add(*path, char_x);
                for 1..abs(end.y - start.y) array_add(*path, char_y);
            } else {
                for 1..abs(end.y - start.y) array_add(*path, char_y);
                for 1..abs(end.x - start.x) array_add(*path, char_x);
            }
        }
    
        array_add(*path, #char "A");
        return path;
    }
    
    get_paths :: (buttons: Button_Table, gap: Vector2_Int) -> Path_Table {
        paths: Path_Table;
        for first_position, first: buttons {
            for second_position, second: buttons {
                table_add(*paths, .[first, second], get_path(
                    first_position,
                    second_position,
                    gap
                ));
            }
        }
    
        return paths;
    }
    
    free_paths :: (paths: *Path_Table) {
        for paths {
            array_free(it);
        }
    
        deinit(paths);
    }
    
    get_sequence :: (code: string, depth: int, paths: *Path_Table) -> string {
        sequence: [..] u8;
        previous: u8 = #char "A";
    
        for 0..code.count - 1 {
            path, _ := table_find(paths, .[previous, code[it]]);
            array_add(*sequence, .. path);
            previous = code[it];
        }
    
        return xx sequence;
    }
    
    get_sequence_length :: (
        code: string,
        depth: int,
        paths: *Path_Table,
        known_lengths: *Length_Table
    ) -> int {
        state := State.{code, depth};
        known_length, success := table_find(known_lengths, state);
    
        if success {
            return known_length;
        }
    
        length := 0;
        previous: u8 = #char "A";
    
        for code {
            path, _ := table_find(paths, .[previous, it]);
    
            if depth > 1 {
                length += get_sequence_length(
                    xx path,
                    depth - 1,
                    paths,
                    known_lengths
                );
            } else {
                length += path.count;
            }
    
            previous = it;
        }
    
        table_add(known_lengths, state, length);
        return length;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_21.txt");
        assert(success);
    
        codes := split(input, "\n");
        pop(*codes);
        defer array_free(codes);
    
        // Might as well run all this at compile time. These don't change based on
        // the input.
        numeric_buttons :: #run () -> Button_Table {
            buttons: Button_Table;
            table_add(*buttons, #char "0", .{1, 3});
            table_add(*buttons, #char "1", .{0, 2});
            table_add(*buttons, #char "2", .{1, 2});
            table_add(*buttons, #char "3", .{2, 2});
            table_add(*buttons, #char "4", .{0, 1});
            table_add(*buttons, #char "5", .{1, 1});
            table_add(*buttons, #char "6", .{2, 1});
            table_add(*buttons, #char "7", .{0, 0});
            table_add(*buttons, #char "8", .{1, 0});
            table_add(*buttons, #char "9", .{2, 0});
            table_add(*buttons, #char "A", .{2, 3});
    
            return buttons;
        }();
    
        directional_buttons :: #run () -> Button_Table {
            buttons: Button_Table;
            table_add(*buttons, #char "<", .{0, 1});
            table_add(*buttons, #char ">", .{2, 1});
            table_add(*buttons, #char "^", .{1, 0});
            table_add(*buttons, #char "v", .{1, 1});
            table_add(*buttons, #char "A", .{2, 0});
    
            return buttons;
        }();
    
        numeric_gap :: Vector2_Int.{0, 3};
        directional_gap :: Vector2_Int.{0, 0};
    
        numeric_paths := get_paths(numeric_buttons, numeric_gap);
        directional_paths := get_paths(directional_buttons, directional_gap);
        defer free_paths(*numeric_paths);
        defer free_paths(*directional_paths);
    
        directional_start :: Vector2_Int.{2, 0};
    
        complexity_sum_part_1 := 0;
        complexity_sum_part_2 := 0;
    
        known_lengths: Length_Table;
        defer deinit(*known_lengths);
    
        for codes {
            // I don't think there's a way around generating the actual sequence for
            // the robot using the numeric keypad - at least, not without massively
            // complicating the get_sequence_length() procedure by adding the
            // numeric keypad as a base case. Besides, doing it this way means we
            // only need to find the sequence once.
            first_sequence := get_sequence(it, 0, *numeric_paths);
            numeric_part := string_to_int(slice(it, 0, 3));
    
            complexity_sum_part_1 += get_sequence_length(
                first_sequence,
                2,
                *directional_paths,
                *known_lengths
            ) * numeric_part;
    
            complexity_sum_part_2 += get_sequence_length(
                first_sequence,
                25,
                *directional_paths,
                *known_lengths
            ) * numeric_part;
        }
    
        print(
            "Part 1: %\nPart 2: %\n",
            complexity_sum_part_1,
            complexity_sum_part_2
        );
    }
    
    2 votes
  13. Comment on Day 20: Race Condition in ~comp.advent_of_code

    lily
    (edited )
    Link
    Took some time to make this a little faster. My original solution ran BFS individually for every position on the grid - twice - to fill the distance tables, which was incredibly inefficient. I...

    Took some time to make this a little faster. My original solution ran BFS individually for every position on the grid - twice - to fill the distance tables, which was incredibly inefficient. I eventually realized I could fill each table with only one traversal, by just allowing BFS to search the whole track. Whereas my original solution took around 6 seconds, my new one runs nearly instantaneously.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 20: Race Condition
     */
    
    #import "Basic";
    #import "File";
    #import "Flat_Pool";
    #import "Hash";
    #import "Hash_Table";
    #import "Math";
    #import "String";
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    Node :: struct {
        cost: int;
        position: Vector2_Int;
    }
    
    Vector2_Int_Set :: Table(
        Vector2_Int,
        void,
        (vector: Vector2_Int) -> u32 {
            return inline sdbm_hash(xx *vector, size_of(Vector2_Int));
        },
        #procedure_of_call(operator ==(Vector2_Int.{}, Vector2_Int.{}))
    );
    
    operator == :: (a: Vector2_Int, b: Vector2_Int) -> bool {
        return a.x == b.x && a.y == b.y;
    }
    
    // Traverse the whole track and fill a (flattened two-dimensional) array with
    // the distances from a given start position to every other position.
    fill_distances_array :: (
        track: [][..] bool,
        width: int,
        start: Vector2_Int,
        distances: [] int
    ) {
        queue: [..] *Node;
        defer array_free(queue);
    
        start_node := New(Node);
        start_node.position = start;
        array_add(*queue, start_node);
    
        seen: Vector2_Int_Set;
        table_add(*seen, start, #run {});
        defer deinit(*seen);
    
        while queue.count {
            current := queue[0];
            array_ordered_remove_by_index(*queue, 0);
    
            index := current.position.y * width + current.position.x;
            if distances[index] == 0 {
                distances[index] = current.cost;
            }
    
            for Vector2_Int.[
                .{current.position.x - 1, current.position.y},
                .{current.position.x + 1, current.position.y},
                .{current.position.x, current.position.y - 1},
                .{current.position.x, current.position.y + 1}
            ] {
                if !track[it.y][it.x] && !table_contains(*seen, it) {
                    new_node := New(Node);
                    new_node.cost = current.cost + 1;
                    new_node.position = it;
    
                    array_add(*queue, new_node);
                    table_add(*seen, it, #run {});
                }
            }
        }
    }
    
    find_efficient_cheats :: (
        track: [][..] bool,
        width: int,
        height: int,
        fastest_time: int,
        distances_from_start: [] int,
        distances_to_end: [] int,
        cheat_distance: int
    ) -> int {
        efficient_cheats := 0;
        for x1: 1..width - 2 {
            for y1: 1..height - 2 {
                for x2: x1 - cheat_distance..x1 + cheat_distance {
                    for y2: y1 - cheat_distance..y1 + cheat_distance {
                        if
                            x2 <= 0 || x2 >= width - 1
                            || y2 <= 0 || y2 >= height - 1
                        {
                            continue;
                        }
    
                        distance := abs(x2 - x1) + abs(y2 - y1);
                        if
                            distance <= cheat_distance
                            && !track[y1][x1] && !track[y2][x2]
                            && fastest_time - (
                                distances_from_start[y1 * width + x1]
                                + distance
                                + distances_to_end[y2 * width + x2]
                            ) >= 100
                        {
                            efficient_cheats += 1;
                        }
                    }
                }
            }
        }
    
        return efficient_cheats;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_20.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        track: [..][..] bool;
        defer {
            for track {
                array_free(it);
            }
    
            array_free(track);
        }
    
        start, end: Vector2_Int;
    
        for line, y: lines {
            if line == "" {
                continue;
            }
    
            row: [..] bool;
            for char, x: line {
                array_add(*row, char == #char "#");
    
                if char == #char "S" {
                    start = .{x, y};
                } else if char == #char "E" {
                    end = .{x, y};
                }
            }
    
            array_add(*track, row);
        }
    
        width := track[0].count;
        height := track.count;
    
        distances_size := width * height;
        distances_from_start := NewArray(distances_size, int);
        distances_to_end := NewArray(distances_size, int);
        defer array_free(distances_from_start);
        defer array_free(distances_to_end);
    
        // Yet again, it's faster to use a pool when we're doing a bunch of graph
        // traversal (though the performance gain in this case is probably
        // negligable, since we're only running BFS twice).
        pool: Flat_Pool;
        defer reset(*pool);
    
        new_context := context;
        new_context.allocator.proc = flat_pool_allocator_proc;
        new_context.allocator.data = *pool;
    
        push_context new_context {
            fill_distances_array(track, width, start, distances_from_start);
            fill_distances_array(track, width, end, distances_to_end);
        }
    
        fastest_time := distances_to_end[start.y * width + start.x];
        print(
            "Part 1: %\nPart 2: %\n",
            find_efficient_cheats(
                track,
                width,
                height,
                fastest_time,
                distances_from_start,
                distances_to_end,
                2
            ),
            find_efficient_cheats(
                track,
                width,
                height,
                fastest_time,
                distances_from_start,
                distances_to_end,
                20
            )
        );
    }
    
    4 votes
  14. Comment on Day 19: Linen Layout in ~comp.advent_of_code

    lily
    (edited )
    Link
    Still in the cooldown period, I see. Didn't mind the easier puzzle, though. The only "trick" to this was using memoization so it could finish in a reasonable time. Solution (Jai) /* Advent of Code...

    Still in the cooldown period, I see. Didn't mind the easier puzzle, though. The only "trick" to this was using memoization so it could finish in a reasonable time.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 19: Linen Layout
     */
    
    #import "Basic";
    #import "File";
    #import "Hash_Table";
    #import "String";
    
    get_solution :: (
        design: string,
        towels: [] string,
        known_solutions: *Table(string, int)
    ) -> int {
        known_solution, success := table_find(known_solutions, design);
        if success {
            return known_solution;
        }
    
        solution := 0;
        for towels {
            if design == it {
                solution += 1;
            } else if begins_with(design, it) {
                solution += get_solution(
                    slice(design, it.count, design.count - it.count),
                    towels,
                    known_solutions
                );
            }
        }
    
        table_add(known_solutions, design, solution);
        return solution;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_19.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        towels := split(lines[0], ", ");
        defer array_free(towels);
    
        designs := array_view(lines, 2, lines.count - 3);
    
        // I didn't want this to be global, so I'm passing a pointer to it into the
        // get_solutions() procedure.
        known_solutions: Table(string, int);
        defer deinit(*known_solutions);
    
        possible_designs := 0;
        total_solutions := 0;
    
        for designs {
            solutions := get_solution(it, towels, *known_solutions);
    
            if solutions > 0 {
                possible_designs += 1;
            }
    
            total_solutions += solutions;
        }
    
        print("Part 1: %\nPart 2: %\n", possible_designs, total_solutions);
    }
    
    3 votes
  15. Comment on Day 18: RAM Run in ~comp.advent_of_code

    lily
    Link
    This was a nice breather after yesterday - I definitely should've solved it quicker than I did, but I got caught up on a couple annoying bugs. Just another simple pathfinding problem, though this...

    This was a nice breather after yesterday - I definitely should've solved it quicker than I did, but I got caught up on a couple annoying bugs. Just another simple pathfinding problem, though this one was even easier since it wasn't weighted (so I didn't have to bother with Djikstra). My solution for part 2 is incredibly lazy and pretty slow, but I don't really feel up to optimizing it further right now.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 18: RAM Run
     */
    
    #import "Basic";
    #import "File";
    #import "Flat_Pool";
    #import "Hash";
    #import "Hash_Table";
    #import "String";
    
    // These need to be configurable to make using the sample input easy.
    GRID_SIZE :: 71;
    PART_1_BYTES :: 1024;
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    Node :: struct {
        parent: *Node;
        position: Vector2_Int;
    }
    
    Vector2_Int_Set :: Table(
        Vector2_Int,
        void,
        (vector: Vector2_Int) -> u32 {
            return inline sdbm_hash(xx *vector, size_of(Vector2_Int));
        },
        #procedure_of_call(operator ==(Vector2_Int.{}, Vector2_Int.{}))
    );
    
    operator == :: (a: Vector2_Int, b: Vector2_Int) -> bool {
        return a.x == b.x && a.y == b.y;
    }
    
    solve_grid :: (grid: [GRID_SIZE][GRID_SIZE] bool) -> int {
        queue: [..] *Node;
        array_add(*queue, New(Node));
        defer array_free(queue);
    
        seen: Vector2_Int_Set;
        table_add(*seen, .{0, 0}, #run {});
        defer deinit(*seen);
    
        while queue.count {
            current := queue[0];
            array_ordered_remove_by_index(*queue, 0);
    
            if current.position == .{GRID_SIZE - 1, GRID_SIZE - 1} {
                path_length := 0;
                while current != null {
                    path_length += 1;
                    current = current.parent;
                }
    
                return path_length - 1;
            }
    
            for Vector2_Int.[
                .{current.position.x - 1, current.position.y},
                .{current.position.x + 1, current.position.y},
                .{current.position.x, current.position.y - 1},
                .{current.position.x, current.position.y + 1}
            ] {
                if
                    it.x >= 0 && it.x < GRID_SIZE && it.y >= 0 && it.y < GRID_SIZE
                    && !grid[it.y][it.x]
                    && !table_contains(*seen, it)
                {
                    new_node := New(Node);
                    new_node.parent = current;
                    new_node.position = it;
    
                    array_add(*queue, new_node);
                    table_add(*seen, it, #run {});
                }
            }
        }
    
        return -1;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_18.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        grid: [GRID_SIZE][GRID_SIZE] bool;
        dropped_bytes := 0;
    
        for lines {
            position := split(it, ",");
            defer array_free(position);
    
            grid[string_to_int(position[1])][string_to_int(position[0])] = true;
    
            dropped_bytes += 1;
            if (dropped_bytes == PART_1_BYTES) {
                break;
            }
        }
    
        // Using a pool saves us time doing memory allocations in the solving
        // procedure. It's not a huge performance gain, but it's just about
        // noticeable.
        pool: Flat_Pool;
        defer reset(*pool);
    
        context.allocator.proc = flat_pool_allocator_proc;
        context.allocator.data = *pool;
    
        print("Part 1: %\n", solve_grid(grid));
    
        for dropped_bytes..lines.count - 2 {
            position := split(lines[it], ",");
            defer array_free(position);
    
            grid[string_to_int(position[1])][string_to_int(position[0])] = true;
    
            if solve_grid(grid) == -1 {
                print("Part 2: %\n", lines[it]);
                break;
            } else {
                // We'll run out of memory if we don't do this. With a smaller input
                // this might not be required, but for the size we're given the
                // program will crash if we don't do it.
                reset(*pool);
            }
        }
    }
    
    1 vote
  16. Comment on Day 17: Chronospatial Computer in ~comp.advent_of_code

    lily
    (edited )
    Link
    I managed to finish this with only about five minutes left before midnight - phew! I'm posting the solution now since I had to immediately move on to tonight's actual puzzle. It took me quite a...

    I managed to finish this with only about five minutes left before midnight - phew! I'm posting the solution now since I had to immediately move on to tonight's actual puzzle.

    It took me quite a while to finally realize I should be looking at what the program actually did, and once I saw what it was doing it didn't take too long to figure out a solution (just bruteforce three bits at a time). I ended up having to use BFS for the bruteforcing, since otherwise it seemed it kept getting stuck on false leads.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 17: Chronospatial Computer
     */
    
    #import "Basic";
    #import "File";
    #import "String";
    
    Registers :: struct {
        a, b, c: int;
    }
    
    State :: struct {
        register_a, iteration: int;
    }
    
    resolve_combo_operand :: (operand: u8, registers: Registers) -> int {
        if operand == {
            case 4; return registers.a;
            case 5; return registers.b;
            case 6; return registers.c;
            case;   return operand;
        }
    }
    
    run_program :: (initial_registers: Registers, program: [] u8) -> [..] int {
        registers := initial_registers;
        instruction_pointer := 0;
        output: [..] int;
    
        while instruction_pointer < program.count {
            opcode := program[instruction_pointer];
            operand := program[instruction_pointer + 1];
            instruction_pointer += 2;
    
            // I don't personally find the instruction names any more readable than
            // the plain opcodes, so I didn't bother using an enum.
            if opcode == {
                case 0;
                    registers.a = (
                        registers.a
                        / 1 << resolve_combo_operand(operand, registers)
                    );
    
                case 1;
                    registers.b = registers.b ^ operand;
    
                case 2;
                    registers.b = resolve_combo_operand(operand, registers) % 8;
    
                case 3;
                    if registers.a != 0 {
                        instruction_pointer = operand;
                    }
    
                case 4;
                    registers.b = registers.b ^ registers.c;
    
                case 5;
                    array_add(
                        *output,
                        resolve_combo_operand(operand, registers) % 8
                    );
    
                case 6;
                    registers.b = (
                        registers.a
                        / 1 << resolve_combo_operand(operand, registers)
                    );
    
                case 7;
                    registers.c = (
                        registers.a
                        / 1 << resolve_combo_operand(operand, registers)
                    );
            }
        }
    
        return output;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_17.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        program_strings := split(slice(lines[4], 9, lines[4].count - 9), ",");
        defer array_free(program_strings);
    
        program: [..] u8;
        defer array_free(program);
    
        for program_strings {
            array_add(*program, xx string_to_int(it));
        }
    
        output := run_program(.{
            a = string_to_int(slice(lines[0], 12, lines[0].count - 12)),
            b = string_to_int(slice(lines[1], 12, lines[1].count - 12)),
            c = string_to_int(slice(lines[2], 12, lines[2].count - 12))
        }, program);
        defer array_free(output);
    
        builder: String_Builder;
        defer reset(*builder);
    
        for output {
            print_to_builder(*builder, "%", it);
            if it_index < output.count - 1 {
                append(*builder, ",");
            }
        }
    
        print("Part 1: %\n", builder_to_string(*builder));
    
        // Every three bits given to the program add another number to the output.
        // Because of this, we can bruteforce three bits at a time. We're using BFS
        // here instead of just bruteforcing the groups of bits in sequence because
        // occasionally we may run into a false lead.
        queue: [..] State;
        array_add(*queue, .{0, 0});
        defer array_free(queue);
    
        while queue.count {
            current := queue[0];
            array_ordered_remove_by_index(*queue, 0);
    
            target := program[program.count - current.iteration - 1];
            for 0..7 {
                new_register_a := current.register_a << 3 | it;
                registers := Registers.{new_register_a, 0, 0};
    
                output := run_program(registers, program);
                defer array_free(output);
    
                if output[0] == target {
                    if current.iteration == program.count - 1 {
                        print("Part 2: %\n", new_register_a);
                        return;
                    } else {
                        array_add(*queue, .{registers.a, current.iteration + 1});
                    }
                }
            }
        }
    }
    
    1 vote
  17. Comment on Day 16: Reindeer Maze in ~comp.advent_of_code

    lily
    Link
    I was waiting for the annual pathfinding puzzle! I thought ahead and wrote a quick binary heap implementation before Advent of Code started this year, since the Jai compiler doesn't come with one...

    I was waiting for the annual pathfinding puzzle! I thought ahead and wrote a quick binary heap implementation before Advent of Code started this year, since the Jai compiler doesn't come with one and I knew I'd need one at some point. Today's puzzle was pretty straightforward as far as pathfinding puzzles go - the only tricky part was making the seen table work properly with part 2.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 16: Reindeer Maze
     */
    
    #import "Basic";
    #import "Binary_Heap";
    #import "File";
    #import "Hash";
    #import "Hash_Table";
    #import "String";
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    Node :: struct {
        parent: *Node;
        cost: int;
        position: Vector2_Int;
    }
    
    // This is used for the table of seen states. The direction isn't stored in Node
    // because it is inferred from the parent's position, but we have to store it
    // here.
    Reindeer_State :: struct {
        position, direction: Vector2_Int;
    }
    
    operator == :: (a: Vector2_Int, b: Vector2_Int) -> bool {
        return a.x == b.x && a.y == b.y;
    }
    
    array_free_2d :: (array: [..][..] $T) {
        for array {
            array_free(it);
        }
    
        array_free(array);
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_16.txt");
        assert(success);
    
        lines := split(input, "\n");
        defer array_free(lines);
    
        map: [..][..] bool;
        defer array_free_2d(map);
    
        start, end: Vector2_Int;
        for line, y: lines {
            if line == "" {
                continue;
            }
    
            row: [..] bool;
            for char, x: line {
                array_add(*row, char == #char "#");
    
                if char == #char "S" {
                    start = .{x, y};
                } else if char == #char "E" {
                    end = .{x, y};
                }
            }
    
            array_add(*map, row);
        }
    
        width := map[0].count;
        height := map.count;
    
        heap: Heap(*Node, (left: *Node, right: *Node) -> bool {
            return left.cost <= right.cost;
        });
    
        seen: Table(
            Reindeer_State,
            int,
            (state: Reindeer_State) -> u32 {
                return inline sdbm_hash(xx *state, size_of(Reindeer_State));
            },
            (a: Reindeer_State, b: Reindeer_State) -> bool {
                return a.position == b.position && a.direction == b.direction;
            }
        );
    
        defer heap_free(heap);
        defer deinit(*seen);
    
        start_node := New(Node);
        start_node.position = start;
        heap_add(*heap, start_node);
        table_set(*seen, .{start_node.position, .{1, 0}}, 0);
    
        minimum_cost := -1;
    
        best_paths: [..][..] Vector2_Int;
        defer array_free_2d(best_paths);
    
        while !heap_is_empty(heap) {
            current := heap_pop(*heap);
    
            if current.position == end {
                if minimum_cost == -1 {
                    minimum_cost = current.cost;
                    print("Part 1: %\n", minimum_cost);
                }
    
                if current.cost == minimum_cost {
                    path: [..] Vector2_Int;
                    while current != null {
                        array_insert_at(*path, current.position, 0);
                        current = current.parent;
                    }
    
                    array_add(*best_paths, path);
                    continue;
                } else {
                    // We're using Djikstra's algorithm, so we know there will be no
                    // more best paths after this.
                    break;
                }
            }
    
            for Vector2_Int.[.{-1, 0}, .{1, 0}, .{0, -1}, .{0, 1}] {
                last_direction: Vector2_Int;
                if current.parent == null {
                    last_direction = .{1, 0};
                } else {
                    last_direction = .{
                        current.position.x - current.parent.position.x,
                        current.position.y - current.parent.position.y
                    };
                }
    
                backwards: bool;
                if      last_direction == .{-1, 0} backwards = (it == .{1, 0});
                else if last_direction == .{1, 0}  backwards = (it == .{-1, 0});
                else if last_direction == .{0, -1} backwards = (it == .{0, 1});
                else if last_direction == .{0, 1}  backwards = (it == .{0, -1});
    
                new_cost := (
                    current.cost + ifx last_direction == it then 1 else 1001
                );
    
                new_position := Vector2_Int.{
                    current.position.x + it.x,
                    current.position.y + it.y
                };
    
                new_state := Reindeer_State.{new_position, last_direction};
                seen_cost, success := table_find(*seen, new_state);
    
                // No need for bounds checking; the map is completely surrounded by
                // walls.
                if
                    !map[new_position.y][new_position.x]
                    && !backwards
                    && (!success || seen_cost >= new_cost)
                {
                    new := New(Node);
                    memcpy(new, current, size_of(Node));
    
                    new.parent = current;
                    new.cost = new_cost;
                    new.position = new_position;
    
                    heap_add(*heap, new);
                    table_set(*seen, new_state, new_cost);
                }
            }
        }
    
        places_to_sit: Table(
            Vector2_Int,
            void,
            (vector: Vector2_Int) -> u32 {
                return inline sdbm_hash(xx *vector, size_of(Vector2_Int));
            },
            #procedure_of_call(operator ==(Vector2_Int.{}, Vector2_Int.{}))
        );
    
        defer deinit(*places_to_sit);
    
        for best_paths {
            for it {
                if !table_contains(*places_to_sit, it) {
                    table_add(*places_to_sit, it, #run {});
                }
            }
        }
    
        print("Part 2: %\n", places_to_sit.count);
    }
    
    Binary heap
    // order_test is used to determine the order of the values in the queue. It
    // should return whether the first passed value should be given higher priority
    // than the second passed value.
    Heap :: struct (
        Value_Type: Type,
        order_test: (Value_Type, Value_Type) -> bool
    ) {
        values: [..] Value_Type;
    }
    
    heap_add :: (heap: *Heap, value: heap.Value_Type) {
        array_add(*heap.values, value);
    
        current_index := heap.values.count - 1;
        while current_index > 0 {
            parent_index := (current_index - 1) / 2;
    
            current_value := heap.values[current_index];
            parent_value := heap.values[parent_index];
    
            if heap.order_test(parent_value, current_value) {
                break;
            }
    
            heap.values[current_index] = parent_value;
            heap.values[parent_index] = current_value;
            current_index = parent_index;
        }
    }
    
    heap_pop :: (heap: *Heap) -> heap.Value_Type {
        assert(!heap_is_empty(heap), "cannot pop from an empty heap");
    
        root_value := heap.values[0];
        heap.values[0] = pop(*heap.values);
    
        current_index := 0;
        while true {
            left_index := current_index * 2 + 1;
            right_index := left_index + 1;
    
            has_left := left_index < heap.values.count;
            has_right := right_index < heap.values.count;
    
            if !has_left && !has_right {
                break;
            }
    
            child_index: int;
            if !has_left {
                child_index = right_index;
            } else if !has_right {
                child_index = left_index;
            } else if heap.order_test(
                heap.values[left_index],
                heap.values[right_index]
            ) {
                child_index = left_index;
            } else {
                child_index = right_index;
            }
    
            current_value := heap.values[current_index];
            child_value := heap.values[child_index];
    
            if heap.order_test(current_value, child_value) {
                break;
            }
    
            heap.values[current_index] = child_value;
            heap.values[child_index] = current_value;
            current_index = child_index;
        }
    
        return root_value;
    }
    
    // These last two procedures don't require the heap be passed as a pointer,
    // since they do not modify it.
    heap_is_empty :: inline (heap: Heap) -> bool {
        return heap.values.count == 0;
    }
    
    heap_free :: inline (heap: Heap) {
        for heap.values {
            free(it);
        }
    
        array_free(heap.values);
    }
    
    #scope_file
    
    #import "Basic";
    
    1 vote
  18. Comment on Day 15: Warehouse Woes in ~comp.advent_of_code

    lily
    Link
    I agree with everyone else here. This was a bit tricky to implement, but I understood what I was supposed to be doing almost instantly. I did get tripped up on edge cases a bit for part 2, though,...

    I agree with everyone else here. This was a bit tricky to implement, but I understood what I was supposed to be doing almost instantly. I did get tripped up on edge cases a bit for part 2, though, and I think my final solution is one of the worst I've written this year so far - it's overly long and hard to parse. Luckily, it seems to be more than performant enough.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 15: Warehouse Woes
     */
    
    #import "Basic";
    #import "File";
    #import "String";
    
    Tile :: enum {
        EMPTY;
        WALL;
        BOX;
        BOX_LEFT;
        BOX_RIGHT;
    }
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    array_free_2d :: (array: [..][..] $T) {
        for array {
            array_free(it);
        }
    
        array_free(array);
    }
    
    is_box :: inline (tile: Tile) -> bool {
        return tile == .BOX || tile == .BOX_LEFT || tile == .BOX_RIGHT;
    }
    
    // Moves a box in a given direction. Does not perform any validity checks; that
    // is all handled by push_box(). The inputs to this procedure are assumed to be
    // valid.
    move_box :: inline (
        map: [..][..] Tile,
        position: Vector2_Int,
        direction: Vector2_Int
    ) {
        tile := map[position.y][position.x];
        map[position.y][position.x] = .EMPTY;
        map[position.y + direction.y][position.x + direction.x] = tile;
    }
    
    // Attempts to push a box in a given direction, along with all boxes in its way.
    // Returns whether the push was successful.
    //
    // If should_push is false, the procedure will return whether the push is
    // possible, but will not actually move the boxes. This is necessary for part 2,
    // where we have to check whether multiple pushes are possible before pushing
    // some boxes (and thus don't want a possible push to still physically happen if
    // the other push isn't possible).
    push_box :: (
        map: [..][..] Tile,
        position: Vector2_Int,
        direction: Vector2_Int,
        should_push := true
    ) -> bool {
        tile := map[position.y][position.x];
        if direction.x == 0 && (tile == .BOX_LEFT || tile == .BOX_RIGHT) {
            other_side := ifx tile == .BOX_LEFT then 1 else -1;
            next_tile_1 := map[position.y + direction.y][position.x];
            next_tile_2 := map[position.y + direction.y][position.x + other_side];
    
            aligned_with_next_box := (
                tile == .BOX_LEFT && next_tile_1 == .BOX_LEFT
                || tile == .BOX_RIGHT && next_tile_2 == .BOX_LEFT
            );
    
            if
                aligned_with_next_box && push_box(map, .{
                    position.x + ifx tile == .BOX_RIGHT then -1 else 0,
                    position.y + direction.y
                }, direction, false)
                || (
                    next_tile_1 == .EMPTY || is_box(next_tile_1) && push_box(map, .{
                        position.x,
                        position.y + direction.y
                    }, direction, false)
                ) && (
                    next_tile_2 == .EMPTY || is_box(next_tile_2) && push_box(map, .{
                        position.x + other_side,
                        position.y + direction.y
                    }, direction, false)
                )
            {
                if should_push {
                    // I'm not a huge fan of calling push_box() a second time here,
                    // but without writing a separate can_push_box() procedure for
                    // above (which would heavily complicate things), I don't think
                    // there's a way around it. Either way, there are no performance
                    // issues in the current version of the program, so it luckily
                    // doesn't seem like much of a problem.
                    if aligned_with_next_box {
                        push_box(map, .{
                            position.x + ifx tile == .BOX_RIGHT then -1 else 0,
                            position.y + direction.y
                        }, direction);
                    } else {
                        if next_tile_1 != .EMPTY {
                            push_box(map, .{
                                position.x,
                                position.y + direction.y
                            }, direction);
                        }
    
                        if next_tile_2 != .EMPTY {
                            push_box(map, .{
                                position.x + other_side,
                                position.y + direction.y
                            }, direction);
                        }
                    }
    
                    move_box(map, position, direction);
                    move_box(map, .{position.x + other_side, position.y}, direction);
                }
    
                return true;
            }
        } else {
            next_tile := map[position.y + direction.y][position.x + direction.x];
            if next_tile == .EMPTY || is_box(next_tile) && push_box(map, .{
                position.x + direction.x,
                position.y + direction.y
            }, direction, should_push) {
                if should_push {
                    move_box(map, position, direction);
                }
    
                return true;
            }
        }
    
        return false;
    }
    
    // Performs a series of instructions on a robot in a warehouse, then calculates
    // the sum of the GPS coordinates of the boxes in the warehouse after the robot
    // has finished moving.
    //
    // We don't have to bother with bounds checking, because the warehouse is
    // surrounded on all sides with walls.
    get_gps_sum :: (
        map: [][..] Tile,
        robot: Vector2_Int,
        instructions: [] Vector2_Int
    ) -> int {
        // The map and robot parameters are immutable (since we don't want to modify
        // them outside this function), so since we need to be able to modify them
        // in this function, we thus need to make copies of them.
        mutable_map: [..][..] Tile;
        defer array_free_2d(mutable_map);
    
        for map {
            row: [..] Tile;
            array_copy(*row, it);
            array_add(*mutable_map, row);
        }
    
        mutable_robot := Vector2_Int.{robot.x, robot.y};
    
        for instructions {
            new_position := Vector2_Int.{
                mutable_robot.x + it.x,
                mutable_robot.y + it.y
            };
    
            new_tile := mutable_map[new_position.y][new_position.x];
            if new_tile == .EMPTY || is_box(new_tile) && push_box(
                mutable_map,
                new_position,
                it
            ) {
                mutable_robot = new_position;
            }
        }
    
        gps_sum := 0;
        for row, y: mutable_map {
            for tile, x: row {
                if tile == .BOX || tile == .BOX_LEFT {
                    gps_sum += x + y * 100;
                }
            }
        }
    
        return gps_sum;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_15.txt");
        assert(success);
    
        groups := split(input, "\n\n");
        defer array_free(groups);
    
        map_lines := split(groups[0], "\n");
        defer array_free(map_lines);
    
        map: [..][..] Tile;
        defer array_free_2d(map);
    
        robot: Vector2_Int;
        for line, y: map_lines {
            row: [..] Tile;
            for char, x: line {
                if char == {
                    case #char "@"; robot = .{x, y}; #through;
                    case #char "."; array_add(*row, .EMPTY);
                    case #char "#"; array_add(*row, .WALL);
                    case #char "O"; array_add(*row, .BOX);
                }
            }
    
            array_add(*map, row);
        }
    
        instructions: [..] Vector2_Int;
        defer array_free(instructions);
    
        for groups[1] {
            if it == {
                case #char "<"; array_add(*instructions, .{-1, 0});
                case #char ">"; array_add(*instructions, .{1, 0});
                case #char "^"; array_add(*instructions, .{0, -1});
                case #char "v"; array_add(*instructions, .{0, 1});
            }
        }
    
        print("Part 1: %\n", get_gps_sum(map, robot, instructions));
    
        wide_map: [..][..] Tile;
        defer array_free_2d(wide_map);
    
        for map {
            row: [..] Tile;
            for it {
                if it == .BOX {
                    array_add(*row, .BOX_LEFT);
                    array_add(*row, .BOX_RIGHT);
                } else {
                    array_add(*row, it);
                    array_add(*row, it);
                }
            }
    
            array_add(*wide_map, row);
        }
    
        robot.x *= 2;
        print("Part 2: %\n", get_gps_sum(wide_map, robot, instructions));
    }
    

    My original solution for part 1 did not use recursion, and, in my opinion, was much nicer than what I ended up with. I'll include it here; it's a replacement for the get_gps_sum() procedure.

    Part 1-only solution
    // Attempts to push a box in a given direction, along with all boxes in its way.
    // Returns whether the push was successful.
    get_gps_sum :: (
        map: [][..] Tile,
        width: int,
        height: int,
        robot: Vector2_Int,
        instructions: [] Vector2_Int
    ) -> int {
        // The map and robot parameters are not pointers (since we don't want to
        // modify them outside this function), so they are immutable. Since we need
        // to be able to modify them in this function, we thus need to make copies
        // of them.
        mutable_map: [..][..] Tile;
        defer array_free_2d(mutable_map);
    
        for map {
            row: [..] Tile;
            array_copy(*row, it);
            array_add(*mutable_map, row);
        }
    
        mutable_robot := Vector2_Int.{robot.x, robot.y};
    
        for instructions {
            new_robot := Vector2_Int.{
                mutable_robot.x + it.x,
                mutable_robot.y + it.y
            };
    
            new_tile := mutable_map[new_robot.y][new_robot.x];
            if new_tile == .EMPTY {
                mutable_robot = new_robot;
            } else if
                new_tile == .BOX || new_tile == .BOX_LEFT || new_tile == .BOX_RIGHT
            {
                position := Vector2_Int.{new_robot.x + it.x, new_robot.y + it.y};
                while mutable_map[position.y][position.x] == .BOX {
                    position.x += it.x;
                    position.y += it.y;
                }
    
                if mutable_map[position.y][position.x] == .EMPTY {
                    while true {
                        position.x -= it.x;
                        position.y -= it.y;
    
                        if
                            position.x == mutable_robot.x
                            && position.y == mutable_robot.y
                        {
                            break;
                        }
    
                        mutable_map[position.y][position.x] = .EMPTY;
                        mutable_map[position.y + it.y][position.x + it.x] = .BOX;
                    }
    
                    mutable_robot = new_robot;
                }
            }
        }
    
        gps_sum := 0;
        for row, y: mutable_map {
            for tile, x: row {
                if tile == .BOX || tile == .BOX_LEFT {
                    gps_sum += x + y * 100;
                }
            }
        }
    
        return gps_sum;
    }
    
    2 votes
  19. Comment on Day 14: Restroom Redoubt in ~comp.advent_of_code

    lily
    Link Parent
    Yeah, the speed at which it worked and how it did so with no issues was surprising, and suggests to me that the designer may have thought of that solution - though, what I did and checking for the...

    Yeah, the speed at which it worked and how it did so with no issues was surprising, and suggests to me that the designer may have thought of that solution - though, what I did and checking for the minimum safety scores are somewhat similar solutions, so it's possible that my method works because the former was planned for. I can't say I'm very satisfied with myself for figuring it out, though, since it was just a hunch that I didn't really expect to work (as you said, it seems unlikely that it would do so without the input being specifically prepared for it). I think there's a good puzzle somewhere in here, but it just needed some extra clarification in my opinion.

  20. Comment on Day 14: Restroom Redoubt in ~comp.advent_of_code

    lily
    Link
    I initially was ready to give up upon seeing part 2, because I thought I'd have to sit there and comb through an output log for ages to find the Christmas tree (which, in my opinion, would be a...

    I initially was ready to give up upon seeing part 2, because I thought I'd have to sit there and comb through an output log for ages to find the Christmas tree (which, in my opinion, would be a terrible puzzle). I eventually realized there's no way that would be the intended solution, so tried a few conditions before settling on "no robots on the same space". I'm still not really happy with this puzzle, though, since "in the shape of a Christmas tree" is incredibly vague, and my solution was just a guess - there could easily be a Christmas tree shape with multiple robots on the same tile, it just seemed like the puzzle wouldn't have been designed that way to me. I don't know. Probably my least favorite day so far, because it doesn't feel there's a "foolproof" solution. This puzzle seems to have been designed more to be cute than to be interesting to solve.

    Solution (Jai)
    /* Advent of Code 2024
     * Day 14: Restroom Redoubt
     */
    
    #import "Basic";
    #import "File";
    #import "String";
    
    Vector2_Int :: struct {
        x, y: int;
    }
    
    Robot :: struct {
        position, velocity: Vector2_Int;
    }
    
    main :: () {
        input, success := read_entire_file("inputs/day_14.txt");
        assert(success);
    
        lines := split(input, "\n");
        pop(*lines);
        defer array_free(lines);
    
        robots: [..] Robot;
        defer array_free(robots);
    
        for lines {
            vectors := split(it, " ");
            defer array_free(vectors);
    
            position := split(slice(vectors[0], 2, vectors[0].count - 2), ",");
            velocity := split(slice(vectors[1], 2, vectors[1].count - 2), ",");
            defer array_free(position);
            defer array_free(velocity);
    
            array_add(*robots, .{
                .{string_to_int(position[0]), string_to_int(position[1])},
                .{string_to_int(velocity[0]), string_to_int(velocity[1])}
            });
        }
    
        // The example input uses a different size for the space the robots are in,
        // so it's best to define constants for that so it can be easily changed.
        width :: 101;
        height :: 103;
        middle_x :: width / 2;
        middle_y :: height / 2;
    
        // We need the untouched robots array again for part 2, so we have to copy
        // it here.
        robots_part_1: [..] Robot;
        array_copy(*robots_part_1, robots);
        defer array_free(robots_part_1);
    
        robot_counts: [4] int;
        for *robots_part_1 {
            it.position.x += it.velocity.x * 100;
            it.position.y += it.velocity.y * 100;
    
            while it.position.x < 0       it.position.x += width;
            while it.position.x >= width  it.position.x -= width;
            while it.position.y < 0       it.position.y += height;
            while it.position.y >= height it.position.y -= height;
    
            if it.position.x != middle_x && it.position.y != middle_y {
                robot_counts[
                    cast(int) (it.position.y > middle_y) * 2
                    + cast(int) (it.position.x > middle_x)
                ] += 1;
            }
        }
    
        print(
            "Part 1: %\n",
            robot_counts[0] * robot_counts[1] * robot_counts[2] * robot_counts[3]
        );
    
        // The best way to find the Christmas tree picture seems to be looking for
        // the first second where no robots are on the same tile.
        seconds := 1;
        while true {
            occupied_tiles: [height][width] bool;
            duplicate_found := false;
    
            for *robots {
                it.position.x += it.velocity.x;
                it.position.y += it.velocity.y;
    
                if it.position.x < 0       it.position.x += width;
                if it.position.x >= width  it.position.x -= width;
                if it.position.y < 0       it.position.y += height;
                if it.position.y >= height it.position.y -= height;
    
                if occupied_tiles[it.position.y][it.position.x] {
                    duplicate_found = true;
                } else {
                    occupied_tiles[it.position.y][it.position.x] = true;
                }
            }
    
            if duplicate_found {
                seconds += 1;
            } else {
                print("Part 2: %\n", seconds);
                break;
            }
        }
    }
    
    2 votes