Game messages won't show up unless you're playing or watching.` : ''}
`
);
}
}
onStart(isAutostart?: boolean) {
if (this.playerCount < 2) {
if (isAutostart) {
this.room.add("The game of UNO was forcibly ended because there aren't enough users.");
this.destroy();
return false;
} else {
throw new Chat.ErrorMessage("There must be at least 2 players to start a game of UNO.");
}
}
if (this.autostartTimer) clearTimeout(this.autostartTimer);
this.sendToRoom(`|uhtmlchange|uno-${this.gameNumber}|
The game of UNO has started.
${this.suppressMessages ? `
Game messages won't show up unless you're playing or watching.` : ''}
`, true);
this.state = 'play';
this.onNextPlayer(); // determines the first player
// give cards to the players
for (const player of this.players) {
player.hand.push(...this.drawCard(7));
}
// top card of the deck.
do {
this.topCard = this.drawCard(1)[0];
this.discards.unshift(this.topCard);
} while (this.topCard.color === 'Black');
this.sendToRoom(`|raw|The top card is ${this.topCard.name}.`);
this.onRunEffect(this.topCard.value, true);
this.nextTurn(true);
}
override joinGame(user: User) {
if (user.id in this.playerTable) {
throw new Chat.ErrorMessage("You have already joined the game of UNO.");
}
if (this.state === 'signups' && this.addPlayer(user)) {
this.sendToRoom(`${user.name} has joined the game of UNO.`);
return true;
}
return false;
}
override leaveGame(user: User) {
if (!(user.id in this.playerTable)) return false;
const player = this.playerTable[user.id];
if ((this.state === 'signups' && this.removePlayer(player)) || this.eliminate(user.id)) {
this.sendToRoom(`${user.name} has left the game of UNO.`);
return true;
}
return false;
}
/**
* Overwrite the default makePlayer so it makes an UNOPlayer instead.
*/
makePlayer(user: User) {
return new UNOPlayer(user, this);
}
override onRename(user: User, oldUserid: ID, isJoining: boolean, isForceRenamed: boolean) {
if (!(oldUserid in this.playerTable) || user.id === oldUserid) return false;
if (!user.named && !isForceRenamed) {
user.games.delete(this.roomid);
user.updateSearch();
return; // dont set users to their guest accounts.
}
this.renamePlayer(user, oldUserid);
}
eliminate(userid: ID | undefined) {
if (!userid) return null;
const player = this.playerTable[userid];
if (!player) return false;
const name = player.name;
if (this.playerCount === 2) {
this.removePlayer(player);
this.onWin(this.players[0]);
return name;
}
// handle current player...
const removingCurrentPlayer = player === this.currentPlayer;
if (removingCurrentPlayer) {
if (this.state === 'color') {
if (!this.topCard) {
// should never happen
throw new Error(`No top card in the discard pile.`);
}
this.topCard.changedColor = this.discards[1].changedColor || this.discards[1].color;
this.sendToRoom(`|raw|${Utils.escapeHTML(name)} has not picked a color, the color will stay as ${this.topCard.changedColor}.`);
}
}
if (this.awaitUnoPlayer === player) this.awaitUnoPlayer = null;
if (!this.topCard) {
throw new Chat.ErrorMessage(`Unable to disqualify ${name}.`);
}
// put that player's cards into the discard pile to prevent cards from being permanently lost
this.discards.push(...player.hand);
if (removingCurrentPlayer) {
this.onNextPlayer();
}
this.removePlayer(player);
if (removingCurrentPlayer) {
this.nextTurn(true);
}
return name;
}
sendToRoom(msg: string, overrideSuppress = false) {
if (!this.suppressMessages || overrideSuppress) {
this.room.add(msg).update();
} else {
// send to the players first
for (const player of this.players) {
player.sendRoom(msg);
}
// send to spectators
for (const i in this.spectators) {
if (i in this.playerTable) continue; // don't double send to users already in the game.
const user = Users.getExact(i);
if (user) user.sendTo(this.roomid, msg);
}
}
}
getPlayers(showCards?: boolean): string {
let playerList = this.players;
if (this.direction === -1) playerList = [...playerList].reverse();
if (!showCards) {
return playerList.map(p => Utils.escapeHTML(p.name)).join(', ');
}
let buf = ``;
for (const player of playerList) {
buf += `
`;
}
buf += ``;
return buf;
}
onAwaitUno() {
return new Promise(resolve => {
if (!this.awaitUnoPlayer) return void resolve();
this.state = "uno";
// the throttle for sending messages is at 600ms for non-authed users,
// wait 750ms before sending the next person's turn.
// this allows games to be fairer, so the next player would not spam the pass command blindly
// to force the player to draw 2 cards.
// this also makes games with uno bots not always turn in the bot's favour.
// without a delayed turn, 3 bots playing will always result in a endless game
setTimeout(() => resolve(), 750);
});
}
nextTurn(starting?: boolean) {
void this.onAwaitUno().then(() => {
if (!starting) this.onNextPlayer();
if (this.timer) clearTimeout(this.timer);
const player = this.currentPlayer!;
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${player.name}'s turn.`);
this.state = 'play';
if (player.cardLock) player.cardLock = null;
player.sendDisplay();
this.timer = setTimeout(() => {
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${player.name} has been automatically disqualified.`);
this.eliminate(player.id);
}, this.maxTime * 1000);
});
}
onNextPlayer() {
this.currentPlayer = this.getNextPlayer();
}
getNextPlayer() {
// if none is set
this.currentPlayer ||= this.players[Math.floor(this.playerCount * Math.random())];
let player = this.players[this.players.indexOf(this.currentPlayer) + this.direction];
// wraparound
player ||= (this.direction === 1 ? this.players[0] : this.players[this.playerCount - 1]);
return player;
}
onDraw(player: UNOPlayer) {
if (this.currentPlayer !== player || this.state !== 'play') return false;
if (player.cardLock) return true;
this.onCheckUno();
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${player.name} has drawn a card.`);
const card = this.onDrawCard(player, 1);
player.sendDisplay();
player.cardLock = card[0].name;
}
onPlay(player: UNOPlayer, cardName: string) {
if (this.currentPlayer !== player || this.state !== 'play') return false;
const card = player.hasCard(cardName);
if (!card) return "You do not have that card.";
// check for legal play
if (!this.topCard) {
// should never happen
throw new Error(`No top card in the discard pile.`);
}
if (player.cardLock && player.cardLock !== cardName) return `You can only play ${player.cardLock} after drawing.`;
if (
card.color !== 'Black' &&
card.color !== (this.topCard.changedColor || this.topCard.color) &&
card.value !== this.topCard.value
) {
return `You cannot play this card; you can only play: Wild cards, ${this.topCard.changedColor ? 'and' : ''} ${this.topCard.changedColor || this.topCard.color} cards${this.topCard.changedColor ? "" : ` and cards with the digit ${this.topCard.value}`}.`;
}
if (card.value === '+4' && !player.canPlayWildFour()) {
return "You cannot play Wild +4 when you still have a card with the same color as the top card.";
}
if (this.timer) clearTimeout(this.timer); // reset the autodq timer.
this.onCheckUno();
// update the game information.
this.topCard = card;
player.removeCard(cardName);
this.discards.unshift(card);
// update the unoId here, so when the display is sent to the player when the play is made
if (player.hand.length === 1) {
this.awaitUnoPlayer = player;
this.unoId = Math.floor(Math.random() * 100).toString() as ID;
}
player.sendDisplay(); // update display without the card in it for purposes such as choosing colors
this.sendToRoom(`|raw|${Utils.escapeHTML(player.name)} has played a ${card.name}.`);
// handle hand size
if (!player.hand.length) {
this.onWin(player);
return;
}
// continue with effects and next player
this.onRunEffect(card.value);
if (this.state === 'play') this.nextTurn();
}
onRunEffect(value: string, initialize?: boolean) {
const colorDisplay = `|uhtml|uno-color|
`;
switch (value) {
case 'Reverse':
this.direction *= -1;
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|The direction of the game has changed.`);
// in 2 player games, reverse sends the turn back to the player.
if (!initialize && this.playerCount === 2) this.onNextPlayer();
break;
case 'Skip':
this.onNextPlayer();
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${this.currentPlayer!.name}'s turn has been skipped.`);
break;
case '+2':
this.onNextPlayer();
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${this.currentPlayer!.name} has been forced to draw 2 cards.`);
this.onDrawCard(this.currentPlayer!, 2);
break;
case '+4':
this.currentPlayer!.sendRoom(colorDisplay);
this.state = 'color';
// apply to the next in line, since the current player still has to choose the color
const next = this.getNextPlayer();
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${next.name} has been forced to draw 4 cards.`);
this.onDrawCard(next, 4);
this.isPlusFour = true;
this.timer = setTimeout(() => {
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${this.currentPlayer!.name} has been automatically disqualified.`);
this.eliminate(this.currentPlayer!.id);
}, this.maxTime * 1000);
break;
case 'Wild':
this.currentPlayer!.sendRoom(colorDisplay);
this.state = 'color';
this.timer = setTimeout(() => {
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${this.currentPlayer!.name} has been automatically disqualified.`);
this.eliminate(this.currentPlayer!.id);
}, this.maxTime * 1000);
break;
}
if (initialize) this.onNextPlayer();
}
onSelectColor(player: UNOPlayer, color: Color) {
if (
!['Red', 'Blue', 'Green', 'Yellow'].includes(color) ||
player !== this.currentPlayer ||
this.state !== 'color'
) {
return false;
}
if (!this.topCard) {
// should never happen
throw new Error(`No top card in the discard pile.`);
}
this.topCard.changedColor = color;
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|The color has been changed to ${color}.`);
if (this.timer) clearTimeout(this.timer);
// remove color change menu and send the display of their cards again
player.sendRoom("|uhtmlchange|uno-color|");
player.sendDisplay();
if (this.isPlusFour) {
this.isPlusFour = false;
this.onNextPlayer(); // handle the skipping here.
}
this.nextTurn();
}
onDrawCard(player: UNOPlayer, count: number) {
if (typeof count === 'string') count = parseInt(count);
if (!count || isNaN(count) || count < 1) count = 1;
const drawnCards = this.drawCard(count);
player.hand.push(...drawnCards);
player.sendRoom(
`|raw|You have drawn the following card${Chat.plural(drawnCards)}: ` +
`${drawnCards.map(card => `${card.name}`).join(', ')}.`
);
return drawnCards;
}
drawCard(count: number) {
if (typeof count === 'string') count = parseInt(count);
if (!count || isNaN(count) || count < 1) count = 1;
const drawnCards: Card[] = [];
for (let i = 0; i < count; i++) {
if (!this.deck.length) {
// shuffle the cards back into the deck, or if there are no discards, add another deck into the game.
this.deck = this.discards.length ? Utils.shuffle(this.discards) : Utils.shuffle(createDeck());
this.discards = []; // clear discard pile
}
drawnCards.push(this.deck[this.deck.length - 1]);
this.deck.pop();
}
return drawnCards;
}
onUno(player: UNOPlayer, unoId: ID) {
// uno id makes spamming /uno uno impossible
if (this.unoId !== unoId || player !== this.awaitUnoPlayer) return false;
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|**UNO!** ${player.name} is down to their last card!`);
this.awaitUnoPlayer = null;
this.unoId = null;
}
onCheckUno() {
if (!this.awaitUnoPlayer) return;
// if the previous player hasn't hit UNO before the next player plays something, they are forced to draw 2 cards;
if (this.awaitUnoPlayer !== this.currentPlayer) {
this.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${this.awaitUnoPlayer.name} forgot to say UNO! and is forced to draw 2 cards.`);
this.onDrawCard(this.awaitUnoPlayer, 2);
}
this.awaitUnoPlayer = null;
this.unoId = null;
}
onSendHand(user: User) {
if (this.state === 'signups') return false;
this.playerTable[user.id]?.sendDisplay();
}
onWin(player: UNOPlayer) {
this.sendToRoom(
Utils.html`|raw|
Congratulations to ${player.name} for winning the game of UNO!
`,
true
);
this.destroy();
}
override destroy() {
if (this.timer) clearTimeout(this.timer);
if (this.autostartTimer) clearTimeout(this.autostartTimer);
this.sendToRoom(`|uhtmlchange|uno-${this.gameNumber}|
The game of UNO has ended.
`, true);
this.setEnded();
for (const player of this.players) player.destroy();
this.room.game = null;
}
}
class UNOPlayer extends Rooms.RoomGamePlayer {
hand: Card[];
cardLock: string | null;
constructor(user: User, game: UNO) {
super(user, game);
this.hand = [];
this.cardLock = null;
}
canPlayWildFour() {
if (!this.game.topCard) {
// should never happen
throw new Error(`No top card in the discard pile.`);
}
const color = (this.game.topCard.changedColor || this.game.topCard.color);
if (this.hand.some(c => c.color === color)) return false;
return true;
}
hasCard(cardName: string) {
return this.hand.find(card => card.name === cardName);
}
removeCard(cardName: string) {
for (const [i, card] of this.hand.entries()) {
if (card.name === cardName) {
this.hand.splice(i, 1);
break;
}
}
}
buildHand() {
return Utils.sortBy(this.hand, card => [card.color, card.value])
.map((card, i) => cardHTML(card, i === this.hand.length - 1));
}
sendDisplay() {
const hand = this.buildHand().join('');
const players = `
Players (${this.game.playerCount}):
${this.game.getPlayers(true)}`;
const draw = '';
const pass = '';
const uno = ``;
if (!this.game.topCard) {
// should never happen
throw new Error(`No top card in the discard pile.`);
}
const top = `Top Card: ${this.game.topCard.name}`;
// clear previous display and show new display
this.sendRoom("|uhtmlchange|uno-hand|");
this.sendRoom(
`|uhtml|uno-hand|
${hand}
${this.game.currentPlayer === this ? `
${top}
` : ""}` +
`
${players}
` +
`${this.game.currentPlayer === this ? `
${draw}${pass} ${uno}
` : ""}
`
);
}
}
export const commands: Chat.ChatCommands = {
uno: {
// roomowner commands
off: 'disable',
disable(target, room, user) {
room = this.requireRoom();
this.checkCan('gamemanagement', null, room);
if (room.settings.unoDisabled) {
throw new Chat.ErrorMessage("UNO is already disabled in this room.");
}
room.settings.unoDisabled = true;
room.saveSettings();
return this.sendReply("UNO has been disabled for this room.");
},
on: 'enable',
enable(target, room, user) {
room = this.requireRoom();
this.checkCan('gamemanagement', null, room);
if (!room.settings.unoDisabled) {
throw new Chat.ErrorMessage("UNO is already enabled in this room.");
}
delete room.settings.unoDisabled;
room.saveSettings();
return this.sendReply("UNO has been enabled for this room.");
},
// moderation commands
new: 'create',
make: 'create',
createpublic: 'create',
makepublic: 'create',
createprivate: 'create',
makeprivate: 'create',
create(target, room, user, connection, cmd) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
if (room.settings.unoDisabled) throw new Chat.ErrorMessage("UNO is currently disabled for this room.");
if (room.game) throw new Chat.ErrorMessage("There is already a game in progress in this room.");
const suppressMessages = cmd.includes('private') || !(cmd.includes('public') || room.roomid === 'gamecorner');
let cap = parseInt(target);
if (isNaN(cap)) cap = 12;
if (cap < 2) cap = 2;
room.game = new UNO(room, cap, suppressMessages);
this.privateModAction(`A game of UNO was created by ${user.name}.`);
this.modlog('UNO CREATE');
},
cap: 'setcap',
setcap(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
const game = this.requireGame(UNO);
if (game.state !== 'signups') {
throw new Chat.ErrorMessage(`There is no UNO game in the signups phase in this room, so adjusting the player cap would do nothing.`);
}
let cap = parseInt(target);
if (isNaN(cap) || cap < 2) {
cap = 2;
}
game.playerCap = cap;
this.privateModAction(`The playercap was set to ${game.playerCap} by ${user.name}.`);
this.modlog('UNO PLAYERCAP');
},
start(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
const game = this.requireGame(UNO);
if (game.state !== 'signups') {
throw new Chat.ErrorMessage("There is no UNO game in signups phase in this room.");
}
game.onStart();
this.privateModAction(`The game of UNO was started by ${user.name}.`);
this.modlog('UNO START');
},
stop: 'end',
end(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
if (!room.game || room.game.gameid !== 'uno') {
throw new Chat.ErrorMessage("There is no UNO game going on in this room.");
}
room.game.destroy();
room.add("The game of UNO was forcibly ended.").update();
this.privateModAction(`The game of UNO was ended by ${user.name}.`);
this.modlog('UNO END');
},
autodq: 'timer',
timer(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
const game = this.requireGame(UNO);
const amount = parseInt(target);
if (!amount || amount < 5 || amount > 300) {
throw new Chat.ErrorMessage("The amount must be a number between 5 and 300.");
}
game.maxTime = amount;
if (game.timer) clearTimeout(game.timer);
game.timer = setTimeout(() => {
game.eliminate(game.currentPlayer?.id);
}, amount * 1000);
this.addModAction(`${user.name} has set the UNO automatic disqualification timer to ${amount} seconds.`);
this.modlog('UNO TIMER', null, `${amount} seconds`);
},
autostart(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
const game = this.requireGame(UNO);
if (toID(target) === 'off') {
if (!game.autostartTimer) throw new Chat.ErrorMessage("There is no autostart timer running on.");
this.addModAction(`${user.name} has turned off the UNO autostart timer.`);
clearTimeout(game.autostartTimer);
return;
}
const amount = parseInt(target);
if (!amount || amount < 30 || amount > 600) {
throw new Chat.ErrorMessage("The amount must be a number between 30 and 600 seconds.");
}
if (game.state !== 'signups') throw new Chat.ErrorMessage("The game of UNO has already started.");
if (game.autostartTimer) clearTimeout(game.autostartTimer);
game.autostartTimer = setTimeout(() => {
game.onStart(true);
}, amount * 1000);
this.addModAction(`${user.name} has set the UNO autostart timer to ${amount} seconds.`);
},
dq: 'disqualify',
disqualify(target, room, user) {
room = this.requireRoom();
this.checkCan('minigame', null, room);
const game = this.requireGame(UNO);
const disqualified = game.eliminate(toID(target));
if (disqualified === false) throw new Chat.ErrorMessage(`Unable to disqualify ${target}.`);
this.privateModAction(`${user.name} has disqualified ${disqualified} from the UNO game.`);
this.modlog('UNO DQ', toID(target));
room.add(`${disqualified} has been disqualified from the UNO game.`).update();
},
// player/user commands
j: 'join',
join(target, room, user) {
const game = this.requireGame(UNO);
this.checkChat();
if (!game.joinGame(user)) throw new Chat.ErrorMessage("Unable to join the game.");
return this.sendReply("You have joined the game of UNO.");
},
l: 'leave',
leave(target, room, user) {
const game = this.requireGame(UNO);
if (!game.leaveGame(user)) throw new Chat.ErrorMessage("Unable to leave the game.");
return this.sendReply("You have left the game of UNO.");
},
play(target, room, user) {
const game = this.requireGame(UNO);
if (!game) throw new Chat.ErrorMessage("There is no UNO game going on in this room right now.");
const player: UNOPlayer | undefined = game.playerTable[user.id];
if (!player) throw new Chat.ErrorMessage(`You are not in the game of UNO.`);
const error = game.onPlay(player, target);
if (typeof error === 'string') throw new Chat.ErrorMessage(error);
},
draw(target, room, user) {
room = this.requireRoom();
const game = room.getGame(UNO);
if (!game) throw new Chat.ErrorMessage("There is no UNO game going on in this room right now.");
const player: UNOPlayer | undefined = game.playerTable[user.id];
if (!player) throw new Chat.ErrorMessage(`You are not in the game of UNO.`);
const error = game.onDraw(player);
if (error) throw new Chat.ErrorMessage("You have already drawn a card this turn.");
},
pass(target, room, user) {
const game = this.requireGame(UNO);
if (!game) throw new Chat.ErrorMessage("There is no UNO game going on in this room right now.");
const player: UNOPlayer | undefined = game.playerTable[user.id];
if (!player) throw new Chat.ErrorMessage(`You are not in the game of UNO.`);
if (game.currentPlayer !== player) throw new Chat.ErrorMessage("It is currently not your turn.");
if (!player.cardLock) throw new Chat.ErrorMessage("You cannot pass until you draw a card.");
if (game.state === 'color') throw new Chat.ErrorMessage("You cannot pass until you choose a color.");
game.sendToRoom(`|c:|${Math.floor(Date.now() / 1000)}|~|${user.name} has passed.`);
game.nextTurn();
},
color(target, room, user) {
const game = this.requireGame(UNO);
const player: UNOPlayer | undefined = game.playerTable[user.id];
if (!player) throw new Chat.ErrorMessage(`You are not in the game of UNO.`);
let color: Color;
if (target === 'Red' || target === 'Green' || target === 'Blue' || target === 'Yellow' || target === 'Black') {
color = target;
} else {
throw new Chat.ErrorMessage(`"${target}" is not a valid color.`);
}
game.onSelectColor(player, color);
},
uno(target, room, user) {
const game = this.requireGame(UNO);
const player: UNOPlayer | undefined = game.playerTable[user.id];
if (!player) throw new Chat.ErrorMessage(`You are not in the game of UNO.`);
game.onUno(player, toID(target));
},
// information commands
'': 'hand',
hand(target, room, user) {
const game = this.requireGame(UNO);
game.onSendHand(user);
},
'c': 'cards',
cards(target, room, user) {
const game = this.requireGame(UNO);
if (!this.runBroadcast()) return false;
const players = `Players (${game.playerCount}):
${game.getPlayers(true)}`;
this.sendReplyBox(`
${players}
`);
},
players: 'getusers',
users: 'getusers',
getplayers: 'getusers',
getusers(target, room, user) {
const game = this.requireGame(UNO);
if (!this.runBroadcast()) return false;
this.sendReplyBox(`Players (${game.playerCount}):${game.getPlayers()}`);
},
help(target, room, user) {
this.parse('/help uno');
},
// suppression commands
suppress(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(UNO);
this.checkCan('minigame', null, room);
target = toID(target);
const state = target === 'on' ? true : target === 'off' ? false : undefined;
if (state === undefined) {
return this.sendReply(`Suppression of UNO game messages is currently ${game.suppressMessages ? 'on' : 'off'}.`);
}
if (state === game.suppressMessages) {
throw new Chat.ErrorMessage(`Suppression of UNO game messages is already ${game.suppressMessages ? 'on' : 'off'}.`);
}
game.suppressMessages = state;
this.addModAction(`${user.name} has turned ${state ? 'on' : 'off'} suppression of UNO game messages.`);
this.modlog('UNO SUPRESS', null, (state ? 'ON' : 'OFF'));
},
spectate(target, room, user) {
const game = this.requireGame(UNO);
if (!game) throw new Chat.ErrorMessage("There is no UNO game going on in this room right now.");
if (!game.suppressMessages) throw new Chat.ErrorMessage("The current UNO game is not suppressing messages.");
if (user.id in game.spectators) throw new Chat.ErrorMessage("You are already spectating this game.");
game.spectators[user.id] = 1;
this.sendReply("You are now spectating this private UNO game.");
},
unspectate(target, room, user) {
const game = this.requireGame(UNO);
if (!game) throw new Chat.ErrorMessage("There is no UNO game going on in this room right now.");
if (!game.suppressMessages) throw new Chat.ErrorMessage("The current UNO game is not suppressing messages.");
if (!(user.id in game.spectators)) throw new Chat.ErrorMessage("You are currently not spectating this game.");
delete game.spectators[user.id];
this.sendReply("You are no longer spectating this private UNO game.");
},
},
unohelp: [
`/uno create [player cap] - creates a new UNO game with an optional player cap (default player cap at 12). Use the command [createpublic] to force a public game or [createprivate] to force a private game. Requires: % @ # ~`,
`/uno setcap [player cap] - adjusts the player cap of the current UNO game. Requires: % @ # ~`,
`/uno timer [amount] - sets an auto disqualification timer for [amount] seconds. Requires: % @ # ~`,
`/uno autostart [amount] - sets an auto starting timer for [amount] seconds. Requires: % @ # ~`,
`/uno end - ends the current game of UNO. Requires: % @ # ~`,
`/uno start - starts the current game of UNO. Requires: % @ # ~`,
`/uno disqualify [player] - disqualifies the player from the game. Requires: % @ # ~`,
`/uno hand - displays your own hand.`,
`/uno cards - displays the number of cards for each player.`,
`/uno getusers - displays the players still in the game.`,
`/uno [spectate|unspectate] - spectate / unspectate the current private UNO game.`,
`/uno suppress [on|off] - Toggles suppression of game messages.`,
],
};
export const roomSettings: Chat.SettingsHandler = room => ({
label: "UNO",
permission: 'editroom',
options: [
[`disabled`, room.settings.unoDisabled || 'uno disable'],
[`enabled`, !room.settings.unoDisabled || 'uno enable'],
],
});