File size: 2,528 Bytes
7fe39f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The turn loop.

GameEngine ties the pieces together:

    player action ─▢ build prompt from GameState ─▢ model.chat() ─▢ parser
                  ◀─ narrative + updated state β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

It also enforces a couple of rules the model should not be trusted with:
  * if the model forgets to make a defeated enemy stop attacking, combat still ends
    when enemy HP hits 0 (handled in the parser);
  * a guaranteed enemy counter-attack option exists even if the model is lazy
    (the engine never *adds* damage on its own β€” it only ever clamps).
"""

from __future__ import annotations

from .game_state import GameState
from .llm import Backend
from . import prompts, parser


class GameEngine:
    def __init__(self, backend: Backend, state: GameState | None = None):
        self.backend = backend
        self.state = state or GameState()
        self.history: list[parser.TurnResult] = []

    # ------------------------------------------------------------------ public
    def start(self) -> parser.TurnResult:
        """Generate the opening scene."""
        user = prompts.build_opening_prompt(self.state.context_snapshot())
        return self._turn(user)

    def act(self, player_action: str) -> parser.TurnResult:
        """Process one player action."""
        if self.state.game_over:
            return parser.TurnResult(
                narrative="Your tale has ended. Start a new adventure to play again.",
                choices=[],
                applied=[],
                raw="",
            )
        action = player_action.strip() or "look around"
        user = prompts.build_turn_prompt(self.state.context_snapshot(), action)
        return self._turn(user)

    def reset(self) -> "GameEngine":
        self.state = GameState()
        self.history.clear()
        return self

    # ----------------------------------------------------------------- internal
    def _turn(self, user_message: str) -> parser.TurnResult:
        try:
            raw = self.backend.chat(prompts.SYSTEM_PROMPT, user_message)
        except Exception as exc:  # network/model errors shouldn't crash the UI
            return parser.TurnResult(
                narrative=f"(The mists swirl β€” the storyteller faltered: {exc})",
                choices=["Try again."],
                applied=[],
                raw="",
            )
        result = parser.run_turn(self.state, raw)
        self.history.append(result)
        return result