10
\$\begingroup\$

On almost every page in the Stack Exchange Network, there lives a list of some the current "hot" questions across the network (sometimes abbreviated HNQs). Some of these posts have very exotic titles (especially the ones from Arqade) and some have very simple titles.

Either way, I've always found it fun to try and guess what site an question title is associated with. As a result, I created a UserScript that plants this game into a Stack Exchange webpage.

// ==UserScript==
// @name         Hot Question Game
// @namespace    https://github.com/SirPython/Hot-Postato
// @version      0.1
// @description  A Stack Exchange mini game
// @author       SirPython
// @match        *://*.stackexchange.com/*
// @match        *://*.stackoverflow.com/*
// @match        *://*.superuser.com/*
// @match        *://*.serverfault.com/*
// @match        *://*.askubuntu.com/*
// @match        *://*.stackapps.com/*
// @match        *://*.mathoverflow.net/*
// @grant        none
// ==/UserScript==
/* jshint -W097 */
'use strict';

/* Place a button that says "play" right next to "help" in the top-left */
var button = createTopbarButton();
var topbar = document.getElementsByClassName("topbar-menu-links")[0];
topbar.appendChild(button);

var questions = getHNQuestions();

/**
 * Play a single round of the guessing game. If the user wishes to 
 * play again, they can click the play button again.
 */
function play() {
    do {
        var toGuess = questions[Math.floor(Math.random() * questions.length - 1)];
        var guess   = prompt(toGuess.title);

        /* Case-insensitive string checking */
        if(!new RegExp(guess, "i").test(toGuess.site)) {
            alert("Incorrect! The answer is...\n" + toGuess.site);
        } else {
            alert("Correct!");
        }
    } while(confirm("Play again?"));
}

/**
 * Represents a single question in the HNQ list.
 */
function HNQuestion(site, title) {
    this.site = site;
    this.title = title;
}
/**
 * Gets all the questions in the HNQ list and returns them as
 * an array of HNQuestions
 */
function getHNQuestions() {
    var questionElements = document.getElementById("hot-network-questions").children[1].children;
    var questions        = [];

    for(var i = 0; i < questionElements.length; i++) {
        var site = questionElements[i].children[0].title;
        var title= questionElements[i].children[1].innerHTML;

        site = site.replace(" Stack Exchange", ""); 
        questions.push(new HNQuestion(site, title));
    }

    return questions;
}

/**
 * Initializes the top bar button to be placed on the top bar.
 */
function createTopbarButton() {
    var button = document.createElement("a");
    button.href = "#";
    button.onclick = play;
    button.innerHTML = "play";
    return button;
}

The game plays out like this:

  1. The user clicks the "play" button in the top right corner.
  2. A prompt pops up with a question title in the center of it. This question title is one of the HNQ titles listed on the side.
  3. The user then tries to guess which SE site this question is coming from by typing in its name.
  4. The user is told whether or not they were correct and they are asked if they want to play again.

The source code can be found on GitHub and you can download the extension here.


This was meant as a small project for fun, but I still have a few questions.

  • Is my game design okay? I'm not that familiar with game programming, and although this is hardly a game, I am still curious about code design. Is the idea of having this single play function good? Is there anything else?

  • Is my "play" button initialization okay? I've always done it this way for UserScripts that need edit the existing webpage with new, flowing content, but I don't know if this is good practice. Did I split up the functions that accomplish this well?

  • I have quite a few global variables, and even one that is constantly used (questions). Is this sort of practice okay for UserScripts/games in JavaScript? Even if this may suffice for a simple game such as this, would it suffice for other, large games but in a similar context?

  • Are there any inefficiencies in my code, specifically around the DOM traversing and element creation?

  • How's my design? I know; the alerts and prompts look terrible, but I couldn't quite think of anything else. Perhaps there are better ways I could be utilizing the pre-existing webpage overall?

  • The UserScript header was pre-generated by Tampermonkey, so I'm not really concerned about practices in that area. However, how are my UserScript practices in other areas?

