Skip to content

Commit 975b35c

Browse files
committed
Implement observation API for gymnasium integration (Phase 2)
Add comprehensive observation API to extract game state for reinforcement learning. This provides structured access to all observable game metrics without requiring direct access to internal city_data structures. Changes: - Created src/gymnasium/observation.h: Defines gymnasium_observation_t structure containing all observable game state (ratings, finance, population, labor, resources, buildings, time, victory) - Created src/gymnasium/observation.c: Implements state extraction using existing public city_* APIs. Extracts ~40+ different metrics including: * Ratings (culture, prosperity, peace, favor) * Finance (treasury, tax rate, income/expenses) * Population (total, working age, sentiment) * Labor (workers, unemployment, wages) * Resources (food stocks, types, supply months) * Buildings (aggregated counts by category) * Migration (newcomers) * Culture coverage (entertainment, education, health) * Time (year, month) * Victory goals and status - Updated CMakeLists.txt: Added GYMNASIUM_FILES group to build - Created test/gymnasium/test_observation.c: Unit tests for observation API including: * Structure clearing * NULL pointer handling * Basic observation extraction * Range validation for all fields - Updated test/CMakeLists.txt: Added test_observation executable and test registration All tests pass (3/3). The observation API successfully extracts game state even without Caesar III data files loaded. Note: Some fields set to 0 where public APIs don't exist: - immigration/emigration amounts (internal to city_data) - housing capacity (no public accessor) - food consumed/produced last month (not exposed) - average religion coverage (per-god only) Part of Phase 2 from HEADLESS_GYMNASIUM_SCOPE.md
1 parent 4dbfccf commit 975b35c

5 files changed

Lines changed: 537 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,9 @@ set(TRANSLATION_FILES
630630
${PROJECT_SOURCE_DIR}/src/translation/traditional_chinese.c
631631
${PROJECT_SOURCE_DIR}/src/translation/translation.c
632632
)
633+
set(GYMNASIUM_FILES
634+
${PROJECT_SOURCE_DIR}/src/gymnasium/observation.c
635+
)
633636

634637
set(MACOSX_FILES "")
635638
if(APPLE AND NOT ${TARGET_PLATFORM} STREQUAL "ios")
@@ -660,6 +663,7 @@ set(SOURCE_FILES
660663
${FIGURE_FILES}
661664
${FIGURETYPE_FILES}
662665
${GAME_FILES}
666+
${GYMNASIUM_FILES}
663667
${INPUT_FILES}
664668
${MAP_FILES}
665669
${SCENARIO_FILES}

src/gymnasium/observation.c

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#include "observation.h"
2+
3+
#include "building/count.h"
4+
#include "city/culture.h"
5+
#include "city/data.h"
6+
#include "city/finance.h"
7+
#include "city/health.h"
8+
#include "city/labor.h"
9+
#include "city/migration.h"
10+
#include "city/population.h"
11+
#include "city/ratings.h"
12+
#include "city/resource.h"
13+
#include "city/sentiment.h"
14+
#include "city/victory.h"
15+
#include "game/time.h"
16+
#include "scenario/criteria.h"
17+
18+
#include <string.h>
19+
20+
void gymnasium_clear_observation(gymnasium_observation_t *obs)
21+
{
22+
memset(obs, 0, sizeof(gymnasium_observation_t));
23+
}
24+
25+
int gymnasium_get_observation(gymnasium_observation_t *obs)
26+
{
27+
if (!obs) {
28+
return -1; // Invalid pointer
29+
}
30+
31+
// Clear the observation first
32+
gymnasium_clear_observation(obs);
33+
34+
// Extract ratings (all 0-100)
35+
obs->ratings.culture = city_rating_culture();
36+
obs->ratings.prosperity = city_rating_prosperity();
37+
obs->ratings.peace = city_rating_peace();
38+
obs->ratings.favor = city_rating_favor();
39+
40+
// Extract finance data
41+
obs->finance.treasury = city_finance_treasury();
42+
obs->finance.tax_percentage = city_finance_tax_percentage();
43+
obs->finance.estimated_tax_income = city_finance_estimated_tax_income();
44+
obs->finance.estimated_wages = city_finance_estimated_wages();
45+
46+
// Last year's financial overview
47+
const finance_overview *last_year = city_finance_overview_last_year();
48+
if (last_year) {
49+
obs->finance.last_year_income = last_year->income.total;
50+
obs->finance.last_year_expenses = last_year->expenses.total;
51+
obs->finance.last_year_net = last_year->net_in_out;
52+
}
53+
54+
// Extract population data
55+
obs->population.total = city_population();
56+
obs->population.school_age = city_population_school_age();
57+
obs->population.academy_age = city_population_academy_age();
58+
obs->population.working_age = city_population_people_of_working_age();
59+
obs->population.sentiment = city_sentiment();
60+
61+
// Extract labor data
62+
obs->labor.workers_available = city_labor_workers_unemployed(); // Available = unemployed
63+
obs->labor.workers_employed = city_labor_workers_employed();
64+
obs->labor.workers_needed = city_labor_workers_needed();
65+
obs->labor.unemployment_pct = city_labor_unemployment_percentage();
66+
obs->labor.wages = city_labor_wages();
67+
68+
// Extract resource data
69+
obs->resources.food_stocks = city_resource_food_stored();
70+
obs->resources.food_types_available = city_resource_food_types_available();
71+
obs->resources.food_supply_months = city_resource_food_supply_months();
72+
// Note: food consumed/produced last month not exposed via API
73+
obs->resources.food_consumed_last_month = 0; // Not available
74+
obs->resources.food_produced_last_month = 0; // Not available
75+
76+
// Extract building counts
77+
// Note: We aggregate different building types into categories
78+
obs->buildings.housing = building_count_total(BUILDING_HOUSE_SMALL_TENT) +
79+
building_count_total(BUILDING_HOUSE_LARGE_TENT) +
80+
building_count_total(BUILDING_HOUSE_SMALL_SHACK) +
81+
building_count_total(BUILDING_HOUSE_LARGE_SHACK) +
82+
building_count_total(BUILDING_HOUSE_SMALL_HOVEL) +
83+
building_count_total(BUILDING_HOUSE_LARGE_HOVEL) +
84+
building_count_total(BUILDING_HOUSE_SMALL_CASA) +
85+
building_count_total(BUILDING_HOUSE_LARGE_CASA) +
86+
building_count_total(BUILDING_HOUSE_SMALL_INSULA) +
87+
building_count_total(BUILDING_HOUSE_MEDIUM_INSULA) +
88+
building_count_total(BUILDING_HOUSE_LARGE_INSULA) +
89+
building_count_total(BUILDING_HOUSE_GRAND_INSULA) +
90+
building_count_total(BUILDING_HOUSE_SMALL_VILLA) +
91+
building_count_total(BUILDING_HOUSE_MEDIUM_VILLA) +
92+
building_count_total(BUILDING_HOUSE_LARGE_VILLA) +
93+
building_count_total(BUILDING_HOUSE_GRAND_VILLA) +
94+
building_count_total(BUILDING_HOUSE_SMALL_PALACE) +
95+
building_count_total(BUILDING_HOUSE_MEDIUM_PALACE) +
96+
building_count_total(BUILDING_HOUSE_LARGE_PALACE) +
97+
building_count_total(BUILDING_HOUSE_LUXURY_PALACE);
98+
99+
// Note: total housing capacity is not exposed via public API
100+
obs->buildings.housing_capacity = 0; // Not available
101+
102+
obs->buildings.food_buildings = building_count_total(BUILDING_WHEAT_FARM) +
103+
building_count_total(BUILDING_VEGETABLE_FARM) +
104+
building_count_total(BUILDING_FRUIT_FARM) +
105+
building_count_total(BUILDING_OLIVE_FARM) +
106+
building_count_total(BUILDING_VINES_FARM) +
107+
building_count_total(BUILDING_PIG_FARM) +
108+
building_count_total(BUILDING_GRANARY);
109+
110+
obs->buildings.industrial_buildings = building_count_total(BUILDING_WINE_WORKSHOP) +
111+
building_count_total(BUILDING_OIL_WORKSHOP) +
112+
building_count_total(BUILDING_WEAPONS_WORKSHOP) +
113+
building_count_total(BUILDING_FURNITURE_WORKSHOP) +
114+
building_count_total(BUILDING_POTTERY_WORKSHOP);
115+
116+
obs->buildings.entertainment = building_count_total(BUILDING_THEATER) +
117+
building_count_total(BUILDING_AMPHITHEATER) +
118+
building_count_total(BUILDING_COLOSSEUM) +
119+
building_count_total(BUILDING_HIPPODROME) +
120+
building_count_total(BUILDING_GLADIATOR_SCHOOL) +
121+
building_count_total(BUILDING_LION_HOUSE) +
122+
building_count_total(BUILDING_ACTOR_COLONY) +
123+
building_count_total(BUILDING_CHARIOT_MAKER);
124+
125+
obs->buildings.education = building_count_total(BUILDING_SCHOOL) +
126+
building_count_total(BUILDING_ACADEMY) +
127+
building_count_total(BUILDING_LIBRARY);
128+
129+
obs->buildings.health = building_count_total(BUILDING_DOCTOR) +
130+
building_count_total(BUILDING_HOSPITAL) +
131+
building_count_total(BUILDING_BATHHOUSE) +
132+
building_count_total(BUILDING_BARBER);
133+
134+
obs->buildings.religious = building_count_total(BUILDING_SMALL_TEMPLE_CERES) +
135+
building_count_total(BUILDING_SMALL_TEMPLE_NEPTUNE) +
136+
building_count_total(BUILDING_SMALL_TEMPLE_MERCURY) +
137+
building_count_total(BUILDING_SMALL_TEMPLE_MARS) +
138+
building_count_total(BUILDING_SMALL_TEMPLE_VENUS) +
139+
building_count_total(BUILDING_LARGE_TEMPLE_CERES) +
140+
building_count_total(BUILDING_LARGE_TEMPLE_NEPTUNE) +
141+
building_count_total(BUILDING_LARGE_TEMPLE_MERCURY) +
142+
building_count_total(BUILDING_LARGE_TEMPLE_MARS) +
143+
building_count_total(BUILDING_LARGE_TEMPLE_VENUS) +
144+
building_count_total(BUILDING_ORACLE);
145+
146+
obs->buildings.total_buildings = building_count_total(BUILDING_NONE); // Gets total of all buildings
147+
148+
// Extract migration data
149+
// Note: immigration/emigration amounts are internal to city_data and not exposed via API
150+
obs->migration.immigration_amount = 0; // Not directly accessible
151+
obs->migration.emigration_amount = 0; // Not directly accessible
152+
obs->migration.newcomers = city_migration_newcomers();
153+
154+
// Extract culture/health coverage
155+
obs->culture.health_value = city_health();
156+
obs->culture.average_entertainment = city_culture_average_entertainment();
157+
obs->culture.average_education = city_culture_average_education();
158+
obs->culture.average_health = city_culture_average_health();
159+
// Note: No average_religion function exists; religion coverage is per-god
160+
obs->culture.average_religion = 0; // Not available via API
161+
162+
// Extract time data
163+
obs->time.year = game_time_year();
164+
obs->time.month = game_time_month();
165+
obs->time.total_months = game_time_year() * 12 + game_time_month();
166+
167+
// Extract victory/scenario data
168+
obs->victory.is_active = !city_victory_has_won();
169+
obs->victory.has_won = city_victory_has_won();
170+
obs->victory.population_goal = scenario_criteria_population();
171+
obs->victory.culture_goal = scenario_criteria_culture();
172+
obs->victory.prosperity_goal = scenario_criteria_prosperity();
173+
obs->victory.peace_goal = scenario_criteria_peace();
174+
obs->victory.favor_goal = scenario_criteria_favor();
175+
176+
return 0; // Success
177+
}

src/gymnasium/observation.h

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#ifndef GYMNASIUM_OBSERVATION_H
2+
#define GYMNASIUM_OBSERVATION_H
3+
4+
#include <stdint.h>
5+
6+
/**
7+
* Observation structure for Gymnasium integration
8+
* This structure contains all observable state from the game
9+
* that can be used for reinforcement learning
10+
*/
11+
typedef struct {
12+
// Ratings (0-100 each)
13+
struct {
14+
int32_t culture;
15+
int32_t prosperity;
16+
int32_t peace;
17+
int32_t favor;
18+
} ratings;
19+
20+
// Finance
21+
struct {
22+
int32_t treasury; // Current denarii in treasury
23+
int32_t tax_percentage; // Tax rate (0-25%)
24+
int32_t estimated_tax_income; // Estimated tax income this year
25+
int32_t estimated_wages; // Estimated wages this year
26+
int32_t last_year_income; // Total income last year
27+
int32_t last_year_expenses; // Total expenses last year
28+
int32_t last_year_net; // Net income/loss last year
29+
} finance;
30+
31+
// Population
32+
struct {
33+
int32_t total; // Total population
34+
int32_t school_age; // School age population
35+
int32_t academy_age; // Academy age population
36+
int32_t working_age; // Working age population
37+
int32_t sentiment; // Sentiment value (0-100)
38+
} population;
39+
40+
// Labor
41+
struct {
42+
int32_t workers_available; // Available workers
43+
int32_t workers_employed; // Employed workers
44+
int32_t workers_needed; // Workers needed
45+
int32_t unemployment_pct; // Unemployment percentage
46+
int32_t wages; // Current wage rate (denarii)
47+
} labor;
48+
49+
// Resources
50+
struct {
51+
int32_t food_stocks; // Total food in granaries
52+
int32_t food_types_available; // Number of different food types
53+
int32_t food_supply_months; // Months of food supply
54+
int32_t food_consumed_last_month; // Food consumed last month
55+
int32_t food_produced_last_month; // Food produced last month
56+
} resources;
57+
58+
// Buildings (aggregated counts)
59+
struct {
60+
int32_t housing; // Total housing buildings
61+
int32_t housing_capacity; // Total housing capacity
62+
int32_t food_buildings; // Farms, granaries, etc.
63+
int32_t industrial_buildings; // Workshops, industries
64+
int32_t entertainment; // Entertainment venues
65+
int32_t education; // Schools, libraries, academies
66+
int32_t health; // Doctors, hospitals, baths
67+
int32_t religious; // Temples, oracles
68+
int32_t total_buildings; // Total count of all buildings
69+
} buildings;
70+
71+
// Migration
72+
struct {
73+
int32_t immigration_amount; // Immigration per batch
74+
int32_t emigration_amount; // Emigration per batch
75+
int32_t newcomers; // Newcomers this period
76+
} migration;
77+
78+
// Health & Culture
79+
struct {
80+
int32_t health_value; // Health rating (0-100)
81+
int32_t average_entertainment; // Entertainment coverage
82+
int32_t average_education; // Education coverage
83+
int32_t average_health; // Health coverage
84+
int32_t average_religion; // Religion coverage
85+
} culture;
86+
87+
// Time
88+
struct {
89+
int32_t year;
90+
int32_t month;
91+
int32_t total_months; // Total game time in months
92+
} time;
93+
94+
// Victory/Game State
95+
struct {
96+
int32_t is_active; // 1 if game is running, 0 if ended
97+
int32_t has_won; // 1 if won, 0 otherwise
98+
int32_t population_goal; // Population goal for scenario
99+
int32_t culture_goal; // Culture goal for scenario
100+
int32_t prosperity_goal; // Prosperity goal for scenario
101+
int32_t peace_goal; // Peace goal for scenario
102+
int32_t favor_goal; // Favor goal for scenario
103+
} victory;
104+
105+
} gymnasium_observation_t;
106+
107+
/**
108+
* Get current game state as an observation
109+
* @param obs Pointer to observation structure to fill
110+
* @return 0 on success, non-zero on error
111+
*/
112+
int gymnasium_get_observation(gymnasium_observation_t *obs);
113+
114+
/**
115+
* Reset/clear an observation structure
116+
* @param obs Pointer to observation structure to clear
117+
*/
118+
void gymnasium_clear_observation(gymnasium_observation_t *obs);
119+
120+
#endif // GYMNASIUM_OBSERVATION_H

test/CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,31 @@ add_integration_test(sav_native1 brugle-lugdunum-native.sav brugle-lugdunum-nati
130130
add_integration_test(sav_native2 cicero-lugdunum-trade.sav cicero-lugdunum-trade-after.sav 926)
131131

132132
add_integration_test(sav_palace1 brugle-palacepeaks.sav brugle-palacepeaks-2.sav 2562)
133+
134+
# Gymnasium observation test
135+
add_executable(test_observation
136+
gymnasium/test_observation.c
137+
stub/image.c
138+
stub/input.c
139+
stub/lang.c
140+
stub/log.c
141+
stub/model.c
142+
stub/sound_device.c
143+
stub/ui.c
144+
stub/video.c
145+
${PROJECT_SOURCE_DIR}/src/platform/file_manager.c
146+
${TEST_CORE_FILES}
147+
${TEST_BUILDING_FILES}
148+
${CITY_FILES}
149+
${EMPIRE_FILES}
150+
${FIGURE_FILES}
151+
${FIGURETYPE_FILES}
152+
${GAME_FILES}
153+
${GYMNASIUM_FILES}
154+
${MAP_FILES}
155+
${SCENARIO_FILES}
156+
${SOUND_FILES}
157+
${EDITOR_FILES}
158+
)
159+
160+
add_test(NAME gymnasium_observation COMMAND test_observation)

0 commit comments

Comments
 (0)