Click here to Skip to main content
15,888,521 members
Articles / Web Development / HTML

HTML5 Sudoku Solver

Rate me:
Please Sign up or sign in to vote.
4.93/5 (37 votes)
15 Dec 2011CPOL7 min read 75.9K   2.9K   74  
An HTML5 JavaScript Sudoku solver.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<style type="text/css">
body
{
	font-size: 0.9em;
	font-family: verdana, arial, sans-serif;
	background-color: #FAEBD7;
}
button
{
	width: 90px;
	margin-top: 8px;
}
canvas
{
background-color: #fffff0;
padding-bottom:10px;
display:block;
}
#sudoku
{
margin-left:auto;
margin-right:auto;
width: 945px;
height:900px;
background-color: #fefefe;
padding:10px;
}
#banner
{
position: relative;
left: 0px;
top:0px;
padding: 10px;
}
#help
{
top:60px;
font-size: 10px;
width: 220px;
float: right;
}
#main
{
top:60px;
padding: 10px;
float:left;
}
#big
{
font-size: 32px;
color:#448844;
}
#small
{
display:block;
font-size: 10px;
}
#help li
{
padding-bottom: 6px;
}
h2
{
color: #444444;
text-align:center;
}
td.help li
{
padding-bottom: 6px;
font-size: 0.7em;
}
</style>
<script type="text/javascript" >

	// Please leave this notice intact.
	// Code developed by Ron de Jong 2011.
	// You are free to use this code for whatever purpose.

	// Converts global co-ords used in mouse events to relative to element
	function relMouseCoords(event) {
		var totalOffsetX = 0;
		var totalOffsetY = 0;
		var canvasX = 0;
		var canvasY = 0;
		var currentElement = this;

		do {
			totalOffsetX += currentElement.offsetLeft;
			totalOffsetY += currentElement.offsetTop;
		}
		while (currentElement = currentElement.offsetParent)

		canvasX = event.pageX - totalOffsetX;
		canvasY = event.pageY - totalOffsetY;

		return { x: canvasX, y: canvasY }
	}
	HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords;

	// Javascript doesn't have 'contains' so added here for later readability
	Array.prototype.contains = function (element) {
		for (var i = 0; i < this.length; i++) {
			if (this[i] == element) {
				return true;
			}
		}
		return false;
	}

	var SquareSize = 3;
	var BoardSize = SquareSize * SquareSize;

	function AllowedValues(n) {
		this._mask = n;
	}

	AllowedValues.prototype.getSingle = function () {
		// Count number of on bits from 1..9
		var single = 0;
		var count = 0;
		for (var i = 1; i <= BoardSize; i++)
			if ((this._mask & (1 << i)) != 0) {
				count++;
				single = i;
			}
		return count == 1 ? single : 0;
	};

	// Used when the answer is known at the Cell, level this sets the only allowed value to be that answer
	AllowedValues.prototype.setSingle = function (n) {
		this._mask = 1 << n;
	};

	AllowedValues.prototype.count = function () {
		// Count number of on bits from 1..9
		var count = 0;
		for (var i = 1; i <= BoardSize; i++)
			if ((this._mask & (1 << i)) != 0)
				count++;
		return count;
	};

	AllowedValues.prototype.isAllowed = function (n) {
		return n >= 1 && n <= BoardSize && ((this._mask & (1 << n)) != 0);
	};

	AllowedValues.prototype.removeValues = function (bm) {
		this._mask &= ~bm._mask;
	};

	AllowedValues.prototype.allowedValuesArray = function () {
		var ret = new Array();
		for (var i = 1; i <= BoardSize; i++)
			if (((1 << i) & this._mask) != 0)
				ret.push(i);
		return ret;
	};

	AllowedValues.prototype.clone = function () {
		return new AllowedValues(this._mask);
	};

	function Cell(value) {
		this._value = value; // 0 means unassigned
		this._allowed = new AllowedValues(0x3e); // all possible
		this._answer = 0; // no answer
		this._given = false;
	}

	Cell.prototype.clone = function (value) {
		var clone = new Cell();
		clone._value = this._value;
		clone._allowed = this._allowed.clone();
		clone._answer = this._answer;
		clone._given = this._given;
		return clone;
	};

	Cell.prototype.single = function (value) {
		this._value = value; // value user (or auto solve functions) has assigned as a possible answer
		this._allowed = new AllowedValues(0x3e); // the allowed values as a bit mask
		this._answer = 0; // calculated as the only possible correct value
	};

	Cell.prototype.valueMask = function () {
		return this._value == 0 ? 0 : 1 << this._value;
	};

	Cell.prototype.hasAnswer = function () {
		return this._answer != 0;
	};

	Cell.prototype.getAnswer = function () {
		return this._answer;
	};

	Cell.prototype.setAnswer = function (n) {
		if (n < 0 || n > 9)
			throw "Illegal value not in the range 1..9.";
		this._answer = n;
	};

	Cell.prototype.getValue = function () {
		return this._value;
	};

	Cell.prototype.setValue = function (n) {
		if (n < 0 || n > 9)
			throw "Illegal value not in the range 1..9.";
		if (n != 0 && !this._allowed.isAllowed(n))
			throw "Not allowed.";
		this._value = n;
		this._given = false;
	};

	Cell.prototype.setGiven = function (n) {
		if (n < 0 || n > 9)
			throw "Illegal value not in the range 1..9.";
		this._value = n;
		this._given = n != 0;
		this._answer = 0;
	};

	Cell.prototype.isGiven = function () {
		return this._given;
	};

	Cell.prototype.isAssigned = function () {
		return this._value != 0;
	};

	Cell.prototype.clear = function () {
		this._value = 0; // means unassigned
		this._allowed = new AllowedValues(0x3E); // all possible
		this._answer = 0;
		this._given = 0;
	};

	Cell.prototype.isAllowed = function (value) {
		return this._allowed.isAllowed(value);
	};

	Cell.prototype.setAllowed = function (value) {
		this._allowed = new AllowedValues(value);
	};

	Cell.prototype.getAllowedClone = function (value) {
		return this._allowed.clone();
	};

	var SibType = { "Row": 1, "Col": 2, "Square": 3 };

	function Location(row, col) {
		this.row = row;
		this.col = col;
	}

	Location.empty = new Location(-1, -1);

	Location.prototype.isEmpty = function () {
		return this.row < 0;
	};

	Location.prototype.modulo = function (n) {
		if (n < 0)
			return n + BoardSize;
		return n % BoardSize;
	};

	Location.prototype.left = function () {
		return new Location(this.row, this.modulo(this.col - 1));
	};

	Location.prototype.right = function () {
		return new Location(this.row, this.modulo(this.col + 1));
	};

	Location.prototype.up = function () {
		return new Location(this.modulo(this.row - 1), this.col);
	};

	Location.prototype.down = function () {
		return new Location(this.modulo(this.row + 1), this.col);
	};

	Location.prototype.toString = function () {
		return "Row=" + String(this.row) + "Col=" + String(this.col);
	};

	Location.prototype.getSquare = function () {
		return 3 * Math.floor(this.row / 3) + Math.floor(this.col / 3);
	};

	Location.prototype.equals = function (a) {
		return a.row == this.row && a.col == this.col;
	};

	Location.prototype.notEquals = function (a) {
		return a.row != this.row || a.col != this.col;
	};

	// Enumerator for locations of all cells
	Location.grid = function () {
		var locs = new Array();
		for (var i = 0; i < BoardSize; i++)
			for (var j = 0; j < BoardSize; j++)
				locs.push(new Location(i, j));
		return locs;
	};

	// Enumerator for locations of cell siblings in the same row
	Location.prototype.rowSibs = function () {
		var locs = new Array();
		for (var i = 0; i < BoardSize; i++)
			if (i != this.col)
				locs.push(new Location(this.row, i));
		return locs;
	};

	// Enumerator for locations of cell siblings in the same column
	Location.prototype.colSibs = function () {
		var locs = new Array();
		for (var i = 0; i < BoardSize; i++)
			if (i != this.row)
				locs.push(new Location(i, this.col));
		return locs;
	};

	// Enumerator for locations of cell siblings in the same square
	Location.prototype.squareSibs = function () {
		var locs = new Array();
		var baseRow = 3 * Math.floor(this.row / 3); // this is how to convert float to an "int" - Javascript doesn't have ints!
		var baseCol = 3 * Math.floor(this.col / 3);
		for (var i = 0; i < SquareSize; i++) {
			var r = baseRow + i;
			for (var j = 0; j < SquareSize; j++) {
				var c = baseCol + j;
				if (r != this.row || c != this.col)
					locs.push(new Location(r, c));
			}
		}
		return locs;
	};

	Location.prototype.getSibs = function (type) {
		switch (type) {
			case SibType.Row:
				return this.rowSibs();
			case SibType.Col:
				return this.colSibs();
			case SibType.Square:
				return this.squareSibs();
		}
	};

	function Board() {

		function MultiDimArray(rows, cols) {
			var a = new Array(rows);
			for (var i = 0; i < rows; i++) {
				a[i] = new Array(cols);
				for (var j = 0; j < cols; j++)
					a[i][j] = new Cell();
			}
			return a;
		}

		this._digits = MultiDimArray(BoardSize, BoardSize);
		this._isSolved = false;
		this._isValid = false;

	}

	Board.prototype.clone = function () {
		var clone = new Board();
		clone._isSolved = this._isSolved;
		clone._isValid = this._isValid;
		clone._digits = new Array(BoardSize);
		for (var i = 0; i < BoardSize; i++) {
			clone._digits[i] = new Array(BoardSize);
			for (var j = 0; j < BoardSize; j++)
				clone._digits[i][j] = this._digits[i][j].clone();
		}
		return clone;
	};

	Board.prototype.copyTo = function (target) {
		target._isSolved = this._isSolved;
		target._isValid = this._isValid;
		for (var i = 0; i < BoardSize; i++)
			for (var j = 0; j < BoardSize; j++)
				target._digits[i][j] = this._digits[i][j].clone();
	};

	Board.prototype.getCell = function (loc) {
		return this._digits[loc.row][loc.col];
	};

	Board.prototype.setCell = function (loc, value) {
		this._digits[loc.row][loc.col] = value;
	};

	Board.prototype.clear = function () {
		for (var i = 0; i < BoardSize; i++)
			for (var j = 0; j < BoardSize; j++)
				this._digits[i][j].clear();
		this.updateAllowed();
	};

	Board.prototype.reset = function () {// return Baord to only the givens
		for (var i = 0; i < BoardSize; i++)
			for (var j = 0; j < BoardSize; j++) {
				var cell = this._digits[i][j];
				if (!cell.isGiven())
					cell.clear();
			}
		this.updateAllowed();
	};

	Board.prototype.checkIsValidSibs = function (loc, digit, locs) {
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			var cell = this._digits[loc.row][loc.col];
			if (cell.getAnswer() == digit)
				return false;
		}
		return true;
	};

	Board.prototype.checkIsValid = function (loc, digit) {
		// Checks if the digit can go in that location by checking it doesn't
		// exist in either the row, col or square siblings
		if (!this.checkIsValidSibs(loc, digit, loc.colSibs()))
			return false;
		if (!this.checkIsValidSibs(loc, digit, loc.rowSibs()))
			return false;
		if (!this.checkIsValidSibs(loc, digit, loc.squareSibs()))
			return false;

		return true;
	};

	Board.prototype.acceptPossibles = function () {
		var more = false;
		var locs = Location.grid();
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			var cell = this._digits[loc.row][loc.col];
			if (!cell.isAssigned() && cell.hasAnswer() && this.checkIsValid(loc, cell.getAnswer())) {
				cell.setValue(cell.getAnswer()); // if unassigned and has the answer then assign the answer
				more = true;
			}
		}
		return more;
	};

	Board.prototype.checkForHiddenSingles = function (loc, st) {
		// Check each cell - if not assigned and has no answer then check its siblings
		// get all its allowed then remove all the allowed
		var cell = this.getCell(loc);
		if (!cell.isAssigned() && !cell.hasAnswer()) {
			var allowed = cell.getAllowedClone(); // copy of bit mask of allowed values for this cell
			var locs = loc.getSibs(st);
			for (var i = 0; i < locs.length; i++) {
				var sib = locs[i];
				var sibCell = this.getCell(sib);
				if (!sibCell.isAssigned())
					allowed.removeValues(sibCell.getAllowedClone()); // remove allowed values from siblings
			}
			var answer = allowed.getSingle(); // if there is only one allowed value left (i.e. this cell is the only one amonsgt its sibs with this allowed value)
			// then apply it as the answer. Note getSingle will return 0 (i.e. no anser) if the number of allowed values is not exactly one
			if (answer != 0) {
				cell.setAnswer(answer);
				return true; // no need to check others sibling collections
			}
		}
		return false;
	};

	Board.prototype.findCellWithFewestChoices = function () {
		var minLocation = Location.empty;
		var minCount = 9;
		var locs = Location.grid();
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			var cell = this.getCell(loc);
			if (!cell.isAssigned()) {
				var count = cell.getAllowedClone().count();
				if (count < minCount) {
					minLocation = loc;
					minCount = count;
				}
			}
		}
		return minLocation;
	};

	Board.prototype.updateAllowed = function () {
		// Called whenever the user sets a value or via auto solve
		// Updates the allowed values for each cell based on existing digits
		// entered in a cell's row, col or square
		var cols = new Array(BoardSize);
		var rows = new Array(BoardSize);
		var squares = new Array(BoardSize);

		// First aggregate assigned values to rows, cols, squares
		var locs = Location.grid();
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			// Disallow for all cells in this row
			var contains = this.getCell(loc).valueMask();
			rows[loc.row] |= contains;
			cols[loc.col] |= contains;
			squares[loc.getSquare()] |= contains;
		}

		// For each cell, aggregate the values already set in that row, col and square.
		// Since the aggregate is a bitmask, the bitwise inverse of that is therefore the allowed values.
		this._isValid = true;
		this._isSolved = true;
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			// Set allowed values
			var contains = rows[loc.row] | cols[loc.col] | squares[loc.getSquare()];
			var cell = this.getCell(loc);
			cell.setAllowed(~contains); // set allowed values to what values are not already set in this row, col or square
			cell.setAnswer(0); //clear any previous answers
			// As an extra step look for "naked singles", i.e. cells that have only one allowed value, and use
			// that to set the answer (note this is different from the "value" as this can only be assigned
			// by the user or any auto solve functions like "accept singles"
			if (!cell.isAssigned()) {
				this._isSolved = false;
				var mask = new AllowedValues(~contains);
				var count = mask.count();
				if (count == 0)
					this._isValid = false;
				else if (count == 1)
					cell.setAnswer(mask.getSingle());
			}
		}

		// Step 2: Look for "hidden singles".
		// For each row, col, square, count number of times each digit appears.
		// If any appear once then set that as the answer for that cell.
		// Count in rows
		for (var i = 0; i < locs.length; i++) {
			var loc = locs[i];
			if (!this.checkForHiddenSingles(loc, SibType.Row))// first check row sibs for a hiddne single
				if (!this.checkForHiddenSingles(loc, SibType.Col))// then check cols
					this.checkForHiddenSingles(loc, SibType.Square); // then check square
		}

		// TO DO: Add code here to detect naked/hidden doubles/triples/quads
	};

	Board.prototype.trySolve = function (loc, value) {// empty Location allowed
		if (!loc.isEmpty())// assign a value to a location if provided
		{
			var cell = this.getCell(loc);
			if (!cell.isAllowed(value))
				throw "Internal error.";
			cell.setValue(value);
		}

		do {
			this.updateAllowed();
			if (!this._isValid)
				return false;
		} while (this.acceptPossibles()); // keep doing deterministic answers

		if (this._isSolved)
			return true;

		if (!this._isValid)
			return false;

		// No deterministic solutions, find cell with the fewest choices and try each one in turn
		// until success. 
		var locChoice = this.findCellWithFewestChoices();
		if (locChoice.isEmpty())
			return false;

		var cell = this.getCell(locChoice);
		var allowedValues = cell._allowed.allowedValuesArray();
		for (var i = 0; i < allowedValues.length; i++) {
			var val = allowedValues[i];
			var board = this.clone();
			if (board.trySolve(locChoice, val)) {
				board.copyTo(this);
				return true;
			}
		}

		return false;
	};

	Board.prototype.toString = function () {
		var text = "";
		for (var row = 0; row < BoardSize; row++)
			for (var col = 0; col < BoardSize; col++) {
				var val = this._digits[row][col].getValue();
				text += val == 0 ? "." : String(val);
			}
		return text;
	};

	Board.prototype.setString = function (value) {
		// Assumes all input is digits 1..9 or ./space
		if (value.length != (BoardSize * BoardSize))
			return false; //Input string is not of length 81
		var n = 0;
		for (var row = 0; row < BoardSize; row++)
			for (var col = 0; col < BoardSize; col++) {
				var ch = parseInt(value.charAt(n++)); // converts '0' to 0 etc
				var cell = this._digits[row][col];
				cell.setGiven(!isNaN(ch) ? ch : 0);
			}
		this.updateAllowed();
		return true;
	};

</script>


</head>
<body>
<div id='sudoku'>
<div id='banner'>
<span id='big'>Free Sudoku Solver!</span>
<span id='small'>Solves even the most difficult puzzles and provides optional hints as a training aid.</span>
</div>

<div id='main'>
<table>
<tr>
<td>
	<canvas id='canvas2' width='541' height='55' title="Click here to set the currently selected digit">Canvas is not supported by this browser.</canvas>
	<canvas id='canvas1' width='541' height='541'>Canvas is not supported by this browser.</canvas>
	<input id='tbSerial' style='width: 538px; background-color: #faf0e6; margin-top: 10px;'/>
	<br/><br/>
	<span id="extraInfo" style='font-size:0.7em; color: #696969;'></span><br/>
</td>
<td style="width: 135px">
	<button onclick="clearGame()" title='Clears all cells including givens. Use this initially when entering a new problem.' >Clear</button><br/>
	<button onclick="loadText()" title='Loads the digits from the textbox below the board in order left to right, top to bottom. Click this after entering a problem to convert them to givens'  >Load</button><br/>
	<button id="btnReset" onclick="reset()" title='Clears all user entered cells. The givens are NOT cleared.'>Reset</button><br/>
	<button id="btnAccept" onclick="acceptPossibles()" title='Makes all singles into values' >Accept</button><br/>
	<button id="btnHint" onclick="hint()" title='Enters the answer to the currently selected cell' >Hint</button><br/>
	<button id="btnUndo" onclick="undo()" title='Undoes the last user action except load, clear and reset'>Undo</button><br/>
	<button id="btnSolve" onclick="solve()" title='If possible, it will solve the problem. The amount of time taken to solve is displayed below.' >Solve</button><br/><br/>
	Show allowed: <input id='chbAllowed' type="checkbox" checked='yes' title='Untick this to hide allowed values'/><br/>
	Show singles: <input id='chbShowSingles' type="checkbox" checked='yes' title='Untick this to hide naked and hidden singles - these are shown in red'/><br/>
	<span id="message" style="width: 130; color: Red; "></span><br/>	

