I was going through my portfolio recently and realized that I have an entry for my Press Your Luck game but I’ve only described how it works, never taken a deep dive into the code.
The current version (if you can call something no longer in use “current”) runs entirely on the client side. There is one HTML file (with inline jQuery), one CSS file, an XML file with configuration values, and a handful of images and sounds.
Some parts of these files have been modified for display purposes. None of the changes impact functionality.
We’ll start with the config file…
<?xml version="1.0" encoding="utf-8" ?> | |
<images> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_choices_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_choices_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_giftcard_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_giftcard_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_lunch_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_lunch_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_mug_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_mug_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_parking_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_parking_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_swagbag_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_swagbag_full.jpg</large> | |
<type>prize</type> | |
</image> | |
<image> | |
<thumb>http://pressyourluck.info/images/prize_whammy1_thumb.jpg</thumb> | |
<large>http://pressyourluck.info/images/prize_whammy1_full.jpg</large> | |
<type>whammy</type> | |
</image> | |
</images> |
We’re defining a set of images, the tiles that make up the game board. Each has a thumbnail (the image displayed on the standard game board) and a full-size image (the one displayed in the center when that tile is selected by the player) and we define their URLs here. We also define whether this is a prize image or a whammy, which determines what sound plays when that tile is selected.
Fairly simple. Now we move on to the CSS…
body { | |
margin: 0; | |
padding: 0; | |
background-image: url(./images/bg_page.jpg); | |
background-repeat: no-repeat; | |
background-position: center; | |
background-color: #1a1a1a; | |
} | |
div#board { | |
position: relative; | |
margin: 30px auto 30px auto; | |
padding: 0; | |
width: 1002px; | |
height: 800px; | |
border: 5px solid #000; | |
border-radius: 12px; | |
background-color: #000; | |
} | |
div#board div { | |
width: 167px; | |
height: 160px; | |
} | |
div#board div img { | |
position: relative; | |
width: 150px; | |
height: 140px; | |
margin: 8px 6px 8px 7px; | |
padding: 0; | |
border: 2px solid #000; | |
} | |
div#board div.inactive { | |
background-image: url(./images/bg_item_inactive.jpg); | |
opacity: 0.35; | |
} | |
div#board div.active { | |
background-color: yellow; | |
background-image: url(./images/bg_item_active.gif); | |
opacity: 1; | |
} | |
div#board div#item0 { | |
position: absolute; | |
left: 0; | |
top: 0; | |
} | |
div#board div#item1 { | |
position: absolute; | |
left: 167px; | |
top: 0; | |
} | |
div#board div#item2 { | |
position: absolute; | |
left: 334px; | |
top: 0; | |
} | |
div#board div#item3 { | |
position: absolute; | |
left: 501px; | |
top: 0; | |
} | |
div#board div#item4 { | |
position: absolute; | |
left: 668px; | |
top: 0; | |
} | |
div#board div#item5 { | |
position: absolute; | |
left: 835px; | |
top: 0; | |
} | |
div#board div#item6 { | |
position: absolute; | |
left: 0; | |
top: 160px; | |
} | |
div#board div#item7 { | |
position: absolute; | |
left: 835px; | |
top: 160px; | |
} | |
div#board div#item8 { | |
position: absolute; | |
left: 0; | |
top: 320px; | |
} | |
div#board div#item9 { | |
position: absolute; | |
left: 835px; | |
top: 320px; | |
} | |
div#board div#item10 { | |
position: absolute; | |
left: 0; | |
top: 480px; | |
} | |
div#board div#item11 { | |
position: absolute; | |
left: 835px; | |
top: 480px; | |
} | |
div#board div#item12 { | |
position: absolute; | |
left: 0; | |
top: 640px; | |
} | |
div#board div#item13 { | |
position: absolute; | |
left: 167px; | |
top: 640px; | |
} | |
div#board div#item14 { | |
position: absolute; | |
left: 334px; | |
top: 640px; | |
} | |
div#board div#item15 { | |
position: absolute; | |
left: 501px; | |
top: 640px; | |
} | |
div#board div#item16 { | |
position: absolute; | |
left: 668px; | |
top: 640px; | |
} | |
div#board div#item17 { | |
position: absolute; | |
left: 835px; | |
top: 640px; | |
} | |
div#board div#item_middle { | |
position: absolute; | |
left: 167px; | |
top: 160px; | |
width: 668px; | |
height: 520px; | |
} | |
div#board div#item_middle img { | |
position: relative; | |
width: 648px; | |
height: 460px; | |
margin: 10px; | |
padding: 0; | |
border: 0; | |
} | |
div#board div#item_middle img.prize { | |
width: 514px; | |
height: 480px; | |
margin: 0 77px 0 77px; | |
} | |
div#sound { | |
width: 0; | |
height: 0; | |
overflow: hidden; | |
} |
More pretty simple stuff. The page has a background. There’s a div that contains all the game elements. Those are positioned as needed. The tiles have a background image for their active and inactive states. The sound controls are hidden.
Now we get to the fun, the HTML and jQuery. Here’s the full page, we’ll break down the important parts afterwards…
<!DOCTYPE HTML> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | |
<title>Bravo Press Your Luck</title> | |
<style type="text/css" media="screen"> | |
@import "./styles.css"; | |
</style> | |
<link href="./jquery-ui/css/custom-theme/jquery-ui-1.8.16.custom.css" rel="stylesheet" type="text/css" /> | |
<script src="./jquery-ui/js/jquery-1.6.2.min.js" type="text/javascript"></script> | |
<script src="./jquery-ui/js/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script> | |
</head> | |
<body> | |
<div id="board"> | |
<div id="item0" class="inactive"> | |
<img src="./images/space.gif" id="image0" /> | |
</div> | |
<div id="item1" class="inactive"> | |
<img src="./images/space.gif" id="image1" /> | |
</div> | |
<div id="item2" class="inactive"> | |
<img src="./images/space.gif" id="image2" /> | |
</div> | |
<div id="item3" class="inactive"> | |
<img src="./images/space.gif" id="image3" /> | |
</div> | |
<div id="item4" class="inactive"> | |
<img src="./images/space.gif" id="image4" /> | |
</div> | |
<div id="item5" class="inactive"> | |
<img src="./images/space.gif" id="image5" /> | |
</div> | |
<div id="item6" class="inactive"> | |
<img src="./images/space.gif" id="image6" /> | |
</div> | |
<div id="item7" class="inactive"> | |
<img src="./images/space.gif" id="image7" /> | |
</div> | |
<div id="item8" class="inactive"> | |
<img src="./images/space.gif" id="image8" /> | |
</div> | |
<div id="item9" class="inactive"> | |
<img src="./images/space.gif" id="image9" /> | |
</div> | |
<div id="item10" class="inactive"> | |
<img src="./images/space.gif" id="image10" /> | |
</div> | |
<div id="item11" class="inactive"> | |
<img src="./images/space.gif" id="image11" /> | |
</div> | |
<div id="item12" class="inactive"> | |
<img src="./images/space.gif" id="image12" /> | |
</div> | |
<div id="item13" class="inactive"> | |
<img src="./images/space.gif" id="image13" /> | |
</div> | |
<div id="item14" class="inactive"> | |
<img src="./images/space.gif" id="image14" /> | |
</div> | |
<div id="item15" class="inactive"> | |
<img src="./images/space.gif" id="image15" /> | |
</div> | |
<div id="item16" class="inactive"> | |
<img src="./images/space.gif" id="image16" /> | |
</div> | |
<div id="item17" class="inactive"> | |
<img src="./images/space.gif" id="image17" /> | |
</div> | |
<div id="item_middle"> | |
<img src="./images/space.gif" id="image_middle" /> | |
</div> | |
</div> | |
<div id="sound"> | |
<audio id="player_board" name="player_board" preload="auto" loop="loop"> | |
<source src="./sounds/board.ogg" /> | |
<source src="./sounds/board.mp3" /> | |
</audio> | |
<audio id="player_buzz" name="player_buzz" preload="auto"> | |
<source src="./sounds/buzz.ogg" /> | |
<source src="./sounds/buzz.mp3" /> | |
</audio> | |
<audio id="player_whammy" name="player_whammy" preload="auto"> | |
<source src="./sounds/whammy.ogg" /> | |
<source src="./sounds/whammy.mp3" /> | |
</audio> | |
</div> | |
<script type="text/javascript"> | |
//<![CDATA[ | |
$(document).ready(function () { | |
var images = define_board_images(); | |
window.game_data = build_game_boards(images); | |
load_game_board(); | |
}); | |
function define_board_images () { | |
var images = new Array(); | |
$.ajax({ | |
url: 'config.xml', | |
dataType: 'xml', | |
async: false, | |
success: function (data) { | |
$($(data)).find('image').each(function () { | |
images.push({'thumb': $(this).find('thumb').text(), 'large': $(this).find('large').text(), 'type': $(this).find('type').text()}); | |
}); | |
} | |
}); | |
return images; | |
} | |
function build_game_boards (images) { | |
var boards = new Array(); | |
do { | |
var set = new Array(); | |
do { | |
images = array_shuffle(images); | |
var n = 0; | |
do { | |
set.push(images[n]); | |
n++; | |
} while ((set.length < 18) && (n < images.length)); | |
} while (set.length < 18); | |
boards.push(set); | |
} while (boards.length < 50); | |
return boards; | |
} | |
function load_game_board () { | |
$('div#board div.active').removeClass('active'); | |
$('img#image_middle').removeClass('prize'); | |
$('img#image_middle').attr('src', './images/logo.jpg'); | |
window.active_set = -1; | |
window.active_space = -1; | |
print_set(get_random_set(false)); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
start_game(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
start_game(); | |
}); | |
} | |
function get_random_set (no_repeat) { | |
var r = -1; | |
if (no_repeat) { | |
do { | |
r = Math.floor(Math.random() * window.game_data.length); | |
} while (r == window.active_set); | |
window.active_set = r; | |
} else { | |
r = Math.floor(Math.random() * window.game_data.length); | |
} | |
return window.game_data[r]; | |
} | |
function print_set (data) { | |
$('div#board div img').each(function (index) { | |
if ($(this).attr('id') != 'image_middle') { | |
$(this).attr('src', data[index].thumb) | |
$(this).attr('data-type', data[index].type) | |
$(this).attr('data-large', data[index].large) | |
} | |
}); | |
} | |
function move_active_space () { | |
var r = -1; | |
do { | |
r = Math.floor(Math.random() * 18); | |
} while (r == window.active_space); | |
window.active_space = r; | |
var new_selector = 'div#item' + r; | |
$('div.active').removeClass('active'); | |
$(new_selector).addClass('active'); | |
} | |
function start_game () { | |
window.active_set = -1; | |
window.active_space = -1; | |
$(document).unbind(); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
stop_game(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
stop_game(); | |
}); | |
$('#player_board')[0].play(); | |
window.interval_set = setInterval(function () { print_set(get_random_set(true)); }, 850); | |
window.interval_space = setInterval(function () { move_active_space(); }, 500); | |
} | |
function stop_game () { | |
clearInterval(interval_set); | |
clearInterval(interval_space); | |
$(document).unbind(); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
load_game_board(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
load_game_board(); | |
}); | |
var winning_cell = 'div#item' + window.active_space; | |
$('#player_board')[0].pause(); | |
$('#player_board')[0].currentTime = 0; | |
if ($(winning_cell + ' img').attr('data-type') == 'whammy') { | |
$('#player_whammy')[0].play(); | |
} else { | |
$('#player_buzz')[0].play(); | |
} | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$('img#image_middle').attr('src', './images/space.gif'); | |
$('img#image_middle').addClass('prize'); | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$('img#image_middle').attr('src', $(winning_cell + ' img').attr('data-large')); | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
} | |
function array_shuffle (orig_array) { | |
var shuffled_array = orig_array.slice(); | |
var len = shuffled_array.length; | |
var i = len; | |
while (i--) { | |
var p = parseInt(Math.random()*len); | |
var t = shuffled_array[i]; | |
shuffled_array[i] = shuffled_array[p]; | |
shuffled_array[p] = t; | |
} | |
return shuffled_array; | |
}; | |
//]]> | |
</script> | |
</body> | |
</html> |
Get the basic stuff out of the way… We import our CSS. We import jQuery UI. We lay out the game board and we set up some audio elements for the game sounds (which I pulled from some site that had all sorts of game show sounds archived, I can’t remember where it was).
$(document).ready(function () { | |
var images = define_board_images(); | |
window.game_data = build_game_boards(images); | |
load_game_board(); | |
}); |
The first thing we do is initialize some stuff. Define our board images, build our possible game boards, throw a board onto the screen. Now let’s see how we do that.
function define_board_images () { | |
var images = new Array(); | |
$.ajax({ | |
url: 'config.xml', | |
dataType: 'xml', | |
async: false, | |
success: function (data) { | |
$($(data)).find('image').each(function () { | |
images.push({'thumb': $(this).find('thumb').text(), 'large': $(this).find('large').text(), 'type': $(this).find('type').text()}); | |
}); | |
} | |
}); | |
return images; | |
} |
We’re loading that config file, then looping through each “image” element to find the “thumb”, “large”, and “type” definitions we discussed earlier. Then we’re dropping those into an array.
When I wrote this I was shocked that there wasn’t an easier way to do this using XML. If it were similarly-structured JSON, it’d just parse automatically. Instead I have to do it manually. Considering what the X in AJAX stands for, I expected more out-of-the-box support for XML. Maybe I’m just missing something.
function build_game_boards (images) { | |
var boards = new Array(); | |
do { | |
var set = new Array(); | |
do { | |
images = array_shuffle(images); | |
var n = 0; | |
do { | |
set.push(images[n]); | |
n++; | |
} while ((set.length < 18) && (n < images.length)); | |
} while (set.length < 18); | |
boards.push(set); | |
} while (boards.length < 50); | |
return boards; | |
} |
With our available images defined, we cache a set of fifty possible game boards. We do this by shuffling the array of images (using a function I just grabbed from somewhere else) and adding them in order to a new set until there are 18 in that set. If we run out before we get to 18, we shuffle again and keep going. This means we can have as many or as few (as long as there’s at least one) images configured.
function load_game_board () { | |
$('div#board div.active').removeClass('active'); | |
$('img#image_middle').removeClass('prize'); | |
$('img#image_middle').attr('src', './images/logo.jpg'); | |
window.active_set = -1; | |
window.active_space = -1; | |
print_set(get_random_set(false)); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
start_game(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
start_game(); | |
}); | |
} |
Finally we load the game board. We make sure no tiles are active, we set the middle image back to our placeholder, we get a randomly-selected one of our cached tile sets and display it on the board. Then we define some key events that allow the game to be controlled from the keyboard or from a presentation mouse, so that any event will trigger the start of the game. We bind the same action on touchend so that the person who commissioned this can play on her phone.
function get_random_set (no_repeat) { | |
var r = -1; | |
if (no_repeat) { | |
do { | |
r = Math.floor(Math.random() * window.game_data.length); | |
} while (r == window.active_set); | |
window.active_set = r; | |
} else { | |
r = Math.floor(Math.random() * window.game_data.length); | |
} | |
return window.game_data[r]; | |
} |
Our function for getting a random set is simple enough. Get a random number from 0 to the size of the set (should always be 50). If we don’t want to allow the same set to be picked twice in a row, compare that number to the current one and do it again until we get something different. Return the set of images with that number as the key.
function print_set (data) { | |
$('div#board div img').each(function (index) { | |
if ($(this).attr('id') != 'image_middle') { | |
$(this).attr('src', data[index].thumb) | |
$(this).attr('data-type', data[index].type) | |
$(this).attr('data-large', data[index].large) | |
} | |
}); | |
} |
To print out the board, we loop through each image on the board that isn’t the one in the middle. We use the index of the image and pull from the array we set in get_random_set() to reset said image’s attributes.
function start_game () { | |
window.active_set = -1; | |
window.active_space = -1; | |
$(document).unbind(); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
stop_game(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
stop_game(); | |
}); | |
$('#player_board')[0].play(); | |
window.interval_set = setInterval(function () { print_set(get_random_set(true)); }, 850); | |
window.interval_space = setInterval(function () { move_active_space(); }, 500); | |
} |
Ahh, yes, now we start the actual gameplay. We wipe out all of the events we set earlier and set new ones on the same triggers, this time for stopping the game. We start playing our in-game music. Then we set an interval to reload the game board every 850 milliseconds (allowing for the same board to be played twice in a row this time) and for the active tile to shift every half-second. I got those numbers from watching way too much Press Your Luck.
function move_active_space () { | |
var r = -1; | |
do { | |
r = Math.floor(Math.random() * 18); | |
} while (r == window.active_space); | |
window.active_space = r; | |
var new_selector = 'div#item' + r; | |
$('div.active').removeClass('active'); | |
$(new_selector).addClass('active'); | |
} |
How do we switch the active tile? Well we know there are 18 tiles so we randomly select a number 0 to 17 until that number is not the same as the one we’ve already got. Then we remove the active class from whatever tile is active and add it to the one that corresponds to our randomly-selected number.
Our last step is to stop the game and it’s made up of a bunch of little things.
clearInterval(interval_set); | |
clearInterval(interval_space); | |
$(document).unbind(); | |
$(document).keydown(function (e) { | |
if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) { | |
e.preventDefault(); | |
load_game_board(); | |
} | |
}); | |
$(document).bind('touchend', function (e) { | |
e.preventDefault(); | |
load_game_board(); | |
}); |
First we clear our intervals so the game won’t continue, then we wipe out our event bindings and set up new ones for the same triggers. These new ones will reset the game board and get us in a position to start a new game.
var winning_cell = 'div#item' + window.active_space; | |
$('#player_board')[0].pause(); | |
$('#player_board')[0].currentTime = 0; | |
if ($(winning_cell + ' img').attr('data-type') == 'whammy') { | |
$('#player_whammy')[0].play(); | |
} else { | |
$('#player_buzz')[0].play(); | |
} |
We get the winning tile and stop the in-game music. Based on what type of image that winning tile is, we play either the “buzz-in” sound or the “whammy” sound.
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$('img#image_middle').attr('src', './images/space.gif'); | |
$('img#image_middle').addClass('prize'); | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$('img#image_middle').attr('src', $(winning_cell + ' img').attr('data-large')); | |
$(this).addClass('active').delay(100).queue(function (next) { | |
$(winning_cell).removeClass('active').delay(100).queue(function (next) { | |
$(this).addClass('active').delay(100).queue(function (next) { | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); | |
next(); | |
}); |
This is how we make the lights around the winning tile flash and it’s ugly. We add and remove the “active” class from that tile in 100 millisecond intervals. Partway through that, we change the center image on the game board to match that of the winning tile. Again, those times were selected from watching way too much Press Your Luck.
And that’s really all there is to it. There may be a better way by now (I hope there is for that flashing bit) but this is what I knew at the time. It was a lot of fun to write and it was a lot of fun to see people play.