diff --git a/.vscode/launch.json b/.vscode/launch.json
index f13f8e982..326c536d8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,6 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "frontend",
+ "url": "http://localhost:3001",
+ "webRoot": "${workspaceFolder}/react_main"
+ },
{
"type": "node",
"request": "launch",
diff --git a/Games/core/ArrayHash.js b/Games/core/ArrayHash.js
index bbdff4e49..fb9a26c52 100644
--- a/Games/core/ArrayHash.js
+++ b/Games/core/ArrayHash.js
@@ -45,6 +45,15 @@ module.exports = class ArrayHash {
return this.array()[index];
}
+ indexOf(item) {
+ let arr = this.array()
+ for (let i in arr)
+ if (arr[i] == item)
+ return i
+
+ return -1
+ }
+
get length() {
return Object.values(this).length;
}
diff --git a/Games/types/Ghost/Action.js b/Games/types/Ghost/Action.js
new file mode 100644
index 000000000..c1a34c229
--- /dev/null
+++ b/Games/types/Ghost/Action.js
@@ -0,0 +1,9 @@
+const Action = require("../../core/Action");
+
+module.exports = class GhostAction extends Action {
+
+ constructor(options) {
+ super(options);
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Card.js b/Games/types/Ghost/Card.js
new file mode 100644
index 000000000..3b32d14d3
--- /dev/null
+++ b/Games/types/Ghost/Card.js
@@ -0,0 +1,9 @@
+const Card = require("../../core/Card");
+
+module.exports = class GhostCard extends Card {
+
+ constructor(role) {
+ super(role);
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Game.js b/Games/types/Ghost/Game.js
new file mode 100644
index 000000000..893efabfb
--- /dev/null
+++ b/Games/types/Ghost/Game.js
@@ -0,0 +1,218 @@
+const Game = require("../../core/Game");
+const Player = require("./Player");
+const Action = require("./Action");
+const Queue = require("../../core/Queue");
+const Winners = require("../../core/Winners");
+
+const Random = require("../../../lib/Random");
+const wordList = require("./data/words");
+
+module.exports = class GhostGame extends Game {
+
+ constructor(options) {
+ super(options);
+
+ this.type = "Ghost";
+ this.Player = Player;
+ this.states = [
+ {
+ name: "Postgame"
+ },
+ {
+ name: "Pregame"
+ },
+ {
+ name: "Night",
+ length: options.settings.stateLengths["Night"],
+ skipChecks: [
+ () => this.playerGivingClue
+ ]
+ },
+ {
+ name: "Give Clue",
+ length: options.settings.stateLengths["Give Clue"],
+ },
+ {
+ name: "Day",
+ length: options.settings.stateLengths["Day"],
+ skipChecks: [
+ () => this.playerGivingClue
+ ]
+ },
+ {
+ name: "Guess Word",
+ length: options.settings.stateLengths["Guess Word"],
+ skipChecks: [
+ () => this.playerGivingClue
+ ]
+ }
+ ];
+
+ // game settings
+ this.configureWords = options.settings.configureWords;
+ this.wordLength = options.settings.wordLength;
+ this.townWord = options.settings.townWord;
+ this.foolWord = options.settings.townWord;
+
+ // giving clue
+ this.playerGivingClue = false;
+ this.currentPlayerList = [];
+ this.startIndex = -1;
+ this.currentIndex = -1;
+
+ this.responseHistory = [];
+ this.currentClueHistory = [];
+ }
+
+ start() {
+ if (!this.configureWords) {
+ let wordPack = Random.randArrayVal(wordList);
+ let shuffledWordPack = Random.randomizeArray(wordPack);
+ this.townWord = shuffledWordPack[0];
+ this.foolWord = shuffledWordPack[1];
+ this.wordLength = this.townWord.length;
+ }
+
+ super.start();
+ }
+
+ startRoundRobin(firstPick) {
+ if (this.currentClueHistory.length > 0) {
+ this.responseHistory.push({
+ "type": "clue",
+ "data": this.currentClueHistory,
+ })
+ this.currentClueHistory = [];
+ }
+
+ this.currentPlayerList = this.alivePlayers();
+ this.startIndex = this.currentPlayerList.indexOf(firstPick);
+ this.currentIndex = this.startIndex;
+
+ firstPick.holdItem("Microphone");
+ this.playerGivingClue = true;
+ }
+
+ incrementCurrentIndex() {
+ this.currentIndex = (this.currentIndex + 1) % this.currentPlayerList.length;
+ }
+
+ incrementState() {
+ let previousState = this.getStateName();
+
+ if (previousState == "Give Clue") {
+ this.incrementCurrentIndex();
+ while (true) {
+ if (this.currentIndex == this.startIndex) {
+ this.playerGivingClue = false;
+ break;
+ }
+
+ let nextPlayer = this.currentPlayerList[this.currentIndex];
+ if (nextPlayer.alive) {
+ nextPlayer.holdItem("Microphone");
+ break
+ }
+ this.incrementCurrentIndex();
+ }
+ }
+
+ super.incrementState();
+ }
+
+ recordClue(player, clue) {
+ this.currentClueHistory.push({
+ "name": player.name,
+ "clue": clue
+ })
+ }
+
+ recordGuess(player, guess) {
+ let data = {
+ "name": player.name,
+ "guess": guess,
+ };
+
+ this.responseHistory.push({
+ "type": "guess",
+ "data": data
+ })
+ }
+
+ // send player-specific state
+ broadcastState() {
+ for (let p of this.players) {
+ p.sendStateInfo();
+ }
+ }
+
+ getStateInfo(state) {
+ var info = super.getStateInfo(state);
+ info.extraInfo = {
+ "responseHistory": this.responseHistory,
+ "currentClueHistory": this.currentClueHistory
+ }
+ return info;
+ }
+
+ // process player leaving immediately
+ async playerLeave(player) {
+ if (this.started) {
+ let action = new Action({
+ actor: player,
+ target: player,
+ game: this,
+ run: function () {
+ this.target.kill("leave", this.actor, true);
+ }
+ });
+
+ this.instantAction(action);
+ }
+
+ await super.playerLeave(player);
+ }
+
+ checkWinConditions() {
+ var finished = false;
+ var counts = {};
+ var winQueue = new Queue();
+ var winners = new Winners(this);
+ var aliveCount = this.alivePlayers().length;
+
+ for (let player of this.players) {
+ let alignment = player.role.alignment;
+
+ if (!counts[alignment])
+ counts[alignment] = 0;
+
+ if (player.alive)
+ counts[alignment]++;
+
+ winQueue.enqueue(player.role.winCheck);
+ }
+
+ for (let winCheck of winQueue) {
+ winCheck.check(counts, winners, aliveCount);
+ }
+
+ if (winners.groupAmt() > 0)
+ finished = true;
+ else if (aliveCount == 0) {
+ winners.addGroup("No one");
+ finished = true;
+ }
+
+ winners.determinePlayers();
+ return [finished, winners];
+ }
+
+ getGameTypeOptions() {
+ return {
+ disableRehost: true,
+ };
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Item.js b/Games/types/Ghost/Item.js
new file mode 100644
index 000000000..0526fc119
--- /dev/null
+++ b/Games/types/Ghost/Item.js
@@ -0,0 +1,9 @@
+const Item = require("../../core/Item");
+
+module.exports = class GhostItem extends Item {
+
+ constructor(role) {
+ super(role);
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Meeting.js b/Games/types/Ghost/Meeting.js
new file mode 100644
index 000000000..c62d9eff6
--- /dev/null
+++ b/Games/types/Ghost/Meeting.js
@@ -0,0 +1,9 @@
+const Meeting = require("../../core/Meeting");
+
+module.exports = class GhostMeeting extends Meeting {
+
+ constructor(game, name) {
+ super(game, name);
+ }
+
+};
\ No newline at end of file
diff --git a/Games/types/Ghost/Player.js b/Games/types/Ghost/Player.js
new file mode 100644
index 000000000..4fe8f12fd
--- /dev/null
+++ b/Games/types/Ghost/Player.js
@@ -0,0 +1,17 @@
+const Player = require("../../core/Player");
+
+module.exports = class GhostPlayer extends Player {
+
+ constructor(user, game, isBot) {
+ super(user, game, isBot);
+ }
+
+ // add player-specific state info
+ sendStateInfo() {
+ let info = this.game.getStateInfo();
+ info.extraInfo.word = this.role?.word;
+ info.extraInfo.wordLength = this.game.wordLength;
+ this.send("state", info);
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Role.js b/Games/types/Ghost/Role.js
new file mode 100644
index 000000000..0720b40d8
--- /dev/null
+++ b/Games/types/Ghost/Role.js
@@ -0,0 +1,8 @@
+const Role = require("../../core/Role");
+
+module.exports = class GhostRole extends Role {
+
+ constructor(name, player, data) {
+ super(name, player, data);
+ }
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/Winners.js b/Games/types/Ghost/Winners.js
new file mode 100644
index 000000000..c210bd5a1
--- /dev/null
+++ b/Games/types/Ghost/Winners.js
@@ -0,0 +1,8 @@
+const Winners = require("../../core/Winners");
+
+module.exports = class GhostWinners extends Winners {
+
+ constructor(game) {
+ super(game);
+ }
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/data/words.js b/Games/types/Ghost/data/words.js
new file mode 100644
index 000000000..9a3cf3a64
--- /dev/null
+++ b/Games/types/Ghost/data/words.js
@@ -0,0 +1,99 @@
+module.exports = [
+ // adjectives
+ ["wet", "dry", "sad", "cry", "big", "fat"],
+ ["warm", "cold", "good", "evil", "near", "away", "love", "hate"],
+ ["tall", "flat"],
+ // emotions
+ ["smile", "happy", "anger"]
+ // colours
+ ["pink", "blue", "teal"],
+ ["green", "white", "black", "brown", "olive"],
+ ["yellow", "purple", "orange"],
+ // countries
+ ["china", "japan", "spain", "italy", "india"],
+ // actions
+ ["jump", "spin", "kick", "open", "fold"],
+ // food
+ ["egg", "ham", "jam", "pea", "pie", "yam"],
+ ["kiwi", "lime", "pear", "plum"],
+ ["apple", "dates", "grape", "lemon", "melon", "mango", "peach", "olive"],
+ ["oven", "bake", "cake", "fork", "bowl", "milk", "soup"],
+ ["pasta", "pizza", "sauce", "steak", "fries", "crust", "donut", "candy", "bread", "toast"],
+ ["meat", "pork"],
+ ["onion", "round"],
+ ["peanut", "butter"],
+ // music
+ ["harp", "beat", "gong", "drum", "sing", "song", "band", "tune"],
+ // animals
+ ["bee", "cat", "dog", "fox", "hen", "bat", "cow", "owl", "ant", "eel"],
+ ["wolf", "lion", "duck", "deer", "bear", "goat", "crab", "mole", "boar", "orca", "toad", "dove", "frog", "slug", "swan"],
+ ["monkey", "donkey", "beaver", "jaguar", "iguana", "baboon", "alpaca", "weasel", "rabbit", "spider", "beetle", "toucan", "falcon", "parrot", "shrimp", "urchin", "turtle", "walrus", "pigeon"],
+ // automobile
+ ["car", "van"],
+ ["road", "kill"],
+ // marine
+ ["war", "sea"],
+ ["boat", "fish", "sail", "ship", "port"],
+ ["ocean", "beach"],
+ // school
+ ["grade", "paper", "graph", "class", "tutor", "major"],
+ ["one", "two", "six", "ten"],
+ ["four", "five", "nine"],
+ ["math", "nerd"],
+ ["pencil", "eraser", "number", "letter"],
+ // sports
+ ["karate", "boxing", "kungfu", "taichi"],
+ ["bowling", "fencing", "surfing", "archery", "cricket", "cycling", "skating", "parkour", "frisbee", "sailing", "jogging"]
+ ["football", "swimming", "handball", "baseball", "climbing"],
+ // clothes
+ ["sock", "shoe"],
+ // garden
+ ["pond", "weed", "duck", "frog", "lily", "tree", "bush"],
+ // health and body
+ ["salt", "hurt", "heal"],
+ ["mask", "sick"],
+ ["virus", "covid", "cough"],
+ // minecraft/ runescape
+ ["iron", "gold", "rock", "farm"],
+ // computer
+ ["copy", "edit", "type", "code", "java", "site"],
+ // cartoons
+ ["pooh", "bear"],
+ ["winnie", "tigger", "eeyore", "piglet"],
+ // beyondmafia
+ ["town", "fool", "king", "jinx", "tree", "bomb", "chef", "cult"],
+ ["ghost", "mafia", "curse", "nurse", "clown", "mason", "thing", "alien"],
+ ["forger", "doctor", "granny", "oracle", "priest", "sniper", "yakuza", "lawyer", "tailor", "deputy", "turkey"],
+ ["janitor", "caroler", "mafioso", "sheriff", "tracker", "watcher", "actress", "slasher", "courier"],
+ ["suit", "bomb", "dawn", "lone", "loud"],
+ ["probe", "bread", "knife", "armor"],
+ ["humble", "astral", "famine"],
+ // others
+ ["rick", "roll"],
+ ["plan", "fail"],
+]
+
+/*
+uncategorised
+smile,anger
+cloak,smoke
+hover,float
+happy,peace
+knife,spoon
+angel,demon
+power,plant
+bread,crust
+paper,plane
+train,track
+inner,outer
+major,minor
+place,space
+awake,sleep
+fairy,magic
+sport,arena
+world,globe
+rocket,cannon
+promise,destroy
+parallel,sequence
+magical,rainbow
+chocolate,blueberry*/
diff --git a/Games/types/Ghost/items/Microphone.js b/Games/types/Ghost/items/Microphone.js
new file mode 100644
index 000000000..7cd8fcc2c
--- /dev/null
+++ b/Games/types/Ghost/items/Microphone.js
@@ -0,0 +1,36 @@
+const Item = require("../Item");
+
+module.exports = class Microphone extends Item {
+
+ constructor() {
+ super("Microphone");
+
+ this.meetings = {
+ "Give Clue": {
+ actionName: "Give Clue (1-50)",
+ states: ["Give Clue"],
+ flags: ["voting"],
+ inputType: "text",
+ textOptions: {
+ minLength: 1,
+ maxLength: 50,
+ submit: "Confirm"
+ },
+ action: {
+ item: this,
+ run: function() {
+ this.game.recordClue(this.actor, this.target);
+ this.game.sendAlert(`${this.actor.name}: ${this.target}`);
+ this.item.drop();
+ }
+ }
+ }
+ }
+ }
+
+ hold(player) {
+ super.hold(player);
+ player.game.queueAlert(`${player.name} is giving a clue...`);
+ }
+
+};
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/Ghost/Ghost.js b/Games/types/Ghost/roles/Ghost/Ghost.js
new file mode 100644
index 000000000..427c6118c
--- /dev/null
+++ b/Games/types/Ghost/roles/Ghost/Ghost.js
@@ -0,0 +1,12 @@
+const Role = require("../../Role");
+
+module.exports = class Ghost extends Role {
+
+ constructor(player, data) {
+ super("Ghost", player, data);
+
+ this.alignment = "Ghost";
+ this.cards = ["TownCore", "WinWithGhost", "MeetingGhost", "GuessWordOnLynch"];
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/Host/Host.js b/Games/types/Ghost/roles/Host/Host.js
new file mode 100644
index 000000000..5c85d3bf2
--- /dev/null
+++ b/Games/types/Ghost/roles/Host/Host.js
@@ -0,0 +1,17 @@
+const Role = require("../../Role");
+
+module.exports = class Host extends Role {
+
+ constructor(player, data) {
+ super("Host", player, data);
+
+ this.alignment = "Host";
+ this.cards = ["TownCore"];
+ this.meetingMods = {
+ "Village": {
+ canVote: false,
+ }
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/Town/Fool.js b/Games/types/Ghost/roles/Town/Fool.js
new file mode 100644
index 000000000..754ff8442
--- /dev/null
+++ b/Games/types/Ghost/roles/Town/Fool.js
@@ -0,0 +1,27 @@
+const Role = require("../../Role");
+
+module.exports = class Fool extends Role {
+
+ constructor(player, data) {
+ super("Fool", player, data);
+
+ this.alignment = "Town";
+ this.cards = ["TownCore", "WinWithTown", "AnnounceAndCheckWord"];
+ this.appearance = {
+ self: "Town"
+ };
+
+ this.listeners = {
+ "roleAssigned": [
+ function (player) {
+ if (player != this.player) {
+ return;
+ }
+
+ this.word = this.game.foolWord;
+ }
+ ]
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/Town/Town.js b/Games/types/Ghost/roles/Town/Town.js
new file mode 100644
index 000000000..ba4679c35
--- /dev/null
+++ b/Games/types/Ghost/roles/Town/Town.js
@@ -0,0 +1,24 @@
+const Role = require("../../Role");
+
+module.exports = class Town extends Role {
+
+ constructor(player, data) {
+ super("Town", player, data);
+
+ this.alignment = "Town";
+ this.cards = ["TownCore", "WinWithTown", "AnnounceAndCheckWord"];
+
+ this.listeners = {
+ "roleAssigned": [
+ function (player) {
+ if (player != this.player) {
+ return;
+ }
+
+ this.word = this.game.townWord;
+ }
+ ]
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js b/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js
new file mode 100644
index 000000000..61d1bb494
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js
@@ -0,0 +1,21 @@
+const Card = require("../../Card");
+
+module.exports = class AnnounceAndCheckWord extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.listeners = {
+ "start": function () {
+ this.player.queueAlert(`The secret word is: ${this.word}`);
+ }
+ };
+ }
+
+ speak(message) {
+ if (message.content.replace(' ', '').toLowerCase().includes(this.word)) {
+ this.player.sendAlert('Be careful not to say the secret word!');
+ }
+ };
+
+}
diff --git a/Games/types/Ghost/roles/cards/GuessWordOnLynch.js b/Games/types/Ghost/roles/cards/GuessWordOnLynch.js
new file mode 100644
index 000000000..b7124b3ac
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/GuessWordOnLynch.js
@@ -0,0 +1,47 @@
+const Card = require("../../Card");
+
+module.exports = class GuessWordOnLynch extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.immunity["lynch"] = 1;
+
+ this.listeners = {
+ "immune": function (action) {
+ if (action.target == this.player) {
+ this.dead = true;
+ }
+ }
+ };
+
+ this.meetings = {
+ "Guess Word": {
+ states: ["Guess Word"],
+ flags: ["instant", "voting"],
+ inputType: "text",
+ textOptions: {
+ minLength: 2,
+ maxLength: 20,
+ alphaOnly: true,
+ toLowerCase: true,
+ submit: "Confirm"
+ },
+ action: {
+ run: function() {
+ let word = this.target.toLowerCase();
+ this.game.recordGuess(this.actor, word);
+
+ this.actor.role.guessedWord = word;
+ if (word !== this.game.townWord) {
+ this.actor.kill();
+ }
+ }
+ },
+ shouldMeet: function() {
+ return this.dead;
+ }
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/cards/MeetingGhost.js b/Games/types/Ghost/roles/cards/MeetingGhost.js
new file mode 100644
index 000000000..9736b7a09
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/MeetingGhost.js
@@ -0,0 +1,35 @@
+const Card = require("../../Card");
+
+module.exports = class MeetingGhost extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.listeners = {
+ "start": function () {
+ for (let player of this.game.players) {
+ if (player.role.alignment == "Ghost" && player != this.player) {
+ this.revealToPlayer(player);
+ }
+ }
+
+ this.player.queueAlert(`Guess the hidden word of length: ${this.game.wordLength}`);
+ }
+ }
+
+ this.meetings = {
+ "Ghost": {
+ actionName: "Select Leader",
+ states: ["Night"],
+ flags: ["group", "speech", "voting", "mustAct"],
+ targets: { include: ["alive"] },
+ action: {
+ run: function () {
+ this.game.startRoundRobin(this.target);
+ }
+ }
+ }
+ };
+ }
+
+}
diff --git a/Games/types/Ghost/roles/cards/TownCore.js b/Games/types/Ghost/roles/cards/TownCore.js
new file mode 100644
index 000000000..27cfe8e3e
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/TownCore.js
@@ -0,0 +1,26 @@
+const Card = require("../../Card");
+
+module.exports = class TownCore extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.meetings = {
+ "Village": {
+ states: ["Day"],
+ flags: ["group", "speech", "voting"],
+ targets: { include: ["alive"] },
+ whileDead: true,
+ passiveDead: true,
+ action: {
+ labels: ["lynch"],
+ run: function () {
+ if (this.dominates())
+ this.target.kill()
+ }
+ }
+ }
+ };
+ }
+
+}
diff --git a/Games/types/Ghost/roles/cards/WinWithGhost.js b/Games/types/Ghost/roles/cards/WinWithGhost.js
new file mode 100644
index 000000000..b6f750700
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/WinWithGhost.js
@@ -0,0 +1,19 @@
+const Card = require("../../Card");
+
+module.exports = class WinWithGhost extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.winCheck = {
+ priority: 0,
+ check: function (counts, winners, aliveCount) {
+ if (aliveCount > 0 && (counts["Ghost"] >= aliveCount / 2)
+ || (this.guessedWord === this.game.townWord)) {
+ winners.addPlayer(this.player, "Ghost");
+ }
+ }
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/roles/cards/WinWithTown.js b/Games/types/Ghost/roles/cards/WinWithTown.js
new file mode 100644
index 000000000..9dbf48f80
--- /dev/null
+++ b/Games/types/Ghost/roles/cards/WinWithTown.js
@@ -0,0 +1,17 @@
+const Card = require("../../Card");
+
+module.exports = class WinWithTown extends Card {
+
+ constructor(role) {
+ super(role);
+
+ this.winCheck = {
+ priority: 0,
+ check: function (counts, winners, aliveCount) {
+ if (aliveCount > 0 && counts["Town"] == aliveCount)
+ winners.addPlayer(this.player, "Town");
+ }
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/Games/types/Ghost/templates/death.js b/Games/types/Ghost/templates/death.js
new file mode 100644
index 000000000..82ae373cf
--- /dev/null
+++ b/Games/types/Ghost/templates/death.js
@@ -0,0 +1,7 @@
+module.exports = function (type, name) {
+ const templates = {
+ "lynch": `${name} was executed by the town.`,
+ };
+
+ return templates[type];
+};
diff --git a/data/constants.js b/data/constants.js
index 30707fb8a..e489ace9c 100644
--- a/data/constants.js
+++ b/data/constants.js
@@ -1,18 +1,20 @@
module.exports = {
restart: null,
- gameTypes: ["Mafia", "Split Decision", "Resistance", "One Night"],
+ gameTypes: ["Mafia", "Split Decision", "Resistance", "One Night", "Ghost"],
lobbies: ["Main", "Sandbox", "Competitive", "Games"],
alignments: {
"Mafia": ["Village", "Mafia", "Monsters", "Independent"],
"Split Decision": ["Blue", "Red", "Independent"],
"Resistance": ["Resistance", "Spies"],
"One Night": ["Village", "Werewolves", "Independent"],
+ "Ghost": ["Town", "Ghost"],
},
startStates: {
"Mafia": ["Night", "Day"],
"Split Decision": ["Round"],
"Resistance": ["Team Selection"],
"One Night": ["Night"],
+ "Ghost": ["Night"],
},
configurableStates: {
"Mafia": {
@@ -67,6 +69,28 @@ module.exports = {
max: 10 * 60 * 1000,
default: 2 * 60 * 1000
}
+ },
+ "Ghost": {
+ "Night": {
+ min: 1 * 60 * 1000,
+ max: 1 * 60 * 1000,
+ default: 1 * 60 * 1000
+ },
+ "Give Clue": {
+ min: 1 * 60 * 1000,
+ max: 3 * 60 * 1000,
+ default: 2 * 60 * 1000
+ },
+ "Day": {
+ min: 1 * 60 * 1000,
+ max: 30 * 60 * 1000,
+ default: 10 * 60 * 1000
+ },
+ "Guess Word": {
+ min: 1 * 60 * 1000,
+ max: 3 * 60 * 1000,
+ default: 2 * 60 * 1000
+ },
},
},
noQuotes: {},
@@ -94,6 +118,7 @@ module.exports = {
"Split Decision": {},
"Resistance": {},
"One Night": {},
+ "Ghost": {},
},
maxPlayers: 50,
diff --git a/data/roles.js b/data/roles.js
index fc1a18dc9..f2a586c4e 100644
--- a/data/roles.js
+++ b/data/roles.js
@@ -1292,6 +1292,29 @@ const roleData = {
]
},
},
+ "Ghost": {
+ "Town": {
+ alignment: "Town",
+ description: [
+ "Knows the hidden word."
+ ]
+ },
+ "Fool": {
+ alignment: "Town",
+ description: [
+ "Knows the decoy word, which has the same number of letters as the hidden word.",
+ "Appears to self as Town, and does not know that their word is the decoy word.",
+ ]
+ },
+ "Ghost": {
+ alignment: "Ghost",
+ description: [
+ "Knows other Ghosts.",
+ "Only knows the number of letters in the hidden word.",
+ "Must blend in and guess the hidden word.",
+ ]
+ },
+ }
};
module.exports = roleData;
diff --git a/react_main/src/Constants.jsx b/react_main/src/Constants.jsx
index 93342eca9..5d72b11ad 100644
--- a/react_main/src/Constants.jsx
+++ b/react_main/src/Constants.jsx
@@ -1,4 +1,4 @@
-export const GameTypes = ["Mafia", "Split Decision", "Resistance", "One Night"];
+export const GameTypes = ["Mafia", "Split Decision", "Resistance", "One Night", "Ghost"];
export const Lobbies = ["Main", "Sandbox", "Competitive", "Games"];
export const Alignments = {
@@ -6,6 +6,7 @@ export const Alignments = {
"Split Decision": ["Blue", "Red", "Independent"],
"Resistance": ["Resistance", "Spies"],
"One Night": ["Village", "Werewolves", "Independent"],
+ "Ghost": ["Town", "Ghost"],
};
export const GameStates = {
@@ -13,6 +14,7 @@ export const GameStates = {
"Split Decision": ["Initial Round", "Hostage Swap"],
"Resistance": ["Team Selection", "Team Approval", "Mission"],
"One Night": ["Day", "Night"],
+ "Ghost": ["Night", "Give Clue", "Day", "Guess Word"],
};
export const RatingThresholds = {
diff --git a/react_main/src/css/gameGhost.css b/react_main/src/css/gameGhost.css
new file mode 100644
index 000000000..488d592cf
--- /dev/null
+++ b/react_main/src/css/gameGhost.css
@@ -0,0 +1,35 @@
+
+/* ghost */
+
+.game .ghost {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+
+ margin-bottom: 10px;
+ padding: 0px 5px;
+
+ color: #bd4c4c;
+}
+
+.game .ghost-name {
+ margin-top: 10px;
+ padding-top: 0px;
+
+ text-align: center;
+ font-weight: bold;
+}
+
+.game .ghost-history-group {
+ margin-bottom: 15px;
+}
+
+.game .ghost-input span {
+ margin-right: 4px;
+ color: #bd4c4c;
+}
+
+.game .ghost-input {
+ color: black;
+ size: 8px;
+}
\ No newline at end of file
diff --git a/react_main/src/pages/Game/Game.jsx b/react_main/src/pages/Game/Game.jsx
index c5151ab58..c819cd7e8 100644
--- a/react_main/src/pages/Game/Game.jsx
+++ b/react_main/src/pages/Game/Game.jsx
@@ -11,6 +11,7 @@ import MafiaGame from "./MafiaGame";
import SplitDecisionGame from "./SplitDecisionGame";
import ResistanceGame from "./ResistanceGame";
import OneNightGame from "./OneNightGame";
+import GhostGame from "./GhostGame";
import { GameContext, PopoverContext, SiteInfoContext, UserContext } from "../../Contexts";
import Dropdown, { useDropdown } from "../../components/Dropdown";
import Setup from "../../components/Setup";
@@ -671,6 +672,9 @@ function GameWrapper(props) {
{gameType == "One Night" &&