The tutorial bird of happiness

This commit is contained in:
Howard Abrams 2025-03-02 10:26:12 -08:00
parent 107241f925
commit 1876919acd
5 changed files with 333 additions and 59 deletions

1
.gitignore vendored
View file

@ -55,3 +55,4 @@ nosetests.xml
# VSCode config
.vscode
/.venv/
.DS_Store

View file

@ -13,7 +13,25 @@ from evennia.prototypes.spawner import spawn
from utils.word_list import Token, routput
from .objects import Object
from .tutorial import TutorBird, TutorialState
INTRO = """
As the surrounding mists dissipate, you find yourself in an ancient, halcyon forest dripping with moss. You see an envelope of parchment wedged under a scaly protrusion of bark...inside, a letter in familiar penmanship, personally addressed to you, which you pick up.
A little blue bird flies by you, almost grazing your ear!"""
READ_LETTER = """You read a letter with an oddly familiar penmanship:
My dearest {0},
If you are reading this, you've found the world I was overly excited in relaying to you over drinks in Marsivan. Most excellent. Enjoy this halcyon world, unspoiled and idyllic.
I'm here, so join me in a cup of tea and we can reconnect and reminisce of glorious days gone by, and the utter curiosity that surrounds us.
Your friend,
Dabbler
(Type 'help start' for details on playing this game)"""
class Character(Object, DefaultCharacter):
"""
@ -26,23 +44,21 @@ class Character(Object, DefaultCharacter):
"""
def at_object_creation(self):
"called when a character is first created."
self.db.tutorstate = 0
if self.dbref != "#1":
self.create_letter()
TutorBird.do_start_tutorial(self)
def at_post_puppet(self):
if self.db.visited:
self.msg(f"""\n“Welcome back, {self.key.capitalize()}.”\n""")
self.execute_cmd("look")
else:
self.db.visited = True
self.msg("""
As the surrounding mists dissipate, you find yourself in an ancient, halcyon forest dripping with moss.
You see an envelope of parchment wedged under a scaly protrusion of bark...inside, a letter in familiar penmanship, personally addressed to you, which you pick up.
(Type 'inventory' or 'inv' or just 'i' to see what you carry).
""")
self.db.tutorstate = 0
self.msg(INTRO)
self.account.db._last_puppet = self
self.execute_cmd("look")
def create_letter(self):
"create a welcome letter in a character's inventory"
@ -51,19 +67,7 @@ You see an envelope of parchment wedged under a scaly protrusion of bark...insid
"key": "letter",
"desc": "A letter of familiar penmanship stuffed in an envelope.",
})[0]
letter.db.inside = f"""You read a letter with an oddly familiar penmanship:
My dearest {self.name.capitalize()},
If you are reading this, you've found the world I was overly excited in relaying to you over drinks in Marsivan. Most excellent. Enjoy this halcyon world, unspoiled and idyllic.
I'm here, so join me in a cup of tea and we can reconnect and reminisce of glorious days gone by, and the utter curiosity that surrounds us.
Your friend,
Dabbler
(Type 'help me start' for details on playing this game)
"""
letter.db.inside = READ_LETTER.format(self.name.capitalize())
letter.location = self
def do_take(self, args):
@ -81,7 +85,7 @@ You see an envelope of parchment wedged under a scaly protrusion of bark...insid
victim = self.search(from_whom)
if victim:
thing = victim.has(to_take)
if thing:
if thing and thing.db.can_take:
self.msg(f"You take {thing.key} from {victim.key}.")
self.location.msg_contents(
f"{self.key} takes {thing.key} from {victim.key}!",
@ -94,11 +98,50 @@ You see an envelope of parchment wedged under a scaly protrusion of bark...insid
self.msg(f"You don't see {from_whom}.")
def at_pre_move(self, destination, **kwargs):
"""
Called by self.move_to when trying to move somewhere. If this returns
False, the move is immediately cancelled.
"""
if self.db.is_sitting:
self.msg("You stand up first...")
self.db.is_sitting = False;
return True
"""
Called by self.move_to when trying to move somewhere. If this returns
False, the move is immediately canceled.
"""
self.db.tutorstate = self.db.tutorstate | TutorialState.MOVE.value
self.msg(f"State: {self.db.tutorstate}")
if self.db.is_sitting:
self.msg("You stand up first...")
self.db.is_sitting = False;
return super().at_pre_move(destination)
def at_pre_say(self, message, **kwargs):
"While we could/should do 'at_say', this should be easier."
self.db.tutorstate = self.db.tutorstate | TutorialState.SAY.value
return super().at_pre_say(message)
def at_look(self, target, **kwargs):
"""
When we look at something that _might_ be hidden, we check
if we are looking at something that _is_ hidden, i.e. has a
view lock of a particular tag, and if so, we interpret this as
'you are looking at something by name', therefore you see it.
So we give ourselves the right to see it from now on.
To use this, simply add the following lock:
@lock thing = view:tag(hidden_thing, mp)
Where thing is the single name of the hidden object.
"""
hidden_tag = f"hidden_{target}"
lock_string = f"view:tag({hidden_tag}, mp)"
view_lock = target.locks.get("view")
if view_lock == lock_string:
self.tags.add(hidden_tag, category="mp")
self.msg(f"Initial look State: {self.db.tutorstate}")
if target.is_typeclass("typeclasses.rooms.Room"):
self.db.tutorstate = self.db.tutorstate | TutorialState.LOOK.value
else:
self.db.tutorstate = self.db.tutorstate | TutorialState.LOOKAT.value
# Regardless of what happened before, we return the normal
# function call.
return super().at_look(target)

View file

@ -234,28 +234,3 @@ class Object(ObjectParent, DefaultObject):
return i
elif i == item:
return i
def at_look(self, target, **kwargs):
"""
When we look at something that _might_ be hidden, we check
if we are looking at something that _is_ hidden, i.e. has a
view lock of a particular tag, and if so, we interpret this as
'you are looking at something by name', therefore you see it.
So we give ourselves the right to see it from now on.
To use this, simply add the following lock:
@lock thing = view:tag(hidden_thing, mp)
Where thing is the single name of the hidden object.
"""
hidden_tag = f"hidden_{target}"
lock_string = f"view:tag({hidden_tag}, mp)"
view_lock = target.locks.get("view")
if view_lock == lock_string:
self.tags.add(hidden_tag, category="mp")
# Regardless of what happened before, we return the normal
# function call.
return super().at_look(target)

183
typeclasses/tutorial.py Executable file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env python
from evennia import (
Command,
CmdSet,
TICKER_HANDLER,
syscmdkeys,
create_script,
)
from evennia.prototypes.spawner import spawn
from enum import Enum
from typeclasses.objects import Object
from typeclasses.npcs import CarriableNPC
from utils.word_list import routput
import random
MSGS = {
"START": [
"A small bird flies over to you, and perches on your shoulder. “Hello,” it says in your ear. “Im the Tutorial Bird,” it chirps, “and Im here to break the fourth wall, and help you figure out this game. Feel free to |bshoo|n me away, and Ill leave you alone to explore.”",
"The bird says, “In this story game, you type commands (verbs) to do things. For instance, type the word |blook|n, and press the |wReturn|n key to look around at your surroundings and to see where you are, what you can do, and where you can go.”",
"The bird perched on your shoulder looks at you expectantly.",
"“Go ahead,” says the bird, “Type |blook|n. If you are on the web site, you may need to click in the box in the lower part of the screen.”",
"The bird preens itself.",
"The bird says, “Would you like me to stay and help you out?”",
"“If so,” says the bird, “type |blook|n to look around at your surroundings.”",
],
"LOOK": [
"“Thats great,” says the little blue bird. “From that, you know you are standing in a grove of trees near a puddle. You can now type |blook puddle|n to examine it.”",
"“You can also |blook trees|n, |blook moss|n, or … well, you get the idea,” it says.",
],
"LOOKAT": [
"“You seem to be getting the hang of this game,” chirps the bird. “When you typed |blook|n before, you noticed exits. These are directions or places you can go. For instance, if you type |bsouth|n (or just |bs|n as a shortcut), you can check out our pond where you can catch obnoxious fish.”",
"“The path to the |beast|n is the meadow of the BHB, the Big Hairy Beast,” the little bird on your shoulder says. “It's adorable if you can find it. The path to the |bwest|n leads to Dabblers place.”",
"“Yes, I could fly myself,” warbles the little bird, “but Im trying to help you get around.”",
"The bird looks at you expectantly.",
"“Shall we explore other places?” asks the bird on your shoulder.",
"“I hope Im not rushing you,” the bird mentions, “as you can always come back to explore more.”",
],
"MOVE": [
"\"Thanks for lift. This is a nice place,\" says the bird on your shoulder. \"When you come to a new area, the game shows you what you are seeing, so you dont have to type |blook|n … even though you still can.\"",
"\"Another interesting command,\" it chirps, \"is the |bsay|n command, that lets you talk to anyone in the area. Usually, we use this command for talking to other players in the game, but some creatures listen to what you say.\"",
"\"Go ahead, just typing |bsay Hello.|n\"",
],
"SAY": [
"“Hows it going?” the bird asks, “Are you enjoying this game so far?”",
"The bird says, “This place has been a respite from the outside turbulence.”",
"\"A shortcut to |bsay|n is to type either a double or single quote, and then your message.\"",
"\"You can also use |bpose|n to state something about yourself, \" it says. \" For instance, type |bpose smiles.|n\"",
# POSE:
"The bird stands on one leg.",
"“See,” says the bird, “I spose I can pose too.”",
"“A shortcut to that command is typing |b:|n (a colon character).”",
"The bird says, \"The game has more commands. Typing |bhelp|n gives you a list of commands. That list changes depending on where you are and what you are carrying. For instance, you could type |bshoo|n for me to fly away, and then that command wouldnt be available again.\"",
# ],
# "HELP": [
"The bird chirps, “You can learn more about the individual commands. For instance, type |bhelp say|n and you would see about the shortcuts I told you about. You can also see new commands, like |bhelp get|n.”",
"“Type |bhelp start|n for a repeat of much of what weve talked about,” says the bird.",
"The bird says, “The |binv|n shows you what you are carrying. I see you picked up a letter. You can also type |bread letter|n.”",
"“Since this game hosts many users,” says the bird, “you can show them what they see when they look at you, using the |b@setdesc|n command. Check out the help, |bhelp @setdesc|n, or just type: |w@setdesc A pixie with a shock of blue hair and a gold chain.|n”",
"“Im guessing your last question,” chirps the little bird on your shoulder, “is the goal of this game.”",
"“That is a good question,” says the bird, “and Im not sure what to say. This game has no goal, nor point. I guess you could look at it as a philosophical metaphor for existential existence in a post-modern world in the death throes of capitalism, but…”",
"The bird says, “This is a cozy little game about role playing a storybook character and maybe exploring and looking for eggs… no, thats not right. Something about eggs. Easter eggs? Something like that.”",
"“Wander and look around. Find a cozy place to chat with others who have logged in,” says the bird.",
"“If youre interested in |wbuilding|n and expanding this world,” chirps the bird, “talk to Dabbler... Oh, see if you can find Dabbler, the guy that made most of this. That is a great goal.”",
"“What do you say?” says the bird. “Think you got the hang of playing this?”",
]
}
MSGS_WAITING = [
"The [little|blue|] bird [on your shoulder|] preens itself.",
"The [little|blue|] bird [on your shoulder|] [looks|stares] at you [expectantly|waiting|patiently].",
"The bird looks up at the sky. “It rains here often,” it says.",
"The bird looks up at the sky. “Looks like rain is coming,” it says.",
"“I like the rain,” the bird says, “as it keeps the moss green and the puddles full.”",
"“They say if you dont like the weather,” tweets the bird, “just wait.”",
]
class TutorialState(Enum):
"The acceptable states of fishing."
START = 0
LOOK = 1
LOOKAT = 2
MOVE = 4
SAY = 8
class CmdShoo(Command):
"""
End the tutorial by shooing the bird away.
"""
key = "shoo"
def func(self):
self.obj.do_end_tutorial(self.caller)
class CmdSetBird(CmdSet):
def at_cmdset_creation(self):
self.add(CmdShoo)
class TutorBird(CarriableNPC):
"""
Bird that routinely tells a player a new hint.
"""
# The number of ticks to check for the time for a message:
bird_tick = 18
# The number of waiting messages to issue before stopping the tutorial:
wait_messages = 3
def at_object_creation(self):
self.cmdset.add_default(CmdSetBird)
# Set the positions for all the different states:
self.db.tutor_msg_num = {
"START": 0,
"LOOK": 0,
"LOOKAT": 0,
"MOVE": 0,
"SAY": 0,
}
self.db.waiting = 0
TICKER_HANDLER.add(interval=self.bird_tick,
callback=self.do_speak)
def do_start_tutorial(character):
bird = spawn({
"typeclass": "typeclasses.tutorial.TutorBird",
"key": "bird",
"aliases": ["blue bird", "tutor", "tutor bird"],
"desc": "You see a tiny bird with blue feathers trimmed with white. It's large black eyes looks at you quizically. It seems friendly, and overly helpful.",
})[0]
bird.location = character
def do_end_tutorial(self, character):
"""
A visual way to stop the tutorial
"""
character.msg("“Have fun, and Ill leave you here,” chirps the bird, as it flies away.")
self.delete()
def get_msg_level(self, character, level):
choices = MSGS[level.name]
msg_num = self.db.tutor_msg_num.get(level.name, 0)
if msg_num >= len(choices):
# Ran out of messages?
if self.db.waiting > self.wait_messages or level == TutorialState.SAY:
msg = self.do_end_tutorial(character)
else:
msg = routput(random.choice(MSGS_WAITING))
self.db.waiting += 1
else:
msg = choices[msg_num]
self.db.tutor_msg_num[level.name] += 1
self.db.waiting = 0 # Reset the waiting period
return msg
def get_msg(self, character, remaining_states):
try:
# Take the highest value from states...
level = remaining_states.pop()
if character.db.tutorstate >= level.value:
return self.get_msg_level(character, level)
else:
return self.get_msg(character, remaining_states)
except IndexError:
return self.get_msg_level(character, TutorialState.START)
def do_speak(self):
"""
Called at a repeatable sequence by the ticker, and
it calls at_say() in order to do a type of monologue.
"""
character = self.location
msg = self.get_msg(character, [level for level in TutorialState])
if msg:
character.msg(f"\n{msg}")

View file

@ -24,26 +24,98 @@ Each dict is on the form
"""
# """
# },
# {
# "key": "commands",
# "alias": ["command"],
# "locks": "read:all()",
# "text": """
HELP_ENTRY_DICTS = [
{
"key": "start",
"aliases": ["me start"],
"aliases": ["intro"],
"locks": "read:all()",
"text": """Again, welcome to my cozy little game.
To |wplay this game|n, you typically type a |w<verb>|b for an action, or |w<verb> <object>|b combinations. For instance, type |blook|n to look around the area, and |blook tree|n to examine the trees in particular. The more you look, the more you explore.
What verbs are available depends on where you are and what you might be holding. Type |bhelp|n with so other option to get a list. Then type |bhelp look|n to get details on how to use the |wlook|n verb.
What verbs are available depends on where you are and what you might be holding. Type |bhelp|n with no other option to get a list of those commands. Then type |bhelp look|n to get details on how to use the |wlook|n verb.
This is a multi-user game, you might run into other characters, and they may look at you, so let's use some special verbs to affect the world from outside the storybook. Type:
A bit about this |bhelp|n system. This document is longer, and you'll see at the end of this paragraph, some _transient commands_, for instance, you can type |bnext|n to see the next page. However, you can ignore that list, and type |blook puddle|n to skip this and gander at the puddle that might be at your feet. For now, type |bnext|n to keep reading...
\f
This is a multi-user game, you might run into other characters, and they may look at you, so let's type the following to tell others what they see when they look at you:
|w@setdesc A frumpy, but spry person with large ears and a dark blue cloak.|n
You see what people see when they look at you by typing:
While it can be quite long and descriptive, you are limited to a single paragraph, since when you hit the 'Return' key, you submit your description.
To see what people see when they look at you, type:
|wlook self|n
What is the goal of this game? Just to escape the chaos of the world and explore an idyllic setting. I call it my |gegg hunt game|n, as the game is full odd stuff to discover. Feel free to find me to chat, but good luck finding me, as I may be hiding.
This is the end of this help section, but I have some related topics to this intro, so you can type |bhelp start/commands|n to get a list of typical commands, or just start playing this game.
Enjoy!
# Subtopics
## Commands
A command is typically a 'verb'. The most common ones in this game are:
- 'look' to look around at your surroundings
- 'look <thing>' to look at something particular
- 'get <thing>' to pick up something
- 'inventory' (or 'inv' or just 'i') for a list of what you carry
- 'drop <thing>' to leave the thing in this area (but don't litter)
- 'say' to talk to the characters and object in your current location
- 'pose' to state a fact about yourself.
### Say
We often want to chat in these sorts of games, so instead of typing:
|wsay Good evening!|n
You can simply type:
|w"Good evening!
Or:
|w'Good evening!
Creatures and objects in this game can sometimes respond to what you say.
### Pose
To tell others what you are doing (without |bsay|ning it), you can |bpose|n, as in:
|wpose smiles.|n
We do this a lot, so use this shortcut:
|w: smiles.|n
What about abbreviating "is" statements? Yeah, this works:
|w:'s happy, and wants you to know it.|n
Your "pose" can be anything, for instance, you can pretend to be a wizard:
|w: casts a fireball, exploding the bush behind you!|n
But you may find staying in character more 'escape-y'.
## Exits
Exits are special commands, and move you to a new location. So typing |bsouth|n will move you to a new location (assuming that exit is available). Some exits can be abbreviated, so typing |bs|n alone is the same as 'south'.
Keep in mind that some exits may not be available until you |blook|n for them (but that is part of the exploratory game, like this).
"""
},
{