theRAPTLab / gsgo

GEM-STEP Foundation repo migrated from GitLab June 2023
1 stars 1 forks source link

Feature: GVar Dictionaries (aka variable "options") #773

Closed benloh closed 9 months ago

benloh commented 9 months ago

See #755 for discussion.

GVar Dictionaries

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.

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.

Methods

General Options Methods

addOption GVar method

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

Example

prop character.colour addOption 'RED' '#f00'

setToOption GVar method

Assign a prop value to the selected option value.

Syntax

prop <propName> setToOption <optionLabel>

where

Example

prop character.colour setToOption 'RED'

equalToOption GVar method

Evaluates whether a prop value matches the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> equalToOption <optionLabel> [[ ... ]]

where

Example

ifProp character.colour equalToOption 'RED' [[ ... ]]

notEqualToOption GVar method

Evaluates whether a prop value does not match the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> notEqualToOption <optionLabel> [[ ... ]]

where

Example

ifProp character.colour notEqualToOption 'RED' [[ ... ]]

Number Methods

greaterThanOption number-only GVar method

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

Example

ifProp character.colour greaterThanOption 'LOW' [[ ... ]]

greaterThanOrEqualToOption number-only GVar method

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

Example

ifProp character.colour greaterThanOrEqualToOption 'LOW' [[ ... ]]

lessThanOption number-only GVar method

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

Example

ifProp character.colour lessThanOption 'LOW' [[ ... ]]

lessThanOrEqualToOption number-only GVar method

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

Example

ifProp character.colour lessThanOrEqualToOption 'LOW' [[ ... ]]

Use Case

There are three basic needs:

  1. Define multiple Dictionary Items -- a list of items each consisting of a key that is assigned to a value.
  2. 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).
  3. Be able to test whether a character property matches a dictionary item key (rather than matching it to the dictionary value).

Four example use cases:

  1. Simple CONSTANT key with no value-- Assign an entityType property to a character designating it a PRODUCER, CONSUMER, or DECOMPOSER, and use a property operation test to trigger behaviors based on the selected entityType. No value is used.
  2. Assignment to a "numeric" value -- Assign a energyLevel property where LOW is 0, MEDIUM is 5, and HIGH is 10, and use a property operation test to trigger behaviors based on numeric thresholds, e.g. do something if energyLevel is higher than LOW.
  3. Assignment to a "string" value -- Assign a entityState property to a character designating it HUNGRY, CONTENT, FULL, or DEAD, displaying the selected value during exectution, e.g. HUNGRY would be displayed as the value Hungry.
  4. Assignment to a "string" value and use a Feature property to trigger special operations -- Assign a colour to a character property where RED = "#f00", GREEN = "#0f0", and BLUE = "#00f"and use the value to set a costume color.

Implementation History

  1. Original Request
  2. Trying Constants
  3. Hacking in Options + UI

1. Original Request

The original request was to provide a way to define constant-like declarations to use as part of GEMSCRIPT. For example:

2. Trying Constants

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

3. Hacking in Options + UI

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:

  1. Add an extra compile loop that is used purely for pulling out the list of "options" that have been defined for the blueprint, and...
  2. 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

Technical Aside

[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.

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.

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).


To Test

  1. git checkout dev-bl/enum
  2. npm run gem
  3. 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!' ]]


4. The web console on "Main" should show the following output:
![screenshot_1320](https://github.com/theRAPTLab/gsgo/assets/1403057/04f3512d-e6f4-4889-9864-1518703687d2)

This should demonstrate:
* adding a new option
* setting a property to an option
* testing an option's assignment

---

@jdanish I'm still writing this out, but it's good enough to  try out.  `number` and `boolean` haven't been implemented yet.

# TO DO
* [X] Add `number` and `boolean` tests
* [x] Add method definitions
* [X] Stash or push constant-abandoned
* [X] Add "To Test" instructions
jdanish commented 9 months ago

What's in there looks good!!

Question: is there a reason we won't be able to check other characters or global? Long-term they'd be nice to have. I might even prioritize them over other things when we re-evaluate our list.

jdanish commented 9 months ago

Also a suggestion: when you update the to test section it might be good long-term if the comments indicating the output are valid Gem-script since the current ones crash the system. Easy enough to delete so don't bother if it's a hassle.

benloh commented 9 months ago

Question: is there a reason we won't be able to check other characters or global? Long-term they'd be nice to have. I might even prioritize them over other things when we re-evaluate our list.

Ironically, our wizard hack will allow us to construct the UI to support other characters and global characters, but the sim engine/compile side will not.

It occurs to me however, that we could introduce a new keyword similar to when that explicitly sets one character prop to another. So perhaps something like this (with a better keyword name).

// simple approach -- set one prop to another prop not using options
# BLUEPRINT Global
addProp 'GOOD' string 'GOOD'
addProp 'EVIL' string 'EVIL'

# BLUEPRINT Shark
addProp alignment string
propSetTo character.alignment global.GOOD

So propSetTo would use a two part column selector where you first select the agent, followed by the props that have been defined. In this case, we wouldn't have a popup menu, but just the standard prop selector. We would not need to use options at all.

If you want to set props just for the character, you would just use standard props:

# BLUEPRINT Shark
addProp 'GOOD' string 'GOOD'
addProp 'EVIL' string 'EVIL'
addProp alignment string
propSetTo character.alignment character.GOOD

And for tests, we would use:

# BLUEPRINT Shark
ifPropEqualTo character.alignment global.GOOD [[ ... ]]

(but this means we have to introduce a ton of keywords for all the math operations).

We probably would not want to use options at all as the constructs would get REALLY hairy.

// options would require...
# BLUEPRINT Global
addProp ALIGNMENTTYPE string
prop ALIGNMENTTYPE addOption 'GOOD'
prop ALIGNMENTTYPE addOption 'EVIL'

# BLUEPRINT Shark
propSetToOption character.alignment global.ALIGNMENTTYPE.getOption GOOD
// -> REALLY confusing

It's definitely awkward, and I'm not sure this would actually work. I'd have to confer with Sri to see if this is a terrible hack that breaks all kinds of rules. This will probably eat up another week (it'll be another half week to clean up and add support for numbers and strings already). Did you want to revisit the priorities before we do this?

