13 votes

Day 4: Giant Squid

Today's problem description: https://adventofcode.com/2021/day/4

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

25 comments

  1. pnutzh4x0r
    Link
    Python Repo Link Part 1 Solution was straightforward... it was just a matter of reading the board and performing the appropriate operations. One trick I used to make checking for BINGO easier was...

    Python

    Repo Link

    Part 1

    Solution was straightforward... it was just a matter of reading the board and performing the appropriate operations. One trick I used to make checking for BINGO easier was to mark cells with -1 and then I could just sum up the row or column and check if it was -5 to see if there was BINGO.

    # Constants
    
    ROWS = 5
    COLS = 5
    MARK = -1
    
    # Functions
    
    def read_boards():
        while sys.stdin.readline():
            try:
                yield [list(map(int, sys.stdin.readline().split())) for _ in range(ROWS)]
            except (IOError, ValueError):
                break
    
    def mark_board(board, number):
        for rindex, row in enumerate(board):
            for cindex, cell in enumerate(row):
                if cell == number:
                    board[rindex][cindex] = MARK
    
    def check_board(board):
        return any(sum(row) == ROWS * MARK for row in board) or \
               any(sum(board[row][col] for row in range(ROWS)) == COLS * MARK for col in range(COLS))
    
    def dump_board(board):
        for row in board:
            print(' '.join(map(str, row)))
    
    def sum_board(board):
        return sum(
            sum(number for number in row if number >= 0) for row in board
        )
    
    # Main Execution
    
    def main():
        numbers = [int(n) for n in sys.stdin.readline().split(',')]
        boards  = list(read_boards())
        winner  = None
    
        while numbers and winner is None:
            number = numbers.pop(0)
    
            for board in boards:
                mark_board(board, number)
                if check_board(board):
                    winner = board
    
        total = sum_board(winner)
        print(number*total)
    
    Part 2

    Only my main function changes here. Instead of looking for the first board, I simply remove boards as they win and just keep track of the last winner.

    def main():
        numbers = [int(n) for n in sys.stdin.readline().split(',')]
        boards  = list(read_boards())
        winner  = None
    
        while numbers and boards:
            number = numbers.pop(0)
    
            active = []
            for board in boards:
                mark_board(board, number)
                if check_board(board):
                    winner = board
                else:
                    active.append(board)
            boards = active
    
        total = sum_board(winner)
        print(number*total)
    
    5 votes
  2. Crespyl
    (edited )
    Link
    This one fits nicely into that sweet spot where creating an object/class for tracking the boards makes the rest of the logic pretty easy to follow. The built in Set type comes in handy here too....

    This one fits nicely into that sweet spot where creating an object/class for tracking the boards makes the rest of the logic pretty easy to follow. The built in Set type comes in handy here too.

    Board class
    class Board
      def initialize(rows)
        @rows = rows
        @hits = Set.new
      end
    
      def mark(number)
        @hits += [number]
      end
    
      def win?
        return false unless @hits.size >= 5
    
        @rows.size.times do |i|
          # check if every number in row i is in @hits
          return true if @hits.superset? @rows[i].to_set
    
          # check if every number in column i is in @hits
          return true if @hits.superset? @rows.reduce([]) { |column, row| column << row[i] }
                                              .to_set
        end
    
        return false
      end
    
      def unmarked_numbers
        @rows.flatten.to_set - @hits
      end
    end
    
    Part 1 Ruby
    def compute_p1(input)
      numbers = input
                  .lines
                  .first
                  .chomp
                  .split(',')
                  .map(&:to_i)
    
      boards = input
                 .split("\n\n")
                 .drop(1)
                 .map { |l| l.split("\n")
                             .map { _1.split(' ').map(&:to_i) } }
                 .map { Board.new(_1) }
    
      winner = nil
      number = nil
    
      until numbers.empty?
        number = numbers.shift
        boards.each { _1.mark(number) }
    
        break if (winner = boards.select { _1.win? }.first)
      end
    
      winner.unmarked_numbers.sum * number
    end
    
    Part 2 Ruby
    def compute_p2(input)
      numbers = input
                  .lines
                  .first
                  .chomp
                  .split(',')
                  .map(&:to_i)
    
      boards = input
                 .split("\n\n")
                 .drop(1)
                 .map { |l| l.split("\n")
                             .map { _1.split(' ').map(&:to_i) } }
                 .map { Board.new(_1) }
    
      last_board = nil
      number = nil
    
      until numbers.empty? || last_board&.win?
        number = numbers.shift
        boards.each { _1.mark(number) }
    
        boards = boards.filter { ! _1.win? }
        last_board = boards.first if boards.size == 1
      end
    
      last_board.unmarked_numbers.sum * number
    end
    
    Extra discussion

    This is pretty inefficient, Part 2 is especially slow (~0.2s on my machine). I think a lot of that is in how I'm checking for wins on each board. Right now I'm always revisiting all the rows to find the numbers in each column, and then making a Set out of them. If performance was a bigger issue I might do something like pre-compute the numbers in each row/column, or actually remove numbers from the board once they get marked so that we have fewer things to check as the process goes on.

    5 votes
  3. [3]
    tomf
    Link
    ok, I did this one -- but I'm not 100% certain that part 2 will work for all datasets. Check this sheet if you want to see some magic There are some fancy tricks in there -- like a single formula...

    ok, I did this one -- but I'm not 100% certain that part 2 will work for all datasets.

    Check this sheet if you want to see some magic

    There are some fancy tricks in there -- like a single formula to transpose individual blocks of data, etc.

    Most formulas are on the second row. I tested it with two datasets and it worked both times, so that's good enough for me. The part 2 bit in AO5 is sketchy, though.

    5 votes
    1. [2]
      3d12
      Link Parent
      Hey, good to see you're back and doing this again! I really enjoy checking out your spreadsheet implementations, even if I don't quite understand fully what's going on. :) Are the...

      Hey, good to see you're back and doing this again! I really enjoy checking out your spreadsheet implementations, even if I don't quite understand fully what's going on. :)

      Are the functions/features you're using specific to Sheets, or is all this logic able to be run in Excel as well?

      2 votes
      1. tomf
        Link Parent
        thanks! I was up so late last night working on this beast. In typical fashion, I woke up a few hours later and rewerote the formula for part 2 so its more logical. Most of it can translate with a...

        thanks! I was up so late last night working on this beast. In typical fashion, I woke up a few hours later and rewerote the formula for part 2 so its more logical.

        Most of it can translate with a few changes. The major difference (unless something has changed recently) is the ARRAYFORMULA function instead of using ctrl+shift+enter.

        I'm wondering if topaz and crew took spreadsheets into consideration this year. So far everything has been fairly straightforward for us who don't know anything else.

        2 votes
  4. PapaNachos
    Link
    I think I made part 1 a lot harder than it needed to be, but for whatever reason, my part 2 only took about 30 seconds. I also fucked up by making my board checker look for diagonals. Don't do...

    I think I made part 1 a lot harder than it needed to be, but for whatever reason, my part 2 only took about 30 seconds. I also fucked up by making my board checker look for diagonals. Don't do that, these boards don't use diagonals.

    Day 4 Part A – Python I just converted each board into a list of numbers and slowly checked them off. Then I made lists of indicies and checked if they were all called since the possible winning combinations are fixed and known. When a given board completes, write down the number of calls remaining and calculate the score. Then print out the score of the board with the most calls remaining. I REALLY didn't want to try to manage each board individually while walking through the list since I figured part 2 would throw a horrible wrench into my plans if I did that.

    I struggled a bit with getting check boards to work. I was trying to get all() to work for a while, but eventually just gave up and chained some == together. I also forgot to multiply my indices at first, so that made my horizontal check break. Also as I mentioned before I implemented diagonals. That fucked me up for a bit.

    The hoz_check, diag_check, vert_check aren't used. They're remnants of an earlier attempt. I realized they weren't really saving any time or space

    #data = test_data
    data = real_data
    data = data.split('\n\n')
    
    numbers = data[0].split(',')
    
    boards = data[1:]
    
    hoz_check = [0,1,2,3,4]
    vert_check = [0,5,10,15,20]
    diag_check = [[0, 6, 12, 18, 24], [20, 16, 12, 8, 4]]
    
    def check_board(b_l):
        for i in range(5):
            if b_l[0+(i*5)] == b_l[1+(i*5)] == b_l[2+(i*5)] == b_l[3+(i*5)] == b_l[4+(i*5)] ==  "X":
                return True
            if b_l[0+i] == b_l[5+i] == b_l[10+i] == b_l[15+i] == b_l[20+i] ==  "X":
                return True
        #if b_l[0+i] == b_l[6+i] == b_l[12+i] == b_l[18+i] == b_l[24+i] ==  "X":
        #        return True
        #if b_l[20+i] == b_l[16+i] == b_l[12+i] == b_l[8+i] == b_l[4+i] ==  "X":
        #        return True
        return False
    
    def print_board(board):
        print(board[0:5],"\n",board[5:10],"\n",board[10:15],"\n",board[15:20],"\n",board[20:25],"\n")
    
    def score(b_l, c_n):
        b_l = [x for x in b_l if x != "X"]
        b_l = [int(x) for x in b_l]
        #print(sum(b_l))
        #print(c_n)
        return sum(b_l) * int(c_n)
              
    board_scores = []
    turns_remaining = []
    for board in boards:
        #print("---------------------------------")
        board_list = []
        numbers_copy = list(numbers)
        
        for row in board.split("\n"):
            board_list = board_list + row.split()
        complete = False
        while not complete and len(numbers_copy) >= 1:
            called_number = numbers_copy.pop(0)
            board_list = ["X" if s==called_number else s for s in board_list]
            complete = check_board(board_list)
        if complete:
            turns_remaining.append(len(numbers_copy))
            board_scores.append(score(board_list, called_number))
        #print_board(board_list)
    print(turns_remaining)
    print(board_scores)
    
    max_value = max(turns_remaining)
    max_value_index = turns_remaining.index(max_value)
    print(board_scores[max_value_index])
    
    Day 4 Part B – Python Literally just swapped out the max in the last 3 lines for min and it worked for part B. Huh. Okay.
    Tips
    • Make sure to read the descript of the problem. In this case it doesn't do diagonals which can cause problems.

    • You'll need to think about how to store the data for a given board in a way you can work with it. Fortunately boards are all a fixed size and the ways they can win are fixed

    • For me building a function that let me visualize the status of a given board was very useful in debugging

    4 votes
  5. [4]
    wycy
    (edited )
    Link
    Rust Edit: Code fixed. Turns out the way I wanted to slice the cards wouldn't work. If anyone knows why the commented out portion of my validate function doesn't work, pls let me know. I was...

    Rust

    Edit: Code fixed. Turns out the way I wanted to slice the cards wouldn't work.

    If anyone knows why the commented out portion of my validate function doesn't work, pls let me know. I was getting an error that I couldn't compare &bool to bool, but no amount of referencing/dereferencing the bools would work, so I feel like there may be some other problem with what I was trying to do.

    Rust
    use std::env;
    use std::io::{self};
    
    const CARDSIZE: usize = 5;
    
    #[derive(Clone)]
    struct BingoCard {
        pub card: Vec<Vec<usize>>,
        pub marked: Vec<Vec<bool>>,
        pub won: bool,
    }
    impl From<&str> for BingoCard {
        fn from(s: &str) -> Self {
            let mut card: Vec<Vec<usize>> = Vec::new();
            for row in s.split("\n") {
                card.push(row.split_ascii_whitespace().map(|x| x.parse::<usize>().unwrap()).collect());
            }
            Self { card: card, marked: vec![vec![false;5];5], won: false }
        }
    }
    impl BingoCard {
        pub fn validate(&mut self) -> bool {
            /*for i in 0..CARDSIZE {
                if self.marked[i][..].iter().all(|&x| x) { self.won = true; return true; }
                if self.marked[..][i].iter().all(|&x| x) { self.won = true; return true; }
            }*/
            for row in 0..CARDSIZE {
                let mut bingo = true;
                for col in 0..CARDSIZE {
                    if !self.marked[row][col] { bingo = false; }
                }
                if bingo { self.won = true; }
            }
            for col in 0..CARDSIZE {
                let mut bingo = true;
                for row in 0..CARDSIZE {
                    if !self.marked[row][col] { bingo = false; }
                }
                if bingo { self.won = true; }
            }
            self.won
        }
        pub fn score(&self) -> usize {
            let mut score = 0;
            for row in 0..CARDSIZE {
                for col in 0..CARDSIZE {
                    if !self.marked[row][col] { score += self.card[row][col]; }
                }
            }
            if self.won { score } else { 0 }
        }
        pub fn mark_number(&mut self, number: usize) {
            'outer: for (i,row) in self.card.iter().enumerate() {
                for (j,col) in row.iter().enumerate() {
                    if col == &number {
                        self.marked[i][j] = true;
                        break 'outer;
                    }
                }
            }
    
        }
    }
    
    fn day04(input: &str) -> io::Result<()> {
        // Input
        let input_str = std::fs::read_to_string(input).unwrap();
        let input_str = input_str.trim();
        let input: Vec<_> = input_str.split("\n\n").collect();
        
        // Get Bingo numbers and cards
        let drawn: Vec<_> = input[0].split(",").map(|x| x.parse::<usize>().unwrap()).collect();
        let mut cards: Vec<_> = input[1..].into_iter().map(|x| BingoCard::from(*x)).collect();
        let num_bingo_cards = cards.len();
    
        // Play Bingo
        let mut bingos = 0;
        'bingo_loop: for number in drawn {
            //println!("Drawing: {}", number);
            for card in &mut cards {
                if card.won { continue; }
                card.mark_number(number);
                if card.validate() {
                    bingos += 1;
                    if bingos == 1 {
                        println!("Part 1: {}", number * card.score()); // 89001
                    } else if bingos == num_bingo_cards {
                        println!("Part 2: {}", number * card.score()); // 7296
                        break 'bingo_loop;
                    }
                }
            }
        }
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        day04(&filename).unwrap();
    }
    
    4 votes
    1. [3]
      DataWraith
      (edited )
      Link Parent
      Wow, I didn't know you could iterate over 2D vectors like that. Thanks for sharing! I fiddled a little with the validate function and came up with the following: Rust code impl BingoCard { pub fn...

      Wow, I didn't know you could iterate over 2D vectors like that. Thanks for sharing!

      I fiddled a little with the validate function and came up with the following:

      Rust code
      impl BingoCard {
          pub fn validate(&mut self) -> bool {
              for i in 0..CARDSIZE {
                  if self.marked[i][0..CARDSIZE].iter().all(|&x| x) {
                      self.won = true;
                      return true;
                  }
      
                  if self.marked[0..CARDSIZE][i].iter().all(|&x| x) {
                      self.won = true;
                      return true;
                  }
              }
              
              return false;
          }
      }
      

      .iter() gives you references to the data (unless you call .copied() on it, which will clone everything). You can add an ampersand before the x in the closure definition, and it will give you a dereferenced x. That kind of pattern matching is neat. Lastly, the comparison of x == true is redundant if x is a boolean, because it itself will evaluate to trueor false.

      Edit: Hm. I looked at it a bit more, and even a simple dereference (*x == true) worked for me. Tried it in both stable and nightly rust, so I'm not sure why it wasn't working for you :/

      2 votes
      1. [2]
        wycy
        (edited )
        Link Parent
        I swear I tried .all(|&x| x == true) before and it wasn't working, but now it clearly is so I don't know what I thought I was doing before. 😅 And I feel like a buffoon for actually writing x ==...

        I swear I tried .all(|&x| x == true) before and it wasn't working, but now it clearly is so I don't know what I thought I was doing before. 😅 And I feel like a buffoon for actually writing x == true--I'd certainly never do that in an if statement, so I don't know why I thought I had to do it in .all(). Thanks

        For whatever reason, though, with this change I now get the wrong answers. Puzzling. Maybe you really can’t slice Vecs this way.

        Rust
        impl BingoCard {
            pub fn validate(&mut self) -> bool {
                for i in 0..CARDSIZE {
                    if self.marked[i][..].iter().all(|&x| x) { self.won = true; return true; }
                    if self.marked[..][i].iter().all(|&x| x) { self.won = true; return true; }
                }
                return false;
            }
        }
        
        2 votes
        1. Liru
          Link Parent
          I can probably guess -- just |x| x gives a compile error (due to the same issue), so you probably tried something "obvious" to compare it.

          And I feel like a buffoon for actually writing x == true--I'd certainly never do that in an if statement, so I don't know why I thought I had to do it in .all().

          I can probably guess -- just |x| x gives a compile error (due to the same issue), so you probably tried something "obvious" to compare it.

          2 votes
  6. [2]
    DataWraith
    (edited )
    Link
    Here's my solution in Rust. I opted to use an actual parser, nom, to parse the puzzle input. It's kind of verbose, but not too bad. After parsing everything, the remainder was almost trivial....

    Here's my solution in Rust. I opted to use an actual parser, nom, to parse the puzzle input. It's kind of verbose, but not too bad.
    After parsing everything, the remainder was almost trivial.

    Setup & Board parsing
    use nom::{
        bytes::complete::tag, character::complete::multispace1, character::complete::newline,
        character::complete::space0, character::complete::space1, multi::separated_list1, IResult,
    };
    
    struct Board {
        numbers: Vec<Vec<i64>>,
        marks: Vec<Vec<bool>>,
        won: bool,
    }
    
    struct Bingo {
        numbers: Vec<i64>,
        boards: Vec<Board>,
    }
    
    const BOARD_WIDTH: usize = 5;
    const BOARD_HEIGHT: usize = 5;
    
    fn parse_chosen_numbers(input: &str) -> IResult<&str, Vec<i64>> {
        separated_list1(tag(","), nom::character::complete::i64)(input)
    }
    
    fn parse_board_line(input: &str) -> IResult<&str, Vec<i64>> {
        let (input, _) = space0(input)?;
        separated_list1(space1, nom::character::complete::i64)(input)
    }
    
    fn parse_board(input: &str) -> IResult<&str, Board> {
        let (input, numbers) = separated_list1(newline, parse_board_line)(input)?;
    
        Ok((
            input,
            Board {
                numbers,
                marks: vec![vec![false; BOARD_WIDTH]; BOARD_HEIGHT],
                won: false,
            },
        ))
    }
    
    fn parse_boards(input: &str) -> IResult<&str, Vec<Board>> {
        let (input, _) = multispace1(input)?;
        separated_list1(multispace1, parse_board)(input)
    }
    
    fn parse_input(input: &str) -> Bingo {
        let (input, numbers) = parse_chosen_numbers(input).unwrap();
        let (_, boards) = parse_boards(input).unwrap();
    
        Bingo { numbers, boards }
    }
    
    Helper methods
    fn board_won(b: &Board) -> bool {
        // Rows
        for y in 0..BOARD_HEIGHT {
            let mut num_marked = 0;
            for x in 0..BOARD_WIDTH {
                if b.marks[y][x] {
                    num_marked += 1;
                }
            }
    
            if num_marked == BOARD_WIDTH {
                return true;
            }
        }
    
        // Columns
        for x in 0..BOARD_WIDTH {
            let mut num_marked = 0;
            for y in 0..BOARD_HEIGHT {
                if b.marks[y][x] {
                    num_marked += 1;
                }
            }
    
            if num_marked == BOARD_HEIGHT {
                return true;
            }
        }
    
        false
    }
    
    fn mark(b: &mut Board, number: i64) {
        for y in 0..BOARD_HEIGHT {
            for x in 0..BOARD_WIDTH {
                if b.numbers[y][x] == number {
                    b.marks[y][x] = true;
                }
            }
        }
    }
    
    fn calculate_score(b: &Board) -> i64 {
        let mut sum = 0;
        for y in 0..BOARD_HEIGHT {
            for x in 0..BOARD_WIDTH {
                if !b.marks[y][x] {
                    sum += b.numbers[y][x];
                }
            }
        }
    
        sum
    }
    
    Part I
    fn solve(input: &str) -> i64 {
        let mut bingo = parse_input(input);
    
        for number in bingo.numbers {
            for b in bingo.boards.iter_mut() {
                mark(b, number);
                if board_won(&b) {
                    return calculate_score(&b) * number;
                }
            }
        }
    
        0
    }
    
    fn main() {
        println!("{}", solve(include_str!("../../input-04.txt")));
    }
    
    Part II
    fn solve(input: &str) -> i64 {
        let mut bingo = parse_input(input);
        let mut remaining_boards = bingo.boards.len();
    
        for number in bingo.numbers {
            for b in bingo.boards.iter_mut() {
                mark(b, number);
    
                if !b.won && board_won(&b) {
                    b.won = true;
                    remaining_boards -= 1;
    
                    if remaining_boards == 0 {
                        return number * calculate_score(&b);
                    }
                }
            }
        }
    
        0
    }
    
    fn main() {
        println!("{}", solve(include_str!("../../input-04.txt")));
    }
    

    As an interesting side-note: when I got the solution wrong in part II (did not mark the won boards as won at first so they were double-counted...) it told me the solution was wrong, but it would be right for someone else, and told me to check if I was logged into the correct account... I wonder what the chances are for that.

    3 votes
    1. 3d12
      Link Parent
      Adding my anecdote to yours, I got that response on one of the problems last year. Because everyone's answer is numeric and since the puzzle constraints put a likely upper and lower limit on the...

      Adding my anecdote to yours, I got that response on one of the problems last year. Because everyone's answer is numeric and since the puzzle constraints put a likely upper and lower limit on the possible answers, I think the chances of this would actually be pretty high. I would expect this mostly happens when reading or interpreting the input goes horribly wrong, but your solution to getting this response seems pretty creative as well. :)

      3 votes
  7. [2]
    bhrgunatha
    (edited )
    Link
    Racket Notes Linus Torvalds Racket has classes but for AoC if I need to maintain state I'll generaly use a struct. So my bingo card is: (struct card (grid [marked #:mutable] [winner? #:mutable])...

    Racket

    Notes

    Bad programmers worry about the code. Good programmers worry about data structures and their relationships.

    • Linus Torvalds

    Racket has classes but for AoC if I need to maintain state I'll generaly use a struct.
    So my bingo card is:

    (struct card (grid [marked #:mutable] [winner? #:mutable]) #:transparent)
    

    The Racket compiler automatically generates a bunch of useful procedures for structs.
    e.g.field selectors (card-grid, card-marked, ...) and mutators if any fields or the struct is marked #:mutable ( set-card-marked!, set-card-winner?!)

    Scheme has a convention where mutator procedures end in "!" and procedures returning a boolean end in "?. It pleases me too much use ? for boolean fields too so I get procedure names like set-card-winner?! ...yeah I'm easily amused.

    Input parsing.
    Racket's iterators are built on for forms and sequences.

    After extracting the game numbers, I removed blank lines and grabbed the card definitions.

    (define (parse-bingo input)
      (define game
        (for/list ([n (string-split (first input) ",")])
          (string->number n)))
      (define grids
        (for/list ([line (rest input)]
                   #:unless (string=? line ""))
          line))
      (values game
              (for/list ([grid (in-slice 5 grids)])
                (parse-grid grid))))
    

    in-slice is an iterator that groups a sequence by a specific count.

    (sequence->list (in-slice 3 (range 10)))
    ;; '((0 1 2) (3 4 5) (6 7 8) (9))
    

    Nice! saves me that drudgery.

    The only thing left is to convert the numbers into a card.
    card-grid is a hash table from number -> grid co-ordinate to help check for a bingo later.
    The co-ordinates are generated using in-indexed - like Python's enumerate.

    (define (parse-grid numbers)
      (for*/fold ([grid (hash)]
                  #:result (card grid null #f))
                ([(row i) (in-indexed numbers)]
                 [(column j) (in-indexed (string-split row))])
        (define n (string->number column))
        (define co-ord (list i j))
        (hash-set grid n co-ord)))
    
    Part 1

    for/first returns the value when the body first returns anything that isn't #f - false.

    (define (part-01 input)
      (define-values (game cards) (parse-bingo input))
      (for*/first ([n game]
                  [c cards]
                  #:when (bingo? (mark-card! n c)))
        (score n c)))
    

    The card struct has a list of the numbers called that are on the card.

    (define (mark-card! n c)
      (when (hash-ref (card-grid c) n #f)
        (set-card-marked! c (cons n (card-marked c))))
      c)
    

    That way it's easy to sum them and subtract from the card's total.

    (define (score n c)
      (* n (- (apply + (hash-keys (card-grid c)))
              (apply + (card-marked c)))))
    

    The only thing left is to check for a line of matched numbers.
    I extract the co-ordinates of the marked numbers and group them by row and by column.
    If any group has 5 members I mark the card as a winner and return #t.
    (I only added the winner? field after I read part 2.)

    (define (bingo? c)
      (match-define (card grid marked _) c)
      (define co-ords
        (for/list ([mark marked]
                   #:when (hash-ref grid mark #f))
          (hash-ref grid mark)))
      (define winning-row?
        (for/first ([row (group-by first co-ords)]
                    #:when (= 5 (length row)))
          row))
      (define winning-column?
        (for/first ([column (group-by second co-ords)]
                    #:when (= 5 (length column)))
          column))
      (define winner? (or winning-row? winning-column?))
      (when winner? (set-card-winner?! c #t))
      winner?)
    
    Part 2

    I added the winner? field to the struct after reading Part 2 and set it when bingo? is true, which made it trivial. All I do is ignore complete cards and instead of using
    for/first I use for/last

    (define (part-02 input)
      (define-values (game cards) (parse-bingo input))
      (for*/last ([n game]
                 [c cards]
                  #:unless (card-winner? c)
                  #:when (bingo? (mark-card! n c)))
        (score n c)))
    

    I lost a lot of time today head-scratching because I converted the card's co-ordinates to numbers but forgot to convert the first line to numbers 😃💩.

    3 votes
    1. bhrgunatha
      Link Parent
      After smugly quoting Linus about data structures, I'm ashamed at the inefficient check for a winning card so I threw some memory at it. Part 1: 21ms -> 9ms Part 2: 44ms -> 10 ms Changes Add a...

      After smugly quoting Linus about data structures, I'm ashamed at the inefficient check for a winning card so I threw some memory at it.

      • Part 1: 21ms -> 9ms
      • Part 2: 44ms -> 10 ms
      Changes

      Add a count for each row and column marked in a hash keyed on "r<num>" and "c<num>". Bingo is when any count is 5.

      (struct card (grid marked winner? counts) #:mutable #:transparent)
      
      (define (mark-card! n c)
        (define co-ord (hash-ref (card-grid c) n #f))
        (when co-ord
          (define row (format "r~a" (first co-ord)))
          (define col (format "c~a" (second co-ord)))
          (define counts (card-counts c))
          (hash-set! counts row (add1 (hash-ref counts row 0)))
          (hash-set! counts col (add1 (hash-ref counts col 0)))
          (set-card-marked! c (cons n (card-marked c))))
        c)
      
      (define (bingo? c)
        (define counts (hash-values (card-counts c)))
        (cond [(null? counts) #f]
              [(= 5 (apply max counts)) (set-card-winner?! c #t)
                                        #t]
              [else #f]))
      
      2 votes
  8. asterisk
    Link
    Python from collections import defaultdict from itertools import chain lines = [i.strip() for i in open("input.txt")] boards = defaultdict(list) b = 0 for i in range(2, len(lines)): if not...
    Python
    from collections import defaultdict
    from itertools import chain
    
    lines = [i.strip() for i in open("input.txt")]
    
    boards = defaultdict(list)
    b = 0
    
    for i in range(2, len(lines)):
        if not lines[i]:
            b += 1
            continue
    
        boards[b].append(list(map(int, lines[i].split())))
    
    for board in boards:
        boards[board] += list(zip(*boards[board]))
    
    numbers = list(map(int, lines[0].split(",")))
    
    
    def task(boards: list) -> list:
        scores = list()
        marked = numbers[:4]
        for number in numbers[4:]:
            marked.append(number)
            for board in list(boards):
                for b in boards[board]:
                    if set(b).issubset(marked):
                        scores.append(sum(set(chain(*boards[board])) - set(marked)) * marked[-1])
                        boards.pop(board)
        return scores
    
    
    print("Part One:", task(boards.copy())[0])
    print("Part Two:", task(boards.copy())[-2])
    
    3 votes
  9. 3d12
    Link
    Compared to day 3, this one was easier and actually kind of fun to implement. I was quite happy with the change from part 1 to part 2. I was expecting a different twist, so my implementation only...

    Compared to day 3, this one was easier and actually kind of fun to implement. I was quite happy with the change from part 1 to part 2. I was expecting a different twist, so my implementation only required minimal changes to get the new answer.

    Part 1
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function parseBingoCard(bingoCardText) {
    	let bingoCardArray = [];
    	let rows = bingoCardText.split('\n');
    	for (const row of rows) {
    		let regex = /(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;
    		let found = row.match(regex);
    		let rowArray = [found[1],found[2],found[3],found[4],found[5]];
    		bingoCardArray.push(rowArray);
    	}
    	return bingoCardArray;
    }
    
    function splitBingoCards() {
    	let splitArray = [];
    	let currentBingoCardArray = [];
    	// skip the first two lines, bingo cards start on line 3
    	let bingoCardArray = inputArr.slice(2);
    	for (const line of bingoCardArray) {
    		// on blank line, reset accumulated line and send for parsing
    		if (line.trim().length === 0) {
    			splitArray.push(parseBingoCard(currentBingoCardArray.join('\n')));
    			currentBingoCardArray = [];
    		} else {
    			currentBingoCardArray.push(line);
    		}
    	}
    	// flushing the buffer, in case the last line is not newline-terminated
    	if (currentBingoCardArray.length != 0) {
    		splitArray.push(parseBingoCard(currentBingoCardArray.join('\n')));
    	}
    	return splitArray;
    }
    
    function findBingo(bingoCard, numbersCalled) {
    	// look for horizontal bingos
    	for (const row of bingoCard) {
    		let horizontalBingo = true;
    		for (const number of row) {
    			if (!numbersCalled.includes(parseInt(number))) {
    				horizontalBingo = false;
    				break;
    			}
    		}
    		if (horizontalBingo === true) {
    			return true;
    		}
    	}
    	// look for vertical bingos
    	for (let colIndex = 0; colIndex < bingoCard[0].length; colIndex++) {
    		let verticalBingo = true;
    		for (const row of bingoCard) {
    			if (!numbersCalled.includes(parseInt(row[colIndex]))) {
    				verticalBingo = false;
    				break;
    			}
    		}
    		if (verticalBingo === true) {
    			return true;
    		}
    	}
    	return false;
    }
    
    function findBingoScore(bingoCard, numbersCalled) {
    	let pointSpaces = [];
    	for (const row of bingoCard) {
    		for (const num of row) {
    			if (!numbersCalled.includes(parseInt(num))) {
    				pointSpaces.push(parseInt(num));
    			}
    		}
    	}
    	return (pointSpaces.reduce((a,b) => a+b) * numbersCalled[numbersCalled.length-1]);
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let numbersToCall = inputArr[0].split(',').map(e => parseInt(e));
    	console.log("DEBUG: numbersToCall = " + numbersToCall);
    	let bingoCardArray = splitBingoCards();
    	let numberCalledIndex = 0;
    	let numbersCalled = [];
    	let winningBoard = [];
    	while (winningBoard.length === 0) {
    		// call new number
    		numberCalledIndex++;
    		numbersCalled = numbersToCall.slice(0,numberCalledIndex);
    
    		// check for win
    		for (bingoCard of bingoCardArray) {
    			if (findBingo(bingoCard,numbersCalled)) {
    				winningBoard = bingoCard;
    				break;
    			}
    		}
    	}
    	console.log("DEBUG: winningBoard =\n" + winningBoard.join('\n'));
    	console.log("DEBUG: numbersCalled = " + numbersCalled);
    	console.log("Answer found! " + findBingoScore(winningBoard,numbersCalled));
    })();
    
    Part 2
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function parseBingoCard(bingoCardText) {
    	let bingoCardArray = [];
    	let rows = bingoCardText.split('\n');
    	for (const row of rows) {
    		let regex = /(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;
    		let found = row.match(regex);
    		let rowArray = [found[1],found[2],found[3],found[4],found[5]];
    		bingoCardArray.push(rowArray);
    	}
    	return bingoCardArray;
    }
    
    function splitBingoCards() {
    	let splitArray = [];
    	let currentBingoCardArray = [];
    	// skip the first two lines, bingo cards start on line 3
    	let bingoCardArray = inputArr.slice(2);
    	for (const line of bingoCardArray) {
    		// on blank line, reset accumulated line and send for parsing
    		if (line.trim().length === 0) {
    			splitArray.push(parseBingoCard(currentBingoCardArray.join('\n')));
    			currentBingoCardArray = [];
    		} else {
    			currentBingoCardArray.push(line);
    		}
    	}
    	// flushing the buffer, in case the last line is not newline-terminated
    	if (currentBingoCardArray.length != 0) {
    		splitArray.push(parseBingoCard(currentBingoCardArray.join('\n')));
    	}
    	return splitArray;
    }
    
    function findBingo(bingoCard, numbersCalled) {
    	// look for horizontal bingos
    	for (const row of bingoCard) {
    		let horizontalBingo = true;
    		for (const number of row) {
    			if (!numbersCalled.includes(parseInt(number))) {
    				horizontalBingo = false;
    				break;
    			}
    		}
    		if (horizontalBingo === true) {
    			return true;
    		}
    	}
    	// look for vertical bingos
    	for (let colIndex = 0; colIndex < bingoCard[0].length; colIndex++) {
    		let verticalBingo = true;
    		for (const row of bingoCard) {
    			if (!numbersCalled.includes(parseInt(row[colIndex]))) {
    				verticalBingo = false;
    				break;
    			}
    		}
    		if (verticalBingo === true) {
    			return true;
    		}
    	}
    	return false;
    }
    
    function findBingoScore(bingoCard, numbersCalled) {
    	let pointSpaces = [];
    	for (const row of bingoCard) {
    		for (const num of row) {
    			if (!numbersCalled.includes(parseInt(num))) {
    				pointSpaces.push(parseInt(num));
    			}
    		}
    	}
    	return (pointSpaces.reduce((a,b) => a+b) * numbersCalled[numbersCalled.length-1]);
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let numbersToCall = inputArr[0].split(',').map(e => parseInt(e));
    	console.log("DEBUG: numbersToCall = " + numbersToCall);
    	let bingoCardArray = splitBingoCards();
    	let numberCalledIndex = 0;
    	let numbersCalled = [];
    	let winningBoards = [];
    	while (winningBoards.length < bingoCardArray.length) {
    		// call new number
    		numberCalledIndex++;
    		numbersCalled = numbersToCall.slice(0,numberCalledIndex);
    
    		// check for wins
    		for (let i=0; i<bingoCardArray.length; i++) {
    			if (!winningBoards.includes(i)) {
    				if (findBingo(bingoCardArray[i], numbersCalled)) {
    					winningBoards.push(i);
    				}
    			}
    		}
    	}
    	console.log("DEBUG: last winningBoard =\n" + bingoCardArray[winningBoards[winningBoards.length-1]].join('\n'));
    	console.log("DEBUG: numbersCalled = " + numbersCalled);
    	console.log("Answer found! " + findBingoScore(bingoCardArray[winningBoards[winningBoards.length-1]],numbersCalled));
    })();
    
    3 votes
  10. tjf
    Link
    Python golf, day 4. This was a pretty poor golf on my part (at least compared to yesterday), but I'm calling it a day here. Part 1 I=[*map(str.strip,open(0))] N=I.pop(0).split(',')...

    Python golf, day 4. This was a pretty poor golf on my part (at least compared to yesterday), but I'm calling it a day here.

    Part 1
    I=[*map(str.strip,open(0))]
    N=I.pop(0).split(',')
    B=[[*map(str.split,I[i:i+5])]for i in range(1,len(I),6)]
    R=*range(5),
    m=lambda b,n:[[''if b[r][c]==n else b[r][c]for c in R]for r in R]
    k=lambda b:any([not any(r)for r in b]+[not any([b[r][c]for r in R])for c in R])
    for i in range(len(N)):
     B=[m(b,N[i])for b in B]
     w=[k(b)for b in B]
     if any(w):break
    W=B[w.index(1)]
    print(sum([sum([int(W[r][c])for c in R if W[r][c]])for r in R])*int(N[i]))
    
    Part 2
    I=[*map(str.strip,open(0))]
    N=I.pop(0).split(',')
    B=[[*map(str.split,I[i:i+5])]for i in range(1,len(I),6)]
    R=*range(5),
    m=lambda b,n:[[''if b[r][c]==n else b[r][c]for c in R]for r in R]
    k=lambda b:any([not any(r)for r in b]+[not any([b[r][c]for r in R])for c in R])
    for i in range(len(N)):
     B=filter(None,B);B=[m(b,N[i])for b in B];w=[k(b)for b in B]
     if any(w)and len(B)-1:
      for j in range(sum(w)):t=w.index(1);B[t]=w[t]=0
     elif w[0]:break
    W=B[w.index(1)]
    print(sum([sum([int(W[r][c])for c in R if W[r][c]])for r in R])*int(N[i]))
    
    3 votes
  11. kari
    Link
    I really enjoyed today's problem(s)! I think my code isn't too terrible for once. Rust (both parts) use crate::lib::aoc; #[derive(Clone, Copy, Debug)] struct Row(u32, u32, u32, u32, u32);...

    I really enjoyed today's problem(s)! I think my code isn't too terrible for once.

    Rust (both parts)
    use crate::lib::aoc;
    
    #[derive(Clone, Copy, Debug)]
    struct Row(u32, u32, u32, u32, u32);
    
    #[derive(Clone, Copy, Debug)]
    struct Board(Row, Row, Row, Row, Row);
    
    impl Row {
        fn new(row: String) -> Self {
            let row = row.trim().split_whitespace().collect::<Vec<_>>();
            if row.len() != 5 {
                panic!("Tried to read a row with len != 5");
            }
    
            Row(
                row[0]
                    .parse::<u32>()
                    .expect("Error reading row 0 (somewhere)"),
                row[1]
                    .parse::<u32>()
                    .expect("Error reading row 1 (somewhere)"),
                row[2]
                    .parse::<u32>()
                    .expect("Error reading row 2 (somewhere)"),
                row[3]
                    .parse::<u32>()
                    .expect("Error reading row 3 (somewhere)"),
                row[4]
                    .parse::<u32>()
                    .expect("Error reading row 4 (somewhere)"),
            )
        }
    
        fn check_for_num(&mut self, num: u32) {
            if self.0 == num {
                self.0 = 0;
            }
            if self.1 == num {
                self.1 = 0;
            }
            if self.2 == num {
                self.2 = 0;
            }
            if self.3 == num {
                self.3 = 0;
            }
            if self.4 == num {
                self.4 = 0;
            }
        }
    
        fn get_score(&mut self) -> u32 {
            self.0 + self.1 + self.2 + self.3 + self.4
        }
    }
    
    impl Board {
        fn new(rows: Vec<String>) -> Self {
            if rows.len() != 5 {
                panic!("Tried to read a board with len != 5");
            }
    
            Board(
                Row::new(rows[0].to_owned()),
                Row::new(rows[1].to_owned()),
                Row::new(rows[2].to_owned()),
                Row::new(rows[3].to_owned()),
                Row::new(rows[4].to_owned()),
            )
        }
    
        fn check_for_num(&mut self, num: u32) {
            self.0.check_for_num(num);
            self.1.check_for_num(num);
            self.2.check_for_num(num);
            self.3.check_for_num(num);
            self.4.check_for_num(num);
        }
    
        fn check_diags(&mut self) -> bool {
            if self.0 .0 + self.1 .1 + self.2 .2 + self.3 .3 + self.4 .4 == 0
                || self.0 .4 + self.1 .3 + self.2 .2 + self.3 .1 + self.4 .4 == 0
            {
                true
            } else {
                false
            }
        }
    
        fn check_verticals(&mut self) -> bool {
            if self.0 .0 + self.1 .0 + self.2 .0 + self.3 .0 + self.4 .0 == 0
                || self.0 .1 + self.1 .1 + self.2 .1 + self.3 .1 + self.4 .1 == 0
                || self.0 .2 + self.1 .2 + self.2 .2 + self.3 .2 + self.4 .2 == 0
                || self.0 .3 + self.1 .3 + self.2 .3 + self.3 .3 + self.4 .3 == 0
                || self.0 .4 + self.1 .4 + self.2 .4 + self.3 .4 + self.4 .4 == 0
            {
                true
            } else {
                false
            }
        }
    
        fn has_won(&mut self) -> bool {
            if self.0.get_score() == 0
                || self.1.get_score() == 0
                || self.2.get_score() == 0
                || self.3.get_score() == 0
                || self.4.get_score() == 0
                || self.check_diags()
                || self.check_verticals()
            {
                return true;
            }
    
            false
        }
    
        fn get_score(&mut self) -> u32 {
            self.0.get_score()
                + self.1.get_score()
                + self.2.get_score()
                + self.3.get_score()
                + self.4.get_score()
        }
    }
    
    impl PartialEq for Row {
        fn eq(&self, other: &Self) -> bool {
            self.0 == other.0
                && self.1 == other.1
                && self.2 == other.2
                && self.3 == other.3
                && self.4 == other.4
        }
    }
    
    impl Eq for Row {}
    
    impl PartialEq for Board {
        fn eq(&self, other: &Self) -> bool {
            self.0 == other.0
                && self.1 == other.1
                && self.2 == other.2
                && self.3 == other.3
                && self.4 == other.4
        }
    }
    
    impl Eq for Board {}
    
    pub fn run4() {
        let lines = aoc::get_lines("./inputs/day4.in");
    
        // Split the numbers off
        let numbers = lines[0]
            .split(",")
            .map(|number| {
                number
                    .parse::<u32>()
                    .expect("Unable to read some number as a u32")
            })
            .collect::<Vec<_>>();
    
        // Get just the boards
        let lines = lines
            .get(2..)
            .expect("Did you forget to include boards in the input?");
        let mut boards: Vec<Board> = Vec::new();
        for i in (0..lines.len()).step_by(6) {
            boards.push(Board::new(
                lines
                    .get(i..(i + 5))
                    .expect("There was an issue slicing your board!")
                    .to_vec(),
            ));
        }
    
        let mut first_win = false;
        let mut winning_number: u32 = 0;
        let mut winning_score: u32 = 0;
        let mut losing_number: u32 = 0;
        let mut losing_score: u32 = 0;
        // TODO: Is there a way to make this loop more idiomatic?
        for number in numbers {
            let mut winning_boards: Vec<Board> = Vec::new();
            for board in &mut boards {
                board.check_for_num(number);
    
                if board.has_won() {
                    if !first_win {
                        first_win = true;
                        winning_number = number;
                        winning_score = board.get_score();
                    }
                    winning_boards.push(*board);
                }
            }
    
            boards.retain(|board| !winning_boards.contains(board));
    
            if boards.is_empty() {
                losing_number = number;
                losing_score = winning_boards[0].get_score();
            }
    
            if losing_score > 0 {
                break;
            }
        }
    
        aoc::output(
            4,
            "score",
            (winning_number * winning_score) as i32,
            (losing_number * losing_score) as i32,
        );
    }
    

    My solution only works with a 5x5 grid but I wanted to try out tuple structs.

    3 votes
  12. Liru
    Link
    I cleaned up my solution for today. Not really my best Rust code (pretty brittle, assumes a lot of things about the puzzle) but it does the job well. Solution type Num = usize; #[derive(Debug,...

    I cleaned up my solution for today. Not really my best Rust code (pretty brittle, assumes a lot of things about the puzzle) but it does the job well.

    Solution
    type Num = usize;
    
    #[derive(Debug, Clone)]
    pub struct Board {
        board: Vec<Vec<Tile>>,
        solved: bool,
    }
    
    #[derive(Debug, Clone, Copy)]
    pub struct Tile {
        value: Num,
        marked: bool,
    }
    
    impl Tile {
        pub fn new(value: &Num) -> Self {
            let value = value.clone();
            Self {
                value,
                marked: false,
            }
        }
    
        pub fn marked(&self) -> bool {
            self.marked
        }
        pub fn unmarked(&self) -> bool {
            !self.marked
        }
    }
    
    impl Board {
        pub fn new(mut board: Vec<Vec<Num>>) -> Self {
            let board = board
                .iter_mut()
                .map(|x| x.iter().map(Tile::new).collect())
                .collect();
    
            Self {
                board,
                solved: false,
            }
        }
    
        fn row_solved(&self) -> bool {
            self.board.iter().any(|x| x.iter().all(Tile::marked))
        }
    
        fn col_solved(&self) -> bool {
            let transposed = transpose(&self.board);
            transposed.iter().any(|x| x.iter().all(Tile::marked))
        }
    
        /// Returns if this particular call solved the board.
        pub fn call(&mut self, number: Num) -> bool {
            if self.solved {
                return false;
            }
            for i in self
                .board
                .iter_mut()
                .flatten()
                .filter(|tile| tile.value == number)
            {
                i.marked = true;
            }
    
            if self.row_solved() || self.col_solved() {
                self.solved = true;
            }
    
            self.solved
        }
    
        pub fn score(&self) -> usize {
            //  self.board.iter().flatten().filter_map(|&(val, picked)| picked.then_some(val)).sum() // only in unstable
    
            self.board
                .iter()
                .flatten()
                .filter_map(|&tile| if tile.marked { None } else { Some(tile.value) })
                .sum()
        }
    }
    
    fn transpose<T>(v: &[Vec<T>]) -> Vec<Vec<T>>
    where
        T: Clone,
    {
        assert!(!v.is_empty());
        (0..v[0].len())
            .map(|i| v.iter().map(|inner| inner[i].clone()).collect::<Vec<T>>())
            .collect()
    }
    
    #[derive(Debug)]
    pub struct Game {
        numbers: Vec<Num>,
        boards: Vec<Board>,
    }
    
    impl Game {
        pub fn new(numbers: Vec<Num>, boards: Vec<Board>) -> Self {
            Self { numbers, boards }
        }
    
        pub fn run(&mut self) -> usize {
            for &called_number in &self.numbers {
                for board in self.boards.iter_mut() {
                    if board.call(called_number) {
                        return board.score() * called_number;
                    }
                }
            }
    
            unreachable!()
        }
    
        pub fn run_2(&mut self) -> usize {
            let mut won = 0;
            let num_boards = self.boards.len();
            for &called_number in &self.numbers {
                for board in self.boards.iter_mut() {
                    if board.call(called_number) {
                        won += 1;
                        if won == num_boards {
                            return board.score() * called_number;
                        }
                    }
                }
            }
            unreachable!()
        }
    
        pub fn from_str(input: &str) -> Self {
            let mut iter = input.trim().split("\n\n");
            let mut boards = vec![];
    
            let numbers: Vec<Num> = iter
                .next()
                .unwrap()
                .split_terminator(',')
                .flat_map(str::parse)
                .collect();
    
            for board_str in iter {
                let board = Board::new(
                    board_str
                        .trim()
                        .lines()
                        .map(|line| {
                            line.split_ascii_whitespace()
                                .flat_map(str::parse)
                                .collect::<Vec<_>>()
                        })
                        .collect(),
                );
                boards.push(board);
            }
    
            Game { boards, numbers }
        }
    }
    

    There's a lot I don't like about it, like the transpose call. It's slow, but it looks nice, at least.

    3 votes
  13. Gyrfalcon
    Link
    Today was a good day for me to actually try and do things right and maybe define some functions and use object oriented programming! Much like others on the thread, my part 2 was not too difficult...

    Today was a good day for me to actually try and do things right and maybe define some functions and use object oriented programming! Much like others on the thread, my part 2 was not too difficult to add in once part 1 was complete thanks to the preparation. Working through these I have started to notice some things I am not loving about Nim. Not a huge gripe, but something that irked me today is that the all function for checking if everything in a sequence meets a condition always requires a proc to work, and it won't work with sensible default on a sequence of boolean types. Anyway, parts 1 and 2, with 2 as a diff since I added it into things:

    Part 1
    import sugar, std/[strformat, sequtils, strutils, math, enumerate]
    
    
    type Board = object of RootObj
      numbers : array[0 .. 4, array[0 .. 4, int]]
      called : array[0 .. 4, array[0 .. 4, bool]]
    
    proc markNumber(self: var Board, calledNumber: int) =
      for rowIdx, row in self.numbers:
        for colIdx, number in row:
          if number == calledNumber:
            self.called[rowIdx][colIdx] = true
            return
    
    proc isWinning(self: Board): bool =
      
      for row in self.called:
        if all(row, proc(x : bool): bool = x):
          return true
    
      for colIdx in 0 ..< self.called.len:
        var column: seq[bool]
        for row in self.called:
          column.add(row[colIdx])
        
        if all(column, proc(x : bool): bool = x):
          return true
    
      return false
    
    proc score(self: Board): int = 
      
      var score: int
    
      for rowIdx, row in self.called:
        for colIdx, called in row:
          if not called:
            score += self.numbers[rowIdx][colIdx]
    
      return score
    
    
    proc parseCalledNumbers(fileName : string): seq[int] =
      var input: seq[string] = split(readLines(fileName, 1)[0], ",")
      return map(input, proc(x : string): int = parseInt(x))
    
    proc parseBoards(fileName : string): seq[Board] =
      var input: seq[string] = collect(newSeq):
        for line in fileName.lines: line
      input = input[2 .. ^1]
      var boards: seq[Board]
    
      for startIdx in countup(0, input.len, 6):
        var newBoard: Board
        for rowIdx, inputIdx in enumerate(startIdx .. (startIdx + 4)):
          var values: seq[string] = split(input[inputIdx], " ")
          values = filter(values, proc(x : string): bool = x.len > 0)
          var intValues: seq[int] = map(values, proc(x : string): int = parseInt(x))
          for colIdx, value in intValues:
            newBoard.numbers[rowIdx][colIdx] = value
    
        boards.add(newBoard)
    
      return boards
    
    proc playGame(calledNumbers: seq[int], boards: var seq[Board]): int =
    
      for calledNumber in calledNumbers:
        for boardIdx in 0 ..< boards.len:
          boards[boardIdx].markNumber(calledNumber)
    
        for board in boards:
          if board.isWinning():
            return board.score() * calledNumber
    
      return 0
    
      
    proc main(inputFile: string) =
      var calledNumbers: seq[int] = parseCalledNumbers(inputFile)
      var boards: seq[Board] = parseBoards(inputFile)
      var score: int = playGame(calledNumbers, boards)
    
      echo &"Score of the winning board is {score}"
      
    
    when is_main_module:
      main("input.txt")
    
    Part 2
    @@ -63,25 +63,40 @@ proc parseBoards(fileName : string): seq[Board] =
     
       return boards
     
    -proc playGame(calledNumbers: seq[int], boards: var seq[Board]): int =
    +proc playGame(calledNumbers: seq[int], boards: var seq[Board]): (int, int) =
    +
    +  var firstScore: int
    +  var lastScore: int
     
       for calledNumber in calledNumbers:
         for boardIdx in 0 ..< boards.len:
           boards[boardIdx].markNumber(calledNumber)
     
    -    for board in boards:
    -      if board.isWinning():
    -        return board.score() * calledNumber
    +    if firstScore == 0:
    +      for board in boards:
    +        if board.isWinning():
    +          firstScore = board.score() * calledNumber
    +
    +    if firstScore != 0:
    +      if all(map(boards, isWinning), proc(x: bool): bool = x):
    +        lastScore = boards[0].score() * calledNumber
    +        return (firstScore, lastScore)
    +      else:
    +        boards = filter(boards, proc(x: Board): bool = not x.isWinning())
     
    -  return 0
    +  return (0, 0)
     
       
     proc main(inputFile: string) =
       var calledNumbers: seq[int] = parseCalledNumbers(inputFile)
       var boards: seq[Board] = parseBoards(inputFile)
    -  var score: int = playGame(calledNumbers, boards)
    +  var firstScore: int
    +  var lastScore: int
    +  (firstScore, lastScore) = playGame(calledNumbers, boards)
    +
    +  echo &"Score of the winning board is {firstScore}"
     
    -  echo &"Score of the winning board is {score}"
    +  echo &"Score of last to win board is {lastScore}"
       
     
     when is_main_module:
    
    3 votes
  14. jzimbel
    (edited )
    Link
    Elixir, with a message-passing/agent example I decided to define a "BingoPlayer" Agent to create an API around updating and querying each bingo board. This is Elixir's version of object-oriented...

    Elixir, with a message-passing/agent example

    I decided to define a "BingoPlayer" Agent to create an API around updating and querying each bingo board. This is Elixir's version of object-oriented programming—it's closer to the original sense of the term used by Smalltalk, for example. "Objects" are Elixir processes running send/receive loops, which communicate with each other exclusively via message passing. It's comparable to a server/client relationship, just running within the confines of your own application. The Agent module provides an abstraction over this to simplify setting up a stateful process.

    One part I'm not happy with is the bingo-checking code. It's an unreadable mess! I think a list comprehension would have been better for generating all the possible rows/columns to check, but my brain was a little fried at the time.

    BingoPlayer agent
    defmodule AdventOfCode.Solution.Year2021.Day04.BingoPlayer do
      use Agent
    
      ## Public client functions, which run in the calling process
    
      def start_link(initial_board) do
        Agent.start_link(fn -> initial_board end)
      end
    
      def call_draw(pid, draw) do
        Agent.update(pid, &do_call_draw(&1, draw))
      end
    
      def bingo?(pid) do
        Agent.get(pid, &do_bingo?/1)
      end
    
      def score(pid, last_draw) do
        Agent.get(pid, &do_score(&1, last_draw))
      end
    
      ## Server functions, which run in the agent process
    
      defp do_call_draw(board, draw) do
        Enum.into(board, %{}, fn
          {k, {^draw, _}} -> {k, {draw, true}}
          entry -> entry
        end)
      end
    
      defp do_bingo?(board) do
        row_column_coord_lists()
        |> Stream.map(fn coords -> Enum.map(coords, fn coord -> board[coord] |> elem(1) end) end)
        |> Enum.any?(&Enum.all?/1)
      end
    
      defp row_column_coord_lists do
        columns = fn x -> Enum.map(0..4, &{x, &1}) end
        rows = fn y -> Enum.map(0..4, &{&1, y}) end
    
        Stream.map(0..4, columns) |> Stream.concat(Stream.map(0..4, rows))
      end
    
      defp do_score(board, last_draw) do
        board
        |> Map.values()
        |> Enum.reject(fn {_n, marked} -> marked end)
        |> Enum.map(fn {n, _marked} -> n end)
        |> Enum.sum()
        |> Kernel.*(last_draw)
      end
    end
    
    Solution, both parts
    defmodule AdventOfCode.Solution.Year2021.Day04 do
      alias __MODULE__.BingoPlayer
    
      def part1(input) do
        {draws, boards} = parse_input(input)
    
        players =
          boards
          |> Enum.map(&BingoPlayer.start_link/1)
          |> Enum.map(fn {:ok, pid} -> pid end)
    
        get_first_winner_score(draws, players)
      end
    
      def part2(input) do
        {draws, boards} = parse_input(input)
    
        players =
          boards
          |> Enum.map(&BingoPlayer.start_link/1)
          |> Enum.map(fn {:ok, pid} -> pid end)
    
        get_last_winner_score(draws, players)
      end
    
      defp get_first_winner_score([draw | draws], players) do
        Enum.each(players, &BingoPlayer.call_draw(&1, draw))
    
        players
        |> Enum.find(&BingoPlayer.bingo?/1)
        |> case do
          nil -> get_first_winner_score(draws, players)
          player -> BingoPlayer.score(player, draw)
        end
      end
    
      defp get_last_winner_score([draw | draws], players) do
        Enum.each(players, &BingoPlayer.call_draw(&1, draw))
    
        if Enum.all?(players, &BingoPlayer.bingo?/1) do
          players
          |> List.last()
          |> BingoPlayer.score(draw)
        else
          get_last_winner_score(draws, Enum.reject(players, &BingoPlayer.bingo?/1))
        end
      end
    
      defp parse_input(input) do
        [draws | boards] = String.split(input, "\n\n", trim: true)
    
        {parse_draws(draws), Enum.map(boards, &parse_board/1)}
      end
    
      defp parse_draws(draws) do
        draws
        |> String.split(",")
        |> Enum.map(&String.to_integer/1)
      end
    
      defp parse_board(board) do
        for {line, y} <- board |> String.split("\n") |> Enum.with_index(),
            {num, x} <- line |> String.split(" ", trim: true) |> Enum.with_index(),
            into: %{},
            do: {{x, y}, {String.to_integer(num), false}}
      end
    end
    
    3 votes
  15. Kremor
    (edited )
    Link
    Solutions in Go. So far I haven't included the code that reads the data, I still won't include but to make the code more clear I will start typing the ReadData results. Part 1 type Cell struct {...

    Solutions in Go.

    So far I haven't included the code that reads the data, I still won't include but to make the code more clear I will start typing the ReadData results.

    Part 1
    type Cell struct {
    	Check bool
    	N     int
    }
    
    func main() {
    	I, N := 0, 0
    	var numbers []int
    	var boards [][][]Cell
    
    	numbers, boards = ReadData()
    
    	for _, n := range numbers {
    		isWin := false
    		for i, board := range boards {
    			MarkCheck(&board, n)
    			if CheckWin(board) {
    				I, N, isWin = i, n, true
    				break
    			}
    		}
    
    		if isWin {
    			break
    		}
    	}
    
    	sum := GetBoardSum(boards[I])
    	fmt.Println(sum * N)
    }
    
    func CheckWin(b [][]Cell) bool {
    	column, row := true, true
    
    	for i, r := range b {
    		for j, _ := range r {
    			column = column && b[j][i].Check
    			row = row && b[i][j].Check
    		}
    
    		if column || row {
    			return true
    		}
    
    		column, row = true, true
    	}
    
    	return false
    }
    
    func GetBoardSum(board [][]Cell) int {
    	sum := 0
    
    	for _, row := range board {
    		for _, cell := range row {
    			if !cell.Check {
    				sum += cell.N
    			}
    		}
    	}
    
    	return sum
    }
    
    func MarkCheck(b *[][]Cell, n int) {
    	for i, row := range *b {
    		for j, cell := range row {
    			if cell.N == n {
    				(*b)[i][j].Check = true
    			}
    		}
    	}
    }
    
    Part 2
    type Cell struct {
    	Check bool
    	N     int
    }
    
    func main() {
    	I, N, wins := 0, 0, []int{}
    	var numbers []int
    	var boards [][][]Cell
    
    	numbers, boards = ReadData()
    
    	for _, n := range numbers {
    		finish := false
    		for i, board := range boards {
    			MarkCheck(&board, n)
    			if CheckWin(board) && !AlreadyWon(&wins, i) {
    				wins = append(wins, i)
    
    				if len(wins) == len(boards) {
    					I, N, finish = i, n, true
    					break
    				}
    			}
    		}
    
    		if finish {
    			break
    		}
    	}
    
    	sum := GetBoardSum(boards[I])
    	fmt.Println(sum * N)
    }
    
    func AlreadyWon(a *[]int, n int) bool {
    	for _, nn := range *a {
    		if n == nn {
    			return true
    		}
    	}
    	return false
    }
    
    func CheckWin(b [][]Cell) bool {
    	column := true
    	row := true
    
    	for i, r := range b {
    		for j, _ := range r {
    			column = column && b[j][i].Check
    			row = row && b[i][j].Check
    		}
    
    		if column || row {
    			return true
    		}
    
    		column, row = true, true
    	}
    
    	return false
    }
    
    func GetBoardSum(board [][]Cell) int {
    	sum := 0
    
    	for _, row := range board {
    		for _, cell := range row {
    			if !cell.Check {
    				sum += cell.N
    			}
    		}
    	}
    
    	return sum
    }
    
    func MarkCheck(b *[][]Cell, n int) {
    	for i, row := range *b {
    		for j, cell := range row {
    			if cell.N == n {
    				(*b)[i][j].Check = true
    			}
    		}
    	}
    }
    
    2 votes
  16. blitz
    Link
    Like everyone else here, I was really happy to see that part 2 was pretty easily implementable if you set up the data structures in the right way! I did the problem with arrays of arrays just to...

    Like everyone else here, I was really happy to see that part 2 was pretty easily implementable if you set up the data structures in the right way!

    I did the problem with arrays of arrays just to see how they differed from Vecs, though I know that the actual use cases for owned arrays are scarce in Rust.

    Day 4 in Rust on Sourcehut

    2 votes
  17. spit-evil-olive-tips
    Link
    first problem where object-orientation really makes sense to use. Part 1 import re BOARD_RE = re.compile('(\d+)', re.MULTILINE) SIZE = 5 class Board: def __init__(self, entries): assert...

    first problem where object-orientation really makes sense to use.

    Part 1
    import re
    
    BOARD_RE = re.compile('(\d+)', re.MULTILINE)
    
    SIZE = 5
    
    class Board:
        def __init__(self, entries):
            assert len(entries) == (SIZE ** 2)
            self.entries = entries
            self.marked = [False] * len(entries)
    
        @classmethod
        def parse(cls, input):
            entries = [int(entry) for entry in BOARD_RE.findall(input)]
            return cls(entries)
    
        def mark(self, value):
            for i, entry in enumerate(self.entries):
                if entry == value:
                    self.marked[i] = True
    
        def get_row(self, index):
            return range(index*SIZE, (index+1) * SIZE)
    
        def get_col(self, index):
            return range(index, SIZE*SIZE, SIZE)
    
        def is_winner(self):
            for i in range(SIZE):
                row = self.get_row(i)
                if all(self.marked[index] for index in row):
                    return True
    
                col = self.get_col(i)
                if all(self.marked[index] for index in col):
                    return True
    
            return False
    
        def sum_of_unmarked(self):
            sum = 0
            for index, entry in enumerate(self.entries):
                if not self.marked[index]:
                    sum += entry
            return sum
    
    
    def main():
        with open('004.txt') as file:
            numbers = [int(number) for number in file.readline().split(',')]
            board_lines = file.read()
    
        boards = []
        for board_string in board_lines.split('\n\n'):
            board = Board.parse(board_string)
            boards.append(board)
    
        for number in numbers:
            for board in boards:
                board.mark(number)
    
            for board in boards:
                if board.is_winner():
                    print(number * board.sum_of_unmarked())
                    return
    
    main()
    
    Part 2
    --- 004a.py     2021-12-04 14:54:18.621412650 -0800
    +++ 004b.py     2021-12-04 14:54:38.418542209 -0800
    @@ -60,9 +60,12 @@
             for board in boards:
                 board.mark(number)
     
    -        for board in boards:
    +        for board in list(boards):
                 if board.is_winner():
    -                print(number * board.sum_of_unmarked())
    -                return
    +                if len(boards) == 1:
    +                    print(number * board.sum_of_unmarked())
    +                    return
    +
    +                boards.remove(board)
     
     main()
    

    I had a nasty copy-paste bug (all(self.marked[index] for index in row) twice instead of having col in the second one) where I was checking each row twice to see if the board had won, and not checking the columns at all.

    my solution to part 1 actually got the right answer despite this, and then I had an interminable amount of head-scratching trying to figure out why I was getting the wrong answer to part 2, until I noticed that stupid little copy-paste error. and then of course it immediately worked.

    2 votes
  18. Crestwave
    Link
    This was a lot simpler after I realized that diagonals were not included. Part 1 #!/usr/bin/awk -f NR == 1 { split($0, input, ",") } NF == 0 { y = 0 boards += 1 } NF == 5 { y += 1 for (x = 1; x <=...

    This was a lot simpler after I realized that diagonals were not included.

    Part 1
    #!/usr/bin/awk -f
    NR == 1 { split($0, input, ",") }
    
    NF == 0 {
    	y = 0
    	boards += 1
    }
    
    NF == 5 {
    	y += 1
    
    	for (x = 1; x <= NF; ++x) {
    		board[x, y, boards] = $x
    	}
    }
    
    END {
    	for (i in board) {
    		marked[i] = board[i]
    	}
    
    	for (num in input) {
    		for (i in board) {
    			if (board[i] == input[num]) {
    				marked[i] = 0
    
    				split(i, xy, SUBSEP)
    
    				for (j = 1; j <= 2; ++j) {
    					bingo[j, xy[j], xy[3]] += 1
    					if (bingo[j, xy[j], xy[3]] >= 5) {
    						for (x = 1; x <= 5; ++x) {
    							for (y = 1; y <= 5; ++y) {
    								total += marked[x, y, xy[3]]
    							}
    						}
    
    						print total * input[num]
    						exit
    					}
    				}
    			}
    		}
    	}
    }
    
    Part 2
    #!/usr/bin/awk -f
    NR == 1 { split($0, input, ",") }
    
    NF == 0 {
    	y = 0
    	boards += 1
    }
    
    NF == 5 {
    	y += 1
    
    	for (x = 1; x <= NF; ++x) {
    		board[x, y, boards] = $x
    	}
    }
    
    END {
    	for (i in board) {
    		marked[i] = board[i]
    	}
    
    	for (num in input) {
    		for (i in board) {
    			if (board[i] == input[num]) {
    				marked[i] = 0
    
    				split(i, xy, SUBSEP)
    
    				for (j = 1; j <= 2; ++j) {
    					bingo[j, xy[j], xy[3]] += 1
    					if (bingo[j, xy[j], xy[3]] >= 5) {
    						total = 0
    
    						for (x = 1; x <= 5; ++x) {
    							for (y = 1; y <= 5; ++y) {
    								total += marked[x, y, xy[3]]
    							}
    						}
    
    						if (! won[xy[3]]) {
    							won[xy[3]] = 1
    							wins += 1
    						}
    
    						if (wins == boards) {
    							print total * input[num]
    							exit
    						}
    					}
    				}
    			}
    		}
    	}
    }
    
    2 votes