This repository has been archived by the owner on Apr 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
/
bot.js
285 lines (244 loc) · 8.31 KB
/
bot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// Command-line Arguments
var program = require('commander');
program
.option('--console', 'Only start the web console - not the game playing bot.')
.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')
.option('--port [port]', 'The port on which to serve the web console. [3000]', "3000")
.option('--ranked', 'Challenge on the ranked league.')
.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')
.option('--algorithm [algorithm]', "Can be 'minimax', 'greedy', or 'random'. ['minimax']", "minimax")
.option('--account [file]', "File from which to load credentials. ['account.json']", "account.json")
.option('--nosave', "Don't save games to the in-memory db.")
.option('--nolog', "Don't append to log files.")
.option('--startchallenging', "Start out challenging, instead of requiring a manual activation first.")
.parse(process.argv);
var request = require('request'); // Used for making post requests to login server
var util = require('./util');
var fs = require('fs');
// Setup Logging
var log4js = require('log4js');
log4js.loadAppender('file');
var logger = require('log4js').getLogger("bot");
if(!program.nolog) {
// Ensure that logging directory exists
if(!fs.existsSync("./logs")) { fs.mkdirSync("logs") };
log4js.addAppender(log4js.appenders.file('logs/bot.log'), 'bot');
log4js.addAppender(log4js.appenders.file('logs/minimax.log'), 'minimax');
log4js.addAppender(log4js.appenders.file('logs/learning.log'), 'learning');
log4js.addAppender(log4js.appenders.file('logs/battleroom.log'), 'battleroom');
log4js.addAppender(log4js.appenders.file('logs/decisions.log'), 'decisions');
log4js.addAppender(log4js.appenders.file('logs/webconsole.log'), 'webconsole');
log4js.addAppender(log4js.appenders.file('logs/battle.log'), 'battle');
log4js.addAppender(log4js.appenders.file('logs/battlepokemon.log'), 'battlepokemon');
log4js.addAppender(log4js.appenders.file('logs/battleside.log'), 'battleside');
log4js.addAppender(log4js.appenders.file('logs/greedy.log'), 'greedy');
} else {
logger.setLevel("INFO");
log4js.configure({
appenders : [
{
type: "console",
category: ["bot"]
}
]
});
}
// Login information for this bot
var account = JSON.parse(fs.readFileSync(program.account));
module.exports.account = account;
var webconsole = require("./console.js");// Web console
// Connect to server
var sockjs = require('sockjs-client-ws');
var client = null;
if(!program.console) client = sockjs.create(program.host);
// Domain (replay button redirects here)
var DOMAIN = "http://play.pokemonshowdown.com/";
exports.DOMAIN = DOMAIN;
// PHP endpoint used to login / authenticate
var ACTION_PHP = DOMAIN + "~~showdown/action.php";
// Values that need to be globally stored in order to login properly
var CHALLENGE_KEY_ID = null;
var CHALLENGE = null;
// BattleRoom object
var BattleRoom = require('./battleroom');
// The game type that we want to search for on startup
var GAME_TYPE = (program.ranked) ? "randombattle" : "unratedrandombattle";
// Load in Game Data
var Pokedex = require("./data/pokedex");
var Typechart = require("./data/typechart");
// Sends a piece of data to the given room
// Room can be null for a global command
var send = module.exports.send = function(data, room) {
if (room && room !== 'lobby' && room !== true) {
data = room+'|'+data;
} else if (room !== true) {
data = '|'+data;
}
client.write(data);
logger.trace(">> " + data);
}
// Login to a new account
function rename(name, password) {
var self = this;
request.post({
url : ACTION_PHP,
formData : {
act: "login",
name: name,
pass: password,
challengekeyid: CHALLENGE_KEY_ID,
challenge: CHALLENGE
}
},
function (err, response, body) {
var data = util.safeJSON(body);
if(data && data.curuser && data.curuser.loggedin) {
send("/trn " + account.username + ",0," + data.assertion);
} else {
// We couldn't log in for some reason
logger.fatal("Error logging in...");
process.exit();
}
});
}
// Global room counter (this allows multiple battles at the same time)
var ROOMS = {};
exports.ROOMS = ROOMS;
// Add a new room (only supports rooms of type battle)
function addRoom(id, type) {
if(type == "battle") {
ROOMS[id] = new BattleRoom(id, send);
return ROOMS[id];
} else {
logger.error("Unkown room type: " + type);
}
}
// Remove a room from the global list
function removeRoom(id) {
var room = ROOMS[id];
if(room) {
delete ROOMS[id];
return true;
}
return false;
}
// Code to execute once we have succesfully authenticated
function onLogin() {
//do nothing
}
function searchBattle() {
logger.info("Searching for an unranked random battle");
send("/search " + GAME_TYPE);
}
module.exports.searchBattle = searchBattle;
// Global recieve function - tries to interpret command, or send to the correct room
function recieve(data) {
logger.trace("<< " + data);
var roomid = '';
if (data.substr(0,1) === '>') { // First determine if this command is for a room
var nlIndex = data.indexOf('\n');
if (nlIndex < 0) return;
roomid = util.toRoomid(data.substr(1,nlIndex-1));
data = data.substr(nlIndex+1);
}
if (data.substr(0,6) === '|init|') { // If it is an init command, create the room
if (!roomid) roomid = 'lobby';
var roomType = data.substr(6);
var roomTypeLFIndex = roomType.indexOf('\n');
if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex);
roomType = util.toId(roomType);
logger.info(roomid + " is being opened.");
addRoom(roomid, roomType);
} else if ((data+'|').substr(0,8) === '|expire|') { // Room expiring
var room = ROOMS[roomid];
logger.info(roomid + " has expired.");
if(room) {
room.expired = true;
if (room.updateUser) room.updateUser();
}
return;
} else if ((data+'|').substr(0,8) === '|deinit|' || (data+'|').substr(0,8) === '|noinit|') {
if (!roomid) roomid = 'lobby';
// expired rooms aren't closed when left
if (ROOMS[roomid] && ROOMS[roomid].expired) return;
logger.info(roomid + " has been closed.");
removeRoom(roomid);
return;
}
if(roomid) { //Forward command to specific room
if(ROOMS[roomid]) {
ROOMS[roomid].recieve(data);
} else {
logger.error("Room of id " + roomid + " does not exist to send data to.");
}
return;
}
// Split global command into parts
var parts;
if(data.charAt(0) === '|') {
parts = data.substr(1).split('|');
} else {
parts = [];
}
switch(parts[0]) {
// Recieved challenge string
case 'challenge-string':
case 'challstr':
logger.info("Recieved challenge string...");
CHALLENGE_KEY_ID = parseInt(parts[1], 10);
CHALLENGE = parts[2];
// Now try to rename to the given user
rename(account.username, account.password);
break;
// Server is telling us to update the user that we are currently logged in as
case 'updateuser':
// The update user command can actually come with a second command (after the newline)
var nlIndex = data.indexOf('\n');
if (nlIndex > 0) {
recieve(data.substr(nlIndex+1));
nlIndex = parts[3].indexOf('\n');
parts[3] = parts[3].substr(0, nlIndex);
}
var name = parts[1];
var named = !!+parts[2];
if(name == account.username) {
logger.info("Successfully logged in.");
onLogin()
}
break;
// Server tried to send us a popup
case 'popup':
logger.info("Popup: " + data.substr(7).replace(/\|\|/g, '\n'));
break;
// Someone has challenged us to a battle
case 'updatechallenges':
var challenges = JSON.parse(data.substr(18));
if(challenges.challengesFrom) {
for(var user in challenges.challengesFrom) {
if(challenges.challengesFrom[user] == "gen6randombattle") {
logger.info("Accepting challenge from " + user);
send("/accept " + user);
} else {
logger.warn("Won't accept challenge of type: " + challenges.challengesFrom[user]);
send("/reject " + user);
}
}
}
break;
// Unkown global command
default:
logger.warn("Did not recognize command of type: " + parts[0]);
break;
}
}
if(client) {
client.on('connection', function() {
logger.info('Connected to server.');
});
client.on('data', function(msg) {
recieve(msg);
});
client.on('error', function(e) {
logger.error(e);
});
}