For Project 02, our brief is to “create a web application that allows a user to send and receive data” using NodeJS, Express and either a database library for storage, Socket.io for real-time multi-person data exchange or both.
The aim is to produce an experience where users can interact, either alone or with one another, and submit, store and/or exchange data.
We're working in pairs for this project. Mishka and I have been paired again for our first collab since the very first Low Res project. As two designers who aren't particularly strong at code, I predict an aesthetic experience. Hopefully it'll also be a functional one.
We decided to each do a bit of everything, as our skillsets are quite similar. We both enjoy the front-end, visual side of things so it was important we each did a little back-end. Also, we both tend to procrastiwork by doing things that might not be the most critical to avoid the things we'd rather not be doing. Knowing that up front, we were able to manage the process and only had one really late night.

Ideation
- Tic tac toe
- Connect 4
- A game involving a grid of squares where players hover over each one to change its colour. The player with the most squares at the end of a time period wins (I might end up doing this for my late week 7/8 homework)
- Pong
- A trivia game where people have to guess a fun fact about members of the cohort (of course we ended up doing the hardest one)
In class, each team presented their idea and received feedback. The overwhelming class favourite was the quiz game, so we whipped up a submission form on the fact and got 12 responses—enough to build a database.

Early Questions
- Should the game be multiplayer and synchronous, multiplayer and asynchronous or single-player?
- How will we store the "questions" (facts) and "answers" (people connected to those facts)?
- JSON file with “question”, “options” (array) and “answer” key-value pairs.
- In the future, it could be interesting to have the code pick random “wrong” answers in real time and scramble them together with the correct answer. This might require a different database structure (especially combined with user submission as below)
- How will we make use of Socket.io and/or MongoDB?
- For now, following the Quiz example in our weekly lessons, we'll use Socket.io to serve questions to the client, receive the answer and check it against the correct answer. This way, the client never actually receives the correct answer data and can't cheat by inspecting the code.
- MongoDB will come in later for storing scores and creating a leaderboard.
- As a stretch goal, we could also use a database to enable adding more fun facts. The user would submit their fact, then either select their name from the database if it already exists or add it as a new entry. We'd need to keep track of the relationship between fact and person and be able to point multiple facts to the same person.
Essential | Nice To Have | Random Ideas |
---|---|---|
Match fact to person (JSON key-value pairs?) | Multiplayer (either first correct answer gets the point or all correct answers get a point) | Timer - either each question has a countdown or the whole thing has a countdown |
Award a point for each correct answer; keep track of correct answers | Avatars for each answer (cohort member) | Ranking players by score |
Display a points total | Post-answer feedback (show what you chose and what the right answer was) | Ability for players to pick their own name |
End the game after a certain number of questions | Randomise the “wrong” options and shuffle the “right” one in the array (the alternative for this is to show every possible answer or manually pick the options) | Seeing who other players picked (after everybody has picked) |
Randomise question order | ||
Add your fact directly to the database (MongoDB) | ||
Link to submit your fact (G Form) | ||
SVG Avatars for each member of the cohort |
Design

