Site icon WP Smith

Creating a Fun Shortcode: Tetris on WordPress

For a project that I am currently doing, they had some great ideas for great user experience throughout the site. One of them included a game, so I created a Tetris Shortcode (all future changes will only be maintained in the Github repository, not in the gist) where they could place the game anywhere they wanted. So, for your pleasure, enjoy!

How I Built It

I found a JavaScript library called blockrain.js that I refactored, documented, and extended. If you were to compare the original library and mine, there are some substantial differences. Essentially, I made these changes:

Below I will describe how I built the WordPress Shortcode plugin.

Plugin File

First, I started with a basic plugin header:

/**
* Plugin Name: Tetris
* Plugin URI: http://wpsmith.net/
* Description: Adds tetris game shortcode.
* Version: 0.0.1
* Author: Travis Smith, WP Smith
* Author URI: http://wpsmith.net
* Text Domain: tetris
*
* @copyright 2015
* @author Travis Smith
* @link http://wpsmith.net/
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
*/

Next, I added basic security that I add to every plugin base file (and probably should add on every file) to prevent direct access to the file.

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
}

Then, I add my three basic constants that I add to every plugin I build.

define( 'TETRIS_VERSION', '0.0.1' );
define( 'TETRIS_SLUG', 'tetris' );
define( 'TETRIS_FILE', __FILE__ );

I do not really use TETRIS_VERSION and probably could easily remove it, but I add it out of habit. TETRIS_SLUG serves as both the plugin slug and my text domain. TETRIS_FILE is a constant that I use to access __FILE__ of the root/main plugin file instead of having to worry about adding extra functions around __FILE__ on other files to get back to that root file reference.

Now, I add my auto-loader function. Essentially this function checks to see if the class does not exist and that the class name has a Tetris_ prefix. I name all my class files the same as my class contained within the file. If it meets those conditions, then I include the file.

spl_autoload_register( 'tetris_autoload' );
/**
* SPL Class Autoloader for classes.
*
* @param string $class_name Class name being autoloaded.
* @link http://us1.php.net/spl_autoload_register
* @author Travis Smith
* @since 0.1.0
*/
function tetris_autoload( $class_name ) {
// Do nothing if class already exists, not prefixed WPS_
if ( class_exists( $class_name, false ) ||
( !class_exists( $class_name, false ) && false === strpos( $class_name, 'Tetris_' ) ) ) {
return;
}
// Set file
$file = plugin_dir_path( __FILE__ ) . "includes/classes/$class_name.php";
// Load file
if ( file_exists( $file ) ) {
include_once( $file );
}
}

Normally, I add code for plugin activation and deactivation as well as for loading the text domain, but I am going to skip that for this post. For the purposes of the scope of the plugin, we are only instantiating the Shortcode class.

new Tetris_Shortcode();

Shortcode Class

Class Variables

The shortcode class has 3 variables: $debug, $scripts_registered, and $scripts_enqueued. These three variables are for tracking whether the site is in debug mode, set by WP_DEBUG and/or SCRIPT_DEBUG. The other two variables track whether the scripts have already been registered and/or enqueued to ensure that the class does not add them again.

The Constructor

Once the plugin has been instantiated, the Shortcode class adds the shortcode, sets the $debug variable, and registers the scripts on the init hook.

/**
* Constructor
*/
public function __construct() {
// Add the shortcode
add_shortcode( 'tetris', array( $this, 'shortcode' ) );
// Set debug class var.
$this->debug = ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) );
// Register scripts!!
add_action( 'init', array( $this, 'register_scripts' ), 11 );
}

Registering the Scripts/Styles

This is really tricky with shortcodes primarily because the shortcode could or could not be used within the displayed page (content/post type agnostic). With shortcodes, I always register the scripts at the init hook, just like core. The really cool part of how I register scripts is that I do it dynamically: if in debug mode, use the debug script (the non-minified file) otherwise, use the minified script (*.min.js). What is more, I also do not use the TETRIS_VERSION constant created earlier for my script versions any more. Call me lazy, but now I use filemtime(). This bursts the cache every time I change the file, so I never have to worry about caching with my scripts/styles.

Registering Scripts/Styles Dynamically

