Spaces:
Running
Running
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]'
)
|