Skip to content

Observation Space

The Gymnasium definition for the observation space. At the root is a Dict space with the following keys: ['game', 'rules', 'map', 'player', 'city', 'tech', 'unit', 'options', 'dipl', 'gov', 'client']. We describe the important state spaces below.

Click on the source code below to show the space definition.

Map State

Source code in src/civrealm/freeciv/map/map_state.py
def get_observation_space(self):
    map_shape = self._state['status'].shape
    self._observation_space = gymnasium.spaces.Dict({
        'status': gymnasium.spaces.Box(
            low=0, high=2, shape=map_shape, dtype=np.uint8),
        'terrain': gymnasium.spaces.Box(
            low=0, high=len(self.rule_ctrl.terrains) - 1,
            shape=map_shape, dtype=np.uint8),
        'extras': gymnasium.spaces.Box(
            low=0, high=1, shape=(*map_shape, self._extra_num),
            dtype=np.uint8),
        'output': gymnasium.spaces.Box(
            low=0, high=255, shape=(*map_shape, 6),
            dtype=np.uint8),
        'tile_owner': gymnasium.spaces.Box(
            low=0, high=255, shape=map_shape, dtype=np.uint8),
        'city_owner': gymnasium.spaces.Box(
            low=0, high=255, shape=map_shape, dtype=np.uint8),
        'unit': gymnasium.spaces.Box(
            low=0, high=255, shape=(*map_shape, self._unit_type_num),
            dtype=np.uint8),
        'unit_owner': gymnasium.spaces.Box(
            low=0, high=255, shape=map_shape, dtype=np.uint8), })
    return self._observation_space

City State

Source code in src/civrealm/freeciv/city/city_state.py
def get_observation_space(self):
    city_space = gymnasium.spaces.Dict({
        # Common city fields
        'id': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'owner': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        'size': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        # TODO: may change this to actual map size
        'x': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        'y': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        'name': gymnasium.spaces.Text(max_length=100),

        # My city fields
        'food_stock': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'granary_size': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'granary_turns': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'production_kind': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'production_value': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'city_radius_sq': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'buy_cost': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'shield_stock': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'disbanded_shields': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'caravan_shields': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'last_turns_shield_surplus': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'luxury': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'science': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'prod_food': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'surplus_food': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'prod_gold': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'surplus_gold': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'prod_shield': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'surplus_shield': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'prod_trade': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'surplus_trade': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'bulbs': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'city_waste': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'city_corruption': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'city_pollution': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        # -1: None, 1: Disorder, 2: Peace, 3: Celebrating
        'state': gymnasium.spaces.Box(low=-1, high=3, shape=(1,), dtype=np.int8),
        'growth_in': gymnasium.spaces.Text(max_length=100),
        'turns_to_prod_complete': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'prod_process': gymnasium.spaces.Box(low=-32768, high=32767, shape=(1,), dtype=np.int16),
        'ppl_angry': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'ppl_unhappy': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'ppl_content': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        'ppl_happy': gymnasium.spaces.Box(low=-1, high=127, shape=(1,), dtype=np.int8),
        # Boolean vector
        'can_build_unit': gymnasium.spaces.Box(low=0, high=1, shape=(self.rule_ctrl.ruleset_control['num_unit_types'],), dtype=np.int8),
        # Boolean vector
        'improvements': gymnasium.spaces.Box(low=0, high=1, shape=(self.rule_ctrl.ruleset_control['num_impr_types'],), dtype=np.int8),
        'turn_last_built': gymnasium.spaces.Box(low=1, high=32767, shape=(1,), dtype=np.int16),
    })

    return gymnasium.spaces.Dict({city_id: city_space for city_id in self.city_dict.keys()})

Unit State

Source code in src/civrealm/freeciv/units/unit_state.py
def get_observation_space(self):
    unit_space = gymnasium.spaces.Dict({
        # Common unit fields
        'owner': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        'health': gymnasium.spaces.Box(low=0, high=100, shape=(1,), dtype=np.uint8),
        'veteran': gymnasium.spaces.Box(low=0, high=1, shape=(1,), dtype=np.uint8),
        # TODO: may change this to actual map size
        'x': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),
        'y': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=np.uint8),

        # Unit type fields
        'type_rule_name': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.unit_types)-1, shape=(1,), dtype=np.uint8),
        'type_attack_strength': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_defense_strength': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_firepower': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_build_cost': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_convert_time': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_converted_to': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.unit_types)-1, shape=(1,), dtype=np.uint8),
        'type_obsoleted_by': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.unit_types)-1, shape=(1,), dtype=np.uint8),
        'type_hp': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_move_rate': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_vision_radius_sq': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=np.uint16),
        'type_worker': gymnasium.spaces.Discrete(1),  # Boolean
        'type_can_transport': gymnasium.spaces.Discrete(1),  # Boolean

        # My unit specific fields
        'home_city': gymnasium.spaces.Box(low=-1, high=len(self.city_ctrl.cities)-1, shape=(1,), dtype=np.int16),
        'moves_left': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'upkeep_food': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'upkeep_shield': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
        'upkeep_gold': gymnasium.spaces.Box(low=-1, high=32767, shape=(1,), dtype=np.int16),
    })

    return gymnasium.spaces.Dict({unit_id: unit_space for unit_id in self.unit_ctrl.units.keys()})

Tech State

Get observation space.

Returns:

Type Description
gymnasium.space.Dict(

"tech_status": Box, "current_tech": Box

)
"tech_status": Box of shape (reqtree_size,)

list status of all techs, with values of each entry: -1: obtained, 0: under research, 1: can be researched, 2: need other prerequest tech(s),

"current_tech": Box(tech_id: Int, current_bulbs_on_it: Int)
Source code in src/civrealm/freeciv/tech/tech_state.py
def get_observation_space(self) -> Dict:
    """
    Get observation space.

    Returns
    -------
    gymnasium.space.Dict(
        "tech_status": Box,
        "current_tech": Box
    )
    "tech_status": Box of shape (reqtree_size,)
                   list status of all techs, with values of each entry:
                   -1: obtained,
                    0: under research,
                    1: can be researched,
                    2: need other prerequest tech(s),
    "current_tech": Box(tech_id: Int, current_bulbs_on_it: Int)
    """
    return Dict({
        "tech_status":
        Box(np.ones(self.reqtree_size) * (-1),
            np.ones(self.reqtree_size) * self.UPPER_TECH_STATUS,
            dtype=int),
        "current_tech":
        Tuple(Discrete(self.reqtree_size), Discrete(self.UPPER_BULB_BOUND),
              Box(0, self.UPPER_BULB_BOUND, dtype=float))
    })

Player State

Source code in src/civrealm/freeciv/players/player_state.py
def get_observation_space(self):
    player_space = gymnasium.spaces.Dict({
        **{
            # Common player fields
            'player_id': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=int),
            'name': gymnasium.spaces.Text(max_length=100),
            'score': gymnasium.spaces.Box(low=-1, high=65535, shape=(1,), dtype=int),
            'team': gymnasium.spaces.Box(low=0, high=255, shape=(1,), dtype=int),
            'is_alive': gymnasium.spaces.Discrete(2),  # Boolean
            'nation': gymnasium.spaces.Box(low=0, high=list(self.rule_ctrl.nations.keys())[-1], shape=(1,), dtype=int),
            'turns_alive': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'government': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.governments)-1, shape=(1,), dtype=int),
            'target_government': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.governments)-1, shape=(1,), dtype=int),
            'government_name': gymnasium.spaces.Text(max_length=100),
            'researching': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.techs)-1, shape=(1,), dtype=int),
            'research_name': gymnasium.spaces.Text(max_length=100),
            # Tax, science, luxury are percentages, should sum to 100
            'tax': gymnasium.spaces.Box(low=0, high=100, shape=(1,), dtype=int),
            'science': gymnasium.spaces.Box(low=0, high=100, shape=(1,), dtype=int),
            'luxury': gymnasium.spaces.Box(low=0, high=100, shape=(1,), dtype=int),

            # My player fields
            'gold': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'culture': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            # mood_type, values are MOOD_PEACEFUL and MOOD_COMBAT
            'mood': gymnasium.spaces.Discrete(2),
            # The turn when the revolution finishes
            'revolution_finishes': gymnasium.spaces.Box(low=-1, high=65535, shape=(1,), dtype=int),
            'science_cost': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'bulbs_researched': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'researching_cost': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'tech_goal': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.techs)-1, shape=(1,), dtype=int),
            'tech_upkeep': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'techs_researched': gymnasium.spaces.Box(low=0, high=len(self.rule_ctrl.techs)-1, shape=(1,), dtype=int),
            'total_bulbs_prod': gymnasium.spaces.Box(low=0, high=65535, shape=(1,), dtype=int),
            'embassy_txt': gymnasium.spaces.Text(max_length=100),

            # Other player fields
            # Possible values are player_const.ATTITUDE_TXT
            'love': gymnasium.spaces.Text(max_length=100),
        },
        **{
            f'tech_{tech_id}': gymnasium.spaces.Discrete(2) for tech_id in self.rule_ctrl.techs.keys()
        }
    })

    return gymnasium.spaces.Dict({player_id: player_space for player_id in self.player_ctrl.players.keys()})

Diplomatic state

Source code in src/civrealm/freeciv/players/diplomacy_state_ctrl.py
def get_observation_space(self):
    diplomacy_space = gymnasium.spaces.Dict({
        'diplomatic_state': gymnasium.spaces.Discrete(player_const.DS_LAST),
        # TODO: to be specified
        'diplomacy_clause_map': gymnasium.spaces.Sequence(gymnasium.spaces.Dict()),
        'meeting_initializers': gymnasium.spaces.Discrete(255),
    })
    return gymnasium.spaces.Dict({player_id: diplomacy_space for player_id in self.diplomatic_state.keys()})