From 5e6f24c1d4934b8a1fc8dc67f3c09e1290883791 Mon Sep 17 00:00:00 2001 From: 22Chaos <48512901+22Chaos@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:14:58 +0530 Subject: [PATCH 1/4] Add norebo: A resistance who unwittingly appears as a spy to other spies. Implemented as a card, not a role. Currently, only the Assassin can see Norebo. Not saved in database records. Not yet announced at the end of the game. --- .../gameEngine/cards/avalon/norebo.ts | 62 +++++++++++++++++++ src/gameplay/gameEngine/cards/cards.ts | 2 + src/gameplay/gameEngine/cards/types.ts | 1 + src/gameplay/gameEngine/game.ts | 12 ++-- .../gameEngine/roles/avalon/assassin.ts | 7 ++- 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/gameplay/gameEngine/cards/avalon/norebo.ts diff --git a/src/gameplay/gameEngine/cards/avalon/norebo.ts b/src/gameplay/gameEngine/cards/avalon/norebo.ts new file mode 100644 index 000000000..4e16b45c3 --- /dev/null +++ b/src/gameplay/gameEngine/cards/avalon/norebo.ts @@ -0,0 +1,62 @@ +import { Card, ICard } from '../types'; +import { SocketUser } from '../../../../sockets/types'; +import { Alliance } from '../../types'; +import shuffleArray from '../../../../util/shuffleArray'; + + + +class Norebo implements ICard { + private thisRoom: any; + + static card = Card.Norebo; + card = Card.Norebo; + + indexOfPlayerHolding = -1; //TODO not sure if should be 0 like other cards. + + description = +'A random resistance member who unknowingly appears as a spy to other spies. Card does not change hands.'; + constructor(thisRoom: any) { + this.thisRoom = thisRoom; + } + + initialise(): void { + this.setHolder(0) + //the number 0 doesn't matter + ; + } + + setHolder(index: number): void { + //assign to a random Resistance member + let resPlayersIndexes=[]; + for (let i = 0; i < this.thisRoom.playersInGame.length; i++) { + if (this.thisRoom.playersInGame[i].alliance === Alliance.Resistance) { + resPlayersIndexes.push(i); + this.indexOfPlayerHolding=i; + } + } + resPlayersIndexes = shuffleArray(resPlayersIndexes); + this.indexOfPlayerHolding=resPlayersIndexes[0]; + // console.log("Norebo started with player", this.indexOfPlayerHolding); + } + + checkSpecialMove( + socket: SocketUser, + buttonPressed: 'yes' | 'no', + selectedPlayers: string[], + ): boolean { + return false; + } + + getPublicGameData(): any { + /* TODO: (Can delete this function. Not absolutely necessary) + Public data to show the user(s) e.g. who holds the lady of the lake */ + return { + norebo: { + index: this.indexOfPlayerHolding, + name: this.card, + }, + }; + } +} + +export default Norebo; diff --git a/src/gameplay/gameEngine/cards/cards.ts b/src/gameplay/gameEngine/cards/cards.ts index b3e61ae3b..1f5c2a073 100644 --- a/src/gameplay/gameEngine/cards/cards.ts +++ b/src/gameplay/gameEngine/cards/cards.ts @@ -1,9 +1,11 @@ import LadyOfTheLake from './avalon/ladyOfTheLake'; import RefOfTheRain from './avalon/refOfTheRain'; import SireOfTheSea from './avalon/sireOfTheSea'; +import Norebo from './avalon/norebo'; export const avalonCards = { [LadyOfTheLake.card]: LadyOfTheLake, [RefOfTheRain.card]: RefOfTheRain, [SireOfTheSea.card]: SireOfTheSea, + [Norebo.card]: Norebo, }; diff --git a/src/gameplay/gameEngine/cards/types.ts b/src/gameplay/gameEngine/cards/types.ts index 27a0fcf6b..d593ff133 100644 --- a/src/gameplay/gameEngine/cards/types.ts +++ b/src/gameplay/gameEngine/cards/types.ts @@ -4,6 +4,7 @@ export enum Card { LadyOfTheLake = 'Lady of the Lake', RefOfTheRain = 'Ref of the Rain', SireOfTheSea = 'Sire of the Sea', + Norebo = 'Norebo', } export interface ICard { diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index f03d68edb..451df3676 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -112,6 +112,7 @@ class Game extends Room { specialRoles: any; specialPhases: any; specialCards: any; + //cardKeysInPlay: any; //making public TODO maybe this line is useless gameTimer: GameTimer; dateTimerExpires: Date; @@ -519,6 +520,12 @@ class Game extends Room { } } + // Initialise all the Cards + for (let i = 0; i < this.cardKeysInPlay.length; i++) { + this.specialCards[this.cardKeysInPlay[i]].initialise(); + } + + // Prepare the data for each person to see for the rest of the game. // The following data do not change as the game goes on. for (let i = 0; i < this.playersInGame.length; i++) { @@ -601,11 +608,6 @@ class Game extends Room { 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(); - } - this.distributeGameData(); this.botIndexes = []; diff --git a/src/gameplay/gameEngine/roles/avalon/assassin.ts b/src/gameplay/gameEngine/roles/avalon/assassin.ts index b8a0940e0..efa3376e8 100644 --- a/src/gameplay/gameEngine/roles/avalon/assassin.ts +++ b/src/gameplay/gameEngine/roles/avalon/assassin.ts @@ -3,6 +3,8 @@ import Game from '../../game'; import { Phase } from '../../phases/types'; import { IRole, Role } from '../types'; import Assassination from '../../phases/avalon/assassination'; +import { Card } from '../../cards/types'; + class Assassin implements IRole { room: Game; @@ -30,9 +32,10 @@ class Assassin implements IRole { if (this.room.gameStarted === true) { const spies = []; - for (let i = 0; i < this.room.playersInGame.length; i++) { - if (this.room.playersInGame[i].alliance === Alliance.Spy) { + if (this.room.playersInGame[i].alliance === Alliance.Spy + || i === this.room.specialCards[Card.Norebo].indexOfPlayerHolding + ) { if (this.room.playersInGame[i].role === Role.Oberon) { // don't add oberon } else { From fdcedc1367b5bd256737cdf9123e7492c35b64a7 Mon Sep 17 00:00:00 2001 From: 22Chaos <48512901+22Chaos@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:14:08 +0530 Subject: [PATCH 2/4] fix: all spies now see norebo too. Moregano sees an additional dummy-spy if norebo is in game. --- src/gameplay/gameEngine/roles/avalon/mordred.ts | 5 ++++- src/gameplay/gameEngine/roles/avalon/moregano.ts | 5 +++++ src/gameplay/gameEngine/roles/avalon/morgana.ts | 5 ++++- src/gameplay/gameEngine/roles/avalon/spy.ts | 4 +++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/gameplay/gameEngine/roles/avalon/mordred.ts b/src/gameplay/gameEngine/roles/avalon/mordred.ts index f16dbe912..d6b8ba31a 100644 --- a/src/gameplay/gameEngine/roles/avalon/mordred.ts +++ b/src/gameplay/gameEngine/roles/avalon/mordred.ts @@ -1,6 +1,8 @@ import { Alliance, See } from '../../types'; import { IRole, Role } from '../types'; import Game from '../../game'; +import { Card } from '../../cards/types'; + class Mordred implements IRole { room: Game; @@ -26,7 +28,8 @@ class Mordred implements IRole { const spies = []; for (let i = 0; i < this.room.playersInGame.length; i++) { - if (this.room.playersInGame[i].alliance === Alliance.Spy) { + if (this.room.playersInGame[i].alliance === Alliance.Spy + || i === this.room.specialCards[Card.Norebo].indexOfPlayerHolding) { if (this.room.playersInGame[i].role === Role.Oberon) { // don't add oberon } else { diff --git a/src/gameplay/gameEngine/roles/avalon/moregano.ts b/src/gameplay/gameEngine/roles/avalon/moregano.ts index db8e365dd..15646219f 100644 --- a/src/gameplay/gameEngine/roles/avalon/moregano.ts +++ b/src/gameplay/gameEngine/roles/avalon/moregano.ts @@ -2,6 +2,7 @@ import { Alliance, See } from '../../types'; import { IRole, Role } from '../types'; import Game from '../../game'; import shuffleArray from '../../../../util/shuffleArray'; +import { Card } from '../../cards/types'; /** * Moregano (Resistance) — believes they are Morgana. @@ -58,6 +59,10 @@ class Moregano implements IRole { visibleSpyCount++; } } + //add one more visible spy if norebo exists + if(this.room.specialCards[Card.Norebo].indexOfPlayerHolding != -1){ + visibleSpyCount++; + } const othersNeeded = visibleSpyCount - 1; diff --git a/src/gameplay/gameEngine/roles/avalon/morgana.ts b/src/gameplay/gameEngine/roles/avalon/morgana.ts index b4988e982..27819fc09 100644 --- a/src/gameplay/gameEngine/roles/avalon/morgana.ts +++ b/src/gameplay/gameEngine/roles/avalon/morgana.ts @@ -1,6 +1,8 @@ import { Alliance, See } from '../../types'; import { IRole, Role } from '../types'; import Game from '../../game'; +import { Card } from '../../cards/types'; + class Morgana implements IRole { room: Game; @@ -26,7 +28,8 @@ class Morgana implements IRole { const spies = []; for (let i = 0; i < this.room.playersInGame.length; i++) { - if (this.room.playersInGame[i].alliance === Alliance.Spy) { + if (this.room.playersInGame[i].alliance === Alliance.Spy + || i === this.room.specialCards[Card.Norebo].indexOfPlayerHolding) { if (this.room.playersInGame[i].role === Role.Oberon) { // don't add oberon } else { diff --git a/src/gameplay/gameEngine/roles/avalon/spy.ts b/src/gameplay/gameEngine/roles/avalon/spy.ts index 27b41753a..91779c6aa 100644 --- a/src/gameplay/gameEngine/roles/avalon/spy.ts +++ b/src/gameplay/gameEngine/roles/avalon/spy.ts @@ -1,6 +1,7 @@ import { Alliance, See } from '../../types'; import { IRole, Role } from '../types'; import Game from '../../game'; +import { Card } from '../../cards/types'; class Spy implements IRole { room: Game; @@ -27,7 +28,8 @@ class Spy implements IRole { const spies = []; for (let i = 0; i < this.room.playersInGame.length; i++) { - if (this.room.playersInGame[i].alliance === Alliance.Spy) { + if (this.room.playersInGame[i].alliance === Alliance.Spy + || i === this.room.specialCards[Card.Norebo].indexOfPlayerHolding) { if (this.room.playersInGame[i].role === Role.Oberon) { // don't add oberon } else { From ac4c48ac3cf1838302344adacd6a79fccd006ccc Mon Sep 17 00:00:00 2001 From: 22Chaos <48512901+22Chaos@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:08:57 +0530 Subject: [PATCH 3/4] Norebo identity now announces at the end of the game. --- src/gameplay/gameEngine/game.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index 451df3676..5981ce8fa 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -1291,7 +1291,7 @@ class Game extends Room { this.sendText('The resistance wins!', 'gameplay-text-blue'); } - // Now announce Melron / Moregano illusions + // Now announce Melron / Moregano / Norebo illusions this.announceIllusionsIfAny(); // Post results of Merlin guesses @@ -2103,6 +2103,19 @@ class Game extends Room { ); } } + + //Norebo + const noreboIndex = this.specialCards[Card.Norebo].indexOfPlayerHolding; + if (noreboIndex!=-1) { + const noreboUsername = this.anonymizer.anon(this.playersInGame[noreboIndex].username); + + console.log(noreboUsername); + this.sendText( + `Norebo was: ${noreboUsername}`, + 'gameplay-text-blue', + ); + } + } private usernameIsPlayer(username: string) { From 86d36cf6f34daa02b47bafa22c9359cd372613c7 Mon Sep 17 00:00:00 2001 From: 22Chaos <48512901+22Chaos@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:08:32 +0530 Subject: [PATCH 4/4] stores Norebo username in post-game database --- src/gameplay/gameEngine/game.ts | 10 ++++++++++ src/models/gameRecord.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index 5981ce8fa..d6d56d9b3 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -1379,6 +1379,14 @@ class Game extends Room { this.specialCards['sire of the sea'].sireHistoryUsernames; } + let noreboUsername; + if (this.specialCards && this.specialCards[Card.Norebo]) { + const noreboIndex = this.specialCards[Card.Norebo].indexOfPlayerHolding; + if(noreboIndex!=-1) { + noreboUsername = this.anonymizer.anon(this.playersInGame[noreboIndex].username); + } + } + // console.log(this.gameMode); let botUsernames; if (this.botSockets !== undefined) { @@ -1441,6 +1449,8 @@ class Game extends Room { sireChain, sireHistoryUsernames, + noreboUsername, + whoAssassinShot: this.whoAssassinShot, whoAssassinShot2: this.whoAssassinShot2, diff --git a/src/models/gameRecord.js b/src/models/gameRecord.js index 94ce66d90..a7f110c93 100644 --- a/src/models/gameRecord.js +++ b/src/models/gameRecord.js @@ -37,6 +37,8 @@ const gameRecordSchema = new mongoose.Schema({ sireChain: [String], sireHistoryUsernames: [String], + noreboUsername: String, + missionHistory: [String], numFailsHistory: [Number], voteHistory: Object,