GVar Dictionaries - theRAPTLab/gsgo GitHub Wiki
See #755 for discussion.
GVar Dictionaries adds the ability to define multiple static key
-value
pairings for character (agent) properties and to conduct test operations on the dictionary values.
- Dictionary items are defined via an
addOption
method for GVars. - A specific GVar property's value can be assigned to one of the predefined "option" values using the
setToOption
method. - GVar property values can be tested against dictionary
keys
using GVar methods. e.g.ifProp character.colour equalToOption RED
checks ifcharacter.colour
value is equal to the dictionary value#f00
(not the keyRED
). - Assignment of Dictionary items to a another character property or feature property needs to be handled via stack operations.
Example Scripts:
// options
addProp colour string 'black'
prop character.colour addOption 'RED' '#f00'
prop character.colour addOption 'GREEN' '#0f0'
prop character.colour addOption 'BLUE' '#00f'
prop character.colour setToOption 'RED'
// test assignment
propPush colour
dbgStack
// -> should log "STACK(enum0): ['#f00']"
// test equalToOption condition
ifProp character.colour equalToOption 'RED' [[
dbgOut 'is RED!'
]]
// -> should log ''is RED'
// test notEqualToOption condition
ifProp character.colour notEqualToOption 'GREEN' [[
dbgOut 'is NOT GREEN!'
]]
// -> should log 'is NOT GREEN'
// assign dictionary option to a feature property
prop character.colour setToOption `BLUE`
// -> should set character.colour value to `#00f`
propPush character.colour
featPropPop Costume.colorCSS
// -> should set the Costume feature's `colorCSS` property to `#00f`
NOTE GVar dictionaries only work in the context of the current character. You cannot reference global agent properties or properties from other characters.
Add a new option to the designated GVar. Note options values match the GVar type, e.g. a string GVar's option will be a string, a number GVar's option would be string.
Syntax
prop <propName> addOption <optionLabel> <optionValue>
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the options in the option methods. e.g. defining a'RED'
option label allows you to use a constant label to set a constant value, e.g....setToOption 'RED'
-
<optionValue>
is the value that the<optionLabel>
is mapped to, e.g. if theRED
option label is set to the option value#f00
, then setting a prop value to the optionRED
would set the prop to#f00
.
Example
prop character.colour addOption 'RED' '#f00'
Assign a prop value to the selected option value.
Syntax
prop <propName> setToOption <optionLabel>
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the options in the option methods. e.g. defining a'RED'
option label allows you to use a constant label to set a constant value, e.g....setToOption 'RED'
.
Example
prop character.colour setToOption 'RED'
Evaluates whether a prop value matches the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> equalToOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'RED'
option label allows you to see if your property value matches'RED'
's value ('#f00').
Example
ifProp character.colour equalToOption 'RED' [[ ... ]]
Evaluates whether a prop value does not match the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> notEqualToOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'RED'
option label allows you to see if your property value does not match'RED'
's value ('#f00').
Example
ifProp character.colour notEqualToOption 'RED' [[ ... ]]
Evaluates whether a prop number value is greater than the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> greaterThanOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'LOW'
option label allows you to see if your property value is greater than'LOW'
's value ('10').
Example
ifProp character.colour greaterThanOption 'LOW' [[ ... ]]
Evaluates whether a prop number value is greater than or equal to the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> greaterThanOrEqualToOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'LOW'
option label allows you to see if your property value is greater than or equal to the'LOW'
's value ('10').
Example
ifProp character.colour greaterThanOrEqualToOption 'LOW' [[ ... ]]
Evaluates whether a prop number value is less than the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> lessThanOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'LOW'
option label allows you to see if your property value is less than'LOW'
's value ('10').
Example
ifProp character.colour lessThanOption 'LOW' [[ ... ]]
Evaluates whether a prop number value is less than or equal to the selected option value. This is generally used with ifProp
Syntax
ifProp <propName> lessThanOrEqualToOption <optionLabel> [[ ... ]]
where
-
<propName>
is the name of a property object reference. e.g. could becharacter.propname
oragent.propname.
or simplypropname
-
<optionLabel>
is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a'LOW'
option label allows you to see if your property value is less than or equal to the'LOW'
's value ('10').
Example
ifProp character.colour lessThanOrEqualToOption 'LOW' [[ ... ]]
There are three basic needs:
- Define multiple Dictionary Items -- a list of items each consisting of a
key
that is assigned to avalue
. - Be able to set a character property to a dictionary item by simply selecting the
key
from a list (rather than having to type it). - Be able to test whether a character property matches a dictionary item
key
(rather than matching it to the dictionaryvalue
).
Four example use cases:
- Simple CONSTANT key with no
value
-- Assign anentityType
property to a character designating it aPRODUCER
,CONSUMER
, orDECOMPOSER
, and use a property operation test to trigger behaviors based on the selectedentityType
. Novalue
is used. - Assignment to a "numeric"
value
-- Assign aenergyLevel
property whereLOW
is 0,MEDIUM
is 5, andHIGH
is 10, and use a property operation test to trigger behaviors based on numeric thresholds, e.g. do something ifenergyLevel
is higher thanLOW
. - Assignment to a "string"
value
-- Assign aentityState
property to a character designating itHUNGRY
,CONTENT
,FULL
, orDEAD
, displaying the selectedvalue
during exectution, e.g.HUNGRY
would be displayed as the valueHungry
. - Assignment to a "string"
value
and use a Feature property to trigger special operations -- Assign acolour
to a character property whereRED
="#f00"
,GREEN
="#0f0"
, andBLUE
="#00f"
and use thevalue
to set a costume color.
- Original Request
- Trying Constants
- Hacking in Options + UI
The original request was to provide a way to define constant-like declarations to use as part of GEMSCRIPT. For example:
- Organism type: producer, consumer, decomposer
- Energy level low (1), medium (5), high (10)
- Fish state: hungry, content, full, dead
After conferring with Sri, we felt that a cleaner implementation of "options" would be to emulate the notion of constants. The requested features were less "enums" and more like "dictionaries". We suggested the use of the keywords addConstants
and constantPush
as a way to implement dictionaries. The implementation was fairly complex but essentially introduced a new set of agent properties called constants
to go along with the existing properties
and methods
properties. constants
essentially replicated the functionality of properties
as a parallel subsystem.
While this implementation does work, the problem was that GEMSCRIPT can only handle assignments and tests to a simple javascript string, number, or boolean. We could not support assignments or tests to objects that are properties of agents or feature properties. e.g. you cannot use prop character.colour setTo RED
because the setTo
method only supports the assignment of pure strings, not RED
which would be a constant
object. e.g. you cannot use ifProp character.colour equal RED
for the same reason -- the equal
method can only work with a simple string, not RED
which is a constant
object. The only way to assign values was to use stack operations. While this implementation "works", it is confusing for students and because of the complexity does not lend itself to "Let's try this" comment instructions.
See discussion of constants implementation
The third approach was to implement a hybrid solution. This involves a two part solution.
First, it adds the ability to add and use "options" to existing GVars like class-sm-string
, class-sm-number
, and class-sm-boolean
. By adding options directly to GVars, we can introduce GVar-specific options methods for use within the scope of the GVar. This allows us to do assignments and run tests on options. This also allows the simulation engine to compile and run code that references agent property-specific options during assignments and tests without fundamentally altering how the simulation engine works.
Second, although we can introduce "options" methods to GVars that support the simulation engine, using the Script Wizard to construct GEMSCRIPT for "options" manipulation is not as straightforward. The Script Wizard uses a complex system that parses script text into tokens that are then in turn used to construct the wizard UI. The system is designed to work with standard keywords and syntax. But "options" do not quite adhere to the way keywords are handled, as a result, it has no way of constructing a list of user-defined "options" for the user to select -- we can generate lists of properties that have been added via code (e.g. user-defined character properties), but not the derivative methods (where the "options" are defined).
The workaround is to do two things:
- Add an extra compile loop that is used purely for pulling out the list of "options" that have been defined for the blueprint, and...
- Inject a custom UI into the standard Script Wizard UI to render and allow selection of an "option" from the list of possible "options" (emulating the approach taken with the Comment Styles selection interface).
This way the code data remains pure, can run on the standard simulation engine, and wizard hacks are confined to the purely visual rendering side.
LIMITATIONS with this approach
-
Cross-agent options do not work. e.g. you can't define an option for the
global
agent and use it with theFish
agent. Similarly, aFish
agent's option is completely separate from aAlgae
agent's options. -
We also cannot completely get around the need for some stack operations when working with GVar Dictionaries. E.g. if we're working with a character
prop
and you need to reference afeatProp
, you can only do cross-type assignments using a stack operation. e.g.
// 1. set a character prop
prop character.colour setToOption 'RED'
// 2. this is illegal because 'RED' is defined in the context of the character prop, the featProp
~featProp Costume.colorHex setToOption 'RED'~
// 3. instead, use a stack operation to do a cross-type assignment
propPush character.colour
featPropPop Costume.colorCSS
// -> assigns the value of character.colour option 'RED' (value = "#f00") to the Costume.colorCSS property
[In which Ben tries to document different approaches to the problem and serve as reminders of how the system works...feel free to skip.]
Bundle the option symbols as part of normal compile
One possible approach to streamline the "options" implementation: If we could bundle the "options" definitions as part of the tokens used to render the wizard, then we do not need a extra separate compile cycle.
- How does
addProps
figure out the list of newly defined props?addProps
defines new prop names that are listed when editing aprop
script line. So in wizard slot editor, the interpreted bundles do contain a list of the prop names as keys to theprop
object (e.g.props: { colour, scale, ...}
). - How are the bundles constructed? Bundles are constructed during the compile process.
script-compiler
calls:-
script-compiler.CompileBlueprint
, which breaks the blueprint into single lines in -
script-compiler.CompileStatement
, which breaks down into -
script-compiler.DecodeStatement
, which in turn is broken down into tokens in -
script-compiler.DecodeToken
, which returns the single token. - When adding a new prop, we deconstruct the statement into four tokens (e.g.
addProp colour string 'black'
: a. keyword:addProp
b. propName:colour
c. GVar type:string
d. GVar arg:black
- During compile for
addProp
, the fourth token for "b. propName" is added to an array of newly defined property names. - Later, when processing a statement that references the
prop
keyword, we look up the list of previously defined propNames to provide a list of options. This also requires processing 4 tokens (e.g.prop character.colour setToOption 'RED'
): a. keyword:prop
b. propName:character.colour
c. GVar method:setToOption
d. GVar method argument:RED
- WHERE ARE THESE propNames stored and retrieved from? In order to show the list of available predefined propNames when defining the second propName slot,
class-symbol-interpreter
has to look up the available propNames.- Bundler adds symbols by retrieving all the symbols for each keyword (e.g. each keyword has a
symbolize
function that generates all the available symbols). This is fed to... - ...
script-bundler.AddSymbols
constructs thefeatures
, andprops
(andconstants
if we keep that) lists from the symbol data generated by the bundler. Everything else is derived from that assymbolData
. - ...which eventually leads to...
- ...
EditSymbol_Block.f_renderchoices
generates the list of options fromsymbolData
- ...which is constructed from
symbol-utilities.DecodeSymbolViewData
generates lists based onprops
(andconstants
if we keep that).
- Bundler adds symbols by retrieving all the symbols for each keyword (e.g. each keyword has a
- If we wanted to add propOptions to the list, it would need to happen here in
script-bundler.AddSymbols
. We would probably either:- a) figure out how to shove it into props, or
- b) figure out how to shove it in as
constants
(if we decide to implement constants) - c) add a new symbol set
propOptions
or something like that?
- This means we either add the symbolization in the
prop
keyword (if we're adding symbols to theaddOption
keyword), which is awkward, because we would then be diving down to the next level to process prop methods in order to symbolize values... - ...or we introduce a new
addConstant
oraddPropOption
keyword that allows us to symbolize the keywords separately. BUT, then we would lose the ability to use prop methods to reference stored dictionary values (since they no longer a part of theprop
object)?
-
SO...we either have a nicely constructed menu of symbols that cannot be compiled, or we have a compilable script but a wizard UI that is not able to generate the list of symbols.
- This is where things get even more complicated. In order to provide a list of options (
RED
,GREEN
,BLUE
) for the 4th token "d. GVar method argument", we need to look up the options that have been defined for the second token "2. propName". THIS IS WHERE using aconstant
is potentially easier? e.g. rather than having to look up the propName, and then look up the option stuffed into the propName, we can use looking upaddConstant
keyword the same way thataddProp
works. But again, we have nice UI, but the code can't compile because we can't reference the option's value.
CONCLUSION: Although it's less than ideal, the "simple" solution is to use the hybrid approach:
a. define options as part of the GVar
b. during wizard construction, scrub the script text to pull out "options" that have been defined in the code
c. inject a menu selection UI into the normal wizard to allow selection of the "option"
d. during code compile and execution, the "option" references the value of the "option" (e.g. #f00
) rather than the "label" (e.g. RED
).
- git checkout
dev-bl/enum
npm run gem
- Edit a blueprint, adding the following lines to any blueprint:
# BLUEPRINT enum
# TAG isCharControllable true
# TAG isPozyxControllable false
# TAG isPTrackControllable false
# PROGRAM INIT
addFeature Costume
featProp character.Costume.costumeName setTo 'AQ_algae.png'
// STRING OPTIONS
addProp colour string 'black'
prop character.colour addOption 'RED' '#f00'
prop character.colour addOption 'GREEN' '#0f0'
prop character.colour addOption 'BLUE' '#00f'
prop character.colour setToOption 'RED'
// test assignment
propPush colour
dbgStack
// -> should log '#f00'
// test equalToOption condition
ifProp character.colour equalToOption 'RED' [[
dbgOut '1 is RED!'
]]
// -> should log ''is RED'
// test equalToOption condition
ifProp character.colour notEqualToOption 'GREEN' [[
dbgOut '2 is NOT GREEN!'
]]
// -> should log 'is NOT GREEN'
// NUMBER OPTIONS
addProp hungerLevel number 0
prop character.hungerLevel addOption 'LOW' 0
prop character.hungerLevel addOption 'MED' 50
prop character.hungerLevel addOption 'HIGH' 100
prop character.hungerLevel setToOption 'MED'
ifProp character.hungerLevel equalToOption 'MED' [[
dbgOut '0 Fish hunger is not MED!'
]]
ifProp character.hungerLevel notEqualToOption 'HIGH' [[
dbgOut '1 Fish hunger is not HIGH!'
]]
ifProp character.hungerLevel greaterThanOption 'LOW' [[
dbgOut '2 Fish hunger is greater than LOW'
]]
ifProp character.hungerLevel greaterThanOrEqualToOption 'MED' [[
dbgOut '3 Fish hunger is greater or equal to MED'
]]
ifProp character.hungerLevel lessThanOption 'HIGH' [[
dbgOut '4 Fish hunger is greater than HIGH'
]]
ifProp character.hungerLevel lessThanOrEqualToOption 'MED' [[
dbgOut '5 Fish hunger is greater than MED'
]]
// BOOLEAN OPTIONS
addProp isDead boolean false
prop character.isDead addOption 'DEAD' true
prop character.isDead addOption 'ALIVE' false
ifProp character.isDead equalToOption 'ALIVE' [[
dbgOut '1 Fish isDead is ALIVE!'
]]
ifProp character.isDead notEqualToOption 'DEAD' [[
dbgOut '2 Fish isDead is not DEAD!'
]]
- The web console on "Main" should show the following output:
This should demonstrate:
- adding a new option
- setting a property to an option
- testing an option's assignment