mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-10-01 15:22:35 -05:00
Teambuilder: Add EV optimization checker (#2240)
--------- Co-authored-by: pyuk-bot <msaimrkon@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ module.exports = {
|
||||
"BattleFormats": false, "BattleFormatsData": false, "BattleLearnsets": false, "BattleItems": false, "BattleMoveAnims": false, "BattleMovedex": false, "BattleNatures": false,
|
||||
"BattleOtherAnims": false, "BattlePokedex": false,"BattlePokemonSprites": false, "BattlePokemonSpritesBW": false, "BattleSearchCountIndex": false, "BattleSearchIndex": false, "BattleArticleTitles": false,
|
||||
"BattleSearchIndexOffset": false, "BattleSearchIndexType": false, "BattleStatIDs": false, "BattleStatNames": false, "BattleStatusAnims": false, "BattleStatuses": false, "BattleTeambuilderTable": false,
|
||||
"ModifiableValue": false, "BattleStatGuesser": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false,
|
||||
"ModifiableValue": false, "BattleStatGuesser": false, "BattleStatOptimizer": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false,
|
||||
"BattleTextParser": false,
|
||||
|
||||
// Generic global variables
|
||||
|
||||
@@ -2040,6 +2040,7 @@
|
||||
}
|
||||
}
|
||||
this.$chart.find('select[name=nature]').val(set.nature || 'Serious');
|
||||
this.checkStatOptimizations();
|
||||
},
|
||||
curChartType: '',
|
||||
curChartName: '',
|
||||
@@ -2465,15 +2466,58 @@
|
||||
}
|
||||
buf += '</select></p>';
|
||||
|
||||
buf += '<p><em>Protip:</em> You can also set natures by typing <kbd>+</kbd> and <kbd>-</kbd> next to a stat.</p>';
|
||||
buf += '<p><small><em>Protip:</em> You can also set natures by typing <kbd>+</kbd> and <kbd>-</kbd> next to a stat.</small></p>';
|
||||
|
||||
buf += '<p id="statoptimizer"></p>';
|
||||
}
|
||||
|
||||
buf += '</div>';
|
||||
this.$chart.html(buf);
|
||||
this.checkStatOptimizations();
|
||||
},
|
||||
setStatFormGuesses: function () {
|
||||
this.updateStatForm(true);
|
||||
},
|
||||
checkStatOptimizations: function () {
|
||||
var optimized = BattleStatOptimizer(this.curSet, this.curTeam.format);
|
||||
|
||||
if (optimized) {
|
||||
var buf = '';
|
||||
var msg = '';
|
||||
if (optimized.savedEVs) {
|
||||
msg = 'save ' + optimized.savedEVs + ' EVs';
|
||||
} else {
|
||||
msg = 'get higher stats';
|
||||
}
|
||||
buf += '<small><em>Protip:</em> Use a different nature to ' + msg + ': </small>';
|
||||
buf += ' <button name="setStatFormOptimization" class="button">';
|
||||
for (var i in BattleStatNames) {
|
||||
if (optimized.evs[i]) {
|
||||
buf += '' + optimized.evs[i] + ' ' + BattleStatNames[i] + ' / ';
|
||||
}
|
||||
}
|
||||
if (!optimized.plus && !optimized.minus) {
|
||||
buf += ' (Neutral nature)';
|
||||
} else {
|
||||
buf += ' (+' + BattleStatNames[optimized.plus] + ', -' + BattleStatNames[optimized.minus] + ')';
|
||||
}
|
||||
buf += '</button>';
|
||||
this.$chart.find('#statoptimizer').html(buf).show();
|
||||
} else {
|
||||
this.$chart.find('#statoptimizer').hide();
|
||||
}
|
||||
},
|
||||
setStatFormOptimization: function () {
|
||||
var optimized = BattleStatOptimizer(this.curSet, this.curTeam.format);
|
||||
this.curSet.evs = optimized.evs;
|
||||
this.plus = optimized.plus;
|
||||
this.minus = optimized.minus;
|
||||
this.updateNature();
|
||||
this.save();
|
||||
this.updateStatGraph();
|
||||
this.natureChange();
|
||||
this.$chart.find('#statoptimizer').hide();
|
||||
},
|
||||
setSlider: function (stat, val) {
|
||||
this.$chart.find('input[name=evslider-' + stat + ']').val(val || 0);
|
||||
},
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
*/
|
||||
type ID = string & {__isID: true};
|
||||
|
||||
const BattleNatures: {[k in NatureName]: {plus?: StatName, minus?: StatName}} = {
|
||||
interface Nature {
|
||||
plus?: StatNameExceptHP;
|
||||
minus?: StatNameExceptHP;
|
||||
}
|
||||
|
||||
const BattleNatures: {[k in NatureName]: Nature} = {
|
||||
Adamant: {
|
||||
plus: 'atk',
|
||||
minus: 'spa',
|
||||
|
||||
@@ -2281,7 +2281,7 @@ interface PokemonSet {
|
||||
/** Defaults to random legal gender, NOT subject to gender ratios */
|
||||
gender?: string;
|
||||
/** Defaults to flat 252's (200's/0's in Let's Go) (error in gen 3+) */
|
||||
evs?: StatsTable;
|
||||
evs?: Partial<StatsTable>;
|
||||
/** Defaults to whatever makes sense - flat 31's unless you have Gyro Ball etc */
|
||||
ivs?: StatsTable;
|
||||
/** Defaults as you'd expect (100 normally, 50 in VGC-likes, 5 in LC) */
|
||||
@@ -2904,7 +2904,155 @@ class BattleStatGuesser {
|
||||
}
|
||||
}
|
||||
|
||||
function BattleStatOptimizer(set: PokemonSet, formatid: ID) {
|
||||
if (!set.evs) return null;
|
||||
|
||||
const dex = Dex.mod(formatid.slice(0, 4) as ID);
|
||||
const ignoreEVLimits = (
|
||||
dex.gen < 3 ||
|
||||
((formatid.endsWith('hackmons') || formatid.endsWith('bh')) && dex.gen !== 6) ||
|
||||
formatid.includes('metronomebattle') || formatid.endsWith('norestrictions')
|
||||
);
|
||||
const supportsEVs = !formatid.includes('letsgo');
|
||||
if (!supportsEVs || ignoreEVLimits) return false;
|
||||
|
||||
const species = dex.species.get(set.species);
|
||||
const level = set.level || 100;
|
||||
const getStat = (stat: StatNameExceptHP, ev: number, nature: Nature) => {
|
||||
const baseStat = species.baseStats[stat];
|
||||
const iv = set.ivs?.[stat] || 31;
|
||||
let val = ~~(~~(2 * baseStat + iv + ~~(ev / 4)) * level / 100 + 5);
|
||||
if (nature.plus === stat) {
|
||||
val *= 1.1;
|
||||
} else if (nature.minus === stat) {
|
||||
val *= 0.9;
|
||||
}
|
||||
return ~~(val);
|
||||
};
|
||||
|
||||
const origNature = BattleNatures[set.nature || 'Serious'];
|
||||
const origStats = {
|
||||
// no need to calculate hp
|
||||
atk: getStat('atk', set.evs.atk || 0, origNature),
|
||||
def: getStat('def', set.evs.def || 0, origNature),
|
||||
spa: getStat('spa', set.evs.spa || 0, origNature),
|
||||
spd: getStat('spd', set.evs.spd || 0, origNature),
|
||||
spe: getStat('spe', set.evs.spe || 0, origNature),
|
||||
};
|
||||
const getMinEVs = (stat: StatNameExceptHP, nature: Nature) => {
|
||||
let ev = 0;
|
||||
while (getStat(stat, ev, nature) < origStats[stat]) {
|
||||
ev += 4;
|
||||
}
|
||||
return ev;
|
||||
};
|
||||
|
||||
const origSpread = {evs: set.evs, ...origNature};
|
||||
let origLeftoverEVs = 508;
|
||||
for (const stat of Dex.statNames) {
|
||||
origLeftoverEVs -= origSpread.evs?.[stat] || 0;
|
||||
}
|
||||
// Only check for optimizations if EVs are completed
|
||||
if (origLeftoverEVs > 4) return null;
|
||||
|
||||
// Can't move the plus if it boosts its stat past normal EV limit
|
||||
const plusTooHigh = origNature.plus && getStat(origNature.plus, 252, {}) < origStats[origNature.plus];
|
||||
// Can't move the minus if there's no investment in its stat to redistribute
|
||||
const minusTooLow = origNature.minus && !origSpread.evs?.[origNature.minus];
|
||||
// If we can't move either of them, do nothing
|
||||
if (plusTooHigh && minusTooLow) return null;
|
||||
|
||||
let bestPlus = origNature.plus;
|
||||
let bestPlusMinEVs = bestPlus && origSpread.evs[bestPlus];
|
||||
let bestMinus = origNature.minus || 'atk';
|
||||
let bestMinusMinEVs = origSpread.evs[bestMinus];
|
||||
let savedEVs = 0;
|
||||
|
||||
// Try and move the minus first, as figuring out where the plus should go is harder if the minus hasn't been placed
|
||||
if (!minusTooLow) {
|
||||
for (const stat of Dex.statNamesExceptHP) {
|
||||
if (origStats[stat] < origStats[bestMinus]) {
|
||||
const minEVs = getMinEVs(stat, {minus: stat});
|
||||
if (minEVs > 252) continue;
|
||||
// This number can go negative at this point, but we'll make up for it later (and check to make sure)
|
||||
savedEVs = (origSpread.evs[stat] || 0) - minEVs;
|
||||
if (origNature.minus) {
|
||||
savedEVs += (origSpread.evs[origNature.minus] || 0) - getMinEVs(origNature.minus, {minus: stat});
|
||||
}
|
||||
bestMinus = stat;
|
||||
bestMinusMinEVs = minEVs;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!plusTooHigh) {
|
||||
for (const stat of Dex.statNamesExceptHP) {
|
||||
// Don't move the plus to an uninvested stat
|
||||
if (stat !== origNature.plus && origSpread.evs[stat] && stat !== bestMinus) {
|
||||
const minEVs = getMinEVs(stat, {plus: stat});
|
||||
let plusEVsSaved = (origNature.minus === stat ? getMinEVs(stat, {}) : origSpread.evs[stat] || 0) - minEVs;
|
||||
if (bestPlus && bestPlus !== bestMinus) {
|
||||
plusEVsSaved += bestPlusMinEVs! - getMinEVs(bestPlus, {plus: stat, minus: bestMinus});
|
||||
}
|
||||
if (plusEVsSaved > 0 && savedEVs + plusEVsSaved > 0) {
|
||||
savedEVs += plusEVsSaved;
|
||||
bestPlus = stat;
|
||||
bestPlusMinEVs = minEVs;
|
||||
} else if (plusEVsSaved === 0 && (bestPlus || savedEVs > 0) || plusEVsSaved > 0 && savedEVs + plusEVsSaved === 0) {
|
||||
if (!bestPlus || getStat(stat, getMinEVs(stat, {plus: stat}), {plus: stat}) > origStats[stat]) {
|
||||
savedEVs += plusEVsSaved;
|
||||
bestPlus = stat;
|
||||
bestPlusMinEVs = minEVs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPlus && savedEVs >= 0) {
|
||||
const newSpread: {
|
||||
evs: Partial<StatsTable>,
|
||||
plus?: StatNameExceptHP,
|
||||
minus?: StatNameExceptHP,
|
||||
} = {evs: {...origSpread.evs}, plus: bestPlus, minus: bestMinus};
|
||||
if (bestPlus !== origNature.plus || bestMinus !== origNature.minus) {
|
||||
if (bestPlusMinEVs) newSpread.evs[bestPlus] = bestPlusMinEVs;
|
||||
if (bestMinusMinEVs) newSpread.evs[bestMinus] = bestMinusMinEVs;
|
||||
if (origNature.plus && origNature.plus !== bestPlus && origNature.plus !== bestMinus) {
|
||||
const oldPlusEVs = getMinEVs(origNature.plus, newSpread);
|
||||
if (oldPlusEVs) newSpread.evs[origNature.plus] = oldPlusEVs;
|
||||
}
|
||||
if (origNature.minus && origNature.minus !== bestPlus && origNature.minus !== bestMinus) {
|
||||
const oldMinusEVS = getMinEVs(origNature.minus, newSpread);
|
||||
if (oldMinusEVS) newSpread.evs[origNature.minus] = oldMinusEVS;
|
||||
}
|
||||
for (const stat of Dex.statNames) {
|
||||
if (!newSpread.evs[stat]) delete newSpread.evs[stat];
|
||||
}
|
||||
return {...newSpread, savedEVs};
|
||||
} else if (!plusTooHigh && !minusTooLow) {
|
||||
if (Math.floor(getStat(bestPlus, bestMinusMinEVs!, newSpread) / 11) <= Math.ceil(origStats[bestMinus] / 9)) {
|
||||
// We're not gaining more points from our plus than we're losing to our minus
|
||||
// So a neutral nature would be better
|
||||
delete newSpread.plus;
|
||||
delete newSpread.minus;
|
||||
newSpread.evs[origNature.plus] = getMinEVs(origNature.plus, newSpread);
|
||||
newSpread.evs[origNature.minus] = getMinEVs(origNature.minus, newSpread);
|
||||
savedEVs += (origSpread.evs[origNature.plus] || 0) - newSpread.evs[origNature.plus]!;
|
||||
savedEVs += (origSpread.evs[origNature.minus] || 0) - newSpread.evs[origNature.minus]!;
|
||||
if (savedEVs < 0) return null;
|
||||
for (const stat of Dex.statNames) {
|
||||
if (!newSpread.evs[stat]) delete newSpread.evs[stat];
|
||||
}
|
||||
return {...newSpread, savedEVs};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof require === 'function') {
|
||||
// in Node
|
||||
(global as any).BattleStatGuesser = BattleStatGuesser;
|
||||
(global as any).BattleStatOptimizer = BattleStatOptimizer;
|
||||
}
|
||||
|
||||
105
test/ev-optimizer.test.js
Normal file
105
test/ev-optimizer.test.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const assert = require('assert').strict;
|
||||
|
||||
try {
|
||||
global.BattlePokedex = require('../play.pokemonshowdown.com/data/pokedex.js').BattlePokedex;
|
||||
} catch (err) {}
|
||||
require('../play.pokemonshowdown.com/js/battle-dex-data.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-dex.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-tooltips.js');
|
||||
|
||||
describe('EV Optimizer', () => {
|
||||
(global.BattlePokedex ? it : it.skip)('should find the spreads that saves the most EVs', () => {
|
||||
const trapinch = BattleStatOptimizer({
|
||||
species: "Trapinch",
|
||||
nature: "Lax",
|
||||
evs: {hp: 204, atk: 252, def: 52},
|
||||
level: 100
|
||||
}, 'gen9');
|
||||
assert.deepStrictEqual(trapinch, {
|
||||
evs: {hp: 204, atk: 144, def: 104},
|
||||
plus: 'atk',
|
||||
minus: 'spd',
|
||||
savedEVs: 56,
|
||||
});
|
||||
|
||||
const groudon = BattleStatOptimizer({
|
||||
species: "Groudon-Primal",
|
||||
nature: "Serious",
|
||||
evs: {atk: 252, spa: 156, spe: 100},
|
||||
level: 100
|
||||
}, 'gen7');
|
||||
assert.deepStrictEqual(groudon, {
|
||||
evs: {atk: 88, spa: 156, spd: 96, spe: 100},
|
||||
plus: 'atk',
|
||||
minus: 'spd',
|
||||
savedEVs: 68,
|
||||
});
|
||||
|
||||
const thundurus = BattleStatOptimizer({
|
||||
species: "Thundurus",
|
||||
nature: "Timid",
|
||||
evs: {hp: 252, spa: 232, spe: 24},
|
||||
ivs: {hp: 31, atk: 2, def: 31, spa: 30, spd: 31, spe: 30},
|
||||
level: 50
|
||||
}, 'gen5');
|
||||
assert.deepStrictEqual(thundurus, {
|
||||
evs: {hp: 252, spa: 112, spe: 128},
|
||||
plus: 'spa',
|
||||
minus: 'atk',
|
||||
savedEVs: 16,
|
||||
});
|
||||
|
||||
const amoonguss = BattleStatOptimizer({
|
||||
species: "Amoonguss",
|
||||
nature: "Bold",
|
||||
evs: {hp: 252, def: 100, spd: 156},
|
||||
level: 50
|
||||
}, 'gen9');
|
||||
assert.deepStrictEqual(amoonguss, {
|
||||
evs: {hp: 252, def: 180, spd: 76},
|
||||
plus: 'spd',
|
||||
minus: 'atk',
|
||||
savedEVs: 0,
|
||||
});
|
||||
|
||||
const avalugg = BattleStatOptimizer({
|
||||
species: "Avalugg",
|
||||
nature: "Hasty",
|
||||
evs: {hp: 56, atk: 200, def: 252},
|
||||
level: 100
|
||||
}, 'gen9');
|
||||
assert.deepStrictEqual(avalugg, {
|
||||
evs: {hp: 56, atk: 84, def: 64, spe: 84},
|
||||
plus: 'atk',
|
||||
minus: 'spe',
|
||||
savedEVs: 220,
|
||||
});
|
||||
|
||||
const mew = BattleStatOptimizer({
|
||||
species: "Mew",
|
||||
nature: "Sassy",
|
||||
evs: {hp: 24, atk: 92, def: 92, spa: 92, spd: 104, spe: 104},
|
||||
level: 100
|
||||
}, 'gen9');
|
||||
assert.deepStrictEqual(mew, {
|
||||
evs: {hp: 24, atk: 92, def: 92, spa: 92, spd: 208},
|
||||
savedEVs: 0,
|
||||
});
|
||||
|
||||
const mew2 = BattleStatOptimizer({
|
||||
species: "Mew",
|
||||
nature: "Sassy",
|
||||
evs: {hp: 24, atk: 92, def: 92, spa: 92, spd: 144, spe: 64},
|
||||
level: 100
|
||||
}, 'gen9');
|
||||
assert.equal(mew2, null);
|
||||
|
||||
const mienfoo = BattleStatOptimizer({
|
||||
species: "Mienfoo",
|
||||
nature: "Jolly",
|
||||
evs: {atk: 236, def: 116, spe: 156},
|
||||
level: 5
|
||||
}, 'gen9');
|
||||
assert.equal(mienfoo, null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user