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
┌─ nix/apps/simulation/default.nix ──────────────────────────────────────────┐
│ 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"; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/apps/simulation/default.nix ────┐
│ 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"; │
└──────────────────────────────────────────────┘
┌─ nix/apps/simulation/mkMassSim.nix ────────────────────────────────────────┐
│ 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; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...apps/simulation/mkMassSim.nix ───┐
│ 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; │
│ } │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/testRaid.nix ────────────────────────────────────────────────────┐
│ 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"; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/pkgs/testRaid.nix ──────────────┐
│ 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