Well, here we go again.
I've never finished Advent of Code. I think the most I've ever managed is less than half. As an old and somewhat distractable dog, I'm pretty slow to complete each "day" and they tend to stretch into consuming multiple days, especially since I'm usually learning a new language, tool, or library.
I'm sure this time will be no different. But maybe I'll get to 25... who can say? I'm only about five days behind, at time of writing!
Maybe by March, 2025.
I figured it'd be interesting to do an in-Bevy version, since that's where I'm spending most of my time these days. Maybe try to do a few visualisations, get a bit more familiar with asset loading and various other attendant tools.
To build a Bevy harness for AoC, we just start with a little cargo generate
:
cargo generate thebevyflock/bevy_new_minimal
I made sure we were at the just-released Bevy v0.15, for all that required components goodness.
We can set up a simple control flow with Bevy states:
// main.rs
#[derive(States, Debug, Hash, PartialEq, Eq, Copy, Clone, Default)]
pub enum AoCState {
#[default]
Loading,
Menu,
Day1,
// ...
}
fn main() {
tracing_subscriber::fmt::init();
App::new()
.add_plugins((DefaultPlugins, loading::plugin, menu::plugin, days::plugin))
.init_state::<AoCState>()
.enable_state_scoped_entities::<AoCState>()
.add_systems(Update, log_transitions::<AoCState>)
.run();
}
This way, we can just keep adding states as we need 'em. Sure, it doesn't scale too well, but at
most we only need 25. We can also add
tracing_subscriber
for a bit
more detail on function inputs and returns. State-scoped entities are the best thing since sliced
bread, so those get enabled as well.
We need a real simple menu.
// menu.rs
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(AoCState::Menu), init);
}
#[derive(Component)]
struct Menu;
fn init(mut commands: Commands) {
commands.spawn((Name::new("Camera"), Camera2d));
commands.spawn((
Name::new("Menu"),
Menu,
Node {
align_items: AlignItems::Center,
align_self: AlignSelf::Center,
justify_content: JustifyContent::Center,
justify_self: JustifySelf::Center,
padding: UiRect::all(Val::Px(10.)),
..default()
},
StateScoped(AoCState::Menu),
));
commands.run_system_cached_with(spawn_puzzle_link, (AoCState::Day1, "One".into()));
}
Taking advantage of one-shot
systems isn't the only way to
get this done, but it does let us repeat a bunch of code as often as we need, and when we want it as
opposed to doing it on one of Bevy's schedules. v0.15 introduces
.run_system_cached_with
,
which keeps an id for the command around so it can be called repeatedly with more efficiency.
Just a simple, vaguely xmas-y button:
// menu.rs
fn spawn_puzzle_link(
state: In<(AoCState, String)>,
mut commands: Commands,
menu: Single<Entity, With<Menu>>,
) {
commands.entity(*menu).with_children(|p| {
let (day, label) = state.0;
p.spawn((
Button,
BackgroundColor(FIRE_BRICK.into()),
BorderColor(GREEN.into()),
Node {
// snip some red, green, and white styling
},
))
.with_children(|p| {
p.spawn((
Text::new(label),
TextFont {
font_size: 14.,
..default()
},
));
})
.observe(
move |_ev: Trigger<Pointer<Click>>, mut next_state: ResMut<NextState<AoCState>>| {
next_state.set(day);
},
);
});
}
Really all we're trying to do here is have the state change to the correct "day" when a button is clicked. An observer takes care of that for us, and each button "knows" which state to switch to.
I've never written an asset loader before. Typically, I'll use the excellent bevy_assset_loader with one of the existing asset types, or with the accompanying bevy_common_assets for file formats like RON or JSON. However, Alice happily assures me that:
so what could possibly go wrong?
Still:
Back when I was in PICU, we'd never say the word "quiet", especially on night shift. Inevitably, it would invite chaos for the next twelve hours.
Still, as it turns out it is kinda easy, at least for a naiive implementation:
// puzzle_input_asset.rs
#[derive(Asset, TypePath, Debug)]
pub(crate) struct PuzzleInputAsset {
#[allow(dead_code)]
pub rows: Vec<Vec<i32>>,
}
#[derive(Default)]
pub(crate) struct PuzzleInputAssetLoader;
/// Possible errors that can be produced by [`PuzzleInputAssetLoader`]
#[non_exhaustive]
#[derive(Debug, Error)]
pub(crate) enum PuzzleInputAssetLoaderError {
/// An [IO](std::io) Error
#[error("Could not load asset: {0}")]
Io(#[from] std::io::Error),
/// A [string conversion](std::string::FromUtf8Error) error
#[error("Could not parse utf8 from bytes: {0}")]
FromUtf8(#[from] std::string::FromUtf8Error),
/// An [integer conversion](std::num::ParseIntError)
#[error("Could not convert to integer: {0}")]
IntegerConversionError(#[from] std::num::ParseIntError),
}
impl AssetLoader for PuzzleInputAssetLoader {
type Asset = PuzzleInputAsset;
type Settings = ();
type Error = PuzzleInputAssetLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let file_contents = String::from_utf8(bytes)?;
let lines = file_contents.lines();
let mut rows: Vec<Vec<i32>> = vec![];
for line in lines {
let cols: Vec<&str> = line.split(" ").collect();
let mut row: Vec<i32> = vec![];
for col in cols {
if col.is_empty() {
continue;
}
let n: i32 = col.parse()?;
row.push(n);
}
rows.push(row);
}
Ok(PuzzleInputAsset { rows })
}
fn extensions(&self) -> &[&str] {
&["aoc"]
}
}
One should not look at this and imagine it's a flawless attempt! But acceptable for a one-off file format that looks like this:
76569 66648
38663 66530
60350 60777
35330 13469
88681 66648
30057 83262
...
I knew there was no chance we'd be able to re-use the same asset loader for long, but this one would handle anything that was merely columns of numbers. I'd let future Basie worry about anything else.
Now that we implement AssetLoader
, bevy_asset_loader
knows how to do the rest:
// loading.rs
pub fn plugin(app: &mut App) {
app.init_asset::<PuzzleInputAsset>()
.init_asset_loader::<PuzzleInputAssetLoader>()
.add_loading_state(
LoadingState::new(AoCState::Loading)
.continue_to_state(AoCState::Menu)
.load_collection::<PuzzleInputs>(),
);
}
#[derive(AssetCollection, Resource)]
pub struct PuzzleInputs {
#[asset(path = "input/1.aoc")]
pub one: Handle<PuzzleInputAsset>,
}
In other words: tell Bevy we have a new asset type, use bevy_asset_loader
to proceed to the Menu
state once we're done with loading, and tell it where to find the puzzle's input for the day.
This approach has the downside of always loading all the inputs into memory, though there are plenty of other methods if this proved to overwhelm available resources. Since AoC uses text files for its inputs, it's unlikely that we'll struggle. Simple is best here.
Now we can trivially reach in and grab the puzzle input we're after from any Bevy system:
// day1.rs
fn process(
mut processed: ResMut<OrderedLocationLists>,
puzzle_assets: Res<Assets<PuzzleInputAsset>>,
puzzle_inputs: Res<PuzzleInputs>,
) {
if let Some(puzzle) = puzzle_assets.get(&puzzle_inputs.one) {
// write to `processed` once we've done whatever sorting/filtering
// magic we need for the puzzle's solution
}
}
The Bevy Discord is incredibly helpful, supportive and non-judge-y. I am exhibit 'A' in this regard, if it was a harsh environment an ignoramus like me would never have survived! Highly recommended for either random questions, simple debugging help and more complex "why did it break?" questions. Obviously, still try to read the docs and examples first.
Chris Biscardi is a Rust & Bevy content creator, and runs an excellent Discord with various people undertaking AoC this year. They're also publishing videos solving the puzzles for each day with commentary on the approach and solution.
The untidy ongoing Advent of Bevy saga is available on GitHub.
Finally, chances are if I'm working on AoC things, I'll be on stream at Twitch and YouTube. We've had some great suggestions and helpful ideas come in via chat so far. The Eloquent Geek Discord is also around if you have questions or want to chat all things Rust gamedev.