Day 22

Feelings

Part 1 ➟ easy and the code came out quite nice.

Part 2 ➟ what the hell am I trying to implement?

The rules in part 2 were quite confusing and explained a few times to increase the confusion even more.

And then there's this:

To play a sub-game of Recursive Combat, each player creates a new deck by making a copy of the next cards in their deck (the quantity of cards copied is equal to the number on the card they drew to trigger the sub-game).

I completely missed the part in bold for quite some time. Also the examples pass very well even if that functionality is missing.

Improvements for the future

Do some magic to run multiple games in parallel. For example in subgames, you could run three games in parallel: one that decides the winner, and two to precalculate next round for both winners.

Learnings

VecDeque

std::collections::VecDeque is just a double-ended queue, no biggies there. Just tried it out here the first.

iter_mut

I guess I may have used iter_mut actually successfully the first time here. Have tried a few times and it hasn't gone so well. But this time it did. So I've got that going for me. \o/

This gets a card from every player from the top of their decks.

    fn pop_top(&mut self) -> Vec<u8> {
        self.players.iter_mut().map(|p|p.deck.pop_front().unwrap()).collect()
    }

Code that is enabled based on selected features

Cargo.toml:

[features]
vag_emissions = []

Cheat code somewhere in the .rs:

        #[cfg(feature = "vag_emissions")]
        if self.depth > 1 {

And the build with:

cargo build --release --features=vag_emissions

Implementing Debug-trait

#[derive(Default)]
pub struct Game {
    pub game_id: usize,
    pub depth: usize,
    pub players: Vec<Player>,
    pub round: usize,
    pub winner: Option<usize>,
    round_states: DeckStates,
}

Since my Game struct can grow quite large (particularly round_states: DeckStates), default derived Debug-trait doesn't make sense at all.

This one implements it without making it too verbose.

impl std::fmt::Debug for Game {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Game")
         .field("id", &self.game_id)
         .field("depth", &self.depth)
         .field("winner", &self.winner)
         .field("round", &self.round)
         .field("players", &self.players)
         .finish()
    }
}

Implementing Clone-trait

Game is cloned by cloning only the player data.

impl Clone for Game {
    fn clone(&self) -> Self {
        Self {
            players: self.players.clone(),
            game_id: 0,
            depth: self.depth + 1,
            // clone just the players and their decks
            .. Default::default()
        }
    }
}

Using Default-trait in new

Default just makes new so much nicer to write. and Game::new() just looks good elsewhere.

    pub fn new() -> Self {
        Self { ..Default::default() }
    }

AtomicUsize

To make sure every simulated game gets unique ID, AtomicUsize is pretty nice choice.

To start using it:

use std::sync::atomic::{AtomicUsize, Ordering};
static GAME_ID: AtomicUsize = AtomicUsize::new(1);

To increment:

    pub fn play(&mut self) {
        let game_id = GAME_ID.fetch_add(1, Ordering::SeqCst);
        self.game_id = game_id;

And to return current value:

pub fn game_id() -> usize {
    GAME_ID.load(Ordering::SeqCst)
}