5
\$\begingroup\$

Here is the JavaScript code:

(function($) {
  const shuffleArray = (arr) => {
    let currentIndex = arr.length;
    let temporaryValue;
    let randomIndex;
    // While there remain elements to shuffle...
    while (currentIndex !== 0) {
      // Pick a remaining element...
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex -= 1;
      // And swap it with the current element.
      temporaryValue = arr[currentIndex];
      arr[currentIndex] = arr[randomIndex];
      arr[randomIndex] = temporaryValue;
    }
    return arr;
  };

  //Each object contains the question, an array of the possible answers, and the correct answer.
  let questions = [
    {
      text: "Which one of Daenerys' handmaidens died in The Red Waste prior to arriving in Qarth ?",
      options: ['Irri', 'Doreah', 'Jhiqui', 'Missandei'],
      answerIndexInOptions: 1
    },
    {
      text: "Which one of the nine free cities of Essos was not originally a colony of Old Valyria ?",
      options: ['Pentos', 'Braavos', 'Volantis', 'Myr'],
      answerIndexInOptions: 1
    },
    {
      text: "Who forged Robert Baratheon's warhammer?",
      options: ['Ironbelly', 'Mikken', 'Donal Noye', 'Tobho Mott'],
      answerIndexInOptions: 2
    },
    {
      text: "During one of Arya's dancing lessons, Syrio Forel talks of an exotic land with lizards with scythes for claws. What continent is he alluding to?",
      options: ['Sothoryos', 'Ulthos', 'Westeros', 'Essos'],
      answerIndexInOptions: 0
    },
    {
      text: "Who does Eddard Stark deem the greatest warrior he ever encountered?",
      options: ['Barristan Selmy', 'Gerold Hightower', 'Oswell Whent', 'Arthur Dayne'],
      answerIndexInOptions: 3
    },
    {
      text: "What house is home to Greywater Watch ?",
      options: ['House Karstark', 'House Reed', 'House Glover', 'House Tallhart'],
      answerIndexInOptions: 1
    },
    {
      text: "Who died at the hands of Catelyn Stark during the Red Wedding?",
      options: ['Aegon Frey', 'Roslin Frey', 'Joyeuse Erenford', 'Ryman Frey'],
      answerIndexInOptions: 0
    },
    {
      text: "Who attempted to kill Myrcella Baratheon in the midst of Arianne Martell's failed plan to put her on the iron throne?",
      options: ['Doran Martell', 'Areo Hotah', 'Gerold Dayne', 'Quentyn Martell'],
      answerIndexInOptions: 2
    },
    {
      text: "Who won the melee at the hand's tourney in King's Landing?",
      options: ['Beric Dondarrion', 'Loras Tyrell', 'Thoros of Myr', 'Balon Swann'],
      answerIndexInOptions: 2
    },
    {
      text: "What are the Lannister's house words?",
      options: ['Growing strong', 'Hear me roar', 'A Lannister always pays his debts', 'We do not sow'],
      answerIndexInOptions: 1
    },
  ];
  questions.shuffleOptions = () => {
    questions.forEach( el => {
      const correctAnswer = el.options[el.answerIndexInOptions];
      el.options = shuffleArray(el.options);
      el.answerIndexInOptions = el.options.indexOf(correctAnswer);
    } );
  };

  const quiz = (function() {
    const getFinalResultsString = (score, questionsCount) => {
      return `Your final score is ${score} out of ${questionsCount}.`;
    };

    const getIncorrectAnswerString = (correctAnswer) => {
      return `You should have chosen ${correctAnswer}.`;
    };

    const showPopUp = (popUp, text) => {
      $('main, header').addClass('body-transparent');
      popUp
        .fadeIn()
        .find('.pop-up-text')
        .text( text );
    };

    const hidePopUp = (popUp) => {
      $('main, header').removeClass('body-transparent');
      popUp.fadeOut('fast');
      popUp.find('pop-up-text').empty();
    };

    const setQuestionCount = (num, questionsCount) => {
      $('.current-question-div').text(
        `Questions: ${num}/${questionsCount}`
      );
    };

    const setScoreCount = (score, questionsCount) => {
      $('.current-score-div').text(
        `Score: ${score}/${questionsCount}`
      );
    };

    const setOption = (answerElem, optionIndex, optionText) => {
      $(answerElem)
        .find('input[type="radio"]')
        .prop('checked', optionIndex === 0)
        .attr( {
          'id': optionText,
          'value': optionText,
          'data-index': optionIndex,
        } );
      $(answerElem)
        .find('label')
        .attr( 'for', optionText )
        .text( optionText );
    };

    const setQuestion = (question) => {
      $('.quiz-questions').text( question.text );
      $('.answer-to-question').each(
        (index, elem) => setOption(elem, index, question.options[index])
       );
    };

    const showResults = (score, questionsCount) => {
      $('.final-results').removeClass('hidden');
      $('.final-score-paragraph').text(
        getFinalResultsString(score, questionsCount)
       );
    };

    const questionsCount = questions.length;
    let currentQuestionIndex = 0;
    let score = 0;

    return {
      start: () => {
        $('.quiz-div').removeClass('hidden');
        $('.start-page-div').addClass('hidden');
        questions = shuffleArray(questions);
        questions.shuffleOptions();
        setScoreCount( score, questionsCount );
        setQuestion( questions[currentQuestionIndex] );
        setQuestionCount( currentQuestionIndex, questionsCount );
      },
      reset: () => {
        currentQuestionIndex = 0;
        score = 0;
        $('.start-page-div').removeClass('hidden');
        $('.final-results').addClass('hidden');
      },
      handleAnswer: () => {
        const isAnswerCorrect = questions[currentQuestionIndex].answerIndexInOptions === Number(
            $('.answer-to-question input:checked').attr('data-index')
          );
        const correctAnswer =
          questions[currentQuestionIndex].options[questions[currentQuestionIndex].answerIndexInOptions];
        if ( isAnswerCorrect ) {
          showPopUp( $('.pop-outer-correct'), '' );
          score++
        }
        else {
          showPopUp(
            $('.pop-outer-incorrect'),
            getIncorrectAnswerString( correctAnswer )
          );
        }
        setScoreCount( score, questionsCount );
      },
      proceed: () => {
        currentQuestionIndex++;
        // if quiz has not ended yet
        if ( currentQuestionIndex < questionsCount ) {
          setQuestion( questions[currentQuestionIndex] );
          setQuestionCount( currentQuestionIndex, questionsCount );
        } else {
          $('.quiz-div').addClass('hidden');
          showResults( score, questionsCount );
        }
      },
      hidePopUp: hidePopUp,
    };
  })();

  const submitQuestion = () => {
    quiz.handleAnswer();
    quiz.proceed();
  };

  const closePopUp = ev => 
    quiz.hidePopUp( $(ev.target).parents('.pop-outer') );


  const events = [
    {
      'type': 'click',
      'selector': '.start-button', 
      'handler': quiz.start,
    },
    {
      'type': 'click',
      'selector': '.start-over', 
      'handler': quiz.reset,
    },
    {
      'type': 'click',
      'selector': '.submit-question', 
      'handler': submitQuestion,
    },
    {
      'type': 'click',
      'selector': '.pop-outer .close', 
      'handler': closePopUp,
    },
  ];

  events.forEach( (ev) => {
    $(document).on( ev.type, ev.selector, ev.handler);
  } );

})($);

Here is the Github repo for the rest of the code: https://github.com/doubleOrt/a-song-of-ice-and-fire-quiz

Here is a runnable fiddle: https://jsfiddle.net/m4um5ek2/

Interested mostly in code structure improvements, but would love feed back on the events object or the event attachment architecture as well (with the loop). Naming improvements are welcome, as is anything else.

P.S please don't hesitate to comment/post even if only for a tiny improvement.

\$\endgroup\$
2
  • \$\begingroup\$ Is there any particular reason you wrapped everything in an IIFE? Is this being ran as a userscript with something like Tampermonkey? \$\endgroup\$
    – Phrancis
    Commented Apr 2, 2018 at 22:58
  • \$\begingroup\$ @Phrancis negative, only the fact that globals are considered evil. \$\endgroup\$
    – doubleOrt
    Commented Apr 2, 2018 at 23:00

1 Answer 1

2
\$\begingroup\$

This code looks okay, though there are am excessive amount of DOM lookups. It would be wise to cache those, since they can be expensive.

bridge toll

”...DOM access is actually pretty costly - I think of it like if I have a bridge - like two pieces of land with a toll bridge, and the JavaScript engine is on one side, and the DOM is on the other, and every time I want to access the DOM from the JavaScript engine, I have to pay that toll”
    - John Hrvatin, Microsoft, MIX09, in this talk Building High Performance Web Applications and Sites at 29:38, also cited in the O'Reilly Javascript book by Nicholas C Zakas Pg 36, as well as mentioned in this post

In events the keys need not be string literals... e.g.

const events = [
{
  type: 'click',
  selector: '.start-button', 
  handler: quiz.start,
},

using features, variables can be swapped with destructuring assignment without the need for a temporary variable - so shuffleArray() can be greatly simplified - also using the prefix decrement operator.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.