const WebSocket = require('ws') const { v4: uuidv4 } = require('uuid') const puzzles = require('./puzzles2.json') const names = require('./assignedNames.json') const crypto = require('crypto') const wss = new WebSocket.Server({ port: 8080 }) const rooms = {} // Stores rooms and their clients const wheel = [ 'lose a turn',800,350,450,700,300,600,5000, 300,600,300,500,800,550,400,300,900,500,'spin again', 900,'Bankrupt',600,400,300 ] //represents wheel in wheel of fortune game. // const wheel = ['spin again','Bankrupt','lose a turn'] function removeProp(obj, prop) { obj = JSON.parse(JSON.stringify(obj)) if(!Array.isArray(prop)){ if (!Object.hasOwn(obj,prop)) { console.error(`the property '${prop}' you are trying to remove doesn't exist on this object!`) } else { delete obj[prop] } } else { for (let i = 0; i < prop.length; i++) { const key = prop[i] if (!Object.hasOwn(obj, key)) { console.error(`The property '${key}' you are trying to remove doesn't exist on this object!`) } else { delete obj[key] } } } return obj } //make sure this is run so puzzles are easier to contruct function checkPuzzleData(puzzles) { for(let puzzle in puzzles) { let i = puzzle puzzle = puzzles[puzzle] if (Array.isArray(puzzle.answer)) { if (puzzle.answer.length != 4) { throw new Error(` puzzle at index ${i} doesn't have the correct amount of slots defined. The proper amount of slots is 4, and you have ${puzzle.answer.length}. use "" to denote an empty line. `) } for (let p of puzzle.answer){ if (p.length > 12) { throw new Error(`${p} (${p.length} characters long) is longer than 12 characters. only twelve characters allowed per line`) } } } else if (typeof puzzle.answer === 'string') { if (puzzle.answer.length > 12) { throw new Error(`${puzzle.answer} (${puzzle.answer.length} characters long) is longer than 12 characters. only twelve characters allowed per line`) } } } } function getRandomValue(array) { const randomIndex = crypto.randomInt(0, array.length) return array[randomIndex] } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { // Generate a cryptographically secure random index const j = crypto.randomInt(0, i + 1); // Swap elements [array[i], array[j]] = [array[j], array[i]] } return array } const DefaultNames = shuffleArray(names) let NameIndex = 0 function getRandomName() { if (NameIndex >= DefaultNames.length) { NameIndex = 0 }else{ NameIndex++ } //since we've incremented the index but not returned the value, //calculate the index again return NameIndex === 0 ? DefaultNames[NameIndex] : DefaultNames[NameIndex-1] } function loadCurrentPuzzle(gameStateObject) { console.log(gameStateObject) let letterArray //make sure we only show whats given in the letterArray, and empty values for everything else function processPuzzle(letterArray,given) { let puzzleArray = [] for (let letter of letterArray) { if (letter === ' ') { puzzleArray.push(' ') continue } let letterFound = false for (let g of given ) { if(g.toUpperCase() === letter.toUpperCase()) { puzzleArray.push(g) letterFound = true } } if (!letterFound) {puzzleArray.push('')} } console.log('in loadCurrentPuzzle: ',puzzleArray) return puzzleArray } //calculate the puzzle for now we'll just go through however many iterations there are //for each slot if (Array.isArray(gameStateObject.puzzles[gameStateObject.puzzleLevel].answer)) { let formattedPuzzle = [] for (let i of gameStateObject.puzzles[gameStateObject.puzzleLevel].answer) { letterArray = i.split('') const given = gameStateObject.puzzles[gameStateObject.puzzleLevel].given formattedPuzzle.push(processPuzzle(letterArray,given)) } return { 'answer':gameStateObject.puzzles[gameStateObject.puzzleLevel].answer, 'given':gameStateObject.puzzles[gameStateObject.puzzleLevel].given, 'category':gameStateObject.puzzles[gameStateObject.puzzleLevel].category, 'puzzle':formattedPuzzle, } } else { letterArray = gameStateObject.puzzles[gameStateObject.puzzleLevel].answer.split('') const given = gameStateObject.puzzles[gameStateObject.puzzleLevel].given gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle = processPuzzle(letterArray, given) //console.log(gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle); return { 'answer':gameStateObject.puzzles[gameStateObject.puzzleLevel].answer, 'given':gameStateObject.puzzles[gameStateObject.puzzleLevel].given, 'category':gameStateObject.puzzles[gameStateObject.puzzleLevel].category, 'puzzle':gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle, } } } function checkGuess(letter,gameStateObject) { console.log('checkguess: ',gameStateObject,letter) const currentPuzzle = gameStateObject.puzzles[gameStateObject.puzzleLevel] console.log(currentPuzzle) if (typeof currentPuzzle.answer === 'string') { //check if user already guessed one of teh given letters if (currentPuzzle.puzzle.find((x)=> x.toUpperCase() === letter.toUpperCase())) { console.log('user gave a guess on a given letter...') return false } //first create an indexed hashMap let charArray = currentPuzzle.answer.split('') let matches = [] for(const c in charArray) { const char = charArray[c] if (letter.toUpperCase() === char.toUpperCase()) { matches.push({id:c,letter:char}) } } //if there are any matches write them to the gameState Object if (!matches.length) { return false } else { for(const m of matches) { gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle[m.id] = m.letter } //make sure to check if a win occurred // convert the answer string into an array of characters let checkAnswer = currentPuzzle.answer.split('') if (currentPuzzle.puzzle.every((val,i)=> val.toUpperCase() === checkAnswer[i].toUpperCase())) { return 'puzzleSolved' } console.log('matches found!!!', gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle) return [true, matches.length] } } else if (Array.isArray(currentPuzzle.answer)) { console.log(currentPuzzle.puzzle.flat()) if (currentPuzzle.puzzle.flat().find((x)=> x.toUpperCase() === letter.toUpperCase())) { console.log('user gave a guess on a given letter...') return false } //check each row of letters for the answer let matches = [] console.log(currentPuzzle.answer) for (let r in currentPuzzle.answer) { let row = currentPuzzle.answer[r] //first create an indexed hashMap let charArray = row.split('') for(const c in charArray) { const char = charArray[c] if (letter.toUpperCase() === char.toUpperCase()) { matches.push({id:c,row_index:r,letter:char}) } } } console.log(matches) //if there are any matches write them to the gameState Object if (!matches.length) { return false } else { for (let i in matches) { let match = matches[i] gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle[match.row_index][match.id] = match.letter } console.log(gameStateObject.puzzles[gameStateObject.puzzleLevel].answer) //make sure to check if a win occurred // convert the answer string into an array of characters console.log(gameStateObject.puzzles[gameStateObject.puzzleLevel].answer.flat()) let checkAnswer = gameStateObject.puzzles[gameStateObject.puzzleLevel].answer.flat().filter((letter) => letter != ' ').join('') let puzzleToCheck = gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle.flat().filter((letter) => letter != ' ').join('') console.log(checkAnswer,puzzleToCheck) if (gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle.flat().every((val,i)=> val.toUpperCase() === checkAnswer[i].toUpperCase())) { return 'puzzleSolved' } console.log('matches found!!!', gameStateObject.puzzles[gameStateObject.puzzleLevel].puzzle) return [true, matches.length] } } else console.error('invalid input to checkGuess() function',currentPuzzle.puzzle[0]) } function checkWinState(gameStateobject,ws){ if (gameStateobject.puzzleLevel === gameStateobject.puzzles.length) { console.log('announcing winner') announceWinner(gameStateobject,ws) return true } else return false } function checkSolvePuzzleGuess(guess,ws) { const room = rooms[ws.roomCode] console.log(room.gameState) //check if the answer is an array first as we have to parse it differently if thats the case. if (!Array.isArray(room.gameState.puzzles[room.gameState.puzzleLevel].answer)) { if (guess.toUpperCase() != room.gameState.puzzles[room.gameState.puzzleLevel].answer.toUpperCase()) { console.log(guess.toUpperCase(),room.gameState.puzzles[room.gameState.puzzleLevel].answer.toUpperCase()) //guess is wrong change turns console.log('guess is incorrect') const currentUserTurn = room.gameState.players[room.gameState.turn] room.clients.forEach((client) => { client.send(JSON.stringify({ type:'incorrect_puzzle_guess', message: `${currentUserTurn.name} guessed the puzzle incorrectly` })) }) changeTurn(room.gameState,ws) } else { console.log('guess i correct') //guessed correctly reward the user rewardUser(room.gameState,ws,true) if (!checkWinState(room.gameState,ws)) { console.log('winstate should be false here 214') loadNewPuzzle(room.gameState,ws) } } } else { //its probably an array console.log(room.gameState.puzzles[room.gameState.puzzleLevel]) let answer = room.gameState.puzzles[room.gameState.puzzleLevel].answer //convert and filter the answer into a string that can be compared seperated by spaces answer = answer.filter(word => word != '').join(' ') if (answer.toUpperCase() != guess.toUpperCase()) { console.log('wrong',guess.toUpperCase(),answer.toUpperCase()) //wrong changeTurn(room.gameState,ws) } else { //correct console.log('correct.',guess.toUpperCase(),answer.toUpperCase()) rewardUser(room.gameState,ws,true) if (!checkWinState(room.gameState,ws)) { console.log('winstate should be false here 236') loadNewPuzzle(room.gameState,ws) } } } } function announceWinner(gameStateObject,ws) { //find user with highest points. //announce to the world. const room = rooms[ws.roomCode] let winner = gameStateObject.players.sort((a,b)=> b.points - a.points) console.log(winner) //needs to be tested on multiple winners and decide ties. room.clients.forEach((client) => { client.send(JSON.stringify({ type:'game_over', winner: `${winner[0].name} wins with ${winner[0].points} points!!!`, playerStats: room.gameState.players })) }) console.log('winner', winner) } //these functions must have both gameStateobject and websockets initialized before use!!! function rewardUser(gameStateObject,ws,puzzleSolved = false) { const roomCode = ws.roomCode const room = rooms[ws.roomCode] //find the current player let currentUserTurn = gameStateObject.players[gameStateObject.turn] currentUserTurn.wins++ gameStateObject.puzzleLevel++ if (puzzleSolved) { currentUserTurn.points = currentUserTurn.points*3 console.log(currentUserTurn.points) } room.clients.forEach((client) => { client.send(JSON.stringify({ type: 'puzzle_solved', roomCode, user: currentUserTurn.name })) }) } function changeTurn(gameStateObject,ws) { console.log('change turn hit') const room = rooms[ws.roomCode] if (gameStateObject.turn === gameStateObject.players.length - 1) { gameStateObject.turn = 0 } else gameStateObject.turn++ //alert next user of their turn room.clients.forEach((client) => { client.send(JSON.stringify( { type: 'next_turn', user:room.gameState.players[room.gameState.turn].name, })) }) } function loadNewPuzzle(gameStateObject,ws) { console.log('loadNewPuzzle called.') const room = rooms[ws.roomCode] const newPuzzle = loadCurrentPuzzle(gameStateObject) room.gameState.turnState = 'spin' room.gameState.puzzles[room.gameState.puzzleLevel] = newPuzzle room.gameState.levelsRemaining = (room.gameState.puzzles.length - 1 - room.gameState.puzzleLevel) room.clients.forEach((client) => { client.send(JSON.stringify({ type: 'new_puzzle', roomCode: ws.roomCode, // eslint-disable-next-line no-undef puzzle: removeProp(newPuzzle,'answer'), turn:room.gameState.turn, turnState:room.gameState.turnState, playerStats: room.gameState.players })) }) } // function checkIfClientExistsInRoom(ws,room) { // console.log(room,ws) // //assumes ws message is sent and room code exists // for(const c in room.clients) { // const client = room.clients[c] // if (ws.identifierToken === client.identifierToken && ws.roomCode === client.roomCode) { // return true // } // } // return false // } checkPuzzleData(puzzles) // server.js wss.on('connection', (ws) => { ws.on('message', (message) => { const data = JSON.parse(message) if (data.type === 'change_name') { const roomCode = ws.roomCode const room = rooms[ws.roomCode] console.log(room) console.log(room.leader.name,room.clients[0].name) console.log(room.clients.filter((i)=> i.name === data.deadName)) console.log(ws.name,ws.identifierToken) //change names in all places. //if the leader wants to change their name account for this //note this could probably be exploited oneday and break the game but whatever //since there is no logging in of users this is bound to happen. //find the names by index using the identifier Token which i user can't easily create //a name collision. const nameToChange = room.clients.findIndex((i)=> i.identifierToken === ws.identifierToken ) console.log(nameToChange) if (nameToChange != -1) { room.clients[nameToChange].name = data.newName } if (room.leader.identifierToken === room.clients[nameToChange].identifierToken) { room.leader.name = data.newName } //if the user is not the leader, in theory just change the name on the clients list //not sure if it exists in other places but test and see //send name changed event to client, similar to joined room but it will work when people are in game. ws.send(JSON.stringify({ type: 'changed_name_you', roomCode, isLeader: rooms[roomCode].leader === ws, you:data.newName })) room.clients.forEach((client) => { client.send(JSON.stringify({ type: 'changed_name', roomCode, isLeader: room.leader === ws , clients: room.clients.map((i)=> i.name), leaderName:room.leader.name })) }) } if (data.type === 'solve_puzzle') { console.log('solve_puzzle hit') const room = rooms[ws.roomCode] if(data.guess == '') return if (room.gameState.turnState === null) { ws.send(JSON.stringify({ type: 'error', message: 'the game hasn\'t started yet!' })) return }else if (ws.identifierToken !== room.gameState.players[room.gameState.turn].id) { ws.send(JSON.stringify({ type: 'error', message: 'its not your turn to guess!' })) return } checkSolvePuzzleGuess(data.guess, ws) } if (data.type === 'confirm_id') { const room = rooms[ws.roomCode] const currentUserTurn = room.gameState.players[room.gameState.turn] room.gameState.turnState = 'spin' if (ws.name === currentUserTurn.name && ws.identifierToken === currentUserTurn.id) { ws.send(JSON.stringify({ type:'next_turn_confirmed', turnState:room.gameState.turnState})) } // } else ws.send(JSON.stringify({ type:'next_turn_denied'})) room.clients.forEach((client) => { if (client.name !== currentUserTurn.name && ws.identifierToken === currentUserTurn.id) { console.log(client) client.send(JSON.stringify({ type:'next_turn_denied'})) } }) } if (data.type === 'create_room') { const roomCode = uuidv4().slice(0, 5) ws.name = getRandomName() ws.identifierToken = uuidv4().slice(0, 5) rooms[roomCode] = { clients: [ws], leader: ws, gameState: { started:false, puzzles:shuffleArray(puzzles), puzzleLevel:0, turn:0, turnState:null, players:[] }, } console.log(rooms[roomCode].clients[0].name) ws.roomCode = roomCode ws.send(JSON.stringify({ type: 'room_created', roomCode, isLeader: true, leaderName: ws.name, clients: rooms[roomCode].clients.map((i)=> i.name), you:ws.name })) } if (data.type === 'join_room') { const { roomCode } = data console.log(rooms) console.log(rooms[roomCode],roomCode) if (!rooms[roomCode]) { ws.send(JSON.stringify({ type: 'error', message: 'Room not found' })) } else if (rooms[roomCode].gameState.started) { ws.send(JSON.stringify({ type: 'error', message: 'Game has already Started!!!' })) } else { ws.name = getRandomName() ws.identifierToken = uuidv4().slice(0, 5) rooms[roomCode].clients.push(ws) ws.roomCode = roomCode const room = rooms[ws.roomCode] ws.send(JSON.stringify({ type: 'joined_room_you', roomCode, isLeader: rooms[roomCode].leader === ws, you:ws.name })) room.clients.forEach((client) => { client.send(JSON.stringify({ type: 'joined_room', roomCode, isLeader: rooms[roomCode].leader === ws , clients: rooms[roomCode].clients.map((i)=> i.name), leaderName:rooms[roomCode].leader.name })) }) } //console.log('clients: ',rooms[roomCode].leader.name); } if (data.type === 'start_game') { const room = rooms[ws.roomCode] if (room.gameState.started) { ws.send(JSON.stringify({ type: 'error', message: 'game has already been started' })) } const currentPuzzle = loadCurrentPuzzle(room.gameState) // const clientPuzzle = Object.entries(currentPuzzle).filter(([key])=> key != 'answer') room.gameState.started = true room.gameState.turnState = 'spin' room.gameState.puzzles[room.gameState.puzzleLevel] = currentPuzzle room.gameState.levelsRemaining = (room.gameState.puzzles.length - 1 - room.gameState.puzzleLevel) room.clients.forEach((client)=>{ room.gameState.players.push({ id:client.identifierToken, name:client.name, points:0, wins:0 }) }) //console.log(room.gameState) console.log('game started for:',room) let selectedPlayer = room.clients.find((x)=>x.identifierToken === room.gameState.players[room.gameState.turn].id) if (selectedPlayer) { console.log('hit') selectedPlayer.send(JSON.stringify({type:'your_turn'})) } if (room && room.leader === ws) { room.clients.forEach((client) => { console.log(client.identifierToken) client.send(JSON.stringify({ type: 'game_started', roomCode: ws.roomCode, puzzle: removeProp(currentPuzzle,'answer'), turn:room.gameState.turn, turnState:room.gameState.turnState, playerStats:room.gameState.players.map(({id,...rest}) => rest) })) }) } else { ws.send(JSON.stringify({ type: 'error', message: 'Only the leader can start the game' })) } } if (data.type === 'spin_wheel' || data.type === 'guess_letter') { const room = rooms[ws.roomCode] if (room && room.clients.includes(ws)) { // Handle spin and guess events if (data.type === 'spin_wheel') { if (room.gameState.turnState === null) { ws.send(JSON.stringify({ type: 'error', message: 'the game hasn\'t started yet!' })) return }else if (ws.identifierToken !== room.gameState.players[room.gameState.turn].id) { ws.send(JSON.stringify({ type: 'error', message: 'its not your turn to spin!' })) return } // Simulate a wheel spin result and update room state let spinResult = getRandomValue(wheel) if (spinResult != ['lose a turn', 'spin again', 'Bankrupt'].indexOf(spinResult)) { room.gameState.turnState = 'guess' room.gameState.players = room.gameState.players.map((player) => { return player.id === ws.identifierToken ? {...player, points:parseInt(player.points)+parseInt(spinResult)} : player }) } if (spinResult === 'Bankrupt') { room.gameState.players = room.gameState.players.map((player) => { return player.id === ws.identifierToken ? {...player, points:0} : player }) } if (spinResult === 'spin again') { console.log('in spin again') room.gameState.turnState = 'spin' room.gameState.players = room.gameState.players.map((player) => { return player.id === ws.identifierToken ? {...player, points:player.points + 0, condition:'spin again'} : player }) } if (spinResult === 'lose a turn') { console.log('lose a turn here ig...?') changeTurn(room.gameState,ws) } room.gameState.players = room.gameState.players.map((player) => { return player.id === ws.identifierToken ? {...player, points:player.points + 0} : player }) console.log('players', room.gameState.players) room.gameState.spinResult = spinResult console.log('spin_result',room.gameState) room.clients.forEach((client) => client.send(JSON.stringify({ type: 'spin_result', spinResult, player: room.gameState.players[room.gameState.turn], turnState: room.gameState.turnState, playerStats:room.gameState.players.map(({id,...rest}) => rest) })) ) } if (data.type === 'guess_letter') { if (room.gameState.turnState === null) { ws.send(JSON.stringify({ type: 'error', message: 'the game hasn\'t started yet!' })) return }else if (ws.identifierToken !== room.gameState.players[room.gameState.turn].id) { ws.send(JSON.stringify({ type: 'error', message: 'its not your turn to guess!' })) return } room.gameState.turnState = 'guess' const { letter } = data // Handle guess logic (e.g., check if the letter is in the puzzle) const guessResult = checkGuess(letter,room.gameState) if (!guessResult) { room.gameState.turnState = 'spin' //if the player guesses incorrectly, and theres more than one player, //its the next players turn room.clients.forEach((client) => { client.send(JSON.stringify( { type: 'guess_result', letter, correct: guessResult, player: room.gameState.players[room.gameState.turn], turnState:room.gameState.turnState, playerStats:room.gameState.players.map(({id,...rest}) => rest) })) }) changeTurn(room.gameState,ws) } else if(guessResult === 'puzzleSolved') { rewardUser(room.gameState,ws) if (!checkWinState(room.gameState,ws)) { console.log('winstate should be false here 583') loadNewPuzzle(room.gameState,ws) } } else { //the player guessed correctly and its still their turn room.gameState.turnState = 'spin' console.log('correct guess!!',room.gameState.turn) room.clients.forEach((client) => client.send(JSON.stringify( { type: 'guess_result', letter, correct: guessResult, player: room.gameState.players[room.gameState.turn], puzzle: room.gameState.puzzles[room.gameState.puzzleLevel], turn:room.gameState.turn, turnState:room.gameState.turnState, playerStats:room.gameState.players.map(({id,...rest}) => rest) })) ) } } } else { ws.send(JSON.stringify({ type: 'error', message: 'You are not in this room' })) } } }) ws.on('close', () => { if (ws.roomCode && rooms[ws.roomCode]) { console.log('closing') const room = rooms[ws.roomCode] const roomCode = ws.roomCode room.clients = room.clients.filter((client) => client !== ws) room.gameState.players = room.gameState.players.filter((client)=> client.id != ws.identifierToken) //decide who's turn it is based on who leaves... if (room.leader === ws && room.clients.length > 0) { console.log('closing within room leader') room.leader = room.clients[0] room.leader.send(JSON.stringify({ type: 'new_leader' })) } room.clients.forEach((client) => client.send(JSON.stringify({ type: 'joined_room', roomCode, isLeader: rooms[roomCode].leader === ws , clients: rooms[roomCode].clients.map((i)=> i.name), leaderName:rooms[roomCode].leader.name })) ) if (room.clients.length === 0) delete rooms[ws.roomCode] } }) }) console.log('WebSocket server is running on ws://localhost:8080') module.exports = { DefaultNames,NameIndex, getRandomName,getRandomValue,shuffleArray }