Buckets:
| """ | |
| FarmState — one farmer's private state and action logic. | |
| No OpenEnv dependency. This is a plain class used by MultiAgentCroprlEnvironment | |
| as the per-farm state container. All action methods are public. | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional, Tuple | |
| import numpy as np | |
| from cropRL.config import EnvConfig | |
| from cropRL.dynamics import ( | |
| apply_spoilage, | |
| calculate_expected_yield_potential, | |
| calculate_interest_rate, | |
| calculate_yield, | |
| generate_market_prices, | |
| generate_rainfall, | |
| realise_rainfall, | |
| ) | |
| from cropRL.enums import ActionType, CropType | |
| class FarmState: | |
| """ | |
| Holds the mutable state for a single farm and exposes action helpers. | |
| All ``do_*`` methods return ``(penalty, message)`` — exactly like the | |
| old ``CroprlEnvironment._do_*`` private methods, but now public. | |
| """ | |
| def __init__(self, config: EnvConfig, rng: np.random.Generator) -> None: | |
| self.config = config | |
| self._rng = rng | |
| self._s: dict = {} | |
| self._init_state() | |
| # ────────────────────────────────────────────────────────────── | |
| # Initialisation | |
| # ────────────────────────────────────────────────────────────── | |
| def _init_state(self) -> None: | |
| cfg = self.config | |
| month = 1 | |
| rainfall = generate_rainfall(month, cfg, self._rng) | |
| prices = generate_market_prices(month, cfg, self._rng) | |
| interest_rate = calculate_interest_rate( | |
| cfg.base_interest_rate, month, rainfall, 0.0 | |
| ) | |
| self._s = { | |
| "month": month, | |
| "step": 0, | |
| "month_count": 0, | |
| "year": 1, | |
| "expected_rainfall": rainfall, | |
| "prices": prices, | |
| "interest_rate": interest_rate, | |
| # Crop | |
| "active_crop_type": CropType.FALLOW, | |
| "crop_age_months": 0, | |
| "planting_month": 0, | |
| # Soil | |
| "soil_nitrogen": cfg.initial_soil_nitrogen, | |
| # Water | |
| "water_level": 0.0, | |
| # Finance | |
| "cash": cfg.initial_cash, | |
| "debt": 0.0, | |
| "has_active_loan": False, | |
| "loan_interest_rate": 0.0, | |
| # Storage | |
| "stored_crop_type": CropType.FALLOW, | |
| "stored_amount": 0.0, | |
| "stored_age_months": 0, | |
| # Per-step flags | |
| "irrigated": False, | |
| "fertilized": False, | |
| # Inflated values (mutated by inflation each year) | |
| "inflated_seed_costs": list(cfg.seed_costs), | |
| "inflated_cost_irrigate": cfg.cost_irrigate, | |
| "inflated_cost_fertilize": cfg.cost_fertilize, | |
| "inflated_loan_chunk": cfg.loan_chunk, | |
| "inflated_base_land_price": cfg.base_land_price, | |
| "inflated_monthly_fixed_cost": cfg.monthly_fixed_cost, | |
| "inflated_base_market_prices": list(cfg.base_market_prices), | |
| } | |
| self._s["prev_net_worth"] = self.compute_net_worth() | |
| def reset(self) -> None: | |
| """Reset farm state to initial conditions (re-uses existing rng).""" | |
| self._init_state() | |
| # ────────────────────────────────────────────────────────────── | |
| # State access helpers | |
| # ────────────────────────────────────────────────────────────── | |
| def s(self) -> dict: | |
| """Direct access to the state dict.""" | |
| return self._s | |
| def month(self) -> int: | |
| return self._s["month"] | |
| def month_count(self) -> int: | |
| return self._s["month_count"] | |
| def cash(self) -> float: | |
| return self._s["cash"] | |
| def has_active_loan(self) -> bool: | |
| return self._s["has_active_loan"] | |
| # ────────────────────────────────────────────────────────────── | |
| # Action handlers — all return (penalty, message) | |
| # ────────────────────────────────────────────────────────────── | |
| def do_plant(self, action_id: int) -> Tuple[float, str]: | |
| """Plant a standard crop (action_id 1/2/3).""" | |
| s = self._s | |
| cfg = self.config | |
| crop_idx = action_id | |
| seed_cost = s["inflated_seed_costs"][crop_idx] | |
| if s["active_crop_type"] != CropType.FALLOW: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Cannot plant — land already has " | |
| f"{cfg.crop_names[s['active_crop_type']]} growing." | |
| ) | |
| if s["cash"] < seed_cost: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Not enough cash to plant " | |
| f"{cfg.crop_names[crop_idx]} " | |
| f"(need ₹{seed_cost:,.0f}, have ₹{s['cash']:,.0f})." | |
| ) | |
| s["cash"] -= seed_cost | |
| s["active_crop_type"] = crop_idx | |
| s["crop_age_months"] = 0 | |
| s["planting_month"] = s["month"] | |
| return 0.0, ( | |
| f"Planted {cfg.crop_names[crop_idx]}. Cost: ₹{seed_cost:,.0f}." | |
| ) | |
| def do_plant_hype(self, crop_type: int) -> Tuple[float, str]: | |
| """Plant a hype crop (CropType 4/5/6).""" | |
| s = self._s | |
| cfg = self.config | |
| crop_idx = int(crop_type) | |
| seed_cost = s["inflated_seed_costs"][crop_idx] | |
| if s["active_crop_type"] != CropType.FALLOW: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Cannot plant — land already has " | |
| f"{cfg.crop_names[s['active_crop_type']]} growing." | |
| ) | |
| if s["cash"] < seed_cost: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Not enough cash to plant " | |
| f"{cfg.crop_names[crop_idx]} " | |
| f"(need ₹{seed_cost:,.0f}, have ₹{s['cash']:,.0f})." | |
| ) | |
| s["cash"] -= seed_cost | |
| s["active_crop_type"] = crop_idx | |
| s["crop_age_months"] = 0 | |
| s["planting_month"] = s["month"] | |
| return 0.0, ( | |
| f"Planted {cfg.crop_names[crop_idx]} (hype crop). " | |
| f"Cost: ₹{seed_cost:,.0f}." | |
| ) | |
| def do_irrigate(self) -> Tuple[float, str]: | |
| s = self._s | |
| cfg = self.config | |
| cost = s["inflated_cost_irrigate"] | |
| if s["active_crop_type"] == CropType.FALLOW: | |
| return cfg.invalid_action_penalty, ( | |
| "INVALID: Nothing to irrigate — land is fallow." | |
| ) | |
| if s["cash"] < cost: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Not enough cash to irrigate (need ₹{cost:,.0f})." | |
| ) | |
| s["cash"] -= cost | |
| s["irrigated"] = True | |
| crop_t = s["active_crop_type"] | |
| s["water_level"] += cfg.irrigate_amount[crop_t] | |
| s["water_level"] = min(s["water_level"], cfg.optimal_water_level[crop_t]) | |
| return 0.0, ( | |
| f"Irrigated. Water level now {s['water_level']:.2f}. " | |
| f"Cost: ₹{cost:,.0f}." | |
| ) | |
| def do_fertilize(self) -> Tuple[float, str]: | |
| s = self._s | |
| cfg = self.config | |
| cost = s["inflated_cost_fertilize"] | |
| if s["cash"] < cost: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Not enough cash to fertilize (need ₹{cost:,.0f})." | |
| ) | |
| s["cash"] -= cost | |
| s["soil_nitrogen"] = min(1.0, s["soil_nitrogen"] + cfg.fertilize_nitrogen_boost) | |
| s["fertilized"] = True | |
| return 0.0, ( | |
| f"Fertilized. Soil nitrogen boosted to " | |
| f"{s['soil_nitrogen']:.2f}. Cost: ₹{cost:,.0f}." | |
| ) | |
| def do_harvest_store(self) -> Tuple[float, str, int, float]: | |
| s = self._s | |
| cfg = self.config | |
| if s["active_crop_type"] == CropType.FALLOW or s["crop_age_months"] < 1: | |
| return cfg.invalid_action_penalty, ( | |
| "INVALID: Nothing to harvest — no crop planted or crop too young." | |
| ), CropType.FALLOW, 0.0 | |
| crop_type = s["active_crop_type"] | |
| harvested = calculate_yield( | |
| crop_type, s["crop_age_months"], s["soil_nitrogen"], | |
| s["water_level"], s["planting_month"], cfg, rng=self._rng, | |
| ) | |
| parts: list[str] = [] | |
| old_type = CropType.FALLOW | |
| old_volume = 0.0 | |
| if s["stored_amount"] > 0: | |
| old_type = s["stored_crop_type"] | |
| old_volume = s["stored_amount"] | |
| parts.append( | |
| f"Displaced {old_volume:.1f} tons of {cfg.crop_names[old_type]} " | |
| "to market queue." | |
| ) | |
| s["stored_crop_type"] = crop_type | |
| s["stored_amount"] = harvested | |
| s["stored_age_months"] = 0 | |
| s["active_crop_type"] = CropType.FALLOW | |
| s["crop_age_months"] = 0 | |
| s["planting_month"] = 0 | |
| parts.append( | |
| f"Harvested {harvested:.1f} tons of {cfg.crop_names[crop_type]} " | |
| f"and stored it." | |
| ) | |
| return 0.0, " ".join(parts), old_type, old_volume | |
| def do_harvest_sell_queued(self) -> Tuple[float, str, int, float]: | |
| """ | |
| Harvest and prepare for queued sale. Does NOT credit cash. | |
| Returns (penalty, message, crop_type, harvested_amount). | |
| The caller (MultiAgentCroprlEnvironment) queues the sale in the MarketEngine. | |
| """ | |
| s = self._s | |
| cfg = self.config | |
| if s["active_crop_type"] == CropType.FALLOW or s["crop_age_months"] < 1: | |
| return cfg.invalid_action_penalty, ( | |
| "INVALID: Nothing to harvest — no crop planted or crop too young." | |
| ), 0, 0.0 | |
| crop_type = s["active_crop_type"] | |
| harvested = calculate_yield( | |
| crop_type, s["crop_age_months"], s["soil_nitrogen"], | |
| s["water_level"], s["planting_month"], cfg, rng=self._rng, | |
| ) | |
| s["active_crop_type"] = CropType.FALLOW | |
| s["crop_age_months"] = 0 | |
| s["planting_month"] = 0 | |
| return 0.0, ( | |
| f"Harvested {harvested:.1f} tons of {cfg.crop_names[crop_type]}. " | |
| f"Sale queued for month-end market clearing." | |
| ), crop_type, harvested | |
| def do_sell_inventory_queued(self) -> Tuple[float, str, int, float]: | |
| """ | |
| Queue inventory for sale. Does NOT credit cash. | |
| Returns (penalty, message, crop_type, volume). | |
| """ | |
| s = self._s | |
| cfg = self.config | |
| if s["stored_amount"] <= 0: | |
| return cfg.invalid_action_penalty, ( | |
| "INVALID: Storage is empty — nothing to sell." | |
| ), 0, 0.0 | |
| crop_t = s["stored_crop_type"] | |
| volume = s["stored_amount"] | |
| s["stored_crop_type"] = CropType.FALLOW | |
| s["stored_amount"] = 0.0 | |
| s["stored_age_months"] = 0 | |
| return 0.0, ( | |
| f"Queued {volume:.1f} tons of {cfg.crop_names[crop_t]} " | |
| f"for month-end market clearing." | |
| ), crop_t, volume | |
| def do_take_loan(self) -> Tuple[float, str]: | |
| s = self._s | |
| cfg = self.config | |
| if s["has_active_loan"]: | |
| return cfg.invalid_action_penalty, ( | |
| "INVALID: You already have an active loan. " | |
| "Repay it first before taking another." | |
| ) | |
| amount = s["inflated_loan_chunk"] | |
| s["cash"] += amount | |
| s["debt"] += amount | |
| s["has_active_loan"] = True | |
| s["loan_interest_rate"] = s["interest_rate"] | |
| return 0.0, ( | |
| f"Took a loan of ₹{amount:,.0f} at " | |
| f"{s['loan_interest_rate'] * 100:.1f}% annual. " | |
| f"Total debt: ₹{s['debt']:,.0f}." | |
| ) | |
| def do_repay_loan(self) -> Tuple[float, str]: | |
| s = self._s | |
| cfg = self.config | |
| if not s["has_active_loan"]: | |
| return cfg.invalid_action_penalty, "INVALID: No active loan to repay." | |
| if s["cash"] < s["debt"]: | |
| return cfg.invalid_action_penalty, ( | |
| f"INVALID: Not enough cash to repay full debt. " | |
| f"Need ₹{s['debt']:,.0f}, have ₹{s['cash']:,.0f}." | |
| ) | |
| repay = s["debt"] | |
| s["cash"] -= repay | |
| s["debt"] = 0.0 | |
| s["has_active_loan"] = False | |
| s["loan_interest_rate"] = 0.0 | |
| return 0.0, f"Repaid full loan of ₹{repay:,.0f}. You are now debt-free." | |
| # ────────────────────────────────────────────────────────────── | |
| # Monthly dynamics | |
| # ────────────────────────────────────────────────────────────── | |
| def advance_month(self, skip_price_generation=False) -> list[str]: | |
| """Advance all monthly dynamics. Returns list of event messages.""" | |
| s = self._s | |
| cfg = self.config | |
| messages: list[str] = [] | |
| old_month = s["month"] | |
| s["month"] = (s["month"] % 12) + 1 | |
| s["month_count"] += 1 | |
| # Inflation (year boundary) | |
| if s["month"] == 1 and old_month == 12: | |
| self.apply_inflation() | |
| s["year"] += 1 | |
| messages.append( | |
| f"Year {s['year']} begins. " | |
| f"Inflation applied ({cfg.inflation_rate * 100:.0f}%)." | |
| ) | |
| # Realise rainfall | |
| realised = realise_rainfall( | |
| s["expected_rainfall"], cfg.weather_sigma_realisation, self._rng, | |
| ) | |
| # Water level | |
| crop_t = s["active_crop_type"] | |
| s["water_level"] += realised | |
| if crop_t != CropType.FALLOW: | |
| s["water_level"] -= cfg.water_utilised_monthly[crop_t] | |
| s["water_level"] = max(0.0, s["water_level"]) | |
| s["water_level"] = min(s["water_level"], cfg.optimal_water_level[crop_t]) | |
| # Age crop & nitrogen | |
| if crop_t != CropType.FALLOW: | |
| s["crop_age_months"] += 1 | |
| s["soil_nitrogen"] += cfg.monthly_nitrogen_impact[crop_t] | |
| s["soil_nitrogen"] = max(0.0, min(1.0, s["soil_nitrogen"])) | |
| # Natural nitrogen recovery | |
| s["soil_nitrogen"] = min(1.0, s["soil_nitrogen"] + cfg.natural_nitrogen_recovery) | |
| # Storage spoilage | |
| if s["stored_amount"] > 0: | |
| s["stored_age_months"] += 1 | |
| remaining, spoiled = apply_spoilage( | |
| s["stored_age_months"], s["stored_amount"], cfg.max_storage_age | |
| ) | |
| if spoiled: | |
| messages.append( | |
| f"SPOILAGE: Your stored {cfg.crop_names[s['stored_crop_type']]} " | |
| f"({s['stored_amount']:.1f} tons) has rotted!" | |
| ) | |
| s["stored_amount"] = 0.0 | |
| s["stored_crop_type"] = CropType.FALLOW | |
| s["stored_age_months"] = 0 | |
| else: | |
| s["stored_amount"] = remaining | |
| # Interest accrual | |
| if s["has_active_loan"] and s["debt"] > 0: | |
| s["debt"] *= 1.0 + s["loan_interest_rate"] / 12.0 | |
| # Monthly fixed cost | |
| s["cash"] -= s["inflated_monthly_fixed_cost"] | |
| # Storage cost (future) | |
| if cfg.enable_storage_cost and s["stored_amount"] > 0: | |
| s["cash"] -= cfg.cost_storage_monthly * s["stored_amount"] | |
| # New prices and weather | |
| s["expected_rainfall"] = generate_rainfall(s["month"], cfg, self._rng) | |
| prev_prices = s["prices"] | |
| if not skip_price_generation: | |
| s["prices"] = generate_market_prices( | |
| s["month"], cfg, self._rng, | |
| prev_prices=prev_prices, | |
| effective_base_prices=tuple(s["inflated_base_market_prices"]), | |
| ) | |
| # Interest rate | |
| optimal_water = ( | |
| cfg.optimal_water_level[s["active_crop_type"]] | |
| if s["active_crop_type"] != CropType.FALLOW | |
| else 0.0 | |
| ) | |
| s["interest_rate"] = calculate_interest_rate( | |
| cfg.base_interest_rate, s["month"], | |
| s["expected_rainfall"], optimal_water, | |
| ) | |
| # Reset per-step flags | |
| s["irrigated"] = False | |
| s["fertilized"] = False | |
| return messages | |
| def apply_inflation(self) -> None: | |
| """Apply compounding inflation to all inflatable values.""" | |
| s = self._s | |
| factor = 1.0 + self.config.inflation_rate | |
| s["inflated_seed_costs"] = [c * factor for c in s["inflated_seed_costs"]] | |
| s["inflated_cost_irrigate"] *= factor | |
| s["inflated_cost_fertilize"] *= factor | |
| s["inflated_loan_chunk"] *= factor | |
| s["inflated_base_land_price"] *= factor | |
| s["inflated_monthly_fixed_cost"] *= factor | |
| s["inflated_base_market_prices"] = [ | |
| p * factor for p in s["inflated_base_market_prices"] | |
| ] | |
| # ────────────────────────────────────────────────────────────── | |
| # Net worth | |
| # ────────────────────────────────────────────────────────────── | |
| def compute_net_worth(self, clearing_prices: Optional[Tuple[float, ...]] = None) -> float: | |
| """net_worth = cash + land_value + stored_value + growing_value − debt""" | |
| s = self._s | |
| cfg = self.config | |
| prices_to_use = clearing_prices if clearing_prices is not None else s["prices"] | |
| land_value = s["inflated_base_land_price"] * s["soil_nitrogen"] | |
| stored_value = 0.0 | |
| if s["stored_amount"] > 0 and s["stored_crop_type"] != CropType.FALLOW: | |
| stored_value = s["stored_amount"] * prices_to_use[s["stored_crop_type"] - 1] | |
| growing_value = 0.0 | |
| if s["active_crop_type"] != CropType.FALLOW: | |
| est_yield = calculate_yield( | |
| s["active_crop_type"], s["crop_age_months"], | |
| s["soil_nitrogen"], s["water_level"], | |
| s["planting_month"] or s["month"], cfg, rng=None, | |
| ) | |
| growing_value = est_yield * prices_to_use[s["active_crop_type"] - 1] | |
| return s["cash"] + land_value + stored_value + growing_value - s["debt"] | |
| def compute_terminal_value(self) -> float: | |
| """Terminal profit: final_value − initial_value.""" | |
| cfg = self.config | |
| initial = cfg.initial_cash + (cfg.base_land_price * cfg.initial_soil_nitrogen) | |
| return self.compute_net_worth() - initial | |
Xet Storage Details
- Size:
- 19.1 kB
- Xet hash:
- f620ca1d4895938018e9269dae09abf1514d8ee05fb9d4a5d610780352aa1494
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.