├── .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 |
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 | [](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"}]
--------------------------------------------------------------------------------