lardbucket: leaderboard with movement tracking

6/29/2010

Leaderboard with Movement Tracking

Filed under: Hacks, Programming — Andy @ 9:00 pm

In putting together SLV Scav with Dan Meyer, I ended up writing a (relatively) simple script for generating a scoreboard with rankings, as well as the amount the rankings had changed. It looks something like this: (picture from Dan Meyer, names blurred to protect the innocent)

Dylan Faullin asked for more information, so I’m posting most of the relevant code here.

It’s in PHP, and while it’s not necessarily the most efficient way of doing the scoreboard, it seems to work reasonably well. It does have a few assumptions:

  1. You’ve separated your points into a number of distinct “challenges,” and points for any of them are all assigned at the same time, and not adjusted later.
  2. Your players have a first name, last name, and grade level (although any part of that can obviously be modified).
  3. Your scores for every player and every challenge are in an array of $scores[$challengeNumber][$playerId] = score, and challenge numbers are sequential and start at 1.
  4. You have a function getPlayerInfo() that takes a player’s ID and returns an array with keys for ‘last’ (last name), ‘first’ (first name), and ‘grade’ (grade level).
  5. If you want to highlight the current user, you have functions loggedIn() and getCurrentUserId() that act as one might expect.

Most of these assumptions can be changed with appropriate changes in the code, but they worked for what I was doing. (Notably: no assumptions are made regarding user IDs, other than them being unique, and scores are not required to exist for any given user in any given competition.)

Computing the scoreboard

Note that because this code requires looping through every score every user has received, it may be slow for large competitions. Therefore, you might want to cache the output $scoreboard value, and only refresh the data every time a challenge ends. This avoids doing the intensive calculations every time someone views the scoreboard, at a cost of a little more complexity. I’ve noted one way to do this in the code.

So, here’s the code. It’s in PHP, and expects that $scores is already in memory, in the format listed above. (Hold your mouse over the code for options in the upper-right corner to view or copy the un-highlighted source.)

// Set up the scoreboard.
$scoreboard = Array();
$scoreboard['individual'] = Array(); // We'll populate this later.
// Set up the classes in the scoreboard.
$scoreboard['class'] = Array(
	9 => Array('name' => 'Freshmen', 'last' => 'Freshmen'),
	10 => Array('name' => 'Sophomores', 'last' => 'Sophomores'),
	11 => Array('name' => 'Juniors', 'last' => 'Juniors'),
	12 => Array('name' => 'Seniors', 'last' => 'Seniors'),
);

// Loop through each challenge thus far.
for($challengeNumber = 1; $challengeNumber < = $challengeTotal; $challengeNumber++) {
	if ($challengeNumber == $currentChallenge) {
		// We reached the current challenge, stop here.
		break;
	}
	
	// Have there been any scores from this challenge?
	if (isset($scores[$challengeNumber])) {
		// Yes, so for each one:
		foreach($scores[$challengeNumber] as $userId => $score) {
			// Get the player's information
			$userInfo = getPlayerInfo($userId);
			// If the user has a grade level (this lets us keep staff, etc. off
			//  the leaderboard):
			if ($userInfo['grade'] != 0) {
				// Set up information about this player.
				//  (Yes, this is often redundant, but this is an
				//   infrequently-run routine, so we should be okay.)
				$scoreboard['individual'][$userId]['name'] = $userInfo['first'].' '.$userInfo['last'];
				$scoreboard['individual'][$userId]['first'] = $userInfo['first'];
				$scoreboard['individual'][$userId]['last'] = $userInfo['last'];
				
				// Increment their score by however much they scored this time.
				$scoreboard['individual'][$userId]['score'] += $score;
				
				// If they are in a grade level we're tracking, add their
				//  points to their class' total score.
				if (isset($scoreboard['class'][$userInfo['grade']])) {
					$scoreboard['class'][$userInfo['grade']]['score'] += $score;
				}
			}
		}
	}
	
	// Sort and assign ranks to the individuals and classes.
	$scoreboard['individual'] = assignRanks($scoreboard['individual']);
	$scoreboard['class'] = assignRanks($scoreboard['class']);
}

