Add POV template engine for combat messages

This commit is contained in:
Jared Miller 2026-02-13 23:05:19 -05:00
parent 292557e5fd
commit 2b21257d26
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 299 additions and 0 deletions

106
src/mudlib/render/pov.py Normal file
View file

@ -0,0 +1,106 @@
"""POV template engine for combat messages."""
import re
from typing import Any
def render_pov(
template: str,
viewer: Any | None,
attacker: Any | None,
defender: Any | None,
) -> str:
"""Render a combat message template from a specific viewer's POV.
Args:
template: Message template with {attacker}, {defender}, and
contextual tags like {s}, {es}, {y|ies}, {his}, {him}
viewer: Entity viewing the message (determines "You" vs name)
attacker: Attacking entity
defender: Defending entity
Returns:
Rendered message with appropriate perspective substitutions
Contextual tags apply to the most recently mentioned entity:
- {s} "" for 2nd person, "s"/"es" for 3rd (smart conjugation)
- {es} "" for 2nd person, "es" for 3rd person
- {y|ies} left form for 2nd person, right form for 3rd person
- {his} "your" for 2nd person, "his" for 3rd person
- {him} "you" for 2nd person, "him" for 3rd person
"""
if not template:
return template
# Track whether the last entity mentioned was "You" (2nd person)
last_was_you = False
# Pattern to match all tags
tag_pattern = re.compile(r"\{(attacker|defender|s|es|his|him|[^}]*\|[^}]*)\}")
# Process template character by character, building result
result = []
pos = 0
for match in tag_pattern.finditer(template):
# Add text before this tag
result.append(template[pos : match.start()])
tag = match.group(0)
tag_name = match.group(1)
# Entity tags
if tag_name in ("attacker", "defender"):
entity = attacker if tag_name == "attacker" else defender
if entity is viewer:
last_was_you = True
result.append("You")
else:
last_was_you = False
result.append(entity.name if entity else "")
# Contextual verb conjugation tags
elif tag == "{s}":
if last_was_you:
result.append("")
else:
# Check previous character for smart conjugation
prev_text = "".join(result)
if (
prev_text
and prev_text[-1:] in "sxz"
or (len(prev_text) >= 2 and prev_text[-2:] in ("ch", "sh"))
):
result.append("es")
else:
result.append("s")
elif tag == "{es}":
result.append("" if last_was_you else "es")
elif tag == "{his}":
result.append("your" if last_was_you else "his")
elif tag == "{him}":
# Check if "self" follows to form reflexive pronoun
remaining = template[match.end() :]
if remaining.startswith("self"):
result.append("your" if last_was_you else "him")
else:
result.append("you" if last_was_you else "him")
elif "|" in tag:
# Handle {a|b} pattern
content = tag[1:-1] # Strip { and }
left, right = content.split("|", 1)
result.append(left if last_was_you else right)
else:
result.append(tag)
pos = match.end()
# Add remaining text after last tag
result.append(template[pos:])
return "".join(result)

193
tests/test_pov.py Normal file
View file

@ -0,0 +1,193 @@
"""Tests for POV template engine."""
from dataclasses import dataclass
import pytest
from mudlib.render.pov import render_pov
@dataclass
class MockEntity:
"""Simple entity for testing."""
name: str
@pytest.fixture
def jared():
"""Attacker entity."""
return MockEntity(name="Jared")
@pytest.fixture
def goku():
"""Defender entity."""
return MockEntity(name="Goku")
@pytest.fixture
def vegeta():
"""Bystander entity."""
return MockEntity(name="Vegeta")
def test_attacker_pov_basic(jared, goku):
"""Attacker sees 'You' for self, defender name for target."""
template = "{attacker} hit{s} {defender}"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You hit Goku"
def test_defender_pov_basic(jared, goku):
"""Defender sees attacker name, 'You' for self."""
template = "{attacker} hit{s} {defender}"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "Jared hits You"
def test_bystander_pov_basic(jared, goku, vegeta):
"""Bystander sees both entity names."""
template = "{attacker} hits {defender}"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared hits Goku"
def test_verb_s_conjugation_you(jared, goku):
"""Verb with {s} after You gets no suffix."""
template = "{attacker} punch{s} {defender}"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You punch Goku"
def test_verb_s_conjugation_third_person(jared, goku, vegeta):
"""Verb with {s} after name gets 's' suffix."""
template = "{attacker} punch{s} {defender}"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared punches Goku"
def test_verb_es_conjugation_you(jared, goku):
"""Verb with {es} after You gets no suffix."""
template = "{attacker} lurch{es} forward"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You lurch forward"
def test_verb_es_conjugation_third_person(jared, goku, vegeta):
"""Verb with {es} after name gets 'es' suffix."""
template = "{attacker} lurch{es} forward"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared lurches forward"
def test_irregular_verb_y_ies_you(jared, goku):
"""Irregular verb {y|ies} after You uses left form."""
template = "{attacker} parr{y|ies} the blow"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You parry the blow"
def test_irregular_verb_y_ies_third_person(jared, goku, vegeta):
"""Irregular verb {y|ies} after name uses right form."""
template = "{attacker} parr{y|ies} the blow"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared parries the blow"
def test_pronoun_his_you(jared, goku):
"""{his} after You becomes 'your'."""
template = "{attacker} raise{s} {his} fist"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You raise your fist"
def test_pronoun_his_third_person(jared, goku, vegeta):
"""{his} after name becomes 'his'."""
template = "{attacker} raise{s} {his} fist"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared raises his fist"
def test_pronoun_him_you(jared, goku):
"""{him} after You becomes 'you'."""
template = "{attacker} brace{s} {him}self"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You brace yourself"
def test_pronoun_him_third_person(jared, goku, vegeta):
"""{him} after name becomes 'him'."""
template = "{attacker} brace{s} {him}self"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared braces himself"
def test_mixed_template_attacker_pov(jared, goku):
"""Complex template with multiple substitutions, attacker POV."""
template = "{attacker} slam{s} {his} fist into {defender}"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You slam your fist into Goku"
def test_mixed_template_defender_pov(jared, goku):
"""Complex template with multiple substitutions, defender POV."""
template = "{attacker} slam{s} {his} fist into {defender}"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "Jared slams his fist into You"
def test_mixed_template_bystander_pov(jared, goku, vegeta):
"""Complex template with multiple substitutions, bystander POV."""
template = "{attacker} slam{s} {his} fist into {defender}"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Jared slams his fist into Goku"
def test_defender_contextual_tags(jared, goku):
"""Contextual tags apply to defender when they precede them."""
template = "{defender} brace{s} {him}self against {attacker}"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "You brace yourself against Jared"
def test_defender_contextual_third_person(jared, goku, vegeta):
"""Contextual tags apply to defender in third person."""
template = "{defender} brace{s} {him}self against {attacker}"
result = render_pov(template, viewer=vegeta, attacker=jared, defender=goku)
assert result == "Goku braces himself against Jared"
def test_plain_text_passthrough():
"""Template with no POV tags passes through unchanged."""
template = "The battle rages on!"
result = render_pov(template, viewer=None, attacker=None, defender=None)
assert result == "The battle rages on!"
def test_you_capitalization_mid_sentence(jared, goku):
"""'You' is capitalized even when not at start of sentence."""
template = "The blow strikes {defender} hard"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "The blow strikes You hard"
def test_multiple_entity_references(jared, goku):
"""Context switches between entities correctly."""
template = "{attacker} throw{s} a punch at {defender}, but {defender} dodge{s} it"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "Jared throws a punch at You, but You dodge it"
def test_multiple_entity_references_attacker_pov(jared, goku):
"""Context switches correctly from attacker POV."""
template = "{attacker} throw{s} a punch at {defender}, but {defender} dodg{es} it"
result = render_pov(template, viewer=jared, attacker=jared, defender=goku)
assert result == "You throw a punch at Goku, but Goku dodges it"
def test_you_capitalized_after_comma(jared, goku):
"""'You' stays capitalized after comma/conjunction."""
template = "{attacker} strikes, and {defender} fall{s}"
result = render_pov(template, viewer=goku, attacker=jared, defender=goku)
assert result == "Jared strikes, and You fall"