After a brief collaborative wireframing session, we whipped up a quick mockup in Figma to work through some key interface concepts:
- Question and answer would display as the main part of the “app”
- We'll display both a name and image for each answer—Mishka will make avatars
- Question count, timer and scoreboard (featuring any live players)
- (Because we ended up with an asynchronous game, we got rid of the timer)
Creating the Database
Our Google form links to a spreadsheet, from which we can technically export direct to JSON. However, it wouldn't be exactly formatted for our needs, so I manually made a file (not too hard with only 12 questions but if we wanted to expand the game, it'd be a nightmare).
"questions": [
{
"question": "I went to the same high school as Madonna.",
"options": [
"Ruta",
"Ryan",
"Anastasia",
"Amber",
"Elisabeth"
],
"answer": 1
},
- First, we have a key-value pair where the key is "questions" and the value is the array of questions
- Each item in that array has 3 further pairs
- question: the value of which is the fun fact
- options: the value of which is an array of multiple-choice answers
- answer: the value of which is the position in the array of the correct answer (remember, coding numbers start from zero, so "1" in this case is the second answer, Ryan.
Organising the data like this allows us to manually adjust the difficulty of the question by choosing convincing "wrong" answers. For example, if you know where Madonna grew up, you might want to add other people who could conceivably have gone to school in the same area (or at least country).
Building
OK, so I don't really get sockets after a few weeks of trying and failing. Dora helped Mishka and I figure the setup out for our index.js (server) and public/app.js (client).
Let's start with index.js. First, we require express, node, socket.io and our JSON file, quiz.json.
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');
const { Server } = require('socket.io');
let quiz = require('./data/quiz.json'); // import questions from quiz.json
Next, we initialise our HTTP Server, Socket.io and express.
//Initialise HTTP Server
const app = express();
const server = createServer(app);
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log("Server listening at port: " + port)
});
//Initialise Socket.io
const io = new Server(server);
app.use('/', express.static('public'));
const users = {} // object to store names and scores
Everything from here will require a connection, so we wrap it in an io.on('connection'):
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
On the client side, we want players to be able to choose their own names, so we get the value from an input field and emit it back to the server. We'll attach it to the User ID there. This also lets us show every player currently online.
let socket = io();
const submitButton = document.getElementById("submit-button");
submitButton.addEventListener("click", () => {
//get the user's name from input box
const playerName = document.getElementById('name-field').value; // set playerName to what was just typed in the field
socket.emit('new user', playerName);
})
Back in index.js, we'll grab the player's name and add it to their 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
Getting and Displaying Questions
To get a question, we'll emit a request for a question from the client
socket.emit('getquestion');
console.log("requesting a question");
On the server, we listen to this request and serve a random question from the array of questions. To do this, we need a questionNo
variable.
// When client requests a question, send a random question
socket.on('getquestion', () => {
console.log("question requested");
// Get a random question from our array
questionNo = Math.floor(Math.random() * quiz.questions.length);
console.log(questionNo);
// send the question and options to client
io.emit('question', quiz.questions[questionNo].question, quiz.questions[questionNo].options);
// log what we sent
console.log(quiz.questions[questionNo].question);
console.log(quiz.questions[questionNo].options);
});
By only sending the question and options, we never reveal the correct answer.
We use an empty div (or section?) in the original HTML as a placeholder for the quiz content and append both the question and answers as children of quizDiv
. For the answers, we create a for loop with the length of the array, options.length, to create a button for each answer.
Something that'll be super useful later: we create a URL to the image file for each answer by inserting the name stored in the options array of our JSON into an otherwise static link. This means all we have to do is name the images exactly the same as the answers and put them in the same folder, and we'll always have the right image for the right person.
Finally, we emit an "answer" event once the player has made a selection. For now, in the absence of a timer, we won't let players change their answer.
//create new div for question and add a paragraph with the question text
let quizDiv = document.getElementById('quiz');
let questionText = document.createElement('p');
questionText.id = "question";
questionText.className = 'question';
questionText.innerHTML = question;
quizDiv.appendChild(questionText);
//create new div for options
let optionsDiv = document.createElement('div');
optionsDiv.id = "options";
optionsDiv.className = "options";
quizDiv.appendChild(optionsDiv);
// 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";
// add avatar to the button
let avatarURL = "./avatars/" + options[i] + ".png";
option.style.backgroundImage = `url(${avatarURL})`;
// add option text below the avatar
option.innerHTML = options[i];
optionsDiv.appendChild(option);
// when player selects answer
let optionButton = document.getElementById("option" + i);
optionButton.onclick = function () {
if (!isAnswered) {
optionButton.classList.add("selected");
socket.emit("answer", i);
console.log("Player answered: " + i);
isAnswered = true;
}
}
}
})
Styling Questions
Mishka made incredible avatars for each of the people who submitted a fact. As mentioned above, we're using a dynamic-ish (for us low-level coders at least!) URL variable to find the right image for the right person. and setting it as a background image for the button. Background images can be a bit of a pain to deal with and I may consider using an <img> tag in future. But this way lets us keep the button as one element, rather than a parent element with both an image and text inside.
In our CSS, we create a Flexbox row for our questions which lets us space them evenly on different size monitors. To make sure our buttons are always square (because our avatars are), we're currently using pixel values, which feels a bit...unresponsive. We use hover and selected states to send a little more information to the player about what they're doing.
/* Styling Option Section */
.options {
display: flex;
flex-direction: row;
gap: 1rem;
/* justify-content: space-evenly; */
align-items: center;
/* width: 100%; */
margin-top: 1rem;
}
/* Styling Option Buttons */
.option {
width: 200px;
height: 200px;
background-color: #DBE4FB;
background-size: cover;
border: 1px solid #1B1C21;
border-radius: 0.5rem;
/* display: flex;
flex-direction: column; */
}
.option:hover {
cursor: pointer;
transition: 0.2s;
box-shadow: rgba(27, 28, 33, 0.25) 0px 13px 27px -5px, rgba(27, 28, 33, 0.3) 0px 8px 16px -8px;
}
.selected {
background-color: #FBEBDD;
border: 3px solid #1b1c21;
}
Checking for Correct Answers
Once the server receives the answer from the client (they've picked one of the buttons in an array from 0-4), we can check that number against the number stored in our JSON. Then we can send back an answer: true or answer: false results message.
Annoyingly, because we're using numbers to check, we need to write a little more code to pass the name of the person whodunnit back to the client (technically, we could also pull it up from the options using the number, but why do that?).
// When client submits an answer, check if it's correct
socket.on('answer', (answer) => {
console.log("answer submitted: " + answer);
console.log("correct answer: " + quiz.questions[questionNo].answer);
// get the NAME of the correct answer
let correctAnswer = quiz.questions[questionNo].options[quiz.questions[questionNo].answer];
if (answer == quiz.questions[questionNo].answer) {
console.log("correct!");
currentScore++;
console.log("current score: " + currentScore);
socket.emit("results", { answer: true, name: correctAnswer });
} else {
console.log("incorrect!");
socket.emit('results', { answer: false, name: correctAnswer });
}
Removing Asked Questions from the Array
We don't want to ask this question again, so we use quiz.questions.splice(questionNo, 1);
to remove it (only for the duration of this active session).
Back on the client side, we display feedback on whether the player got the question right. For the longest time, this wasn't working properly (it would always say the answer was correct). I finally realised I needed a double equal sign for data.answer == true
. A single equal sign would just check if an answer exists.
// Display whether the player got the answer right or wrong
socket.on('results', (data) => {
console.log(data);
let quizDiv = document.getElementById('quiz');
let results = document.createElement('p');
results.id = "results";
quizDiv.appendChild(results);
if (data.answer == true) {
results.innerHTML = "Correct! The answer was " + data.name + ".";
} else {
results.innerHTML = "Wrong! The answer was " + data.name + ".";
}
// add a button to request the next question
let nextButton = document.createElement('button');
nextButton.innerHTML = "Next Question";
nextButton.id = "next-button";
nextButton.className = "button";
quizDiv.appendChild(nextButton);
// When the player clicks the next question button, request a new question
nextButton.addEventListener("click", () => {
removeQuestion();
socket.emit('getquestion');
console.log("requesting next question");
})
})
Rules, Edge Cases & General Tidying Up
Removing the Intro
Once you've submitted your name, we need to clear that field for the duration of the game. We use a boolean, introVisible
to stop the function more than once.
function removeIntro() {
if (introVisible) {
let intro = document.getElementById('intro');
intro.remove();
introVisible = false;
}
}
Ending the Game
Using a maxQuestions variable on the server side, we can continue the cycle of requesting a new question until we hit that ceiling, then emit a gameOver
message and trigger an end scenario. This currently doesn't quite work as expected.
// check if we've reached the maximum number of questions
if (currentQuestion < maxQuestions) {
currentQuestion++;
console.log(currentQuestion + " out of " + maxQuestions + " questions asked");
} else {
console.log(currentQuestion + " out of " + maxQuestions + " questions asked");
console.log("game over!");
io.emit('gameOver', { score: currentScore, max: maxQuestions });
}
io.emit('user scores', users);
})
Next Steps
We had a week to build this project and couldn't get it running on Glitch in that time. That's our next mission. We also weren't able to get the score updating (I don't quite get the users
object and how best to update it). There's a little hiccup in the multiplayer nature of the game that means one player asking for a new question gives everybody a new question. That would work well for a synchronous game but not an asynchronous one. What we'd like to do is effectively run the quiz part like a single-player game but keep the part that shows who is currently playing and what their score is.
Example/Reference Projects
- Chrome Experiments - WebSockets
- p5 Play Library
- PaperPlanes.world (mobile experience)
- Student projects:
- Bumper Mouse (this was completed as project #3)
- A Secret Forest (this was completed as project #3)
- Neon Graveyard
- Collaborative Dissonance
- Luck Game (this was completed as project #3)
No Comments.