Add POV template engine for combat messages
This commit is contained in:
parent
292557e5fd
commit
2b21257d26
2 changed files with 299 additions and 0 deletions
106
src/mudlib/render/pov.py
Normal file
106
src/mudlib/render/pov.py
Normal 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
193
tests/test_pov.py
Normal 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"
|
||||||
Loading…
Reference in a new issue