# AoC 2023 Day 4: Scratchinator

## Source: Day 4: Scratchcards

Full solution for today (spoilers!). Note: I did slightly change my solutions template after writing this blog post, so the final solution is structured slightly differently than the code in this post. The functionality itself hasn’t changed.

## Part 1

Simulate scratchcards. Given a list of winning numbers and guessed numbers, count how many guessed numbers are in the winning list. Your score is 1, 2, 4, 8, … for 1, 2, 3, 4, … matching numbers.

Okay, data structure:

#[allow(dead_code)]
#[derive(Debug)]
pub struct Card {
pub id: u32,
pub winning_numbers: Vec<u8>,
pub guesses: Vec<u8>,
}

impl Card {
fn matches(&self) -> usize {
self.guesses
.iter()
.filter(|guess: &&u8| self.winning_numbers.contains(guess))
.count()
}
}


This includes the matches function. We’re using Vec<u8> here, although a HashSet or BTreeSet might be slightly more performant. I don’t think that (at this scale) it really matters, although being able to use .intersect(...) would be nice.

Now nom for parsing:

mod parse {
use crate::*;
use nom::{
bytes::complete::*,
character::complete,
character::complete::{newline, space0, space1},
multi::*,
sequence::*,
*,
};

pub fn cards(s: &str) -> IResult<&str, Vec<Card>> {
separated_list1(newline, card)(s)
}

fn card(s: &str) -> IResult<&str, Card> {
let (s, _) = tag("Card")(s)?;
let (s, _) = space1(s)?;
let (s, id) = complete::u32(s)?;
let (s, _) = tag(":")(s)?;
let (s, _) = space0(s)?;
let (s, winning_numbers) = separated_list1(space1, complete::u8)(s)?;
let (s, _) = delimited(space0, tag("|"), space0)(s)?;
let (s, guesses) = separated_list1(space1, complete::u8)(s)?;

Ok((
s,
Card {
id,
winning_numbers,
guesses,
},
))
}

#[cfg(test)]
mod test {
// ...
}
}


The sequence of parsers is a bit ugly, but I’m not sure a better way to do that. I could probably handle it with a nom tuple, but then I still have to ignore the right values. As it is, we get a list for each value, so we can just push it out:

fn part1(filename: &Path) -> Result<String> {
let (_, cards) = parse::cards(&input).unwrap();

// Wrapper to avoid calculating 2^(-1) or 2^(usize::MAX)
fn score(matches: usize) -> usize {
if matches == 0 {
0
} else {
2_usize.pow((matches - 1) as u32)
}
}

Ok(cards
.iter()
.map(|card| score(card.matches()))
.sum::<usize>()
.to_string())
}


And that’s it!

## Part 2

Instead of scoring each card, take the number of matches for each card and gain that many of the next cards (so if Card 5 scores 3, add one each of Card 6, 7, 8).

Count the total number of cards once you are not gaining any more.

You may assume that you won’t be asked to gain cards that don’t exist (off the end of the list).

Because each card only adds future cards, we can probably do this in one pass. But my first solution was instead to go through and loop until the simulation stabilizes. Certainly not a very functional model!

fn part2(filename: &Path) -> Result<String> {
let (_, cards) = parse::cards(&input).unwrap();

let mut total = 0;
let mut counts = vec![1; cards.len()];
let mut next_counts = vec![0; cards.len()];

// Earn new cards until stable
loop {
// Count all cards earned before updating
total += counts.iter().sum::<usize>();

// Each card earns
// NOTE: We're explicitly guaranteed that next_counts[i + j + 1] doesn't overflow
for (i, card) in cards.iter().enumerate() {
for j in 0..card.matches() {
next_counts[i + j + 1] += counts[i];
}
}

// If no cards were earned, we're done
if next_counts.iter().all(|&c| c == 0) {
break;
}

// Swap buffers and clear
// This could be a std::mem::swap, but we'd still need to init the new next_counts
for i in 0..cards.len() {
counts[i] = next_counts[i];
next_counts[i] = 0;
}
}

Ok(total.to_string())
}


But…

## Performance

$cargo run --release --bin 04-scratchinator 1 data/04.txt Finished release [optimized] target(s) in 0.00s Running target/release/04-scratchinator 1 data/04.txt 23028 took 142.75µs$ cargo run --release --bin 04-scratchinator 2 data/04.txt

Finished release [optimized] target(s) in 0.00s
Running target/release/04-scratchinator 2 data/04.txt
9236992
took 696µs


It still runs in microseconds. So I don’t feel much need to optimize it.

Onward!