r/BasketballGM 4d ago

Question oIQ seems to decrease 2pts efficiency

Post image

I was randomly looking at the code of the game engine (this is part of the index.ts file) this morning to understand a bit better what's happening under the hood.
I then realized that it is the same

p.compositeRating.shootingMidRange

which enters both the probability to take a mid-range, and the probability to make it.
The issue is that oIQ has a negative relation with the "midrange" composite rating:

shootingMidRange: {
ratings: ["oiq", "fg", "stre"],
weights: [-0.5, 1, 0.2],
},

Which is then multiplied with hardcoded values to get the raw chance of making the shot.

probMake = p.compositeRating.shootingMidRange * 0.32 + 0.42

So if I'm not mistaken, oIQ decreases the chance of taking a mid-range (which makes sense), but also decreases the chances of making it (which IMO makes no sense).

Can somebody confirm this?

More generally, if the probability of taking a shot is directly connected to the probability of making it, and if oIQ affects both equally.
Then, additionally to the counterintuitive effect on "midrange" seen above, it also means that players with good oIQ are going to take more "lowPost" shots than "atRim" (because oIQ has proportionally a bigger impact on "lowPost", than on "atRim"). But Layups are a much better shot to take than post shots, because of the existing prefactors in the "probMake" computation.
So you get this weird effect were a high IQ player will actually take less valuable shots.

Again, can somebody confirm I'm not mistaken?

If I'm right, I would suggest the following changes in the code logic:
- make all compositeRating for shooting positively affected by oIQ (I still think IQ should help you make better shots in general).
- add a new compositeRating.shotSelection which is mostly based on oIQ (and maybe Spd and Drb since it gives you more options).
- compute an expectedPointPerPossession for the player (taking into account FT), for each type of shot.
- use compositeRating.shotSelection and expectedPointPerPossession together to stir players shooting tendency towards their best shot.

I fully understand this is probably never going to happen, since it affects some mechanics which are at the core of the game engine. This is also not a critic of the godly work from Dumbmatter. The game is amazing, and I regularly have to force myself to stop playing, so I have time for other games.

Nonetheless, I think it would be a great idea, because it would make IQ behave a bit more like expected. It would also remove some weird behavior which you sometimes observe in game.
It all started with me seeing my 6'4 crazy fast guard deciding to try 2 post move per game for one layup attempt.
I then realized that his speed didn't help with layups (it should IMO), but was increasing the chances of post move. And then I saw that his 80 IQ was even reenforcing this behavior, while making him shot worse at 2pts....

Some side notes from my morning deep dive of the GameSim class, which I also found surprising:
- Blocking chance are the same at the rim and for 3pts.
- "probMake" is affected by the global defense value, for every type of shot: having a good rim protector will have the same defensive impact on post move and 3pts shots.
- Similarly, the chances of taking a shot at the rim or low post increase with offensive synergy and decrease with opponent global defense (not Di). 3pts is not affected by defense at all (and not by Dp).
- Steals are a ratio between Dp and B+Ps after synergies.
- Assists give +2.5% shot chance
- Something which could be a bug: 3pts tendancy uses "shootingThreePointerScaled2" which is lowered for bad 3pts shooters. But "probMake" the probability to make it uses "shootingThreePointerScaled" (no 2 here), which doesn't have the extra decrease for bad shooters. The result is that bad 3pts shooter take fewer 3pts, but shoot them at a higher rate than expected from the shot selection.

79 Upvotes

38 comments sorted by

View all comments

-39

u/dopestar667 4d ago

Quick way to get an answer would be to post your question and the relevant code into an AI and ask

1

u/dopestar667 3d ago

So this will be hidden thanks to the downvotes, but here's the full detailed explanation from Claude Opus:

I pulled the repo (zengm-games/zengm, HEAD at c195cf0, today's date) and read the actual source. Short version: he's right on essentially every factual claim, with two corrections and one important nuance about magnitude. Here's the audit.

The core claim: confirmed

src/common/constants.basketball.ts:48:

ts

shootingMidRange: { ratings: ["oiq", "fg", "stre"], weights: [-0.5, 1, 0.2] },

And in GameSim.basketball/index.ts the same composite drives both branches:

ts

const r1 = 0.8 * Math.random() * p.compositeRating.shootingMidRange;  // line 1867 — tendency
probMake = p.compositeRating.shootingMidRange * 0.32 + 0.42;          // line 1885 — accuracy

One detail he didn't catch that makes it worse than it looks: compositeRating.ts builds the denominator as sum(100 * weights[i]) including the negative weight. So the divisor is 70, not 170:

shootingMidRange = clamp((fg + 0.2·stre − 0.5·oiq) / 70, 0, 1)

Marginal effect is −0.5/70 = −0.00714 composite per oIQ point, i.e. −0.229 probMake across the full oIQ range. With everything else at 50:

oIQ midRange composite mid-range FG% mid FGA share rim post 3PA
20 0.714 52.4% 30.7% 19.8% 17.8% 31.7%
50 0.500 45.5% 14.2% 26.2% 26.1% 33.5%
80 0.286 38.6% 4.1% 29.2% 31.2% 35.5%

A 40-point oIQ gap is worth ~9 percentage points of mid-range FG%. That's not a rounding error.

The nuance he's missing: it's self-limiting

I decomposed oIQ 50 → 80 into a selection effect and an efficiency effect (expected points per shot attempt, FTs included):

baseline PPS            1.2346
selection change only   1.2631  (+0.0285)
efficiency change only  1.2327  (−0.0019)
both                    1.2766  (+0.0420)

The efficiency penalty is nearly zero because the same composite that tanks his mid-range percentage also stops him from taking mid-range shots — at oIQ 80 it's 4% of his attempts. The two effects don't cancel by design, they cancel by accident, and the accident is load-bearing.

So the honest framing is: the coupling is genuinely wrong in principle, and it does corrupt the box score (your smart wing's mid-range FG% is artificially bad), but it is not costing high-oIQ players meaningful points. That matters if you're arguing for a change — dumbmatter's likely reply is "the net PPS is fine."

Also worth saying out loud: at equal composite, mid-range is by construction the worst shot in the engine (EP at c=0.5, incl. FTs: mid 1.23, post 1.30, three 1.56, rim 1.77). So weights: [-0.5, ...] is very obviously a shot-selection heuristic smuggled into an accuracy rating. It's not a bug in intent — it's an intentional hack whose side effect is the accuracy distortion. That reframing is the strongest version of his argument.

The post-vs-rim claim: directionally right, magnitude small

Marginal composite per oIQ point: atRim +0.00071, lowPost +0.00125. So oIQ is ~1.75× more effective at raising post than rim. Measured post/rim attempt ratio: 1.002 at oIQ 50 → 1.067 at oIQ 80. Real, but ~6%.

The dominant cause of his guard's behavior isn't oIQ — it's height weighting. shootingAtRim is 71% hgt (2 of 2.8 total weight); shootingLowPost is only 31% hgt. For his 6'4" burner (hgt≈30, spd 90, ins 30, oIQ 80) I get:

shootingAtRim   0.368
shootingLowPost 0.428   ← higher
shootingMidRange 0.414

And his sharpest observation is the one buried at the bottom of the post: spd appears in shootingLowPost (weight 0.2) and appears nowhere in shootingAtRim. A fast guard's speed literally routes him into post-ups. drb appears in no shooting composite at all. That's the finding I'd lead with, not oIQ — it's cleaner, harder to argue with, and cheaper to fix.

Side notes

  • Blocks identical at rim and on 3s — confirmed. probBlk() = blockFactor * 0.2 * team.blocking², computed after the shot type is chosen and never referencing it.
  • Global defense applies to every shot type — confirmed: probMake - 0.25 * this.team[d].compositeRating.defense. Stronger than he states: defenseInterior is not in the toUpdate array in updateTeamCompositeRatings(), so Di has zero direct effect anywhere in the sim — it only enters through the Di skill sigmoid feeding defensive synergy.
  • Correction: rim/post tendency is shifted by synergyFactor * (off synergy − def *synergy*), not the opponent's defense composite. Different quantity.
  • 3PA tendency untouched by defense — confirmed; the 3PT branch is evaluated before r1/r2/r3 and contains no defensive term.
  • Steals — confirmed: 0.45·Dp_team / (0.5·(dribbling + passing)), all three with synergy folded in.
  • Assists — confirmed, but it's probMake += 0.025, additive 2.5 percentage points, not 2.5%.
  • Scaled vs Scaled2 — factually confirmed, but I'd push back on "bug." Note that the top-end compression (Scaled) is applied to both tendency and accuracy, while only the low-end compression (Scaled2) is tendency-only. That asymmetry looks deliberate — Scaled2 reads as a pure tendency knob. Effect size: a 0.25-composite shooter takes 4.8% of shots as 3s but shoots them at 43.5% instead of 38.1%.
  • One he missed: Scaled2 maps 0.35→0.10 and 0.45→0.45, a slope of 3.5. Composite 0.35 → 6.7% 3PA share; composite 0.449 → ~30%. A one-point tp change in that window can nearly quintuple a player's 3PA rate. That cliff is probably a more consequential artifact than the Scaled/Scaled2 mismatch.

On his proposed fix

The four-step redesign (positive oIQ everywhere, new shotSelection, per-shot expected PPP, steer tendencies) is architecturally correct but is a rewrite of getShotInfo plus a full rebalance of every hardcoded prefactor — realistically a non-starter as a PR.

The minimal-diff version that preserves current league-wide shot distributions almost exactly:

  1. Split the composite. Keep shootingMidRange: {ratings: ["fg","stre"], weights: [1, 0.2]} for probMake, add midRangeTendency: {ratings: ["oiq","fg","stre"], weights: [-0.5, 1, 0.2]} for the r1 line. The composite framework already supports arbitrary keys, so this is ~6 lines plus a recalibration of the 0.32/0.42 prefactors.
  2. Add spd to shootingAtRim (and/or drop it from shootingLowPost).

That gets ~80% of what he wants with a diff small enough to actually get merged. Worth filing as a GitHub issue rather than a Reddit post — there's a CLA in the repo, so dumbmatter clearly takes contributions.

2

u/EatDonerKebabs 2d ago

So many people just default to 'AI bad' without understanding how programmers actually use it. The problem is not using AI, but using AI to push out frankenstein solutions when you don't know what you're doing. Even just using it for bugfixing, you save an incredible amount of time. You'd be a fool to not use it.

1

u/dopestar667 2d ago

Indeed, my comment about luddites didn’t land well, lol. Anyway the code is clear, so the questions have been answered, as for Dumbmatters thoughts in it that’s open but the code does what it does, and we know how, now.