Episode 11: Restricting the Dataset

Julien Truffaut image

Julien Truffaut

2 December 2025

In the previous episode, I implemented a nested loop exploring all possible gear combinations. While it technically works, it would take an astronomical amount of time to complete. Let’s start by looking at the scale of the problem.

My dataset currently contains:

  • 323 amulets
  • 352 belts
  • 364 boots
  • 300 cloaks
  • 367 hats
  • 370 rings
  • 145 shields
  • 696 weapons

If we try every possible build, that’s 6 × 10²² combinations

To put this in perspective:

Even at 1,000,000 builds per second, the search would take over 2 billion years. Safe to say I won’t be playing Dofus by then.

Before exploring smarter algorithms, we should drastically shrink the dataset. A very effective first step: remove all gears outside the player’s level range (e.g. for a level 150 build, ignore all items above 150). This alone can cut the search space by several orders of magnitude.

Gathering User Requirements

Let’s add CLI parameters to set a minimum and maximum gear level. As seen in the dofus_db crate, we can easily do this with Clap:

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    #[arg(long = "min-level")]
    min_level: Option<u32>,

    #[arg(long = "max-level")]
    max_level: Option<u32>,
}

And in main.rs:

fn main() -> Result<()> {
    let args = Args::parse();

    let mut amulets = import_gears(&[GearType::Amulet])?;
    amulets.retain(|g| is_level_in_range(g.level, args.min_level, args.max_level));

    // repeat for other gear types...
}

Using this helper function:

fn is_level_in_range(
    level: u32,
    min: Option<u32>,
    max: Option<u32>
) -> bool {
    min.map_or(true, |m| level >= m) &&
    max.map_or(true, |m| level <= m)
}

Then, we can call the CLI with min and max level arguments:

cargo run -p dofus-opti-build -- 
    --min-level 190 
    --max-level 200

Even this small improvement has a huge impact: The number of builds between levels 190–200 is around 2 × 10¹³. At one million builds per second, that’s “only” 7.5 months — still too long, but massively better than 2 billion years.

Adding Validation

We should validate CLI input because mistakes here can easily lead to empty or nonsensical results. Let’s implement two simple rules:

  • Levels must be between 1 and 200
  • min_level ≤ max_level

The first rule is easily supported by Clap:

#[arg(long = "min-level", value_parser = clap::value_parser!(u32).range(1..=200))]
min_level: Option<u32>,

Incorrect input yields:

cargo run -p dofus-opti-build -- 
    --min-level 220

error: invalid value '220' for '--min-level <MIN_LEVEL>': 220 is not in 1..=200

Great!

The second rule requires custom validation because we need to validate two fields together:

use clap::{CommandFactory, Parser};

impl Args {
    fn validate(&self) -> Result<(), clap::Error> {
        if let (Some(min), Some(max)) = (self.min_level, self.max_level) {
            if min > max {
                return Err(Args::command().error(
                    clap::error::ErrorKind::ValueValidation,
                    "min-level must be ≤ max-level",
                ));
            }
        }
        Ok(())
    }
}

Now invalid ranges report a clean error:

cargo run -p dofus-opti-build -- 
    --min-level 180 
    --max-level 150

Error: error: min-level must be  max-level

Alternatively, we could have defined a struct with a min and max level and define the validation there:

struct LevelRange {
    min: Option<u32>,
    max: Option<u32>,
}

impl LevelRange {
    fn new(min: Option<u32>, max: Option<u32>) -> Result<LevelRange, String> {
        // validation logic
    }
}

impl FromStr for LevelRange {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // parsing logic
    }
}

struct Args {
    #[arg(long = "level")]
    level_range: Option<LevelRange>,
}

I prefer this approach as it makes easier to test the parsing and validation logic and it allows some cool syntax:

cargo run -p dofus-opti-build -- 
    --level 150..180

cargo run -p dofus-opti-build -- 
    --level 200

You can see my implementation here.

Build Targets

Different gears support different playstyles: some boost elemental damage, others improve tankiness, AP/MP mechanics, and so on. If a player is looking for a strength build, we can safely eliminate all items that don’t meaningfully contribute to it.

Let’s start by allowing requirements such as:

cargo run -p dofus-opti-build -- 
    --require "Vitality >= 3000"
    --require "Strength >= 500"

First, let’s define the model:

struct MinRequirement {
    id: RequirementId,
    desired_value: i32,
}

enum RequirementId {
    Strength,
    Vitality,
}

We can define how to parse a MinRequirement with FromStr:

impl FromStr for MinRequirement {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<_> = s.split_whitespace().collect();
        if parts.len() != 3 {
            return Err(format!("Invalid requirement: {}", s));
        }
        let id: RequirementId = parts[0].parse()?;
        if parts[1] != ">=" {
            return Err(format!("Unsupported operator: {}", parts[1]));
        }
        let desired_value = parts[2]
            .parse()
            .map_err(|_| "Invalid number".to_string())?;
        Ok(MinRequirement { id, desired_value })
    }
}

And in the CLI:

#[arg(short, long = "require", num_args(1..), action = clap::ArgAction::Append)]
requirements: Vec<MinRequirement>,

Now, running:

cargo run -p dofus-opti-build -- 
    --require "Vitality >= 3000" 
    --require "Strength >= 800"

produces:

[
    MinRequirement { id: Vitality, desired_value: 3000 },
    MinRequirement { id: Strength, desired_value: 800 }
]

Perfect!

To actually use these requirements, we’ll need to compute the combined effects of all gears in a build — but that’s for the next episode.

Conclusion

I continue to be impressed by how ergonomic and powerful Clap is. Writing a robust CLI is very straightforward.

Today, we saw that the naïve search algorithm is hopeless when applied to the full dataset — but if we aggressively prune it (level filtering, build archetypes, etc.), the search space becomes drastically smaller.

In the next episodes, we’ll explore additional ways to reduce the dataset and begin experimenting with more promising search strategies.