The key to dynamically registering the script is my $debug variable which uses the $debug class variable.

$suffix = $this->debug ? '.js' : '.min.js';

Next is the portion that actually registers the jgestures, blockrain, and blockrain-init scripts.

wp_register_script(
'jgestures',
plugins_url( "js/jgestures$suffix", TETRIS_FILE ),
array( 'jquery', ),
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/jgestures$suffix" ),
false
);
wp_register_script(
'blockrain',
plugins_url( "js/jquery.blockrain$suffix", TETRIS_FILE ),
array( 'jquery', 'jquery-ui-widget', 'jgestures', ),
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/jquery.blockrain$suffix" ),
false
);
wp_register_script(
'blockrain-init',
plugins_url( "js/blockrain.init$suffix", TETRIS_FILE ),
array( 'blockrain', ),
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/blockrain.init$suffix" ),
false
);

*Note the use of filemtime().

Also, when I registered the blockrain script, notice that I set jquery, jquery-ui-widget, and jgestures as dependent scripts, and when I registered the blockrain-init script, notice that I set blockrain as its dependent. This is extremely important later.

array( 'jquery', 'jquery-ui-widget', 'jgestures', ),

array( 'blockrain', ),

Next, I need to localize some scripts because blockrain has a couple string variables.

$args = array(
'playText' => __( 'Let\'s play some Tetris', TETRIS_SLUG ),
'playButtonText' => __( 'Play', TETRIS_SLUG ),
'gameOverText' => __( 'Game Over', TETRIS_SLUG ),
'restartButtonText' => __( 'Play Again', TETRIS_SLUG ),
'scoreText' => __( 'Score', TETRIS_SLUG ),
);
wp_localize_script( 'blockrain-init', 'blockrainI10n', $args );

Finally, I register my style similarly to my scripts.

$suffix = $this->debug ? '.css' : '.min.css';
wp_register_style(
'blockrain',
plugins_url( "css/blockrain$suffix", TETRIS_FILE ),
null,
filemtime( plugin_dir_path( TETRIS_FILE ) . "css/blockrain$suffix" )
);

Enqueuing Scripts

My next method enqueues the scripts with only two lines. These two lines output all the scripts because blockrain-init depends on blockrain which depends on jquery, jquery-ui-widget, and jgestures. So, WordPress's script manager class will output the scripts in the proper order:

  1. jquery
  2. jquery-ui-widget
  3. jgestures
  4. blockrain
  5. blockrain-init
/**
* Output the scripts for WordPress
*
* @uses wp_enqueue_script
*/
public function enqueue_scripts() {
if ( $this->scripts_enqueued ) {
return;
}
// Ensure scripts are registered
$this->register_scripts();
/* SCRIPTS */
wp_enqueue_script( 'blockrain-init' );
/* STYLES */
wp_enqueue_style( 'blockrain' );
// Prevent redundant calls
$this->scripts_enqueued = true;
}

The Shortcode

Now, I have my scripts registered and I have my enqueue (script output) method. For my shortcode, I only want to accept two arguments: height and weight, which I will parse and merge using shortcode_atts(), noting that someone can filter these options with shortcode_atts_tetris filter.

$atts = shortcode_atts( array(
'height' => '100%',
'width' => '100%'
), $atts, 'tetris' );

Next, I want to output my scripts.

$this->enqueue_scripts();

Then I return my HTML markup.

return sprintf( '<div class="tetris-game" style="max-width:%s; max-height:%s;"></div>', $atts['width'], $atts['height'] );

For those who do not know what sprintf() does, let me explain it briefly. Simply, it takes the string and locates various sub-strings (e.g., %s, %d, %1$s, %1$d, etc.). The %s replacement does a simple validation and replaces the %s sub-string with only a string or an empty string. Likewise, %d does numbers. If you see 1$, 2$, etc. between %s or %d (etc), that is identifying the index of the following parameters. For example, I could have written that line as:
return sprintf( '
', $atts['width'], $atts['height'] );

So, 1$ refers to $atts['width'] and 2$ refers to $atts['width'], both which must be strings.

That completes the shortcode. Again, all future changes will only be maintained in the Github repository, Tetris Shortcode, not in the gist found in this post.

Remaining Work