User:Prowler/Attribute distribution
So I figured that with the new reroll option in 5.0 it's reasonably easy to sample the attribute distribution for a role just by starting a new game, sending 100000 'r' inputs and parsing the game output instead of running a simulation. With this approach, we can also see the effect of strength being increased if the hero is encumbered by the starting inventory, which is hard to sim and supposed to matter for Knights (spoiler: turns out, it doesn't).
Here are the full results. I've also thrown in a few percentiles in addition to the average and the standard deviations. NB: The values for Str in this output are raw and not formatted in the usual NetHack fashion, so 19 should be read as 18/01, 20 as 18/02 and so on.
=== arc hum === Str: avg=12.51 std=2.12 p10=10 p25=11 p50=12 p75=14 p90=15 Dex: avg=9.79 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 Con: avg=12.50 std=2.10 p10=10 p25=11 p50=12 p75=14 p90=15 Int: avg=15.33 std=1.87 p10=13 p25=14 p50=15 p75=17 p90=18 Wis: avg=15.34 std=1.86 p10=13 p25=14 p50=15 p75=17 p90=18 Cha: avg=9.79 std=1.63 p10=8 p25=9 p50=10 p75=11 p90=12 === arc dwa === Str: avg=12.76 std=2.10 p10=10 p25=11 p50=13 p75=14 p90=15 Dex: avg=9.91 std=1.65 p10=8 p25=9 p50=10 p75=11 p90=12 Con: avg=12.76 std=2.10 p10=10 p25=11 p50=13 p75=14 p90=15 Int: avg=14.93 std=1.31 p10=13 p25=14 p50=15 p75=16 p90=16 Wis: avg=14.93 std=1.31 p10=13 p25=14 p50=15 p75=16 p90=16 Cha: avg=9.91 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 === arc gno === Str: avg=12.49 std=2.13 p10=10 p25=11 p50=12 p75=14 p90=15 Dex: avg=9.78 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 Con: avg=12.48 std=2.11 p10=10 p25=11 p50=12 p75=14 p90=15 Int: avg=15.41 std=2.00 p10=13 p25=14 p50=15 p75=17 p90=18 Wis: avg=15.32 std=1.87 p10=13 p25=14 p50=15 p75=17 p90=18 Cha: avg=9.78 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 === arc (all races) === Str: avg=12.59 std=2.12 p10=10 p25=11 p50=12 p75=14 p90=15 Dex: avg=9.82 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 Con: avg=12.58 std=2.10 p10=10 p25=11 p50=12 p75=14 p90=15 Int: avg=15.23 std=1.77 p10=13 p25=14 p50=15 p75=16 p90=18 Wis: avg=15.20 std=1.71 p10=13 p25=14 p50=15 p75=16 p90=18 Cha: avg=9.82 std=1.64 p10=8 p25=9 p50=10 p75=11 p90=12 === bar hum === Str: avg=18.80 std=1.32 p10=17 p25=18 p50=19 p75=20 p90=20 Dex: avg=16.73 std=1.01 p10=15 p25=16 p50=17 p75=18 p90=18 Con: avg=17.69 std=0.59 p10=17 p25=17 p50=18 p75=18 p90=18 Int: avg=7.61 std=0.87 p10=7 p25=7 p50=7 p75=8 p90=9 Wis: avg=7.70 std=0.91 p10=7 p25=7 p50=8 p75=8 p90=9 Cha: avg=6.69 std=0.91 p10=6 p25=6 p50=6 p75=7 p90=8 === bar orc === Str: avg=18.81 std=1.33 p10=17 p25=18 p50=19 p75=20 p90=20 Dex: avg=16.72 std=1.01 p10=15 p25=16 p50=17 p75=18 p90=18 Con: avg=17.69 std=0.59 p10=17 p25=17 p50=18 p75=18 p90=18 Int: avg=7.60 std=0.86 p10=7 p25=7 p50=7 p75=8 p90=9 Wis: avg=7.69 std=0.90 p10=7 p25=7 p50=8 p75=8 p90=9 Cha: avg=6.70 std=0.91 p10=6 p25=6 p50=7 p75=7 p90=8 === bar (all races) === Str: avg=18.80 std=1.33 p10=17 p25=18 p50=19 p75=20 p90=20 Dex: avg=16.72 std=1.01 p10=15 p25=16 p50=17 p75=18 p90=18 Con: avg=17.69 std=0.59 p10=17 p25=17 p50=18 p75=18 p90=18 Int: avg=7.60 std=0.87 p10=7 p25=7 p50=7 p75=8 p90=9 Wis: avg=7.69 std=0.91 p10=7 p25=7 p50=8 p75=8 p90=9 Cha: avg=6.69 std=0.91 p10=6 p25=6 p50=6 p75=7 p90=8 === cav hum === Str: avg=19.30 std=2.46 p10=16 p25=18 p50=19 p75=21 p90=22 Dex: avg=13.20 std=2.17 p10=10 p25=12 p50=13 p75=15 p90=16 Con: avg=16.44 std=1.76 p10=14 p25=15 p50=17 p75=18 p90=18 Int: avg=8.90 std=1.40 p10=7 p25=8 p50=9 p75=10 p90=11 Wis: avg=9.21 std=1.49 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=8.21 std=1.48 p10=6 p25=7 p50=8 p75=9 p90=10 === cav dwa === Str: avg=19.11 std=2.52 p10=16 p25=17 p50=19 p75=21 p90=22 Dex: avg=13.09 std=2.22 p10=10 p25=12 p50=13 p75=15 p90=16 Con: avg=16.88 std=2.26 p10=14 p25=15 p50=17 p75=19 p90=20 Int: avg=8.87 std=1.40 p10=7 p25=8 p50=9 p75=10 p90=11 Wis: avg=9.16 std=1.48 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=8.17 std=1.48 p10=6 p25=7 p50=8 p75=9 p90=10 === cav gno === Str: avg=19.30 std=2.46 p10=16 p25=18 p50=19 p75=21 p90=22 Dex: avg=13.19 std=2.16 p10=10 p25=12 p50=13 p75=15 p90=16 Con: avg=16.44 std=1.76 p10=14 p25=15 p50=17 p75=18 p90=18 Int: avg=8.90 std=1.40 p10=7 p25=8 p50=9 p75=10 p90=11 Wis: avg=9.21 std=1.49 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=8.22 std=1.49 p10=6 p25=7 p50=8 p75=9 p90=10 === cav (all races) === Str: avg=19.24 std=2.49 p10=16 p25=18 p50=19 p75=21 p90=22 Dex: avg=13.16 std=2.18 p10=10 p25=12 p50=13 p75=15 p90=16 Con: avg=16.58 std=1.95 p10=14 p25=15 p50=17 p75=18 p90=18 Int: avg=8.89 std=1.40 p10=7 p25=8 p50=9 p75=10 p90=11 Wis: avg=9.19 std=1.49 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=8.20 std=1.48 p10=6 p25=7 p50=8 p75=9 p90=10 === hea hum === Str: avg=9.16 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Dex: avg=9.17 std=1.43 p10=7 p25=8 p50=9 p75=10 p90=11 Con: avg=14.56 std=1.63 p10=13 p25=13 p50=14 p75=16 p90=17 Int: avg=9.88 std=1.57 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=15.78 std=1.40 p10=14 p25=15 p50=16 p75=17 p90=18 Cha: avg=16.68 std=0.77 p10=16 p25=16 p50=17 p75=17 p90=18 === hea gno === Str: avg=9.18 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Dex: avg=9.16 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Con: avg=14.55 std=1.63 p10=13 p25=13 p50=14 p75=16 p90=17 Int: avg=9.88 std=1.57 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=15.78 std=1.40 p10=14 p25=15 p50=16 p75=17 p90=18 Cha: avg=16.68 std=0.77 p10=16 p25=16 p50=17 p75=17 p90=18 === hea (all races) === Str: avg=9.17 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Dex: avg=9.17 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Con: avg=14.56 std=1.63 p10=13 p25=13 p50=14 p75=16 p90=17 Int: avg=9.88 std=1.57 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=15.78 std=1.40 p10=14 p25=15 p50=16 p75=17 p90=18 Cha: avg=16.68 std=0.77 p10=16 p25=16 p50=17 p75=17 p90=18 === kni hum === Str: avg=14.89 std=1.23 p10=13 p25=14 p50=15 p75=16 p90=16 Dex: avg=8.66 std=0.89 p10=8 p25=8 p50=8 p75=9 p90=10 Con: avg=11.28 std=1.10 p10=10 p25=10 p50=11 p75=12 p90=13 Int: avg=7.97 std=1.01 p10=7 p25=7 p50=8 p75=8 p90=9 Wis: avg=14.96 std=0.97 p10=14 p25=14 p50=15 p75=15 p90=16 Cha: avg=17.46 std=0.54 p10=17 p25=17 p50=17 p75=18 p90=18 === mon hum === Str: avg=17.07 std=2.33 p10=14 p25=15 p50=17 p75=19 p90=20 Dex: avg=13.62 std=2.10 p10=11 p25=12 p50=14 p75=15 p90=16 Con: avg=11.26 std=1.95 p10=9 p25=10 p50=11 p75=12 p90=14 Int: avg=9.85 std=1.66 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=13.63 std=2.10 p10=11 p25=12 p50=14 p75=15 p90=17 Cha: avg=9.85 std=1.66 p10=8 p25=9 p50=10 p75=11 p90=12 === pri hum === Str: avg=11.89 std=2.02 p10=9 p25=10 p50=12 p75=13 p90=15 Dex: avg=11.90 std=2.02 p10=9 p25=10 p50=12 p75=13 p90=15 Con: avg=13.46 std=2.16 p10=11 p25=12 p50=13 p75=15 p90=16 Int: avg=10.26 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13 Wis: avg=17.44 std=1.07 p10=16 p25=17 p50=18 p75=18 p90=18 Cha: avg=10.28 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13 === pri elf === Str: avg=11.69 std=1.99 p10=9 p25=10 p50=12 p75=13 p90=14 Dex: avg=11.68 std=1.99 p10=9 p25=10 p50=12 p75=13 p90=14 Con: avg=13.09 std=1.97 p10=10 p25=12 p50=13 p75=15 p90=16 Int: avg=10.16 std=1.72 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=18.45 std=1.75 p10=16 p25=17 p50=19 p75=20 p90=20 Cha: avg=10.15 std=1.72 p10=8 p25=9 p50=10 p75=11 p90=12 === pri (all races) === Str: avg=11.79 std=2.01 p10=9 p25=10 p50=12 p75=13 p90=14 Dex: avg=11.79 std=2.01 p10=9 p25=10 p50=12 p75=13 p90=14 Con: avg=13.28 std=2.07 p10=11 p25=12 p50=13 p75=15 p90=16 Int: avg=10.21 std=1.73 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=17.95 std=1.54 p10=16 p25=17 p50=18 p75=19 p90=20 Cha: avg=10.22 std=1.73 p10=8 p25=9 p50=10 p75=11 p90=12 === ran hum === Str: avg=15.15 std=1.31 p10=14 p25=14 p50=15 p75=16 p90=17 Dex: avg=10.45 std=1.17 p10=9 p25=10 p50=10 p75=11 p90=12 Con: avg=14.44 std=1.15 p10=13 p25=14 p50=14 p75=15 p90=16 Int: avg=13.74 std=0.93 p10=13 p25=13 p50=14 p75=14 p90=15 Wis: avg=13.75 std=0.93 p10=13 p25=13 p50=14 p75=14 p90=15 Cha: avg=7.75 std=0.93 p10=7 p25=7 p50=8 p75=8 p90=9 === ran elf === Str: avg=15.14 std=1.25 p10=14 p25=14 p50=15 p75=16 p90=17 Dex: avg=10.46 std=1.17 p10=9 p25=10 p50=10 p75=11 p90=12 Con: avg=14.38 std=1.01 p10=13 p25=14 p50=14 p75=15 p90=16 Int: avg=13.75 std=0.93 p10=13 p25=13 p50=14 p75=14 p90=15 Wis: avg=13.75 std=0.93 p10=13 p25=13 p50=14 p75=14 p90=15 Cha: avg=7.75 std=0.93 p10=7 p25=7 p50=8 p75=8 p90=9 === ran gno === Str: avg=15.15 std=1.32 p10=14 p25=14 p50=15 p75=16 p90=17 Dex: avg=10.45 std=1.17 p10=9 p25=10 p50=10 p75=11 p90=12 Con: avg=14.44 std=1.14 p10=13 p25=14 p50=14 p75=15 p90=16 Int: avg=13.75 std=0.94 p10=13 p25=13 p50=14 p75=14 p90=15 Wis: avg=13.75 std=0.93 p10=13 p25=13 p50=14 p75=14 p90=15 Cha: avg=7.75 std=0.94 p10=7 p25=7 p50=8 p75=8 p90=9 === ran orc === Str: avg=15.15 std=1.31 p10=14 p25=14 p50=15 p75=16 p90=17 Dex: avg=10.45 std=1.17 p10=9 p25=10 p50=10 p75=11 p90=12 Con: avg=14.44 std=1.14 p10=13 p25=14 p50=14 p75=15 p90=16 Int: avg=13.73 std=0.87 p10=13 p25=13 p50=14 p75=14 p90=15 Wis: avg=13.73 std=0.86 p10=13 p25=13 p50=14 p75=14 p90=15 Cha: avg=7.75 std=0.94 p10=7 p25=7 p50=8 p75=8 p90=9 === ran (all races) === Str: avg=15.15 std=1.30 p10=14 p25=14 p50=15 p75=16 p90=17 Dex: avg=10.46 std=1.17 p10=9 p25=10 p50=10 p75=11 p90=12 Con: avg=14.43 std=1.11 p10=13 p25=14 p50=14 p75=15 p90=16 Int: avg=13.75 std=0.92 p10=13 p25=13 p50=14 p75=14 p90=15 Wis: avg=13.74 std=0.91 p10=13 p25=13 p50=14 p75=14 p90=15 Cha: avg=7.75 std=0.94 p10=7 p25=7 p50=8 p75=8 p90=9 === rog hum === Str: avg=13.78 std=2.26 p10=11 p25=12 p50=14 p75=15 p90=17 Dex: avg=17.52 std=0.99 p10=16 p25=18 p50=18 p75=18 p90=18 Con: avg=13.72 std=2.17 p10=11 p25=12 p50=14 p75=15 p90=17 Int: avg=10.40 std=1.77 p10=8 p25=9 p50=10 p75=11 p90=13 Wis: avg=10.41 std=1.77 p10=8 p25=9 p50=10 p75=12 p90=13 Cha: avg=9.41 std=1.77 p10=7 p25=8 p50=9 p75=11 p90=12 === rog orc === Str: avg=13.78 std=2.26 p10=11 p25=12 p50=14 p75=15 p90=17 Dex: avg=17.51 std=1.00 p10=16 p25=17 p50=18 p75=18 p90=18 Con: avg=13.73 std=2.18 p10=11 p25=12 p50=14 p75=15 p90=17 Int: avg=10.40 std=1.76 p10=8 p25=9 p50=10 p75=11 p90=13 Wis: avg=10.41 std=1.76 p10=8 p25=9 p50=10 p75=12 p90=13 Cha: avg=9.40 std=1.77 p10=7 p25=8 p50=9 p75=10 p90=12 === rog (all races) === Str: avg=13.78 std=2.26 p10=11 p25=12 p50=14 p75=15 p90=17 Dex: avg=17.52 std=1.00 p10=16 p25=18 p50=18 p75=18 p90=18 Con: avg=13.72 std=2.18 p10=11 p25=12 p50=14 p75=15 p90=17 Int: avg=10.40 std=1.76 p10=8 p25=9 p50=10 p75=11 p90=13 Wis: avg=10.41 std=1.76 p10=8 p25=9 p50=10 p75=12 p90=13 Cha: avg=9.41 std=1.77 p10=7 p25=8 p50=9 p75=10 p90=12 === sam hum === Str: avg=15.72 std=1.93 p10=13 p25=14 p50=16 p75=17 p90=18 Dex: avg=15.53 std=1.75 p10=13 p25=14 p50=16 p75=17 p90=18 Con: avg=17.91 std=0.32 p10=18 p25=18 p50=18 p75=18 p90=18 Int: avg=9.93 std=1.38 p10=8 p25=9 p50=10 p75=11 p90=12 Wis: avg=8.56 std=1.26 p10=7 p25=8 p50=8 p75=9 p90=10 Cha: avg=7.56 std=1.27 p10=6 p25=7 p50=7 p75=8 p90=9 === tou hum === Str: avg=11.32 std=1.95 p10=9 p25=10 p50=11 p75=13 p90=14 Dex: avg=11.32 std=1.93 p10=9 p25=10 p50=11 p75=13 p90=14 Con: avg=15.30 std=2.09 p10=12 p25=14 p50=15 p75=17 p90=18 Int: avg=12.89 std=1.65 p10=11 p25=12 p50=13 p75=14 p90=15 Wis: avg=8.90 std=1.66 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=15.52 std=1.84 p10=13 p25=14 p50=16 p75=17 p90=18 === val hum === Str: avg=18.58 std=2.33 p10=16 p25=17 p50=19 p75=20 p90=22 Dex: avg=12.72 std=2.08 p10=10 p25=11 p50=13 p75=14 p90=15 Con: avg=17.11 std=1.32 p10=15 p25=16 p50=18 p75=18 p90=18 Int: avg=8.76 std=1.35 p10=7 p25=8 p50=9 p75=10 p90=11 Wis: avg=9.03 std=1.43 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=9.04 std=1.43 p10=7 p25=8 p50=9 p75=10 p90=11 === val dwa === Str: avg=18.26 std=2.36 p10=15 p25=17 p50=18 p75=20 p90=21 Dex: avg=12.53 std=2.11 p10=10 p25=11 p50=12 p75=14 p90=15 Con: avg=17.82 std=1.94 p10=15 p25=16 p50=18 p75=20 p90=20 Int: avg=8.70 std=1.33 p10=7 p25=8 p50=9 p75=9 p90=10 Wis: avg=8.97 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=8.98 std=1.42 p10=7 p25=8 p50=9 p75=10 p90=11 === val (all races) === Str: avg=18.42 std=2.35 p10=15 p25=17 p50=18 p75=20 p90=21 Dex: avg=12.63 std=2.10 p10=10 p25=11 p50=13 p75=14 p90=15 Con: avg=17.47 std=1.70 p10=15 p25=16 p50=18 p75=18 p90=20 Int: avg=8.73 std=1.34 p10=7 p25=8 p50=9 p75=9 p90=10 Wis: avg=9.00 std=1.43 p10=7 p25=8 p50=9 p75=10 p90=11 Cha: avg=9.01 std=1.43 p10=7 p25=8 p50=9 p75=10 p90=11 === wiz hum === Str: avg=10.27 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13 Dex: avg=13.47 std=2.16 p10=11 p25=12 p50=13 p75=15 p90=16 Con: avg=13.47 std=2.16 p10=11 p25=12 p50=13 p75=15 p90=16 Int: avg=17.45 std=1.06 p10=16 p25=17 p50=18 p75=18 p90=18 Wis: avg=10.28 std=1.73 p10=8 p25=9 p50=10 p75=11 p90=13 Cha: avg=10.28 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13 === wiz elf === Str: avg=10.15 std=1.72 p10=8 p25=9 p50=10 p75=11 p90=12 Dex: avg=13.22 std=2.14 p10=11 p25=12 p50=13 p75=15 p90=16 Con: avg=13.09 std=1.97 p10=10 p25=12 p50=13 p75=15 p90=16 Int: avg=18.46 std=1.74 p10=16 p25=17 p50=19 p75=20 p90=20 Wis: avg=10.16 std=1.71 p10=8 p25=9 p50=10 p75=11 p90=12 Cha: avg=10.15 std=1.71 p10=8 p25=9 p50=10 p75=11 p90=12 === wiz gno === Str: avg=10.19 std=1.72 p10=8 p25=9 p50=10 p75=11 p90=12 Dex: avg=13.30 std=2.15 p10=11 p25=12 p50=13 p75=15 p90=16 Con: avg=13.33 std=2.15 p10=11 p25=12 p50=13 p75=15 p90=16 Int: avg=18.02 std=1.42 p10=16 p25=17 p50=19 p75=19 p90=19 Wis: avg=10.19 std=1.73 p10=8 p25=9 p50=10 p75=11 p90=12 Cha: avg=10.21 std=1.72 p10=8 p25=9 p50=10 p75=11 p90=12 === wiz orc === Str: avg=10.51 std=1.79 p10=8 p25=9 p50=10 p75=12 p90=13 Dex: avg=13.90 std=2.16 p10=11 p25=12 p50=14 p75=15 p90=17 Con: avg=13.92 std=2.16 p10=11 p25=12 p50=14 p75=15 p90=17 Int: avg=15.86 std=0.49 p10=16 p25=16 p50=16 p75=16 p90=16 Wis: avg=10.50 std=1.78 p10=8 p25=9 p50=10 p75=12 p90=13 Cha: avg=10.51 std=1.77 p10=8 p25=9 p50=10 p75=12 p90=13 === wiz (all races) === Str: avg=10.28 std=1.75 p10=8 p25=9 p50=10 p75=11 p90=13 Dex: avg=13.47 std=2.17 p10=11 p25=12 p50=13 p75=15 p90=16 Con: avg=13.45 std=2.13 p10=11 p25=12 p50=13 p75=15 p90=16 Int: avg=17.45 std=1.60 p10=16 p25=16 p50=18 p75=19 p90=20 Wis: avg=10.28 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13 Cha: avg=10.29 std=1.74 p10=8 p25=9 p50=10 p75=11 p90=13
How to reproduce
I'm running it on a clean local NetHack 5.0.0 setup that I run from ~/nh/install/games/nethack (the default path where it's installed on Linux via make install) with the basic .nethackrc:
OPTIONS=reroll OPTIONS=!tutorial
Here's the code for the sampler (sampler.c, to compile: gcc -o sampler sampler.c).
This program takes in an amount of samples it needs to generate and a command line to run the game. It expects that the game would immediately start with the reroll screen and will hang if it starts with some other prompt, hence the use of -@ below. It prints the sampled attribute lists, one in a line, in the same form they are presented in game: St:14 Dx:11 Co:12 In:13 Wi:16 Ch:9
AI use disclosure: this code was mostly written by an LLM agent.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/poll.h>
#include <fcntl.h>
#include <pty.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>
#define BUF_SIZE (1024 * 64)
static void strip_ansi(const char *src, char *dst, size_t dst_cap)
{
size_t j = 0;
for (size_t i = 0; src[i] && j + 1 < dst_cap; ) {
if (src[i] == '\x1b' && src[i + 1] == '[') {
i += 2;
while (src[i] && !(isalpha((unsigned char)src[i])))
i++;
if (src[i]) i++;
} else {
dst[j++] = src[i++];
}
}
dst[j] = '\0';
}
static const char *parse_field_value(const char *p, char *out, size_t *j, size_t cap)
{
while (*p && isdigit((unsigned char)*p) && *j + 1 < cap)
out[(*j)++] = *p++;
if (*p == '/' && *j + 1 < cap) {
out[(*j)++] = *p++;
if (*p == '*' && *(p + 1) == '*' && *j + 2 < cap) {
out[(*j)++] = '*';
out[(*j)++] = '*';
p += 2;
} else {
while (*p && isdigit((unsigned char)*p) && *j + 1 < cap)
out[(*j)++] = *p++;
}
}
return p;
}
static const char *find_stat_line(const char *reroll_pos, char *out, size_t cap,
size_t *out_len, const char **stat_end)
{
const char *st_pos = strstr(reroll_pos, "St:");
if (!st_pos) {
*out_len = 0;
*stat_end = NULL;
return NULL;
}
static const char *labels[] = {"St:", "Dx:", "Co:", "In:", "Wi:", "Ch:"};
static const size_t lens[] = {3, 3, 3, 3, 3, 3};
const char *p = st_pos;
size_t j = 0;
for (int i = 0; i < 6; i++) {
if (strncmp(p, labels[i], lens[i]) != 0) {
*out_len = 0;
*stat_end = NULL;
return NULL;
}
for (size_t k = 0; k < lens[i] && j + 1 < cap; k++)
out[j++] = p[k];
p += lens[i];
p = parse_field_value(p, out, &j, cap);
if (i < 5 && *p == ' ' && j + 1 < cap)
out[j++] = *p++;
}
out[j] = '\0';
*out_len = j;
*stat_end = p;
return st_pos;
}
static void send_r(int mfd)
{
for (;;) {
ssize_t w = write(mfd, "r", 1);
if (w == 1) return;
if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
struct pollfd wpfd = { .fd = mfd, .events = POLLOUT };
poll(&wpfd, 1, 100);
continue;
}
if (w < 0 && errno == EINTR) continue;
return;
}
}
int main(int argc, char *argv[])
{
int n = 1;
int opt;
while ((opt = getopt(argc, argv, "n:")) != -1) {
switch (opt) {
case 'n':
n = atoi(optarg);
if (n <= 0) {
fprintf(stderr, "sampler: -n must be positive\n");
return 1;
}
break;
default:
fprintf(stderr, "Usage: sampler -n COUNT -- COMMAND [ARGS...]\n");
return 1;
}
}
int cmd_start = -1;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--") == 0) {
cmd_start = i + 1;
break;
}
}
if (cmd_start < 0 || cmd_start >= argc) {
fprintf(stderr, "sampler: expected -- followed by command\n");
return 1;
}
// make sure we have enough rows for Tou inventory
struct winsize ws = { .ws_row = 40, .ws_col = 80 };
int mfd;
pid_t pid = forkpty(&mfd, NULL, NULL, &ws);
if (pid < 0) {
perror("forkpty");
return 1;
}
if (pid == 0) {
setenv("TERM", "xterm", 1);
execvp(argv[cmd_start], &argv[cmd_start]);
perror("execvp");
_exit(127);
}
fcntl(mfd, F_SETFL, fcntl(mfd, F_GETFL) | O_NONBLOCK);
char *buf = malloc(BUF_SIZE);
char *clean = malloc(BUF_SIZE);
char stat_out[256];
if (!buf || !clean) {
perror("malloc");
return 1;
}
size_t buflen = 0;
int collected = 0;
struct pollfd pfd = { .fd = mfd, .events = POLLIN };
for (;;) {
int status;
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == pid) {
fprintf(stderr, "sampler: child exited prematurely\n");
goto done;
}
int r = poll(&pfd, 1, 1000);
if (r < 0 && errno != EINTR) {
perror("poll");
goto done;
}
if (r <= 0) continue;
for (;;) {
ssize_t nr = read(mfd, buf + buflen, BUF_SIZE - buflen - 1);
if (nr < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
if (errno == EINTR) continue;
if (errno == EIO) { buflen = 0; break; }
perror("read");
goto done;
}
if (nr == 0) goto done;
buflen += (size_t)nr;
buf[buflen] = '\0';
}
if (buflen == 0) continue;
strip_ansi(buf, clean, BUF_SIZE);
const char *reroll_pos = strstr(clean, "Reroll this character?");
if (!reroll_pos)
continue;
size_t stat_len;
const char *stat_end;
if (!find_stat_line(reroll_pos, stat_out, sizeof(stat_out),
&stat_len, &stat_end))
continue;
puts(stat_out);
fflush(stdout);
collected++;
if (collected >= n)
goto done;
send_r(mfd);
buflen = 0;
}
done:
kill(pid, SIGHUP);
waitpid(pid, NULL, 0);
close(mfd);
free(buf);
free(clean);
return 0;
}
The script that takes in the list of samples and aggregates them into the statistics shown above is called stats.py:
#!/usr/bin/env python
import sys
import math
def parse_common(s, prefix):
assert s.startswith(prefix+':')
return s[len(prefix)+1:]
def parse_strength(s):
s = parse_common(s, 'St')
try:
return int(s)
except ValueError:
s1, s2 = s.split('/')
assert s1 == '18', s1
return 18 + int(s2)
def parse_line(line):
attrs = line.strip().split()
assert len(attrs) == 6
return {
'Str': parse_strength(attrs[0]),
'Dex': int(parse_common(attrs[1], 'Dx')),
'Con': int(parse_common(attrs[2], 'Co')),
'Int': int(parse_common(attrs[3], 'In')),
'Wis': int(parse_common(attrs[4], 'Wi')),
'Cha': int(parse_common(attrs[5], 'Ch')),
}
ATTRS = ('Str', 'Dex', 'Con', 'Int', 'Wis', 'Cha')
PERCENTILES = (10, 25, 50, 75, 90)
if __name__ == '__main__':
by_attr = {a: [] for a in ATTRS}
for line in sys.stdin:
attrs = parse_line(line)
for attr, val in attrs.items():
by_attr[attr].append(val)
for attr in ATTRS:
by_attr[attr].sort()
n = len(by_attr[attr])
val_sum = 0
val_sq_sum = 0
for val in by_attr[attr]:
val_sum += val
val_sq_sum += val**2
avg = val_sum / n
std = math.sqrt((n*val_sq_sum - val_sum**2) / n**2)
print(f'{attr}:\tavg={avg:.2f}\tstd={std:.2f}', end='')
for p in PERCENTILES:
print(f'\tp{p}={by_attr[attr][p*n//100]}', end='')
print()
And finally, the shell script that runs it for all role/race combinations:
#!/bin/sh -e
gcc -o sampler sampler.c
mkdir -p raw_output
samples=100000
run_role_race()
{
echo "=== $1 $2 ==="
./sampler -n "$samples" -- ~/nh/install/games/nethack -p "$1" -r "$2" -@ | tee "raw_output/${1}_${2}.txt" | ./stats.py
echo
}
all_races()
{
echo "=== $1 (all races) ==="
cat raw_output/${1}_* | ./stats.py
echo
}
run_role_race arc hum
run_role_race arc dwa
run_role_race arc gno
all_races arc
run_role_race bar hum
run_role_race bar orc
all_races bar
run_role_race cav hum
run_role_race cav dwa
run_role_race cav gno
all_races cav
run_role_race hea hum
run_role_race hea gno
all_races hea
run_role_race kni hum
run_role_race mon hum
run_role_race pri hum
run_role_race pri elf
all_races pri
run_role_race ran hum
run_role_race ran elf
run_role_race ran gno
run_role_race ran orc
all_races ran
run_role_race rog hum
run_role_race rog orc
all_races rog
run_role_race sam hum
run_role_race tou hum
run_role_race val hum
run_role_race val dwa
all_races val
run_role_race wiz hum
run_role_race wiz elf
run_role_race wiz gno
run_role_race wiz orc
all_races wiz