// Here, it would be a good idea to save $scoreboard somewhere, and access it
//  as a cached value later, as scoreboard processing might take a while with
//  hundreds of students. In our case, this wasn't necessary, but it is a good
//  idea in practice.
// You can use PHP's serialize() function like so:
//  file_put_contents('scoreboard.dat', serialize($scoreboard));
// ... and then later load the scoreboard with:
//  $scoreboard = unserialize(file_get_contents('scoreboard.dat'));
// to avoid re-computing the scoreboard every time someone views it.

// Helper functions

// assignRanks assigns a rank to each contestant in the set, as well as the
//  amount they have moved in the last round, if relevant.
function assignRanks($input) {
	uasort($input, "sortByScore"); // Use sortByScore to sort everyone.
	$oldScore = -1; // Store the last player's score
	$rankOn = 0;    // The ranking we're currently on. Incremented manually,
	                //  because there may be a tie.
	$nextRank = 1;  // The next ranking that we should use if there is no tie.
	                //  We always increment this, even in a tie, so we can have
	                //  two first places, with the next highest score being
	                //  given third place.
	
	foreach($input as $key => $value) {
		// If the score is different than the last player's, it's not a tie.
		if ($value['score'] != $oldScore) {
			$rankOn = $nextRank;
		}
		
		// Save this score to detect ties, and increment the next rank.
		$oldScore = $value['score'];
		$nextRank++;
		
		// If there was a prior round, calculate how much the player's rank
		//  changed.
		if (isset($input[$key]['rank'])) {
			// This subtraction is counterintuitive, but we say someone moved
			//  "up" (positively) if their rank decreases (from 2nd to 1st).
			$input[$key]['move'] = $input[$key]['rank'] - $rankOn;
			
			if ($input[$key]['move'] == 0) {
				// If there was no movement, remove this key.
				unset($input[$key]['move']);
			}
		}
		
		// Finally, assign the rank.
		$input[$key]['rank'] = $rankOn;
	}
	return $input;
}

// sortByScore is used as a custom comparison operator in assignRanks() to sort
//  by score, then by last name, and finally by first name.
function sortByScore($a, $b) {
	// Pull out scores
	$aC = $a['score'];
	$bC = $b['score'];
	
	// If the score is equal,
	if ($aC == $bC) {
		// try comparing last names.
		if (strcmp($a['last'], $b['last']) == 0) {
			// If they're equal, compare first names.
			return strcmp($a['first'], $b['first']);
		}
		// Last names differ, so use those for sorting.
		return strcmp($a['last'], $b['last']);
	}
	// Scores are different, so we can sort by that.
	return ($aC > $bC) ? -1 : 1;
}

Displaying the scoreboard

Displaying the computed scoreboard information is a bit tricky (especially in terms of getting the CSS “just right”). If you want to make your own custom display, feel free to poke at $scoreboard. (Run a print_r($scoreboard), and look through that for the information it contains.) If you just want something that seems to work, here’s the code I use:

// To display the scoreboard, I used this code:
//  (written for the 960 grid system: http://960.gs/. Some extra CSS was used.)

// I use h() as a short alias for htmlentities():
function h($in) { return htmlentities($in); }

// This loops through each set of scoreboards (individual and class-based).
foreach($scoreboard as $scoreboardType => $scoreboardRows) { ?>
< ?=h($scoreboardType)?>
< ? $firstRow = true; // This loops through every entry on the relevant scoreboard. foreach($scoreboardRows as $rowUser => $row) { // We highlight the logged-in user, if any. $extraClass = (loggedIn() && ($rowUser == getCurrentUserId())) ? ' me' : ''; // We have a special CSS class for the first row (to avoid an extra // border). The :first CSS pseudo-selector may be a better idea, // but I didn't remember about it when I wrote this section of code. if ($firstRow) { $extraClass .= ' first'; $firstRow = false; } ?>
< ? if (isset($row['move'])) { // If the rank changed, show it if ($row['move'] > 0) { echo '
'.abs($row['move']).'
'; } else { echo '
'.abs($row['move']).'
'; } } ?>
< ?=h($row['rank'])?>
< ?=h($row['name'])?>
< ?=h($row['score'])?>
< ? } ?>
< ? } ?>

