├── .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 | 
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 |
30 | Uploaded: {{game.date}} 31 |
32 |