Skip to content

Tracking01

Tim Erickson edited this page Sep 27, 2018 · 1 revision

Tracking an Event #1: catching fish

A user action can take a tortured path through an app. Tim hopes that following an event through code will help the reader understand what's going on.

We'll look at fish and what happens when the user clicks the button for catching fish.

The setup

The button is in the html, like this:

    <span id="catchManualSpan">
        <label id="fishQuantityLabel" for="howManyFish">xxx HOW MANY xxx</label>
        <input type="number" id="howManyFish">
        <button id="catchButton" onclick="fish.userActions.catchFish()"> xxx CATCH xxx</button>
    </span>

The label and the button title will have been replaced in the appropriate language. So when the user clicks on the button, it calls fish.userActions.catchFish().

fish.userActions.catchFish()

This is in fish.userActions.js. An abbreviated version of the function looks like this:

    catchFish: async function () {

        let tSought = Number($("#howManyFish").val()); // get how many from the text box
        $("#catchButton").hide();       //  hide immediately after pressing the button

        if (fish.readyToCatch()) { // [1]
            fish.state.playerState = fish.constants.kSellingString;     //  set the player state to selling [2]
            const tCatchModelResult = await fish.model.catchFish(tSought);  // [3]
            await fish.phpConnector.newCatchRecord(tCatchModelResult);  //  [4]
            await fish.CODAPConnector.addSingleFishItemInCODAP(tCatchModelResult);  //  [5]

            fish.state.currentTurnResult = tCatchModelResult; [6]
        } 
    },
  1. This should never evaluate to false, so we will ignore it for now
  2. We maintain some variables about the "player state" and the "game state" to determine what's possible at any given time. We have been fishing; now we're selling. The .ui methods will use this to keep the catch button invisible.
  3. The model takes the number we want to catch, tSought, and makes an object tCatchModelResult containing useful things. More on that soon.
  4. We store values from that result in the database; phpConnector.newCatchRecord() is in charge of that.
  5. Likewise we store results in CODAP, using CODAPConnector.addSingleFishItemInCODAP()
  6. Finally, we store the result object as the most recent result; we can use that for other purposes.

fish.model.catchFish(tSought)

Again, an abbreviated listing of the function. This is in fishModel.js:

    catchFish: async function (iSought) {
        const tGame = await fish.phpConnector.getGameData();  // [1]

        try {
            // limit our catch to what we can see [2]

            let tMaxPossibleCatch = iSought;
            let tPop = Number(tGame['population']);          
            const   tVisible = Math.round(fish.game.visibleProbability * tPop);
            if (tMaxPossibleCatch > tVisible) { tMaxPossibleCatch = tVisible;}

            //  now calculate how many we caught

            let tCaught = 0;
            if (fish.game.binomialProbabilityModel) { // [3]
                for (let i = 0; i < tVisible; i++) {
                    if (Math.random() < fish.game.catchProbability) {
                        tCaught++;
                    }
                }
            } else {
                tCaught = Math.round(fish.game.catchProbability * tVisible);
            }
            if (tCaught > tMaxPossibleCatch) { tCaught = tMaxPossibleCatch; } // [4]
            let tExpenses = fish.game.overhead;

            return {    // [5]
                sought: iSought,
                visible: tVisible,
                caught: tCaught,
                expenses: tExpenses
            };
        } catch (msg) {
            console.log('model catch fish error: ', msg);
        }
    },
  1. We need the true population of fish to do our calculations. So we go to the database (via phpConnector) to find out.
  2. This section limits the maximum catch to what we can see. That number, tVisible, is a fraction of the population. In fact, the relevant number, visibleProbability, is about 0.5; it's set in fishGameConfigurations.js.
  3. Some configurations (levels) use a binomial probability model to determine how many you can see (which I have excised) or how many you actually catch. The relevant rate, catchProbability, is between 0.6 and 1.0.
  4. Just in case, we should check this and cap the result at the maximum we computed earlier.
  5. This is the object we return to the method in fish.userActions; it will be transmitted to php for insertion into the database, and to CODAPconnector for insertion into the CODAP data table.

fish.phpConnector.newCatchRecord(tCatchModelResult)

