powered by Slim Framework
Follow @NesbittBrian rss

Fun Play 2.0 project launch : Sidney Crosby Art Ross Watch and My NHL

Nov 29, 2011

This post is meant for 2 audiences. The first are hockey fans and the second are programmers. If you are like me and fall into both categories than just keep reading. If you want to to skip ahead to the programmer portion than click here as the hockey part will be first.

If you want to have a look at the site first, go here : http://hockey.nesbot.com/

Hockey Fans : about the site

Sidney Crosby Art Ross Watch

Let me start by saying I live in Ottawa, ON, Canada and I am a SENS fan. Apart from the world jr tournament the first lengthy exposure I had to Crosby was the year the SENS beat the PENS in 5 games on their way to the Stanley cup finals. I respected Crosby's obvious hockey skills but he was young and got labeled as a "whiner", to the point where it references that reputation on his wiki page and over 1/2 a million google hits for crosby whiner. I think this comeback is doing a lot for hockey fans to rid him of that label, I know it has for me. He has obviously worked hard to come back and has shown it on the ice. To try and repair the relationship between him and I (ok its really between me and him ;-) I wanted to create a site to track Sidney's progress for what could be a history making season. The Art Ross Trophy is given to the player who leads the league in scoring at the end of the regular season. Crosby won the trophy in his 2nd season with 120 points as a teenager. This site will track his progress towards the Art Ross by providing a projection of points at the end of the season for him and the top 10 scoring leaders in the league. The projection is calculated using the players points/game average (so its accomodates injuries) and the remaining games for their respective teams.

So where does he rank today?

Crosby was just today given a 2nd assist from saturday night's game against Montreal so he now officially has 9 points in 4 games which gives him a 2.25 pts/game average. The penguins have 58 games remaining which puts him on pace for 139 points. This is 34 points ahead of next best Phil Kessel who has had a 20 game head start. Kessel has averaged 1.29 pts/game and is projected to get 105 points by season end. The site is automatically updated often so it will be exciting to watch as the season progresses and Crosby surely continues to rack up the points.

http://hockey.nesbot.com/crosbywatch

MyNHL

I am not a big fan of the current scoring system used by the NHL. Giving 2 points for a shootout win and the same 2 points for a regulation win seems unfair to me. Also awarding teams for a loss in overtime or shootout is also unfair in my eyes. A good example is Detroit and Pittsburgh. Assuming Detroit wins their next 2 games they would be trailing Pittsburgh by 2 points even though they would have 14 regulation wins to Pittsburgh's 10.

The MyNHL site allows you to configure the number of points awarded for wins, losses, OT wins, OT losses, shootout wins and shootout losses. I don't sort the teams by division or conference, it is just a list showing where the teams would be with your scoring system. I also disagree with the division leaders getting ranked 1, 2, 3 in the conference as someday the 3rd division leader will not have enough points to make it into the playoffs but will be 3rd in the conference none the less. If it were up to me the division leaders would be automatically granted a position in the playoffs but then just get ranked by points.

http://hockey.nesbot.com/mynhl

Programmers : how I made the site

If you haven't read my first post on Play 2.0 you might want to go back and read through This blog is now running on Play 2.0 BETA as it has some thoughts on the BETA status and its current state.

This is my first attempt at working with Scala and specifically Play with Scala. I did this site to start learning Scala using Play 2.0 and figured I might as well create something more than a hello world!

The site is broken up into 3 controllers for the 3 portions of the site. The Application controller just outputs the index view and has no logic. The CrosbyWatch controller gathers data and then renders the template. The third controller, MyNhl, has 2 actions. The index action gets all the teams and renders the initial template, while the second action, dataTable, parses some querystring data that is ajax posted (or applies defaults) and then generates the data table that will get rendered client side. This is done twice for the intial page load, once for the official NHL scoring system and once for the MyNHL scoring. Each change event to the MyNHL scoring configuration triggers another call to dataTable which regenerates the table. All of the sorting is actually done client side with the jquery.tablesorter plugin. Of course almost the whole site could have been done just client side but there would not have been much Scala involved in that!

Automatically updating the stats

I am using http://jsoup.org/ to scrape the NHL team and player stats and parse the HTML. Its a really nice library to use and mimics jquery selectors rather than xpath which was rare in the ones I found. Sometimes parsing HTML can feel pretty brittle but using jsoup was like a breath of fresh air. There are 2 pages and a little math involved in extracting the data elements for the teams because most sites group OT losses and shootout losses together and only show wins, losses and OT losses. I go directly to the Pittsburgh Penguins site for the Crosby specific stats.

The Play 1.X asynchronous jobs framework is exactly what I would have used to schedule the scraping. Play 2.0 doesn't have jobs built into it yet so I used the java.util.concurrent.Executors to run the job on a regular schedule. I looked around for the Scala equivalent but most sites just said to use the java api. I created a Global object and used the beforeStart to setup the thread scheduler and the onStop for shutdown. The mongoDB and morphia intialization code will be talked about in the next section. As you can see the StatsUpdater runs immediately as there is 0 intial delay and then is scheduled to run every 30 minutes. The thread pool only has 1 thread since its the only scheduled task in the pool.


