Request for Comment: cslib_class

Peoples,

In a recent discussion with @CJW and @cup_half_empty (Simulating Objects) @CJW suggested the possibility of a cslib module to handle classes or objects.

The use case for this was:
I have a scene in which a dragon will appear but I don’t know beforehand which of the many dragons will appear.
I have to be able to code it generically such that it will work whatever dragon appears.

I couldn’t locate anything on the forum so what I came up with, and where I would appreciate comment, is the following:

The basic syntax for this is (per the dragon example)
*create dragon_proto_1 “name”
*create dragon_proto_2 “hp”
*create dragon_proto_max 2

*create dragon_name “”
*create dragon_hp “”

*create dragon_1_name “Wally”
*create dragon_1_hp 100
*create dragon_2_name “Dave”
*create dragon_2_hp 150
*create dragon_max 2

When I am coding my scene, I simply use dragon_name and dragon_hp.

When I find out what dragon it is, I use a"set" command which copies the relevant dragon information into dragon_name and dragon_hp.

Very simple. There are all sorts of funky things you could do like allowing inheriting fields from virtual classes but I’m not sure that level of complexity is desirable.

I’ve written the beginnings of a possible cslib_class file (incomplete and probably buggy) which I would attach if I could figure out how to do so. In default of which I am pasting the whole thing in here.

Thoughts?
Stephen


*comment cslib_class
*comment Allows the simulation of objects/classes (sort of)
*comment @StephenHart
*comment
*comment How it works:
*comment A class is always accessed through its main instance.
*comment When you “get” an instance of a class, the data for that particular instance
*comment is copied into the main instance
*comment
*comment A class definition has the following elements defined in startup.txt:
*comment 1. A class “prototype” array defining all the field names
*comment classname_proto_N fieldname
*comment 2. A main instance of this class. This is used to ‘return’
*comment the class (similar to cslib_ret).
*comment 3. An array of instances of the class
*comment
*comment Public methods
*comment get_by_index class index
*comment copy the instance of the class at index into main instance
*comment get_by_field class field value
*comment copy the first instance of the class that has specified field equal to specified value
*comment into the main instance
*comment get_random class
*comment select a random instance of the class and copy it into the main instance
*comment set_by_index class index field value
*comment set the specified field to the specified value in the instance
*comment of the class at index
*comment set_by_field class field oldvalue newvalue
*comment select the first instance of the class that has specified field equal to specified oldvalue
*comment and set that field to the newvalue
*comment
*comment ------------------------------------------------------------------
*comment EXAMPLE startup.txt - class “dragon”
*comment
*comment *create dragon_proto_1 “name”
*comment *create dragon_proto_2 “hp”
*comment *create dragon_proto_max 2
*comment
*comment *create dragon_name “”
*comment *create dragon_hp “”
*comment
*comment *create dragon_1_name “Wally”
*comment *create dragon_1_hp 100
*comment *create dragon_2_name “Dave”
*comment *create dragon_2_hp 150
*comment *create dragon_max 2
*comment
*comment EXAMPLES of getting a dragon instance
*comment
*comment cslib_class get_by_index “dragon” 2
*comment sets dragon_name to “Dave” and dragon_hp to 150
*comment cslib_class get_by_field “dragon” “name” “Wally”
*comment sets dragon_name to “Wally” and dragon_hp to 100
*comment

*comment used for passing around results in this file
*temp local_ret false

*comment GET_BY_INDEX
*comment ------------------------------------------------------------------
*comment Takes the instance of a class specified by the index and copies
*comment the data into the main instance
*comment ------------------------------------------------------------------
*comment
*comment params:
*comment p_classname (string): the name of the class
*comment p_index (integer): index of this class instance
*comment returns:
*comment updates main class instance
*comment usage example:
*comment cslib_class get_by_index “dragon” 2
*comment ------------------------------------------------------------------

*label get_by_index
*params p_classname p_index

*gosub _set_main_instance p_classname p_index
*return

*comment GET_BY_FIELD
*comment ------------------------------------------------------------------
*comment Gets the class instance that matches the given field value pair
*comment Does not check for duplicates and returns the first match it finds
*comment searching from lowest index to highest
*comment ------------------------------------------------------------------
*comment params:
*comment p_classname (string): the name of the class
*comment p_fieldname (string): the field to be matched
*comment p_value (string): the value to be matched
*comment returns:
*comment updates main class instance
*comment usage example:
*comment cslib_class get_by_field “dragon” “name” “Wally”
*comment ------------------------------------------------------------------

