Add day 4 solution
This commit is contained in:
parent
b5cd02fab1
commit
846b02a815
39
day4/Cargo.lock
generated
Normal file
39
day4/Cargo.lock
generated
Normal file
|
@ -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"
|
9
day4/Cargo.toml
Normal file
9
day4/Cargo.toml
Normal file
|
@ -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"
|
284
day4/src/main.rs
Normal file
284
day4/src/main.rs
Normal file
|
@ -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<u8>,
|
||||||
|
boards: Vec<BingoBoard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `BoardState` indicates whether or not a board has won
|
||||||
|
struct BoardState {
|
||||||
|
won: bool,
|
||||||
|
board: BingoBoard,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BingoGame {
|
||||||
|
calls: VecDeque<u8>,
|
||||||
|
boards: Vec<BoardState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `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<Input> 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<Self::Item> {
|
||||||
|
while let Some(call) = self.game.calls.pop_front() {
|
||||||
|
let boards = &mut self.game.boards;
|
||||||
|
let mut winning_board: Option<BingoBoard> = 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<u8>> {
|
||||||
|
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::<u32>() * 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));
|
||||||
|
}
|
Loading…
Reference in a new issue