Monster On The Mind

My son received the Mastermind “board” game for Easter and he and his sister started playing it (nonstop) after figuring out how it worked. They came up with rules for whether or not they could use two pegs of a single color in their code (rarely) and whether it was “fair” to use a blank instead of a color (no way!).

Having previously stumbled upon Anko’s JavaScript example (search the page for “Same in JS”) when looking for a simple, functional platform for running a JS text adventure game (for a programming session with my daughter and some classmates at school), I thought it would work nicely as a base for recreating Mastermind as a simple web app.

The HTML (index.html) to display the game and handle input and output is very straightforward:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>Monstermind</title>
	</head>
	<body>
		<script src="js/game.js"></script>
		<textarea id="id_txaOutputWindow" rows="30" cols="55" readonly="readonly"></textarea>
		<form onSubmit="parse(); return false;">
			<input type="text" id="id_txtGuessPosn0" maxlength="1" size="1" required="required" />
			<input type="text" id="id_txtGuessPosn1" maxlength="1" size="1" required="required" />
			<input type="text" id="id_txtGuessPosn2" maxlength="1" size="1" required="required" />
			<input type="text" id="id_txtGuessPosn3" maxlength="1" size="1" required="required" />
			<input type="submit" value="Submit Guess" />
		</form>
	</body>
</html>

My first approach for managing game state involved placing the data in hidden form elements and madly passing them around via post. However, I quickly realized this was cumbersome and presented me with a nice opportunity to experiment with localStorage.

Here’s the JavaScript (game.js) that powers the game; I’ve commented it relatively thoroughly so I don’t think it requires much additional commentary beyond directing your attention to the fact that the game state is loaded each time the page loads and that each time the user hits the “Submit Guess” button the parse() function is called:

// Declare global vars to keep track of game state
var gameState;
var codeArr = [];


window.onload = function WindowLoad(event) {

	// Check for an existing game
	var gameStateJSON = window.localStorage.getItem("monstermindGameState"); 
	if (gameStateJSON) { 

		// Populate game state from localStorage
		gameState = JSON.parse(gameStateJSON);
		codeArr = gameState["codeArr"];
		
		if (gameState["codeNotBroken"] == 0) {
			
			// Game was won on user's previous guess, so start a new game
			newGame();	
		}
	}
	else {
	
		// Game is NEW
		newGame();
	}
}

function newGame() {
		
	// Pick random secret 4-digit code comprised of letters A - F
	// NOTE: I gave up on trying to make the codes numeric as JSON.parse apparently casts everything as a char
	//		so then indexOf() fails to behave properly (e.g., 2 != 2 and the like)
	for (var i = 0; i < 4; i++) {
		
		// NOTE: Pick a number between 65 and 70 (inclusive) and then convert that ASCII value into a character
		codeArr[i] = String.fromCharCode(Math.floor((Math.random() * 6) + 65));
	}

	// Initialize game state
	gameState = { "codeArr": codeArr, "codeNotBroken": 1, "guessCount": 0 };

	// Put game state into localStorage
	localStorage.setItem("monstermindGameState", JSON.stringify(gameState));
		
	// Blank out game board and welcome player to game
	var textOut = document.getElementById("id_txaOutputWindow");
	textOut.value = "";
	textOut.value += "Welcome to Monstermind.  You have 10 guesses to get the code right.  The code is 4 characters long and consists of letters 'A' through 'F'.  You will get a black marker for each letter that you guess correctly and a white marker for each letter that is contained in the code but in the wrong position.  Feel free to make your first guess!\n\n";
    
	// Blank out player's guesses from text input fields (if they exist from a previous game)
	document.getElementById("id_txtGuessPosn0").value = "";
	document.getElementById("id_txtGuessPosn1").value = "";
	document.getElementById("id_txtGuessPosn2").value = "";
	document.getElementById("id_txtGuessPosn3").value = "";
}