The code above also uses bits of the 960 Grid System CSS, although it could be easily adapted to something else. The relevant extra CSS is below, although you’ll want to change the colors if your scoreboard doesn’t appear on a dark background.

.scoreboardRow {
	font-size: 50%;
	line-height: 100%;
	color: #aaa;
	border-bottom: 1px solid #aaa;
	font-family: Georgia, 'Times New Roman', Times, serif;
	padding-top: 6px;
	padding-bottom: 6px;
}

.scoreboardRow.first {
	border-top: 1px solid #aaa;
}

.scoreboardRow.me {
	color: #fff;
}

.scoreboardRow .movement {
	width: 30px;
	font-family: Helvetica, Tahoma, Arial, sans-serif;
	font-size: 80%;
	float: left;
	background-repeat: no-repeat;
	background-position: center left;
	padding-left: 17px;
	margin-right: -17px;
	padding-top: 1px;
}
.scoreboardRow .movement.red {
	background-image: url('/images/down.png');
	color: #99413f;
}
.scoreboardRow .movement.green {
	background-image: url('/images/up.png');
	color: #429756;
}

.scoreboardRow .rank {
	width: 30px;
	text-align: right;
	float: right;
	margin-top: -2px;
}

.alignRight {
	text-align: right;
}

And finally, links to the up.png and down.png images that are referenced in that CSS. Either put them in your /images/ directory, or change the CSS to match where you put them.

You can feel free to use any of the above code for any purpose. If you do use it, I’d appreciate a comment letting me know. And of course, if you have any questions, leave a comment and I’ll try to get back to you. (That’s more likely, however, if you provide a fair amount of information as to what’s going wrong if you’re having trouble.)

Thanks,
Andy Schmitz

1 Comment »

  1. Wow. I appreciate all the effort you went through to share this code! I’ll get busy with it sometime this week and show you what I come up with when I get it all put together. Thanks again!

    Comment by Dylan Faullin — 6/29/2010 @ 11:03 pm

  2. Sorry, I actually has not been obvious. I am just wanting to decide the particular MS targeted iSCSI tax’? A number of have got indicated for the MASTER OF SCIENCE target SW and advised that performs improperly (usually VMware forums). In past times, I’ve performed a bit benchmarking to get Starwind along with HP’s VSA by means of evaluating effectiveness on the second .

    Comment by christian louboutin — 10/16/2014 @ 11:40 pm

  3. Elaborate the easiest method to obtain the ssh crucial over to typically the apple ipad tablet not having contacting this?

    Comment by spaccio moncler — 10/17/2014 @ 7:37 pm

  4. Well your publish ended up being worthy of the volume of do the job it took a little time for my family to actually view it. You should not necessarily believe how many hoops I had to bounce by means of see your write-up.

    Comment by moncler paris — 11/20/2014 @ 1:25 pm

  5. Hello! I was just thinking exactly how this operates to your adolescent kids. I will find it would be ideal for elementary, but you may be asking yourself what in relation to Jr. Substantial as well as High School (I get one associated with each) Will she include this throughout her guide (The Workbox E-book? ) Excellent scaled-down house, some youngsters and one along the way i cannot think about seizing much room!

    Comment by longchamp pas cher — 11/20/2014 @ 1:25 pm

  6. It is perfect time to make some plans for the future and it is time to be happy. I’ve read this publish and if I could I wish to counsel you few attention-grabbing issues or suggestions. Perhaps you can write subsequent articles regarding this article. I desire to read more issues approximately it!|

    Comment by christian louboutin high heels — 11/21/2014 @ 3:36 pm

  7. Hey just wanted to give you a quick heads up. The text in your article seem to be running off the screen in Firefox. I’m not sure if this is a format issue or something to do with internet browser compatibility but I figured I’d post to let you know. The layout look great though! Hope you get the issue resolved soon. Many thanks|

    Comment by http://momentumaroostook.com/about/louboutin-shoes/louboutin_women_455022.html — 11/21/2014 @ 3:36 pm

RSS feed for comments on this post.

Leave a comment

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

My Stuff
Blog Stuff
Categories
Archives