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