-
Notifications
You must be signed in to change notification settings - Fork 1
Tracking01
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 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()
.
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]
}
},
- This should never evaluate to
false
, so we will ignore it for now - 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. - The model takes the number we want to catch,
tSought
, and makes an objecttCatchModelResult
containing useful things. More on that soon. - We store values from that result in the database;
phpConnector.newCatchRecord()
is in charge of that. - Likewise we store results in CODAP, using
CODAPConnector.addSingleFishItemInCODAP()
- Finally, we store the result object as the most recent result; we can use that for other purposes.
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);
}
},
- We need the true population of fish to do our calculations. So we go to the database (via
phpConnector
) to find out. - 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 infishGameConfigurations.js
. - 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. - Just in case, we should check this and cap the result at the maximum we computed earlier.
- This is the object we return to the method in
fish.userActions
; it will be transmitted to php for insertion into the database, and toCODAPconnector
for insertion into the CODAP data table.
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;
}
},
- We assemble this
theCommands
object that contains all of the variables and values we will want to transmit and save. - In addition, we have a special one, keyed
c
, that holds a string that's our command to php. - That
theCommands
object is the sole argument tosendCommand
(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
.
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;
- 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
andwhence
(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. - 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. - 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
. - Now we call our utility query-sending routine, using
$DBH
, the indispensable database handle. Because this is anINSERT
query, it returns nothing. ASELECT
would return an array of associated arrays holding the returned data. - Just so we can return something of potential use, we construct a JSON object containing the number of fish caught.
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
.
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.