LiveCode Stock Trader: Part 2

Posted by Scott McDonald

Here is Part 2 for the Stock Trader Game. While Part 1 covered the overall structure of the game, much of the code was calling handlers in another file. This article looks at those functions and commands. This code is less directly related to the gameplay of Stock Trader, but without it the game would not work. Making games is unfortunately, how should I say, not always fun and games.

In this code you will see:

  • Parameters passed by reference
  • Handlers that accept a (no pun intended) variable number of parameters
  • The power of the UPDATE statement in SQL
  • Loops that access repeated HTML form elements with a minimum of code
  • Simplifying code by putting common actions into a tiny library of re-usable handlers

In this part, the code in trader-fns.lc is examined. This file stores some of the "lower-level" functions of the game and is an important part of making the Stock Trader work.

Let's begin. A data file named stockvars.cfg sits on The LiveCode Lab server in the same folder as the Stock Trader code. This file stores the information about the current trends of the stock market. The file is read into a variable, which is then split into lines to put each value into the parameters of loadVars. With one value per line it is easy to extract the values from the data.

Note the @ in front of each parameter name. The @ indicates that these parameters are "passed by reference". What this means is that putting a value into, for example, pTrendLength actually puts the value into the variable that was passed to loadVars. Using parameters in this way can be convenient in cases where the single value returned by function is not enough.

If pTrendLength is empty, which is the case the first time the game is run, then the variables are filled with default values.

command loadVars @pTrendLength,@pTrendSlope,@pTrendUpCounter,@pTrendDownCounter,@pTrendUpStock,@pTrendDownStock
  local libBuffer
  put URL("file:stockvars.cfg") into libBuffer
  put line 1 of libBuffer into pTrendLength
  put line 2 of libBuffer into pTrendSlope
  put line 3 of libBuffer into pTrendUpCounter
  put line 4 of libBuffer into pTrendDownCounter
  put line 5 of libBuffer into pTrendUpStock
  put line 6 of libBuffer into pTrendDownStock
  if pTrendLength is empty then
    put Random(5) into pTrendLength
    put Trunc(Random(20) - 10)/1000 into pTrendSlope
    put 0 into pTrendUpCounter
    put 0 into pTrendDownCounter
    put empty into pTrendUpStock
    put empty into pTrendDownStock
  end if
end loadVars

The storeVars handler, does the reverse of loadVars. It uses the parameters to construct the lines in a buffer which is then written back to the stockvars.cfg file. But where are the parameters in the declaration of storeVars? They are not declared, but instead the param function is used within a loop that repeats according to the value in the paramCount property. The param function allows you to access parameters without knowing how many there will be.

While not essential in storeVars (I could have just listed them like in loadVars), using this technique means that provided the calls to loadVars and storeVars have the same parameters as shown previously in Part 1, then storeVars never needs to be changed. Even when if the variables to be stored change.

command storeVars
  local libBuffer = ""
  repeat with loopIndex = 1 to the paramCount
    put param(loopIndex) & cr after libBuffer
  end repeat
  put libBuffer into URL("file:stockvars.cfg")
end storeVars

The loadStockHistory and storeStockHistory handlers retrieve and store the stock prices. Again this data is stored in a plain text file because the data is mostly read-only and the extra resources required for a SQL table are not justified, or necessary. See how the stockvars.cfg and stockhistory.cfg files both use the cfg extension? This is done because The LiveCode Lab specifically denies permission for files with the cfg extension from being served up to a web browser. This means no one can peek inside the contents of these files, even when the URL is known.

For example, try:

http://thelivecodelab.com/original-demo/stock-trader-game/stockvars.cfg

in your browser and see that it returns a "You don't have permission" type of message. This is convenient because even when your app is Public at The LiveCode Lab and the source is available, provided the name of a file uses the cfg extension, the content is hidden from everyone. Which is important, for many types of apps, including games. You don't want people cheating by peeking at the state of the variables in your game.

command loadStockHistory @pStockHistory
  put URL("file:stockhistory.cfg") into pStockHistory
  if pStockHistory is empty then
    put the seconds - 100,10000,8500,15000,14000,11000 into pStockHistory
  end if
end loadStockHistory

command storeStockHistory pStockHistory
  put pStockHistory into URL("file:stockhistory.cfg")
end storeStockHistory

