Need help looping through unknown number of nested arrays

Hello.

FULL DISCLOSURE:

I've gone 99% crazy trying to make this work, and I know I'm just thinking too hard and/or ignorant (meaning there's some thing(s) I don't know, not that I'm unintelligent (although that is also arguable)).


I can't make JS create the string I want! (And I'm frustrated.)

The Old Code

My first attempt (in my currently unfinished item_links library) did not print it out correctly.

Here is an example of my old code's output (it shows the doohickey in the Grue Bot 5000 rather than in the box):

image


I just noticed the extra comma after the Grue Bot 5000. That tells me "(containing ...)" was its own array item, which is not how I'd thought I'd coded it. Bah!

Here: https://github.com/KVonGit/Quest6Game1/blob/main/game/rhfiles/item_links.js#L153

getItemsLinks and getItemLinkContents are the culprits.

Don't even click on that, though. Just keep reading. (Sorry. You were warned: I've gone mad.)


A New Hope (?)

I'm taking it back to basics. I made these two "old-school Quest" functions:

function getDirectChildren(item){
	return item.getContents(item);
}
function getAllChildren(item){
	let result = [];
	let children = getDirectChildren(item);
	if (children.length < 1) return [];
	children.forEach(child => {
		result.push(child);
		// Added the check on the next line due to occasional errors.
		let grandchildren = typeof(child.getContents) == 'function' ? child.getContents(child) : [];
		if (grandchildren.length > 0){
			result.push(getAllChildren(child));
		}
	})
	return result;
}

In the game, I have a table. On the table, is a box and Grue Bot 5000. In the box, is a doohickey.

I want it to say: "You can see a table (on which there is a box (in which there is a doohickey) and a Grue Bot 5000)."

Now, my "new" function:

getAllChildren(w.table) returns this:

image


That seems like it's what I want to work with.

Now, I just need to figure out how to go through it all while creating the string properly.

Once I can make it create the string I want, I can easily add the item links into the mix.

I hate to ask for help with this one. I have tried and tried to figure it out. I even tried taking a break and coming back to it, because I know it's something simple.

I'm also pretty sure mrangel helped me with this in Quest 5 a while ago, but I can't find that thread.

Sigh.


ADDED THIS TEXT TO UPDATE THIS POST


This seems to hold the solution (maybe):

let s = "";
let objArr = getAllChildren(w.table);


function makeString(arr){
    let string = ''
    arr.forEach(a => {
        if (a.name) {
            string += a.name
        } else if (a.length) {
            string += " (containing: ";
            string += makeString(a, string)
            string += " ) "
        }
        string += " "
    })
    return string
}

s = makeString(objArr)
console.log (s)

I would give each item the responsibility of providing its own string. So the table should return its name and its stuff, and when it gets to the box, it just inserts whatever the box hands it. The box, then, is responsible for its name and contents.

The way Quest 6 does it is with nameModifierFunctions. One item can have several of these - it might be switched on and worn and contain various other items - so this is not trivial. Containers all use a function util.nameModifierFunctionForContainer, so you might want to look at that.


This looks pretty confusing. It feels like there isn't a clear concept of what each function is supposed to do. getItemLinkContents particularly appears to return the string returned by listItemContents, with the result of calling getItemLinkContents on each item appended to the end. You're adding a string to an array, which will just look weird if there's more than one container in the list.

I can't follow your structure, so I'll try to work out how I'd do it, using only the functions which make sense.

I'd suggest something looking like…

function getItemsLinks (arr, turn, art) {
  return arr.map(obj => getObjectLinkWithContents (obj, turn, art));
}

function getObjectLinkWithContents (obj, turn, art) {
  var link = getItemLink (obj, turn, art);
  var contents = (o.listContents ? !o.closed : o.npc) && o.getContents();
  if (contents && contents.length) {
    link += " ("
        + (({container: "containing: ", surface: "on which you see: "})[o.contentsType]
            || (o.npc ? "carrying: " : ""))
        + formatList(getItemsLinks (contents, turn, art), {lastJoiner:lang.list_and})
        + ")";
  }
  return (link);
}

Notes: You seem to vary whether you're using item.listContents(item) or just item.listContents(). I've used the latter because it seems more likely and I don't have time to check the details.

I've not looked into the method signature for formatList or getItemLink; I just copied the way you used them.and assumed that they will work.


Ah, I forgot to hit 'Post Reply' over lunch, and you beat me to it :p

Yes, I would suggest making the object handle its own contents. I was just trying to replicate the behaviour of the code in the first post, but working.


(In response to the subject: you're not looping over an n-dimensional array. You're looping over a nested array, which is exactly what recursion is for. Putting the items in a nested array seems a bit of an odd thing to do)


Okay... Sorry, guys. I think I figured it out (or at least a roundabout way to do it) before checking back to see if anyone answered.

function makeString(arr){
    let string = ''
    arr.forEach(a => {
        if (a.name) {
            string += a.name
        } else if (a.length) {
            string += "@ " + makeString(a, string) + '_END_'
        }
        string += ":"
    })
    string = string.replace(/:@/g, ' (containing')
    string = string.replace(/:_END_/g, ')')
    return string
}

function fakeLinkStringer(arr){
    let s = "";
    s = makeString(arr)
    console.log (s)
    let newArr = s.split(':')
    newArr = newArr.filter(el => {
        return el != [];
    });
    let realString = formatList(newArr, {doNotSort:true, lastJoiner:'and'})
    console.log(realString)
}

fakeLinkStringer(getAllChildren(w.table));

The Grue Bot 5000 is holding an open box in which is a doohickey.

THE OUTPUT: Grue_Bot_5000 (containing box (containing doohickey))

Hurray!

Adding the articles and the links is the easy part. Getting this string print this way was the hard part.


All I plan to use is this along with getItemLink. (I'm going to make this apply "carrying", "on top of which you see", and "in which you see", according to whether it's an NPC, a surface, or a container.

I'm getting rid of all the confusing functions.

Each item in the game gets a linkAlias set up in settings.setup.

I've already got the function to add the article, too. So, I may have cracked this one. (Big MAY HAVE.)


Containers all use a function util.nameModifierFunctionForContainer, so you might want to look at that.

I'll check that out.


I'll test that code you posted out, too, mrangel.


not looping over an n-dimensional array. You're looping over a nested array

Fixed the subject.

Putting the items in a nested array seems a bit of an odd thing to do

I was copying off of GetAllChildren in Quest 5, and that's how it turned out.


I would give each item the responsibility of providing its own string. So the table should return its name and its stuff, and when it gets to the box, it just inserts whatever the box hands it. The box, then, is responsible for its name and contents.

Interesting.

I'll see if I can figure out how to pull this off after a break.


IMPORTANT

No one should waste time looking at the existing code in the item_links library. I found a bug, tried to fix it, and now the code is all jacked up (and that code is on GitHub).

My plan is to revamp nearly all of that library. Hopefully, I'll be done with that tonight or on the morrow.

If all else fails, I have backups of every version of that library. So, I can back up to whatever step is necessary, if the rewrite fails (but I think the rewrite will be successful).


No one should waste time looking at the existing code in the item_links library. I found a bug, tried to fix it, and now the code is all jacked up (and that code is on GitHub).

You probably know this, but in case you do not, you can revert to an earlier version on GitHub.


My brain is going to silly places with this one now.

(still haven't looked at the basic code, this was just a silly idea)

function getVisibleContents() {
  var result;
  if (this is a container and is open) {
    result = some function that gets the contents of this and filters it for visibility, scenery, etc
  } else {
    result = [];
  }
  result.toString = function () {
    if (this.length) {
      return formatList( this.map(obj => {
        var children = obj.getVisibleContents();
        return getItemLink (obj, false, true) + (children.length ? (" (" + lang.getContentsPrefixForContainer(obj) + children + ")") : "");
      }), {lastJoiner:lang.list_and});
    } else {
      return lang.list_nothing;
    }
  };
  return result;
}

Although on closer inspection, I think that it might make more sense to have this function defined somewhere more global, so that it can be used by any function that returns lists of objects.

It means that you can do stuff like:

==> console.log(w.table.getContents())
[Object, Object, Object]

==> msgOrWhateverTheEquivalentFunctionIs("On the table is "+w.table.getContents())
On the table is a bell, a box (containing: a banana), and a small piece of cheese

Yes, in javascript it is perfectly legal to give an array a custom toString attribute so that it will come up with something human readable if you add the array to the end of a string.


@ Pixie

Concerning util.nameModifierFunctionForContainer

Is this invoked already somehow? Or do I need to add something to io.modulesToUpdate to call them on the items with the attribute? (Or would you handle that differently?)


(yes, I'm just rambling now, stuff I thought of when reading through this thread)

With regard to getItemLink, it seems a little odd to have this as a static function.

Would it make more sense (guessing the names of classes/functions/etc just to give you an idea what I mean) to have something like:

Item.prototype.toString = function() {
  if (settings.enableHyperlinks && !this.disableObjectLink && this.getObjectLink) {
    return this.getObjectLink();
  } else {
    return this.getDisplayName();
  }
};

Item.prototype.getObjectLink = function(linktext='') {
  if (!linktext) { linktext = this.getDisplayName(); }
  return '<a class="object-link" objectid="' + this.id + '">' + linktext + '</a>';
};

Is this invoked already somehow? Or do I need to add something to io.modulesToUpdate to call them on the items with the attribute? (Or would you handle that differently?)

It is added in the CONTAINER and SURFACE templates, and gets invoked in lang.getName.


Would it make more sense [ ... ] to have something like:

Most probably so, and I'm in the process of switching it over to handling it that way.

I've got all the old code stripped down to just a few functions and modifications now. So, it should be easy to plug a few things into some prototype item attributes.

...but first I want to know how the name modifier functions are supposed to work (as opposed to how I could make something work, which is what I normally aim for).

I've had the feeling I've been trying to reinvent wheels throughout this process, and I've been correct most of the time (in more ways than one). I search GitHub for the code I seek, and it returns nothing. So, I write a function or two to cover it. Later, I find the existing function with the string I searched right there in the code. Now, I've cloned the whole repo and the wiki, and I use GREP to search. It works much better.

For instance, there is code for nameModifierFunctionForContainer in the GitHub repo. If you search for nameModifier, it will not return that code. I even tried nameModifier*, and got nothing. Maybe there's some wildcard symbol I'm supposed to use on GitHub? I don't know.

image


Searching the entire string does provide results, though:

image


Are those all the results one could want? My Magic 8-ball says, "Probably not."

(Here's the code, by the way.)

Anyway, I've learned to use GREP in place of Github's search thingy.

[[email protected] QuestJS.wiki]$ grep -nr nameMod* *
Attributes-for-items.md:293:A name modifier is a brief word or phrase that flags the current state of an object. It might flag a hat as "worn" or a torch as "providing light". Templates do this for you in a lot of instances, but you might want to add you own. Just add a "nameModifierFunction" function that accepts a list for the parameter.
Attributes-for-items.md:299:  nameModifierFunction:function(list) {
Attributes-for-items.md:313:  for (let f of item.nameModifierFunctions) f(item, list)
Attributes-for-items.md:314:  if (item.nameModifierFunction) item.nameModifierFunctions(list)
Custom-Templates.md:91:    o.nameModifierFunctions.push(function(o, list) {
Custom-Templates.md:113:    o.nameModifierFunctions.push(function(o, list) {
[[email protected] QuestJS-work_in_progress]$ grep -nr nameMod* *
lib/_templates.js:71:  res.nameModifierFunctions = [function(o, list) {
lib/_templates.js:405:    o.nameModifierFunctions.push(function(o, list) {
lib/_templates.js:545:    o.nameModifierFunctions.push(util.nameModifierFunctionForContainer) 
lib/_templates.js:546:    //console.log(o.nameModifierFunctions)
lib/_templates.js:584:  res.onCreation = function(o) { o.nameModifierFunctions.push(util.nameModifierFunctionForContainer) }
lib/_templates.js:605:    o.nameModifierFunctions.push(function(o, list) {
lib/_templates.js:802:const SWITCHABLE = function(alreadyOn, nameModifier) {
lib/_templates.js:805:  res.nameModifier = nameModifier
lib/_templates.js:813:    o.nameModifierFunctions.push(function(o, list) {
lib/_templates.js:814:      if (o.nameModifier && o.switchedon) list.push(o.nameModifier)
lib/_templates.js:1485:      o.nameModifierFunctions.push(function(o, l) {
lib/_util.js:726:util.nameModifierFunctionForContainer = function(o, list) {
lib/_util.js:976:  for (let f of item.nameModifierFunctions) f(item, list)
lib/_util.js:977:  if (item.nameModifierFunction) item.nameModifierFunction(list)
lib/rpg.js:565:    o.nameModifierFunctions.push(function(o, list) {
lib/rpg.js:629:    o.nameModifierFunctions.push(function(o, list) {
lib/_world.js:143:  item.nameModifierFunctions = []

So, now I can actually find stuff I search for, and I need to learn how to use name modifiers.

Moving on . . .


I've done away with a good 80% of the old item_links code at this point, and I'm looking to do away with half of what's left.

The name modifier stuff in the Quest code looks like it behaves just like your last example, mrangel. It's already setup on containers specifically, too. I am just now attempting to make it do something. I'm just going to add to the modules to update and see how that goes. If Pixie has it thought out differently, I can always change it.

I'm extra concerned with Pixie's thoughts on this because this library will be included as an optional library in the new Quest, and I definitely want it to work how Pixie thinks it should work.


It is added in the CONTAINER and SURFACE templates, and gets invoked in lang.getName.

I would have seen this before my last post, had I refreshed the page before posting.

I'm about to get this going on right now.


https://github.com/ThePix/QuestJS/wiki/Attributes-for-items#a-note-about-name-modifiers

Aw, snap!

Ahem. I mean . . .

By Jove, I think I've got it!


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums