NPC-NPC interaction?

I know this is a weird question and a big topic… So I figured I’d get started now, and see what kind of progress I can make.

I’m looking to make livelier NPCs that interact with each other in various ways. I’m aware text games usually are PC-centric, and this is not acknowledged game design – but I think it’d be cool. I was working on this in a preliminary way a year ago.

My current thinking is this–

Let’s say the model problem is for the NPCs to play rock-paper-scissors on a whim. We’ll define “a whim” as being when either one wants to, which is a randomly occurring event that will fire, say, 1 turn in 3 when nothing more important to either NPC is going on. (1 in 3 so we only have to wait a turn or two for either one to make a move.)

So, we’ll make a “Let’s Play!” AgendaItem that fires off the behavior.

The behavior code will be in a block of code in the NPCs themselves. This will contain (a) criteria defining whether the behavior is available to the NPC, (b) some kind of logic defining the behavior itself – decision code, reporting, and so forth – and © logic and reporting for handling the outcome of the game.

That code can be run from within either NPCs AgendaItem.

So, let’s consider Garry and Ned.

Garry’s whim AgendaItem activates this turn.

That AgendaItem knows Garry could play r-p-s, or it wouldn’t have fired. Now it consults Ned’s r-p-s criteria (a) and perhaps determines Ned qualifies for a round of r-p-s. This is from Garry’s and Ned’s individual r-p-s (a) blocks.

Now the AgendaItem runs Garry’s r-p-s (b) block and Ned’s r-p-s (b) block. Each makes their move.

Finally, the AgendaItem runs Garry’s r-p-s © block and Ned’s r-p-s © block. Each reports their response to having won, lost, or tied.

Probably, the r-p-s criteria should be nestled inside the NPC’s current state. That way, if the NPC ends up in a specialized state, you don’t have to worry about another NPC coming up to him and playing r-p-s incongruously.

Anyone have thoughts on this?

Conrad.

Rather than attempting to do it all in one NPC or the other’s AgendaItem routine, I would fold it into the normal action handling.

E.g. Garry’s AgendaItem fires, he’s ready to play. He looks for other NPCs in the room. For each NPC, he tries a PlayRPS action. If it fails (somewhere in that NPC’s dobjFor(PlayRPS) verify or check routines) he tries the next NPC. If it succeeds, he lets the other NPC handle the output and he stops trying. If none of the NPCs want to play he gives up quietly.

You could run the action with newActorAction. It will return a CommandTranscript object, and you can check that object’s isFailure property to see if it failed.

The conceptual benefit is that the player could also play rock-paper-scissors with any NPC and get similar output. You also avoid defining parallel structures for verifying, checking, carrying out, and reporting actions.

I think I commented on this last year, but I’m not sure. My take on it is probably not what you’re looking for, but as an author, I don’t think it matters what the NPCs are actually doing behind the scenes. All that matters is what’s reported to the player. So if one NPC’s AgendaItem decides it’s time to play RPC (because there’s another Person object in the location who is able to play), the AgendaItem itself can produce the entire response that the player sees, and adjust any other values that need to be adjusted, such as the win/loss records of various characters.

The only thing that has to be done, in this case, is that you don’t want two NPCs to all produce a round-of-RPC output in the same turn. So each Person object needs its own readyToPlay flag, which is set to true at the beginning of each turn (by some suitable daemon that can be guaranteed to run at the beginning of the turn). This flag is set to nil by the AgendaItem, both for the NPC that initiated the round of play and for the NPC that it is playing with.

The daemon that resets readyToPlay would of course call a getReady method in the Person object. That method would determine whether a given NPC was cooperative or not. If asleep==true, for instance, the NPC’s getReady method would simply not set readyToPlay to true.

That’s how I’d do it.

Jim,

You did give me a version of this same advice – that is, just manipulate the transcript. And it was very useful, too, in getting me to refocus and to simplify my thinking.

There are two reasons I’m not doing it that way, though.

The first is that, as Ben anticipates, I’m looking for a general tool that will allow me to build NPCs that will respond to each other the same way they’ll respond to the PC.

