2013-12-29
Experiments in Trivia

Bored, want to see how fast I can develop a simple multiplayer trivia app in my blog environment using Firebase.

Let’s have a QuizBot manage a timer and a state machine that allows others to answer questions in the Chat window that are posed.

Simple Quiz Experiment

For kicks, I’ll use the code from Christmas Presence earlier in this blog to keep track of the number of participants and their locations.

.controller('PresenceController', ['$scope', 'PresenceService',
  function($scope, PresenceService) {
    $scope.totalViewers = 0;
    $scope.allViewers = [];

    $scope.$on('onOnlineUser', function() {
      $scope.$apply(function() {
        $scope.totalViewers = PresenceService.getOnlineUserCount();
        $scope.allViewers = PresenceService.getOnlineUserList();
      });
    });
  }
])

.factory('PresenceService', ['$rootScope', '$http',
  function($rootScope, $http) {
    var onlineUsers = [];
    var numOnlineUsers = 0;
    var myUserInfo = {};

    // Create our references
    var listRef = new Firebase('https://doctorbud.firebaseIO.com/simplequizpresence1');
    var userRef = listRef.push(); // This creates a unique reference for each user
    var presenceRef = new Firebase('https://doctorbud.firebaseIO.com/.info/connected');

    var jsonp = $http.jsonp('https://ipinfo.io/');

    jsonp.then(
      function(response) {
        response = response.data;
        myUserInfo.city = response.city;
        myUserInfo.country = response.country;
        myUserInfo.hostname = response.hostname || 'unknown';
        myUserInfo.ip = response.ip;
        myUserInfo.loc = response.loc;
        myUserInfo.org = response.org;
        myUserInfo.region = response.region;
      });

    // Add ourselves to presence list when online.
    presenceRef.on('value', function(snap) {
      if (snap.val()) {
        userRef.set(myUserInfo);
        // Remove ourselves when we disconnect.
        userRef.onDisconnect().remove();
      }
    });

    // Get the user count and notify the application
    listRef.on('value', function(snap) {
      numOnlineUsers = snap.numChildren();
      onlineUsers = [];
      snap.forEach(function(childSnapshot) {
        // This code will be called twice.
        var name = childSnapshot.name();
        var childData = childSnapshot.val();
        onlineUsers.push(childData);
      });

      $rootScope.$broadcast('onOnlineUser');
    });

    var getOnlineUserCount = function() {
      return numOnlineUsers;
    }

    var getOnlineUserList = function() {
      return onlineUsers;
    }

    return {
      getOnlineUserCount: getOnlineUserCount,
      getOnlineUserList: getOnlineUserList
    }
  }
]);

<div scroll-glue class="overflowable" style="font-size:small; color:#1111AA;background-color:#DDDDDD;">
  <ul>
    <li ng-repeat='viewer in allViewers'>
      Geo: {[viewer.city]}, {[viewer.region]}, {[viewer.country]} -- {[viewer.loc]}
      <br/>
      Net: {[viewer.ip]} {[viewer.hostname]} {[viewer.org]}
    </li>
  </ul>
</div>

The code for the above is below:

<div>
  <script>
    angular.module('BlogApp')

    .controller('PresenceController', ['$scope', 'PresenceService',
      function($scope, PresenceService) {
        $scope.totalViewers = 0;
        $scope.allViewers = [];

        $scope.$on('onOnlineUser', function() {
          $scope.$apply(function() {
            $scope.totalViewers = PresenceService.getOnlineUserCount();
            $scope.allViewers = PresenceService.getOnlineUserList();
          });
        });
      }
    ])

    .factory('PresenceService', ['$rootScope', '$http',
      function($rootScope, $http) {
        var onlineUsers = [];
        var numOnlineUsers = 0;
        var myUserInfo = {};

        // Create our references
        var listRef = new Firebase('https://doctorbud.firebaseIO.com/simplequizpresence1');
        var userRef = listRef.push(); // This creates a unique reference for each user
        var presenceRef = new Firebase('https://doctorbud.firebaseIO.com/.info/connected');

        var jsonp = $http.jsonp('https://ipinfo.io/');

        jsonp.then(
          function(response) {
            myUserInfo.city = response.city;
            myUserInfo.country = response.country;
            myUserInfo.hostname = response.hostname;
            myUserInfo.ip = response.ip;
            myUserInfo.loc = response.loc;
            myUserInfo.org = response.org;
            myUserInfo.region = response.region;
          });

        // Add ourselves to presence list when online.
        presenceRef.on('value', function(snap) {
          if (snap.val()) {
            userRef.set(myUserInfo);
            // Remove ourselves when we disconnect.
            userRef.onDisconnect().remove();
          }
        });

        // Get the user count and notify the application
        listRef.on('value', function(snap) {
          numOnlineUsers = snap.numChildren();
          onlineUsers = [];
          snap.forEach(function(childSnapshot) {
            // This code will be called twice.
            var name = childSnapshot.name();
            var childData = childSnapshot.val();
            onlineUsers.push(childData);
          });

          $rootScope.$broadcast('onOnlineUser');
        });

        var getOnlineUserCount = function() {
          return numOnlineUsers;
        }

        var getOnlineUserList = function() {
          return onlineUsers;
        }

        return {
          getOnlineUserCount: getOnlineUserCount,
          getOnlineUserList: getOnlineUserList
        }
      }
    ]);
  </script>

  <div ng-controller="PresenceController" class="card bg-well p-1" style="padding:20px;max-width:600px;">
    <h6>{{totalViewers}} viewers are viewing</h6>

    <div scroll-glue class="overflowable" style="font-size:small; color:#1111AA;background-color:#DDDDDD;">
      <ul>
        <li ng-repeat='viewer in allViewers'>
          Geo: {{viewer.city}}, {{viewer.region}}, {{viewer.country}} -- {{viewer.loc}}
          <br/>
          Net: {{viewer.ip}} {{viewer.hostname}} {{viewer.org}}
        </li>
      </ul>
    </div>
  </div>
</div>

The Trivia Game

Here is the main event, a trivia game.

  $scope.addMessage = function(e) {
    var curState = $scope.state[$scope.state.$getIndex()[0]];
    console.log('stateMachine: ', curState);

    if (e.keyCode != 13) return;
    if ($scope.msg === "#clear")
    {
      $scope.messages.$set([]);
      $scope.answers.$set([]);
    }
    else
    {
      if (curState == 'asked')
      {
        $scope.answers.$add({from: $scope.name, body: $scope.msg});
        $scope.messages.$add({from: $scope.name, body: $scope.name + ' has answered.'});
      }
      else
      {
        $scope.messages.$add({from: $scope.name, body: $scope.msg});
      }
      $scope.msg = "";
    }
  };

  $scope.addBotMessage = function(msg) {
    $scope.messages.$add({from: "QuizBot", body: msg});
  };

  $scope.resetGame = function() {
    $scope.messages.$set([]);
    $scope.answers.$set([]);
    $scope.state.$remove();
    $scope.state.$add("asking");
    $scope.countdown = 10;
    $scope.questionNumber = 4;
  };


  $scope.stateMachine = function() {
    var curState = $scope.state[$scope.state.$getIndex()[0]];
    console.log('stateMachine: ', curState);

    if (curState == 'asking')
    {
      $scope.addBotMessage('Question... What is ' + $scope.questionNumber + ' + ' + $scope.questionNumber + '?');
      $scope.state.$remove();
      $scope.state.$add("asked");
    }
    else if (curState == 'asked')
    {
      if ($scope.countdown > 0)
      {
        $scope.addBotMessage('T:' + $scope.countdown);
        $scope.countdown = $scope.countdown - 1;
      }
      else
      {
        $scope.state.$remove();
        $scope.state.$add("scoring");
      }
    }
    else if (curState == 'scoring')
    {
      $scope.addBotMessage('Scores for Q:' + $scope.questionNumber);
      console.log('$scope.answers=', $scope.answers);

      var keys = $scope.answers.$getIndex();
      keys.forEach(
          function(key, i) {
            var   answer = $scope.answers[key];
            console.log($scope.answers[key]);
            $scope.addBotMessage('   ' + answer.from + ' answered "' + answer.body + '"');
          });

      $scope.questionNumber = $scope.questionNumber - 1;
      $scope.countdown = 10;
      $scope.answers.$remove();
      $scope.state.$remove();
      $scope.state.$add("asking");
    }
  };
}
angular
  .module('BlogApp')
  .controller('GameController', GameController);

<div scroll-glue class="overflowable" id="messagesDiv" style="color:#00AA00;background-color:#000000;height:300px;overflow:scroll;">
  <div ng-repeat="msg in messages"><em>{[msg.from]}</em>: {[msg.body]}</div>
</div>
<br/>
<input type="text" ng-model="name" placeholder="Name" size="10">
<input type="text" ng-model="msg" ng-keydown="addMessage($event)" placeholder="Message..." size="50">

<h6>Machine Time: {[ MA.getMachineTime() ]}</h6>
<h6>Machine State: {[ MA.getMachineState() ]}</h6>
<h6>Machine Running: {[ MA.isRunning() ]}</h6>

<h6>Question Number: {[ questionNumber ]}</h6>
<h6>Countdown: {[ countdown ]}</h6>
<h6>Answers: {[ answers ]}</h6>
<h6>State: {[ state ]}</h6>

<mlmmachine
  ptr="MA"
  ng-init="resetGame()"
  reset-fn="resetGame()"
  step-fn="stateMachine()"
  class="mlm background">

<mlmpanel></mlmpanel>

</mlmmachine>

The code for the above is below:

<div>
  <script>
    function GameController($scope, $firebase) {
      $scope.messages = $firebase(new Firebase('https://doctorbud.firebaseIO.com/simplequizchatmessages'));
      $scope.answers = $firebase(new Firebase('https://doctorbud.firebaseIO.com/simplequizchatanswers'));
      $scope.state = $firebase(new Firebase('https://doctorbud.firebaseIO.com/simplequizchatstate'));
      $scope.countdown = $firebase(new Firebase('https://doctorbud.firebaseIO.com/simplequizchatcountdown'));
      $scope.questionNumber = $firebase(new Firebase('https://doctorbud.firebaseIO.com/simplequizchatquestionNumber'));

      $scope.addMessage = function(e) {
        var curState = $scope.state[$scope.state.$getIndex()[0]];
        console.log('stateMachine: ', curState);

        if (e.keyCode != 13) return;
        if ($scope.msg === "#clear")
        {
          $scope.messages.$set([]);
          $scope.answers.$set([]);
        }
        else
        {
          if (curState == 'asked')
          {
            $scope.answers.$add({from: $scope.name, body: $scope.msg});
            $scope.messages.$add({from: $scope.name, body: $scope.name + ' has answered.'});
          }
          else
          {
            $scope.messages.$add({from: $scope.name, body: $scope.msg});
          }
          $scope.msg = "";
        }
      };

      $scope.addBotMessage = function(msg) {
        $scope.messages.$add({from: "QuizBot", body: msg});
      };

      $scope.resetGame = function() {
        $scope.messages.$set([]);
        $scope.answers.$set([]);
        $scope.state.$remove();
        $scope.state.$add("asking");
        $scope.countdown = 10;
        $scope.questionNumber = 4;
      };


      $scope.stateMachine = function() {
        var curState = $scope.state[$scope.state.$getIndex()[0]];
        console.log('stateMachine: ', curState);

        if (curState == 'asking')
        {
          $scope.addBotMessage('Question... What is ' + $scope.questionNumber + ' + ' + $scope.questionNumber + '?');
          $scope.state.$remove();
          $scope.state.$add("asked");
        }
        else if (curState == 'asked')
        {
          if ($scope.countdown > 0)
          {
            $scope.addBotMessage('T:' + $scope.countdown);
            $scope.countdown = $scope.countdown - 1;
          }
          else
          {
            $scope.state.$remove();
            $scope.state.$add("scoring");
          }
        }
        else if (curState == 'scoring')
        {
          $scope.addBotMessage('Scores for Q:' + $scope.questionNumber);
          console.log('$scope.answers=', $scope.answers);

          var keys = $scope.answers.$getIndex();
          keys.forEach(
              function(key, i) {
                var   answer = $scope.answers[key];
                console.log($scope.answers[key]);
                $scope.addBotMessage('   ' + answer.from + ' answered "' + answer.body + '"');
              });

          $scope.questionNumber = $scope.questionNumber - 1;
          $scope.countdown = 10;
          $scope.answers.$remove();
          $scope.state.$remove();
          $scope.state.$add("asking");
        }
      };
    }
  </script>

  <div style="padding:20px;max-width:500px; background-color: aliceblue;" ng-controller="GameController">

    <div scroll-glue class="overflowable" id="messagesDiv" style="color:#00AA00;background-color:#000000;">
      <div ng-repeat="msg in messages"><em>{{msg.from}}</em>: {{msg.body}}</div>
    </div>
    <br/>
    <input type="text" ng-model="name" placeholder="Name" size="10">
    <input type="text" ng-model="msg" ng-keydown="addMessage($event)" placeholder="Message..." size="50">

    <h6>Machine Time: {{ MA.getMachineTime() }}</h6>
    <h6>Machine State: {{ MA.getMachineState() }}</h6>
    <h6>Machine Running: {{ MA.isRunning() }}</h6>

    <h6>Question Number: {{ questionNumber }}</h6>
    <h6>Countdown: {{ countdown }}</h6>
    <h6>Answers: {{ answers }}</h6>
    <h6>State: {{ state }}</h6>

    <mlmmachine
      ptr="MA"
      ng-init="resetGame()"
      reset-fn="resetGame()"
      step-fn="stateMachine()"
      class="mlm background">

    <mlmpanel></mlmpanel>

    </mlmmachine>
  </div>
</div>