function processGuess(guessArr) {
	
	var blackMarkerCount = 0;
	var whiteMarkerCount = 0;
	var i;
	
	var searchArr = [];
	
	for (i = 0; i < 4; i++) {
		if (guessArr[i] == codeArr[i]) {
			
			// Character chosen in this position matches code, so increment black marker count
			blackMarkerCount++;
		}
		else {
		
			// Character chosen in this position does NOT match code, so add this position's code to the search array for 
			//	white marker candidates
			searchArr[i] = codeArr[i];
		}
	}
	if (blackMarkerCount != 4) {
		
		// Player has NOT guessed the code correctly
		for (i = 0; i < 4; i++) {
			if (searchArr[i]) {
				
				// Look for character match, but in wrong position, and ONLY in positions where the guess did NOT match
				//	the code up in the black marker search section of code
				var matchFoundIndex = searchArr.indexOf(guessArr[i]);
				if (matchFoundIndex !== -1) {
										
					// Character match found in wrong location AND wrong location has NOT already been matched with the
					//	correct character (i.e. a black marker) so this gets a white marker
					whiteMarkerCount++;
					
					// Mangle search array value just used so it is not available to use in subsequent searches (which
					//	would lead to false white markers)
					searchArr[matchFoundIndex] = "x";
				}
			}
		}
	}
	return { "blackMarkerCount": blackMarkerCount, "whiteMarkerCount": whiteMarkerCount };
}

function inputErrors(guessArr) {

	var errorMessage = "";
	
	for (var i = 0; i < 4; i++) {
		if ((guessArr[i].charCodeAt() < 65 ) || (guessArr[i].charCodeAt() > 70 )) {
			
			// Invalid input
			errorMessage += "'" + guessArr[i] + "', ";			
		}
	}
	if (errorMessage.length) {
		errorMessage = errorMessage.substring(0, errorMessage.length - 2);
	}
	return errorMessage;
	
}

function parse() {

	var errorMessage;

	// Increment guess # by 1
	gameState["guessCount"] = Number(gameState["guessCount"]) + 1;
 
	if ((gameState["guessCount"] == 11) || (gameState["codeNotBroken"] == 0)) {
	
		// Player hit Submit Guess button instead of reloading page - start new game for them!
		newGame();
		return;
	}

	// Assign variable for writing to game board
	var textOut = document.getElementById("id_txaOutputWindow");

	// Retrieve player's guesses from text input fields and force them to uppercase
	var guessArr = [ document.getElementById("id_txtGuessPosn0").value.toUpperCase()
		, document.getElementById("id_txtGuessPosn1").value.toUpperCase()
		, document.getElementById("id_txtGuessPosn2").value.toUpperCase()
		, document.getElementById("id_txtGuessPosn3").value.toUpperCase()
	];

	// Validate input
	if ((errorMessage = inputErrors(guessArr)).length > 0) {
		textOut.value += "Sorry but the following input is invalid: " + errorMessage + "\n\n";
		
		// Don't count invalid input as a guess
		gameState["guessCount"] = Number(gameState["guessCount"]) - 1;
		return;
	}
	
	// Process guesses
	var newStateArray = processGuess(guessArr);
  
	// Render results of current guess to game board
	textOut.value += "Guess #" + gameState["guessCount"] + ": " 
    	+ guessArr[0] 
    	+ guessArr[1] 
    	+ guessArr[2] 
    	+ guessArr[3] 
    	+ "\nblack count: " + newStateArray["blackMarkerCount"] 
    	+ ", white count: " + newStateArray["whiteMarkerCount"] 
    	+ "\n\n";
    
	if (newStateArray["blackMarkerCount"] == 4) {
    	
		// Player has guessed the code correctly!
		gameState["codeNotBroken"] = 0;
		textOut.value += "You WIN!!\n\nReload the page to play again";
	}
	else if (gameState["guessCount"] == 10) {
    	
		// Player has exhausted all of their guesses, reveal code
		gameState["codeNotBroken"] = 0;
		textOut.value += "Sorry, your 10 guesses are up!\n\nThe correct code was: "
			+ codeArr[0] 
			+ codeArr[1]
			+ codeArr[2] 
			+ codeArr[3] 
			+ "\n\nReload the page to play again";
	}
    	    
	// Write the updated game state to localStorage so that, on page reload, the game knows to restart
	localStorage.setItem('monstermindGameState', JSON.stringify(gameState));
}

