File size: 6,525 Bytes
5c40041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""
server/environment.py
─────────────────────────────────────────────────────────────────────────────
Pydantic v2 models that mirror the React game state sent from the frontend,
plus the prompt builder that converts a GameState into the [INST] string
consumed by the LLM.
"""

from __future__ import annotations

from typing import List, Optional
from pydantic import BaseModel, Field


# ── Request / Response models ─────────────────────────────────────────────────

class Inventory(BaseModel):
    spear: bool = False
    bow: bool = False
    fishingRod: bool = False
    boat: bool = False


class BaseCamp(BaseModel):
    x: Optional[float] = None
    y: Optional[float] = None
    level: int = 0


class AIMemory(BaseModel):
    evolutionLevel: int = 1
    pastDeaths: List[str] = Field(default_factory=list)
    totalGenerations: int = 0
    challengesWon: int = 0


class ActiveChallenge(BaseModel):
    name: str
    type: str
    timeLimit: int
    maxTime: int = 0
    progress: str = ""


class GameState(BaseModel):
    """Full agent snapshot sent by the frontend to /api/infer."""

    # Vitals
    health:  float = Field(..., ge=0, le=100)
    hunger:  float = Field(..., ge=0, le=100)
    thirst:  float = Field(..., ge=0, le=100)
    stamina: float = Field(..., ge=0, le=100)
    fear:    float = Field(default=0, ge=0, le=100)

    # Resources
    wood:  int = 0
    stone: int = 0
    food:  int = 0
    water: int = 0

    # World context
    playerX:      float = 1000
    isNight:      bool  = False
    activeEvents: List[str] = Field(default_factory=list)
    predatorNear: bool  = False

    # Agent state
    inventory: Inventory  = Field(default_factory=Inventory)
    baseCamp:  BaseCamp   = Field(default_factory=BaseCamp)
    memory:    AIMemory   = Field(default_factory=AIMemory)
    generation: int = 1

    # Optional active challenge
    activeChallenge: Optional[ActiveChallenge] = None


class InferResponse(BaseModel):
    action: str
    thought: str
    source: str   # "local" | "api" | "fallback"


# ── Prompt construction ───────────────────────────────────────────────────────

_DEATH_LESSON_MAP: dict[str, str] = {
    "starvation":  "CRITICAL: Prioritize food β€” starvation has killed me before.",
    "dehydration": "CRITICAL: Prioritize water β€” dehydration has killed me before.",
    "hypothermia": "CRITICAL: Seek shelter at night β€” hypothermia has killed me before.",
    "heatstroke":  "CRITICAL: Find shade/water during heatwaves β€” heatstroke has killed me before.",
    "lion":        "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
    "mauled":      "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
    "panther":     "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
    "crocodile":   "CRITICAL: Avoid water edges without a boat β€” crocodiles are deadly.",
    "flood":       "CRITICAL: During floods, evacuate eastward IMMEDIATELY.",
}

VALID_ACTIONS = (
    "FORAGE, HUNT, FISH, GET_WATER, SEEK_SHELTER, BUILD_CAMP, UPGRADE_CAMP, "
    "CRAFT_SPEAR, CRAFT_BOW, CRAFT_ROD, CRAFT_BOAT, EVACUATE, FIGHT, FLEE, WANDER"
)


def _derive_lessons(past_deaths: list[str]) -> list[str]:
    seen: set[str] = set()
    lessons: list[str] = []
    for death in past_deaths:
        low = death.lower()
        for keyword, lesson in _DEATH_LESSON_MAP.items():
            if keyword in low and lesson not in seen:
                seen.add(lesson)
                lessons.append(lesson)
    return lessons


def _strategy_label(memory: AIMemory) -> str:
    n = len(memory.pastDeaths)
    if n >= 5:
        return "veteran"
    if n >= 3:
        return "cautious"
    if memory.evolutionLevel > 1:
        return "experienced"
    return "basic"


def build_prompt(state: GameState) -> str:
    """
    Convert a GameState into the [INST] prompt consumed by the LLM.
    Mirrors the prompt built in App.jsx so server-side inference
    produces identical quality to client-side inference.
    """
    lessons = _derive_lessons(state.memory.pastDeaths)
    strategy = _strategy_label(state.memory)

    lessons_block = (
        "\n".join(f"{i + 1}. {l}" for i, l in enumerate(lessons))
        if lessons
        else "No prior deaths β€” explore and gather resources."
    )

    challenge_block = ""
    if state.activeChallenge:
        c = state.activeChallenge
        challenge_block = (
            f'\nACTIVE CHALLENGE: "{c.name}" (type: {c.type}) β€” {c.timeLimit}s remaining.\n'
            f"Challenge progress: {c.progress or 'just started'}.\n"
            "Prioritize completing this challenge above all else!"
        )

    inv = state.inventory
    return (
        f"<s>[INST] You are the survival instinct AI (Generation {state.generation}) "
        f"of Subject-01. You have died {len(state.memory.pastDeaths)} times.\n\n"
        f"STRATEGY LEVEL: {strategy.upper()}\n"
        f"\nLESSONS FROM PAST DEATHS:\n{lessons_block}"
        f"{challenge_block}\n\n"
        f"Current status:\n"
        f"HP:{state.health:.0f}, Hunger:{state.hunger:.0f}, "
        f"Thirst:{state.thirst:.0f}, Fear:{state.fear:.0f}/100.\n"
        f"Resources: Wood:{state.wood}, Stone:{state.stone}, "
        f"Food:{state.food}, Water:{state.water}.\n"
        f"Equipped: Spear:{inv.spear}, Bow:{inv.bow}, "
        f"Rod:{inv.fishingRod}, Boat:{inv.boat}.\n"
        f"Camp Level: {state.baseCamp.level}. Position X: {state.playerX:.0f}.\n"
        f"Environment: {'Night' if state.isNight else 'Day'}, "
        f"Events: {', '.join(state.activeEvents) or 'None'}.\n"
        f"Predator Nearby: {'YES - HIGH DANGER' if state.predatorNear else 'No'}.\n"
        f"{f'ACTIVE CHALLENGE: {state.activeChallenge.name} ({state.activeChallenge.type}) β€” {state.activeChallenge.timeLimit}s left' if state.activeChallenge else 'No active challenge.'}\n\n"
        f"Valid Actions: {VALID_ACTIONS}.\n\n"
        'Respond ONLY with a raw JSON object β€” no markdown, no extra text. '
        'Example: {"action":"FORAGE","thought":"Need wood and resources"} [/INST]'
    )