Create a recorder

These objects record the goings-on of a room into an HTML file.
This commit is contained in:
Howard Abrams 2026-02-27 20:09:04 -08:00
parent b33f6ca7df
commit be13c8cb6e
8 changed files with 329 additions and 107 deletions

View file

@ -1,17 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python
from random import choice from random import choice
from re import match from re import match, findall
from evennia import CmdSet, create_script from evennia import CmdSet, create_script
from evennia.utils import delay, logger from evennia.utils import delay, logger
from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote from evennia.contrib.rpg.rpsystem import send_emote
from evennia.prototypes.spawner import spawn
from evennia.utils.utils import int2str
from .command import Command from .command import Command
from typeclasses.scripts import DonkeyHeadSpell from typeclasses.scripts import DonkeyHeadSpell
from typeclasses.drinkables import Cocktail from typeclasses.drinkables import Cocktail
from utils.word_list import routput from utils.word_list import routput, pluralize
class CmdFly(Command): class CmdFly(Command):
@ -88,6 +90,98 @@ class CmdMagic(Command):
wizard.spell_sequence(None, msgs, wizard.db.magic_delay or 3) wizard.spell_sequence(None, msgs, wizard.db.magic_delay or 3)
class CmdMakeItem(Command):
"""
Create one or more items from thin air.
Usage:
make [ number ] name [ description ]
Where 'effects' is a statement of what happens after the magic erupts.
Note that you can have multiple effects separated by two semicolons.
You can tailor the effects to your character with the following:
- |y$you()|n: Replaced by "you" and your name for others, like "old gnome"
- |y$your()|n: Replaced by "your" and your possessive name for others, like "old gnome's"
- |y$conj(verb)|n: Replaced by "verb" for you, and "verbs" for others.
- |y$pron(you)|n: Replaced by "you" for you, but "he" or "she" for others.
- |y$pron(your)|n: Replaced by "your" for you, but "his" or "her" for others.
While flexible, you would use this complex replacement in either
|gnicks|n or as part of a standard magical prefix, by setting the
property:
@set self/magic_msg = "$You() $conj(shake) $pron(your) necklace of bones!"
"""
key = "make"
locks = "cmd:holds()"
def parse(self):
"""
Allows the following phrases:
candy
2 candy Piece of Turkish Delight.
candy Piece of Turkish Delight.
"turkish delight" Piece of Turkish Delight.
4 "turkish delight" Piece of Turkish Delight.
"""
pattern = r'"(.*?)"|(\S+)'
matches = findall(pattern, self.args.strip())
try:
self.item_number = int(matches[0][0] or matches[0][1])
self.item_name = matches[1][0] or matches[1][1]
start = 2
except ValueError:
self.item_number = 1
self.item_name = matches[0][0] or matches[0][1]
start = 1
words = [m[0] or m[1] for m in matches[start:]]
self.item_desc = " ".join(words)
def func(self):
"""
Call the 'do_magic' method on the caller.
"""
wizard = self.caller
msgs = wizard.db.make_msg or wizard.db.magic_msg
if msgs:
msgs = msgs.split(';;')
else:
msgs = [
"$You() $conj(snap) $pron(your) fingers.",
]
if self.item_number == 1:
if match(r"^[aeiou]"):
name = f"an {self.item_name}"
else:
name = f"a {self.item_name}"
else:
name = int2str(self.item_number) + " " + \
pluralize(self.item_name)
# from evennia.utils.utils import int2str ; print(int2str("2"))
msgs = msgs + [ name + " appear in $pron(your) hand." ]
wizard.spell_sequence(None, msgs, wizard.db.magic_delay or 3)
# FIXME So that the cat will eat it and it can't be littered:
if not self.item_desc or self.item_desc == "":
self.item_desc = f"Conjured by {wizard.get_name()}."
for _ in range(self.item_number):
item = spawn({
"typeclass": "typeclasses.consumables.Litterable",
"key": self.item_name,
"desc": self.item_desc
})[0]
item.location = wizard
class CmdSetWand(CmdSet): class CmdSetWand(CmdSet):
""" """
All wizard spells are tied to a 'wand' that might be flavored. All wizard spells are tied to a 'wand' that might be flavored.
@ -96,6 +190,7 @@ class CmdSetWand(CmdSet):
super().at_cmdset_creation() super().at_cmdset_creation()
self.add(CmdFly) self.add(CmdFly)
self.add(CmdMagic) self.add(CmdMagic)
self.add(CmdMakeItem)
class CmdMakeCocktail(MuxCommand): class CmdMakeCocktail(MuxCommand):