The second is – well, this is basically an outcome of looking into how broadly we can allow realtime player switching of viewpoint. Right? I mean, if the player switches out of a character, and it suddenly stands there like a zombie, or a robot with the battery pack removed – not so good.

In effect, I’m looking to build a kind of model train set that the player can move around inside of. I know this isn’t the usual thing to do with IF, but it’s something I’ve been interested in for some time.

How do I do that?

I’m thinking currently of using something like a RoomActorGrouper, a SocialSituation object, that would mediate the communication among characters that are communicating with one another. The RoomActorGrouper nature would handle the outside appearance, whereas the SocialSituation nature would handle the message-passing between characters.

But frankly, I don’t have the faintest clue how to get the NPC to look outside itself, or to do anything apart from test whether a particular object currently exists in its space.

Yeah, that’s what I’m looking for. Probably game code would be modified into the Actor object, and strategy data somehow default-overrided on a case by case basis.

All thoughts welcomed.

Conrad.

forEachInstance?

C.

forEachInstance would work but I believe it traverses the entire object tree. Doing this every turn with every NPC in the game might cause a noticeable performance hit. It’s the right idea and sometimes you have to do it that way, but it’s better if you can restrict the scope of the search.

+ AgendaItem
	// ...
	invokeItem() {
		local npcs = getActor.getOutermostRoom.contents.subset({obj:
			obj.ofKind(Actor) && obj != getActor
		});
		npcs.forEach({npc: "<<npc.name>> is here with me! " });
		// ...
	}
;

It’s going to look a bit funny if you’re not used to lists and anonymous functions, but the basic idea is that the actor running the AgendaItem looks through the contents of his room to find actors (excluding himself) then does something with each actor he finds.

You can filter out the player character by changing the subset function a bit:

local npcs = getActor.getOutermostRoom.contents.subset({obj:
	obj.ofKind(Actor) && obj != getActor && obj != gPlayerChar
});

Heh. Looks like heiroglyphics.

Thanks for the code, Ben.

Conrad.

– Is it possible to do something like this, not with location, but with SenseConnection?

Yes, by swapping sensePresenceList for getOutermostRoom.contents.

local npcs = getActor.sensePresenceList(sight).subset({obj:
	obj.ofKind(Actor) && obj != getActor && obj != gPlayerChar
});

Ben,

I’m moving a bit slowly on this, as I often do with things that befuddle.

I notice that you suggest having the second NPC – the “recipient” of the offer to play RPS – handle the output.

What’s your thinking there? Is it to ensure that the other NPC is actually doing what he’s supposed to be? To make it easier to catch bugs? Is it just simpler to pass the ball one way and not the other?

I’m not question that the strategy is a good one so much as wondering what its purpose is.

Many thanks,

Conrad.

Currently it’s giving me that “function pointer required” jazz.

//////// the RPS verb does not yet exist...  figure it's not needed yet

DefineTAction(PlayRPS)
;


modify Actor
    dobjFor(PlayRPS)
    {
        check () {
            failCheck ('No one plays r-p-s any more! ');
        }
    }
;

And then after the NPC…

+ AgendaItem initiallyActive = true isReady = true invokeItem{ local npcs = getActor.getOutermostRoom.contents.subset ({obj: obj.ofKind(Actor) && obj !=getActor}); --> npcs.forEach({npc: npc(PlayRPSAction) //arrow indicating offending code points here }); } ;

Somehow I’m calling the npc.PlayRPS action wrong.

C.

Mostly that for actions, it’s usually the direct object that decides whether and how to respond. If the player tries to take a ball, the ball handles the output. You can divide up the code in other ways, of course, but it helps to be consistent.

I will try to post a basic demo later today.

So here’s what I’ve got.

[rant][code]
me: Person ‘me’ ‘me’
"It’s me! "
location = lab
;

lab: Room ‘lab’
"Welcome to the TADS Laboratory! "
;

DefineTAction(RPS);

VerbRule(RPS)
‘play’ ‘rps’ ‘with’ singleDobj
: RPSAction
verbPhrase = ‘play/playing rps (with whom)’
;

modify Thing
dobjFor(RPS) {
verify() { illogical('But {the dobj/he} doesn’t have hands! '); }
}
;

enum ROCK, PAPER, SCISSORS;
enum WIN, LOSE, DRAW;

enumText(e) {
switch (e) {
case ROCK: return ‘rock’;
case PAPER: return ‘paper’;
case SCISSORS: return ‘scissors’;
default: return ‘’;
}
}

modify Person
dobjFor(RPS) {
verify() {}

	check() {
		if (!wantsToPlay) {
			local msg = '{You/he} want{s} to play RPS with
				{the dobj/him}, but ';
			switch (lastRPSresult) {
				case WIN:
					msg += '{the dobj/he} is still gloating over
						{its/hers} win. ';
					break;
				case LOSE:
					msg += '{the dobj/he} is still smarting over
						{its/hers} loss. ';
					break;
				case DRAW:
					msg += '{the dobj/he} is still thinking over
						{its/hers} tactics. ';
					break;
				default:
					msg += '{the dobj/he} is busy. ';
					break;
			}
			failCheck(msg);
		}
	}

	action() {
		local player1 = gActor;
		local player2 = self;
		local throw1 = rand(ROCK, PAPER, SCISSORS);
		local throw2 = rand(ROCK, PAPER, SCISSORS);

		gAction.setMessageParam('player1', player1);
		gAction.setMessageParam('player2', player2);
		gAction.setMessageParam('throw1', enumText(throw1));
		gAction.setMessageParam('throw2', enumText(throw2));

		if (player1 != gPlayerChar)
			"{The player1/he} and {the player2/he} decide to play a
			 quick game of RPS. ";

		"{The player1/he} throw{s} {throw1}! ";
		"{The player2/he} throw{s} {throw2}! ";

		local winner = nil;
		if (throw1 == ROCK) {
			if (throw2 == SCISSORS)
				winner = player1;
			else if (throw2 == PAPER)
				winner = player2;
		}
		else if (throw1 == PAPER) {
			if (throw2 == ROCK)
				winner = player1;
			else if (throw2 == SCISSORS)
				winner = player2;
		}
		else /* throw1 == SCISSORS */ {
			if (throw2 == PAPER)
				winner = player1;
			else if (throw2 == ROCK)
				winner = player2;
		}

		if (!winner) {
			"A draw! It's a draw! A beautiful draw! ";
			player1.lastRPSresult = DRAW;
			player2.lastRPSresult = DRAW;
		}
		else {
			gAction.setMessageParam('winner', winner);
			"{The winner/he} win{s}! ";
			player1.lastRPSresult = (winner == player1) ? WIN : LOSE;
			player2.lastRPSresult = (winner == player2) ? WIN : LOSE;
		}

		player1.lastRPS = libGlobal.totalTurns;
		player2.lastRPS = libGlobal.totalTurns;
	}
}

wantsToPlay() {
	return (rpsFrequency && (lastRPS == nil ||
			(lastRPS <= libGlobal.totalTurns - rpsFrequency)));
}

lastRPS = nil
lastRPSresult = nil

// how often the person is willing to play RPS
rpsFrequency = 3

isHim = true

;

class RPSAgenda: AgendaItem
initiallyActive = true

isReady() {
	return getActor.wantsToPlay && rand(3) == 2;
}

invokeItem() {
	local npcs = getActor.sensePresenceList(sight).subset({obj:
		obj.ofKind(Actor) && obj != getActor && obj != gPlayerChar
	});
	if (npcs.length)
		newActorAction(getActor, RPS, rand(npcs));
}

;

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

  • RPSAgenda;

scientist: Person ‘scientist’ ‘scientist’ @lab
"Just another scientist. "
rpsFrequency = 0
;

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

  • RPSAgenda;
    [/code][/rant]

I decided I liked it better if the NPC picked another NPC at random to play with, instead of trying to play with every NPC in the room. It’s a better fit with the default “one action per actor per turn” model.

Thanks Ben! – I was just posting to follow up on your earlier post, that I’m willing to struggle through with just pointers when I get stuck. But I greatly appreciate the solution, which I’ll now try to figure out.

Conrad.

Thanks to all you guys, and in particular especially Ben, I’ve generalized the rock-paper-scissors code such that you only define some data for an NPC-NPC game object. This is for a one-turn game.

Later, I’ll be looking to expand this structure to apply to other kinds of (N)PC-NPC interaction, too… such as trying to sneak behind a guard, or knocking him out, or bribing him to let you pass, or so on.

This is probably more interesting and useful than going on to multi-turn games, like Tic-Tac-Toe, which are pretty specific, require geometric modeling of a game board, or physical modeling of a deck of cards, or so forth, and the logic of which can’t be extended to narrative (that I can see).

Feedback?

This is how you use this code:

[code]rpsGame : GameThing

gameName = 'Rock-Paper-Scissors'
moveNameV = 'throw{s}'
legalmovesList = ['rock', 'scissors', 'paper']
nolegalmoves = 3

p1winList = ['rock-v-scissors', 'scissors-v-paper', 'paper-v-rock']
p2winList = ['scissors-v-rock', 'paper-v-scissors', 'rock-v-paper']
tieList = ['rock-v-rock', 'paper-v-paper', 'scissors-v-scissors']

;
[/code]

[rant]The full code:

[code]////////////////////////////////////////////////////////////////////////////////

GameThing : object

gameName = ''
moveNameV = 'move{s}'
p1 = nil
p2 = nil
p1move = ''
p2move = ''
p1result = ''
p2result = ''
outcome = ''

legalmovesList = []

p1winList = []
p2winList = []
tieList = []


p1randstrat() {
    p1move = rand(rpsGame.legalmovesList);
    p2move = rand(rpsGame.legalmovesList);
}
p2randstrat() {
    p1move = rand(rpsGame.legalmovesList);
    p2move = rand(rpsGame.legalmovesList);
}
p0randstrat() {
    p1move = rand(rpsGame.legalmovesList);
    p2move = rand(rpsGame.legalmovesList);
}


reportbegin() {
    gAction.setMessageParam('p1', p1);
    gAction.setMessageParam('p2', p2);
    if (p1 != gPlayerChar)
        "<<'{The p1/he} and {the p2/he} decide to play a
        quick game of ' + gameName +'. '>>";
}  

reportmoves() {
     "{The p1/he} <<moveNameV>> <<p1move>>! ";
     "{The p2/he} <<moveNameV>> <<p2move>>! ";
}


movekey () {
    return (p1move + '-v-' + p2move);
}   

resolveresult() {
    local bothmoves = movekey();
    p1.lastRPS = libGlobal.totalTurns;
    p2.lastRPS = libGlobal.totalTurns;
    if ( p1winList.indexOf(bothmoves) ) {
        p1.lastRPSresult = 'win';
        p2.lastRPSresult = 'lose';
        return ('{The p1/he} wins! ');
    };
    if ( p2winList.indexOf(bothmoves) ) {
        p1.lastRPSresult = 'lose';
        p2.lastRPSresult = 'win';
        return ('{The p2/he} wins! '); 
    };
    if ( tieList.indexOf(bothmoves) ) {
        p1.lastRPSresult = 'draw';
        p2.lastRPSresult = 'draw';
        return ('It\'s a tie -- thanks! ');
    };
    return ('They totally fumble this game, and seem inclined 
        to pretend it never happened. ');
}

mainLoop() {
    reportbegin();
    p0randstrat();
    reportmoves();
    "<<resolveresult()>>";
}

;

//////////////////////////////////////////////////////////////////////

rpsGame : GameThing

gameName = 'Rock-Paper-Scissors'
moveNameV = 'throw{s}'
legalmovesList = ['rock', 'scissors', 'paper']
nolegalmoves = 3

p1winList = ['rock-v-scissors', 'scissors-v-paper', 'paper-v-rock']
p2winList = ['scissors-v-rock', 'paper-v-scissors', 'rock-v-paper']
tieList = ['rock-v-rock', 'paper-v-paper', 'scissors-v-scissors']

;

//////////////////////////////////////////////////////////////////////

DefineTAction(RPS);

VerbRule(RPS)
‘play’ ‘rps’ ‘with’ singleDobj
: RPSAction
verbPhrase = ‘play/playing rps (with whom)’
;

modify Thing
dobjFor(RPS) {
verify() { illogical('But {the dobj/he} doesn’t have hands! '); }
}
;

modify Person
dobjFor(RPS) {
verify() {}

  check() {
     if (!wantsToPlay) {
        local msg = '{You/he} want{s} to play RPS with
           {the dobj/him}, but ';
        switch (lastRPSresult) {
           case 'win':
              msg += '{the dobj/he} is still gloating over
                 {its/hers} win. ';
              break;
           case 'lose':
              msg += '{the dobj/he} is still smarting over
                 {its/hers} loss. ';
              break;
           case 'draw':
              msg += '{the dobj/he} is still thinking over
                 {its/hers} tactics. ';
              break;
           default:
              msg += '{the dobj/he} is busy. ';
              break;
        }
        failCheck(msg);
     }
  }

  action() {
        rpsGame.p1 = gActor;
        rpsGame.p2 = self;
        rpsGame.mainLoop;
  }

}

wantsToPlay() {
return (rpsFrequency && (lastRPS == nil ||
(lastRPS <= libGlobal.totalTurns - rpsFrequency)));
}

lastRPS = nil
lastRPSresult = nil

// how often the person is willing to play RPS
rpsFrequency = 3

isHim = true
;

class RPSAgenda: AgendaItem
initiallyActive = true

isReady() {
return getActor.wantsToPlay && rand(3) == 2;
}

invokeItem() {
local npcs = getActor.sensePresenceList(sight).subset({obj:
obj.ofKind(Actor) && obj != getActor && obj != gPlayerChar
});
if (npcs.length)
newActorAction(getActor, RPS, rand(npcs));
}
;

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

  • RPSAgenda;

scientist: Person ‘scientist’ ‘scientist’ @lab
"Just another scientist. "
rpsFrequency = 0
;

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

  • RPSAgenda;
    [/code][/rant]
    You’ll see this is largely Ben’s code, encapsulated in a game object.

I guess the next step is to encapsulate RPS as an object, so we have a general PLAY object and a means of handling whether we want to play.

Conrad.

Good progress since Wednesday! – will post something later today or tomorrow.

I’ve got them playing Prisoner’s Dilemma, too, and the code for it is general. Tricky because PD’s not symmetrical.

Conrad.

Very cool! I’m looking forward to seeing the code.

I’ll tidy up the code and finish up what I’m doing before posting all the source.

I use an object to tie together the win (or lose) conditions with the data handling how it should respond to that outcome. Then, I tie all such records of possible results together in another list, which is held by the r-p-s object.

The r-p-s object iterates through its list of results, and for each result object iterates through the result conditions, testing against actual conditions. And here’s the trick – when it finds one, it doesn’t terminate. This makes it possible for player 1 and player 2 to be considered separately, and thus it doesn’t matter if the game is symmetrical (kinda= zero-sum), or not.

So in the new system, here’s a result object:

p1winRPS : Result name = 'p1win' p1 = true res = 1 report = '{The p1/he} wins! ' conditions = [ 'rock-v-scissors' , 'scissors-v-paper' , 'paper-v-rock' ] ;

And here’s the RPS game object (now spelled “gaem” to avoid visual confusion with references to the work of IF) that handles such objects:

rpsGaem : GaemThing

    gaemName = 'Rock-Paper-Scissors'
    moveNameV = 'throw{s}'
    legalmovesList = ['rock', 'scissors', 'paper']
    nolegalmoves = 3  // <-- as in, "Number of legal moves"

    possibleResults =
    [
        p1winRPS ,
        p2winRPS ,
        p1loseRPS ,
        p2loseRPS ,
        p1drawRPS ,
        p2drawRPS
    ]
;

Not a big deal, but kind of nifty.

C.

PS - The gaem object has a default strategy method that selects a random move from the legal moves list. In play, these moves are smashed together in a string, with ‘p1move-v-p2move’, which are then compared against the Result criteria. Working with strings like this was just nicer than objects and fields.

I’ve got to get the player playing, and then set up NPCs so they can prefer strategies.

The outcome of all of this will be NPCs that can flexibly interact with each other, or with the player, in one-turn interactions that can be flexibly described in strings. In other words, you could apply this to NPCs talking about each other’s cooking, to knocking out the guard or trying to sneak past, or so forth. As long as it’s one turn.

Here’s the full code, so far. It plays Prisoner’s Dilemma like it’s RPS… a little odd. The game selection is hard-coded in. Making the game selectable is the next step.

What works well in this code is that the PC can ask any NPC to play, or be asked, and accept, or be turned down, and move processing and gaem logic needn’t be defined any further than the winning/losing -criteria definitions shown in the last post.

OTOH, NPCs pick moves strictly at random. Setting up NPC strategies is to do after making the gaem selectable, and not hard-coded.

[rant][code]#charset “us-ascii”
#include <adv3.h>
#include <en_us.h>
#include <file.h>

////////////////////////////////////////////////////////////////////////////////
prand: object { //“Gerhard’s generator”, an extremely simple algorithm that generates low-quality pseudorandom numbers.

seed = 10; // Use whatever value you want as initial seed.

ok(lim)  // 0..(lim-1)
{
    seed = (seed * 32719 + 3) % 32749;

// "\nok \nseed = <>. lim = <> \n seed % lim = <<seed % lim>>. \n (seed % lim) + 1 = <<(seed % lim )>> ";
return seed % lim;
}

n0(lim)  // 1..lim
{
    seed = (seed * 32719 + 3) % 32749;

// "\nn0 \nseed = <>. lim = <> \n seed % lim = <<seed % lim>>. \n (seed % lim) + 1 = <<(seed % lim ) + 1>> ";
return (seed % lim ) + 1;
}
}
;

////////////////////////////////////////////////////////////////////////////////

//////// THE GENERAL GAEM OBJECT ///////////////////////////////////////////////

GaemThing : object

gaemName = ''
moveNameV = 'move{s}'
p1 = nil
p2 = nil
p1move = ''
p2move = ''
p1result = ''
p2result = ''
outcome = ''
nolegalmoves = 0

legalmovesList = []

p1winList = []
p2winList = []
p1loseList = []
p2loseList = []
tieList = []

// ex.
// legalmovesList = [‘rock’, ‘scissors’, ‘paper’]
// nolegalmoves = 3

randstrat() {
    return (legalmovesList[prand.n0(nolegalmoves)] );
}

reportbegin() {
    gAction.setMessageParam('p1', p1);
    gAction.setMessageParam('p2', p2);
    if (p1 != gPlayerChar)
        "<<'{The p1/he} and {the p2/he} decide to play a
        quick game of ' + gaemName +'. \n'>>";
}  

reportmoves() {
     "{The p1/he} <<moveNameV>> <<p1move>>! ";
     "{The p2/he} <<moveNameV>> <<p2move>>! ";
}

movekey () {
    return (p1move + '-v-' + p2move);
}   

resolveresult () {
    local bothmoves = movekey();
    outcome = '';
    foreach (local thisPosRes in possibleResults) {
        if ( thisPosRes.conditions.indexOf(bothmoves) )
        {
            //"\n<<bothmoves>> so therefore 
            //"<<thisPosRes.report>>";  // <--debugging
            outcome += thisPosRes.report;
            if (thisPosRes.p1) p1.lastGAEMresult = thisPosRes.res;
            if (thisPosRes.p2) p2.lastGAEMresult = thisPosRes.res;
            p1.lastGAEM = libGlobal.totalTurns;
            p2.lastGAEM = libGlobal.totalTurns;
        }
    }
    return (outcome);
}

getstrats () {
    p1move = '';
    p2move = '';
    if (p1 != gPlayerChar)
        p1move = randstrat();
    else do 
    {
        "You can enter... <<reportMovesLaundry()>>What is your choice? ";
        p1move = toString(inputManager.getInputLine (nil, nil) );
        //"You entered << '"' + p1move + '"' >>! \n";
    }   
    while (legalmovesList.indexOf(p1move) == nil);
    if (p2 != gPlayerChar)
        p2move = randstrat();
    else do 
    {
        "You can enter... <<reportMovesLaundry()>>What is your choice? ";
        p2move = toString(inputManager.getInputLine (nil, nil) );
        //"You entered << '"' + p1move + '"' >>! \n";
    }   
    while (legalmovesList.indexOf(p2move) == nil);
}

reportMovesLaundry ()
{
    local accrete = '\n';
    legalmovesList.forEach ({x: accrete += ('          ' + x+ '\n') });
    return accrete;
}


mainLoop() {
    reportbegin();
    getstrats();
    reportmoves();
    "<<resolveresult()>>";
}

fin () {
    p1 = nil;
    p2 = nil;
    gaemName = nil;
}

;

//////// THE GENERAL RESULT OBJECT ///////////////////////////////////

Strat : object
gaem = ‘’
name = ‘’
opponent = ‘’
data = ‘’
;

Result : object
name = ‘’
report = ‘’
condits = [’’, ‘’]
// because = [’’, ‘’]
// sotherefore () {
// "modify and update players here; do not run inherited. ";
// }
p1 = nil
p2 = nil
res = 0
;

//////////////////////////////////////////////////////////////////////
//////// ROCK - PAPER - SCISSORS /////////////////////////////////////
//////////////////////////////////////////////////////////////////////

frpsGaem : GaemThing
//set up possible moves somehow…

gaemName = 'Rock-Paper-Scissors'
moveNameV = 'throw{s}'
legalmovesList = ['rock', 'scissors', 'paper']
nolegalmoves = 3

possibleResults =
[
    p1winRPS ,
    p2winRPS ,
    p1loseRPS ,
    p2loseRPS ,
    p1drawRPS ,
    p2drawRPS
]

;

//////// ROCK - PAPER - SCISSORS RESULTS /////////////////////////////

p1winRPS : Result
name = ‘p1win’
p1 = true
res = 1
report = '{The p1/he} win{s}! ’
conditions = [
‘rock-v-scissors’ ,
‘scissors-v-paper’ ,
‘paper-v-rock’
]
;

p1loseRPS : Result
name = ‘p1lose’
p1 = true
res = -1
report = '{The p1/he} lose{s}! ’
conditions = [
‘scissors-v-rock’ ,
‘paper-v-scissors’ ,
‘rock-v-paper’
]
;

p2winRPS : Result
name = ‘p2win’
p2 = true
res = 1
report = '{The p2/he} win{s}! ’
conditions = [
‘scissors-v-rock’ ,
‘paper-v-scissors’ ,
‘rock-v-paper’
]
;

p2loseRPS : Result
name = ‘p2lose’
p2 = true
res = -1
report = '{The p2/he} lose{s}! ’
conditions = [
‘rock-v-scissors’ ,
‘scissors-v-paper’ ,
‘paper-v-rock’
]
;

p1drawRPS : Result
name = ‘p1draw’
p1 = true
res = 0
report = '{The p1/he} ties! ’
conditions = [
‘rock-v-rock’ ,
‘paper-v-paper’ ,
‘scissors-v-scissors’
]
;

p2drawRPS : Result
name = ‘p2draw’
p2 = true
res = 0
report = '{The p2/he} ties! ’
conditions = [
‘rock-v-rock’,
‘paper-v-paper’,
‘scissors-v-scissors’
]
;

//////////////////////////////////////////////////////////////////////
/////// THE PRISONER’S DILEMMA ///////////////////////////////////////
//////////////////////////////////////////////////////////////////////

rpsGaem : GaemThing
//pdGaem : GaemThing
//set up possible moves somehow…

gaemName = 'Prisoner\'s Dilemma'
moveNameV = 'choose{s} to'
legalmovesList = ['stay loyal', 'defect']
nolegalmoves = 2

possibleResults =
[
    p1winPD ,
    p2winPD ,
    p1losePD ,
    p2losePD ,
    p1loseBadlyPD ,
    p2loseBadlyPD 
]

;

/////// THE PRISONER’S DILEMMA RESULTS /////////////////////////////////////

p1winPD : Result
name = ‘p1win’
p1 = true
res = 1
report = '{The p1/he} win{s}! ’
conditions = [
‘stay loyal-v-stay loyal’
]
;

p1losePD : Result
name = ‘p1lose’
p1 = true
res = -1
report = '{The p1/he} lose{s}! ’
conditions = [
‘stay loyal-v-defect’ ,
‘defect-v-stay loyal’
]
;

p1loseBadlyPD : Result
name = ‘p1losebadly’
p1 = true
res = -10
report = '{The p1/he} lose{s} badly! ’
conditions = [
‘defect-v-defect’
]
;

p2winPD : Result
name = ‘p2win’
p2 = true
res = 1
report = '{The p2/he} win{s}! ’
conditions = [
‘stay loyal-v-stay loyal’
]
;

p2losePD : Result
name = ‘p2lose’
p2 = true
res = -1
report = '{The p2/he} lose{s}! ’
conditions = [
‘defect-v-stay loyal’ ,
‘stay loyal-v-defect’
]
;

p2loseBadlyPD : Result
name = ‘p2losebadly’
p2 = true
res = -10
report = '{The p2/he} lose{s} badly! ’
conditions = [
‘defect-v-defect’
]
;

//////////////////////////////////////////////////////////////////////
///////////// PLAY (RPS) HANDLING STUFF //////////////////////////////
//////////////////////////////////////////////////////////////////////

DefineTAction(RPS);

VerbRule(RPS)
‘play’ ‘rps’ ‘with’ singleDobj
: RPSAction
verbPhrase = ‘play/playing rps (with whom)’
;

modify Thing
dobjFor(RPS) {
verify() { illogical(’{The dobj/he} would be a pretty dull counterpart. '); }
}
;

modify Person

strat = [
]
lastGAEM = nil
lastGAEMresult = nil

// how often the person is willing to play RPS
GAEMfreq = 3

isHim = true

dobjFor(RPS) {
  verify() {}

  check() {
     if (!wantsToPlay) {
            local msg = '{You/he} want{s} to play RPS with
                {the dobj/him}, but ';
            if (lastGAEMresult == nil ) 
                msg += '{the dobj/he} {is} busy. ';
            else if (lastGAEMresult > 0 ) 
                msg += '{the dobj/he} {is} still gloating over
                 {its/hers} win. ';
            else if (lastGAEMresult < 0 ) 
                msg += '{the dobj/he} {is} still smarting over
                 {its/hers} loss. ';
            else if (lastGAEMresult == 0 ) 
                msg += '{the dobj/he} {is} still thinking over
                 {its/hers} tactics. ';
        failCheck(msg);
     }
  }

  action() {
        rpsGaem.p1 = gActor;
        rpsGaem.p2 = self;
        rpsGaem.mainLoop;
  }

}

wantsToPlay()
{
local ans = ‘’;
if ( self != gPlayerChar)
return (GAEMfreq && (lastGAEM == nil ||
(lastGAEM <= libGlobal.totalTurns - GAEMfreq)));
else do
{
"{The actor/he} asks you to play a game. ";
"Do you want to (y/n)? ";
ans = toString(inputManager.getInputLine (nil, nil) );
ans.toLower();
}
while ([‘y’, ‘yes’, ‘n’, ‘no’].indexOf(ans) == nil);
return ([‘y’, ‘yes’].indexOf(ans) != nil);
}

;

//////////////////////////////////////////////////////////////////////

class RPSAgenda: AgendaItem
initiallyActive = true

isReady() {
return getActor.wantsToPlay && prand.n0(3) == 2; //pseudo-randomize these
}

invokeItem() {
local npcs = getActor.sensePresenceList(sight).subset({obj:
obj.ofKind(Actor) && obj != getActor //&& obj != gPlayerChar
});
// “<<(npcs.sublist(1)).valWhich({x: x==x}) >>”;
local pdn = prand.n0( npcs.length() ); //?
// "PDN = <>. ";
if (npcs.length)
newActorAction(getActor, RPS, npcs[pdn] ); //rand(npcs)); //pseudo-randomize this
}
;

//////////////////////////////////////////////////////////////////////

guard: Person ‘guard’ ‘guard’ @lab
"Just another guard. Although doubtless with his own inner life when
he’s not playing an NPC in a TADS game. "
GAEMfreq = 5
;

  • RPSAgenda;

scientist: Person ‘scientist’ ‘scientist’ @lab
“Just another scientist. But with his own human dramas and foibles
when he goes home to his family, no doubt.”
GAEMfreq = 0
;

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

  • RPSAgenda;

//////////////////////////////////////////////////////////////////////
[/code][/rant]

Many thanks, Ben, for all the help with this; and the project continues.

Conrad.