add bots to a game

This commit is contained in:
AlecM33
2023-01-30 12:40:28 -05:00
parent e362f6bdb2
commit 79309f5062
16 changed files with 106 additions and 43 deletions

View File

@@ -81,7 +81,8 @@ export const globals = {
PLAYER: 'player', PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod', TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed', KILLED_PLAYER: 'killed',
SPECTATOR: 'spectator' SPECTATOR: 'spectator',
BOT: 'bot'
}, },
ENVIRONMENT: { ENVIRONMENT: {
LOCAL: 'local', LOCAL: 'local',
@@ -92,6 +93,7 @@ export const globals = {
moderator: ' \uD83D\uDC51', moderator: ' \uD83D\uDC51',
'temp mod': ' \uD83C\uDFAE\uD83D\uDC51', 'temp mod': ' \uD83C\uDFAE\uD83D\uDC51',
spectator: ' \uD83D\uDC7B', spectator: ' \uD83D\uDC7B',
killed: '\uD83D\uDC80' killed: '\uD83D\uDC80',
bot: '\uD83E\uDD16'
} }
}; };

View File

@@ -1,9 +1,10 @@
export class Game { export class Game {
constructor (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams = null) { constructor (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams = null, isTestGame = false) {
this.deck = deck; this.deck = deck;
this.hasTimer = hasTimer; this.hasTimer = hasTimer;
this.timerParams = timerParams; this.timerParams = timerParams;
this.hasDedicatedModerator = hasDedicatedModerator; this.hasDedicatedModerator = hasDedicatedModerator;
this.moderatorName = moderatorName; this.moderatorName = moderatorName;
this.isTestGame = isTestGame;
} }
} }

View File

@@ -38,6 +38,13 @@ export const HTMLFragments = {
<div> <div>
<input type="text" id="moderator-name" autocomplete='given-name' placeholder="enter your name..."> <input type="text" id="moderator-name" autocomplete='given-name' placeholder="enter your name...">
</div> </div>
<div>
<label for="test-game">Populate the game with bots?</label>
<select id="test-game">
<option value="no" selected>No</option>
<option value="yes">Yes</option>
</select>
</div>
</div>`, </div>`,
START_GAME_PROMPT: START_GAME_PROMPT:
`<div> `<div>
@@ -120,7 +127,7 @@ export const HTMLFragments = {
TRANSFER_MOD_MODAL: TRANSFER_MOD_MODAL:
`<div id='transfer-mod-modal-background' class='modal-background'></div> `<div id='transfer-mod-modal-background' class='modal-background'></div>
<div tabindex='-1' id='transfer-mod-modal' class='modal'> <div tabindex='-1' id='transfer-mod-modal' class='modal'>
<h3>Transfer Mod Powers &#128081;</h3> <h2>Select a new moderator &#128081;</h2>
<div id='transfer-mod-modal-content'></div> <div id='transfer-mod-modal-content'></div>
<div class='modal-button-container'> <div class='modal-button-container'>
<button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button> <button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button>

View File

@@ -130,7 +130,8 @@ export class GameCreationStepManager {
this.currentGame.hasTimer, this.currentGame.hasTimer,
this.currentGame.hasDedicatedModerator, this.currentGame.hasDedicatedModerator,
this.currentGame.moderatorName, this.currentGame.moderatorName,
this.currentGame.timerParams this.currentGame.timerParams,
this.currentGame.isTestGame
) )
) )
) )
@@ -304,6 +305,12 @@ function renderNameStep (containerId, step, game, steps) {
const nameInput = document.querySelector('#moderator-name'); const nameInput = document.querySelector('#moderator-name');
nameInput.value = game.moderatorName; nameInput.value = game.moderatorName;
nameInput.addEventListener('keyup', steps['4'].forwardHandler); nameInput.addEventListener('keyup', steps['4'].forwardHandler);
const testGameInput = document.getElementById('test-game');
testGameInput.onchange = (event) => {
game.isTestGame = testGameInput.value === 'yes';
};
testGameInput.value = game.isTestGame ? 'yes' : 'no';
} }
function renderModerationTypeStep (game, containerId, stepNumber) { function renderModerationTypeStep (game, containerId, stepNumber) {
@@ -390,22 +397,28 @@ function renderReviewAndCreateStep (containerId, stepNumber, game, deckManager)
div.innerHTML = div.innerHTML =
'<div>' + '<div>' +
"<label for='mod-name'>Your name</label>" + "<label for='mod-name'>Your name:</label>" +
"<div id='mod-name' class='review-option'></div>" + "<div id='mod-name' class='review-option'></div>" +
'</div>' + '</div>' +
'<div>' + '<div>' +
"<label for='mod-option'>Moderation</label>" + "<label for='test-game'>Populate game with bots?</label>" +
"<div id='test-game' class='review-option'></div>" +
'</div>' +
'<div>' +
"<label for='mod-option'>Moderation:</label>" +
"<div id='mod-option' class='review-option'></div>" + "<div id='mod-option' class='review-option'></div>" +
'</div>' + '</div>' +
'<div>' + '<div>' +
"<label for='timer-option'>Timer</label>" + "<label for='timer-option'>Timer:</label>" +
"<div id='timer-option' class='review-option'></div>" + "<div id='timer-option' class='review-option'></div>" +
'</div>' + '</div>' +
'<div>' + '<div>' +
"<label id='roles-option-label' for='roles-option'>Game Deck</label>" + "<label id='roles-option-label' for='roles-option'>Game Deck:</label>" +
"<div id='roles-option' class='review-option'></div>" + "<div id='roles-option' class='review-option'></div>" +
'</div>'; '</div>';
div.querySelector('#test-game').innerText = game.isTestGame ? 'Yes' : 'No';
div.querySelector('#mod-option').innerText = game.hasDedicatedModerator div.querySelector('#mod-option').innerText = game.hasDedicatedModerator
? "Dedicated Moderator - don't deal me a card." ? "Dedicated Moderator - don't deal me a card."
: 'Temporary Moderator - deal me into the game.'; : 'Temporary Moderator - deal me into the game.';

View File

@@ -28,8 +28,7 @@ export class Ended {
const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null;
renderGroupOfPlayers( renderGroupOfPlayers(
this.stateBucket.currentGameState.people.filter( this.stateBucket.currentGameState.people.filter(
p => p.userType === globals.USER_TYPES.PLAYER p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| p.killed || p.killed
), ),
this.stateBucket.currentGameState.accessCode, this.stateBucket.currentGameState.accessCode,

View File

@@ -101,8 +101,7 @@ export class InProgress {
: null; : null;
this.renderGroupOfPlayers( this.renderGroupOfPlayers(
this.stateBucket.currentGameState.people.filter( this.stateBucket.currentGameState.people.filter(
p => p.userType === globals.USER_TYPES.PLAYER p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| p.killed || p.killed
), ),
this.killPlayerHandlers, this.killPlayerHandlers,
@@ -168,7 +167,9 @@ export class InProgress {
if (killedPerson) { if (killedPerson) {
killedPerson.out = true; killedPerson.out = true;
killedPerson.killed = true; killedPerson.killed = true;
killedPerson.userType = globals.USER_TYPES.KILLED_PLAYER; killedPerson.userType = killedPerson.userType === globals.USER_TYPES.BOT
? globals.USER_TYPES.KILLED_BOT
: globals.USER_TYPES.KILLED_PLAYER;
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPerson.name + ' killed.', 'success', true, true, 'medium'); toast(killedPerson.name + ' killed.', 'success', true, true, 'medium');
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED);
@@ -253,14 +254,12 @@ export class InProgress {
}); });
const teamGood = this.stateBucket.currentGameState.people.filter( const teamGood = this.stateBucket.currentGameState.people.filter(
(p) => p.alignment === globals.ALIGNMENT.GOOD (p) => p.alignment === globals.ALIGNMENT.GOOD
&& (p.userType === globals.USER_TYPES.PLAYER && ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| p.killed) || p.killed)
); );
const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL
&& (p.userType === globals.USER_TYPES.PLAYER && ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
|| p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| p.killed) || p.killed)
); );
this.renderGroupOfPlayers( this.renderGroupOfPlayers(
@@ -385,7 +384,8 @@ export class InProgress {
); );
if (document.querySelectorAll('.potential-moderator').length === 0) { if (document.querySelectorAll('.potential-moderator').length === 0) {
document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.'; document.getElementById('transfer-mod-modal-content').innerText =
'There is nobody available to transfer to. Only spectators or killed players (who are not bots) can be mods.';
} }
} }

View File

@@ -82,7 +82,7 @@ export class Lobby {
const lobbyPlayersContainer = this.container.querySelector('#lobby-players'); const lobbyPlayersContainer = this.container.querySelector('#lobby-players');
const sorted = this.stateBucket.currentGameState.people.sort( const sorted = this.stateBucket.currentGameState.people.sort(
function (a, b) { function (a, b) {
if (a.userType === globals.USER_TYPES.MODERATOR) { if (a.userType === globals.USER_TYPES.MODERATOR || a.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
return -1; return -1;
} }
return 1; return 1;
@@ -92,7 +92,7 @@ export class Lobby {
lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType)); lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType));
} }
const playerCount = this.stateBucket.currentGameState.people.filter( const playerCount = this.stateBucket.currentGameState.people.filter(
p => p.userType === globals.USER_TYPES.PLAYER || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR
).length; ).length;
document.querySelector("label[for='lobby-players']").innerText = document.querySelector("label[for='lobby-players']").innerText =
'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; 'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)';
@@ -194,7 +194,7 @@ function renderLobbyPerson (name, userType) {
personNameEl.innerText = name; personNameEl.innerText = name;
personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType];
el.classList.add('lobby-player'); el.classList.add('lobby-player');
if (userType === globals.USER_TYPES.MODERATOR) { if (userType === globals.USER_TYPES.MODERATOR || userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
el.classList.add('moderator'); el.classList.add('moderator');
} }

