mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-05 21:17:43 -05:00
369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
/**
|
|
* Rock Paper Scissors plugin by Mia
|
|
* @author mia-pi-git
|
|
*/
|
|
import {Utils} from '../../lib';
|
|
|
|
const MAX_ROUNDS = 200;
|
|
const TIMEOUT = 10 * 1000;
|
|
const ICONS: {[k: string]: string} = {
|
|
Rock: `<i class="fa fa-hand-rock-o"></i>`,
|
|
Paper: '<i class="fa fa-hand-paper-o"></i>',
|
|
Scissors: '<i class="fa fa-hand-scissors-o"></i>',
|
|
};
|
|
|
|
const MATCHUPS = new Map<string, string>([
|
|
['scissors', 'paper'],
|
|
['rock', 'scissors'],
|
|
['paper', 'rock'],
|
|
]);
|
|
|
|
export const challenges: Map<string, string> = Chat.oldPlugins['rock-paper-scissors']?.challenges || new Map();
|
|
|
|
export class RPSPlayer extends Rooms.RoomGamePlayer {
|
|
currentChoice = '';
|
|
points = 0;
|
|
}
|
|
|
|
export class RPSGame extends Rooms.RoomGame {
|
|
room: Room;
|
|
currentRound: number;
|
|
playerTable: {[k: string]: RPSPlayer};
|
|
readonly checkChat = true;
|
|
roundTimer?: NodeJS.Timeout;
|
|
players: RPSPlayer[];
|
|
wins: ({name: string, choice: string} | null)[];
|
|
constructor(room: Room) {
|
|
super(room);
|
|
this.room = room;
|
|
this.currentRound = 0;
|
|
this.playerTable = {};
|
|
this.title = 'Rock Paper Scissors';
|
|
this.gameid = 'rockpaperscissors' as ID;
|
|
this.players = [];
|
|
this.wins = [];
|
|
|
|
this.room.update();
|
|
this.room.add(`|controlshtml|<center>Waiting for another player to join....</center>`);
|
|
this.room.add(`|fieldhtml|<center><h2>Waiting to start the game...</h2></center>`);
|
|
}
|
|
onJoin(user: User) {
|
|
if (user.id in this.playerTable) return;
|
|
if (this.players.length < 2) {
|
|
this.addPlayer(user);
|
|
}
|
|
}
|
|
static checkMatchup(p1: RPSPlayer, p2: RPSPlayer) {
|
|
const p1Choice = p1.currentChoice;
|
|
const p2Choice = p2.currentChoice;
|
|
if (!p1Choice && p2Choice) return p2;
|
|
if (!p2Choice && p1Choice) return p1;
|
|
if (MATCHUPS.get(p1Choice) === p2Choice) return p1;
|
|
if (MATCHUPS.get(p2Choice) === p1Choice) return p2;
|
|
return null;
|
|
}
|
|
sendOptions() {
|
|
const button = (cmd: string, title: string) => `<button class="button" name="send" value="/${cmd}">${title}</button>`;
|
|
let buf = `<center><strong>Make your choice, quick!</strong><br />`;
|
|
for (const item of ['Rock', 'Paper', 'Scissors']) {
|
|
buf += `${button(`choose ${item}`, `${item} ${ICONS[item]}`)}`;
|
|
}
|
|
buf += `<br />${button('rps end', "End game")}</center>`;
|
|
this.addControls(buf);
|
|
}
|
|
sendFullLog() {
|
|
let buf = `|html|`;
|
|
for (const [i, entry] of this.wins.entries()) {
|
|
if (!entry) {
|
|
buf += `<div class="broadcast-red">Nobody won round ${i + 1}...</div>`;
|
|
continue;
|
|
}
|
|
const {name, choice} = entry;
|
|
buf += Utils.html`<div class="broadcast-green">${name} won round ${i + 1} with ${choice}!</div>`;
|
|
}
|
|
this.room.add(buf).update();
|
|
}
|
|
resetOptions(user: User) {
|
|
const player = this.getPlayer(user);
|
|
player.sendRoom(
|
|
`|controlshtml|<div class="pad"><h2>You have selected your choice. Wait for the next round.</h2></div>`
|
|
);
|
|
}
|
|
getScrollback() {
|
|
// some html borrowed from trivia plugin, credits Morfent? I think?
|
|
let buf = `|fieldhtml|<div class="broadcast-blue"><table style="width: 100%; background-color: #9CBEDF; margin: 2px 0">`;
|
|
buf += `<tr style="background-color: #6688AA"><th>Username</th><th>Points</th></tr>`;
|
|
for (const id in this.playerTable) {
|
|
const player = this.playerTable[id];
|
|
buf += Utils.html`<tr style="background-color: #6688AA"><td>${player.name}</td>`;
|
|
buf += Utils.html`<td style="text-align: center">${player.points}</td></tr>`;
|
|
}
|
|
buf += `</table></div>`;
|
|
if (this.wins.length) {
|
|
buf += `<br />`;
|
|
for (const [i, entry] of this.wins.entries()) {
|
|
if (this.wins.length > 6) {
|
|
// we only wanna show the last 6 rounds
|
|
const diff = this.wins.length - 6;
|
|
if (i < diff) continue;
|
|
}
|
|
if (!entry) {
|
|
buf += `<div class="broadcast-red">Nobody won round ${i + 1}...</div>`;
|
|
continue;
|
|
}
|
|
const {name, choice} = entry;
|
|
buf += Utils.html`<div class="broadcast-green">${name} won round ${i + 1} with ${choice}!</div>`;
|
|
}
|
|
}
|
|
return buf;
|
|
}
|
|
sendScrollback() {
|
|
this.room.add(this.getScrollback()).update();
|
|
}
|
|
end() {
|
|
const [p1, p2] = Object.keys(this.playerTable).map(item => this.playerTable[item]);
|
|
this.addControls(`<h2>The game is over!</h2>`);
|
|
const winner = p1.points > p2.points ? p1 : p2;
|
|
const points = winner.points;
|
|
const message = Utils.html`<strong>${winner.name} won the game with ${Chat.count(points, 'points')}!</strong>`;
|
|
this.addField(message);
|
|
this.add(message);
|
|
this.sendFullLog();
|
|
this.destroy();
|
|
}
|
|
clearChoices() {
|
|
const [p1, p2] = this.players;
|
|
p1.currentChoice = "";
|
|
p2.currentChoice = "";
|
|
}
|
|
addControls(message: string) {
|
|
for (const id in this.playerTable) {
|
|
this.playerTable[id].sendRoom(`|controlshtml|<div class="pad">${message}</div>`);
|
|
}
|
|
}
|
|
getPlayers() {
|
|
return Object.keys(this.playerTable).map(item => this.playerTable[item]);
|
|
}
|
|
addField(message: string) {
|
|
return this.room.add(`${this.getScrollback()}<br /><div class="broadcast-green">${message}</div>`).update();
|
|
}
|
|
runMatch() {
|
|
const [p1, p2] = this.players;
|
|
const winner = RPSGame.checkMatchup(p1, p2);
|
|
if (winner === null) { // tie
|
|
this.add(`The players have tied! Nobody wins this round....`);
|
|
this.wins.push(null);
|
|
} else {
|
|
this.add(Utils.html`${winner.name} wins the round! They gain a point.`);
|
|
winner.points++;
|
|
this.wins.push({
|
|
name: winner.name,
|
|
choice: winner.currentChoice,
|
|
});
|
|
}
|
|
if (this.currentRound >= MAX_ROUNDS) {
|
|
this.add(`The game has hit the max number of rounds, and so will be ending.`);
|
|
return this.end();
|
|
}
|
|
this.clearChoices();
|
|
this.sendScrollback();
|
|
return this.startNextRound();
|
|
}
|
|
add(message: string) {
|
|
return this.room.add(`|html|${message}`).update();
|
|
}
|
|
start() {
|
|
if (this.players.length < 2) {
|
|
this.add(`<h2>There are not enough players to start. Use /rps start to start when all players are ready.</h2>`);
|
|
return;
|
|
}
|
|
this.addField(`The Rock Paper Scissors match has begun!`);
|
|
this.add(Utils.html`(Use /rps end to end the game)`);
|
|
this.startNextRound();
|
|
}
|
|
getPlayer(user: User) {
|
|
const player = this.playerTable[user.id];
|
|
if (!player) throw new Chat.ErrorMessage(`You are not a player in this game.`);
|
|
return player;
|
|
}
|
|
pause(user: User) {
|
|
const player = this.getPlayer(user);
|
|
if (!this.roundTimer) throw new Chat.ErrorMessage(`The game is not running, and cannot be paused.`);
|
|
this.room.add(Utils.html`|html|<h2>The game has been paused by ${player.name}.</h2>`).update();
|
|
clearTimeout(this.roundTimer);
|
|
this.addControls(`The game is paused.`);
|
|
this.add(`The game is paused.`);
|
|
}
|
|
unpause(user: User) {
|
|
const player = this.getPlayer(user);
|
|
if (this.roundTimer) throw new Chat.ErrorMessage(`The game is not paused.`);
|
|
this.room.add(Utils.html`|html|${player.name} unpaused the game.`).update();
|
|
this.startNextRound();
|
|
}
|
|
startNextRound() {
|
|
this.currentRound++;
|
|
if (this.currentRound > 0) {
|
|
this.addField(
|
|
`Round ${this.currentRound} has begun! ` +
|
|
`Players, you have ${Chat.toDurationString(TIMEOUT)} to make your moves!`
|
|
);
|
|
}
|
|
if (this.currentRound >= 100 && this.wins.filter(Boolean).length < (this.currentRound - 20)) {
|
|
// forcefully end if no one's progressed in 20 turns
|
|
return this.end();
|
|
}
|
|
this.room.add(`|html|<h2>Round ${this.currentRound}</h2>`).update();
|
|
this.sendOptions();
|
|
this.roundTimer = setTimeout(() => {
|
|
this.runMatch();
|
|
}, TIMEOUT);
|
|
}
|
|
destroy() {
|
|
if (this.roundTimer) clearTimeout(this.roundTimer);
|
|
this.room.pokeExpireTimer();
|
|
this.addControls(`The game has ended.`);
|
|
this.ended = true;
|
|
this.room.add(`The game has been ended.`); // for the benefit of those in the room
|
|
this.room.log.log = [];
|
|
for (const id in this.playerTable) {
|
|
this.playerTable[id].unlinkUser();
|
|
}
|
|
this.playerTable = {};
|
|
}
|
|
choose(user: User, option: string) {
|
|
option = toID(option);
|
|
const player = this.getPlayer(user);
|
|
if (!MATCHUPS.get(option)) {
|
|
throw new Chat.ErrorMessage(`Invalid choice: ${option}.`);
|
|
}
|
|
if (player.currentChoice) throw new Chat.ErrorMessage("You have already made your choice!");
|
|
player.currentChoice = option;
|
|
this.add(Utils.html`${user.name} has made their choice.`);
|
|
this.resetOptions(user);
|
|
if (this.players.filter(item => item.currentChoice).length > 1) {
|
|
clearTimeout(this.roundTimer!);
|
|
return this.runMatch();
|
|
}
|
|
return true;
|
|
}
|
|
leaveGame(user: User) {
|
|
const player = this.getPlayer(user);
|
|
player.sendRoom(`You left the game.`);
|
|
delete this.playerTable[user.id];
|
|
this.end();
|
|
}
|
|
addPlayer(user: User) {
|
|
if (this.playerTable[user.id]) throw new Chat.ErrorMessage(`You are already a player in this game.`);
|
|
this.playerTable[user.id] = new RPSPlayer(user, this);
|
|
this.players.push(this.playerTable[user.id]);
|
|
this.room.auth.set(user.id, Users.PLAYER_SYMBOL);
|
|
user.sendTo(this.room, `You have successfully joined the Rock Paper Scissors game.`);
|
|
return this.playerTable[user.id];
|
|
}
|
|
}
|
|
|
|
function findExisting(user1: string, user2: string) {
|
|
return Rooms.get(`rps-${user1}-${user2}`) || Rooms.get(`rps-${user2}-${user1}`);
|
|
}
|
|
|
|
export const commands: Chat.ChatCommands = {
|
|
rps: 'rockpaperscissors',
|
|
rockpaperscissors: {
|
|
challenge: 'create',
|
|
create(target, room, user) {
|
|
target = target.trim();
|
|
const userid = toID(target);
|
|
const targetUser = userid ? Users.get(userid) : this.pmTarget;
|
|
if (targetUser === user) return this.errorReply(`You cannot challenge yourself.`);
|
|
if (!targetUser) {
|
|
return this.errorReply(
|
|
`User ${this.targetUsername} not found. Either specify a username or use this command in PMs.`
|
|
);
|
|
}
|
|
const existingRoom = findExisting(user.id, targetUser.id);
|
|
if (existingRoom?.game && !existingRoom.game.ended) {
|
|
return this.errorReply(`You already have a Rock Paper Scissors game against ${targetUser.name}.`);
|
|
}
|
|
if (!this.pmTarget) this.pmTarget = targetUser;
|
|
challenges.set(targetUser.id, user.id);
|
|
this.sendChatMessage(
|
|
`/raw ${user.name} challenged you to Rock Paper Scissors!`
|
|
);
|
|
targetUser.send(
|
|
`|pm|${user.getIdentity()}|${targetUser.getIdentity()}|` +
|
|
`/raw <button class="button" name="send" value="/rps accept"><strong>Accept</strong></button></div>`
|
|
);
|
|
},
|
|
|
|
accept(target, room, user) {
|
|
const id = challenges.get(user.id);
|
|
if (!id) return this.errorReply(`You have no Rock Paper Scissors request pending.`);
|
|
const targetUser = Users.get(id);
|
|
if (!targetUser) return this.errorReply(`The user who challenged you to Rock Paper Scissors is offline.`);
|
|
const existingRoom = findExisting(user.id, targetUser.id);
|
|
const roomid = `rps-${targetUser.id}-${user.id}`;
|
|
const gameRoom = existingRoom || Rooms.createGameRoom(
|
|
roomid as RoomID, `[RPS] ${user.name} vs ${targetUser.name}`, {
|
|
modchat: '+',
|
|
isPrivate: true,
|
|
}
|
|
);
|
|
gameRoom.game = new RPSGame(gameRoom);
|
|
gameRoom.add(
|
|
`|raw|<h2>Rock Paper Scissors: ${user.name} vs ${targetUser.name}!</h2>` +
|
|
`Use /rps start to start the game, once both players have joined!`
|
|
).update();
|
|
user.joinRoom(gameRoom.roomid);
|
|
targetUser.joinRoom(gameRoom.roomid);
|
|
(gameRoom.game as RPSGame).start();
|
|
},
|
|
|
|
deny(target, room, user) {
|
|
const request = challenges.get(user.id);
|
|
if (!request) return this.errorReply(`You have no Rock Paper Scissors challenge pending.`);
|
|
const [sender] = request;
|
|
Users.get(sender)?.popup(`${user.name} denied your Rock Paper Scissors challenge.`);
|
|
challenges.delete(user.id);
|
|
},
|
|
|
|
end(target, room, user) {
|
|
const game = this.requireGame(RPSGame);
|
|
if (!game.playerTable[user.id]) {
|
|
return this.errorReply(`You are not a player, and so cannot end the game.`);
|
|
}
|
|
game.end();
|
|
},
|
|
|
|
choose(target, room, user) {
|
|
this.parse(`/choose ${target}`);
|
|
},
|
|
|
|
leave(target, room, user) {
|
|
this.parse(`/leavegame`);
|
|
},
|
|
|
|
pause(target, room, user) {
|
|
const game = this.requireGame(RPSGame);
|
|
game.pause(user);
|
|
},
|
|
|
|
resume(target, room, user) {
|
|
const game = this.requireGame(RPSGame);
|
|
game.unpause(user);
|
|
},
|
|
|
|
help() {
|
|
this.runBroadcast();
|
|
const strings = [
|
|
`/rockpaperscissors OR /rps<br />`,
|
|
`/rps create OR /rps challenge [user], [rounds] - Challenges a user to a game of Rock Paper Scissors with [rounds] (or 3) rounds`, `/rps leave - Leave the game.`,
|
|
`/rps start - Start the Rock Paper Scissors game.`,
|
|
`/rps end - End the Rock Paper Scissors game`,
|
|
`/rps pause - Pauses the game, if it's in progress.`,
|
|
`/rps resume - Resumes the game, if it's paused.`,
|
|
];
|
|
return this.sendReplyBox(strings.join('<br />'));
|
|
},
|
|
},
|
|
};
|