*label get_by_field
*params p_classname p_fieldname p_value

*comment TBD

*return

*comment GET_RANDOM
*comment ------------------------------------------------------------------
*comment Get a random instance of a class
*comment ------------------------------------------------------------------
*comment params:
*comment p_classname (string): the name of the class
*comment returns:
*comment updates main class instance
*comment usage example:
*comment cslib_class get_random “dragon”
*comment ------------------------------------------------------------------

*label get_random
*params p_classname

*temp max = {p_classname&"_max"}
*temp idx 0
*rand idx 1 max
*gosub _set_main_instance p_classname idx
*return

*comment SET_BY_INDEX
*comment ------------------------------------------------------------------
*comment Set the value of a particular field in a particular instance of a class
*comment ------------------------------------------------------------------
*comment params:
*comment p_classname (string): the name of the class
*comment p_index (integer): index of this class instance
*comment p_fieldname (string): the field to be matched
*comment p_value (string): the value to be matched
*comment returns:
*comment none
*comment usage example:
*comment cslib_class set_by_index “dragon” 2 “name” “Bruce”
*comment ------------------------------------------------------------------

*label set_by_index
*params p_classname p_index p_fieldname p_value

*set {p_classname&"${p_index}${p_fieldname}"} p_value
*return

*comment SET_BY_FIELD
*comment ------------------------------------------------------------------
*comment — find the instance of a field with the given name and replace it
*comment ------------------------------------------------------------------
*comment params:
*comment p_classname (string): the name of the class
*comment p_fieldname (string): the field to be matched
*comment p_oldvalue (string): the current value in the field
*comment p_newvalue (string): the new value for this field
*comment returns:
*comment none
*comment usage_example
*comment cslib_class set_by_field “dragon” “name” “Dave” “Bruce”
*comment ------------------------------------------------------------------
*label set_by_field
*params p_classname p_fieldname p_oldvalue p_newvalue

*comment TBD

*return

*comment ------------------------------------------------------------------
*comment — PRIVATE METHODS
*comment ------------------------------------------------------------------

*comment — ------------------------------------------------------------------
*comment — Copies the values in the instance at the given index into the main instance
*comment — ------------------------------------------------------------------
*label _set_main_instance
*params p_class p_index

*temp n 1
*temp field_count {p_class&"_proto_max"}

*label set_loop
*if (n <= field_count)
*temp field {p_class&“proto${n}”}
*set {p_class&"
${field}"} {p_class&"${p_index}${field}"}
*set n + 1
*goto _set_loop

*return

*comment — ------------------------------------------------------------------
*comment clear all the fields in the main instance
*comment — ------------------------------------------------------------------
*label _clear_main_instance
*params p_class

*temp n 1
*temp field_count {p_class&"_proto_max"}

*label clear_loop
*if (n <= field_count)
*temp field (class&“proto${n}”)
*set {p_class&"
${field}"} “”
*set n + 1
*goto _clear_loop

*return

5 Likes

Wow! I’m impressed, I certainly wasn’t expecting anything that fast. Awesome stuff, thanks for sharing!

@choicehacker may also be interested, if he’s active.

I think this is a great start. I haven’t looked through the code in great detail (I’ll wait till it’s somewhere more reviewable, see below), but I’ve taken a look at the overall API.

I definitely agree with the approach, I think it’s by far the most sensible. Part of me wishes we could “create” a new instance (i.e. set all its fields) in one call, but I think the implementation for that would be too messy and complex (you’d need the user to define a string of field names, or something).

The user can always define their own sub routines to simplify this for their favourite classes.

On that note, I don’t think we should provide the “random” functionality as part of the cslib API. It’s easy enough for a user to implement that themselves.

I’d usually say the same for the field/name call as well, but I actually think, given the typical ChoiceScript audience, and complexity of classes, this might be a very useful addition that streamlines adoption. Good call. Though there is a question as to what happens when there are two matching values, or when there is no match at all (do we set the main instance to “null” across the board, leave it as it was, or throw a *bug?).

I’d also suggest not using the word “prototype”/proto in any examples, the word won’t mean much to most CS users.

Are you comfortable with GitHub? A pull request against the repository (GitHub - ChoicescriptIDE/cslib) would be ideal.

I’m inclined to agree about keeping it simple, but you’ve piqued my curiosity here. How do you think you would you go about implementing inheritance?


EDIT:

Some more thoughts below on interaction with cslib_array.

Would the format:

*create dragon_1_name “Wally”
*create dragon_1_hp 100
*create dragon_2_name “Dave”
*create dragon_2_hp 150
*create dragon_max 2

Be better as:

*create dragon_name_1 “Wally”
*create dragon_hp_1 100
*create dragon_name_2 “Dave”
*create dragon_hp_2 150
*create dragon_max 2

The former reads better, but the latter would more easily allow cslib_array features to be applied (if you created the extra array variables).


On that note, would we also want a “dragon_count” variable, to keep track of the number of active instances? And some functionality to manage instances (create/delete), and/or mark instances as unused/deleted?

This isn’t so obvious in games where you can just define everything up front and run with it. But there may be situations where people would want to generate “dragons” (or other objects) at runtime, most likely in simulator or RPG style projects.


And then thinking back to the “get by field” function: I think any main instance definition should be expected to have a compulsory “id” field. Else you could end up grabbing an object with no way to identify it.

Hi @StephenHart , thanks for sharing this. (And thanks @CJW for tagging me). This is very nice code, I hope it can get widely adopted.

A suggestion I have is that this seems to offer a rather specific functionality - an array of similar objects. It may be more general if the object functionality was independent from the array manipulation. Let’s say you have multiple object “character”, each with strength, speed etc, and you just keep their references around (“warrior”, “elf”…). (We can then combine them in an array using some different approach.) A simple object library may find more usage.

Another small comment. I believe the bracket syntax is more flexible, and can take a string:

*set dragon[“strength”][2] 100
*set dragon[“strength”][index] 100
*set class[fieldname][index] value

where fieldname is a string.

In any case, I will be happy to help with reviews, tests etc.

2 Likes

I’m not sure I understand. Are you suggesting dropping the numerical ID in favour of a string based ID?

Like so?

*comment main instance
*create dragon_hp 0
*create dragon_name ""

*create dragon_red_hp 200
*create dragon_red_name "Smaug"

*create dragon_blue_hp 100
*create dragon_blue_name "Saphira"

*gosub_scene cslib_class get_by_index "dragon" "red"
${dragon_hp} == 200
${dragon_name} == Smaug

In which case, I don’t think the distinction really matters too much: I think a routine written correctly could work with both.

That said, the numerical “array” approach has the advantage of allowing the “creation” of new objects at runtime. You simply can’t do that with string IDs.

Thanks for all the feedback @CJW and @choicehacker.

My basic philosophy on this was that a non-programmer who had only read the standard ChoiceScript documentation could look at it and understand it easily. That said, it’s easier to say that than define what features you actually want.

@CJW I don’t think we should provide the “random” functionality as part of the cslib API

This comes under the heading of defining what we want. I don’t feel too strongly about it but I think it might be useful to our hypothetical user.

@CJW Though there is a question as to what happens when there are two matching values, or when there is no match at all (do we set the main instance to “null” across the board, leave it as it was, or throw a *bug?).

Yes, could be problematic. Have a look at the end of this reply where I’ve had some thoughts on error checking (doesn’t really cover this case but it could be adapted to do so)

@CJW I’d also suggest not using the word “prototype”/proto in any examples

I don’t feel strongly about it. Suggestions welcome.

@CJW Are you comfortable with GitHub?

I’ve never used it but I’m sure I can figure it out.

@CJW How do you think you would you go about implementing inheritance?

After more thought, I’m not sure you can (ahem)
What I was think of was an ISA field.
As part of the class definition and the main instance you have:
*create dragon_isa “monster”
“monster” is just another class definition that contains a list of fields common to all monsters. So, instead of having to type out the same fields (e.g. name and hit points) for every monster type, we just use dragon_isa and the code will know to include those fields automatically.
You would never instantiate a monster, just use it as a field list.

The problem with this idea, of course, this makes instantiating objects tricky. You would need to be able to set values via a sub call. Which probably isn’t worth the effort.

@CJW - *create dragon_1_name “Wally” vs *create dragon_name_1 “Wally”

I find the former more intuitive and more user-friendly but I’m open to being convinced.

@CJW would we also want a “dragon_count” variable

Maybe. I had a look at the array lib and it both sings and dances. I’m not sure whether we want this level of functionality.

[as an aside, I find it annoying that there is no “exists” functionality for variables - it just barfs if it isn’t there. Every time you add extra complexity, you force the user to add extra fields they may not need. Please tell me if there is a way around this!]

@CJW I think any main instance definition should be expected to have a compulsory “id” field.

Yes, that was in my first-pass definition - it would always give you the index. I think I should have kept this one.

@choicehacker It may be more general if the object functionality was independent from the array manipulation

I wasn’t sure about whether to do this. As I said to @CJW above,the array lib is awesome but it adds a lot of complexity that may not be necessary.

@choicehacker I believe the bracket syntax is more flexible

I accept that. I’m not necessarily in favour of using it though. It is not standard vanilla ChoiceScript so there is extra learning for our user.

@choicehacker I will be happy to help with reviews, tests etc.

Thanks. I will take you up on it when this progresses a bit further.


Discussion of error handling

Per my whinge about about the lack of an “exists” function, it is possible to do some checking if it is set up correctly.

If we require people to list all the classes they are using, thus:
*create class_list_1 “dragon”
*create class_list_2 “undead”
*create class_list_max 2

Then the following is possible. I’ve also considered a “safemode” variable to allow this to be turned on and off (it does have an overhead)

*comment — ------------------------------------------------------------------
*comment in safemode - cheeck that a class actually exists
*comment loops through the classlist to check whether the classname provided matches a field
*comment — ------------------------------------------------------------------
*label does_class_exist
*params class
*set local_ret false
*temp n 1
*label_class_list_loop
*if (n <= class_list_max)
*temp classname ("class_list
${n}")
*if (classname = class)
*set local_ret true
*return
*goto _class_list_loop

*return

*comment — ------------------------------------------------------------------
*comment in safemode - cheeck that an index sits within the array boundaries
*comment — ------------------------------------------------------------------
*label _does_index_exist
*params class index

*set local_ret true
*if ((index <= 0) or (index > {class&"_max"}))
*set local_ret false
*return

*comment — ------------------------------------------------------------------
*comment in safemode - cheeck that a field actually exists
*comment loops through the object ‘prototype’ to check whether the name provided matches a field
*comment — ------------------------------------------------------------------
*label _does_field_exist
*params class field

*set local_ret false
*temp n 1
*temp max_fields {class&"_proto_max"}

*label _check_fields_loop
*if (n <= max_fields)
*temp fieldname (class&“proto${n}”)
*if (fieldname = field)
*set local_ret true
*return
*set n + 1
*goto _check_fields_loop

*return

You could add a compulsory classname_error (e.g. dragon_error) field which would either be an empty string (no error) or an error message, e.g. “class ‘dragon’ does not contain field ‘vegetarian’”

While you are debugging, you could actually display the error field on your page as there would be nothing there if everything was correct.

2 Likes

I’ve had a change of heart on this one. I now think it should be as simple as possible, which will cover the use case for 90% of people.

What I’m thinking now is called cslib_class_simple.

It has the same API but only two methods - get and set. Reference to the array is always by numeric index.

Users are encouraged to define ‘constants’ and use those in the place of raw numbers e.g.

*create DRAGON_WALLY 1
*create DRAGON_DAVE 2

Thus:

cslib_class_simple get “dragon” DRAGON_WALLY
cslib_class_simple set “dragon” DRAGON_WALLY “name” “Sir Walter”

And that’s it.

Someone can write cslib_class in due course for people who need the singing and dancing version.

4 Likes

We could go for cslib_object, and then use cslib_class at a later date, if we feel it necessary?

But I’d be delighted to see a pull request for the simple version in the meantime :slight_smile:

cslib_object it is. ‘Class’ was always a bit pretentious…

I’ve got some time this weekend so maybe I can do it over the next couple of days.

2 Likes

I’ve created a new file and testfile on branch_objects but I’m not able to push the new branch. Looks like I need some sort of permission set?

Stephens-iMac:cslib stephen$ git push origin branch_objects
Username for ‘https://github.com’: StephenXHart
Password for ‘https://StephenXHart@github.com’:
remote: Permission to ChoicescriptIDE/cslib.git denied to StephenXHart.
fatal: unable to access ‘GitHub - ChoicescriptIDE/cslib’: The requested URL returned error: 403

Note: I’m not an habitual git user so it’s quite possible I have stuffed up somewhere.

If you go the repo page, in the top right you can “fork” it. This will make a copy of the repo under your profile, which you’ll be able to commit to. Once you’re happy with your commit(s), you can then use the UI to create a pull request of the differences between the two repos, allowing you to merge into the original repository (subject to review and approval).

Feel free to PM me if you’re still having trouble.

Got it, thanks. Pull request submitted.

(My entire career has been CVS or Subversion. I’m still a git noob.)

1 Like

Hmm, I don’t see it. What’s your Github username? You might have pulled against your local repo, rather than the parent (forked) repo.

EDIT: Ignore me, I’m being blind :flushed:

EDIT2: I’ve made some comments :blush:

1 Like

The only limitation I see in this kind of implementation is that we cannot create and destroy objects dynamically, since ChoiceScript does not allow variables to be created during runtime. That is, if I’m not mistaken, we have to know up front how many dragons we’re going to have in the game, or at least the maximum we’re going to allow to exist.

For example, in the case of my current project, I needed several instances of a given “object” with eleven fields. Since I don’t really know how many will be needed (it depends on how the game goes each time) I estimated as many as possible and decided to create ten instances, which immediately forced me to have 110 *create commands in startup.txt (and more because there are other auxiliary variables). The part that is very similar to this approach is that I also created an additional generic “instance”, with the same eleven fields, to fetch the information needed at a given time.

On the other hand, for inventory management, I preferred a different, simpler approach that I have already shared in this topic, which is list-based. In that approach, the “object” is an item with two fields, description and amount, but that could be extended, although limited of course by the maximum size a string can have in ChoiceScript, of which I am not aware. The advantage is that it is very easy to use even for those without much programming knowledge.

I think an abstraction layer could exist over such an implementation of lists to make an object-based approach possible and allow different types of “objects” with specific fields. I don’t know if that would bring too much value for the authors, but it would be a lot of fun to implement. :slight_smile:

1 Like

The lack of dynamic variables is one of the real pains of ChoiceScript. I don’t think there is any way around defining everything up front. Sounds like you came to pretty much the same conclusion I did :slight_smile:
Had a look at your inventory system and it looks pretty cool. I’m guessing that when the inventory gets too big it will be a bit slow, given it involves manipulating strings. Performance might be the issue.
cslib_object has been added to cslib (GitHub - ChoicescriptIDE/cslib) if you are interested in the implementation.

2 Likes

Regarding performance issues with a string-based approach, I have that same perception. I tried to do some testing, but I got this error in my inventory module: “visited this line too many times (1000)”. I still haven’t found a way to override this rule in order to proceed with the tests.

EDIT: just found the *looplimit command!

An example of the kind of commands I would imagine in such an approach would be:

*gosub_scene objects class "dragon" "name,hp,color"
*gosub_scene objects new "dragon" "currentdragon" "John,50,blue"
*gosub_scene objects set "currentdragon" "hp" "30"
*gosub_scene objects get "currentdragon" "name" "mydragonname"

In any case, I will be available to help with the implementation of cslib_class, perhaps with any tests that you need to be done.

2 Likes

Help in any form: be it thoughts, comments or patches is very welcome! Unfortunately we agreed to keep cslib to vanilla CS, so we can’t use *script to create objects dynamically.

I have an open PR up for “create_array”: Add *create_array, *temp_array, *delete_array by CareyJWilliams · Pull Request #136 · dfabulich/choicescript · GitHub

Which I feel would significantly reduce the pain of pre-defined arrays and objects :crossed_fingers:

2 Likes

It would be great to have these commands available!

1 Like

If you could find a way to do this without the performance hit, it would be awesome. Class/Set/Get we effectively have but ‘new’ is the killer.

A slightly kludgy alternative would be to follow the approach of cslib_array which (I think - I haven’t used it) predefines some empty slots, enabling your usual push/pop/etc (up to the size limit defined by the number of slots).

You could predefine say, 24 dragon slots, and then use ‘new’ to fill one of these slots. You still have the upfront work in startup.txt but you don’t have to actually have real values until run time.

2 Likes

Notes on performance testing on a string-based approach using the inventory module:

The inventory management module was poorly optimized and I have been improving it. Then I wrote a routine that created 100 things in it (would be 100 instances of the same class, with two fields, name and amount) followed by 100 searches for random things (half of those non-existent, forcing to search the entire base string). Note that when creating a thing in the inventory, the program checks if it already exists, so it always goes through the entire base string.

image

This whole execution took between 17 and 20 seconds. In a real-time transactional system, it would be an eternity. In this use case, given that an item (object) is added or checked from time to time, it would be unnoticeable to the reader. My only concern is not to reach the maximum length of a string.

But it should be noted that these times are measured on the server that is on my machine. Also, the elapsed time is exponential (when adding the 20th unique item, the previous 19 are checked, but when adding the 1000th unique item, the other 999 items are checked, which takes much longer). Creating 1000 unique items in the same base string (i.e. 1000 objects of the same class) took almost nine minutes.

The question is: do you really want 1000 dragons? Maybe.

This would be easily done with the *create_array command :slight_smile: