Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Steve Withington
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Steve Withington ( @stevewithington )

jQuery Powered Mine Sweeper Game

By on

While I was on the road going from CFinNC to BFusion / BFLEX, I tinkered around with some jQuery code. I wanted to try and make something fun but not something that would take too much time (since being on the road is exhausting). And so, I created this jQuery powered Mine Sweeper game. Much like the old computer game, you can select the number of rows, columns, and bombs (using either an explicit number or a percentage). Clicking on the cell reveals the number of surrounding bombs. Clicking, while holding the ALT key, flags the cells as a potential bomb.

If you want to try the game for yourself, click on one of the links. Notice that the URL variables control the display:

jQuery Mine Sweeper - Easy

jQuery Mine Sweeper - Medium

jQuery Mine Sweeper - Hard

The markup for this game is extremely simple as the real logic has been factored out into a number of Javascript files:

<!DOCTYPE HTML>
<html>
<head>
	<title>jQuery Powered Mine Sweeper</title>
	<style type="text/css">

		body.caution {
			cursor: help ;
			}

		table.mine-sweeper {}

		table.mine-sweeper td {
			background-color: #F0F0F0 ;
			border: 1px solid #333333 ;
			cursor: pointer ;
			height: 25px ;
			line-height: 25px ;
			overflow: hidden ;
			text-align: center ;
			width: 25px ;
			}

		table.mine-sweeper td.active {
			background-color: #D8D8D8 ;
			}

		table.mine-sweeper td.bomb {
			font-weight: bold ;
			}

		table.mine-sweeper td.bombed {
			background-color: #FFD0D0 ;
			color: #CC0000 ;
			font-weight: bold ;
			}

		table.mine-sweeper td.caution {
			background-color: #FFD0D0 ;
			color: #CC0000 ;
			font-weight: bold ;
			}

	</style>
	<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
	<script type="text/javascript" src="jquery.randrange.js"></script>
	<script type="text/javascript" src="jquery.repeatstring.js"></script>
	<script type="text/javascript" src="jquery.randomfilter.js"></script>
	<script type="text/javascript" src="jquery.near.js"></script>
	<script type="text/javascript" src="jquery.minesweeper.js"></script>
	<script type="text/javascript">

		// When the DOM is ready, build the mine sweeper game.
		jQuery(function( $ ){

			var mineSweerper = new MineSweeper(
				$( "table.mine-sweeper" ),
				17,
				12,
				"99%"
			);

		});

	</script>
</head>
<body>

	<h1>
		jQuery Powered Mine Sweeper
	</h1>

	<table cellspacing="2" class="mine-sweeper">
		<!--- Will be populated dynamically. --->
	</table>

</body>
</html>

The game has to be played in a TABLE node because the game logic requires the structured relationship between table cells. The game controller itself is wrapped up in this Javascript class:

jquery.minesweeper.js

(function( $ ){

	// I am the controller for the mine sweeper game.
	function MineSweeper( selector, columnCount, rowCount, bombCount ){
		var self = this;
		this.table = $( selector );
		this.columnCount = (columnCount || 30);
		this.rowCount = (rowCount || 30);

		// Check to see if the bomb count contains a percent sign.
		if (
			(typeof( bombCount ) == "string") &&
			(bombCount.indexOf( "%" ) > 0)
			){

			// The bomb count is a percentage of the total number
			// of cells.
			this.bombCount = Math.floor(
				(this.columnCount * this.rowCount) *
				(parseInt( bombCount ) / 100)
			);

		} else {

			// The bomb count is just a standard number.
			this.bombCount = (bombCount || 15);

		}

		// Bind the click handler for the table. This way, we
		// don't have to attach event handlers to each cell.
		this.table.click(
			function( event ){
				// Pass off to the table click handler.
				self.onClick( event );

				// Cancel default event.
				return( false );
			}
		);

		// Initialize the table.
		this.initTable();
	};


	// I build the actual markup of the table using the
	// given number of columns and rows.
	MineSweeper.prototype.buildTable = function(){
		// Build the markup for a given row.
		var rowHtml = ("<tr>" + $.repeatString( "<td class=\"active\">&nbsp;</td>", this.columnCount ) + "</tr>");

		// Build the markup for the table using the row
		// data the given number of times.
		var tableHtml = $.repeatString( rowHtml, this.rowCount );

		// Set the HTML of the table to fill it out.
		this.table.html( tableHtml );
	};


	// I check to see if an end-game has been reached.
	// If so, then I give the option to restart.
	MineSweeper.prototype.checkEndGame = function(){
		var message = "";
		var isEndGame = false;

		// Check to see if any of the bombs have exploded.
		if (this.bombCells.filter( ".bombed" ).size()){

			// Set the message.
			message = "You LOSE - Play Again?";

			// Flag the end game.
			isEndGame = true;

		// Check to see if there are any more active
		// non-bomb cells. If not, then the user has
		// successfully clicked all non-bomb cells.
		} else if (!this.nonBombCells.filter( ".active" ).size()){

			// Set the message.
			message = "You WIN - Play Again?";

			// Flag the end game.
			isEndGame = true;

		}

		// Check to see if the game is over.
		if (isEndGame){

			// Prompt for replay.
			if (confirm( message )){

				// Restart the game.
				this.restart();

			}

		}
	};


	// I clear the table of any markup.
	MineSweeper.prototype.clearTable = function(){
		this.table.empty();
	};


	// I initialize the table.
	MineSweeper.prototype.initTable = function(){
		var self = this;

		// Clear the table if there is any existing markup.
		this.clearTable();

		// Now that we have ensured that the table is
		// empty, let's build out the HTML for the table.
		this.buildTable();

		// Gather the cells of the table.
		this.cells = this.table.find( "td" );

		// Set the "near bombs" data for each cell to
		// zero. This is the number of bombs that the cell
		// is near.
		this.cells.data( "nearBombs", 0 );

		// For each cell, keep a collection of the cells
		// that are near this cell.
		this.cells.each(
			function( index, cellNode ){
				var cell = $( this );

				// Store the near cells.
				cell.data( "near", cell.near() );
			}
		);

		// Randomly select and gather the bomb cells.
		this.bombCells = this.cells
			.randomFilter( this.bombCount )
			.addClass( "bomb" );
		;

		// Now that we've selected the bomb cells, let's
		// get teh non-bomb cells.
		this.nonBombCells = this.cells.filter(
			function( index ){
				// If this cell does NOT appear in the bomb
				// cells collection, then it's a non-bomb
				// cell.
				return( self.bombCells.index( this ) == -1 );
			}
		);

		// Now that we have the bomb cells, let's go through
		// each of them and apply its "nearness" to the
		// cells around it.
		this.bombCells.each(
			function( index, node ){
				var cell = $( this );
				var nearCells = cell.data( "near" );

				// For each near cell, increment the near
				// data counter.
				nearCells.each(
					function(){
						var nearCell = $( this );

						// Get the current near data and
						// increment it.
						nearCell.data(
							"nearBombs",
							(nearCell.data( "nearBombs" ) + 1)
						);
					}
				);
			}
		);
	};


	// I handle the clicks at the table level.
	MineSweeper.prototype.onClick = function( event ){
		// Get the trigger for the event.
		var target = $( event.target );

		// Check to make sure the target is an active cell.
		// If it is not, then we are not interested.
		if (!target.is( "td.active" )){

			// This cell is not of any concern; simply
			// return out to prevent processing.
			return;

		}


		// Check to see if the ALT key was pressed. If it
		// was, then we are handling the caution toggle.
		// If not, then we are going to process a normal
		// click event.
		if (event.altKey){

			// Toggle the caution nature of this cell.
			this.toggleCaution( target );

		} else {

			// Check to see if the target was a bomb cell.
			if (target.is( ".bomb" )){

				// The user clicked on a bomb, which will end
				// the game. Reveal the whole board (end-game
				// check comes below).
				this.revealBoard();

			} else {

				// The target was not a bomb, so show it.
				this.revealCell( target );

			}

			// Check end game.
			this.checkEndGame();

		}
	};


	// I restart the game.
	MineSweeper.prototype.restart = function(){
		// Re-initialize the table.
		this.initTable();
	};


	// I reveal the entire board.
	MineSweeper.prototype.revealBoard = function(){
		// Remove the transient classes.
		this.cells
			.removeClass( "active" )
			.removeClass( "caution" )
		;

		// Add the bombed classes to the bombs.
		this.bombCells.addClass( "bombed" );

		// Set the cell contents.
		this.cells.each(
			function( index, cellNode ){
				var cell = $( this );

				// Check to see if this is a bomb cell.
				if (cell.is( ".bomb" )){

					// Show an *.
					cell.html( "*" );

				} else if (cell.data( "nearBombs" )){

					// Show the count.
					cell.html( cell.data( "nearBombs" ) );

				}
			}
		);
	};


	// I reveal the given cell.
	MineSweeper.prototype.revealCell = function( cell ){
		var self = this;

		// Remove the active nature of the cell.
		cell
			.removeClass( "active" )
			.removeClass( "caution" )
		;

		// Check to see if the current cell is near any
		// bombs. If it is, then we'll just show the
		// current cell and it's nearness. If not, then
		// we'll continue to show the surrounding cells.
		if (cell.data( "nearBombs" )){

			// Set the content of the cell.
			cell.html( cell.data( "nearBombs" ) );

		} else {

			// Make sure the cell has no markup.
			cell.html( "&nbsp;" );

			// This cell was not near any bombs. Therefore,
			// it is reasonable to assume the user would
			// quickly reveal all cells around it. As such,
			// we will do that for them.
			cell.data( "near" )
				.filter( ".active" )
					.each(
						function( index, cellNode ){
							self.revealCell( $( this ) );
						}
					)
			;

		}
	};


	// I toggle the cautionary nature and display of the
	// given cell.
	MineSweeper.prototype.toggleCaution = function( cell ){
		// Check to see if there is already a caution on it.
		if (cell.is( ".caution" )){

			// Remove caution class.
			cell.removeClass( "caution" );

			// Set appropriate markup.
			cell.html( "&nbsp;" );

		} else {

			// Add caution class.
			cell.addClass( "caution" );

			// Set appropriate markup.
			cell.html( "?" );

		}
	};


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// Store the mine sweeper class in the window scope so
	// that people can reach it ouside of this bubble.
	window.MineSweeper = MineSweeper;

})( jQuery );

The rest of the Javascript files are simply jQuery plugins.

This jQuery plugin selects a random value between the two given integers.

jquery.randrange.js

(function( $ ){

	// I get a random value between the min and max values
	// (inclusive). The min and max values are expected to be
	// interger values.
	$.randRange = function( minValue, maxValue ){
		// Get the range of our random values.
		var delta = (maxValue - minValue);

		// Select a random number for our range and truncate it.
		var randomValue = Math.floor( Math.random() * delta );

		// Get a random value by adding the random selection to
		// our min value.
		return( minValue + randomValue );
	};

})( jQuery );

This jQuery plugin repeats the given string a given number of times. I use this one to create the table markup. Creating the entire markup of the table is faster and more performant than creating individual table cells.

jquery.repeatstring.js

(function( $ ){

	// I repeat the given string the given number of times.
	$.repeatString = function( value, count ){
		return(
			(new Array( count + 1 )).join( value )
		);
	};

})( jQuery );

This jQuery plugin randomly filters a collection down to the given size.

jquery.randomfilter.js

(function( $ ){

	// I filter the given collection down to the given size by
	// randomly selecting the filtered elements.
	$.fn.randomFilter = function( size ){
		// Make sure that the size is not more than the size of
		// the collection.
		size = Math.min( size, this.size() );

		// Build up an array of possible index values.
		var indexes = new Array( this.size() );

		// Loop over the size of the collection and store the
		// current index value at the corresponding index of
		// the lookup array.
		for (var i = 0 ; i < this.size() ; i++){
			indexes[ i ] = i;
		}

		// Create an object to hold our random indexes.
		var randomIndexes = {};

		// Get the correct number of random index values.
		for (var i = 0 ; i < size ; i++){

			// Select a random index from the lookup array.
			// Remember that this array will be getting smaller
			// with each random selection.
			randomIndex = $.randRange( 0, indexes.length - 1 );

			// Add the randomly selected index to the index
			// collection.
			randomIndexes[ indexes[ randomIndex ] ] = true;

			// Remove the selected index from the available pool.
			indexes.splice( randomIndex, 1 );

		}

		// Return the filtered collection.
		return(
			this.filter(
				function( index ){
					return( index in randomIndexes );
				}
			)
		);
	};

})( jQuery );

This jQuery plugin selects the table cells around the given cell. This will return, at most, the three cells above, the three cells below, and the cell to the left and right of the given cell.

jquery.near.js

(function( $ ){

	// This is meant to be used on collections of TD elements.
	// It will get at most the 8 surrounding TD cells.
	$.fn.near = function(){
		var nearNodes = $( [] );
		var currentCell = $( this );
		var currentRow = currentCell.parent( "tr" );
		var tbody = currentRow.parent();
		var prevRow = currentRow.prev();
		var nextRow = currentRow.next();
		var currentCellIndex = currentRow.find( "td" ).index( currentCell );

		// Check to see if there is a previous row.
		if (prevRow.size()){

			// Grab the cell just above the current cell.
			var prevRowCell = prevRow.find( "td:eq(" + currentCellIndex + ")" );

			// Add the top 3 near cells to the collection that
			// we are going to return.
			nearNodes = nearNodes
				.add( prevRowCell.prev() )
				.add( prevRowCell )
				.add( prevRowCell.next() )
			;

		}

		// Add the left / right near cells to the collection
		// that we are going to return.
		nearNodes = nearNodes
			.add( currentCell.prev() )
			.add( currentCell.next() )
		;

		// Check to see if there is a next row.
		if (nextRow.size()){

			// Grab the cell just below the current cell.
			var nextRowCell = nextRow.find( "td:eq(" + currentCellIndex + ")" );

			// Add the bottom 3 near cells to the collection that
			// we are going to return.
			nearNodes = nearNodes
				.add( nextRowCell.prev() )
				.add( nextRowCell )
				.add( nextRowCell.next() )
			;

		}

		// Return the collection of near cells.
		return( nearNodes );
	}

})( jQuery );

I'm not going to go into any further explanation since there's a lot of code here. Mostly, it was just for funzies.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Great game, man.

May want to consider using event handlers for onContextMenu / onMouseDown (well, specifically right clicks) instead of using the Alt + Click approach, as it's not working for me in Linux.

When you use alt + click (in Gnome at least), it simply tries to move your window instead of allowing the click to pass through to the browser.

Just letting you know...

15,643 Comments

@Dan,

Yeah, it seems like the functional keys do something different on the different browsers. I originally was gonna go with CTRL+Click, but in FireFox, that selects the actual markup element for copy.

198 Comments

One thing to keep in mind is since you're using the "bomb" class on the element to mark a "bomb", one can easily use Firebug to find the bombs.

If doing in a real environment (such as for an online contest,) you'd want to avoid using the client-side to store the results. You'd really want to use AJAX to check with the server--to avoid people being able to "cheat."

Just something to keep in mind for anyone who might be using this as inspiration for something mission critical.

15,643 Comments

@Dan,

Point taken... but I have to admit that it's funny to see "Mine Sweeper" and "mission critical" in the same blog post ;)

15,643 Comments

@Jasonmcleod,

Noooooooo! << insert dramatic, slow motion scream >>

@Sreenath,

Hmmm, Sodoku is definitely a complicated game. I am not sure how I would go about creating the actual boards.

39 Comments

That is very cool. Only one feature is missing. (If you care to add it).

I heard that the original MineSweeper has logic built in to always leave the bottom right corner free, and if the first click is on a mine, it moves the mine to that corner. Thus preventing any one click losses. Though, I'm sure that may be a bit outside the scope of this example, but still could be a fun exercise.

1 Comments

Wow, I hadn't thought about it that way before. Good write up, very clearly written. Have you written previously about jQuery Powered Mine Sweeper Game? I'd love to read more.

111 Comments

I don't think the "moving the mine" is accurate. There have been many times where I've clicked once and got blown up (especially once you start getting a lot of mines on the screen).

39 Comments

..or are we completely missing the forest by getting stuck on the actual first click logic of minesweeper, and forgetting the awesomeness of the fact that Ben did a really snazzy job at emulating this game.

1 Comments

Tweeted your post!

I have downloaded the code and hope to explore it very soon. Especially I would like to see the code flow in jquery- I would learn a lot from your code!

Thank you.

1 Comments

this is a pritty cool game it is a game i like and i can play it in school. THANKS! for takeing the time to make this happen.<=

1 Comments

Just started learning jQuery and want to write a game myself. I love this version or minesweeper you have done.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel