In Their Own Words (Connections Lab Final Project)

gravatar
By nuff
 · 
December 28, 2023
 · 
10 min read
Note: this is a collab with classmate, homie and general legend Mishka. Being both design-oriented, we split tasks rather than roles, both doing a bit of design and a bit of coding. For Mishka's perspective, read her documentation.

Play on Glitch | Code on Glitch | Github

These videos give a quick summary of what we did between Project 02 and 03—read on below for details!

Previously…

For Project 02, Mishka and I built about 65% of a functioning quiz game about our cohort. We started by collecting fun facts from our classmates via Google Forms, worked on a design in Figma and following the quiz example video, created a sockets-based quiz where multiple people could answer questions simultaneously.

Except this didn’t quite work. First of all, Glitch was glitching so we could only run the app locally. Because we had originally envisioned everybody playing along in a joint session, sockets initially made sense. Once we pivoted to having it so that each person could play individually, serving questions via sockets meant every time one player requested a new question, everybody got one. We also didn’t have the score updating properly (more on that later, I still don’t understand object notation). Finally, we hadn’t had time to design an end state for the game in Project 02.

TL;DR Here’s The To-Do List

  • [x] Get it working on Glitch!!!
  • [x] Move the database of fun facts to Mongo DB so that people can add questions directly to the game
  • [x] Create a submission form to do the above
  • [x] Switch from each question having an array of answers to dynamically generating “wrong” answers from the answers to other questions (this will scale better if more people get added)
  • [x] Figure out the async thing—each player should get a unique instance of the game without influencing others’ questions, BUT the scoreboard should be visible to other players
    • [x] Easiest way is to use express fetch for all the quiz stuff instead of sockets, and only use sockets to broadcast the user id and store whenever someone joins, leaves, or has a new score
  • [x] Get the score updating correctly (get the user object by id and update the score ++ on correct answer)
  • [x] Full-screen Menu
  • [ ] Add a leaderboard?
  • [x] End the game! Doesn’t quite work right now.
    • [x] Styling this screen
  • [x] Design a footer, about page or other context section
  • [x] Audio?
  • [ ] New Avatars for folks who submitted
  • [x] Find a way to use a mystery/silhouette avatar for anybody who doesn’t have one
  • [x] Environment variables locally and on Github
  • [x] Add question number before each question (to be updated to progress bar later?)

Migrating to Mongo

The first thing we did was move the database of questions and answers to MongoDB using the Insert Document feature. This would allow us to add an in-game feature where people could add their own fun facts to the game, replacing us hard-coding them.

Using QuickMongo, we connect to the database to pull our question list and add new questions. This was a pain to get working—eventually, it did work but made a whole new databse, ignoring the one we built. I was way too tired at this point to examine why. Trust the process and move on, I suppose.

db.connect();
console.log("db connected!");