View File

@@ -209,6 +209,12 @@ a {
text-decoration: none; text-decoration: none;
} }
.checkbox {
border-radius: 5px;
border: 2px solid #d7d7d7;
padding: 10px;
}
textarea, input { textarea, input {
font-family: 'signika-negative', sans-serif; font-family: 'signika-negative', sans-serif;
font-size: 16px; font-size: 16px;
@@ -572,7 +578,8 @@ input {
} }
.good, .compact-card.good .card-role { .good, .compact-card.good .card-role {
color: #5f7cfb; color: #5f7cfb !important;
font-weight: bold;
} }
.good-players, #deck-good { .good-players, #deck-good {
@@ -584,7 +591,8 @@ input {
} }
.evil, .compact-card.evil .card-role { .evil, .compact-card.evil .card-role {
color: #e73333; color: #dd2929 !important;
font-weight: bold;
} }
@keyframes placeholder { @keyframes placeholder {

View File

@@ -7,7 +7,7 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background-color: #2d2c38; background-color: #16141e;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
max-width: 25em; max-width: 25em;

View File

@@ -314,6 +314,15 @@ option {
margin: 0 auto; margin: 0 auto;
} }
#step-4 div:nth-child(2) {
margin-top: 25px;
}
#step-4 label[for="test-game"] {
display: block;
margin-bottom: 10px;
}
#step-4 input { #step-4 input {
padding: 15px 5px; padding: 15px 5px;
width: 95%; width: 95%;

View File

