diff --git a/src/gameplay/gameEngine/game.ts b/src/gameplay/gameEngine/game.ts index 781011d3c..4321b436b 100644 --- a/src/gameplay/gameEngine/game.ts +++ b/src/gameplay/gameEngine/game.ts @@ -503,6 +503,17 @@ class Game extends Room { } } +// After roles are assigned, set displayRole for deceptive roles (Melron/Moregano). +for (let i = 0; i < this.playersInGame.length; i++) { + const p = this.playersInGame[i]; + if (p.role === Role.Melron) { + p.displayRole = Role.Merlin; // Melron thinks they are Merlin + } else if (p.role === Role.Moregano) { + p.displayRole = Role.Morgana; // Moregano thinks they are Morgana + } +} + + // 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++) { diff --git a/src/gameplay/gameEngine/phases/common/votingMission.ts b/src/gameplay/gameEngine/phases/common/votingMission.ts index fe9f2972e..b0e80066e 100644 --- a/src/gameplay/gameEngine/phases/common/votingMission.ts +++ b/src/gameplay/gameEngine/phases/common/votingMission.ts @@ -2,6 +2,8 @@ import usernamesIndexes from '../../../../myFunctions/usernamesIndexes'; import { ButtonSettings, IPhase, Phase } from '../types'; import { Alliance } from '../../types'; import { SocketUser } from '../../../../sockets/types'; +import { Role } from '../../roles/types'; + class VotingMission implements IPhase { static phase = Phase.VotingMission; @@ -32,22 +34,39 @@ class VotingMission implements IPhase { ) ] = 'succeed'; // console.log("received succeed from " + socket.request.user.username); - } else if (buttonPressed === 'no') { - // If the user is a res, they shouldn't be allowed to fail - const index = usernamesIndexes.getIndexFromUsername( - this.thisRoom.playersInGame, - socket.request.user.username, - ); - if ( - index !== -1 && - this.thisRoom.playersInGame[index].alliance === Alliance.Resistance - ) { - socket.emit( - 'danger-alert', - 'You are resistance! Surely you want to succeed!', - ); - return; - } +} else if (buttonPressed === 'no') { + // Determine the player index + const index = usernamesIndexes.getIndexFromUsername( + this.thisRoom.playersInGame, + socket.request.user.username, + ); + + // If player is Resistance and NOT Moregano, block failing + if ( + index !== -1 && + this.thisRoom.playersInGame[index].alliance === Alliance.Resistance && + this.thisRoom.playersInGame[index].role !== Role.Moregano + ) { + socket.emit( + 'danger-alert', + 'You are resistance! Surely you want to succeed!', + ); + return; + } + + // If player is Moregano and pressed "no", silently record "succeed". + const effectiveVote = + index !== -1 && this.thisRoom.playersInGame[index].role === Role.Moregano + ? 'succeed' + : 'fail'; + + this.thisRoom.missionVotes[ + usernamesIndexes.getIndexFromUsername( + this.thisRoom.playersInGame, + socket.request.user.username, + ) + ] = effectiveVote; + this.thisRoom.missionVotes[ usernamesIndexes.getIndexFromUsername( diff --git a/src/gameplay/gameEngine/roles/avalon/melron.ts b/src/gameplay/gameEngine/roles/avalon/melron.ts new file mode 100644 index 000000000..75125d843 --- /dev/null +++ b/src/gameplay/gameEngine/roles/avalon/melron.ts @@ -0,0 +1,79 @@ +import { Alliance, See } from '../../types'; +import { IRole, Role } from '../types'; +import Game from '../../game'; + +/** + * Melron (Resistance) — believes they are Merlin. + * Sees a RANDOM set of players as spies: size mirrors Merlin’s count + * (real spies minus 1 if Mordred/MordredAssassin is in play). + * Percival does NOT see Melron. + */ +class Melron implements IRole { + room: Game; + + static role = Role.Melron; + role = Role.Melron; + + alliance = Alliance.Resistance; + + description = 'Thinks they are Merlin; sees a random “spy” list mirroring Merlin’s count.'; + orderPriorityInOptions = 75; // place near Percival/Morgana if you care about ordering + + specialPhase: string; + + constructor(thisRoom: any) { + this.room = thisRoom; + } + + see(): See { + const spies: string[] = []; + if (!this.room.gameStarted) return { spies, roleTags: {} }; + + // Count real spies and detect Mordred/MordredAssassin (Merlin doesn’t see them) + let realSpyCount = 0; + let hasMordred = false; + + for (let i = 0; i < this.room.playersInGame.length; i++) { + const p = this.room.playersInGame[i]; + if (p.alliance === Alliance.Spy) { + realSpyCount++; + if (p.role === Role.Mordred || p.role === Role.MordredAssassin) { + hasMordred = true; + } + } + } + + const k = Math.max(0, realSpyCount - (hasMordred ? 1 : 0)); + + // Build pool of all non-self usernames + const self = this.getSelfUsername(); + const pool = this.room.playersInGame + .map((p: any) => p.username) + .filter((u: string) => u !== self); + + // Shuffle pool (Fisher–Yates) and pick k + for (let i = pool.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [pool[i], pool[j]] = [pool[j], pool[i]]; + } + const picks = pool.slice(0, k); + + for (const u of picks) spies.push(this.room.anonymizer.anon(u)); + + return { spies, roleTags: {} }; + } + + private getSelfUsername(): string { + for (let i = 0; i < this.room.playersInGame.length; i++) { + if (this.room.playersInGame[i].role === Role.Melron) { + return this.room.playersInGame[i].username; + } + } + return ''; + } + + checkSpecialMove(): void {} + getPublicGameData(): any {} +} + +export default Melron; diff --git a/src/gameplay/gameEngine/roles/avalon/moregano.ts b/src/gameplay/gameEngine/roles/avalon/moregano.ts new file mode 100644 index 000000000..d676b029a --- /dev/null +++ b/src/gameplay/gameEngine/roles/avalon/moregano.ts @@ -0,0 +1,85 @@ +import { Alliance, See } from '../../types'; +import { IRole, Role } from '../types'; +import Game from '../../game'; + +/** + * Moregano (Resistance) — believes they are Morgana. + * Sees a FAKE spy team that MUST include self; size mirrors Morgana’s count + * (real spies minus Oberon). + * If they press "Fail", it is silently processed as "Succeed". + */ +class Moregano implements IRole { + room: Game; + + static role = Role.Moregano; + role = Role.Moregano; + + alliance = Alliance.Resistance; + + description = 'Thinks they are Morgana; sees a fake spy team; their Fail counts as Success.'; + orderPriorityInOptions = 72; + + specialPhase: string; + + constructor(thisRoom: any) { + this.room = thisRoom; + } + + see(): See { + const spies: string[] = []; + if (!this.room.gameStarted) return { spies, roleTags: {} }; + + // Count real spies and detect Oberon (Morgana doesn’t see Oberon) + let realSpyCount = 0; + let hasOberon = false; + + for (let i = 0; i < this.room.playersInGame.length; i++) { + const p = this.room.playersInGame[i]; + if (p.alliance === Alliance.Spy) { + realSpyCount++; + if (p.role === Role.Oberon) hasOberon = true; + } + } + + const k = Math.max(1, realSpyCount - (hasOberon ? 1 : 0)); // include self, so at least 1 + + const self = this.getSelfUsername(); + if (!self) return { spies, roleTags: {} }; + + // Start with self + spies.push(this.room.anonymizer.anon(self)); + + const othersNeeded = k - 1; + if (othersNeeded <= 0) return { spies, roleTags: {} }; + + // Pool of non-self usernames + const pool = this.room.playersInGame + .map((p: any) => p.username) + .filter((u: string) => u !== self); + + // Shuffle pool and pick othersNeeded + for (let i = pool.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [pool[i], pool[j]] = [pool[j], pool[i]]; + } + const picks = pool.slice(0, othersNeeded); + + for (const u of picks) spies.push(this.room.anonymizer.anon(u)); + + return { spies, roleTags: {} }; + } + + private getSelfUsername(): string { + for (let i = 0; i < this.room.playersInGame.length; i++) { + if (this.room.playersInGame[i].role === Role.Moregano) { + return this.room.playersInGame[i].username; + } + } + return ''; + } + + checkSpecialMove(): void {} + getPublicGameData(): any {} +} + +export default Moregano; diff --git a/src/gameplay/gameEngine/roles/roles.ts b/src/gameplay/gameEngine/roles/roles.ts index c59381a8f..fb087af76 100644 --- a/src/gameplay/gameEngine/roles/roles.ts +++ b/src/gameplay/gameEngine/roles/roles.ts @@ -13,6 +13,9 @@ import Spy from './avalon/spy'; import Mordred from './avalon/mordred'; import MordredAssassin from './avalon/mordredassassin'; import Hitberon from './avalon/hitberon'; +import Melron from './avalon/melron'; +import Moregano from './avalon/moregano'; + type Class = new (...args: Args) => I; export const avalonRoles: Record> = { @@ -32,6 +35,10 @@ export const avalonRoles: Record> = { [MordredAssassin.role]: MordredAssassin, [Hitberon.role]: Hitberon, + + [Melron.role]: Melron, + [Moregano.role]: Moregano, + }; export const rolesThatCantGuessMerlin = [ diff --git a/src/gameplay/gameEngine/roles/types.ts b/src/gameplay/gameEngine/roles/types.ts index a55bff466..4bedd0521 100644 --- a/src/gameplay/gameEngine/roles/types.ts +++ b/src/gameplay/gameEngine/roles/types.ts @@ -18,6 +18,10 @@ export enum Role { MordredAssassin = 'MordredAssassin', Hitberon = 'Hitberon', + + // ADD THESE TWO + Melron = 'Melron', + Moregano = 'Moregano', } export interface IRole {