This program works well (enough) and has been lots of fun for both me and the kids to play whenever we feel like it, particularly because it requires neither the tedium of the setup/cleanup of the physical game board nor the cooperation of a second human to serve as the code master. 🙂

There is certainly room for improvement, however. More accurately recreating the experience of the board game by, for instance, having the game use colors in the text input/output (e.g, “green” instead of “a” and “red” instead of “b”), not to mention actually switching from the current text interface to an actual graphical experience, complete with colored pegs and markers, and mouse-clicks to position each guess, would make Monstermind more like Mastermind.

But there are many online implementations of Mastermind that aim to be true, literal recreations of the original, so my personal interests lie more in improvements to the approach I’ve chosen. Of particular interest to me is discovering a means to obscure localStorage contents from scrutiny via browser developer tools, which would make the game immune to the temptation of technically-savvy players to cheat before they hit their 10th and final guess. I have been unable, thus far, to find any real info on the web about how to do this. If you have any suggestions, please do leave them in the comments!

Here is Monstermind on GitHub1 if you’re interested in pulling down a copy for your own use (versus copying-and-pasting from this post). Either way, enjoy – it IS a fun game any way you play it!

  1. I felt it was only proper to put our family’s spin on the name of the game in honor of our dog, Monty, a husky Yorkie whose passion for consumption of all things (even remotely) edible (including Mastermind pegs!) combined with his frequent (and lengthy) high-decibel monologues has earned him the nickname “Monster.”
1
1

Post Notif v1.0.9 (German edition)

OK, I should probably stop starting each of my posts with something resembling “the very next release will contain this big set of new features…”. Every time I do that, a reason to put out a quick point release crops up.

However, barring a complete and utter disaster manifesting itself (in the form of a terribly impactive bug that needs immediate attention) in the meantime, this will be the final 1.0.x release for Post Notif. What that means is that Post Notif version 1.1.0, containing some significant new functionality (as well as some accumulated cleanup items), will be up next.

For now, though, let’s turn our attention to version 1.0.9, which is all about translation!

A huge thanks to Ruediger Walter for all of his hard work in creating not one, but two (both formal and informal) German translations for Post Notif!

Additionally, a few more strings were added to the Spanish translation (as I’d missed them during the release of 1.0.8).

That is “all” that this release contains, but I hope and believe that these items will significantly improve the experience for those of you who run your sites with German (and to a lesser extent, with Spanish) as your base language.

Please let me know if you have any feedback.

0
0

Post Notif v1.0.8 (AKA “The Last Stop On The Road To New Functionality”)

Though my intention had been to incorporate some additional (substantial) features in this release of Post Notif, in the interest of getting a fix for a long-elusive problem in to the plugin sooner, rather than later, I’ve chosen to postpone those until the next release. That being said, here is what Post Notif version 1.0.8 contains:

  • A fix for custom permalink configuration (thanks to Amandine for helping to identify and test this). Incidentally, I believe this is something others have run into previously but it had remained undiagnosed up until now.
  • A fix to handle an incorrect error message, regarding the translation check nag functionality, from appearing during the core update process
  • The addition of the “@@postauthor” and “@@postexcerpt” variables to the set of those you can use to define your post notification templates. Additionally, I’ve added a comprehensive explanation to the FAQ explaining all available variables (and where each can be used).

That’s it for now. Please let me know if you run into any issues with this release!

1
0

Post Notif v1.0.7 – Cleanup and Performance Enhancement

This is another relatively small release (as I work toward introducing significant new functionality in the next version of Post Notif).

Specifically, version 1.0.7 repairs an issue that revealed itself once the Spanish translation was added in version 1.0.6. Namely, I had to remove translation of the ‘subscriber’ string in a couple of places as the code that handles processing on the Staged Subscribers and Manage Subscribers pages expects this string to ALWAYS be in English.

