@pkmn/randoms 0.5.1 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/gen8.js CHANGED
@@ -49,6 +49,10 @@ const NoStab = [
49
49
  const Hazards = [
50
50
  'spikes', 'stealthrock', 'stickyweb', 'toxicspikes',
51
51
  ];
52
+ function sereneGraceBenefits(move) {
53
+ var _a;
54
+ return ((_a = move.secondary) === null || _a === void 0 ? void 0 : _a.chance) && move.secondary.chance >= 20 && move.secondary.chance < 100;
55
+ }
52
56
  class RandomTeams {
53
57
  constructor(dex, format, prng) {
54
58
  this.randomCAP1v1Sets = {};
@@ -206,6 +210,8 @@ class RandomTeams {
206
210
  */
207
211
  sampleNoReplace(list) {
208
212
  const length = list.length;
213
+ if (length === 0)
214
+ return null;
209
215
  const index = this.random(length);
210
216
  return this.fastPop(list, index);
211
217
  }
@@ -221,6 +227,49 @@ class RandomTeams {
221
227
  }
222
228
  return samples;
223
229
  }
230
+ /**
231
+ * Check if user has directly tried to ban/unban/restrict things in a custom battle.
232
+ * Doesn't count bans nested inside other formats/rules.
233
+ */
234
+ hasDirectCustomBanlistChanges() {
235
+ if (!this.format.customRules)
236
+ return false;
237
+ for (const rule of this.format.customRules) {
238
+ for (const banlistOperator of ['-', '+', '*']) {
239
+ if (rule.startsWith(banlistOperator))
240
+ return true;
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+ /**
246
+ * Inform user when custom bans are unsupported in a team generator.
247
+ */
248
+ enforceNoDirectCustomBanlistChanges() {
249
+ if (this.hasDirectCustomBanlistChanges()) {
250
+ throw new Error(`Custom bans are not currently supported in ${this.format.name}.`);
251
+ }
252
+ }
253
+ /**
254
+ * Inform user when complex bans are unsupported in a team generator.
255
+ */
256
+ enforceNoDirectComplexBans() {
257
+ if (!this.format.customRules)
258
+ return false;
259
+ for (const rule of this.format.customRules) {
260
+ if (rule.includes('+') && !rule.startsWith('+')) {
261
+ throw new Error(`Complex bans are not currently supported in ${this.format.name}.`);
262
+ }
263
+ }
264
+ }
265
+ /**
266
+ * Validate set element pool size is sufficient to support size requirements after simple bans.
267
+ */
268
+ enforceCustomPoolSizeNoComplexBans(effectTypeName, basicEffectPool, requiredCount, requiredCountExplanation) {
269
+ if (basicEffectPool.length >= requiredCount)
270
+ return;
271
+ throw new Error(`Legal ${effectTypeName} count is insufficient to support ${requiredCountExplanation} (${basicEffectPool.length} / ${requiredCount}).`);
272
+ }
224
273
  unrejectableMovesInSingles(move) {
225
274
  // These moves cannot be rejected in favor of a forced move in singles
226
275
  return (move.category !== 'Status' || !move.flags.heal) && ![
@@ -233,6 +282,7 @@ class RandomTeams {
233
282
  return move.id !== 'bodypress';
234
283
  }
235
284
  randomCCTeam() {
285
+ this.enforceNoDirectCustomBanlistChanges();
236
286
  const dex = this.dex;
237
287
  const team = [];
238
288
  const natures = this.dex.natures.all();
@@ -370,7 +420,7 @@ class RandomTeams {
370
420
  }
371
421
  return team;
372
422
  }
373
- randomNPokemon(n, requiredType, minSourceGen) {
423
+ randomNPokemon(n, requiredType, minSourceGen, ruleTable) {
374
424
  // Picks `n` random pokemon--no repeats, even among formes
375
425
  // Also need to either normalize for formes or select formes at random
376
426
  // Unreleased are okay but no CAP
@@ -380,20 +430,77 @@ class RandomTeams {
380
430
  if (requiredType && !this.dex.types.get(requiredType).exists) {
381
431
  throw new Error(`"${requiredType}" is not a valid type.`);
382
432
  }
433
+ const isNotCustom = !ruleTable;
383
434
  const pool = [];
384
- for (const species of this.dex.species.all()) {
385
- if (species.isNonstandard && species.isNonstandard !== 'Unobtainable')
386
- continue;
387
- if (requiredType && !species.types.includes(requiredType))
388
- continue;
389
- if (minSourceGen && species.gen < minSourceGen)
390
- continue;
391
- const num = species.num;
392
- if (num <= 0 || pool.includes(num))
393
- continue;
394
- if (num > last)
395
- break;
396
- pool.push(num);
435
+ let speciesPool = [];
436
+ if (isNotCustom) {
437
+ speciesPool = [...this.dex.species.all()];
438
+ for (const species of speciesPool) {
439
+ if (species.isNonstandard && species.isNonstandard !== 'Unobtainable')
440
+ continue;
441
+ if (requiredType && !species.types.includes(requiredType))
442
+ continue;
443
+ if (minSourceGen && species.gen < minSourceGen)
444
+ continue;
445
+ const num = species.num;
446
+ if (num <= 0 || pool.includes(num))
447
+ continue;
448
+ if (num > last)
449
+ break;
450
+ pool.push(num);
451
+ }
452
+ }
453
+ else {
454
+ const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent'];
455
+ const nonexistentBanReason = ruleTable.check('nonexistent');
456
+ // Assume tierSpecies does not differ from species here (mega formes can be used without their stone, etc)
457
+ for (const species of this.dex.species.all()) {
458
+ if (requiredType && !species.types.includes(requiredType))
459
+ continue;
460
+ let banReason = ruleTable.check('pokemon:' + species.id);
461
+ if (banReason)
462
+ continue;
463
+ if (banReason !== '') {
464
+ if (species.isMega && ruleTable.check('pokemontag:mega'))
465
+ continue;
466
+ banReason = ruleTable.check('basepokemon:' + (0, sim_1.toID)(species.baseSpecies));
467
+ if (banReason)
468
+ continue;
469
+ if (banReason !== '' || this.dex.species.get(species.baseSpecies).isNonstandard === species.isNonstandard) {
470
+ const nonexistentCheck = sim_1.Tags.nonexistent.genericFilter(species) && nonexistentBanReason;
471
+ let tagWhitelisted = false;
472
+ let tagBlacklisted = false;
473
+ for (const ruleid of ruleTable.tagRules) {
474
+ if (ruleid.startsWith('*'))
475
+ continue;
476
+ const tagid = ruleid.slice(12);
477
+ const tag = sim_1.Tags[tagid];
478
+ if ((tag.speciesFilter || tag.genericFilter)(species)) {
479
+ const existenceTag = EXISTENCE_TAG.includes(tagid);
480
+ if (ruleid.startsWith('+')) {
481
+ if (!existenceTag && nonexistentCheck)
482
+ continue;
483
+ tagWhitelisted = true;
484
+ break;
485
+ }
486
+ tagBlacklisted = true;
487
+ break;
488
+ }
489
+ }
490
+ if (tagBlacklisted)
491
+ continue;
492
+ if (!tagWhitelisted) {
493
+ if (ruleTable.check('pokemontag:allpokemon'))
494
+ continue;
495
+ }
496
+ }
497
+ }
498
+ const num = species.num;
499
+ if (pool.includes(num))
500
+ continue;
501
+ pool.push(num);
502
+ speciesPool.push(species);
503
+ }
397
504
  }
398
505
  const hasDexNumber = {};
399
506
  for (let i = 0; i < n; i++) {
@@ -401,14 +508,18 @@ class RandomTeams {
401
508
  hasDexNumber[num] = i;
402
509
  }
403
510
  const formes = [];
404
- for (const species of this.dex.species.all()) {
511
+ for (const species of speciesPool) {
405
512
  if (!(species.num in hasDexNumber))
406
513
  continue;
407
- if (species.gen <= this.gen && (!species.isNonstandard || species.isNonstandard === 'Unobtainable')) {
408
- if (!formes[hasDexNumber[species.num]])
409
- formes[hasDexNumber[species.num]] = [];
410
- formes[hasDexNumber[species.num]].push(species.name);
411
- }
514
+ if (isNotCustom && (species.gen > this.gen ||
515
+ (species.isNonstandard && species.isNonstandard !== 'Unobtainable')))
516
+ continue;
517
+ if (!formes[hasDexNumber[species.num]])
518
+ formes[hasDexNumber[species.num]] = [];
519
+ formes[hasDexNumber[species.num]].push(species.name);
520
+ }
521
+ if (formes.length < n) {
522
+ throw new Error(`Legal Pokemon forme count insufficient to support Max Team Size: (${formes.length} / ${n}).`);
412
523
  }
413
524
  const nPokemon = [];
414
525
  for (let i = 0; i < n; i++) {
@@ -420,41 +531,177 @@ class RandomTeams {
420
531
  return nPokemon;
421
532
  }
422
533
  randomHCTeam() {
534
+ const hasCustomBans = this.hasDirectCustomBanlistChanges();
535
+ const ruleTable = this.dex.formats.getRuleTable(this.format);
536
+ const hasNonexistentBan = hasCustomBans && ruleTable.check('nonexistent');
537
+ const hasNonexistentWhitelist = hasCustomBans && (hasNonexistentBan === '');
538
+ if (hasCustomBans) {
539
+ this.enforceNoDirectComplexBans();
540
+ }
541
+ // Item Pool
542
+ const doItemsExist = this.gen > 1;
543
+ let itemPool = [];
544
+ if (doItemsExist) {
545
+ if (!hasCustomBans) {
546
+ itemPool = [...this.dex.items.all()].filter(item => (item.gen <= this.gen && !item.isNonstandard));
547
+ }
548
+ else {
549
+ const hasAllItemsBan = ruleTable.check('pokemontag:allitems');
550
+ for (const item of this.dex.items.all()) {
551
+ let banReason = ruleTable.check('item:' + item.id);
552
+ if (banReason)
553
+ continue;
554
+ if (banReason !== '' && item.id) {
555
+ if (hasAllItemsBan)
556
+ continue;
557
+ if (item.isNonstandard) {
558
+ banReason = ruleTable.check('pokemontag:' + (0, sim_1.toID)(item.isNonstandard));
559
+ if (banReason)
560
+ continue;
561
+ if (banReason !== '' && item.isNonstandard !== 'Unobtainable') {
562
+ if (hasNonexistentBan)
563
+ continue;
564
+ if (!hasNonexistentWhitelist)
565
+ continue;
566
+ }
567
+ }
568
+ }
569
+ itemPool.push(item);
570
+ }
571
+ if (ruleTable.check('item:noitem')) {
572
+ this.enforceCustomPoolSizeNoComplexBans('item', itemPool, this.maxTeamSize, 'Max Team Size');
573
+ }
574
+ }
575
+ }
576
+ // Ability Pool
577
+ const doAbilitiesExist = (this.gen > 2) && (this.dex.currentMod !== 'gen7letsgo');
578
+ let abilityPool = [];
579
+ if (doAbilitiesExist) {
580
+ if (!hasCustomBans) {
581
+ abilityPool = [...this.dex.abilities.all()].filter(ability => (ability.gen <= this.gen && !ability.isNonstandard));
582
+ }
583
+ else {
584
+ const hasAllAbilitiesBan = ruleTable.check('pokemontag:allabilities');
585
+ for (const ability of this.dex.abilities.all()) {
586
+ let banReason = ruleTable.check('ability:' + ability.id);
587
+ if (banReason)
588
+ continue;
589
+ if (banReason !== '') {
590
+ if (hasAllAbilitiesBan)
591
+ continue;
592
+ if (ability.isNonstandard) {
593
+ banReason = ruleTable.check('pokemontag:' + (0, sim_1.toID)(ability.isNonstandard));
594
+ if (banReason)
595
+ continue;
596
+ if (banReason !== '') {
597
+ if (hasNonexistentBan)
598
+ continue;
599
+ if (!hasNonexistentWhitelist)
600
+ continue;
601
+ }
602
+ }
603
+ }
604
+ abilityPool.push(ability);
605
+ }
606
+ if (ruleTable.check('ability:noability')) {
607
+ this.enforceCustomPoolSizeNoComplexBans('ability', abilityPool, this.maxTeamSize, 'Max Team Size');
608
+ }
609
+ }
610
+ }
611
+ // Move Pool
612
+ const setMoveCount = ruleTable.maxMoveCount;
613
+ let movePool = [];
614
+ if (!hasCustomBans) {
615
+ movePool = [...this.dex.moves.all()].filter(move => (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power ')));
616
+ }
617
+ else {
618
+ const hasAllMovesBan = ruleTable.check('pokemontag:allmoves');
619
+ for (const move of this.dex.moves.all()) {
620
+ // Legality of specific HP types can't be altered in built formats anyway
621
+ if (move.name.startsWith('Hidden Power '))
622
+ continue;
623
+ let banReason = ruleTable.check('move:' + move.id);
624
+ if (banReason)
625
+ continue;
626
+ if (banReason !== '') {
627
+ if (hasAllMovesBan)
628
+ continue;
629
+ if (move.isNonstandard) {
630
+ banReason = ruleTable.check('pokemontag:' + (0, sim_1.toID)(move.isNonstandard));
631
+ if (banReason)
632
+ continue;
633
+ if (banReason !== '' && move.isNonstandard !== 'Unobtainable') {
634
+ if (hasNonexistentBan)
635
+ continue;
636
+ if (!hasNonexistentWhitelist)
637
+ continue;
638
+ }
639
+ }
640
+ }
641
+ movePool.push(move);
642
+ }
643
+ this.enforceCustomPoolSizeNoComplexBans('move', movePool, this.maxTeamSize * setMoveCount, 'Max Team Size * Max Move Count');
644
+ }
645
+ // Nature Pool
646
+ const doNaturesExist = this.gen > 2;
647
+ let naturePool = [];
648
+ if (doNaturesExist) {
649
+ if (!hasCustomBans) {
650
+ if (!hasCustomBans) {
651
+ naturePool = [...this.dex.natures.all()];
652
+ }
653
+ else {
654
+ const hasAllNaturesBan = ruleTable.check('pokemontag:allnatures');
655
+ for (const nature of this.dex.natures.all()) {
656
+ let banReason = ruleTable.check('nature:' + nature.id);
657
+ if (banReason)
658
+ continue;
659
+ if (banReason !== '' && nature.id) {
660
+ if (hasAllNaturesBan)
661
+ continue;
662
+ if (nature.isNonstandard) {
663
+ banReason = ruleTable.check('pokemontag:' + (0, sim_1.toID)(nature.isNonstandard));
664
+ if (banReason)
665
+ continue;
666
+ if (banReason !== '' && nature.isNonstandard !== 'Unobtainable') {
667
+ if (hasNonexistentBan)
668
+ continue;
669
+ if (!hasNonexistentWhitelist)
670
+ continue;
671
+ }
672
+ }
673
+ }
674
+ naturePool.push(nature);
675
+ }
676
+ // There is no 'nature:nonature' rule so do not constrain pool size
677
+ }
678
+ }
679
+ }
680
+ const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, hasCustomBans ? ruleTable : undefined);
423
681
  const team = [];
424
- const itemPool = [...this.dex.items.all()];
425
- const abilityPool = [...this.dex.abilities.all()];
426
- const movePool = [...this.dex.moves.all()];
427
- const naturePool = this.dex.natures.all();
428
- const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype);
429
682
  for (const forme of randomN) {
430
683
  // Choose forme
431
684
  const species = this.dex.species.get(forme);
432
685
  // Random unique item
433
686
  let item = '';
434
687
  let itemData;
435
- if (this.gen >= 2) {
436
- do {
437
- itemData = this.sampleNoReplace(itemPool);
438
- item = itemData.name;
439
- } while (itemData.gen > this.gen || itemData.isNonstandard);
688
+ if (doItemsExist) {
689
+ itemData = this.sampleNoReplace(itemPool);
690
+ item = itemData === null || itemData === void 0 ? void 0 : itemData.name;
440
691
  }
441
692
  // Random unique ability
442
693
  let ability = 'No Ability';
443
694
  let abilityData;
444
- if (this.gen >= 3) {
445
- do {
446
- abilityData = this.sampleNoReplace(abilityPool);
447
- ability = abilityData.name;
448
- } while (abilityData.gen > this.gen || abilityData.isNonstandard);
695
+ if (doAbilitiesExist) {
696
+ abilityData = this.sampleNoReplace(abilityPool);
697
+ ability = abilityData === null || abilityData === void 0 ? void 0 : abilityData.name;
449
698
  }
450
699
  // Random unique moves
451
700
  const m = [];
452
701
  do {
453
702
  const move = this.sampleNoReplace(movePool);
454
- if (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power ')) {
455
- m.push(move.id);
456
- }
457
- } while (m.length < 4);
703
+ m.push(move.id);
704
+ } while (m.length < setMoveCount);
458
705
  // Random EVs
459
706
  const evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
460
707
  if (this.gen === 6) {
@@ -481,7 +728,10 @@ class RandomTeams {
481
728
  spe: this.random(32),
482
729
  };
483
730
  // Random nature
484
- const nature = this.sample(naturePool).name;
731
+ let nature = '';
732
+ if (doNaturesExist && (naturePool.length > 0)) {
733
+ nature = this.sample(naturePool).name;
734
+ }
485
735
  // Level balance
486
736
  const mbstmin = 1307;
487
737
  const stats = species.baseStats;
@@ -593,7 +843,7 @@ class RandomTeams {
593
843
  // Moves with secondary effects:
594
844
  if (move.secondary) {
595
845
  counter.add('sheerforce');
596
- if (move.secondary.chance && move.secondary.chance >= 20 && move.secondary.chance < 100) {
846
+ if (sereneGraceBenefits(move)) {
597
847
  counter.add('serenegrace');
598
848
  }
599
849
  }
@@ -990,10 +1240,6 @@ class RandomTeams {
990
1240
  (moves.has('toxic') && movePool.includes('earthpower'))) };
991
1241
  case 'airslash':
992
1242
  return { cull: (species.id === 'naganadel' && moves.has('nastyplot')) ||
993
- // I'm told that Nasty Plot Noctowl wants Air Slash (presumably for consistent damage),
994
- // and Defog Noctowl wants Hurricane—presumably for a high-risk, high-reward damaging move
995
- // after its main job of removing hazards is done.
996
- (species.id === 'noctowl' && !counter.setupType) ||
997
1243
  hasRestTalk ||
998
1244
  (abilities.has('Simple') && !!counter.get('recovery')) ||
999
1245
  counter.setupType === 'Physical',
@@ -1002,9 +1248,7 @@ class RandomTeams {
1002
1248
  // Special case for Mew, which only wants Brave Bird with Swords Dance
1003
1249
  return { cull: moves.has('dragondance') };
1004
1250
  case 'hurricane':
1005
- // Special case for Noctowl, which wants Air Slash if Nasty Plot instead
1006
- const noctowlCase = (!isNoDynamax && !isDoubles && species.id === 'noctowl' && !!counter.setupType);
1007
- return { cull: counter.setupType === 'Physical' || noctowlCase };
1251
+ return { cull: counter.setupType === 'Physical' };
1008
1252
  case 'futuresight':
1009
1253
  return { cull: moves.has('psyshock') || moves.has('trick') || movePool.includes('teleport') };
1010
1254
  case 'photongeyser':
@@ -1605,7 +1849,7 @@ class RandomTeams {
1605
1849
  // Pokemon should have moves that benefit their types, stats, or ability
1606
1850
  const isLowBP = move.basePower && move.basePower < 50;
1607
1851
  // Genesect-Douse should never reject Techno Blast
1608
- const moveIsRejectable = /* !(species.id === 'genesectdouse' && move.id === 'technoblast') && */ (move.category === 'Status' ||
1852
+ const moveIsRejectable = !(species.id === 'genesectdouse' && move.id === 'technoblast') && (move.category === 'Status' ||
1609
1853
  !types.has(move.type) ||
1610
1854
  (isLowBP && !move.multihit && !abilities.has('Technician')));
1611
1855
  // Setup-supported moves should only be rejected under specific circumstances
@@ -1631,8 +1875,9 @@ class RandomTeams {
1631
1875
  (moves.has('leechseed') && runEnforcementChecker('leechseed'))) {
1632
1876
  cull = true;
1633
1877
  // Pokemon should have moves that benefit their typing
1878
+ // Don't cull Sticky Web in type-based enforcement, and make sure Azumarill always has Aqua Jet
1634
1879
  }
1635
- else if (move.id !== 'stickyweb') { // Don't cull Sticky Web in type-based enforcement
1880
+ else if (move.id !== 'stickyweb' && !(species.id === 'azumarill' && move.id === 'aquajet')) {
1636
1881
  for (const type of types) {
1637
1882
  if (runEnforcementChecker(type)) {
1638
1883
  cull = true;
@@ -1936,6 +2181,7 @@ class RandomTeams {
1936
2181
  }
1937
2182
  randomTeam() {
1938
2183
  var _a, _b;
2184
+ this.enforceNoDirectCustomBanlistChanges();
1939
2185
  const seed = this.prng.seed;
1940
2186
  const ruleTable = this.dex.formats.getRuleTable(this.format);
1941
2187
  const pokemon = [];
@@ -2125,6 +2371,7 @@ class RandomTeams {
2125
2371
  return pokemon;
2126
2372
  }
2127
2373
  randomCAP1v1Team() {
2374
+ this.enforceNoDirectCustomBanlistChanges();
2128
2375
  const pokemon = [];
2129
2376
  const pokemonPool = Object.keys(this.randomCAP1v1Sets);
2130
2377
  while (pokemonPool.length && pokemon.length < this.maxTeamSize) {
@@ -2221,6 +2468,7 @@ class RandomTeams {
2221
2468
  }
2222
2469
  randomBSSFactoryTeam(side, depth = 0) {
2223
2470
  var _a;
2471
+ this.enforceNoDirectCustomBanlistChanges();
2224
2472
  const forceResult = (depth >= 4);
2225
2473
  const pokemon = [];
2226
2474
  const pokemonPool = Object.keys(this.randomBSSFactorySets);