Xeotroid
Xeotroid

Reputation: 13

How can I store a code block in a variable and call it and get its return value whenever needed?

I'm making a little text adventure in Smalltalk. It's made up of "screens" that have their texts and choices for other screens included. Since I want the game to be dynamic, I also want to include branching. For instance, if the player is at a blacksmith and wants to buy an axe, the screen the player goes to immediately checks if the player has enough money and jumps to one of two other screens based on that.

I already have this working: The screens (classes named Place) have a list where the first item is the function and the following items are the arguments. However, I have it done in a very ugly way: the first item is a string that is then compared against in a big "action" method, so it looks something like this:
game data method:

blacksmith := Place new.
blacksmith choiceText: 'I would like an axe.';
blacksmith action add: 'money'; add: 20; add: blacksmith_good; add: blacksmith_bad.

action method: (currentScreen is also a Place; the class also contains a BranchMoney method that does the actual decision making)

(currentScreen action at: 1) = 'money'
ifTrue: [
    currentScreen := (currentScreen BranchMoney)
]

That's obviously not ideal, and I would like to compact it by doing something like this:
game data method:

blacksmith action add: [blacksmith BranchMoney]; add: 20; add: blacksmith_good; add: blacksmith_bad.

action method:

currentScreen := (currentScreen action at: 1)

So that instead of string checking the game would just directly proceed with the method I want it to do.

However, it doesn't seem to work - I've tried different changes to the code, and the problem seems to be that the currentScreen := (currentScreen action at: 1) line just replaces the contents of currentScreen with the code block contents – it doesn't calculate the block's contents and use its resulting value that is of type Place.

I've tried using round brackets in the game data method – that throws a list out of bounds exception, because it tries to calculate the expression immediately, before other arguments have even been added. Changing the first item name in game data method to currentScreen BranchMoney doesn't seem to make a difference.
I've also tried adding a return in the game data method, like this: blacksmith action add: [^blacksmith BranchMoney], so that it would have a value to return, no luck. Doing something like currentScreen := [^currentScreen action at: 1] in the action method doesn't work either.
For some shots in the dark, I tried the ExternalProcedure call and call: methods, but that failed too.

Upvotes: 1

Views: 452

Answers (1)

Leandro Caniglia
Leandro Caniglia

Reputation: 14868

In Smalltalk every block is a regular object that you can store and retrieve the same you would do with any other object:

b := [self doSomething]

stores in b the block (much as b := 'Hello' stores a string in b). What I think you are missing is the #value message. To execute the block do the following

b value   "execute self doSomething and answer with the result"

In case your block has one argument use #value: instead

b := [:arg | self doSomethingWith: arg]

later on

b value: 17   "execute the block passing 17 as the argument"

(for two arguments use #value:value:, for three #value:value:value: and for many #valueWithArguments:.)

Note however that this approach of using blocks and Arrays of arguments doesn't look very elegant (or even convenient). However, to help you with some better alternative we would need to learn more about your game. So, go check whether #value (and friends) let you progress a little bit and feel free to come back here with your next question. After some few iterations we could guide you towards a clearer route.


Example

b := [:m | m < 20 ifTrue: ['bad'] ifFalse: ['good']].

will produce

b value: 15 "==> 'bad'"
b value: 25 "==> 'good'"

Upvotes: 3

Related Questions