From 7fca9296b9281267329f6d220ef862a36aeed1ee Mon Sep 17 00:00:00 2001 From: QuinnWuest Date: Wed, 10 Jun 2026 14:16:34 -0400 Subject: [PATCH 1/3] Multiple card changes - Fix unaccounted card overlap check. (Lady of the Lake and Sire of the Sea had a check to avoid overlap, but Ref of the Rain lacked this check.) - Add lobby option to randomize starting card positions. (Cards will never overlap) --- assets/scripts/lobby/buttonEventListeners2.js | 1 + assets/scripts/lobby/publicData/avalon.js | 24 ++++++++-- .../gameEngine/cards/avalon/ladyOfTheLake.ts | 6 +-- .../gameEngine/cards/avalon/refOfTheRain.ts | 6 +-- .../gameEngine/cards/avalon/sireOfTheSea.ts | 12 +---- src/gameplay/gameEngine/cards/types.ts | 2 +- src/gameplay/gameEngine/game.ts | 47 +++++++++++++++++-- src/gameplay/gameEngine/room.ts | 2 + src/sockets/sockets.ts | 3 ++ src/views/lobby.ejs | 3 ++ 10 files changed, 79 insertions(+), 27 deletions(-) diff --git a/assets/scripts/lobby/buttonEventListeners2.js b/assets/scripts/lobby/buttonEventListeners2.js index 2407d0eec..4dde434c1 100644 --- a/assets/scripts/lobby/buttonEventListeners2.js +++ b/assets/scripts/lobby/buttonEventListeners2.js @@ -59,6 +59,7 @@ async function greenButtonFunction() { assassination: ((parseInt($('#startGameOptionsAssassinationPhaseTimeoutMin').val()) * 60 + parseInt($('#startGameOptionsAssassinationPhaseTimeoutSec').val())) * 1000).toString(), }, anonymousMode: $('#startGameOptionsAnonymousMode')[0].checked, + randomizeCardPosition: $('#startGameOptionsRandomizeCardPosition')[0].checked, }; socket.emit('startGame', startGameData); } else if (await confirmUserClick('yes')) { diff --git a/assets/scripts/lobby/publicData/avalon.js b/assets/scripts/lobby/publicData/avalon.js index 997d4738f..2552db447 100644 --- a/assets/scripts/lobby/publicData/avalon.js +++ b/assets/scripts/lobby/publicData/avalon.js @@ -16,8 +16,8 @@ function runPublicDataAvalon(gameDataInc) { } // Reset cards container - $('.playerDiv').find('.cardsContainer')[0].innerHTML = ''; - + $('.playerDiv').find('.cardsContainer').html = ''; + // Draw cards: for (const key in gd.publicData.cards) { if (gd.publicData.cards.hasOwnProperty(key) === true) { @@ -39,10 +39,24 @@ function runPublicDataAvalon(gameDataInc) { const padding = " "; + + const playerDiv = $('.playerDiv')[index]; + if (!playerDiv) { + console.log('Card index out of bounds', { + index, card: key, playerDivCount: $('.playerDiv').length, cardData: gd.publicData.cards[key] + }); + continue; + } + const cardsContainer = $(playerDiv).find('.cardsContainer')[0]; + if (!cardsContainer) { + console.log('No cardsContainer found for player', { + index, card: key, playerDiv + }); + continue; + } - $($('.playerDiv')[index]).find('.cardsContainer')[0].innerHTML += card; - $($('.playerDiv')[index]).find('.cardsContainer')[0].innerHTML += - padding; + cardsContainer.innerHTML += card; + cardsContainer.innerHTML += padding; // Initialise the tooltip. $('.cardObject').tooltip(); diff --git a/src/gameplay/gameEngine/cards/avalon/ladyOfTheLake.ts b/src/gameplay/gameEngine/cards/avalon/ladyOfTheLake.ts index e18e1605e..c3bf7efd1 100644 --- a/src/gameplay/gameEngine/cards/avalon/ladyOfTheLake.ts +++ b/src/gameplay/gameEngine/cards/avalon/ladyOfTheLake.ts @@ -24,10 +24,8 @@ class LadyOfTheLake implements ICard { this.thisRoom = thisRoom; } - initialise(): void { - this.setHolder( - (this.thisRoom.teamLeader + 1) % this.thisRoom.playersInGame.length, - ); + initialise(indexOfPlayerHolding: number): void { + this.setHolder(indexOfPlayerHolding); } setHolder(index: number): void { diff --git a/src/gameplay/gameEngine/cards/avalon/refOfTheRain.ts b/src/gameplay/gameEngine/cards/avalon/refOfTheRain.ts index 4a39eade0..168bea723 100644 --- a/src/gameplay/gameEngine/cards/avalon/refOfTheRain.ts +++ b/src/gameplay/gameEngine/cards/avalon/refOfTheRain.ts @@ -24,10 +24,8 @@ class RefOfTheRain implements ICard { this.thisRoom = thisRoom; } - initialise(): void { - this.setHolder( - (this.thisRoom.teamLeader + 1) % this.thisRoom.playersInGame.length, - ); + initialise(indexOfPlayerHolding: number): void { + this.setHolder(indexOfPlayerHolding); } setHolder(index: number): void { diff --git a/src/gameplay/gameEngine/cards/avalon/sireOfTheSea.ts b/src/gameplay/gameEngine/cards/avalon/sireOfTheSea.ts index b71648394..a412af0d4 100644 --- a/src/gameplay/gameEngine/cards/avalon/sireOfTheSea.ts +++ b/src/gameplay/gameEngine/cards/avalon/sireOfTheSea.ts @@ -24,16 +24,8 @@ class SireOfTheSea implements ICard { this.thisRoom = thisRoom; } - initialise(): void { - // If lady of the sea is in the game, give the card to the next person. - let addOne = 0; - if (this.thisRoom.options.includes('Lady of the Lake')) { - addOne = 1; - } - this.setHolder( - (this.thisRoom.teamLeader + 1 + addOne) % - this.thisRoom.playersInGame.length, - ); + initialise(indexOfPlayerHolding: number): void { + this.setHolder(indexOfPlayerHolding); } setHolder(index: number): void { diff --git a/src/gameplay/gameEngine/cards/types.ts b/src/gameplay/gameEngine/cards/types.ts index 27a0fcf6b..514951699 100644 --- a/src/gameplay/gameEngine/cards/types.ts +++ b/src/gameplay/gameEngine/cards/types.ts @@ -9,7 +9,7 @@ export enum Card { export interface ICard { card: Card; - initialise(): void; + initialise(indexOfPlayerHolding: number): void; setHolder(index: number): void; diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index 8acafab26..eaf400f81 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -126,6 +126,8 @@ class Game extends Room { anonymizer: Anonymizer = new Anonymizer(); anonymousMode = false; + randomizeCardPosition = false; + recoverableComponents: IRecoverable[] = []; constructor(gameConfig: GameConfig) { @@ -181,6 +183,12 @@ class Game extends Room { } } + configurerandomizeCardPosition(randomizeCardPosition: boolean): void { + if (this.gameStarted == false) { + this.randomizeCardPosition = randomizeCardPosition; + } + } + setupRecoverableComponents(): void { this.recoverableComponents = []; this.recoverableComponents.push(this.anonymizer); @@ -597,14 +605,46 @@ class Game extends Room { } } + if (this.randomizeCardPosition) { + this.sendText('The game is running with randomized card positions.', 'gameplay-text'); + } + // seed the starting data into the VH for (let i = 0; i < this.playersInGame.length; i++) { this.voteHistory[this.playersInGame[i].request.user.username] = []; } - + // Initialise all the Cards - for (let i = 0; i < this.cardKeysInPlay.length; i++) { - this.specialCards[this.cardKeysInPlay[i]].initialise(); + const cardPosition = {}; + const numPlayers = this.playersInGame.length; + + let availableIndexes = []; + for (let i = 0; i < numPlayers; i++) { + availableIndexes.push(i); + } + + if (this.randomizeCardPosition) { + availableIndexes = shuffleArray(availableIndexes); + } + + for (const cardKey of this.cardKeysInPlay) { + if (this.randomizeCardPosition) { + cardPosition[cardKey] = availableIndexes.shift(); + } + else { + const firstCardPosition = (this.hammer + 5 + numPlayers) % numPlayers; + let offset = 0; + if (cardKey === Card.SireOfTheSea && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) + offset++; + if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) + offset++; + if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.SireOfTheSea)) + offset++; + + cardPosition[cardKey] = (firstCardPosition + offset) % numPlayers; + } + + this.specialCards[cardKey].initialise(cardPosition[cardKey]); } this.distributeGameData(); @@ -1413,6 +1453,7 @@ class Game extends Room { gameMode: this.gameMode, roomCreationType: this.roomCreationType, anonymousMode: this.anonymousMode, + randomizeCardPosition: this.randomizeCardPosition, botUsernames, playerUsernamesOrdered: getUsernamesOfPlayersInGame(this), diff --git a/src/gameplay/gameEngine/room.ts b/src/gameplay/gameEngine/room.ts index af031216d..7b535c49f 100644 --- a/src/gameplay/gameEngine/room.ts +++ b/src/gameplay/gameEngine/room.ts @@ -603,6 +603,7 @@ class Room { gameMode: string, timeouts: Timeouts, anonymousMode: boolean, + randomizeCardPosition: boolean, ) { if (this.gameStarted === true) { return false; @@ -651,6 +652,7 @@ class Room { timeouts.assassination, )}`; rolesInStr += `
Anonymous mode: ${anonymousMode}`; + rolesInStr += `
Randomize card position: ${randomizeCardPosition}`; this.sendText('The game is starting!', 'gameplay-text'); diff --git a/src/sockets/sockets.ts b/src/sockets/sockets.ts index fa83cc432..d7e7b729c 100644 --- a/src/sockets/sockets.ts +++ b/src/sockets/sockets.ts @@ -1578,6 +1578,7 @@ function startGame(data) { const options = data.options; const gameMode = data.gameMode; const anonymousMode = data.anonymousMode; + const randomizeCardPosition = data.randomizeCardPosition; const timeoutsStr = data.timeouts; // start the game @@ -1614,11 +1615,13 @@ function startGame(data) { ) { rooms[this.request.user.inRoomId].configureTimeouts(timeouts); rooms[this.request.user.inRoomId].configureAnonymousMode(anonymousMode); + rooms[this.request.user.inRoomId].configurerandomizeCardPosition(randomizeCardPosition); rooms[this.request.user.inRoomId].hostTryStartGame( options, gameMode, timeouts, anonymousMode, + randomizeCardPosition, ); } } diff --git a/src/views/lobby.ejs b/src/views/lobby.ejs index cdfe8468d..d5f7a314d 100644 --- a/src/views/lobby.ejs +++ b/src/views/lobby.ejs @@ -974,6 +974,9 @@
+
+ +
Date: Wed, 10 Jun 2026 15:08:44 -0400 Subject: [PATCH 2/3] Changelog update --- src/views/changelog.ejs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/views/changelog.ejs b/src/views/changelog.ejs index 08ec16777..ab961ace0 100644 --- a/src/views/changelog.ejs +++ b/src/views/changelog.ejs @@ -15,6 +15,18 @@

Changelog!

    + +
  • + 10-06-2026: +
      +
    • Added: Lobby option that allows the position of the card(s) to be randomized, without overlapping.
    • +
    • Fixed: If Ref of the Rain is in the same lobby as Lady of the Lake or Sire of the Sea, make sure they don't overlap/start with the same person.
    • +
    • Improved: Card initialization (happens in the main game script file instead of each individual card's script file)
    • +
    • Improved: Card container resetting
    • +
    • Thanks QuinnWuest for these changes!
    • +
    +
  • +
  • 10-05-2026:
      From ece6de9962411265131359ebb2ae8ce3a4ff2ea7 Mon Sep 17 00:00:00 2001 From: QuinnWuest Date: Sun, 28 Jun 2026 16:17:35 -0400 Subject: [PATCH 3/3] Remove card randomization (keeping it in commit history in case reimplementation ever gets considered) --- assets/scripts/lobby/buttonEventListeners2.js | 1 - src/gameplay/gameEngine/game.ts | 49 +++++-------------- src/gameplay/gameEngine/room.ts | 2 - src/sockets/sockets.ts | 1 - src/views/changelog.ejs | 1 - src/views/lobby.ejs | 3 -- 6 files changed, 11 insertions(+), 46 deletions(-) diff --git a/assets/scripts/lobby/buttonEventListeners2.js b/assets/scripts/lobby/buttonEventListeners2.js index 4dde434c1..2407d0eec 100644 --- a/assets/scripts/lobby/buttonEventListeners2.js +++ b/assets/scripts/lobby/buttonEventListeners2.js @@ -59,7 +59,6 @@ async function greenButtonFunction() { assassination: ((parseInt($('#startGameOptionsAssassinationPhaseTimeoutMin').val()) * 60 + parseInt($('#startGameOptionsAssassinationPhaseTimeoutSec').val())) * 1000).toString(), }, anonymousMode: $('#startGameOptionsAnonymousMode')[0].checked, - randomizeCardPosition: $('#startGameOptionsRandomizeCardPosition')[0].checked, }; socket.emit('startGame', startGameData); } else if (await confirmUserClick('yes')) { diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index eaf400f81..dbcaf87f1 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -126,8 +126,6 @@ class Game extends Room { anonymizer: Anonymizer = new Anonymizer(); anonymousMode = false; - randomizeCardPosition = false; - recoverableComponents: IRecoverable[] = []; constructor(gameConfig: GameConfig) { @@ -183,12 +181,6 @@ class Game extends Room { } } - configurerandomizeCardPosition(randomizeCardPosition: boolean): void { - if (this.gameStarted == false) { - this.randomizeCardPosition = randomizeCardPosition; - } - } - setupRecoverableComponents(): void { this.recoverableComponents = []; this.recoverableComponents.push(this.anonymizer); @@ -605,10 +597,6 @@ class Game extends Room { } } - if (this.randomizeCardPosition) { - this.sendText('The game is running with randomized card positions.', 'gameplay-text'); - } - // seed the starting data into the VH for (let i = 0; i < this.playersInGame.length; i++) { this.voteHistory[this.playersInGame[i].request.user.username] = []; @@ -618,32 +606,18 @@ class Game extends Room { const cardPosition = {}; const numPlayers = this.playersInGame.length; - let availableIndexes = []; - for (let i = 0; i < numPlayers; i++) { - availableIndexes.push(i); - } - - if (this.randomizeCardPosition) { - availableIndexes = shuffleArray(availableIndexes); - } - for (const cardKey of this.cardKeysInPlay) { - if (this.randomizeCardPosition) { - cardPosition[cardKey] = availableIndexes.shift(); - } - else { - const firstCardPosition = (this.hammer + 5 + numPlayers) % numPlayers; - let offset = 0; - if (cardKey === Card.SireOfTheSea && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) - offset++; - if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) - offset++; - if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.SireOfTheSea)) - offset++; - - cardPosition[cardKey] = (firstCardPosition + offset) % numPlayers; - } - + const firstCardPosition = (this.hammer + 5 + numPlayers) % numPlayers; + let offset = 0; + // potentially refactor this so that the indexes of the cards matter rather than the cards themselves, would make life easier if a new card gets implemented + if (cardKey === Card.SireOfTheSea && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) + offset++; + if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.LadyOfTheLake)) + offset++; + if (cardKey === Card.RefOfTheRain && this.cardKeysInPlay.includes(Card.SireOfTheSea)) + offset++; + + cardPosition[cardKey] = (firstCardPosition + offset) % numPlayers; this.specialCards[cardKey].initialise(cardPosition[cardKey]); } @@ -1453,7 +1427,6 @@ class Game extends Room { gameMode: this.gameMode, roomCreationType: this.roomCreationType, anonymousMode: this.anonymousMode, - randomizeCardPosition: this.randomizeCardPosition, botUsernames, playerUsernamesOrdered: getUsernamesOfPlayersInGame(this), diff --git a/src/gameplay/gameEngine/room.ts b/src/gameplay/gameEngine/room.ts index 7b535c49f..af031216d 100644 --- a/src/gameplay/gameEngine/room.ts +++ b/src/gameplay/gameEngine/room.ts @@ -603,7 +603,6 @@ class Room { gameMode: string, timeouts: Timeouts, anonymousMode: boolean, - randomizeCardPosition: boolean, ) { if (this.gameStarted === true) { return false; @@ -652,7 +651,6 @@ class Room { timeouts.assassination, )}`; rolesInStr += `
      Anonymous mode: ${anonymousMode}`; - rolesInStr += `
      Randomize card position: ${randomizeCardPosition}`; this.sendText('The game is starting!', 'gameplay-text'); diff --git a/src/sockets/sockets.ts b/src/sockets/sockets.ts index d7e7b729c..0f0fa4bdc 100644 --- a/src/sockets/sockets.ts +++ b/src/sockets/sockets.ts @@ -1578,7 +1578,6 @@ function startGame(data) { const options = data.options; const gameMode = data.gameMode; const anonymousMode = data.anonymousMode; - const randomizeCardPosition = data.randomizeCardPosition; const timeoutsStr = data.timeouts; // start the game diff --git a/src/views/changelog.ejs b/src/views/changelog.ejs index ab961ace0..868cf7c43 100644 --- a/src/views/changelog.ejs +++ b/src/views/changelog.ejs @@ -19,7 +19,6 @@
    • 10-06-2026:
        -
      • Added: Lobby option that allows the position of the card(s) to be randomized, without overlapping.
      • Fixed: If Ref of the Rain is in the same lobby as Lady of the Lake or Sire of the Sea, make sure they don't overlap/start with the same person.
      • Improved: Card initialization (happens in the main game script file instead of each individual card's script file)
      • Improved: Card container resetting
      • diff --git a/src/views/lobby.ejs b/src/views/lobby.ejs index d5f7a314d..cdfe8468d 100644 --- a/src/views/lobby.ejs +++ b/src/views/lobby.ejs @@ -974,9 +974,6 @@
        -
        - -