diff --git a/src/worker/core/draft/genPlayersWithoutSaving.ts b/src/worker/core/draft/genPlayersWithoutSaving.ts index 291426fc69..f1a552f4f6 100644 --- a/src/worker/core/draft/genPlayersWithoutSaving.ts +++ b/src/worker/core/draft/genPlayersWithoutSaving.ts @@ -158,7 +158,7 @@ const genPlayersWithoutSaving = async ( } // Small chance of making top 4 players (in 70 player draft) special - on average, one per draft class - if (existingPlayers.length === 0) { + if (!isSport("basketball") && existingPlayers.length === 0) { const numSpecialPlayerChances = Math.round((4 / 70) * numPlayers); for (let i = 0; i < numSpecialPlayerChances; i++) { diff --git a/src/worker/core/player/developSeason.basketball.ts b/src/worker/core/player/developSeason.basketball.ts index 94b7281dd9..e446ba6bc0 100644 --- a/src/worker/core/player/developSeason.basketball.ts +++ b/src/worker/core/player/developSeason.basketball.ts @@ -5,200 +5,38 @@ import type { RatingKey, } from "../../../common/types.basketball"; -type RatingFormula = { - ageModifier: (age: number) => number; - changeLimits: (age: number) => [number, number]; -}; - -const shootingFormula: RatingFormula = { - ageModifier: (age: number) => { - // Reverse most of the age-related decline in calcBaseChange - if (age <= 27) { - return 0; - } - - if (age <= 29) { - return 0.5; - } - - if (age <= 31) { - return 1.5; - } - - return 2; - }, - changeLimits: () => [-3, 13], -}; -const iqFormula: RatingFormula = { - ageModifier: (age: number) => { - if (age <= 21) { - return 4; - } - - if (age <= 23) { - return 3; - } - - // Reverse most of the age-related decline in calcBaseChange - if (age <= 27) { - return 0; - } - - if (age <= 29) { - return 0.5; - } - - if (age <= 31) { - return 1.5; - } - - return 2; - }, - changeLimits: age => { - if (age >= 24) { - return [-3, 9]; - } - - // For 19: [-3, 32] - // For 23: [-3, 12] - return [-3, 7 + 5 * (24 - age)]; - }, -}; -const ratingsFormulas: Record, RatingFormula> = { - stre: { - ageModifier: () => 0, - changeLimits: () => [-Infinity, Infinity], - }, - spd: { - ageModifier: (age: number) => { - if (age <= 27) { - return 0; - } - - if (age <= 30) { - return -2; - } - - if (age <= 35) { - return -3; - } - - if (age <= 40) { - return -4; - } - - return -8; - }, - changeLimits: () => [-12, 2], - }, - jmp: { - ageModifier: (age: number) => { - if (age <= 26) { - return 0; - } - - if (age <= 30) { - return -3; - } - - if (age <= 35) { - return -4; - } - - if (age <= 40) { - return -5; - } - - return -10; - }, - changeLimits: () => [-12, 2], - }, - endu: { - ageModifier: (age: number) => { - if (age <= 23) { - return random.uniform(0, 9); - } - - if (age <= 30) { - return 0; - } - - if (age <= 35) { - return -2; - } - - if (age <= 40) { - return -4; - } - - return -8; - }, - changeLimits: () => [-11, 19], - }, - dnk: { - ageModifier: (age: number) => { - // Like shootingForumla, except for old players - if (age <= 27) { - return 0; - } - - return 0.5; - }, - changeLimits: () => [-3, 13], - }, - ins: shootingFormula, - ft: shootingFormula, - fg: shootingFormula, - tp: shootingFormula, - oiq: iqFormula, - diq: iqFormula, - drb: { - ageModifier: shootingFormula.ageModifier, - changeLimits: () => [-2, 5], - }, - pss: { - ageModifier: shootingFormula.ageModifier, - changeLimits: () => [-2, 5], - }, - reb: { - ageModifier: shootingFormula.ageModifier, - changeLimits: () => [-2, 5], - }, +// (age coefficient, age offset) for mean, than std. dev. +const ratingsFormulas: Record, Array> = { + diq: [0.008, -0.18, -0.0, 0.0012], + dnk: [0.006, -0.1601, -0.0009, 0.0317], + drb: [0.0087, -0.2156, 0.0, 0.0], + endu: [-0.0398, 1.0722, 0.0029, -0.0604], + fg: [0.0024, -0.0443, -0.0008, 0.054], + ft: [0.0052, -0.1124, -0.0012, 0.0704], + ins: [0.0027, -0.0933, -0.0006, 0.083], + jmp: [-0.0146, 0.2996, 0.0077, -0.1821], + oiq: [-0.0016, 0.0676, -0.0, 0.0012], + pss: [0.0062, -0.1502, -0.0, 0.0001], + reb: [0.0067, -0.1769, -0.0, 0.0006], + spd: [-0.0079, 0.1606, 0.0033, -0.0793], + stre: [-0.0019, 0.0375, 0.0, 0.0], + tp: [0.0079, -0.1909, -0.0025, 0.1013], }; const calcBaseChange = (age: number, coachingRank: number): number => { let val: number; - if (age <= 21) { - val = 2; - } else if (age <= 25) { - val = 1; - } else if (age <= 27) { - val = 0; - } else if (age <= 29) { - val = -1; - } else if (age <= 31) { - val = -2; - } else if (age <= 34) { - val = -3; - } else if (age <= 40) { - val = -4; - } else if (age <= 43) { - val = -5; - } else { - val = -6; - } + const base_coef = [-0.0148, 0.3846, -0.0001, 0.1659]; - // Noise - if (age <= 23) { - val += helpers.bound(random.realGauss(0, 5), -4, 20); - } else if (age <= 25) { - val += helpers.bound(random.realGauss(0, 5), -4, 10); - } else { - val += helpers.bound(random.realGauss(0, 3), -2, 4); - } + val = base_coef[0] * age + base_coef[1]; + const std_base = base_coef[2] * age + base_coef[3]; + const std_noise = helpers.bound( + random.realGauss(0, Math.max(0.00001, std_base)), + -0.1, + 0.4, + ); + val += std_noise; - // Modulate by coaching. g.get("numActiveTeams") doesn't exist when upgrading DB, but that doesn't matter if (g.hasOwnProperty("numActiveTeams")) { const numActiveTeams = g.get("numActiveTeams"); if (numActiveTeams > 1) { @@ -230,21 +68,23 @@ const developSeason = ( ratings.hgt += 1; } } + const age_bounds = helpers.bound(age, 19, 50); - const baseChange = calcBaseChange(age, coachingRank); + const baseChange = calcBaseChange(age_bounds, coachingRank); for (const key of helpers.keys(ratingsFormulas)) { - const ageModifier = ratingsFormulas[key].ageModifier(age); - const changeLimits = ratingsFormulas[key].changeLimits(age); - + const ageModifier = + ratingsFormulas[key][0] * age_bounds + ratingsFormulas[key][1]; + const ageStd = + ratingsFormulas[key][2] * age_bounds + ratingsFormulas[key][3]; + + const ageChange = + ageModifier + + helpers.bound(random.realGauss(0, Math.max(0.00001, ageStd)), -0.4, 0.5); ratings[key] = limitRating( - ratings[key] + - helpers.bound( - (baseChange + ageModifier) * random.uniform(0.4, 1.4), - changeLimits[0], - changeLimits[1], - ), + (Math.sqrt(Math.max(1, ratings[key])) + baseChange + ageChange) ** 2, ); + //console.log(baseChange,ageChange); } }; diff --git a/src/worker/core/player/genRatings.basketball.ts b/src/worker/core/player/genRatings.basketball.ts index e6fa9ac499..908a232761 100644 --- a/src/worker/core/player/genRatings.basketball.ts +++ b/src/worker/core/player/genRatings.basketball.ts @@ -2,46 +2,7 @@ import genFuzz from "./genFuzz"; import heightToRating from "./heightToRating"; import limitRating from "./limitRating"; import { helpers, random } from "../../util"; -import type { - PlayerRatings, - RatingKey, -} from "../../../common/types.basketball"; - -const typeFactors: Record< - "point" | "wing" | "big", - Partial> -> = { - point: { - jmp: 1.65, - spd: 1.65, - drb: 1.5, - pss: 1.5, - ft: 1.4, - fg: 1.4, - tp: 1.4, - oiq: 1.2, - endu: 1.4, - }, - wing: { - drb: 1.2, - dnk: 1.5, - jmp: 1.4, - spd: 1.4, - ft: 1.2, - fg: 1.2, - tp: 1.2, - }, - big: { - stre: 1.2, - ins: 1.6, - dnk: 1.5, - reb: 1.4, - ft: 0.8, - fg: 0.8, - tp: 0.8, - diq: 1.2, - }, -}; +import type { PlayerRatings } from "../../../common/types.basketball"; /** * Generate initial ratings for a newly-created player. @@ -66,87 +27,104 @@ const genRatings = ( const hgt = heightToRating(wingspanAdjust); heightInInches = Math.round(heightInInches); // Pick type of player (point, wing, or big) based on height - const randType = Math.random(); - let type: keyof typeof typeFactors; + const pca_comp = [ + [ + 0.01144491, 0.18678923, -0.30245945, -0.11454368, -0.30940279, + -0.31179763, 0.39192477, 0.17167505, -0.33114137, -0.06918906, + -0.31791678, 0.18375956, -0.29565832, 0.12444836, -0.36355192, + ], + [ + 0.26091959, 0.32038081, 0.22341533, 0.27030209, 0.08278275, 0.06169217, + 0.00644845, 0.5170028, 0.07126779, 0.26298184, 0.29825086, 0.32866972, + 0.06902588, 0.37508656, -0.1078734, + ], + [ + -0.1527201, 0.38744556, -0.26238251, 0.04986639, 0.28690744, 0.25874772, + 0.33139227, 0.01208661, 0.25434695, 0.0442386, -0.46999709, -0.15012807, + 0.22910478, 0.23225618, 0.2819238, + ], + ]; - if (hgt >= 59) { - // 6'10" or taller - if (randType < 0.01) { - type = "point"; - } else if (randType < 0.05) { - type = "wing"; - } else { - type = "big"; - } - } else if (hgt <= 33) { - // 6'3" or shorter - if (randType < 0.1) { - type = "wing"; - } else { - type = "point"; - } - } else { - // eslint-disable-next-line no-lonely-if - if (randType < 0.03) { - type = "point"; - } else if (randType < 0.3) { - type = "big"; - } else { - type = "wing"; - } - } + const pca1 = 1.72 * hgt - 83.45 + random.realGauss(0, 0.015) - 0.8; + const pca2 = 0.01 * hgt - 0.48 + random.realGauss(0, 22.3) - 11.1; + const pca3 = 0.34 * hgt - 16.39 + random.realGauss(0, 0.19) + 1.8; - // Tall players are less talented, and all tend towards dumb and can't shoot because they are rookies const rawRatings = { - stre: 37, - spd: 40, - jmp: 40, - endu: 17, - ins: 27, - dnk: 27, - ft: 32, - fg: 32, - tp: 32, - oiq: 22, - diq: 22, - drb: 37, - pss: 37, - reb: 37, + diq: + 42.1 + + pca1 * pca_comp[0][0] + + pca2 * pca_comp[1][0] + + pca3 * pca_comp[2][0], + dnk: + 46.5 + + pca1 * pca_comp[0][1] + + pca2 * pca_comp[1][1] + + pca3 * pca_comp[2][1], + drb: + 50.5 + + pca1 * pca_comp[0][2] + + pca2 * pca_comp[1][2] + + pca3 * pca_comp[2][2], + endu: + 32.6 + + pca1 * pca_comp[0][3] + + pca2 * pca_comp[1][3] + + pca3 * pca_comp[2][3], + fg: + 42.5 + + pca1 * pca_comp[0][4] + + pca2 * pca_comp[1][4] + + pca3 * pca_comp[2][4], + ft: + 42.6 + + pca1 * pca_comp[0][5] + + pca2 * pca_comp[1][5] + + pca3 * pca_comp[2][5], + hgt: hgt, + ins: + 41.1 + + pca1 * pca_comp[0][7] + + pca2 * pca_comp[1][7] + + pca3 * pca_comp[2][7], + jmp: + 51.0 + + pca1 * pca_comp[0][8] + + pca2 * pca_comp[1][8] + + pca3 * pca_comp[2][8], + oiq: + 40.2 + + pca1 * pca_comp[0][9] + + pca2 * pca_comp[1][9] + + pca3 * pca_comp[2][9], + pss: + 47.3 + + pca1 * pca_comp[0][10] + + pca2 * pca_comp[1][10] + + pca3 * pca_comp[2][10], + reb: + 48.7 + + pca1 * pca_comp[0][11] + + pca2 * pca_comp[1][11] + + pca3 * pca_comp[2][11], + spd: + 51.4 + + pca1 * pca_comp[0][12] + + pca2 * pca_comp[1][12] + + pca3 * pca_comp[2][12], + stre: + 47.6 + + pca1 * pca_comp[0][13] + + pca2 * pca_comp[1][13] + + pca3 * pca_comp[2][13], + tp: + 44.9 + + pca1 * pca_comp[0][14] + + pca2 * pca_comp[1][14] + + pca3 * pca_comp[2][14], }; - // For correlation across ratings, to ensure some awesome players, but athleticism and skill are independent to - // ensure there are some who are elite in one but not the other - const factorAthleticism = helpers.bound(random.realGauss(1, 0.2), 0.2, 1.2); - const factorShooting = helpers.bound(random.realGauss(1, 0.2), 0.2, 1.2); - const factorSkill = helpers.bound(random.realGauss(1, 0.2), 0.2, 1.2); - const factorIns = helpers.bound(random.realGauss(1, 0.2), 0.2, 1.2); - const athleticismRatings = ["stre", "spd", "jmp", "endu", "dnk"]; - const shootingRatings = ["ft", "fg", "tp"]; - const skillRatings = ["oiq", "diq", "drb", "pss", "reb"]; // ins purposely left out - for (const key of helpers.keys(rawRatings)) { - const typeFactor = typeFactors[type].hasOwnProperty(key) - ? typeFactors[type][key] - : 1; - let factor = factorIns; - - if (athleticismRatings.includes(key)) { - factor = factorAthleticism; - } else if (shootingRatings.includes(key)) { - factor = factorShooting; - } else if (skillRatings.includes(key)) { - factor = factorSkill; - } - - // For TypeScript - // https://github.com/microsoft/TypeScript/issues/21732 - if (typeFactor === undefined) { - throw new Error("Should never happen"); - } - - rawRatings[key] = limitRating( - factor * typeFactor * random.realGauss(rawRatings[key], 3), - ); + rawRatings[key] = limitRating(rawRatings[key] * random.uniform(0.81, 1.22)); } const ratings = { diff --git a/src/worker/core/player/ovr.basketball.ts b/src/worker/core/player/ovr.basketball.ts index 4af809df14..e39dd8125d 100644 --- a/src/worker/core/player/ovr.basketball.ts +++ b/src/worker/core/player/ovr.basketball.ts @@ -10,22 +10,22 @@ import type { PlayerRatings } from "../../../common/types.basketball"; const ovr = (ratings: PlayerRatings): number => { // See analysis/player-ovr-basketball const r = - 0.159 * (ratings.hgt - 47.5) + - 0.0777 * (ratings.stre - 50.2) + - 0.123 * (ratings.spd - 50.8) + - 0.051 * (ratings.jmp - 48.7) + - 0.0632 * (ratings.endu - 39.9) + - 0.0126 * (ratings.ins - 42.4) + - 0.0286 * (ratings.dnk - 49.5) + - 0.0202 * (ratings.ft - 47.0) + - 0.0726 * (ratings.tp - 47.1) + - 0.133 * (ratings.oiq - 46.8) + - 0.159 * (ratings.diq - 46.7) + - 0.059 * (ratings.drb - 54.8) + - 0.062 * (ratings.pss - 51.3) + - 0.01 * (ratings.fg - 47.0) + - 0.01 * (ratings.reb - 51.4) + - 48.5; + 0.0935 * ratings.diq + + 0.042 * ratings.dnk + + 0.0969 * ratings.drb + + 0.00725 * ratings.endu + + -0.00948 * ratings.fg + + 0.0488 * ratings.ft + + 0.225 * ratings.hgt + + -0.0143 * ratings.ins + + 0.0502 * ratings.jmp + + 0.0974 * ratings.oiq + + 0.0656 * ratings.pss + + 0.0533 * ratings.reb + + 0.156 * ratings.spd + + 0.0962 * ratings.stre + + 0.105 * ratings.tp + + -6.4; // Fudge factor to keep ovr ratings the same as they used to be (back before 2018 ratings rescaling) // +8 at 68