Skip to content

Commit 10d5a54

Browse files
committed
Working smoke test, needs polish and for smogon#5370 to land
1 parent 0f1f52e commit 10d5a54

File tree

2 files changed

+202
-84
lines changed

2 files changed

+202
-84
lines changed

dev-tools/harness.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const PRNG = require('../.sim-dist/prng').PRNG;
2222
const RandomPlayerAI = require('../.sim-dist/examples/random-player-ai').RandomPlayerAI;
2323

2424
// 'move' 70% of the time (ie. 'switch' 30%) and ' mega' 60% of the time that its an option.
25-
const AI_OPTIONS = {move: 0.7, mega: 0.6};
25+
const AI_OPTIONS = {move: 0.7, mega: 0.6, createAI: (s, o) => new RandomPlayerAI(s, o)};
2626

2727
class Runner {
2828
constructor(options) {
@@ -55,9 +55,9 @@ class Runner {
5555
const p1spec = this.getPlayerSpec("Bot 1", this.p1options);
5656
const p2spec = this.getPlayerSpec("Bot 2", this.p2options);
5757

58-
const p1 = new RandomPlayerAI(
58+
const p1 = this.p1options.createAI(
5959
streams.p1, Object.assign({seed: this.newSeed()}, this.p1options)).start();
60-
const p2 = new RandomPlayerAI(
60+
const p2 = this.p2options.createAI(
6161
streams.p2, Object.assign({seed: this.newSeed()}, this.p2options)).start();
6262

6363
streams.omniscient.write(`>start ${JSON.stringify(spec)}\n` +

dev-tools/smoke.js

+199-81
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,37 @@ if (require.main === module) shell('node ../build');
1010
const Dex = require('../.sim-dist/dex');
1111
const PRNG = require('../.sim-dist/prng').PRNG;
1212
const RandomPlayerAI = require('../.sim-dist/examples/random-player-ai').RandomPlayerAI;
13+
const toId = Dex.getId;
1314

1415
class TeamGenerator {
15-
constructor(mod, prng, nonStandard) {
16-
const dex = Dex.mod(mod);
16+
constructor(dex, prng, pools) {
1717
this.dex = dex;
1818
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+
2721
this.natures = Object.keys(this.dex.data.Natures);
2822
}
2923

3024
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);
3428
return Math.min.apply(null, exhausted);
3529
}
3630

3731
generate() {
3832
const team = [];
39-
for (let pokemon of this.pokemon.next(6)) {
33+
for (let pokemon of this.pools.pokemon.next(6)) {
4034
const template = this.dex.getTemplate(pokemon);
4135
const randomEVs = () => this.prng.next(253);
4236
const randomIVs = () => this.prng.next(32);
4337
team.push({
4438
name: template.baseSpecies,
4539
species: template.species,
4640
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),
5044
evs: {
5145
hp: randomEVs(),
5246
atk: randomEVs(),
@@ -64,76 +58,161 @@ class TeamGenerator {
6458
spe: randomIVs(),
6559
},
6660
nature: this.prng.sample(this.natures),
67-
level: this.prng.next(70, 100),
61+
level: this.prng.next(50, 100),
6862
happiness: this.prng.next(256),
6963
shiny: this.prng.randomChance(1, 1024),
7064
});
7165
}
7266
return team;
7367
}
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-
}
8368
}
8469

8570
class Pool {
8671
constructor(possible, prng) {
8772
this.possible = possible;
8873
this.prng = prng;
89-
90-
this.remaining = this.possible.slice();
9174
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);
92123
}
93124

94125
next(num) {
126+
if (!num) return this.choose();
95127
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;
100164
}
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;
105174
}
106175

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+
}
109182
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);
113187
return element;
114188
}
115189
}
116190

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).
118200
class CoordinatedPlayerAI extends RandomPlayerAI {
119-
constructor(playerStream, options, pokemon, abilities, items, moves) {
201+
constructor(playerStream, options, pools) {
120202
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;
125204
}
126205

127206
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}`;
129208
}
130209

131210
chooseMove(moves) {
132211
// Prefer to use a move which hasn't been used yet.
133212
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);
137216
return choice;
138217
}
139218
}
@@ -147,49 +226,88 @@ class CoordinatedPlayerAI extends RandomPlayerAI {
147226
choosePokemon(choices) {
148227
// Prefer to choose a Pokemon that has a species/ability/item/move we haven't seen yet.
149228
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);
158237
return slot;
159238
}
160239
}
161240
}
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+
});
162271
}
163272

164273
if (require.main === module) {
165-
const Runner = require('./runner');
274+
Dex.includeModData();
275+
const Runner = require('./harness');
166276

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.
167280
const CYCLES = Number(process.argv[2]) || 1;
168281
const MODS = ['gen1', 'gen2', 'gen3', 'gen4', 'gen5', 'gen6', 'gen7'];
169-
const prng = new PRNG();
170-
const games = [];
171282

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();
184284

185285
(async () => {
186286
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);
193311
}
194312
process.exit(failures);
195313
})();

0 commit comments

Comments
 (0)