View file

@ -0,0 +1,48 @@
<html>
<head>
<title>Wyldwood Bar Transcript</title>
<style type="text/css" media="screen">
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Goudy+Bookletter+1911&family=Merienda:wght@300..900&display=swap');
html {
background-color: #FCF5E5;
color: #26201a;
font-family: "Goudy Bookletter 1911", serif;
font-weight: 400;
font-style: normal;
font-size: 21px;
line-height: 1.4em;
padding: 40px;
}
p {
margin: auto;
margin-bottom: .5em;
}
blockquote {
font-style: italic;
}
blockquote i {
font-weight: bold;
}
.special {
margin: 20px;
padding: 2px 20px;
border: 2px solid brown;
border-radius: 12px;
}
.special .title {
font-variant-caps: small-caps;
}
.heavy {
font-weight: 700;
}
</style>
</head>
<body>
<h1>Another Cozy Day</h1>

View file

@ -493,9 +493,15 @@ class Character(Object, GenderCharacter, ContribRPCharacter):
""" """
# All this does is add the character's gender to the message. # All this does is add the character's gender to the message.
# So $pron(you,op) will render "you" or "him" # So $pron(you,op) will render "you" or "him"
newmsg = sub(r"\$pron\((.*?),(.*?)\)", f"$pron(\\1,\\2 {self.db.gender})", message) newmsg = sub(r"\$pron\(([^\)]*?),([^\)]*?)\)",
f"$pron(\\1,\\2 {self.db.gender})",
message)
# While $pron(you) will render "you" and "he": # While $pron(you) will render "you" and "he":
newmsg = sub(r"\$pron\(([^,]*?)\)", f"$pron(\\1, {self.db.gender})", newmsg) newmsg = sub(r"\$pron\(([^,\)]*?)\)",
f"$pron(\\1, {self.db.gender})",
newmsg)
choose = choices(newmsg) choose = choices(newmsg)
logger.info(choose) logger.info(choose)
self.location.msg_contents(f"{choose}", from_obj=self, exclude=exclude) self.location.msg_contents(f"{choose}", from_obj=self, exclude=exclude)

View file