object Global extends GlobalSettings {
  val executor = Executors.newSingleThreadScheduledExecutor()

  override def beforeStart(app: Application) {
    MongoDB.init("mynhl").mapPackage("models").indexes
    executor.scheduleAtFixedRate(StatsUpdater, 0, Dater.secondsPerMinute*30, TimeUnit.SECONDS)
  }
  override def onStop(app: Application) {
    executor.shutdownNow
  }
}

The Runnable StatsUpdater is probably not as elegant as you can get with Scala since this is my first time and I am just getting used to all of the sugar provided. The run() calls the appropriate upate*() methods and catches all exceptions and logs any errors via Play.


object StatsUpdater extends Runnable {
  def run() {
    try {
      updateTeams
      updatePlayers
      updateCrosby
    }
    catch {
      case e: Exception => play.Logger.error("Exception caught : " + e.getMessage, e)
    }
  }

  // update methods etc
}

Datastore is using morphia and mongoDB

I am (strangely?!?) using morphia to access mongoDB as the datastore with my own quick and dirty Scala based wrapper. Yes I looked at casbah, rogue and salat but came back to morphia in the end since I had used it before. I'll probably go back to some of those other Scala specific libraries, probably salat and casbah, but it just wasn't my focus this time around and I learned more by hacking up my own morphia wrapper anyway - how ever terrible it might be.

The initialization for morphia is done in the Global.beforeStart(). It configures the logger which is what I had an issue with yesterday and prompted using morphia with Play 2.0 and the sl4j logging error. It then creates the Morphia datastore for the database mynhl. Finally it pre-maps, using reflection, the classes for the Team and Player models. The morphia wrapper is broken into 2 base classes. MongoModel is the base class for the model instances and MongoObject is the base class for their accompanying Scala singletons. This seemed like a logical split to me. The instance model has all of the save/update/delete methods where the finders/counters are in the singleton. I also extend the morphia DAO object and added a protected variable to each of the model and singleton. The helper methods can then use the typed DAO interface rather than having to pass the class type to all of the morphia Datastore calls. Not all of the helper methods are wrapped but the majority of the basic operations are available. It needs to be augmented to handle WriteConcerns and return proper WriteResults but for this site its sufficient.

CrosbyWatch Controller & View

This controller is pretty simple. It gather the players by projected points and determines if Crosby is in the projected to win by checking the head element.


object CrosbyWatch extends Controller {
  def index = Action {
    val players = Player.findAllOrderByProjectedPoints
    val winner = players.head
    Ok(views.html.crosbywatch.index(players, winner.name.equals(Player.CrosbyName), winner.name))
  }
}

The view is also straight forward. It uses @if(crosbyToWin) {success} else {error} to set the css class appropriately so if he drops out of 1st place there will be some red on the page. Otherwise it mainly loops over the players and generates the necessary rows in the table.

I chose to use the @players.map { player => block } syntax. I could also use @for(player <- players) {}. There are other syntax that could be used for looping over the players. If you wanted to display a counter you could do @for(i <- 0 until players.size-1) {}.

MyNhl Controller

First the index action simply renders the template passing in a Seq[Team] to populate the drop down for selecting your favourite team. Then it uses the pointsConfig.scala.html tag to render the points config section for the NHL and one that allows the user to configure their scoring system. Nothing too complicated yet. On document.ready a few things happen. Event handlers are setup to highlight the users favourite team when the drop down changes. Also when any scoring configuration value is changed for the user scenario it makes an ajax call to the server action MyNhl.dataTable() sending in all of the new point parameters. The server action parses the querystring variables and renders the template. This generates the standings table and calculates the points for each team as it is rendered using the parsed querystring values. The sample provided in the Play 2.0 wiki for parsing querystring variables is shown below. Its done like this because the querystring entries are Seq[String] in order to handle multiple values per variable name.

val name = request.queryString.get("name").flatMap(_.headOption)

This is pretty verbose so I wrapped it in a ControllerHelper parse function. I also needed to parse Ints from the String values so I added those as well while handling improper number formatting. There is probably a more elegant way to get this done but this was easy enough for now. I tried a few times with the new form handling as indicated on the wiki but none of the samples from there worked for me and the majority I couldn't even get to compile.

Wrap Up

I think that wraps up this post and project. I'll be following Sidney's progress this season to see if he can capture the Art Ross even in this era of lower scoring. As for the programming side, using the Scala API was certainly tougher for me compared to the Java API and my Scala skills have a long way to go. I struggle with the syntax but also structuring the code to be more functional is still a bit of a mind shift for me that will just take some time.

As always catch me on email/twitter/comments/google group to ask specific questions and I'll do my best to answer them.

The full site code is available on github : http://github.com/briannesbitt/hockey.nesbot.com.

Using morphia with Play 2.0 and the sl4j logging error  Home CoffeeScript : Why is function binding not the default?  
blog comments powered by Disqus