├── .gitignore ├── README.md ├── battle-engine ├── battle.js ├── battlepokemon.js ├── battleside.js └── globals.js ├── battleroom.js ├── bot.js ├── bots ├── greedybot.js ├── minimaxbot.js └── randombot.js ├── clone.js ├── console.js ├── data ├── abilities.js ├── aliases.js ├── formats-data.js ├── items.js ├── learnsets-g6.js ├── learnsets.js ├── moves.js ├── pokedex.js ├── rulesets.js ├── scripts.js ├── statuses.js └── typechart.js ├── db.js ├── epicwin.png ├── formats-data.js ├── minimax_job.sh ├── mods └── README.md ├── package.json ├── screenshot.png ├── templates ├── home.html ├── layout.html ├── replay.html └── room.html ├── tools.js ├── util.js └── weights.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | account.json 3 | *.log 4 | *.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Percymon: A Pokemon Showdown AI 2 | 3 | > This project was built for an old version of Pokemon Showdown and won't work with newer versions. You might have luck checking out some of the forks. 4 | > - https://github.com/shingaryu/showdownbot 5 | > - https://github.com/amharfm/percymon-gen7 6 | > - [More Forks](https://github.com/rameshvarun/showdownbot/forks?include=active&page=1&period=&sort_by=stargazer_counts) 7 | 8 | ![Screenshot](./screenshot.png) 9 | 10 | Percymon is a Pokemon battling AI that plays matches on Pokemon Showdown. Percymon is built using Node.js. Check out [the full project write-up here](https://varunramesh.net/content/documents/cs221-final-report.pdf). 11 | 12 | ## Setting up the repository 13 | 14 | To set up the server, you need to first install dependencies: 15 | 16 | npm install 17 | 18 | In order to actually play games you must create an account on Pokemon Showdown. Once the log-in information has been obtained, you need to create an `account.json` file containing information. The format of `account.json` is as follows: 19 | 20 | { 21 | "username" : "sillybot", 22 | "password": "arbitrary password", 23 | "message" : "gl hf" 24 | } 25 | 26 | The `message` field indicates the message that will be sent when the bot first connects to the game. 27 | 28 | Finally, to start the server, issue the following command: 29 | 30 | node bot.js 31 | 32 | By default, the server searches for unranked random battles when the option is toggled in the web console. There are several command line options that can be supplied: 33 | 34 | --console: Only start the web console, not the game playing bot. 35 | --host [url]: The websocket endpoint of the host to try to connect to. Default: http://sim.smogon.com:8000/showdown 36 | --port [port]: The port on which to serve the web console. Default: 3000 37 | --ranked: Challenge on the ranked league. 38 | --net [action]: Neural network configurations. 'create' - generate a new network. 'update' - use and modify existing network. 'use' - use, but don't modify network. 'none' - use hardcoded weights. Default: none 39 | --algorithm [algorithm]: Can be 'minimax', 'greedy', or 'random'. Default: minimax 40 | --account [file]: File from which to load credentials. Default: account.json 41 | --nosave: Don't save games to the in-memory db. 42 | --nolog: Don't append to log files. 43 | --startchallenging: Start out challenging, instead of requiring a manual activation first. 44 | -------------------------------------------------------------------------------- /battle-engine/battleside.js: -------------------------------------------------------------------------------- 1 | require('sugar'); 2 | require('./globals'); 3 | 4 | // Logging 5 | var log4js = require('log4js'); 6 | var logger = require('log4js').getLogger("battleside"); 7 | 8 | BattlePokemon = require('./battlepokemon') 9 | 10 | BattleSide = (function () { 11 | function BattleSide(name, battle, n, team) { 12 | var sideScripts = battle.data.Scripts.side; 13 | if (sideScripts) Object.assign(this, sideScripts); 14 | 15 | this.battle = battle; 16 | this.n = n; 17 | this.name = name; 18 | this.pokemon = []; 19 | this.active = [null]; 20 | this.sideConditions = {}; 21 | 22 | this.id = n ? 'p2' : 'p1'; 23 | 24 | switch (this.battle.gameType) { 25 | case 'doubles': 26 | this.active = [null, null]; 27 | break; 28 | case 'triples': case 'rotation': 29 | this.active = [null, null, null]; 30 | break; 31 | } 32 | 33 | this.team = this.battle.getTeam(this, team); 34 | for (var i = 0; i < this.team.length && i < 6; i++) { 35 | this.pokemon.push(new BattlePokemon(Tools.getTemplate('Bulbasaur'), this)); 36 | } 37 | this.pokemonLeft = this.pokemon.length; 38 | for (var i = 0; i < this.pokemon.length; i++) { 39 | this.pokemon[i].position = i; 40 | } 41 | } 42 | 43 | BattleSide.prototype.isActive = false; 44 | BattleSide.prototype.pokemonLeft = 0; 45 | BattleSide.prototype.faintedLastTurn = false; 46 | BattleSide.prototype.faintedThisTurn = false; 47 | BattleSide.prototype.decision = null; 48 | BattleSide.prototype.foe = null; 49 | 50 | BattleSide.prototype.toString = function () { 51 | return this.id + ': ' + this.name; 52 | }; 53 | BattleSide.prototype.getData = function () { 54 | var data = { 55 | name: this.name, 56 | id: this.id, 57 | pokemon: [] 58 | }; 59 | for (var i = 0; i < this.pokemon.length; i++) { 60 | var pokemon = this.pokemon[i]; 61 | data.pokemon.push({ 62 | ident: pokemon.fullname, 63 | details: pokemon.details, 64 | condition: pokemon.getHealth(pokemon.side), 65 | active: (pokemon.position < pokemon.side.active.length), 66 | stats: { 67 | atk: pokemon.baseStats['atk'], 68 | def: pokemon.baseStats['def'], 69 | spa: pokemon.baseStats['spa'], 70 | spd: pokemon.baseStats['spd'], 71 | spe: pokemon.baseStats['spe'] 72 | }, 73 | moves: pokemon.moves.map(function (move) { 74 | if (move === 'hiddenpower') { 75 | return move + toId(pokemon.hpType) + (pokemon.hpPower === 70 ? '' : pokemon.hpPower); 76 | } 77 | return move; 78 | }), 79 | baseAbility: pokemon.baseAbility, 80 | item: pokemon.item, 81 | pokeball: pokemon.pokeball, 82 | canMegaEvo: pokemon.canMegaEvo 83 | }); 84 | } 85 | return data; 86 | }; 87 | BattleSide.prototype.randomActive = function () { 88 | var actives = this.active.filter(function (active) { 89 | return active && !active.fainted; 90 | }); 91 | if (!actives.length) return null; 92 | var i = Math.floor(Math.random() * actives.length); 93 | return actives[i]; 94 | }; 95 | BattleSide.prototype.addSideCondition = function (status, source, sourceEffect) { 96 | status = this.battle.getEffect(status); 97 | if (this.sideConditions[status.id]) { 98 | if (!status.onRestart) return false; 99 | return this.battle.singleEvent('Restart', status, this.sideConditions[status.id], this, source, sourceEffect); 100 | } 101 | this.sideConditions[status.id] = {id: status.id}; 102 | this.sideConditions[status.id].target = this; 103 | if (source) { 104 | this.sideConditions[status.id].source = source; 105 | this.sideConditions[status.id].sourcePosition = source.position; 106 | } 107 | if (status.duration) { 108 | this.sideConditions[status.id].duration = status.duration; 109 | } 110 | if (status.durationCallback) { 111 | this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect); 112 | } 113 | if (!this.battle.singleEvent('Start', status, this.sideConditions[status.id], this, source, sourceEffect)) { 114 | delete this.sideConditions[status.id]; 115 | return false; 116 | } 117 | this.battle.update(); 118 | return true; 119 | }; 120 | BattleSide.prototype.getSideCondition = function (status) { 121 | status = this.battle.getEffect(status); 122 | if (!this.sideConditions[status.id]) return null; 123 | return status; 124 | }; 125 | BattleSide.prototype.removeSideCondition = function (status) { 126 | status = this.battle.getEffect(status); 127 | if (!this.sideConditions[status.id]) return false; 128 | this.battle.singleEvent('End', status, this.sideConditions[status.id], this); 129 | delete this.sideConditions[status.id]; 130 | this.battle.update(); 131 | return true; 132 | }; 133 | BattleSide.prototype.send = function () { 134 | var parts = Array.prototype.slice.call(arguments); 135 | var functions = parts.map(function (part) { 136 | return typeof part === 'function'; 137 | }); 138 | var sideUpdate = []; 139 | if (functions.indexOf(true) < 0) { 140 | sideUpdate.push('|' + parts.join('|')); 141 | } else { 142 | var line = ''; 143 | for (var j = 0; j < parts.length; ++j) { 144 | line += '|'; 145 | if (functions[j]) { 146 | line += parts[j](this); 147 | } else { 148 | line += parts[j]; 149 | } 150 | } 151 | sideUpdate.push(line); 152 | } 153 | this.battle.send('sideupdate', this.id + "\n" + sideUpdate); 154 | }; 155 | BattleSide.prototype.emitCallback = function () { 156 | this.battle.send('callback', this.id + "\n" + 157 | Array.prototype.slice.call(arguments).join('|')); 158 | }; 159 | BattleSide.prototype.emitRequest = function (update) { 160 | this.request = update; // Keep track of current request 161 | this.battle.send('request', this.id + "\n" + this.battle.rqid + "\n" + JSON.stringify(update)); 162 | }; 163 | BattleSide.prototype.destroy = function () { 164 | // deallocate ourself 165 | 166 | // deallocate children and get rid of references to them 167 | for (var i = 0; i < this.pokemon.length; i++) { 168 | if (this.pokemon[i]) this.pokemon[i].destroy(); 169 | this.pokemon[i] = null; 170 | } 171 | this.pokemon = null; 172 | for (var i = 0; i < this.active.length; i++) { 173 | this.active[i] = null; 174 | } 175 | this.active = null; 176 | 177 | if (this.decision) { 178 | delete this.decision.side; 179 | delete this.decision.pokemon; 180 | } 181 | this.decision = null; 182 | 183 | // get rid of some possibly-circular references 184 | this.battle = null; 185 | this.foe = null; 186 | }; 187 | return BattleSide; 188 | })(); 189 | 190 | module.exports = BattleSide 191 | -------------------------------------------------------------------------------- /battle-engine/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts anything to an ID. An ID must have only lowercase alphanumeric 3 | * characters. 4 | * If a string is passed, it will be converted to lowercase and 5 | * non-alphanumeric characters will be stripped. 6 | * If an object with an ID is passed, its ID will be returned. 7 | * Otherwise, an empty string will be returned. 8 | */ 9 | global.toId = function (text) { 10 | if (text && text.id) text = text.id; 11 | else if (text && text.userid) text = text.userid; 12 | 13 | return string(text).toLowerCase().replace(/[^a-z0-9]+/g, ''); 14 | }; 15 | 16 | /** 17 | * Validates a username or Pokemon nickname 18 | */ 19 | global.toName = function (name) { 20 | name = string(name); 21 | name = name.replace(/[\|\s\[\]\,]+/g, ' ').trim(); 22 | if (name.length > 18) name = name.substr(0, 18).trim(); 23 | return name; 24 | }; 25 | 26 | /** 27 | * Safely ensures the passed variable is a string 28 | * Simply doing '' + str can crash if str.toString crashes or isn't a function 29 | * If we're expecting a string and being given anything that isn't a string 30 | * or a number, it's safe to assume it's an error, and return '' 31 | */ 32 | global.string = function (str) { 33 | if (typeof str === 'string' || typeof str === 'number') return '' + str; 34 | return ''; 35 | }; 36 | 37 | global.Tools = require('./../tools.js'); -------------------------------------------------------------------------------- /battleroom.js: -------------------------------------------------------------------------------- 1 | // Class libary 2 | JS = require('jsclass'); 3 | JS.require('JS.Class'); 4 | 5 | //does this work? will it show up? 6 | 7 | require("sugar"); 8 | 9 | // Account file 10 | var bot = require("./bot.js"); 11 | var account = bot.account; 12 | 13 | // Results database 14 | var db = require("./db"); 15 | 16 | // Logging 17 | var log4js = require('log4js'); 18 | var logger = require('log4js').getLogger("battleroom"); 19 | var decisionslogger = require('log4js').getLogger("decisions"); 20 | 21 | //battle-engine 22 | var Battle = require('./battle-engine/battle'); 23 | var BattlePokemon = require('./battle-engine/battlepokemon'); 24 | 25 | var Abilities = require("./data/abilities").BattleAbilities; 26 | var Items = require("./data/items").BattleItems; 27 | 28 | var _ = require("underscore"); 29 | 30 | var clone = require("./clone"); 31 | 32 | var program = require('commander'); // Get Command-line arguments 33 | 34 | var BattleRoom = new JS.Class({ 35 | initialize: function(id, sendfunc) { 36 | this.id = id; 37 | this.title = "Untitled"; 38 | this.send = sendfunc; 39 | 40 | // Construct a battle object that we will modify as our state 41 | this.state = Battle.construct(id, 'base', false); 42 | this.state.join('p1', 'botPlayer'); // We will be player 1 in our local simulation 43 | this.state.join('p2', 'humanPlayer'); 44 | this.state.reportPercentages = true; 45 | 46 | this.previousState = null; // For TD Learning 47 | 48 | setTimeout(function() { 49 | sendfunc(account.message, id); // Notify User that this is a bot 50 | sendfunc("/timer", id); // Start timer (for user leaving or bot screw ups) 51 | }, 10000); 52 | 53 | this.decisions = []; 54 | this.log = ""; 55 | 56 | this.state.start(); 57 | }, 58 | init: function(data) { 59 | var log = data.split('\n'); 60 | if (data.substr(0, 6) === '|init|') { 61 | log.shift(); 62 | } 63 | if (log.length && log[0].substr(0, 7) === '|title|') { 64 | this.title = log[0].substr(7); 65 | log.shift(); 66 | logger.info("Title for " + this.id + " is " + this.title); 67 | } 68 | }, 69 | //given a player and a pokemon, returns the corresponding pokemon object 70 | getPokemon: function(battleside, pokename) { 71 | for(var i = 0; i < battleside.pokemon.length; i++) { 72 | if(battleside.pokemon[i].name === pokename || //for mega pokemon 73 | battleside.pokemon[i].name.substr(0,pokename.length) === pokename) 74 | return battleside.pokemon[i]; 75 | } 76 | return undefined; //otherwise Pokemon does not exist 77 | }, 78 | //given a player and a pokemon, updates that pokemon in the battleside object 79 | updatePokemon: function(battleside, pokemon) { 80 | for(var i = 0; i < battleside.pokemon.length; i++) { 81 | if(battleside.pokemon[i].name === pokemon.name) { 82 | battleside.pokemon[i] = pokemon; 83 | return; 84 | } 85 | } 86 | logger.info("Could not find " + pokemon.name + " in the battle side, creating new Pokemon."); 87 | for(var i = battleside.pokemon.length - 1; i >= 0; i--) { 88 | if(battleside.pokemon[i].name === "Bulbasaur") { 89 | battleside.pokemon[i] = pokemon; 90 | return; 91 | } 92 | } 93 | }, 94 | 95 | //returns true if the player object is us 96 | isPlayer: function(player) { 97 | return player === this.side + 'a:' || player === this.side + ':'; 98 | }, 99 | // TODO: Understand more about the opposing pokemon 100 | updatePokemonOnSwitch: function(tokens) { 101 | var tokens2 = tokens[2].split(' '); 102 | var level = tokens[3].split(', ')[1].substring(1); 103 | var tokens4 = tokens[4].split(/\/| /); //for health 104 | 105 | var player = tokens2[0]; 106 | var pokeName = tokens2[1]; 107 | var health = tokens4[0]; 108 | var maxHealth = tokens4[1]; 109 | 110 | var battleside = undefined; 111 | 112 | if (this.isPlayer(player)) { 113 | logger.info("Our pokemon has switched! " + tokens[2]); 114 | battleside = this.state.p1; 115 | //remove boosts for current pokemon 116 | this.state.p1.active[0].clearVolatile(); 117 | } else { 118 | logger.info("Opponents pokemon has switched! " + tokens[2]); 119 | battleside = this.state.p2; 120 | //remove boosts for current pokemon 121 | this.state.p2.active[0].clearVolatile(); 122 | } 123 | var pokemon = this.getPokemon(battleside, pokeName); 124 | 125 | if(!pokemon) { //pokemon has not been defined yet, so choose Bulbasaur 126 | //note: this will not quite work if the pokemon is actually Bulbasaur 127 | pokemon = this.getPokemon(battleside, "Bulbasaur"); 128 | var set = this.state.getTemplate(pokeName); 129 | set.moves = set.randomBattleMoves; 130 | //set.moves = _.sample(set.randomBattleMoves, 4); //for efficiency, need to implement move ordering 131 | set.level = parseInt(level); 132 | //choose the best ability 133 | var abilities = Object.values(set.abilities).sort(function(a,b) { 134 | return this.state.getAbility(b).rating - this.state.getAbility(a).rating; 135 | }.bind(this)); 136 | set.ability = abilities[0]; 137 | pokemon = new BattlePokemon(set, battleside); 138 | pokemon.trueMoves = []; //gradually add moves as they are seen 139 | } 140 | //opponent hp is recorded as percentage 141 | pokemon.hp = Math.ceil(health / maxHealth * pokemon.maxhp); 142 | pokemon.position = 0; 143 | 144 | battleside.active[0].isActive = false; 145 | pokemon.isActive = true; 146 | this.updatePokemon(battleside,pokemon); 147 | 148 | battleside.active = [pokemon]; 149 | 150 | //Ensure that active pokemon is in slot zero 151 | battleside.pokemon = _.sortBy(battleside.pokemon, function(pokemon) { return pokemon == battleside.active[0] ? 0 : 1 }); 152 | }, 153 | updatePokemonOnMove: function(tokens) { 154 | var tokens2 = tokens[2].split(' '); 155 | var player = tokens2[0]; 156 | var pokeName = tokens2[1]; 157 | var move = tokens[3]; 158 | var battleside = undefined; 159 | 160 | if(this.isPlayer(player)) { 161 | battleside = this.state.p1; 162 | } else { 163 | battleside = this.state.p2; 164 | } 165 | 166 | var pokemon = this.getPokemon(battleside, pokeName); 167 | if(!pokemon) { 168 | logger.error("We have never seen " + pokeName + " before in this battle. Should not have happened."); 169 | return; 170 | } 171 | 172 | //update last move (doesn't actually affect the bot...) 173 | pokemon.lastMove = toId(move); 174 | 175 | //if move is protect or detect, update stall counter 176 | if('stall' in pokemon.volatiles) { 177 | pokemon.volatiles.stall.counter++; 178 | } 179 | //update status duration 180 | if(pokemon.status) { 181 | pokemon.statusData.duration = (pokemon.statusData.duration? 182 | pokemon.statusData.duration+1: 183 | 1); 184 | } 185 | //we are no longer newly switched (so we don't fakeout after the first turn) 186 | pokemon.activeTurns += 1; 187 | if(!this.isPlayer(player)) { //anticipate more about the Pokemon's moves 188 | if(pokemon.trueMoves.indexOf(toId(move)) < 0 && pokemon.trueMoves.length < 4) { 189 | pokemon.trueMoves.push(toId(move)); 190 | logger.info("Determined that " + pokeName + " can use " + toId(move)); 191 | //if we have collected all of the moves, eliminate all other possibilities 192 | if(pokemon.trueMoves.length >= 4) { 193 | logger.info("Collected all of " + pokeName + "'s moves!"); 194 | var newMoves = []; 195 | var newMoveset = []; 196 | for(var i = 0; i < pokemon.moveset.length; i++) { 197 | if(pokemon.trueMoves.indexOf(pokemon.moveset[i].id) >= 0) { 198 | newMoves.push(pokemon.moveset[i].id); //store id 199 | newMoveset.push(pokemon.moveset[i]); //store actual moves 200 | } 201 | } 202 | pokemon.moves = newMoves; 203 | pokemon.moveset = newMoveset; 204 | } 205 | 206 | } 207 | } 208 | 209 | this.updatePokemon(battleside, pokemon); 210 | 211 | }, 212 | updatePokemonOnDamage: function(tokens) { 213 | //extract damage dealt to a particular pokemon 214 | //also takes into account passives 215 | //note that opponent health is recorded as percent. Keep this in mind 216 | 217 | var tokens2 = tokens[2].split(' '); 218 | var tokens3 = tokens[3].split(/\/| /); 219 | var player = tokens2[0]; 220 | var pokeName = tokens2[1]; 221 | var health = tokens3[0]; 222 | var maxHealth = tokens3[1]; 223 | var battleside = undefined; 224 | 225 | if(this.isPlayer(player)) { 226 | battleside = this.state.p1; 227 | } else { 228 | battleside = this.state.p2; 229 | } 230 | 231 | var pokemon = this.getPokemon(battleside, pokeName); 232 | if(!pokemon) { 233 | logger.error("We have never seen " + pokeName + " before in this battle. Should not have happened."); 234 | return; 235 | } 236 | 237 | //update hp 238 | pokemon.hp = Math.ceil(health / maxHealth * pokemon.maxhp); 239 | this.updatePokemon(battleside, pokemon); 240 | 241 | }, 242 | updatePokemonOnBoost: function(tokens, isBoost) { 243 | var tokens2 = tokens[2].split(' '); 244 | var stat = tokens[3]; 245 | var boostCount = parseInt(tokens[4]); 246 | var player = tokens2[0]; 247 | var pokeName = tokens2[1]; 248 | var battleside = undefined; 249 | 250 | if(this.isPlayer(player)) { 251 | battleside = this.state.p1; 252 | } else { 253 | battleside = this.state.p2; 254 | } 255 | 256 | var pokemon = this.getPokemon(battleside, pokeName); 257 | if(!pokemon) { 258 | logger.error("We have never seen " + pokeName + " before in this battle. Should not have happened."); 259 | return; 260 | } 261 | 262 | if(isBoost) { 263 | if(stat in pokemon.boosts) 264 | pokemon.boosts[stat] += boostCount; 265 | else 266 | pokemon.boosts[stat] = boostCount; 267 | } else { 268 | if(stat in pokemon.boosts) 269 | pokemon.boosts[stat] -= boostCount; 270 | else 271 | pokemon.boosts[stat] = -boostCount; 272 | } 273 | this.updatePokemon(battleside, pokemon); 274 | }, 275 | updatePokemonSetBoost: function(tokens) { 276 | var tokens2 = tokens[2].split(' '); 277 | var stat = tokens[3]; 278 | var boostCount = parseInt(tokens[4]); 279 | var player = tokens2[0]; 280 | var pokeName = tokens2[1]; 281 | var battleside = undefined; 282 | 283 | if(this.isPlayer(player)) { 284 | battleside = this.state.p1; 285 | } else { 286 | battleside = this.state.p2; 287 | } 288 | 289 | var pokemon = this.getPokemon(battleside, pokeName); 290 | if(!pokemon) { 291 | logger.error("We have never seen " + pokeName + " before in this battle. Should not have happened."); 292 | return; 293 | } 294 | 295 | pokemon.boosts[stat] = boostCount; 296 | this.updatePokemon(battleside, pokemon); 297 | }, 298 | updatePokemonRestoreBoost: function(tokens) { 299 | var tokens2 = tokens[2].split(' '); 300 | var player = tokens2[0]; 301 | var pokeName = tokens2[1]; 302 | var battleside = undefined; 303 | 304 | if(this.isPlayer(player)) { 305 | battleside = this.state.p1; 306 | } else { 307 | battleside = this.state.p2; 308 | } 309 | 310 | var pokemon = this.getPokemon(battleside, pokeName); 311 | if(!pokemon) { 312 | logger.error("We have never seen " + pokeName + " before in this battle. Should not have happened."); 313 | return; 314 | } 315 | 316 | for(var stat in pokemon.boosts) { 317 | if(pokemon.boosts[stat] < 0) 318 | delete pokemon.boosts[stat]; 319 | } 320 | this.updatePokemon(battleside, pokemon); 321 | 322 | 323 | }, 324 | updatePokemonStart: function(tokens, newStatus) { 325 | //add condition such as leech seed, substitute, ability, confusion, encore 326 | //move: yawn, etc. 327 | //ability: flash fire, etc. 328 | 329 | var tokens2 = tokens[2].split(' '); 330 | var player = tokens2[0]; 331 | var pokeName = tokens2[1]; 332 | var status = tokens[3]; 333 | var battleside = undefined; 334 | 335 | if(this.isPlayer(player)) { 336 | battleside = this.state.p1; 337 | } else { 338 | battleside = this.state.p2; 339 | } 340 | 341 | var pokemon = this.getPokemon(battleside, pokeName); 342 | 343 | if(status.substring(0,4) === 'move') { 344 | status = status.substring(6); 345 | } else if(status.substring(0,7) === 'ability') { 346 | status = status.substring(9); 347 | } 348 | 349 | if(newStatus) { 350 | pokemon.addVolatile(status); 351 | } else { 352 | pokemon.removeVolatile(status); 353 | } 354 | this.updatePokemon(battleside, pokemon); 355 | }, 356 | updateField: function(tokens, newField) { 357 | //as far as I know, only applies to trick room, which is a pseudo-weather 358 | var fieldStatus = tokens[2].substring(6); 359 | if(newField) { 360 | this.state.addPseudoWeather(fieldStatus); 361 | } else { 362 | this.state.removePseudoWeather(fieldStatus); 363 | } 364 | }, 365 | updateWeather: function(tokens) { 366 | var weather = tokens[2]; 367 | if(weather === "none") { 368 | this.state.clearWeather(); 369 | } else { 370 | this.state.setWeather(weather); 371 | //we might want to keep track of how long the weather has been lasting... 372 | //might be done automatically for us 373 | } 374 | }, 375 | updateSideCondition: function(tokens, newSide) { 376 | var player = tokens[2].split(' ')[0]; 377 | var sideStatus = tokens[3]; 378 | if(sideStatus.substring(0,4) === "move") 379 | sideStatus = tokens[3].substring(6); 380 | var battleside = undefined; 381 | if(this.isPlayer(player)) { 382 | battleside = this.state.p1; 383 | } else { 384 | battleside = this.state.p2; 385 | } 386 | 387 | if(newSide) { 388 | battleside.addSideCondition(sideStatus); 389 | //Note: can have multiple layers of toxic spikes or spikes 390 | } else { 391 | battleside.removeSideCondition(sideStatus); 392 | //remove side status 393 | } 394 | }, 395 | updatePokemonStatus: function(tokens, newStatus) { 396 | var tokens2 = tokens[2].split(' '); 397 | var player = tokens2[0]; 398 | var pokeName = tokens2[1]; 399 | var status = tokens[3]; 400 | var battleside = undefined; 401 | 402 | if(this.isPlayer(player)) { 403 | battleside = this.state.p1; 404 | } else { 405 | battleside = this.state.p2; 406 | } 407 | var pokemon = this.getPokemon(battleside, pokeName); 408 | 409 | if(newStatus) { 410 | pokemon.setStatus(status); 411 | //record a new Pokemon's status 412 | //also keep track of how long the status has been going? relevant for toxic poison 413 | //actually, might be done by default 414 | } else { 415 | pokemon.clearStatus(); 416 | //heal a Pokemon's status 417 | } 418 | this.updatePokemon(battleside, pokemon); 419 | }, 420 | updatePokemonOnItem: function(tokens, newItem) { 421 | //record that a pokemon has an item. Most relevant if a Pokemon has an air balloon/chesto berry 422 | //TODO: try to predict the opponent's current item 423 | 424 | var tokens2 = tokens[2].split(' '); 425 | var player = tokens2[0]; 426 | var pokeName = tokens2[1]; 427 | var item = tokens[3]; 428 | var battleside = undefined; 429 | 430 | if(this.isPlayer(player)) { 431 | battleside = this.state.p1; 432 | } else { 433 | battleside = this.state.p2; 434 | } 435 | var pokemon = this.getPokemon(battleside, pokeName); 436 | 437 | if(newItem) { 438 | pokemon.setItem(item); 439 | } else { 440 | pokemon.clearItem(item); 441 | } 442 | this.updatePokemon(battleside, pokemon); 443 | }, 444 | 445 | //Apply mega evolution effects, or aegislash/meloetta 446 | updatePokemonOnFormeChange: function(tokens) { 447 | var tokens2 = tokens[2].split(' '); 448 | var tokens3 = tokens[3].split(', '); 449 | var player = tokens2[0]; 450 | var pokeName = tokens2[1]; 451 | var newPokeName = tokens3[0]; 452 | var battleside = undefined; 453 | 454 | if(this.isPlayer(player)) { 455 | battleside = this.state.p1; 456 | } else { 457 | battleside = this.state.p2; 458 | } 459 | //Note: crashes when the bot mega evolves. 460 | logger.info(pokeName + " has transformed into " + newPokeName + "!"); 461 | var pokemon = this.getPokemon(battleside, pokeName, true); 462 | 463 | //apply forme change 464 | pokemon.formeChange(newPokeName); 465 | this.updatePokemon(battleside, pokemon); 466 | }, 467 | //for ditto exclusively 468 | updatePokemonOnTransform: function(tokens) { 469 | var tokens2 = tokens[2].split(' '); 470 | var tokens3 = tokens[3].split(' '); 471 | var player = tokens2[0]; 472 | var pokeName = tokens2[1]; 473 | var newPokeName = tokens3[1]; 474 | var battleside = undefined; 475 | var pokemon = undefined; 476 | 477 | if(this.isPlayer(player)) { 478 | battleside = this.state.p1; 479 | pokemon = this.getPokemon(battleside, pokeName); 480 | pokemon.transformInto(this.state.p2.active[0]); 481 | } else { 482 | battleside = this.state.p2; 483 | pokemon = this.getPokemon(battleside, pokeName); 484 | pokemon.transformInto(this.state.p1.active[0]); 485 | } 486 | this.updatePokemon(battleside, pokemon); 487 | 488 | }, 489 | recieve: function(data) { 490 | if (!data) return; 491 | 492 | logger.trace("<< " + data); 493 | 494 | if (data.substr(0, 6) === '|init|') { 495 | return this.init(data); 496 | } 497 | if (data.substr(0, 9) === '|request|') { 498 | return this.receiveRequest(JSON.parse(data.substr(9))); 499 | } 500 | 501 | var log = data.split('\n'); 502 | for (var i = 0; i < log.length; i++) { 503 | this.log += log[i] + "\n"; 504 | 505 | var tokens = log[i].split('|'); 506 | if (tokens.length > 1) { 507 | 508 | if (tokens[1] === 'tier') { 509 | this.tier = tokens[2]; 510 | } else if (tokens[1] === 'win') { 511 | this.send("gg", this.id); 512 | 513 | this.winner = tokens[2]; 514 | if (this.winner == account.username) { 515 | logger.info(this.title + ": I won this game"); 516 | } else { 517 | logger.info(this.title + ": I lost this game"); 518 | } 519 | 520 | if(program.net === "update" && this.previousState) { 521 | var playerAlive = _.any(this.state.p1.pokemon, function(pokemon) { return pokemon.hp > 0; }); 522 | var opponentAlive = _.any(this.state.p2.pokemon, function(pokemon) { return pokemon.hp > 0; }); 523 | 524 | if(!playerAlive || !opponentAlive) minimaxbot.train_net(this.previousState, null, (this.winner == account.username)); 525 | } 526 | 527 | if(!program.nosave) this.saveResult(); 528 | 529 | // Leave in two seconds 530 | var battleroom = this; 531 | setTimeout(function() { 532 | battleroom.send("/leave " + battleroom.id); 533 | }, 2000); 534 | 535 | } else if (tokens[1] === 'switch' || tokens[1] === 'drag') { 536 | this.updatePokemonOnSwitch(tokens); 537 | } else if (tokens[1] === 'move') { 538 | this.updatePokemonOnMove(tokens); 539 | } else if(tokens[1] === 'faint') { //we could outright remove a pokemon... 540 | //record that pokemon has fainted 541 | } else if(tokens[1] === 'detailschange' || tokens[1] === 'formechange') { 542 | this.updatePokemonOnFormeChange(tokens); 543 | } else if(tokens[1] === '-transform') { 544 | this.updatePokemonOnTransform(tokens); 545 | } else if(tokens[1] === '-damage') { //Error: not getting to here... 546 | this.updatePokemonOnDamage(tokens); 547 | } else if(tokens[1] === '-heal') { 548 | this.updatePokemonOnDamage(tokens); 549 | } else if(tokens[1] === '-boost') { 550 | this.updatePokemonOnBoost(tokens, true); 551 | } else if(tokens[1] === '-unboost') { 552 | this.updatePokemonOnBoost(tokens, false); 553 | } else if(tokens[1] === '-setboost') { 554 | this.updatePokemonSetBoost(tokens); 555 | } else if(tokens[1] === '-restoreboost') { 556 | this.updatePokemonRestoreBoost(tokens); 557 | } else if(tokens[1] === '-start') { 558 | this.updatePokemonStart(tokens, true); 559 | } else if(tokens[1] === '-end') { 560 | this.updatePokemonStart(tokens, false); 561 | } else if(tokens[1] === '-fieldstart') { 562 | this.updateField(tokens, true); 563 | } else if(tokens[1] === '-fieldend') { 564 | this.updateField(tokens, false); 565 | } else if(tokens[1] === '-weather') { 566 | this.updateWeather(tokens); 567 | } else if(tokens[1] === '-sidestart') { 568 | this.updateSideCondition(tokens, true); 569 | } else if(tokens[1] === '-sideend') { 570 | this.updateSideCondition(tokens, false); 571 | } else if(tokens[1] === '-status') { 572 | this.updatePokemonStatus(tokens, true); 573 | } else if(tokens[1] === '-curestatus') { 574 | this.updatePokemonStatus(tokens, false); 575 | } else if(tokens[1] === '-item') { 576 | this.updatePokemonOnItem(tokens, true); 577 | } else if(tokens[1] === '-enditem') { 578 | this.updatePokemonOnItem(tokens, false); 579 | } else if(tokens[1] === '-ability') { 580 | //relatively situational -- important for mold breaker/teravolt, etc. 581 | //needs to be recorded so that we don't accidentally lose a pokemon 582 | 583 | //We don't actually care about the rest of these effects, as they are merely visual 584 | } else if(tokens[1] === '-supereffective') { 585 | 586 | } else if(tokens[1] === '-crit') { 587 | 588 | } else if(tokens[1] === '-singleturn') { //for protect. But we only care about damage... 589 | 590 | } else if(tokens[1] === 'c') {//chat message. ignore. (or should we?) 591 | 592 | } else if(tokens[1] === '-activate') { //protect, wonder guard, etc. 593 | 594 | } else if(tokens[1] === '-fail') { 595 | 596 | } else if(tokens[1] === '-immune') { 597 | 598 | } else if(tokens[1] === 'message') { 599 | 600 | } else if(tokens[1] === 'cant') { 601 | 602 | } else if(tokens[1] === 'leave') { 603 | 604 | } else if(tokens[1]) { //what if token is defined 605 | logger.info("Error: could not parse token '" + tokens[1] + "'. This needs to be implemented"); 606 | } 607 | 608 | } 609 | } 610 | }, 611 | saveResult: function() { 612 | // Save game data to data base 613 | game = { 614 | "title": this.title, 615 | "id": this.id, 616 | "win": (this.winner == account.username), 617 | "date": new Date(), 618 | "decisions": "[]", //JSON.stringify(this.decisions), 619 | "log": this.log, 620 | "tier": this.tier 621 | }; 622 | db.insert(game, function(err, newDoc) { 623 | if(newDoc) logger.info("Saved result of " + newDoc.title + " to database."); 624 | else logger.error("Error saving result to database."); 625 | }); 626 | }, 627 | receiveRequest: function(request) { 628 | if (!request) { 629 | this.side = ''; 630 | return; 631 | } 632 | 633 | if (request.side) this.updateSide(request.side, true); 634 | 635 | if (request.active) logger.info(this.title + ": I need to make a move."); 636 | if (request.forceSwitch) logger.info(this.title + ": I need to make a switch."); 637 | 638 | if (request.active || request.forceSwitch) this.makeMove(request); 639 | }, 640 | 641 | //note: we should not be recreating pokemon each time 642 | //is this redundant? 643 | updateSide: function(sideData) { 644 | if (!sideData || !sideData.id) return; 645 | logger.info("Starting to update my side data."); 646 | for (var i = 0; i < sideData.pokemon.length; ++i) { 647 | var pokemon = sideData.pokemon[i]; 648 | 649 | var details = pokemon.details.split(","); 650 | var name = details[0].trim(); 651 | var level = parseInt(details[1].trim().substring(1)); 652 | var gender = details[2] ? details[2].trim() : null; 653 | 654 | var template = { 655 | name: name, 656 | moves: pokemon.moves, 657 | ability: Abilities[pokemon.baseAbility].name, 658 | evs: { 659 | hp: 85, 660 | atk: 85, 661 | def: 85, 662 | spa: 85, 663 | spd: 85, 664 | spe: 85 665 | }, 666 | ivs: { 667 | hp: 31, 668 | atk: 31, 669 | def: 31, 670 | spa: 31, 671 | spd: 31, 672 | spe: 31 673 | }, 674 | item: (!pokemon.item || pokemon.item === '') ? '' : Items[pokemon.item].name, 675 | level: level, 676 | active: pokemon.active, 677 | shiny: false 678 | }; 679 | 680 | //keep track of old pokemon 681 | var oldPokemon = this.state.p1.pokemon[i]; 682 | 683 | // Initialize pokemon 684 | this.state.p1.pokemon[i] = new BattlePokemon(template, this.state.p1); 685 | this.state.p1.pokemon[i].position = i; 686 | 687 | // Update the pokemon object with latest stats 688 | for (var stat in pokemon.stats) { 689 | this.state.p1.pokemon[i].baseStats[stat] = pokemon.stats[stat]; 690 | } 691 | // Update health/status effects, if any 692 | var condition = pokemon.condition.split(/\/| /); 693 | this.state.p1.pokemon[i].hp = parseInt(condition[0]); 694 | if(condition.length > 2) {//add status condition 695 | this.state.p1.pokemon[i].setStatus(condition[2]); //necessary 696 | } 697 | if(oldPokemon.isActive && oldPokemon.statusData) { //keep old duration 698 | pokemon.statusData = oldPokemon.statusData; 699 | } 700 | 701 | // Keep old boosts 702 | this.state.p1.pokemon[i].boosts = oldPokemon.boosts; 703 | 704 | // Keep old volatiles 705 | this.state.p1.pokemon[i].volatiles = oldPokemon.volatiles; 706 | 707 | if (pokemon.active) { 708 | this.state.p1.active = [this.state.p1.pokemon[i]]; 709 | this.state.p1.pokemon[i].isActive = true; 710 | } 711 | 712 | // TODO(rameshvarun): Somehow parse / load in current hp and status conditions 713 | } 714 | 715 | // Enforce that the active pokemon is in the first slot 716 | this.state.p1.pokemon = _.sortBy(this.state.p1.pokemon, function(pokemon) { return pokemon.isActive ? 0 : 1 }); 717 | 718 | this.side = sideData.id; 719 | this.oppSide = (this.side === "p1") ? "p2" : "p1"; 720 | logger.info(this.title + ": My current side is " + this.side); 721 | }, 722 | makeMove: function(request) { 723 | var room = this; 724 | 725 | setTimeout(function() { 726 | if(program.net === "update") { 727 | if(room.previousState != null) minimaxbot.train_net(room.previousState, room.state); 728 | room.previousState = clone(room.state); 729 | } 730 | 731 | var decision = BattleRoom.parseRequest(request); 732 | 733 | // Use specified algorithm to determine resulting choice 734 | var result = undefined; 735 | if(decision.choices.length == 1) result = decision.choices[0]; 736 | else if(program.algorithm === "minimax") result = minimaxbot.decide(clone(room.state), decision.choices); 737 | else if(program.algorithm === "greedy") result = greedybot.decide(clone(room.state), decision.choices); 738 | else if(program.algorithm === "random") result = randombot.decide(clone(room.state), decision.choices); 739 | 740 | room.decisions.push(result); 741 | room.send("/choose " + BattleRoom.toChoiceString(result, room.state.p1) + "|" + decision.rqid, room.id); 742 | }, 5000); 743 | }, 744 | // Static class methods 745 | extend: { 746 | toChoiceString: function(choice, battleside) { 747 | if (choice.type == "move") { 748 | if(battleside && battleside.active[0].canMegaEvo) //mega evolve if possible 749 | return "move " + choice.id + " mega"; 750 | else 751 | return "move " + choice.id; 752 | } else if (choice.type == "switch") { 753 | return "switch " + (choice.id + 1); 754 | } 755 | }, 756 | parseRequest: function(request) { 757 | var choices = []; 758 | 759 | if(!request) return choices; // Empty request 760 | if(request.wait) return choices; // This player is not supposed to make a move 761 | 762 | // If we can make a move 763 | if (request.active) { 764 | _.each(request.active[0].moves, function(move) { 765 | if (!move.disabled) { 766 | choices.push({ 767 | "type": "move", 768 | "id": move.id 769 | }); 770 | } 771 | }); 772 | } 773 | 774 | // Switching options 775 | var trapped = (request.active) ? (request.active[0].trapped || request.active[0].maybeTrapped) : false; 776 | var canSwitch = request.forceSwitch || !trapped; 777 | if (canSwitch) { 778 | _.each(request.side.pokemon, function(pokemon, index) { 779 | if (pokemon.condition.indexOf("fnt") < 0 && !pokemon.active) { 780 | choices.push({ 781 | "type": "switch", 782 | "id": index 783 | }); 784 | } 785 | }); 786 | } 787 | 788 | return { 789 | rqid: request.rqid, 790 | choices: choices 791 | }; 792 | } 793 | } 794 | }); 795 | module.exports = BattleRoom; 796 | 797 | var minimaxbot = require("./bots/minimaxbot"); 798 | var greedybot = require("./bots/greedybot"); 799 | var randombot = require("./bots/randombot"); 800 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | // Command-line Arguments 2 | var program = require('commander'); 3 | program 4 | .option('--console', 'Only start the web console - not the game playing bot.') 5 | .option('--host [url]', 'The websocket endpoint of the host to try to connect to. ["http://sim.smogon.com:8000/showdown"]', 'http://sim.smogon.com:8000/showdown') 6 | .option('--port [port]', 'The port on which to serve the web console. [3000]', "3000") 7 | .option('--ranked', 'Challenge on the ranked league.') 8 | .option('--net [action]', "'create' - generate a new network. 'update' - use and modify existing network. 'use' - use, but don't modify network. 'none' - use hardcoded weights. ['none']", 'none') 9 | .option('--algorithm [algorithm]', "Can be 'minimax', 'greedy', or 'random'. ['minimax']", "minimax") 10 | .option('--account [file]', "File from which to load credentials. ['account.json']", "account.json") 11 | .option('--nosave', "Don't save games to the in-memory db.") 12 | .option('--nolog', "Don't append to log files.") 13 | .option('--startchallenging', "Start out challenging, instead of requiring a manual activation first.") 14 | .parse(process.argv); 15 | 16 | var request = require('request'); // Used for making post requests to login server 17 | var util = require('./util'); 18 | var fs = require('fs'); 19 | 20 | // Setup Logging 21 | var log4js = require('log4js'); 22 | log4js.loadAppender('file'); 23 | var logger = require('log4js').getLogger("bot"); 24 | 25 | if(!program.nolog) { 26 | // Ensure that logging directory exists 27 | if(!fs.existsSync("./logs")) { fs.mkdirSync("logs") }; 28 | 29 | log4js.addAppender(log4js.appenders.file('logs/bot.log'), 'bot'); 30 | 31 | log4js.addAppender(log4js.appenders.file('logs/minimax.log'), 'minimax'); 32 | log4js.addAppender(log4js.appenders.file('logs/learning.log'), 'learning'); 33 | 34 | log4js.addAppender(log4js.appenders.file('logs/battleroom.log'), 'battleroom'); 35 | log4js.addAppender(log4js.appenders.file('logs/decisions.log'), 'decisions'); 36 | 37 | log4js.addAppender(log4js.appenders.file('logs/webconsole.log'), 'webconsole'); 38 | 39 | log4js.addAppender(log4js.appenders.file('logs/battle.log'), 'battle'); 40 | log4js.addAppender(log4js.appenders.file('logs/battlepokemon.log'), 'battlepokemon'); 41 | log4js.addAppender(log4js.appenders.file('logs/battleside.log'), 'battleside'); 42 | 43 | log4js.addAppender(log4js.appenders.file('logs/greedy.log'), 'greedy'); 44 | } else { 45 | logger.setLevel("INFO"); 46 | log4js.configure({ 47 | appenders : [ 48 | { 49 | type: "console", 50 | category: ["bot"] 51 | } 52 | ] 53 | }); 54 | } 55 | 56 | // Login information for this bot 57 | var account = JSON.parse(fs.readFileSync(program.account)); 58 | module.exports.account = account; 59 | 60 | var webconsole = require("./console.js");// Web console 61 | 62 | // Connect to server 63 | var sockjs = require('sockjs-client-ws'); 64 | var client = null; 65 | if(!program.console) client = sockjs.create(program.host); 66 | 67 | // Domain (replay button redirects here) 68 | var DOMAIN = "http://play.pokemonshowdown.com/"; 69 | exports.DOMAIN = DOMAIN; 70 | 71 | // PHP endpoint used to login / authenticate 72 | var ACTION_PHP = DOMAIN + "~~showdown/action.php"; 73 | 74 | // Values that need to be globally stored in order to login properly 75 | var CHALLENGE_KEY_ID = null; 76 | var CHALLENGE = null; 77 | 78 | // BattleRoom object 79 | var BattleRoom = require('./battleroom'); 80 | 81 | // The game type that we want to search for on startup 82 | var GAME_TYPE = (program.ranked) ? "randombattle" : "unratedrandombattle"; 83 | 84 | // Load in Game Data 85 | var Pokedex = require("./data/pokedex"); 86 | var Typechart = require("./data/typechart"); 87 | 88 | // Sends a piece of data to the given room 89 | // Room can be null for a global command 90 | var send = module.exports.send = function(data, room) { 91 | if (room && room !== 'lobby' && room !== true) { 92 | data = room+'|'+data; 93 | } else if (room !== true) { 94 | data = '|'+data; 95 | } 96 | client.write(data); 97 | 98 | logger.trace(">> " + data); 99 | } 100 | 101 | // Login to a new account 102 | function rename(name, password) { 103 | var self = this; 104 | request.post({ 105 | url : ACTION_PHP, 106 | formData : { 107 | act: "login", 108 | name: name, 109 | pass: password, 110 | challengekeyid: CHALLENGE_KEY_ID, 111 | challenge: CHALLENGE 112 | } 113 | }, 114 | function (err, response, body) { 115 | var data = util.safeJSON(body); 116 | if(data && data.curuser && data.curuser.loggedin) { 117 | send("/trn " + account.username + ",0," + data.assertion); 118 | } else { 119 | // We couldn't log in for some reason 120 | logger.fatal("Error logging in..."); 121 | process.exit(); 122 | } 123 | }); 124 | } 125 | 126 | // Global room counter (this allows multiple battles at the same time) 127 | var ROOMS = {}; 128 | exports.ROOMS = ROOMS; 129 | 130 | // Add a new room (only supports rooms of type battle) 131 | function addRoom(id, type) { 132 | if(type == "battle") { 133 | ROOMS[id] = new BattleRoom(id, send); 134 | return ROOMS[id]; 135 | } else { 136 | logger.error("Unkown room type: " + type); 137 | } 138 | } 139 | // Remove a room from the global list 140 | function removeRoom(id) { 141 | var room = ROOMS[id]; 142 | if(room) { 143 | delete ROOMS[id]; 144 | return true; 145 | } 146 | return false; 147 | } 148 | 149 | // Code to execute once we have succesfully authenticated 150 | function onLogin() { 151 | //do nothing 152 | 153 | 154 | } 155 | 156 | function searchBattle() { 157 | logger.info("Searching for an unranked random battle"); 158 | send("/search " + GAME_TYPE); 159 | } 160 | module.exports.searchBattle = searchBattle; 161 | 162 | // Global recieve function - tries to interpret command, or send to the correct room 163 | function recieve(data) { 164 | logger.trace("<< " + data); 165 | 166 | var roomid = ''; 167 | if (data.substr(0,1) === '>') { // First determine if this command is for a room 168 | var nlIndex = data.indexOf('\n'); 169 | if (nlIndex < 0) return; 170 | roomid = util.toRoomid(data.substr(1,nlIndex-1)); 171 | data = data.substr(nlIndex+1); 172 | } 173 | if (data.substr(0,6) === '|init|') { // If it is an init command, create the room 174 | if (!roomid) roomid = 'lobby'; 175 | var roomType = data.substr(6); 176 | var roomTypeLFIndex = roomType.indexOf('\n'); 177 | if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex); 178 | roomType = util.toId(roomType); 179 | 180 | logger.info(roomid + " is being opened."); 181 | addRoom(roomid, roomType); 182 | 183 | } else if ((data+'|').substr(0,8) === '|expire|') { // Room expiring 184 | var room = ROOMS[roomid]; 185 | logger.info(roomid + " has expired."); 186 | if(room) { 187 | room.expired = true; 188 | if (room.updateUser) room.updateUser(); 189 | } 190 | return; 191 | } else if ((data+'|').substr(0,8) === '|deinit|' || (data+'|').substr(0,8) === '|noinit|') { 192 | if (!roomid) roomid = 'lobby'; 193 | 194 | // expired rooms aren't closed when left 195 | if (ROOMS[roomid] && ROOMS[roomid].expired) return; 196 | 197 | logger.info(roomid + " has been closed."); 198 | removeRoom(roomid); 199 | return; 200 | } 201 | if(roomid) { //Forward command to specific room 202 | if(ROOMS[roomid]) { 203 | ROOMS[roomid].recieve(data); 204 | } else { 205 | logger.error("Room of id " + roomid + " does not exist to send data to."); 206 | } 207 | return; 208 | } 209 | 210 | // Split global command into parts 211 | var parts; 212 | if(data.charAt(0) === '|') { 213 | parts = data.substr(1).split('|'); 214 | } else { 215 | parts = []; 216 | } 217 | 218 | switch(parts[0]) { 219 | // Recieved challenge string 220 | case 'challenge-string': 221 | case 'challstr': 222 | logger.info("Recieved challenge string..."); 223 | CHALLENGE_KEY_ID = parseInt(parts[1], 10); 224 | CHALLENGE = parts[2]; 225 | 226 | // Now try to rename to the given user 227 | rename(account.username, account.password); 228 | break; 229 | // Server is telling us to update the user that we are currently logged in as 230 | case 'updateuser': 231 | // The update user command can actually come with a second command (after the newline) 232 | var nlIndex = data.indexOf('\n'); 233 | if (nlIndex > 0) { 234 | recieve(data.substr(nlIndex+1)); 235 | nlIndex = parts[3].indexOf('\n'); 236 | parts[3] = parts[3].substr(0, nlIndex); 237 | } 238 | 239 | var name = parts[1]; 240 | var named = !!+parts[2]; 241 | 242 | if(name == account.username) { 243 | logger.info("Successfully logged in."); 244 | onLogin() 245 | } 246 | break; 247 | // Server tried to send us a popup 248 | case 'popup': 249 | logger.info("Popup: " + data.substr(7).replace(/\|\|/g, '\n')); 250 | break; 251 | // Someone has challenged us to a battle 252 | case 'updatechallenges': 253 | var challenges = JSON.parse(data.substr(18)); 254 | if(challenges.challengesFrom) { 255 | for(var user in challenges.challengesFrom) { 256 | if(challenges.challengesFrom[user] == "gen6randombattle") { 257 | logger.info("Accepting challenge from " + user); 258 | send("/accept " + user); 259 | } else { 260 | logger.warn("Won't accept challenge of type: " + challenges.challengesFrom[user]); 261 | send("/reject " + user); 262 | } 263 | } 264 | } 265 | break; 266 | // Unkown global command 267 | default: 268 | logger.warn("Did not recognize command of type: " + parts[0]); 269 | break; 270 | } 271 | } 272 | 273 | if(client) { 274 | client.on('connection', function() { 275 | logger.info('Connected to server.'); 276 | }); 277 | 278 | client.on('data', function(msg) { 279 | recieve(msg); 280 | }); 281 | 282 | client.on('error', function(e) { 283 | logger.error(e); 284 | }); 285 | } 286 | -------------------------------------------------------------------------------- /bots/greedybot.js: -------------------------------------------------------------------------------- 1 | //Logging 2 | var log4js = require('log4js'); 3 | var logger = log4js.getLogger("greedy"); 4 | 5 | var _ = require("underscore"); 6 | var BattleRoom = require("./../battleroom"); 7 | 8 | var randombot = require("./randombot"); 9 | 10 | var Tools = require("./../tools"); 11 | var damagingMoves = ["return", "grassknot", "lowkick", "gyroball", "heavyslam"]; 12 | 13 | var switchPriority = module.exports.switchPriority = function(battle, pokemon, p1, p2) { 14 | var oppPokemon = p2.active[0]; 15 | var myPokemon = p1.pokemon[pokemon.id]; 16 | //Incoming poke is immune to both of opponent Pokemon's types: 5 17 | if(_.all(oppPokemon.getTypes(), function(type) { 18 | return !Tools.getImmunity(type, myPokemon.getTypes()); 19 | })) { 20 | return 5; 21 | } 22 | //Incoming poke resists both of opponents' types: 4 23 | if(_.all(oppPokemon.getTypes(), function(type) { 24 | return Tools.getEffectiveness(type, myPokemon) < 0 || !Tools.getImmunity(type, myPokemon.getTypes()); 25 | })) { 26 | return 4; 27 | } 28 | //Incoming poke receives neutral damage from opponent: 3 29 | if(_.all(oppPokemon.getTypes(), function(type) { 30 | return Tools.getEffectiveness(type, myPokemon) <= 0 ||!Tools.getImmunity(type, myPokemon.getTypes()); 31 | })) { 32 | return 3; 33 | } 34 | //Incoming poke can deal super effective damage to opponents' pokemon: 2 35 | if(_.any(myPokemon.getMoves(), function(move) { 36 | var moveData = Tools.getMove(move.id); 37 | return Tools.getEffectiveness(moveData, oppPokemon) > 0 && 38 | (moveData.basePower > 0 || damagingMoves.indexOf(move.id) >= 0) && 39 | Tools.getImmunity(moveData.type, oppPokemon.getTypes()); 40 | })) { 41 | return 2; 42 | } 43 | 44 | //Otherwise, give 0 priority 45 | return 0; 46 | }; 47 | 48 | var movePriority = module.exports.movePriority = function(battle, move, p1, p2) { 49 | var myPokemon = p1.active[0]; 50 | var oppPokemon = p2.active[0]; 51 | 52 | var moveData = Tools.getMove(move.id); 53 | 54 | //Light screen, reflect, or tailwind, and make sure they aren't already put up: 12 55 | var helpfulSideEffects = ["reflect","lightscreen","tailwind"]; 56 | if(helpfulSideEffects.indexOf(move.id) >= 0 && !p1.getSideCondition(move.id)) { 57 | return 12; 58 | } 59 | 60 | //Entry hazard: stealth rock, spikes, toxic spikes, or sticky web: 11 61 | var entryHazards = ["stealthrock","spikes","toxicspikes","stickyweb"]; 62 | if(entryHazards.indexOf(move.id) >= 0 && !p2.getSideCondition(move.id)) { 63 | return 11; 64 | } 65 | 66 | //Status effect: thunder wave, toxic, willowisp, glare, nuzzle: 10 67 | if(move.category === "Status" && move.status && !oppPokemon.status) { 68 | return 10; 69 | } 70 | 71 | //Recovery move: soft-boiled, recover, synthesis, moonlight, morning sun if hp is low enough: 9 72 | var recovery = ["softboiled", "recover", "synthesis", "moonlight", "morningsun"]; 73 | if(recovery.indexOf(move.id) >= 0 && myPokemon.hp * 2 < myPokemon.maxhp) { 74 | return 9; 75 | } 76 | 77 | //Super effective move with STAB: 8 78 | if(Tools.getEffectiveness(moveData, oppPokemon) > 0 && 79 | (moveData.basePower > 0 || damagingMoves.indexOf(move.id) >= 0) && 80 | myPokemon.getTypes().indexOf(moveData.type) >= 0 && 81 | Tools.getImmunity(moveData.type, oppPokemon.getTypes())) { 82 | return 8; 83 | } 84 | 85 | //Super effective move with no STAB: 7 86 | if(Tools.getEffectiveness(moveData, oppPokemon) > 0 && 87 | (moveData.basePower > 0 || damagingMoves.indexOf(move.id) >= 0) && 88 | Tools.getImmunity(moveData.type, oppPokemon.getTypes())) { 89 | return 7; 90 | } 91 | 92 | /*//If there is a super effective move, return 0 if there are good switches 93 | if(_.any(this.oppPokemon.getTypes(), function(oppType) { 94 | return Tools.getEffectiveness(oppType, myPokemon.getTypes()) > 0 && 95 | Tools.getImmunity(oppType, myPokemon.getTypes()); 96 | })) { 97 | return 0; 98 | }*/ 99 | 100 | //Find move with STAB: 6 101 | if(Tools.getEffectiveness(moveData, oppPokemon) === 0 && 102 | (moveData.basePower > 0 || damagingMoves.indexOf(move.id) >= 0) && 103 | myPokemon.getTypes().indexOf(moveData.type) >= 0 && 104 | Tools.getImmunity(moveData.type, oppPokemon.getTypes())) { 105 | return 6; 106 | } 107 | 108 | //Find normally effective move: 1 109 | if(Tools.getEffectiveness(moveData, oppPokemon) === 0 && 110 | (moveData.basePower > 0 || damagingMoves.indexOf(move.id) >= 0) && 111 | Tools.getImmunity(moveData.type, oppPokemon.getTypes())) { 112 | return 1; 113 | } 114 | 115 | //Otherwise, give 0 priority 116 | return 0; 117 | 118 | }; 119 | 120 | var getPriority = module.exports.getPriority = function(battle, choice, p1, p2) { 121 | if(choice.type === "switch") 122 | return switchPriority(battle, choice, p1, p2); 123 | else 124 | return movePriority(battle, choice, p1, p2); 125 | }; 126 | 127 | var decide = module.exports.decide = function(battle, choices, p1, p2) { 128 | if(!p1 || !p2) { //if not supplied, assume we are p1 129 | p1 = battle.p1; 130 | p2 = battle.p2; 131 | } 132 | 133 | var bestChoice = _.max(choices, function(choice) { 134 | var priority = getPriority(battle, choice, p1, p2); 135 | choice.priority = priority; 136 | return priority; 137 | }); 138 | 139 | switch(bestChoice.priority) { 140 | case 12: logger.info("Chose " + bestChoice.id + " because it provides helpful side effects."); break; 141 | case 11: logger.info("Chose " + bestChoice.id + " because it is an entry hazard."); break; 142 | case 10: logger.info("Chose " + bestChoice.id + " because it causes a status effect."); break; 143 | case 9: logger.info("Chose " + bestChoice.id + " because it recovers hp."); break; 144 | case 8: logger.info("Chose " + bestChoice.id + " because it is super effective with STAB."); break; 145 | case 7: logger.info("Chose " + bestChoice.id + " because it is super effective."); break; 146 | case 6: logger.info("Chose " + bestChoice.id + " because it has STAB."); break; 147 | case 5: logger.info("Switched to " + p1.pokemon[bestChoice.id].name + " because it is immmune to the opponent's types."); break; 148 | case 4: logger.info("Switched to " + p1.pokemon[bestChoice.id].name + " because it resists the opponent's types."); break; 149 | case 3: logger.info("Switched to " + p1.pokemon[bestChoice.id].name + " because it recieves neutral damage from the opponent."); break; 150 | case 2: logger.info("Switched to " + p1.pokemon[bestChoice.id].name + " because it can deal super effective damage to the opponent."); break; 151 | case 1: logger.info("Chose " + bestChoice.id + " because it is normally effective."); break; 152 | case 0: logger.info("Chose " + bestChoice.id + " because we had no better option."); break; 153 | default: logger.error("Unknown priority."); 154 | } 155 | logger.info("Move has priority: " + bestChoice.priority); 156 | return { 157 | type: bestChoice.type, 158 | id: bestChoice.id, 159 | priority: bestChoice.priority 160 | }; 161 | }; 162 | -------------------------------------------------------------------------------- /bots/minimaxbot.js: -------------------------------------------------------------------------------- 1 | // Logging 2 | var log4js = require('log4js'); 3 | var logger = require('log4js').getLogger("minimax"); 4 | var learnlog = require('log4js').getLogger("learning"); 5 | 6 | var program = require('commander'); // Program settings 7 | var fs = require('fs'); 8 | 9 | var _ = require("underscore"); 10 | var BattleRoom = require("./../battleroom"); 11 | 12 | var randombot = require("./randombot"); 13 | var greedybot = require("./greedybot"); 14 | 15 | var clone = require("./../clone"); 16 | 17 | var convnetjs = require("convnetjs"); 18 | 19 | // Extract a feature vector from the hash. This is to maintain a specific order 20 | var BATTLE_FEATURES = ["items", "faster", "has_supereffective", "has_stab"]; 21 | var SIDE_CONDITIONS = ["reflect", "spikes", "stealthrock", "stickyweb", "toxicspikes", "lightscreen", "tailwind"]; 22 | var VOLATILES = ["substitute", 'confusion', 'leechseed', 'infestation']; 23 | var STATUSES = ["psn", "tox", "slp", "brn", "frz", "par"]; 24 | var BOOSTS = ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; 25 | 26 | _.each(SIDE_CONDITIONS, function(condition) { 27 | BATTLE_FEATURES.push("p1_" + condition); 28 | BATTLE_FEATURES.push("p2_" + condition); 29 | }); 30 | 31 | _.each(VOLATILES, function(volatile) { 32 | BATTLE_FEATURES.push("p1_" + volatile); 33 | BATTLE_FEATURES.push("p2_" + volatile); 34 | }); 35 | 36 | _.each(BOOSTS, function(boost) { 37 | BATTLE_FEATURES.push("p1_" + boost); 38 | BATTLE_FEATURES.push("p2_" + boost); 39 | }); 40 | 41 | _.each(STATUSES, function(status) { 42 | BATTLE_FEATURES.push("p1_" + status + "_count"); 43 | BATTLE_FEATURES.push("p2_" + status + "_count"); 44 | }); 45 | 46 | BATTLE_FEATURES.push("p1_hp"); 47 | BATTLE_FEATURES.push("p2_hp"); 48 | 49 | module.exports.BATTLE_FEATURES = BATTLE_FEATURES; 50 | 51 | function featureVector(battle) { 52 | var features = getFeatures(battle); 53 | var vec = _.map(BATTLE_FEATURES, function(feature) { 54 | return features[feature]; 55 | }); 56 | return new convnetjs.Vol(vec); 57 | } 58 | 59 | 60 | // Initialize neural network 61 | var net = undefined; 62 | var trainer = undefined; 63 | if(program.net === "create") { 64 | learnlog.info("Creating neural network..."); 65 | 66 | // Multi-layer neural network 67 | var layer_defs = []; 68 | layer_defs.push({type: 'input', out_sx: 1, out_sy: 1, out_depth: BATTLE_FEATURES.length}); 69 | layer_defs.push({type:'fc', num_neurons:10, activation:'relu'}); 70 | layer_defs.push({type:'fc', num_neurons:10, activation:'sigmoid'}); 71 | layer_defs.push({type: 'regression', num_neurons: 1}); 72 | 73 | net = new convnetjs.Net(); 74 | net.makeLayers(layer_defs); 75 | 76 | _.each(net.layers, function(layer) { 77 | if(layer.filters) { 78 | _.each(layer.filters, function(filter) { 79 | if(filter.w) { 80 | var num = filter.w.byteLength / filter.w.BYTES_PER_ELEMENT; 81 | for(var i = 0; i < num; ++i) filter.w.set([0.0], i); 82 | } 83 | }); 84 | } 85 | }); 86 | 87 | fs.writeFileSync("network.json", JSON.stringify(net.toJSON())); 88 | program.net = "update"; // Now that the network is created, it should also be updated 89 | learnlog.info("Created neural network..."); 90 | } else if(program.net === "use" || program.net === "update") { 91 | learnlog.info("Loading neural network..."); 92 | net = new convnetjs.Net(); 93 | net.fromJSON(JSON.parse(fs.readFileSync("network.json", "utf8"))); 94 | } 95 | module.exports.net = net; 96 | 97 | // If we need to be able to update the network, create a trainer object 98 | if(program.net === "update") { 99 | trainer = new convnetjs.Trainer(net, {method: 'adadelta', l2_decay: 0.001, 100 | batch_size: 1}); 101 | learnlog.trace("Created SGD Trainer"); 102 | } 103 | 104 | // Train the network on a battle, newbattle 105 | // If this is a reward state, set newbattle to null, and win to whether or not the bot won 106 | var train_net = module.exports.train_net = function(battle, newbattle, win) { 107 | learnlog.info("Training neural network..."); 108 | 109 | var value = undefined; 110 | 111 | if (newbattle == null) { 112 | value = win ? GAME_END_REWARD : -GAME_END_REWARD; 113 | 114 | // Apply discount 115 | value *= DISCOUNT; 116 | } 117 | else { 118 | value = DISCOUNT * eval(newbattle); 119 | 120 | var isAlive = function(pokemon) { return pokemon.hp > 0; }; 121 | var opponentDied = _.filter(battle.p2.pokemon, isAlive).length - _.filter(newbattle.p2.pokemon, isAlive).length; 122 | var playerDied = _.filter(battle.p1.pokemon, isAlive).length - _.filter(newbattle.p1.pokemon, isAlive).length; 123 | value += opponentDied * 10; 124 | value -= playerDied * 10; 125 | 126 | if(opponentDied > 0) learnlog.info("Rewarded for killing an opponent pokemon."); 127 | if(playerDied > 0) learnlog.info("Negative rewarded for losing a pokemon."); 128 | } 129 | 130 | 131 | 132 | var vec = featureVector(battle); 133 | trainer.train(vec, [value]); 134 | 135 | fs.writeFileSync("network.json", JSON.stringify(net.toJSON(), undefined, 2)); 136 | } 137 | 138 | //TODO: Features should not take into account Bulbasaur pokemon. (Doesn't really matter now, but it will...) 139 | function getFeatures(battle) { 140 | var features = {}; 141 | 142 | // Side conditions 143 | _.each(SIDE_CONDITIONS, function(condition) { 144 | features["p1_" + condition] = (condition in battle.p1.sideConditions) ? 1 : 0; 145 | features["p2_" + condition] = (condition in battle.p2.sideConditions) ? 1 : 0; 146 | }); 147 | 148 | // Volatile statuses on current pokemon 149 | _.each(VOLATILES, function(volatile) { 150 | features["p1_" + volatile] = (volatile in battle.p1.active[0].volatiles ? 1 : 0); 151 | features["p2_" + volatile] = (volatile in battle.p2.active[0].volatiles ? 1 : 0); 152 | }); 153 | 154 | // Boosts on pokemon 155 | _.each(BOOSTS, function(boost) { 156 | features["p1_" + boost] = battle.p1.active[0].boosts[boost]; 157 | features["p2_" + boost] = battle.p2.active[0].boosts[boost]; 158 | }); 159 | 160 | //total hp 161 | //Note: as hp depletes to zero it becomes increasingly less important. 162 | //Pokemon with low hp have a higher chance of dying upon switching in 163 | //or dying due to a faster opponent. This is more important for slow 164 | //pokemon. 165 | //good to keep into consideration... 166 | features["p1_hp"] = 0; 167 | features["p2_hp"] = 0; 168 | 169 | //alive pokemon 170 | features["p1_alive"] = 0; 171 | features["p2_alive"] = 0; 172 | 173 | //alive fast pokemon? 174 | features["p1_fast_alive"] = 0; 175 | features["p2_fast_alive"] = 0; 176 | 177 | //status effects. TODO: some status effects are worse on some pokemon than others 178 | //paralyze: larger effects on fast, frail pokemon 179 | //burn: larger effects on physical attackers 180 | //toxic poison: larger effects on bulky attackers 181 | //sleep: bad for everyone 182 | //freeze: quite unfortunate. 183 | _.each(STATUSES, function(status) { 184 | features["p1_" + status + "_count"] = 0; 185 | features["p2_" + status + "_count"] = 0; 186 | }); 187 | 188 | // Per pokemon features 189 | for(var i = 0; i < 6; ++i) { 190 | features["p1_hp"] += (battle.p1.pokemon[i].hp ? battle.p1.pokemon[i].hp : 0) / battle.p1.pokemon[i].maxhp; 191 | features["p2_hp"] += (battle.p2.pokemon[i].hp ? battle.p2.pokemon[i].hp : 0) / battle.p2.pokemon[i].maxhp; 192 | 193 | if(battle.p1.pokemon[i].hp) ++features["p1_alive"]; 194 | if(battle.p2.pokemon[i].hp) ++features["p2_alive"]; 195 | 196 | if(_.contains(STATUSES, battle.p1.pokemon[i].status)) { 197 | ++features["p1_" + battle.p1.pokemon[i].status + "_count"]; 198 | if(battle.p1.pokemon[i].status === "brn" && //weight burn and par differently 199 | battle.p1.pokemon[i].baseStats.atk >= 180) { 200 | ++features["p1_" + battle.p1.pokemon[i].status + "_count"]; 201 | } 202 | if(battle.p1.pokemon[i].status === "par" && 203 | battle.p1.pokemon[i].baseStats.spe >= 180) { 204 | ++features["p1_" + battle.p1.pokemon[i].status + "_count"]; 205 | } 206 | } 207 | if(_.contains(STATUSES, battle.p2.pokemon[i].status)) { 208 | ++features["p2_" + battle.p2.pokemon[i].status + "_count"]; 209 | if(battle.p2.pokemon[i].status === "brn" && 210 | battle.p2.pokemon[i].baseStats.atk >= 180) { 211 | ++features["p2_" + battle.p2.pokemon[i].status + "_count"]; 212 | } 213 | if(battle.p2.pokemon[i].status === "par" && 214 | battle.p2.pokemon[i].baseStats.spe >= 180) { 215 | ++features["p2_" + battle.p2.pokemon[i].status + "_count"]; 216 | } 217 | 218 | } 219 | } 220 | 221 | // If slp count is greater than 1, set cost to losing the game (sleep clause mod) 222 | // Record this for opponent as well 223 | if(features["p1_slp_count"] > 1) 224 | features["p1_slp_count"] = -GAME_END_REWARD; 225 | if(features["p2_slp_count"] < 1) 226 | features["p2_slp_count"] = -GAME_END_REWARD; 227 | //features["p1_slp_count"] = Math.min(features["p1_slp_count"], 1); 228 | //features["p2_slp_count"] = Math.min(features["p2_slp_count"], 1); 229 | 230 | //If sleep count is 231 | 232 | //items: prefer to have items rather than lose them (such as berries, focus sash, ...) 233 | features.items = _.reduce(battle.p1.pokemon, function (memo, pokemon) { 234 | return memo + (pokemon.item && pokemon.hp ? 1 : 0); 235 | }, 0); 236 | 237 | //the current matchup. Dependent on several factors: 238 | //-speed comparison. generally want higher speed (unless we're bulky, in which case that's fine) 239 | features.faster = (battle.p1.active[0].speed > battle.p2.active[0].speed) ? 1 : 0; 240 | 241 | //-damage potential. Use greedybot to determine if there are good moves that we have in this state 242 | var choices = BattleRoom.parseRequest(battle.p1.request).choices; 243 | var priorities = _.map(choices, function(choice) { 244 | return greedybot.getPriority(battle, choice, battle.p1, battle.p2); 245 | }); 246 | 247 | features["has_supereffective"] = (_.contains(priorities, 7) || _.contains(priorities, 8)) ? 1 : 0; 248 | features["has_stab"] = (_.contains(priorities, 6) || _.contains(priorities, 8)) ? 1 : 0; 249 | 250 | //overall pokemon variety. Overall we want a diverse set of pokemon. 251 | //-types: want a variety of types to be good defensively vs. opponents 252 | //-moves: want a vareity of types to be good offensively vs. opponents 253 | //-stat spreads: we don't really want all physical or all special attackers. 254 | // also, our pokemon should be able to fulfill different roles, so we want 255 | // to keep a tanky pokemon around or a wall-breaker around 256 | 257 | return features; 258 | } 259 | 260 | var weights = require("./../weights.js"); 261 | 262 | //TODO: Eval function needs to be made 1000x better 263 | function eval(battle) { 264 | var value = 0; 265 | var features = getFeatures(battle); 266 | 267 | if(program.net === "none") { 268 | for (var key in weights) { 269 | if(key in features) value += weights[key] * features[key]; 270 | } 271 | } else if (program.net === "update" || program.net === "use") { 272 | var vec = featureVector(battle); 273 | value = net.forward(vec).w[0]; 274 | } 275 | 276 | logger.trace(JSON.stringify(features) + ": " + value); 277 | return value; 278 | } 279 | 280 | var overallMinNode = {}; 281 | var lastMove = ''; 282 | var decide = module.exports.decide = function(battle, choices) { 283 | var startTime = new Date(); 284 | battle.start(); 285 | 286 | var MAX_DEPTH = 2; //for now... 287 | var maxNode = playerTurn(battle, MAX_DEPTH, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, choices); 288 | if(!maxNode.action) return randombot.decide(battle, choices); 289 | logger.info("My action: " + maxNode.action.type + " " + maxNode.action.id); 290 | if(overallMinNode.action) 291 | logger.info("Predicted opponent action: " + overallMinNode.action.type + " " + overallMinNode.action.id); 292 | lastMove = maxNode.action.id; 293 | var endTime = new Date(); 294 | logger.info("Decision took: " + (endTime - startTime) / 1000 + " seconds"); 295 | return { 296 | type: maxNode.action.type, 297 | id: maxNode.action.id, 298 | tree: maxNode 299 | }; 300 | } 301 | 302 | var GAME_END_REWARD = module.exports.GAME_END_REWARD = 1000000; 303 | var DISCOUNT = module.exports.DISCOUNT = 0.98; 304 | 305 | //TODO: Implement move ordering, which can be based on the original greedy algorithm 306 | //However, it should have slightly different priorities, such as status effects... 307 | function playerTurn(battle, depth, alpha, beta, givenchoices) { 308 | logger.trace("Player turn at depth " + depth); 309 | 310 | // Node in the minimax tree 311 | var node = { 312 | type : "max", 313 | value : Number.NEGATIVE_INFINITY, 314 | depth : depth, 315 | choices : [], 316 | children : [], 317 | action : null, 318 | state : battle.toString() 319 | }; 320 | 321 | // Look for win / loss 322 | var playerAlive = _.any(battle.p1.pokemon, function(pokemon) { return pokemon.hp > 0; }); 323 | var opponentAlive = _.any(battle.p2.pokemon, function(pokemon) { return pokemon.hp > 0; }); 324 | if (!playerAlive || !opponentAlive) { 325 | node.value = playerAlive ? GAME_END_REWARD : -GAME_END_REWARD; 326 | return node; 327 | } 328 | 329 | if(depth == 0) { 330 | node.value = eval(battle); 331 | node.state += "\n" + JSON.stringify(getFeatures(battle), undefined, 2); 332 | } else { 333 | // If the request is a wait request, the opposing player has to take a turn, and we don't 334 | if(battle.p1.request.wait) { 335 | return opponentTurn(battle, depth, alpha, beta, null); 336 | } 337 | var choices = (givenchoices) ? givenchoices : BattleRoom.parseRequest(battle.p1.request).choices; 338 | //sort choices 339 | choices = _.sortBy(choices, function(choice) { 340 | var priority = greedybot.getPriority(battle, choice, battle.p1, battle.p2); 341 | choice.priority = priority; 342 | return -priority; 343 | }); 344 | for(var i = 0; i < choices.length; i++) { 345 | logger.info(choices[i].id + " with priority " + choices[i].priority); 346 | } 347 | //choices = _.sample(choices, 1); // For testing 348 | //TODO: before looping through moves, move choices from array to priority queue to give certain moves higher priority than others 349 | //Essentially, the greedy algorithm 350 | //Perhaps then we can increase the depth... 351 | 352 | for(var i = 0; i < choices.length; ++i) { 353 | if(choices[i].id === 'wish' && lastMove === 'wish') //don't wish twice in a row 354 | continue; 355 | if(choices[i].id === 'protect' && lastMove === 'protect') //don't protect twice in a row. Not completely accurate... 356 | continue; 357 | if(choices[i].id === 'spikysheild' && lastMove === 'spikyshield') //don't protect twice in a row. Not completely accurate... 358 | continue; 359 | if(choices[i].id === 'kingsshield' && lastMove === 'kingssheild') //don't protect twice in a row. Not completely accurate... 360 | continue; 361 | if(choices[i].id === 'detect' && lastMove === 'detect') //don't protect twice in a row. Not completely accurate... 362 | continue; 363 | 364 | if(choices[i].id === 'fakeout' && lastMove === 'fakeout') //don't fakeout twice in a row. Not completely accurate... 365 | continue; 366 | 367 | // Try action 368 | var minNode = opponentTurn(battle, depth, alpha, beta, choices[i]); 369 | node.children.push(minNode); 370 | 371 | if(minNode.value != null && isFinite(minNode.value) ) { 372 | if(minNode.value > node.value) { 373 | node.value = minNode.value; 374 | node.action = choices[i]; 375 | overallMinNode = minNode; 376 | } 377 | alpha = Math.max(alpha, minNode.value); 378 | if(beta <= alpha) break; 379 | } 380 | } 381 | 382 | node.choices = choices; 383 | } 384 | 385 | return node; 386 | } 387 | 388 | function opponentTurn(battle, depth, alpha, beta, playerAction) { 389 | logger.trace("Opponent turn turn at depth " + depth); 390 | 391 | // Node in the minimax tree 392 | var node = { 393 | type : "min", 394 | value : Number.POSITIVE_INFINITY, 395 | depth : depth, 396 | choices : [], 397 | children : [], 398 | action : null, 399 | state: battle.toString() 400 | } 401 | 402 | // If the request is a wait request, only the player chooses an action 403 | if(battle.p2.request.wait) { 404 | var newbattle = clone(battle); 405 | newbattle.p2.decision = true; 406 | newbattle.choose('p1', BattleRoom.toChoiceString(playerAction, newbattle.p1), newbattle.rqid); 407 | return playerTurn(newbattle, depth - 1, alpha, beta); 408 | } 409 | 410 | var choices = BattleRoom.parseRequest(battle.p2.request).choices; 411 | 412 | // Make sure we can't switch to a Bulbasaur or to a fainted pokemon 413 | choices = _.reject(choices, function(choice) { 414 | if(choice.type == "switch" && 415 | (battle.p2.pokemon[choice.id].name == "Bulbasaur" || 416 | !battle.p2.pokemon[choice.id].hp)) return true; 417 | return false; 418 | }); 419 | 420 | // We don't have enough info to simulate the battle anymore 421 | if(choices.length == 0) { 422 | node.value = eval(battle); 423 | node.state += "\n" + JSON.stringify(getFeatures(battle), undefined, 2); 424 | return node; 425 | } 426 | 427 | //sort choices 428 | choices = _.sortBy(choices, function(choice) { 429 | var priority = greedybot.getPriority(battle, choice, battle.p2, battle.p1); 430 | choice.priority = priority; 431 | return -priority; 432 | }); 433 | for(var i = 0; i < choices.length; i++) { 434 | logger.info(choices[i].id + " with priority " + choices[i].priority); 435 | } 436 | 437 | // Take top 10 choices, to limit breadth of tree 438 | choices = _.take(choices, 10); 439 | 440 | for(var i = 0; i < choices.length; ++i) { 441 | logger.trace("Cloning battle..."); 442 | var newbattle = clone(battle); 443 | 444 | // Register action, let battle simulate 445 | if(playerAction) 446 | newbattle.choose('p1', BattleRoom.toChoiceString(playerAction, newbattle.p1), newbattle.rqid); 447 | else 448 | newbattle.p1.decision = true; 449 | newbattle.choose('p2', BattleRoom.toChoiceString(choices[i], newbattle.p2), newbattle.rqid); 450 | 451 | logger.info("Player action: " + BattleRoom.toChoiceString(playerAction, newbattle.p1)); 452 | logger.info("Opponent action: " + BattleRoom.toChoiceString(choices[i], newbattle.p2)); 453 | logger.info("My Resulting Health:"); 454 | for(var j = 0; j < newbattle.p1.pokemon.length; j++) { 455 | logger.info(newbattle.p1.pokemon[j].id + ": " + newbattle.p1.pokemon[j].hp + "/" + newbattle.p1.pokemon[j].maxhp); 456 | } 457 | logger.info("Opponent's Resulting Health:"); 458 | for(var j = 0; j < newbattle.p2.pokemon.length; j++) { 459 | logger.info(newbattle.p2.pokemon[j].id + ": " + newbattle.p2.pokemon[j].hp + "/" + newbattle.p2.pokemon[j].maxhp); 460 | } 461 | var maxNode = playerTurn(newbattle, depth - 1, alpha, beta); 462 | node.children.push(maxNode); 463 | 464 | if(maxNode.value != null && isFinite(maxNode.value)) { 465 | if(maxNode.value < node.value) { 466 | node.value = maxNode.value; 467 | node.action = choices[i]; 468 | } 469 | beta = Math.min(beta, maxNode.value); 470 | if(beta <= alpha) break; 471 | } 472 | 473 | // Hopefully prompt garbage collection, so we don't maintain too many battle object 474 | delete newbattle; 475 | if(global.gc) global.gc() 476 | } 477 | 478 | node.choices = choices; 479 | return node; 480 | } 481 | -------------------------------------------------------------------------------- /bots/randombot.js: -------------------------------------------------------------------------------- 1 | /* Randombot - to be used primarily for testing 2 | Can also be used as a fallback, in case another decision algorithm 3 | fails or crashes */ 4 | 5 | var _ = require("underscore"); 6 | 7 | var decide = module.exports.decide = function(battle, choices) { 8 | return _.shuffle(choices)[0]; 9 | }; -------------------------------------------------------------------------------- /clone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function objectToString(o) { 4 | return Object.prototype.toString.call(o); 5 | } 6 | 7 | // shim for Node's 'util' package 8 | // DO NOT REMOVE THIS! It is required for compatibility with EnderJS (http://enderjs.com/). 9 | var util = { 10 | isArray: function (ar) { 11 | return Array.isArray(ar) || (typeof ar === 'object' && objectToString(ar) === '[object Array]'); 12 | }, 13 | isDate: function (d) { 14 | return typeof d === 'object' && objectToString(d) === '[object Date]'; 15 | }, 16 | isRegExp: function (re) { 17 | return typeof re === 'object' && objectToString(re) === '[object RegExp]'; 18 | }, 19 | getRegExpFlags: function (re) { 20 | var flags = ''; 21 | re.global && (flags += 'g'); 22 | re.ignoreCase && (flags += 'i'); 23 | re.multiline && (flags += 'm'); 24 | return flags; 25 | } 26 | }; 27 | 28 | 29 | if (typeof module === 'object') 30 | module.exports = clone; 31 | 32 | /** 33 | * Clones (copies) an Object using deep copying. 34 | * 35 | * This function supports circular references by default, but if you are certain 36 | * there are no circular references in your object, you can save some CPU time 37 | * by calling clone(obj, false). 38 | * 39 | * Caution: if `circular` is false and `parent` contains circular references, 40 | * your program may enter an infinite loop and crash. 41 | * 42 | * @param `parent` - the object to be cloned 43 | * @param `circular` - set to true if the object to be cloned may contain 44 | * circular references. (optional - true by default) 45 | * @param `depth` - set to a number if the object is only to be cloned to 46 | * a particular depth. (optional - defaults to Infinity) 47 | * @param `prototype` - sets the prototype to be used when cloning an object. 48 | * (optional - defaults to parent prototype). 49 | */ 50 | 51 | function clone(parent, circular, depth, prototype) { 52 | // maintain cache of already cloned objects, to deal with circular references 53 | var children = []; 54 | var parents = []; 55 | 56 | var useBuffer = typeof Buffer != 'undefined'; 57 | 58 | if (typeof circular == 'undefined') 59 | circular = true; 60 | 61 | if (typeof depth == 'undefined') 62 | depth = Infinity; 63 | 64 | // recurse this function so we don't reset allParents and allChildren 65 | function _clone(parent, depth) { 66 | // cloning null always returns null 67 | if (parent === null) 68 | return null; 69 | 70 | if (depth == 0) 71 | return parent; 72 | 73 | var child; 74 | var proto; 75 | if (typeof parent != 'object') { 76 | return parent; 77 | } 78 | 79 | if (util.isArray(parent)) { 80 | child = []; 81 | } else if (util.isRegExp(parent)) { 82 | child = new RegExp(parent.source, util.getRegExpFlags(parent)); 83 | if (parent.lastIndex) child.lastIndex = parent.lastIndex; 84 | } else if (util.isDate(parent)) { 85 | child = new Date(parent.getTime()); 86 | } else if (useBuffer && Buffer.isBuffer(parent)) { 87 | child = new Buffer(parent.length); 88 | parent.copy(child); 89 | return child; 90 | } else { 91 | if (typeof prototype == 'undefined') { 92 | proto = Object.getPrototypeOf(parent); 93 | child = Object.create(proto); 94 | } 95 | else { 96 | child = Object.create(prototype); 97 | proto = prototype; 98 | } 99 | } 100 | 101 | if (circular) { 102 | if (parent.clone_id != undefined) { 103 | return children[parent.clone_id]; 104 | } 105 | 106 | parent.clone_id = children.length; 107 | children.push(child); 108 | parents.push(parent); 109 | } 110 | 111 | for (var i in parent) { 112 | if(parent.hasOwnProperty(i)) child[i] = _clone(parent[i], depth - 1); 113 | } 114 | 115 | return child; 116 | } 117 | 118 | var cloned = _clone(parent, depth); 119 | 120 | // Remove the temporary clone id's 121 | for(var i in parents) delete parents[i].clone_id; 122 | for(var i in children) delete children[i].clone_id; 123 | 124 | return cloned; 125 | } 126 | 127 | /** 128 | * Simple flat clone using prototype, accepts only objects, usefull for property 129 | * override on FLAT configuration object (no nested props). 130 | * 131 | * USE WITH CAUTION! This may not behave as you wish if you do not know how this 132 | * works. 133 | */ 134 | clone.clonePrototype = function(parent) { 135 | if (parent === null) 136 | return null; 137 | 138 | var c = function () {}; 139 | c.prototype = parent; 140 | return new c(); 141 | }; 142 | -------------------------------------------------------------------------------- /console.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var nunjucks = require('nunjucks'); 4 | var bot = require('./bot') 5 | var program = require('commander'); // Get Command-line arguments 6 | 7 | // Results database 8 | var db = require("./db"); 9 | 10 | var _ = require("underscore") 11 | 12 | // Setup Logging 13 | var log4js = require('log4js'); 14 | var logger = require('log4js').getLogger("webconsole"); 15 | 16 | var CHALLENGING = false; 17 | if(program.startchallenging) CHALLENGING = true; 18 | 19 | var minimaxbot = require("./bots/minimaxbot"); 20 | 21 | // Challenging logic 22 | var MAX_ROOMS = 1; 23 | setInterval(function() { 24 | if(CHALLENGING && _.values(bot.ROOMS).length < MAX_ROOMS) { 25 | logger.info("Challenging..."); 26 | bot.searchBattle(); 27 | } 28 | }, 45000); 29 | 30 | nunjucks.configure('templates', { 31 | autoescape: true, 32 | express: app, 33 | watch: true 34 | }); 35 | 36 | app.get('/', function(req, res){ 37 | db.find({}).sort({ date: -1}).exec(function(err, history) { 38 | res.render('home.html', { 39 | "games" : _.values(bot.ROOMS), 40 | "domain" : bot.DOMAIN, 41 | "history" : history, 42 | "challenging" : CHALLENGING 43 | }); 44 | }); 45 | }); 46 | 47 | // Challenge a specific user 48 | app.get('/challenge', function(req, res){ 49 | bot.send("/challenge " + req.query.user + ", randombattle", null); 50 | res.redirect("/"); 51 | }); 52 | 53 | app.get('/weights', function(req, res){ 54 | var text = ""; 55 | _.each(minimaxbot.BATTLE_FEATURES, function (feature, index) { 56 | var value = minimaxbot.net.layers[1].filters[0].w.get(index); 57 | text += feature + ": " + value + "
"; 58 | }) 59 | res.send(text); 60 | }); 61 | 62 | // Challenging control 63 | app.get('/startchallenging', function(req, res){ 64 | CHALLENGING = true; 65 | res.redirect("/"); 66 | }); 67 | app.get('/endchallenging', function(req, res){ 68 | CHALLENGING = false; 69 | res.redirect("/"); 70 | }); 71 | 72 | app.get('/room', function(req, res){ 73 | if(bot.ROOMS[req.query.id]) { 74 | res.render("room.html", { 75 | game: bot.ROOMS[req.query.id], 76 | stringify : JSON.stringify, 77 | format: function(str) { 78 | return str.replace(/\n/g, "
").replace(/\t/g, "    "); 79 | } 80 | }); 81 | } else { 82 | res.redirect("/"); 83 | } 84 | }); 85 | 86 | app.get('/replay', function(req, res){ 87 | db.findOne({ id: req.query.id }).exec(function(err, game) { 88 | if(!game) { 89 | res.redirect("/"); 90 | return; 91 | } 92 | 93 | game.decisions = JSON.parse(game.decisions); 94 | res.render('replay.html', { 95 | game : game, 96 | stringify : JSON.stringify 97 | }); 98 | }); 99 | }); 100 | 101 | app.get('/search', function(req, res){ 102 | logger.debug("Asked to query from web console."); 103 | bot.searchBattle(); 104 | res.redirect("/"); 105 | }); 106 | 107 | var port = parseInt(program.port); 108 | app.listen(port); 109 | logger.info("Started web console on port " + port + "..."); 110 | 111 | module.exports = app; 112 | -------------------------------------------------------------------------------- /data/aliases.js: -------------------------------------------------------------------------------- 1 | exports.BattleAliases = { 2 | // formats 3 | "randbats": "Random Battle", 4 | "overused": "OU", 5 | "underused": "UU", 6 | "rarelyused": "RU", 7 | "neverused": "NU", 8 | "vgc": "VGC 2014", 9 | "bh": "Balanced Hackmons", 10 | "createapokemon": "CAP", 11 | "cc1v1": "Challenge Cup 1vs1", 12 | 13 | // mega evos 14 | "megaabomasnow": "Abomasnow-Mega", 15 | "megaabsol": "Absol-Mega", 16 | "megaaerodactyl": "Aerodactyl-Mega", 17 | "megaaggron": "Aggron-Mega", 18 | "megaalakazam": "Alakazam-Mega", 19 | "megaaltaria": "Altaria-Mega", 20 | "megaampharos": "Ampharos-Mega", 21 | "megaaudino": "Audino-Mega", 22 | "megabanette": "Banette-Mega", 23 | "megabeedrill": "Beedrill-Mega", 24 | "megablastoise": "Blastoise-Mega", 25 | "megablaziken": "Blaziken-Mega", 26 | "megacamerupt": "Camerupt-Mega", 27 | "megacharizard": "Charizard-Mega-Y", 28 | "megacharizardx": "Charizard-Mega-X", 29 | "megacharizardy": "Charizard-Mega-Y", 30 | "megadiancie": "Diancie-Mega", 31 | "megagallade": "Gallade-Mega", 32 | "megagarchomp": "Garchomp-Mega", 33 | "megagardevoir": "Gardevoir-Mega", 34 | "megagengar": "Gengar-Mega", 35 | "megaglalie": "Glalie-Mega", 36 | "megagyarados": "Gyarados-Mega", 37 | "megaheracross": "Heracross-Mega", 38 | "megahoundoom": "Houndoom-Mega", 39 | "megakangaskhan": "Kangaskhan-Mega", 40 | "megalatias": "Latias-Mega", 41 | "megalatios": "Latios-Mega", 42 | "megalopunny": "Lopunny-Mega", 43 | "megalucario": "Lucario-Mega", 44 | "megaluke": "Lucario-Mega", 45 | "megamanectric": "Manectric-Mega", 46 | "megamawile": "Mawile-Mega", 47 | "megamaw": "Mawile-Mega", 48 | "megamedicham": "Medicham-Mega", 49 | "megamedi": "Medicham-Mega", 50 | "megametagross": "Metagross-Mega", 51 | "megamewtwo": "Mewtwo-Mega-Y", 52 | "megamewtwox": "Mewtwo-Mega-X", 53 | "megamewtwoy": "Mewtwo-Mega-Y", 54 | "megaobama": "Abomasnow-Mega", 55 | "megapidgeot": "Pidgeot-Mega", 56 | "megapinsir": "Pinsir-Mega", 57 | "megarayquaza": "Rayquaza-Mega", 58 | "megasableye": "Sableye-Mega", 59 | "megasalamence": "Salamence-Mega", 60 | "megamence": "Salamence-Mega", 61 | "megasceptile": "Sceptile-Mega", 62 | "megascizor": "Scizor-Mega", 63 | "megasharpedo": "Sharpedo-Mega", 64 | "megaslowbro": "Slowbro-Mega", 65 | "megasteelix": "Steelix-Mega", 66 | "megaswampert": "Swampert-Mega", 67 | "megatyranitar": "Tyranitar-Mega", 68 | "megattar": "Tyranitar-Mega", 69 | "megavenusaur": "Venusaur-Mega", 70 | "megavenu": "Venusaur-Mega", 71 | "megazam": "Alakazam-Mega", 72 | "megazardx": "Charizard-Mega-X", 73 | "megazardy": "Charizard-Mega-y", 74 | "mmx": "Mewtwo-Mega-X", 75 | "mmy": "Mewtwo-Mega-Y", 76 | 77 | // primal reversions 78 | "primalgroudon": "Groudon-Primal", 79 | "primaldon": "Groudon-Primal", 80 | "primalkyogre": "Kyogre-Primal", 81 | "primalogre": "Kyogre-Primal", 82 | 83 | // formes 84 | "bugceus": "Arceus-Bug", 85 | "darkceus": "Arceus-Dark", 86 | "dragonceus": "Arceus-Dragon", 87 | "eleceus": "Arceus-Electric", 88 | "fairyceus": "Arceus-Fairy", 89 | "fightceus": "Arceus-Fighting", 90 | "fireceus": "Arceus-Fire", 91 | "flyceus": "Arceus-Flying", 92 | "ghostceus": "Arceus-Ghost", 93 | "grassceus": "Arceus-Grass", 94 | "groundceus": "Arceus-Ground", 95 | "iceceus": "Arceus-Ice", 96 | "poisonceus": "Arceus-Poison", 97 | "psyceus": "Arceus-Psychic", 98 | "rockceus": "Arceus-Rock", 99 | "steelceus": "Arceus-Steel", 100 | "waterceus": "Arceus-Water", 101 | "basculinb": "Basculin-Blue-Striped", 102 | "basculinblue": "Basculin-Blue-Striped", 103 | "basculinbluestripe": "Basculin-Blue-Striped", 104 | "castformh": "Castform-Snowy", 105 | "castformice": "Castform-Snowy", 106 | "castformr": "Castform-Rainy", 107 | "castformwater": "Castform-Rainy", 108 | "castforms": "Castform-Sunny", 109 | "castformfire": "Castform-Sunny", 110 | "cherrims": "Cherrim-Sunshine", 111 | "cherrimsunny": "Cherrim-Sunshine", 112 | "darmanitanz": "Darmanitan-Zen", 113 | "darmanitanzenmode": "Darmanitan-Zen", 114 | "deoxysnormal": "Deoxys", 115 | "deon": "Deoxys", 116 | "deoxysa": "Deoxys-Attack", 117 | "deoa": "Deoxys-Attack", 118 | "deoxysd": "Deoxys-Defense", 119 | "deoxysdefence": "Deoxys-Defense", 120 | "deod": "Deoxys-Defense", 121 | "deoxyss": "Deoxys-Speed", 122 | "deos": "Deoxys-Speed", 123 | "giratinao": "Giratina-Origin", 124 | "gourgeisthuge": "Gourgeist-Super", 125 | "hoopau": "Hoopa-Unbound", 126 | "keldeor": "Keldeo-Resolute", 127 | "keldeoresolution": "Keldeo-Resolute", 128 | "kyuremb": "Kyurem-Black", 129 | "kyuremw": "Kyurem-White", 130 | "landorust": "Landorus-Therian", 131 | "meloettap": "Meloetta-Pirouette", 132 | "meloettas": "Meloetta-Pirouette", 133 | "pumpkaboohuge": "Pumpkaboo-Super", 134 | "rotomc": "Rotom-Mow", 135 | "rotomf": "Rotom-Frost", 136 | "rotomh": "Rotom-Heat", 137 | "rotoms": "Rotom-Fan", 138 | "rotomw": "Rotom-Wash", 139 | "shaymins": "Shaymin-Sky", 140 | "skymin": "Shaymin-Sky", 141 | "thundurust": "Thundurus-Therian", 142 | "tornadust": "Tornadus-Therian", 143 | "tornt": "Tornadus-Therian", 144 | "wormadamg": "Wormadam-Sandy", 145 | "wormadamground": "Wormadam-Sandy", 146 | "wormadams": "Wormadam-Trash", 147 | "wormadamsteel": "Wormadam-Trash", 148 | "floettee": "Floette-Eternal-Flower", 149 | 150 | // base formes 151 | "nidoranfemale": "Nidoran-F", 152 | "nidoranmale": "Nidoran-M", 153 | "giratinaa": "Giratina", 154 | "giratinaaltered": "Giratina", 155 | "cherrimo": "Cherrim", 156 | "cherrimovercast": "Cherrim", 157 | "meloettaa": "Meloetta", 158 | "meloettaaria": "Meloetta", 159 | "basculinr": "Basculin", 160 | "basculinred": "Basculin", 161 | "basculinredstripe": "Basculin", 162 | "basculinredstriped": "Basculin", 163 | "tornadusi": "Tornadus", 164 | "tornadusincarnation": "Tornadus", 165 | "thundurusi": "Thundurus", 166 | "thundurusincarnation": "Thundurus", 167 | "landorusi": "Landorus", 168 | "landorusincarnation": "Landorus", 169 | "pumpkabooaverage": "Pumpkaboo", 170 | "gourgeistaverage": "Gourgeist", 171 | 172 | // cosmetic formes 173 | "gastrodone": "Gastrodon", 174 | "gastrodoneast": "Gastrodon", 175 | "gastrodonw": "Gastrodon", 176 | "gastrodonwest": "Gastrodon", 177 | 178 | // items 179 | "assvest": "Assault Vest", 180 | "av": "Assault Vest", 181 | "band": "Choice Band", 182 | "cb": "Choice Band", 183 | "chesto": "Chesto Berry", 184 | "chople": "Chople Berry", 185 | "custap": "Custap Berry", 186 | "fightgem": "Fighting Gem", 187 | "flightgem": "Flying Gem", 188 | "goggles": "Safety Goggles", 189 | "lefties": "Leftovers", 190 | "leppa": "Leppa Berry", 191 | "lo": "Life Orb", 192 | "lum": "Lum Berry", 193 | "occa": "Occa Berry", 194 | "salac": "Salac Berry", 195 | "sash": "Focus Sash", 196 | "scarf": "Choice Scarf", 197 | "sitrus": "Sitrus Berry", 198 | "specs": "Choice Specs", 199 | "yache": "Yache Berry", 200 | 201 | // gen 1-2 berries 202 | "berry": "Oran Berry", 203 | "bitterberry": "Persim Berry", 204 | "burntberry": "Rawst Berry", 205 | "goldberry": "Sitrus Berry", 206 | "iceberry": "Aspear Berry", 207 | "mintberry": "Chesto Berry", 208 | "miracleberry": "Lum Berry", 209 | "mysteryberry": "Leppa Berry", 210 | "przcureberry": "Cheri Berry", 211 | "psncureberry": "Pecha Berry", 212 | 213 | // pokemon 214 | "aboma": "Abomasnow", 215 | "chomp": "Garchomp", 216 | "cofag": "Cofagrigus", 217 | "dnite": "Dragonite", 218 | "don": "Groudon", 219 | "dogars": "Koffing", 220 | "ekiller": "Arceus", 221 | "esca": "Escavalier", 222 | "ferro": "Ferrothorn", 223 | "forry": "Forretress", 224 | "gar": "Gengar", 225 | "garde": "Gardevoir", 226 | "hippo": "Hippowdon", 227 | "kyub": "Kyurem-Black", 228 | "kyuw": "Kyurem-White", 229 | "lando": "Landorus", 230 | "landoi": "Landorus", 231 | "landot": "Landorus-Therian", 232 | "luke": "Lucario", 233 | "mence": "Salamence", 234 | "obama": "Abomasnow", 235 | "ogre": "Kyogre", 236 | "p2": "Porygon2", 237 | "pory2": "Porygon2", 238 | "pz": "Porygon-Z", 239 | "poryz": "Porygon-Z", 240 | "rank": "Reuniclus", 241 | "smogon": "Koffing", 242 | "talon": "Talonflame", 243 | "terra": "Terrakion", 244 | "ttar": "Tyranitar", 245 | "zam": "Alakazam", 246 | "ohmagod":"Plasmanta", 247 | 248 | // moves 249 | "bpass": "Baton Pass", 250 | "bp": "Baton Pass", 251 | "cc": "Close Combat", 252 | "cm": "Calm Mind", 253 | "dd": "Dragon Dance", 254 | "eq": "Earthquake", 255 | "espeed": "ExtremeSpeed", 256 | "faintattack": "Feint Attack", 257 | "glowpunch": "Power-up Punch", 258 | "hp": "Hidden Power", 259 | "hpbug": "Hidden Power Bug", 260 | "hpdark": "Hidden Power Dark", 261 | "hpdragon": "Hidden Power Dragon", 262 | "hpelectric": "Hidden Power electric", 263 | "hpfighting": "Hidden Power Fighting", 264 | "hpfire": "Hidden Power Fire", 265 | "hpflying": "Hidden Power Flying", 266 | "hpghost": "Hidden Power Ghost", 267 | "hpgrass": "Hidden Power Grass", 268 | "hpground": "Hidden Power Ground", 269 | "hpice": "Hidden Power Ice", 270 | "hppoison": "Hidden Power Poison", 271 | "hppsychic": "Hidden Power Psychic", 272 | "hprock": "Hidden Power Rock", 273 | "hpsteel": "Hidden Power Steel", 274 | "hpwater": "Hidden Power Water", 275 | "hjk": "High Jump Kick", 276 | "hijumpkick": "High Jump Kick", 277 | "np": "Nasty Plot", 278 | "playaround": "Play Rough", 279 | "pup": "Power-up Punch", 280 | "qd": "Quiver Dance", 281 | "rocks": "Stealth Rock", 282 | "sd": "Swords Dance", 283 | "se": "Stone Edge", 284 | "spin": "Rapid Spin", 285 | "sr": "Stealth Rock", 286 | "sub": "Substitute", 287 | "tr": "Trick Room", 288 | "troom": "Trick Room", 289 | "tbolt": "Thunderbolt", 290 | "tspikes": "Toxic Spikes", 291 | "twave": "Thunder Wave", 292 | "web": "Sticky Web", 293 | "wow": "Will-O-Wisp", 294 | 295 | // Japanese names 296 | "birijion": "Virizion", 297 | "terakion": "Terrakion", 298 | "agirudaa": "Accelgor", 299 | "randorosu": "Landorus", 300 | "urugamosu": "Volcarona", 301 | "erufuun": "Whimsicott", 302 | "doryuuzu": "Excadrill", 303 | "burungeru": "Jellicent", 304 | "nattorei": "Ferrothorn", 305 | "shandera": "Chandelure", 306 | "roobushin": "Conkeldurr", 307 | "ononokusu": "Haxorus", 308 | "sazandora": "Hydreigon", 309 | "chirachiino": "Cinccino", 310 | "kyuremu": "Kyurem", 311 | "jarooda": "Serperior", 312 | "zoroaaku": "Zoroark", 313 | "shinboraa": "Sigilyph", 314 | "barujiina": "Mandibuzz", 315 | "rankurusu": "Reuniclus", 316 | "borutorosu": "Thundurus" 317 | // there's no need to type out the other Japanese names 318 | // I'll autogenerate them at some point 319 | }; 320 | -------------------------------------------------------------------------------- /data/rulesets.js: -------------------------------------------------------------------------------- 1 | // Note: These are the rules that formats use 2 | // The list of formats is stored in config/formats.js 3 | 4 | exports.BattleFormats = { 5 | 6 | // Rulesets 7 | /////////////////////////////////////////////////////////////////// 8 | 9 | standard: { 10 | effectType: 'Banlist', 11 | ruleset: ['Sleep Clause Mod', 'Species Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod'], 12 | banlist: ['Unreleased', 'Illegal'] 13 | }, 14 | standardnext: { 15 | effectType: 'Banlist', 16 | ruleset: ['Sleep Clause Mod', 'Species Clause', 'OHKO Clause', 'HP Percentage Mod'], 17 | banlist: ['Illegal', 'Soul Dew'] 18 | }, 19 | standardubers: { 20 | effectType: 'Banlist', 21 | ruleset: ['Sleep Clause Mod', 'Species Clause', 'Moody Clause', 'OHKO Clause', 'Endless Battle Clause', 'HP Percentage Mod'], 22 | banlist: ['Unreleased', 'Illegal'] 23 | }, 24 | standardgbu: { 25 | effectType: 'Banlist', 26 | ruleset: ['Species Clause', 'Item Clause'], 27 | banlist: ['Unreleased', 'Illegal', 'Soul Dew', 28 | 'Mewtwo', 29 | 'Mew', 30 | 'Lugia', 31 | 'Ho-Oh', 32 | 'Celebi', 33 | 'Kyogre', 34 | 'Groudon', 35 | 'Rayquaza', 36 | 'Jirachi', 37 | 'Deoxys', 'Deoxys-Attack', 'Deoxys-Defense', 'Deoxys-Speed', 38 | 'Dialga', 39 | 'Palkia', 40 | 'Giratina', 'Giratina-Origin', 41 | 'Phione', 42 | 'Manaphy', 43 | 'Darkrai', 44 | 'Shaymin', 'Shaymin-Sky', 45 | 'Arceus', 46 | 'Victini', 47 | 'Reshiram', 48 | 'Zekrom', 49 | 'Kyurem', 'Kyurem-Black', 'Kyurem-White', 50 | 'Keldeo', 51 | 'Meloetta', 52 | 'Genesect', 53 | 'Xerneas', 54 | 'Yveltal', 55 | 'Zygarde', 56 | 'Diancie' 57 | ] 58 | }, 59 | standarddoubles: { 60 | effectType: 'Banlist', 61 | ruleset: ['Species Clause', 'OHKO Clause', 'Moody Clause', 'Evasion Abilities Clause', 'Evasion Moves Clause', 'Endless Battle Clause', 'HP Percentage Mod'], 62 | banlist: ['Unreleased', 'Illegal'] 63 | }, 64 | pokemon: { 65 | effectType: 'Banlist', 66 | validateSet: function (set, format) { 67 | var item = this.getItem(set.item); 68 | var template = this.getTemplate(set.species); 69 | var problems = []; 70 | var totalEV = 0; 71 | var allowCAP = !!(format && format.banlistTable && format.banlistTable['allowcap']); 72 | 73 | if (set.species === set.name) delete set.name; 74 | if (template.gen > this.gen) { 75 | problems.push(set.species + ' does not exist in gen ' + this.gen + '.'); 76 | } 77 | var ability = {}; 78 | if (set.ability) { 79 | ability = this.getAbility(set.ability); 80 | if (ability.gen > this.gen) { 81 | problems.push(ability.name + ' does not exist in gen ' + this.gen + '.'); 82 | } 83 | } 84 | if (set.moves) { 85 | for (var i = 0; i < set.moves.length; i++) { 86 | var move = this.getMove(set.moves[i]); 87 | if (move.gen > this.gen) { 88 | problems.push(move.name + ' does not exist in gen ' + this.gen + '.'); 89 | } else if (!allowCAP && move.isNonstandard) { 90 | problems.push(move.name + ' is not a real move.'); 91 | } 92 | } 93 | } 94 | if (item.gen > this.gen) { 95 | problems.push(item.name + ' does not exist in gen ' + this.gen + '.'); 96 | } 97 | if (set.moves && set.moves.length > 4) { 98 | problems.push((set.name || set.species) + ' has more than four moves.'); 99 | } 100 | if (set.level && set.level > 100) { 101 | problems.push((set.name || set.species) + ' is higher than level 100.'); 102 | } 103 | 104 | if (!allowCAP || template.tier !== 'CAP') { 105 | if (template.isNonstandard) { 106 | problems.push(set.species + ' is not a real Pokemon.'); 107 | } 108 | if (ability.isNonstandard) { 109 | problems.push(ability.name + ' is not a real ability.'); 110 | } 111 | if (item.isNonstandard) { 112 | problems.push(item.name + ' is not a real item.'); 113 | } 114 | } 115 | for (var k in set.evs) { 116 | if (typeof set.evs[k] !== 'number' || set.evs[k] < 0) { 117 | set.evs[k] = 0; 118 | } 119 | totalEV += set.evs[k]; 120 | } 121 | // In gen 6, it is impossible to battle other players with pokemon that break the EV limit 122 | if (totalEV > 510 && this.gen >= 6) { 123 | problems.push((set.name || set.species) + " has more than 510 total EVs."); 124 | } 125 | 126 | // ----------- legality line ------------------------------------------ 127 | if (!format.banlistTable || !format.banlistTable['illegal']) return problems; 128 | // everything after this line only happens if we're doing legality enforcement 129 | 130 | // only in gen 1 and 2 it was legal to max out all EVs 131 | if (this.gen >= 3 && totalEV > 510) { 132 | problems.push((set.name || set.species) + " has more than 510 total EVs."); 133 | } 134 | 135 | // limit one of each move 136 | var moves = []; 137 | if (set.moves) { 138 | var hasMove = {}; 139 | for (var i = 0; i < set.moves.length; i++) { 140 | var move = this.getMove(set.moves[i]); 141 | var moveid = move.id; 142 | if (hasMove[moveid]) continue; 143 | hasMove[moveid] = true; 144 | moves.push(set.moves[i]); 145 | } 146 | } 147 | set.moves = moves; 148 | 149 | if (template.isMega) { 150 | // Mega evolutions evolve in-battle 151 | set.species = template.baseSpecies; 152 | var baseAbilities = Tools.getTemplate(set.species).abilities; 153 | var niceAbility = false; 154 | for (var i in baseAbilities) { 155 | if (baseAbilities[i] === set.ability) { 156 | niceAbility = true; 157 | break; 158 | } 159 | } 160 | if (!niceAbility) set.ability = baseAbilities['0']; 161 | } else if (template.isPrimal) { 162 | // Primal Reversion happens in-battle 163 | set.species = template.baseSpecies; 164 | set.ability = Tools.getTemplate(set.species).abilities['0']; 165 | } 166 | if (template.requiredItem && item.name !== template.requiredItem) { 167 | problems.push((set.name || set.species) + ' needs to hold ' + template.requiredItem + '.'); 168 | } 169 | if (template.requiredMove && set.moves.indexOf(toId(template.requiredMove)) < 0) { 170 | problems.push((set.name || set.species) + ' needs to have the move ' + template.requiredMove + '.'); 171 | } 172 | if (template.num === 351) { // Castform 173 | set.species = 'Castform'; 174 | } 175 | if (template.num === 421) { // Cherrim 176 | set.species = 'Cherrim'; 177 | } 178 | if (template.num === 493) { // Arceus 179 | if (set.ability === 'Multitype' && item.onPlate) { 180 | set.species = 'Arceus-' + item.onPlate; 181 | } else { 182 | set.species = 'Arceus'; 183 | } 184 | } 185 | if (template.num === 555) { // Darmanitan 186 | if (set.species === 'Darmanitan-Zen' && ability.id !== 'zenmode') { 187 | problems.push('Darmanitan-Zen transforms in-battle with Zen Mode.'); 188 | } 189 | set.species = 'Darmanitan'; 190 | } 191 | if (template.num === 487) { // Giratina 192 | if (item.id === 'griseousorb') { 193 | set.species = 'Giratina-Origin'; 194 | set.ability = 'Levitate'; 195 | } else { 196 | set.species = 'Giratina'; 197 | set.ability = 'Pressure'; 198 | } 199 | } 200 | if (template.num === 647) { // Keldeo 201 | if (set.moves.indexOf('secretsword') < 0) { 202 | set.species = 'Keldeo'; 203 | } 204 | } 205 | if (template.num === 648) { // Meloetta 206 | if (set.species === 'Meloetta-Pirouette' && set.moves.indexOf('relicsong') < 0) { 207 | problems.push('Meloetta-Pirouette transforms in-battle with Relic Song.'); 208 | } 209 | set.species = 'Meloetta'; 210 | } 211 | if (template.num === 649) { // Genesect 212 | switch (item.id) { 213 | case 'burndrive': 214 | set.species = 'Genesect-Burn'; 215 | break; 216 | case 'chilldrive': 217 | set.species = 'Genesect-Chill'; 218 | break; 219 | case 'dousedrive': 220 | set.species = 'Genesect-Douse'; 221 | break; 222 | case 'shockdrive': 223 | set.species = 'Genesect-Shock'; 224 | break; 225 | default: 226 | set.species = 'Genesect'; 227 | } 228 | } 229 | if (template.num === 681) { // Aegislash 230 | set.species = 'Aegislash'; 231 | } 232 | 233 | if (template.unobtainableShiny) { 234 | set.shiny = false; 235 | } 236 | return problems; 237 | } 238 | }, 239 | kalospokedex: { 240 | effectType: 'Rule', 241 | validateSet: function (set) { 242 | var validKalosDex = { 243 | "Abomasnow":1, "Abomasnow-Mega":1, "Abra":1, "Absol":1, "Absol-Mega":1, "Accelgor":1, "Aegislash":1, "Aegislash-Blade":1, "Aerodactyl":1, "Aerodactyl-Mega":1, "Aggron":1, "Aggron-Mega":1, "Alakazam":1, "Alakazam-Mega":1, "Alomomola":1, "Altaria":1, "Amaura":1, "Amoonguss":1, "Ampharos":1, "Ampharos-Mega":1, "Arbok":1, "Ariados":1, "Aromatisse":1, "Aron":1, "Articuno":1, "Audino":1, "Aurorus":1, "Avalugg":1, "Axew":1, "Azumarill":1, "Azurill":1, "Bagon":1, "Banette":1, "Banette-Mega":1, "Barbaracle":1, "Barboach":1, "Basculin":1, "Basculin-Blue-Striped":1, "Beartic":1, "Beedrill":1, "Bellossom":1, "Bellsprout":1, "Bergmite":1, "Bibarel":1, "Bidoof":1, "Binacle":1, "Bisharp":1, "Blastoise":1, "Blastoise-Mega":1, "Boldore":1, "Bonsly":1, "Braixen":1, "Budew":1, "Buizel":1, "Bulbasaur":1, "Bunnelby":1, "Burmy":1, "Butterfree":1, "Carbink":1, "Carnivine":1, "Carvanha":1, "Caterpie":1, "Chandelure":1, "Charizard":1, "Charizard-Mega-X":1, "Charizard-Mega-Y":1, "Charmander":1, "Charmeleon":1, "Chatot":1, "Chesnaught":1, "Chespin":1, "Chimecho":1, "Chinchou":1, "Chingling":1, "Clamperl":1, "Clauncher":1, "Clawitzer":1, "Cloyster":1, "Combee":1, "Conkeldurr":1, "Corphish":1, "Corsola":1, "Crawdaunt":1, "Croagunk":1, "Crobat":1, "Crustle":1, "Cryogonal":1, "Cubchoo":1, "Cubone":1, "Dedenne":1, "Deino":1, "Delcatty":1, "Delibird":1, "Delphox":1, "Diggersby":1, "Diglett":1, "Ditto":1, "Dodrio":1, "Doduo":1, "Doublade":1, "Dragalge":1, "Dragonair":1, "Dragonite":1, "Drapion":1, "Dratini":1, "Drifblim":1, "Drifloon":1, "Druddigon":1, "Ducklett":1, "Dugtrio":1, "Dunsparce":1, "Duosion":1, "Durant":1, "Dwebble":1, "Eevee":1, "Ekans":1, "Electrike":1, "Electrode":1, "Emolga":1, "Escavalier":1, "Espeon":1, "Espurr":1, "Exeggcute":1, "Exeggutor":1, "Exploud":1, "Farfetch'd":1, "Fearow":1, "Fennekin":1, "Ferroseed":1, "Ferrothorn":1, "Flaaffy":1, "Flabebe":1, "Flareon":1, "Fletchinder":1, "Fletchling":1, "Floatzel":1, "Floette":1, "Florges":1, "Flygon":1, "Foongus":1, "Fraxure":1, "Froakie":1, "Frogadier":1, "Furfrou":1, "Furret":1, "Gabite":1, "Gallade":1, "Garbodor":1, "Garchomp":1, "Garchomp-Mega":1, "Gardevoir":1, "Gardevoir-Mega":1, "Gastly":1, "Gengar":1, "Gengar-Mega":1, "Geodude":1, "Gible":1, "Gigalith":1, "Glaceon":1, "Gligar":1, "Gliscor":1, "Gloom":1, "Gogoat":1, "Golbat":1, "Goldeen":1, "Golduck":1, "Golem":1, "Golett":1, "Golurk":1, "Goodra":1, "Goomy":1, "Gorebyss":1, "Gothita":1, "Gothitelle":1, "Gothorita":1, "Gourgeist-Small":1, "Gourgeist":1, "Gourgeist-Large":1, "Gourgeist-Super":1, "Granbull":1, "Graveler":1, "Greninja":1, "Grumpig":1, "Gulpin":1, "Gurdurr":1, "Gyarados":1, "Gyarados-Mega":1, "Hariyama":1, "Haunter":1, "Hawlucha":1, "Haxorus":1, "Heatmor":1, "Heliolisk":1, "Helioptile":1, "Heracross":1, "Heracross-Mega":1, "Hippopotas":1, "Hippowdon":1, "Honchkrow":1, "Honedge":1, "Hoothoot":1, "Hoppip":1, "Horsea":1, "Houndoom":1, "Houndoom-Mega":1, "Houndour":1, "Huntail":1, "Hydreigon":1, "Igglybuff":1, "Illumise":1, "Inkay":1, "Ivysaur":1, "Jigglypuff":1, "Jolteon":1, "Jumpluff":1, "Jynx":1, "Kadabra":1, "Kakuna":1, "Kangaskhan":1, "Kangaskhan-Mega":1, "Karrablast":1, "Kecleon":1, "Kingdra":1, "Kirlia":1, "Klefki":1, "Krokorok":1, "Krookodile":1, "Lairon":1, "Lampent":1, "Lanturn":1, "Lapras":1, "Larvitar":1, "Leafeon":1, "Ledian":1, "Ledyba":1, "Lickilicky":1, "Lickitung":1, "Liepard":1, "Linoone":1, "Litleo":1, "Litwick":1, "Lombre":1, "Lotad":1, "Loudred":1, "Lucario":1, "Lucario-Mega":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, "Malamar":1, "Mamoswine":1, "Manectric":1, "Manectric-Mega":1, "Mantine":1, "Mantyke":1, "Mareep":1, "Marill":1, "Marowak":1, "Masquerain":1, "Mawile":1, "Mawile-Mega":1, "Medicham":1, "Medicham-Mega":1, "Meditite":1, "Meowstic":1, "Meowstic-F":1, "Metapod":1, "Mewtwo":1, "Mewtwo-Mega-X":1, "Mewtwo-Mega-Y":1, "Mienfoo":1, "Mienshao":1, "Mightyena":1, "Miltank":1, "Mime Jr.":1, "Minun":1, "Moltres":1, "Mothim":1, "Mr. Mime":1, "Munchlax":1, "Murkrow":1, "Nidoking":1, "Nidoqueen":1, "Nidoran-M":1, "Nidoran-F":1, "Nidorina":1, "Nidorino":1, "Nincada":1, "Ninjask":1, "Noctowl":1, "Noibat":1, "Noivern":1, "Nosepass":1, "Octillery":1, "Oddish":1, "Onix":1, "Pachirisu":1, "Pancham":1, "Pangoro":1, "Panpour":1, "Pansage":1, "Pansear":1, "Patrat":1, "Pawniard":1, "Pelipper":1, "Phantump":1, "Pichu":1, "Pidgeot":1, "Pidgeotto":1, "Pidgey":1, "Pikachu":1, "Piloswine":1, "Pinsir":1, "Pinsir-Mega":1, "Plusle":1, "Politoed":1, "Poliwag":1, "Poliwhirl":1, "Poliwrath":1, "Poochyena":1, "Probopass":1, "Psyduck":1, "Pumpkaboo-Small":1, "Pumpkaboo":1, "Pumpkaboo-Large":1, "Pumpkaboo-Super":1, "Pupitar":1, "Purrloin":1, "Pyroar":1, "Quagsire":1, "Quilladin":1, "Qwilfish":1, "Raichu":1, "Ralts":1, "Relicanth":1, "Remoraid":1, "Reuniclus":1, "Rhydon":1, "Rhyhorn":1, "Rhyperior":1, "Riolu":1, "Roggenrola":1, "Roselia":1, "Roserade":1, "Rotom":1, "Rotom-Heat":1, "Rotom-Wash":1, "Rotom-Frost":1, "Rotom-Fan":1, "Rotom-Mow":1, "Sableye":1, "Salamence":1, "Sandile":1, "Sandshrew":1, "Sandslash":1, "Sawk":1, "Scatterbug":1, "Scizor":1, "Scizor-Mega":1, "Scolipede":1, "Scrafty":1, "Scraggy":1, "Scyther":1, "Seadra":1, "Seaking":1, "Sentret":1, "Seviper":1, "Sharpedo":1, "Shedinja":1, "Shelgon":1, "Shellder":1, "Shelmet":1, "Shuckle":1, "Shuppet":1, "Sigilyph":1, "Simipour":1, "Simisage":1, "Simisear":1, "Skarmory":1, "Skiddo":1, "Skiploom":1, "Skitty":1, "Skorupi":1, "Skrelp":1, "Skuntank":1, "Sliggoo":1, "Slowbro":1, "Slowking":1, "Slowpoke":1, "Slugma":1, "Slurpuff":1, "Smeargle":1, "Smoochum":1, "Sneasel":1, "Snorlax":1, "Snover":1, "Snubbull":1, "Solosis":1, "Solrock":1, "Spearow":1, "Spewpa":1, "Spinarak":1, "Spinda":1, "Spoink":1, "Spritzee":1, "Squirtle":1, "Staraptor":1, "Staravia":1, "Starly":1, "Starmie":1, "Staryu":1, "Steelix":1, "Stunfisk":1, "Stunky":1, "Sudowoodo":1, "Surskit":1, "Swablu":1, "Swalot":1, "Swanna":1, "Swellow":1, "Swinub":1, "Swirlix":1, "Swoobat":1, "Sylveon":1, "Taillow":1, "Talonflame":1, "Tauros":1, "Teddiursa":1, "Tentacool":1, "Tentacruel":1, "Throh":1, "Timburr":1, "Torkoal":1, "Toxicroak":1, "Trapinch":1, "Trevenant":1, "Trubbish":1, "Tyranitar":1, "Tyranitar-Mega":1, "Tyrantrum":1, "Tyrunt":1, "Umbreon":1, "Ursaring":1, "Vanillish":1, "Vanillite":1, "Vanilluxe":1, "Vaporeon":1, "Venipede":1, "Venusaur":1, "Venusaur-Mega":1, "Vespiquen":1, "Vibrava":1, "Victreebel":1, "Vileplume":1, "Vivillon":1, "Volbeat":1, "Voltorb":1, "Wailmer":1, "Wailord":1, "Wartortle":1, "Watchog":1, "Weavile":1, "Weedle":1, "Weepinbell":1, "Whirlipede":1, "Whiscash":1, "Whismur":1, "Wigglytuff":1, "Wingull":1, "Wobbuffet":1, "Woobat":1, "Wooper":1, "Wormadam":1, "Wormadam-Sandy":1, "Wormadam-Trash":1, "Wynaut":1, "Xerneas":1, "Yanma":1, "Yanmega":1, "Yveltal":1, "Zangoose":1, "Zapdos":1, "Zigzagoon":1, "Zoroark":1, "Zorua":1, "Zubat":1, "Zweilous":1, "Zygarde":1 244 | }; 245 | if (!(set.species in validKalosDex)) { 246 | return [set.species + " is not in the Kalos Pokedex."]; 247 | } 248 | } 249 | }, 250 | potd: { 251 | effectType: 'Rule', 252 | onStart: function () { 253 | if (Config.potd) { 254 | this.add('rule', "Pokemon of the Day: " + this.getTemplate(Config.potd).name); 255 | } 256 | } 257 | }, 258 | teampreviewvgc: { 259 | onStartPriority: -10, 260 | onStart: function () { 261 | this.add('clearpoke'); 262 | for (var i = 0; i < this.sides[0].pokemon.length; i++) { 263 | this.add('poke', this.sides[0].pokemon[i].side.id, this.sides[0].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 264 | } 265 | for (var i = 0; i < this.sides[1].pokemon.length; i++) { 266 | this.add('poke', this.sides[1].pokemon[i].side.id, this.sides[1].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 267 | } 268 | }, 269 | onTeamPreview: function () { 270 | this.makeRequest('teampreview', 4); 271 | } 272 | }, 273 | teampreview1v1: { 274 | onStartPriority: -10, 275 | onStart: function () { 276 | this.add('clearpoke'); 277 | for (var i = 0; i < this.sides[0].pokemon.length; i++) { 278 | this.add('poke', this.sides[0].pokemon[i].side.id, this.sides[0].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 279 | } 280 | for (var i = 0; i < this.sides[1].pokemon.length; i++) { 281 | this.add('poke', this.sides[1].pokemon[i].side.id, this.sides[1].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 282 | } 283 | }, 284 | onTeamPreview: function () { 285 | this.makeRequest('teampreview', 1); 286 | } 287 | }, 288 | teampreview: { 289 | onStartPriority: -10, 290 | onStart: function () { 291 | this.add('clearpoke'); 292 | for (var i = 0; i < this.sides[0].pokemon.length; i++) { 293 | this.add('poke', this.sides[0].pokemon[i].side.id, this.sides[0].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 294 | } 295 | for (var i = 0; i < this.sides[1].pokemon.length; i++) { 296 | this.add('poke', this.sides[1].pokemon[i].side.id, this.sides[1].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 297 | } 298 | }, 299 | onTeamPreview: function () { 300 | this.makeRequest('teampreview'); 301 | } 302 | }, 303 | teampreviewgbu: { 304 | onStartPriority: -10, 305 | onStart: function () { 306 | this.add('clearpoke'); 307 | for (var i = 0; i < this.sides[0].pokemon.length; i++) { 308 | this.add('poke', this.sides[0].pokemon[i].side.id, this.sides[0].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 309 | } 310 | for (var i = 0; i < this.sides[1].pokemon.length; i++) { 311 | this.add('poke', this.sides[1].pokemon[i].side.id, this.sides[1].pokemon[i].details.replace(/(Arceus|Gourgeist|Genesect|Pumpkaboo)(-[a-zA-Z?]+)?/g, '$1-*')); 312 | } 313 | }, 314 | onTeamPreview: function () { 315 | this.makeRequest('teampreview', 3); 316 | } 317 | }, 318 | littlecup: { 319 | effectType: 'Rule', 320 | validateSet: function (set) { 321 | var template = this.getTemplate(set.species || set.name); 322 | if (template.prevo) { 323 | return [set.species + " isn't the first in its evolution family."]; 324 | } 325 | if (!template.nfe) { 326 | return [set.species + " doesn't have an evolution family."]; 327 | } 328 | } 329 | }, 330 | speciesclause: { 331 | effectType: 'Rule', 332 | onStart: function () { 333 | this.add('rule', 'Species Clause: Limit one of each Pokémon'); 334 | }, 335 | validateTeam: function (team, format) { 336 | var speciesTable = {}; 337 | for (var i = 0; i < team.length; i++) { 338 | var template = this.getTemplate(team[i].species); 339 | if (speciesTable[template.num]) { 340 | return ["You are limited to one of each Pokémon by Species Clause.", "(You have more than one " + template.name + ")"]; 341 | } 342 | speciesTable[template.num] = true; 343 | } 344 | } 345 | }, 346 | itemclause: { 347 | effectType: 'Rule', 348 | onStart: function () { 349 | this.add('rule', 'Item Clause: Limit one of each item'); 350 | }, 351 | validateTeam: function (team, format) { 352 | var itemTable = {}; 353 | for (var i = 0; i < team.length; i++) { 354 | var item = toId(team[i].item); 355 | if (!item) continue; 356 | if (itemTable[item]) { 357 | return ["You are limited to one of each item by Item Clause.", "(You have more than one " + this.getItem(item).name + ")"]; 358 | } 359 | itemTable[item] = true; 360 | } 361 | } 362 | }, 363 | abilityclause: { 364 | effectType: 'Rule', 365 | onStart: function () { 366 | this.add('rule', 'Ability Clause: Limit two of each ability'); 367 | }, 368 | validateTeam: function (team, format) { 369 | var abilityTable = {}; 370 | for (var i = 0; i < team.length; i++) { 371 | var ability = toId(team[i].ability); 372 | if (!ability) continue; 373 | if (ability in abilityTable) { 374 | if (abilityTable[ability] >= 2) { 375 | return ["You are limited to two of each ability by the Ability Clause.", "(You have more than two " + this.getAbility(ability).name + ")"]; 376 | } 377 | abilityTable[ability]++; 378 | } else { 379 | abilityTable[ability] = 1; 380 | } 381 | } 382 | } 383 | }, 384 | ohkoclause: { 385 | effectType: 'Rule', 386 | onStart: function () { 387 | this.add('rule', 'OHKO Clause: OHKO moves are banned'); 388 | }, 389 | validateSet: function (set) { 390 | var problems = []; 391 | if (set.moves) { 392 | for (var i in set.moves) { 393 | var move = this.getMove(set.moves[i]); 394 | if (move.ohko) problems.push(move.name + ' is banned by OHKO Clause.'); 395 | } 396 | } 397 | return problems; 398 | } 399 | }, 400 | evasionabilitiesclause: { 401 | effectType: 'Banlist', 402 | name: 'Evasion Abilities Clause', 403 | banlist: ['Sand Veil', 'Snow Cloak'], 404 | onStart: function () { 405 | this.add('rule', 'Evasion Abilities Clause: Evasion abilities are banned'); 406 | } 407 | }, 408 | evasionmovesclause: { 409 | effectType: 'Banlist', 410 | name: 'Evasion Moves Clause', 411 | banlist: ['Minimize', 'Double Team'], 412 | onStart: function () { 413 | this.add('rule', 'Evasion Moves Clause: Evasion moves are banned'); 414 | } 415 | }, 416 | endlessbattleclause: { 417 | effectType: 'Banlist', 418 | name: 'Endless Battle Clause', 419 | banlist: ['Leppa Berry + Recycle', 'Harvest + Leppa Berry', 'Shadow Tag + Leppa Berry + Trick'], 420 | onStart: function () { 421 | this.add('rule', 'Endless Battle Clause: Forcing endless battles is banned'); 422 | } 423 | }, 424 | moodyclause: { 425 | effectType: 'Banlist', 426 | name: 'Moody Clause', 427 | banlist: ['Moody'], 428 | onStart: function () { 429 | this.add('rule', 'Moody Clause: Moody is banned'); 430 | } 431 | }, 432 | swaggerclause: { 433 | effectType: 'Banlist', 434 | name: 'Swagger Clause', 435 | banlist: ['Swagger'], 436 | onStart: function () { 437 | this.add('rule', 'Swagger Clause: Swagger is banned'); 438 | } 439 | }, 440 | batonpassclause: { 441 | effectType: 'Banlist', 442 | name: 'Baton Pass Clause', 443 | onStart: function () { 444 | this.add('rule', 'Baton Pass Clause: Limit one Pokémon knowing Baton Pass'); 445 | }, 446 | validateTeam: function (team, format) { 447 | var problems = []; 448 | var BPcount = 0; 449 | for (var i = 0; i < team.length; i++) { 450 | if (team[i].moves.indexOf('Baton Pass') > -1) BPcount++; 451 | if (BPcount > 1) { 452 | problems.push("You are limited to one Pokémon with the move Baton Pass by the Baton Pass Clause."); 453 | break; 454 | } 455 | } 456 | return problems; 457 | } 458 | }, 459 | hppercentagemod: { 460 | effectType: 'Rule', 461 | name: 'HP Percentage Mod', 462 | onStart: function () { 463 | this.add('rule', 'HP Percentage Mod: HP is shown in percentages'); 464 | this.reportPercentages = true; 465 | } 466 | }, 467 | sleepclausemod: { 468 | effectType: 'Rule', 469 | onStart: function () { 470 | this.add('rule', 'Sleep Clause Mod: Limit one foe put to sleep'); 471 | }, 472 | onSetStatus: function (status, target, source) { 473 | if (source && source.side === target.side) { 474 | return; 475 | } 476 | if (status.id === 'slp') { 477 | for (var i = 0; i < target.side.pokemon.length; i++) { 478 | var pokemon = target.side.pokemon[i]; 479 | if (pokemon.status === 'slp') { 480 | if (!pokemon.statusData.source || 481 | pokemon.statusData.source.side !== pokemon.side) { 482 | this.add('-message', 'Sleep Clause Mod activated.'); 483 | return false; 484 | } 485 | } 486 | } 487 | } 488 | } 489 | }, 490 | freezeclause: { 491 | effectType: 'Rule', 492 | onStart: function () { 493 | this.add('rule', 'Freeze Clause: Limit one foe frozen'); 494 | }, 495 | onSetStatus: function (status, target, source) { 496 | if (source && source.side === target.side) { 497 | return; 498 | } 499 | if (status.id === 'frz') { 500 | for (var i = 0; i < target.side.pokemon.length; i++) { 501 | var pokemon = target.side.pokemon[i]; 502 | if (pokemon.status === 'frz') { 503 | this.add('-message', 'Freeze Clause activated.'); 504 | return false; 505 | } 506 | } 507 | } 508 | } 509 | }, 510 | sametypeclause: { 511 | effectType: 'Rule', 512 | onStart: function () { 513 | this.add('rule', 'Same Type Clause: Pokémon in a team must share a type'); 514 | }, 515 | validateTeam: function (team, format, teamHas) { 516 | if (!team[0]) return; 517 | var template = this.getTemplate(team[0].species); 518 | var typeTable = template.types; 519 | if (!typeTable) return ["Your team must share a type."]; 520 | for (var i = 1; i < team.length; i++) { 521 | template = this.getTemplate(team[i].species); 522 | if (!template.types) return ["Your team must share a type."]; 523 | 524 | typeTable = typeTable.intersect(template.types); 525 | if (!typeTable.length) return ["Your team must share a type."]; 526 | } 527 | if (format.id === 'monotype') { 528 | // Very complex bans 529 | if (typeTable.length > 1) return; 530 | switch (typeTable[0]) { 531 | case 'Dragon': 532 | if (teamHas['kyuremwhite']) return ["Kyurem-White is banned from Dragon monotype teams."]; 533 | break; 534 | case 'Flying': 535 | if (teamHas['shayminsky']) return ["Shaymin-Sky is banned from Flying monotype teams."]; 536 | break; 537 | case 'Steel': 538 | if (teamHas['aegislash']) return ["Aegislash is banned from Steel monotype teams."]; 539 | if (teamHas['genesect'] || teamHas['genesectdouse'] || teamHas['genesectshock'] || teamHas['genesectburn'] || teamHas['genesectchill']) return ["Genesect is banned from Steel monotype teams."]; 540 | break; 541 | case 'Water': 542 | if (teamHas['damprock']) return ["Damp Rock is banned from Water monotype teams."]; 543 | } 544 | } 545 | } 546 | }, 547 | megarayquazabanmod: { 548 | effectType: 'Rule', 549 | onStart: function () { 550 | this.add('rule', 'Mega Rayquaza Ban Mod: You cannot mega evolve Rayquaza'); 551 | for (var i = 0; i < this.sides[0].pokemon.length; i++) { 552 | if (this.sides[0].pokemon[i].speciesid === 'rayquaza') this.sides[0].pokemon[i].canMegaEvo = false; 553 | } 554 | for (var i = 0; i < this.sides[1].pokemon.length; i++) { 555 | if (this.sides[1].pokemon[i].speciesid === 'rayquaza') this.sides[1].pokemon[i].canMegaEvo = false; 556 | } 557 | } 558 | } 559 | }; 560 | -------------------------------------------------------------------------------- /data/statuses.js: -------------------------------------------------------------------------------- 1 | exports.BattleStatuses = { 2 | brn: { 3 | effectType: 'Status', 4 | onStart: function (target, source, sourceEffect) { 5 | if (sourceEffect && sourceEffect.id === 'flameorb') { 6 | this.add('-status', target, 'brn', '[from] item: Flame Orb'); 7 | return; 8 | } 9 | this.add('-status', target, 'brn'); 10 | }, 11 | onBasePower: function (basePower, attacker, defender, move) { 12 | if (move && move.category === 'Physical' && attacker && attacker.ability !== 'guts' && move.id !== 'facade') { 13 | return this.chainModify(0.5); // This should really take place directly in the damage function but it's here for now 14 | } 15 | }, 16 | onResidualOrder: 9, 17 | onResidual: function (pokemon) { 18 | this.damage(pokemon.maxhp / 8); 19 | } 20 | }, 21 | par: { 22 | effectType: 'Status', 23 | onStart: function (target) { 24 | this.add('-status', target, 'par'); 25 | }, 26 | onModifySpe: function (speMod, pokemon) { 27 | if (pokemon.ability !== 'quickfeet') { 28 | return this.chain(speMod, 0.25); 29 | } 30 | }, 31 | onBeforeMovePriority: 2, 32 | onBeforeMove: function (pokemon) { 33 | if (this.random(4) === 0) { 34 | this.add('cant', pokemon, 'par'); 35 | return false; 36 | } 37 | } 38 | }, 39 | slp: { 40 | effectType: 'Status', 41 | onStart: function (target) { 42 | this.add('-status', target, 'slp'); 43 | // 1-3 turns 44 | this.effectData.startTime = this.random(2, 5); 45 | this.effectData.time = this.effectData.startTime; 46 | }, 47 | onBeforeMovePriority: 2, 48 | onBeforeMove: function (pokemon, target, move) { 49 | if (pokemon.getAbility().isHalfSleep) { 50 | pokemon.statusData.time--; 51 | } 52 | pokemon.statusData.time--; 53 | if (pokemon.statusData.time <= 0) { 54 | pokemon.cureStatus(); 55 | return; 56 | } 57 | this.add('cant', pokemon, 'slp'); 58 | if (move.sleepUsable) { 59 | return; 60 | } 61 | return false; 62 | } 63 | }, 64 | frz: { 65 | effectType: 'Status', 66 | onStart: function (target) { 67 | this.add('-status', target, 'frz'); 68 | if (target.species === 'Shaymin-Sky' && target.baseTemplate.species === target.species) { 69 | var template = this.getTemplate('Shaymin'); 70 | target.formeChange(template); 71 | target.baseTemplate = template; 72 | target.setAbility(template.abilities['0']); 73 | target.baseAbility = target.ability; 74 | target.details = template.species + (target.level === 100 ? '' : ', L' + target.level) + (target.gender === '' ? '' : ', ' + target.gender) + (target.set.shiny ? ', shiny' : ''); 75 | this.add('detailschange', target, target.details); 76 | this.add('message', target.species + " has reverted to Land Forme! (placeholder)"); 77 | } 78 | }, 79 | onBeforeMovePriority: 2, 80 | onBeforeMove: function (pokemon, target, move) { 81 | if (move.thawsUser || this.random(5) === 0) { 82 | pokemon.cureStatus(); 83 | return; 84 | } 85 | this.add('cant', pokemon, 'frz'); 86 | return false; 87 | }, 88 | onHit: function (target, source, move) { 89 | if (move.thawsTarget || move.type === 'Fire' && move.category !== 'Status') { 90 | target.cureStatus(); 91 | } 92 | } 93 | }, 94 | psn: { 95 | effectType: 'Status', 96 | onStart: function (target) { 97 | this.add('-status', target, 'psn'); 98 | }, 99 | onResidualOrder: 9, 100 | onResidual: function (pokemon) { 101 | this.damage(pokemon.maxhp / 8); 102 | } 103 | }, 104 | tox: { 105 | effectType: 'Status', 106 | onStart: function (target, source, sourceEffect) { 107 | this.effectData.stage = 0; 108 | if (sourceEffect && sourceEffect.id === 'toxicorb') { 109 | this.add('-status', target, 'tox', '[from] item: Toxic Orb'); 110 | return; 111 | } 112 | this.add('-status', target, 'tox'); 113 | }, 114 | onSwitchIn: function () { 115 | this.effectData.stage = 0; 116 | }, 117 | onResidualOrder: 9, 118 | onResidual: function (pokemon) { 119 | if (this.effectData.stage < 15) { 120 | this.effectData.stage++; 121 | } 122 | this.damage(this.clampIntRange(pokemon.maxhp / 16, 1) * this.effectData.stage); 123 | } 124 | }, 125 | confusion: { 126 | // this is a volatile status 127 | onStart: function (target, source, sourceEffect) { 128 | var result = this.runEvent('TryConfusion', target, source, sourceEffect); 129 | if (!result) return result; 130 | this.add('-start', target, 'confusion'); 131 | this.effectData.time = this.random(2, 6); 132 | }, 133 | onEnd: function (target) { 134 | this.add('-end', target, 'confusion'); 135 | }, 136 | onBeforeMove: function (pokemon) { 137 | pokemon.volatiles.confusion.time--; 138 | if (!pokemon.volatiles.confusion.time) { 139 | pokemon.removeVolatile('confusion'); 140 | return; 141 | } 142 | this.add('-activate', pokemon, 'confusion'); 143 | if (this.random(2) === 0) { 144 | return; 145 | } 146 | this.directDamage(this.getDamage(pokemon, pokemon, 40)); 147 | return false; 148 | } 149 | }, 150 | flinch: { 151 | duration: 1, 152 | onBeforeMovePriority: 1, 153 | onBeforeMove: function (pokemon) { 154 | if (!this.runEvent('Flinch', pokemon)) { 155 | return; 156 | } 157 | this.add('cant', pokemon, 'flinch'); 158 | return false; 159 | } 160 | }, 161 | trapped: { 162 | noCopy: true, 163 | onModifyPokemon: function (pokemon) { 164 | pokemon.tryTrap(); 165 | }, 166 | onStart: function (target) { 167 | this.add('-activate', target, 'trapped'); 168 | } 169 | }, 170 | trapper: { 171 | noCopy: true 172 | }, 173 | partiallytrapped: { 174 | duration: 5, 175 | durationCallback: function (target, source) { 176 | if (source.hasItem('gripclaw')) return 8; 177 | return this.random(5, 7); 178 | }, 179 | onStart: function (pokemon, source) { 180 | this.add('-activate', pokemon, 'move: ' + this.effectData.sourceEffect, '[of] ' + source); 181 | }, 182 | onResidualOrder: 11, 183 | onResidual: function (pokemon) { 184 | if (this.effectData.source && (!this.effectData.source.isActive || this.effectData.source.hp <= 0)) { 185 | pokemon.removeVolatile('partiallytrapped'); 186 | return; 187 | } 188 | if (this.effectData.source.hasItem('bindingband')) { 189 | this.damage(pokemon.maxhp / 6); 190 | } else { 191 | this.damage(pokemon.maxhp / 8); 192 | } 193 | }, 194 | onEnd: function (pokemon) { 195 | this.add('-end', pokemon, this.effectData.sourceEffect, '[partiallytrapped]'); 196 | }, 197 | onModifyPokemon: function (pokemon) { 198 | pokemon.tryTrap(); 199 | } 200 | }, 201 | lockedmove: { 202 | // Outrage, Thrash, Petal Dance... 203 | duration: 2, 204 | onResidual: function (target) { 205 | if (target.status === 'slp') { 206 | // don't lock, and bypass confusion for calming 207 | delete target.volatiles['lockedmove']; 208 | } 209 | this.effectData.trueDuration--; 210 | }, 211 | onStart: function (target, source, effect) { 212 | this.effectData.trueDuration = this.random(2, 4); 213 | this.effectData.move = effect.id; 214 | }, 215 | onRestart: function () { 216 | if (this.effectData.trueDuration >= 2) { 217 | this.effectData.duration = 2; 218 | } 219 | }, 220 | onEnd: function (target) { 221 | if (this.effectData.trueDuration > 1) return; 222 | this.add('-end', target, 'rampage'); 223 | target.addVolatile('confusion'); 224 | }, 225 | onLockMove: function (pokemon) { 226 | return this.effectData.move; 227 | } 228 | }, 229 | twoturnmove: { 230 | // Skull Bash, SolarBeam, Sky Drop... 231 | duration: 2, 232 | onStart: function (target, source, effect) { 233 | this.effectData.move = effect.id; 234 | // source and target are reversed since the event target is the 235 | // pokemon using the two-turn move 236 | this.effectData.targetLoc = this.getTargetLoc(source, target); 237 | target.addVolatile(effect.id, source); 238 | }, 239 | onEnd: function (target) { 240 | target.removeVolatile(this.effectData.move); 241 | }, 242 | onLockMove: function () { 243 | return this.effectData.move; 244 | }, 245 | onLockMoveTarget: function () { 246 | return this.effectData.targetLoc; 247 | } 248 | }, 249 | choicelock: { 250 | onStart: function (pokemon) { 251 | if (!this.activeMove.id || this.activeMove.sourceEffect && this.activeMove.sourceEffect !== this.activeMove.id) return false; 252 | this.effectData.move = this.activeMove.id; 253 | }, 254 | onModifyPokemon: function (pokemon) { 255 | if (!pokemon.getItem().isChoice || !pokemon.hasMove(this.effectData.move)) { 256 | pokemon.removeVolatile('choicelock'); 257 | return; 258 | } 259 | if (pokemon.ignore['Item']) { 260 | return; 261 | } 262 | var moves = pokemon.moveset; 263 | for (var i = 0; i < moves.length; i++) { 264 | if (moves[i].id !== this.effectData.move) { 265 | pokemon.disableMove(moves[i].id, false, this.effectData.sourceEffect); 266 | } 267 | } 268 | } 269 | }, 270 | mustrecharge: { 271 | duration: 2, 272 | onBeforeMove: function (pokemon) { 273 | this.add('cant', pokemon, 'recharge'); 274 | pokemon.removeVolatile('mustrecharge'); 275 | return false; 276 | }, 277 | onLockMove: 'recharge' 278 | }, 279 | futuremove: { 280 | // this is a side condition 281 | onStart: function (side) { 282 | this.effectData.positions = []; 283 | for (var i = 0; i < side.active.length; i++) { 284 | this.effectData.positions[i] = null; 285 | } 286 | }, 287 | onResidualOrder: 3, 288 | onResidual: function (side) { 289 | var finished = true; 290 | for (var i = 0; i < side.active.length; i++) { 291 | var posData = this.effectData.positions[i]; 292 | if (!posData) continue; 293 | 294 | posData.duration--; 295 | 296 | if (posData.duration > 0) { 297 | finished = false; 298 | continue; 299 | } 300 | 301 | // time's up; time to hit! :D 302 | var target = side.foe.active[posData.targetPosition]; 303 | var move = this.getMove(posData.move); 304 | if (target.fainted) { 305 | this.add('-hint', '' + move.name + ' did not hit because the target is fainted.'); 306 | this.effectData.positions[i] = null; 307 | continue; 308 | } 309 | 310 | this.add('-end', target, 'move: ' + move.name); 311 | target.removeVolatile('Protect'); 312 | target.removeVolatile('Endure'); 313 | 314 | if (typeof posData.moveData.affectedByImmunities === 'undefined') { 315 | posData.moveData.affectedByImmunities = true; 316 | } 317 | 318 | if (target.hasAbility('wonderguard') && this.gen > 5) { 319 | this.debug('Wonder Guard immunity: ' + move.id); 320 | if (target.runEffectiveness(move) <= 0) { 321 | this.add('-activate', target, 'ability: Wonder Guard'); 322 | this.effectData.positions[i] = null; 323 | return null; 324 | } 325 | } 326 | 327 | this.moveHit(target, posData.source, move, posData.moveData); 328 | 329 | this.effectData.positions[i] = null; 330 | } 331 | if (finished) { 332 | side.removeSideCondition('futuremove'); 333 | } 334 | } 335 | }, 336 | stall: { 337 | // Protect, Detect, Endure counter 338 | duration: 2, 339 | counterMax: 256, 340 | onStart: function () { 341 | this.effectData.counter = 3; 342 | }, 343 | onStallMove: function () { 344 | // this.effectData.counter should never be undefined here. 345 | // However, just in case, use 1 if it is undefined. 346 | var counter = this.effectData.counter || 1; 347 | this.debug("Success chance: " + Math.round(100 / counter) + "%"); 348 | return (this.random(counter) === 0); 349 | }, 350 | onRestart: function () { 351 | if (this.effectData.counter < this.effect.counterMax) { 352 | this.effectData.counter *= 3; 353 | } 354 | this.effectData.duration = 2; 355 | } 356 | }, 357 | gem: { 358 | duration: 1, 359 | affectsFainted: true, 360 | onBasePower: function (basePower, user, target, move) { 361 | this.debug('Gem Boost'); 362 | return this.chainModify([0x14CD, 0x1000]); 363 | } 364 | }, 365 | aura: { 366 | duration: 1, 367 | onBasePowerPriority: 8, 368 | onBasePower: function (basePower, user, target, move) { 369 | var modifier = 4 / 3; 370 | this.debug('Aura Boost'); 371 | if (user.volatiles['aurabreak']) { 372 | modifier = 0.75; 373 | this.debug('Aura Boost reverted by Aura Break'); 374 | } 375 | return this.chainModify(modifier); 376 | } 377 | }, 378 | 379 | // weather 380 | 381 | // weather is implemented here since it's so important to the game 382 | 383 | raindance: { 384 | effectType: 'Weather', 385 | duration: 5, 386 | durationCallback: function (source, effect) { 387 | if (source && source.hasItem('damprock')) { 388 | return 8; 389 | } 390 | return 5; 391 | }, 392 | onBasePower: function (basePower, attacker, defender, move) { 393 | if (move.type === 'Water') { 394 | this.debug('rain water boost'); 395 | return this.chainModify(1.5); 396 | } 397 | if (move.type === 'Fire') { 398 | this.debug('rain fire suppress'); 399 | return this.chainModify(0.5); 400 | } 401 | }, 402 | onStart: function (battle, source, effect) { 403 | if (effect && effect.effectType === 'Ability' && this.gen <= 5) { 404 | this.effectData.duration = 0; 405 | this.add('-weather', 'RainDance', '[from] ability: ' + effect, '[of] ' + source); 406 | } else { 407 | this.add('-weather', 'RainDance'); 408 | } 409 | }, 410 | onResidualOrder: 1, 411 | onResidual: function () { 412 | this.add('-weather', 'RainDance', '[upkeep]'); 413 | this.eachEvent('Weather'); 414 | }, 415 | onEnd: function () { 416 | this.add('-weather', 'none'); 417 | } 418 | }, 419 | primordialsea: { 420 | effectType: 'Weather', 421 | duration: 0, 422 | onTryMove: function (target, source, effect) { 423 | if (effect.type === 'Fire' && effect.category !== 'Status') { 424 | this.debug('Primordial Sea fire suppress'); 425 | this.add('-fail', source, effect, '[from] Primordial Sea'); 426 | return null; 427 | } 428 | }, 429 | onBasePower: function (basePower, attacker, defender, move) { 430 | if (move.type === 'Water') { 431 | this.debug('Rain water boost'); 432 | return this.chainModify(1.5); 433 | } 434 | }, 435 | onStart: function () { 436 | this.add('-weather', 'PrimordialSea'); 437 | }, 438 | onResidualOrder: 1, 439 | onResidual: function () { 440 | this.add('-weather', 'PrimordialSea', '[upkeep]'); 441 | this.eachEvent('Weather'); 442 | }, 443 | onEnd: function () { 444 | this.add('-weather', 'none'); 445 | } 446 | }, 447 | sunnyday: { 448 | effectType: 'Weather', 449 | duration: 5, 450 | durationCallback: function (source, effect) { 451 | if (source && source.hasItem('heatrock')) { 452 | return 8; 453 | } 454 | return 5; 455 | }, 456 | onBasePower: function (basePower, attacker, defender, move) { 457 | if (move.type === 'Fire') { 458 | this.debug('Sunny Day fire boost'); 459 | return this.chainModify(1.5); 460 | } 461 | if (move.type === 'Water') { 462 | this.debug('Sunny Day water suppress'); 463 | return this.chainModify(0.5); 464 | } 465 | }, 466 | onStart: function (battle, source, effect) { 467 | if (effect && effect.effectType === 'Ability' && this.gen <= 5) { 468 | this.effectData.duration = 0; 469 | this.add('-weather', 'SunnyDay', '[from] ability: ' + effect, '[of] ' + source); 470 | } else { 471 | this.add('-weather', 'SunnyDay'); 472 | } 473 | }, 474 | onImmunity: function (type) { 475 | if (type === 'frz') return false; 476 | }, 477 | onResidualOrder: 1, 478 | onResidual: function () { 479 | this.add('-weather', 'SunnyDay', '[upkeep]'); 480 | this.eachEvent('Weather'); 481 | }, 482 | onEnd: function () { 483 | this.add('-weather', 'none'); 484 | } 485 | }, 486 | desolateland: { 487 | effectType: 'Weather', 488 | duration: 0, 489 | onTryMove: function (target, source, effect) { 490 | if (effect.type === 'Water' && effect.category !== 'Status') { 491 | this.debug('Desolate Land water suppress'); 492 | this.add('-fail', source, effect, '[from] Desolate Land'); 493 | return null; 494 | } 495 | }, 496 | onBasePower: function (basePower, attacker, defender, move) { 497 | if (move.type === 'Fire') { 498 | this.debug('Sunny Day fire boost'); 499 | return this.chainModify(1.5); 500 | } 501 | }, 502 | onStart: function () { 503 | this.add('-weather', 'DesolateLand'); 504 | }, 505 | onImmunity: function (type) { 506 | if (type === 'frz') return false; 507 | }, 508 | onResidualOrder: 1, 509 | onResidual: function () { 510 | this.add('-weather', 'DesolateLand', '[upkeep]'); 511 | this.eachEvent('Weather'); 512 | }, 513 | onEnd: function () { 514 | this.add('-weather', 'none'); 515 | } 516 | }, 517 | sandstorm: { 518 | effectType: 'Weather', 519 | duration: 5, 520 | durationCallback: function (source, effect) { 521 | if (source && source.hasItem('smoothrock')) { 522 | return 8; 523 | } 524 | return 5; 525 | }, 526 | // This should be applied directly to the stat before any of the other modifiers are chained 527 | // So we give it increased priority. 528 | onModifySpDPriority: 10, 529 | onModifySpD: function (spd, pokemon) { 530 | if (pokemon.hasType('Rock') && this.isWeather('sandstorm')) { 531 | return this.modify(spd, 1.5); 532 | } 533 | }, 534 | onStart: function (battle, source, effect) { 535 | if (effect && effect.effectType === 'Ability' && this.gen <= 5) { 536 | this.effectData.duration = 0; 537 | this.add('-weather', 'Sandstorm', '[from] ability: ' + effect, '[of] ' + source); 538 | } else { 539 | this.add('-weather', 'Sandstorm'); 540 | } 541 | }, 542 | onResidualOrder: 1, 543 | onResidual: function () { 544 | this.add('-weather', 'Sandstorm', '[upkeep]'); 545 | if (this.isWeather('sandstorm')) this.eachEvent('Weather'); 546 | }, 547 | onWeather: function (target) { 548 | this.damage(target.maxhp / 16); 549 | }, 550 | onEnd: function () { 551 | this.add('-weather', 'none'); 552 | } 553 | }, 554 | hail: { 555 | effectType: 'Weather', 556 | duration: 5, 557 | durationCallback: function (source, effect) { 558 | if (source && source.hasItem('icyrock')) { 559 | return 8; 560 | } 561 | return 5; 562 | }, 563 | onStart: function (battle, source, effect) { 564 | if (effect && effect.effectType === 'Ability' && this.gen <= 5) { 565 | this.effectData.duration = 0; 566 | this.add('-weather', 'Hail', '[from] ability: ' + effect, '[of] ' + source); 567 | } else { 568 | this.add('-weather', 'Hail'); 569 | } 570 | }, 571 | onResidualOrder: 1, 572 | onResidual: function () { 573 | this.add('-weather', 'Hail', '[upkeep]'); 574 | if (this.isWeather('hail')) this.eachEvent('Weather'); 575 | }, 576 | onWeather: function (target) { 577 | this.damage(target.maxhp / 16); 578 | }, 579 | onEnd: function () { 580 | this.add('-weather', 'none'); 581 | } 582 | }, 583 | deltastream: { 584 | effectType: 'Weather', 585 | duration: 0, 586 | onEffectiveness: function (typeMod, target, type, move) { 587 | if (move && move.effectType === 'Move' && type === 'Flying' && typeMod > 0) { 588 | this.add('-activate', '', 'deltastream'); 589 | return 0; 590 | } 591 | }, 592 | onStart: function () { 593 | this.add('-weather', 'DeltaStream'); 594 | }, 595 | onResidualOrder: 1, 596 | onResidual: function () { 597 | this.add('-weather', 'DeltaStream', '[upkeep]'); 598 | this.eachEvent('Weather'); 599 | }, 600 | onEnd: function () { 601 | this.add('-weather', 'none'); 602 | } 603 | }, 604 | 605 | arceus: { 606 | // Arceus's actual typing is implemented here 607 | // Arceus's true typing for all its formes is Normal, and it's only 608 | // Multitype that changes its type, but its formes are specified to 609 | // be their corresponding type in the Pokedex, so that needs to be 610 | // overridden. This is mainly relevant for Hackmons and Balanced 611 | // Hackmons. 612 | onSwitchInPriority: 101, 613 | onSwitchIn: function (pokemon) { 614 | var type = 'Normal'; 615 | if (pokemon.ability === 'multitype') { 616 | type = this.runEvent('Plate', pokemon); 617 | if (!type || type === true) { 618 | type = 'Normal'; 619 | } 620 | } 621 | pokemon.setType(type, true); 622 | } 623 | } 624 | }; 625 | -------------------------------------------------------------------------------- /data/typechart.js: -------------------------------------------------------------------------------- 1 | exports.BattleTypeChart = { 2 | "Bug": { 3 | damageTaken: { 4 | "Bug": 0, 5 | "Dark": 0, 6 | "Dragon": 0, 7 | "Electric": 0, 8 | "Fairy": 0, 9 | "Fighting": 2, 10 | "Fire": 1, 11 | "Flying": 1, 12 | "Ghost": 0, 13 | "Grass": 2, 14 | "Ground": 2, 15 | "Ice": 0, 16 | "Normal": 0, 17 | "Poison": 0, 18 | "Psychic": 0, 19 | "Rock": 1, 20 | "Steel": 0, 21 | "Water": 0 22 | }, 23 | HPivs: {"atk":30, "def":30, "spd":30} 24 | }, 25 | "Dark": { 26 | damageTaken: { 27 | "Bug": 1, 28 | "Dark": 2, 29 | "Dragon": 0, 30 | "Electric": 0, 31 | "Fairy": 1, 32 | "Fighting": 1, 33 | "Fire": 0, 34 | "Flying": 0, 35 | "Ghost": 2, 36 | "Grass": 0, 37 | "Ground": 0, 38 | "Ice": 0, 39 | "Normal": 0, 40 | "Poison": 0, 41 | "Psychic": 3, 42 | "Rock": 0, 43 | "Steel": 0, 44 | "Water": 0 45 | }, 46 | HPivs: {} 47 | }, 48 | "Dragon": { 49 | damageTaken: { 50 | "Bug": 0, 51 | "Dark": 0, 52 | "Dragon": 1, 53 | "Electric": 2, 54 | "Fairy": 1, 55 | "Fighting": 0, 56 | "Fire": 2, 57 | "Flying": 0, 58 | "Ghost": 0, 59 | "Grass": 2, 60 | "Ground": 0, 61 | "Ice": 1, 62 | "Normal": 0, 63 | "Poison": 0, 64 | "Psychic": 0, 65 | "Rock": 0, 66 | "Steel": 0, 67 | "Water": 2 68 | }, 69 | HPivs: {"atk":30} 70 | }, 71 | "Electric": { 72 | damageTaken: { 73 | par: 3, 74 | "Bug": 0, 75 | "Dark": 0, 76 | "Dragon": 0, 77 | "Electric": 2, 78 | "Fairy": 0, 79 | "Fighting": 0, 80 | "Fire": 0, 81 | "Flying": 2, 82 | "Ghost": 0, 83 | "Grass": 0, 84 | "Ground": 1, 85 | "Ice": 0, 86 | "Normal": 0, 87 | "Poison": 0, 88 | "Psychic": 0, 89 | "Rock": 0, 90 | "Steel": 2, 91 | "Water": 0 92 | }, 93 | HPivs: {"spa":30} 94 | }, 95 | "Fairy": { 96 | damageTaken: { 97 | "Bug": 2, 98 | "Dark": 2, 99 | "Dragon": 3, 100 | "Electric": 0, 101 | "Fairy": 0, 102 | "Fighting": 2, 103 | "Fire": 0, 104 | "Flying": 0, 105 | "Ghost": 0, 106 | "Grass": 0, 107 | "Ground": 0, 108 | "Ice": 0, 109 | "Normal": 0, 110 | "Poison": 1, 111 | "Psychic": 0, 112 | "Rock": 0, 113 | "Steel": 1, 114 | "Water": 0 115 | } 116 | }, 117 | "Fighting": { 118 | damageTaken: { 119 | "Bug": 2, 120 | "Dark": 2, 121 | "Dragon": 0, 122 | "Electric": 0, 123 | "Fairy": 1, 124 | "Fighting": 0, 125 | "Fire": 0, 126 | "Flying": 1, 127 | "Ghost": 0, 128 | "Grass": 0, 129 | "Ground": 0, 130 | "Ice": 0, 131 | "Normal": 0, 132 | "Poison": 0, 133 | "Psychic": 1, 134 | "Rock": 2, 135 | "Steel": 0, 136 | "Water": 0 137 | }, 138 | HPivs: {"def":30, "spa":30, "spd":30, "spe":30} 139 | }, 140 | "Fire": { 141 | damageTaken: { 142 | brn: 3, 143 | "Bug": 2, 144 | "Dark": 0, 145 | "Dragon": 0, 146 | "Electric": 0, 147 | "Fairy": 2, 148 | "Fighting": 0, 149 | "Fire": 2, 150 | "Flying": 0, 151 | "Ghost": 0, 152 | "Grass": 2, 153 | "Ground": 1, 154 | "Ice": 2, 155 | "Normal": 0, 156 | "Poison": 0, 157 | "Psychic": 0, 158 | "Rock": 1, 159 | "Steel": 2, 160 | "Water": 1 161 | }, 162 | HPivs: {"atk":30, "spa":30, "spe":30} 163 | }, 164 | "Flying": { 165 | damageTaken: { 166 | "Bug": 2, 167 | "Dark": 0, 168 | "Dragon": 0, 169 | "Electric": 1, 170 | "Fairy": 0, 171 | "Fighting": 2, 172 | "Fire": 0, 173 | "Flying": 0, 174 | "Ghost": 0, 175 | "Grass": 2, 176 | "Ground": 3, 177 | "Ice": 1, 178 | "Normal": 0, 179 | "Poison": 0, 180 | "Psychic": 0, 181 | "Rock": 1, 182 | "Steel": 0, 183 | "Water": 0 184 | }, 185 | HPivs: {"hp":30, "atk":30, "def":30, "spa":30, "spd":30} 186 | }, 187 | "Ghost": { 188 | damageTaken: { 189 | trapped: 3, 190 | "Bug": 2, 191 | "Dark": 1, 192 | "Dragon": 0, 193 | "Electric": 0, 194 | "Fairy": 0, 195 | "Fighting": 3, 196 | "Fire": 0, 197 | "Flying": 0, 198 | "Ghost": 1, 199 | "Grass": 0, 200 | "Ground": 0, 201 | "Ice": 0, 202 | "Normal": 3, 203 | "Poison": 2, 204 | "Psychic": 0, 205 | "Rock": 0, 206 | "Steel": 0, 207 | "Water": 0 208 | }, 209 | HPivs: {"def":30, "spd":30} 210 | }, 211 | "Grass": { 212 | damageTaken: { 213 | powder: 3, 214 | "Bug": 1, 215 | "Dark": 0, 216 | "Dragon": 0, 217 | "Electric": 2, 218 | "Fairy": 0, 219 | "Fighting": 0, 220 | "Fire": 1, 221 | "Flying": 1, 222 | "Ghost": 0, 223 | "Grass": 2, 224 | "Ground": 2, 225 | "Ice": 1, 226 | "Normal": 0, 227 | "Poison": 1, 228 | "Psychic": 0, 229 | "Rock": 0, 230 | "Steel": 0, 231 | "Water": 2 232 | }, 233 | HPivs: {"atk":30, "spa":30} 234 | }, 235 | "Ground": { 236 | damageTaken: { 237 | sandstorm: 3, 238 | "Bug": 0, 239 | "Dark": 0, 240 | "Dragon": 0, 241 | "Electric": 3, 242 | "Fairy": 0, 243 | "Fighting": 0, 244 | "Fire": 0, 245 | "Flying": 0, 246 | "Ghost": 0, 247 | "Grass": 1, 248 | "Ground": 0, 249 | "Ice": 1, 250 | "Normal": 0, 251 | "Poison": 2, 252 | "Psychic": 0, 253 | "Rock": 2, 254 | "Steel": 0, 255 | "Water": 1 256 | }, 257 | HPivs: {"spa":30, "spd":30} 258 | }, 259 | "Ice": { 260 | damageTaken: { 261 | hail: 3, 262 | frz: 3, 263 | "Bug": 0, 264 | "Dark": 0, 265 | "Dragon": 0, 266 | "Electric": 0, 267 | "Fairy": 0, 268 | "Fighting": 1, 269 | "Fire": 1, 270 | "Flying": 0, 271 | "Ghost": 0, 272 | "Grass": 0, 273 | "Ground": 0, 274 | "Ice": 2, 275 | "Normal": 0, 276 | "Poison": 0, 277 | "Psychic": 0, 278 | "Rock": 1, 279 | "Steel": 1, 280 | "Water": 0 281 | }, 282 | HPivs: {"atk":30, "def":30} 283 | }, 284 | "Normal": { 285 | damageTaken: { 286 | "Bug": 0, 287 | "Dark": 0, 288 | "Dragon": 0, 289 | "Electric": 0, 290 | "Fairy": 0, 291 | "Fighting": 1, 292 | "Fire": 0, 293 | "Flying": 0, 294 | "Ghost": 3, 295 | "Grass": 0, 296 | "Ground": 0, 297 | "Ice": 0, 298 | "Normal": 0, 299 | "Poison": 0, 300 | "Psychic": 0, 301 | "Rock": 0, 302 | "Steel": 0, 303 | "Water": 0 304 | } 305 | }, 306 | "Poison": { 307 | damageTaken: { 308 | psn: 3, 309 | tox: 3, 310 | "Bug": 2, 311 | "Dark": 0, 312 | "Dragon": 0, 313 | "Electric": 0, 314 | "Fairy": 2, 315 | "Fighting": 2, 316 | "Fire": 0, 317 | "Flying": 0, 318 | "Ghost": 0, 319 | "Grass": 2, 320 | "Ground": 1, 321 | "Ice": 0, 322 | "Normal": 0, 323 | "Poison": 2, 324 | "Psychic": 1, 325 | "Rock": 0, 326 | "Steel": 0, 327 | "Water": 0 328 | }, 329 | HPivs: {"def":30, "spa":30, "spd":30} 330 | }, 331 | "Psychic": { 332 | damageTaken: { 333 | "Bug": 1, 334 | "Dark": 1, 335 | "Dragon": 0, 336 | "Electric": 0, 337 | "Fairy": 0, 338 | "Fighting": 2, 339 | "Fire": 0, 340 | "Flying": 0, 341 | "Ghost": 1, 342 | "Grass": 0, 343 | "Ground": 0, 344 | "Ice": 0, 345 | "Normal": 0, 346 | "Poison": 0, 347 | "Psychic": 2, 348 | "Rock": 0, 349 | "Steel": 0, 350 | "Water": 0 351 | }, 352 | HPivs: {"atk":30, "spe":30} 353 | }, 354 | "Rock": { 355 | damageTaken: { 356 | sandstorm: 3, 357 | "Bug": 0, 358 | "Dark": 0, 359 | "Dragon": 0, 360 | "Electric": 0, 361 | "Fairy": 0, 362 | "Fighting": 1, 363 | "Fire": 2, 364 | "Flying": 2, 365 | "Ghost": 0, 366 | "Grass": 1, 367 | "Ground": 1, 368 | "Ice": 0, 369 | "Normal": 2, 370 | "Poison": 2, 371 | "Psychic": 0, 372 | "Rock": 0, 373 | "Steel": 1, 374 | "Water": 1 375 | }, 376 | HPivs: {"def":30, "spd":30, "spe":30} 377 | }, 378 | "Steel": { 379 | damageTaken: { 380 | psn: 3, 381 | tox: 3, 382 | sandstorm: 3, 383 | "Bug": 2, 384 | "Dark": 0, 385 | "Dragon": 2, 386 | "Electric": 0, 387 | "Fairy": 2, 388 | "Fighting": 1, 389 | "Fire": 1, 390 | "Flying": 2, 391 | "Ghost": 0, 392 | "Grass": 2, 393 | "Ground": 1, 394 | "Ice": 2, 395 | "Normal": 2, 396 | "Poison": 3, 397 | "Psychic": 2, 398 | "Rock": 2, 399 | "Steel": 2, 400 | "Water": 0 401 | }, 402 | HPivs: {"spd":30} 403 | }, 404 | "Water": { 405 | damageTaken: { 406 | "Bug": 0, 407 | "Dark": 0, 408 | "Dragon": 0, 409 | "Electric": 1, 410 | "Fairy": 0, 411 | "Fighting": 0, 412 | "Fire": 2, 413 | "Flying": 0, 414 | "Ghost": 0, 415 | "Grass": 1, 416 | "Ground": 0, 417 | "Ice": 2, 418 | "Normal": 0, 419 | "Poison": 0, 420 | "Psychic": 0, 421 | "Rock": 0, 422 | "Steel": 2, 423 | "Water": 2 424 | }, 425 | HPivs: {"atk":30, "def":30, "spa":30} 426 | } 427 | }; 428 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb'); 2 | 3 | var db = new Datastore({ filename: 'results.db', autoload: true }); 4 | module.exports = db; -------------------------------------------------------------------------------- /epicwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rameshvarun/showdownbot/00dcfccab2c54fa6aab45eec7c22447cb2623a9a/epicwin.png -------------------------------------------------------------------------------- /minimax_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #tell grid engine to use current directory 4 | #$ -cwd 5 | 6 | nodejs bot.js --nolog --startchallenging --ranked 7 | -------------------------------------------------------------------------------- /mods/README.md: -------------------------------------------------------------------------------- 1 | This is the mods folder. However, we don't plan on adding any mods. 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "showdownbot", 3 | "description": "AI bot for playing Pokemon Showdown.", 4 | "dependencies": { 5 | "commander": "2.x", 6 | "convnetjs": "^0.3.0", 7 | "express": "^4.10.0", 8 | "jsclass": "4.x", 9 | "log4js": "1.1.1", 10 | "nedb": "^1.8.0", 11 | "nunjucks": "3.x", 12 | "request": "2.x", 13 | "sockjs-client-ws": "0.1.0", 14 | "sugar": "^1.4.1", 15 | "underscore": "1.x" 16 | }, 17 | "scripts": { 18 | "start": "bot.js" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rameshvarun/showdownbot/00dcfccab2c54fa6aab45eec7c22447cb2623a9a/screenshot.png -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Home{% endblock %} 4 | 5 | {% block content %} 6 | Search for Match 7 | 8 | {% if challenging %} 9 | End Challenging 10 | {% else %} 11 | Start Challenging 12 | {% endif %} 13 | 14 |

Current Games

15 | 16 | {% for game in games %} 17 | Title: 18 | {{game.title}} 19 | Spectate 20 | Details 21 | {% endfor %} 22 | 23 |

History

24 | {% for game in history %} 25 |
26 |
{{game.tier}}: {{game.title}} {% if game.win %}(Win){% else %}(Loss){% endif %}
27 |
28 | Watch Replay 29 |
30 |
31 | {% endfor %} 32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %} 11 | {% endblock %} 12 | 13 | 14 | 15 | 16 | {% block content %} 17 | {% endblock %} 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/replay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{game.title}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

{{game.tier}}: {{game.title}}

29 |

30 | Uploaded: {{game.date}} 31 |

32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | Speed: 42 |
43 |
44 |
45 | Color scheme: 46 |
47 |
48 | 52 |
53 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 |

Decision Log:

72 | 73 | 74 | {% for d in game.decisions %} 75 |
76 |
{{d.prompt}}
77 |
78 | Choices: {{stringify(d.choices)}}
79 | Result: {{d.choice}}
80 | Reason: {{d.reason}} 81 |
82 |
83 | 84 |

85 | {% endfor %} 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /templates/room.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Home{% endblock %} 4 | 5 | {% block content %} 6 |

{{game.title}}

7 | 8 | {{ format(game.state.toString()) | safe }} 9 |
10 |
11 | 12 | 60 | {% endblock %} -------------------------------------------------------------------------------- /tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tools 3 | * Pokemon Showdown - http://pokemonshowdown.com/ 4 | * 5 | * Handles getting data about pokemon, items, etc. 6 | * 7 | * This file is used by the main process (to validate teams) 8 | * as well as the individual simulator processes (to get 9 | * information about pokemon, items, etc to simulate). 10 | * 11 | * @license MIT license 12 | */ 13 | 14 | var fs = require('fs'); 15 | 16 | module.exports = (function () { 17 | var moddedTools = {}; 18 | 19 | var dataTypes = ['FormatsData', 'Learnsets', 'Pokedex', 'Movedex', 'Statuses', 'TypeChart', 'Scripts', 'Items', 'Abilities', 'Formats', 'Aliases']; 20 | var dataFiles = { 21 | 'Pokedex': 'pokedex.js', 22 | 'Movedex': 'moves.js', 23 | 'Statuses': 'statuses.js', 24 | 'TypeChart': 'typechart.js', 25 | 'Scripts': 'scripts.js', 26 | 'Items': 'items.js', 27 | 'Abilities': 'abilities.js', 28 | 'Formats': 'rulesets.js', 29 | 'FormatsData': 'formats-data.js', 30 | 'Learnsets': 'learnsets.js', 31 | 'Aliases': 'aliases.js' 32 | }; 33 | function Tools(mod, parentMod) { 34 | if (!mod) { 35 | mod = 'base'; 36 | this.isBase = true; 37 | } else if (!parentMod) { 38 | parentMod = 'base'; 39 | } 40 | this.currentMod = mod; 41 | 42 | var data = this.data = { 43 | mod: mod 44 | }; 45 | if (mod === 'base') { 46 | dataTypes.forEach(function (dataType) { 47 | try { 48 | var path = './data/' + dataFiles[dataType]; 49 | if (fs.existsSync(path)) { 50 | data[dataType] = require(path)['Battle' + dataType]; 51 | } 52 | } catch (e) { 53 | console.log('CRASH LOADING DATA: ' + e.stack); 54 | } 55 | if (!data[dataType]) data[dataType] = {}; 56 | }, this); 57 | try { 58 | var path = './config/formats.js'; 59 | if (fs.existsSync(path)) { 60 | var configFormats = require(path).Formats; 61 | for (var i = 0; i < configFormats.length; i++) { 62 | var format = configFormats[i]; 63 | var id = toId(format.name); 64 | format.effectType = 'Format'; 65 | if (format.challengeShow === undefined) format.challengeShow = true; 66 | if (format.searchShow === undefined) format.searchShow = true; 67 | data.Formats[id] = format; 68 | } 69 | } 70 | } catch (e) { 71 | console.log('CRASH LOADING FORMATS: ' + e.stack); 72 | } 73 | } else { 74 | var parentData = moddedTools[parentMod].data; 75 | dataTypes.forEach(function (dataType) { 76 | try { 77 | var path = './mods/' + mod + '/' + dataFiles[dataType]; 78 | if (fs.existsSync(path)) { 79 | data[dataType] = require(path)['Battle' + dataType]; 80 | } 81 | } catch (e) { 82 | console.log('CRASH LOADING MOD DATA: ' + e.stack); 83 | } 84 | if (!data[dataType]) data[dataType] = {}; 85 | for (var i in parentData[dataType]) { 86 | if (data[dataType][i] === null) { 87 | // null means don't inherit 88 | delete data[dataType][i]; 89 | } else if (!(i in data[dataType])) { 90 | // If it doesn't exist it's inherited from the parent data 91 | if (dataType === 'Pokedex') { 92 | // Pokedex entries can be modified too many different ways 93 | data[dataType][i] = Object.clone(parentData[dataType][i], true); 94 | } else { 95 | data[dataType][i] = parentData[dataType][i]; 96 | } 97 | } else if (data[dataType][i] && data[dataType][i].inherit) { 98 | // {inherit: true} can be used to modify only parts of the parent data, 99 | // instead of overwriting entirely 100 | delete data[dataType][i].inherit; 101 | Object.assign(data[dataType][i], parentData[dataType][i], false, false); 102 | } 103 | } 104 | }); 105 | } 106 | data['Natures'] = { 107 | adamant: {name:"Adamant", plus:'atk', minus:'spa'}, 108 | bashful: {name:"Bashful"}, 109 | bold: {name:"Bold", plus:'def', minus:'atk'}, 110 | brave: {name:"Brave", plus:'atk', minus:'spe'}, 111 | calm: {name:"Calm", plus:'spd', minus:'atk'}, 112 | careful: {name:"Careful", plus:'spd', minus:'spa'}, 113 | docile: {name:"Docile"}, 114 | gentle: {name:"Gentle", plus:'spd', minus:'def'}, 115 | hardy: {name:"Hardy"}, 116 | hasty: {name:"Hasty", plus:'spe', minus:'def'}, 117 | impish: {name:"Impish", plus:'def', minus:'spa'}, 118 | jolly: {name:"Jolly", plus:'spe', minus:'spa'}, 119 | lax: {name:"Lax", plus:'def', minus:'spd'}, 120 | lonely: {name:"Lonely", plus:'atk', minus:'def'}, 121 | mild: {name:"Mild", plus:'spa', minus:'def'}, 122 | modest: {name:"Modest", plus:'spa', minus:'atk'}, 123 | naive: {name:"Naive", plus:'spe', minus:'spd'}, 124 | naughty: {name:"Naughty", plus:'atk', minus:'spd'}, 125 | quiet: {name:"Quiet", plus:'spa', minus:'spe'}, 126 | quirky: {name:"Quirky"}, 127 | rash: {name:"Rash", plus:'spa', minus:'spd'}, 128 | relaxed: {name:"Relaxed", plus:'def', minus:'spe'}, 129 | sassy: {name:"Sassy", plus:'spd', minus:'spe'}, 130 | serious: {name:"Serious"}, 131 | timid: {name:"Timid", plus:'spe', minus:'atk'} 132 | }; 133 | } 134 | Tools.loadMods = function () { 135 | if (Tools.modsLoaded) return; 136 | var parentMods = {}; 137 | 138 | try { 139 | var mods = fs.readdirSync('./mods/'); 140 | 141 | mods.forEach(function (mod) { 142 | if (fs.existsSync('./mods/' + mod + '/scripts.js')) { 143 | parentMods[mod] = require('./mods/' + mod + '/scripts.js').BattleScripts.inherit || 'base'; 144 | } else { 145 | parentMods[mod] = 'base'; 146 | } 147 | }); 148 | 149 | var didSomething = false; 150 | do { 151 | didSomething = false; 152 | for (var i in parentMods) { 153 | if (!moddedTools[i] && moddedTools[parentMods[i]]) { 154 | moddedTools[i] = Tools.construct(i, parentMods[i]); 155 | didSomething = true; 156 | } 157 | } 158 | } while (didSomething); 159 | } catch (e) { 160 | console.log("Error while loading mods: " + e); 161 | } 162 | Tools.modsLoaded = true; 163 | }; 164 | 165 | Tools.prototype.mod = function (mod) { 166 | Tools.loadMods(); 167 | if (!moddedTools[mod]) { 168 | mod = this.getFormat(mod).mod; 169 | } 170 | if (!mod) mod = 'base'; 171 | return moddedTools[mod]; 172 | }; 173 | Tools.prototype.modData = function (dataType, id) { 174 | if (this.isBase) return this.data[dataType][id]; 175 | var parentMod = this.data.Scripts.inherit; 176 | if (!parentMod) parentMod = 'base'; 177 | if (this.data[dataType][id] !== moddedTools[parentMod].data[dataType][id]) return this.data[dataType][id]; 178 | return (this.data[dataType][id] = Object.clone(this.data[dataType][id], true)); 179 | }; 180 | 181 | Tools.prototype.effectToString = function () { 182 | return this.name; 183 | }; 184 | Tools.prototype.getImmunity = function (source, target) { 185 | // returns false if the target is immune; true otherwise 186 | // also checks immunity to some statuses 187 | var sourceType = source.type || source; 188 | var targetTyping = target.getTypes && target.getTypes() || target.types || target; 189 | if (Array.isArray(targetTyping)) { 190 | for (var i = 0; i < targetTyping.length; i++) { 191 | if (!this.getImmunity(sourceType, targetTyping[i])) return false; 192 | } 193 | return true; 194 | } 195 | var typeData = this.data.TypeChart[targetTyping]; 196 | if (typeData && typeData.damageTaken[sourceType] === 3) return false; 197 | return true; 198 | }; 199 | Tools.prototype.getEffectiveness = function (source, target) { 200 | var sourceType = source.type || source; 201 | var totalTypeMod = 0; 202 | var targetTyping = target.getTypes && target.getTypes() || target.types || target; 203 | if (Array.isArray(targetTyping)) { 204 | for (var i = 0; i < targetTyping.length; i++) { 205 | totalTypeMod += this.getEffectiveness(sourceType, targetTyping[i]); 206 | } 207 | return totalTypeMod; 208 | } 209 | var typeData = this.data.TypeChart[targetTyping]; 210 | if (!typeData) return 0; 211 | switch (typeData.damageTaken[sourceType]) { 212 | case 1: return 1; // super-effective 213 | case 2: return -1; // resist 214 | // in case of weird situations like Gravity, immunity is 215 | // handled elsewhere 216 | default: return 0; 217 | } 218 | }; 219 | Tools.prototype.getTemplate = function (template) { 220 | if (!template || typeof template === 'string') { 221 | var name = (template || '').trim(); 222 | var id = toId(name); 223 | if (this.data.Aliases[id]) { 224 | name = this.data.Aliases[id]; 225 | id = toId(name); 226 | } 227 | template = {}; 228 | if (id && this.data.Pokedex[id]) { 229 | template = this.data.Pokedex[id]; 230 | if (template.cached) return template; 231 | template.cached = true; 232 | template.exists = true; 233 | } 234 | name = template.species || template.name || name; 235 | if (this.data.FormatsData[id]) { 236 | Object.assign(template, this.data.FormatsData[id]); 237 | } 238 | if (this.data.Learnsets[id]) { 239 | Object.assign(template, this.data.Learnsets[id]); 240 | } 241 | if (!template.id) template.id = id; 242 | if (!template.name) template.name = name; 243 | if (!template.speciesid) template.speciesid = id; 244 | if (!template.species) template.species = name; 245 | if (!template.baseSpecies) template.baseSpecies = name; 246 | if (!template.forme) template.forme = ''; 247 | if (!template.formeLetter) template.formeLetter = ''; 248 | if (!template.spriteid) template.spriteid = toId(template.baseSpecies) + (template.baseSpecies !== name ? '-' + toId(template.forme) : ''); 249 | if (!template.prevo) template.prevo = ''; 250 | if (!template.evos) template.evos = []; 251 | if (!template.nfe) template.nfe = !!template.evos.length; 252 | if (!template.gender) template.gender = ''; 253 | if (!template.genderRatio && template.gender === 'M') template.genderRatio = {M:1, F:0}; 254 | if (!template.genderRatio && template.gender === 'F') template.genderRatio = {M:0, F:1}; 255 | if (!template.genderRatio && template.gender === 'N') template.genderRatio = {M:0, F:0}; 256 | if (!template.genderRatio) template.genderRatio = {M:0.5, F:0.5}; 257 | if (!template.tier && template.baseSpecies !== template.species) template.tier = this.data.FormatsData[toId(template.baseSpecies)].tier; 258 | if (!template.tier) template.tier = 'Illegal'; 259 | if (!template.gen) { 260 | if (template.forme && template.forme in {'Mega':1, 'Mega-X':1, 'Mega-Y':1}) { 261 | template.gen = 6; 262 | template.isMega = true; 263 | } else if (template.forme === 'Primal') { 264 | template.gen = 6; 265 | template.isPrimal = true; 266 | } else if (template.num >= 650) template.gen = 6; 267 | else if (template.num >= 494) template.gen = 5; 268 | else if (template.num >= 387) template.gen = 4; 269 | else if (template.num >= 252) template.gen = 3; 270 | else if (template.num >= 152) template.gen = 2; 271 | else if (template.num >= 1) template.gen = 1; 272 | else template.gen = 0; 273 | } 274 | } 275 | return template; 276 | }; 277 | Tools.prototype.getMove = function (move) { 278 | if (!move || typeof move === 'string') { 279 | var name = (move || '').trim(); 280 | var id = toId(name); 281 | if (this.data.Aliases[id]) { 282 | name = this.data.Aliases[id]; 283 | id = toId(name); 284 | } 285 | move = {}; 286 | if (id.substr(0, 11) === 'hiddenpower') { 287 | var matches = /([a-z]*)([0-9]*)/.exec(id); 288 | id = matches[1]; 289 | } 290 | if (id && this.data.Movedex[id]) { 291 | move = this.data.Movedex[id]; 292 | if (move.cached) return move; 293 | move.cached = true; 294 | move.exists = true; 295 | } 296 | if (!move.id) move.id = id; 297 | if (!move.name) move.name = name; 298 | if (!move.fullname) move.fullname = 'move: ' + move.name; 299 | move.toString = this.effectToString; 300 | if (!move.critRatio) move.critRatio = 1; 301 | if (!move.baseType) move.baseType = move.type; 302 | if (!move.effectType) move.effectType = 'Move'; 303 | if (!move.secondaries && move.secondary) move.secondaries = [move.secondary]; 304 | if (!move.gen) { 305 | if (move.num >= 560) move.gen = 6; 306 | else if (move.num >= 468) move.gen = 5; 307 | else if (move.num >= 355) move.gen = 4; 308 | else if (move.num >= 252) move.gen = 3; 309 | else if (move.num >= 166) move.gen = 2; 310 | else if (move.num >= 1) move.gen = 1; 311 | else move.gen = 0; 312 | } 313 | if (!move.priority) move.priority = 0; 314 | } 315 | return move; 316 | }; 317 | /** 318 | * Ensure we're working on a copy of a move (and make a copy if we aren't) 319 | * 320 | * Remember: "ensure" - by default, it won't make a copy of a copy: 321 | * moveCopy === Tools.getMoveCopy(moveCopy) 322 | * 323 | * If you really want to, use: 324 | * moveCopyCopy = Tools.getMoveCopy(moveCopy.id) 325 | * 326 | * @param move Move ID, move object, or movecopy object describing move to copy 327 | * @return movecopy object 328 | */ 329 | Tools.prototype.getMoveCopy = function (move) { 330 | if (move && move.isCopy) return move; 331 | move = this.getMove(move); 332 | var moveCopy = Object.clone(move, true); 333 | moveCopy.isCopy = true; 334 | return moveCopy; 335 | }; 336 | Tools.prototype.getEffect = function (effect) { 337 | if (!effect || typeof effect === 'string') { 338 | var name = (effect || '').trim(); 339 | var id = toId(name); 340 | effect = {}; 341 | if (id && this.data.Statuses[id]) { 342 | effect = this.data.Statuses[id]; 343 | effect.name = effect.name || this.data.Statuses[id].name; 344 | } else if (id && this.data.Movedex[id] && this.data.Movedex[id].effect) { 345 | effect = this.data.Movedex[id].effect; 346 | effect.name = effect.name || this.data.Movedex[id].name; 347 | } else if (id && this.data.Abilities[id] && this.data.Abilities[id].effect) { 348 | effect = this.data.Abilities[id].effect; 349 | effect.name = effect.name || this.data.Abilities[id].name; 350 | } else if (id && this.data.Items[id] && this.data.Items[id].effect) { 351 | effect = this.data.Items[id].effect; 352 | effect.name = effect.name || this.data.Items[id].name; 353 | } else if (id && this.data.Formats[id]) { 354 | effect = this.data.Formats[id]; 355 | effect.name = effect.name || this.data.Formats[id].name; 356 | if (!effect.mod) effect.mod = this.currentMod; 357 | if (!effect.effectType) effect.effectType = 'Format'; 358 | } else if (id === 'recoil') { 359 | effect = { 360 | effectType: 'Recoil' 361 | }; 362 | } else if (id === 'drain') { 363 | effect = { 364 | effectType: 'Drain' 365 | }; 366 | } 367 | if (!effect.id) effect.id = id; 368 | if (!effect.name) effect.name = name; 369 | if (!effect.fullname) effect.fullname = effect.name; 370 | effect.toString = this.effectToString; 371 | if (!effect.category) effect.category = 'Effect'; 372 | if (!effect.effectType) effect.effectType = 'Effect'; 373 | } 374 | return effect; 375 | }; 376 | Tools.prototype.getFormat = function (effect) { 377 | if (!effect || typeof effect === 'string') { 378 | var name = (effect || '').trim(); 379 | var id = toId(name); 380 | if (this.data.Aliases[id]) { 381 | name = this.data.Aliases[id]; 382 | id = toId(name); 383 | } 384 | effect = {}; 385 | if (id && this.data.Formats[id]) { 386 | effect = this.data.Formats[id]; 387 | if (effect.cached) return effect; 388 | effect.cached = true; 389 | effect.name = effect.name || this.data.Formats[id].name; 390 | if (!effect.mod) effect.mod = this.currentMod; 391 | if (!effect.effectType) effect.effectType = 'Format'; 392 | } 393 | if (!effect.id) effect.id = id; 394 | if (!effect.name) effect.name = name; 395 | if (!effect.fullname) effect.fullname = effect.name; 396 | effect.toString = this.effectToString; 397 | if (!effect.category) effect.category = 'Effect'; 398 | if (!effect.effectType) effect.effectType = 'Effect'; 399 | } 400 | return effect; 401 | }; 402 | Tools.prototype.getItem = function (item) { 403 | if (!item || typeof item === 'string') { 404 | var name = (item || '').trim(); 405 | var id = toId(name); 406 | if (this.data.Aliases[id]) { 407 | name = this.data.Aliases[id]; 408 | id = toId(name); 409 | } 410 | item = {}; 411 | if (id && this.data.Items[id]) { 412 | item = this.data.Items[id]; 413 | if (item.cached) return item; 414 | item.cached = true; 415 | item.exists = true; 416 | } 417 | if (!item.id) item.id = id; 418 | if (!item.name) item.name = name; 419 | if (!item.fullname) item.fullname = 'item: ' + item.name; 420 | item.toString = this.effectToString; 421 | if (!item.category) item.category = 'Effect'; 422 | if (!item.effectType) item.effectType = 'Item'; 423 | if (item.isBerry) item.fling = {basePower: 10}; 424 | if (!item.gen) { 425 | if (item.num >= 577) item.gen = 6; 426 | else if (item.num >= 537) item.gen = 5; 427 | else if (item.num >= 377) item.gen = 4; 428 | // Due to difference in storing items, gen 2 items must be specified specifically 429 | else item.gen = 3; 430 | } 431 | } 432 | return item; 433 | }; 434 | Tools.prototype.getAbility = function (ability) { 435 | if (!ability || typeof ability === 'string') { 436 | var name = (ability || '').trim(); 437 | var id = toId(name); 438 | ability = {}; 439 | if (id && this.data.Abilities[id]) { 440 | ability = this.data.Abilities[id]; 441 | if (ability.cached) return ability; 442 | ability.cached = true; 443 | ability.exists = true; 444 | } 445 | if (!ability.id) ability.id = id; 446 | if (!ability.name) ability.name = name; 447 | if (!ability.fullname) ability.fullname = 'ability: ' + ability.name; 448 | ability.toString = this.effectToString; 449 | if (!ability.category) ability.category = 'Effect'; 450 | if (!ability.effectType) ability.effectType = 'Ability'; 451 | if (!ability.gen) { 452 | if (ability.num >= 165) ability.gen = 6; 453 | else if (ability.num >= 124) ability.gen = 5; 454 | else if (ability.num >= 77) ability.gen = 4; 455 | else if (ability.num >= 1) ability.gen = 3; 456 | else ability.gen = 0; 457 | } 458 | } 459 | return ability; 460 | }; 461 | Tools.prototype.getType = function (type) { 462 | if (!type || typeof type === 'string') { 463 | var id = toId(type); 464 | id = id.substr(0, 1).toUpperCase() + id.substr(1); 465 | type = {}; 466 | if (id && this.data.TypeChart[id]) { 467 | type = this.data.TypeChart[id]; 468 | if (type.cached) return type; 469 | type.cached = true; 470 | type.exists = true; 471 | type.isType = true; 472 | type.effectType = 'Type'; 473 | } 474 | if (!type.id) type.id = id; 475 | if (!type.effectType) { 476 | // man, this is really meta 477 | type.effectType = 'EffectType'; 478 | } 479 | } 480 | return type; 481 | }; 482 | Tools.prototype.getNature = function (nature) { 483 | if (!nature || typeof nature === 'string') { 484 | var name = (nature || '').trim(); 485 | var id = toId(name); 486 | nature = {}; 487 | if (id && this.data.Natures[id]) { 488 | nature = this.data.Natures[id]; 489 | if (nature.cached) return nature; 490 | nature.cached = true; 491 | nature.exists = true; 492 | } 493 | if (!nature.id) nature.id = id; 494 | if (!nature.name) nature.name = name; 495 | nature.toString = this.effectToString; 496 | if (!nature.effectType) nature.effectType = 'Nature'; 497 | } 498 | return nature; 499 | }; 500 | Tools.prototype.natureModify = function (stats, nature) { 501 | nature = this.getNature(nature); 502 | if (nature.plus) stats[nature.plus] *= 1.1; 503 | if (nature.minus) stats[nature.minus] *= 0.9; 504 | return stats; 505 | }; 506 | 507 | Tools.prototype.getBanlistTable = function (format, subformat, depth) { 508 | var banlistTable; 509 | if (!depth) depth = 0; 510 | if (depth > 8) return; // avoid infinite recursion 511 | if (format.banlistTable && !subformat) { 512 | banlistTable = format.banlistTable; 513 | } else { 514 | if (!format.banlistTable) format.banlistTable = {}; 515 | if (!format.setBanTable) format.setBanTable = []; 516 | if (!format.teamBanTable) format.teamBanTable = []; 517 | 518 | banlistTable = format.banlistTable; 519 | if (!subformat) subformat = format; 520 | if (subformat.banlist) { 521 | for (var i = 0; i < subformat.banlist.length; i++) { 522 | // don't revalidate what we already validate 523 | if (banlistTable[toId(subformat.banlist[i])]) continue; 524 | 525 | banlistTable[subformat.banlist[i]] = subformat.name || true; 526 | banlistTable[toId(subformat.banlist[i])] = subformat.name || true; 527 | 528 | var plusPos = subformat.banlist[i].indexOf('+'); 529 | var complexList; 530 | if (plusPos && plusPos > 0) { 531 | var plusPlusPos = subformat.banlist[i].indexOf('++'); 532 | if (plusPlusPos && plusPlusPos > 0) { 533 | complexList = subformat.banlist[i].split('++'); 534 | for (var j = 0; j < complexList.length; j++) { 535 | complexList[j] = toId(complexList[j]); 536 | } 537 | format.teamBanTable.push(complexList); 538 | } else { 539 | complexList = subformat.banlist[i].split('+'); 540 | for (var j = 0; j < complexList.length; j++) { 541 | complexList[j] = toId(complexList[j]); 542 | } 543 | format.setBanTable.push(complexList); 544 | } 545 | } 546 | } 547 | } 548 | if (subformat.ruleset) { 549 | for (var i = 0; i < subformat.ruleset.length; i++) { 550 | // don't revalidate what we already validate 551 | if (banlistTable['Rule:' + toId(subformat.ruleset[i])]) continue; 552 | 553 | banlistTable['Rule:' + toId(subformat.ruleset[i])] = subformat.ruleset[i]; 554 | if (format.ruleset.indexOf(subformat.ruleset[i]) === -1) format.ruleset.push(subformat.ruleset[i]); 555 | 556 | var subsubformat = this.getFormat(subformat.ruleset[i]); 557 | if (subsubformat.ruleset || subsubformat.banlist) { 558 | this.getBanlistTable(format, subsubformat, depth + 1); 559 | } 560 | } 561 | } 562 | } 563 | return banlistTable; 564 | }; 565 | 566 | Tools.prototype.levenshtein = function (s, t, l) { // s = string 1, t = string 2, l = limit 567 | // Original levenshtein distance function by James Westgate, turned out to be the fastest 568 | var d = []; // 2d matrix 569 | 570 | // Step 1 571 | var n = s.length; 572 | var m = t.length; 573 | 574 | if (n === 0) return m; 575 | if (m === 0) return n; 576 | if (l && Math.abs(m - n) > l) return Math.abs(m - n); 577 | 578 | // Create an array of arrays in javascript (a descending loop is quicker) 579 | for (var i = n; i >= 0; i--) d[i] = []; 580 | 581 | // Step 2 582 | for (var i = n; i >= 0; i--) d[i][0] = i; 583 | for (var j = m; j >= 0; j--) d[0][j] = j; 584 | 585 | // Step 3 586 | for (var i = 1; i <= n; i++) { 587 | var s_i = s.charAt(i - 1); 588 | 589 | // Step 4 590 | for (var j = 1; j <= m; j++) { 591 | // Check the jagged ld total so far 592 | if (i === j && d[i][j] > 4) return n; 593 | 594 | var t_j = t.charAt(j - 1); 595 | var cost = (s_i === t_j) ? 0 : 1; // Step 5 596 | 597 | // Calculate the minimum 598 | var mi = d[i - 1][j] + 1; 599 | var b = d[i][j - 1] + 1; 600 | var c = d[i - 1][j - 1] + cost; 601 | 602 | if (b < mi) mi = b; 603 | if (c < mi) mi = c; 604 | 605 | d[i][j] = mi; // Step 6 606 | } 607 | } 608 | 609 | // Step 7 610 | return d[n][m]; 611 | }; 612 | 613 | Tools.prototype.clampIntRange = function (num, min, max) { 614 | if (typeof num !== 'number') num = 0; 615 | num = Math.floor(num); 616 | if (num < min) num = min; 617 | if (max !== undefined && num > max) num = max; 618 | return num; 619 | }; 620 | 621 | Tools.prototype.escapeHTML = function (str) { 622 | if (!str) return ''; 623 | return ('' + str).escapeHTML(); 624 | }; 625 | 626 | Tools.prototype.dataSearch = function (target, searchIn) { 627 | if (!target) { 628 | return false; 629 | } 630 | 631 | searchIn = searchIn || ['Pokedex', 'Movedex', 'Abilities', 'Items', 'Natures']; 632 | 633 | var searchFunctions = {Pokedex: 'getTemplate', Movedex: 'getMove', Abilities: 'getAbility', Items: 'getItem', Natures: 'getNature'}; 634 | var searchTypes = {Pokedex: 'pokemon', Movedex: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature'}; 635 | var searchResults = []; 636 | for (var i = 0; i < searchIn.length; i++) { 637 | var res = this[searchFunctions[searchIn[i]]](target); 638 | if (res.exists) { 639 | res.searchType = searchTypes[searchIn[i]]; 640 | searchResults.push(res); 641 | } 642 | } 643 | if (searchResults.length) { 644 | return searchResults; 645 | } 646 | 647 | var cmpTarget = target.toLowerCase(); 648 | var maxLd = 3; 649 | if (cmpTarget.length <= 1) { 650 | return false; 651 | } else if (cmpTarget.length <= 4) { 652 | maxLd = 1; 653 | } else if (cmpTarget.length <= 6) { 654 | maxLd = 2; 655 | } 656 | for (var i = 0; i < searchIn.length; i++) { 657 | var searchObj = this.data[searchIn[i]]; 658 | if (!searchObj) { 659 | continue; 660 | } 661 | 662 | for (var j in searchObj) { 663 | var word = searchObj[j]; 664 | if (typeof word === "object") { 665 | word = word.name || word.species; 666 | } 667 | if (!word) { 668 | continue; 669 | } 670 | 671 | var ld = this.levenshtein(cmpTarget, word.toLowerCase(), maxLd); 672 | if (ld <= maxLd) { 673 | searchResults.push({word: word, ld: ld}); 674 | } 675 | } 676 | } 677 | 678 | if (searchResults.length) { 679 | var newTarget = ""; 680 | var newLD = 10; 681 | for (var i = 0, l = searchResults.length; i < l; i++) { 682 | if (searchResults[i].ld < newLD) { 683 | newTarget = searchResults[i]; 684 | newLD = searchResults[i].ld; 685 | } 686 | } 687 | 688 | // To make sure we aren't in an infinite loop... 689 | if (cmpTarget !== newTarget.word) { 690 | return this.dataSearch(newTarget.word); 691 | } 692 | } 693 | 694 | return false; 695 | }; 696 | 697 | Tools.prototype.packTeam = function (team) { 698 | if (!team) return ''; 699 | 700 | var buf = ''; 701 | 702 | for (var i = 0; i < team.length; i++) { 703 | var set = team[i]; 704 | if (buf) buf += ']'; 705 | 706 | // name 707 | buf += (set.name || set.species); 708 | 709 | // species 710 | var id = toId(set.species || set.name); 711 | buf += '|' + (toId(set.name || set.species) === id ? '' : id); 712 | 713 | // item 714 | buf += '|' + toId(set.item); 715 | 716 | // ability 717 | var template = moddedTools.base.getTemplate(set.species || set.name); 718 | var abilities = template.abilities; 719 | id = toId(set.ability); 720 | if (abilities) { 721 | if (id === toId(abilities['0'])) { 722 | buf += '|'; 723 | } else if (id === toId(abilities['1'])) { 724 | buf += '|1'; 725 | } else if (id === toId(abilities['H'])) { 726 | buf += '|H'; 727 | } else { 728 | buf += '|' + id; 729 | } 730 | } else { 731 | buf += '|' + id; 732 | } 733 | 734 | // moves 735 | buf += '|' + set.moves.map(toId).join(','); 736 | 737 | // nature 738 | buf += '|' + set.nature; 739 | 740 | // evs 741 | var evs = '|'; 742 | if (set.evs) { 743 | evs = '|' + (set.evs['hp'] || '') + ',' + (set.evs['atk'] || '') + ',' + (set.evs['def'] || '') + ',' + (set.evs['spa'] || '') + ',' + (set.evs['spd'] || '') + ',' + (set.evs['spe'] || ''); 744 | } 745 | if (evs === '|,,,,,') { 746 | buf += '|'; 747 | } else { 748 | buf += evs; 749 | } 750 | 751 | // gender 752 | if (set.gender && set.gender !== template.gender) { 753 | buf += '|' + set.gender; 754 | } else { 755 | buf += '|'; 756 | } 757 | 758 | // ivs 759 | var ivs = '|'; 760 | if (set.ivs) { 761 | ivs = '|' + (set.ivs['hp'] === 31 || set.ivs['hp'] === undefined ? '' : set.ivs['hp']) + ',' + (set.ivs['atk'] === 31 || set.ivs['atk'] === undefined ? '' : set.ivs['atk']) + ',' + (set.ivs['def'] === 31 || set.ivs['def'] === undefined ? '' : set.ivs['def']) + ',' + (set.ivs['spa'] === 31 || set.ivs['spa'] === undefined ? '' : set.ivs['spa']) + ',' + (set.ivs['spd'] === 31 || set.ivs['spd'] === undefined ? '' : set.ivs['spd']) + ',' + (set.ivs['spe'] === 31 || set.ivs['spe'] === undefined ? '' : set.ivs['spe']); 762 | } 763 | if (ivs === '|,,,,,') { 764 | buf += '|'; 765 | } else { 766 | buf += ivs; 767 | } 768 | 769 | // shiny 770 | if (set.shiny) { 771 | buf += '|S'; 772 | } else { 773 | buf += '|'; 774 | } 775 | 776 | // level 777 | if (set.level && set.level !== 100) { 778 | buf += '|' + set.level; 779 | } else { 780 | buf += '|'; 781 | } 782 | 783 | // happiness 784 | if (set.happiness !== undefined && set.happiness !== 255) { 785 | buf += '|' + set.happiness; 786 | } else { 787 | buf += '|'; 788 | } 789 | } 790 | 791 | return buf; 792 | }; 793 | 794 | Tools.prototype.fastUnpackTeam = function (buf) { 795 | if (!buf) return null; 796 | 797 | var team = []; 798 | var i = 0, j = 0; 799 | 800 | // limit to 24 801 | for (var count = 0; count < 24; count++) { 802 | var set = {}; 803 | team.push(set); 804 | 805 | // name 806 | j = buf.indexOf('|', i); 807 | if (j < 0) return; 808 | set.name = buf.substring(i, j); 809 | i = j + 1; 810 | 811 | // species 812 | j = buf.indexOf('|', i); 813 | if (j < 0) return; 814 | set.species = buf.substring(i, j) || set.name; 815 | i = j + 1; 816 | 817 | // item 818 | j = buf.indexOf('|', i); 819 | if (j < 0) return; 820 | set.item = buf.substring(i, j); 821 | i = j + 1; 822 | 823 | // ability 824 | j = buf.indexOf('|', i); 825 | if (j < 0) return; 826 | var ability = buf.substring(i, j); 827 | var template = moddedTools.base.getTemplate(set.species); 828 | set.ability = (template.abilities && ability in {'':1, 0:1, 1:1, H:1} ? template.abilities[ability || '0'] : ability); 829 | i = j + 1; 830 | 831 | // moves 832 | j = buf.indexOf('|', i); 833 | if (j < 0) return; 834 | set.moves = buf.substring(i, j).split(','); 835 | i = j + 1; 836 | 837 | // nature 838 | j = buf.indexOf('|', i); 839 | if (j < 0) return; 840 | set.nature = buf.substring(i, j); 841 | i = j + 1; 842 | 843 | // evs 844 | j = buf.indexOf('|', i); 845 | if (j < 0) return; 846 | if (j !== i) { 847 | var evs = buf.substring(i, j).split(','); 848 | set.evs = { 849 | hp: Number(evs[0]) || 0, 850 | atk: Number(evs[1]) || 0, 851 | def: Number(evs[2]) || 0, 852 | spa: Number(evs[3]) || 0, 853 | spd: Number(evs[4]) || 0, 854 | spe: Number(evs[5]) || 0 855 | }; 856 | } 857 | i = j + 1; 858 | 859 | // gender 860 | j = buf.indexOf('|', i); 861 | if (j < 0) return; 862 | if (i !== j) set.gender = buf.substring(i, j); 863 | i = j + 1; 864 | 865 | // ivs 866 | j = buf.indexOf('|', i); 867 | if (j < 0) return; 868 | if (j !== i) { 869 | var ivs = buf.substring(i, j).split(','); 870 | set.ivs = { 871 | hp: ivs[0] === '' ? 31 : Number(ivs[0]) || 0, 872 | atk: ivs[1] === '' ? 31 : Number(ivs[1]) || 0, 873 | def: ivs[2] === '' ? 31 : Number(ivs[2]) || 0, 874 | spa: ivs[3] === '' ? 31 : Number(ivs[3]) || 0, 875 | spd: ivs[4] === '' ? 31 : Number(ivs[4]) || 0, 876 | spe: ivs[5] === '' ? 31 : Number(ivs[5]) || 0 877 | }; 878 | } 879 | i = j + 1; 880 | 881 | // shiny 882 | j = buf.indexOf('|', i); 883 | if (j < 0) return; 884 | if (i !== j) set.shiny = true; 885 | i = j + 1; 886 | 887 | // level 888 | j = buf.indexOf('|', i); 889 | if (j < 0) return; 890 | if (i !== j) set.level = parseInt(buf.substring(i, j), 10); 891 | i = j + 1; 892 | 893 | // happiness 894 | j = buf.indexOf(']', i); 895 | if (j < 0) { 896 | if (buf.substring(i)) { 897 | set.happiness = Number(buf.substring(i)); 898 | } 899 | break; 900 | } 901 | if (i !== j) set.happiness = Number(buf.substring(i, j)); 902 | i = j + 1; 903 | } 904 | 905 | return team; 906 | }; 907 | 908 | /** 909 | * Install our Tools functions into the battle object 910 | */ 911 | Tools.prototype.install = function (battle) { 912 | for (var i in this.data.Scripts) { 913 | battle[i] = this.data.Scripts[i]; 914 | } 915 | }; 916 | 917 | Tools.construct = function (mod, parentMod) { 918 | var tools = new Tools(mod, parentMod); 919 | // Scripts override Tools. 920 | var ret = Object.create(tools); 921 | tools.install(ret); 922 | if (ret.init) { 923 | if (parentMod && ret.init === moddedTools[parentMod].data.Scripts.init) { 924 | // don't inherit init 925 | delete ret.init; 926 | } else { 927 | ret.init(); 928 | } 929 | } 930 | return ret; 931 | }; 932 | 933 | moddedTools.base = Tools.construct(); 934 | 935 | // "gen6" is an alias for the current base data 936 | moddedTools.gen6 = moddedTools.base; 937 | 938 | Object.getPrototypeOf(moddedTools.base).moddedTools = moddedTools; 939 | 940 | return moddedTools.base; 941 | })(); 942 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | // Some Pokemon Showdown-specific JSON parsing rules 2 | module.exports.safeJSON = function(data) { 3 | if (data.length < 1) return; 4 | if (data[0] == ']') data = data.substr(1); 5 | return JSON.parse(data); 6 | } 7 | 8 | // Sanitizes a Room name 9 | module.exports.toRoomid = function(roomid) { 10 | return roomid.replace(/[^a-zA-Z0-9-]+/g, ''); 11 | } 12 | 13 | // Unsure exactly - sanitizes roomType? 14 | module.exports.toId = function(text) { 15 | text = text || ''; 16 | if (typeof text === 'number') text = ''+text; 17 | if (typeof text !== 'string') return toId(text && text.id); 18 | return text.toLowerCase().replace(/[^a-z0-9]+/g, ''); 19 | } 20 | -------------------------------------------------------------------------------- /weights.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Side conditions 3 | "p1_reflect": 64, 4 | "p2_reflect": -64, 5 | 6 | "p1_spikes": -150, 7 | "p2_spikes": 150, 8 | 9 | "p1_stealthrock": -200, 10 | "p2_stealthrock": 200, 11 | 12 | "p1_stickyweb": -40, 13 | "p2_stickyweb": 40, 14 | 15 | "p1_toxicspikes": -100, 16 | "p2_toxicspikes": 100, 17 | 18 | "p1_lightscreen": 64, 19 | "p2_lightscreen": -64, 20 | 21 | "p1_tailwind": 32, 22 | "p2_tailwind": -32, 23 | 24 | // Volatile Status Effects 25 | "p1_substitute": 150, 26 | "p2_substitute": -150, 27 | 28 | "p1_confusion": -32, 29 | "p2_confusion": 32, 30 | 31 | "p1_leechseed": -80, 32 | "p2_leechseed": 80, 33 | 34 | "p1_infestation": 0, 35 | "p2_infestation": 0, 36 | 37 | // Boosts 38 | "p1_atk": 50, 39 | "p2_atk": -50, 40 | 41 | "p1_def": 25, 42 | "p2_def": -25, 43 | 44 | "p1_spa": 50, 45 | "p2_spa": -50, 46 | 47 | "p1_spd": 25, 48 | "p2_spd": -25, 49 | 50 | "p1_spe": 0, 51 | "p2_spe": 0, 52 | 53 | "p1_accuracy": 10, 54 | "p2_accuracy": -10, 55 | 56 | "p1_evasion": 10, 57 | "p2_evasion": -10, 58 | 59 | "p1_hp": 1024, 60 | "p2_hp": -1024, 61 | 62 | "p1_alive": 512, //was 1024 //TODO: incremental p1_alive? so hp when a pokemon is at full health is worth more than when a pokemon is at partial health 63 | "p2_alive": -512, //was -1024 64 | 65 | "p1_fast_alive": 512, 66 | "p2_fast_alive": -512, 67 | 68 | // Status effects 69 | "p1_psn_count": -100, 70 | "p2_psn_count": 100, 71 | 72 | "p1_tox_count": -100, 73 | "p2_tox_count": 100, 74 | 75 | "p1_slp_count": -120, 76 | "p2_slp_count": 120, 77 | 78 | "p1_brn_count": -100, 79 | "p2_brn_count": 100, 80 | 81 | "p1_frz_count": -100, 82 | "p2_frz_count": 100, 83 | 84 | "p1_par_count": -100, 85 | "p2_par_count": 100, 86 | 87 | // Other features 88 | "items": 4, 89 | "faster": 50, 90 | "has_supereffective": 200, 91 | "has_stab": 100 92 | } 93 | --------------------------------------------------------------------------------