File size: 4,931 Bytes
2803d7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import re
from dataclasses import dataclass

from agents.master.base import SUPPORTED_DIRECTIONS

_TOKEN_RE = re.compile(r"^[a-z0-9]+(?: [a-z0-9]+)*$")
_BANNED_OBJECT_TOKENS = {"a", "an", "the"}


@dataclass(frozen=True)
class CliCommandAst:
    kind: str
    normalized_command: str
    arguments: tuple[str, ...] = ()


@dataclass(frozen=True)
class CliCommandParseResult:
    valid: bool
    normalized_command: str | None = None
    ast: CliCommandAst | None = None
    error: str | None = None


def parse_cli_command(raw_command: str) -> CliCommandParseResult:
    normalized = normalize_cli_command(raw_command)
    if not normalized:
        return CliCommandParseResult(valid=False, error="Command must not be empty.")

    if normalized in {"look", "inventory", "wait"}:
        return _ok(normalized, normalized)

    if normalized in SUPPORTED_DIRECTIONS:
        return _ok("move", f"go {normalized}", normalized)
    if normalized.startswith("go "):
        direction = normalized[3:].strip()
        if direction in SUPPORTED_DIRECTIONS:
            return _ok("move", f"go {direction}", direction)
        return CliCommandParseResult(valid=False, error="Unknown direction.")

    if match := re.fullmatch(r"look in (?P<object>.+)", normalized):
        object_text = match.group("object").strip()
        return _object_result("look_in", normalized, object_text)
    if match := re.fullmatch(r"take (?P<object>.+) from (?P<source>.+)", normalized):
        return _two_object_result("take_from", normalized, match.group("object"), match.group("source"))

    one_target_patterns = {
        "open": r"open (?P<object>.+)",
        "read": r"read (?P<object>.+)",
        "talk": r"talk (?P<object>.+)",
        "examine": r"examine (?P<object>.+)",
    }
    for kind, pattern in one_target_patterns.items():
        if match := re.fullmatch(pattern, normalized):
            object_text = match.group("object").strip()
            return _object_result(kind, normalized, object_text)
    if match := re.fullmatch(r"take (?P<object>.+)", normalized):
        object_text = match.group("object").strip()
        return _object_result("take", normalized, object_text)
    if match := re.fullmatch(r"unlock (?P<object>.+) with (?P<tool>.+)", normalized):
        return _two_object_result("unlock", normalized, match.group("object"), match.group("tool"))
    if match := re.fullmatch(r"use (?P<object>.+) on (?P<target>.+)", normalized):
        return _two_object_result("use", normalized, match.group("object"), match.group("target"))
    if match := re.fullmatch(r"combine (?P<object>.+) with (?P<target>.+)", normalized):
        return _two_object_result("combine", normalized, match.group("object"), match.group("target"))
    if match := re.fullmatch(r"give (?P<object>.+) to (?P<target>.+)", normalized):
        return _two_object_result("give", normalized, match.group("object"), match.group("target"))

    if match := re.fullmatch(r"submit (?P<answer>[a-z0-9]+(?: [a-z0-9]+)*)", normalized):
        answer = match.group("answer").strip()
        return _ok("submit", normalized, answer)

    return CliCommandParseResult(valid=False, error="Command does not match the strict CLI grammar.")


def normalize_cli_command(raw_command: str) -> str:
    return re.sub(r"\s+", " ", raw_command.strip().lower())


def _object_result(kind: str, normalized_command: str, object_text: str) -> CliCommandParseResult:
    object_error = _validate_object_text(object_text)
    if object_error is not None:
        return CliCommandParseResult(valid=False, error=object_error)
    return _ok(kind, normalized_command, object_text)


def _two_object_result(kind: str, normalized_command: str, first: str, second: str) -> CliCommandParseResult:
    first_error = _validate_object_text(first)
    if first_error is not None:
        return CliCommandParseResult(valid=False, error=first_error)
    second_error = _validate_object_text(second)
    if second_error is not None:
        return CliCommandParseResult(valid=False, error=second_error)
    return _ok(kind, normalized_command, first.strip(), second.strip())


def _validate_object_text(value: str) -> str | None:
    candidate = value.strip()
    if not candidate:
        return "Command target must not be empty."
    if not _TOKEN_RE.fullmatch(candidate):
        return "Command targets must use lowercase letters, numbers, and spaces only."
    if any(token in _BANNED_OBJECT_TOKENS for token in candidate.split()):
        return "Strict CLI commands must use exact parser-safe object names without articles."
    return None


def _ok(kind: str, normalized_command: str, *arguments: str) -> CliCommandParseResult:
    return CliCommandParseResult(
        valid=True,
        normalized_command=normalized_command,
        ast=CliCommandAst(kind=kind, normalized_command=normalized_command, arguments=arguments),
    )