</td>
<td class="help">
<h2>Instructions</h2>
<ul >
<li>Hit the Clear button and enter the Sudoku 'givens' manually. You can do this by
clicking on a cell, or use the keyboard arrow keys, then type the digit from '1' to '9'.
The Delete key or the '0' key will clear a cell. You can also enter a digit or clear it by clicking in the row
of digits above the board.</li>
<li>When you are finished hit the Load button. The givens will now appear in blue.</li>
<li>Hitting the Reset button will now undo all changes and take you back to the original givens.</li>
<li>The allowed digits are shown as a small light gray digit in the background of each cell. You can turn this off by unticking the 'Show allowed' option.</li>
<li>If an allowed digit is the only possible digit (these are called singles) then it is highlighted in red.
You can turn this off by unticking the 'Show singles' option.</li>
<li>Hitting the Accept button will convert singles to actual entered digits.</li>
<li>The Hint button will fill in the current selected cell with the correct digit.</li>
<li>Clicking in a cell with a digit entered will highlight the same digit anywhere alse in red.</li>
<li>Solve of course will solve the entire puzzle.</li>
<li>Undo will undo the last user action such as entering a digit or htting the Accept button.</li>
</ul>
</td>
</tr>
</table>

<script type="text/javascript" >
	var CellSize = 60;
	var SubCellSize = 18;

	var canvas1 = document.getElementById("canvas1");
	var canvas2 = document.getElementById("canvas2");
	var chbAllowed = document.getElementById("chbAllowed");
	var chbShowSingles = document.getElementById("chbShowSingles");
	var tbSerial = document.getElementById("tbSerial");
	var extraInfo = document.getElementById("extraInfo");

	var board1 = new Board();
	var selectRow = 0;
	var selectCol = 0;
	var showAllowed = true;
	var showSingles = true;
	var undoStack = Array();

	function undo() {
		var tos = undoStack.pop();
		if (tos) {
			board1 = tos;
			updateUI();
		}
	}

	function clearUndo() {
		undoStack= Array();
	}

	function pushBoard() {
		undoStack.push(board1.clone());
	}

	function checkStatus() {
		extraInfo.innerHTML = "";
		if (!board1._isValid)
			message.innerHTML = "*Invalid*";
		else if (board1._isSolved)
			message.innerHTML = "*Solved*";
		else
			message.innerHTML = "";
	}

	function drawGrid() {
		// Only ever called once!
		var context = canvas1.getContext('2d'); 
		context.strokeStyle = '#808080';
		for (var i = 0; i <= BoardSize; i++) {
			context.beginPath();
			var thick = i % 3 == 0;
			// Draw vertical lines
			context.lineWidth = thick ? 2 : 1;
			context.moveTo(i * CellSize + 0.5, 0.5);
			context.lineTo(i * CellSize + 0.5, BoardSize * CellSize + 0.5);

			// Draw horizontal lines
			context.moveTo(0.5, i * CellSize + 0.5);
			context.lineTo(BoardSize * CellSize + 0.5, i * CellSize + 0.5);
			context.stroke();
		}
	}

	function drawCells() {
		var context = canvas1.getContext('2d'); 

		context.font = "12pt Calibri"; // small text
		context.textAlign = "center";
		context.textBaseline = "middle";
		var normalColor = "#aaaaaa";
		var singleColor = "#ff143c";

		// Draw background for selected cell
		for (var row = 0; row < BoardSize; row++)
			for (var col = 0; col < BoardSize; col++) {

				// Draw background of selected cell
				if (row == selectRow && col == selectCol) {
					var margin = 2;
					context.beginPath();
					context.rect(col * CellSize + margin + 0.5, row * CellSize + margin + 0.5, CellSize - 2 * margin, CellSize - 2 * margin);
					context.fillStyle = "#ffe4e1";
					context.fill()
				}
			}
		context.fillStyle = "#999999"; // text color - light

		// Draw allowed values
		if (showAllowed)
			for (var row = 0; row < BoardSize; row++)
				for (var col = 0; col < BoardSize; col++) {
					var cell = board1.getCell(new Location(row, col));
					if (!cell.isAssigned()) {
						var allowedValues = cell._allowed.allowedValuesArray();
						for (var i = 0; i < allowedValues.length; i++) {
							var val = allowedValues[i];
							var x = (col + 0.5) * CellSize; // center of cell for textAlign center, textBaseline middle
							var y = (row + 0.5) * CellSize;
							var subRow = Math.floor((val - 1) / 3) - 1;
							var subCol = Math.floor((val - 1) % 3) - 1;
							x += subCol * SubCellSize;
							y += subRow * SubCellSize;
							var hiddenSingle = allowedValues.length != 1 && val == cell.getAnswer(); // naked single would have only one allowed value
							context.fillStyle = normalColor; // show hidden single in purple
							if (showSingles && val == cell.getAnswer())
								context.fillStyle = singleColor; // show hidden single in purple
							context.fillText(val, x, y);
						}
					}
				}

		// New if a digit is selected then make all cells with the same digit foreground red
		var selectCell = board1.getCell(new Location(selectRow, selectCol));
		var selectValue = selectCell.getValue();

		// Draw values last
		context.font = "32pt Calibri";
		context.textAlign = "center";
		context.textBaseline = "middle";
		var normalForeColor = "#191929";
		var sameDigitForeColor = "#F91919";
		context.fillStyle = normalForeColor; // text color - dark
		for (var row = 0; row < BoardSize; row++)
			for (var col = 0; col < BoardSize; col++) {
				var cell = board1.getCell(new Location(row, col));
				var x = (col + 0.5) * CellSize; // center of cell for textAlign center, textBaseline middle
				var y = (row + 0.5) * CellSize;
				var sameDigit = cell.getValue() == selectValue && selectValue != 0;
				// Draw value
				var value = cell.getValue();
				if (value != 0) {
					context.fillStyle = cell.isGiven() ? "#2200aa" : "#696969"; // show "givens" in a darker color
					if (sameDigit)// then override
						context.fillStyle = sameDigitForeColor; // text color - dark
					context.fillText(value, x, y);
				}
			}
	}

	function drawCanvas() {
		canvas1.width = canvas1.width;
		drawGrid();
		drawCells();
	}

	function updateUI() {
		drawCanvas();
		checkStatus();
		tbSerial.value = board1.toString();
	}

	function readOptions() {
		showAllowed = chbAllowed.checked;
		showSingles = chbShowSingles.checked;
		drawCanvas();
	}

	chbAllowed.onclick = readOptions;
	chbShowSingles.onclick = readOptions;

	function selectCell(row, col) {
		selectRow = row;
		selectCol = col;
		drawCanvas();
	}

	function moveSelection(row, col) {
		selectRow += row;
		selectCol += col;
		if (selectRow < 0)
			selectRow = 8;
		else if (selectRow > 8)
			selectRow = 0;
		if (selectCol < 0)
			selectCol = 8;
		else if (selectCol > 8)
			selectCol = 0;
		drawCanvas();
	}

	function setDigitInCell(digit) {
		var cell = board1.getCell(new Location(selectRow, selectCol));
		message.innerHTML = "";
		if (cell.isGiven())
			return;
		if (digit != 0 && !cell.isAllowed(digit)) {
			message.innerHTML = "Digit not allowed";
			return;
		}
		pushBoard();
		cell.setValue(digit);
		board1.updateAllowed();
		updateUI();
	}

	canvas1.onmousedown = function canvasMouseDown(ev) {
		var x = ev.pageX - this.offsetLeft;
		var y = ev.pageY - this.offsetTop;
		var coords = this.relMouseCoords(ev);
		selectCell(Math.floor(coords.y / CellSize), Math.floor(coords.x / CellSize));
	}

	document.onkeydown = function (ev) {
		switch (ev.keyCode) {
			case 37: // left arrow
				moveSelection(0, -1);
				break;
			case 38: // up arrow
				moveSelection(-1, 0);
				break;
			case 39: // right arrow
				moveSelection(0, 1);
				break;
			case 40: // down arrow
				moveSelection(1, 0);
				break;
			default:
				var key = Number(ev.keyCode);
				var digit = key >= 96 ? key - 96 : key - 48;// handle keypad digits as well
				if (digit >= 0 && digit <= 9)
					setDigitInCell(digit);
				break;
		}
	}

	function loadText() {
		var ret = board1.setString(tbSerial.value);
		updateUI();
		if (!ret)
			message.innerHTML = "String is not of length 81";
	}

	function clearGame() {
		clearUndo();
		board1.clear();
		updateUI();
	}

	function acceptPossibles() {
		pushBoard();
		board1.acceptPossibles();
		board1.updateAllowed();
		updateUI();
	}

	function hint() {
		// First check if we had calculated a solution, if not do so now
		solution = board1.clone();
		if (solution.trySolve(Location.empty, 0)) {
			// There is a solution to the board from its current state
			var cell = solution.getCell(new Location(selectRow, selectCol));
			if (!cell.isGiven())
				setDigitInCell(cell.getValue());
		}
	}

	function reset() {
		clearUndo();
		board1.reset();
		updateUI();
	}

	function solve() {
		pushBoard();
		var n = new Date();           // Grab new copy of date
		var s = n.getTime();          // Grab current millisecond #
		board1.trySolve(Location.empty, 0);
		var diff = new Date().getTime() - s;
		updateUI();
		extraInfo.innerHTML = "Solve took " + String(diff) + " milliseconds";
	}

	//http://magictour.free.fr/sudoku.htm for list of hard Sudoku puzzles
	// http://www.sudokuwiki.org/sudoku.htm good on-line solver accepting serial format
	//board1.setString("7.8...3.....2.1...5.........4.....263...8.......1...9..9.6....4....7.5..........."); //very hard
	board1.setString("7.8...3.....2.1...5..7..2...4.....263.948...7...1...9..9.6....4....7.5....5......"); // medium
	updateUI();
	var digCellSize = 54;

	// New stuff - draw a digit selector in canvas above board
	function initDigitSource() {
		// Only ever called once!
		var context = canvas2.getContext('2d');
		context.strokeStyle = '#808080';
		var SourceSize = BoardSize + 1;
		for (var i = 0; i <= SourceSize; i++) {
			context.beginPath();
			// Draw vertical lines
			context.lineWidth = 1;
			context.moveTo(i * digCellSize + 0.5, 0.5);
			context.lineTo(i * digCellSize + 0.5, digCellSize + 0.5);
			context.stroke();
		}
		for (var i = 0; i <= 1; i++) {
			context.beginPath();
			// Draw horizontal lines
			context.lineWidth = 1;
			context.moveTo(0.5, i * digCellSize + 0.5);
			context.lineTo(SourceSize * digCellSize + 0.5, i * digCellSize + 0.5);
			context.stroke();
		}
		context.font = "24pt Calibri";
		context.textAlign = "center";
		context.textBaseline = "middle";
		var normalForeColor = "#708090";
		context.fillStyle = normalForeColor; // text color - dark
		for (var col = 0; col < SourceSize; col++) {
			var x = (col + 0.5) * digCellSize; // center of cell for textAlign center, textBaseline middle
			var y = 0.5 * digCellSize;
			var value = col < 9 ? col + 1 : "Del";
			context.fillStyle = normalForeColor; // show "givens" in a darker color
			context.fillText(value, x, y);
		}

	}
	initDigitSource();

	canvas2.onmousedown = function canvasMouseDown(ev) {
		var x = ev.pageX - this.offsetLeft;
		var y = ev.pageY - this.offsetTop;
		var coords = this.relMouseCoords(ev);
		var dig = Math.floor(coords.x / digCellSize) + 1;
		if (dig == 10)
			dig = 0;
		setDigitInCell(dig);
	}


</script>
</div>

</div>

</body>
</html>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Product Manager
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions