Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 24 additions & 14 deletions server/multiplayer/ServerMultiplayerRoomMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,46 +273,51 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass {
}

setCategories ({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setCategories({ userId, username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents });
}

setDifficulties ({ userId, username }, { difficulties }) {
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setDifficulties({ userId, username }, { difficulties });
}

setMode ({ userId, username }, { mode }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
if (this.mode !== MODE_ENUM.SET_NAME && this.mode !== MODE_ENUM.RANDOM) { return; }
super.setMode({ userId, username }, { mode });
this.adjustQuery(['setName'], [this.query.setName]);
}

setPacketNumbers ({ userId, username }, { packetNumbers }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setPacketNumbers({ userId, username }, { doNotFetch: false, packetNumbers });
}

setReadingSpeed ({ userId, username }, { readingSpeed }) {
if (this.isPermanent || !this.allowed(userId)) { return false; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return false; }
super.setReadingSpeed({ userId, username }, { readingSpeed });
}

async setSetName ({ userId, username }, { setName }) {
if (!this.allowed(userId)) { return; }
if (this.settings.controlled || !this.allowed(userId)) { return; }
if (!this.packetList) { return; }
if (!this.packetList.includes(setName)) { return; }
super.setSetName({ userId, username }, { doNotFetch: false, setName });
}

setStrictness ({ userId, username }, { strictness }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setStrictness({ userId, username }, { strictness });
}

setMinYear ({ userId, username }, { minYear }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setMinYear({ userId, username }, { minYear });
}

setMaxYear ({ userId, username }, { maxYear }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.setMaxYear({ userId, username }, { maxYear });
}

Expand Down Expand Up @@ -341,7 +346,7 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass {
}

toggleEnableBonuses ({ userId, username }, { enableBonuses }) {
if (this.isPermanent || !this.allowed(userId)) { return; }
if (this.isPermanent || this.settings.controlled || !this.allowed(userId)) { return; }
super.toggleEnableBonuses({ userId, username }, { enableBonuses });
}

Expand All @@ -363,20 +368,25 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass {
}

togglePowermarkOnly ({ userId, username }, { powermarkOnly }) {
if (!this.allowed(userId)) { return; }
if (this.settings.controlled || !this.allowed(userId)) { return; }
super.togglePowermarkOnly({ userId, username }, { powermarkOnly });
}

toggleSkip ({ userId, username }, { skip }) {
if (!this.allowed(userId)) { return; }
if (this.settings.controlled || !this.allowed(userId)) { return; }
super.toggleSkip({ userId, username }, { skip });
}

toggleStandardOnly ({ userId, username }, { standardOnly }) {
if (!this.allowed(userId)) { return; }
if (this.settings.controlled || !this.allowed(userId)) { return; }
super.toggleStandardOnly({ userId, username }, { doNotFetch: false, standardOnly });
}

toggleStopOnPower ({ userId, username }, { stopOnPower }) {
if (this.settings.controlled || !this.allowed(userId)) { return; }
super.toggleStopOnPower({ userId, username }, { stopOnPower });
}

togglePublic ({ userId, username }, { public: isPublic }) {
if (this.isPermanent || this.settings.controlled) { return; }
this.settings.public = isPublic;
Expand All @@ -389,12 +399,12 @@ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass {
}

toggleRebuzz ({ userId, username }, { rebuzz }) {
if (!this.allowed(userId)) { return false; }
if (this.settings.controlled || !this.allowed(userId)) { return false; }
super.toggleRebuzz({ userId, username }, { rebuzz });
}

toggleTimer ({ userId, username }, { timer }) {
if (this.settings.public || !this.allowed(userId)) { return; }
if (this.settings.public || this.settings.controlled || !this.allowed(userId)) { return; }
super.toggleTimer({ userId, username }, { timer });
}

Expand Down
10 changes: 10 additions & 0 deletions server/multiplayer/ServerPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ export default class ServerPlayer extends Player {
constructor (userId) {
super(userId, USERNAME_MAX_LENGTH);
this.online = true;
this.consecutiveEarlyCorrect = 0;
}

resetBotDetectionCounter () {
this.consecutiveEarlyCorrect = 0;
}

recordEarlyCorrect () {
this.consecutiveEarlyCorrect++;
return this.consecutiveEarlyCorrect >= 3;
}
}
26 changes: 24 additions & 2 deletions server/multiplayer/ServerTossupBonusRoom.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EARLY_CORRECT_CELERITY_THRESHOLD } from './constants.js';
import ServerMultiplayerRoomMixin from './ServerMultiplayerRoomMixin.js';
import TossupBonusRoom from '../../quizbowl/TossupBonusRoom.js';
import { QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../quizbowl/constants.js';
Expand All @@ -8,7 +9,6 @@ export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(To
}

giveAnswerLiveUpdate ({ userId, username }, { givenAnswer }) {
// Allow live updates during bonuses (when buzzedIn is null) or from the user who buzzed
switch (this.currentQuestionType) {
case QUESTION_TYPE_ENUM.TOSSUP:
if (userId !== this.buzzedIn) { return false; }
Expand All @@ -20,8 +20,30 @@ export default class ServerTossupBonusRoom extends ServerMultiplayerRoomMixin(To
super.giveAnswerLiveUpdate({ userId, username }, { givenAnswer });
}

giveTossupAnswer ({ userId, username }, { givenAnswer }) {
if (typeof givenAnswer !== 'string') { return false; }
if (this.buzzedIn !== userId) { return false; }

if (Object.keys(this.tossup || {}).length === 0) { return; }

const { celerity, directive } = this.scoreTossup({ givenAnswer });

if (directive === 'accept' && celerity >= EARLY_CORRECT_CELERITY_THRESHOLD) {
const shouldKick = this.players[userId].recordEarlyCorrect();
if (shouldKick) {
console.log(`Bot detected: User ${userId} (${username}) got 3 correct with abnormally high celerity. Kicking.`);
this.emitMessage({ type: 'bot-kicked', userId, username });
setTimeout(() => this.closeConnection({ userId, username }), 100);
return;
}
} else {
this.players[userId].resetBotDetectionCounter();
}

super.giveTossupAnswer({ userId, username }, { givenAnswer });
}

next ({ userId, username }) {
// prevents spam-skipping trolls
if (
this.currentQuestionType === QUESTION_TYPE_ENUM.TOSSUP &&
this.tossupProgress === TOSSUP_PROGRESS_ENUM.READING &&
Expand Down
52 changes: 52 additions & 0 deletions server/multiplayer/configure-permanent-room.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export function configurePermanentRoomSettings (room, roomName) {
room.settings.public = true;
room.settings.controlled = true;

room.settings.skip = false;
room.settings.rebuzz = false;

room.settings.timer = true;

const difficultyConfig = getPermanentRoomDifficulty(roomName);
if (difficultyConfig) {
room.query.difficulties = difficultyConfig.difficulties;
if (room.adjustQuery) {
room.adjustQuery(['difficulties'], [difficultyConfig.difficulties]);
}
}
}

function getPermanentRoomDifficulty (roomName) {
switch (roomName) {
case 'msquizbowl':
return { difficulties: [1] };
case 'hsquizbowl':
return { difficulties: [2, 3, 4, 5] };
case 'collegequizbowl':
return { difficulties: [6, 7, 8, 9] };
case 'literature':
case 'history':
case 'science':
case 'fine-arts':
case 'rmpss':
case 'geography':
case 'pop-culture':
return { difficulties: [2, 3, 4, 5] };
case 'verified-msquizbowl':
return { difficulties: [1] };
case 'verified-hsquizbowl':
return { difficulties: [2, 3, 4, 5] };
case 'verified-collegequizbowl':
return { difficulties: [6, 7, 8, 9] };
case 'verified-literature':
case 'verified-history':
case 'verified-science':
case 'verified-fine-arts':
case 'verified-rmpss':
case 'verified-geography':
case 'verified-pop-culture':
return { difficulties: [2, 3, 4, 5] };
default:
return null;
}
}
2 changes: 2 additions & 0 deletions server/multiplayer/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const USERNAME_MAX_LENGTH = 32;
export const MAX_ONLINE_PLAYERS = 500;
export const MAX_CONNECTIONS_PER_IP = 50;

export const EARLY_CORRECT_CELERITY_THRESHOLD = 0.95;

/**
* List of multiplayer permanent room names.
*/
Expand Down
9 changes: 7 additions & 2 deletions server/multiplayer/handle-wss-connection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MAX_ONLINE_PLAYERS, MAX_CONNECTIONS_PER_IP, PERMANENT_ROOMS, VERIFIED_ROOMS, ROOM_NAME_MAX_LENGTH } from './constants.js';
import ServerTossupBonusRoom from './ServerTossupBonusRoom.js';
import { configurePermanentRoomSettings } from './configure-permanent-room.js';
import { checkToken } from '../authentication.js';
import CategoryManager from '../../quizbowl/category-manager.js';
import getRandomName from '../../quizbowl/get-random-name.js';
Expand All @@ -22,15 +23,19 @@ export const tossupBonusRooms = {};
const connectionsByIp = new Map();
for (const room of PERMANENT_ROOMS) {
const { name, categories, subcategories } = room;
tossupBonusRooms[name] = new ServerTossupBonusRoom(
const permanentRoom = new ServerTossupBonusRoom(
name, Symbol('unique permanent room owner'), true, new CategoryManager(categories, subcategories), false
);
configurePermanentRoomSettings(permanentRoom, name);
tossupBonusRooms[name] = permanentRoom;
}
for (const room of VERIFIED_ROOMS) {
const { name, categories, subcategories } = room;
tossupBonusRooms[name] = new ServerTossupBonusRoom(
const verifiedRoom = new ServerTossupBonusRoom(
name, Symbol('unique verified room owner'), true, new CategoryManager(categories, subcategories), true
);
configurePermanentRoomSettings(verifiedRoom, name);
tossupBonusRooms[name] = verifiedRoom;
}

/**
Expand Down