Pixel Perfect Challenge

Can you spot the better design?

2019/04/02


Play the final product here: Pixel Perfect Design Challenge

pixel perfect hero

The premise was simple: make a game where you are presented with two designs and have to pick the “better” design.

visual design screen

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.

visual design screen

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?

Take the Pixel Perfect Design Challenge