Additionally, I added a bit of code to spare UK admins from the translation nag I added in 1.0.6. Thanks to Jake for his confirmation that the existing, default en_US string set is satisfactory for sites running with their language set to en_UK. 🙂

Finally, I added minified versions of the existing Post Notif JavaScript files and changed all code that referenced these files to instead use the minified files. The intent here is to reduce the size of the files your users (particularly those accessing your site via mobile devices) have to pull down when they visit your site.

Per usual, I would sincerely appreciate it if you’d let me know about any bugs you encounter with this new release. I’m always open to hearing your feature requests too.

0
0

Post Notif v1.0.6 (Spanish edition)

This new version is strictly focused on translation.

Namely, thanks to Enrique’s considerable efforts, Post Notif now has a Spanish translation!

Also, thanks to Clorith’s example, translation nag messages will now appear in the admin console of all those using Post Notif with an install language other than U.S. English (en_US) or Spanish Spanish (es_ES). Don’t worry, once you dismiss it, you’ll never see it again.

However, if you ARE willing to translate Post Notif into your native language, I would be truly appreciative; please contact me through the email icon at the top right of this blog.

Please let me know if you run into bugs with this new release. And, as always, please feel free to send me any questions or feature requests you have.

0
0

A Little Bit Of Nonce Sense

I was recently reminded, while improving my WordPress plugins, of the importance of protecting forms and URLs that “perform work” from CSRF attacks. The core WordPress nonce implementation works very well in the WordPress realm (and if you want a true, single-use nonce for WordPress, check out Cal Evans’ solution), but, for my generic PHP needs, I wanted a simple class that was a bit more flexible than the others I’d found while scouring the web.

I won’t go into a detailed discussion of nonces here (that’s been done by Wikipedia and plenty of other resources around the internet). Instead, I’ll dig in to why I wrote my ONonce class the way I did.

To begin with, I defined constants with an eye toward making it a matter of simply changing one line of code if you want things configured differently:

	// Hash algorithm to use
	const HASH_ALGORITHM = 'sha512';
	
	// Length of nonce
	const NONCE_LENGTH = 12;
	
	// Position within hash to start nonce generation
	const NONCE_OFFSET = -15;	// 15 chars from end of string
	
	// Name of form input to store nonce
	const NONCE_FORM_INPUT_NAME = '_ononce';
	
	// Name of query parameter to pass nonce
	const NONCE_QUERY_PARAM_NAME = 'ononce';
	
	// Secret: Make this a looong (64? character) string of randomness
	const SECRET = 'R*Q3Q*t|?,;c;RR2g?<:PIB0J+T/7kDkSS-VQeH^N3-q:XbdCgS>G+!*l<}|)S.h';

This starts with defining the hash algorithm used and calling it via PHP’s hash() function, so that, as better algorithms are released down the road, you can change a single statement in ONonce to update ALL nonces throughout your application.

Different hash algorithms generate different length outputs so I defined both the length of the substring of the hash output to use as the nonce and the point in the hash output from which to start the substring.

Developers all have their preferences, when it comes to naming conventions, so the names of both the nonce form hidden input and the nonce query parameter are defined here as well. Additionally, I wanted to add some flexibility to accommodate applications (including those I build with Kohana) that don’t use query string parameters (you might know them as “fields” instead)1.

The secret MUST be updated (don’t use the sample string included with the code! 🙂 ) to a long string of gibberish to ensure that nonces generated are decently random. A good place to get one, if you’re not feeling particularly creative, is from WordPress’ secret key generator.

I also determined that there were a couple of settings that would be particularly helpful to define as defaults but allow the developer to override, as needed, in their method calls:

	// Default current user ID
	const DEFAULT_CURRENT_USER_ID = 123456789;
	
	// Default lifetime for nonces (in seconds)
	const DEFAULT_LIFETIME = 3600;	// 1 hour

