Split off team validation to team-validator.js

This is the first step in a refactor to split team validation into
its own process.
This commit is contained in:
Guangcong Luo 2014-01-09 17:35:19 -08:00
parent f57f997614
commit a39fdd6006
7 changed files with 603 additions and 596 deletions

2
app.js
View File

@ -412,6 +412,8 @@ global.Tools = require('./tools.js');
// After loading tools, generate and cache the format list.
Rooms.global.formatListText = Rooms.global.getFormatListText();
global.TeamValidator = require('./team-validator.js');
// load ipbans at our leisure
fs.readFile('./config/ipbans.txt', function (err, data) {
if (err) return;

View File

@ -424,7 +424,7 @@ var commands = exports.commands = {
move = Tools.getMove(i);
if (move.id !== 'count') {
if (!move.exists) return this.sendReplyBox('"' + move + '" is not a known move.');
problem = Tools.checkLearnset(move, template, lsetData);
problem = TeamValidator().checkLearnset(move, template, lsetData);
if (problem) break;
}
}
@ -492,7 +492,7 @@ var commands = exports.commands = {
if (!move.exists) {
return this.sendReply('Move "'+move.id+'" not found.');
}
problem = Tools.checkLearnset(move, template, lsetData);
problem = TeamValidator().checkLearnset(move, template, lsetData);
if (problem) break;
}
var buffer = ''+template.name+(problem?" <span class=\"message-learn-cannotlearn\">can't</span> learn ":" <span class=\"message-learn-canlearn\">can</span> learn ")+(targets.length>2?"these moves":move.name);

View File

@ -2,17 +2,6 @@ exports.BattleScripts = {
inherit: 'gen5',
gen: 3,
init: function() {
for (var i in this.data.Learnsets) {
this.modData('Learnsets', i);
var learnset = this.data.Learnsets[i].learnset;
for (var moveid in learnset) {
if (typeof learnset[moveid] === 'string') learnset[moveid] = [learnset[moveid]];
learnset[moveid] = learnset[moveid].filter(function(source) {
return source[0] === '3';
});
if (!learnset[moveid].length) delete learnset[moveid];
}
}
for (var i in this.data.Pokedex) {
delete this.data.Pokedex[i].abilities['H'];
}

View File

@ -2,17 +2,6 @@ exports.BattleScripts = {
inherit: 'gen5',
gen: 4,
init: function() {
for (var i in this.data.Learnsets) {
this.modData('Learnsets', i);
var learnset = this.data.Learnsets[i].learnset;
for (var moveid in learnset) {
if (typeof learnset[moveid] === 'string') learnset[moveid] = [learnset[moveid]];
learnset[moveid] = learnset[moveid].filter(function(source) {
return source[0] !== '5';
});
if (!learnset[moveid].length) delete learnset[moveid];
}
}
for (var i in this.data.Pokedex) {
delete this.data.Pokedex[i].abilities['H'];
}

598
team-validator.js Normal file
View File

@ -0,0 +1,598 @@
/**
* Team Validator
* Pokemon Showdown - http://pokemonshowdown.com/
*
* Handles team validation, and specifically learnset checking.
*
* @license MIT license
*/
var Validator = (function() {
function Validator(format) {
this.format = Tools.getFormat(format);
this.tools = Tools.mod(this.format);
}
Validator.prototype.validateTeam = function(team) {
var format = this.format;
var tools = this.tools;
var problems = [];
tools.getBanlistTable(format);
if (format.team) {
return false;
}
if (!team || !Array.isArray(team)) {
if (format.canUseRandomTeam) {
return false;
}
return ["You sent invalid team data. If you're not using a custom client, please report this as a bug."];
}
if (!team.length) {
return ["Your team has no pokemon."];
}
if (team.length>6) {
return ["Your team has more than 6 pokemon."];
}
var teamHas = {};
for (var i=0; i<team.length; i++) {
if (!team[i]) return ["You sent invalid team data. If you're not using a custom client, please report this as a bug."];
var setProblems = this.validateSet(team[i], teamHas);
if (setProblems) {
problems = problems.concat(setProblems);
}
}
for (var i=0; i<format.teamBanTable.length; i++) {
var bannedCombo = '';
for (var j=0; j<format.teamBanTable[i].length; j++) {
if (!teamHas[format.teamBanTable[i][j]]) {
bannedCombo = false;
break;
}
if (j == 0) {
bannedCombo += format.teamBanTable[i][j];
} else {
bannedCombo += ' and '+format.teamBanTable[i][j];
}
}
if (bannedCombo) {
var clause = format.name ? " by "+format.name : '';
problems.push("Your team has the combination of "+bannedCombo+", which is banned"+clause+".");
}
}
if (format.ruleset) {
for (var i=0; i<format.ruleset.length; i++) {
var subformat = tools.getFormat(format.ruleset[i]);
if (subformat.validateTeam) {
problems = problems.concat(subformat.validateTeam.call(tools, team, format)||[]);
}
}
}
if (format.validateTeam) {
problems = problems.concat(format.validateTeam.call(tools, team, format)||[]);
}
if (!problems.length) return false;
return problems;
};
Validator.prototype.validateSet = function(set, teamHas) {
var format = this.format;
var tools = this.tools;
var problems = [];
if (!set) {
return ["This is not a Pokemon."];
}
var template = tools.getTemplate(string(set.species));
if (!template.exists) {
return ["The Pokemon '"+set.species+"' does not exist."];
}
set.species = template.species;
set.name = toName(set.name);
var item = tools.getItem(string(set.item));
set.item = item.name;
var ability = tools.getAbility(string(set.ability));
set.ability = ability.name;
if (!Array.isArray(set.moves)) set.moves = [];
var maxLevel = format.maxLevel || 100;
var maxForcedLevel = format.maxForcedLevel || maxLevel;
if (!set.level) {
set.level = (format.defaultLevel || maxLevel);
}
if (format.forcedLevel) {
set.forcedLevel = format.forcedLevel;
} else if (set.level >= maxForcedLevel) {
set.forcedLevel = maxForcedLevel;
}
if (set.level > maxLevel || set.level == set.forcedLevel || set.level == set.maxForcedLevel) {
set.level = maxLevel;
}
var nameTemplate = tools.getTemplate(set.name);
if (nameTemplate.exists && nameTemplate.name.toLowerCase() === set.name.toLowerCase()) set.name = null;
set.species = set.species;
set.name = set.name || set.species;
var name = set.species;
if (set.species !== set.name) name = set.name + " ("+set.species+")";
var isHidden = false;
var lsetData = {set:set, format:format};
var setHas = {};
if (!template || !template.abilities) {
set.species = 'Unown';
template = tools.getTemplate('Unown');
}
if (format.ruleset) {
for (var i=0; i<format.ruleset.length; i++) {
var subformat = tools.getFormat(format.ruleset[i]);
if (subformat.validateSet) {
problems = problems.concat(subformat.validateSet.call(tools, set, format)||[]);
}
}
}
template = tools.getTemplate(set.species);
item = tools.getItem(set.item);
ability = tools.getAbility(set.ability);
var banlistTable = tools.getBanlistTable(format);
var check = toId(set.species);
var clause = '';
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(set.species+' is banned'+clause+'.');
}
check = toId(set.ability);
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s ability "+set.ability+" is banned"+clause+".");
}
check = toId(set.item);
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s item "+set.item+" is banned"+clause+".");
}
if (banlistTable['illegal'] && item.isUnreleased) {
problems.push(name+"'s item "+set.item+" is unreleased.");
}
if (banlistTable['Unreleased'] && template.isUnreleased) {
if (!format.requirePentagon || (template.eggGroups[0] === 'Undiscovered' && !template.evos)) {
problems.push(name+" ("+template.species+") is unreleased.");
}
}
setHas[toId(set.ability)] = true;
if (banlistTable['illegal']) {
var totalEV = 0;
for (var k in set.evs) {
if (typeof set.evs[k] !== 'number') {
set.evs[k] = 0;
}
totalEV += set.evs[k];
}
// In gen 1 and 2, it was possible to max out all EVs
if (tools.gen >= 3 && totalEV > 510) {
problems.push(name+" has more than 510 total EVs.");
}
// Don't check abilities for metagames with All Abilities
if (tools.gen <= 2) {
set.ability = 'None';
} else if (!banlistTable['ignoreillegalabilities']) {
if (!ability.name) {
problems.push(name+" needs to have an ability.");
} else if (ability.name !== template.abilities['0'] &&
ability.name !== template.abilities['1'] &&
ability.name !== template.abilities['H']) {
problems.push(name+" can't have "+set.ability+".");
}
if (ability.name === template.abilities['H']) {
isHidden = true;
if (template.unreleasedHidden && banlistTable['illegal']) {
problems.push(name+"'s hidden ability is unreleased.");
} else if (tools.gen === 5 && set.level < 10 && (template.maleOnlyHidden || template.gender === 'N')) {
problems.push(name+" must be at least level 10 with its hidden ability.");
}
if (template.maleOnlyHidden) {
set.gender = 'M';
lsetData.sources = ['5D'];
}
}
}
}
if (set.moves && Array.isArray(set.moves)) {
set.moves = set.moves.filter(function(val){ return val; });
}
if (!set.moves || !set.moves.length) {
problems.push(name+" has no moves.");
} else {
// A limit is imposed here to prevent too much engine strain or
// too much layout deformation - to be exact, this is the Debug
// Mode limitation.
// The usual limit of 4 moves is handled elsewhere - currently
// in the cartridge-compliant set validator: formats.js:pokemon
set.moves = set.moves.slice(0,24);
for (var i=0; i<set.moves.length; i++) {
if (!set.moves[i]) continue;
var move = tools.getMove(string(set.moves[i]));
set.moves[i] = move.name;
check = move.id;
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s move "+set.moves[i]+" is banned"+clause+".");
}
if (banlistTable['illegal']) {
var problem = this.checkLearnset(move, template, lsetData);
if (problem) {
var problemString = name+" can't learn "+move.name;
if (problem.type === 'incompatible') {
if (isHidden) {
problemString = problemString.concat(" because it's incompatible with its ability or another move.");
} else {
problemString = problemString.concat(" because it's incompatible with another move.");
}
} else if (problem.type === 'oversketched') {
problemString = problemString.concat(" because it can only sketch "+problem.maxSketches+" move"+(problem.maxSketches>1?"s":"")+".");
} else if (problem.type === 'pokebank') {
problemString = problemString.concat(" because it's only obtainable from a previous generation.");
} else {
problemString = problemString.concat(".");
}
problems.push(problemString);
}
}
}
if (lsetData.sources && lsetData.sources.length === 1 && !lsetData.sourcesBefore) {
// we're restricted to a single source
var source = lsetData.sources[0];
if (source.substr(1,1) === 'S') {
// it's an event
var eventData = null;
var splitSource = source.substr(2).split(' ');
var eventTemplate = tools.getTemplate(splitSource[1]);
if (eventTemplate.eventPokemon) eventData = eventTemplate.eventPokemon[parseInt(splitSource[0],10)];
if (eventData) {
if (eventData.nature && eventData.nature !== set.nature) {
problems.push(name+" must have a "+eventData.nature+" nature because it comes from a specific event.");
}
if (eventData.shiny) {
set.shiny = true;
}
if (eventData.generation < 5) eventData.isHidden = false;
if (eventData.isHidden !== undefined && eventData.isHidden !== isHidden) {
problems.push(name+(isHidden?" can't have":" must have")+" its hidden ability because it comes from a specific event.");
}
if (tools.gen <= 5 && eventData.abilities && eventData.abilities.indexOf(ability.id) < 0) {
problems.push(name+" must have "+eventData.abilities.join(" or ")+" because it comes from a specific event.");
}
if (eventData.gender) {
set.gender = eventData.gender;
}
if (eventData.level && set.level < eventData.level) {
problems.push(name+" must be at least level "+eventData.level+" because it comes from a specific event.");
}
}
isHidden = false;
}
}
if (isHidden && lsetData.sourcesBefore < 5) {
if (!lsetData.sources) {
problems.push(name+" has a hidden ability - it can't have moves only learned before gen 5.");
} else if (template.gender) {
var compatibleSource = false;
for (var i=0,len=lsetData.sources.length; i<len; i++) {
if (lsetData.sources[i].charAt(1) === 'E' || (lsetData.sources[i].substr(0,2) === '5D' && set.level >= 10)) {
compatibleSource = true;
break;
}
}
if (!compatibleSource) {
problems.push(name+" has moves incompatible with its hidden ability.");
}
}
}
if (set.level < template.evoLevel) {
// FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive
problems.push(name+" must be at least level "+template.evoLevel+".");
}
if (!lsetData.sources && lsetData.sourcesBefore <= 3 && tools.getAbility(set.ability).gen === 4 && !template.prevo && tools.gen <= 5) {
problems.push(name+" has a gen 4 ability and isn't evolved - it can't use anything from gen 3.");
}
if (!lsetData.sources && lsetData.sourcesBefore >= 3 && (isHidden || tools.gen <= 5) && template.gen <= lsetData.sourcesBefore) {
var oldAbilities = tools.mod('gen'+lsetData.sourcesBefore).getTemplate(set.species).abilities;
if (ability.name !== oldAbilities['0'] && ability.name !== oldAbilities['1'] && ability.name !== oldAbilities['H']) {
problems.push(name+" has moves incompatible with its ability.");
}
}
}
setHas[toId(template.tier)] = true;
if (banlistTable[template.tier]) {
problems.push(name+" is in "+template.tier+", which is banned.");
}
if (teamHas) {
for (var i in setHas) {
teamHas[i] = true;
}
}
for (var i=0; i<format.setBanTable.length; i++) {
var bannedCombo = '';
for (var j=0; j<format.setBanTable[i].length; j++) {
if (!setHas[format.setBanTable[i][j]]) {
bannedCombo = false;
break;
}
if (j == 0) {
bannedCombo += format.setBanTable[i][j];
} else {
bannedCombo += ' and '+format.setBanTable[i][j];
}
}
if (bannedCombo) {
clause = format.name ? " by "+format.name : '';
problems.push(name+" has the combination of "+bannedCombo+", which is banned"+clause+".");
}
}
if (format.validateSet) {
problems = problems.concat(format.validateSet.call(tools, set, format)||[]);
}
if (!problems.length) return false;
return problems;
};
Validator.prototype.checkLearnset = function(move, template, lsetData) {
var tools = this.tools;
move = toId(move);
template = tools.getTemplate(template);
lsetData = lsetData || {};
var set = (lsetData.set || (lsetData.set={}));
var format = (lsetData.format || (lsetData.format={}));
var alreadyChecked = {};
var level = set.level || 100;
var limit1 = true;
var sketch = false;
var sometimesPossible = false; // is this move in the learnset at all?
// This is a pretty complicated algorithm
// Abstractly, what it does is construct the union of sets of all
// possible ways this pokemon could be obtained, and then intersect
// it with a the pokemon's existing set of all possible ways it could
// be obtained. If this intersection is non-empty, the move is legal.
// We apply several optimizations to this algorithm. The most
// important is that with, for instance, a TM move, that Pokemon
// could have been obtained from any gen at or before that TM's gen.
// Instead of adding every possible source before or during that gen,
// we keep track of a maximum gen variable, intended to mean "any
// source at or before this gen is possible."
// set of possible sources of a pokemon with this move, represented as an array
var sources = [];
// the equivalent of adding "every source at or before this gen" to sources
var sourcesBefore = 0;
var noPastGen = format.requirePentagon;
do {
alreadyChecked[template.speciesid] = true;
// Stabmons hack to avoid copying all of validateSet to formats.
if (format.id === 'stabmons' && template.types.indexOf(tools.getMove(move).type) > -1) return false;
if (template.learnset) {
if (template.learnset[move] || template.learnset['sketch']) {
sometimesPossible = true;
var lset = template.learnset[move];
if (!lset || template.speciesid === 'smeargle') {
lset = template.learnset['sketch'];
sketch = true;
// Chatter, Struggle and Magikarp's Revenge cannot be sketched
if (move in {'chatter':1, 'struggle':1, 'magikarpsrevenge':1}) return true;
}
if (typeof lset === 'string') lset = [lset];
for (var i=0, len=lset.length; i<len; i++) {
var learned = lset[i];
if (noPastGen && learned.charAt(0) !== '6') continue;
if (parseInt(learned.charAt(0),10) > tools.gen) continue;
if (!template.isNonstandard) {
// HMs can't be transferred
if (tools.gen >= 4 && learned.charAt(0) <= 3 && move in {'cut':1, 'fly':1, 'surf':1, 'strength':1, 'flash':1, 'rocksmash':1, 'waterfall':1, 'dive':1}) continue;
if (tools.gen >= 5 && learned.charAt(0) <= 4 && move in {'cut':1, 'fly':1, 'surf':1, 'strength':1, 'rocksmash':1, 'waterfall':1, 'rockclimb':1}) continue;
}
if (learned.substr(0,2) in {'4L':1,'5L':1,'6L':1}) {
// gen 4-6 level-up moves
if (level >= parseInt(learned.substr(2),10)) {
// we're past the required level to learn it
return false;
}
if (!template.gender || template.gender === 'F') {
// available as egg move
learned = learned.charAt(0)+'Eany';
} else {
// this move is unavailable, skip it
continue;
}
}
if (learned.charAt(1) in {L:1,M:1,T:1}) {
if (learned.charAt(0) === '6') {
// current-gen TM or tutor moves:
// always available
return false;
}
// past-gen level-up, TM, or tutor moves:
// available as long as the source gen was or was before this gen
limit1 = false;
sourcesBefore = Math.max(sourcesBefore, parseInt(learned.charAt(0),10));
} else if (learned.charAt(1) in {E:1,S:1,D:1}) {
// egg, event, or DW moves:
// only if that was the source
if (learned.charAt(1) === 'E') {
// it's an egg move, so we add each pokemon that can be bred with to its sources
if (learned.charAt(0) === '6') {
// gen 6 doesn't have egg move incompatibilities
sources.push('6E');
continue;
}
var eggGroups = template.eggGroups;
if (!eggGroups) continue;
if (eggGroups[0] === 'Undiscovered') eggGroups = tools.getTemplate(template.evos[0]).eggGroups;
var atLeastOne = false;
var fromSelf = (learned.substr(1) === 'Eany');
learned = learned.substr(0,2);
for (var templateid in tools.data.Pokedex) {
var dexEntry = tools.getTemplate(templateid);
if (
// CAP pokemon can't breed
!dexEntry.isNonstandard &&
// can't breed mons from future gens
dexEntry.gen <= parseInt(learned.charAt(0),10) &&
// genderless pokemon can't pass egg moves
dexEntry.gender !== 'N') {
if (
// chainbreeding
fromSelf ||
// otherwise parent must be able to learn the move
!alreadyChecked[dexEntry.speciesid] && dexEntry.learnset && (dexEntry.learnset[move]||dexEntry.learnset['sketch'])) {
if (dexEntry.eggGroups.intersect(eggGroups).length) {
// we can breed with it
atLeastOne = true;
sources.push(learned+dexEntry.id);
}
}
}
}
// chainbreeding with itself from earlier gen
if (!atLeastOne) sources.push(learned+template.id);
} else if (learned.charAt(1) === 'S') {
sources.push(learned+' '+template.id);
} else {
sources.push(learned);
}
}
}
}
if (format.mimicGlitch && template.gen < 5) {
// include the Mimic Glitch when checking this mon's learnset
var glitchMoves = {metronome:1, copycat:1, transform:1, mimic:1, assist:1};
var getGlitch = false;
for (var i in glitchMoves) {
if (template.learnset[i]) {
if (i === 'mimic' && tools.getAbility(set.ability).gen == 4 && !template.prevo) {
// doesn't get the glitch
} else {
getGlitch = true;
break;
}
}
}
if (getGlitch) {
sourcesBefore = Math.max(sourcesBefore, 4);
if (tools.getMove(move).gen < 5) {
limit1 = false;
}
}
}
}
// also check to see if the mon's prevo or freely switchable formes can learn this move
if (!template.learnset && template.baseSpecies !== template.species) {
// forme takes precedence over prevo only if forme has no learnset
template = tools.getTemplate(template.baseSpecies);
} else if (template.prevo) {
template = tools.getTemplate(template.prevo);
} else if (template.speciesid === 'shaymin') {
template = tools.getTemplate('shayminsky');
} else if (template.baseSpecies !== template.species && template.baseSpecies !== 'Kyurem') {
template = tools.getTemplate(template.baseSpecies);
} else {
template = null;
}
} while (template && template.species && !alreadyChecked[template.speciesid]);
if (limit1 && sketch) {
// limit 1 sketch move
if (lsetData.sketchMove) {
return {type:'oversketched', maxSketches: 1};
}
lsetData.sketchMove = move;
}
// Now that we have our list of possible sources, intersect it with the current list
if (!sourcesBefore && !sources.length) {
if (noPastGen && sometimesPossible) return {type:'pokebank'};
return true;
}
if (!sources.length) sources = null;
if (sourcesBefore || lsetData.sourcesBefore) {
// having sourcesBefore is the equivalent of having everything before that gen
// in sources, so we fill the other array in preparation for intersection
if (sourcesBefore && lsetData.sources) {
if (!sources) sources = [];
for (var i=0, len=lsetData.sources.length; i<len; i++) {
var learned = lsetData.sources[i];
if (parseInt(learned.substr(0,1),10) <= sourcesBefore) {
sources.push(learned);
}
}
if (!lsetData.sourcesBefore) sourcesBefore = 0;
}
if (lsetData.sourcesBefore && sources) {
if (!lsetData.sources) lsetData.sources = [];
for (var i=0, len=sources.length; i<len; i++) {
var learned = sources[i];
if (parseInt(learned.substr(0,1),10) <= lsetData.sourcesBefore) {
lsetData.sources.push(learned);
}
}
if (!sourcesBefore) delete lsetData.sourcesBefore;
}
}
if (sources) {
if (lsetData.sources) {
var intersectSources = lsetData.sources.intersect(sources);
if (!intersectSources.length && !(sourcesBefore && lsetData.sourcesBefore)) {
return {type:'incompatible'};
}
lsetData.sources = intersectSources;
} else {
lsetData.sources = sources.unique();
}
}
if (sourcesBefore) {
lsetData.sourcesBefore = Math.min(sourcesBefore, lsetData.sourcesBefore||6);
}
return false;
};
return Validator;
})();
function TeamValidator(mod) {
return new Validator(mod);
}
module.exports = TeamValidator;

571
tools.js
View File

@ -444,231 +444,6 @@ module.exports = (function () {
return stats;
};
Tools.prototype.checkLearnset = function(move, template, lsetData) {
move = toId(move);
template = this.getTemplate(template);
lsetData = lsetData || {};
var set = (lsetData.set || (lsetData.set={}));
var format = (lsetData.format || (lsetData.format={}));
var alreadyChecked = {};
var level = set.level || 100;
var limit1 = true;
var sketch = false;
var sometimesPossible = false; // is this move in the learnset at all?
// This is a pretty complicated algorithm
// Abstractly, what it does is construct the union of sets of all
// possible ways this pokemon could be obtained, and then intersect
// it with a the pokemon's existing set of all possible ways it could
// be obtained. If this intersection is non-empty, the move is legal.
// We apply several optimizations to this algorithm. The most
// important is that with, for instance, a TM move, that Pokemon
// could have been obtained from any gen at or before that TM's gen.
// Instead of adding every possible source before or during that gen,
// we keep track of a maximum gen variable, intended to mean "any
// source at or before this gen is possible."
// set of possible sources of a pokemon with this move, represented as an array
var sources = [];
// the equivalent of adding "every source at or before this gen" to sources
var sourcesBefore = 0;
var noPastGen = format.requirePentagon;
do {
alreadyChecked[template.speciesid] = true;
// Stabmons hack to avoid copying all of validateSet to formats.
if (format.id === 'stabmons' && template.types.indexOf(this.getMove(move).type) > -1) return false;
if (template.learnset) {
if (template.learnset[move] || template.learnset['sketch']) {
sometimesPossible = true;
var lset = template.learnset[move];
if (!lset || template.speciesid === 'smeargle') {
lset = template.learnset['sketch'];
sketch = true;
// Chatter, Struggle and Magikarp's Revenge cannot be sketched
if (move in {'chatter':1, 'struggle':1, 'magikarpsrevenge':1}) return true;
}
if (typeof lset === 'string') lset = [lset];
for (var i=0, len=lset.length; i<len; i++) {
var learned = lset[i];
if (noPastGen && learned.charAt(0) !== '6') continue;
if (parseInt(learned.charAt(0),10) > this.gen) continue;
if (!template.isNonstandard) {
// HMs can't be transferred
if (this.gen >= 4 && learned.charAt(0) <= 3 && move in {'cut':1, 'fly':1, 'surf':1, 'strength':1, 'flash':1, 'rocksmash':1, 'waterfall':1, 'dive':1}) continue;
if (this.gen >= 5 && learned.charAt(0) <= 4 && move in {'cut':1, 'fly':1, 'surf':1, 'strength':1, 'rocksmash':1, 'waterfall':1, 'rockclimb':1}) continue;
}
if (learned.substr(0,2) in {'4L':1,'5L':1,'6L':1}) {
// gen 4-6 level-up moves
if (level >= parseInt(learned.substr(2),10)) {
// we're past the required level to learn it
return false;
}
if (!template.gender || template.gender === 'F') {
// available as egg move
learned = learned.charAt(0)+'Eany';
} else {
// this move is unavailable, skip it
continue;
}
}
if (learned.charAt(1) in {L:1,M:1,T:1}) {
if (learned.charAt(0) === '6') {
// current-gen TM or tutor moves:
// always available
return false;
}
// past-gen level-up, TM, or tutor moves:
// available as long as the source gen was or was before this gen
limit1 = false;
sourcesBefore = Math.max(sourcesBefore, parseInt(learned.charAt(0),10));
} else if (learned.charAt(1) in {E:1,S:1,D:1}) {
// egg, event, or DW moves:
// only if that was the source
if (learned.charAt(1) === 'E') {
// it's an egg move, so we add each pokemon that can be bred with to its sources
if (learned.charAt(0) === '6') {
// gen 6 doesn't have egg move incompatibilities
sources.push('6E');
continue;
}
var eggGroups = template.eggGroups;
if (!eggGroups) continue;
if (eggGroups[0] === 'Undiscovered') eggGroups = this.getTemplate(template.evos[0]).eggGroups;
var atLeastOne = false;
var fromSelf = (learned.substr(1) === 'Eany');
learned = learned.substr(0,2);
for (var templateid in this.data.Pokedex) {
var dexEntry = this.getTemplate(templateid);
if (
// CAP pokemon can't breed
!dexEntry.isNonstandard &&
// can't breed mons from future gens
dexEntry.gen <= parseInt(learned.charAt(0),10) &&
// genderless pokemon can't pass egg moves
dexEntry.gender !== 'N') {
if (
// chainbreeding
fromSelf ||
// otherwise parent must be able to learn the move
!alreadyChecked[dexEntry.speciesid] && dexEntry.learnset && (dexEntry.learnset[move]||dexEntry.learnset['sketch'])) {
if (dexEntry.eggGroups.intersect(eggGroups).length) {
// we can breed with it
atLeastOne = true;
sources.push(learned+dexEntry.id);
}
}
}
}
// chainbreeding with itself from earlier gen
if (!atLeastOne) sources.push(learned+template.id);
} else if (learned.charAt(1) === 'S') {
sources.push(learned+' '+template.id);
} else {
sources.push(learned);
}
}
}
}
if (format.mimicGlitch && template.gen < 5) {
// include the Mimic Glitch when checking this mon's learnset
var glitchMoves = {metronome:1, copycat:1, transform:1, mimic:1, assist:1};
var getGlitch = false;
for (var i in glitchMoves) {
if (template.learnset[i]) {
if (i === 'mimic' && this.getAbility(set.ability).gen == 4 && !template.prevo) {
// doesn't get the glitch
} else {
getGlitch = true;
break;
}
}
}
if (getGlitch) {
sourcesBefore = Math.max(sourcesBefore, 4);
if (this.getMove(move).gen < 5) {
limit1 = false;
}
}
}
}
// also check to see if the mon's prevo or freely switchable formes can learn this move
if (!template.learnset && template.baseSpecies !== template.species) {
// forme takes precedence over prevo only if forme has no learnset
template = this.getTemplate(template.baseSpecies);
} else if (template.prevo) {
template = this.getTemplate(template.prevo);
} else if (template.speciesid === 'shaymin') {
template = this.getTemplate('shayminsky');
} else if (template.baseSpecies !== template.species && template.baseSpecies !== 'Kyurem') {
template = this.getTemplate(template.baseSpecies);
} else {
template = null;
}
} while (template && template.species && !alreadyChecked[template.speciesid]);
if (limit1 && sketch) {
// limit 1 sketch move
if (lsetData.sketchMove) {
return {type:'oversketched', maxSketches: 1};
}
lsetData.sketchMove = move;
}
// Now that we have our list of possible sources, intersect it with the current list
if (!sourcesBefore && !sources.length) {
if (noPastGen && sometimesPossible) return {type:'pokebank'};
return true;
}
if (!sources.length) sources = null;
if (sourcesBefore || lsetData.sourcesBefore) {
// having sourcesBefore is the equivalent of having everything before that gen
// in sources, so we fill the other array in preparation for intersection
if (sourcesBefore && lsetData.sources) {
if (!sources) sources = [];
for (var i=0, len=lsetData.sources.length; i<len; i++) {
var learned = lsetData.sources[i];
if (parseInt(learned.substr(0,1),10) <= sourcesBefore) {
sources.push(learned);
}
}
if (!lsetData.sourcesBefore) sourcesBefore = 0;
}
if (lsetData.sourcesBefore && sources) {
if (!lsetData.sources) lsetData.sources = [];
for (var i=0, len=sources.length; i<len; i++) {
var learned = sources[i];
if (parseInt(learned.substr(0,1),10) <= lsetData.sourcesBefore) {
lsetData.sources.push(learned);
}
}
if (!sourcesBefore) delete lsetData.sourcesBefore;
}
}
if (sources) {
if (lsetData.sources) {
var intersectSources = lsetData.sources.intersect(sources);
if (!intersectSources.length && !(sourcesBefore && lsetData.sourcesBefore)) {
return {type:'incompatible'};
}
lsetData.sources = intersectSources;
} else {
lsetData.sources = sources.unique();
}
}
if (sourcesBefore) {
lsetData.sourcesBefore = Math.min(sourcesBefore, lsetData.sourcesBefore||6);
}
return false;
};
Tools.prototype.getBanlistTable = function(format, subformat, depth) {
var banlistTable;
if (!depth) depth = 0;
@ -726,352 +501,6 @@ module.exports = (function () {
}
return banlistTable;
};
Tools.prototype.validateTeam = function(team, format, forceThisMod) {
format = this.getFormat(format);
if (!forceThisMod && this.isBase && format.mod !== this.currentMod) {
return this.mod(format).validateTeam(team, format, true);
}
var problems = [];
this.getBanlistTable(format);
if (format.team) {
return false;
}
if (!team || !Array.isArray(team)) {
if (format.canUseRandomTeam) {
return false;
}
return ["You sent invalid team data. If you're not using a custom client, please report this as a bug."];
}
if (!team.length) {
return ["Your team has no pokemon."];
}
if (team.length>6) {
return ["Your team has more than 6 pokemon."];
}
var teamHas = {};
for (var i=0; i<team.length; i++) {
if (!team[i]) return ["You sent invalid team data. If you're not using a custom client, please report this as a bug."];
var setProblems = this.validateSet(team[i], format, teamHas);
if (setProblems) {
problems = problems.concat(setProblems);
}
}
for (var i=0; i<format.teamBanTable.length; i++) {
var bannedCombo = '';
for (var j=0; j<format.teamBanTable[i].length; j++) {
if (!teamHas[format.teamBanTable[i][j]]) {
bannedCombo = false;
break;
}
if (j == 0) {
bannedCombo += format.teamBanTable[i][j];
} else {
bannedCombo += ' and '+format.teamBanTable[i][j];
}
}
if (bannedCombo) {
var clause = format.name ? " by "+format.name : '';
problems.push("Your team has the combination of "+bannedCombo+", which is banned"+clause+".");
}
}
if (format.ruleset) {
for (var i=0; i<format.ruleset.length; i++) {
var subformat = this.getFormat(format.ruleset[i]);
if (subformat.validateTeam) {
problems = problems.concat(subformat.validateTeam.call(this, team, format)||[]);
}
}
}
if (format.validateTeam) {
problems = problems.concat(format.validateTeam.call(this, team, format)||[]);
}
if (!problems.length) return false;
return problems;
};
Tools.prototype.validateSet = function(set, format, teamHas, forceThisMod) {
format = this.getFormat(format);
if (!forceThisMod && this.isBase && format.mod !== this.currentMod) {
return this.mod(format).validateSet(set, format, teamHas, true);
}
var problems = [];
if (!set) {
return ["This is not a Pokemon."];
}
var template = this.getTemplate(string(set.species));
if (!template.exists) {
return ["The Pokemon '"+set.species+"' does not exist."];
}
set.species = template.species;
set.name = toName(set.name);
var item = this.getItem(string(set.item));
set.item = item.name;
var ability = this.getAbility(string(set.ability));
set.ability = ability.name;
if (!Array.isArray(set.moves)) set.moves = [];
var maxLevel = format.maxLevel || 100;
var maxForcedLevel = format.maxForcedLevel || maxLevel;
if (!set.level) {
set.level = (format.defaultLevel || maxLevel);
}
if (format.forcedLevel) {
set.forcedLevel = format.forcedLevel;
} else if (set.level >= maxForcedLevel) {
set.forcedLevel = maxForcedLevel;
}
if (set.level > maxLevel || set.level == set.forcedLevel || set.level == set.maxForcedLevel) {
set.level = maxLevel;
}
var nameTemplate = this.getTemplate(set.name);
if (nameTemplate.exists && nameTemplate.name.toLowerCase() === set.name.toLowerCase()) set.name = null;
set.species = set.species;
set.name = set.name || set.species;
var name = set.species;
if (set.species !== set.name) name = set.name + " ("+set.species+")";
var isHidden = false;
var lsetData = {set:set, format:format};
var setHas = {};
if (!template || !template.abilities) {
set.species = 'Unown';
template = this.getTemplate('Unown');
}
if (format.ruleset) {
for (var i=0; i<format.ruleset.length; i++) {
var subformat = this.getFormat(format.ruleset[i]);
if (subformat.validateSet) {
problems = problems.concat(subformat.validateSet.call(this, set, format)||[]);
}
}
}
template = this.getTemplate(set.species);
item = this.getItem(set.item);
ability = this.getAbility(set.ability);
var banlistTable = this.getBanlistTable(format);
var check = toId(set.species);
var clause = '';
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(set.species+' is banned'+clause+'.');
}
check = toId(set.ability);
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s ability "+set.ability+" is banned"+clause+".");
}
check = toId(set.item);
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s item "+set.item+" is banned"+clause+".");
}
if (banlistTable['illegal'] && item.isUnreleased) {
problems.push(name+"'s item "+set.item+" is unreleased.");
}
if (banlistTable['Unreleased'] && template.isUnreleased) {
if (!format.requirePentagon || (template.eggGroups[0] === 'Undiscovered' && !template.evos)) {
problems.push(name+" ("+template.species+") is unreleased.");
}
}
setHas[toId(set.ability)] = true;
if (banlistTable['illegal']) {
var totalEV = 0;
for (var k in set.evs) {
if (typeof set.evs[k] !== 'number') {
set.evs[k] = 0;
}
totalEV += set.evs[k];
}
// In gen 1 and 2, it was possible to max out all EVs
if (this.gen >= 3 && totalEV > 510) {
problems.push(name+" has more than 510 total EVs.");
}
// Don't check abilities for metagames with All Abilities
if (this.gen <= 2) {
set.ability = 'None';
} else if (!banlistTable['ignoreillegalabilities']) {
if (!ability.name) {
problems.push(name+" needs to have an ability.");
} else if (ability.name !== template.abilities['0'] &&
ability.name !== template.abilities['1'] &&
ability.name !== template.abilities['H']) {
problems.push(name+" can't have "+set.ability+".");
}
if (ability.name === template.abilities['H']) {
isHidden = true;
if (template.unreleasedHidden && banlistTable['illegal']) {
problems.push(name+"'s hidden ability is unreleased.");
} else if (this.gen === 5 && set.level < 10 && (template.maleOnlyHidden || template.gender === 'N')) {
problems.push(name+" must be at least level 10 with its hidden ability.");
}
if (template.maleOnlyHidden) {
set.gender = 'M';
lsetData.sources = ['5D'];
}
}
}
}
if (set.moves && Array.isArray(set.moves)) {
set.moves = set.moves.filter(function(val){ return val; });
}
if (!set.moves || !set.moves.length) {
problems.push(name+" has no moves.");
} else {
// A limit is imposed here to prevent too much engine strain or
// too much layout deformation - to be exact, this is the Debug
// Mode limitation.
// The usual limit of 4 moves is handled elsewhere - currently
// in the cartridge-compliant set validator: formats.js:pokemon
set.moves = set.moves.slice(0,24);
for (var i=0; i<set.moves.length; i++) {
if (!set.moves[i]) continue;
var move = this.getMove(string(set.moves[i]));
set.moves[i] = move.name;
check = move.id;
setHas[check] = true;
if (banlistTable[check]) {
clause = typeof banlistTable[check] === 'string' ? " by "+ banlistTable[check] : '';
problems.push(name+"'s move "+set.moves[i]+" is banned"+clause+".");
}
if (banlistTable['illegal']) {
var problem = this.checkLearnset(move, template, lsetData);
if (problem) {
var problemString = name+" can't learn "+move.name;
if (problem.type === 'incompatible') {
if (isHidden) {
problemString = problemString.concat(" because it's incompatible with its ability or another move.");
} else {
problemString = problemString.concat(" because it's incompatible with another move.");
}
} else if (problem.type === 'oversketched') {
problemString = problemString.concat(" because it can only sketch "+problem.maxSketches+" move"+(problem.maxSketches>1?"s":"")+".");
} else if (problem.type === 'pokebank') {
problemString = problemString.concat(" because it's only obtainable from a previous generation.");
} else {
problemString = problemString.concat(".");
}
problems.push(problemString);
}
}
}
if (lsetData.sources && lsetData.sources.length === 1 && !lsetData.sourcesBefore) {
// we're restricted to a single source
var source = lsetData.sources[0];
if (source.substr(1,1) === 'S') {
// it's an event
var eventData = null;
var splitSource = source.substr(2).split(' ');
var eventTemplate = this.getTemplate(splitSource[1]);
if (eventTemplate.eventPokemon) eventData = eventTemplate.eventPokemon[parseInt(splitSource[0],10)];
if (eventData) {
if (eventData.nature && eventData.nature !== set.nature) {
problems.push(name+" must have a "+eventData.nature+" nature because it comes from a specific event.");
}
if (eventData.shiny) {
set.shiny = true;
}
if (eventData.generation < 5) eventData.isHidden = false;
if (eventData.isHidden !== undefined && eventData.isHidden !== isHidden) {
problems.push(name+(isHidden?" can't have":" must have")+" its hidden ability because it comes from a specific event.");
}
if (this.gen <= 5 && eventData.abilities && eventData.abilities.indexOf(ability.id) < 0) {
problems.push(name+" must have "+eventData.abilities.join(" or ")+" because it comes from a specific event.");
}
if (eventData.gender) {
set.gender = eventData.gender;
}
if (eventData.level && set.level < eventData.level) {
problems.push(name+" must be at least level "+eventData.level+" because it comes from a specific event.");
}
}
isHidden = false;
}
}
if (isHidden && lsetData.sourcesBefore < 5) {
if (!lsetData.sources) {
problems.push(name+" has a hidden ability - it can't have moves only learned before gen 5.");
} else if (template.gender) {
var compatibleSource = false;
for (var i=0,len=lsetData.sources.length; i<len; i++) {
if (lsetData.sources[i].charAt(1) === 'E' || (lsetData.sources[i].substr(0,2) === '5D' && set.level >= 10)) {
compatibleSource = true;
break;
}
}
if (!compatibleSource) {
problems.push(name+" has moves incompatible with its hidden ability.");
}
}
}
if (set.level < template.evoLevel) {
// FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive
problems.push(name+" must be at least level "+template.evoLevel+".");
}
if (!lsetData.sources && lsetData.sourcesBefore <= 3 && this.getAbility(set.ability).gen === 4 && !template.prevo && this.gen <= 5) {
problems.push(name+" has a gen 4 ability and isn't evolved - it can't use anything from gen 3.");
}
if (!lsetData.sources && lsetData.sourcesBefore >= 3 && (isHidden || this.gen <= 5) && template.gen <= lsetData.sourcesBefore) {
var oldAbilities = this.mod('gen'+lsetData.sourcesBefore).getTemplate(set.species).abilities;
if (ability.name !== oldAbilities['0'] && ability.name !== oldAbilities['1'] && ability.name !== oldAbilities['H']) {
problems.push(name+" has moves incompatible with its ability.");
}
}
}
setHas[toId(template.tier)] = true;
if (banlistTable[template.tier]) {
problems.push(name+" is in "+template.tier+", which is banned.");
}
if (teamHas) {
for (var i in setHas) {
teamHas[i] = true;
}
}
for (var i=0; i<format.setBanTable.length; i++) {
var bannedCombo = '';
for (var j=0; j<format.setBanTable[i].length; j++) {
if (!setHas[format.setBanTable[i][j]]) {
bannedCombo = false;
break;
}
if (j == 0) {
bannedCombo += format.setBanTable[i][j];
} else {
bannedCombo += ' and '+format.setBanTable[i][j];
}
}
if (bannedCombo) {
clause = format.name ? " by "+format.name : '';
problems.push(name+" has the combination of "+bannedCombo+", which is banned"+clause+".");
}
}
if (format.validateSet) {
problems = problems.concat(format.validateSet.call(this, set, format)||[]);
}
if (!problems.length) return false;
return problems;
};
Tools.prototype.levenshtein = function(s, t, l) { // s = string 1, t = string 2, l = limit
// Original levenshtein distance function by James Westgate, turned out to be the fastest

View File

@ -1137,7 +1137,7 @@ var User = (function () {
return false;
}
var team = this.team;
var problems = Tools.validateTeam(team, formatid);
var problems = TeamValidator(formatid).validateTeam(team);
if (problems) {
connection.popup("Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
return false;