{
"version": 3,
"sources": ["../../../../server/chat-plugins/hangman.ts"],
"sourcesContent": ["/*\r\n* Hangman chat plugin\r\n* By bumbadadabum and Zarel. Art by crobat.\r\n*/\r\nimport {FS, Utils} from '../../lib';\r\n\r\nconst HANGMAN_FILE = 'config/chat-plugins/hangman.json';\r\n\r\nconst DIACRITICS_AFTER_UNDERSCORE = /_[\\u0300-\\u036f\\u0483-\\u0489\\u0610-\\u0615\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06ED\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E]+/g;\r\nconst MAX_HANGMAN_LENGTH = 30;\r\nconst MAX_INDIVIDUAL_WORD_LENGTH = 20;\r\nconst MAX_HINT_LENGTH = 150;\r\n\r\ninterface HangmanEntry {\r\n\thints: string[];\r\n\ttags?: string[];\r\n}\r\n\r\n// futureproofing this into one single object so that new params can be added\r\n// more easily\r\ninterface HangmanOptions {\r\n\tallowCreator?: boolean;\r\n}\r\n\r\nexport let hangmanData: {[roomid: string]: {[phrase: string]: HangmanEntry}} = {};\r\n\r\ntry {\r\n\thangmanData = JSON.parse(FS(HANGMAN_FILE).readSync());\r\n\tlet save = false;\r\n\tfor (const roomid in hangmanData) {\r\n\t\tconst roomData = hangmanData[roomid] || {};\r\n\t\tconst roomKeys = Object.keys(roomData);\r\n\t\tif (roomKeys.length && !roomData[roomKeys[0]].hints) {\r\n\t\t\tsave = true;\r\n\t\t\tfor (const key of roomKeys) {\r\n\t\t\t\troomData[key] = {hints: roomData[key] as any};\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tif (save) {\r\n\t\tFS(HANGMAN_FILE).writeUpdate(() => JSON.stringify(hangmanData));\r\n\t}\r\n} catch {}\r\n\r\nconst maxMistakes = 6;\r\n\r\nexport class Hangman extends Rooms.SimpleRoomGame {\r\n\tgameNumber: number;\r\n\tcreator: ID;\r\n\tword: string;\r\n\thint: string;\r\n\tincorrectGuesses: number;\r\n\toptions: HangmanOptions;\r\n\r\n\tguesses: string[];\r\n\tletterGuesses: string[];\r\n\tlastGuesser: string;\r\n\twordSoFar: string[];\r\n\treadonly checkChat = true;\r\n\r\n\tconstructor(\r\n\t\troom: Room,\r\n\t\tuser: User,\r\n\t\tword: string,\r\n\t\thint = '',\r\n\t\tgameOptions: HangmanOptions = {}\r\n\t) {\r\n\t\tsuper(room);\r\n\r\n\t\tthis.gameNumber = room.nextGameNumber();\r\n\r\n\t\tthis.gameid = 'hangman' as ID;\r\n\t\tthis.title = 'Hangman';\r\n\t\tthis.creator = user.id;\r\n\t\tthis.word = word;\r\n\t\tthis.hint = hint;\r\n\t\tthis.incorrectGuesses = 0;\r\n\t\tthis.options = gameOptions;\r\n\r\n\t\tthis.guesses = [];\r\n\t\tthis.letterGuesses = [];\r\n\t\tthis.lastGuesser = '';\r\n\t\tthis.wordSoFar = [];\r\n\r\n\t\tfor (const letter of word) {\r\n\t\t\tif (/[a-zA-Z]/.test(letter)) {\r\n\t\t\t\tthis.wordSoFar.push('_');\r\n\t\t\t} else {\r\n\t\t\t\tthis.wordSoFar.push(letter);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tchoose(user: User, word: string) {\r\n\t\tif (user.id === this.creator && !this.options.allowCreator) {\r\n\t\t\tthrow new Chat.ErrorMessage(\"You can't guess in your own hangman game.\");\r\n\t\t}\r\n\r\n\t\tconst sanitized = word.replace(/[^A-Za-z ]/g, '');\r\n\t\tconst normalized = toID(sanitized);\r\n\t\tif (normalized.length < 1) {\r\n\t\t\tthrow new Chat.ErrorMessage(`Use \"/guess [letter]\" to guess a letter, or \"/guess [phrase]\" to guess the entire Hangman phrase.`);\r\n\t\t}\r\n\t\tif (sanitized.length > MAX_HANGMAN_LENGTH) {\r\n\t\t\tthrow new Chat.ErrorMessage(`Guesses must be ${MAX_HANGMAN_LENGTH} or fewer letters \u2013 \"${word}\" is too long.`);\r\n\t\t}\r\n\r\n\t\tfor (const guessid of this.guesses) {\r\n\t\t\tif (normalized === toID(guessid)) throw new Chat.ErrorMessage(`Your guess \"${word}\" has already been guessed.`);\r\n\t\t}\r\n\r\n\t\tif (sanitized.length > 1) {\r\n\t\t\tif (!this.guessWord(sanitized, user.name)) {\r\n\t\t\t\tthrow new Chat.ErrorMessage(`Your guess \"${sanitized}\" is invalid.`);\r\n\t\t\t} else {\r\n\t\t\t\tthis.room.addByUser(user, `${user.name} guessed \"${sanitized}\"!`);\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\tif (!this.guessLetter(sanitized, user.name)) {\r\n\t\t\t\tthrow new Chat.ErrorMessage(`Your guess \"${sanitized}\" is not a valid letter.`);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tguessLetter(letter: string, guesser: string) {\r\n\t\tletter = letter.toUpperCase();\r\n\t\tif (this.guesses.includes(letter)) return false;\r\n\t\tif (this.word.toUpperCase().includes(letter)) {\r\n\t\t\tfor (let i = 0; i < this.word.length; i++) {\r\n\t\t\t\tif (this.word[i].toUpperCase() === letter) {\r\n\t\t\t\t\tthis.wordSoFar[i] = this.word[i];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif (!this.wordSoFar.includes('_')) {\r\n\t\t\t\tthis.incorrectGuesses = -1;\r\n\t\t\t\tthis.guesses.push(letter);\r\n\t\t\t\tthis.letterGuesses.push(`${letter}1`);\r\n\t\t\t\tthis.lastGuesser = guesser;\r\n\t\t\t\tthis.finish();\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t\tthis.letterGuesses.push(`${letter}1`);\r\n\t\t} else {\r\n\t\t\tthis.incorrectGuesses++;\r\n\t\t\tthis.letterGuesses.push(`${letter}0`);\r\n\t\t}\r\n\r\n\t\tthis.guesses.push(letter);\r\n\t\tthis.lastGuesser = guesser;\r\n\t\tthis.update();\r\n\t\treturn true;\r\n\t}\r\n\r\n\tguessWord(word: string, guesser: string) {\r\n\t\tconst ourWord = toID(this.word.replace(/[0-9]+/g, ''));\r\n\t\tconst guessedWord = toID(word.replace(/[0-9]+/g, ''));\r\n\t\tconst wordSoFar = this.wordSoFar.filter(letter => /[a-zA-Z_]/.test(letter)).join('').toLowerCase();\r\n\r\n\t\t// Can't be a correct guess if the lengths don't match\r\n\t\tif (ourWord.length !== guessedWord.length) return false;\r\n\r\n\t\tfor (let i = 0; i < ourWord.length; i++) {\r\n\t\t\tif (wordSoFar.charAt(i) === '_') {\r\n\t\t\t\t// Can't be a correct guess if it contains letters already guessed\r\n\t\t\t\tif (this.letterGuesses.some(guess => guess.toLowerCase().startsWith(guessedWord.charAt(i)))) return false;\r\n\t\t\t} else if (wordSoFar.charAt(i) !== guessedWord.charAt(i)) {\r\n\t\t\t\t// Can't be a correct guess if the guess has incorrect letters in already guessed indexes\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (ourWord === guessedWord) {\r\n\t\t\tfor (const [i, letter] of this.wordSoFar.entries()) {\r\n\t\t\t\tif (letter === '_') {\r\n\t\t\t\t\tthis.wordSoFar[i] = this.word[i];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tthis.incorrectGuesses = -1;\r\n\t\t\tthis.guesses.push(word);\r\n\t\t\tthis.lastGuesser = guesser;\r\n\t\t\tthis.finish();\r\n\t\t} else {\r\n\t\t\tthis.incorrectGuesses++;\r\n\t\t\tthis.guesses.push(word);\r\n\t\t\tthis.lastGuesser = guesser;\r\n\t\t\tthis.update();\r\n\t\t}\r\n\t\treturn true;\r\n\t}\r\n\r\n\thangingMan() {\r\n\t\treturn ``;\r\n\t}\r\n\r\n\tgenerateWindow() {\r\n\t\tlet result = 0;\r\n\r\n\t\tif (this.incorrectGuesses === maxMistakes) {\r\n\t\t\tresult = 1;\r\n\t\t} else if (!this.wordSoFar.includes('_')) {\r\n\t\t\tresult = 2;\r\n\t\t}\r\n\r\n\t\tconst color = result === 1 ? 'red' : (result === 2 ? 'green' : 'blue');\r\n\t\tconst message = `${result === 1 ? 'Too bad! The mon has been hanged.' : (result === 2 ? 'The word has been guessed. Congratulations!' : 'Hangman')}`;\r\n\t\tlet output = `
${message}
`;\r\n\t\toutput += `| ${this.hangingMan()} | `;\r\n\r\n\t\tlet escapedWord = this.wordSoFar.map(Utils.escapeHTML);\r\n\t\tif (result === 1) {\r\n\t\t\tconst word = this.word;\r\n\t\t\tescapedWord = escapedWord.map((letter, index) =>\r\n\t\t\t\tletter === '_' ? `${word.charAt(index)}` : letter);\r\n\t\t}\r\n\t\tconst wordString = escapedWord.join('').replace(DIACRITICS_AFTER_UNDERSCORE, '_');\r\n\r\n\t\tif (this.hint) output += Utils.html` (Hint: ${this.hint}) `;\r\n\t\toutput += `${wordString} `;\r\n\t\tif (this.guesses.length) {\r\n\t\t\tif (this.letterGuesses.length) {\r\n\t\t\t\toutput += 'Letters: ' + this.letterGuesses.map(\r\n\t\t\t\t\tg => `${g[0]}`\r\n\t\t\t\t).join(', ');\r\n\t\t\t}\r\n\t\t\tif (result === 2) {\r\n\t\t\t\toutput += Utils.html`Winner: ${this.lastGuesser}`;\r\n\t\t\t} else if (this.guesses[this.guesses.length - 1].length === 1) {\r\n\t\t\t\t// last guess was a letter\r\n\t\t\t\toutput += Utils.html` – ${this.lastGuesser}`;\r\n\t\t\t} else {\r\n\t\t\t\toutput += Utils.html` Guessed: ${this.guesses[this.guesses.length - 1]} ` +\r\n\t\t\t\t\t`– ${this.lastGuesser}`;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\toutput += ' |