WheelOfFortune/server.js

541 lines
19 KiB
JavaScript

const WebSocket = require('ws')
const { v4: uuidv4 } = require('uuid')
const puzzles = require('./puzzles.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)) {
//
}
else console.error('invalid input to checkGuess() function',currentPuzzle.puzzle[0])
}
//these functions must have both gameStateobject and websockets initialized before use!!!
function rewardUser(gameStateObject,ws) {
const roomCode = ws.roomCode
const room = rooms[ws.roomCode]
//find the current player
let currentUserTurn = gameStateObject.players[gameStateObject.turn]
currentUserTurn.win++
gameStateObject.puzzleLevel++
room.clients.forEach((client) => {
client.send(JSON.stringify({
type: 'puzzle_solved', roomCode,
user: currentUserTurn.name
}))
})
}
function changeTurn(gameStateObject,ws) {
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) {
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
}))
})
}
// 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 === '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'}))
} else ws.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)
if (room && room.leader === ws) {
room.clients.forEach((client) => {
client.send(JSON.stringify({
type: 'game_started',
roomCode: ws.roomCode,
// eslint-disable-next-line no-undef
puzzle: removeProp(currentPuzzle,'answer'),
turn:room.gameState.turn,
turnState:room.gameState.turnState
}))
})
} 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 == 'Bankrupt') {
room.gameState.players = room.gameState.players.map((player) => {
return player.id == ws.identifierToken ? {...player, points:0} : player
})
}
if (spinResult == 'spin again') {
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: ws === room.leader ? 'Leader' : 'Player'
}))
)
}
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: ws === room.leader ? 'Leader' : 'Player',
turnState:room.gameState.turnState
}))
})
changeTurn(room.gameState,ws)
}
else if(guessResult === 'puzzleSolved') {
rewardUser(room.gameState,ws)
loadNewPuzzle(room.gameState,ws)
}
else {
//the player guessed correctly and its still their turn
room.gameState.turn = 'spin'
room.clients.forEach((client) =>
client.send(JSON.stringify(
{ type: 'guess_result', letter,
correct: guessResult,
player: ws === room.leader ? 'Leader' : 'Player',
puzzle: room.gameState.puzzles[room.gameState.puzzleLevel],
turn:room.gameState.turn,
turnState:room.gameState.turnState
}))
)
}
}
} 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)
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
}