File size: 4,291 Bytes
18fe584
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
"""The rules of the game: reading what the model wrote, and deciding how much of it to believe."""
import re
import unicodedata

MAX_INPUT = 300
ANNOUNCE = {"money", "gold", "health"}
AMOUNTS = {"money", "gold", "coins", "health"}

CHANGE = re.compile(r"^\s*(.+?)\s*<\|(add|set)\|>\s*(-?\s*\d+)\s*$")
SPELLED_MARKER = re.compile(r"<?\|\s*(vars|choices|sep|end|add|set)\s*\|\s*>?", re.I)
STRAY_MARKER = re.compile(r"<\|[a-z_]+\|>")


META_CHOICE = re.compile(
    r"leave (a )?comments?|rate (this|the) (story|game)|end (the )?game|give (me )?(a )?rating|"
    r"back to (the )?(menu|site|story list|main menu)|return to (the )?main menu|main menu|"
    r"play again|^\W*(restart|start over|start again|comments?)\W*$|(reset|refresh) (the )?game|"
    r"previous page|^\W*go back to the (beginning|start)\W*$|"
    r"^\W*(the )?end\W*(\[[^\]]*\]|\([^)]*\))?\W*$|^\W*the end of (the|this|your) "
    r"(tale|story|game|adventure|line)\W*$|this is the end of (your|the) (adventure|story|game)",
    re.I)


MONEY_WORDS = (
    r"\b(money|coins?|gold|silver|copper|cash|dollars?|bucks|credits?|price|pay|paid|"
    r"buy|bought|sell|sold|cost|steal|stole|stolen|rob|robbed|reward|bribe|purse|wallet|"
    r"payment|fee|tip|earn|earned|spend|spent|wage|loot|treasure)\b|\$"
)
HEALTH_WORDS = (
    r"\b(health|hurt|wound|wounded|injur\w*|bleed\w*|blood|pain|heal\w*|damage\w*|hit|"
    r"stab\w*|cut|bite|bitten|burn\w*|poison\w*|potion|bandage\w*|recover\w*|weak\w*)\b"
)
EVIDENCE = {
    "money": re.compile(MONEY_WORDS, re.I),
    "gold": re.compile(MONEY_WORDS, re.I),
    "health": re.compile(HEALTH_WORDS, re.I),
}


def is_story_choice(choice):
    return not META_CHOICE.search(choice)


def is_backed_by(name, narration):
    rule = EVIDENCE.get(name)
    return rule is None or bool(rule.search(narration))


def is_possible(name, old, new):
    if name not in AMOUNTS or old is None:
        return True
    return 0 <= new <= max(1000, 10 * old)


def clean_input(text):
    text = "".join(c for c in text if c == " " or unicodedata.category(c)[0] != "C")
    text = text.replace("<|", "<").replace("|>", ">")
    return re.sub(r"\s+", " ", text).strip()[:MAX_INPUT]


def state_text(state):
    return "<|sep|>".join(f"{name}\n{value}" for name, value in state.items())


def narration_so_far(raw):
    visible = re.split(r"<\||\|", raw, maxsplit=1)[0]
    return visible[:-1] if visible.endswith("<") else visible


def without_repeats(choices):
    seen, kept = set(), []
    for choice in choices:
        key = re.sub(r"\W+", " ", choice.lower()).strip()
        if key not in seen:
            seen.add(key)
            kept.append(choice)
    return kept


def parse_changes(text):
    changes, unreadable = [], []
    for item in filter(None, text.split("<|sep|>")):
        match = CHANGE.match(item)
        if match:
            name = re.sub(r"\s+", " ", match[1]).strip().lower()
            changes.append((name, match[2], int(match[3].replace(" ", ""))))
        else:
            unreadable.append(item)
    return changes, unreadable


def parse_turn(raw):
    problems = []
    fixed = SPELLED_MARKER.sub(lambda m: f"<|{m[1].lower()}|>", raw)
    if fixed != raw:
        problems.append("a marker written as text")
        raw = fixed

    narration, _, choice_text = raw.partition("<|choices|>")
    narration, _, change_text = narration.partition("<|vars|>")

    changes, unreadable = parse_changes(change_text)
    problems += [f"unreadable change {item!r}" for item in unreadable]

    choices = [c.strip() for c in choice_text.split("<|sep|>") if c.strip()]
    choices = without_repeats([c for c in choices if is_story_choice(c)])

    stray = STRAY_MARKER.findall(narration)
    if stray:
        problems.append(f"stray markers in the narration: {stray}")
        narration = STRAY_MARKER.sub(" ", narration)
    return narration.strip(), changes, choices, problems


def apply_changes(state, changes, narration):
    for name, op, value in changes:
        old = state.get(name)
        new = value if op == "set" else (old or 0) + value
        if not is_backed_by(name, narration):
            continue
        if name in ANNOUNCE and op == "set" and value != 0:
            continue
        if is_possible(name, old, new):
            state[name] = new