Julien Truffaut
19th November 2025
In the previous episodes, I scraped a third-party API to gather the full list of available gear in Dofus. With that dataset finally in hand, I can start working on the main goal of this project: a tool that searches for the best character builds given user-defined constraints.
In Dofus, a character’s power comes from several sources:
All these bonuses stack together to produce the final character stats you see in the character sheet.

To keep things manageable, my first version of the build optimizer will focus exclusively on the nine main gear slots:
This is the most complex part of a build, because some items belong to sets—equipping multiple pieces of the same set grants extra set bonuses.
Other sources of characteristics (pet, Dofus, trophies, stat points) are independent and can be tuned afterward to strengthen an already-good gear selection. That’s something players are familiar with, and it's much simpler algorithmically.
Before diving into build optimisation, the codebase needs a bit of structure. It's time to split it into three crates:
With this setup, dofus_db and build both depend on core, but are otherwise completely independent modules.
Here’s the updated project layout:
Cargo.toml
core/
├── Cargo.toml
└── src/
├── lib.rs
└── …
dofus_db/
├── Cargo.toml
└── src/
├── main.rs
└── …
build/
├── Cargo.toml
└── src/
├── main.rs
└── …
The top-level workspace Cargo.toml simply links the three crates:
[workspace]
members = [
"build",
"core",
"dofus_db",
]
resolver = "2"
This structure lets each tool evolve independently while sharing the same underlying model.
The Build CLI will take user-specified constraints, such as:
The core challenge is implementing the algorithm that explores all viable gear combinations.
There are many possible algorithms but I like to start with the dumbest possible solution and see how far it gets.
Since a build consists of exactly one item per slot, the simplest brute-force algorithm is a Cartesian product across all slots:
let constraints = ...
let all_gears = ...
let mut build = Build::empty();
for amulet in all_gears.get(Amulet)
build.set_gear(amulet)
for belt in all_gears.get(Belt)
build.set_gear(belt)
for hat in all_gears.get(Hat)
build.set_gear(hat)
...
if build.satisfy(constraints) {
println(build)
}
This is straightforward but ... very slow.
The total number of builds is:
|Amulet| × |Belt| × |Boots| × … × |Weapon|
Some examples:
This approach is only feasible if we aggressively prune bad choices and order items so that promising builds appear early.
But it’s still a good starting point to reason about the problem.
In the next episode, I’ll focus on the Build data structure—how to represent a partially-filled build and how to update it efficiently.