That model result, with sought, visible, caught, and expenses, gets passed to the php connector file in this function:

    newCatchRecord: async function (iModelResult) {

        try {
            const theCommands = {       // [1]
                gameCode: fish.state.gameCode,
                playerName: fish.state.playerName,
                onTurn: fish.state.turn,
                visible: iModelResult.visible,
                sought: iModelResult.sought,
                caught: iModelResult.caught,
                balanceBefore: fish.state.balance,
                expenses: iModelResult.expenses,
            };

            theCommands.c = "newCatchRecord"; // [2]
            const iData = await fish.phpConnector.sendCommand(theCommands);      //  [3] returns {caught : 17}
            return iData;
        }
    },
  1. We assemble this theCommands object that contains all of the variables and values we will want to transmit and save.
  2. In addition, we have a special one, keyed c, that holds a string that's our command to php.
  3. That theCommands object is the sole argument to sendCommand (at the top of this file), which actually sends the command to php. Read about that in the page on php and mySQL.

When the php has communicated with MySQL, the result (iData here) gets returned, unused at this time, to the method in userActions.

In fish.php

Now we look at fish.php. The method fish.phpConnector.sendCommand uses fetch() to access php using PDO. The commands in theCommands get translated into the php global $_REQUEST as an associative array. After connecting to the database, the script extracts the c command, like this:

$command = $_REQUEST["c"];     //  this is the overall command, the only required part of the POST

switch ($command) {
    case 'foo' :
        (etc)

We will look for newCatchRecord of course, and find this:

    case 'newCatchRecord':
        $params = array();
        $namesA = array();

        foreach ($_REQUEST as $key => $value) {      //  [1]
            if ($key != "c" && $key != "whence") {
                $params[$key] = $value;
                array_push($namesA, $key);
            }
        }

        $caught = $params['caught'];

        $names = implode(",", $namesA);    // [2]
        $values = implode(",:", $namesA);

        //  insert a new turns record. Notice the syntax with the colons...
        $query2 = "INSERT INTO turns (" . $names . ") VALUES (:" . $values . ")";   // [3]
        $out = CODAP_MySQL_doQueryWithoutResult($DBH, $query2, $params);      // [4]

        $out = json_encode(array('caught' => $caught));    // [5]
        break;
  1. Most of the elements in the theCommands object were values we want to store, and we have carefully named them with the same names they need in mySQL. But there are two interlopers: command and whence (which got inserted into the stream while we were not looking). So this loop constructs the $params array to be the same as $_REQUEST except for not having those two fields.
  2. The syntax of the mySQL query is something like, INSERT into MYTABLE var1, var2, var3 VALUES 4, 5, 6. That is, the names of the variables and the values are separated by commas. So we make strings ($names, $values) that contain those comma-separated representations.
  3. EXCEPT THAT the values are not actual values, but rather the names again, with colons before them. These will get replaced by the actual values inside MySQL; it will find those values using $params. That is, the query that we send to MySQL is not the final query; it looks like: INSERT into MYTABLE var1, var2, var3 VALUES :var1, :var2, :var3.
  4. Now we call our utility query-sending routine, using $DBH, the indispensable database handle. Because this is an INSERT query, it returns nothing. A SELECT would return an array of associated arrays holding the returned data.
  5. Just so we can return something of potential use, we construct a JSON object containing the number of fish caught.

fish.CODAPConnector.addSingleFishItemInCODAP(tCatchModelResult)

Next, we send the same object (with sought, visible, caught, and expenses) to be installed into CODAP. Here is the function in fish.CODAPConnector.js:

    addSingleFishItemInCODAP: async function (iModelResult) {
        let aTurn = {
            year: Number(fish.state.turn),
            seen: iModelResult.visible,
            want: iModelResult.sought,
            caught: iModelResult.caught,
            before: fish.state.balance,
            expenses: iModelResult.expenses,
            player: fish.state.playerName,
            game: fish.state.gameCode
        };

        await pluginHelper.createItems(aTurn, fish.constants.kFishDataSetName);
        return aTurn;  
    },

As you can see, we just create an object with the names and values we want and send it off to pluginHelper.createItems. About half of these items come from the model result we just passed in; the rest (e.g., the year) we already know from their attachments as members of fish.state.

And that's it!

The data are all where they need to be. In the case of this plugin, the timer polls the database to see what the game year is; when it advances, we go see how much our fish sold for, and get our "catch fish" button restored for the next turn.

Clone this wiki locally