Multiplayer Game
Neil Haddley • May 9, 2023
Creating a new firebase project
I created a firebase project using https://console.firebase.google.com
I deployed Drew Conley's Multiplayer Game to Firebase.

https://haddley-multiplayer.web.app/

Firebase project settings

firebase initfirebase deploy
Responding to updated state
The state of the games is maintained in a Firebase Realtime Database.
The allPlayersRef.on("XXX", (snapshot) => {...}) functions are called when a player joins the game, leaves the game or moves.
The allCoinsRef.on("value", (snapshot) => {...}) functions are called when coins are created or collected.

Realtime Database
ChatGPT's commentary
This code appears to be a JavaScript implementation of a game that involves players moving characters on a map and collecting coins. Let's go through the code and explain its different parts:
1. The mapData object defines the boundaries of the game map (minX, maxX, minY, maxY) and the coordinates of blocked spaces on the map (blockedSpaces).
2. The playerColors array contains different colors that can be assigned to players in the game.
3. The randomFromArray function takes an array as input and returns a random element from that array.
4. The getKeyString function takes x and y coordinates and returns a string in the format "x:y".
5. The createName function generates a random name for a player by combining a random prefix and a random animal name.
6. The isSolid function checks if a given coordinate (x, y) represents a solid space on the map. It returns true if the space is blocked or if the coordinates are outside the map boundaries.
7. The getRandomSafeSpot function returns a random coordinate from a predefined set of safe spots on the map where players can initially spawn.
8. The code then defines an immediately-invoked function expression (IIFE) that encapsulates the game logic. Inside this function, various variables are declared and initialized:
playerId and playerRef represent the unique ID and reference for the current player.
players is an object that stores information about all the players in the game.
playerElements is an object that stores references to DOM elements representing each player's character.
coins is an object that stores information about the coins present on the map.
coinElements is an object that stores references to DOM elements representing each coin.
9. The code selects relevant DOM elements using document.querySelector.
10. The placeCoin function is responsible for randomly placing a coin on the map. It selects a safe spot using getRandomSafeSpot, creates a reference to the coin in the Firebase database, and schedules a recursive call to placeCoin after a random timeout.
11. The attemptGrabCoin function is called when a player moves to a new coordinate. It checks if there is a coin at the given coordinates and, if so, removes the coin from the database and increments the player's coin count.
12. The handleArrowPress function is called when a player presses an arrow key to move their character. It calculates the new coordinates based on the current position and the arrow key pressed. If the new coordinates represent a valid move (not blocked by a solid space), it updates the player's position, direction, and saves the changes to the database. It also calls attemptGrabCoin to check if the player grabbed a coin at the new position.
13. The initGame function initializes the game by adding event listeners for arrow key presses, setting up database listeners for player and coin updates, and creating DOM elements for players and coins.
14. The code sets up Firebase authentication using anonymous sign-in (firebase.auth().signInAnonymously()) and handles the authentication state change using firebase.auth().onAuthStateChanged. When a user is authenticated, the game initializes by generating a random name for the player, setting up the player's initial position, and adding the player to the database. It also sets up a disconnection handler to remove the player's data from the database when they disconnect.
15. Finally, the IIFE is invoked immediately, starting the execution of the game code.
In summary, this JavaScript code sets up a multiplayer game where players can control characters on a map. The game uses Firebase for real-time data synchronization. Players can move their characters using arrow keys, collect coins, and see the positions and coin counts of other players. The code initializes the game, handles player authentication, updates player and coin data in the database, and manages the rendering of player and coin elements in the game interface.
Security Rules
TEXT
1{ 2 "rules": { 3 ".read": "auth != null", 4 ".write": false, 5 "players": { 6 "$uid": { 7 ".write": "auth != null && auth.uid == $uid" 8 } 9 }, 10 "coins": { 11 ".write": "auth != null" 12 } 13 } 14}
index.html
TEXT
1<html> 2 3<head> 4 <title>Multiplayer Demo</title> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <link rel="preconnect" href="https://fonts.gstatic.com"> 7 <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700&display=swap" rel="stylesheet"> 8 <link rel="stylesheet" href="/styles.css" type="text/css"> 9</head> 10 11<body> 12 13 <div class="game-container"> 14 </div> 15 <div class="player-info"> 16 <div> 17 <label for="player-name">Your Name</label> 18 <input id="player-name" maxlength="10" type="text" /> 19 </div> 20 <div> 21 <button id="player-color">Change Color</button> 22 </div> 23 </div> 24 25 <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script> 26 <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script> 27 <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script> 28 <script> 29 // NOTE - INCLUDE YOUR FIREBASE CONFIG HERE FOR ANYTHING TO WORK: 30 const firebaseConfig = { 31 apiKey: "AIzaSyBfoImSImcCkM7TD-Z3X-LxWs4OEbGOaUQ", 32 authDomain: "haddley-multiplayer.firebaseapp.com", 33 projectId: "haddley-multiplayer", 34 storageBucket: "haddley-multiplayer.appspot.com", 35 messagingSenderId: "119673300748", 36 appId: "1:119673300748:web:8cb5b0ba8dbbae0834cb29" 37 }; 38 firebase.initializeApp(firebaseConfig); 39 </script> 40 41 42 <script src="/KeyPressListener.js"></script> 43 <script src="/app.js"></script> 44</body> 45 46</html>
app.js
TEXT
1const mapData = { 2 minX: 1, 3 maxX: 14, 4 minY: 4, 5 maxY: 12, 6 blockedSpaces: { 7 "7x4": true, 8 "1x11": true, 9 "12x10": true, 10 "4x7": true, 11 "5x7": true, 12 "6x7": true, 13 "8x6": true, 14 "9x6": true, 15 "10x6": true, 16 "7x9": true, 17 "8x9": true, 18 "9x9": true, 19 }, 20}; 21 22// Options for Player Colors... these are in the same order as our sprite sheet 23const playerColors = ["blue", "red", "orange", "yellow", "green", "purple"]; 24 25//Misc Helpers 26function randomFromArray(array) { 27 return array[Math.floor(Math.random() * array.length)]; 28} 29function getKeyString(x, y) { 30 return `${x}x${y}`; 31} 32 33function createName() { 34 const prefix = randomFromArray([ 35 "COOL", 36 "SUPER", 37 "HIP", 38 "SMUG", 39 "COOL", 40 "SILKY", 41 "GOOD", 42 "SAFE", 43 "DEAR", 44 "DAMP", 45 "WARM", 46 "RICH", 47 "LONG", 48 "DARK", 49 "SOFT", 50 "BUFF", 51 "DOPE", 52 ]); 53 const animal = randomFromArray([ 54 "BEAR", 55 "DOG", 56 "CAT", 57 "FOX", 58 "LAMB", 59 "LION", 60 "BOAR", 61 "GOAT", 62 "VOLE", 63 "SEAL", 64 "PUMA", 65 "MULE", 66 "BULL", 67 "BIRD", 68 "BUG", 69 ]); 70 return `${prefix} ${animal}`; 71} 72 73function isSolid(x,y) { 74 75 const blockedNextSpace = mapData.blockedSpaces[getKeyString(x, y)]; 76 return ( 77 blockedNextSpace || 78 x >= mapData.maxX || 79 x < mapData.minX || 80 y >= mapData.maxY || 81 y < mapData.minY 82 ) 83} 84 85function getRandomSafeSpot() { 86 //We don't look things up by key here, so just return an x/y 87 return randomFromArray([ 88 { x: 1, y: 4 }, 89 { x: 2, y: 4 }, 90 { x: 1, y: 5 }, 91 { x: 2, y: 6 }, 92 { x: 2, y: 8 }, 93 { x: 2, y: 9 }, 94 { x: 4, y: 8 }, 95 { x: 5, y: 5 }, 96 { x: 5, y: 8 }, 97 { x: 5, y: 10 }, 98 { x: 5, y: 11 }, 99 { x: 11, y: 7 }, 100 { x: 12, y: 7 }, 101 { x: 13, y: 7 }, 102 { x: 13, y: 6 }, 103 { x: 13, y: 8 }, 104 { x: 7, y: 6 }, 105 { x: 7, y: 7 }, 106 { x: 7, y: 8 }, 107 { x: 8, y: 8 }, 108 { x: 10, y: 8 }, 109 { x: 8, y: 8 }, 110 { x: 11, y: 4 }, 111 ]); 112} 113 114 115(function () { 116 117 let playerId; 118 let playerRef; 119 let players = {}; 120 let playerElements = {}; 121 let coins = {}; 122 let coinElements = {}; 123 124 const gameContainer = document.querySelector(".game-container"); 125 const playerNameInput = document.querySelector("#player-name"); 126 const playerColorButton = document.querySelector("#player-color"); 127 128 129 function placeCoin() { 130 const { x, y } = getRandomSafeSpot(); 131 const coinRef = firebase.database().ref(`coins/${getKeyString(x, y)}`); 132 coinRef.set({ 133 x, 134 y, 135 }) 136 137 const coinTimeouts = [2000, 3000, 4000, 5000]; 138 setTimeout(() => { 139 placeCoin(); 140 }, randomFromArray(coinTimeouts)); 141 } 142 143 function attemptGrabCoin(x, y) { 144 const key = getKeyString(x, y); 145 if (coins[key]) { 146 // Remove this key from data, then uptick Player's coin count 147 firebase.database().ref(`coins/${key}`).remove(); 148 playerRef.update({ 149 coins: players[playerId].coins + 1, 150 }) 151 } 152 } 153 154 155 function handleArrowPress(xChange=0, yChange=0) { 156 const newX = players[playerId].x + xChange; 157 const newY = players[playerId].y + yChange; 158 if (!isSolid(newX, newY)) { 159 //move to the next space 160 players[playerId].x = newX; 161 players[playerId].y = newY; 162 if (xChange === 1) { 163 players[playerId].direction = "right"; 164 } 165 if (xChange === -1) { 166 players[playerId].direction = "left"; 167 } 168 playerRef.set(players[playerId]); 169 attemptGrabCoin(newX, newY); 170 } 171 } 172 173 function initGame() { 174 175 new KeyPressListener("ArrowUp", () => handleArrowPress(0, -1)) 176 new KeyPressListener("ArrowDown", () => handleArrowPress(0, 1)) 177 new KeyPressListener("ArrowLeft", () => handleArrowPress(-1, 0)) 178 new KeyPressListener("ArrowRight", () => handleArrowPress(1, 0)) 179 180 const allPlayersRef = firebase.database().ref(`players`); 181 const allCoinsRef = firebase.database().ref(`coins`); 182 183 allPlayersRef.on("value", (snapshot) => { 184 //Fires whenever a change occurs 185 players = snapshot.val() || {}; 186 Object.keys(players).forEach((key) => { 187 const characterState = players[key]; 188 let el = playerElements[key]; 189 // Now update the DOM 190 el.querySelector(".Character_name").innerText = characterState.name; 191 el.querySelector(".Character_coins").innerText = characterState.coins; 192 el.setAttribute("data-color", characterState.color); 193 el.setAttribute("data-direction", characterState.direction); 194 const left = 16 * characterState.x + "px"; 195 const top = 16 * characterState.y - 4 + "px"; 196 el.style.transform = `translate3d(${left}, ${top}, 0)`; 197 }) 198 }) 199 allPlayersRef.on("child_added", (snapshot) => { 200 //Fires whenever a new node is added the tree 201 const addedPlayer = snapshot.val(); 202 const characterElement = document.createElement("div"); 203 characterElement.classList.add("Character", "grid-cell"); 204 if (addedPlayer.id === playerId) { 205 characterElement.classList.add("you"); 206 } 207 characterElement.innerHTML = (` 208 <div class="Character_shadow grid-cell"></div> 209 <div class="Character_sprite grid-cell"></div> 210 <div class="Character_name-container"> 211 <span class="Character_name"></span> 212 <span class="Character_coins">0</span> 213 </div> 214 <div class="Character_you-arrow"></div> 215 `); 216 playerElements[addedPlayer.id] = characterElement; 217 218 //Fill in some initial state 219 characterElement.querySelector(".Character_name").innerText = addedPlayer.name; 220 characterElement.querySelector(".Character_coins").innerText = addedPlayer.coins; 221 characterElement.setAttribute("data-color", addedPlayer.color); 222 characterElement.setAttribute("data-direction", addedPlayer.direction); 223 const left = 16 * addedPlayer.x + "px"; 224 const top = 16 * addedPlayer.y - 4 + "px"; 225 characterElement.style.transform = `translate3d(${left}, ${top}, 0)`; 226 gameContainer.appendChild(characterElement); 227 }) 228 229 230 //Remove character DOM element after they leave 231 allPlayersRef.on("child_removed", (snapshot) => { 232 const removedKey = snapshot.val().id; 233 gameContainer.removeChild(playerElements[removedKey]); 234 delete playerElements[removedKey]; 235 }) 236 237 238 //New - not in the video! 239 //This block will remove coins from local state when Firebase `coins` value updates 240 allCoinsRef.on("value", (snapshot) => { 241 coins = snapshot.val() || {}; 242 }); 243 // 244 245 allCoinsRef.on("child_added", (snapshot) => { 246 const coin = snapshot.val(); 247 const key = getKeyString(coin.x, coin.y); 248 coins[key] = true; 249 250 // Create the DOM Element 251 const coinElement = document.createElement("div"); 252 coinElement.classList.add("Coin", "grid-cell"); 253 coinElement.innerHTML = ` 254 <div class="Coin_shadow grid-cell"></div> 255 <div class="Coin_sprite grid-cell"></div> 256 `; 257 258 // Position the Element 259 const left = 16 * coin.x + "px"; 260 const top = 16 * coin.y - 4 + "px"; 261 coinElement.style.transform = `translate3d(${left}, ${top}, 0)`; 262 263 // Keep a reference for removal later and add to DOM 264 coinElements[key] = coinElement; 265 gameContainer.appendChild(coinElement); 266 }) 267 allCoinsRef.on("child_removed", (snapshot) => { 268 const {x,y} = snapshot.val(); 269 const keyToRemove = getKeyString(x,y); 270 gameContainer.removeChild( coinElements[keyToRemove] ); 271 delete coinElements[keyToRemove]; 272 }) 273 274 275 //Updates player name with text input 276 playerNameInput.addEventListener("change", (e) => { 277 const newName = e.target.value || createName(); 278 playerNameInput.value = newName; 279 playerRef.update({ 280 name: newName 281 }) 282 }) 283 284 //Update player color on button click 285 playerColorButton.addEventListener("click", () => { 286 const mySkinIndex = playerColors.indexOf(players[playerId].color); 287 const nextColor = playerColors[mySkinIndex + 1] || playerColors[0]; 288 playerRef.update({ 289 color: nextColor 290 }) 291 }) 292 293 //Place my first coin 294 placeCoin(); 295 296 } 297 298 firebase.auth().onAuthStateChanged((user) => { 299 console.log(user) 300 if (user) { 301 //You're logged in! 302 playerId = user.uid; 303 playerRef = firebase.database().ref(`players/${playerId}`); 304 305 const name = createName(); 306 playerNameInput.value = name; 307 308 const {x, y} = getRandomSafeSpot(); 309 310 311 playerRef.set({ 312 id: playerId, 313 name, 314 direction: "right", 315 color: randomFromArray(playerColors), 316 x, 317 y, 318 coins: 0, 319 }) 320 321 //Remove me from Firebase when I diconnect 322 playerRef.onDisconnect().remove(); 323 324 //Begin the game now that we are signed in 325 initGame(); 326 } else { 327 //You're logged out. 328 } 329 }) 330 331 firebase.auth().signInAnonymously().catch((error) => { 332 var errorCode = error.code; 333 var errorMessage = error.message; 334 // ... 335 console.log(errorCode, errorMessage); 336 }); 337 338 339})();