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

Marius Berg to Kim, me on Thu, Dec 16, 2010 at 9:49 AM

Hi Andrea

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

On 14 December 2010 16:27, Kim Daniel
Arthur wrote:

style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex">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)
      award.timer_id = setTimeout(function(){on_timeout(i);}, (next_time - now) * 1000);
      then 4 (the last i value) would be used for each timeout call. The meta function makes each call different.
  • 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',
      ),
    ),
  ),
)