When building a new application, I found that I had a compelling need for more than just a single, set nonce lifetime. Further, not all applications require a user be signed in to fill out forms, and, even those that do, implement this in many different ways (not always relying on Session variables, for instance).

My solution for both of these situations was to define defaults (as constants) for each in the class definition and then allow them to be optionally overridden by the developer, if/when needed, for each pair of nonce creation and validation calls. This also sets up nicely for extensibility, since, if you want to incorporate additional overrides, the $overrides array is an existing vehicle, already present in the methods’ signatures.

Speaking of nonce creation and validation calls, here are the methods to the madness:

 	/**
	 * Create nonce as a string of NONCE_LENGTH, valid for DEFAULT_LIFETIME
	 * seconds (unless overridden).
	 *
	 * @since	1.0.0
	 * @param	string		$name			Nonce name
	 * @param	string		$action			Nonce action
	 * @param	string[]	$overrides		Optional. Array containing 'current_user' and/or 'lifetime' override values.
	 * @uses	self::DEFAULT_CURRENT_USER_ID
	 * @uses	self::DEFAULT_LIFETIME
	 * @uses	self::HASH_ALGORITHM
	 * @uses	self::SECRET
	 * @uses	self::NONCE_OFFSET
	 * @uses	self::NONCE_LENGTH
	 * @return	string
	 */
    private static function create($name, $action, array $overrides = NULL)
    {		  
    	
    	if ( ! isset($overrides['current_user']))
    	{
    		
    		// No current user ID was passed - use default
    		$overrides['current_user'] = self::DEFAULT_CURRENT_USER_ID;
    	}
    	if ( ! isset($overrides['lifetime']))
    	{

    		// No lifetime was passed - use default
    		$overrides['lifetime'] = self::DEFAULT_LIFETIME;
    	}

        $deadline = ceil(time() / ($overrides['lifetime'] / 2));

        return substr(hash(self::HASH_ALGORITHM, $deadline.$name.$action.$overrides['current_user'].self::SECRET), self::NONCE_OFFSET, self::NONCE_LENGTH);
                
    }

This is the heart of the class. It checks for overrides and uses them, if present, and then creates (and returns) the nonce as a substring of the hash of all of the concatenated constituents.

 	/**
	 * Create a string containing HTML markup, for a hidden form input field,
	 * with a name of NONCE_FORM_INPUT_NAME and a value consisting of the nonce
	 * generated by self::create().
	 *
	 * @since	1.0.0
	 * @param	string		$name			Nonce name
	 * @param	string		$action			Nonce action
	 * @param	string[]	$overrides		Optional. Array containing 'current_user' and/or 'lifetime' override values.
	 * @uses	self::NONCE_FORM_INPUT_NAME
	 * @uses	self::create
	 * @return	string
	 */
    public static function create_form_input($name, $action, array $overrides = NULL)
    {               

        return '<input type="hidden" name="'.self::NONCE_FORM_INPUT_NAME.'" value="'.self::create($name, $action, $overrides).'" />';
 
    }

This creates the hidden form input field, containing the nonce generated by calling the create() method.

 	/**
	 * Create a string containing a URL fragment with a name of 
	 * NONCE_QUERY_PARAM_NAME (and, if NONCE_QUERY_PARAM_NAME is not blank, a
	 * literal "="), and a value consisting of the nonce generated by
	 * self::create().
	 *
	 * @since	1.0.0
	 * @param	string		$name			Nonce name
	 * @param	string		$action			Nonce action
	 * @param	string[]	$overrides		Optional. Array containing 'current_user' and/or 'lifetime' override values.
	 * @uses	self::NONCE_QUERY_PARAM_NAME
	 * @uses	self::create
	 * @return	string
	 */
    public static function create_url_fragment($name, $action, array $overrides = NULL)
    {               
                            
        // NOTE: Some routing schemes may not use query string parameters, instead opting to use only delimited
        //	values
        if (($url_fragment = self::NONCE_QUERY_PARAM_NAME) != '')
        {
        	$url_fragment .= '=';
        }
        $url_fragment .= self::create($name, $action, $overrides);
       
        return $url_fragment;

    }

This creates the URL fragment, consisting of (the optionally-defined nonce query parameter name and ‘=’ and) the nonce generated by calling the create() method.

 	/**
	 * Determine whether a supplied nonce is valid and return success/failure.
	 *
	 * @since	1.0.0
	 * @param	string		$name			Nonce name
	 * @param	string		$action			Nonce action
	 * @param	string		$nonce			Nonce value
	 * @param	string[]	$overrides		Optional. Array containing 'current_user' and/or 'lifetime' override values.
	 * @uses	self::create
	 * @return	boolean
	 */
    public static function is_valid($name, $action, $nonce, array $overrides = NULL)
    {               
                    
        return self::create($name, $action, $overrides) == $nonce;

    }

Finally, this method returns true or false based on a comparison of the passed nonce and the nonce generated by calling the create() method.

That’s all well and good, but how about some examples of how to actually use the class:

  1. Include the class in each file from which you need to call it:

    include 'ONonce.php';
    
  2. Let’s look at how to create and confirm a hidden form input:

    1. Create (with defaults):

      echo ONonce::create_form_input('name_value', 'action_value');
      
    2. Confirm (with defaults):

      $nonce_from_form = sanitize($_POST['_ononce']);  
      if (ONonce::is_valid('name_value', 'action_value', $nonce_from_form))  
      {  
      
          // Do something  
      
      }  
      
    3. Create (with overrides):

      echo ONonce::create_form_input('name_value', 'action_value', array('current_user' => 12, 'lifetime' => 300));
      
    4. Confirm (with overrides):

      $nonce_from_form = sanitize($_POST['_ononce']);
      if (ONonce::is_valid('name_value', 'action_value', $nonce_from_form, array('current_user' => 12, 'lifetime' => 300)))
      {
      
          // Do something
      
      }
      
  3. And how to create and confirm a URL fragment:

    1. Create (with defaults):

      echo 'https://some_url.com?query_param1=foo/&'.ONonce::create_url_fragment('name_value', 'action_value');
      
    2. Confirm (with defaults):

      $nonce_from_url = sanitize($_GET['ononce']);
      if (ONonce::is_valid('name_value', 'action_value', $nonce_from_url))
      { 
      
          // Do something  
      
      }
      
    3. Create (with overrides):

      echo 'https://some_url.com?query_param1=foo/&'.ONonce::create_url_fragment('name_value', 'action_value', array('current_user' => 12, 'lifetime' => 300));
      
    4. Confirm (with overrides):

      $nonce_from_url = sanitize($_GET['ononce']);
      if (ONonce::is_valid('name_value', 'action_value', $nonce_from_url, array('current_user' => 12, 'lifetime' => 300)))
      {
      
          // Do something  
      
      }
      

That about wraps it up for now. If you have a PHP project requiring nonces, and you give ONonce a try, I’d love to hear your feedback. Or if you just want to critique my code, without actually trying it out, I’m fine with that too! 🙂

  1. You’ll see more about this below, in the discussion of the class methods.
0
0

Post Notif v1.0.5 Is Here (At Long Last)

Well, that sure took some time, but Post Notif v1.0.5 is now out-and-about, for those of you who have been eagerly anticipating its release! 🙂

It fills a couple of holes in functionality that have been bothering me for quite awhile as well as some UI improvements. Namely, it:

  • Allows admins to make all or a subset of system categories available to subscribers or turn off categories entirely
  • Adds a configurable “processing” AJAX message to the subscription widget.
  • Adds undo subscriber delete (both single and bulk) to the Manage Subscribers page

This release also includes a fairly heavy dose of security improvement, including the addition of nonces to forms and URLs that were missing them, as well as inclusion of a ton of “esc()” sanitization function calls where they’d been lacking.

A hearty thanks to Jake and nando99 for their input and feedback on the changes.

Please do get in touch with any questions or feature requests you have, and most importantly, please pass along any bugs you stumble upon!

0
1