pokemon-showdown/ladders-matchmaker.js
Guangcong Luo 6dd58b40d3 Refactor simulator into new sim/ directory
This is a surprisingly minor refactor considering how many files it
touches, but most of this is only renames.

In terms of file renames:
- `tools.js` is now `sim/dex.js`
- `battle-engine.js` is now `sim/index.js` and its three classes are
  in `sim/battle.js`, `sim/side.js`, and `sim/pokemon.js`
- `prng.js` is now `sim/prng.js`

In terms of variable renames:
- `Tools` is now `Dex`
- `BattleEngine` is now `Sim`
- `BattleEngine.Battle` is now `Sim.Battle`
- `BattleEngine.BattleSide` is now `Sim.Side`
- `BattleEngine.BattlePokemon` is now `Sim.Pokemon`
2017-05-05 16:48:38 -05:00

202 lines
6.2 KiB
JavaScript

/**
* Matchmaker
* Pokemon Showdown - http://pokemonshowdown.com/
*
* This keeps track of challenges to battle made between users, setting up
* matches between users looking for a battle, and starting new battles.
*
* @License MIT License
*/
'use strict';
const PERIODIC_MATCH_INTERVAL = 60 * 1000;
function Search(userid, team, rating = 1000) {
this.userid = userid;
this.team = team;
this.rating = rating;
this.time = new Date().getTime();
}
class Matchmaker {
constructor() {
this.searches = new Map();
this.periodicMatchInterval = setInterval(
() => this.periodicMatch(),
PERIODIC_MATCH_INTERVAL
);
}
cancelSearch(user, format) {
if (format && !user.searching[format]) return false;
let searchedFormats = Object.keys(user.searching);
if (!searchedFormats.length) return false;
for (let searchedFormat of searchedFormats) {
if (format && searchedFormat !== format) continue;
let formatSearches = this.searches.get(searchedFormat);
for (let search of formatSearches) {
if (search.userid !== user.userid) continue;
formatSearches.delete(search);
delete user.searching[searchedFormat];
break;
}
}
user.updateSearch();
return true;
}
searchBattle(user, formatid) {
if (!user.connected) return;
formatid = Dex.getFormat(formatid).id;
return user.prepBattle(formatid, 'search', null)
.then(result => this.finishSearchBattle(user, formatid, result));
}
finishSearchBattle(user, formatid, result) {
if (!result) return;
// Get the user's rating before actually starting to search.
Ladders(formatid).getRating(user.userid).then(rating => {
let search = new Search(user.userid, user.team, rating);
this.addSearch(search, user, formatid);
}, error => {
// Rejects if we retrieved the rating but the user had changed their name;
// the search simply doesn't happen in this case.
});
}
matchmakingOK(search1, search2, user1, user2, formatid) {
// This should never happen.
if (!user1 || !user2) {
return void require('./crashlogger')(new Error(`Matched user ${user1 ? search2.userid : search1.userid} not found`), "The main process");
}
// users must be different
if (user1 === user2) return false;
// users must have different IPs
if (user1.latestIp === user2.latestIp) return false;
// users must not have been matched immediately previously
if (user1.lastMatch === user2.userid || user2.lastMatch === user1.userid) return false;
// search must be within range
let searchRange = 100, elapsed = Date.now() - Math.min(search1.time, search2.time);
if (formatid === 'ou' || formatid === 'oucurrent' ||
formatid === 'oususpecttest' || formatid === 'randombattle') {
searchRange = 50;
}
searchRange += elapsed / 300; // +1 every .3 seconds
if (searchRange > 300) searchRange = 300 + (searchRange - 300) / 10; // +1 every 3 sec after 300
if (searchRange > 600) searchRange = 600;
if (Math.abs(search1.rating - search2.rating) > searchRange) return false;
user1.lastMatch = user2.userid;
user2.lastMatch = user1.userid;
return Math.min(search1.rating, search2.rating) || 1;
}
addSearch(newSearch, user, formatid) {
// Filter racing conditions
if (!user.connected || user !== Users.getExact(user.userid)) return;
if (user.searching[formatid]) return;
// Prioritize players who have been searching for a match the longest.
let formatSearches = this.searches.get(formatid);
if (!formatSearches) {
formatSearches = new Set();
this.searches.set(formatid, formatSearches);
}
for (let search of formatSearches) {
let searchUser = Users.getExact(search.userid);
let minRating = this.matchmakingOK(search, newSearch, searchUser, user, formatid);
if (minRating) {
delete user.searching[formatid];
delete searchUser.searching[formatid];
formatSearches.delete(search);
this.startBattle(searchUser, user, formatid, search.team, newSearch.team, {rated: minRating});
return;
}
}
user.searching[formatid] = 1;
formatSearches.add(newSearch);
user.updateSearch();
}
periodicMatch() {
this.searches.forEach((formatSearches, formatid) => {
if (formatSearches.size < 2) return;
// Prioritize players who have been searching for a match the longest.
let [longestSearch, ...searches] = formatSearches;
let longestSearcher = Users.getExact(longestSearch.userid);
for (let search of searches) {
let searchUser = Users.getExact(search.userid);
let minRating = this.matchmakingOK(search, longestSearch, searchUser, longestSearcher, formatid);
if (minRating) {
delete longestSearcher.searching[formatid];
delete searchUser.searching[formatid];
formatSearches.delete(search);
formatSearches.delete(longestSearch);
this.startBattle(searchUser, longestSearcher, formatid, search.team, longestSearch.team, {rated: minRating});
return;
}
}
});
}
verifyPlayers(player1, player2, format) {
let p1 = (typeof player1 === 'string') ? Users(player1) : player1;
let p2 = (typeof player2 === 'string') ? Users(player2) : player2;
if (!p1 || !p2) {
this.cancelSearch(p1, format);
this.cancelSearch(p2, format);
return false;
}
if (p1 === p2) {
this.cancelSearch(p1, format);
this.cancelSearch(p2, format);
p1.popup("You can't battle your own account. Please use something like Private Browsing to battle yourself.");
return false;
}
if (Rooms.global.lockdown === true) {
this.cancelSearch(p1, format);
this.cancelSearch(p2, format);
p1.popup("The server is restarting. Battles will be available again in a few minutes.");
p2.popup("The server is restarting. Battles will be available again in a few minutes.");
return false;
}
return true;
}
startBattle(p1, p2, format, p1team, p2team, options) {
if (!this.verifyPlayers(p1, p2, format)) return;
let roomid = Rooms.global.prepBattleRoom(format);
let room = Rooms.createBattle(roomid, format, p1, p2, options);
p1.joinRoom(room);
p2.joinRoom(room);
room.battle.addPlayer(p1, p1team);
room.battle.addPlayer(p2, p2team);
this.cancelSearch(p1, format);
this.cancelSearch(p2, format);
Rooms.global.onCreateBattleRoom(p1, p2, room, options);
return room;
}
}
module.exports = {
Search,
Matchmaker,
matchmaker: new Matchmaker(),
};