The updateStockHistory handler is long, but in summary, it updates the trends in the stocks prices and sets the prices for a new day. As mentioned in Part 1, this is only called once every 24 hours. The first player who happens to login when 24 hours have elapsed since the last call makes it execute. In updateStockHistory, lots of random numbers are used to:

  • Select one stock that is rising more than the others.
  • Select one stock that is falling more than the others.
  • Set the overall trend, i.e. are the stocks generally rising or falling and by how much.

The randomStockList variable is used to make sure the same stock cannot be set to rise and fall at the same time. That wonderful LiveCode any keyword is used to randomly pick any item from the comma delimited list in randomStockList.

put any item of randomStockList into pTrendUpStock

Expressions like this are very readable and help make LiveCode self-documenting when used with meaningful variable names.

command updateStockHistory pStockCodes,pTradeInterval,@pStockHistory,@pValueArray,@pTrendLength,@pTrendSlope,@pTrendUpCounter,@pTrendDownCounter,@pTrendUpStock,@pTrendDownStock
  put pStockCodes into randomStockList
  if pTrendUpCounter > 0 then replace pTrendUpStock with empty in randomStockList
  if pTrendDownCounter > 0 then replace pTrendDownStock with empty in randomStockList
  if pTrendUpCounter = 0 then
    put item 1 of pStockCodes into highestStock
    repeat for each item loopStockCode in pStockCodes
      if pValueArray[loopStockCode] > pValueArray[highestStock] then
        put loopStockCode into highestStock
      end if
    end repeat
    put any item of randomStockList into pTrendUpStock
    repeat while (pTrendUpStock = highestStock) or (pTrendUpStock is empty)
     put any item of randomStockList into pTrendUpStock
    end repeat
    replace pTrendUpStock with empty in randomStockList
    put 1 + random(4) into pTrendUpCounter
  end if
  if pTrendDownCounter = 0 then
    put item 1 of pStockCodes into lowestStock
    repeat for each item loopStockCode in pStockCodes
      if pValueArray[loopStockCode] < pValueArray[lowestStock] then
        put loopStockCode into lowestStock
      end if
    end repeat
    replace lowestStock with empty in randomStockList
    put any item of randomStockList into pTrendDownStock
    repeat while pTrendDownStock is empty
     put any item of randomStockList into pTrendDownStock
    end repeat
    replace pTrendDownStock with empty in randomStockList
    put 1 + random(4) into pTrendDownCounter
  end if
  subtract 1 from pTrendUpCounter
  subtract 1 from pTrendDownCounter
  put item 1 of pStockHistory + pTradeInterval into newStockLine
  repeat for each item loopStockCode in pStockCodes
    put random(100) into overallFactor
    put 0 into trendingFactor
    if loopStockCode = pTrendUpStock then put 250 + Random(250) into trendingFactor
    if loopStockCode = pTrendDownStock then put - 250 - Random(250) into trendingFactor
    put Trunc(pTrendSlope * pValueArray[loopStockCode]) + overallFactor + Trunc(3 - Random(6)) + trendingFactor into stockDelta
    add round(stockDelta) to pValueArray[loopStockCode]
    put 1000 + Random(500) into stockFloor
    if pValueArray[loopStockCode] < stockFloor then put stockFloor into pValueArray[loopStockCode]
    put comma & pValueArray[loopStockCode] after newStockLine
  end repeat
  subtract 1 from pTrendLength
  if pTrendLength = 0 then
    put Trunc(Random(20) - 10)/1000 into pTrendSlope
    put 1 + Random(5) into pTrendLength
  end if
  put newStockLine & cr before pStockHistory
end updateStockHistory

The updateUserAssets handler uses SQL to update each players total_assets which is necessary when the stock prices have been updated after the call to updateStockHistory. Note the contents of sqlQuery has text like ^1 ^2 etc. The subText function examined later substitutes values into this text before executing the query. If you have used SQL in LiveCode you may know that revExecuteSQL already has a feature that allows placeholders to be used, but which uses the : character. So why bother with the extra coding required for subText?

Three reasons:

  1. I use the technique of placeholders in a lot of my coding. And so for consistency I use my own function subText not just for SQL, but in many places.
  2. Sometimes, in other programs, the colon occurs in the text that has placeholders, and so to minimise problems the ^ (caret symbol) is used.
  3. Do you find the ^1 easier to read than :1, or is it just me?

Getting back to the SQL UPDATE statement, this shows the power and convenience of SQL, on top of the usual database feature of integrity when there is multi-user access.

This single statement updates the total_assets field for all players with a single SQL call. No explicit looping is required, the SQL applies the formula to all players, whether there is 1 or one thousand. How cool and convenient it that?

