@pkmn/randoms 0.5.4 → 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/src/gen3.ts CHANGED
@@ -28,7 +28,10 @@ export class RandomGen3Teams extends RandomGen4Teams {
28
28
  Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting'),
29
29
  Fire: (movePool, moves, abilities, types, counter) => !counter.get('Fire'),
30
30
  Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'),
31
- Normal: (movePool, moves, abilities, types, counter) => !counter.get('Normal') && counter.setupType === 'Physical',
31
+ Normal: (movePool, moves, abilities, types, counter, species) => {
32
+ if (species.id === 'blissey' && movePool.includes('softboiled')) return true;
33
+ return !counter.get('Normal') && counter.setupType === 'Physical';
34
+ },
32
35
  Psychic: (movePool, moves, abilities, types, counter, species) => (
33
36
  types.has('Psychic') &&
34
37
  (movePool.includes('psychic') || movePool.includes('psychoboost')) &&
@@ -41,6 +44,7 @@ export class RandomGen3Teams extends RandomGen4Teams {
41
44
  // If the Pokémon has this move, the other move will be forced
42
45
  protect: movePool => movePool.includes('wish'),
43
46
  sunnyday: movePool => movePool.includes('solarbeam'),
47
+ sleeptalk: movePool => movePool.includes('rest'),
44
48
  };
45
49
  }
46
50
 
@@ -82,11 +86,12 @@ export class RandomGen3Teams extends RandomGen4Teams {
82
86
 
83
87
  // Not very useful without their supporting moves
84
88
  case 'amnesia': case 'sleeptalk':
89
+ if (!moves.has('rest')) return {cull: true};
85
90
  if (movePool.length > 1) {
86
91
  const rest = movePool.indexOf('rest');
87
92
  if (rest >= 0) this.fastPop(movePool, rest);
88
93
  }
89
- return {cull: !moves.has('rest')};
94
+ break;
90
95
  case 'barrier':
91
96
  return {cull: !moves.has('calmmind') && !moves.has('batonpass') && !moves.has('mirrorcoat')};
92
97
  case 'batonpass':
@@ -575,6 +580,8 @@ export class RandomGen3Teams extends RandomGen4Teams {
575
580
  }
576
581
 
577
582
  randomTeam() {
583
+ this.enforceNoDirectCustomBanlistChanges();
584
+
578
585
  const seed = this.prng.seed;
579
586
  const ruleTable = this.dex.formats.getRuleTable(this.format);
580
587
  const pokemon: RandomTeamsTypes.RandomSet[] = [];
package/src/gen5.ts CHANGED
@@ -729,6 +729,9 @@ export class RandomGen5Teams extends RandomGen6Teams {
729
729
  NUBL: 86,
730
730
  NU: 86,
731
731
  '(NU)': 88,
732
+ PUBL: 88,
733
+ PU: 88,
734
+ '(PU)': 90,
732
735
  };
733
736
  const customScale: {[forme: string]: number} = {
734
737
  Delibird: 100, 'Farfetch\u2019d': 100, Luvdisc: 100, Unown: 100,
@@ -780,6 +783,8 @@ export class RandomGen5Teams extends RandomGen6Teams {
780
783
  }
781
784
 
782
785
  randomTeam() {
786
+ this.enforceNoDirectCustomBanlistChanges();
787
+
783
788
  const seed = this.prng.seed;
784
789
  const ruleTable = this.dex.formats.getRuleTable(this.format);
785
790
  const pokemon: RandomTeamsTypes.RandomSet[] = [];
package/src/gen6.ts CHANGED
@@ -1207,6 +1207,8 @@ export class RandomGen6Teams extends RandomGen7Teams {
1207
1207
  }
1208
1208
 
1209
1209
  randomFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
1210
+ this.enforceNoDirectCustomBanlistChanges();
1211
+
1210
1212
  const forceResult = (depth >= 4);
1211
1213
 
1212
1214
  // The teams generated depend on the tier choice in such a way that
package/src/gen7.ts CHANGED
@@ -1467,6 +1467,8 @@ export class RandomGen7Teams extends RandomTeams {
1467
1467
  }
1468
1468
 
1469
1469
  randomTeam() {
1470
+ this.enforceNoDirectCustomBanlistChanges();
1471
+
1470
1472
  const seed = this.prng.seed;
1471
1473
  const ruleTable = this.dex.formats.getRuleTable(this.format);
1472
1474
  const pokemon = [];
@@ -1739,6 +1741,8 @@ export class RandomGen7Teams extends RandomTeams {
1739
1741
  }
1740
1742
 
1741
1743
  randomFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
1744
+ this.enforceNoDirectCustomBanlistChanges();
1745
+
1742
1746
  const forceResult = (depth >= 4);
1743
1747
  const isMonotype = !!this.forceMonotype || this.dex.formats.getRuleTable(this.format).has('sametypeclause');
1744
1748
 
@@ -2026,6 +2030,8 @@ export class RandomGen7Teams extends RandomTeams {
2026
2030
  }
2027
2031
 
2028
2032
  randomBSSFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
2033
+ this.enforceNoDirectCustomBanlistChanges();
2034
+
2029
2035
  const forceResult = (depth >= 4);
2030
2036
 
2031
2037
  const pokemon = [];
package/src/gen8.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import {Utils} from './utils';
2
2
  import {
3
+ Ability,
3
4
  AnyObject,
5
+ BasicEffect,
6
+ Dex,
4
7
  Format,
5
- ID,
8
+ Item,
6
9
  ModdedDex,
7
10
  Move,
11
+ Nature,
8
12
  PRNG,
9
13
  PRNGSeed,
10
14
  PlayerOptions,
@@ -13,6 +17,7 @@ import {
13
17
  Species,
14
18
  StatID,
15
19
  StatsTable,
20
+ Tags,
16
21
  toID,
17
22
  } from '@pkmn/sim';
18
23
 
@@ -276,6 +281,7 @@ export class RandomTeams {
276
281
  */
277
282
  sampleNoReplace(list: any[]) {
278
283
  const length = list.length;
284
+ if (length === 0) return null;
279
285
  const index = this.random(length);
280
286
  return this.fastPop(list, index);
281
287
  }
@@ -294,6 +300,54 @@ export class RandomTeams {
294
300
  return samples;
295
301
  }
296
302
 
303
+ /**
304
+ * Check if user has directly tried to ban/unban/restrict things in a custom battle.
305
+ * Doesn't count bans nested inside other formats/rules.
306
+ */
307
+ private hasDirectCustomBanlistChanges() {
308
+ if (!this.format.customRules) return false;
309
+ for (const rule of this.format.customRules) {
310
+ for (const banlistOperator of ['-', '+', '*']) {
311
+ if (rule.startsWith(banlistOperator)) return true;
312
+ }
313
+ }
314
+ return false;
315
+ }
316
+
317
+ /**
318
+ * Inform user when custom bans are unsupported in a team generator.
319
+ */
320
+ protected enforceNoDirectCustomBanlistChanges() {
321
+ if (this.hasDirectCustomBanlistChanges()) {
322
+ throw new Error(`Custom bans are not currently supported in ${this.format.name}.`);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Inform user when complex bans are unsupported in a team generator.
328
+ */
329
+ protected enforceNoDirectComplexBans() {
330
+ if (!this.format.customRules) return false;
331
+ for (const rule of this.format.customRules) {
332
+ if (rule.includes('+') && !rule.startsWith('+')) {
333
+ throw new Error(`Complex bans are not currently supported in ${this.format.name}.`);
334
+ }
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Validate set element pool size is sufficient to support size requirements after simple bans.
340
+ */
341
+ private enforceCustomPoolSizeNoComplexBans(
342
+ effectTypeName: string,
343
+ basicEffectPool: BasicEffect[],
344
+ requiredCount: number,
345
+ requiredCountExplanation: string
346
+ ) {
347
+ if (basicEffectPool.length >= requiredCount) return;
348
+ throw new Error(`Legal ${effectTypeName} count is insufficient to support ${requiredCountExplanation} (${basicEffectPool.length} / ${requiredCount}).`);
349
+ }
350
+
297
351
  unrejectableMovesInSingles(move: Move) {
298
352
  // These moves cannot be rejected in favor of a forced move in singles
299
353
  return (move.category !== 'Status' || !move.flags.heal) && ![
@@ -308,6 +362,8 @@ export class RandomTeams {
308
362
  }
309
363
 
310
364
  randomCCTeam(): RandomTeamsTypes.RandomSet[] {
365
+ this.enforceNoDirectCustomBanlistChanges();
366
+
311
367
  const dex = this.dex;
312
368
  const team = [];
313
369
 
@@ -467,7 +523,7 @@ export class RandomTeams {
467
523
  return team;
468
524
  }
469
525
 
470
- randomNPokemon(n: number, requiredType?: string, minSourceGen?: number) {
526
+ randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: Dex.RuleTable) {
471
527
  // Picks `n` random pokemon--no repeats, even among formes
472
528
  // Also need to either normalize for formes or select formes at random
473
529
  // Unreleased are okay but no CAP
@@ -478,15 +534,65 @@ export class RandomTeams {
478
534
  throw new Error(`"${requiredType}" is not a valid type.`);
479
535
  }
480
536
 
537
+ const isNotCustom = !ruleTable;
538
+
481
539
  const pool: number[] = [];
482
- for (const species of this.dex.species.all()) {
483
- if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue;
484
- if (requiredType && !species.types.includes(requiredType)) continue;
485
- if (minSourceGen && species.gen < minSourceGen) continue;
486
- const num = species.num;
487
- if (num <= 0 || pool.includes(num)) continue;
488
- if (num > last) break;
489
- pool.push(num);
540
+ let speciesPool: Species[] = [];
541
+ if (isNotCustom) {
542
+ speciesPool = [...this.dex.species.all()];
543
+ for (const species of speciesPool) {
544
+ if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue;
545
+ if (requiredType && !species.types.includes(requiredType)) continue;
546
+ if (minSourceGen && species.gen < minSourceGen) continue;
547
+ const num = species.num;
548
+ if (num <= 0 || pool.includes(num)) continue;
549
+ if (num > last) break;
550
+ pool.push(num);
551
+ }
552
+ } else {
553
+ const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent'];
554
+ const nonexistentBanReason = ruleTable.check('nonexistent');
555
+ // Assume tierSpecies does not differ from species here (mega formes can be used without their stone, etc)
556
+ for (const species of this.dex.species.all()) {
557
+ if (requiredType && !species.types.includes(requiredType)) continue;
558
+
559
+ let banReason = ruleTable.check('pokemon:' + species.id);
560
+ if (banReason) continue;
561
+ if (banReason !== '') {
562
+ if (species.isMega && ruleTable.check('pokemontag:mega')) continue;
563
+
564
+ banReason = ruleTable.check('basepokemon:' + toID(species.baseSpecies));
565
+ if (banReason) continue;
566
+ if (banReason !== '' || this.dex.species.get(species.baseSpecies).isNonstandard === species.isNonstandard) {
567
+ const nonexistentCheck = Tags.nonexistent.genericFilter!(species) && nonexistentBanReason;
568
+ let tagWhitelisted = false;
569
+ let tagBlacklisted = false;
570
+ for (const ruleid of ruleTable.tagRules) {
571
+ if (ruleid.startsWith('*')) continue;
572
+ const tagid = ruleid.slice(12);
573
+ const tag = Tags[tagid];
574
+ if ((tag.speciesFilter || tag.genericFilter)!(species)) {
575
+ const existenceTag = EXISTENCE_TAG.includes(tagid);
576
+ if (ruleid.startsWith('+')) {
577
+ if (!existenceTag && nonexistentCheck) continue;
578
+ tagWhitelisted = true;
579
+ break;
580
+ }
581
+ tagBlacklisted = true;
582
+ break;
583
+ }
584
+ }
585
+ if (tagBlacklisted) continue;
586
+ if (!tagWhitelisted) {
587
+ if (ruleTable.check('pokemontag:allpokemon')) continue;
588
+ }
589
+ }
590
+ }
591
+ const num = species.num;
592
+ if (pool.includes(num)) continue;
593
+ pool.push(num);
594
+ speciesPool.push(species);
595
+ }
490
596
  }
491
597
 
492
598
  const hasDexNumber: {[k: string]: number} = {};
@@ -496,12 +602,16 @@ export class RandomTeams {
496
602
  }
497
603
 
498
604
  const formes: string[][] = [];
499
- for (const species of this.dex.species.all()) {
605
+ for (const species of speciesPool) {
500
606
  if (!(species.num in hasDexNumber)) continue;
501
- if (species.gen <= this.gen && (!species.isNonstandard || species.isNonstandard === 'Unobtainable')) {
502
- if (!formes[hasDexNumber[species.num]]) formes[hasDexNumber[species.num]] = [];
503
- formes[hasDexNumber[species.num]].push(species.name);
504
- }
607
+ if (isNotCustom && (species.gen > this.gen ||
608
+ (species.isNonstandard && species.isNonstandard !== 'Unobtainable'))) continue;
609
+ if (!formes[hasDexNumber[species.num]]) formes[hasDexNumber[species.num]] = [];
610
+ formes[hasDexNumber[species.num]].push(species.name);
611
+ }
612
+
613
+ if (formes.length < n) {
614
+ throw new Error(`Legal Pokemon forme count insufficient to support Max Team Size: (${formes.length} / ${n}).`);
505
615
  }
506
616
 
507
617
  const nPokemon = [];
@@ -515,15 +625,138 @@ export class RandomTeams {
515
625
  }
516
626
 
517
627
  randomHCTeam(): PokemonSet[] {
518
- const team = [];
628
+ const hasCustomBans = this.hasDirectCustomBanlistChanges();
629
+ const ruleTable = this.dex.formats.getRuleTable(this.format);
630
+ const hasNonexistentBan = hasCustomBans && ruleTable.check('nonexistent');
631
+ const hasNonexistentWhitelist = hasCustomBans && (hasNonexistentBan === '');
519
632
 
520
- const itemPool = [...this.dex.items.all()];
521
- const abilityPool = [...this.dex.abilities.all()];
522
- const movePool = [...this.dex.moves.all()];
523
- const naturePool = this.dex.natures.all();
633
+ if (hasCustomBans) {
634
+ this.enforceNoDirectComplexBans();
635
+ }
524
636
 
525
- const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype);
637
+ // Item Pool
638
+ const doItemsExist = this.gen > 1;
639
+ let itemPool: Item[] = [];
640
+ if (doItemsExist) {
641
+ if (!hasCustomBans) {
642
+ itemPool = [...this.dex.items.all()].filter(item => (item.gen <= this.gen && !item.isNonstandard));
643
+ } else {
644
+ const hasAllItemsBan = ruleTable.check('pokemontag:allitems');
645
+ for (const item of this.dex.items.all()) {
646
+ let banReason = ruleTable.check('item:' + item.id);
647
+ if (banReason) continue;
648
+ if (banReason !== '' && item.id) {
649
+ if (hasAllItemsBan) continue;
650
+ if (item.isNonstandard) {
651
+ banReason = ruleTable.check('pokemontag:' + toID(item.isNonstandard));
652
+ if (banReason) continue;
653
+ if (banReason !== '' && item.isNonstandard !== 'Unobtainable') {
654
+ if (hasNonexistentBan) continue;
655
+ if (!hasNonexistentWhitelist) continue;
656
+ }
657
+ }
658
+ }
659
+ itemPool.push(item);
660
+ }
661
+ if (ruleTable.check('item:noitem')) {
662
+ this.enforceCustomPoolSizeNoComplexBans('item', itemPool, this.maxTeamSize, 'Max Team Size');
663
+ }
664
+ }
665
+ }
526
666
 
667
+ // Ability Pool
668
+ const doAbilitiesExist = (this.gen > 2) && (this.dex.currentMod !== 'gen7letsgo');
669
+ let abilityPool: Ability[] = [];
670
+ if (doAbilitiesExist) {
671
+ if (!hasCustomBans) {
672
+ abilityPool = [...this.dex.abilities.all()].filter(ability => (ability.gen <= this.gen && !ability.isNonstandard));
673
+ } else {
674
+ const hasAllAbilitiesBan = ruleTable.check('pokemontag:allabilities');
675
+ for (const ability of this.dex.abilities.all()) {
676
+ let banReason = ruleTable.check('ability:' + ability.id);
677
+ if (banReason) continue;
678
+ if (banReason !== '') {
679
+ if (hasAllAbilitiesBan) continue;
680
+ if (ability.isNonstandard) {
681
+ banReason = ruleTable.check('pokemontag:' + toID(ability.isNonstandard));
682
+ if (banReason) continue;
683
+ if (banReason !== '') {
684
+ if (hasNonexistentBan) continue;
685
+ if (!hasNonexistentWhitelist) continue;
686
+ }
687
+ }
688
+ }
689
+ abilityPool.push(ability);
690
+ }
691
+ if (ruleTable.check('ability:noability')) {
692
+ this.enforceCustomPoolSizeNoComplexBans('ability', abilityPool, this.maxTeamSize, 'Max Team Size');
693
+ }
694
+ }
695
+ }
696
+
697
+ // Move Pool
698
+ const setMoveCount = ruleTable.maxMoveCount;
699
+ let movePool: Move[] = [];
700
+ if (!hasCustomBans) {
701
+ movePool = [...this.dex.moves.all()].filter(move =>
702
+ (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power ')));
703
+ } else {
704
+ const hasAllMovesBan = ruleTable.check('pokemontag:allmoves');
705
+ for (const move of this.dex.moves.all()) {
706
+ // Legality of specific HP types can't be altered in built formats anyway
707
+ if (move.name.startsWith('Hidden Power ')) continue;
708
+ let banReason = ruleTable.check('move:' + move.id);
709
+ if (banReason) continue;
710
+ if (banReason !== '') {
711
+ if (hasAllMovesBan) continue;
712
+ if (move.isNonstandard) {
713
+ banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard));
714
+ if (banReason) continue;
715
+ if (banReason !== '' && move.isNonstandard !== 'Unobtainable') {
716
+ if (hasNonexistentBan) continue;
717
+ if (!hasNonexistentWhitelist) continue;
718
+ }
719
+ }
720
+ }
721
+ movePool.push(move);
722
+ }
723
+ this.enforceCustomPoolSizeNoComplexBans('move', movePool, this.maxTeamSize * setMoveCount, 'Max Team Size * Max Move Count');
724
+ }
725
+
726
+ // Nature Pool
727
+ const doNaturesExist = this.gen > 2;
728
+ let naturePool: Nature[] = [];
729
+ if (doNaturesExist) {
730
+ if (!hasCustomBans) {
731
+ if (!hasCustomBans) {
732
+ naturePool = [...this.dex.natures.all()];
733
+ } else {
734
+ const hasAllNaturesBan = ruleTable.check('pokemontag:allnatures');
735
+ for (const nature of this.dex.natures.all()) {
736
+ let banReason = ruleTable.check('nature:' + nature.id);
737
+ if (banReason) continue;
738
+ if (banReason !== '' && nature.id) {
739
+ if (hasAllNaturesBan) continue;
740
+ if (nature.isNonstandard) {
741
+ banReason = ruleTable.check('pokemontag:' + toID(nature.isNonstandard));
742
+ if (banReason) continue;
743
+ if (banReason !== '' && nature.isNonstandard !== 'Unobtainable') {
744
+ if (hasNonexistentBan) continue;
745
+ if (!hasNonexistentWhitelist) continue;
746
+ }
747
+ }
748
+ }
749
+ naturePool.push(nature);
750
+ }
751
+ // There is no 'nature:nonature' rule so do not constrain pool size
752
+ }
753
+ }
754
+ }
755
+
756
+ const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined,
757
+ hasCustomBans ? ruleTable : undefined);
758
+
759
+ const team = [];
527
760
  for (const forme of randomN) {
528
761
  // Choose forme
529
762
  const species = this.dex.species.get(forme);
@@ -531,31 +764,25 @@ export class RandomTeams {
531
764
  // Random unique item
532
765
  let item = '';
533
766
  let itemData;
534
- if (this.gen >= 2) {
535
- do {
536
- itemData = this.sampleNoReplace(itemPool);
537
- item = itemData.name;
538
- } while (itemData.gen > this.gen || itemData.isNonstandard);
767
+ if (doItemsExist) {
768
+ itemData = this.sampleNoReplace(itemPool);
769
+ item = itemData?.name;
539
770
  }
540
771
 
541
772
  // Random unique ability
542
773
  let ability = 'No Ability';
543
774
  let abilityData;
544
- if (this.gen >= 3) {
545
- do {
546
- abilityData = this.sampleNoReplace(abilityPool);
547
- ability = abilityData.name;
548
- } while (abilityData.gen > this.gen || abilityData.isNonstandard);
775
+ if (doAbilitiesExist) {
776
+ abilityData = this.sampleNoReplace(abilityPool);
777
+ ability = abilityData?.name;
549
778
  }
550
779
 
551
780
  // Random unique moves
552
781
  const m = [];
553
782
  do {
554
783
  const move = this.sampleNoReplace(movePool);
555
- if (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power ')) {
556
- m.push(move.id);
557
- }
558
- } while (m.length < 4);
784
+ m.push(move.id);
785
+ } while (m.length < setMoveCount);
559
786
 
560
787
  // Random EVs
561
788
  const evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0};
@@ -584,7 +811,10 @@ export class RandomTeams {
584
811
  };
585
812
 
586
813
  // Random nature
587
- const nature = this.sample(naturePool).name;
814
+ let nature = '';
815
+ if (doNaturesExist && (naturePool.length > 0)) {
816
+ nature = this.sample(naturePool).name;
817
+ }
588
818
 
589
819
  // Level balance
590
820
  const mbstmin = 1307;
@@ -2127,6 +2357,8 @@ export class RandomTeams {
2127
2357
  }
2128
2358
 
2129
2359
  randomTeam() {
2360
+ this.enforceNoDirectCustomBanlistChanges();
2361
+
2130
2362
  const seed = this.prng.seed;
2131
2363
  const ruleTable = this.dex.formats.getRuleTable(this.format);
2132
2364
  const pokemon: RandomTeamsTypes.RandomSet[] = [];
@@ -2299,6 +2531,8 @@ export class RandomTeams {
2299
2531
  randomCAP1v1Sets: AnyObject = {};
2300
2532
 
2301
2533
  randomCAP1v1Team() {
2534
+ this.enforceNoDirectCustomBanlistChanges();
2535
+
2302
2536
  const pokemon = [];
2303
2537
  const pokemonPool = Object.keys(this.randomCAP1v1Sets);
2304
2538
 
@@ -2402,6 +2636,8 @@ export class RandomTeams {
2402
2636
  }
2403
2637
 
2404
2638
  randomBSSFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
2639
+ this.enforceNoDirectCustomBanlistChanges();
2640
+
2405
2641
  const forceResult = (depth >= 4);
2406
2642
 
2407
2643
  const pokemon = [];