-
Notifications
You must be signed in to change notification settings - Fork 13
/
index.js
327 lines (283 loc) · 11.1 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"use strict";
const fs = require('fs');
const path = require('path');
const cProduct = require('cartesian-product');
const Tools = global.Tools = require('./Pokemon-Showdown/tools').includeData();
const toId = global.toId = Tools.getId;
const utils = require('./utils.js');
const parseTeams = require('./parser.js');
const Pokedex = Tools.data.Pokedex;
// const Movedex = Tools.data.Movedex;
const Items = Tools.data.Items;
const Natures = Tools.data.Natures;
const factoryTiers = ['Uber', 'OU', 'UU', 'RU', 'NU', 'PU'];
const uniqueOptionMoves = utils.toDict(['stealthrock', 'spikes', 'toxicspikes', 'rapidspin', 'defog', 'batonpass']); // High-impact moves
function proofRead(setLists, strict) {
const sets = {};
let errors = [];
for (let tier in setLists) {
for (let speciesid in setLists[tier]) {
if (!Pokedex.hasOwnProperty(speciesid)) {
errors.push("Invalid species id: " + speciesid);
continue;
}
let speciesResult = proofReadSpeciesSets(setLists[tier][speciesid].sets, speciesid, tier, strict);
if (speciesResult.errors.length) {
errors = errors.concat(speciesResult.errors);
} else {
if (!sets[tier]) sets[tier] = {};
sets[tier][speciesid] = {flags: {}, sets: speciesResult.sets};
}
}
}
return {errors: errors, sets: sets};
}
function proofReadSpeciesSets(setList, startSpecies, tier, strict) {
const errors = [];
const minTierIndex = utils.getTierIndex(tier);
let output = [];
for (let i = 0; i < setList.length; i++) {
let set = setList[i];
let speciesid = startSpecies;
if (set.isClone) throw new Error("Unexpected `isClone` property");
if (set.item && !Items.hasOwnProperty(toId(set.item))) errors.push("Invalid item for " + tier + " " + speciesid + ": '" + set.item + "'.");
if (set.nature && !Natures.hasOwnProperty(toId(set.nature))) errors.push("Invalid nature for " + tier + " " + speciesid + ": '" + set.nature + "'.");
if (set.evs && (!Object.values(set.evs).every(utils.isValidEV) || utils.sumIterate(Object.values(set.evs)) > 510)) errors.push("Invalid EVs for " + tier + " " + speciesid + ": '" + Object.values(set.evs).join(", ") + "'.");
if (set.ivs && !Object.values(set.ivs).every(utils.isValidIV)) errors.push("Invalid IVs for " + tier + " " + speciesid + ": '" + Object.values(set.evs).join(", ") + "'.");
if (set.happiness && !utils.isValidHappiness(set.happiness)) errors.push("Happiness out of bounds for " + tier + " " + speciesid + ": '" + set.happiness + "'.");
if ('level' in set && !utils.isValidLevel(set.level)) errors.push("Level out of bounds for " + tier + " " + speciesid + ": '" + set.level + "'.");
if (!utils.inValues(Pokedex[speciesid].abilities, set.ability)) errors.push("Invalid ability for " + tier + " " + speciesid + ": '" + set.ability + "'.");
// Mega formes are tiered separately
if (set.item && toId(Tools.getItem(set.item).megaEvolves) === speciesid) {
speciesid = toId(Tools.getItem(set.item).megaStone);
if (utils.getTierIndex(Tools.getTemplate(speciesid).tier) < minTierIndex) errors.push("Pokémon " + speciesid + " is banned from " + tier);
} else {
if (utils.getTierIndex(Tools.getTemplate(speciesid).tier) < minTierIndex) errors.push("Pokémon " + speciesid + " is banned from " + tier);
}
let setsSplit = splitSetClosed(set);
output = output.concat(setsSplit.valid);
for (let j = 0; j < setsSplit.invalid.length; j++) {
errors.push("Conflict between moves for " + tier + " " + speciesid + ": '" + Object.keys(setsSplit.invalid[j].conflict).join("', '") + "'");
}
if (strict) {
for (let j = 0; j < setsSplit.discarded.length; j++) {
errors.push("Conflict between alternate moves for " + tier + " " + speciesid + "'");
}
}
}
for (let i = 0; i < output.length; i++) {
let happinessSlot = 4; // Only one slot allowed for Return / Frustration.
let moveSlots = Object.create(null); // Only one slot allowed for any other move as well.
let set = output[i];
for (let j = 0; j < set.moves.length; j++) {
let moveSlot = set.moves[j];
for (let k = 0, totalSlashed = moveSlot.length; k < totalSlashed; k++) {
let move = Tools.getMove(moveSlot[k]);
if (!move.exists) {
errors.push("Invalid move for " + startSpecies + ": '" + move.name + "'");
continue;
}
let moveName = move.name;
if (moveName !== moveSlot[k]) moveSlot[k] = moveName;
if (moveSlots[move.id] <= j) {
errors.push("Duplicate move " + moveName + " for " + startSpecies + ".");
} else {
moveSlots[move.id] = j;
}
if (move.id === 'frustration' || move.id === 'return') {
if (happinessSlot < j) {
errors.push("Duplicate happiness-based moves for " + startSpecies + "."); // Meta-based rejection
} else {
happinessSlot = j;
set.happiness = (move.id === 'frustration' ? 0 : 255);
}
}
if (move.id === 'hiddenpower') {
let hpType = moveName.slice(13);
set.ivs = utils.clone(Tools.getType(hpType).HPivs || {});
}
}
}
}
return {errors: errors, sets: output};
}
function getSetVariants(set) {
const setVariants = {moves: []};
const moveCount = Object.create(null);
const duplicateMoves = Object.create(null);
for (let i = 0; i < set.moves.length; i++) {
for (let j = 0; j < set.moves[i].length; j++) {
let move = Tools.getMove(set.moves[i][j]);
if (moveCount[move.id]) {
moveCount[move.id]++;
duplicateMoves[move.id] = 1;
} else {
moveCount[move.id] = 1;
}
}
}
for (let i = 0; i < set.moves.length; i++) {
let slotAlts = set.moves[i];
let setsBase = [];
let setsImplied = [];
for (let j = 0, totalOptions = slotAlts.length; j < totalOptions; j++) {
let move = Tools.getMove(slotAlts[j]);
let moveName = move.name;
moveCount[moveName] = moveCount[moveName] ? moveCount[moveName] + 1 : 1;
if (move.id === 'hiddenpower') {
setsImplied.push([move.name]);
} else if (move.id === 'frustration' || move.id === 'return') {
setsImplied.push([move.name]);
} else if (totalOptions > 1 && (uniqueOptionMoves[move.id] || duplicateMoves[move.id])) {
setsImplied.push([move.name]);
} else {
setsBase.push(move.name);
}
}
let slotAltsOutput = [].concat(setsImplied);
if (setsBase.length) slotAltsOutput.unshift(setsBase);
setVariants.moves.push(slotAltsOutput);
}
return setVariants;
}
// `setDivided` has a property `moves`, which is an array (thereafter "the moveset"), whose elements are n arrays with arbitrary dimensions D1, D2, ..., Dn.
// Returns an array of up to Π Di copies of `set`, having their property `moves` replaced by each element of the n-ary Cartesian product of the moveset elements, holding the condition:
// a) Subsets of each such element should be disjoint sets.
function combineVariants(set, setDivided) {
// 1) `valid`: Valid combinations
// 2) `discarded`: Invalid combinations between slashed moves only
// 3) `invalid`: Invalid combinations including fixed moves
const output = {valid: [], discarded: [], invalid: []};
const combinations = cProduct(setDivided.moves);
const fixedMoves = Object.create(null);
for (let i = 0; i < set.moves.length; i++) {
if (set.moves[i].length <= 1) fixedMoves[Tools.getMove(set.moves[i][0]).name] = 1;
}
for (let i = 0; i < combinations.length; i++) {
let combination = combinations[i];
let partitionCheck = checkPartition(combination);
let setClone = utils.copySet(set);
setClone.moves = combination;
if (partitionCheck.result) {
output.valid.push(setClone);
} else {
partitionCheck = checkPartition([Object.keys(fixedMoves), Object.keys(partitionCheck.intersection)]);
if (partitionCheck.result) {
utils.markConflict(setClone, partitionCheck.intersection);
output.discarded.push(setClone);
} else {
utils.markConflict(setClone, partitionCheck.intersection);
output.invalid.push(setClone);
}
}
}
return output;
}
function checkPartition(arr) {
let result = true;
const duplicateMoves = Object.create(null);
const elems = Object.create(null);
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
if (elems[arr[i][j]]) {
duplicateMoves[arr[i][j]] = 1;
result = false;
} else {
elems[arr[i][j]] = 1;
}
}
}
return {result: result, intersection: duplicateMoves};
}
function splitSetClosed(set) {
const variantsSplit = getSetVariants(set);
const combinedVariants = combineVariants(set, variantsSplit);
return combinedVariants;
}
function addFlags(setLists) {
const hasMegaEvo = Tools.data.Scripts.hasMegaEvo.bind(Tools);
for (let tier in setLists) {
for (let speciesId in setLists[tier]) {
let flags = setLists[tier][speciesId].flags;
let template = Tools.getTemplate(speciesId);
if (hasMegaEvo(template)) {
let megaOnly = true;
for (let i = 0, len = setLists[tier][speciesId].sets.length; i < len; i++) {
let set = setLists[tier][speciesId].sets[i];
if (Tools.getItem(set.item).megaStone) continue;
megaOnly = false;
break;
}
if (megaOnly) flags.megaOnly = 1;
}
}
}
}
function buildSets(options, callback) {
if (typeof callback === 'undefined' && typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
} else {
// Validate options
if (options.output && !options.output.write) throw new TypeError("Option `output` must be a writable stream");
if (options.setData && typeof options.setData !== 'object') throw new TypeError("Option `setData` must be an object");
}
const setListsRaw = {};
const setListsByTier = {};
const setData = [];
if (!options.setData) {
factoryTiers.forEach(function (tier) {
setData.push({
tier: tier,
path: path.resolve(__dirname, 'data', tier.toLowerCase() + '.txt'),
});
});
} else {
for (let tier in options.setData) {
setData.push({
tier: tier,
path: options.setData[tier],
});
}
}
setData.forEach(function (tierData) {
tierData.content = fs.readFileSync(tierData.path, 'utf8');
});
for (let i = 0; i < setData.length; i++) {
setListsRaw[setData[i].tier] = parseTeams(setData[i].content);
setListsByTier[setData[i].tier] = {};
}
// Classify sets according to tier and species
for (let tier in setListsRaw) {
let viableSets = setListsByTier[tier];
for (let i = 0, len = setListsRaw[tier].length; i < len; i++) {
let set = setListsRaw[tier][i];
let speciesid = toId(set.species);
if (!viableSets[speciesid]) viableSets[speciesid] = {sets: []};
viableSets[speciesid].sets.push(set);
}
}
// Check for weird stuff, and fix if possible
const result = proofRead(setListsByTier, !!options.strict);
if (result.errors.length) {
return callback(new Error(result.errors.join('\n')));
}
// Add flags to describe the sets of each Pokémon
addFlags(result.sets);
// Export as JSON
const output = options.output || fs.createWriteStream(path.resolve(__dirname, 'factory-sets.json'), {encoding: 'utf8'});
output.on('finish', callback);
output.write(JSON.stringify(result.sets) + '\n');
output.end();
}
exports.run = buildSets;
exports.addFlags = addFlags;
exports.proofRead = proofRead;
if (require.main === module) {
buildSets(function (error) {
if (error) return console.error("Failed:\n" + error.message);
console.log("Battle Factory sets built.");
});
}