Episode 9: Build Data Structure
Julien Truffaut
21 November 2025
In the previous episode, I reorganised the project into three crates: core, dofus_db, and build. The last one will be the new playground where I’ll develop the gear–selection optimisation tool.
Today’s task: designing the Build data structure.
Defining a Build
A build needs to store gears in specific positions: a hat, a shield, a weapon, etc. We need a type that represents these gear slots.
It might be tempting to reuse the GearType enum from the core crate, but the mapping isn’t 1-to-1:
- A Hat can only go in the Hat slot ✔
- But the Weapon slot can accept axes, swords, bows, hammers, etc.
- And there are two ring slots.
The simplest approach is a dedicated enum:
enum GearSlot {
Amulet,
Belt,
Boots,
Cloak,
Hat,
Ring1,
Ring2,
Shield,
Weapon,
} A build is then a map from slot → gear:
use std::collections::HashMap;
struct Build {
gear_slots: HashMap<GearSlot, Gear>,
} I prefer not to expose gear_slots directly. Having accessors gives flexibility to change the internals later.
impl Build {
fn get_gear(&self, gear_slot: &GearSlot) -> Option<&Gear> {
self.gear_slots.get(gear_slot)
}
fn set_gear(&mut self, gear_slot: GearSlot, gear: Gear) {
self.gear_slots.insert(gear_slot, gear);
}
} So far so good… until the compiler complains:
enum GearSlot {
^^^ doesn't satisfy `GearSlot: Hash` or `GearSlot: Eq`
self.gear_slots.get(&gear_slot)
^^^ method cannot be called on `HashMap<GearSlot, Gear>` due to unsatisfied trait bounds As it often the case in rust, the compiler gives a very helpful suggestion:
help: consider annotating `GearSlot` with `#[derive(Eq, Hash, PartialEq)]` Structs and enums don’t automatically implement Eq or Hash (unlike JVM languages like Java or Scala). But adding these trait using derives is trivial:
#[derive(Eq, Hash, PartialEq)]
enum GearSlot { ... } Validation
The above implementation of set_gear is incorrect because it allows inserting any gear into any slot—for example a ring into a hat slot.
We need validation logic:
impl GearSlot {
pub fn is_valid_for(&self, gear_type: &GearType) -> bool {
match (self, gear_type) {
(GearSlot::Amulet, GearType::Amulet) => true,
(GearSlot::Belt, GearType::Belt) => true,
(GearSlot::Boots, GearType::Boots) => true,
(GearSlot::Cloak, GearType::Cloak) => true,
(GearSlot::Hat, GearType::Hat) => true,
(GearSlot::Ring1, GearType::Ring) => true,
(GearSlot::Ring2, GearType::Ring) => true,
(GearSlot::Shield, GearType::Shield) => true,
(GearSlot::Weapon, GearType::Axe) => true,
(GearSlot::Weapon, GearType::Bow) => true,
(GearSlot::Weapon, GearType::Dagger) => true,
// ... all other weapon types ...
_ => false,
}
}
}
fn check_gear_slot(gear: &Gear, gear_slot: &GearSlot) -> Result<(), String> {
if gear_slot.is_valid_for(&gear.gear_type) {
Ok(())
} else {
Err(format!(
"Gear cannot be put in the expected slot, gear: {}, slot: {}",
gear.name.en, gear_slot
))
}
} Now we can update set_gear to propagate errors using ?:
fn set_gear(&mut self, gear_slot: GearSlot, gear: Gear) -> Result<(), String> {
check_gear_slot(&gear, &gear_slot)?;
self.gear_slots.insert(gear_slot, gear);
Ok(())
} But the compiler again gives an error because gear_slot variable was moved into check_gear_slot and then reused afterward:
error[E0382]: use of moved value: `gear_slot`
check_gear_slot(&gear, gear_slot)?;
^^^^^^^^^ value moved here
self.gear_slots.insert(gear_slot, gear);
^^^^^^^^^ value used here after move Since GearSlot is tiny, we can cheaply copy it. Again using the Copy and Clone derivation:
#[derive(Eq, Hash, PartialEq, Clone, Copy)]
enum GearSlot { ... } And if you don’t trust me that GearSlot is small, you can ask rust how much memory it takes:
println!("GearSlot takes {} Bytes", std::mem::size_of::<GearSlot>());
// GearSlot takes 1 Bytes Custom Error Type
Returning a String as an error isn’t great both for testing or writing error handling logic. Let’s define a dedicated error type instead:
enum BuildError {
InvalidGearSlot(String, GearSlot),
} To integrate with Rust’s error ecosystem we need to implement:
- std::fmt::Display
- std::fmt::Debug
- std::error::Error
Rather than implementing these traits manually, we can use thiserror crate:
[dependencies]
thiserror = "1.0" use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BuildError {
#[error("Gear cannot be put in the expected slot, gear: {0}, slot: {1}")]
InvalidGearSlot(String, GearSlot),
} Much cleaner!
Conclusion
We now have the foundation of the Build data structure with:
- a GearSlot enum tailored to the build problem
- safe get_gear and set_gear functions
- the foundation for most advanced validation using the BuildError enum
In the next episode, I’ll implement the “dumb” brute-force build search and test whether the Build data structure holds up.