Saturday, September 13, 2008

Javascript Hangman

Hangman is a popular pencil and paper game. One player chooses a word and draws dashes to represent the length of the word.

The other player guesses letters. If the letter is in the word the dash is replaced with the letter. If the guessed letter is not in the word then a part of the hanging body is drawn (head, body, left leg, right leg, left arm, right arm) once the body is completely drawn or the entire word has been guessed the game is over.

I decided to re-create this game using Javascript and the html canvas object. Javascript is used to handle the game while the canvas is used to draw the hangman character when the guesses are incorrect.

To start lets define a simple html page that includes our canvas and a couple of spans to hold the word and messages for the user.

<html>
<body onload="draw();" onkeyup="handleKeyUp(event);">
<table>
<tr>
<td colspan=2>
<span id='alreadyGuessed' style="visibility:hidden">Letter already guessed</span>
<span id='strike' style="visibility:hidden">Strike!</span>
</td>
</tr>
<tr>
<td>
<canvas id='hangman' width=150 height=150>
</td>
<td>
<span id='word'></span>
</td>
</tr>
</table>
</body>
</html>


When the page is loaded we call 'draw()' and we setup a keyboard listener to listen to keystrokes. We've defined a few simple span's that will be messages to the user, a canvas to draw the victim and a span to hold the word that the user will be guessing.

The game expects a character from A-Z so our keyboard listener should filter out all other values here is what our listener looks like:

function handleKeyUp(evt) {
var e = evt ? evt : event;
if (e.keyCode >= 65 && e.keyCode < (65+26) ) {
guess(String.fromCharCode(e.keyCode));
}
draw();
}



The keyCode value is the ASCII representation we know that the ASCII characters are in sequence with capital 'A' being 65 and capital 'Z' being 65+26 characters away. If the user enters a valid character we enter our 'guess' function else we do nothing. After guessing our draw() function is called to redraw the screen.

The draw function as you have now seen is called when the game is first loaded and after each keypress lets see what it does.

function draw() {
var str='';
for(i=0;i<word.length;++i) {
if (foundLetters[i]) {
str += word[i];
} else {
str += '_';
}
str += ' ';
}
document.getElementById('word').innerHTML = str;
}



The draw function looks at our internal data structures which will explore later and determines if the user has guessed a letter yet or not. If the letter has been guessed the letter is shown, else a '_' is shown instead.


So now to the heart of the game, the guess function.

function guess(key) {
hideMessages();
if (guessedLetters[key]) {
showAlreadyGuessed();
return;
}
guessedLetters[key] = true;
var found = false;
for(i=0;i<word.length;++i) {
if (word[i] == key) {
foundLetters[i] = true;
found=true;
}
}
if(!found) showStrike();
return found;

}



The guess function iterates over the chosen word and determines if the character in the word has been guessed. If it has it marks foundLetters[index] where index is the index of the character that has been revealed. We saw in the 'draw' function how this is used to change the display.

The last thing to note is how we use the canvas element to draw a very crude hangman as the user guesses incorrectly.


function drawHangMan(numberOfStrikes) {
var canvas = document.getElementById('hangman');
if(canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.save() ;
ctx.translate(45,45);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
if(numberOfStrikes >= 1) ctx.fillRect(0,0,10,10); //head
if(numberOfStrikes >= 2) ctx.fillRect(-5,10,20,20); //body
if(numberOfStrikes >= 3) ctx.fillRect(-20,10,20,5); //left arm
if(numberOfStrikes >= 4) ctx.fillRect(0,10,30,5); // right arm
if(numberOfStrikes >= 5) ctx.fillRect(-5,30,5,10); // left leg
if(numberOfStrikes >= 6) ctx.fillRect(10,30,5,10); // right leg

ctx.fill();
ctx.restore();
}
}



This method is pretty self explanatory. Based on the number of strikes more of the hangman's body will be revealed.

Here is the entire code listing, let me know if you have any questions. Enjoy...


<html>
<head>
<script type="application/x-javascript">

var words = new Array();
words[0] = "Ford";
words[1] = "Chevy";
words[2] = "Mazda";
words[3] = "Volvo";
words[4] = "Javascript";
words[5] = "Google";
words[6] = "Microsoft";
words[7] = "Nvidia";


var rand_no = Math.random();
rand_no = Math.ceil(rand_no * words.length)-1;


word = words[rand_no].toUpperCase();
var foundLetters = new Array();


var guessedLetters = new Array();
function hideMessages() {
document.getElementById("alreadyGuessed").style.visibility='hidden';
document.getElementById("strike").style.visibility='hidden';
}
function showAlreadyGuessed() {
document.getElementById("alreadyGuessed").style.visibility='';
}

function drawHangMan(numberOfStrikes) {
var canvas = document.getElementById('hangman');
if(canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.save() ;
ctx.translate(45,45);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
if(numberOfStrikes >= 1) ctx.fillRect(0,0,10,10); //head
if(numberOfStrikes >= 2) ctx.fillRect(-5,10,20,20); //body
if(numberOfStrikes >= 3) ctx.fillRect(-20,10,20,5); //left arm
if(numberOfStrikes >= 4) ctx.fillRect(0,10,30,5); // right arm
if(numberOfStrikes >= 5) ctx.fillRect(-5,30,5,10); // left leg
if(numberOfStrikes >= 6) ctx.fillRect(10,30,5,10); // right leg

ctx.fill();
ctx.restore();
}
}

var numberOfStrikes=0;
function showStrike() {
numberOfStrikes++;
drawHangMan(numberOfStrikes);
document.getElementById("strike").style.visibility='';
}


function guess(key) {
hideMessages();
if (guessedLetters[key]) {
showAlreadyGuessed();
return;
}
guessedLetters[key] = true;
var found = false;
for(i=0;i<word.length;++i) {
if (word[i] == key) {
foundLetters[i] = true;
found=true;
}
}
if(!found) showStrike();
return found;

}
function draw() {
var str='';
for(i=0;i<word.length;++i) {
if (foundLetters[i]) {
str += word[i];
} else {
str += '_';
}
str += ' ';
}
document.getElementById('word').innerHTML = str;
}
function handleKeyUp(evt) {
var e = evt ? evt : event;
if (e.keyCode >= 65 && e.keyCode < (65+26) ) {
guess(String.fromCharCode(e.keyCode));
}
else {
//alert("You entered " + e.keyCode);
}
draw();
}


</script>
</head>
<body onload="draw();" onkeyup="handleKeyUp(event);">
<table>
<tr>
<td colspan=2>
<span id='alreadyGuessed' style="visibility:hidden">Letter already guessed</span>
<span id='strike' style="visibility:hidden">Strike!</span>
</td>
</tr>
<tr>
<td>
<canvas id='hangman' width=150 height=150>
</td>
<td>
<span id='word'></span>
</td>
</tr>
</table>
</body>
</html>

No comments: