File size: 5,715 Bytes
e68cec5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""μΉ΄λ₯΄λ°€λΌ 챕터1 ν”Œλ ˆμ΄ CLI β€” 리딩 루프 + κ°œμž… 포켓 (M4 톡합).

μ‚¬μš©λ²•:
  .venv/bin/python scripts/play.py                    # λŒ€ν™”ν˜• (포켓은 LLM ν‚€ 있으면 μ±„νŒ…)
  .venv/bin/python scripts/play.py --policy trust --quiet    # μžλ™ ν”Œλ ˆμ΄, μ—”λ”© 확인
  .venv/bin/python scripts/play.py --chat             # 포켓 μžμœ μ±„νŒ… κ°•μ œ (OPENAI_API_KEY ν•„μš”)
  .venv/bin/python scripts/play.py --no-chat          # 포켓 μ±„νŒ… 끄기 (μ„ νƒμ§€λ§Œ)

포켓 μ±„νŒ…: 선택지 μΉ΄λ“œμ—μ„œ 번호 λŒ€μ‹  자유 ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•˜λ©΄ μΉ΄λ₯΄λ°€λΌμ™€ λŒ€ν™”κ°€ 열리고,
director 수렴(λ˜λŠ” ν„΄ μƒν•œ) ν›„ μ„ νƒμ§€λ‘œ λ³΅κ·€ν•œλ‹€.
"""

from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from engine.repositories.content import ContentRepository  # noqa: E402
from engine.services.story import StepResult, StoryEngine  # noqa: E402

DIM = "\033[2m"
BOLD = "\033[1m"
RESET = "\033[0m"


def render(step: StepResult, quiet: bool):
    if quiet or step.card is None:
        return
    c = step.card
    print(f"\n{BOLD}── {c.id} Β· {c.title} ──{RESET}")
    for b in step.narration:
        if b.type == "dialogue_center":
            print(f"\n{BOLD}    ❝ {b.text} ❞{RESET}\n")
        elif b.type == "character_dialogue":
            print(f'  {b.speaker}: "{b.text}"')
        elif b.type == "stage_direction":
            print(f"{DIM}  ({b.text}){RESET}")
        else:
            print("\n" + b.text)


async def pocket_chat(pocket_service, engine: StoryEngine, card_id: str, first_line: str):
    """κ°œμž… 포켓 μžμœ μ±„νŒ… β€” 수렴/ν„΄ μƒν•œκΉŒμ§€ 돌고 μˆ¨μ€ μƒνƒœλ₯Ό μ»€λ°‹ν•œλ‹€."""
    sid = await pocket_service.open(card_id, engine.state)
    line = first_line
    while True:
        turn = await pocket_service.run_turn(sid, line)
        print(f"\n  {BOLD}μΉ΄λ₯΄λ°€λΌ{RESET}: {turn.reply}")
        if turn.converged:
            print(f"{DIM}  (μž₯면이 쑰용히 λ‹€μŒμœΌλ‘œ κΈ°μš΄λ‹€ β€” {turn.reason}){RESET}")
            break
        line = input("λ‚˜(둜라) > ").strip()
        if not line:
            break
    log = await pocket_service.close(sid, engine.state)
    return log


def pick_or_chat(step: StepResult, policy: str, chat_enabled: bool):
    """선택지 μž…λ ₯ λ˜λŠ” 자유 ν…μŠ€νŠΈ(포켓 μ±„νŒ… μ§„μž…). λ°˜ν™˜: ('choice', idx) | ('chat', text)."""
    choices = step.choices
    if policy != "ask":
        for i, ch in enumerate(choices):
            if ch.axis == policy:
                return "choice", i
        for i, ch in enumerate(choices):
            if ch.axis in (None, "neutral"):
                return "choice", i
        return "choice", 0
    for i, ch in enumerate(choices):
        print(f"  [{i + 1}] {ch.label}")
    if chat_enabled:
        print(f"{DIM}  (번호 λŒ€μ‹  ν•˜κ³  싢은 말을 μ“°λ©΄ μΉ΄λ₯΄λ°€λΌμ™€ λŒ€ν™”ν•©λ‹ˆλ‹€){RESET}")
    while True:
        raw = input("선택 > ").strip()
        if raw.isdigit() and 1 <= int(raw) <= len(choices):
            return "choice", int(raw) - 1
        if raw and chat_enabled:
            return "chat", raw
        if raw and not chat_enabled:
            print(f"{DIM}  (포켓 μ±„νŒ… 꺼짐 β€” 번호λ₯Ό μž…λ ₯ν•˜μ„Έμš”){RESET}")


async def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--policy", default="ask", choices=["ask", "trust", "doubt", "neutral"])
    ap.add_argument("--quiet", action="store_true")
    ap.add_argument("--content", default="content")
    ap.add_argument("--chat", action="store_true", help="포켓 μžμœ μ±„νŒ… κ°•μ œ ν™œμ„±ν™”")
    ap.add_argument("--no-chat", action="store_true", help="포켓 μžμœ μ±„νŒ… λΉ„ν™œμ„±ν™”")
    args = ap.parse_args()

    chat_enabled = not args.no_chat and (args.chat or bool(os.environ.get("OPENAI_API_KEY")))
    repo = ContentRepository(args.content)
    engine = StoryEngine(repo)

    pocket_service = None
    if chat_enabled:
        from engine.plugins.logging import TurnLoggingPlugin
        from engine.services.pocket import PocketService
        pocket_service = PocketService(repo, plugins=[TurnLoggingPlugin()])

    step = engine.start()
    while step.ending is None:
        render(step, args.quiet)
        if step.choices:
            while True:
                kind, val = pick_or_chat(step, args.policy, chat_enabled)
                if kind == "choice":
                    choice, step = engine.choose(val)
                    if not args.quiet:
                        print(f"\n{DIM}β–· {choice.label}{RESET}")
                        if choice.result:
                            print(choice.result)
                    break
                card = repo.get_card(engine.current_id)
                if not card.can_chat:
                    print(f"{DIM}  (이 μž₯면에선 κ·Έλ…€κ°€ μžλ¦¬μ— μ—†λ‹€ β€” 번호λ₯Ό μž…λ ₯ν•˜μ„Έμš”){RESET}")
                    continue
                await pocket_chat(pocket_service, engine, engine.current_id, val)
                # μ±„νŒ… ν›„ 같은 μΉ΄λ“œμ˜ μ„ νƒμ§€λ‘œ 볡귀 (λ„λ‹¬μ Β·λΆ„κΈ°λŠ” μ½”λ“œκ°€ 보μž₯)
        else:
            if not args.quiet and args.policy == "ask":
                input(f"{DIM}(λ‹€μŒ ⏎){RESET}")
            step = engine.advance()

    s = engine.state
    print(f"\n{BOLD}═══ 챕터 μ’…λ£Œ: {step.ending.label} ═══{RESET}")
    print(f"{DIM}[디버그 β€” μœ μ € λΉ„λ…ΈμΆœ] trust={s.trust_score} doubt={s.doubt_score} "
          f"frailty={s.frailty} level={s.trust_level} μΉ΄λ“œ {len(engine.visited)}μž₯ λ°©λ¬Έ{RESET}")


if __name__ == "__main__":
    asyncio.run(main())