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
from random import choice
from re import match
from re import match, findall
from evennia import CmdSet, create_script
from evennia.utils import delay, logger
from evennia.commands.default.muxcommand import MuxCommand
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 typeclasses.scripts import DonkeyHeadSpell
from typeclasses.drinkables import Cocktail
from utils.word_list import routput
from utils.word_list import routput, pluralize
class CmdFly(Command):
@ -88,6 +90,98 @@ class CmdMagic(Command):
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):
"""
All wizard spells are tied to a 'wand' that might be flavored.
@ -96,6 +190,7 @@ class CmdSetWand(CmdSet):
super().at_cmdset_creation()
self.add(CmdFly)
self.add(CmdMagic)
self.add(CmdMakeItem)
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.
# 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":
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)
logger.info(choose)
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 random import randint
from shutil import copyfile
from random import randint, choice
from django.conf import settings
@ -719,3 +723,116 @@ class Listener:
self.msg(f"You can't give '{gift}' to {receiver.key}... {gifts.keys()}")
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
# Emacs environement
# (setq python-shell-interpreter "/Users/howard/src/moss-n-puddles/.venv/bin/ipython")
"""
Pets
@ -16,7 +14,7 @@ from time import time
import random
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.search import search_object
@ -482,9 +480,10 @@ class WeeBeastie(Friendly, Familiar, Listener):
"Override to return a string in response to message."
owner = self.search("Dabbler")
if owner:
owner.announce_action(f"$Your() {name} purrs.")
delay(3, owner.announce_action,
f"$Your() {self.get_name()} purrs.")
else:
self.execute_cmd(f"emote /me purrs.")
delay(3, self.execute_cmd, f"emote /me purrs.")
def feed(self, feeder, item=None):
"""
@ -492,9 +491,14 @@ class WeeBeastie(Friendly, Familiar, Listener):
the character has, and go with that...
"""
# Categorize items that can be used to feed the beast:
def is_flower(item):
return (not item and feeder.has("yellow flower")) or \
(item and item.key == 'yellow flower')
def is_some(item, name):
return (not item and feeder.has(name)) or \
(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:
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",
])
if is_flower(item):
msg = f"{noun} {how_sniff} sniffs $your() << hand holding a ^>> flower. It {how_eat} eats it, and {and_then}."
edible = is_edible(item)
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)
feeder.has('yellow flower').delete()
edible.delete()
else:
msg = f"{noun} doesn't appear interested in anything you have."
@ -654,7 +659,6 @@ class BHB(Friendly):
if msg:
feeder.announce_action(msg)
# feeder.msg(msg)
def thrown_stick(self, thrower):
"""

View file

@ -1,16 +1,13 @@
#!/usr/bin/env python
from os.path import exists
from datetime import datetime
from re import split, match, sub, IGNORECASE
from shutil import copyfile
from evennia import CmdSet
from evennia.utils import logger
from commands.command import Command
from typeclasses.characters import Character
from typeclasses.objects import Listener
from typeclasses.objects import Listener, Recorder
from utils.word_list import routput
@ -101,7 +98,7 @@ class CmdSetShrubSay(CmdSet):
self.add(CmdShrubSay)
class Shrub(Puppet):
class Shrub(Puppet, Recorder):
"""
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.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):
"""
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.scoring import Scores
from typeclasses.consumables import Litterable
from typeclasses.objects import Object
from typeclasses.objects import Object, Recorder
from typeclasses.scripts import KnockScript
@ -1142,3 +1142,16 @@ class GlobalAlarmClock(Object):
if start:
chan.msg(start)
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:
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