Can you spot the better design?
2019/04/02
Play the final product here: Pixel Perfect Design Challenge
The premise was simple: make a game where you are presented with two designs and have to pick the “better” design.
I started to build out the question bank by going through a list of principles or best practices, and then picking an archetypical example of that principle. Later I organized these by level groups, such as questions related to visual design.
So now each question has a set of images — one correct one incorrect, and each level has a set of questions. We need to load a level, pick a random set of images from the question bank, and then randomize the position of the correct and incorrect images. Additionally, we need a way to advance to the next question, and know when all questions have been asked:
class Level {
constructor(name, total_questions, images, instruction) {
this.name = name;
this.total_questions = total_questions;
this.images = images;
this.current_images = images[0];
this.instruction = instruction;
this.current_question = 0;
this.image_count = [];
}
make_bag_of_images() {
for(let i=0; i < this.total_questions; i++) {
this.image_count.push(i);
}
}
pick_new_images() {
let random = Math.floor(Math.random()*this.image_count.length);
let index = this.image_count.splice(random,1)[0]; //Picks random number 1–10, no replace
this.current_images = this.images[index];
document.getElementById(“c_img”).src = this.current_images.correct;
document.getElementById(“i_img”).src = this.current_images.incorrect;
this.random_position();
}
random_position() {
let num = Math.random();
if (num >= 0.5) {
document.getElementById(“box”).style.flexDirection = “row”;
}
else {
document.getElementById(“box”).style.flexDirection = “row-reverse”;
}
}
next_question() {
let instruction = document.getElementById(“instruction”);
instruction.innerHTML = levels[game.current_level].helptext;
let msg = document.getElementById(“message”);
msg.innerHTML = “”;
window.onkeydown = window.onkeyup = null;
this.current_question++;
if (this.current_question <= this.total_questions) {
document.getElementById(“progress”).style.width = String((this.current_question/this.total_questions * 100) + “%”);
document.getElementById(“leveldisplay”).innerHTML = `${levels[game.current_level].name} ${this.current_question} / ${this.total_questions}`;
this.pick_new_images();
}
else {
game.game_over();
}
}
Now we need a controller for the main game engine, which includes handling tasks like loading the different game views for initial load, level select, question phase, review phase, and game over screens. Also we want to update the score when users get the question correct, and keep track of how long the user has been playing.
const game = {
current_level:0,
total_levels:4,
score:0,
startTime: Date.now(),
init() {
//Creates level selector UIs per level
for (let i=0; i<levels.length; i++) {
levels[i].button = document.createElement(“div”);
levels[i].button.innerHTML = levels[i].name;
levels[i].button.classList.add(“gametype”);
document.getElementById(“level_select_view”).appendChild(levels[i].button);
levels[i].button.onclick = function(){
location.href = “#”;
game.init_level(i);
};
}
},
main_menu_view() {
document.getElementById(“init_view”).style.display = “flex”;
document.getElementById(“game_view”).style.display = “none”;
document.getElementById(“end_view”).style.display = “none”;
},
init_level(n) {
this.current_level = n;
this.score = 0;
document.getElementById(“init_view”).style.display = “none”;
document.getElementById(“game_view”).style.display = “block”;
document.getElementById(“progress”).style.width = String((levels[this.current_level].current_question/levels[this.current_level].total_questions * 100) + “%”);
document.getElementById(“scoredisplay”).innerHTML = “00000”;
document.getElementById(“leveldisplay”).innerHTML = `${levels[this.current_level].name} ${levels[this.current_level].current_question} / ${levels[this.current_level].total_questions}`;
levels[n].current_question = 0;
levels[n].make_bag_of_images();
levels[n].next_question();
},
review(){
c_btn.onclick = “”;
i_btn.onclick = “”;
document.getElementById(“review-btns”).style.display = “block”;
document.getElementById(“instruction”).style.display = “none”;
document.getElementById(“message”).style.display = “block”;
inputs.not_clicked.style.transition = “width 0.2s ease-in-out”;
inputs.not_clicked.style.width = “0px”;
inputs.not_clicked.style.margin = “10px 0px”;
window.onkeydown = window.onkeyup = inputs.keypress;
},
async update_score() {
var increase_by = 200 + Math.floor(Math.random()*50);
for (let i = 0; i < increase_by; i++) {
await this.counterDelay(5);
this.score++;
document.getElementById(“scoredisplay”).innerHTML = String(“00000” + this.score).slice(-5);
}
},
counterDelay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
game_over() {
document.getElementById(“game_view”).style.display = “none”;
document.getElementById(“end_view”).style.display = “block”;
document.getElementById(“final_score”).innerHTML = this.score;
document.getElementById(“final_time”).innerHTML = Math.floor(((Date.now() — this.startTime)/1000)/60) + “ minutes “ + Math.floor(((Date.now() — this.startTime)/1000)%60) + “ seconds” ;
},
play_again(){
game.score = 0;
game.startTime = Date.now();
levels[game.current_level].current_question = 0;
game.main_menu_view();
}
};
Finally, we need a way to handle user inputs for answering the question, comparing the two images, and advancing to the next question. Futhermore, they should be able to select a level that they want to play.
const play_btn = document.getElementById(“play_btn”);
const level_select_btn = document.getElementById(“level_select_btn”);
const c_btn = document.getElementById(“correct”);
const i_btn = document.getElementById(“incorrect”);
const next_btn = document.getElementById(“next”);
const again_btn = document.getElementById(“play_again”);
const compare_btn = document.getElementById(“compare”);
const inputs = {
clicked: null,
not_clicked: null,
init(){
play_btn.addEventListener(“click”, function(){game.init_level(0);});
level_select_btn.onclick = null;
c_btn.onclick = inputs.correct;
i_btn.onclick = inputs.incorrect;
next_btn.onclick = function(){levels[game.current_level].next_question();};
compare_btn.onmousedown = compare_btn.onmouseup = compare_btn.ontouchstart = compare_btn.ontouchend = inputs.compare;
again_btn.onclick = game.play_again;
},
correct(){
inputs.clicked = c_btn;
inputs.not_clicked = i_btn;
var msg = document.getElementById(“message”);
msg.innerHTML = “<i class=’fas fa-check-circle’></i> “ + levels[game.current_level].current_images.message;
msg.classList.add(“success”);
game.update_score();
game.review();
},
incorrect(){
var msg = document.getElementById(“message”);
inputs.clicked = i_btn;
inputs.not_clicked = c_btn;
msg.innerHTML = “<i class=’far fa-times-circle’></i> “ + levels[game.current_level].current_images.message;
msg.classList.add(“wrong”);
game.review();
},
compare() {
if (inputs.clicked.children[0].src.includes(levels[game.current_level].current_images.incorrect)) {
inputs.clicked.children[0].src = levels[game.current_level].current_images.correct;
}
else {
inputs.clicked.children[0].src = levels[game.current_level].current_images.incorrect;
}
},
keypress(e){
if (e.key == “Shift”) {
inputs.compare();
}
else if (e.key == “Enter” ) {
levels[game.current_level].next_question();
}
},
};
Why don't you give it a try?