@@ -3,7 +3,7 @@
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: #171522; background-color: #000000;
color: #e7e7e7; color: #e7e7e7;
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
@@ -19,6 +19,12 @@
.potential-moderator { .potential-moderator {
margin: 0.5em auto; margin: 0.5em auto;
border: 2px solid #46455299;
background: #4645523b;
}
#transfer-mod-modal h2 {
margin-bottom: 15px;
} }
#lobby-players { #lobby-players {
@@ -615,14 +621,14 @@ label[for='moderator'] {
canvas { canvas {
border-radius: 5px; border-radius: 5px;
margin: 1em; margin: 20px 5px;
} }
.game-player { .game-player {
border-left: 3px solid #21ba45; border-left: 3px solid #21ba45;
display: flex; display: flex;
color: #d7d7d7; color: #f1f1f1;
background-color: #171522; background-color: #000000;
align-items: center; align-items: center;
padding: 0 5px; padding: 0 5px;
justify-content: space-between; justify-content: space-between;
@@ -714,6 +720,7 @@ canvas {
.game-player-role { .game-player-role {
min-width: 7em; min-width: 7em;
color: #bbbbbb;
} }
.game-player.killed { .game-player.killed {
@@ -800,9 +807,10 @@ canvas {
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
min-height: 25em; min-height: 25em;
background-color: #3b3a4a; border: 1px solid #46455299;
background: #4645523b;
max-width: 35em; max-width: 35em;
min-width: 17em; min-width: 19em;
margin-top: 1em; margin-top: 1em;
} }

View File

@@ -100,7 +100,9 @@ const globals = {
PLAYER: 'player', PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod', TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed', KILLED_PLAYER: 'killed',
SPECTATOR: 'spectator' KILLED_BOT: 'killed bot',
SPECTATOR: 'spectator',
BOT: 'bot'
}, },
ERROR_MESSAGE: { ERROR_MESSAGE: {
GAME_IS_FULL: 'This game is full', GAME_IS_FULL: 'This game is full',

View File

@@ -9,7 +9,8 @@ class Game {
hasDedicatedModerator, hasDedicatedModerator,
originalModeratorId, originalModeratorId,
createTime, createTime,
timerParams = null timerParams = null,
isTestGame = false
) { ) {
this.accessCode = accessCode; this.accessCode = accessCode;
this.status = status; this.status = status;
@@ -26,7 +27,7 @@ class Game {
this.previousModeratorId = null; this.previousModeratorId = null;
this.createTime = createTime; this.createTime = createTime;
this.timerParams = timerParams; this.timerParams = timerParams;
this.isFull = this.gameSize === 1 && !this.hasDedicatedModerator; this.isFull = (this.gameSize === 1 && !this.hasDedicatedModerator) || isTestGame;
this.timeRemaining = null; this.timeRemaining = null;
} }
} }

View File

@@ -6,17 +6,19 @@ class GameCreationRequest {
hasTimer, hasTimer,
timerParams, timerParams,
moderatorName, moderatorName,
hasDedicatedModerator hasDedicatedModerator,
isTestGame
) { ) {
this.deck = deck; this.deck = deck;
this.hasTimer = hasTimer; this.hasTimer = hasTimer;
this.timerParams = timerParams; this.timerParams = timerParams;
this.moderatorName = moderatorName; this.moderatorName = moderatorName;
this.hasDedicatedModerator = hasDedicatedModerator; this.hasDedicatedModerator = hasDedicatedModerator;
this.isTestGame = isTestGame;
} }
static validate = (gameParams) => { static validate = (gameParams) => {
const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName', 'hasDedicatedModerator']; const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName', 'hasDedicatedModerator', 'isTestGame'];
if (gameParams === null if (gameParams === null
|| typeof gameParams !== 'object' || typeof gameParams !== 'object'
|| expectedKeys.some((key) => !Object.keys(gameParams).includes(key)) || expectedKeys.some((key) => !Object.keys(gameParams).includes(key))
@@ -31,6 +33,7 @@ class GameCreationRequest {
function valid (gameParams) { function valid (gameParams) {
return typeof gameParams.hasTimer === 'boolean' return typeof gameParams.hasTimer === 'boolean'
&& typeof gameParams.isTestGame === 'boolean'
&& typeof gameParams.hasDedicatedModerator === 'boolean' && typeof gameParams.hasDedicatedModerator === 'boolean'
&& typeof gameParams.moderatorName === 'string' && typeof gameParams.moderatorName === 'string'
&& gameParams.moderatorName.length > 0 && gameParams.moderatorName.length > 0

View File

@@ -86,7 +86,9 @@ const Events = [
stateChange: async (game, socketArgs, vars) => { stateChange: async (game, socketArgs, vars) => {
const person = game.people.find((person) => person.id === socketArgs.personId); const person = game.people.find((person) => person.id === socketArgs.personId);
if (person && !person.out) { if (person && !person.out) {
person.userType = globals.USER_TYPES.KILLED_PLAYER; person.userType = person.userType === globals.USER_TYPES.BOT
? globals.USER_TYPES.KILLED_BOT
: globals.USER_TYPES.KILLED_PLAYER;
person.out = true; person.out = true;
person.killed = true; person.killed = true;
} }

View File

@@ -59,8 +59,10 @@ class GameManager {
gameParams.hasTimer, gameParams.hasTimer,
gameParams.timerParams, gameParams.timerParams,
gameParams.moderatorName, gameParams.moderatorName,
gameParams.hasDedicatedModerator gameParams.hasDedicatedModerator,
gameParams.isTestGame
); );
console.log(req.isTestGame);
const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL);
if (newAccessCode === null) { if (newAccessCode === null) {
return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE);
@@ -76,14 +78,15 @@ class GameManager {
const newGame = new Game( const newGame = new Game(
newAccessCode, newAccessCode,
globals.STATUS.LOBBY, globals.STATUS.LOBBY,
initializePeopleForGame(req.deck, moderator, this.shuffle), initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame),
req.deck, req.deck,
req.hasTimer, req.hasTimer,
moderator.id, moderator.id,
req.hasDedicatedModerator, req.hasDedicatedModerator,
moderator.id, moderator.id,
new Date().toJSON(), new Date().toJSON(),
req.timerParams req.timerParams,
req.isTestGame
); );
await this.eventManager.publisher.set(newAccessCode, JSON.stringify(newGame), { await this.eventManager.publisher.set(newAccessCode, JSON.stringify(newGame), {
EX: globals.STALE_GAME_SECONDS EX: globals.STALE_GAME_SECONDS
@@ -242,6 +245,10 @@ class GameManager {
game.people[i].userType = globals.USER_TYPES.PLAYER; game.people[i].userType = globals.USER_TYPES.PLAYER;
game.people[i].out = false; game.people[i].out = false;
} }
if (game.people[i].userType === globals.USER_TYPES.KILLED_BOT) {
game.people[i].userType = globals.USER_TYPES.BOT;
game.people[i].out = false;
}
game.people[i].revealed = false; game.people[i].revealed = false;
game.people[i].killed = false; game.people[i].killed = false;
if (game.people[i].gameRole) { if (game.people[i].gameRole) {
@@ -314,7 +321,7 @@ function initializeModerator (name, hasDedicatedModerator) {
return new Person(createRandomId(), createRandomId(), name, userType); return new Person(createRandomId(), createRandomId(), name, userType);
} }
function initializePeopleForGame (uniqueRoles, moderator, shuffle) { function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame) {
const people = []; const people = [];
const cards = []; const cards = [];
@@ -335,10 +342,11 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle) {
createRandomId(), createRandomId(),
createRandomId(), createRandomId(),
UsernameGenerator.generate(), UsernameGenerator.generate(),
globals.USER_TYPES.PLAYER, isTestGame ? globals.USER_TYPES.BOT : globals.USER_TYPES.PLAYER,
cards[j].role, cards[j].role,
cards[j].description, cards[j].description,
cards[j].team cards[j].team,
isTestGame
); );
person.customRole = cards[j].custom; person.customRole = cards[j].custom;
person.hasEnteredName = false; person.hasEnteredName = false;