Don't feel the need to answer all of these in an answer. Any other recommendations are also appreciated.

\$\endgroup\$
  • \$\begingroup\$ Installation instructions? \$\endgroup\$ – Roamer-1888 Dec 19 '15 at 6:51
  • \$\begingroup\$ @Roamer-1888 If you have Tampermonkey (Chrome) or Greasemonkey (Firefox), you can click the download link above and you are all set. \$\endgroup\$ – SirPython Nov 16 '16 at 2:18
  • \$\begingroup\$ OK, but after 11 months I can't even remember posting the question! \$\endgroup\$ – Roamer-1888 Nov 16 '16 at 2:30
  • 1
    \$\begingroup\$ @Roamer-1888 Sorry for the slight delay :). I stumbled across this question again and just happened to find your comment unanswered. \$\endgroup\$ – SirPython Nov 16 '16 at 22:41
7
\$\begingroup\$

Button adding:

var button = createTopbarButton();
var topbar = document.getElementsByClassName("topbar-menu-links")[0];
topbar.appendChild(button);

Why not bundle this all in createTopbarButton?

function createTopbarButton() {
    var topbar = document.getElementsByClassName("topbar-menu-links")[0];;
    var button = document.createElement("a");
    button.href = "#";
    button.onclick = play;
    button.innerHTML = "play";
    topbar.appendChild(button)
}

Strange Indentation:

you have a strange indentation

(I couldn't identify the issue, though...)


Regex issues

Your regex does a search through the string for instances of the characters, but what happens if I put in a single character, a vowel, for instance?

Using e as a challenge string gave me correct answers in most cases.


Code Structure:

Instead of using and calling globals, use a prototype chain instead.

var HotPostatoGame = function(){};
HotPostatoGame.prototype.gameLoop = function(){};

That kind of format is better for linking functions and variables, which then you can use this to store internal variables, just like in your HNQ constructor.


Game Structure:

While your game seems to flow somewhat well, question -> answer, play again,

Your, or perhaps, the usage of alerts is kind of annoying.

I always found alerts like forcefully delivering a message, you want to be delivering it peacefully and in a nice dialog, thus, I would suggest creating a dialog box, or using a dialog plugin.

Additionally this will allow you to deliver the content better and add an end game button for when you want to end the game mid-game.

Beyond that, I would consider implementing a score check, and from that a check to make sure you don't ask the same question twice in the game.


Misc:

  • Hot Question Game: why is the UserScript name different to the project one?
  • You can use a ternary here:
    if(!new RegExp(guess, "i").test(toGuess.site)) {
        alert("Incorrect! The answer is...\n" + toGuess.site);
    } else {
        alert("Correct!");
    }
  • I don't know where you were going with the spacing between your operators on this one:
    var site = questionElements[i].children[0].title;
    var title= questionElements[i].children[1].innerHTML;

Misleading comments:

Your comment in the following block is a tad wrong.

/**
 * Play a single round of the guessing game. If the user wishes to 
 * play again, they can click the play button again.
 */
function play() {

The function doesn't do a "single" round, it starts a loop.


Chat

This script attempts to append itself in chat.stackexchange.com, where it fails because the header doesn't exist.

\$\endgroup\$
  • \$\begingroup\$ The indentation is like that because of how it is in the element itself. The original name was "Hot Question Game"; I forgot to change it though (along with that misleading comment). \$\endgroup\$ – SirPython Dec 19 '15 at 17:49
5
\$\begingroup\$

Math.random() would need to return 1 for Math.floor(Math.random() * questions.length - 1) to return questions.length - 1. Therefore the last question is unlikely ever to be selected.

For equi-probability (well as near as possible) use :

Math.min(Math.floor(Math.random() * questions.length), questions.length - 1)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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