Playfish Job Application Test
I’ve recently failed the application for a web developer job at Playfish. After a phone call between Kim and me, Kim made Marius send me a PHP / JavaScript test on Thursday. I sent my solution back the day after and began to wait. I waited 4 days, before sending a whatsup email. Then they came back to me saying that they decided to move on with other candidates. I’ve tried to get some feedback about what was wrong with my solution, but I’m still waiting. Here is the test and my solution.
The test
Thanks for applying for the position and taking the time to talk to us.
Sending a practical test, as described by Kim earlier.
Practical test:
---------------------------
JavaScript / PHP
-
Read the data from attached example PHP file into javascript. And represent the data in an HTML UI. Example graphics attached.
-
Create a set of countdown timers from the awards data. Each of the awards should count down to next_time. Once the timer is completed invoke a animated UI presenting the user with the award he has received.
-
On award acceptation the javascript should post the the data back to a PHP page using AJAX and validate that the award is ready for collection as well as validate the awarded number. You can chose how to store needed data.
-
Once collected the users data should update accordingly in the UI and the timer should restart counting down based on the “interval” in the corresponding award array.
Layout:
-
The page should be scaled for view on mobile devices ( iOs / Android )
-
Page loading and graphics should be optimized for fast loading where possible.
You can use jQuery.
Attached files:
jQuery library, example main screen and award graphics, as well as an example data file.
Feel free to use other graphics, and data structure if you feel necessary.
---------------------------If you have any questions regarding the test, just email me.
Thanks and best regards
Marius
Hi Andrea,
Thanks for taking time to chat today, was great to be introduced!
As agreed we will send you a practical tech test on thursday morning which you can spend time over the weekend completing and then send the code back to us on monday. I've CC'ed Marius runs our mobile development efforts and who will be sending the test to you and you will get to speak to about tech stuff next week :)
if you have any questions in the meanwhile, feel free to email.
Cheers,Kim
| webdev.zip 1154K Download |
Contents of webdev.zip
Contents of data.php
<?php
$user_data = array(
"user"=>
array(
"coins"=>rand(1000,2000),
"name"=>"User Name"
),
"awards"=>
array(
array("next_time"=>time()+rand(20,360), "interval"=>rand(60,360), "reward"=>rand(10,1000), "text"=>"Reward for playing"),
array("next_time"=>time()+rand(20,360), "interval"=>rand(60,360), "reward"=>rand(10,1000), "text"=>"Reward for winning"),
array("next_time"=>time()+rand(20,360), "interval"=>rand(60,360), "reward"=>rand(10,1000), "text"=>"Reward for friends"),
array("next_time"=>time()+rand(20,360), "interval"=>rand(60,360), "reward"=>rand(10,1000), "text"=>"Reward for completing")
)
);
echo json_encode($user_data);
?>
My solution
When I spoke with Kim I made it clear that I never work with graphics, so I was surprised to see graphics as an element of the test, but I thought that the test could be standard for them, so I didn’t complain but asked (twice) if they really wanted me to do graphics. I got no answer. So I simply didn’t do anything with the layout thing.
Files sent back
Demo
Before presenting the contents of any file of my solution, I’d like you to see a short screencast of a demo of my solution. It’s simpler than having a live demo on my server. Note that I made the screencast for this post, I didn’t send it to Playfish.
Contents of playfish-test.css
Nothing to notice here, except the template class (@25-27).
body {
overflow: hidden;
}
.button {
padding: 10px;
width: 50px;
text-align: center;
}
.button a {
text-decoration: none;
}
#status {
padding: 8px;
border: 1px solid olive;
}
#status p {
margin: 0;
padding: 0;
}
.template {
display: none;
}
.reward {
margin-top: 4px;
padding: 8px;
border: 1px solid maroon;
}
.reward p {
margin: 0;
padding: 0;
}
Contents of playfish-test.html
Elements initially invisible (@19-22, @25) have a style=”display: none;”. On the contrary, the reward popup template (@27-31) is explicitly identified by the template class.
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Playfish Test</title>
<meta name="Description" content="" />
<meta name="Keywords" content="" />
<link rel="stylesheet" type="text/css" media="screen,print" href="playfish-test.css" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="playfish-test.js"></script>
</head>
<body>
<div id="profile">
<p class="user">USER: <input type="text"/></p>
</div>
<div id="status" style="display:none">
<p class="user">USER: <span></span></p>
<p class="coins">COINS: <span></span></p>
</div>
<div id="actions">
<p class="Start button"><a href="#">Start</a></p>
<p class="Stop button" style="display:none"><a href="#">Stop</a></p>
</div>
<div class="reward template">
<p class="title">TITLE</p>
<p class="coins">COINS</p>
<p class="Accept button"><a href="#">Accept</a></p>
</div>
</body>
</html>
Contents of playfish-test.js
The “on load” handler (@20-82) binds click handlers to two buttons: Start (@21-67) and Stop (@69-81).
- The Start handler posts the user name back to data.php (@23-62), marks the game as started (@63), and updates (@64-66) UI elements (playfish-test.html@16-26).
- The “on success” handler of the post to data.php verifies and extracts received data (@25-44, @48-53), and creates upcoming awards (@54-59).
- The first next_time received from the server is checked (@55) against the current time so that all awards get a chance to popup. If the first next_time of a given award is in the past, then that next_time is set to now + award.interval (@57).
- The timeout id is saved (@58) so that it can later be used to clear that timeout (@72-76), in the Stop handler.
- The timeout handler is bound (@58) to the timeout by means of the makeHandler meta function (@84-87) because a specific call is needed for each award. If it was bound without the meta function, but like this (compare with @134-136)
then 4 (the last i value) would be used for each timeout call. The meta function makes each call different.award.timer_id = setTimeout(function(){on_timeout(i);}, (next_time - now) * 1000);
- The Stop handler marks the game as stopped (@71), clears current (@77-79) and upcoming (@72-76) awards, and updates (@80) UI elements (playfish-test.html@23-25).
The “on timeout” handler (@121-142) immediately exits (@122-125) if the game is over, otherwise sets the handler (@128-140) for the Accept action (playfish-test.html@30) in the reward dialog (playfish-test.html@27-31), and displays (@141) the dialog.
- The “on timeout” handler is called many times, the first being set from the “on success” handler of the post to data.php (which is executed by the Start handler), and any other time set from inside the “on success” handler of the post to validate.php (which is executed by the Accept handler).
- The “on success” handler (@129-139) of the post to validate.php checks the validation result and udpates the status of the game if the award is ready for collection (@131-133), then sets the next timeout for the same award (compare with @58).
- The “on timeout” handler only sends the Accept event of a given award to the server. Reward value and current user coins are only displayed by the client. After an event is validated the new user coins are displayed as they come from the server.
- The award showing function (@89-115) clones the template, updates placeholders, sets up fade in / fade out / remove animations, binds the Accept handler to the button, and appends the dialog to the body (another animation effect).
/*
* //data sample
* {
* "user": {"coins":1067,"name":"User Name"},
* "awards":[
* {"next_time":1292494663,"interval":165,"reward":682,"text":"Reward for playing"},
* {"next_time":1292494742,"interval":300,"reward":271,"text":"Reward for winning"},
* {"next_time":1292494700,"interval": 83,"reward":577,"text":"Reward for friends"},
* {"next_time":1292494695,"interval":281,"reward":441,"text":"Reward for completing"}
* ]
* }
*/
(function($) {
var user = {name: 'Guest', coins: 0};
var awards = [];
var game_over = false;
$(function() {
$('.Start a').click(function(e) {
e.preventDefault();
$.post('data.php', {user: $('.user input').val()}, function(data) {
if (! (data.user && data.user.coins && data.user.name)) {
console.error('DATA ERROR (malformed user)');
//should notify user
return;
}
user = $.extend(user, data.user || {});
$('#status .user span').text(user.name);
$('#status .coins span').text(user.coins);
if (! (data.awards && $.isArray(data.awards))) {
console.error('DATA ERROR (awards should be an array)');
//should notify user
return;
}
var iTop = data.awards.length;
if (! (iTop > 0)) {
//this could be just a game without rewards, boring but possible
return;
}
awards = data.awards;
for (var i = 0; i < iTop; i++) {
var award = awards[i];
if (! (award.next_time && award.interval && award.reward && award.text)) {
console.error('DATA ERROR (malformed award)');
//should notify user
game_over = true;
return;
}
var now = (new Date()).valueOf() / 1000;
var next_time = award.next_time > now
? award.next_time
: now + award.interval;
award.timer_id = setTimeout(makeHandler(i), (next_time - now) * 1000);
console.log('timeout ' + award.timer_id + ' set for ' + award.text);
}
}, 'json');
game_over = false;
$('#profile').hide();
$('#status').show();
$('#actions .button').toggle();
});
$('.Stop a').click(function(e) {
e.preventDefault();
game_over = true;
for (var i = 0, iTop = awards.length; i < iTop; i++) {
var award = awards[i];
clearTimeout(award.timer_id);
console.log('timeout ' + award.timer_id + ' unset for ' + award.text);
}
$('.reward')
.not('.template')
.remove();
$('#actions .button').toggle();
});
});
function updateStatus(coins) {
user.coins = coins;
$('#status .coins span').text(coins);
}
function show(award) {
$('.reward.template')
.clone()
.removeClass('template')
.hide()
.find('.title')
.text(award.text)
.end()
.find('.coins')
.text(award.reward)
.end()
.find('.Accept a')
.click(function(e) {
e.preventDefault();
award.accept();
$(this)
.parents('.reward')
.fadeOut('slow', function() {
$(this).remove();
})
;
})
.end()
.appendTo('body')
.fadeIn('slow')
;
}
function makeHandler(i) {
return new Function("on_timeout(" + i + ");");
}
on_timeout = function(i) { //implicit global
if (game_over) {
console.log('game-over detected');
return;
}
var award = awards[i];
console.log('timeout ' + award.timer_id + ' elapsed for ' + award.text);
award.accept = function() {
$.post('validate.php', {text: award.text}, function(result) {
if (result.ready_for_collection) {
updateStatus(result.coins);
}
award.timer_id = setTimeout(function() {
on_timeout(i);
}, award.interval * 1000);
console.log('timeout ' + award.timer_id + ' set for ' + award.text);
}, 'json');
};
show(award);
};
})(jQuery);
Contents of data.php
Nothing to notice here, except that user data is saved into the session.
(I’ve shortened times for speeding up testing)
<?php
session_start();
$user_data = array(
"user" => array(
"coins" => rand(1000,2000),
"name" => $_POST['user'],
),
"awards" => array(
array("next_time" => time()+rand(10,25), "interval" => rand(10,25), "reward" => rand(10,1000), "text" => "Reward for playing"),
array("next_time" => time()+rand(10,25), "interval" => rand(10,25), "reward" => rand(10,1000), "text" => "Reward for winning"),
array("next_time" => time()+rand(10,25), "interval" => rand(10,25), "reward" => rand(10,1000), "text" => "Reward for friends"),
array("next_time" => time()+rand(10,25), "interval" => rand(10,25), "reward" => rand(10,1000), "text" => "Reward for completing"),
),
);
$_SESSION['data'] = $user_data;
echo json_encode($user_data);
?>
Contents of validate.php
If the awards were into an associative array with keys suitable for identifying them (like playing, winning, friends, completing) one could access them directly in PHP (and save the selection find) but in that case it would be more difficult to manage more or less awards, functionality that is now already implemented in the JavaScript code “for free”.
This code (@26-33) takes control of what the JavaScript code simply displays, thus making sure that the client cannot steal awards or just mess things up.
<?php
session_start();
$user_data = $_SESSION['data'];
function logdata( $data ) {
$data = "\n\n" . session_id() . ' - ' . date(DATE_ISO8601) . ' - ' . var_export($data, true);
file_put_contents('validate.log', $data, FILE_APPEND);
}
logdata(array('request' => $_POST, 'user_data' => $user_data));
try {
$found = null;
foreach ($user_data['awards'] as $key => $award) {
if ($_POST['text'] == $award['text']) {
$found = $award;
break;
}
}
if (! $found) {
throw new Exception('Expected a valid reward text value');
}
$now = time();
if (! ($now > $found['next_time'])) {
throw new Exception('Expected a valid event time');
}
$user_data['user']['coins'] += $found['reward'];
$user_data['awards'][$key]['next_time'] += $found['interval'];
$_SESSION['data'] = $user_data;
$result = array('ready_for_collection' => true, 'coins' => $user_data['user']['coins']);
}
catch (Exception $e) {
$result = array('ready_for_collection' => false, 'reason' => $e->getMessage());
}
logdata(array('response' => $result, 'user_data' => $user_data));
$result = json_encode($result);
echo $result;
Contents of validate.log
Here are two consecutive logs that show validation input / output. The first log tells that a “Reward for completing” accept event has been generated in the client by the “Miho San” user, which currently holds 1483 coins. The second log tells that the award was ready for collection and now the user holds 2473 coins (= 1483 + 990).
46b6bab34d828616a0970222380bd180 - 2010-12-17T17:19:37+0000 - array (
'request' =>
array (
'text' => 'Reward for completing',
),
'user_data' =>
array (
'user' =>
array (
'coins' => 1483,
'name' => 'Miho San',
),
'awards' =>
array (
0 =>
array (
'next_time' => 1292606378,
'interval' => 25,
'reward' => 354,
'text' => 'Reward for playing',
),
1 =>
array (
'next_time' => 1292606389,
'interval' => 18,
'reward' => 466,
'text' => 'Reward for winning',
),
2 =>
array (
'next_time' => 1292606391,
'interval' => 19,
'reward' => 249,
'text' => 'Reward for friends',
),
3 =>
array (
'next_time' => 1292606376,
'interval' => 25,
'reward' => 990,
'text' => 'Reward for completing',
),
),
),
)
46b6bab34d828616a0970222380bd180 - 2010-12-17T17:19:37+0000 - array (
'response' =>
array (
'ready_for_collection' => true,
'coins' => 2473,
),
'user_data' =>
array (
'user' =>
array (
'coins' => 2473,
'name' => 'Miho San',
),
'awards' =>
array (
0 =>
array (
'next_time' => 1292606378,
'interval' => 25,
'reward' => 354,
'text' => 'Reward for playing',
),
1 =>
array (
'next_time' => 1292606389,
'interval' => 18,
'reward' => 466,
'text' => 'Reward for winning',
),
2 =>
array (
'next_time' => 1292606391,
'interval' => 19,
'reward' => 249,
'text' => 'Reward for friends',
),
3 =>
array (
'next_time' => 1292606401,
'interval' => 25,
'reward' => 990,
'text' => 'Reward for completing',
),
),
),
)

