├── .gitignore ├── README.md ├── app.json ├── battle-text.js ├── index.js ├── move-types.js ├── package.json ├── pkmntrainer.png ├── poke-api.js ├── state-machine.js └── supplementary_json ├── conf.js └── move_types.json /.gitignore: -------------------------------------------------------------------------------- 1 | dump.rdb 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slack-Pokemon 2 | 3 | This is a bot for having Pokemon battles within [Slack](https://slack.com/). It was originally built at Vox Media's product hackathon, [Vax](http://product.voxmedia.com/2014/7/3/5861220/vax-14-the-things-we-built). Read more about it [here](http://www.polygon.com/2014/6/27/5850720/pokemon-battle-slack-vox). 4 | 5 | Here's an example battle: 6 | 7 | Example Battle 8 | 9 | ## Setting up 10 | 11 | This is written in [Node.js.](http://nodejs.org) After installing Node, you also need to install [npm](https://npmjs.org) and [Redis.](http://redis.io/) 12 | 13 | ### Spinning up a server 14 | 15 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 16 | 17 | However, if you would like set up the server manually through [Heroku](https://www.heroku.com/), you can read the following articles: 18 | 19 | - [Node](https://devcenter.heroku.com/articles/getting-started-with-nodejs) 20 | - [Redis to Go](https://addons.heroku.com/redistogo) 21 | 22 | Please note that there is some RedisToGo/Heroku specific code in `state-machine.js`. Don't use that if you're using some other type of server. 23 | 24 | ### Running locally 25 | 26 | To run locally, start Redis in a new tab: 27 | 28 | ```Shell 29 | $ redis-server 30 | ``` 31 | 32 | and then start the node app: 33 | 34 | ```Shell 35 | $ npm start 36 | ``` 37 | 38 | This should build dependencies for you and run `index.js`. 39 | 40 | Your app should now be running on `localhost:5000`. 41 | 42 | To test locally, you'll have to send some POST commands to `localhost:5000/commands`. Here's a one-liner to test starting a battle: 43 | 44 | ```Shell 45 | curl -X POST -d '{"text":"pkmn battle me"}' -H 'Content-Type:application/json' "localhost:5000/commands" 46 | ``` 47 | 48 | To test other commands, change the text in the JSON object above. 49 | 50 | ### On Slack's end 51 | 52 | Set up an [Outgoing Webhook Integration](https://my.slack.com/services/new/outgoing-webhook) with the trigger word 'pkmn' that sends to the URL: `your-url.herokuapp.com/commands/` (or whatever your equivalent URL is if you're not using Heroku). You'll need admin access to your Slack Integration to do this. 53 | 54 | To get the bot's avatar to work, you need to set up a [Custom Emoji](https://my.slack.com/customize/emoji) with the name ':pkmntrainer:'. Use the included `pkmntrainer.png` image, or a custom one if you prefer. 55 | 56 | ## How to play 57 | 58 | List of commands: 59 | 60 | `pkmn battle me`: starts a battle. chooses a pokemon for the NPC. 61 | 62 | `pkmn i choose `: chooses a pokemon for the user. Replies with a list of usable moves. 63 | 64 | `pkmn use `: uses an attack. If the pokemon doesn't know that attack, it will respond with an error. You can type the attack with hyphens (hyper-beam) or with spaces (will o wisp). 65 | 66 | `pkmn end battle`: end the battle before someone wins. You can also use this to end battles someone else started but never finished. 67 | 68 | ## Features 69 | 70 | Currently the battle system is a tiny fraction of Pokemon's actual battle system. It supports: 71 | 72 | - one battle between a user and an NPC 73 | - one pokemon per player (of any currently existing pokemon from Bulbasaur to Zygarde. No Volcanion, Diancie, or Hoopa.) 74 | - moves with appropriate power and type effectiveness (moves can be Super Effective or Not Effective, etc.) 75 | 76 | It currently does not support: 77 | 78 | - taking stats into account when calculating damage (including accuracy and critical hits) 79 | - levels, or stats based on levels (including EVs and IVs) 80 | - ANY non-damaging moves 81 | - secondary effects of damaging moves (status, buffs/debuffs, multi-hits) 82 | - items and abilities 83 | - multiple concurrent battles 84 | - player vs player battles 85 | 86 | ### Developing New Features: PJScrape and Supplementary JSON 87 | 88 | If you wish to develop new features for this, you will likely run into a situation in which PokeAPI's data isn't sufficient. This happened to me with move types. I ended up scraping the data with [PJScrape](http://nrabinowitz.github.io/pjscrape/) from an external website. The folder `supplemental_json` contains both the scraped data in JSON format as well as the config file passed to PJScrape in order to generate the data. 89 | 90 | If you end up needing to scrape a page for supplemental data, please take a look at that folder. 91 | 92 | ## Contact 93 | 94 | Feel free to message me on Twitter, [@RobertVinluan](http://twitter.com/robertvinluan). For now I'm making small updates but not adding features. If you would like to add a feature please submit a pull request. I promise I'll look at it (and probably approve it)! 95 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slack Pokemon", 3 | "description": "A bot for having Pokemon battles in Slack.", 4 | "repository": "https://github.com/rvinluan/slack-pokemon", 5 | "keywords": ["node", "express", "static", "pokemon", "battle", "slack"], 6 | "addons": [ "redistogo" ] 7 | } 8 | -------------------------------------------------------------------------------- /battle-text.js: -------------------------------------------------------------------------------- 1 | var pokeapi = require('./poke-api.js'), 2 | stateMachine = require('./state-machine.js'), 3 | moves = require('./move-types.js'), 4 | Q = require('q'); 5 | 6 | //+ Jonas Raoni Soares Silva 7 | //@ http://jsfromhell.com/array/shuffle [v1.0] 8 | function shuffle(o){ //v1.0 9 | for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); 10 | return o; 11 | }; 12 | 13 | module.exports = {} 14 | 15 | /* 16 | * Return a text string when the command doesn't match defined commands. 17 | */ 18 | module.exports.unrecognizedCommand = function(commandsArray) { 19 | var textString = "I don't recognize the command _{cmd}_ ."; 20 | //get rid of the 'pkmn' 21 | commandsArray.shift(); 22 | textString = textString.replace("{cmd}", commandsArray.join(" ")); 23 | return Q.fcall(function(){ return textString; }); 24 | } 25 | 26 | /* 27 | * Return a text string when the user chooses a Pokemon. 28 | * Fetch the pokemon from the API, choose 4 random moves, write them to REDIS, 29 | * and then return a message stating the pokemon, its HP, and its moves. 30 | */ 31 | module.exports.userChoosePokemon = function(commandsArray) { 32 | var commandString = commandsArray.join(" "), 33 | pokemonName = commandsArray[3], 34 | textString = "You chose {pkmnn}. It has {hp} HP, and knows ", 35 | moves = [], 36 | movePromises = []; 37 | //validate that the command was "pkmn i choose {pokemon}" 38 | if(!commandString.match(/i choose/i)) { 39 | return module.exports.unrecognizedCommand(commandsArray); 40 | } 41 | return pokeapi.getPokemon(pokemonName).then(function(pkmndata){ 42 | moves = shuffle(pkmndata.moves); 43 | for(var i = 0; i < 4; i++) { 44 | movePromises.push( 45 | pokeapi.getMove("http://pokeapi.co"+moves[i].resource_uri) 46 | .then(stateMachine.addMove) 47 | ) 48 | //format: "vine whip, leer, solar beam, and tackle." 49 | if(i < 3) { 50 | textString += moves[i].name; 51 | textString += ", "; 52 | } else { 53 | textString += "and "; 54 | textString += moves[i].name; 55 | textString += "."; 56 | } 57 | } 58 | return Q.allSettled(movePromises) 59 | .then(function(){ 60 | return stateMachine.setUserHP(pkmndata.hp); 61 | }) 62 | .then(function(){ 63 | return stateMachine.setUserPkmnTypes(pkmndata.types.map(function(val){ 64 | return val.name; 65 | })); 66 | }) 67 | .then(function(){ 68 | textString = textString.replace("{pkmnn}", pkmndata.name); 69 | textString = textString.replace("{hp}", pkmndata.hp); 70 | var stringy = "" + pkmndata.pkdx_id; 71 | if (stringy.length == 1) { 72 | stringy = "00" + stringy; 73 | } else if (stringy.length == 2) { 74 | stringy = "0" + stringy; 75 | } 76 | return { 77 | text: textString, 78 | spriteUrl: "http://sprites.pokecheck.org/i/"+stringy+".gif" 79 | } 80 | }); 81 | }); 82 | 83 | } 84 | 85 | /* 86 | * Return a text string when the NPC chooses a Pokemon. 87 | * Fetch the pokemon from the API, choose 4 random moves, write them to REDIS, 88 | * and then return a message stating the pokemon. 89 | */ 90 | module.exports.npcChoosePokemon = function(dex_no) { 91 | var textString = "I Choose {pkmnn}!", 92 | moves = [], 93 | movePromises = []; 94 | return pokeapi.getPokemon(dex_no).then(function(pkmnData){ 95 | moves = shuffle(pkmnData.moves); 96 | for(var i = 0; i < 4; i++) { 97 | movePromises.push( 98 | pokeapi.getMove("http://pokeapi.co"+moves[i].resource_uri) 99 | .then(stateMachine.addMoveNPC) 100 | ) 101 | } 102 | return Q.allSettled(movePromises) 103 | .then(function(){ 104 | return stateMachine.setNpcHP(pkmnData.hp); 105 | }) 106 | .then(function(){ 107 | return stateMachine.setNpcPkmnTypes(pkmnData.types.map(function(val){ 108 | return val.name; 109 | })); 110 | }) 111 | .then(function(){ 112 | textString = textString.replace("{pkmnn}", pkmnData.name); 113 | var stringy = "" + pkmnData.pkdx_id; 114 | if (stringy.length == 1) { 115 | stringy = "00" + stringy; 116 | } else if (stringy.length == 2) { 117 | stringy = "0" + stringy; 118 | } 119 | return { 120 | text: textString, 121 | spriteUrl: "http://sprites.pokecheck.org/i/"+stringy+".gif" 122 | } 123 | }); 124 | }); 125 | } 126 | 127 | /* 128 | * Return a text string when the battle starts. 129 | * Stores the player's Slack username and what channel the battle is in, 130 | * and chooses a Pokemon for the NPC from the original 151. 131 | */ 132 | module.exports.startBattle = function(slackData) { 133 | var textString = "OK {name}, I'll battle you! ".replace("{name}", slackData.user_name), 134 | dex_no = Math.ceil(Math.random() * 151); 135 | return stateMachine.newBattle(slackData.user_name, slackData.channel_name) 136 | .then(function() { 137 | return module.exports.npcChoosePokemon(dex_no); 138 | }) 139 | .then(function(pkmnChoice){ 140 | return { 141 | text: textString + '\n' + pkmnChoice.text, 142 | spriteUrl: pkmnChoice.spriteUrl 143 | } 144 | }) 145 | } 146 | 147 | module.exports.endBattle = function() { 148 | return stateMachine.endBattle(); 149 | } 150 | 151 | var effectivenessMessage = function(mult) { 152 | switch(mult) { 153 | case 0: 154 | return "It doesn't have an effect. "; 155 | break; 156 | case 0.5: 157 | case 0.25: 158 | return "It's not very effective... "; 159 | break; 160 | case 1: 161 | return " "; 162 | break; 163 | case 2: 164 | case 4: 165 | return "It's super effective! "; 166 | break; 167 | default: 168 | return " "; 169 | break; 170 | } 171 | } 172 | 173 | /* 174 | * Helper function for using one of the user's pokemon's move. 175 | * First check to see if the move is among the allowed moves, 176 | * calculate the type effectiveness, calculate the damage, 177 | * and then return a message. 178 | */ 179 | var useMoveUser = function(moveName) { 180 | var textString = "You used {mvname}! {effctv}", 181 | textStringDmg = "It did {dmg} damage, leaving me with {hp}HP!"; 182 | return stateMachine.getUserAllowedMoves() 183 | .then(function(moves){ 184 | if(moves.indexOf(moveName) !== -1) { 185 | return stateMachine.getSingleMove(moveName); 186 | } else { 187 | throw new Error("Your pokemon doesn't know that move."); 188 | } 189 | }) 190 | .then(function(moveData){ 191 | textString = textString.replace("{mvname}", moveName); 192 | return stateMachine.getNpcPkmnTypes() 193 | .then(function(types){ 194 | return pokeapi.getAttackMultiplier(moveData.type, types[0], types[1]) 195 | .then(function(multiplier){ 196 | //do damage 197 | var totalDamage = Math.ceil( (moveData.power / 5) * multiplier ) 198 | return stateMachine.doDamageToNpc(totalDamage) 199 | .then(function(hpRemaining){ 200 | if(parseInt(hpRemaining, 10) <= 0) { 201 | return stateMachine.endBattle() 202 | .then(function(){ 203 | return "You Beat Me!"; 204 | }) 205 | } 206 | textString = textString.replace("{effctv}", effectivenessMessage(multiplier)); 207 | textStringDmg = textStringDmg.replace("{dmg}", totalDamage); 208 | textStringDmg = textStringDmg.replace("{hp}", hpRemaining); 209 | if(multiplier == 0) 210 | return textString; 211 | return textString + textStringDmg; 212 | }) 213 | }); 214 | }) 215 | }) 216 | } 217 | 218 | /* 219 | * Helper function for using one of the NPC's pokemon's move. 220 | * First check to see if the move is among the allowed moves, 221 | * calculate the type effectiveness, calculate the damage, 222 | * and then return a message. 223 | */ 224 | var useMoveNpc = function() { 225 | var textString = "I used {mvname}! {effctv}", 226 | textStringDmg = "It did {dmg} damage, leaving you with {hp}HP!", 227 | randMove = Math.floor(Math.random() * 4); 228 | return stateMachine.getNpcAllowedMoves() 229 | .then(function(moves){ 230 | textString = textString.replace("{mvname}", moves[randMove]); 231 | return stateMachine.getSingleMove(moves[randMove]); 232 | }) 233 | .then(function(moveData){ 234 | return stateMachine.getUserPkmnTypes() 235 | .then(function(types){ 236 | return pokeapi.getAttackMultiplier(moveData.type, types[0], types[1]) 237 | .then(function(multiplier){ 238 | //do damage 239 | var totalDamage = Math.ceil( (moveData.power / 5) * multiplier ) 240 | return stateMachine.doDamageToUser(totalDamage) 241 | .then(function(hpRemaining){ 242 | if(parseInt(hpRemaining, 10) <= 0) { 243 | return stateMachine.endBattle() 244 | .then(function(){ 245 | return "You Lost!"; 246 | }) 247 | } 248 | textString = textString.replace("{effctv}", effectivenessMessage(multiplier)); 249 | textStringDmg = textStringDmg.replace("{dmg}", totalDamage); 250 | textStringDmg = textStringDmg.replace("{hp}", hpRemaining); 251 | if(multiplier == 0) 252 | return textString; 253 | return textString + textStringDmg; 254 | }) 255 | }); 256 | }) 257 | }) 258 | } 259 | 260 | /* 261 | * When the user uses a move, calculate the results of the user's turn, 262 | * then the NPC's turn. If either one of them ends the battle, don't show 263 | * the other result. 264 | */ 265 | module.exports.useMove = function(moveName) { 266 | return Q.all([useMoveNpc(), useMoveUser(moveName)]) 267 | .then(function(results){ 268 | if(results[1] === "You Beat Me!") { 269 | return results[1]; 270 | } else if (results[0] === "You Lost!") { 271 | return results[0]; 272 | } else { 273 | return results[1] + "\n" + results[0]; 274 | } 275 | }) 276 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | bodyParser = require('body-parser'), 3 | //Communicate with the PokeAPI 4 | pokeapi = require('./poke-api.js'), 5 | //Generate messages for different situations 6 | battleText = require('./battle-text.js'), 7 | //Communicate with Redis 8 | stateMachine = require('./state-machine.js'), 9 | app = express(); 10 | 11 | app.set('port', (process.env.PORT || 5000)); 12 | app.use(express.static(__dirname + '/public')); 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({extended: true})); 15 | 16 | app.get('/', function(request, response) { 17 | response.send('Hello There!') 18 | }); 19 | 20 | /* 21 | * This is the main function that recieves post commands from Slack. 22 | * They come in this format: 23 | * { 24 | * "text": "pkmn battle me", 25 | * "user": "rvinluan", 26 | * "channel": "#pkmn_battles" 27 | * } 28 | * There's more stuff but that's all we care about. 29 | * All error handling is bubbled up to this function and handled here. 30 | * It doesn't distinguish between different types of errors, but it probably should. 31 | */ 32 | app.post('/commands', function(request, response){ 33 | var commands = request.body.text.toLowerCase().split(" "); 34 | 35 | if(matchCommands(commands, "CHOOSE")) { 36 | battleText.userChoosePokemon(commands) 37 | .then( 38 | function(chosenObject){ 39 | response.send(buildResponse(chosenObject.text + '\n' + chosenObject.spriteUrl)); 40 | }, 41 | function(err){ 42 | console.log(err); 43 | response.send(buildResponse("I don't think that's a real Pokemon. "+err)); 44 | } 45 | ) 46 | } 47 | else if(matchCommands(commands, "ATTACK")) { 48 | var moveName; 49 | if(commands[3]) { 50 | //for moves that are 2+ words, like 'Flare Blitz' or 'Will O Wisp' 51 | moveName = commands.slice(2).join('-'); 52 | } else { 53 | moveName = commands[2]; 54 | } 55 | battleText.useMove(moveName.toLowerCase()) 56 | .then( 57 | function(textString){ 58 | response.end(buildResponse(textString)); 59 | }, 60 | function(err){ 61 | console.log(err); 62 | response.send(buildResponse("You can't use that move. "+err)) 63 | } 64 | ) 65 | } 66 | else if(matchCommands(commands, "START")) { 67 | //send in the whole request.body because it needs the Slack username and channel 68 | battleText.startBattle(request.body) 69 | .then( 70 | function(startObj){ 71 | response.send(buildResponse(startObj.text + "\n" + startObj.spriteUrl)) 72 | }, 73 | function(err) { 74 | console.log(err); 75 | response.send(buildResponse("Something went wrong. "+err)); 76 | } 77 | ) 78 | } 79 | else if(matchCommands(commands, "END")) { 80 | battleText.endBattle() 81 | .then( 82 | function(){ 83 | response.send(buildResponse("Battle Over.")) 84 | }, 85 | function(err){ 86 | console.log(err); 87 | response.send(buildResponse("Couldn't end the battle. "+err)) 88 | } 89 | ) 90 | } 91 | else { 92 | battleText.unrecognizedCommand(commands) 93 | .then(function(text){ 94 | response.send(buildResponse(text)); 95 | }); 96 | } 97 | }) 98 | 99 | app.listen(app.get('port'), function() { 100 | console.log("Node app is running at localhost:" + app.get('port')) 101 | }) 102 | 103 | //utility functions 104 | 105 | /* 106 | * Helper function to build the JSON to send back to Slack. 107 | * Make sure to make a custom emoji in your Slack integration named :pkmntrainer: 108 | * with the included pkmntrainer jpeg, otherwise the profile picture won't work. 109 | */ 110 | function buildResponse(text) { 111 | var json = { 112 | "text": text, 113 | "username": "Pokemon Trainer", 114 | "icon_emoji": ":pkmntrainer:" 115 | } 116 | return JSON.stringify(json); 117 | } 118 | 119 | /* 120 | * Helper function to match commands, instead of a switch statement, 121 | * because then you can do stuff like use Regex here or something fancier. 122 | * Also keeps all the possible commands and their trigger words in one place. 123 | */ 124 | function matchCommands(commandArray, command) { 125 | var commandsDict = { 126 | "CHOOSE": "i choose", 127 | "ATTACK": "use", 128 | "START": "battle me", 129 | "END": "end battle" 130 | } 131 | var cmdString = commandArray.join(" ").toLowerCase().replace("pkmn ", ""); 132 | return cmdString.indexOf(commandsDict[command]) === 0; 133 | } -------------------------------------------------------------------------------- /move-types.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | /* 4 | * This is a SYNCHRONOUS function to get the type of a move from an external file, 5 | * because PokeAPI doesn't have that data. 6 | * I had to scrape it from somewhere and put it into this JSON. 7 | * TODO: make this use promises. 8 | */ 9 | module.exports.getMoveType = function(moveName) { 10 | moveName = moveName.replace("-", " "); 11 | var data = fs.readFileSync("./supplementary_json/move_types.json"); 12 | data = JSON.parse(data); 13 | data = data.filter(function(val, index, arr){ 14 | return Object.keys(val)[0] == moveName; 15 | }); 16 | if(data.length > 0) { 17 | return data[0][moveName]; 18 | } else { 19 | //currently this fails silently; 20 | //once this uses Promises it'll be able to throw an error 21 | return " "; 22 | } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-js-sample", 3 | "version": "0.1.0", 4 | "description": "A sample Node.js app using Express 4", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "express": "^4.0.0", 11 | "body-parser": "*", 12 | "request": "*", 13 | "redis": ">= 0.10.3", 14 | "q": "^1.0.1" 15 | }, 16 | "engines": { 17 | "node": "0.10.x" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/heroku/node-js-sample" 22 | }, 23 | "keywords": [ 24 | "node", 25 | "heroku", 26 | "express" 27 | ], 28 | "author": "Mark Pundsack", 29 | "contributors": [ 30 | "Zeke Sikelianos (http://zeke.sikelianos.com)" 31 | ], 32 | "license": "MIT", 33 | "devDependencies": { 34 | "q": "^1.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkmntrainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rvinluan/slack-pokemon/12b93aa98cd0a86919ccbb9d5f61e09ce5869444/pkmntrainer.png -------------------------------------------------------------------------------- /poke-api.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | Q = require('q'); 3 | 4 | module.exports = {} 5 | 6 | module.exports.getPokemon = function(name) { 7 | var deferred = Q.defer(); 8 | request("http://pokeapi.co/api/v1/pokemon/"+name, function (error, response, body) { 9 | if (response.statusCode == 200) { 10 | deferred.resolve(JSON.parse(body)); 11 | } else { 12 | deferred.reject(new Error("Error Getting Pokemon")); 13 | } 14 | }) 15 | return deferred.promise; 16 | } 17 | 18 | module.exports.getSprite = function(url) { 19 | var deferred = Q.defer(); 20 | request(url, function (error, response, body) { 21 | if (response.statusCode == 200) { 22 | deferred.resolve(JSON.parse(body)); 23 | } else { 24 | deferred.reject(new Error("Error Getting Sprite")); 25 | } 26 | }) 27 | return deferred.promise; 28 | } 29 | 30 | module.exports.getMove = function(url) { 31 | var deferred = Q.defer(); 32 | request(url, function (error, response, body) { 33 | if (response.statusCode == 200) { 34 | deferred.resolve(JSON.parse(body)); 35 | } else { 36 | deferred.reject(new Error("Error Getting Move")); 37 | } 38 | }) 39 | return deferred.promise; 40 | } 41 | 42 | /* 43 | * Calculates the effectiveness of one move on a pokemon with 1 or 2 types. 44 | * When accessing a move from the API, it will return with three arrays that look like this: 45 | * "super_effective": [{name:"fairy", resource_uri:"/api/v1/type/18"} ... ] 46 | * We only care about the name, so we map these arrays to something like: 47 | * supereffective = ["fairy", "ice", ...] 48 | * then we can go through and calculate the damage multiplier based on the three arrays. 49 | */ 50 | module.exports.getAttackMultiplier = function(offensive, defensive1, defensive2) { 51 | var multiplier = 1, 52 | typesArray = [ 53 | "normal", //1 54 | "fighting", 55 | "flying", 56 | "poison", 57 | "ground", 58 | "rock", 59 | "bug", 60 | "ghost", 61 | "steel", 62 | "fire", 63 | "water", 64 | "grass", 65 | "electric", 66 | "psychic", 67 | "ice", 68 | "dragon", 69 | "dark", 70 | "fairy" //18 71 | ], 72 | typeID = typesArray.indexOf(offensive.toLowerCase()) + 1, 73 | deferred = Q.defer(); 74 | request("http://pokeapi.co/api/v1/type/"+typeID, function(error, response, body){ 75 | if(response.statusCode == 200) { 76 | var d = JSON.parse(body), 77 | ineffective = d.ineffective.map(function(val){return val.name}), 78 | noeffect = d.no_effect.map(function(val){return val.name}), 79 | supereffective = d.super_effective.map(function(val){return val.name}); 80 | [defensive1, defensive2].forEach(function(type){ 81 | if(ineffective.indexOf(type) !== -1) { multiplier *= 0.5; } 82 | if(noeffect.indexOf(type) !== -1) { multiplier *= 0; } 83 | if(supereffective.indexOf(type) !== -1) { multiplier *= 2; } 84 | }); 85 | deferred.resolve(multiplier); 86 | } else { 87 | deferred.reject(new Error("Error accessing API while getting type.")); 88 | } 89 | }) 90 | 91 | return deferred.promise; 92 | } -------------------------------------------------------------------------------- /state-machine.js: -------------------------------------------------------------------------------- 1 | var moves = require('./move-types.js'), 2 | Q = require('q'); 3 | 4 | var redis, 5 | rtg; 6 | 7 | /* For using RedisToGo on Heroku. If you're not using RedisToGo or Heroku, 8 | * feel free to remove this part and just use 9 | * redis = require("redis").createClient(); 10 | */ 11 | if(process.env.REDISTOGO_URL) { 12 | rtg = require("url").parse(process.env.REDISTOGO_URL); 13 | redis = require("redis").createClient(rtg.port, rtg.hostname); 14 | 15 | redis.auth(rtg.auth.split(":")[1]); 16 | } else { 17 | //then we're running locally 18 | redis = require("redis").createClient(); 19 | } 20 | 21 | /* Turn Redis Methods Into Promise-returning Ones */ 22 | 23 | QRedis = {}; 24 | 25 | QRedis.sadd = Q.nbind(redis.sadd, redis); 26 | QRedis.hmset = Q.nbind(redis.hmset, redis); 27 | QRedis.hgetall = Q.nbind(redis.hgetall, redis); 28 | QRedis.exists = Q.nbind(redis.exists, redis); 29 | QRedis.del = Q.nbind(redis.del, redis); 30 | QRedis.set = Q.nbind(redis.set, redis); 31 | QRedis.get = Q.nbind(redis.get, redis); 32 | QRedis.decrby = Q.nbind(redis.decrby, redis); 33 | QRedis.smembers = Q.nbind(redis.smembers, redis); 34 | 35 | module.exports = {}; 36 | 37 | module.exports.newBattle = function(playerName, channel) { 38 | return QRedis.exists("currentBattle") 39 | .then(function(exists){ 40 | if(!exists) { 41 | return QRedis.hmset("currentBattle", { 42 | "playerName": playerName, 43 | "channel": channel 44 | }) 45 | } else { 46 | throw new Error("Battle exists"); 47 | } 48 | }) 49 | } 50 | 51 | module.exports.getBattle = function() { 52 | return QRedis.hgetall("currentBattle"); 53 | } 54 | 55 | module.exports.endBattle = function() { 56 | return QRedis.del([ 57 | "currentBattle", 58 | "user:allowedMoves", 59 | "npc:allowedMoves", 60 | "npc:hp", 61 | "user:hp", 62 | "user:pkmnTypes", 63 | "npc:pkmnTypes" 64 | ]) 65 | } 66 | 67 | module.exports.addMove = function(data) { 68 | return QRedis.sadd("user:allowedMoves", data.name.toLowerCase()) 69 | .then(function(addReturned){ 70 | return QRedis.hmset("move:"+data.name.toLowerCase(),{ 71 | "power": data.power, 72 | "type": moves.getMoveType(data.name.toLowerCase()) 73 | }) 74 | }); 75 | } 76 | 77 | module.exports.addMoveNPC = function(data) { 78 | return QRedis.sadd("npc:allowedMoves", data.name.toLowerCase()) 79 | .then(function(addReturned){ 80 | return QRedis.hmset("move:"+data.name.toLowerCase(),{ 81 | "power": data.power, 82 | "type": moves.getMoveType(data.name.toLowerCase()) 83 | }) 84 | }); 85 | } 86 | 87 | module.exports.setUserPkmnTypes = function(typesArray) { 88 | //TODO: use apply (or Q's version of it) 89 | if(typesArray[1]) { 90 | return QRedis.sadd("user:pkmnTypes", typesArray[0], typesArray[1]); 91 | } else { 92 | return QRedis.sadd("user:pkmnTypes", typesArray[0]); 93 | } 94 | } 95 | 96 | module.exports.setNpcPkmnTypes = function(typesArray) { 97 | //TODO: use apply (or Q's version of it) 98 | if(typesArray[1]) { 99 | return QRedis.sadd("npc:pkmnTypes", typesArray[0], typesArray[1]); 100 | } else { 101 | return QRedis.sadd("npc:pkmnTypes", typesArray[0]); 102 | } 103 | } 104 | 105 | module.exports.getUserPkmnTypes = function() { 106 | return QRedis.smembers("user:pkmnTypes"); 107 | } 108 | 109 | module.exports.getNpcPkmnTypes = function() { 110 | return QRedis.smembers("npc:pkmnTypes"); 111 | } 112 | 113 | module.exports.getUserAllowedMoves = function() { 114 | return QRedis.smembers("user:allowedMoves"); 115 | } 116 | module.exports.getNpcAllowedMoves = function() { 117 | return QRedis.smembers("npc:allowedMoves"); 118 | } 119 | 120 | module.exports.getSingleMove = function(moveName) { 121 | return QRedis.hgetall("move:"+moveName.toLowerCase()); 122 | } 123 | 124 | module.exports.setNpcHP = function(hp) { 125 | return QRedis.set("npc:hp", hp); 126 | } 127 | module.exports.getNpcHP = function() { 128 | return QRedis.get("npc:hp"); 129 | } 130 | 131 | module.exports.setUserHP = function(hp) { 132 | return QRedis.set("user:hp", hp); 133 | } 134 | module.exports.getUserHP = function() { 135 | return QRedis.get("user:hp"); 136 | } 137 | module.exports.doDamageToUser = function(damage) { 138 | return QRedis.decrby("user:hp", damage); 139 | } 140 | module.exports.doDamageToNpc = function(damage) { 141 | return QRedis.decrby("npc:hp", damage); 142 | } -------------------------------------------------------------------------------- /supplementary_json/conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a PJScrape config file. 3 | * more on PJScrape here: http://nrabinowitz.github.io/pjscrape/ 4 | * I use this to scrape supplementary information from websites that PokeAPI doesn't have. 5 | * Currently it's just move types but it can be used for more things, 6 | * like whether moves are physical or special. 7 | */ 8 | 9 | pjs.config({ 10 | log: 'stdout', 11 | format: 'json', 12 | writer: 'file', 13 | outFile: 'move_types.json' 14 | }); 15 | 16 | pjs.addSuite({ 17 | url: 'http://veekun.com/dex/moves/search?sort=name&introduced_in=1&introduced_in=2&introduced_in=3&introduced_in=4&introduced_in=5&introduced_in=6', 18 | scraper: function() { 19 | return $('.dex-pokemon-moves tr').map(function(index, elem){ 20 | var ro = {} 21 | var moveName = $(elem).find("td:first-child a").text().toLowerCase(); 22 | var moveType = $(elem).find("td:nth-child(2) img").attr('title'); 23 | ro[moveName] = moveType; 24 | return ro; 25 | }).toArray(); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /supplementary_json/move_types.json: -------------------------------------------------------------------------------- 1 | [{"":"Sort"},{},{"absorb":"Grass"},{"acid":"Poison"},{"acid armor":"Poison"},{"acid spray":"Poison"},{"acrobatics":"Flying"},{"acupressure":"Normal"},{"aerial ace":"Flying"},{"aeroblast":"Flying"},{"after you":"Normal"},{"agility":"Psychic"},{"air cutter":"Flying"},{"air slash":"Flying"},{"ally switch":"Psychic"},{"amnesia":"Psychic"},{"ancient power":"Rock"},{"aqua jet":"Water"},{"aqua ring":"Water"},{"aqua tail":"Water"},{"arm thrust":"Fighting"},{"aromatherapy":"Grass"},{"aromatic mist":"Fairy"},{"assist":"Normal"},{"assurance":"Dark"},{"astonish":"Ghost"},{"attack order":"Bug"},{"attract":"Normal"},{"aura sphere":"Fighting"},{"aurora beam":"Ice"},{"autotomize":"Steel"},{"avalanche":"Ice"},{"baby-doll eyes":"Fairy"},{"barrage":"Normal"},{"barrier":"Psychic"},{"baton pass":"Normal"},{"beat up":"Dark"},{"belch":"Poison"},{"belly drum":"Normal"},{"bestow":"Normal"},{"bide":"Normal"},{"bind":"Normal"},{"bite":"Dark"},{"blast burn":"Fire"},{"blaze kick":"Fire"},{"blizzard":"Ice"},{"block":"Normal"},{"blue flare":"Fire"},{"body slam":"Normal"},{"bolt strike":"Electric"},{"bone club":"Ground"},{"bonemerang":"Ground"},{"bone rush":"Ground"},{"boomburst":"Normal"},{"bounce":"Flying"},{"brave bird":"Flying"},{"brick break":"Fighting"},{"brine":"Water"},{"bubble":"Water"},{"bubble beam":"Water"},{"bug bite":"Bug"},{"bug buzz":"Bug"},{"bulk up":"Fighting"},{"bulldoze":"Ground"},{"bullet punch":"Steel"},{"bullet seed":"Grass"},{"calm mind":"Psychic"},{"camouflage":"Normal"},{"captivate":"Normal"},{"celebrate":"Normal"},{"charge":"Electric"},{"charge beam":"Electric"},{"charm":"Fairy"},{"chatter":"Flying"},{"chip away":"Normal"},{"circle throw":"Fighting"},{"clamp":"Water"},{"clear smog":"Poison"},{"close combat":"Fighting"},{"coil":"Poison"},{"comet punch":"Normal"},{"confide":"Normal"},{"confuse ray":"Ghost"},{"confusion":"Psychic"},{"constrict":"Normal"},{"conversion":"Normal"},{"conversion 2":"Normal"},{"copycat":"Normal"},{"cosmic power":"Psychic"},{"cotton guard":"Grass"},{"cotton spore":"Grass"},{"counter":"Fighting"},{"covet":"Normal"},{"crabhammer":"Water"},{"crafty shield":"Fairy"},{"cross chop":"Fighting"},{"cross poison":"Poison"},{"crunch":"Dark"},{"crush claw":"Normal"},{"crush grip":"Normal"},{"curse":"Ghost"},{"cut":"Normal"},{"dark pulse":"Dark"},{"dark void":"Dark"},{"dazzling gleam":"Fairy"},{"defend order":"Bug"},{"defense curl":"Normal"},{"defog":"Flying"},{"destiny bond":"Ghost"},{"detect":"Fighting"},{"diamond storm":"Rock"},{"dig":"Ground"},{"disable":"Normal"},{"disarming voice":"Fairy"},{"discharge":"Electric"},{"dive":"Water"},{"dizzy punch":"Normal"},{"doom desire":"Steel"},{"double-edge":"Normal"},{"double hit":"Normal"},{"double kick":"Fighting"},{"double slap":"Normal"},{"double team":"Normal"},{"draco meteor":"Dragon"},{"dragon breath":"Dragon"},{"dragon claw":"Dragon"},{"dragon dance":"Dragon"},{"dragon pulse":"Dragon"},{"dragon rage":"Dragon"},{"dragon rush":"Dragon"},{"dragon tail":"Dragon"},{"draining kiss":"Fairy"},{"drain punch":"Fighting"},{"dream eater":"Psychic"},{"drill peck":"Flying"},{"drill run":"Ground"},{"dual chop":"Dragon"},{"dynamic punch":"Fighting"},{"earth power":"Ground"},{"earthquake":"Ground"},{"echoed voice":"Normal"},{"eerie impulse":"Electric"},{"egg bomb":"Normal"},{"electric terrain":"Electric"},{"electrify":"Electric"},{"electro ball":"Electric"},{"electroweb":"Electric"},{"embargo":"Dark"},{"ember":"Fire"},{"encore":"Normal"},{"endeavor":"Normal"},{"endure":"Normal"},{"energy ball":"Grass"},{"entrainment":"Normal"},{"eruption":"Fire"},{"explosion":"Normal"},{"extrasensory":"Psychic"},{"extreme speed":"Normal"},{"facade":"Normal"},{"fairy lock":"Fairy"},{"fairy wind":"Fairy"},{"fake out":"Normal"},{"fake tears":"Dark"},{"false swipe":"Normal"},{"feather dance":"Flying"},{"feint":"Normal"},{"feint attack":"Dark"},{"fell stinger":"Bug"},{"fiery dance":"Fire"},{"final gambit":"Fighting"},{"fire blast":"Fire"},{"fire fang":"Fire"},{"fire pledge":"Fire"},{"fire punch":"Fire"},{"fire spin":"Fire"},{"fissure":"Ground"},{"flail":"Normal"},{"flame burst":"Fire"},{"flame charge":"Fire"},{"flamethrower":"Fire"},{"flame wheel":"Fire"},{"flare blitz":"Fire"},{"flash":"Normal"},{"flash cannon":"Steel"},{"flatter":"Dark"},{"fling":"Dark"},{"flower shield":"Fairy"},{"fly":"Flying"},{"flying press":"Fighting"},{"focus blast":"Fighting"},{"focus energy":"Normal"},{"focus punch":"Fighting"},{"follow me":"Normal"},{"force palm":"Fighting"},{"foresight":"Normal"},{"forest's curse":"Grass"},{"foul play":"Dark"},{"freeze-dry":"Ice"},{"freeze shock":"Ice"},{"frenzy plant":"Grass"},{"frost breath":"Ice"},{"frustration":"Normal"},{"fury attack":"Normal"},{"fury cutter":"Bug"},{"fury swipes":"Normal"},{"fusion bolt":"Electric"},{"fusion flare":"Fire"},{"future sight":"Psychic"},{"gastro acid":"Poison"},{"gear grind":"Steel"},{"geomancy":"Fairy"},{"giga drain":"Grass"},{"giga impact":"Normal"},{"glaciate":"Ice"},{"glare":"Normal"},{"grass knot":"Grass"},{"grass pledge":"Grass"},{"grass whistle":"Grass"},{"grassy terrain":"Grass"},{"gravity":"Psychic"},{"growl":"Normal"},{"growth":"Normal"},{"grudge":"Ghost"},{"guard split":"Psychic"},{"guard swap":"Psychic"},{"guillotine":"Normal"},{"gunk shot":"Poison"},{"gust":"Flying"},{"gyro ball":"Steel"},{"hail":"Ice"},{"hammer arm":"Fighting"},{"happy hour":"Normal"},{"harden":"Normal"},{"haze":"Ice"},{"headbutt":"Normal"},{"head charge":"Normal"},{"head smash":"Rock"},{"heal bell":"Normal"},{"heal block":"Psychic"},{"healing wish":"Psychic"},{"heal order":"Bug"},{"heal pulse":"Psychic"},{"heart stamp":"Psychic"},{"heart swap":"Psychic"},{"heat crash":"Fire"},{"heat wave":"Fire"},{"heavy slam":"Steel"},{"helping hand":"Normal"},{"hex":"Ghost"},{"hidden power":"Normal"},{"high jump kick":"Fighting"},{"hold back":"Normal"},{"hone claws":"Dark"},{"horn attack":"Normal"},{"horn drill":"Normal"},{"horn leech":"Grass"},{"howl":"Normal"},{"hurricane":"Flying"},{"hydro cannon":"Water"},{"hydro pump":"Water"},{"hyper beam":"Normal"},{"hyper fang":"Normal"},{"hyper voice":"Normal"},{"hypnosis":"Psychic"},{"ice ball":"Ice"},{"ice beam":"Ice"},{"ice burn":"Ice"},{"ice fang":"Ice"},{"ice punch":"Ice"},{"ice shard":"Ice"},{"icicle crash":"Ice"},{"icicle spear":"Ice"},{"icy wind":"Ice"},{"imprison":"Psychic"},{"incinerate":"Fire"},{"inferno":"Fire"},{"infestation":"Bug"},{"ingrain":"Grass"},{"ion deluge":"Electric"},{"iron defense":"Steel"},{"iron head":"Steel"},{"iron tail":"Steel"},{"judgment":"Normal"},{"jump kick":"Fighting"},{"karate chop":"Fighting"},{"kinesis":"Psychic"},{"king's shield":"Steel"},{"knock off":"Dark"},{"land's wrath":"Ground"},{"last resort":"Normal"},{"lava plume":"Fire"},{"leaf blade":"Grass"},{"leaf storm":"Grass"},{"leaf tornado":"Grass"},{"leech life":"Bug"},{"leech seed":"Grass"},{"leer":"Normal"},{"lick":"Ghost"},{"light screen":"Psychic"},{"lock-on":"Normal"},{"lovely kiss":"Normal"},{"low kick":"Fighting"},{"low sweep":"Fighting"},{"lucky chant":"Normal"},{"lunar dance":"Psychic"},{"luster purge":"Psychic"},{"mach punch":"Fighting"},{"magical leaf":"Grass"},{"magic coat":"Psychic"},{"magic room":"Psychic"},{"magma storm":"Fire"},{"magnet bomb":"Steel"},{"magnetic flux":"Electric"},{"magnet rise":"Electric"},{"magnitude":"Ground"},{"mat block":"Fighting"},{"mean look":"Normal"},{"meditate":"Psychic"},{"me first":"Normal"},{"mega drain":"Grass"},{"megahorn":"Bug"},{"mega kick":"Normal"},{"mega punch":"Normal"},{"memento":"Dark"},{"metal burst":"Steel"},{"metal claw":"Steel"},{"metal sound":"Steel"},{"meteor mash":"Steel"},{"metronome":"Normal"},{"milk drink":"Normal"},{"mimic":"Normal"},{"mind reader":"Normal"},{"minimize":"Normal"},{"miracle eye":"Psychic"},{"mirror coat":"Psychic"},{"mirror move":"Flying"},{"mirror shot":"Steel"},{"mist":"Ice"},{"mist ball":"Psychic"},{"misty terrain":"Fairy"},{"moonblast":"Fairy"},{"moonlight":"Fairy"},{"morning sun":"Normal"},{"mud bomb":"Ground"},{"muddy water":"Water"},{"mud shot":"Ground"},{"mud-slap":"Ground"},{"mud sport":"Ground"},{"mystical fire":"Fire"},{"nasty plot":"Dark"},{"natural gift":"Normal"},{"nature power":"Normal"},{"needle arm":"Grass"},{"night daze":"Dark"},{"nightmare":"Ghost"},{"night shade":"Ghost"},{"night slash":"Dark"},{"noble roar":"Normal"},{"nuzzle":"Electric"},{"oblivion wing":"Flying"},{"octazooka":"Water"},{"odor sleuth":"Normal"},{"ominous wind":"Ghost"},{"outrage":"Dragon"},{"overheat":"Fire"},{"pain split":"Normal"},{"parabolic charge":"Electric"},{"parting shot":"Dark"},{"payback":"Dark"},{"pay day":"Normal"},{"peck":"Flying"},{"perish song":"Normal"},{"petal blizzard":"Grass"},{"petal dance":"Grass"},{"phantom force":"Ghost"},{"pin missile":"Bug"},{"play nice":"Normal"},{"play rough":"Fairy"},{"pluck":"Flying"},{"poison fang":"Poison"},{"poison gas":"Poison"},{"poison jab":"Poison"},{"poison powder":"Poison"},{"poison sting":"Poison"},{"poison tail":"Poison"},{"pound":"Normal"},{"powder":"Bug"},{"powder snow":"Ice"},{"power gem":"Rock"},{"power split":"Psychic"},{"power swap":"Psychic"},{"power trick":"Psychic"},{"power-up punch":"Fighting"},{"power whip":"Grass"},{"present":"Normal"},{"protect":"Normal"},{"psybeam":"Psychic"},{"psychic":"Psychic"},{"psycho boost":"Psychic"},{"psycho cut":"Psychic"},{"psycho shift":"Psychic"},{"psych up":"Normal"},{"psyshock":"Psychic"},{"psystrike":"Psychic"},{"psywave":"Psychic"},{"punishment":"Dark"},{"pursuit":"Dark"},{"quash":"Dark"},{"quick attack":"Normal"},{"quick guard":"Fighting"},{"quiver dance":"Bug"},{"rage":"Normal"},{"rage powder":"Bug"},{"rain dance":"Water"},{"rapid spin":"Normal"},{"razor leaf":"Grass"},{"razor shell":"Water"},{"razor wind":"Normal"},{"recover":"Normal"},{"recycle":"Normal"},{"reflect":"Psychic"},{"reflect type":"Normal"},{"refresh":"Normal"},{"relic song":"Normal"},{"rest":"Psychic"},{"retaliate":"Normal"},{"return":"Normal"},{"revenge":"Fighting"},{"reversal":"Fighting"},{"roar":"Normal"},{"roar of time":"Dragon"},{"rock blast":"Rock"},{"rock climb":"Normal"},{"rock polish":"Rock"},{"rock slide":"Rock"},{"rock smash":"Fighting"},{"rock throw":"Rock"},{"rock tomb":"Rock"},{"rock wrecker":"Rock"},{"role play":"Psychic"},{"rolling kick":"Fighting"},{"rollout":"Rock"},{"roost":"Flying"},{"rototiller":"Ground"},{"round":"Normal"},{"sacred fire":"Fire"},{"sacred sword":"Fighting"},{"safeguard":"Normal"},{"sand attack":"Ground"},{"sandstorm":"Rock"},{"sand tomb":"Ground"},{"scald":"Water"},{"scary face":"Normal"},{"scratch":"Normal"},{"screech":"Normal"},{"searing shot":"Fire"},{"secret power":"Normal"},{"secret sword":"Fighting"},{"seed bomb":"Grass"},{"seed flare":"Grass"},{"seismic toss":"Fighting"},{"self-destruct":"Normal"},{"shadow ball":"Ghost"},{"shadow claw":"Ghost"},{"shadow force":"Ghost"},{"shadow punch":"Ghost"},{"shadow sneak":"Ghost"},{"sharpen":"Normal"},{"sheer cold":"Ice"},{"shell smash":"Normal"},{"shift gear":"Steel"},{"shock wave":"Electric"},{"signal beam":"Bug"},{"silver wind":"Bug"},{"simple beam":"Normal"},{"sing":"Normal"},{"sketch":"Normal"},{"skill swap":"Psychic"},{"skull bash":"Normal"},{"sky attack":"Flying"},{"sky drop":"Flying"},{"sky uppercut":"Fighting"},{"slack off":"Normal"},{"slam":"Normal"},{"slash":"Normal"},{"sleep powder":"Grass"},{"sleep talk":"Normal"},{"sludge":"Poison"},{"sludge bomb":"Poison"},{"sludge wave":"Poison"},{"smack down":"Rock"},{"smelling salts":"Normal"},{"smog":"Poison"},{"smokescreen":"Normal"},{"snarl":"Dark"},{"snatch":"Dark"},{"snore":"Normal"},{"soak":"Water"},{"soft-boiled":"Normal"},{"solar beam":"Grass"},{"sonic boom":"Normal"},{"spacial rend":"Dragon"},{"spark":"Electric"},{"spider web":"Bug"},{"spike cannon":"Normal"},{"spikes":"Ground"},{"spiky shield":"Grass"},{"spite":"Ghost"},{"spit up":"Normal"},{"splash":"Normal"},{"spore":"Grass"},{"stealth rock":"Rock"},{"steamroller":"Bug"},{"steel wing":"Steel"},{"sticky web":"Bug"},{"stockpile":"Normal"},{"stomp":"Normal"},{"stone edge":"Rock"},{"stored power":"Psychic"},{"storm throw":"Fighting"},{"strength":"Normal"},{"string shot":"Bug"},{"struggle":"Normal"},{"struggle bug":"Bug"},{"stun spore":"Grass"},{"submission":"Fighting"},{"substitute":"Normal"},{"sucker punch":"Dark"},{"sunny day":"Fire"},{"super fang":"Normal"},{"superpower":"Fighting"},{"supersonic":"Normal"},{"surf":"Water"},{"swagger":"Normal"},{"swallow":"Normal"},{"sweet kiss":"Fairy"},{"sweet scent":"Normal"},{"swift":"Normal"},{"switcheroo":"Dark"},{"swords dance":"Normal"},{"synchronoise":"Psychic"},{"synthesis":"Grass"},{"tackle":"Normal"},{"tail glow":"Bug"},{"tail slap":"Normal"},{"tail whip":"Normal"},{"tailwind":"Flying"},{"take down":"Normal"},{"taunt":"Dark"},{"techno blast":"Normal"},{"teeter dance":"Normal"},{"telekinesis":"Psychic"},{"teleport":"Psychic"},{"thief":"Dark"},{"thrash":"Normal"},{"thunder":"Electric"},{"thunderbolt":"Electric"},{"thunder fang":"Electric"},{"thunder punch":"Electric"},{"thunder shock":"Electric"},{"thunder wave":"Electric"},{"tickle":"Normal"},{"topsy-turvy":"Dark"},{"torment":"Dark"},{"toxic":"Poison"},{"toxic spikes":"Poison"},{"transform":"Normal"},{"tri attack":"Normal"},{"trick":"Psychic"},{"trick-or-treat":"Ghost"},{"trick room":"Psychic"},{"triple kick":"Fighting"},{"trump card":"Normal"},{"twineedle":"Bug"},{"twister":"Dragon"},{"uproar":"Normal"},{"u-turn":"Bug"},{"vacuum wave":"Fighting"},{"v-create":"Fire"},{"venom drench":"Poison"},{"venoshock":"Poison"},{"vice grip":"Normal"},{"vine whip":"Grass"},{"vital throw":"Fighting"},{"volt switch":"Electric"},{"volt tackle":"Electric"},{"wake-up slap":"Fighting"},{"waterfall":"Water"},{"water gun":"Water"},{"water pledge":"Water"},{"water pulse":"Water"},{"water shuriken":"Water"},{"water sport":"Water"},{"water spout":"Water"},{"weather ball":"Normal"},{"whirlpool":"Water"},{"whirlwind":"Normal"},{"wide guard":"Rock"},{"wild charge":"Electric"},{"will-o-wisp":"Fire"},{"wing attack":"Flying"},{"wish":"Normal"},{"withdraw":"Water"},{"wonder room":"Psychic"},{"wood hammer":"Grass"},{"work up":"Normal"},{"worry seed":"Grass"},{"wrap":"Normal"},{"wring out":"Normal"},{"x-scissor":"Bug"},{"yawn":"Normal"},{"zap cannon":"Electric"},{"zen headbutt":"Psychic"}] --------------------------------------------------------------------------------