From 846b02a81523b7aec65ee58ad0cbb50b9c515a28 Mon Sep 17 00:00:00 2001 From: Nick Krichevsky Date: Sat, 4 Dec 2021 13:49:08 -0500 Subject: [PATCH] Add day 4 solution --- day4/Cargo.lock | 39 +++++++ day4/Cargo.toml | 9 ++ day4/src/main.rs | 284 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 day4/Cargo.lock create mode 100644 day4/Cargo.toml create mode 100644 day4/src/main.rs diff --git a/day4/Cargo.lock b/day4/Cargo.lock new file mode 100644 index 0000000..458506a --- /dev/null +++ b/day4/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day4" +version = "0.1.0" +dependencies = [ + "nom", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" diff --git a/day4/Cargo.toml b/day4/Cargo.toml new file mode 100644 index 0000000..6b6a458 --- /dev/null +++ b/day4/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "day4" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nom = "7.1" diff --git a/day4/src/main.rs b/day4/src/main.rs new file mode 100644 index 0000000..5cceedc --- /dev/null +++ b/day4/src/main.rs @@ -0,0 +1,284 @@ +#![warn(clippy::all, clippy::pedantic)] + +use nom::{ + bytes::complete::{tag, take_while1}, + character::complete::char, + combinator::{eof, map_res, opt}, + multi::{many0, many_m_n, separated_list1}, + sequence::{preceded, separated_pair, tuple}, + IResult, +}; +use std::collections::VecDeque; +use std::env; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::fs; + +const BOARD_SIZE: usize = 5; + +#[derive(Debug, Clone, Copy)] +enum BingoTile { + Unmarked(u8), + Marked(u8), +} + +#[derive(Clone)] +struct BingoBoard([[BingoTile; BOARD_SIZE]; BOARD_SIZE]); + +#[derive(Debug, Clone)] +struct Input { + calls: Vec, + boards: Vec, +} + +/// `BoardState` indicates whether or not a board has won +struct BoardState { + won: bool, + board: BingoBoard, +} + +struct BingoGame { + calls: VecDeque, + boards: Vec, +} + +/// `BingoPlayer` is an iterator that will iterate over the successive winners of a bingo game. +struct BingoPlayer<'a> { + game: &'a mut BingoGame, +} + +impl Display for BingoTile { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let width = f.width(); + match self { + BingoTile::Marked(_) => write!(f, "{:>width$}", "x", width = width.unwrap_or(1)), + BingoTile::Unmarked(n) => { + write!( + f, + "{:>width$}", + n, + width = width.unwrap_or_else(|| n.to_string().len()) + ) + } + } + } +} + +impl Debug for BingoBoard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for row in self.0 { + for tile in row { + write!(f, "{:2} ", tile)?; + } + + writeln!(f)?; + } + + Ok(()) + } +} + +impl From for BingoGame { + /// Start a new game from the given puzzle input + fn from(input: Input) -> Self { + let calls = VecDeque::from(input.calls); + let boards = input + .boards + .into_iter() + .map(|board| BoardState { won: false, board }) + .collect(); + + Self { calls, boards } + } +} + +impl BingoGame { + /// Return an iterator to play this bingo game + fn play(&mut self) -> BingoPlayer { + BingoPlayer { game: self } + } +} + +impl BingoBoard { + /// Check if this board has won the game + fn is_winner(&self) -> bool { + assert_eq!( + self.0.len(), + BOARD_SIZE, + "Board number of rows doesn't match expected size" + ); + assert_eq!( + self.0[0].len(), + BOARD_SIZE, + "Board number of cols doesn't match expected size" + ); + + for col in 0..BOARD_SIZE { + let mut won_by_col = true; + for row in 0..BOARD_SIZE { + let won_by_row = self.0[row] + .iter() + .all(|item| matches!(item, BingoTile::Marked(_))); + if won_by_row { + return true; + } else if !matches!(self.0[row][col], BingoTile::Marked(_)) { + won_by_col = false; + break; + } + } + if won_by_col { + return true; + } + } + + false + } + + /// Mark the given number on the board, if it exists + fn mark_n(&mut self, n: u8) { + for row in &mut self.0 { + for tile in row { + if let BingoTile::Unmarked(tile_n) = tile { + if *tile_n == n { + *tile = BingoTile::Marked(n); + } + } + } + } + } +} + +impl<'a> Iterator for BingoPlayer<'a> { + type Item = (u8, BingoBoard); + fn next(&mut self) -> Option { + while let Some(call) = self.game.calls.pop_front() { + let boards = &mut self.game.boards; + let mut winning_board: Option = None; + for BoardState { + won: board_has_won, + board, + } in boards.iter_mut() + { + // If a board has one, do not consider it as an item to check. + // This prevents winning boards from attempting to win more than once. + if *board_has_won { + continue; + } + + board.mark_n(call); + if board.is_winner() { + // This is a bit of a hack, but it will work given the use of the iterator in part 2. + // (in production code I'd probably return _all_ boards that have won) + // If two boards win at the same time, the last one needs. + // + // We should mark all boards, though, and not return immediately, so that an early board winning + // does not ruin the winners for everyone else + winning_board = Some(board.clone()); + + // We could probably remove the board from the boards vec, but for debugging, this changes the + // indexes, which makes it difficult to follow the continuity of boards + *board_has_won = true; + } + } + + if let Some(winner) = winning_board { + return Some((call, winner)); + } + } + + None + } +} + +fn parse_bingo_number(input: &str) -> IResult<&str, u8> { + map_res(take_while1(|c: char| c.is_ascii_digit()), str::parse)(input) +} + +fn parse_bingo_calls(calls_line: &str) -> IResult<&str, Vec> { + separated_list1(char(','), parse_bingo_number)(calls_line) +} + +fn parse_bingo_board(input_chunk: &str) -> IResult<&str, BingoBoard> { + let (remaining, raw_board) = separated_list1( + char('\n'), + separated_list1( + many_m_n(1, 2, char(' ')), + preceded(opt(char(' ')), parse_bingo_number), + ), + )(input_chunk)?; + + let mut board = [[BingoTile::Unmarked(0_u8); BOARD_SIZE]; BOARD_SIZE]; + for i in 0..board.len() { + let board_row = &mut board[i]; + let raw_board_row = &raw_board[i]; + assert_eq!( + board_row.len(), + raw_board_row.len(), + "board size was allocated incorrectly for the board, or an invalid board was passed" + ); + for j in 0..board_row.len() { + board_row[j] = BingoTile::Unmarked(raw_board_row[j]); + } + } + + Ok((remaining, BingoBoard(board))) +} + +// Calculate the score of a winning board, which is the same for both parts +fn calculate_score(winning_board: &BingoBoard, winning_call: u32) -> u32 { + let unmarked_tiles_iter = winning_board + .0 + .iter() + .flatten() + .filter(|tile| matches!(tile, BingoTile::Unmarked(_))) + .map(|&tile| match tile { + BingoTile::Unmarked(n) | BingoTile::Marked(n) => n, + }); + + unmarked_tiles_iter.map(u32::from).sum::() * winning_call +} + +fn part1(input: &Input) -> u32 { + let mut game = BingoGame::from(input.clone()); + let (winning_call, winning_board) = game + .play() + .next() + .expect("Puzzle produced no winner for any bingo boards"); + + calculate_score(&winning_board, winning_call.into()) +} + +fn part2(input: &Input) -> u32 { + let mut game = BingoGame::from(input.clone()); + let (winning_call, winning_board) = game + .play() + .last() + .expect("Puzzle produced no winner for any bingo boards"); + + calculate_score(&winning_board, winning_call.into()) +} + +fn parse_input(input: &str) -> IResult<&str, Input> { + let (_, ((calls, boards), _, _)) = tuple(( + separated_pair( + parse_bingo_calls, + tag("\n\n"), + separated_list1(tag("\n\n"), parse_bingo_board), + ), + many0(tag("\n")), + eof, + ))(input)?; + + let input = Input { calls, boards }; + + Ok(("", input)) +} + +fn main() { + let input_file_name = env::args().nth(1).expect("No input filename specified"); + let input = fs::read_to_string(input_file_name).expect("Could not open input file"); + let (_, parsed_input) = parse_input(&input).expect("Failed to parse input"); + + println!("Part 1: {}", part1(&parsed_input)); + println!("Part 2: {}", part2(&parsed_input)); +}