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