@ -9,8 +9,12 @@ with a location in the game world (like Characters, Rooms, Exits).
""" """
from datetime import datetime
from os import makedirs
from os.path import dirname, exists
from re import split, match, sub, IGNORECASE from re import split, match, sub, IGNORECASE
from random import randint from shutil import copyfile
from random import randint, choice
from django.conf import settings from django.conf import settings
@ -719,3 +723,116 @@ class Listener:
self.msg(f"You can't give '{gift}' to {receiver.key}... {gifts.keys()}") self.msg(f"You can't give '{gift}' to {receiver.key}... {gifts.keys()}")
return None return None
class Recorder(Object):
"""Mixin for recording the events in the current room.
Classes that include this mixin, need, in their msg()
should call 'record_msg', for instance:
def msg(self, text=None, from_obj=None, session=None, **kwargs):
self.record_msg(text, from_obj)
Objects should set the following properties:
@set scribe/transcripts_path = "transcripts/%Y-%m-%d.html"
@set scribe/header_file = "transcripts/header-template.html"
The 'directory' is really just a prefix to a file that includes
today's date.
"""
def capitalize_msg(self, text, msg_type=None):
"""
If text is lowercase, capitalize it.
Maybe prepend a 'The' to the front.
"""
logger.info(f"capitalize: {text} / {msg_type}")
if msg_type and msg_type in ('traverse', 'teleport'):
if match(r"^(\|[A-z])?[aeiou]", text):
return "An " + text
else:
return "A " + text
# If the line is colored and lowercase, it probably assumes a
# character's sdesc, so let's add a 'the' to the front:
# Do we need to only do this on 'say'?
elif match(r"^\|[mb][a-z]", text):
return "The " + text
elif match(r"^(\|[A-z])[a-z]", text) and len(text) > 2:
return text[0:2] + text[2].upper() + text[3:]
elif match(r"^[a-z]", text) and len(text) > 2:
return text[0].upper() + text[1:]
return text
def to_html(self, text, msg_type=None):
"""
Convert the 'text' to an HTML formatted string.
"""
# Yellow text should be italics:
text = sub(r"\|y(.*?)\|n", '<i>\\1</i>', text)
# Bold and tag the titles:
text = sub(r"\|[cb](.*?)\|n", '<b class="title">\\1</b>', text)
# Bold anything white:
text = sub(r"\|w(.*?)\|n", '<b class="heavy">\\1</b>', text)
# Remove the rest:
text = sub(r"\|[A-z]", '', text)
# Remove initial or final carriage returns:
text = sub(r"^\n", '', text)
text = sub(r"\n$", '', text)
# Convert the carriage returns:
text = sub(r"\n", ' <br/>\n', text)
if msg_type and msg_type == 'look':
text = text[0].upper() + text[1:]
return f"<div class=\"special\"><p>{text}</p></div>\n"
if msg_type and msg_type == 'note':
return f"<blockquote>{text}</blockquote>"
return f"<p>{text}</p>\n"
def find_actor(self, text, default=None):
"""
Extract the character performing the action from the text.
"""
if default:
return default
m = match(r".*\|b(.*?)\|n.*", text)
if m:
return m.group(1)
def record_msg(self, text, actor=None):
msg_type = None
if isinstance(text, tuple):
if text[1] and isinstance(text[1], dict):
msg_type = text[1]['type']
msg = self.capitalize_msg(text[0], msg_type)
else:
msg = self.capitalize_msg(str(text))
# Make things a little more interesting by substituting the
# name...
names = actor and actor.db.alt_names
if names:
logger.info("lets change this")
msg = sub(f"^{actor.sdesc.get()}", choice(names),
msg, flags=IGNORECASE)
actor = self.find_actor(msg, actor)
msg = self.to_html(msg, msg_type)
filename = datetime.today().strftime(self.db.transcripts_path)
if not exists(filename):
makedirs(dirname(filename), exist_ok=True)
copyfile(self.db.header_file, filename)
with open(filename, "a") as myfile:
myfile.write(msg)
# Follow-up Actions ...
if match(r".* arrives .*", msg) and actor and \
msg_type in ('traverse', 'teleport'):
self.execute_cmd(f"look {actor}")

View file

@ -1,6 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# Emacs environement
# (setq python-shell-interpreter "/Users/howard/src/moss-n-puddles/.venv/bin/ipython")
""" """
Pets Pets
@ -16,7 +14,7 @@ from time import time
import random import random
from evennia import TICKER_HANDLER from evennia import TICKER_HANDLER
from evennia.utils import logger from evennia.utils import logger, delay
from evennia.utils.gametime import schedule from evennia.utils.gametime import schedule
from evennia.utils.search import search_object from evennia.utils.search import search_object
@ -482,9 +480,10 @@ class WeeBeastie(Friendly, Familiar, Listener):
"Override to return a string in response to message." "Override to return a string in response to message."
owner = self.search("Dabbler") owner = self.search("Dabbler")
if owner: if owner:
owner.announce_action(f"$Your() {name} purrs.") delay(3, owner.announce_action,
f"$Your() {self.get_name()} purrs.")
else: else:
self.execute_cmd(f"emote /me purrs.") delay(3, self.execute_cmd, f"emote /me purrs.")
def feed(self, feeder, item=None): def feed(self, feeder, item=None):
""" """
@ -492,9 +491,14 @@ class WeeBeastie(Friendly, Familiar, Listener):
the character has, and go with that... the character has, and go with that...
""" """
# Categorize items that can be used to feed the beast: # Categorize items that can be used to feed the beast:
def is_flower(item): def is_some(item, name):
return (not item and feeder.has("yellow flower")) or \ return (not item and feeder.has(name)) or \
(item and item.key == 'yellow flower') (item and item.key == name)
def is_edible(item):
return is_some(item, "yellow flower") or \
is_some(item, "candy") or \
is_some(item, "turkish delight")
# Based on the reaction to the feeder, the adjectives may alter: # Based on the reaction to the feeder, the adjectives may alter:
noun = "The " + random.choice(["wee", "furry", "white", "adorable"]) + " beastie" noun = "The " + random.choice(["wee", "furry", "white", "adorable"]) + " beastie"
@ -521,10 +525,11 @@ class WeeBeastie(Friendly, Familiar, Listener):
"rubs its wee widdle head under $pron(you,op) chin in gratitude", "rubs its wee widdle head under $pron(you,op) chin in gratitude",
]) ])
if is_flower(item): edible = is_edible(item)
msg = f"{noun} {how_sniff} sniffs $your() << hand holding a ^>> flower. It {how_eat} eats it, and {and_then}." if edible:
msg = f"{noun} {how_sniff} sniffs $your() << hand holding a ^>> {edible}. It {how_eat} eats it, and {and_then}."
self.adjust_character(feeder, 100) self.adjust_character(feeder, 100)
feeder.has('yellow flower').delete() edible.delete()
else: else:
msg = f"{noun} doesn't appear interested in anything you have." msg = f"{noun} doesn't appear interested in anything you have."
@ -654,7 +659,6 @@ class BHB(Friendly):
if msg: if msg:
feeder.announce_action(msg) feeder.announce_action(msg)
# feeder.msg(msg)
def thrown_stick(self, thrower): def thrown_stick(self, thrower):
""" """

View file

@ -1,16 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
from os.path import exists
from datetime import datetime
from re import split, match, sub, IGNORECASE from re import split, match, sub, IGNORECASE
from shutil import copyfile
from evennia import CmdSet from evennia import CmdSet
from evennia.utils import logger from evennia.utils import logger
from commands.command import Command from commands.command import Command
from typeclasses.characters import Character from typeclasses.characters import Character
from typeclasses.objects import Listener from typeclasses.objects import Listener, Recorder
from utils.word_list import routput from utils.word_list import routput
@ -101,7 +98,7 @@ class CmdSetShrubSay(CmdSet):
self.add(CmdShrubSay) self.add(CmdShrubSay)
class Shrub(Puppet): class Shrub(Puppet, Recorder):
""" """
The 'Shrub' has its own way of communicating. The 'Shrub' has its own way of communicating.
""" """
@ -145,89 +142,6 @@ class Shrub(Puppet):
self.db.inside = f"The chalkboard reads: |w{new_text}|n" self.db.inside = f"The chalkboard reads: |w{new_text}|n"
self.record_msg(self.db.inside) self.record_msg(self.db.inside)
def find_actor(self, text, default=None):
"""
Extract the character performing the action from the text.
"""
if default:
return default
m = match(r".*\|b(.*?)\|n.*", text)
if m:
return m.group(1)
def capitalize_msg(self, text, msg_type=None):
"""
If text is lowercase, capitalize it.
Maybe prepend a 'The' to the front.
"""
logger.info(f"capitalize: {text} / {msg_type}")
if msg_type and msg_type in ('traverse', 'teleport'):
if match(r"^(\|[A-z])?[aeiou]", text):
return "An " + text
else:
return "A " + text
# If the line is colored and lowercase, it probably assumes a
# character's sdesc, so let's add a 'the' to the front:
# Do we need to only do this on 'say'?
elif match(r"^\|[mb][a-z]", text):
return "The " + text
elif match(r"^(\|[A-z])[a-z]", text) and len(text) > 2:
return text[0:2] + text[2].upper() + text[3:]
elif match(r"^[a-z]", text) and len(text) > 2:
return text[0].upper() + text[1:]
return text
def to_html(self, text, msg_type=None):
"""
Convert the 'text' to an HTML formatted string.
"""
# Yellow text should be italics:
text = sub(r"\|y(.*?)\|n", '<i>\\1</i>', text)
# Bold and tag the titles:
text = sub(r"\|[cb](.*?)\|n", '<b class="title">\\1</b>', text)
# Bold anything white:
text = sub(r"\|w(.*?)\|n", '<b class="heavy">\\1</b>', text)
# Remove the rest:
text = sub(r"\|[A-z]", '', text)
# Remove initial or final carriage returns:
text = sub(r"^\n", '', text)
text = sub(r"\n$", '', text)
# Convert the carriage returns:
text = sub(r"\n", ' <br/>\n', text)
if msg_type and msg_type == 'look':
text = text[0].upper() + text[1:]
return f"<div class=\"special\"><p>{text}</p></div>\n"
if msg_type and msg_type == 'note':
return f"<blockquote>{text}</blockquote>"
return f"<p>{text}</p>\n"
def record_msg(self, text, actor=None):
msg_type = None
if isinstance(text, tuple):
if text[1] and isinstance(text[1], dict):
msg_type = text[1]['type']
msg = self.capitalize_msg(text[0], msg_type)
else:
msg = self.capitalize_msg(str(text))
actor = self.find_actor(msg, actor)
msg = self.to_html(msg, msg_type)
filename = datetime.today().strftime('transcripts/%Y-%m-%d.html')
if not exists(filename):
copyfile("transcripts/header-template.html", filename)
with open(filename, "a") as myfile:
myfile.write(msg)
# Follow-up Actions ...
if match(r".* arrives .*", msg) and actor and \
msg_type in ('traverse', 'teleport'):
self.execute_cmd(f"look {actor}")
def msg(self, text=None, from_obj=None, session=None, **kwargs): def msg(self, text=None, from_obj=None, session=None, **kwargs):
""" """
Record everything that happens in the room for a transcript. Record everything that happens in the room for a transcript.

View file

@ -25,7 +25,7 @@ from commands.wizards import CmdSetWand
from utils.word_list import routput, choices, paragraph from utils.word_list import routput, choices, paragraph
from utils.scoring import Scores from utils.scoring import Scores
from typeclasses.consumables import Litterable from typeclasses.consumables import Litterable
from typeclasses.objects import Object from typeclasses.objects import Object, Recorder
from typeclasses.scripts import KnockScript from typeclasses.scripts import KnockScript
@ -936,7 +936,7 @@ class BagofJunk(Object):
self.db.latest_item = item self.db.latest_item = item
where = self.db._sdesc or self.name where = self.db._sdesc or self.name
msg = routput(f"$You() << $conj(scrounge) ^ $conj(rummage) ^ $conj(fish) ^ $conj(rifle) ^ $conj(put) $pron(your) hand >> << around ^ >> in $pron(your) {where}, and << $conj(pull) out ^ $conj(find) ^ $conj(stare) at >> |w{item}|n.") msg = routput(f"$You() << $conj(scrounge) ^ $conj(rummage) ^ $conj(fish) ^ $conj(rifle) ^ $conj(put) $pron(your) hand >> << around ^ >> in $pron(your) {where}, and << $conj(pull) out ^ $conj(find) ^ $conj(stare) at >>|w{item}|n.")
owner.announce_action(msg) owner.announce_action(msg)
def do_keep(self, keeper, description=None): def do_keep(self, keeper, description=None):
@ -1142,3 +1142,16 @@ class GlobalAlarmClock(Object):
if start: if start:
chan.msg(start) chan.msg(start)
delay(seconds, chan.msg, finish) delay(seconds, chan.msg, finish)
class Scribe(Recorder):
"""
Probably invisible script that records room events.
Set the following properties:
@set scribe/directory = "transcripts"
@set scribe/header = "transcripts/header-template.html"
"""
def msg(self, text=None, from_obj=None, session=None, **kwargs):
self.record_msg(text, from_obj)

View file

@ -147,3 +147,28 @@ def choices(text, *substitutions):
if text: if text:
return routput(choice(text), *substitutions) return routput(choice(text), *substitutions)
def pluralize(noun):
"""Convert a singular noun to its plural form."""
# Basic pluralization rules
# Change 'y' to 'ies' if preceded by a consonant
if noun.endswith('y') and noun[-2] not in 'aeiou':
return noun[:-1] + 'ies'
# Add 'es' for words ending in 's', 'x', 'z', 'ch', or 'sh'
elif noun.endswith('s') or noun.endswith('x') or \
noun.endswith('z') or noun.endswith('ch') or \
noun.endswith('sh'):
return noun + 'es'
# Default case for regular nouns
else:
return noun + 's'
# print(pluralize("dog")) # Outputs: dogs
# print(pluralize("candy")) # Outputs: candies
# print(pluralize("box")) # Outputs: boxes
# print(pluralize("bush")) # Outputs: bushes
# print(pluralize("city")) # Outputs: cities