Build a Hooper is a browser game where you draft
thirteen attributes onto one player and simulate a season. Your overall rating is a
weighted average of those thirteen, and the weights change by position — the game
publishes the whole matrix on its attribute page.
Thirteen rows, five columns, every column summing to exactly 100. That is a
constraint-satisfaction problem with a lot of freedom in it, so I got curious about
how the numbers were actually distributed.
Key Takeaways
- Distance between any two position columns grows monotonically with how far apart those positions are on the floor. The matrix encodes a one-dimensional spectrum.
- Small forward is the flattest column: 12 of 13 attributes carry ≥5% weight. Center is the spikiest: only 8 do.
- The spread per attribute ranges from 1.5x (Strength) to 13x (Blocks), and the high-spread attributes are exactly the ones that define position identity.
- None of this is stated anywhere in the game. It falls out of the table.
The matrix
Attribute PG SG SF PF C
---------------------- --- --- --- --- ---
Three-Point Shooting 10% 13% 10% 7% 4%
Midrange 7% 10% 9% 7% 4%
Finishing 7% 8% 9% 11% 12%
Dunking 3% 5% 6% 8% 10%
Ball Handle 15% 11% 8% 4% 2%
Passing / Vision 17% 9% 8% 6% 5%
Perimeter Defense 10% 12% 12% 7% 3%
Interior Defense 2% 3% 7% 13% 18%
Blocks 1% 2% 4% 9% 13%
Rebounding 3% 4% 7% 12% 15%
Athleticism 10% 10% 10% 8% 6%
Strength / Physicality 5% 4% 5% 6% 6%
Clutch / IQ 10% 9% 5% 2% 2%
Five columns, each summing to 100. Thirteen attributes competing for that budget.
The positions sit on a line
Treat each column as a 13-dimensional vector and take the L1 distance between pairs:
const W = {
'3PT':[10,13,10,7,4], 'MID':[7,10,9,7,4], 'FIN':[7,8,9,11,12],
'DNK':[3,5,6,8,10], 'HAN':[15,11,8,4,2], 'PAS':[17,9,8,6,5],
'PDEF':[10,12,12,7,3], 'IDEF':[2,3,7,13,18], 'BLK':[1,2,4,9,13],
'REB':[3,4,7,12,15], 'ATH':[10,10,10,8,6], 'STR':[5,4,5,6,6],
'CLU':[10,9,5,2,2],
};
const POS = ['PG','SG','SF','PF','C'];
const dist = (a, b) =>
Object.values(W).reduce((s, v) => s + Math.abs(v[a] - v[b]), 0);
for (let a = 0; a < 5; a++)
for (let b = a + 1; b < 5; b++)
console.log(POS[a], POS[b], dist(a, b), '| gap', b - a);
Group the results by how many slots apart the two positions are:
| Slots apart | Pairs | Distances | Mean |
|---|---|---|---|
| 1 | PG-SG, SG-SF, SF-PF, PF-C | 28, 24, 42, 30 | 31 |
| 2 | PG-SF, SG-PF, SF-C | 42, 66, 72 | 60 |
| 3 | PG-PF, SG-C | 76, 96 | 86 |
| 4 | PG-C | 106 | 106 |
Monotonic, and close to linear — roughly 28 points of L1 distance per slot. Adjacent
positions are near-duplicates of each other; the two ends are maximally far apart.
That is not what you get if you assign thirteen weights per position by feel. It is
what you get when the designer has a mental model of position as a continuum —
guard through big — and places each column along it. The five discrete labels are a
UI convenience on top of a one-dimensional axis.
SG and SF are the closest pair in the whole matrix at 24. Which tracks: the modern
wing is exactly where those two roles have collapsed into each other.
Small forward is the generalist, and the math says so
Rank the columns by concentration — Herfindahl index over the weights, plus how many
attributes clear a 5% floor:
| Position | Top weight | Top 3 sum | HHI | Attributes ≥5% |
|---|---|---|---|---|
| C | 18% | 46% | 0.1108 | 8 |
| PG | 17% | 42% | 0.1060 | 9 |
| SG | 13% | 36% | 0.0930 | 9 |
| PF | 13% | 36% | 0.0882 | 11 |
| SF | 12% | 32% | 0.0834 | 12 |
Small forward is the flattest distribution in the table: twelve of thirteen
attributes carry at least 5%, and nothing exceeds 12%. Center is the opposite —
Interior Defense, Rebounding and Blocks alone are 46%, and five attributes are
effectively dead weight.
In play terms, drafting a center is a narrow optimization problem and drafting a
small forward is a broad one. The game never says that. The Herfindahl index does.
Which attributes carry position identity
Per-attribute spread, high to low:
| Attribute | Low → High | Ratio |
|---|---|---|
| Blocks | 1% → 13% | 13.0x |
| Interior Defense | 2% → 18% | 9.0x |
| Ball Handle | 2% → 15% | 7.5x |
| Rebounding | 3% → 15% | 5.0x |
| Clutch / IQ | 2% → 10% | 5.0x |
| ... | ||
| Finishing | 7% → 12% | 1.7x |
| Athleticism | 6% → 10% | 1.7x |
| Strength / Physicality | 4% → 6% | 1.5x |
The high-spread attributes are precisely the ones that define what a position is —
you cannot describe a center without rim protection and rebounding, or a point guard
without handle. The low-spread ones (Strength, Athleticism, Finishing) are the
baseline athletic floor that every basketball player needs some of.
So the matrix has a clean two-layer structure: a mostly-flat base that applies to
everyone, and a spiky overlay that encodes role. That is a reasonable pattern to
steal for any RPG-ish stat system — it gives you meaningful class identity without
making any stat useless.
Where the ratings come from
Weights are only half of it. The actual numbers you steal come from whichever player
you pick on a given spin, and the rosters are not equal: the
team pool runs 83 team-seasons, 53 of them
classics carrying the highest ratings in the game, topping out with the 1970-71 Bucks
at 99 and the 1995-96 Bulls and 2016-17 Warriors at 98.
Which means a real draft decision is two-sided — how heavily is this attribute
weighted for the position I might get, and is this roster good enough that spending a
pick here beats waiting for a rare pull. The weight table tells you the first half.
Only the spin tells you the second.
The caveat
Everything above is derived from the published weights, not from the simulation
engine. Overall rating drives possessions, but an 82-game season plus a Play-In plus
a playoff bracket puts a lot of variance downstream of that single number. A
well-optimized build loses plenty of runs.
The matrix tells you how to build. It does not tell you how it ends.
Build a Hooper is an unofficial basketball simulator. Not affiliated with the NBA;
no official logos, photos, or uniforms.
Top comments (0)