diff --git a/data/rulesets.js b/data/rulesets.js index 8bcf74f389..f23137f521 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -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."]; } }, diff --git a/data/scripts.js b/data/scripts.js index ec70dc1efc..e72fd499ee 100644 --- a/data/scripts.js +++ b/data/scripts.js @@ -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); } diff --git a/monitor.js b/monitor.js index 481d77f09a..b17f18d16d 100644 --- a/monitor.js +++ b/monitor.js @@ -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} */ +// @ts-ignore TypeScript bug class TimedCounter extends Map { /** * Increments the number of times an action has been committed by one, and diff --git a/punishments.js b/punishments.js index 129998ee80..c468111cc7 100644 --- a/punishments.js +++ b/punishments.js @@ -69,7 +69,10 @@ Punishments.ips = new PunishmentMap(); */ Punishments.userids = new PunishmentMap(); -class NestedPunishmentMap extends Map/*:: > */ { +/** + * @augments {Map} + */ +class NestedPunishmentMap extends Map { nestedSet(k1, k2, value) { if (!this.get(k1)) { this.set(k1, new Map()); diff --git a/sim/battle.js b/sim/battle.js index 30abac6815..02f2be079e 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -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 = '
If all active Pokémon go in an endless loop, Endless Battle Clause will activate.'; if (allStale) activationWarning = ''; diff --git a/sim/dex-data.js b/sim/dex-data.js index 423e7a2471..2f4a656949 100644 --- a/sim/dex-data.js +++ b/sim/dex-data.js @@ -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} + */ +// @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; diff --git a/sim/dex.js b/sim/dex.js index 16872eb893..c55b52fa3c 100644 --- a/sim/dex.js +++ b/sim/dex.js @@ -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; } /** diff --git a/team-validator.js b/team-validator.js index d41d36f1d3..a8d0e7f258 100644 --- a/team-validator.js +++ b/team-validator.js @@ -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']; diff --git a/tournaments/index.js b/tournaments/index.js index 5e3bd26cd3..323546ab4c 100644 --- a/tournaments/index.js +++ b/tournaments/index.js @@ -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 <format>, <type> [, <comma-separated arguments>]: Creates a new tournament in the current room.
" + "- settype <type> [, <comma-separated arguments>]: Modifies the type of tournament after it's been created, but before it has started.
" + - "- banlist <comma-separated arguments>: Sets the supplementary banlist for the tournament before it has started.
" + - "- viewbanlist: Shows the supplementary banlist for the tournament.
" + - "- clearbanlist: Clears the supplementary banlist for the tournament before it has started.
" + + "- banlist <comma-separated arguments>: Sets the custom banlist for the tournament before it has started.
" + + "- viewbanlist: Shows the custom banlist for the tournament.
" + + "- clearbanlist: Clears the custom banlist for the tournament before it has started.
" + "- name <name>: Sets a custom name for the tournament.
" + "- clearname: Clears the custom name of the tournament.
" + "- end/stop/delete: Forcibly ends the tournament in the current room.
" +