File size: 5,372 Bytes
8dc7642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field, model_validator

from openenv.core.env_server.types import Action, Observation, State


class UnitSummary(BaseModel):
    unit_id: int = Field(..., description="Freeciv unit id")
    unit_type: str = Field(..., description="Ruleset unit type name")
    health: int = Field(0, description="Current health")
    moves_left: int = Field(0, description="Movement points remaining")
    home_city_id: int | None = Field(None, description="Home city id, if any")
    veteran_level: int = Field(0, description="Veteran level")
    can_build_city: bool = Field(False, description="Whether the unit can found a city now")
    move_directions: list[int] = Field(default_factory=list, description="Legal move direction indexes")


class CitySummary(BaseModel):
    city_id: int = Field(..., description="Freeciv city id")
    size: int = Field(..., description="Population size")
    prod_food: int = Field(0, description="Gross food output")
    prod_shield: int = Field(0, description="Gross shield output")
    prod_trade: int = Field(0, description="Gross trade output")
    surplus_food: int = Field(0, description="Net food surplus")
    surplus_shield: int = Field(0, description="Net shield surplus")
    surplus_trade: int = Field(0, description="Net trade surplus")
    production_kind: int | None = Field(None, description="Current production kind enum from Freeciv")
    production_value: int | None = Field(None, description="Current production value id from Freeciv")
    turns_to_complete: float | None = Field(None, description="Turns until current production completes")
    production_options: list[str] = Field(default_factory=list, description="Legal production targets")


class LegalAction(BaseModel):
    action_type: Literal[
        "end_turn",
        "move_unit",
        "build_city",
        "set_city_production",
        "set_research",
    ]
    label: str = Field(..., description="Human-readable action label")
    unit_id: int | None = Field(None, description="Target unit id")
    city_id: int | None = Field(None, description="Target city id")
    direction: int | None = Field(None, description="Freeciv direction index 0..7")
    target: str | None = Field(None, description="Production or tech target name")
    raw_action_key: str | None = Field(None, description="Underlying freeciv-bot action key")


class FreecivAction(Action):
    action_type: Literal[
        "end_turn",
        "move_unit",
        "build_city",
        "set_city_production",
        "set_research",
    ]
    unit_id: int | None = None
    city_id: int | None = None
    direction: int | None = None
    target: str | None = None

    @model_validator(mode="after")
    def validate_shape(self) -> "FreecivAction":
        if self.action_type == "end_turn":
            return self
        if self.action_type == "move_unit":
            if self.unit_id is None or self.direction is None:
                raise ValueError("move_unit requires unit_id and direction")
            return self
        if self.action_type == "build_city":
            if self.unit_id is None:
                raise ValueError("build_city requires unit_id")
            return self
        if self.action_type == "set_city_production":
            if self.city_id is None or not self.target:
                raise ValueError("set_city_production requires city_id and target")
            return self
        if self.action_type == "set_research":
            if not self.target:
                raise ValueError("set_research requires target")
            return self
        raise ValueError(f"unsupported action_type: {self.action_type}")


class FreecivObservation(Observation):
    turn: int = Field(..., description="Current game turn")
    score: float = Field(..., description="Current player score")
    known_tiles: int = Field(..., description="Tiles known to the player")
    visible_tiles: int = Field(..., description="Tiles currently visible to the player")
    city_count: int = Field(..., description="Number of owned cities")
    unit_count: int = Field(..., description="Number of owned units")
    techs_researched: int = Field(..., description="Number of researched techs")
    status: str = Field("ok", description="High-level environment status")
    summary: str = Field(..., description="Compact text summary for LLMs")
    units: list[UnitSummary] = Field(default_factory=list, description="Compact unit summaries")
    cities: list[CitySummary] = Field(default_factory=list, description="Compact city summaries")
    legal_actions: list[LegalAction] = Field(default_factory=list, description="Legal actions exposed by the environment")
    reward: float = Field(0.0, description="Reward from the last action")
    done: bool = Field(False, description="Whether the episode is done")


class FreecivState(State):
    turn: int = Field(0, description="Current game turn")
    score: float = Field(0.0, description="Current player score")
    known_tiles: int = Field(0, description="Known tiles")
    visible_tiles: int = Field(0, description="Visible tiles")
    city_count: int = Field(0, description="Owned city count")
    unit_count: int = Field(0, description="Owned unit count")
    techs_researched: int = Field(0, description="Researched tech count")