Making the IDE hang with list management...

It took me a while to chase this down.

I’m trying to filter a list for two different criteria. If by the second pass there is at least one element remaining, I want to return that element. If not, I want to use the most recent list that has one element.

I think the problem is that you can’t alter lists in runtime. (Which I have a hard time thinking around and don’t understand why this is disallowed… Anyone up for a mass protest?)

The lists are of record objects. So if you can hum along when it comes to a list element, this is how I’m trying to filter the list down:

[code] local strats4thisgaem = p1.stratList.subset({rec: rec.gaem = desig});
local strats4thisopp = strats4thisgaem.subset({rec: rec.oppon = p2.desig});

        //use most relevant non-nil case
        local strats4now = [];
        if (strats4thisopp != nil) strats4now = strats4thisopp;
        else if (strats4thisgaem != nil) strats4now = strats4thisgaem;
        else strats4now = ['randmove'];

        //randomly select strategy
        if (strats4now.length)
        local p1strat = prand.el(strats4now); //~~~
            else "no strats4now is empty";

[/code]

–strats4now is always empty.

Conrad.

You can alter lists at runtime, but it creates a new list when you do. You have to assign the result to something else - a local or a property - in order to use it. It looks like you’ve done that.

I suspect one of your first two lines, with the subset filter, is written in a way that none of the list elements match. This returns an empty list object, with length zero. Since it’s a real object, though, you can’t test by comparing it with nil. It will always be non-nil.

So your first if line always matches, and since your end result is always empty, it suggests the strats4thisopp filter is incorrect.

Ben, you may be right. But I can’t work it out in detail and the code has gotten too complicated for something that doesn’t work.

So I’ll start over. Let me ask:

I’d like to allow characters to have a number of different strategies, more than one for a particular game, and particular strategies for particular opponents. So in your view, what would be a good way to design this, to store the strategies data in the characters somewhere, and then for the gaem object to filter out the relevant records, and pick out one strategy from among them?

How do I make life simple here?

Conrad.

Somehow the original list is empty to begin with.

This:

local strats4thisgaem = p1.stratList.subset({rec: rec.gaem = desig}); //the ".gaem" field holds the name of the game it applies to, e.g. 'rps'. "<<p1.stratList[1].gaem>> -> "; "<<strats4thisgaem[1].gaem>>";

Just outputs

Which means that the original list I’m starting from is empty, despite the fact that I assign one in the character definition.

Do I somehow have to work with a pointer to player one, rather than a variable that’s assigned the character as a value?

Conrad.

The idiomatic TADS approach would be to implement a Strategy class, with subclasses for each game to encapsulate variations in rules, etc.

Then each NPC would hold various Strategy objects, using the + notation, similar to the way AgendaItems and ActorStates are added.

Game logic can then find a given NPC’s strategies by accessing its contents list, filtering for objects of the Strategy class (or relevant subclass), and then invoking some class method on each result to see if it wanted to be used in the current situation.

Probably you would pass the opponent’s object as a value to that class method, so it could use that information to decide if the strategy applied.

I can work up another demo if this is not clear.

Ben,

Thanks for the thought – but please don’t go to the trouble of making a demo at this time (especially since we’re still ironing out how such a strategy object is meant to behave.)

The trick is that there are times that I want strategies to cross games. One strategy might just be ‘nice’, for use with certain opponents (listed), for use in any game that takes the ‘nice’ strategy. So the logic is potentially far more complicated.

Conrad.

Basically, the design decision I’ve been looking to implement is that NPCs carry around the strategies and the gaems carry what they mean.

The reason is that gaems are generally going to be short little interactions that can interpret strategies like ‘nice’ or ‘mean’ or ‘selfish’ or ‘selfless.’ More specific information – getting down to the move level – isn’t generally useful, and if it is it can be set in a strategy of this kind (‘always throw rock’).

OTOH, allowing NPCs to set strategies like ‘nice’ or ‘mean’, and to apply them to particular characters, makes them more emotional and social creatures. (Most gaems will be social, if all goes well.)

