| @@ -0,0 +1,104 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"/> | ||
|
|
||
| <!-- Ensure that everything scales appropriately on a mobile device --> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> | ||
|
|
||
| <!-- Let's borrow a cool looking Font from Google --> | ||
| <link href='https://fonts.googleapis.com/css?family=Quicksand:300,400,700' rel='stylesheet' type='text/css'> | ||
|
|
||
| <link href="roomstyle.css" rel="stylesheet"> | ||
| </head> | ||
|
|
||
| <body> | ||
|
|
||
|
|
||
| <div id="gameArea"> | ||
| <!-- This is where the templates defined below will be used --> | ||
| </div> | ||
|
|
||
| <!-- Main Title Screen that appears when the page loads for the first time --> | ||
| <script id="intro-screen-template" type="text/template"> | ||
|
|
||
| <div class="titleWrapper"> | ||
|
|
||
| <div class="title"> | ||
| ANAGRAMMATIX | ||
| </div> | ||
|
|
||
| <div class="buttons"> | ||
|
|
||
| <button id="btnCreateGame" class="btn left">CREATE</button> | ||
| <button id="btnJoinGame" class="btn right">JOIN</button> | ||
|
|
||
| </div> | ||
|
|
||
| </div> | ||
|
|
||
| </script> | ||
|
|
||
| <!-- This screen appears when a user clicks "CREATE" on the Title Screen --> | ||
| <script id="create-game-template" type="text/template"> | ||
| <div class="createGameWrapper"> | ||
|
|
||
| <div class="info">Open this site on your mobile device:</div> | ||
| <div id="gameURL" class="infoBig">Error!</div> | ||
|
|
||
| <div class="info">Then click <strong>JOIN</strong> and <br/> enter the following Game ID:</div> | ||
| <div id="spanNewGameCode" class="gameId">Error!</div> | ||
|
|
||
| <div id="playersWaiting"></div> | ||
| </div> | ||
| </script> | ||
|
|
||
| <!-- This scrreen appears when a player clicks "JOIN" on the Title Screen --> | ||
| <script id="join-game-template" type="text/template"> | ||
| <div class="joinGameWrapper"> | ||
| <div class="info"> | ||
| <label for="inputPlayerName">Your Name:</label> | ||
| <input id="inputPlayerName" type="text" /> | ||
| </div> | ||
|
|
||
| <div class="info"> | ||
| <label for="inputGameId">Game ID:</label> | ||
| <input id="inputGameId" type="text"/> | ||
| </div> | ||
|
|
||
| <div class="info buttons"> | ||
| <button id="btnStart" class="btn">Start</button> | ||
| <div id="playerWaitingMessage"></div> | ||
| </div> | ||
| </div> | ||
| </script> | ||
|
|
||
| <!-- This is the 'Host' screen. It displays the word for each player to match --> | ||
| <script id="host-game-template" type="text/template"> | ||
| <div id="wordArea"> | ||
| <div id="hostWord">5</div> | ||
| </div> | ||
| <div id="playerScores"> | ||
| <div id="player1Score" class="playerScore"> | ||
| <span class="score">0</span><span class="playerName">Player 1</span> | ||
| </div> | ||
| <div id="player2Score" class="playerScore"> | ||
| <span class="playerName">Player 2</span><span class="score">0</span> | ||
| </div> | ||
| </div> | ||
| </script> | ||
|
|
||
| <!-- JavaScript Libraries --> | ||
|
|
||
| <!-- jQuery! --> | ||
| <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> | ||
|
|
||
| <!-- If Socket.IO is used with Express, then the /socket.io/ path will | ||
| serve the proper Socket.IO javascript files used by the browser --> | ||
| <script src="/socket.io/socket.io.js"></script> | ||
|
|
||
| <!-- app.js is where all the client-side Anagrammatix game logic --> | ||
| <script src="gamelogic.js"></script> | ||
|
|
||
| </body> | ||
| </html> |
| @@ -0,0 +1,318 @@ | ||
| /* | ||
| Dark Green : #78BD4C | ||
| Mid Green : #A8FF56 | ||
| Lite : #ECFFE0 | ||
| Dark Purple : #A22FB0 | ||
| */ | ||
|
|
||
| html, body { | ||
| background-color: #78BD4C; | ||
| height: 100%; | ||
| width: 100%; | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /* ****************************** | ||
| BUTTONS! | ||
| (Inspired by Bootstrap 3.0) | ||
| ****************************** */ | ||
|
|
||
| .btn { | ||
| display: inline-block; | ||
| height: 100%; | ||
| width: 49%; | ||
| margin-bottom: 0; | ||
| color: #ECFFE0; | ||
| font-family: 'Quicksand', sans-serif; | ||
| font-weight: 700; | ||
| font-size: 2em; | ||
| line-height: 1.2; | ||
| text-align: center; | ||
| background-color: #8AD453; | ||
| white-space: nowrap; | ||
| vertical-align: middle; | ||
| cursor: pointer; | ||
| border: 1px solid transparent; | ||
| -webkit-user-select: none; | ||
| -moz-user-select: none; | ||
| -ms-user-select: none; | ||
| -o-user-select: none; | ||
| user-select: none; | ||
| } | ||
|
|
||
| .btn:focus { | ||
| outline: thin dotted #333; | ||
| outline: 5px auto -webkit-focus-ring-color; | ||
| outline-offset: -2px; | ||
| } | ||
|
|
||
| .btn:hover, | ||
| .btn:focus { | ||
| color: #AE37B2; | ||
| text-decoration: none; | ||
| } | ||
|
|
||
| .btn:active, | ||
| .btn.active { | ||
| outline: 0; | ||
| -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); | ||
| box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); | ||
| } | ||
|
|
||
| .left { | ||
| float: left; | ||
| } | ||
|
|
||
| .right { | ||
| float: right; | ||
| } | ||
|
|
||
| /* ****************************** | ||
| TITLE SCREEN | ||
| (intro-screen-template) | ||
| ****************************** */ | ||
|
|
||
| #gameArea { | ||
| height: 100%; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .titleWrapper { | ||
| font-family: 'Quicksand', sans-serif; | ||
| font-weight: 400; | ||
| position: absolute; | ||
| height: 50%; | ||
| width: 96%; | ||
| margin: auto; | ||
| bottom: 0; | ||
| left: 0; | ||
| top: 0; | ||
| right: 0; | ||
| } | ||
|
|
||
| .title { | ||
| margin: 0px auto; | ||
| text-align: center; | ||
| width: 100%; | ||
| font-family: 'Quicksand', sans-serif; | ||
| color: #ECFFE0; | ||
| font-weight: 300; | ||
| } | ||
|
|
||
| .buttons { | ||
| width: 100%; | ||
| text-align: center; | ||
| } | ||
|
|
||
| /* ****************************** | ||
| START SCREEN | ||
| (create-game-template) | ||
| ****************************** */ | ||
|
|
||
| .createGameWrapper, .joinGameWrapper, .gameOver { | ||
| font-family: 'Quicksand', sans-serif; | ||
| margin: 0 auto; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .createGameWrapper .info{ | ||
| color: #ECFFE0; | ||
| font-weight: 400; | ||
| font-size: 2em; | ||
| margin-top: 1.5em; | ||
| } | ||
|
|
||
| .createGameWrapper{ | ||
| color: #AE37B2; | ||
| font-weight: 300; | ||
| } | ||
|
|
||
| .info label { | ||
| display: block; | ||
| } | ||
|
|
||
| .info input { | ||
| text-align: center; | ||
| padding: 10px; | ||
| width: 200px; | ||
| height: 60px; | ||
| font-family: 'Quicksand', sans-serif; | ||
| color: #A22FB0; | ||
| font-weight: 300; | ||
| font-size: 54px; | ||
| border: 1px dotted white; | ||
| border-radius: 10px; | ||
| background: transparent; | ||
| } | ||
|
|
||
| .info input#inputPlayerName { | ||
| width: 90%; | ||
| } | ||
|
|
||
|
|
||
| /* ****************************** | ||
| JOIN SCREEN | ||
| (join-game-template) | ||
| ****************************** */ | ||
|
|
||
| .joinGameWrapper { | ||
| margin: 0; | ||
| padding: 0; | ||
| height: 100%; | ||
| min-height: 320px; | ||
| } | ||
|
|
||
| .joinGameWrapper .info { | ||
| font-size: 1.5em; | ||
| color: #ECFFE0; | ||
| height: 30%; | ||
| padding-top: 1%; | ||
| } | ||
|
|
||
| .joinGameWrapper .btn { | ||
| width: 100%; | ||
| height: 50%; | ||
| } | ||
|
|
||
| #playerWaitingMessage { | ||
| font-size: .8em; | ||
| } | ||
|
|
||
| .createGameWrapper #gameURL { | ||
| font-size: 4em; | ||
| } | ||
|
|
||
| .createGameWrapper .gameId { | ||
| font-size: 8em; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /* ****************************** | ||
| PLAYER GAME SCREEN | ||
| ****************************** */ | ||
|
|
||
|
|
||
| #ulAnswers { | ||
| width: 100%; | ||
| height: 100%; | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
|
|
||
| #ulAnswers li { | ||
| width: 100%; | ||
| height: 16.6%; | ||
| } | ||
|
|
||
| .btnAnswer { | ||
| width: 100%; | ||
| border-bottom: 1px dotted white; | ||
| font-weight: 400; | ||
| font-size: 3em; | ||
| color: #AE37B2; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /* ****************************** | ||
| HOST SCREEN | ||
| (host-game-template) | ||
| ****************************** */ | ||
|
|
||
| /* Absolute Centering: http://coding.smashingmagazine.com/2013/08/09/absolute-horizontal-vertical-centering-css/ */ | ||
| #wordArea { | ||
| font-family: 'Quicksand', sans-serif; | ||
| font-weight: 400; | ||
| position: absolute; | ||
| height: 50%; | ||
| width: 50%; | ||
| margin: auto; | ||
| bottom: 0; | ||
| left: 0; | ||
| top: 0; | ||
| right: 0; | ||
| } | ||
|
|
||
| #hostWord { | ||
| height: 100%; | ||
| width: 100%; | ||
| } | ||
|
|
||
| #hostWord h2, #btnPlayerRestart h3 { | ||
| font-size: 161px; | ||
| line-height: 1em; | ||
| font-family: 'Quicksand', sans-serif; | ||
| font-weight: 400; | ||
| margin: 0; | ||
| padding: 0; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| #playerScores { | ||
| height: 10%; | ||
| width: 100%; | ||
| position: fixed; | ||
| top: 0; | ||
| } | ||
|
|
||
| .playerScore { | ||
| width: 50%; | ||
| padding: 0; | ||
| margin: 0; | ||
| font-family: 'Quicksand', sans-serif; | ||
| font-weight: 400; | ||
| height: 100%; | ||
| } | ||
|
|
||
| .playerScore span { | ||
| display: inline-block; | ||
| font-size: 1.4EM; | ||
| padding: 2%; | ||
| min-width: 10%; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .playerScore .score { | ||
| text-align: center; | ||
| background-color: #A8FF56; | ||
| } | ||
|
|
||
| .playerScore .playerName { | ||
| width: 77%; | ||
| background-color: #ECFFE0; | ||
| } | ||
|
|
||
| #player1Score { | ||
| float: left; | ||
| } | ||
|
|
||
| #player2Score { | ||
| float: right; | ||
| text-align: right; | ||
| } | ||
|
|
||
|
|
||
| /* ****************************** | ||
| TITLE SCREEN | ||
| (intro-screen-template) | ||
| ****************************** */ | ||
|
|
||
| .gameOver { | ||
| display: block; | ||
| height: 20%; | ||
| padding-top: 10%; | ||
| font-size: 2em; | ||
| } | ||
|
|
||
| .btnGameOver { | ||
| width: 100%; | ||
| height: 10%; | ||
| position: fixed; | ||
| bottom: 0; | ||
| } | ||
|
|
||
|
|
| @@ -0,0 +1,279 @@ | ||
| var io; | ||
| var gameSocket; | ||
|
|
||
| /** | ||
| * This function is called by index.js to initialize a new game instance. | ||
| * | ||
| * @param sio The Socket.IO library | ||
| * @param socket The socket object for the connected client. | ||
| */ | ||
| exports.initGame = function(sio, socket){ | ||
| io = sio; | ||
| gameSocket = socket; | ||
| gameSocket.emit('connected', { message: "You are connected!" }); | ||
|
|
||
| // Host Events | ||
| gameSocket.on('hostCreateNewGame', hostCreateNewGame); | ||
| gameSocket.on('hostRoomFull', hostPrepareGame); | ||
| gameSocket.on('hostCountdownFinished', hostStartGame); | ||
| gameSocket.on('hostNextRound', hostNextRound); | ||
|
|
||
| // Player Events | ||
| gameSocket.on('playerJoinGame', playerJoinGame); | ||
| gameSocket.on('playerAnswer', playerAnswer); | ||
| gameSocket.on('playerRestart', playerRestart); | ||
| } | ||
|
|
||
| /* ******************************* | ||
| * * | ||
| * HOST FUNCTIONS * | ||
| * * | ||
| ******************************* */ | ||
|
|
||
| /** | ||
| * The 'START' button was clicked and 'hostCreateNewGame' event occurred. | ||
| */ | ||
| function hostCreateNewGame() { | ||
| // Create a unique Socket.IO Room | ||
| var thisGameId = ( Math.random() * 100000 ) | 0; | ||
|
|
||
| // Return the Room ID (gameId) and the socket ID (mySocketId) to the browser client | ||
| this.emit('newGameCreated', {gameId: thisGameId, mySocketId: this.id}); | ||
|
|
||
| // Join the Room and wait for the players | ||
| this.join(thisGameId.toString()); | ||
| }; | ||
|
|
||
| /* | ||
| * Two players have joined. Alert the host! | ||
| * @param gameId The game ID / room ID | ||
| */ | ||
| function hostPrepareGame(gameId) { | ||
| var sock = this; | ||
| var data = { | ||
| mySocketId : sock.id, | ||
| gameId : gameId | ||
| }; | ||
| //console.log("All Players Present. Preparing game..."); | ||
| io.sockets.in(data.gameId).emit('beginNewGame', data); | ||
| } | ||
|
|
||
| /* | ||
| * The Countdown has finished, and the game begins! | ||
| * @param gameId The game ID / room ID | ||
| */ | ||
| function hostStartGame(gameId) { | ||
| console.log('Game Started.'); | ||
| sendWord(0,gameId); | ||
| }; | ||
|
|
||
| /** | ||
| * A player answered correctly. Time for the next word. | ||
| * @param data Sent from the client. Contains the current round and gameId (room) | ||
| */ | ||
| function hostNextRound(data) { | ||
| if(data.round < wordPool.length ){ | ||
| // Send a new set of words back to the host and players. | ||
| sendWord(data.round, data.gameId); | ||
| } else { | ||
| // If the current round exceeds the number of words, send the 'gameOver' event. | ||
| io.sockets.in(data.gameId).emit('gameOver',data); | ||
| } | ||
| } | ||
| /* ***************************** | ||
| * * | ||
| * PLAYER FUNCTIONS * | ||
| * * | ||
| ***************************** */ | ||
|
|
||
| /** | ||
| * A player clicked the 'START GAME' button. | ||
| * Attempt to connect them to the room that matches | ||
| * the gameId entered by the player. | ||
| * @param data Contains data entered via player's input - playerName and gameId. | ||
| */ | ||
| function playerJoinGame(data) { | ||
| //console.log('Player ' + data.playerName + 'attempting to join game: ' + data.gameId ); | ||
|
|
||
| // A reference to the player's Socket.IO socket object | ||
| var sock = this; | ||
|
|
||
| // Look up the room ID in the Socket.IO manager object. | ||
| var room = gameSocket.manager.rooms["/" + data.gameId]; | ||
|
|
||
| // If the room exists... | ||
| if( room != undefined ){ | ||
| // attach the socket id to the data object. | ||
| data.mySocketId = sock.id; | ||
|
|
||
| // Join the room | ||
| sock.join(data.gameId); | ||
|
|
||
| //console.log('Player ' + data.playerName + ' joining game: ' + data.gameId ); | ||
|
|
||
| // Emit an event notifying the clients that the player has joined the room. | ||
| io.sockets.in(data.gameId).emit('playerJoinedRoom', data); | ||
|
|
||
| } else { | ||
| // Otherwise, send an error message back to the player. | ||
| this.emit('error',{message: "This room does not exist."} ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A player has tapped a word in the word list. | ||
| * @param data gameId | ||
| */ | ||
| function playerAnswer(data) { | ||
| // console.log('Player ID: ' + data.playerId + ' answered a question with: ' + data.answer); | ||
|
|
||
| // The player's answer is attached to the data object. \ | ||
| // Emit an event with the answer so it can be checked by the 'Host' | ||
| io.sockets.in(data.gameId).emit('hostCheckAnswer', data); | ||
| } | ||
|
|
||
| /** | ||
| * The game is over, and a player has clicked a button to restart the game. | ||
| * @param data | ||
| */ | ||
| function playerRestart(data) { | ||
| // console.log('Player: ' + data.playerName + ' ready for new game.'); | ||
|
|
||
| // Emit the player's data back to the clients in the game room. | ||
| data.playerId = this.id; | ||
| io.sockets.in(data.gameId).emit('playerJoinedRoom',data); | ||
| } | ||
|
|
||
| /* ************************* | ||
| * * | ||
| * GAME LOGIC * | ||
| * * | ||
| ************************* */ | ||
|
|
||
| /** | ||
| * Get a word for the host, and a list of words for the player. | ||
| * | ||
| * @param wordPoolIndex | ||
| * @param gameId The room identifier | ||
| */ | ||
| function sendWord(wordPoolIndex, gameId) { | ||
| var data = getWordData(wordPoolIndex); | ||
| io.sockets.in(data.gameId).emit('newWordData', data); | ||
| } | ||
|
|
||
| /** | ||
| * This function does all the work of getting a new words from the pile | ||
| * and organizing the data to be sent back to the clients. | ||
| * | ||
| * @param i The index of the wordPool. | ||
| * @returns {{round: *, word: *, answer: *, list: Array}} | ||
| */ | ||
| function getWordData(i){ | ||
| // Randomize the order of the available words. | ||
| // The first element in the randomized array will be displayed on the host screen. | ||
| // The second element will be hidden in a list of decoys as the correct answer | ||
| var words = shuffle(wordPool[i].words); | ||
|
|
||
| // Randomize the order of the decoy words and choose the first 5 | ||
| var decoys = shuffle(wordPool[i].decoys).slice(0,5); | ||
|
|
||
| // Pick a random spot in the decoy list to put the correct answer | ||
| var rnd = Math.floor(Math.random() * 5); | ||
| decoys.splice(rnd, 0, words[1]); | ||
|
|
||
| // Package the words into a single object. | ||
| var wordData = { | ||
| round: i, | ||
| word : words[0], // Displayed Word | ||
| answer : words[1], // Correct Answer | ||
| list : decoys // Word list for player (decoys and answer) | ||
| }; | ||
|
|
||
| return wordData; | ||
| } | ||
|
|
||
| /* | ||
| * Javascript implementation of Fisher-Yates shuffle algorithm | ||
| * http://stackoverflow.com/questions/2450954/how-to-randomize-a-javascript-array | ||
| */ | ||
| function shuffle(array) { | ||
| var currentIndex = array.length; | ||
| var temporaryValue; | ||
| var randomIndex; | ||
|
|
||
| // While there remain elements to shuffle... | ||
| while (0 !== currentIndex) { | ||
|
|
||
| // Pick a remaining element... | ||
| randomIndex = Math.floor(Math.random() * currentIndex); | ||
| currentIndex -= 1; | ||
|
|
||
| // And swap it with the current element. | ||
| temporaryValue = array[currentIndex]; | ||
| array[currentIndex] = array[randomIndex]; | ||
| array[randomIndex] = temporaryValue; | ||
| } | ||
|
|
||
| return array; | ||
| } | ||
|
|
||
| /** | ||
| * Each element in the array provides data for a single round in the game. | ||
| * | ||
| * In each round, two random "words" are chosen as the host word and the correct answer. | ||
| * Five random "decoys" are chosen to make up the list displayed to the player. | ||
| * The correct answer is randomly inserted into the list of chosen decoys. | ||
| * | ||
| * @type {Array} | ||
| */ | ||
| var wordPool = [ | ||
| { | ||
| "words" : [ "sale","seal","ales","leas" ], | ||
| "decoys" : [ "lead","lamp","seed","eels","lean","cels","lyse","sloe","tels","self" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "item","time","mite","emit" ], | ||
| "decoys" : [ "neat","team","omit","tame","mate","idem","mile","lime","tire","exit" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "spat","past","pats","taps" ], | ||
| "decoys" : [ "pots","laps","step","lets","pint","atop","tapa","rapt","swap","yaps" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "nest","sent","nets","tens" ], | ||
| "decoys" : [ "tend","went","lent","teen","neat","ante","tone","newt","vent","elan" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "pale","leap","plea","peal" ], | ||
| "decoys" : [ "sale","pail","play","lips","slip","pile","pleb","pled","help","lope" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "races","cares","scare","acres" ], | ||
| "decoys" : [ "crass","scary","seeds","score","screw","cager","clear","recap","trace","cadre" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "bowel","elbow","below","beowl" ], | ||
| "decoys" : [ "bowed","bower","robed","probe","roble","bowls","blows","brawl","bylaw","ebola" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "dates","stead","sated","adset" ], | ||
| "decoys" : [ "seats","diety","seeds","today","sited","dotes","tides","duets","deist","diets" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "spear","parse","reaps","pares" ], | ||
| "decoys" : [ "ramps","tarps","strep","spore","repos","peris","strap","perms","ropes","super" ] | ||
| }, | ||
|
|
||
| { | ||
| "words" : [ "stone","tones","steno","onset" ], | ||
| "decoys" : [ "snout","tongs","stent","tense","terns","santo","stony","toons","snort","stint" ] | ||
| } | ||
| ] |