Help with using datamaps for an inventory system

Hello,
I’m using Harlowe 3.3.7 in Twine, and I’m pretty new at writing code for this platform. I am trying to set up an inventory system where items can be accessed, added, removed, etc…
I have been trying to use a datamap to do this but keep running into errors with the macros I try. I’d like to be able to loop through the datamap and be able to access and change specific names (representing items) and values (representing cost) stored in it. I know that just calling the inventory’s var name will print its values but that’s not really my goal. Here’s an example of what I’ve tried so far:

(set: $inventory to (dm: "item a", 3, "item b", 2, "item c", 7, "item d", 4))
$inventory

<!--get number of pairs in a datamap-->
(set: $DMpairCount to (macro:datamap-type _datamap, [
    (output-data: (dm-names: _datamap)'s length)]))
count of inventory pairs: ($DMpairCount: $inventory)

<!-- this works to get a name and value pair but i'm not sure how to access them both at once in a single macro -->
(print: (dm-names:$inventory)'s 1st)
(print: $inventory's (dm-names:$inventory)'s 1st)

<!-- NOT WORKING - this returns all of the names but should just print "item a" -->
(nth: 1, (dm-names:$inventory))

(set: $accessInv to (macro: dm-type _inv,  [
    (if: ($DMpairCount: $inventory) is not 0)[
    (for: each _item, ...(dm-names:_inv))[
    <!--If this worker, I would add code here to access the corresponding value of each item, and (see next code section) be able to change the values based on type of transaction-->
    (output-data: _item)]](else:)[(output-data: "this inventory is empty")))]
]))
($accessInv: $inventory)

I’m just stuck and don’t know if what I’m trying to do is possible, although the concept seems simple to me. The code snippet below is an example of a different inventory system (for the player) which works and can handle addition and subtraction of values, but the difference is that it is an array instead of a datamap. Would using 2 sets of arrays (or more) for this other inventory system make more sense since macros like (count:) can only be used with with arrays? Or some other method entirely?

<!--code to update player inventory-->
(set: $updateInv to (macro: str-type _name,str-type _type, [
	(if: $money is > 0)[(set: $inv to it + (a: "$money coins"))]
    (if: _type is "add")[(set: $inv to it + (a: _name))
    (output-data: _name)]
    (else-if: _type is "sub")[(set: $inv to it - (a: _name))
    (output-data: _name)
    ]

Thanks very much to anyone who might have some insight.

First a little background:

1: The is keyword comparison operator should not be combined with the mathematical based comparison operators like > or <=, because is represents “exactly equal to”.

So when you incorrectly write a conditional expression like $money is > 0 you’re stating it evaluates to true when $money is both: “exactly equal to” 0; and " “greater than” 0; …which is not possible (a).

The correct way to write (if: $money is > 0) is:

(if: $money > 0)[ thing to do when the variable's value is greater than zero ]

2: Due to how Harlowe 3 implements its Passage Transition process the content: contained within the Hook associated with the (output:) macro; or passed as an argument to the (output-data:) macro; may be processed / executed twice.

note: The result of the first execution of the “output” related macros is discarded, so you don’t need to worry about the same content being display on the page twice.

warning: If the first execution altered State (Story variables) then under some circumstances those alterations could occur twice!

For this reason the common advice is whenever possible do the “code” part of a macro within its Hook, and then pass the “output” part to (output:) or (output-data:)

eg. the (output-data:) documentation includes this example…

(set: $randomCaps to (macro: str-type _str, [
    (output-data:
        (folded: _char making _out via _out + (either:(lowercase: _char),(uppercase: _char)),
        ..._str)
    )
]))

…but that potentially means the (folded:) macro could be called twice, so the common advise would be to structure the above like so…

(set: $randomCaps to (macro: str-type _str, [
	(set: _output to (folded: _char making _out via _out + (either:(lowercase: _char),(uppercase: _char)), ..._str))
	(output-data: _output)
]))

…so that the (folded:) macro only ever gets called a single time when that custom macro is used.

Now on to your issue of accessing and outputting the content of a Data-map, and crafting custom macros to abstract the “work”.

1: Looping though the name-value pairs of your Inventory Data-map.

The following example uses the (dm-entries:) macro to access an Array of name-value pairs, and the (for:) macro to loop through that Array…

Inventory: {
	(set: _entries to (dm-entries: $inventory))
	(if: _entries's length is 0)[
		Empty.
	]
	(else:)[
		(for: each _item, ..._entries)[
			<br>(print: _item's value) x (print: _item's name)
		]
	]
}

2: Converting the above into a Custom Macro.

Following Harlowe’s documentation would suggest a custom macro like the following…

(set: $print_inv to (macro: dm-type _dm, [
	(output:)[{
		(set: _entries to (dm-entries: $inventory))
		(if: _entries's length is 0)[
			Empty.
		]
		(else:)[
			(for: each _item, ..._entries)[
				<br>(print: _item's value) x (print: _item's name)
			]
		]
	}]
]))

…which would be used like so…

Inventory: ($print_inv: $inventory)

…but that does mean the content within the (output:) macro’s Hook could end up being executed twice, which shouldn’t be a problem in this specific case because the “work” part isn’t doing much or doing it to a lot of data.

However following the common advice would produce a custom macro something like…

(set: $print_inv to (macro: dm-type _dm, [
	(set: _output to '')
	(set: _entries to (dm-entries: _dm))
	(if: _entries's length is 0)[
		(set: _output to it + ' Empty.')
	]
	(else:)[
		(for: each _item, ..._entries)[
			(set: _output to it + '<br>' + (str: _item's value) + ' x ' + _item's name)
		]
	]
	(output:)[_output]
]))

…which results in the “work” part only being executed a single time, but that part is more complex than before because a buffer was needed to create a String representation of the output, which was then “printed” within the (output:) macro’s Hook.

Which in this simple example wasn’t an issue, but it could quickly become one if the generated output includes links for each item, and if those links called code to do something with that item like use it or transfer it to a different container.

3: Using “child” Passages and the (display:) macro instead of custom macros.

Custom macros are an excellent means for abstracting commonly used code, however they do come with some potential issues, like what has already been mentioned regarding the potential behaviour of “output” related macros.

So in some cases the simpler answer is the better one, for example if you manually created a Passage named Inventory and placed the following in it…

{
	(set: _entries to (dm-entries: $inventory))
	(if: _entries's length is 0)[
		Empty.
	]
	(else:)[
		(for: each _item, ..._entries)[
			<br>(print: _item's value) x (print: _item's name)
		]
	]
}

…then you could use code like the following to display the contents of the $inventory variable…

Inventory: (display: "Inventory")

…and there would be no need to worry about the “output” generating code being potentially executed twice. :slight_smile:

Which is the better method? That depends on what you are trying to do, what you are doing it to, and the resource expense involved in doing it.

1 Like

Thanks so much for this.

  • Clarification on the comparison operators - got it, that was a silly mistake. it’s a little strange the syntax doesn’t include ‘==’.
  • I see what you’re saying about the putting the code if a macro in the hook only so it does execute twice- I had no idea this could happen but that makes perfect sense.

About the inventory then, the second example of the commonplace code for printing the datamap’s entries– I think this is exactly what I was trying to do and I’ve used the buffer strategy in other custom macros before, but couldn’t wrap my head around it in this case; your explanation is great. I’m going to try using each of these methods to see which makes sense depending on the passage’s context and how I might edit the inventories later.

Your help is much appreciated :slight_smile:

1 Like

Update: greyelf’s solutions were extremely helpful, but I’ve run into another issue while working on the same inventory system.

Here is the successful code I have (it’s a different datamap example than the original post). Apologies if this section is unnecessarily long or uses too many steps- I haven’t figured out a simpler way to do this yet.

{(set: $craftInv to (dm: "a clay pot", 3, "a knife", 7, "a chisel", 2, "a sweater", 6, "a carved flute", 2, "a small metal clock", 4, "patched boots", 3))

<!--method using custom macro-->
(set: $printInv to (macro: dm-type _dm, [
	(set: _output to '')
	(set: _entries to (dm-entries: _dm))
	(if: _entries's length is 0)[
		(set: _output to it + ' Empty.')
	]
	(else:)[
		(for: each _item, ..._entries)[
			(set: _output to it + '<br>' + '> ' + _item's name)
		]
	]
	(output:)[_output]
]))
}
($printInv: $craftInv)

{
(set: $invAccess to (macro: dm-type _dm, str-type _intPurch, [
	(set: _textoutput to ' ')
    (set: _cost to 0)
	(set: _entries to (dm-entries: _dm))
    (for: each _item, ..._entries)[
    	(if: _item's name is _intPurch)[
			(set: _cost to _item's value)
        (output:)[_cost]
        ]
        (else-if: _item's name is not _intPurch)[
    		(set: _textoutput to 'could not find item in inventory')
        ]
    ]
    (output:)[_textoutput]
]))
}

So I’m able to print an inventory’s contents as well as access a user-given string (that’s intPurch - intended purchase is) to see if it exists in the array, and return the value associated with it if it does exist.

At least that is what I thought, however, in the section of code below, I am trying to confirm the purchase and then check whether the player has enough money in their inventory to buy it (the yes/no would be future options to do this).

I see at least one of the problems with my code is that ($invAccess: $craftInv, $intPurch) does not return value when it is inside the if statement - instead it is treated as a command, so although I can say how much something costs, I can’t evaluate its affordability. I tried this both by putting ($invAccess: $craftInv, $intPurch) directly in the if statement as well as by setting ($invAccess: $craftInv, $intPurch) equal to a separate variable and putting that in the if statement, but neither worked.
Is there a way to access solely the output of a custom macro, or every time I call it, will it just be treated as a command?

(link-rerun: "buy something")[
	(set: $intPurch to (prompt: "what do you want to buy?<br>//enter the item name exactly//", ""))
	(if: ($invAccess: $craftInv, $intPurch) is not "could not find item in inventory")[
		<br>pay ($invAccess: $craftInv, $intPurch) coins for $intPurch?<br>yes<br>no
	]
    (else:)[
    	<!-- hide the message that item was not found-->
    ]
]

Thanks very much.

1 Like

The (output:) macro’s documentation states… (emphasis mine)

Use this macro inside a (macro:)'s CodeHook to output a command that, when run, renders the attached hook.

That bolded part means the content within the (output:) macro’s associated Hook will be displayed on the page.

eg. if you called the macro like so…

(output:)[Hello world]

…then Hello world will be displayed on the page.

The (output-data:) macro’s documentation states… (emphasis mine)

Use this macro inside a (macro:)'s CodeHook to output the value that the macro produces.

The bolded part means that this macro returns a value, which is the value that was passed as an argument to macro.

eg. if you called the macro like so…

(output-data: "Hello World")

…then the String “Hello World” would be returned by the custom macro that contained that (output-data:) call.

So your $invAccess custom macro is simply using the wrong “output” macro call, it should be executing (output-data: _textoutput) instead of (output:)[_textoutput]

2 Likes

Thanks again.
I figured this out and got the code working after reading the documentation again… :upside_down_face:

1 Like