The philosophy of Scoreboard:
It a Dynamic Calculator as a Service, essentially.
Scoreboard needs to know who should have access to what, so Users are Scoreboard’s form of access control. Anyone can log in with OpenId, and Administrators of scoreboards can grant access.
Administrators can also generate application tokens, which can be used to login in the same way a User would.
There are three classes of users:
Users have nothing to do with the data for your Scoreboard, but Players do. A Player is the atomic entity for scoring: that is, it’s the smallest thing you could possibly care about aggregating scores by.
It may be the case that Players map 1:1 to people, but if you are running a Trivia Night, your Player may map to a table of people, since you don’t care about breaking down who at the table answered the question. Equivalently, you could also have multiple Players for a person, but I couldn’t think of a plausible example for that…
If Players are the atomic unit, Groups are the molecular unit. Groups are simply sets of Players. There is no constraint past this: Groups do not need to be disjoint (if you need this, you will need to enforce it yourself).
To give an example, if you were running a school carnival, you would want each student to be a Player, but you could have Groups for each school house, and for each age group.
Groups can be made up of Players and other Groups. Scoreboard will try to catch any created circular dependencies and return an error, but we reserve the right to simply crash any operation if you try to do this. Yeah, don’t do that.
You may want to run multiple events using the same Players and Groups, which are represented with Games. Be aware that all Games are independent, so you should use one Game per time period: don’t try to split an event up into multiple Games (just use multiple Leaderboards to represent the different parts).
Each Game is made up of multiple Questions. When a Player answers a Question (in your application), you will do some check to see if it’s a correct answer or not, and create a Point for that Player and Question.
You can have multiple Points for a (Question, Player) pair, but Points can also have mutations. To understand why both of these exist, imagine you’re running a Trivia Night, and this happens:
In order to avoid this situation, you can use this model:
Each Point has a creation timestamp, and each mutation also has a timestamp.
However, this model means that we need to have some way of evaluating the score for a Player for a Question, and this is done by a Point Scorer, which takes a Question, a collection of Points for the Question and the current game time (which allows for “decaying” scores), and returns some value.
A typical Point Scorer would be to look at the final mutation for all their answers, and choose the maximum value:
def point_scorer(question, points, timestamp):
return max(
point.mutations[-1].value
for point in points
)
In addition to Point Scorers, we also need some way of aggregating the scores for all Questions for a Player, which is a Player Scorer. This takes a collection of (Question, Score) pairs and the current game timestamp, and returns some value. Typically, you would just sum up the values.
def player_scorer(question_scores, timestamp):
return sum(
score
for question, score in question_scores
)
Using the service would be pretty pointless unless you could get your data out in some useful form, which is where Leaderboards come in (not called Scoreboards, to avoid confusion with the name of the service).
Leaderboards specify a set of Questions and Players, apply the Point Scorer for each (Question, Player) pair in those sets, applies the Player Scorer for those scores, sorts the result (either ascending or descending), and optionally limits the number of displayed results, finally exporting this data as JSON or in a HTML table.
If you were paying attention earlier (of course you were!), you would have noticed that the scoring functions have a timestamp, which leads us to the final feature of Leaderboards: they can show you different points in time. For example, if you wanted to “freeze” your scoreboard 20 minutes before the end of the competition, you could do that.
However, this also requires you to write your Scoring Functions so that they respect the current game timestamp correctly: you will likely want to have it ignore Points with a later creation timestamp, but still consider mutations with later timestamps so that mistakes in the scoring can be fixed.