@@ -10,43 +10,37 @@ if (require.main === module) shell('node ../build');
10
10
const Dex = require ( '../.sim-dist/dex' ) ;
11
11
const PRNG = require ( '../.sim-dist/prng' ) . PRNG ;
12
12
const RandomPlayerAI = require ( '../.sim-dist/examples/random-player-ai' ) . RandomPlayerAI ;
13
+ const toId = Dex . getId ;
13
14
14
15
class TeamGenerator {
15
- constructor ( mod , prng , nonStandard ) {
16
- const dex = Dex . mod ( mod ) ;
16
+ constructor ( dex , prng , pools ) {
17
17
this . dex = dex ;
18
18
this . prng = prng && ! Array . isArray ( prng ) ? prng : new PRNG ( prng ) ;
19
- this . nonStandard = ! ! nonStandard ;
20
-
21
- this . pokemon = new Pool ( this . onlyValid ( dex . data . Pokedex , p => dex . getTemplate ( p ) ,
22
- ( _ , p ) => ( p . species !== 'Pichu-Spiky-eared' && p . species . substr ( 0 , 8 ) !== 'Pikachu-' ) ) , this . prng ) ;
23
- this . items = new Pool ( this . onlyValid ( dex . data . Items , i => dex . getItem ( i ) ) , this . prng ) ;
24
- this . abilities = new Pool ( this . onlyValid ( dex . data . Abilities , a => dex . getAbility ( a ) ) , this . prng ) ;
25
- this . moves = new Pool ( this . onlyValid ( dex . data . Movedex , m => dex . getMove ( m ) ,
26
- m => ( m === 'hiddenpower' || m . substr ( 0 , 11 ) !== 'hiddenpower' ) ) , this . prng ) ;
19
+ this . pools = pools ;
20
+
27
21
this . natures = Object . keys ( this . dex . data . Natures ) ;
28
22
}
29
23
30
24
get exhausted ( ) {
31
- const exhausted = [ this . pokemon . exhausted , this . moves . exhausted ] ;
32
- if ( this . dex . gen >= 2 ) exhausted . push ( this . items . exhausted ) ;
33
- if ( this . dex . gen >= 3 ) exhausted . push ( this . abilities . exhausted ) ;
25
+ const exhausted = [ this . pools . pokemon . exhausted , this . pools . moves . exhausted ] ;
26
+ if ( this . dex . gen >= 2 ) exhausted . push ( this . pools . items . exhausted ) ;
27
+ if ( this . dex . gen >= 3 ) exhausted . push ( this . pools . abilities . exhausted ) ;
34
28
return Math . min . apply ( null , exhausted ) ;
35
29
}
36
30
37
31
generate ( ) {
38
32
const team = [ ] ;
39
- for ( let pokemon of this . pokemon . next ( 6 ) ) {
33
+ for ( let pokemon of this . pools . pokemon . next ( 6 ) ) {
40
34
const template = this . dex . getTemplate ( pokemon ) ;
41
35
const randomEVs = ( ) => this . prng . next ( 253 ) ;
42
36
const randomIVs = ( ) => this . prng . next ( 32 ) ;
43
37
team . push ( {
44
38
name : template . baseSpecies ,
45
39
species : template . species ,
46
40
gender : template . gender ,
47
- item : this . dex . gen >= 2 ? this . items . next ( ) : '' ,
48
- ability : this . dex . gen >= 3 ? this . abilities . next ( ) : 'None' ,
49
- moves : this . moves . next ( 4 ) ,
41
+ item : this . dex . gen >= 2 ? this . pools . items . next ( ) : '' ,
42
+ ability : this . dex . gen >= 3 ? this . pools . abilities . next ( ) : 'None' ,
43
+ moves : this . pools . moves . next ( 4 ) ,
50
44
evs : {
51
45
hp : randomEVs ( ) ,
52
46
atk : randomEVs ( ) ,
@@ -64,76 +58,161 @@ class TeamGenerator {
64
58
spe : randomIVs ( ) ,
65
59
} ,
66
60
nature : this . prng . sample ( this . natures ) ,
67
- level : this . prng . next ( 70 , 100 ) ,
61
+ level : this . prng . next ( 50 , 100 ) ,
68
62
happiness : this . prng . next ( 256 ) ,
69
63
shiny : this . prng . randomChance ( 1 , 1024 ) ,
70
64
} ) ;
71
65
}
72
66
return team ;
73
67
}
74
-
75
- onlyValid ( obj , getter , additional ) {
76
- return Object . keys ( obj ) . filter ( k => {
77
- const v = getter ( k ) ;
78
- return v . gen <= this . dex . gen &&
79
- ( ! v . isNonstandard || this . nonStandard ) &&
80
- ( ! additional || additional ( k , v ) ) ;
81
- } ) ;
82
- }
83
68
}
84
69
85
70
class Pool {
86
71
constructor ( possible , prng ) {
87
72
this . possible = possible ;
88
73
this . prng = prng ;
89
-
90
- this . remaining = this . possible . slice ( ) ;
91
74
this . exhausted = 0 ;
75
+
76
+ this . reset ( ) ;
77
+ }
78
+
79
+ toString ( ) {
80
+ return `${ this . exhausted } (${ this . unused . size } /${ this . possible . length } )` ;
81
+ }
82
+
83
+ reset ( ) {
84
+ this . iter = undefined ;
85
+ this . unused = new Set ( this . shuffle ( this . possible ) ) ;
86
+ if ( this . filled ) {
87
+ for ( let used of this . filled ) {
88
+ this . unused . delete ( used ) ;
89
+ }
90
+ this . filled = new Set ( ) ;
91
+ if ( ! this . unused . size ) {
92
+ this . exhausted ++ ;
93
+ this . reset ( ) ;
94
+ }
95
+ } else {
96
+ this . filled = new Set ( ) ;
97
+ }
98
+ this . filler = this . possible . slice ( ) ;
99
+ // POST: this.unused.size > 0
100
+ // POST: this.filler.length > 0
101
+ // POST: this.filled.size === 0
102
+ // POST: this.iter === undefined
103
+ }
104
+
105
+ shuffle ( arr ) {
106
+ for ( let i = arr . length - 1 ; i > 0 ; i -- ) {
107
+ const j = Math . floor ( this . prng . next ( ) * ( i + 1 ) ) ;
108
+ [ arr [ i ] , arr [ j ] ] = [ arr [ j ] , arr [ i ] ] ;
109
+ }
110
+ return arr ;
111
+ }
112
+
113
+ wasUsed ( k ) {
114
+ // NOTE: We are intentionally clearing our iterator even though `unused`
115
+ // hasn't been modified, see explanation below.
116
+ this . iter = undefined ;
117
+ return ! this . unused . has ( k ) ;
118
+ }
119
+
120
+ markUsed ( k ) {
121
+ this . iter = undefined ;
122
+ this . unused . delete ( k ) ;
92
123
}
93
124
94
125
next ( num ) {
126
+ if ( ! num ) return this . choose ( ) ;
95
127
const chosen = [ ] ;
96
- do {
97
- if ( ! this . remaining . length ) {
98
- this . exhausted ++ ;
99
- this . remaining = this . possible . slice ( ) ;
128
+ for ( let i = 0 ; i < num ; i ++ ) {
129
+ chosen . push ( this . choose ( ) ) ;
130
+ }
131
+ return chosen ;
132
+ }
133
+
134
+ // Returns the next option in our set of unused options which were shuffled
135
+ // before insertion so as to come out in random order. The iterator is
136
+ // reset when the pools are manipulated by the CombinedPlayerAI (`markUsed`
137
+ // as it mutates the set, but also `wasUsed` because resetting the
138
+ // iterator isn't so much 'marking it as invalid' as 'signalling that we
139
+ // should move the unused options to the top again').
140
+ //
141
+ // As the pool of options dwindles, we run into scenarios where `choose`
142
+ // will keep returning the same options. This helps ensure they get used,
143
+ // but having a game with every Pokemon having the same move or ability etc
144
+ // is less realistic, so instead we 'fill' out the remaining choices during a
145
+ // generator round (ie. until our iterator gets invalidated during gameplay).
146
+ //
147
+ // The 'filler' choices are tracked in `filled` to later subtract from the next
148
+ // exhaustion cycle of this pool, but in theory we could be so unlucky that
149
+ // we loop through our fillers multiple times while dealing with a few stubborn
150
+ // remaining options in `unused`, therefore undercounting our `exhausted` total,
151
+ // but this is considered to be unlikely enough that we don't care (and
152
+ // `exhausted` is a lower bound anyway).
153
+ choose ( ) {
154
+ if ( ! this . unused . size ) {
155
+ this . exhausted ++ ;
156
+ this . reset ( ) ;
157
+ }
158
+
159
+ if ( this . iter ) {
160
+ if ( ! this . iter . done ) {
161
+ const next = this . iter . next ( ) ;
162
+ this . iter . done = next . done ;
163
+ if ( ! next . done ) return next . value ;
100
164
}
101
- chosen . push ( this . choose ( this . remaining ) ) ;
102
- // eslint-disable-next-line
103
- } while ( ! chosen . length || ( num && chosen . length < num ) ) ;
104
- return num ? chosen : chosen [ 0 ] ;
165
+ return this . fill ( ) ;
166
+ }
167
+
168
+ this . iter = this . unused . values ( ) ;
169
+ const next = this . iter . next ( ) ;
170
+ this . iter . done = next . done ;
171
+ // this.iter.next() must have a value (!this.iter.done) because this.unused.size > 0
172
+ // after this.reset(), and the only places that mutate this.unused clear this.iter.
173
+ return next . value ;
105
174
}
106
175
107
- choose ( list ) {
108
- const length = list . length ;
176
+ fill ( ) {
177
+ let length = this . filler . length ;
178
+ if ( ! length ) {
179
+ this . filler = this . possible . slice ( ) ;
180
+ length = this . filler . length ;
181
+ }
109
182
const index = this . prng . next ( length ) ;
110
- const element = list [ index ] ;
111
- list [ index ] = list [ length - 1 ] ;
112
- list . pop ( ) ;
183
+ const element = this . filler [ index ] ;
184
+ this . filler [ index ] = this . filler [ length - 1 ] ;
185
+ this . filler . pop ( ) ;
186
+ this . filled . add ( element ) ;
113
187
return element ;
114
188
}
115
189
}
116
190
117
- // TODO: make sure all ids
191
+ // Random AI which shares pools with the TeamGenerator to coordinate creating battle simulations
192
+ // that test out as many different Pokemon/Species/Items/Moves as possible. The logic is still
193
+ // random, so it's not going to optimally use as many new effects as would be possible, but it
194
+ // should exhaust its pools much faster than the naive RandomPlayerAI alone.
195
+ //
196
+ // NOTE: We're tracking 'usage' when we make the choice and not what actually gets used in Battle.
197
+ // These can differ in edge cases and so its possible we report that we've 'used' every option
198
+ // when we haven't (for example, we may switch in a Pokemon with an ability, but we're not guaranteeing
199
+ // the ability activates, etc).
118
200
class CoordinatedPlayerAI extends RandomPlayerAI {
119
- constructor ( playerStream , options , pokemon , abilities , items , moves ) {
201
+ constructor ( playerStream , options , pools ) {
120
202
super ( playerStream , options ) ;
121
- this . pokemon = pokemon ;
122
- this . abilities = abilities ;
123
- this . moves = moves ;
124
- this . items = items ; // TODO: reconcile Set and Array!
203
+ this . pools = pools ;
125
204
}
126
205
127
206
chooseTeamPreview ( team ) {
128
- return this . choosePokemon ( team . map ( ( p , i ) => ( { slot : i + 1 , pokemon : p } ) ) ) || `default `;
207
+ return ` ${ this . choosePokemon ( team . map ( ( p , i ) => ( { slot : i + 1 , pokemon : p } ) ) ) || 1 } `;
129
208
}
130
209
131
210
chooseMove ( moves ) {
132
211
// Prefer to use a move which hasn't been used yet.
133
212
for ( const { choice, move} of moves ) {
134
- const id = move . id . startsWith ( 'hiddenpower' ) ? 'hiddenpower' : move . id ;
135
- if ( this . moves . includes ( id ) ) {
136
- this . moves . remove ( id ) ;
213
+ const id = this . fixMove ( move ) ;
214
+ if ( ! this . pools . moves . wasUsed ( id ) ) {
215
+ this . pools . moves . markUsed ( id ) ;
137
216
return choice ;
138
217
}
139
218
}
@@ -147,49 +226,88 @@ class CoordinatedPlayerAI extends RandomPlayerAI {
147
226
choosePokemon ( choices ) {
148
227
// Prefer to choose a Pokemon that has a species/ability/item/move we haven't seen yet.
149
228
for ( const { slot, pokemon} of choices ) {
150
- const species = Dex . toId ( pokemon . details . split ( ',' ) [ 0 ] ) ;
151
- if ( this . pokemon . includes ( species ) ||
152
- this . abilities . includes ( pokemon . baseAbility ) ||
153
- this . items . includes ( pokemon . item ) ||
154
- pokemon . moves . some ( m => this . moves . includes ( m . id . startsWith ( 'hiddenpower' ) ? 'hiddenpower' : m . id ) ) ) {
155
- this . pokemon . remove ( species ) ;
156
- this . abilities . remove ( pokemon . baseAbility ) ;
157
- this . items . remove ( pokemon . item ) ;
229
+ const species = toId ( pokemon . details . split ( ',' ) [ 0 ] ) ;
230
+ if ( ! this . pools . pokemon . wasUsed ( species ) ||
231
+ ! this . pools . abilities . wasUsed ( pokemon . baseAbility ) ||
232
+ ! this . pools . items . wasUsed ( pokemon . item ) ||
233
+ pokemon . moves . some ( m => ! this . pools . moves . wasUsed ( this . fixMove ( m ) ) ) ) {
234
+ this . pools . pokemon . markUsed ( species ) ;
235
+ this . pools . abilities . markUsed ( pokemon . baseAbility ) ;
236
+ this . pools . items . markUsed ( pokemon . item ) ;
158
237
return slot ;
159
238
}
160
239
}
161
240
}
241
+
242
+ // The move options provided by the simulator have been converted from the name
243
+ // which we're tracking, so we need to convert them back;
244
+ fixMove ( m ) {
245
+ const id = toId ( m . move ) ;
246
+ if ( id . startsWith ( 'return' ) ) return 'return' ;
247
+ if ( id . startsWith ( 'frustration' ) ) return 'frustration' ;
248
+ if ( id . startsWith ( 'hiddenpower' ) ) return 'hiddenpower' ;
249
+ return id ;
250
+ }
251
+ }
252
+
253
+ function createPools ( dex , prng ) {
254
+ return {
255
+ pokemon : new Pool ( onlyValid ( dex . gen , dex . data . Pokedex , p => dex . getTemplate ( p ) ,
256
+ ( _ , p ) => ( p . species !== 'Pichu-Spiky-eared' && p . species . substr ( 0 , 8 ) !== 'Pikachu-' ) ) , prng ) ,
257
+ items : new Pool ( onlyValid ( dex . gen , dex . data . Items , i => dex . getItem ( i ) ) , prng ) ,
258
+ abilities : new Pool ( onlyValid ( dex . gen , dex . data . Abilities , a => dex . getAbility ( a ) ) , prng ) ,
259
+ moves : new Pool ( onlyValid ( dex . gen , dex . data . Movedex , m => dex . getMove ( m ) ,
260
+ m => ( m === 'hiddenpower' || m . substr ( 0 , 11 ) !== 'hiddenpower' ) ) , prng ) ,
261
+ } ;
262
+ }
263
+
264
+ function onlyValid ( gen , obj , getter , additional , nonStandard ) {
265
+ return Object . keys ( obj ) . filter ( k => {
266
+ const v = getter ( k ) ;
267
+ return v . gen <= gen &&
268
+ ( ! v . isNonstandard || ! ! nonStandard ) &&
269
+ ( ! additional || additional ( k , v ) ) ;
270
+ } ) ;
162
271
}
163
272
164
273
if ( require . main === module ) {
165
- const Runner = require ( './runner' ) ;
274
+ Dex . includeModData ( ) ;
275
+ const Runner = require ( './harness' ) ;
166
276
277
+ // Because of our handwavey accounting of 'usage', as well as to account for ordering effects and
278
+ // the interplay of various combinations, we should actually run multiple cycles of exhausting our
279
+ // pools to provide more confidence that we're actually smoking out issues.
167
280
const CYCLES = Number ( process . argv [ 2 ] ) || 1 ;
168
281
const MODS = [ 'gen1' , 'gen2' , 'gen3' , 'gen4' , 'gen5' , 'gen6' , 'gen7' ] ;
169
- const prng = new PRNG ( ) ;
170
- const games = [ ] ;
171
282
172
- for ( let mod of MODS ) {
173
- const generator = new TeamGenerator ( mod , prng ) ;
174
- do {
175
- games . push ( new Runner ( {
176
- prng,
177
- p1options : { team : generator . generate ( ) } ,
178
- p2options : { team : generator . generate ( ) } ,
179
- format : `${ mod } customgame` ,
180
- error : true ,
181
- } ) . run ( ) ) ;
182
- } while ( generator . exhausted < CYCLES ) ;
183
- }
283
+ const prng = new PRNG ( ) ;
184
284
185
285
( async ( ) => {
186
286
let failures = 0 ;
187
- for ( let game of games ) {
188
- try {
189
- await game ;
190
- } catch ( err ) {
191
- failures ++ ;
192
- }
287
+ for ( let mod of MODS ) {
288
+ const dex = Dex . mod ( mod ) ;
289
+ const pools = createPools ( dex , prng ) ;
290
+ const createAI = ( s , o ) => new CoordinatedPlayerAI ( s , o , pools ) ;
291
+ const generator = new TeamGenerator ( dex , prng , pools ) ;
292
+ let games = 0 ;
293
+ do {
294
+ games ++ ;
295
+ try {
296
+ // We run these sequentially instead of async so that the team generator
297
+ // and the AI can coordinate usage properly.
298
+ await new Runner ( {
299
+ prng,
300
+ p1options : { team : generator . generate ( ) , createAI} ,
301
+ p2options : { team : generator . generate ( ) , createAI} ,
302
+ format : `${ mod } customgame` ,
303
+ error : true ,
304
+ } ) . run ( ) ;
305
+ //console.log(`[${mod}] P:${pools.pokemon} I:${pools.items} A:${pools.abilities} M:${pools.moves} = ${games}`);
306
+ } catch ( err ) {
307
+ console . error ( err ) ; // TODO
308
+ failures ++ ;
309
+ }
310
+ } while ( generator . exhausted < CYCLES ) ;
193
311
}
194
312
process . exit ( failures ) ;
195
313
} ) ( ) ;
0 commit comments