This page describes an application of an earlier version of the pico-engine. It has not been updated for version 1.0 of the pico-engine
Overview
Inspired by the Live Web series of white papers, particularly Forever, Personal Channels, and Making Everything "Smart", we set up a game at OpenWest 2018. We invited conference attendees to participate, competing for daily prizes. Each participant was given an owner pico and a QR Code which, when scanned, would produce a connection between the person scanning and the person owning the pico. Each such connection counted as one point for each of them. The contest was to see who could make the most connections from their owner pico to other owner picos.
Trial run
A trial run of the contest was undertaken, with its state captured after the first seven connections were made. This clearly shows the three kinds of picos and their interconnections. The "OpenWest 2018" pico (aka "game pico") is an owner pico, as are its siblings, the gray picos named by a four digit number. The "Attendees" pico (aka "collection pico") is a direct child of the game pico, and collects the participant owners with a subscription (shown by a gray line) to each of their picos. Connections between owner picos, shown with a dashed magenta line, are also subscriptions. These are the connections which count for the contest, with one point for the owner at each end of such a connection.
At this point in the practice run (dubbed "series 2500") owner pico 2509 was in first place with four connections, five owner picos were tied for second place with two connections each, and the remaining four owner picos were tied for third place with zero connections each.
Final results
On June 7, 2018, the first day of OpenWest, we opened series 2600 and began signing up participants. During the game, we created 69 owner picos for participants. The first day winner (2622) created 27 connections (of a possible 47) while on the second day the winner (2680) created 32 connections (of a possible 68). At the end of the contest, the interconnection diagram looked like this:
Pico life spans
At this point, the game pico and the collection pico, along with its subscriptions (the gray dashed lines), can be deleted, because the game is over and they are no longer needed. The owner picos can remain, along with all of their connections (potentially, forever).
Game rules
A person came to the Pico Labs booth and asked to join the contest. We showed them a new sign-up QR Code, which they then scanned with a personal device. Their device displayed a page asking for a short name or initials and a tag-line. Once they provided these, their device then showed a landing page with a prominent QR Code. Upon request, we also printed this same code onto a 2" x 2" label. When someone else scanned their QR Code, the new person (not yet a participant) was given a pending connection and asked to visit our booth. When/if they signed up, the connection was vested and both parties were connected, with one point each. When a participant scanned the QR Code of another participant, a connection was immediately formed and both parties were connected, with one point each. We maintained and displayed a "Top Five Scores" page. At the end of each day, we awarded a prize to the person with the most connections.
Implementation
In addition to the owner picos created during the first two days of the conference, the game requires one pico (also an owner pico) to manage the game itself, and another pico (a direct child of the game pico) to collect the participants. At the conclusion of the contest, these two picos can be removed (as their purpose has been fulfilled), while the owner picos can remain, along with their inter-connections, forever (meaning so long as they are not destroyed, and continue to be hosted).
Game pico
The game pico is an owner pico named "OpenWest 2018" and manages the game. Participants in the game are assigned a pin which is a four digit number. When a pin is given out, it is provided as the last four digits of a 15 digit number. The other 11 digits are computed in such a way as to be unique for a given pin. This prevents guessing, and makes even a brute force search expensive. In its 15 digit form, the pin is called a "tag" because it is used to create a QR Code encoding a sign-up or registration event. A sign-up page, at a kiosk at our booth, presents to a potential participant a QR Code (containing the tag) which they can scan to join the contest.
The game pico has three rulesets, named "OpenWest2018.keys", "OpenWest2018.tags", and "OpenWest2018.ui" which manage the sign-up process. These rulesets are described here. A casual reader may wish to read the short description of each ruleset, skipping the implementation details. The first ruleset encapsulates proprietary algorithms, but the last two rulesets are available on GitHub.
keys ruleset
The game pico has a ruleset, named "OpenWest2018.keys" which keeps track of the next available pin, provides sign-up tags, validates sign-up tags, converts sign-up tags into a pin. This ruleset is used as a module by the tag and ui rulesets described later. A casual reader may wish to skip the implementation details, and go directly to a discussion of the tags ruleset.
Implementation details
The ruleset sets the next available pin, provides the next available pin (as a tag), and prevents re-use, with the rules shown below.
The initialization rule (lines 3-8), selected when the ruleset is installed in a pico (this installation makes it be the game pico), sets the next available pin to 1000. A start_ids ruleset (lines 9-14) allows the game operator to set a different starting pin. We tested the game several times, each time using a different range of pins. The live game used a starting pin of 2600.
ruleset OpenWest2018.keys {
...
rule intialization {
select when wrangler ruleset_added where event:attr("rids") >< meta:rid
fired {
ent:current_pin := 1000;
}
}
rule start_ids {
select when ids starting_pin
fired {
ent:current_pin := event:attr("ord").as("Number");
}
}
rule next {
select when ids need_next
pre {
_headers = event:attr("_headers");
ok = ...;
}
if ok then
send_directive("next",{"id":tag_id(ent:current_pin.as("String"))})
fired {
ent:current_pin := ent:current_pin + 1;
}
}
rule avoid_reuse {
select when ids id_used
pre {
pin = as_pin(event:attr("id")).as("Number");
}
if pin >= ent:current_pin then noop();
fired {
ent:current_pin := pin + 1;
}
}
}
The most-used rule, named simply next, in lines 15-26, provides the 15 digit number needed to create the QR Code tag, which is scanned by a participant to register for the game. Line 19 has elided code (which uses HTTP headers to validate the originating system) so that the game can't be gamed by creating fake participants. The avoid_reuse rule (lines 27-36) allows sanity checking that a pin is never re-used.
The functions tag_id, as_pin, and valid are defined as follows:
The function tag_id (lines 11-16) acts on a four digit number (passed in as a String) to produce the only valid 15 digit number (returned as a String) which ends with those four digits. In line 13, we build a String from the four digit number in a repeatable way. Line 14 computes a 64 character hash of that String, from which we extract 9 hexadecimal digits (in a repeatable way) and convert these to decimal. (The hex2dec function is as defined in the page Decoding WWW-Form Encoded Content or Query Parameters.) Finally (line 15), we ensure the resulting number is an eleven digit string and append the pin to it to produce the complete 15 digit tag id.
The function as_pin (lines 17-19) extracts the pin from a tag id, and the function valid (lines 20-22) returns true iff a proposed tag id is one of the one-in-a-hundred-billion valid ones.
tags ruleset
The game pico also has a ruleset named "OpenWest2018.tags" which reacts to the tag:scanned event produced when someone scans one of the sign-up QR Codes. The handling of this event is shown in the diagram below. The event selects four rules, which are evaluated in order left-to-right (code shown in the implementation details section later). If the tag is invalid, it is simply rejected. The ruleset keeps track of tags which it has seen, so the first_scan rule handles the case where this is the first time the pico has seen the tag which was scanned. Four things happen: 1) an owner:creation event is sent to the root pico, to create an owner pico named by the pin; 2) an ids:id_used event is raised to this same pico as a sanity check; 3) a tag:first_scan event is raised to this same pico (and will be handled by a ruleset named "OpenWest2018.ui" discussed later); and, 4) the event sets a cookie in the device which scanned the tag, so that we know its pin in subsequent scans.
Continuing across the rules which are selected by the tag:scanned event is the scanner_unknown rule which handles the case where the pico has seen the tag before, but cannot identify the device doing the scanning. This usually means that device has lost its cookies, and recovery is needed, so the rule raises the tag:recovery_needed event. Finally, the subsequent_scan rule fires when the pico has already seen this tag and the device scanning it is known.
The remaining events which are handled by the game pico include tag:initials_provided and tag:recovery_codes_provided. These both are triggered by owner actions, from the "Enter initials" page and the "Connection recovery" page, respectively. The first of these events, a normal part of the sign-up process, is selected by the record_initials rule of the tags ruleset and also by the first_welcome rule of the ui ruleset. The first of these just sends the event on to the attendees collection pico, while the latter produces a welcome page. The second event triggers the check_recovery_codes rule which, upon a successful match, raises the tags:recovery_codes_accepted event, which will be handled by the ui ruleset.
A casual reader may wish to skip the implementation details, and go directly to a discussion of the ui ruleset.
Implementation details
The game pico keeps track of two things: 1) the tags which it has already seen, and 2) the tags for which it still needs participant initials. This map and array are initialized when the tags ruleset is installed in the game pico (lines 3-10).
ruleset OpenWest2018.tags {
...
rule initialization {
select when wrangler ruleset_added where event:attr("rids") >< meta:rid
if ent:owners.isnull() then noop();
fired {
ent:owners := {};
ent:need_initials := [];
}
}
...
}
The reject_invalid_tag rule (lines 7-18) is the first rule to be evaluated when the game pico receives the tag:scanned event. It uses the keys ruleset as a module (line 3) so that it has access to that ruleset's valid function (in line 13).
ruleset OpenWest2018.tags {
meta {
use module OpenWest2018.keys alias ids
...
}
...
rule reject_invalid_tag {
select when tag scanned
pre {
candidate_id = event:attr("id");
ok = candidate_id.typeof() == "String"
&& candidate_id.length() == 15
&& ids:valid(candidate_id);
}
if not ok then
send_directive("invalid tag",{"id":candidate_id,"page":"reject"})
fired { last; } // may need to ban
}
...
}
In the case where the id value provided with the event is not present (line 11) or is not the correct length (line 12) or is invalid (line 13) this rule sends a rejection directive (line 16) and indicates to the pico engine that it will be the last event in this ruleset to be processed for this event (line 17).
The next rule to be processed (assuming the id is valid) , named tag_first_scan (lines 13-33), shown here:
ruleset OpenWest2018.tags {
meta {
use module OpenWest2018.keys alias ids
use module io.picolabs.wrangler alias wrangler
...
}
global {
child_specs = {
"rids": ["io.picolabs.subscription","OpenWest2018.attendee"] };
...
}
...
rule tag_first_scan {
select when tag scanned id re#^(\d{15})$# setting(id)
pre {
key = id;
pin = ids:as_pin(id);
}
if not (ent:owners >< key) then every {
send_directive("first scan",{"id": id,"page":"sign-up"});
event:send({"eci":wrangler:parent_eci(),
"domain": "owner", "type": "creation",
"attrs": child_specs.put({"name":pin})
});
}
fired {
ent:owners{key} := time:now();
ent:need_initials := ent:need_initials.defaultsTo([]).union([pin]);
raise ids event "id_used" attributes event:attrs;
raise tag event "first_scan" attributes event:attrs;
last;
}
}
...
}
The rule gives the event attribute id another name, key (line 16), and computes the pin by calling the as_pin function in the ids module (line 17). If the game pico has not yet seen this id number (line18) then it takes two actions (lines 20-24) and impacts its own state with four effects (lines 27-30). The actions are 1) returning a "first scan" directive (line 20); and 2) sending an owner:creation event (lines 21-24) to its parent pico (the root pico) which will cause a new owner pico to be created, named by the pin, with listed rulesets (lines 8-9) pre-installed.
The changes to its own state are: 1) storing the id (aka key) into its entity variable named owners (line 27); 2) adding this pin to its ent:need_initials entity variable (line 28); 3) raising the ids:id_used event to the same pico (line 29) (handled by the keys ruleset); and 4) raising the tag:first_scan event to the same pico (line 30) (handled by the ui ruleset). This rule is also declared to be the last rule in this ruleset to process this event (line 31).
When the ui ruleset sends back the "Enter initials" page, it also sets a cookie in the device which did the scanning. The cookie is named "whoami" and has, for its value, the assigned pin.
When a sign-up tag is scanned, the scanner_unknown rule will be evaluated when the game pico has already seen this id. This rule, shown here in lines 9-24, will use three rulesets as modules (lines 3-5): the keys ruleset, wrangler (installed in all picos), and cookies.
ruleset OpenWest2018.tags {
meta {
use module OpenWest2018.keys alias ids
use module io.picolabs.wrangler alias wrangler
use module io.picolabs.cookies alias cookies
...
}
...
rule scanner_unknown {
select when tag scanned id re#^(\d{15})$# setting(id)
pre {
whoami = cookies:cookies(){"whoami"}.klog("scanned_by");
pin = ids:as_pin(id);
need_initials = ent:need_initials >< pin;
}
if whoami.isnull()
then send_directive("unknown scanner",{"id": id,"page":"recovery"});
fired {
raise tag event "recovery_needed" attributes event:attrs if not need_initials;
raise tag event "still_need_initials" attributes event:attrs if need_initials;
last;
} else {
ent:scanned_by := whoami;
}
}
...
}
This ruleset will be selected (line 10) when the tag:scanned event occurs with an attribute named id which is a fifteen digit number. However, other rules will have also been selected and we will only reach this rule if neither of those have declared themselves to be the last rule to evaluate. This means that the id number is valid and has been seen before.
The rule looks in the HTTP headers for a cookie named "whoami" (line 12). Line 13 extracts the pin from the id (which we know is valid because the reject_invalid_tag rule, evaluated before this one, did not fire). Line 14 determines whether we still need to record the initials (short name and tag-line). If there is no "whoami" cookie, this rule fires, and the action is to send an "unknown scanner" directive (lines 16-17). Effects on this pico are: 1) raise the tag:recovery_needed event if we have already received initials for this pin; 2) raise the tag:still_need_initials event otherwise; and 3) let the engine know that this will be the last rule evaluated.
ui ruleset
The game pico also has installed a ruleset named "OpenWest2018.ui" which handles the generation of the web pages which make up the UI surrounding the scanning of the sign-up tag.
The ruleset provides five rules, which have a common structure, shown by exemplar in the implementation details section. Each rule receives the id as an event attribute and computes the pin using the keys ruleset as a module aliased as ids. Each rule computes an HTML page as appropriate for its place in the game, sends a directive named "_html" and possibly a directive named "_cookie". These directives are intercepted by the pico engine and result in the return of an HTML document (masking any other directives), and an HTTP header setting a cookie in the device which caused an event to be received.
rule tag_first_scan
This rule selects for the tag:first_scan event or the tag:still_need_initials event. It computes the pin and causes a cookie named "whoami" to be set in the originating device, whose value is the pin. It also returns the "Enter initials" page.
rule tag_recovery_needed
This rule selects for the tag:recovery_needed event. It computes the pin and returns the "Connections recovery" page.
rule tag_subsequent_scan
This rule selects for the tag:subsequent_scan event. It computes the pin and returns the "Welcome" page.
rule tag_initials_provided
This rule selects for the tag:initials_provided event. It computes the pin and returns a "Welcome" page.
rule tag_recovery_codes_accepted
This rule selects for the tag:recovery_codes_accepted event. It computes the pin and returns the "Welcome" page. Also sets the cookie.
Implementation details
One of the rules shown here. The others are similar.
rule tag_first_scan {
select when tag first_scan
or tag still_need_initials
pre {
id = event:attr("id");
pin = ids:as_pin(id);
}
every {
send_directive("_cookie",{"cookie":<<whoami=#{pin}; Path=/>>});
send_directive("_html",{"content":html(id,pin)});
}
}
The game pico has one child pico, named "Attendees" which collects all of the participant owner picos. This pico serves as a centralized place to know about all of the picos involved. It expects subscription requests from new owner picos. It keeps track of the current score of each owner pico. It is known by the game pico (as the latter's only child pico) and so passes initials and tag-line received by the game pico to the appropriate owner pico. Finally, it has two rules for maintenance purposes: one to reconstruct its collection, and another to allow for two days of gaming.
The collection pico is primarily a collection, so it has the io.picolabs.collection ruleset installed. This ruleset accepts subscription requests from owner picos, which raises the collection:new_member event to the same pico. This ruleset also allows the person running the game to easily remove all the subscriptions preparatory to deleting the collection pico.
collection ruleset
rule new_member
This rule maintains a map from the channel event identifier of the new owner pico to its name. It reacts to the collection:new_member event.
rule update_high_scores
This rule reacts to the attendee:new_connection event, sent by a participant's owner pico when it has made a new connection. The collection pico maintains the scores in its ent:scores entity variable.
rule inform_attendee_of_initials
This rule serves as glue between the game pico, which knows the pin and has the initials and tag-line, and the participant's owner pico. The collection pico is known by the game pico (as its only child) and has a channel event identifier to the owner pico (through its subscription).
rule sync_members
This rule can be used by the game operator to reconstruct the ent:attendees and ent:scores entity variables if needed. One case in which this is needed is between the two days of a game, after using the attendees:new_day event.
rule roll_over_past_scores
This rule reacts to the attendees:new_day event by making a copy of the ent:scores entity variable into a new ent:old_scores entity variable. After using this rule, the game operator will need to send the collection pico the attendees:need_sync event (see above).
Implementation details
The collection pico reacts to the collection:new_member event by maintaining a simple map from the new member's ECI, known to it as Tx to the name (aka the participant's pin).
rule new_member {
select when collection new_member
pre {
key = event:attr("Tx");
name = event:attr("name");
}
fired {
ent:attendees{key} := name;
}
}
Later, given a pin, the collection pico can obtain an ECI to the corresponding owner pico, using a pin_as_Rx function.
When a participant owner pico makes a new connection, it sends an updated connection count to the collection pico. It identifies itself with the id of the subscription linking it to the collection pico. It also includes its new connection count, in two ways. First as a numeric event attribute (retrieved in line 5), and secondly in a signed map (retrieved and verified in lines 8-10). This event includes a signed component so that a participant cannot game the game by simply sending this event to the collection pico asserting a high connection count.
Line 12 verifies that the two connection counts are equal, and if so stores (line 14) the new connection count (less any scores from the previous day, if any). In this case it raises (line 15) the attendees:scores_changed event (currently not used). Otherwise, it raises an attendees:under_attack event (lines 17-19) which is also not used currently.
For the implementation details of the other rules, please see the GitHub repository.
collection.ui ruleset
This ruleset shares a single function, named high_scores_page, which produces the leaderboard page (shown below after the first day of the contest). See the complete ruleset on GitHub.
Implementation details
The high scores page is available from any of the URLs shown below. The first one serves an index.html page which uses an HTML <meta http-equiv="refresh" ...> tag to redirect to the second one which is more widely published. The second one, which the reader will note, uses port 80, is re-written by the Apache mod_rewrite module to the third one, so that a CGI script is executed. That script delegates to the fourth line which is re-written by the pico engine to the fifth and final one, which actually invokes the function defined in the ruleset:
where the particular ECI shown is one belonging to the collection pico used during OpenWest. Note that the fourth URL is not a standard /sky/cloud URL. We are using a URL shortener (registered with the root pico of the game pico engine). What this allows us to do is, after deleting the game and collection picos, create them anew for a different instance of the game without having to modify the CGI script.
Owner picos
Each participant owns an owner pico, named by the owner pin. Each owner pico has a subscription to the collection pico, in which it plays the role of "member".
The owner pico is created by the root pico (when it reacts to an owner:creation event sent to it by the game pico). One of the event attributes is a list of rulesets to install in a newly created pico. The game pico asks for the "OpenWest2018.attendee" ruleset to be installed.
When the new pico receives the wrangler:ruleset_added event about this ruleset, the corresponding rule is selected and it requests the installation of the remaining rulesets needed: "OpenWest2018.attendee.ui", "OpenWest2018.contact_info", "OpenWest2018.export", and "io.picolabs.pds". It also requests the creation of an introduction channel.
As the user provides initials and a tag-line, rules in this ruleset respond appropriately. These actions are shown in the first of two diagrams for the owner pico.
The initialization rule actually raises four events: 1) visual_params:config setting the pico's display size; 2) wrangler:subscription connecting to the collection pico; 3) wrangler:channel_creation_request described below the diagram; and 4) wrangler:install_ruleset_requested to install the remaining rulesets needed.
Notice that the initialization rule raises the wrangler:channel_creation_requested event. This is handled by a rule in the wrangler ruleset (which is installed in every pico). Ultimately, that rule will raise the wrangler:channel_created event to which the record_intro_channel rule (of the attendee ruleset) will react.
When the collection pico has sent the about_me:signup_complete event to new owner pico, the attendee handle_pending_connections ruleset will simulate for the new participant the scanning of tags which she already actually did scan, making the pending connections count (by raising the tag:scanned event).
Another purpose of the attendee ruleset is to deal with another participant scanning this pico's introduction QR Code, which encodes the tag:scanned event sent to the pico's introduction channel.
When the tag:scanned event sent to the pico's introduction channel, the pico engine selects five rules in the attendee ruleset: 1) identify_scanner; 2) handle_unknown; 3) avoid_self; 4) avoid_dup; and 5) make_connection. These rules work together in a chain to: 1) obtaining the pin of the person scanning my introduction QR Code; 2) deal with not knowing who that is; 3) not making a connection to myself; 4) not making a connection to someone with whom this pico is alread connected; and 5) if everything is ok, actually making the connection (see the right hand side of the diagram).
Making a connection is a chain of events started by the make_connection rule raising (to the same pico) the wrangler:subscription event, which proposes a subscription to the other owner pico. The other owner pico will receive the wrangler:inbound_pending_subscription_added event, running its auto_accept rule which will fire, raising the wrangler:pending_subscription_approval event. This owner pico will then receive the wrangler:subscription_added event, which will select two rules: 1) record_new_contact_pin which will record the connection internally, and 2) report_connection_count which will send a signed attendee:new_connection event to the collection pico.
This ruleset is responsible for generating all of the HTML pages required by the various paths from rules reacting to the tag:scanned event.
contact_info ruleset
This ruleset maintains name, address, phone numbers, email address, etc. for the participant.
export ruleset
This ruleset provides JSON and CSV export formats for the information in this owner pico.
io.picolabs.pds ruleset
This ruleset provides key/value storage for the contact_info ruleset.
Kiosk pico
This pico runs on a different pico engine, running on a machine at our booth. It has a ruleset "OpenWest2018.kiosk" which provides a decorative screen, which when touched links to the file kiosk.html. This HTML page uses AJAX to request an introduction tag id (which will be the next valid 15 digit number) from the game pico (via its keys ruleset), and creates and displays a QR Code for a tag:scanned event to the game pico. The URL encoded is:
Note that this is not a standard /sky/event URL. We are using a URL shortener (registered with the root pico of the game pico engine) so that the QR Code doesn't need to be as precise as it might have been. A side-effect of this is that the ECI used for the game pico is not made publicly available (to someone who might read the URL from the result of the device scanning the tag). The shortener internally redirects to:
where the particular ECI shown here is one belonging to the game pico used during OpenWest.
Conclusions
We had a lot of fun running the game, and had many conversations about picos. Putting the code together required the use of many techniques, most of which are documented in the KRL Cookbook section of the documentation.
rolling over from one contest day to the next without losing any peer to peer connections
Owner picos, the very idea
Several people wondered just what we meant by something said to many, "When you participate in this game, we are giving you an owner pico."
For this ownership to be meaningful, and secure, we at Pico Labs either need to be serious about hosting picos, or we need to encourage people to set up their own engines and move their owner pico from our provisional engine to their own.