OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      446f6992722e
DATE      2025-07-21
SUBJECT   nix: wip race comparison script
FILES     3 CHANGED
HASH      446f6992722e
DATE      2025-07-21
SUBJECT   nix: wip race comparison script
FILES     3 CHANGED
 

diff --git a/nix/apps/simulation/default.nix b/nix/apps/simulation/default.nix
index 2168ca1..db7e3f3 100644
--- a/nix/apps/simulation/default.nix
+++ b/nix/apps/simulation/default.nix
@@ -9,7 +9,8 @@
   ...
 }: let
   # Mass simulations using mkMassSim
-  mkMassSim = (import ./mkMassSim.nix {inherit lib pkgs classes encounter buffs d
ebuffs inputs;}).mkMassSim;
+  massSimFunctions = import ./mkMassSim.nix {inherit lib pkgs classes encounter b
uffs debuffs inputs;};
+  inherit (massSimFunctions) mkMassSim mkRaceComparison;
 
   massSimulations = {
     dps-p1-raid-single-long = mkMassSim {
@@ -143,24 +144,170 @@
     };
   };
 
+  # Race comparison simulations for all DPS specs
+  raceComparisonSpecs = [
+    {
+      class = "death_knight";
+      spec = "frost";
+      template = "singleTarget";
+    }
+    {
+      class = "death_knight";
+      spec = "unholy";
+      template = "singleTarget";
+    }
+    {
+      class = "druid";
+      spec = "balance";
+      template = "singleTarget";
+    }
+    # { class = "druid"; spec = "feral"; template = "singleTarget"; }
+    {
+      class = "hunter";
+      spec = "beast_mastery";
+      template = "singleTarget";
+    }
+    {
+      class = "hunter";
+      spec = "marksmanship";
+      template = "singleTarget";
+    }
+    {
+      class = "hunter";
+      spec = "survival";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "arcane";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "fire";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "frost";
+      template = "singleTarget";
+    }
+    {
+      class = "monk";
+      spec = "windwalker";
+      template = "singleTarget";
+    }
+    {
+      class = "paladin";
+      spec = "retribution";
+      template = "singleTarget";
+    }
+    {
+      class = "priest";
+      spec = "shadow";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "assassination";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "combat";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "subtlety";
+      template = "singleTarget";
+    }
+    {
+      class = "shaman";
+      spec = "elemental";
+      template = "singleTarget";
+    }
+    {
+      class = "shaman";
+      spec = "enhancement";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "affliction";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "demonology";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "destruction";
+      template = "singleTarget";
+    }
+    {
+      class = "warrior";
+      spec = "arms";
+      template = "singleTarget";
+    }
+    {
+      class = "warrior";
+      spec = "fury";
+      template = "singleTarget";
+    }
+  ];
+
+  # Generate race comparison simulations for single target long encounters
+  raceComparisons = lib.listToAttrs (map (specConfig: {
+      name = "race-${specConfig.class}-${specConfig.spec}-p1-raid-single-long";
+      value = mkRaceComparison {
+        class = specConfig.class;
+        spec = specConfig.spec;
+        encounter = encounter.raid.long.singleTarget;
+        iterations = 10000;
+        phase = "p1";
+        encounterType = "raid";
+        targetCount = "single";
+        duration = "long";
+        template = specConfig.template;
+      };
+    })
+    raceComparisonSpecs);
+
   # Script that runs all simulations
   allSimulationsScript = pkgs.writeShellApplication {
     name = "all-simulations";
     text = ''
       echo "Running all WoW simulations..."
 
+      echo ""
+      echo "=== DPS Rankings ==="
       ${lib.concatMapStringsSep "\n" (name: ''
         echo ""
         echo "Running ${name}..."
         ${massSimulations.${name}.script}/bin/${massSimulations.${name}.metadata.
output}-aggregator
       '') (lib.attrNames massSimulations)}
 
+      echo ""
+      echo "=== Race Comparisons ==="
+      ${lib.concatMapStringsSep "\n" (name: ''
+        echo ""
+        echo "Running ${name}..."
+        ${raceComparisons.${name}.script}/bin/${raceComparisons.${name}.metadata.
output}-aggregator
+      '') (lib.attrNames raceComparisons)}
+
       echo ""
       echo "All simulations completed successfully!"
-      echo "Generated files:"
-      ls -la web/public/data/*.json 2>/dev/null || echo "No JSON files found"
+      echo ""
+      echo "Generated DPS rankings:"
+      ls -la web/public/data/rankings/*.json 2>/dev/null || echo "No ranking file
s found"
+      echo ""
+      echo "Generated race comparisons:"
+      find web/public/data/comparison -name "*.json" 2>/dev/null || echo "No comp
arison files found"
     '';
-    runtimeInputs = [pkgs.coreutils];
+    runtimeInputs = [pkgs.coreutils pkgs.findutils];
   };
 
   # Convert mass simulations to apps and add the all-simulations app
@@ -170,8 +317,17 @@
       program = "${massSim.script}/bin/${massSim.metadata.output}-aggregator";
     })
     massSimulations;
+
+  # Convert race comparisons to apps
+  raceComparisonApps =
+    lib.mapAttrs (name: raceComp: {
+      type = "app";
+      program = "${raceComp.script}/bin/${raceComp.metadata.output}-aggregator";
+    })
+    raceComparisons;
 in
   simulationApps
+  // raceComparisonApps
   // {
     allSimulations = {
       type = "app";

diff --git a/nix/apps/simulation/default.nix
 b/nix/apps/simulation/default.nix
index 2168ca1..db7e3f3 100644
--- a/nix/apps/simulation/default.nix
+++ b/nix/apps/simulation/default.nix
@@ -9,7 +9,8 @@
   ...
 }: let
   # Mass simulations using mkMassSim
-  mkMassSim = (import ./mkMassSim.nix {inhe
rit lib pkgs classes encounter buffs debuffs
 inputs;}).mkMassSim;
+  massSimFunctions = import ./mkMassSim.nix
 {inherit lib pkgs classes encounter buffs d
ebuffs inputs;};
+  inherit (massSimFunctions) mkMassSim mkRa
ceComparison;
 
   massSimulations = {
     dps-p1-raid-single-long = mkMassSim {
@@ -143,24 +144,170 @@
     };
   };
 
+  # Race comparison simulations for all DPS
 specs
+  raceComparisonSpecs = [
+    {
+      class = "death_knight";
+      spec = "frost";
+      template = "singleTarget";
+    }
+    {
+      class = "death_knight";
+      spec = "unholy";
+      template = "singleTarget";
+    }
+    {
+      class = "druid";
+      spec = "balance";
+      template = "singleTarget";
+    }
+    # { class = "druid"; spec = "feral"; te
mplate = "singleTarget"; }
+    {
+      class = "hunter";
+      spec = "beast_mastery";
+      template = "singleTarget";
+    }
+    {
+      class = "hunter";
+      spec = "marksmanship";
+      template = "singleTarget";
+    }
+    {
+      class = "hunter";
+      spec = "survival";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "arcane";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "fire";
+      template = "singleTarget";
+    }
+    {
+      class = "mage";
+      spec = "frost";
+      template = "singleTarget";
+    }
+    {
+      class = "monk";
+      spec = "windwalker";
+      template = "singleTarget";
+    }
+    {
+      class = "paladin";
+      spec = "retribution";
+      template = "singleTarget";
+    }
+    {
+      class = "priest";
+      spec = "shadow";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "assassination";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "combat";
+      template = "singleTarget";
+    }
+    {
+      class = "rogue";
+      spec = "subtlety";
+      template = "singleTarget";
+    }
+    {
+      class = "shaman";
+      spec = "elemental";
+      template = "singleTarget";
+    }
+    {
+      class = "shaman";
+      spec = "enhancement";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "affliction";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "demonology";
+      template = "singleTarget";
+    }
+    {
+      class = "warlock";
+      spec = "destruction";
+      template = "singleTarget";
+    }
+    {
+      class = "warrior";
+      spec = "arms";
+      template = "singleTarget";
+    }
+    {
+      class = "warrior";
+      spec = "fury";
+      template = "singleTarget";
+    }
+  ];
+
+  # Generate race comparison simulations fo
r single target long encounters
+  raceComparisons = lib.listToAttrs (map (s
pecConfig: {
+      name = "race-${specConfig.class}-${sp
ecConfig.spec}-p1-raid-single-long";
+      value = mkRaceComparison {
+        class = specConfig.class;
+        spec = specConfig.spec;
+        encounter = encounter.raid.long.sin
gleTarget;
+        iterations = 10000;
+        phase = "p1";
+        encounterType = "raid";
+        targetCount = "single";
+        duration = "long";
+        template = specConfig.template;
+      };
+    })
+    raceComparisonSpecs);
+
   # Script that runs all simulations
   allSimulationsScript = pkgs.writeShellApp
lication {
     name = "all-simulations";
     text = ''
       echo "Running all WoW simulations..."
 
+      echo ""
+      echo "=== DPS Rankings ==="
       ${lib.concatMapStringsSep "\n" (name:
 ''
         echo ""
         echo "Running ${name}..."
         ${massSimulations.${name}.script}/b
in/${massSimulations.${name}.metadata.output
}-aggregator
       '') (lib.attrNames massSimulations)}
 
+      echo ""
+      echo "=== Race Comparisons ==="
+      ${lib.concatMapStringsSep "\n" (name:
 ''
+        echo ""
+        echo "Running ${name}..."
+        ${raceComparisons.${name}.script}/b
in/${raceComparisons.${name}.metadata.output
}-aggregator
+      '') (lib.attrNames raceComparisons)}
+
       echo ""
       echo "All simulations completed succe
ssfully!"
-      echo "Generated files:"
-      ls -la web/public/data/*.json 2>/dev/
null || echo "No JSON files found"
+      echo ""
+      echo "Generated DPS rankings:"
+      ls -la web/public/data/rankings/*.jso
n 2>/dev/null || echo "No ranking files foun
d"
+      echo ""
+      echo "Generated race comparisons:"
+      find web/public/data/comparison -name
 "*.json" 2>/dev/null || echo "No comparison
 files found"
     '';
-    runtimeInputs = [pkgs.coreutils];
+    runtimeInputs = [pkgs.coreutils pkgs.fi
ndutils];
   };
 
   # Convert mass simulations to apps and ad
d the all-simulations app
@@ -170,8 +317,17 @@
       program = "${massSim.script}/bin/${ma
ssSim.metadata.output}-aggregator";
     })
     massSimulations;
+
+  # Convert race comparisons to apps
+  raceComparisonApps =
+    lib.mapAttrs (name: raceComp: {
+      type = "app";
+      program = "${raceComp.script}/bin/${r
aceComp.metadata.output}-aggregator";
+    })
+    raceComparisons;
 in
   simulationApps
+  // raceComparisonApps
   // {
     allSimulations = {
       type = "app";
 

diff --git a/nix/apps/simulation/mkMassSim.nix b/nix/apps/simulation/mkMassSim.nix
index 5651ab1..a09ec49 100644
--- a/nix/apps/simulation/mkMassSim.nix
+++ b/nix/apps/simulation/mkMassSim.nix
@@ -10,6 +10,12 @@
 }: let
   inherit (lib.sim.simulation) mkSim;
 
+  # Extract playable races from class definitions
+  getPlayableRaces = className: 
+    if lib.hasAttr className classes && lib.hasAttr "playableRaces" classes.${cla
ssName}
+    then classes.${className}.playableRaces
+    else throw "playableRaces not defined for class ${className}. Please add play
ableRaces = [...] to nix/classes/${className}/default.nix";
+
   getAllDPSSpecs = classes: template: let
     dpsSpecs = {
       death_knight = ["frost" "unholy"];
@@ -49,6 +55,233 @@
   in
     validSpecs;
 
+  # Get race configurations for a specific class/spec combination
+  getRaceConfigs = classes: className: specName: template: phase: encounterType: 
let
+    availableRaces = getPlayableRaces className;
+    baseSpec = classes.${className}.${specName};
+    baseConfig = 
+      if lib.hasAttr "template" baseSpec 
+        && lib.hasAttr phase baseSpec.template
+        && lib.hasAttr encounterType baseSpec.template.${phase}
+        && lib.hasAttr template baseSpec.template.${phase}.${encounterType}
+      then baseSpec.template.${phase}.${encounterType}.${template}
+      else throw "Template ${template} not found for ${className}/${specName} at 
${phase}.${encounterType}";
+  in
+    map (raceName: {
+      inherit className specName raceName;
+      # Use base config but override the race field
+      config = baseConfig // {
+        race = raceName;
+      };
+    }) availableRaces;
+
+  # Race comparison function
+  mkRaceComparison = {
+    class,
+    spec,
+    encounter,
+    iterations ? 10000,
+    phase ? "p1", 
+    encounterType ? "raid",
+    targetCount ? "single",
+    duration ? "long",
+    template ? "singleTarget",
+  }: let
+    # Get race configurations for this class/spec
+    raceConfigs = getRaceConfigs classes class spec template phase encounterType;
+    
+    # Create individual simulation derivations for each race
+    simDerivations = lib.listToAttrs (map (raceConfig: {
+        name = "${raceConfig.className}-${raceConfig.specName}-${raceConfig.raceN
ame}";
+        value = let
+          simInput = mkSim {
+            inherit iterations;
+            player = raceConfig.config;
+            buffs = buffs.full;
+            debuffs = debuffs.full;
+            inherit encounter;
+          };
+        in
+          pkgs.runCommand "race-sim-${raceConfig.className}-${raceConfig.specName
}-${raceConfig.raceName}" {
+            buildInputs = [pkgs.jq];
+            nativeBuildInputs = [inputs.wowsims.packages.${pkgs.system}.wowsimcli
];
+          } ''
+            # Generate input JSON file
+            cat > input.json << 'EOF'
+            ${simInput}
+            EOF
+
+            # Run simulation
+            echo "Running ${raceConfig.className}/${raceConfig.specName}/${raceCo
nfig.raceName} simulation..."
+            if wowsimcli sim --infile input.json --outfile output.json; then
+              # Extract DPS statistics
+              avgDps=$(jq -r '.raidMetrics.dps.avg // 0' output.json)
+              maxDps=$(jq -r '.raidMetrics.dps.max // 0' output.json)
+              minDps=$(jq -r '.raidMetrics.dps.min // 0' output.json)
+              stdevDps=$(jq -r '.raidMetrics.dps.stdev // 0' output.json)
+
+              # Create loadout info
+              loadout=$(echo '${builtins.toJSON raceConfig.config}' | jq '{
+                consumables,
+                talentsString,
+                glyphs,
+                equipment,
+                race,
+                class,
+                profession1,
+                profession2
+              }')
+
+              # Create final result
+              jq -n \
+                --arg raceName "${raceConfig.raceName}" \
+                --arg avgDps "$avgDps" \
+                --arg maxDps "$maxDps" \
+                --arg minDps "$minDps" \
+                --arg stdevDps "$stdevDps" \
+                --argjson loadout "$loadout" \
+                '{
+                  race: $raceName,
+                  dps: ($avgDps | tonumber),
+                  max: ($maxDps | tonumber),
+                  min: ($minDps | tonumber),
+                  stdev: ($stdevDps | tonumber),
+                  loadout: $loadout
+                }' > $out
+            else
+              echo "Simulation failed for ${raceConfig.className}/${raceConfig.sp
ecName}/${raceConfig.raceName}"
+              exit 1
+            fi
+          '';
+      })
+      raceConfigs);
+
+    # Generate output filename: race_<phase>_<encounter-type>_<target-count>_<dur
ation>
+    structuredOutput = "race_${phase}_${encounterType}_${targetCount}_${duration}
";
+
+    # Aggregation script for race comparison
+    aggregationScript = pkgs.writeShellApplication {
+      name = "${structuredOutput}-aggregator";
+      text = ''
+        set -euo pipefail
+
+        echo "Aggregating race comparison results for: ${class}/${spec}"
+        echo "Races simulated: ${toString (lib.length raceConfigs)}"
+
+        # Create base structure
+        result=$(jq -n '{}')
+
+        ${lib.concatMapStringsSep "\n" (raceConfig: ''
+            # Add ${raceConfig.raceName} results
+            raceData=$(cat ${simDerivations."${raceConfig.className}-${raceConfig
.specName}-${raceConfig.raceName}"})
+
+            result=$(echo "$result" | jq \
+              --argjson race "$raceData" \
+              --arg raceName "${raceConfig.raceName}" \
+              '.[$raceName] = {
+                dps: $race.dps,
+                max: $race.max,
+                min: $race.min,
+                stdev: $race.stdev,
+                loadout: $race.loadout
+              }')
+          '')
+          raceConfigs}
+
+        # Create final output with metadata
+        finalResult=$(echo "$result" | jq \
+          --arg class "${class}" \
+          --arg spec "${spec}" \
+          --arg encounter "${phase}_${encounterType}_${targetCount}_${duration}" 
\
+          --arg timestamp "$(date -Iseconds)" \
+          --arg iterations "${toString iterations}" \
+          --arg encounterDuration "${toString encounter.duration}" \
+          --arg encounterVariation "${toString encounter.durationVariation}" \
+          --arg targetCount "${toString (lib.length encounter.targets)}" \
+          --argjson raidBuffs '${builtins.toJSON buffs.full}' \
+          '{
+            metadata: {
+              spec: $spec,
+              class: $class,
+              encounter: $encounter,
+              timestamp: $timestamp,
+              iterations: ($iterations | tonumber),
+              encounterDuration: ($encounterDuration | tonumber),
+              encounterVariation: ($encounterVariation | tonumber),
+              targetCount: ($targetCount | tonumber),
+              raidBuffs: $raidBuffs
+            },
+            results: .
+          }')
+
+        echo "$finalResult" | tee "${structuredOutput}.json"
+
+        # Copy to web public directory with new structure
+        repo_root=""
+        current_dir="$PWD"
+        while [[ "$current_dir" != "/" ]]; do
+          if [[ -f "$current_dir/flake.nix" ]]; then
+            repo_root="$current_dir"
+            break
+          fi
+          current_dir="$(dirname "$current_dir")"
+        done
+
+        if [[ -z "$repo_root" ]]; then
+          echo "Warning: Could not find repo root (flake.nix), using current dire
ctory"
+          repo_root="$PWD"
+        fi
+
+        # Create directory structure: /comparison/<class>/<spec>/
+        comparison_dir="$repo_root/web/public/data/comparison/${class}/${spec}"
+        mkdir -p "$comparison_dir"
+
+        # Copy race comparison file
+        cp "${structuredOutput}.json" "$comparison_dir/"
+        echo "Copied to: $comparison_dir/${structuredOutput}.json"
+
+        echo ""
+        echo "Race DPS Rankings for ${class}/${spec}:"
+        echo "======================================="
+        echo "$finalResult" | jq -r '
+          .results | to_entries[] |
+          select(.value.dps != null and .value.dps > 0) |
+          "\(.key): \(.value.dps | floor) DPS"
+        ' | sort -k2 -nr
+        
+        # Show any failed races
+        failed_races=$(echo "$finalResult" | jq -r '
+          .results | to_entries[] |
+          select(.value.dps == null or .value.dps <= 0) |
+          .key
+        ')
+        
+        if [[ -n "$failed_races" ]]; then
+          echo ""
+          echo "Failed races (no valid DPS data):"
+          echo "$failed_races"
+        fi
+
+        echo ""
+        echo "Results written to: $comparison_dir/${structuredOutput}.json"
+      '';
+      runtimeInputs = [pkgs.jq pkgs.coreutils];
+    };
+  in {
+    # Individual race simulation derivations (for debugging)
+    simulations = simDerivations;
+
+    # Main aggregation script
+    script = aggregationScript;
+
+    metadata = {
+      output = structuredOutput;
+      inherit class spec iterations phase encounterType targetCount duration;
+      raceCount = lib.length raceConfigs;
+      races = map (r: r.raceName) raceConfigs;
+    };
+  };
+
   # Main mkMassSim function
   mkMassSim = {
     specs ? "dps",
@@ -213,10 +446,12 @@
         fi
 
         web_data_dir="$repo_root/web/public/data"
-        archive_dir="$web_data_dir/archive"
-        existing_file="$web_data_dir/${structuredOutput}.json"
+        rankings_dir="$web_data_dir/rankings"
+        archive_dir="$rankings_dir/archive"
+        existing_file="$rankings_dir/${structuredOutput}.json"
 
         mkdir -p "$web_data_dir"
+        mkdir -p "$rankings_dir"
         mkdir -p "$archive_dir"
 
         # Archive existing file if it exists
@@ -281,8 +516,8 @@
           fi
         fi
 
-        cp "${structuredOutput}.json" "$web_data_dir/"
-        echo "Copied to: $web_data_dir/${structuredOutput}.json"
+        cp "${structuredOutput}.json" "$rankings_dir/"
+        echo "Copied to: $rankings_dir/${structuredOutput}.json"
 
         echo ""
         echo "Top DPS Rankings:"
@@ -314,5 +549,5 @@
     };
   };
 in {
-  inherit mkMassSim getAllDPSSpecs;
+  inherit mkMassSim getAllDPSSpecs mkRaceComparison getRaceConfigs getPlayableRac
es;
 }

diff --git a/nix/apps/simulation/mkMassSim.n
ix b/nix/apps/simulation/mkMassSim.nix
index 5651ab1..a09ec49 100644
--- a/nix/apps/simulation/mkMassSim.nix
+++ b/nix/apps/simulation/mkMassSim.nix
@@ -10,6 +10,12 @@
 }: let
   inherit (lib.sim.simulation) mkSim;
 
+  # Extract playable races from class defin
itions
+  getPlayableRaces = className: 
+    if lib.hasAttr className classes && lib
.hasAttr "playableRaces" classes.${className
}
+    then classes.${className}.playableRaces
+    else throw "playableRaces not defined f
or class ${className}. Please add playableRa
ces = [...] to nix/classes/${className}/defa
ult.nix";
+
   getAllDPSSpecs = classes: template: let
     dpsSpecs = {
       death_knight = ["frost" "unholy"];
@@ -49,6 +55,233 @@
   in
     validSpecs;
 
+  # Get race configurations for a specific 
class/spec combination
+  getRaceConfigs = classes: className: spec
Name: template: phase: encounterType: let
+    availableRaces = getPlayableRaces class
Name;
+    baseSpec = classes.${className}.${specN
ame};
+    baseConfig = 
+      if lib.hasAttr "template" baseSpec 
+        && lib.hasAttr phase baseSpec.templ
ate
+        && lib.hasAttr encounterType baseSp
ec.template.${phase}
+        && lib.hasAttr template baseSpec.te
mplate.${phase}.${encounterType}
+      then baseSpec.template.${phase}.${enc
ounterType}.${template}
+      else throw "Template ${template} not 
found for ${className}/${specName} at ${phas
e}.${encounterType}";
+  in
+    map (raceName: {
+      inherit className specName raceName;
+      # Use base config but override the ra
ce field
+      config = baseConfig // {
+        race = raceName;
+      };
+    }) availableRaces;
+
+  # Race comparison function
+  mkRaceComparison = {
+    class,
+    spec,
+    encounter,
+    iterations ? 10000,
+    phase ? "p1", 
+    encounterType ? "raid",
+    targetCount ? "single",
+    duration ? "long",
+    template ? "singleTarget",
+  }: let
+    # Get race configurations for this clas
s/spec
+    raceConfigs = getRaceConfigs classes cl
ass spec template phase encounterType;
+    
+    # Create individual simulation derivati
ons for each race
+    simDerivations = lib.listToAttrs (map (
raceConfig: {
+        name = "${raceConfig.className}-${r
aceConfig.specName}-${raceConfig.raceName}";
+        value = let
+          simInput = mkSim {
+            inherit iterations;
+            player = raceConfig.config;
+            buffs = buffs.full;
+            debuffs = debuffs.full;
+            inherit encounter;
+          };
+        in
+          pkgs.runCommand "race-sim-${raceC
onfig.className}-${raceConfig.specName}-${ra
ceConfig.raceName}" {
+            buildInputs = [pkgs.jq];
+            nativeBuildInputs = [inputs.wow
sims.packages.${pkgs.system}.wowsimcli];
+          } ''
+            # Generate input JSON file
+            cat > input.json << 'EOF'
+            ${simInput}
+            EOF
+
+            # Run simulation
+            echo "Running ${raceConfig.clas
sName}/${raceConfig.specName}/${raceConfig.r
aceName} simulation..."
+            if wowsimcli sim --infile input
.json --outfile output.json; then
+              # Extract DPS statistics
+              avgDps=$(jq -r '.raidMetrics.
dps.avg // 0' output.json)
+              maxDps=$(jq -r '.raidMetrics.
dps.max // 0' output.json)
+              minDps=$(jq -r '.raidMetrics.
dps.min // 0' output.json)
+              stdevDps=$(jq -r '.raidMetric
s.dps.stdev // 0' output.json)
+
+              # Create loadout info
+              loadout=$(echo '${builtins.to
JSON raceConfig.config}' | jq '{
+                consumables,
+                talentsString,
+                glyphs,
+                equipment,
+                race,
+                class,
+                profession1,
+                profession2
+              }')
+
+              # Create final result
+              jq -n \
+                --arg raceName "${raceConfi
g.raceName}" \
+                --arg avgDps "$avgDps" \
+                --arg maxDps "$maxDps" \
+                --arg minDps "$minDps" \
+                --arg stdevDps "$stdevDps" 
\
+                --argjson loadout "$loadout
" \
+                '{
+                  race: $raceName,
+                  dps: ($avgDps | tonumber)
,
+                  max: ($maxDps | tonumber)
,
+                  min: ($minDps | tonumber)
,
+                  stdev: ($stdevDps | tonum
ber),
+                  loadout: $loadout
+                }' > $out
+            else
+              echo "Simulation failed for $
{raceConfig.className}/${raceConfig.specName
}/${raceConfig.raceName}"
+              exit 1
+            fi
+          '';
+      })
+      raceConfigs);
+
+    # Generate output filename: race_<phase
>_<encounter-type>_<target-count>_<duration>
+    structuredOutput = "race_${phase}_${enc
ounterType}_${targetCount}_${duration}";
+
+    # Aggregation script for race compariso
n
+    aggregationScript = pkgs.writeShellAppl
ication {
+      name = "${structuredOutput}-aggregato
r";
+      text = ''
+        set -euo pipefail
+
+        echo "Aggregating race comparison r
esults for: ${class}/${spec}"
+        echo "Races simulated: ${toString (
lib.length raceConfigs)}"
+
+        # Create base structure
+        result=$(jq -n '{}')
+
+        ${lib.concatMapStringsSep "\n" (rac
eConfig: ''
+            # Add ${raceConfig.raceName} re
sults
+            raceData=$(cat ${simDerivations
."${raceConfig.className}-${raceConfig.specN
ame}-${raceConfig.raceName}"})
+
+            result=$(echo "$result" | jq \
+              --argjson race "$raceData" \
+              --arg raceName "${raceConfig.
raceName}" \
+              '.[$raceName] = {
+                dps: $race.dps,
+                max: $race.max,
+                min: $race.min,
+                stdev: $race.stdev,
+                loadout: $race.loadout
+              }')
+          '')
+          raceConfigs}
+
+        # Create final output with metadata
+        finalResult=$(echo "$result" | jq \
+          --arg class "${class}" \
+          --arg spec "${spec}" \
+          --arg encounter "${phase}_${encou
nterType}_${targetCount}_${duration}" \
+          --arg timestamp "$(date -Iseconds
)" \
+          --arg iterations "${toString iter
ations}" \
+          --arg encounterDuration "${toStri
ng encounter.duration}" \
+          --arg encounterVariation "${toStr
ing encounter.durationVariation}" \
+          --arg targetCount "${toString (li
b.length encounter.targets)}" \
+          --argjson raidBuffs '${builtins.t
oJSON buffs.full}' \
+          '{
+            metadata: {
+              spec: $spec,
+              class: $class,
+              encounter: $encounter,
+              timestamp: $timestamp,
+              iterations: ($iterations | to
number),
+              encounterDuration: ($encounte
rDuration | tonumber),
+              encounterVariation: ($encount
erVariation | tonumber),
+              targetCount: ($targetCount | 
tonumber),
+              raidBuffs: $raidBuffs
+            },
+            results: .
+          }')
+
+        echo "$finalResult" | tee "${struct
uredOutput}.json"
+
+        # Copy to web public directory with
 new structure
+        repo_root=""
+        current_dir="$PWD"
+        while [[ "$current_dir" != "/" ]]; 
do
+          if [[ -f "$current_dir/flake.nix"
 ]]; then
+            repo_root="$current_dir"
+            break
+          fi
+          current_dir="$(dirname "$current_
dir")"
+        done
+
+        if [[ -z "$repo_root" ]]; then
+          echo "Warning: Could not find rep
o root (flake.nix), using current directory"
+          repo_root="$PWD"
+        fi
+
+        # Create directory structure: /comp
arison/<class>/<spec>/
+        comparison_dir="$repo_root/web/publ
ic/data/comparison/${class}/${spec}"
+        mkdir -p "$comparison_dir"
+
+        # Copy race comparison file
+        cp "${structuredOutput}.json" "$com
parison_dir/"
+        echo "Copied to: $comparison_dir/${
structuredOutput}.json"
+
+        echo ""
+        echo "Race DPS Rankings for ${class
}/${spec}:"
+        echo "=============================
=========="
+        echo "$finalResult" | jq -r '
+          .results | to_entries[] |
+          select(.value.dps != null and .va
lue.dps > 0) |
+          "\(.key): \(.value.dps | floor) D
PS"
+        ' | sort -k2 -nr
+        
+        # Show any failed races
+        failed_races=$(echo "$finalResult" 
| jq -r '
+          .results | to_entries[] |
+          select(.value.dps == null or .val
ue.dps <= 0) |
+          .key
+        ')
+        
+        if [[ -n "$failed_races" ]]; then
+          echo ""
+          echo "Failed races (no valid DPS 
data):"
+          echo "$failed_races"
+        fi
+
+        echo ""
+        echo "Results written to: $comparis
on_dir/${structuredOutput}.json"
+      '';
+      runtimeInputs = [pkgs.jq pkgs.coreuti
ls];
+    };
+  in {
+    # Individual race simulation derivation
s (for debugging)
+    simulations = simDerivations;
+
+    # Main aggregation script
+    script = aggregationScript;
+
+    metadata = {
+      output = structuredOutput;
+      inherit class spec iterations phase e
ncounterType targetCount duration;
+      raceCount = lib.length raceConfigs;
+      races = map (r: r.raceName) raceConfi
gs;
+    };
+  };
+
   # Main mkMassSim function
   mkMassSim = {
     specs ? "dps",
@@ -213,10 +446,12 @@
         fi
 
         web_data_dir="$repo_root/web/public
/data"
-        archive_dir="$web_data_dir/archive"
-        existing_file="$web_data_dir/${stru
cturedOutput}.json"
+        rankings_dir="$web_data_dir/ranking
s"
+        archive_dir="$rankings_dir/archive"
+        existing_file="$rankings_dir/${stru
cturedOutput}.json"
 
         mkdir -p "$web_data_dir"
+        mkdir -p "$rankings_dir"
         mkdir -p "$archive_dir"
 
         # Archive existing file if it exist
s
@@ -281,8 +516,8 @@
           fi
         fi
 
-        cp "${structuredOutput}.json" "$web
_data_dir/"
-        echo "Copied to: $web_data_dir/${st
ructuredOutput}.json"
+        cp "${structuredOutput}.json" "$ran
kings_dir/"
+        echo "Copied to: $rankings_dir/${st
ructuredOutput}.json"
 
         echo ""
         echo "Top DPS Rankings:"
@@ -314,5 +549,5 @@
     };
   };
 in {
-  inherit mkMassSim getAllDPSSpecs;
+  inherit mkMassSim getAllDPSSpecs mkRaceCo
mparison getRaceConfigs getPlayableRaces;
 }
 

diff --git a/nix/pkgs/testRaid.nix b/nix/pkgs/testRaid.nix
index 6daae51..3413a4c 100644
--- a/nix/pkgs/testRaid.nix
+++ b/nix/pkgs/testRaid.nix
@@ -12,18 +12,17 @@
   # quickly sim a single spec for testing purposes
   inherit (lib.sim.simulation) mkSim;
 
-  class = "shaman";
-  spec = "elemental";
+  class = "monk";
+  spec = "windwalker";
   encounterType = "raid";
-  targetCount = "cleave";
 
   raid = mkSim {
     requestId = "raidSimAsync-f2cf5e22118a43c7";
     iterations = 10000;
-    player = classes.${class}.${spec}.template.p1.${encounterType}.multiTarget;
+    player = classes.${class}.${spec}.template.p1.${encounterType}.singleTarget;
     buffs = buffs.full;
     debuffs = debuffs.full;
-    encounter = encounter.${encounterType}.long.threeTarget;
+    encounter = encounter.${encounterType}.long.singleTarget;
   };
   testRaid = writeShellApplication {
     name = "testRaid";

diff --git a/nix/pkgs/testRaid.nix b/nix/pkg
s/testRaid.nix
index 6daae51..3413a4c 100644
--- a/nix/pkgs/testRaid.nix
+++ b/nix/pkgs/testRaid.nix
@@ -12,18 +12,17 @@
   # quickly sim a single spec for testing p
urposes
   inherit (lib.sim.simulation) mkSim;
 
-  class = "shaman";
-  spec = "elemental";
+  class = "monk";
+  spec = "windwalker";
   encounterType = "raid";
-  targetCount = "cleave";
 
   raid = mkSim {
     requestId = "raidSimAsync-f2cf5e22118a4
3c7";
     iterations = 10000;
-    player = classes.${class}.${spec}.templ
ate.p1.${encounterType}.multiTarget;
+    player = classes.${class}.${spec}.templ
ate.p1.${encounterType}.singleTarget;
     buffs = buffs.full;
     debuffs = debuffs.full;
-    encounter = encounter.${encounterType}.
long.threeTarget;
+    encounter = encounter.${encounterType}.
long.singleTarget;
   };
   testRaid = writeShellApplication {
     name = "testRaid";
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET