487 lines
18 KiB
Python
Executable file
487 lines
18 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
import requests
|
|
import sys
|
|
|
|
from os import listdir, path
|
|
# from os.path import join, isfile
|
|
from pathlib import Path
|
|
from random import choice, randint
|
|
from re import match, IGNORECASE
|
|
from time import time
|
|
|
|
from evennia.utils import logger, delay
|
|
from evennia.utils.search import search_object
|
|
|
|
from typeclasses.objects import AI, personality_dir
|
|
from typeclasses.scripts import Script
|
|
from typeclasses.puppets import Puppet
|
|
|
|
|
|
class ChatBot(AI, Puppet):
|
|
"""
|
|
py me.search("squirrel").backstory("squirrel")
|
|
"""
|
|
def backstory(self, personality=None):
|
|
"""
|
|
Read a file that includes a character's name and knowledge.
|
|
"""
|
|
if not personality:
|
|
files = listdir(personality_dir)
|
|
personalities = [f for f in files
|
|
if path.isfile(path.join(personality_dir, f))]
|
|
personality_file = choice(personalities)
|
|
else:
|
|
personality_file = personality + ".md"
|
|
|
|
filename = Path(path.join(personality_dir, personality_file))
|
|
if filename.exists():
|
|
personality = filename.stem
|
|
else:
|
|
logger.error(f"Chatbot Identity, {personality}, doesn't exist: {filename}")
|
|
|
|
with open(filename, "r") as ids:
|
|
details = ids.read()
|
|
|
|
# Create some paragraphs, and take the second:
|
|
content = details.split('\n\n')[1]
|
|
lines = content.splitlines()
|
|
desc = []
|
|
|
|
# Find name and description
|
|
for line in lines:
|
|
m = match(r"([A-z]+): +(.*)", line)
|
|
if m:
|
|
key = m.group(1).lower()
|
|
value = m.group(2).strip()
|
|
if key == "name":
|
|
self.aliases.remove()
|
|
self.aliases.add(value)
|
|
elif key == "description":
|
|
self.sdesc.add(value)
|
|
elif key == "gender":
|
|
self.db.gender = value
|
|
elif key == "pose":
|
|
self.db.pose = value
|
|
else:
|
|
desc.append(line.strip())
|
|
|
|
self.db.personality = personality
|
|
self.db.desc = ' '.join(desc)
|
|
self.db.personality_file = filename
|
|
return details
|
|
|
|
def other_say(self, speaker, speech):
|
|
# logger.info(f"chatbot hears: '{speech}' from {speaker}.")
|
|
# logger.info(f"characters: {self.characters_here(puppets=True)}")
|
|
if len(self.characters_here(puppets=True)) == 1:
|
|
self.other_sayto(speaker, speech)
|
|
|
|
def other_sayto(self, speaker, speech):
|
|
logger.info(f"Direct Chatbot hears: '{speech}' from {speaker}.")
|
|
if speech:
|
|
logger.info("Starting to think of a reply")
|
|
reply = self.think(speaker, speech)
|
|
self.process_thoughts(reply)
|
|
|
|
def at_msg_receive(self, text=None, from_obj=None, **kwargs):
|
|
"""
|
|
Reset the timer whenever we get any event.
|
|
This might be too much.
|
|
"""
|
|
super().at_msg_receive(text, from_obj=from_obj, **kwargs)
|
|
self.db.last_event_time = time()
|
|
|
|
def other_arrive(self, character):
|
|
"""
|
|
Greet a character when it arrives.
|
|
"""
|
|
if character != self:
|
|
delay(4, self.greet, character)
|
|
|
|
def at_post_move(self, past_location, move_type="move", **kwargs):
|
|
"""
|
|
Call the 'greet' method.
|
|
"""
|
|
super().at_post_move(past_location, move_type)
|
|
chars = self.characters_here()
|
|
|
|
if len(chars) == 1:
|
|
delay(4, self.greet, chars[0])
|
|
elif len(chars) > 1:
|
|
delay(3, self.greet)
|
|
|
|
|
|
class Bartender(ChatBot):
|
|
"""
|
|
Like any other Chatbot, but this one hears and responds to
|
|
more things.
|
|
"""
|
|
def greet(self, character=None):
|
|
if character:
|
|
self.announce_action(f"\"Welcome to the |wWyldwood Bar|n,\" the {self.get_name()} says to the {character.get_name()}, \"Drink list is on the sign.\"")
|
|
|
|
def backstory(self, personality):
|
|
"""
|
|
This overshadows the 'backstory' in the parent Chatbot class.
|
|
|
|
TODO The backstory does too much, and we may want this to be the
|
|
other approach.
|
|
"""
|
|
personality_file = personality + ".md"
|
|
filename = Path(path.join(personality_dir, personality_file))
|
|
if filename.exists():
|
|
self.db.personality = filename.stem
|
|
self.db.personality_file = filename
|
|
|
|
def other_say(self, speaker, speech):
|
|
logger.info(f"Bartender hears: '{speech}' from {speaker}.")
|
|
if len(self.characters_here(puppets=True)) == 3 or \
|
|
match(r".*\b(bartend|barkeep|elendil).*", speech, IGNORECASE):
|
|
self.other_sayto(speaker, speech)
|
|
|
|
|
|
class Witch(ChatBot):
|
|
"""
|
|
@update Trampoli = typeclasses.chatbots.Witch
|
|
@set/delete Trampoli/arrive =
|
|
@set Trampoli/personality = "witch"
|
|
@set Trampoli/personality_file = "personalities/witch.md"
|
|
@script Trampoli = typeclasses.chatbots.TravelingNPC
|
|
"""
|
|
def greet(self, character=None):
|
|
if character and character != self:
|
|
self.announce_action(f"Looking at the {character.sdesc.get()}, {self.get_name()} says, \"Oh, hello, dear. How are you?\"")
|
|
else:
|
|
self.announce_action(f"The {self.get_name()} says, \"Hello, dears.\"")
|
|
|
|
def goodbye(self, character=None):
|
|
delay(2, self.do_cmd, "As she grabs her broom, the little old lady says, \"Well, I must be off.\"")
|
|
|
|
def next_place(self):
|
|
# In not in the Cozy House, Hut, or the Bar, leave:
|
|
if not any(self.location.aliases.get(fav)
|
|
for fav in ["mp03", "mp09", "mp10"]):
|
|
self.leave()
|
|
|
|
def change_direction(self):
|
|
"""
|
|
Hard coded directional change, east <-> west
|
|
Note: Change with the command:
|
|
|
|
@set npc/traveling_direction = "come"
|
|
"""
|
|
if any(self.location.aliases.get(fav)
|
|
for fav in ["mp03", "mp10"]):
|
|
logger.info(f"Witchie is leaving, {self.location}")
|
|
self.leave()
|
|
else:
|
|
logger.info(f"Witchie needs to take off...")
|
|
# Let's go out for a bit ...
|
|
r = randint(1, 100)
|
|
if r < 3:
|
|
destination = "Cozy House"
|
|
elif r < 12:
|
|
destination = "Wyldwood Bar"
|
|
else:
|
|
destination = choice([
|
|
"mp15", # Shore
|
|
"Frog Meadow",
|
|
"Boulder Top",
|
|
"Lonely Island"
|
|
])
|
|
self.visit(destination)
|
|
|
|
def visit(self, location="Cozy House"):
|
|
self.execute_cmd("pose reset")
|
|
dest = self.search(location, global_search=True)
|
|
if not dest:
|
|
logger.warn(f"Witch attempting to go to unknown {location}.")
|
|
return
|
|
|
|
if dest.key == "Cozy House":
|
|
visit_reason = "visit an old friend"
|
|
elif dest.key == "Wyldwood Bar":
|
|
visit_reason = "grab a pint"
|
|
else:
|
|
visit_reason = "forage for ingredients"
|
|
|
|
hut = self.location
|
|
delay(1, hut.msg_contents,
|
|
f"The old lady says, \"I must be off now, dearie, to {visit_reason}.\"")
|
|
delay(5, hut.msg_contents,
|
|
"She grabs an old broom, and flies out the door!")
|
|
|
|
marsh = self.search("mp08", global_search=True)
|
|
delay(5, marsh.msg_contents,
|
|
"An old lady flies out the hut on a broomstick!")
|
|
|
|
if dest.key == "Cozy House":
|
|
landing = self.search("mp04", global_search=True)
|
|
knocker = self.search("knocker", global_search=True,
|
|
location=landing)
|
|
delay(14, knocker.do_knock, self)
|
|
if len(dest.characters_here()) > 0:
|
|
delay(20, landing.msg_contents,
|
|
"\"I think I heard a 'come in',\" says the old lady. \"You heard it too, right?\"")
|
|
delay(24, self.move_to, dest)
|
|
delay(30, dest.msg_contents,
|
|
"\"Now this looks cheery,\" says the old lady, as she places her broom by the door. \"I do believe we are in for a spell of rain.\"")
|
|
else:
|
|
delay(20, landing.msg_contents,
|
|
"\"I guess no one is home, eh?\" says the old lady.")
|
|
delay(23, self.leave)
|
|
|
|
elif dest.key == "Wyldwood Bar":
|
|
landing = self.search("mp19", global_search=True)
|
|
delay(14, landing.execute_cmd, "say This young thing needs a drink")
|
|
delay(18, self.move_to, dest)
|
|
delay(30, dest.msg_contents, "The old lady saunters up to the bar, \"How about an ale, Barkeep?\" she asks.")
|
|
else:
|
|
landing = dest
|
|
if dest.key == "Shore":
|
|
ingredient = "yellow flowers from the pine tree"
|
|
elif dest.key == "Frog Meadow":
|
|
ingredient = "giggling tickleweed"
|
|
elif dest.key == "Boulder Top":
|
|
ingredient = "vibrant mushrooms"
|
|
elif dest.key == "Lonely Island":
|
|
ingredient = "blue moonberries"
|
|
delay(30, dest.msg_contents,
|
|
f"The lady looks around, gathers some {ingredient}" +
|
|
" and puts them in her basket.")
|
|
|
|
delay(6, self.move_to, landing, quiet=True)
|
|
delay(7, landing.msg_contents,
|
|
"An old lady flies in on a broom.")
|
|
delay(10, landing.msg_contents,
|
|
f"The old lady says, \"Hello Dearie, I'm just here to {visit_reason}.\"")
|
|
|
|
def leave(self):
|
|
"""
|
|
Dramatic departures from the current location.
|
|
"""
|
|
self.location.msg_contents("The old lady wraps her shawl tightly around her, and grabs her broom. \"I must be getting home now,\" she says.")
|
|
|
|
visit_reason = "foraging"
|
|
launch_site = self.location
|
|
|
|
if "mp03" in self.location.aliases.get():
|
|
visit_reason = "visiting my friend"
|
|
launch_site = self.search("mp04", global_search=True)
|
|
delay(2, self.move_to, launch_site)
|
|
if "mp10" in self.location.aliases.get():
|
|
visit_reason = "visiting my friends at the bar"
|
|
launch_site = self.search("mp19", global_search=True)
|
|
delay(2, self.move_to, launch_site)
|
|
|
|
delay(6, launch_site.msg_contents,
|
|
"The old lady hops on her broomstick and flies away through the trees.\"")
|
|
|
|
marsh = self.search("mp08", global_search=True)
|
|
delay(6, marsh.msg_contents,
|
|
"An old lady flies through the air on her broomstick, and right into the hut on stilts!")
|
|
|
|
hut = self.search("mp09", global_search=True)
|
|
delay(7, self.move_to, hut, quiet=True)
|
|
delay(8, hut.msg_contents,
|
|
f"An old lady flies through the door on her broomstick! \"Hello dearies!\" she exclaims to everyone and everything in her homey hut. \"I have returned from {visit_reason}. What have I missed while I was away?\"")
|
|
|
|
sleep_pose = self.attributes.get("pose_sleep")
|
|
if sleep_pose:
|
|
self.execute_cmd(f"pose {sleep_pose}")
|
|
|
|
|
|
class Traveler(ChatBot):
|
|
"""
|
|
Needs to walk from room to room, and greets characters.
|
|
"""
|
|
traveling_path = {}
|
|
|
|
def greet(self, character=None):
|
|
self.do_cmd("emote waves.")
|
|
|
|
def goodbye(self, character=None):
|
|
self.do_cmd("emote waves good-bye.")
|
|
|
|
def change_direction(self):
|
|
"""
|
|
Hard coded directional change, east <-> west
|
|
Note: Change with the command:
|
|
|
|
@set npc/traveling_direction = "come"
|
|
"""
|
|
logger.info(f"The {self} needs to override change_direction")
|
|
|
|
def come(self, direction="come"):
|
|
"""
|
|
Set traveling to a particular given direction.
|
|
"""
|
|
self.db.traveling_direction = direction
|
|
|
|
def leave(self):
|
|
"""
|
|
Set traveling to a new direction.
|
|
Same as changing direction.
|
|
"""
|
|
self.change_direction()
|
|
|
|
def next_place(self):
|
|
direction_label = self.db.traveling_direction
|
|
location_details = self.traveling_path.get(self.location.key)
|
|
if location_details:
|
|
logger.info(f"TravelingNPC: {direction_label} :: {location_details}")
|
|
room_name = location_details.get(direction_label)
|
|
if room_name:
|
|
logger.info(f"TravelingNPC: to -> {room_name}")
|
|
new_room = self.search(room_name, global_search=True)
|
|
if new_room:
|
|
logger.info(f"TravelingNPC: to -> {new_room}")
|
|
# Say See ya if it had engaged...
|
|
self.goodbye(new_room)
|
|
# This process will reset the timer
|
|
delay(5, self.move_to, new_room, move_type="teleport")
|
|
|
|
|
|
class Dragon(Traveler):
|
|
"""
|
|
Travels east-to-west along the path, and has a drink at the
|
|
Wyldwood Tavern.
|
|
"""
|
|
# self.db.direction = "east" # or west or whatever
|
|
|
|
traveling_path = {
|
|
"the Deep Forest": {
|
|
"east": "Grotto",
|
|
"come": "Grotto"
|
|
},
|
|
"Grotto": {
|
|
"east": "Grove of the Matriarchs",
|
|
"west": "the Deep Forest",
|
|
"come": "door"
|
|
},
|
|
"Grove of the Matriarchs": {
|
|
"east": "Frog Meadow",
|
|
"west": "Grotto",
|
|
"come": "Grotto"
|
|
},
|
|
"Frog Meadow": {
|
|
"east": "Glittering Glade",
|
|
"west": "Grove of the Matriarchs",
|
|
"come": "Grove of the Matriarchs"
|
|
},
|
|
"Glittering Glade": {
|
|
"east": "Wyldwood Bar",
|
|
"west": "Frog Meadow",
|
|
"come": "Frog Meadow"
|
|
},
|
|
"Wyldwood Bar": {
|
|
"west": "Glittering Glade",
|
|
"come": "Glittering Glade"
|
|
}
|
|
}
|
|
|
|
def change_direction(self):
|
|
"""
|
|
Hard coded directional change, east <-> west
|
|
Note: Change with the command:
|
|
|
|
@set npc/traveling_direction = "come"
|
|
"""
|
|
self.db.traveling_direction = "west" \
|
|
if self.db.traveling_direction == "east" else "east"
|
|
|
|
def at_pre_move(self, destination, move_type="move", **kwargs):
|
|
if self.location.key == "Wyldwood Bar":
|
|
self.location.msg_contents("The little dragon tips his wide-brimmed and says, \"A pleasure, but I must be off.\"")
|
|
return True
|
|
|
|
def at_post_move(self, past_location, move_type="move", **kwargs):
|
|
if self.location.key != "Wyldwood Bar":
|
|
super().at_post_move(past_location, move_type)
|
|
else:
|
|
if len(self.characters_here()) > 0:
|
|
request = choice([
|
|
"a Moonlit Mirage",
|
|
"Puck's Revenge",
|
|
"a Glimmering Gossamer",
|
|
"a Whimsical Willow",
|
|
"a Charmed Chalice",
|
|
"an Enchanted Elixir",
|
|
"a Sylvan Serenade",
|
|
"a Brambleberry Bliss",
|
|
"a Twilight Tonic",
|
|
])
|
|
self.process_thoughts(f"""The little dragon flutters over to the bar, studying the menu. "What am I in the mood for now?" he muses to himself, twiddling a tendril like a mustache with his little claw.
|
|
|
|
"Elendil, dear," he says, "Would you make me {request}?" The little dragon then waves to the band on their mushroom stage.""")
|
|
bartender = self.search("bartender")
|
|
bartender.other_sayto(self,
|
|
f"\"Sir Roblees, the fairy dragon asks, \"Would you make me {request}?\"")
|
|
# from typeclasses.drinkables import Cocktail
|
|
# Cocktail.make(self, bartender, request)
|
|
|
|
def greet(self, character=None):
|
|
logger.info(f"Dragon: greet {character}")
|
|
if character:
|
|
name = character.get_name()
|
|
cmd = choice([
|
|
f"emote \"Hey, {name},\" /me says.",
|
|
f"emote \"Hey there, {name},\" /me says.",
|
|
f"emote \"Hello, {name},\" /me says. \"How are you?\"",
|
|
f"emote waves to {name}."
|
|
])
|
|
else:
|
|
cmd = choice([
|
|
"say Look at all these luscious peoples.",
|
|
"emote waves to everyone.",
|
|
"emote waves to everybody."
|
|
])
|
|
delay(5, self.do_cmd, cmd)
|
|
|
|
def goodbye(self, new_room=None):
|
|
if self.location.key == "Wyldwood Bar":
|
|
self.do_cmd("drop drink")
|
|
system_prompt = self.setting_and_backstory()
|
|
messages = [{"role": "user", "content": "Say goodbye."}]
|
|
reply = self._think(system_prompt, messages)
|
|
self.process_thoughts(reply)
|
|
|
|
|
|
class TravelingNPC(Script):
|
|
"""
|
|
Script to move NPCs along a set path through "rooms".
|
|
|
|
Start the script by running the following:
|
|
|
|
@script npc = typeclasses.chatbots.TravelingNPC
|
|
"""
|
|
|
|
def at_script_creation(self):
|
|
self.key = "Traveling"
|
|
self.desc = "NPCs that Move"
|
|
self.interval = self.obj.db.traveling_interval or 120 # seconds
|
|
self.start_delay = False
|
|
self.persistent = True
|
|
self.reload()
|
|
|
|
def reload(self):
|
|
self.obj.db.last_event_time = time()
|
|
|
|
def at_repeat(self, **kwargs):
|
|
"""
|
|
Do we move or stay for another iteration?
|
|
"""
|
|
# What can keep a traveling NPC from traveling?
|
|
# What if receiving ANY message resets a timer?
|
|
elapsed_time = int(time() - self.obj.db.last_event_time)
|
|
# Time needs to be a little longer than the repeat interval.
|
|
if elapsed_time >= 20 * 60:
|
|
logger.info(f"TravelingNPC({self.obj}): Long- {elapsed_time} > {20 * 60}")
|
|
self.obj.goodbye()
|
|
self.obj.change_direction()
|
|
|
|
elif elapsed_time >= 3 * 60:
|
|
logger.info(f"TravelingNPC({self.obj}): Short- {elapsed_time} > {3 * 60}")
|
|
self.obj.next_place()
|