command updateUserAssets pDBID,pStockCodes,pValueArray
  put "UPDATE stockuser SET total_assets = cash + holding_aapl * ^1 + holding_googl * ^2 + holding_ibm * ^3 + holding_intc * ^4 + holding_msft * ^5;" into sqlQuery
  put subText(sqlQuery,pValueArray[item 1 of pStockCodes],pValueArray[item 2 of pStockCodes],pValueArray[item 3 of pStockCodes],pValueArray[item 4 of pStockCodes],pValueArray[item 5 of pStockCodes]) into sqlQuery
  revExecuteSQL pDBID,sqlQuery
end updateUserAssets

ValidTrades is called in response to a click on the Make Trades button and the form being submitted with the buying and selling details. It first checks that a plain number or a number prefixed by the dollar sign is in the HTML input text. Because the HTML text input elements have names based on the stock codes it is simple to iterate across the elements with a repeat loop. Then for any text input that has a number greater than zero the associated radio button HTML element is checked to make sure either buy or sell is selected. Finally, a check is made that the player is able to buy or sell the amounts entered.

With all the above checks, if an invalid value is found, an appropriate message is returned by the ValidTrades function. If the trades are valid, then empty is returned.

function ValidTrades pStockCodes,pStockValues,pCash,pStockHolding,@pTradeAmount
  put empty into libPrompt
  put 0 into libTotal
  -- make sure all numbers
  repeat for each item loopStockCode in pStockCodes
    put $_POST["amount" & loopStockCode] into loopAmount
    if first char of loopAmount = "$" then
     delete first char of loopAmount
      if (loopAmount is not a number) or (loopAmount < 0) then
        put "Each dollar amount must be a positive number without commas." into libPrompt
        exit repeat
      end if
      put 100* loopAmount div pStockValues[loopStockCode] into loopAmount
    end if
    if (loopAmount is not an integer) or (loopAmount < 0) then
      put "Each amount must be zero, a whole positive number or a dollar value." into libPrompt
      exit repeat
    end if
    if loopAmount is an integer then
      if (loopAmount > 0) and ($_POST["radiAction" & loopStockCode] is empty) then
        put "Choose whether to buy or sell." into libPrompt
        exit repeat
      else
        if loopAmount > 0 then
          add loopAmount to libTotal
        end if
      end if
    end if
    put loopAmount into pTradeAmount[loopStockCode]
  end repeat
  if (libPrompt is empty) and (libTotal = 0) then
    put "Enter the amounts you want to trade." into libPrompt
  end if
  if libPrompt is empty then
    -- get amount of pCash after selling
    repeat for each item loopStockCode in pStockCodes
      if $_POST["radiAction" & loopStockCode] = "sell" then
        add pTradeAmount[loopStockCode] * pStockValues[loopStockCode] to pCash
      end if
    end repeat
    -- then check if have enough to do buying
    repeat for each item loopStockCode in pStockCodes
      if $_POST["radiAction" & loopStockCode] = "buy" then
        subtract pTradeAmount[loopStockCode] * pStockValues[loopStockCode] from pCash
        if pCash < 0 then
          put "You don't have enough cash to buy this much stock." into libPrompt
        end if
      end if
    end repeat
  end if
  if libPrompt is empty then
    repeat for each item loopStockCode in pStockCodes
      if $_POST["radiAction" & loopStockCode] = "sell" then
        if pTradeAmount[loopStockCode] > pStockHolding[loopStockCode] then
          put "You don't have that much stock to sell." into libPrompt
        end if
      end if
    end repeat
  end if
  return libPrompt
end ValidTrades

If ValidTrades returns an empty string, then MakeTrades is called. This has two similar loops to ValidTrades, but instead of just checking the values, the pCash variable and the pStockHolding array is updated with the changed amounts.

command MakeTrades pStockCodes,pStockValues,pTradeAmount,@pCash,@pStockHolding
  repeat for each item loopStockCode in pStockCodes
    if $_POST["radiAction" & loopStockCode] = "sell" then
      add pTradeAmount[loopStockCode]* pStockValues[loopStockCode] to pCash
      subtract pTradeAmount[loopStockCode] from pStockHolding[loopStockCode]
    end if
    if $_POST["radiAction" & loopStockCode] = "buy" then
      subtract pTradeAmount[loopStockCode] * pStockValues[loopStockCode] from pCash
      add pTradeAmount[loopStockCode] to pStockHolding[loopStockCode]
    end if
  end repeat
  put Round(pCash) into pCash
end MakeTrades

While pCash keeps track of the players cash, the CalculateStockAssets calculates the total value of the stock assets of the player, by multiplying the pStockHolding by the pStockValues for each stock.

function CalculateStockAssets pStockCodes,pStockValues,pStockHolding
  local libTotal = 0
  repeat for each item loopStockCode in pStockCodes
    add pStockValues[loopStockCode] * pStockHolding[loopStockCode] to libTotal
  end repeat
  return round(libTotal)
end CalculateStockAssets

The last handler that is specific to the game is nextTradePrompt. This calculates and constructs a prompt for the time before the next update in the stock prices. The value is converted to a string with appropriate unites of either hours, minutes or seconds for display on the screen.

function nextTradePrompt pLastUpdateTime,pTradeInterval
  local libSeconds,libMinutes
  put pLastUpdateTime + pTradeInterval - the seconds into libSeconds
  put libSeconds div 60 into libMinutes
  if libMinutes > 120 then return round(libMinutes / 60) & " hours."
  if libMinutes > 0 then return libMinutes & " minutes."
  return libSeconds & " seconds."
end nextTradePrompt

The remaining handlers are general purpose routines that are used for converting data from one format to another, and other general text handling. In subText the repeat loop does the substitutions in reverse order from highest to lowest. This is important for cases where there are more than nine placeholders. The existing code would fill both the ^1 and ^10 placeholders with the first parameter. By simply reversing the order of the loop, it ensures that the ^10 is filled first.

The alphaNumeric function is used when checking the player login name. Because the player name is stored in a SQL table, limiting the login name to alpha numeric characters is a simple but highly effective way of eliminating SQL injection hacks. This technique cannot be used in all cases, but it is always important to consider and prevent the possibility of harmful SQL code being entered.

The other handlers are left for you to figure out, as an exercise. (i.e. I am too lazy to explain all the code here.)

function convertLineToArray pLine,pKeys
  local libBuffer = ""
  local libIndex = 1
  repeat for each item loopItem in pLine
    put loopItem into libBuffer[item libIndex of pKeys]
    add 1 to libIndex
  end repeat
  return libBuffer
end convertLineToArray

function insertDecimal pText
  local libIndex,minusSign
  if first char of pText = "-" then
    put "-" into minusSign
    delete first char of pText
  else
    put empty into minusSign
  end if
  repeat 3 - length(pText) times
    put "0" before pText
  end repeat
  put "." before char -2 of pText
  put length(pText) - 5 into libIndex
  repeat while libIndex > 1
    put comma before char libIndex of pText
    subtract 3 from libIndex
  end repeat
  return minusSign & pText
end insertDecimal

function insertCommas pText
  local libIndex
  put Round(pText / 100) into pText
  put length(pText) - 2 into libIndex
  repeat while libIndex > 1
    put comma before char libIndex of pText
    subtract 3 from libIndex
  end repeat
  return pText
end insertCommas

function alphaNumeric pText
  local libValid
  put pText is not empty into libValid
  repeat for each char loopCh in pText
    if loopCh is not among the chars of "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" then
      put false into libValid
      exit repeat
    end if
  end repeat
  return libValid
end alphaNumeric

function passwordHash pPassword
  get binaryDecode("H*",sha1Digest(pPassword),pPassword)
  return pPassword
end passwordHash

function subText pText
  repeat with loopIndex = the paramCount down to 2
    replace "^" & loopIndex - 1 with param(loopIndex) in pText
  end repeat
  return pText
end subText

function defaultText pText,pDefault
  if pText is empty then
    return pDefault
  else
    return pText
  end if
end defaultText

function condText pCondition,pTrue,pFalse
  if pCondition then
    return pTrue
  else
    return pFalse
  end if
end condText

There is more code required for player registration and login, but it is more like web site code and not gameplay related. For a summary of how the login process works, an article in the revUP #177 Newsletter has more information.

That completes Part 2 of multi-player game in LiveCode. To find out more, the complete code is here: LiveCode Stock Trader Source. Or you can just play it: Play Stock Trader. You do need to register to play, but this is easy and doesn't require any personal details or an email address.

Happy Trading.

Tagged: advanced multiplayer online simulation

Friday, September 5, 2014

0 Responses

Be the first to make a comment.


COMMENTS ARE CLOSED

 

Legal Stuff

All blog posts are copyright and cannot be re-used without permission. But all the code and scripts are dedicated to the public domain. Use such code and scripts in any way you want, but I am not responsible if they don't work for you.

Contact

Further comments or feedback? You can contact me by email at: