Quest 6: Messing around with JS regex group capture code

I'm running this script in the Firefox Web Developer Web Console.

(For those who are unfamiliar with this: I pushed <F12> then Ctrl+B with Firefox open, entered the code in the field on the left, then pressed Ctrl+ENTER to run the code.)


image


The Mission

The GiveTo command only has one regular expression set up: /^(?:give) (.+) (?:to) (.+)$/

VIEW THE REFERENCED LINE OF CODE ON GITHUB

So, the player must enter: GIVE [object(s)] TO [npc].

I want to add this optional format (or syntax, or whatever): GIVE [npc] [object(s)].

Well . . .

Long story a little shorter, I had to learn how to use regular expression capture groups to pull this off.


The Code

image

[ CLICK HERE TO VIEW THE CODE'S TEXT (for Copying and Pasting purposes) ]
clear()
var log = console.log
var regexes = [
  /^(?:give) (?<obj>.+) (?:to) (?<npc>.+)$/,
  /^(?:give) (?<npc>\w.?(?!= to)) (?<obj>.+)$/
]
var arrS = ["give the sword to rp","give rp the sword","give all to rp",
            "give rp all",  "give mask and hat to rp",  "give rp mask and hat",
           "give mask, hat, and sword to rp", "give rp mask, hat, and sword"]
var res = []
arrS.forEach((s,i) => {
  log(`INPUT: ${s}`)
  regexes.forEach((rgx,j) => {
    log(`rgx[${j}]: ${rgx}`)
    log(rgx.exec(s))
    if(rgx.exec(s)) {
        res.push(rgx.exec(s))
    }
  })
})
log(`==============RESULTS==============\nres:`)
log(res)
log(`======res breakdown=========`)
var obj,npc
res.forEach((cand,i) => {
  log(`----res.${i}----------------\nINPUT: ${cand.input}`)
  npc = cand.groups.npc
  log(`npc: ${npc}`)
  obj = cand.groups.obj
  log(`obj: ${obj}`)
  log(`----==end of res.${i}==-----`)
})


OUTPUT

[ CLICK HERE TO VIEW THE OUTPUT ]

image

image

image

image


RESULTS

[ CLICK HERE TO VIEW THE RESULTS ]

image

image


RESULTS BREAKDOWN

[ CLICK HERE TO VIEW THE RESULTS BREAKDOWN ]

image

image


So, after lots of learning and coding, I made it work (I think).

Now, I only need to learn how to feed that to the QuestJS parser. The parser is not at all set up to work with regex capture groups.

So . . .

I wrote some (sloppy) code to modify "GiveTo".

[ CLICK HERE TO VIEW THE MODIFIED 'GiveTo' CODE ]

//CHANGE GIVE TO FUNCTIONALITY (to add 'GIVE [npc] [object(s)]

findCmd("GiveTo").regexes = [/^(?:give) (?<obj>.+) (?:to) (?<npc>.+)$/,
  /^(?:give) (?<npc>\w.?(?!= to)) (?<obj>.+)$/]
  
findCmd("GiveTo").multiple = true

findCmd("GiveTo").script = function(objects) {
      //parser.debug = true
      parser.msg("Running giveTo function.")
	  var objArr = []
      findCmd("GiveTo").regexes.forEach(rgx => {
		  if (rgx.exec(parser.currentCommand.cmdString)) {
			  objArr = rgx.exec(parser.currentCommand.cmdString)
		  }
	  })
	  //game.objArr = objArr
	  var npc = objArr.groups.npc
	  if (parser.debug) {console.log("npc");console.log(npc)}
	  var obj = objArr.groups.obj
	  var isAll = false;
	  if (obj == "all") {
		  isAll = true;
		  if(parser.debug){console.log("Attempting ALL")}
		  obj = game.player.getContents()
		  if(parser.debug){console.log("Changing obj to game.player.getContents() . . . :");console.log(obj)}
	  }
	  //TODO: if object is multiple objects!!!
	  var newObj1Arr = []
	  var mult = false
	  var multObjs
	  if (!isAll && (obj.includes(', ')||obj.includes(' and '))){
		  mult = true
		  if(parser.debug){console.log("I'm running!")}
		  multObjs = obj.replace(/\, and /g, '|').replace(/ and /g, '|').replace(/\, /g, '|').split('|')
		  if(parser.debug){console.log(`multObjs`);console.log(multObjs)}
	  }
	  if(mult && multObjs.length>1) {
		  multObjs.forEach(o => {
			  if(parser.debug){console.log("Finding: "+o)}
			  var fnd = findObjByParser(o)
			  if(parser.debug){console.log(fnd)}
			  newObj1Arr.push(fnd)
		  })
		  obj = newObj1Arr
	  }
	  if(parser.debug){console.log("mult?");console.log(mult);console.log("npc");console.log(npc);console.log("objArr");console.log(objArr)}
	  objects[1][0] = findObjByParser(npc) //This should be the npc
      if (!isAll && !mult) {
		  objects[0] = [findObjByParser(obj)] //This should be the item(s)(?) given
	  }else{
		  objects[0] = obj
	  }
      if(parser.debug){console.log('objects[1][0] (the npc)');console.log(objects[1][0]);console.log('objects[0][0] (the item(s) given)');console.log(objects[0][0])}
      return handleGiveToNpc(game.player, objects);
}

//END OF CHANGE GIVE TO FUNCTIONALITY


Now, this is where I'm hitting a snag.

I recently added an OOPS command, and I modified parser.parse and parser.matchItemsToCmd to do so. Then, The Pixie updated QuestJS with his own code to add OOPS (which I honestly haven't perused because I've been busy messing with this code).

So, I'm not even certain that I'm editing the same parser.matchItemsToCmd which is now in the QuestJS code, but these are the two blocks of code from the current version of parser.matchItemsToCmd that mess up my "GiveTo" hack:

Lines 295 - 299
https://github.com/ThePix/QuestJS/blob/master/lib/_parser.js#L295

        if (list.length > 1 && !cmd.objects[i].multiple) {
          res.error = no_multiples_msg;
          res.score = -1;
          return res;
        }

...and lines 313 - 317:
https://github.com/ThePix/QuestJS/blob/master/lib/_parser.js#L313


        if (objectNames.length > 1 && !cmd.objects[i].multiple) {
          res.error = no_multiples_msg;
          res.score = -1;
          return res;
        }

I cannot find any object with a multiple attribute set on it. So, I don't understand why that code is there (which definitely doesn't mean that code serves no purpose (it may deal with containers or something)). I commented out both of those blocks of code, and my stuff (mostly) works now! (I just hope I didn't break something else in the game world!)

Here are some screenshots of the gameplay:

[ CLICK HERE TO VIEW THE SCREENSHOTS ]

image


image


image


image


image


image


ISSUES

I just realized there are (at least) two issues whilst writing this.

  1. I didn't code the comma splitting correctly.

image


  1. I can't use articles with the objects.

image


I probably just need to change one bit of code to fix the comma issue, and there's probably a parser function that removes an object's articles (a, an, the, etc.) before trying to match the text to objects.

After a nap, I'll try to fix those two things and come back and edit the code.

As always, comments and suggestions are welcomed!

Happy gaming!


PS

I had to add something to this post to edit the title.


UPDATE

I solved the issue with the articles quite easily.

I mostly (technically) fixed the issue with commas, but I just realized I'm using the version of QuestJS that didn't have the Oxford comma coded. Therefore, I can do give this, that and the other to ralph, but not give this, that, and the other to ralph.

That Oxford comma has been indirectly kicking my butt for two hours. I just now planted enough logs to the console to figure out that it never makes it to my "GiveTo" script at all.

More on this as it comes in. Plus, I'll update the code.


UPDATE

For the record, this is for QuestJS v0.3.


It seems to be in working order now.

Adding this line of code (which familiarizes the parser with the Oxford comma) fixed my main issue:

lang.joiner_regex = /\b\, and\b|\,|\band\b/

I forgot to include this code in the original post, but the function needed to be updated (meaning fixed) anyway:

function findObjByParser(s){
	console.log("Running findObjByParser")
	console.log(s)
	s = lang.article_filter_regex.exec(s)[1]
	console.log(s)
	var scr = parser.findInList(s,allObjects(),{})
	if (scr.length>1) {
		scr = findObjByParser(s,scr)
	}
	scr = scr[0]
	console.log(scr)
	return scr
}


My final (?) "GiveTo" script:

//CHANGE GIVE TO FUNCTIONALITY (to add 'GIVE [npc] [object(s)]

findCmd("GiveTo").regexes = [/^(?:give) (?<obj>.+) (?:to) (?<npc>.+)$/,
  /^(?:give) (?<npc>\w.?(?!= to)) (?<obj>.+)$/]
  
//findCmd("GiveTo").multiple = true

findCmd("GiveTo").script = function(objects) {
      //parser.debug = true
      //console.log("Running giveTo function.")
	  var objArr = []
      findCmd("GiveTo").regexes.forEach(rgx => {
		  if (rgx.exec(parser.currentCommand.cmdString)) {
			  objArr = rgx.exec(parser.currentCommand.cmdString)
		  }
	  })
	  //game.objArr = objArr
	  var npc = objArr.groups.npc
	  if (parser.debug) {console.log("npc");console.log(npc)}
	  var obj = objArr.groups.obj
	  if (parser.debug) {console.log("obj");console.log(obj)}
	  var isAll = false;
	  var newObj1Arr = []
	  var mult = false
	  var multObjs
	  if (obj == "all") {
		  isAll = true;
		  if(parser.debug){console.log("Attempting ALL")}
		  obj = game.player.getContents()
		  if(parser.debug){console.log("Changing obj to game.player.getContents() . . . :");console.log(obj)}
	  }else{
		  //console.log("Not ALL")
		  if (obj.includes(',')||obj.includes(' and ')){
			  mult = true
			  if(parser.debug){console.log("I'm running!")}
			  //console.log(obj)
			  multObjs = obj.replace(/\, and /g, '|').replace(/\, /g, '|').replace(/ and /g, '|').trim().split('|')
			  //console.log(multObjs)
			  multObjs.forEach((o,i) => {
				  multObjs[i] = o.trim()
			  })
			  if(parser.debug){console.log(`multObjs`);console.log(multObjs)}
		  }
		  if(mult && multObjs.length>1) {
			  multObjs.forEach(o => {
				  o = o.trim()
				  if(parser.debug){console.log("Finding: "+o)}
				  var fnd = findObjByParser(o)
				  if(parser.debug){console.log(fnd)}
				  newObj1Arr.push(fnd)
			  })
			  obj = newObj1Arr
		  }
	  }	
	  if(parser.debug){console.log("mult?");console.log(mult);console.log("npc");console.log(npc);console.log("objArr");console.log(objArr)}
	  objects[1][0] = findObjByParser(npc) //This should be the npc
      if (!isAll && !mult) {
		  objects[0] = [findObjByParser(obj)] //This should be the item(s)(?) given
	  }else{
		  objects[0] = obj
	  }
      if(parser.debug){console.log('objects[1][0] (the npc)');console.log(objects[1][0]);console.log('objects[0][0] (the item(s) given)');console.log(objects[0][0])}
      return handleGiveToNpc(game.player, objects);
}

//END OF CHANGE GIVE TO FUNCTIONALITY



Also, I still have to modify these IF blocks in parser.matchItemsToCmd:

        list = list.filter(function(el) { return !exclude.includes(el); });
        //KV removed next block because this seems to want the NPC to have multiple setup by default?
        //TODO  maybe this affects CONTAINERS ???
        //if (list.length > 1 && !cmd.objects[i].multiple) {
          //console.log(list)
          //console.log(cmd)
          //console.log(cmd.objects[i]) //This is the npc.
          //res.error = no_multiples_msg;
          //res.score = -1;
          //return res;
        //}
        score = 2;
        res.objects.push(list.map(function(el) { return [el]; }));
        res.matches.push(arr[i]);
        res.all = true;
      }
      
      else {
        objectNames = arr[i].split(lang.joiner_regex).map(function(el){ return el.trim() });
        //KV removed next block because this seems to want the NPC to have multiple setup by default?
        //TODO  maybe this affects CONTAINERS ???
        //if (objectNames.length > 1 && !cmd.objects[i].multiple) {
          //console.log(cmd)
          //console.log(cmd.objects[i]) //This is the npc
          //res.error = no_multiples_msg;
          //res.score = -1;
          //return res;
        //}

Here's the original code on lines 295 - 313: https://github.com/ThePix/QuestJS/blob/master/lib/_parser.js#L295

It throws an error when I "give ralph all", but not when I "give rp all" or "give all to ralph".

Still a mystery to me, and I haven't tested out other objects after changing that.

image



It's probably a setting on the Ralph object.

Here's Ralph:

createItem("Ralph", NPC(false), {
  loc:"cellar",
  examine:"Your trusty sidekick.",
  regex:/^(ralph|rp)$/,
  shadowingPlayer:true,
  properName:true,
  talkto:"Ralph waves you off.",
  getAgreementGo:function(dir) {
	  msg("Ralph never voluntarily leaves your side in this game.");
	  return false;
  },
  hug:"\"Whoa, whoa, whoa,\" says Ralph.  \"Social distancing!  Remember?!?\"",
  love:"This is not that kind of game!",
});

Will update after a break.


I have yet to read this fully, this is just some initial thoughts.

First, an excellent place to test regular expressions is this web page (ideally set to ECMAScript, though it does not make much difference):
https://regex101.com/

So, the player must enter: GIVE [object(s)] TO [npc].
I want to add this optional format (or syntax, or whatever): GIVE [npc] [object(s)].

The big problem is how the parser will decide where to break the text. Consider:

GIVE BIG MIKE HAT COAT

Do you want to give Mike, the hat and the coat to Big (which could be short for Big Jim)? Or give the hat and coat to Big Mike?

The parser is not at all set up to work with regex capture groups.

Not sure what that means. The parser does just that!

Then, The Pixie updated QuestJS with his own code to add OOPS (which I honestly haven't perused because I've been busy messing with this code).

Should not be an issue; I do not think I edited the parser itself, just added the command to _commands.js and modified _io.js. The only change to _parser.js in the last month was your fix for DROP ALL.

I cannot find any object with a multiple attribute set on it. So, I don't understand why that code is there (which definitely doesn't mean that code serves no purpose (it may deal with containers or something)).

There are commands with "multiple" set, indicating they can be used with multiple items. If you set your command to multiple:true, that may fix it - though the parser may expect the first item to be multiple rather than the last. Thinking about it, that could be an issue in some languages.

I mostly (technically) fixed the issue with commas, but I just realized I'm using the version of QuestJS that didn't have the Oxford comma coded.

It is only coded in the output, i.e., formatList, so not an issue. I am not sure how the input handles commas... I will have to check.

ETA: The commas in the input are handled with a regex called "joiner_regex", and the Oxford comma can be accomodated by changing that:

  joiner_regex:/\band\b|\, ?and\b|\,/,

The commas in the input are handled with a regex called "joiner_regex", and the Oxford comma can be accomodated by changing that

I found that just before my last post last night. My regex works but I like your regex better.

I also found that I had already transferred my game into a QuestJS v0.3 folder. So, I did already have the updated formatList. It took a while to find joiner_regex, but changing that immediately solved that issue.


The big problem is how the parser will decide where to break the text. Consider:

GIVE BIG MIKE HAT COAT

Do you want to give Mike, the hat and the coat to Big (which could be short for Big Jim)? Or give the hat and coat to Big Mike?

Very good point. I had not considered object names with more than one word.

I need to test that with my current code.


The parser is not at all set up to work with regex capture groups.

Not sure what that means. The parser does just that!

Well, I'm probably not using the proper terminology.

I figured out how to add what I believe is called regex templates in Quest 5.

I changed the regex on GiveTo:

//CHANGE GIVE TO FUNCTIONALITY (to add 'GIVE [npc] [object(s)]

findCmd("GiveTo").regexes = [/^(?:give) (?<obj>.+) (?:to) (?<npc>.+)$/,
  /^(?:give) (?<npc>\w.?(?!= to)) (?<obj>.+)$/]

And I easily pull the matched text for <obj> and <npc> like so:

findCmd("GiveTo").script = function(objects) {
	  var objArr = []
          findCmd("GiveTo").regexes.forEach(rgx => {
		  if (rgx.exec(parser.currentCommand.cmdString)) {
			  objArr = rgx.exec(parser.currentCommand.cmdString)
		  }
	  })
	  var npc = objArr.groups.npc //This is the magic!
	  var obj = objArr.groups.obj //This is the magic!


//...the code continues...

Does the parser already check for anything like <object> or <object1> and <object2>in the regex?

I (think I) have to reset objects[1][0] (which is the npc in GiveTo) and objects[0] manually after my script finds the JS objects that match <npc> and <obj> from my regex capture groups.


Right now, everything seems to work except the one way I enter GIVE RALPH ALL.

I get: I don't even know where to begin with that.

But everything else works: GIVE RP ALL or GIVE ALL TO RALPH or GIVE ALL TO RP.

For some reason, the parser doesn't like it when I say RALPH before I say ALL.

ACTUAL TEXT FROM THE GAME:

> give ralph cig

I don't even know where to begin with that.

> give rp cig

Done.

> give mask to rp

Done.

> give lighter to ralph

Done.
>

Like I say, I'm sure I've done something to the code that makes it ignore Ralph's actual name. I shall research that and return here.


Does the parser already check for anything like <object> or <object1> and <object2>in the regex?

It does... But not by name. JavaScript support for named capture groups is pretty recent - 2018, when I started this, and browser support was patching.

It assumes you capture groups are in order. What is passed to the command script is an array of objects in the order of the capture groups.

This would mean two commands for GIVE. But both could use the same function, so not much of a deal.

Does the parser already check for anything like <object> or <object1> and <object2>in the regex?

Yes, if I understand right. The parser will match what is thinks are suitable objects before passing the results to the command script.

I don't even know where to begin with that.

You can use the PARSER command to get some idea of how the parser is comparing different options for commands and then fir objects to work with them.


I am miscommunicating. I should be saying "named capture groups" rather than "capture groups".

As far as I can tell, nothing in the existing code is checking for named capture groups.

Oh, I see. You just said "not by name" because it's relatively new.

Hrmm. I wonder how hard it would be to have the actual parser try to match named groups and move on to do what is already scripted if none exist . . .


That site to which you linked to test regex is freakin' awesome!

SCREENSHOT

image


You can use the PARSER command

I can't get that command to work. I've tried with the game set on "dev" and on "beta".

I have to manually do parser.debug = true to get the parser messages.

Does it have to be all caps? (Or am I just being silly?) If it does need to be all caps, does this line in parser.js affect that?: let cmdString = inputText.toLowerCase().trim().replace(/\s+/g, ' ');


This would mean two commands for GIVE.

Hey . . .

I just looked, and that's how Inform 7 handles it, too. I added a regex to the GiveTo. Perhaps creating a GiveToToo with a separate regex (leaving GiveTo's regex alone) would do the trick.

That's a marvelous idea!

[MANIACAL LAUGH]


SIDETRACK

I can't find the PARSER command.

I listed out all the commands in the console, but I can't see it. (I also printed out the regexes, just to make sure I wasn't overlooking anything.

commands.forEach(n => {console.log(n.name)})

MetaHelp

MetaHint

MetaCredits

MetaDarkMode

MetaSilent

MetaWarnings

MetaSpoken

MetaIntro

MetaBrief

MetaTerse

MetaVerbose

MetaTranscript

MetaTranscriptOn

MetaTranscriptOff

MetaTranscriptClear

MetaTranscriptShow

MetaTranscriptShowWithOptions

MetaTranscriptToWalkthrough

MetaPlayerComment

MetaSave

MetaSaveGame

MetaLoad

MetaLoadGame

MetaDir

MetaDeleteGame

MetaUndo

MetaAgain

Look

Exits

Wait

TopicsNote

Inv

Map

Smell

Listen

PurchaseFromList

Examine

LookAt

LookOut

LookBehind

LookUnder

LookInside

Search

Take

Drop

Wear2

Wear

Remove

Remove2

Read

Eat

Purchase

Sell

Smash

SwitchOn

SwitchOn2

SwitchOff2

SwitchOff

Open

Close

Lock

Unlock

Push

Pull

Fill

Empty

SmellItem

ListenToItem

Eat

Drink

Ingest

SitOn

StandOn

ReclineOn

GetOff

Use

TalkTo

Topics

Say

Stand

NpcStand

FillWith

NpcFillWith

PutIn

NpcPutIn

TakeOut

NpcTakeOut

GiveTo

NpcGiveTo

PushExit

NpcPushExit

TieTo

NpcTieTo

Untie

NpcUntie

UntieFrom

NpcUntieFrom

UseWith

AskAbout

TellAbout

Oops

Love

Hug

Light

Enjoy

Smoke

MetaRestart

MetaQuit

NpcExamine

NpcLookAt

NpcLookOut

NpcLookBehind

NpcLookUnder

NpcLookInside

NpcSearch

NpcTake

NpcDrop

NpcWear2

NpcWear

NpcRemove

NpcRemove2

NpcRead

NpcEat

NpcPurchase

NpcSell

NpcSmash

NpcSwitchOn

NpcSwitchOn2

NpcSwitchOff2

NpcSwitchOff

NpcOpen

NpcClose

NpcLock

NpcUnlock

NpcPush

NpcPull

NpcFill

NpcEmpty

NpcSmellItem

NpcListenToItem

NpcEat

NpcDrink

NpcIngest

NpcSitOn

NpcStandOn

NpcReclineOn

NpcGetOff

NpcUse

GoNorthwest

NpcGoNorthwest2

GoNorth

NpcGoNorth2

GoNortheast

NpcGoNortheast2

GoIn

NpcGoIn2

GoUp

NpcGoUp2

GoWest

NpcGoWest2

GoEast

NpcGoEast2

GoOut

NpcGoOut2

GoDown

NpcGoDown2

GoSouthwest

NpcGoSouthwest2

GoSouth

NpcGoSouth2

GoSoutheast

NpcGoSoutheast2


It is a debugging command, so only present if:

settings.playMode = 'dev'

(EDIT: I see more posts while I was typing this; so it's probably redundant. But may as well share anyway)

(EDIT 3: changed handling of commas in the pseudocode so that objects with commas in their names don't cause issues)

/^(?:give) (?<npc>\w.?(?!= to)) (?<obj>.+)$/

So that matches (taking the parts one by one)…

  • /(?:give) / - the literal word "give". Exactly the same behaviour as /give / so not sure why the parentheses are there
  • /(?<npc>\w.?(?!= to))/ - the named group npc, which matches either one or two characters, the first of which must be a letter or underscore, which are not followed by the string = to.
  • / / - a space (I'm including this as a separate part because it isn't captured)
  • /(?<obj>.+)$/ - the named capture group obj, which matches any one or more characters until the end of the string

I've only skimmed this post because there's so much of it. But do you really want to limit the npc name to 2 letters?

I suspect you want something more like:

/^give(?!.*\bto\b) (?<npc>\w+?) (?<obj>.+)$/i

which is slightly slower, and still forces the NPC name to be a single word, but allows it to be longer than 2 letters.

If you want the NPC name and the object to both have spaces in, you need a lexer rather than a pattern engine. (as in, you need to interpret the line in the context of the available objects). You'd probably want to match everything as a single string and then interpret it later.

An algorithm for this could be (pseudocode):

  • objects = new Array()
  • npcpos = 0
  • words = args.split (/\s+|\b/)
  • while (words) {
    • while (current_word.match(/^(?:and|or|\W+)$/))
      • current_word = words.shift
    • if (current_word == "to") {
      • current_word = words.shift
      • npcpos = objects.length
    • }
    • current_word = words.shift
    • matched_objects = all objects in scope whose names include current_word
    • while (words && any of matched_objects contains words[0] in its name && !current_word.match(/,$/)) {
      • current_word = words.shift
      • remove from matched_objects any whose name doesn't include current_word
    • }
    • if (matched_objects.length == 1)
      • objects.push( matched_objects[0] )
    • else
      • disambiguation time! no idea how this is handled
  • }
  • npc = objects.splice(npcpos, 1).shift

It basically parses a list of objects where the comma and/or "and" between objects is optional; by checking each word to see if it could be part of the name of the previous object, and assuming a new object otherwise. Then assume the first object is the NPC. Making a regex do this would require the regex to be modified based on the objects in scope, and would be horrific.

(EDIT2: fixed the random parser pseudocode so that it works for constructs and errors like "give statue to barry", "give ralph cheese and pickles", "give john smith bell book and candle", "give ham,eggs,key to cellar, and biscuits to mike", and "give dave the key to the cellar")


It is a debugging command, so only present if:

settings.playMode = 'dev'

I have it set that way. I still can't get it to work.

SCREENSHOTS \<---

image


I'll try it in a new game (with none of my added code) later today.

[UPDATE]

In my settings.js file, settings.playMode was set to 'play' on one of the first few lines. I had other code in settings.setup which called a function which sometimes set settings.playMode to 'dev', but this was after the code already set it to 'play'. I'm assuming the initial setting is what decides whether or not the PARSER command will exist in the commands array.


I've only skimmed this post because there's so much of it. But do you really want to limit the npc name to 2 letters?

Ha! No. That has to be most of my problem, too.

I'll read your post in full and try your code out later today.

Thanks, everyone!


Looking at the pseudocode again, it might be worth adding a line at the end of the outermost while loop:

if (current_word.match(/^(?:and|to)\W*$/i)) {words.unshift(current_word);}

for the edge case where you have an object named "key to cellar" and the player types "give key to dave".
This would mean that it doesn't accept "key to" as a shortened form of "key to cellar"; it matches "key" and uses the "to" to indicate which object is the NPC


(depending on the function you use for "does this object's name contain this word", the algorithm could easily be changed to allow, for example, the player entering the words of an object name in the wrong order (treating "book about snails" and "snail book" as equivalent); or to handle pronouns, articles, and other weirdnesses)

I'm pretty sure some variant of this could be included as a parser function, for cases where the arguments to a command could be treated as a list of objects; from which the first and/or last might be treated as a separate argument, allowing for both commas, words like "and", prepositions which indicate the variable of the following word ("to", "in", "with"), and work correctly with object names that contain those words.
Working out a data structure so that commands can indicate what parameters/prepositions they understand would be time consuming, but I think that it would be worthwhile, if only because plain regexes can't tell if words like "to" or "and" are part of an object name or not.

Heck, using this for a "put" command might mean that commands like:
==> give letter to gordon to man who is eager to punch someone
can be parsed correctly :p


I have to look at mrangel's code later. I'm sure it parses better then mine. For now, all I'm using of his is the regex, and it works well with my current code.

Here's what I've got now, and everything seems to work:

[ GiveNpcStuff command ]
commands.push(new Cmd('GiveNpcStuff', {
	regex:/^give(?!.*\bto\b) (?<npc>\w+?) (?<obj>.+)$/i ,
	rules:[cmdRules.canManipulate, cmdRules.isHeld],
    objects:[
      {scope:parser.isPresent, attName: "npc"},  //This one has to go first, or it throws the multiple error.  I don't really know why.
      {scope:parser.isHeld, multiple:true},
    ],
    defmsg:(char,item)=>{return "Char: "+char+" / item: "+item},
	script: function(objects) {
		return handleGiveNpcStuff(objects)
	},
	default:function(item, isMultiple, char) { 
		if (typeof this.defmsg === "string") {
			failedmsg(prefix(item, isMultiple) + this.defmsg, {char:char, item:item});
		}
		else if (typeof this.defmsg === "function") {
			failedmsg(prefix(item, isMultiple) + this.defmsg(char, item), {char:char, item:item});
		}
		else {
			errormsg("No default set for command '" + this.name + "'.");
		}
		return false;
	},
}))
[ handleGiveNpcStuff function ]
function handleGiveNpcStuff(objects){
	  //parser.debug = true
      if(parser.debug){
		   alert("RUNNING GiveNpcStuff function!")
		   console.log("Running GiveNpcSomething_RH function.")
	  }
	  var objArr = []
      findCmd("GiveNpcStuff").regexes.forEach(rgx => {
		  if (rgx.exec(parser.currentCommand.cmdString)) {
			  objArr = rgx.exec(parser.currentCommand.cmdString)
		  }
	  })
	  var npc = objArr.groups.npc
	  if (parser.debug) {console.log("npc");console.log(npc)}
	  var obj = objArr.groups.obj
	  if (parser.debug) {console.log("obj");console.log(obj)}
	  var isAll = false;
	  var newObj1Arr = []
	  var mult = false
	  var multObjs
	  if (obj == "all") {
		  isAll = true;
		  if(parser.debug){console.log("Attempting ALL")}
		  obj = scopeHeldBy(game.player)
		  if(parser.debug){console.log("Changing obj to game.player.getContents() . . . :");console.log(obj)}
	  }else{
		  //console.log("Not ALL")
		  if (obj.includes(',')||obj.includes(' and ')){
			  mult = true
			  if(parser.debug){console.log("I'm running!")}
			  //console.log(obj)
			  multObjs = obj.replace(/\, and /g, '|').replace(/\, /g, '|').replace(/ and /g, '|').trim().split('|')
			  //console.log(multObjs)
			  multObjs.forEach((o,i) => {
				  multObjs[i] = o.trim()
			  })
			  if(parser.debug){console.log(`multObjs`);console.log(multObjs)}
		  }
		  if(mult && multObjs.length>1) {
			  multObjs.forEach(o => {
				  o = o.trim()
				  if(parser.debug){console.log("Finding: "+o)}
				  var fnd = findObjByParser(o)
				  if(parser.debug){console.log(fnd)}
				  newObj1Arr.push(fnd)
			  })
			  obj = newObj1Arr
		  }
	  }	
	  if(parser.debug){console.log("mult?");console.log(mult);console.log("npc");console.log(npc);console.log("objArr");console.log(objArr)}
	  objects[1][0] = findObjByParser(npc) //This should be the npc
      if (!isAll && !mult) {
		  objects[0] = [findObjByParser(obj)] //This should be the item given
	  }else{
		  objects[0] = obj  //The items given have already been put into this list
	  }
      if(parser.debug){console.log('objects[1][0] (the npc)');console.log(objects[1][0]);console.log('objects[0][0] (the item(s) given)');console.log(objects[0][0])}
      return handleGiveToNpc(game.player, objects);	
}
[ findObjByParser function ]
function findObjByParser(s){
	if(parser.debug) {
		console.log("Running findObjByParser")
		console.log("DATA: "+s)
	}
	s = lang.article_filter_regex.exec(s)[1]
	if(parser.debug){console.log("After article filter regexp exec:");console.log(s);}
	var scr = parser.findInList(s,allObjects(),{})
	if(parser.debug){console.log("scr: ");console.log(scr)}
	if (scr.length>1) {
		if(parser.debug){console.log("scr.length is > 1!");console.log(scr)}
		scr = findObjByParser(s,scr)
	}
	scr = scr[0]
	if(parser.debug) console.log(scr)
	return scr
}


TRANSCRIPT

[ TRANSCRIPT TEXT]
You can go up.

> give rp all

Cigarette: Done.

Purple lighter: Done.

Mask: Done.

> undo

Undoing...
The cellar

The cellar is small, dimly lit, and dingy.

You can see Ralph and a TV here.

You can go up.

> give all to rp

Cigarette: Done.

Purple lighter: Done.

Mask: Done.

> undo

Undoing...
The cellar

The cellar is small, dimly lit, and dingy.

You can see Ralph and a TV here.

You can go up.

> give mask, cig, and light to rp

Mask: Done.

Cigarette: Done.

Purple lighter: Done.

> undo

Undoing...
The cellar

The cellar is small, dimly lit, and dingy.

You can see Ralph and a TV here.

You can go up.

> give ralph mask, cig, and light

Mask: Done.

Cigarette: Done.

Purple lighter: Done.

> undo

Undoing...
The cellar

The cellar is small, dimly lit, and dingy.

You can see Ralph and a TV here.

You can go up.

> give all to ralph

Cigarette: Done.

Purple lighter: Done.

Mask: Done.
>


Here is a link to this "test" game's title page, if anyone feels like trying to break something:

Quest 6 Game 1


PS

If anyone gives this a play, you could also test this out (just for kicks):

  • Turn on the TV in the first room. (If on a mobile browser, I believe you have to start the video manually, as autoplay usually won't work with the volume on.)

  • Leave it on and go UP. (The YouTube video should disappear and the volume should lower, as if the TV were a room away.)

  • Go UP once more and the TV's sound should disappear.

  • Go back DOWN, and the muffled sound should return.

  • Going DOWN once more brings you back to the first room, and the video should pop back up on full volume.


UPDATE

The PARSER command works. I had some invalid code.

Sorry, Pixie, if I made you waste time on that.


That is really cool.

Could you write up how you did the video in a form that can be added to the documentation? It is something that has been on the list of outstanding features for over year.

I would also be interested how you did the landing page.


I would also be interested how you did the landing page.

I just created a landing page ('index.html') and added it to my main directory.

index.html
<!DOCTYPE html>
<html>
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
		<meta charset="UTF-8">
		<title>Quest 6 Adventure 1  by Richard Headkid</title>
		<style >
			body{
				background-color:lightgoldenrodyellow;
				font-family: courier, monospace;
				font-weight:bold;
				line-height:16px;
				font-size:16px;
			}
			#game-desc{
				margin-right:15%;
				margin-left:15%;
				text-align: justify;
				text-justify: inter-word;
				border:5px solid black;
				background-color:silver;
				padding:12px;
			}
			
			button {
			    background-color: Green;
			    border: 2px solid black;
			    color: white;
			    padding: 15px 32px;
			    text-align: center;
			    text-decoration: none;
			    display: inline-block;
			    font-size: 22px;
			  	font-family: courier, cursive;
			  	font-weight:bold;
			  	width: 34%;
			}
			button:hover {
			  background-color: blue;
			  color: white;
			  border: 2px solid green;
			}
			
			buttonalt {
			    background-color: green;
			    border: 2px solid black;
			    color: silver;
			    padding: 15px 32px;
			    text-align: center;
			    text-decoration: none;
			    display: inline-block;
			    font-size: 14px;
			  	font-family: 'Rye', cursive;
			  	width: 33%;
			}
			buttonalt:hover {
			  background-color: white;
			  color: green;
			  border: 2px solid saddlebrown;
			}
		
		</style>
		
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
		<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
		<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
		
		
	</head>
	
	<body>
		
		<center id="thetopofthepage">
			<!--<audio controls loop autoplay controlsList="nodownload">
				<source src="" type="audio/ogg">
				Your browser does not support the audio element.
			</audio>-->
		</center>
		<!--<br /><br />
		<img src="" id="base64image" style="width:100%;"><br /><br />-->
		<center><h1>Quest 6 Adventure 1</h1><h3>by Richard Headkid</h3></center>
		<hr>
		<center><u><p id='game-desc-div'>
		<big><b>GAME DESCRIPTION</b></big></u><br/>
		<p id='game-desc'></p> 
		
		</p></center>
		<hr>		
		<center><div class="button">
		  <button onclick="location.href='page.html'">Play Now!</button>
		  <!--<button onclick="location.href='src/hints.html'" target="_blank">Hints & Clues</button>-->
		</div></center>
		<center><div class="button">
		  <button onclick="location.href='quest6game1.zip'">Download the website (zip file)</button>
		</div></center>
		<hr />
		<center>Comments or suggestions?</center><br />
		<center>Please contact me at:<br /> <small><a href="http://textadventures.co.uk/user/view/gudejtsx9komagj-tffyfg/richard-headkid" target="_blank">RICHARD HEADKID @ TextAdventures </a></small></center>
		<hr /><center><br /><br />
		<p style="font-family: courier, monospace;font-weight:bold;color:black;line-height:18px; font-size:16px;" id="demo" onclick="myFunction()"><big><b><u><big>CREDITS</big></u></b></big></p>
		
		</center>
		<script>
		var desc = "In this small text adventure, you and your trusty sidekick, Ralph, "+
		    "need to look around and interact with things (and XanMag) until you figure out what to do."+
		    "<br/><br/>It's pretty straightforward.  It's really just to test out the new version of Quest."+
		    "<br/><br/><center>(Click on CREDITS at the bottom of this page for more info!)</center>";
		    
		$("#game-desc").html(desc);
		    
		    
		function myFunction() {
			var s = "<big><b>CREDITS</b></big><br /><br />"+
			"The main game was created with <a href='https://github.com/ThePix/QuestJS/wiki' target='_blank'>"+
			"Quest 6 AKA QuestJS</a>."+
			//"<br /><br />The HINTS & CLUES menu was "+
			//"created with <a href='https://twinery.org' target='_blank'>Twine</a>."+
			"<br />I learned how to code this "+
			"page by studying up at <a href='https://www.w3schools.com' target='_blank'>www.w3schools.com</a>."+
			"<br /><br/>"+
			"Don't forget those who make these games available to the public AT NO CHARGE:<br />"+
			"<a href='http://textadventures.co.uk/' target='_blank'>TEXTADVENTURES.CO.UK</a><br />"+
			"<a href='http://ifdb.tads.org/' target='_blank'>IFDB</a><br />"+
			"<a href='http://ifarchive.org/' target='_blank'>IFArchive</a><br /><br />"+
			"This game is a work of fiction.<br />Any names, characters, businesses, places, events,"+
			" incidents, products, spells, weapons, talking animals, magikal things, and non-magikal things"+
			" are purely fictitious. Any resemblance to actual persons, living or dead, or actual businesses,"+
			" places, events, incidents, products, spells, weapons, talking animals, magikal things, or non-magikal"+
			" things is purely coincidental.<br /><br />Ralph the penguin was created by an author friend of mine,"+
			" and appears in this game with his consent.<br /><br />"+
			"<strong>SPECIAL THANKS TO:</strong><br /><br />LUKE A. JONES, ANDREW PLOTKIN (AND ASSOCIATES),"+
			" EMILY SHORT (AND ASSOCIATES), THE PIXIE, MRANGEL, XANMAG, AND EVERYONE AT TEXTADVENTURES.CO.UK "+
			"(EXTRA SPECIAL THANKS TO LUIS FELIPE MORALES), CHRIS KLIMAS (AND ASSOCIATES), "+
			"STEVE MERETZKY (AND ASSOCIATES, AND EVERYONE WHO EVER WORKED FOR INFOCOM), EVERYONE AT IFDB, "+
			"EVERYONE AT IFARCHIVE (EXTRA SPECIAL THANKS TO DOUG),<br /><br />"+
			"<big>AND AN ESPECIALLY BIG THANKS TO: PLAYERS LIKE YOU!</big></p>"+
			"<a href='#thetopofthepage'><br />BACK TO TOP</a>";
			
		    $("#demo").html(s);
		    
		    
		}
		</script>
	</body>

</html>




As far as I understand it, when we upload a ZIP file as a game, this site has default ways of choosing which page is the "home" page. First, it checks for 'index.html'. If that doesn't exist, it checks for 'play.html'. If neither exist, and there is only one HTML file, it just loads that page. If there are numerous HTML files in the main directory, and none are named "play" or "index", I have no clue how it works. (Alphabetical order maybe?)


That is really cool.

Well, thank ya'. Thank ya' very much!

EDIT

I started a new thread about YouTube API.


Could you write up how you did the video in a form that can be added to the documentation?

Certainly, but I'd better make sure my code doesn't need any updating before I do that. (It most assuredly does.)


Just to include all the YouTube stuff here (in case I lose it somehow), see this thread. (It's the same one to which I linked above.)


(If on a mobile browser, I believe you have to start the video manually, as autoplay usually won't work with the volume on.)

This isn't necessarily the case. If, rather than using autoplay, you use the API to start the video as a response to a click or keypress on the same page, it would be allowed.

On Quest 5.x, you couldn't autoplay videos because clicking a command link or typing a command has the sole result of sending a string to the server. When you execute a command, the stuff that comes up on screen is in response to a message back from the server, and responses to external events can't trigger autoplay.

But with a more JS-based engine, I think it should be possible to have the video load and initialise when you enter the room, and use the YouTupe API to press play and unhide it in response to the player clicking/typing "turn on tv". Not sure if some workaround would be necessary; but if the command script is called (through however many layers of function calls) from an 'click' or 'keyup' event within the same page as the video, it should work.


This isn't necessarily the case. If, rather than using autoplay, you use the API to start the video as a response to a click or keypress on the same page, it would be allowed.

That fits with what I found when looking at sound.


This isn't necessarily the case. If, rather than using autoplay, you use the API to start the video as a response to a click or keypress on the same page, it would be allowed.

Hrmm... I shall add the call to play the video the first time the TV is turned on. As it is now, it loads the video the first time, and that made it play on my PC. So, I never coded it to play in that code block. After testing on my Android, I just assumed it wouldn't autoplay due to the mobile restrictions. But, like I say, I'm going to add the call to play after loading it initially and see how that works out on mobile.

Will update.


Nope. (Unless it's operator error on my behalf.)

Even having the API call the play function doesn't automatically play it in my Andoid's Chrome browser.

...nor does it work in its Firefox browser.

I have to manually click on it to start the video before the rest of the YouTube functions work. (The phone is a Galaxy S6, so it is rather old.)


Not sure if there could be some indirection somewhere between the click handler and the actual function.
Or if loading the video (but having it hidden) when the room is first entered could make a difference; so that the video is already in existence when the click event is delivered.

I heard that some ads are using a workaround where they have autoplay but are muted (which is allowed for some reason), and then using a timeout to turn the volume up after a second. But I wouldn't try that, as it's likely to be fixed soon if it hasn't already.


UPDATE

I got sidetracked with YouTube.

I removed my changes to the GiveTo command, and I created a GiveNpcStuff command with this regex:

(?!.* to )^(?:give) (?<npc>.+?) (?<obj>.+)$


The (?!.* to )^ tells it NOT to match any string that includes the word "to". (I call this overkill, as it seemed to work already with mrangel's code. I'm just trying to learn stuff. #KnowingIsHalfTheBattle)



Not sure if there could be some indirection somewhere between the click handler and the actual function.

I just added the call to play the video to onPlayerReady function. Now, I have to upload the game again and check with my phone to see if it makes the video start by itself.

More on this as it comes in . . .


PS

(I might create a new thread for this YouTube API stuff. The regex is crazy enough on its own.)

EDIT

http://textadventures.co.uk/forum/questkit/topic/vqas_xivue_l3jugeo7t5a/quest-6-adding-youtube-api#a6228584-2e1c-498e-8ffb-74fd74d17929


Back to RegExp name capture groups . . .

I broke out my Windows 7 machine. When I tried GIVE RALPH MASK, it got an error. The game still did everything else, even after that, but it definitely didn't work. If this were an important thing to do to win the game, that would be bad.

But Firefox constantly badgered me to update. So, I don't know. After updating Firefox, it worked fine in Windows 7. I guess if someone has a machine that's older than Windows 7, they couldn't update to a recent browser version maybe?

I don't know . . .

Either way, Quest 6 is Pixie's baby. If Pixie decides name capture groups are no bueno, then I shall post code without them.


New COMMAND: giveNpcStuff

Pattern: GIVE [npc] [objects]


commands.push(new Cmd('GiveNpcStuff', {
	regex:/(?!.* to )^give (\w+?) (.+)$/i,
	rules:[cmdRules.canManipulate, cmdRules.isHeld],
    objects:[
      
      {scope:parser.isPresent, attName: "npc"},  //This one has to go first, or it throws the multiple error.  I don't really know why.
      {scope:parser.isHeld, multiple:true},
    ],
    defmsg:(char,item)=>{return "Char: "+char+" / item: "+item},
	script: function(objects) {
		return handleGiveNpcStuff(game.player, objects)
	},
	default:function(item, isMultiple, char) { 
		if (typeof this.defmsg === "string") {
			failedmsg(prefix(item, isMultiple) + this.defmsg, {char:char, item:item});
		}
		else if (typeof this.defmsg === "function") {
			failedmsg(prefix(item, isMultiple) + this.defmsg(char, item), {char:char, item:item});
		}
		else {
			errormsg("No default set for command '" + this.name + "'.");
		}
		return false;
	},
}))

function handleGiveNpcStuff(char, objects) {
  let success = false;
  const npc = objects[0][0];
  const multiple = objects[1].length > 1 || parser.currentCommand.all;
  if (!npc.npc && npc !== game.player) {
    failedmsg(lang.not_npc_for_give, {char:char, item:npc});
    return world.FAILED; 
  }
  for (let obj of objects[1]) {
    let flag = true;
    if (!char.getAgreement("Give", obj)) {
      // The getAgreement should give the response
    }
    if (npc.testRestrictions) {
      flag = npc.testRestrictions(obj);
    }
    if (!npc.canManipulate(obj, "give")) {
      return world.FAILED;
    }
    if (flag) {
      if (!obj.isAtLoc(char.name)) {
        failedmsg(prefix(obj, multiple) + lang.not_carrying, {char:char, item:obj});
      }
      else {
        if (npc.giveReaction) {
          npc.giveReaction(obj, multiple, char);
        }
        else {
          msg(prefix(obj, multiple) + lang.done_msg);
          obj.moveToFrom(npc.name, char.name);
        }
        success = true;
      }
    }
  }
  if (success === world.SUCCESS) char.pause();
  return success ? world.SUCCESS : world.FAILED;
}

NOTE

I combined mrangel's regex with mine, and this works perfectly (as far as I've tested it).


See here:
https://github.com/ThePix/QuestJS/wiki/Implementation-notes#no-named-capture-groups

If I was doing this in 2022, it would be different, and it might be worthwhile changing at some point.


If I was doing this in 2022, it would be different

I wasn't trying to be negative (if it seemed that way).


it might be worthwhile changing at some point.

Maybe, but rewriting all that parser stuff sounds like an awful lot of work. If I got this pattern to work without the named capture groups, I bet most other patterns can be matched without it, too.


All in all, I'm having a marvelous time learning all this JS.

And I get to help some people out along the way?

Oh yeah. This is fun.


I didn't realise support was so recent. Javascript regular expressions must have lagged a bit behind other languages.

I remember the first time I was working on a project and got told not to use named groups because not everyone had upgraded to Perl 5.10 yet; that was in 2008.


Log in to post a reply.

Support

Forums