Also a suggestion: when you update the to test section it might be good long-term if the comments indicating the output are valid Gem-script since the current ones crash the system. Easy enough to delete so don't bother if it's a hassle.

Yeah, good suggestion. I think I'm actually using a working test script, but may have taken a shortcut.

jdanish commented 9 months ago

OH, never mind. I think we are good. I wanted to do the following, in Beaver and both appear to work.


when Beaver touches Twig [[
  ifProp global.season equalToOption 'FALL' [[
    dbgOut 'IT IS FALL'
  ]]

  ifProp Twig.type equalToOption 'CORRECT_TYPE' [[
      dbgOut 'IT IS CORRECT'

  ]]
]]

The one other case I can imagine is something like color it might help to do something like the following, possibly even via a feature:


// in global ... 
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'

// in init for something else (fish?)

addProp coulour
prop character.colour copyOptions global.colour

// or maybe something like
copyProp global.coulour

The idea being that it'd sync them up. HOWEVER, I think we have what we need for now, and moving forward on the GUI is ideal next-step? Thanks!

benloh commented 9 months ago

prop character.colour copyOptions global.colour Remember, we can't access other agents as the target of assignments. So we can't reference global.colour as the argument for the copyOptions method. This is why we have to introduce a keyword (propSetTo) that can access two separate agents as arguments to a keyword.

copyProp might be doable if we use the dual-agent reference method for when -- we have to add references to both agents. Then you might be able to do something like this:

# BLUEPRINT Global
addProp colour string
prop colour addOption 'RED' '#f00'
prop colour addOption 'GREEN' '#0f0'

# BLUEPRINT FIsh
addProp colour string
copyProp Fish.colour global.colour
prop Fish.colour setToOption 'RED'

But accessing options for featProps would still be a problem.

For example, assuming we add a Costume featProp called colorCSS if we did not use globals, we could potentially do this:

# BLUEPRINT Fish
featProp Costume.colorCSS addOption 'RED' '#f00'
featProp Costume.colorCSS addOption 'GREEN' '#0f0'
featProp Costume.colorCSS setToOption 'RED'

But we might have to introduce a copyFeatProp command that only works on predefined featProps across agents, e.g.

# BLUEPRINT Global
addProp colour string
prop colour addOption 'RED' '#f00'
prop colour addOption 'GREEN' '#0f0'

# BLUEPRINT Fish
copyFeatProp Costume.colorCSS global.colour
featProp Costume.colorCSS setToOption 'RED'

Otherwise, as before we'd have to revert to using stack operations:

# BLUEPRINT Global
addProp 'RED' string '#f00'
addProp 'GREEN' string '#0f0'

// Set
# BLUEPRINT Fish
propPush global.RED
featPropPop Costume.colorCSS
jdanish commented 9 months ago

Gotcha.I’d say let’s get the gui working and then revisit our full list given remaining time constraints?

benloh commented 9 months ago

If you have specific ideas for how you'd like to set colors we can see which of these approaches would be cleanest. But basically any color work that involves Features will at the very least need some new Feature methods/props to handle colors within agents, and at worse use stack operations across agents.

jdanish commented 9 months ago

Short term, we can use the stack operations and the feature Costume.colorCSS you had listed above (but not implemented) if that is doable? Then we can see what we use, etc, and go from there? Long-term I'd rather we find a way to override color with a color picker (maybe in the next grant)?

benloh commented 9 months ago

@jdanish I believe I have numbers and booleans working now. Please see the updated test scripts.

jdanish commented 9 months ago

Looks good!