So, mostly I want to set up the stratrec in the NPC as a set of filters and conditions indicating when it should be applied, to whom, and under what circumstances. --I’m thinking about creating a ‘context’ object that’ll carry the current moment: what’s going on currently? Did someone just fart, did they tell a joke, proclaim undying love, etc. For an NPC to reply to a wedding proposal with an offer to play r-p-s is not what I’m looking for.

Therefore I’m trying to figure out how to put part of the info in the NPCs stratrec and part in the gaem.

Conrad.

I find it easier to discuss with code rather than in the abstract. :slight_smile:

[rant][code]
// gaem labels
enum RPS, PRISONERS;

// strategy labels
enum NICE, MEAN, SELFISH, SELFLESS, DEFAULT;

// the core strategy class
class GaemStrat: object
// the list of gaems that this strategy applies to
gaemList =

// the strategy for a given NPC
behavior = [ * -> DEFAULT ]

// the relative priority of this strategy
priority = 100

// decide whether we match a particular game
match(gaem) {
	if (gaemList.indexOf(gaem))
		return true;
	else
		return nil;
}

// decide how we act toward a given NPC
select(npc) {
	return behavior[npc];
}

;

// gaem-specific strategy for rock-paper-scissors
class RPSStrat: GaemStrat
gaemList = [ RPS ]
;

// and another one for the Prisoner’s Dilemma
class PrisonersStrat: GaemStrat
gaemList = [ PRISONERS ]
;

modify Actor
// actors hold a list of strategies
stratList =

// given a gaem and an opponent, find a suitable strategy
strategize(gaem, opponent) {
	local strats = stratList.subset({strat:
		// find strategies for this game
		strat.match(gaem) != nil
	}).sort(nil, {a, b:
		// and sort by priority
		a.priority - b.priority
	}); 
	return (strats.length ? strats[1].select(opponent) : DEFAULT);
}

;

// during init, add each held strategy to the actor’s list
PreinitObject
execute() {
forEachInstance(GaemStrat, {obj:
obj.location.stratList += obj
});
}
;

guard: Person ‘guard’ ‘guard’ @lab
"Just another guard. "
rpsFrequency = 5
;

  • RPSAgenda;
  • RPSStrat
    behavior = [ * → MEAN ]
    ;

janitor: Person ‘janitor’ ‘janitor’ @lab
"Just another janitor. "
;

  • RPSAgenda;
  • RPSStrat
    behavior = [
    guard → NICE,
    * → DEFAULT
    ]
    ;

// sample code for demo purposes only
modify WaitAction
execAction() {
local npc1 = guard;
local npc2 = janitor;

	local npc1strat = npc1.strategize(RPS, npc2);
	local npc2strat = npc2.strategize(RPS, npc1);

	"The <<npc1.name>> would select the <<enumText(npc1strat)>> strategy
	 for <<enumText(RPS)>> with <<npc2.name>>. ";

	"The <<npc2.name>> would select the <<enumText(npc2strat)>> strategy 
	 for <<enumText(RPS)>> with <<npc1.name>>. ";
}

;

// this function is really only useful for debugging output
enumText(e) {
switch (e) {
case ROCK: return ‘rock’;
case PAPER: return ‘paper’;
case SCISSORS: return ‘scissors’;
case RPS: return ‘RPS’;
case PRISONERS: return ‘PRISONERS’;
case MEAN: return ‘mean’;
case NICE: return ‘nice’;
case SELFISH: return ‘selfish’;
case SELFLESS: return ‘selfless’;
case DEFAULT: return ‘default’;
default: return ‘’;
}
}
[/code]

[/rant]

This implements individual strategies based on the opponent for each actor; they will choose something like mean, nice, or default. It’s up to the actual game logic (not shown) to decide what that strategy signifies.

You will probably want to nix the RPS offer before you reach the strategy selection stage. I’d suggest tweaking the isReady method for the AgendaItem responsible for initiating games, so that it returns false unless the moment is right.

Very good suggestion with intercepting “social context” at the agendaitem level, and I’ll look over your code.

Many thanks, Ben.

Conrad.

Ok… I’m back with some questions.

[code]modify Actor
// actors hold a list of strategies
stratList = []

// given a gaem and an opponent, find a suitable strategy
strategize(gaem, opponent) {
local strats = stratList.subset({strat:
// find strategies for this game
strat.match(gaem) != nil
}).sort(nil, {a, b:
// and sort by priority
a.priority - b.priority
});
return (strats.length ? strats[1].select(opponent) : DEFAULT);
}[/code]

The syntax here is just too complicated for me. I can follow your comments, but I’m not unraveling what’s happening in the line }).sort(nil, {a, b: --could you explain how that works? (Whatever that’s called, where you put curly braces inside the parameters for a call to a method, I’m not real fluent with.)

Conrad.

The first statement in the strategize method breaks down like this:

local strats = stratList.subset(...).sort(...); 

Where if we ignore the parameters, it’s somewhat clear what is happening: we’re taking part of the list and then sorting it somehow.

First we call the subset method of the stratList object, which returns a new List, consisting of only those list elements that pass the filter function. We then immediately call the sort method of that new, anonymous List, which arranges the list elements based on the sorting function, and returns a new List. We then assign that List to the strats local.

(We could reverse the order of the subset and sort calls without changing the result.)

Now let’s tackle the arguments to these List method calls. subset takes a function as an argument, then calls that function for each element in the list and checks the return value. If it’s anything other than nil or zero, subset adds that element to the new List it’s building. Otherwise it moves on to the next element.

The function argument can be specified in a number of equivalent ways. The most verbose is to write a regular function somewhere else in the code, then pass that as the argument.

// this is a regular function, outside of an object declaration
myFilter(strat) {
    return strat.match(gaem); // UH OH
}

modify Actor
    // this is our method
    strategize(gaem, opponent) {
        local strats = stratList.subset(myFilter);
    }
;

The problem with this is that pesky gaem variable in our myFilter function. It needs to hold the value of the gaem argument that was passed to strategize, but there’s no good way of getting it, short of assigning it to some global variable.

TADS 3 solves this difficulty by allowing functions declared inside a method to access other variables from that method. So let’s move our function inside.

modify Actor
    // this is our method
    strategize(gaem, opponent) {
        local myFilter = new function(strat) {
            return strat.match(gaem);  // YAY
        };
        local strats = stratList.subset(myFilter);
    }
;

It’s not quite the same, though. When you declare a function inside a method, it’s always anonymous - meaning it doesn’t get a name that you can use to call it from anywhere in your code. So to make it equivalent, we declare a myFilter local and assign our unnamed function to that. Then we can refer to the function as myFilter, and we’re basically back where we started, except the code will compile now.

Because this is a common scenario, TADS 3 has a shorthand way of declaring an anonymous function which lets you omit the fluff like “new function” and “return” and semicolons. The above code could also be written like this:

modify Actor
    // this is our method
    strategize(gaem, opponent) {
        local myFilter = {strat: strat.match(gaem)};
        local strats = stratList.subset(myFilter);
    }
;

This is equivalent to the earlier form; we’ve just stripped away some of the boilerplate. The entire function is enclosed in curly braces. The colon marks the boundary between the function’s parameters and its actual code. Here we’ve said that there’s one parameter, which we’ll call strat.

The code is the same, except there’s no semicolon and no return statement. Short-form anonymous functions will implicitly return the value of the last statement.

So we’re looking pretty good at this point; you may even prefer this style for clarity. But there’s one trick left to us: we don’t actually need to assign the anonymous function at all. We can just make it up on the spot when we pass it as a parameter, and let it fall by the wayside when it’s no longer needed.

modify Actor
    // this is our method
    strategize(gaem, opponent) {
        local strats = stratList.subset({strat: strat.match(gaem)});
    }
;

That’s where the curly braces inside the parameters of a function call come from.