Refactor banlistTable -> ruleTable

PS's rule table has been renamed from banlistTable, and works a bit
differently now. It's a Map instead of an object now, and the keys
work a bit differently.

The original banlistTable was designed to store bans, and later
additions shoved rules and then unbans in there. The new table is
designed to support all of these.
This commit is contained in:
Guangcong Luo 2017-07-20 12:08:06 -05:00
parent 341648c8ea
commit d79e348ebc
9 changed files with 216 additions and 257 deletions

View File

@ -57,7 +57,7 @@ exports.BattleFormats = {
onValidateTeam: function (team, format) {
let problems = [];
// ----------- legality line ------------------------------------------
if (!format || !format.banlistTable || !format.banlistTable['illegal']) return problems;
if (!format || !this.getRuleTable(format).has('-illegal')) return problems;
// everything after this line only happens if we're doing legality enforcement
let kyurems = 0;
for (let i = 0; i < team.length; i++) {
@ -76,7 +76,7 @@ exports.BattleFormats = {
let template = this.getTemplate(set.species);
let problems = [];
let totalEV = 0;
let allowCAP = !!(format && format.banlistTable && format.banlistTable['Rule:allowcap']);
let allowCAP = !!(format && this.getRuleTable(format).has('allowcap'));
if (set.species === set.name) delete set.name;
if (template.gen > this.gen) {
@ -142,7 +142,7 @@ exports.BattleFormats = {
}
// ----------- legality line ------------------------------------------
if (!format.banlistTable || !format.banlistTable['illegal']) return problems;
if (!this.getRuleTable(format).has('-illegal')) return problems;
// everything after this line only happens if we're doing legality enforcement
// only in gen 1 and 2 it was legal to max out all EVs
@ -231,7 +231,7 @@ exports.BattleFormats = {
// Autofixed forme.
template = this.getTemplate(set.species);
if (!format.banlistTable['Rule:ignoreillegalabilities'] && !format.noChangeAbility) {
if (!this.getRuleTable(format).has('ignoreillegalabilities') && !format.noChangeAbility) {
// Ensure that the ability is (still) legal.
let legalAbility = false;
for (let i in template.abilities) {
@ -256,7 +256,7 @@ exports.BattleFormats = {
"Abra":1, "Absol":1, "Aggron":1, "Alakazam":1, "Altaria":1, "Anorith":1, "Armaldo":1, "Aron":1, "Azumarill":1, "Azurill":1, "Bagon":1, "Baltoy":1, "Banette":1, "Barboach":1, "Beautifly":1, "Beldum":1, "Bellossom":1, "Blaziken":1, "Breloom":1, "Budew":1, "Cacnea":1, "Cacturne":1, "Camerupt":1, "Carvanha":1, "Cascoon":1, "Castform":1, "Chimecho":1, "Chinchou":1, "Chingling":1, "Clamperl":1, "Claydol":1, "Combusken":1, "Corphish":1, "Corsola":1, "Cradily":1, "Crawdaunt":1, "Crobat":1, "Delcatty":1, "Dodrio":1, "Doduo":1, "Donphan":1, "Dusclops":1, "Dusknoir":1, "Duskull":1, "Dustox":1, "Electrike":1, "Electrode":1, "Exploud":1, "Feebas":1, "Flygon":1, "Froslass":1, "Gallade":1, "Gardevoir":1, "Geodude":1, "Girafarig":1, "Glalie":1, "Gloom":1, "Golbat":1, "Goldeen":1, "Golduck":1, "Golem":1, "Gorebyss":1, "Graveler":1, "Grimer":1, "Grovyle":1, "Grumpig":1, "Gulpin":1, "Gyarados":1, "Hariyama":1, "Heracross":1, "Horsea":1, "Huntail":1, "Igglybuff":1, "Illumise":1, "Jigglypuff":1, "Kadabra":1, "Kecleon":1, "Kingdra":1, "Kirlia":1, "Koffing":1, "Lairon":1, "Lanturn":1, "Latias":1, "Latios":1, "Lileep":1, "Linoone":1, "Lombre":1, "Lotad":1, "Loudred":1, "Ludicolo":1, "Lunatone":1, "Luvdisc":1, "Machamp":1, "Machoke":1, "Machop":1, "Magcargo":1, "Magikarp":1, "Magnemite":1, "Magneton":1, "Magnezone":1, "Makuhita":1, "Manectric":1, "Marill":1, "Marshtomp":1, "Masquerain":1, "Mawile":1, "Medicham":1, "Meditite":1, "Metagross":1, "Metang":1, "Mightyena":1, "Milotic":1, "Minun":1, "Mudkip":1, "Muk":1, "Natu":1, "Nincada":1, "Ninetales":1, "Ninjask":1, "Nosepass":1, "Numel":1, "Nuzleaf":1, "Oddish":1, "Pelipper":1, "Phanpy":1, "Pichu":1, "Pikachu":1, "Pinsir":1, "Plusle":1, "Poochyena":1, "Probopass":1, "Psyduck":1, "Raichu":1, "Ralts":1, "Regice":1, "Regirock":1, "Registeel":1, "Relicanth":1, "Rhydon":1, "Rhyhorn":1, "Rhyperior":1, "Roselia":1, "Roserade":1, "Sableye":1, "Salamence":1, "Sandshrew":1, "Sandslash":1, "Sceptile":1, "Seadra":1, "Seaking":1, "Sealeo":1, "Seedot":1, "Seviper":1, "Sharpedo":1, "Shedinja":1, "Shelgon":1, "Shiftry":1, "Shroomish":1, "Shuppet":1, "Silcoon":1, "Skarmory":1, "Skitty":1, "Slaking":1, "Slakoth":1, "Slugma":1, "Snorunt":1, "Solrock":1, "Spheal":1, "Spinda":1, "Spoink":1, "Starmie":1, "Staryu":1, "Surskit":1, "Swablu":1, "Swalot":1, "Swampert":1, "Swellow":1, "Taillow":1, "Tentacool":1, "Tentacruel":1, "Torchic":1, "Torkoal":1, "Trapinch":1, "Treecko":1, "Tropius":1, "Vibrava":1, "Vigoroth":1, "Vileplume":1, "Volbeat":1, "Voltorb":1, "Vulpix":1, "Wailmer":1, "Wailord":1, "Walrein":1, "Weezing":1, "Whiscash":1, "Whismur":1, "Wigglytuff":1, "Wingull":1, "Wobbuffet":1, "Wurmple":1, "Wynaut":1, "Xatu":1, "Zangoose":1, "Zigzagoon":1, "Zubat":1,
};
let template = this.getTemplate(set.species || set.name);
if (!(template.baseSpecies in hoennDex) && format.banlistTable[template.speciesid] !== false) {
if (!(template.baseSpecies in hoennDex) && !this.getRuleTable(format).has('+' + template.speciesid)) {
return [template.baseSpecies + " is not in the Hoenn Pokédex."];
}
},
@ -269,7 +269,7 @@ exports.BattleFormats = {
"Rowlet":1, "Dartrix":1, "Decidueye":1, "Litten":1, "Torracat":1, "Incineroar":1, "Popplio":1, "Brionne":1, "Primarina":1, "Pikipek":1, "Trumbeak":1, "Toucannon":1, "Yungoos":1, "Gumshoos":1, "Rattata-Alola":1, "Raticate-Alola":1, "Caterpie":1, "Metapod":1, "Butterfree":1, "Ledyba":1, "Ledian":1, "Spinarak":1, "Ariados":1, "Pichu":1, "Pikachu":1, "Raichu-Alola":1, "Grubbin":1, "Charjabug":1, "Vikavolt":1, "Bonsly":1, "Sudowoodo":1, "Happiny":1, "Chansey":1, "Blissey":1, "Munchlax":1, "Snorlax":1, "Slowpoke":1, "Slowbro":1, "Slowking":1, "Wingull":1, "Pelipper":1, "Abra":1, "Kadabra":1, "Alakazam":1, "Meowth-Alola":1, "Persian-Alola":1, "Magnemite":1, "Magneton":1, "Magnezone":1, "Grimer-Alola":1, "Muk-Alola":1, "Growlithe":1, "Arcanine":1, "Drowzee":1, "Hypno":1, "Makuhita":1, "Hariyama":1, "Smeargle":1, "Crabrawler":1, "Crabominable":1, "Gastly":1, "Haunter":1, "Gengar":1, "Drifloon":1, "Drifblim":1, "Misdreavus":1, "Mismagius":1, "Zubat":1, "Golbat":1, "Crobat":1, "Diglett-Alola":1, "Dugtrio-Alola":1, "Spearow":1, "Fearow":1, "Rufflet":1, "Braviary":1, "Vullaby":1, "Mandibuzz":1, "Mankey":1, "Primeape":1, "Delibird":1, "Oricorio":1, "Cutiefly":1, "Ribombee":1, "Petilil":1, "Lilligant":1, "Cottonee":1, "Whimsicott":1, "Psyduck":1, "Golduck":1, "Magikarp":1, "Gyarados":1, "Barboach":1, "Whiscash":1, "Machop":1, "Machoke":1, "Machamp":1, "Roggenrola":1, "Boldore":1, "Gigalith":1, "Carbink":1, "Sableye":1, "Rockruff":1, "Lycanroc":1, "Spinda":1, "Tentacool":1, "Tentacruel":1, "Finneon":1, "Lumineon":1, "Wishiwashi":1, "Luvdisc":1, "Corsola":1, "Mareanie":1, "Toxapex":1, "Shellder":1, "Cloyster":1, "Bagon":1, "Shelgon":1, "Salamence":1, "Lillipup":1, "Herdier":1, "Stoutland":1, "Eevee":1, "Vaporeon":1, "Jolteon":1, "Flareon":1, "Espeon":1, "Umbreon":1, "Leafeon":1, "Glaceon":1, "Sylveon":1, "Mudbray":1, "Mudsdale":1, "Igglybuff":1, "Jigglypuff":1, "Wigglytuff":1, "Tauros":1, "Miltank":1, "Surskit":1, "Masquerain":1, "Dewpider":1, "Araquanid":1, "Fomantis":1, "Lurantis":1, "Morelull":1, "Shiinotic":1, "Paras":1, "Parasect":1, "Poliwag":1, "Poliwhirl":1, "Poliwrath":1, "Politoed":1, "Goldeen":1, "Seaking":1, "Feebas":1, "Milotic":1, "Alomomola":1, "Fletchling":1, "Fletchinder":1, "Talonflame":1, "Salandit":1, "Salazzle":1, "Cubone":1, "Marowak-Alola":1, "Kangaskhan":1, "Magby":1, "Magmar":1, "Magmortar":1, "Stufful":1, "Bewear":1, "Bounsweet":1, "Steenee":1, "Tsareena":1, "Comfey":1, "Pinsir":1, "Oranguru":1, "Passimian":1, "Goomy":1, "Sliggoo":1, "Goodra":1, "Castform":1, "Wimpod":1, "Golisopod":1, "Staryu":1, "Starmie":1, "Sandygast":1, "Palossand":1, "Cranidos":1, "Rampardos":1, "Shieldon":1, "Bastiodon":1, "Archen":1, "Archeops":1, "Tirtouga":1, "Carracosta":1, "Phantump":1, "Trevenant":1, "Nosepass":1, "Probopass":1, "Pyukumuku":1, "Chinchou":1, "Lanturn":1, "Type: Null":1, "Silvally":1, "Zygarde":1, "Trubbish":1, "Garbodor":1, "Skarmory":1, "Ditto":1, "Cleffa":1, "Clefairy":1, "Clefable":1, "Minior":1, "Beldum":1, "Metang":1, "Metagross":1, "Porygon":1, "Porygon2":1, "Porygon-Z":1, "Pancham":1, "Pangoro":1, "Komala":1, "Torkoal":1, "Turtonator":1, "Togedemaru":1, "Elekid":1, "Electabuzz":1, "Electivire":1, "Geodude-Alola":1, "Graveler-Alola":1, "Golem-Alola":1, "Sandile":1, "Krokorok":1, "Krookodile":1, "Trapinch":1, "Vibrava":1, "Flygon":1, "Gible":1, "Gabite":1, "Garchomp":1, "Klefki":1, "Mimikyu":1, "Bruxish":1, "Drampa":1, "Absol":1, "Snorunt":1, "Glalie":1, "Froslass":1, "Sneasel":1, "Weavile":1, "Sandshrew-Alola":1, "Sandslash-Alola":1, "Vulpix-Alola":1, "Ninetales-Alola":1, "Vanillite":1, "Vanillish":1, "Vanilluxe":1, "Snubbull":1, "Granbull":1, "Shellos":1, "Gastrodon":1, "Relicanth":1, "Dhelmise":1, "Carvanha":1, "Sharpedo":1, "Wailmer":1, "Wailord":1, "Lapras":1, "Exeggcute":1, "Exeggutor-Alola":1, "Jangmo-o":1, "Hakamo-o":1, "Kommo-o":1, "Emolga":1, "Scyther":1, "Scizor":1, "Murkrow":1, "Honchkrow":1, "Riolu":1, "Lucario":1, "Dratini":1, "Dragonair":1, "Dragonite":1, "Aerodactyl":1, "Tapu Koko":1, "Tapu Lele":1, "Tapu Bulu":1, "Tapu Fini":1, "Cosmog":1, "Cosmoem":1, "Solgaleo":1, "Lunala":1, "Nihilego":1, "Buzzwole":1, "Pheromosa":1, "Xurkitree":1, "Celesteela":1, "Kartana":1, "Guzzlord":1, "Necrozma":1, "Magearna":1, "Marshadow":1,
};
let template = this.getTemplate(set.species || set.name);
if (!(template.baseSpecies in alolaDex) && !(template.species in alolaDex) && format.banlistTable[template.speciesid] !== false) {
if (!(template.baseSpecies in alolaDex) && !this.getRuleTable(format).has('+' + template.speciesid)) {
return [template.baseSpecies + " is not in the Alola Pokédex."];
}
},

View File

@ -2411,7 +2411,7 @@ exports.BattleScripts = {
// PotD stuff
let potd;
if (Config.potd && 'Rule:potd' in this.getBanlistTable(this.getFormat())) {
if (Config.potd && this.getRuleTable(this.getFormat()).has('potd')) {
potd = this.getTemplate(Config.potd);
}

View File

@ -17,7 +17,9 @@ const MONITOR_CLEAN_TIMEOUT = 2 * 60 * 60 * 1000;
* delta of time since the last time it was committed. Actions include
* connecting to the server, starting a battle, validating a team, and
* sending/receiving data over a connection's socket.
* @augments {Map<string, [number, number]>}
*/
// @ts-ignore TypeScript bug
class TimedCounter extends Map {
/**
* Increments the number of times an action has been committed by one, and

View File

@ -69,7 +69,10 @@ Punishments.ips = new PunishmentMap();
*/
Punishments.userids = new PunishmentMap();
class NestedPunishmentMap extends Map/*:: <string, Map<string, Punishment>> */ {
/**
* @augments {Map<string, Punishment>}
*/
class NestedPunishmentMap extends Map {
nestedSet(k1, k2, value) {
if (!this.get(k1)) {
this.set(k1, new Map());

View File

@ -30,7 +30,6 @@ class Battle extends Dex.ModdedDex {
this.format = toId(format);
this.formatData = {id:this.format};
Dex.mod(format.mod).getBanlistTable(format); // fill in format ruleset
this.ruleset = format.ruleset;
this.effect = {id:''};
@ -217,8 +216,8 @@ class Battle extends Dex.ModdedDex {
return this.getEffect(this.terrain);
}
getFormat() {
return this.getEffect(this.format);
getFormat(format) {
return super.getFormat(format || this.format);
}
addPseudoWeather(status, source, sourceEffect) {
status = this.getEffect(status);
@ -1221,8 +1220,8 @@ class Battle extends Dex.ModdedDex {
// to run it again.
continue;
}
let banlistTable = this.getFormat().banlistTable;
if (banlistTable && !('illegal' in banlistTable) && !this.getFormat().team) {
const ruleTable = this.getRuleTable(this.getFormat());
if (!ruleTable.has('-illegal') && !this.getFormat().team) {
// hackmons format
continue;
} else if (abilitySlot === 'H' && template.unreleasedHidden) {
@ -1230,7 +1229,7 @@ class Battle extends Dex.ModdedDex {
continue;
}
let ability = this.getAbility(abilityName);
if (banlistTable && ability.id in banlistTable) continue;
if (ruleTable.has('-' + ability.id)) continue;
if (pokemon.knownType && !this.getImmunity('trapped', pokemon)) continue;
this.singleEvent('FoeMaybeTrapPokemon',
ability, {}, pokemon, source);
@ -1294,8 +1293,8 @@ class Battle extends Dex.ModdedDex {
this.sides[i].faintedLastTurn = this.sides[i].faintedThisTurn;
this.sides[i].faintedThisTurn = false;
}
let banlistTable = this.getFormat().banlistTable;
if (banlistTable && 'Rule:endlessbattleclause' in banlistTable) {
const ruleTable = this.getRuleTable(this.getFormat());
if (ruleTable.has('endlessbattleclause')) {
if (oneStale) {
let activationWarning = '<br />If all active Pok&eacute;mon go in an endless loop, Endless Battle Clause will activate.';
if (allStale) activationWarning = '';

View File

@ -58,8 +58,9 @@ class Effect {
/**
* @param {AnyObject} data
* @param {?AnyObject} [moreData]
* @param {?AnyObject} [moreData2]
*/
constructor(data, moreData = null) {
constructor(data, moreData = null, moreData2 = null) {
/**
* ID. This will be a lowercase version of the name with all the
* non-alphanumeric characters removed. So, for instance, "Mr. Mime"
@ -112,6 +113,7 @@ class Effect {
Object.assign(this, data);
if (moreData) Object.assign(this, moreData);
if (moreData2) Object.assign(this, moreData2);
this.name = Tools.getString(this.name).trim();
this.fullname = Tools.getString(this.fullname) || this.name;
if (!this.id) this.id = toId(this.name); // Hidden Power hack
@ -124,13 +126,56 @@ class Effect {
}
}
/**
* A RuleTable keeps track of the rules that a format has. The key can be:
* - '[ruleid]' the ID of a rule in effect
* - '-[thing]' or '-[category]:[thing]' ban a thing
* - '+[thing]' or '+[category]:[thing]' allow a thing (override a ban)
* [category] is one of: item, move, ability, species, basespecies
* @augments {Map<string, string>}
*/
// @ts-ignore TypeScript bug
class RuleTable extends Map {
constructor() {
super();
/**
* rule, source, limit, bans
* @type {[string, string, number, string[]][]}
*/
this.complexBans = [];
/**
* rule, source, limit, bans
* @type {[string, string, number, string[]][]}
*/
this.complexTeamBans = [];
}
/**
* @param {string} thing
* @param {{[id: string]: true}} setHas
* @return {string}
*/
check(thing, setHas) {
setHas[thing] = true;
return this.getReason(this.get('-' + thing));
}
/**
* @param {string | undefined} source
* @return {string}
*/
getReason(source) {
if (source === undefined) return '';
return source ? `banned by ${source}` : `banned`;
}
}
class Format extends Effect {
/**
* @param {AnyObject} data
* @param {?AnyObject} [moreData]
* @param {?AnyObject} [moreData2]
*/
constructor(data, moreData = null) {
super(data, moreData);
constructor(data, moreData = null, moreData2 = null) {
super(data, moreData, moreData2);
/** @type {string} */
this.mod = Tools.getString(this.mod) || 'gen6';
/** @type {'Format' | 'Ruleset' | 'Rule' | 'ValidatorRule'} */
@ -161,12 +206,12 @@ class Format extends Effect {
* List of ruleset and banlist changes in a custom format.
* @type {?string[]}
*/
this.customBanlist = this.customBanlist || null;
this.customRules = this.customRules || null;
/**
* Table of rule names and banned effects.
* @type {?{[mod: string]: string | boolean}}
* @type {?RuleTable}
*/
this.banlistTable = this.banlistTable;
this.ruleTable = null;
}
}
@ -559,6 +604,7 @@ class Move extends Effect {
exports.Tools = Tools;
exports.Effect = Effect;
exports.PureEffect = PureEffect;
exports.RuleTable = RuleTable;
exports.Format = Format;
exports.Item = Item;
exports.Template = Template;

View File

@ -46,7 +46,7 @@ const fs = require('fs');
const path = require('path');
const Data = require('./dex-data');
const {Effect, PureEffect, Format, Item, Template, Move, Ability} = Data; // eslint-disable-line no-unused-vars
const {Effect, PureEffect, RuleTable, Format, Item, Template, Move, Ability} = Data; // eslint-disable-line no-unused-vars
const DATA_DIR = path.resolve(__dirname, '../data');
const MODS_DIR = path.resolve(__dirname, '../mods');
@ -488,39 +488,34 @@ class ModdedDex {
let format = this.data.Formats[id];
if (customBanlist) {
if (typeof customBanlist === 'string') customBanlist = customBanlist.split(',');
if (!format.banlistTable) this.getBanlistTable(format);
format = Object.assign({}, format);
format.customBanlist = customBanlist;
format.banlist = format.banlist ? format.banlist.slice() : [];
format.unbanlist = format.unbanlist ? format.unbanlist.slice() : [];
format.ruleset = format.baseRuleset.slice();
for (let i = 0; i < customBanlist.length; i++) {
let ban = customBanlist[i];
let customRules = [];
for (let ban of customBanlist) {
let unban = false;
if (ban.charAt(0) === '!') {
unban = true;
ban = ban.substr(1);
}
if (ban.startsWith('Rule:')) {
ban = ban.substr(5);
ban = ban.substr(5).trim();
if (unban) {
ban = 'Rule:' + toId(ban);
if (!format.unbanlist.includes(ban)) format.unbanlist.push(ban);
customRules.unshift('!' + ban);
} else {
if (!format.ruleset.includes(ban)) format.ruleset.push(ban);
customRules.push(ban);
}
} else {
if (unban) {
if (!format.unbanlist.includes(ban)) format.unbanlist.push(ban);
customRules.push('+' + ban);
} else {
if (!format.banlist.includes(ban)) format.banlist.push(ban);
customRules.push('-' + ban);
}
}
}
delete format.banlistTable;
effect = new Data.Format({name}, format, {customRules});
if (customId === 'pokemon') throw new Error('wtf');
if (customId) this.data.Formats[customId] = format;
} else {
effect = new Data.Format({name}, format);
}
effect = new Data.Format({name}, format);
} else if (this.data.Formats.hasOwnProperty(name)) {
effect = new Data.Format({name}, this.data.Formats[name]);
} else {
@ -692,142 +687,86 @@ class ModdedDex {
}
/**
* @param {AnyObject} format
* @param {AnyObject} [subformat]
* @param {Format} format
* @param {number} [depth = 0]
* @return {AnyObject}
* @return {RuleTable}
*/
getBanlistTable(format, subformat, depth = 0) {
let banlistTable;
if (depth > 8) return {}; // avoid infinite recursion
if (format.banlistTable && !subformat) {
banlistTable = format.banlistTable;
} else {
if (!format.banlistTable) format.banlistTable = {};
if (!format.setBanTable) format.setBanTable = [];
if (!format.teamBanTable) format.teamBanTable = [];
if (!format.teamLimitTable) format.teamLimitTable = [];
getRuleTable(format, depth = 0) {
/** @type {RuleTable} */
let ruleTable = new RuleTable();
if (format.ruleTable) return format.ruleTable;
banlistTable = format.banlistTable;
if (!subformat) subformat = format;
if (subformat.unbanlist) {
for (let i = 0; i < subformat.unbanlist.length; i++) {
banlistTable[subformat.unbanlist[i]] = false;
banlistTable[toId(subformat.unbanlist[i])] = false;
}
}
if (subformat.banlist) {
for (let i = 0; i < subformat.banlist.length; i++) {
// don't revalidate what we already validate
if (banlistTable[toId(subformat.banlist[i])] !== undefined) continue;
banlistTable[subformat.banlist[i]] = subformat.name || true;
banlistTable[toId(subformat.banlist[i])] = subformat.name || true;
let complexList;
if (subformat.banlist[i].includes('>')) {
complexList = subformat.banlist[i].split('>');
let limit = parseInt(complexList[1]);
let banlist = complexList[0].trim();
complexList = banlist.split('+').map(toId);
complexList.unshift(banlist, subformat.name, limit);
format.teamLimitTable.push(complexList);
} else if (subformat.banlist[i].includes('+')) {
if (subformat.banlist[i].includes('++')) {
complexList = subformat.banlist[i].split('++');
let banlist = complexList.join('+');
for (let j = 0; j < complexList.length; j++) {
complexList[j] = toId(complexList[j]);
}
complexList.unshift(banlist);
format.teamBanTable.push(complexList);
} else {
complexList = subformat.banlist[i].split('+');
for (let j = 0; j < complexList.length; j++) {
complexList[j] = toId(complexList[j]);
}
complexList.unshift(subformat.banlist[i]);
format.setBanTable.push(complexList);
}
}
}
}
if (subformat.ruleset) {
for (let i = 0; i < subformat.ruleset.length; i++) {
// don't revalidate what we already validate
if (banlistTable['Rule:' + toId(subformat.ruleset[i])] !== undefined) continue;
banlistTable['Rule:' + toId(subformat.ruleset[i])] = subformat.ruleset[i];
if (!format.ruleset.includes(subformat.ruleset[i])) format.ruleset.push(subformat.ruleset[i]);
let subsubformat = this.getFormat(subformat.ruleset[i]);
if (subsubformat.ruleset || subsubformat.banlist) {
this.getBanlistTable(format, subsubformat, depth + 1);
}
const ruleset = format.ruleset.slice();
for (const ban of format.banlist) {
ruleset.push('-' + ban);
}
for (const ban of format.unbanlist) {
ruleset.push('+' + ban);
}
if (format.customRules) {
for (const rule of format.customRules) {
if (rule.startsWith('!')) {
ruleset.unshift(rule);
} else {
ruleset.push(rule);
}
}
}
return banlistTable;
}
/**
* @param {string | Format} format
* @param {string | string[]} params
* @return {string[]}
*/
getSupplementaryBanlist(format, params) {
format = this.getFormat(format);
if (typeof params === 'string') params = params.split(',');
if (!format.banlistTable) format.banlistTable = this.getBanlistTable(format);
let banlist = [];
for (let i = 0; i < params.length; i++) {
let param = params[i].trim();
let unban = false;
if (param.charAt(0) === '!') {
unban = true;
param = param.substr(1);
}
let ban, oppositeBan;
let subformat = this.getFormat(param);
if (subformat.effectType === 'ValidatorRule' || subformat.effectType === 'Rule' || subformat.effectType === 'Format') {
if (unban) {
if (format.banlistTable['Rule:' + subformat.id] === false) continue;
} else {
if (format.banlistTable['Rule:' + subformat.id]) continue;
for (const rule of ruleset) {
if (rule.charAt(0) === '-' || rule.charAt(0) === '+') { // ban or unban
const type = rule.charAt(0);
let buf = rule.slice(0);
const gtIndex = buf.lastIndexOf('>');
let limit = 1;
if (gtIndex >= 0 && /^[0-9]+$/.test(buf.slice(gtIndex + 1).trim())) {
limit = parseInt(buf.slice(gtIndex + 1));
buf = buf.slice(0, gtIndex);
}
ban = 'Rule:' + subformat.name;
} else {
param = param.toLowerCase();
let baseForme = false;
if (param.endsWith('-base')) {
baseForme = true;
param = param.substr(0, param.length - 5);
let checkTeam = buf.includes('++');
const banNames = buf.split(checkTeam ? '++' : '+').map(v => v.trim());
if (banNames.length === 1 && limit > 1) checkTeam = true;
const innerRule = banNames.join(checkTeam ? ' ++ ' : ' + ');
const bans = banNames.map(v => toId(v));
if (checkTeam) {
ruleTable.complexTeamBans.push([innerRule, '', limit, bans]);
continue;
}
let search = this.dataSearch(param);
if (!search || search.length < 1) continue;
if (search[0].isInexact || search[0].searchType === 'nature') continue;
ban = search[0].name;
if (baseForme) ban += '-Base';
if (unban) {
if (format.banlistTable[ban] === false) continue;
} else {
if (format.banlistTable[ban]) continue;
if (bans.length > 1 || limit !== 1) {
ruleTable.complexBans.push([innerRule, '', limit, bans]);
}
const ban = toId(buf);
ruleTable.delete('+' + ban);
ruleTable.delete('-' + ban);
ruleTable.set(type + ban, '');
continue;
}
if (unban) {
oppositeBan = ban;
ban = '!' + ban;
} else {
oppositeBan = '!' + ban;
if (rule.startsWith('!')) {
ruleTable.set('!' + toId(rule), '');
continue;
}
let index = banlist.indexOf(oppositeBan);
if (index > -1) {
banlist.splice(index, 1);
} else {
banlist.push(ban);
const subformat = this.getFormat(rule);
if (ruleTable.has('!' + subformat.id)) continue;
ruleTable.set(subformat.id, '');
if (!subformat.exists) continue;
if (depth > 16) {
throw new Error(`Excessive ruleTable recursion in ${format.name}: ${rule} of ${format.ruleset}`);
}
const subRuleTable = this.getRuleTable(subformat, depth + 1);
subRuleTable.forEach((v, k) => {
ruleTable.set(k, v || subformat.name);
});
for (const [rule, source, limit, bans] of subRuleTable.complexBans) {
ruleTable.complexBans.push([rule, source || subformat.name, limit, bans]);
}
for (const [rule, source, limit, bans] of subRuleTable.complexTeamBans) {
ruleTable.complexTeamBans.push([rule, source || subformat.name, limit, bans]);
}
}
return banlist;
format.ruleTable = ruleTable;
return ruleTable;
}
/**

View File

@ -12,10 +12,6 @@
let TeamValidator = module.exports = getValidator;
let PM;
function banReason(strings, reason) {
return reason && typeof reason === 'string' ? `banned by ${reason}` : `banned`;
}
class Validator {
constructor(format, customBanlist) {
this.format = Dex.getFormat(format, customBanlist);
@ -38,7 +34,7 @@ class Validator {
let dex = this.dex;
let problems = [];
dex.getBanlistTable(format);
const ruleTable = dex.getRuleTable(format);
if (format.team) {
return false;
}
@ -68,37 +64,25 @@ class Validator {
if (removeNicknames) team[i].name = team[i].baseSpecies;
}
for (let i = 0; i < format.teamBanTable.length; i++) {
let bannedCombo = true;
for (let j = 1; j < format.teamBanTable[i].length; j++) {
if (!teamHas[format.teamBanTable[i][j]]) {
bannedCombo = false;
break;
}
}
if (bannedCombo) {
const reason = banReason`${format.name}`;
problems.push(`Your team has the combination of ${format.teamBanTable[i][0]}, which is ${reason}.`);
}
}
for (let i = 0; i < format.teamLimitTable.length; i++) {
let entry = format.teamLimitTable[i];
for (const [rule, source, limit, bans] of ruleTable.complexTeamBans) {
let count = 0;
for (let j = 3; j < entry.length; j++) {
if (teamHas[entry[j]] > 0) count += teamHas[entry[j]];
for (const ban of bans) {
if (teamHas[ban] > 0) count += teamHas[ban];
}
let limit = entry[2];
if (count > limit) {
let clause = entry[1] ? ` by ${entry[1]}` : ``;
problems.push(`You are limited to ${limit} of ${entry[0]}${clause}.`);
const clause = source ? ` by ${source}` : ``;
if (limit === 1) {
problems.push(`Your team has the combination of ${rule}, which is banned${clause}.`);
} else {
problems.push(`You are limited to ${limit} of ${rule}${clause}.`);
}
}
}
if (format.ruleset) {
for (let i = 0; i < format.ruleset.length; i++) {
let subformat = dex.getFormat(format.ruleset[i]);
if (subformat.onValidateTeam && format.banlistTable['Rule:' + subformat.id]) {
if (subformat.onValidateTeam && ruleTable.has(subformat.id)) {
problems = problems.concat(subformat.onValidateTeam.call(dex, team, format, teamHas) || []);
}
}
@ -154,12 +138,12 @@ class Validator {
let lsetData = {set:set, format:format};
let setHas = {};
let banlistTable = dex.getBanlistTable(format);
const ruleTable = dex.getRuleTable(format);
if (format.ruleset) {
for (let i = 0; i < format.ruleset.length; i++) {
let subformat = dex.getFormat(format.ruleset[i]);
if (subformat.onChangeSet && banlistTable['Rule:' + subformat.id]) {
if (subformat.onChangeSet && ruleTable.has(subformat.id)) {
problems = problems.concat(subformat.onChangeSet.call(dex, set, format) || []);
}
}
@ -170,7 +154,7 @@ class Validator {
if (!template) {
template = dex.getTemplate(set.species);
if (ability.id === 'battlebond' && template.id === 'greninja' && !banlistTable['Rule:ignoreillegalabilities']) {
if (ability.id === 'battlebond' && template.id === 'greninja' && !ruleTable.has('ignoreillegalabilities')) {
template = dex.getTemplate('greninjaash');
set.gender = 'M';
}
@ -205,45 +189,38 @@ class Validator {
problems.push(`${set.species} has an invalid happiness.`);
}
let check = template.id;
setHas[check] = true;
if (banlistTable[check] || banlistTable[check + 'base']) {
const reason = banReason`${banlistTable[check]}`;
return [`${set.species} is ${reason}.`];
let banReason = ruleTable.check(template.id, setHas) || ruleTable.check(template.id + 'base', setHas);
if (banReason) {
return [`${set.species} is ${banReason}.`];
} else {
check = toId(template.baseSpecies);
if (banlistTable[check]) {
const reason = banReason`${banlistTable[check]}`;
return [`${template.baseSpecies} is ${reason}.`];
banReason = ruleTable.check(toId(template.baseSpecies), setHas);
if (banReason) {
return [`${template.baseSpecies} is ${banReason}.`];
}
}
check = toId(set.ability);
setHas[check] = true;
if (banlistTable[check]) {
const reason = banReason`${banlistTable[check]}`;
problems.push(`${name}'s ability ${set.ability} is ${reason}.`);
banReason = ruleTable.check(toId(set.ability), setHas);
if (banReason) {
problems.push(`${name}'s ability ${set.ability} is ${banReason}.`);
}
check = toId(set.item);
setHas[check] = true;
if (banlistTable[check]) {
const reason = banReason`${banlistTable[check]}`;
problems.push(`${name}'s item ${set.item} is ${reason}.`);
banReason = ruleTable.check(toId(set.item), setHas);
if (banReason) {
problems.push(`${name}'s item ${set.item} is ${banReason}.`);
}
if (banlistTable['Unreleased'] && item.isUnreleased) {
if (ruleTable.has('-unreleased') && item.isUnreleased) {
problems.push(`${name}'s item ${set.item} is unreleased.`);
}
if (banlistTable['Unreleased'] && template.isUnreleased) {
if (ruleTable.has('-unreleased') && template.isUnreleased) {
if (template.eggGroups[0] === 'Undiscovered' && !template.evos) {
problems.push(`${name} (${template.species}) is unreleased.`);
}
}
setHas[toId(set.ability)] = true;
if (banlistTable['illegal']) {
if (ruleTable.has('-illegal')) {
// Don't check abilities for metagames with All Abilities
if (dex.gen <= 2) {
set.ability = 'None';
} else if (!banlistTable['Rule:ignoreillegalabilities']) {
} else if (!ruleTable.has('ignoreillegalabilities')) {
if (!ability.name) {
problems.push(`${name} needs to have an ability.`);
} else if (!Object.values(template.abilities).includes(ability.name)) {
@ -252,7 +229,7 @@ class Validator {
if (ability.name === template.abilities['H']) {
isHidden = true;
if (template.unreleasedHidden && banlistTable['Unreleased']) {
if (template.unreleasedHidden && ruleTable.has('-unreleased')) {
problems.push(`${name}'s hidden ability is unreleased.`);
} else if (set.species.endsWith('Orange') || set.species.endsWith('White') && ability.name === 'Symbiosis') {
problems.push(`${name}'s hidden ability is unreleased for the Orange and White forms.`);
@ -286,11 +263,9 @@ class Validator {
if (!set.moves[i]) continue;
let move = dex.getMove(Dex.getString(set.moves[i]));
if (!move.exists) return [`"${move.name}" is an invalid move.`];
check = move.id;
setHas[check] = true;
if (banlistTable[check]) {
const reason = banReason`${banlistTable[check]}`;
problems.push(`${name}'s move ${move.name} is ${reason}.`);
banReason = ruleTable.check(move.id, setHas);
if (banReason) {
problems.push(`${name}'s move ${move.name} is ${banReason}.`);
}
// Note that we don't error out on multiple Hidden Power types
@ -299,16 +274,16 @@ class Validator {
set.hpType = move.type;
}
if (banlistTable['Unreleased']) {
if (ruleTable.has('-unreleased')) {
if (move.isUnreleased) problems.push(`${name}'s move ${move.name} is unreleased.`);
}
if (banlistTable['illegal']) {
if (ruleTable.has('-illegal')) {
let problem = this.checkLearnset(move, template, lsetData);
if (problem) {
// Sketchmons hack
const noSketch = format.noSketch || dex.getFormat('gen7sketchmons').noSketch;
if (banlistTable['Rule:allowonesketch'] && noSketch.indexOf(move.name) < 0 && !set.sketchmonsMove && !move.noSketch && !move.isZ) {
if (ruleTable.has('allowonesketch') && noSketch.indexOf(move.name) < 0 && !set.sketchmonsMove && !move.noSketch && !move.isZ) {
set.sketchmonsMove = move.id;
continue;
}
@ -331,7 +306,7 @@ class Validator {
}
const canBottleCap = (dex.gen >= 7 && set.level === 100);
if (set.hpType && maxedIVs && banlistTable['Rule:pokemon']) {
if (set.hpType && maxedIVs && ruleTable.has('pokemon')) {
if (dex.gen <= 2) {
let HPdvs = dex.getType(set.hpType).HPdvs;
set.ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30};
@ -342,14 +317,14 @@ class Validator {
set.ivs = Validator.fillStats(dex.getType(set.hpType).HPivs, 31);
}
}
if (set.hpType === 'Fighting' && banlistTable['Rule:pokemon']) {
if (set.hpType === 'Fighting' && ruleTable.has('pokemon')) {
if (template.gen >= 6 && template.eggGroups[0] === 'Undiscovered' && !template.nfe && (template.baseSpecies !== 'Diancie' || !set.shiny)) {
// Legendary Pokemon must have at least 3 perfect IVs in gen 6+
problems.push(`${name} must not have Hidden Power Fighting because it starts with 3 perfect IVs because it's a gen 6+ legendary.`);
}
}
const ivHpType = dex.getHiddenPower(set.ivs).type;
if (!canBottleCap && banlistTable['Rule:pokemon'] && set.hpType && set.hpType !== ivHpType) {
if (!canBottleCap && ruleTable.has('pokemon') && set.hpType && set.hpType !== ivHpType) {
problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs are for Hidden Power ${ivHpType}.`);
}
if (dex.gen <= 2) {
@ -497,7 +472,7 @@ class Validator {
let eventProblems = this.validateSource(set, lsetData.sources[0], template, ` because it has a move only available`);
if (eventProblems) problems.push(...eventProblems);
}
} else if (banlistTable['illegal'] && template.eventOnly) {
} else if (ruleTable.has('-illegal') && template.eventOnly) {
let eventTemplate = !template.learnset && template.baseSpecies !== template.species ? dex.getTemplate(template.baseSpecies) : template;
let eventPokemon = eventTemplate.eventPokemon;
let legal = false;
@ -545,7 +520,7 @@ class Validator {
}
}
}
if (banlistTable['illegal'] && set.level < template.evoLevel) {
if (ruleTable.has('-illegal') && 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} to be evolved.`);
}
@ -562,15 +537,13 @@ class Validator {
if (item.megaEvolves === template.species) {
template = dex.getTemplate(item.megaStone);
}
if (banlistTable['mega'] && template.forme in {'Mega': 1, 'Mega-X': 1, 'Mega-Y': 1}) {
if (ruleTable.has('-mega') && template.forme in {'Mega': 1, 'Mega-X': 1, 'Mega-Y': 1}) {
problems.push(`Mega evolutions are banned.`);
}
if (template.tier) {
let tier = template.tier;
if (tier.charAt(0) === '(') tier = tier.slice(1, -1);
setHas[toId(tier)] = true;
if (banlistTable[tier] && banlistTable[template.id] !== false) {
problems.push(`${template.species} is in ${tier}, which is banned.`);
banReason = ruleTable.check(toId(template.tier), setHas);
if (banReason && !ruleTable.has('+' + template.id)) {
problems.push(`${template.species} is in ${template.tier}, which is ${banReason}.`);
}
}
@ -583,24 +556,25 @@ class Validator {
}
}
}
for (let i = 0; i < format.setBanTable.length; i++) {
let bannedCombo = true;
for (let j = 1; j < format.setBanTable[i].length; j++) {
if (!setHas[format.setBanTable[i][j]]) {
bannedCombo = false;
break;
}
for (const [rule, source, limit, bans] of ruleTable.complexBans) {
let count = 0;
for (const ban of bans) {
if (setHas[ban] > 0) count += setHas[ban];
}
if (bannedCombo) {
const reason = banReason`${format.name}`;
problems.push(`${name} has the combination of ${format.setBanTable[i][0]}, which is ${reason}.`);
if (count > limit) {
const clause = source ? ` by ${source}` : ``;
if (limit === 1) {
problems.push(`${name} has the combination of ${rule}, which is banned${clause}.`);
} else {
problems.push(`${name} is limited to ${limit} of ${rule}${clause}.`);
}
}
}
if (format.ruleset) {
for (let i = 0; i < format.ruleset.length; i++) {
let subformat = dex.getFormat(format.ruleset[i]);
if (subformat.onValidateSet && banlistTable['Rule:' + subformat.id]) {
if (subformat.onValidateSet && ruleTable.has(subformat.id)) {
problems = problems.concat(subformat.onValidateSet.call(dex, set, format, setHas, teamHas) || []);
}
}
@ -806,6 +780,7 @@ class Validator {
lsetData = lsetData || {};
let set = (lsetData.set || (lsetData.set = {}));
let format = (lsetData.format || (lsetData.format = {}));
let ruleTable = dex.getRuleTable(format);
let alreadyChecked = {};
let level = set.level || 100;
@ -852,7 +827,7 @@ class Validator {
* The format doesn't allow Pokemon traded from the future
* (This is everything except in Gen 1 Tradeback)
*/
const noFutureGen = !(format.banlistTable && format.banlistTable['Rule:allowtradeback']);
const noFutureGen = !dex.getRuleTable(format).has('allowtradeback');
/**
* If a move can only be learned from a gen 2-5 egg, we have to check chainbreeding validity
* limitedEgg is false if there are any legal non-egg sources for the move, and true otherwise
@ -864,7 +839,7 @@ class Validator {
alreadyChecked[template.speciesid] = true;
if (dex.gen === 2 && template.gen === 1) tradebackEligible = true;
// STABmons hack
if (format.banlistTable && format.banlistTable['ignorestabmoves'] && !(moveid in {'acupressure':1, 'bellydrum':1, 'chatter':1, 'geomancy':1, 'shellsmash':1, 'shiftgear':1, 'thousandarrows':1}) && !move.isZ) {
if (ruleTable.has('ignorestabmoves') && !(moveid in {'acupressure':1, 'bellydrum':1, 'chatter':1, 'geomancy':1, 'shellsmash':1, 'shiftgear':1, 'thousandarrows':1}) && !move.isZ) {
let types = template.types;
if (template.baseSpecies === 'Rotom') types = ['Electric', 'Ghost', 'Fire', 'Water', 'Ice', 'Flying', 'Grass'];
if (template.baseSpecies === 'Shaymin') types = ['Grass', 'Flying'];

View File

@ -115,15 +115,10 @@ class Tournament {
return true;
}
setBanlist(params, output) {
setBanlist(banlist, output) {
let format = Dex.getFormat(this.originalFormat);
if (format.team) {
output.errorReply(format.name + " does not support supplementary banlists.");
return false;
}
let banlist = Dex.getSupplementaryBanlist(format, params);
if (banlist.length < 1) {
output.errorReply("The specified banlist is invalid or already included in " + format.name + ".");
output.errorReply(format.name + " does not support custom banlists.");
return false;
}
if (this.teambuilderFormat === this.originalFormat) this.teambuilderFormat = 'customgame-' + this.room.id + '-' + this.room.tourNumber;
@ -1439,9 +1434,9 @@ Chat.commands.tournamenthelp = function (target, room, user) {
return this.sendReplyBox(
"- create/new &lt;format>, &lt;type> [, &lt;comma-separated arguments>]: Creates a new tournament in the current room.<br />" +
"- settype &lt;type> [, &lt;comma-separated arguments>]: Modifies the type of tournament after it's been created, but before it has started.<br />" +
"- banlist &lt;comma-separated arguments>: Sets the supplementary banlist for the tournament before it has started.<br />" +
"- viewbanlist: Shows the supplementary banlist for the tournament.<br />" +
"- clearbanlist: Clears the supplementary banlist for the tournament before it has started.<br />" +
"- banlist &lt;comma-separated arguments>: Sets the custom banlist for the tournament before it has started.<br />" +
"- viewbanlist: Shows the custom banlist for the tournament.<br />" +
"- clearbanlist: Clears the custom banlist for the tournament before it has started.<br />" +
"- name &lt;name>: Sets a custom name for the tournament.<br />" +
"- clearname: Clears the custom name of the tournament.<br />" +
"- end/stop/delete: Forcibly ends the tournament in the current room.<br />" +