Mini-Cluedo in PunyInform

I thought this would be a fun little exercise, and it was. I chose to aim for Z-code version 5 only. If one really wanted to make this a version 3 game, it could be adapted to v3 in ten minutes or so.

Program part 1:

Constant Story      "Mini-Cluedo";
Constant Headline   "^A PunyInform demonstration.^";

Constant STATUSLINE_SCORE; Statusline score;

Constant NO_SCORE = 0;

!Constant DEBUG;
!Constant RUNTIME_ERRORS = 0; ! 0, 1 or 2. 0 = smallest file, 2 = most info
Constant OPTIONAL_NO_DARKNESS;
Constant OPTIONAL_ALLOW_WRITTEN_NUMBERS;
Constant OPTIONAL_GUESS_MISSING_NOUN;
Constant OPTIONAL_EXTENDED_METAVERBS;
Constant OPTIONAL_PROVIDE_UNDO;

Constant INITIAL_LOCATION_VALUE = Library;

Include "globals.h";

Global teleport_destination;
Global crime_scene;
Global killer;
Global room_count;

Property teleport_words;
Property chances;

First, set the name of the game and the headline, to be printed on game start.

Declare that we want the Score/Moves statusline rathern than the Time statusline, but then again, we’re going to replace the statusline anyway.

Say that we won’t be counting score in this game, we won’t have the concept of darkness (there’s always light to see by), let the player type out numbers (e.g. “WAIT FIVE” instead of “WAIT 5”) if they want, make the parser pick the object when it’s left out if it’s obvious (e.g. “QUESTION” with only one person present, means we want to question that person), add useful metaverbs like TRANSCRIPT etc, and provide an UNDO command.

We also choose to place the player in the Library.

Now we’ve defined the necessary constants, and it’s time to include the first of two library files: globals.h

Then we’ll set up four global variables, that we decided we need.

We will also need two new common properties for objects. We could start using them without declaring them - then they would become individual properties, which are a bit less efficient to use. We like efficiency :slight_smile:

2 Likes

Program part 2:

Replace DrawStatusLine;

[ UnknownVerb p_word p_word_2 i o arr;
	teleport_destination = 0;
	if(NumberWords() > 1) p_word_2 = WordValue(2);
	for(i = room_count: i>=0: i--) {
		o = Rooms-->i;
		arr = o.&teleport_words;
		if(p_word == arr-->0 or arr-->1) {
			teleport_destination = o; 
			if(o == location)
				teleport_destination = "You're already there!";
			break;
		}
	}
	if(teleport_destination)
		return ',teleport';
	rfalse;
];

Constant GS_WRONG = 3;

[ DeathMessage;
	print "You have been humliliated by the Chief of Police";
];

Include "puny.h";

[ DrawStatusLine p_linefeeds exits i o;
	! If called with p_linefeeds == true, print the number of linefeeds needed
	! to make sure text at start of game doesn't get covered by statusline
	if(p_linefeeds) "^";

	! If there is no player location, we shouldn't try to draw status window
	if (location == nothing || parent(player) == nothing)
		return;

	_StatusLineHeight(2);
	_MoveCursor(1, 1); ! This also sets the upper window as active.
	FastSpaces(screen_width);
	_MoveCursor(1, 2);
	print (name) location;
	_PrintSpacesOrMoveBack(12, MOVES__TX);
	print turns;
	_MoveCursor(2, 1);
	FastSpaces(screen_width);
	_MoveCursor(2, 2);
	print "Exits: ";
	for(i = 1: i <= room_count: i++) {
		o = Rooms-->i;
		if(o ~= location) {
			if(exits) print ", ";
			print (name) o;
			exits++;
		}
	 }
	_MainWindow(); ! set_window
];

Include "ext_waittime.h";

Since we want to write our own statusline routine, we need to tell the compiler to ignore the DrawStatusLine routine that it will find in the library.

For this game, we ditch traditional navigation using directions entirely, in favour of a teleportation system, where the player can just type the name of the room they want to go to. We do this by using the UnknownVerb entry point routine - whenever the player types a verb that isn’t recognized, this routine is called. In this routine, we check if what the player has typed is the name of a location, and if it is, we set the global variable teleport_destination to the room ID, or a message if we’re already in this room. If we did manage to match what the player typed to a location, we return the verb ‘,teleport’, which means the parser will look at the grammar for this verb to match what the player typed (That grammar will just accept anything - the real work has been done in this routine).

The game state is held in deadflag and is usually GS_PLAYING until the player wins (GS_WIN) or dies (GS_DEAD). We add another way of ending the game, called GS_WRONG, which means the player has accused someone, but failed to put them behind bars.

Then we need a message to be printed if the game ends in the GS_WRONG state.

We include the main library file: puny.h

Then it’s time to define our own DrawStatusline routine. This routine draws a statusline with two lines. On the top line is the room name and the number of moves we have made. On the second line is a list of rooms we can go to.

Finally, we include the library extension ext_waittime.h, which is included with PunyInform. It allows the player to wait several moves, e.g. “WAIT 5” (but the waiting is interrupted if someone enters the room).

1 Like

Program part 3:

Object Library "Library"
	with
		teleport_words 'library',
		description "You are in a library.";

Object Study "Study"
	with
		teleport_words 'study',
		description "You are in a study.";

Object LivingRoom "Living Room"
	with
		teleport_words 'living' 'livingroom',
		description "You are in a living room.";

Object HobbyRoom "Hobby Room"
	with
		teleport_words 'hobby' 'hobbyroom',
		description "You are in a hobby room.";

Array Rooms table Library Study LivingRoom HobbyRoom;

Now, let’s define our locations.

We create four rooms. For each room, we give it a name, a room description, and the words the player can use to go to that room.¨

Then we create an array holding all the rooms, as this comes in handy in NPC movement etc.

1 Like

Program part 4:

Class Suspect
	with
		chances 0 1 1 1 1, ! First index is 0, not used
		daemon [ i k loc total chosen;
			! Only move 1/3 of the time
			if(random(3) > 1) return;
			loc = parent(self);
			! Sum the weights of all destinations
			for(i=room_count: i>0: i--)
				if(Rooms-->i ~= loc)
					total = total + self.&chances-->i;
			! Choose a random destination, where the probability for picking a
			! certain room is relative to its weight. Adjust weights to make
			! it more likely we visit rooms we haven't visited in a while
			k = random(total);
			for(i=room_count: i>0: i--) {
				if(Rooms-->i == loc)
					self.&chances-->i = 0;
				else {
					k = k - self.&chances-->i;
					if(k <= 0 && chosen == 0) chosen = Rooms-->i;
					(self.&chances-->i)++;
				}
			}
			if(player in loc)
				print "^", (The) self, " leaves the room.^";
			move self to chosen;
			if(player in chosen) {
				waittime_waiting = false;
				print "^", (The) self, " arrives.^";
			}
		],
	has animate proper;

Suspect Bill "Bill" with name 'bill';
Suspect Tom "Tom" with name 'tom';
Suspect Linda "Linda" with name 'linda' has female;
Suspect Sue "Sue" with name 'sue' has female;

Array Suspects table Bill Tom Linda Sue;

Let’s add out suspects. We have four of them.

First we create a class which holds functionality, properties and attributes that all suspects will need. It has a daemon which is responsible for moving the NPC around, as well as printing messages when this happens. The routine assigns a weight to every room. Every time a room is not chosen as a destination, the weight for that room increases. Every time a room is chosen, the weight for that room is reduced to 0. This means movement is random, but the longer an NPC avoids a certain room, the more certain it becomes that they will go there next.

The class also hold the attributes animate and proper, as all NPCs should have them. proper means their name should never be prefixed by an article (E.g. we don’t want to call Bill “the Bill”).

With the class holding almost everything an NPC needs, adding the NPCs themselves is easy enough. Two of them are female. PunyInform assumes NPCs that lack a gender attribute are male.

We’ll also need an array holding our suspects.

1 Like

Program part 5:

[ HeOrShe p_noun;
	if(p_noun has female)
		print "she";
	else
		print "he";
];

[ CHeOrShe p_noun;
	if(p_noun has female)
		print "She";
	else
		print "He";
];


#Ifdef DEBUG;
Verb meta 'chance' 'chances'
	* creature -> Chance;

[ ChanceSub i;
	if(~~(noun provides chances))
		print_ret (The) noun, " lacks the chances property!";
	for(i=1: i<=room_count: i++) {
		print (The) Rooms-->i, ": ", noun.&chances-->i;
		if(noun in Rooms-->i) print " (",(The) noun," 's current location)";
		print "^";
	}
];
#Endif;

It’s time to define the verbs, grammar and actions we need.

Let’s start with two support routines, two print rules to print he or she, with and without a capital letter at the beginning.

Then we add a debug verb ‘chance’, with a single grammar line, leading to the action Chance. The action Chance needs an action routine, and it has to be named as the action + the suffix Sub, so ChanceSub it is. This action shows the relative weights assigned to different rooms for an NPC. It also shows where the NPC is right now. If we compile the game in DEBUG mode, this verb, grammar and actions exists, otherwise it doesn’t.

1 Like

That’s a clever way to handle the movement! It’s also something that Dialog specifically can’t do (at least not elegantly): it’s great at optimizing static relations, but struggles with dynamic ones that change in play.

1 Like

Program part 6:

Verb ',teleport'
	* 			-> Teleport
	* topic 	-> Teleport;

[ TeleportSub;
	if(teleport_destination ofclass String)
		print_ret (string) teleport_destination;
	PlayerTo(teleport_destination);
];

Extend 'search' replace
	* 							-> SearchRoom
	* 'room'/'location'/'scene' -> SearchRoom;
	
[ SearchRoomSub;
	if(location == crime_scene)
		"You find all the evidence you need - this is the crime scene, for sure.";
	"You find nothing that indicates a murder was commited here.";
];

Verb 'question'
	* creature -> Question;

[ QuestionSub;
	if(noun == killer)
		"~Okay, I admit, I did it!~, ", (HeOrShe) noun, " ", 
			(string) random("shouts", "screams", "exclaims"), ".";
	print_ret (CHeOrShe) noun, " calmly denies having anything to do with the murder.";
];

More grammar!

We need a teleport verb, but we don’t want to ask the player to remember a particular verb to use, so we use the verb ‘,teleport’. Note the comma at the start! This means the word can never be matched to player input (a comma is always its own word). Instead, this grammar can only be applied when the player types a verb that the parser doesn’t recognize as a verb, e.g. ‘library’. The UnknownVerbroutine kicks in, returns the verb ‘,teleport’ and voila, this grammar is used, leading to the action Teleport.

The TeleportSub action routine knows that teleport_destination now holds a string or a room ID. If it’s a string, it’s a message telling the player why they can’t go there. If it’s a room ID, let’s move the player there.

Then we need our own handling of the ‘search’ verb. The library normally defines this verb and an action for it, but we bypass all that by saying we want to replace the grammar for ‘search’ entirely, and we create a new SearchRoom action. The player can type “search” or “search room” or even “search location” or “search scene” to trigger this action.

Now we need a way to question suspects. We create the ‘question’ verb and a Question action. When this actions is called, the suspect will immediately confess, or say they’re innocent.

1 Like

Program part 7:

Verb 'accuse'
	* creature -> Accuse;

[ AccuseSub;
	deadflag = GS_WRONG;
	print "Police arrive. ";
	if(location == crime_scene && noun == killer) {
		deadflag = GS_WIN;
		print_ret (The) noun, " ", 
			(string) random("breaks down", "sobs quietly", "laughs diabolically"),
			" as ", (HeOrShe) noun, "'s taken away. 
			^^The Chief of Police gives you an admiring look: ~Well done, detective!~";
	}
	if(location == crime_scene) {
		print_ret (The) noun, " looks at you in confusion: ~But I have an alibi!~ 
			^^The Chief of Police questions the suspect, finds that ", (HeOrShe) noun,
			" actually does	have an alibi, and releases ", (ItOrThem) noun, ".
			^^The chief looks at you and shakes her head in disappointment.";
	}
	if(noun == killer) {
		print_ret (The) noun, " gives you a smile: ~You have no idea, do you? I'm sure
			they'll find no signs of a murder having been commited here.~
			^^The Chief of Police have her officers do a cursory search of the room. With
			no evidence to support your claim, they have to let ", (the) noun, " go.
			The chief looks at you and shakes her head in disappointment.";
	}
	print_ret (The) noun, " looks at you in utter confusion: ~You're saying *I*
		commited a murder, *HERE*? You must be out of your mind!~ 
		^^The Chief of Police have her officers do a cursory search of the room, while
		she questions the suspect. Finding no evidence to support that ", (the) noun,
		" is the perpetrator, or that this is even the crime scene for that matter, ", 
		(the) noun, " is free to go.
		^^The chief looks at you and shakes her head in disappointment.";
];

Of course we need an accuse verb/action! We write code for four different outcomes: All correct, location is correct, suspect is correct, and nothing is correct. At the top of the routine, we change deadflag to GS_WRONG, as this is the outcome of three of these branches. Nothing will happen immediately when we set this, but if it’s still GS_WRONG when we return from the routine, the game ends in tragedy :slight_smile: . In the one code path that leads to success, we change deadflag to GS_WIN instead. In either case, the game will end after this routine.

We use the print rules we created earlier, HeOrShe and CHeOrShe, to print he or she. Also, we use some print rules that are supplied by the language and the library: (The), (the) and (ItOrThem).

Another neat trick we use, is to choose a string at random, so we get some variation.

print_ret is a kind of composite command, meaning: (a) print this, then (b) print a newline character, then (c) return true from the routine.

1 Like

Program part 8:

Verb meta 'help' 'about' 'instructions'
	* -> Help;

[ HelpSub;
	"Someone has commited a murder.
		^* Go to another room by typing its name.
		^* You can QUESTION a person to ask if they did it, and they will reply truthfully. 
		^* You can SEARCH to decide if your current location is the crime scene. 
		^* You can WAIT (or Z, or WAIT 10) to wait for suspects to move.
		^* You can ACCUSE a person. If you accuse the person who is the murderer, 
		and you do it at the scene of the crime, you win.
		Otherwise you lose. Good luck!^^ (Type HELP to repeat this text at any time!)";
];

[Initialise o loc;
	<Help>;
	normal_directions_enabled = false;
	room_count = Rooms-->0;
	objectloop(o ofclass Suspect) {
		loc = Rooms-->(random(room_count));
		move o to loc;
		StartDaemon(o);
	}
	crime_scene = Rooms-->(random(room_count));
	killer = Suspects-->(random(Suspects-->0));
];

Now let’s wrap this up.

We want a ‘help’ verb, to display the instructions on how to play at any time.

And we need an Initialise routine, which is run when the game starts. In this routine, we show the Help text, by issuing a Help action. Then we turn off regular movement commands, e.g. “go north”. We store the number of rooms in a global variable. Then we randomly place all suspects in rooms, and start their daemons. Finally, we randomize the crime scene location and who the killer is.

That’s it.

I hope it can provide some insight or inspiration for someone.

1 Like

Here’s a screenshot of the game:

And here’s the source code and a Z-code file, compiled with -Dv5es~S (DEBUG mode, version 5 of the Z-machine, use abbreviations for better text compression, show compile statistics, turn off Strict mode (which checks for possible bugs at runtime, but makes the game larger and slower)).

cluedo.zip (27.6 KB)

EDIT: Updated the zip archive, after a bug fix

4 Likes