// add route to get all questions from database
app.get('/getQuestions', async (req, res) => {
  try {
    // Fetch from the DB using await
    const questions = await db.get("questions");
    console.log(questions);
    if (!questions) {
      return res.status(404).json({ error: "No questions found" });
    }
    let obj = { questions };
    res.json(obj);
  } catch (error) {
    console.error("Error fetching questions:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

// Fetch request (type POST) to send the name and fact to the server

        fetch('/funFact', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        })
            .then(response => response.json())
            .then(data => {
                console.log(data);

All of this required a slight structural change (more on that below). Each question previously contained “wrong” answers to make the quiz multiple-choice:

But asking someone to submit four other names (or going in ourselves and doing that) felt unsustainable, so…

Dynamic Multiple Choice

After a chat with Dora, we came up with some logic for generating options dynamically:

Get all questions → shuffle all questions → create new array of only the answers → find and remove duplicates in this array → grab a question → add 4x random answers from the answers array

This is a good time to write a shuffle function as we’ll be using it for a few things:

// Function to shuffle an array
    function shuffle(array) {
        for (let i = array.length - 1; i > 0; i--) {

            // Pick a random index from 0 to i
            const j = Math.floor(Math.random() * (i + 1));

            // Swap array[i] with array[j]
            [array[i], array[j]] = [array[j], array[i]];
        }

        return array;
    }

Now we’ve got that, we can go ahead and shuffle the questions as soon as we load them into the game.

window.addEventListener('load', () => {
    // Get all questions from server
    fetch('/getQuestions', {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
    })
        .then(response => response.json())
        .then(data => {
            console.log(data);

            // Shuffle the questions
            shuffledQuestions = shuffle(data.questions);

Then we’ll create an array with just the answers and remove duplicates (some people have multiple facts in the game). Googling how to do this, I stumbled across the sets method:

// Grab the names from the questions and put them in an array
            let answerPool = [];
            for (let i = 0; i < shuffledQuestions.length; i++) {
                answerPool.push(shuffledQuestions[i].answer);
            }

            // Check answerPool for duplicates and remove them
            uniqueAnswers = [...new Set(answerPool)];

            console.log(uniqueAnswers);
            // console.log(shuffledQuestions);
        })

Now we want to create an array of options for the question. It took a while to figure this out but if you add the correct answer to the array first, you can check against it when you start adding random options. This will also avoid duplicates within the randoms.

// create an array to store the options
        let options = [];

        // add the correct answer to the options array
        options.push(answer);

        // pick 4 other answers from the uniqueAnswers array, excluding the correct answer and not repeating any
        while (options.length < 5) {
            let randomAnswer = uniqueAnswers[Math.floor(Math.random() * uniqueAnswers.length)];
            if (!options.includes(randomAnswer)) {
                options.push(randomAnswer);
            }
        }

Then we use the shuffle function again to scramble the answers so the correct answer isn’t always the first one. Now we can go ahead and build out our question and answers in HTML as we did back in Project 02.

Avatars

We insert Mishka's lovely avatars into the game dynamically by giving them the exact same name as the answer. That way, we can use the answer string ("options[i]" in the loop below) to declare the name of the background image. With people able to add themselves to the game at any time, we needed a way to catch any names without a corresponding avatar. We made a “default” avatar (later expanded to six for variety) and used a conditional to specify the default if no match can be found. The internet suggested switching from background images to <img> elements so we could change innerHTML.

// create a button for each option
        for (let i = 0; i < options.length; i++) {
            let option = document.createElement('div');
            option.id = "option" + i;
            option.className = "option";

            // create an image element for the avatar
            let avatarImg = document.createElement('img');
            avatarImg.src = "./avatars/" + options[i] + ".svg";
            avatarImg.alt = "Avatar";

            // if the image fails to load, use the default avatar
            avatarImg.onerror = function () {
                //pick a random number between 1 and 6
                let randomAvatar = Math.floor(Math.random() * 6) + 1;

                // set the image to one of six defaults
                this.src = "./avatars/default" + randomAvatar + ".svg"; // Set the default image path
            };

            // append the image element to the option div
            option.appendChild(avatarImg);

Live Scores

We previously had this effectively working in the code, I just couldn’t figure out how to display it because OBJECT NOTATION MAKES NO SENSE. Also, sockets stuff.

Every time someone connects, or every time a score is updated, our server is listening for the list of users and scores, and emits an updated list.

On the client side, we update the current score variable whenever they get an answer right.

io.on('connection', (socket) => { // when a new user connects
  console.log("we have a new player: " + socket.id);

  io.emit('user scores', users); // send an object with key "user scores" and value [all online players] to everyone
  const userID = socket.id; // store the user's socket id in a constant
  users[userID] = {}; // create an empty object for the user in the users object

  socket.on('new user', (name) => {
    console.log('Our new player is called: ' + name);
    users[userID].name = name; // add the name to the user's object

  });
  // listen for user scores and update the users object
  socket.on('user scores', (score) => {
    console.log('User scores: ' + score);
    users[userID].score = score; // add the score to the user's object

    console.log(users);
    io.emit('user scores', users); // Why do we do it both here and above?
  });

  socket.on('disconnect', () => {
    delete users[userID]; // remove a player from the users object when they disconnect
    console.log(users);
  });
});
if (options[i] == answer) {
                    currentScore++; // add 1 to the score
                    console.log("current score: " + currentScore);

                    // emit the updated score to the server
                    socket.emit('user scores', currentScore);
                }

I do not for the life of me remember this part but apparently we update the scoreboard like this?

// Function to update the scoreboard UI
    function updateScoreboard(users) {
        const scoreboard = document.getElementById('scoreboard');
        scoreboard.innerHTML = "<span style='font-weight: 600;'>Live Scoreboard</span>";

        Object.keys(users).forEach((userID) => {
            const user = users[userID];
            if (!user.name) {
                return;
            }
            const scoreElement = document.createElement('div');
            scoreElement.className = 'score';
            scoreElement.textContent = `${user.name}: ${user.score || 0}`;
            scoreboard.appendChild(scoreElement);
        });
    }

Ending the game

Our new end sequence displays your final score along with an assessment of your prowess. Ruta mentioned in playtesting that this would be a good moment to add a fact to the game, having just gone through it yourself.

Navigation

Full-screen overlay menus are surprisingly hard. The method we used involves a hidden overlay div that is set to display when the menu button is clicked.

<!-- Overlay Menu -->
    <div id="myNav" class="overlay">

        <!-- Button to close the overlay navigation -->
        <img src="/img/icon_close.svg" alt="close menu" id="close-button" />
        <!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a> -->

        <!-- Overlay content -->
        <div class="overlay-content">
            <a href="/">Play Now</a>
            <a href="about.html">About the Game</a>
            <a href="submit.html">Submit a Fact</a>
            <p class="credits">&#x270c;&#xfe0f; by <a href="<https://www.mishka.design>">Mishka</a> & <a
                    href="<https://www.nuff.design>">Nuff</a></p>
        </div>

    </div>

    <!-- Use any element to open/show the overlay navigation menu -->
    <img src="/img/icon_menu.svg" alt="open menu" id="menu-button">
// Open Menu
    document.getElementById('menu-button').addEventListener('click', () => {
        openNav();
        console.log("menu button clicked");
    });

    //Close Menu
    document.getElementById('close-button').addEventListener('click', () => {
        closeNav();
        console.log("menu button clicked");
    });

Music

Mishka found SFX files to add some character to the game. A future goal here would be a sound on/off toggle and maybe background music.

Review & Feedback

During our review with Julie and Pierre, a couple of things came up:

  • It was possible to keep clicking answers until you got the right one.
  • If you did this, multiple “next question” buttons appeared on screen and didn’t go away

We can solve both issues by disabling the buttons once you’ve selected—this also adds a little bit of feedback for the player.

Some people mentioned they would like to know the correct answers and maybe we’ll do that eventually but our position is that not revealing them keeps the game interesting for repeat play.

In Future…Stretch Goals

  • Leaderboard
    • Leaderboard sorts first by score then by speed
      • We’d need a timer
  • Animations
    • Interface motion
    • Microinteractions (right/wrong answer icons)
      • Avatars smile/frown?! (would need 3 states per avatar, unsustainable but funny)
    • Confetti if you get 5/5
  • A way to prioritise “fresher” questions as people play more and add more
  • In-game avatar builder (Ryan!)
  • Session mode (people play simultaneously, fastest correct answer gets the points)
  • “Build-your-own-quiz” mode where you play with a temporary database of only your group’s questions
    • Then we can charge $$$ to companies who want to do team bonding stuff
Comments

No Comments.