Demonstrating Progress Requires Effort

A main focus of Post Notif v1.1, for me, was to both enhance the functionality around the post notification send process (via the addition of schedule post send and “test send”) and improve the feedback in this area. To tackle the latter matter, I set out to add a progress bar to provide the blog author with a visual status of the send process. I was surprised to find that, despite the use, in WordPress core, of several (jQuery) UI goodies (e.g, sliders, draggable objects, sortable elements, themeable menus), the progressbar was not among them.

Fortunately, other people have tackled similar challenges in their own WordPress projects. Merrill Mayer’s post was very helpful in figuring out how to render the progressbar, while Tom McFarlin’s provided guidance on checking for and updating processing status.

Conveniently, WordPress core’s usage of jQuery relies on both of the progressbar’s dependencies (UI Core and Widget Factory, contained in the jquery-ui-core.js and jquery-ui-widget.js scripts, respectively) so they were already enqueued. Making the progressbar widget available, however, necessitated adding its script to the enqueue_scripts() function in Post Notif’s admin class (../admin/class-post-notif-admin.php):

	/**
	 * Register the JavaScript for the admin area.
	 *
	 * @since	1.0.0
	 */
	public function enqueue_scripts() {

		/**
		 * An instance of this class should be passed to the run() function
		 * defined in Post_Notif_Admin_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Post_Notif_Admin_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */

		wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/post-notif-admin.min.js', array( 'jquery' ), $this->version, false );
		 
		// Include this to use jQuery UI progressbar
		wp_enqueue_script( 'jquery-ui-progressbar', $this->version, false );
		
	}

The jQuery ThemeRoller provides several nice-looking, ready-to-use themes to choose from (plus the ability to roll your own if you’re so inclined). Though at some point in the future I’d love to give admins the ability to choose the color of their Post Notif progressbar (or, better yet, have the plugin auto-detect the admin color scheme and tailor something accordingly), for now I opted to stick with Cupertino since its blue is relatively close to the blue in the default WordPress dashboard color scheme:

	/**
	 * Register the stylesheets for the admin area.
	 *
	 * @since    1.1.0
	 */
	public function enqueue_styles() {

		/**
		 * An instance of this class should be passed to the run() function
		 * defined in Post_Notif_Admin_Loader as all of the hooks are defined
		 * in that particular class.
		 *
		 * The Post_Notif_Admin_Loader will then create the relationship
		 * between the defined hooks and the functions defined in this
		 * class.
		 */
		 
		wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/jquery-ui-1_12_1_cupertino_progressbar_only.min.css', array(), $this->version, 'all' );
		
	}	

As you may have gathered from the filename I gave the CSS (enqueued above), I had to excerpt the Cupertino theme’s CSS (downloaded from the jQuery CDN) to provide the progressbar with the functionality I was after while still keeping the CSS file size (which I further minified) to a minimum. I found that the following fit the bill:

.ui-progressbar {
	position: relative;
}
#id_spnSendPostNotifProgressBarLabel {
	position: absolute;
	left: 50%;
	top: 4px;
	font-weight: bold;
	text-shadow: 1px 1px 0 #fff;
}

/*! jQuery UI - v1.12.1 - 2016-09-14
* http://jqueryui.com
* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Lucida%20Grande%2CLucida%20Sans%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=deedf7&bgTextureHeader=highlight_soft&bgImgOpacityHeader=100&borderColorHeader=aed0ea&fcHeader=222222&iconColorHeader=72a7cf&bgColorContent=f2f5f7&bgTextureContent=highlight_hard&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=362b36&iconColorContent=72a7cf&bgColorDefault=d7ebf9&bgTextureDefault=glass&bgImgOpacityDefault=80&borderColorDefault=aed0ea&fcDefault=2779aa&iconColorDefault=3d80b3&bgColorHover=e4f1fb&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=74b2e2&fcHover=0070a3&iconColorHover=2694e8&bgColorActive=3baae3&bgTextureActive=glass&bgImgOpacityActive=50&borderColorActive=2694e8&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=ffef8f&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=25&borderColorHighlight=f9dd34&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=cd0a0a&bgTextureError=flat&bgImgOpacityError=15&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=eeeeee&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=90&opacityOverlay=80&bgColorShadow=000000&bgTextureShadow=highlight_hard&bgImgOpacityShadow=70&opacityShadow=30&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px
* Copyright jQuery Foundation and other contributors; Licensed MIT */


/* Component containers
----------------------------------*/
.ui-widget {
	font-family: Lucida Grande,Lucida Sans,Arial,sans-serif;
	font-size: 1.1em;
}
.ui-widget .ui-widget {
	font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
	font-family: Lucida Grande,Lucida Sans,Arial,sans-serif;
	font-size: 1em;
}
.ui-widget.ui-widget-content {
	border: 1px solid #aed0ea;
}
.ui-widget-content {
	border: 1px solid #dddddd;
	background: #f2f5f7;
	color: #362b36;
}
.ui-widget-content a {
	color: #362b36;
}
.ui-widget-header {
	border: 1px solid #aed0ea;
	background: #deedf7;
	color: #222222;
	font-weight: bold;
}
.ui-widget-header a {
	color: #222222;
}

.ui-progressbar {
	height: 2em;
	text-align: left;
	overflow: hidden;
}
.ui-progressbar .ui-progressbar-value {
	margin: -1px;
	height: 100%;
}
.ui-progressbar .ui-progressbar-overlay {
	background: url("data:image/gif;base64,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");
	height: 100%;
	filter: alpha(opacity=25); /* support: IE8 */
	opacity: 0.25;
}
.ui-progressbar-indeterminate .ui-progressbar-value {
	background-image: none;
}

/* Misc visuals
----------------------------------*/

/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
	border-top-left-radius: 6px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
	border-top-right-radius: 6px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
	border-bottom-left-radius: 6px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
	border-bottom-right-radius: 6px;
}

I supplemented the existing admin JavaScript (../admin/js/post-notif-admin.js) to handle instantiation of the progressbar, status updates (one per second), and completion of the post notification send process:

(function( $ ) {
	'use strict';
	
	$(function() {
	 	$("#id_btnPostNotifSend").click(function(e) { 
	 		var post_id = document.getElementById("id_hdnPostID").value;

 			// Hide Send button  	
 			jQuery('#id_btnPostNotifSend').hide();
 			
 			// Set Post Notif status message span to contain processing message
	 		jQuery("#id_spnPostNotifStatus").text(post_notif_send_ajax_obj.processing_msg);	 			 		
 			 		
	 		var send_post_notif_now = document.getElementById('id_radSendPostNotifNow');
	 		if (send_post_notif_now.checked) {

	 			// Send NOW	

	 			// Hide radio buttons
	 			jQuery('#id_spnPostNotifSchedRadioButtons').hide();
 		 			
	 			// Call Post Notif send init function
	 			$.post(ajaxurl, {
	 				action: 'init_post_notif_send',
	 				post_id: post_id,
	 			}, function(data) {

	 				if ('-1' === data.status) {

	 					// Process is already running - STOP PROCESSING!
	 					jQuery("#id_spnPostNotifStatus").text(data.message);	 					
	 				} 
	 				else {

	 					// All is well, carry on
	 			
	 					// Create a jQuery progressbar
	 					jQuery("#id_post_notif_progress_bar").progressbar();

	 					// Check status of process every second
	 					var processCheckTimer = setInterval(function() {
	 				
	 						// Get the current status of the send process
	 						$.post(ajaxurl, {
	 							action: 'get_post_notif_send_status',
	 							post_id: post_id,
	 						}, function(response) {

	 							if ('-1' === response) {

	 								// Set the progress bar to display 100% complete
	 								jQuery("#id_divSendPostNotifProgressBar").progressbar({
	 									value: 100
	 								});
	 			
	 								// Kill timer
	 								clearInterval(processCheckTimer);
	 						
	 								// Hardcode progress bar label to 100% 
	 								jQuery("#id_spnSendPostNotifProgressBarLabel").text( "100%" );
	 							} 
	 							else {

	 								// Update the progress bar to display appropriate percent complete
	 								var percentComplete = Math.floor(100 * response);
	 								jQuery("#id_divSendPostNotifProgressBar").progressbar({
	 									value: percentComplete
	 								});
	 						
	 								// Set progress bar label to appropriate percent complete 
	 								jQuery("#id_spnSendPostNotifProgressBarLabel").text( percentComplete + "%" );
	 							}	 					
	 						});

	 					}, 1000);

	 					$.post(post_notif_send_ajax_obj.ajax_url, {
	 						_ajax_nonce: post_notif_send_ajax_obj.nonce,
	 						action: "post_notif_send",
	 						post_id: post_id,
	 					}, function(data) {

	 						// Update Post Notif status message span with total count of notifs sent
	 						jQuery("#id_spnPostNotifStatus").text(data.message);
	 						
	 						// Update and show Post Notif last sent span with last run timestamp
	 						jQuery("#id_spnPostNotifLastSent").text(data.timestamp);
	 						jQuery("#id_spnPostNotifLastSent").show();	 			
	 					});
	 				} 					

	 			});
	 		}
	 		
.
.
.

	 	});
	});

.
.
.

})( jQuery );	

This took more work (and considerably more code!) than I had anticipated but I think the end results justified the effort. Now I know the precise steps required to add a progress indicator when I need one in the future. Further, I hope sharing this with you will prove useful for your projects as well!

1
0

Rendering An Initially Collapsed Meta Box

As I worked on adding the “Test Send” post notification functionality to Post Notif version 1.1.0, I considered adding a second meta box, to contain it, to the Edit Post page. I was deadset on rendering the new metabox as initially collapsed, leading me to thoroughly scour both WordPress.org and the wider internet for an example I could pattern my code after. I was shocked to find no straightforward example but, through some guessing-and-checking, following a few promising leads, determined that it was simple to accomplish. Merely adding a div, with the “postbox” and “closed” classes, to your meta box markup, will render the meta box as collapsed when you navigate to the page it lives on.1

In the case of Post Notif, its meta box lives on the Edit Post page so the add_meta_box() call looks like the following, with ‘post’ as the value passed as the $screen parameter:

	/**
	 * Add meta box to Edit Post page.
	 *
	 * @since	1.0.0
	 */	
	public function add_post_notif_meta_box() {
	
		add_meta_box(
			'post_notif'
			,'Post Notif'
			,array( $this, 'render_post_notif_meta_box' )
			,'post'
		);
		
	}

In version 1.1.0, I broke out the guts of the meta box into a partial (partials/post-notif-admin-meta-box.php) called by the render_post_notif_meta_box() function:

	
	/**
	 * Render meta box on Edit Post page.
	 *
	 * @since	1.0.0
	 * @param	WP_Post	$post	The object for the current post/page.
	 */
	public function render_post_notif_meta_box( $post ) {
		
		if ( 'publish' == get_post_status( $post->ID ) ) {
			
			// Post has been published, allow Post Notif send
				  
.
.
.
			
			// Render meta box	
			include( plugin_dir_path( __FILE__ ) . 'partials/post-notif-admin-meta-box.php' );			

		}
		else {
			_e( 'Post has not yet been published.', 'post-notif' );
		}
	
	
	}

As I was trying to figure out how I’d get the meta box to default to collapsed, I replaced the partial’s real markup with my test code, eventually hitting on:

<?php

/**
 * Contents of Post Notif meta box.
 *
 * This markup generates the contents of the Post Notif meta box.
 *
 * @link		https://devonostendorf.com/projects/#post-notif
 * @since		1.1.0
 *
 * @package		Post_Notif
 * @subpackage	Post_Notif/admin/partials
 */
?>
<div id="testpostnotifdiv" class="postbox closed">
	<span>Testing to see if this div appears collapsed initially!</span>
</div>

Voilà, there you have it!

In the end, I decided that minimizing clutter was imperative, so I opted to add this to the existing Post Notif meta box, but I wouldn’t be surprised to find myself using this at some point in the future. And I suspect those of you who enjoy plugin development (as much as I do 🙂 ) might find this knowledge handy. If you do try this out, please let me know how it goes. I’d love to see other, related examples, if you find them (or, better yet, create them yourself), as I think this is woefully under-documented presently.

  1. This pointed me to the need to add the “postbox” class to a div, while this confirmed the need to use the “postbox” class and led to this, whose comments confirmed that adding class=”postbox closed” should get me what I was after.
0
0

Post Notif v1.1.2 (Dutch edition)

..And the releases keep rolling out!

This time it is not (entirely) to fix a bug, though Post Notif version 1.1.2 does contain a very slight update to a custom table CREATE statement (to adhere to a WordPress 4.6 dbDelta() KEY format change and thus avoid errors generated on plugin install or update).

The real focus is the addition of a Dutch translation to complement the existing German and Spanish. A hearty thank you to frankmaNL for supplying it mere days after the big Post Notif 1.1.0 release!

That’s it for the contents of this release; I hope this new translation is a welcome one for those of you who run your site(s) with Dutch as your base language.

Here’s hoping November has started out well for you and that we get a break from updates for a bit1!

As always, please do let me know if you have any feedback.

  1. Obviously, if you’d like to create a translation for a language Post Notif has not yet been translated to, by all means send it my way and I WILL roll out another point release!
1
1

Post Notif v1.1.1 – Sanding Down The Rough Edges!

Though I’m not exactly pleased about the need to release this next version so very quickly after the previous one, I can’t say it is totally surprising that there was a bug discovered in version 1.1.0. Fortunately, thanks to Dan’s quick reporting, I was able to eradicate it without much trouble. The root cause was that, per the PHP docs, prior to PHP 5.5, empty() only supports variables; anything else results in a parse error. The example given in the docs, that “empty(trim($name))” will not work, pre PHP 5.5, is precisely what I’d added as a part of the new code in Post Notif v1.1.0. Switching to “false == trim($name)” (always be thinking Yoda conditions! 🙂 ) fixed the problem.

On a more positive note, Ruediger Walter executed a fantastic turnaround on translating the new Post Notif v1.1.0 strings into German (both formal and informal), so those are included in version 1.1.1 as well!

I sincerely hope this is the last of the (negative) fallout from the significant code changes made in version 1.1.0. Thanks for bearing with me!

0
0

Post Notif v1.1.0 – A Conucopia Of Improvements!

Well, it was a long time coming, but Post Notif version 1.1.0 is finally available!

I apologize for the lag time between releases but I am hopeful that you will find at least one thing in this set of fixes and new functionality that was worth waiting for. Without further ado, here is a list of what’s new:

  • Added additional, more specific, post excerpt vars for post notif email template
  • Added @@permalinkurl, @@featuredimage vars for use in post notif email template
  • Fixed issue where subscriber import directly from textarea generates file error
  • Added progress indicators to post notification send
  • Added scheduling for post notification send process
  • Added (force-)confirm subscriber (single and bulk) action to “Manage Subscribers” page
  • Added post notification test send process
  • Fixed code to prevent PHP notice in Import Subscribers functionality
  • Made all datetimes stored in DB UTC but displayed on pages in local timezone
  • Fixed code to prevent invalid, selectable page, displaying in Post Notif admin menu in multisite environment
  • Fixed code which was preventing display of data on View Post Notifs Sent page in multisite environment

My thanks go out to (in alphabetical order): aaron13100, Amandine, Brian, Dan, harisdozz, Heidi, and Ruediger Walter for identifying issues and/or suggesting fixes and/or requesting legitimately missing functionality that made it into this release.

Astute observers, who happen to run Spanish or German-based blogs, will notice quickly that the new set of translated strings, introduced in this release, are not well translated. I bear this blame, completely, as I have just now sent the strings for translation to my patient multilingual friends. I did not want to delay release of this new version any longer, for the bulk of those using Post Notif do so in English and have been very patiently waiting for these new features to be available.

For those of you who are interested, I will be composing some follow up posts, reflecting on the various lessons I learned throughout this arduous development process, in the spirit of attempting to save my fellow WordPress (plugin) developers some time hunting down things I had trouble finding explanations for and demonstrations of.

In the meantime, once you have applied the latest version of Post Notif, please do let me know what you like, dislike, and what’s missing. Thanks!

0
0

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