From d5bbaf232650231e2f0ec0de120d7e1010a6379c Mon Sep 17 00:00:00 2001 From: bobbycxy Date: Fri, 21 Nov 2025 08:22:39 +0000 Subject: [PATCH] Initial commit from Openverse UI --- README.md | 209 ++++++++++++++++++++++++++++++++++++++++++ env.py | 242 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 14 +++ 3 files changed, 465 insertions(+) create mode 100644 README.md create mode 100644 env.py create mode 100644 pyproject.toml diff --git a/README.md b/README.md new file mode 100644 index 0000000..61ec8b2 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# TURN-BASED GAME DESIGN DOCUMENT +*(Environment Name: “Stellar Orchard”)* + +--- + +## 1. Concept Paragraph + +**Stellar Orchard** is a deterministic, turn-based strategy game where two rival horticulturists compete to cultivate the most thriving orchard on a distant exoplanet. The environment simulates a grid of orchard plots, each plot able to host an alien tree that produces **lumen fruit**—a glowing bioengineered product that yields energy points. Players must choose each turn to **Plant**, **Nurture**, or **Harvest** specific plots to maximize their total energy yield before the season ends. Weather and soil conditions are fixed by a deterministic seed at reset, ensuring reproducibility. The design, theme, and terminology are **completely unrelated to any negotiation or trading example**. + +--- + +## 2. Roles and Win Condition + +- **Player Roles:** + - *Player A (Solar Gardener)* + - *Player B (Lunar Gardener)* + Each controls their own half of the orchard (plots tagged `A1–A5` for A; `B1–B5` for B). + +- **Objective:** + Accumulate the highest **Energy Points (EP)** via strategic planting, nurturing, and harvesting of trees. + +- **Win Conditions:** + - The game ends after **10 turns** or when both players have harvested all trees. + - The player with the **highest cumulative EP** wins. + - If total EPs are equal, the result is a **draw**. + - If a player performs two invalid moves consecutively, they **forfeit** the match and lose automatically. + +--- + +## 3. Turn Structure and Determinism + +- Fixed alternating turns: Player A → Player B → Player A → ... +- Each turn, a player chooses **one valid action**. +- Game ends after **Turn 10** (each player acts 5 times). +- A `random_seed` parameter initializes soil fertility levels and initial weather pattern with reproducible deterministic effects. +- No hidden randomness during play; outcomes are computed deterministically based on seed and previous actions. + +--- + +## 4. Action Grammar (Machine-Parseable) + +All player actions are deterministic commands describing their move this turn. Actions must match **exactly one** of the grammar patterns below. + +| Action Type | Format | Description and Rule | Example (Valid) | Example (Invalid) and Reason | +|--------------|---------|---------------------|-----------------|------------------------------| +| **Plant** | `Plant:` | Plants a new tree on the specified empty plot. `` ∈ {A1–A5 (for Player A), B1–B5 (for Player B)} | `Plant:A3` | `Plant:C2` → Invalid (nonexistent plot) | +| **Nurture** | `Nurture:` | Boosts growth stage of one existing, unharvested tree on a valid occupied plot. | `Nurture:B4` | `Nurture:B6` → Invalid (out of range) | +| **Harvest** | `Harvest:` | Harvests a fully grown tree from the specified plot, removing it and collecting EP. | `Harvest:A1` | `Harvest:A1,A2` → Invalid (multiple plots not allowed) | +| **Pass** | `Pass` | Player skips the turn intentionally (strategic or necessary if no valid move). | `Pass` | `[Pass]` → Invalid (extra brackets not part of syntax) | + +**Regular Expression Patterns** +- `^Plant:(A[1-5]|B[1-5])$` +- `^Nurture:(A[1-5]|B[1-5])$` +- `^Harvest:(A[1-5]|B[1-5])$` +- `^Pass$` + +All valid actions will be wrapped by players inside `\boxed{{}}` at runtime, e.g., `\boxed{{Harvest:A1}}`. + +--- + +## 5. Game State Schema + +Example `game_state` format: + +```json +{ + "turn_number": 3, + "max_turns": 10, + "active_player": "Solar Gardener", + "plots": { + "A1": {"owner": "A", "status": "grown", "growth_level": 3}, + "A2": {"owner": "A", "status": "empty", "growth_level": 0}, + "A3": {"owner": "A", "status": "seedling", "growth_level": 1}, + "B1": {"owner": "B", "status": "grown", "growth_level": 3}, + "B2": {"owner": "B", "status": "harvested", "growth_level": 0} + }, + "energy_points": { + "A": 15, + "B": 12 + }, + "soil_fertility": { + "A1": 0.9, + "A2": 0.6, + "B1": 0.8 + }, + "weather_pattern": "Radiant Skies", + "transcript": [ + {"player": "A", "action": "Plant:A3"}, + {"player": "B", "action": "Nurture:B1"} + ], + "winner": null, + "random_seed": 57 +} +``` + +--- + +## 6. Initialization Rules + +- `random_seed` is set during environment reset; it governs deterministic generation of: + - `soil_fertility` for each plot (each between 0.5 and 1.0). + - `weather_pattern` (chosen deterministically from a fixed set: Radiant Skies / Lunar Mist / Crystal Winds). +- `energy_points[A]` and `[B]` start at **0**. +- All plots start as `"empty"`. +- Observations include the common introduction, initial weather, soil fertility summary, and allowed actions. + +--- + +## 7. Validation and Error Handling + +On every step: + +1. Extract literal content from `\boxed{{...}}` using `_extract_answer_content`. +2. Validate syntax against regex patterns. +3. Check logical validity: + - **Plant** → must target empty plot owned by that player. + - **Nurture** → must target already planted, not yet grown tree. + - **Harvest** → must target grown, unharvested tree. + - **Pass** → always valid. +4. Any violation triggers `set_invalid_move(player, reason)`, e.g.: + - `"Invalid format"` + - `"Plot not owned by player"` + - `"Plot already occupied"` + - `"Tree not ready to harvest"` + +--- + +## 8. Terminal Conditions and Scoring + +- **Automatic End:** + - When `turn_number >= max_turns` + - OR when all plots are `harvested` or `empty` (no active trees remain) + +- **Scoring:** + - For each harvested tree: `EP += int(10 * soil_fertility[plot])`. + - No fractional points. + - Final score is the total `EP` accumulated. + +- **Result Determination:** + - Higher total → **Winner**. + - Equal totals → **Draw**. + - Consecutive double invalid move by a player → automatic **Loss**. + +--- + +## 9. Player Prompt Specification + +**Identity Context:** +“You are a cosmic horticulturist tending bioluminescent trees on the exoplanet Selora. Your goal is to maximize your orchard’s energy yield before the season ends.” + +**Prompt Structure Includes:** +- Current turn number and remaining turns. +- Your current EP and plots' status summary. +- Weather pattern and soil fertility hints. +- List of all valid actions: + - `Plant:` + - `Nurture:` + - `Harvest:` + - `Pass` +- Explicit response requirement: “Put your final answer within `\boxed{{}}` at the end of your response.” + +**Few-shot Examples:** +``` +Example valid response: +I will start by planting my first tree in plot A2. +\boxed{{Plant:A2}} +``` + +``` +Example invalid response: +Let's see how this goes! +\boxed{{Grow:A2}} # Invalid because 'Grow' is not a recognized action type. +``` + +**Dialogue Elements:** None (pure command game). All player utterances (including justification text) get appended to transcript for transparency. + +--- + +## 10. API Mapping Plan + +- **`reset(seed: Optional[int])`** + - Initializes `game_state` using the deterministic seed. + - Builds initial soil fertility and weather data. + - Clears all plots, transcript, and EPs. + - Returns the observation containing theme introduction and available actions. + +- **`step(action: str)`** + - Extracts action content via `_extract_answer_content`. + - Validates syntax + legality. + - Updates game state deterministically (adjust plot status, growth level, EPs). + - Adds the player’s message and resulting action to transcript. + - Advances to the next turn or marks the game as terminal if conditions met. + - Returns updated observation, reward, and termination flag. + +- **`_generate_player_prompt(player_id)`** + - Constructs text prompt including: + - Game identity summary + - Current turn, soil info, plot statuses + - Weather condition and EP scores + - List of valid actions and formatting requirement + - Few-shot examples above + - Returns formatted prompt string. + +--- + +## 11. Copy-Check Against the Example + +All terms—**plots**, **trees**, **lumen fruit**, **Energy Points**, **soil fertility**, **weather pattern**, **Solar/Lunar Gardeners**, and **orchard management theme**—are *original* and entirely **unrelated to any negotiation, trading, or economic dialogue example**. +The `game_state` keys (`plots`, `soil_fertility`, `energy_points`, etc.) and all action types are unique to **Stellar Orchard** and do not replicate any element from other sample environments. \ No newline at end of file diff --git a/env.py b/env.py new file mode 100644 index 0000000..06c79a4 --- /dev/null +++ b/env.py @@ -0,0 +1,242 @@ +```python +import re +import random +from typing import Any, Dict, Optional, Tuple, List + +import textarena as ta + + +class StellarOrchardEnv(ta.Env): + """ + Stellar Orchard – Turn-based deterministic horticulture strategy game + Implements Stage 1 specification of "Stellar Orchard" environment. + """ + + def __init__(self, max_turns: int = 10): + self.max_turns = max_turns + # Grammar – exactly per Stage 1 + self.patterns = { + "Plant": re.compile(r"^Plant:(A[1-5]|B[1-5])$"), + "Nurture": re.compile(r"^Nurture:(A[1-5]|B[1-5])$"), + "Harvest": re.compile(r"^Harvest:(A[1-5]|B[1-5])$"), + "Pass": re.compile(r"^Pass$"), + } + self.weather_types = ["Radiant Skies", "Lunar Mist", "Crystal Winds"] + + # ---------------------------------------------------------------------- + # Helper: Extract boxed content + def _extract_answer_content(self, action: str) -> str: + """Extract literal content inside \\boxed{{...}}.""" + match = re.search(r"\\boxed\{\{(.*?)\}\}", action, re.DOTALL) + if match: + return match.group(1).strip() + # fallback to direct content + return action.strip() + + # ---------------------------------------------------------------------- + def reset(self, num_players: int, seed: Optional[int] = None): + """ + Resets the environment to an initial state. + + Args: + num_players: Must be 2 for Stellar Orchard. + seed: Optional deterministic seed. + + Returns: + None + """ + if num_players != 2: + raise ValueError("Stellar Orchard requires exactly 2 players.") + + self.state = ta.TwoPlayerState(num_players=num_players, seed=seed, max_turns=self.max_turns, error_allowance=1) + random.seed(seed) + + # deterministic environment setup + soil_fertility = {f"A{i}": random.uniform(0.5, 1.0) for i in range(1, 6)} + soil_fertility.update({f"B{i}": random.uniform(0.5, 1.0) for i in range(1, 6)}) + weather_pattern = self.weather_types[seed % len(self.weather_types)] if seed is not None else random.choice(self.weather_types) + + plots: Dict[str, Dict[str, Any]] = {} + for pid, owner in [("A", "A"), ("B", "B")]: + for i in range(1, 6): + plots[f"{pid}{i}"] = {"owner": owner, "status": "empty", "growth_level": 0} + + game_state = { + "turn_number": 0, + "max_turns": self.max_turns, + "active_player": "Solar Gardener", + "plots": plots, + "energy_points": {"A": 0, "B": 0}, + "soil_fertility": soil_fertility, + "weather_pattern": weather_pattern, + "transcript": [], + "winner": None, + "random_seed": seed if seed is not None else random.randint(0, 100000), + } + + role_mapping = {0: "Solar Gardener", 1: "Lunar Gardener"} + self.state.reset(game_state=game_state, player_prompt_function=self._generate_player_prompt, role_mapping=role_mapping) + + self.state.add_observation("Welcome to Stellar Orchard!", ta.ObservationType.GAME_MESSAGE) + self.state.add_observation(f"Weather pattern: {weather_pattern}", ta.ObservationType.GAME_MESSAGE) + return None + + # ---------------------------------------------------------------------- + def _generate_player_prompt(self, player_id: int, game_state: Dict[str, Any]) -> str: + """Construct a prompt for a player according to design spec.""" + role = "Solar Gardener" if player_id == 0 else "Lunar Gardener" + player_key = "A" if player_id == 0 else "B" + opposite_key = "B" if player_id == 0 else "A" + + # summarize plots + plot_summary = "\n".join( + [f"{pid}: {info['status']} (growth {info['growth_level']})" for pid, info in game_state["plots"].items() if info["owner"] == player_key] + ) + + soil_summary = ", ".join([f"{pid}:{game_state['soil_fertility'][pid]:.2f}" for pid in game_state["soil_fertility"] if pid.startswith(player_key)]) + + ep = game_state["energy_points"][player_key] + weather = game_state["weather_pattern"] + remaining_turns = game_state["max_turns"] - game_state["turn_number"] + + valid_actions = ( + "Possible actions this turn:\n" + " - Plant:\n" + " - Nurture:\n" + " - Harvest:\n" + " - Pass" + ) + + instr = ( + f"You are the {role}, a cosmic horticulturist tending glowing alien trees on Selora.\n" + f"Your goal is to maximize Energy Points (EP) by cultivating your plots before the season ends.\n\n" + f"Current Weather: {weather}\n" + f"Soil Fertility (your plots): {soil_summary}\n" + f"Your Energy Points: {ep}\n" + f"Your Orchard Status:\n{plot_summary}\n\n" + f"Turn: {game_state['turn_number']} | Remaining turns: {remaining_turns}\n\n" + f"{valid_actions}\n\n" + "Put your final answer within \\boxed{{}} at the end of your response.\n\n" + "Example valid response:\n" + "I will plant my first tree in A2.\n" + "\\boxed{{Plant:A2}}\n\n" + "Example invalid response:\n" + "Let's go!\n" + "\\boxed{{Grow:A2}} # Invalid keyword" + ) + + return instr + + # ---------------------------------------------------------------------- + def step(self, action: str) -> Tuple[bool, ta.Info]: + """ + Perform a single turn step with validation and deterministic game update. + """ + player_id = self.state.current_player_id + player_symbol = "A" if player_id == 0 else "B" + role_name = "Solar Gardener" if player_symbol == "A" else "Lunar Gardener" + + self.state.add_observation(action, ta.ObservationType.PLAYER_ACTION, from_id=player_id, to_id=-1) + game_state = self.state.game_state + + literal = self._extract_answer_content(action) + + # record transcript + game_state["transcript"].append({"player": player_symbol, "action": literal}) + + # validation + valid_type = None + for k, pat in self.patterns.items(): + if pat.match(literal): + valid_type = k + break + if valid_type is None: + self.state.set_invalid_move(reason="Invalid format") + return self.state.step() + + # process the deterministic mechanics + plots = game_state["plots"] + action_valid = True + reason_if_invalid = "" + target_plot = None + if valid_type != "Pass": + target_plot = literal.split(":")[1] + + if not target_plot.startswith(player_symbol): + action_valid = False + reason_if_invalid = "Plot not owned by player." + + if not action_valid: + self.state.set_invalid_move(reason=reason_if_invalid) + return self.state.step() + + # Logic for each action type + if valid_type == "Plant": + plot = plots[target_plot] + if plot["status"] != "empty": + self.state.set_invalid_move(reason="Plot already occupied.") + return self.state.step() + plot["status"] = "seedling" + plot["growth_level"] = 1 + elif valid_type == "Nurture": + plot = plots[target_plot] + if plot["status"] not in ["seedling", "growing"]: + self.state.set_invalid_move(reason="No tree to nurture.") + return self.state.step() + if plot["growth_level"] >= 3: + self.state.set_invalid_move(reason="Tree already fully grown.") + return self.state.step() + plot["growth_level"] += 1 + plot["status"] = "grown" if plot["growth_level"] >= 3 else "growing" + elif valid_type == "Harvest": + plot = plots[target_plot] + if plot["status"] != "grown": + self.state.set_invalid_move(reason="Tree not ready to harvest.") + return self.state.step() + # deterministic energy gain + gain = int(10 * game_state["soil_fertility"][target_plot]) + game_state["energy_points"][player_symbol] += gain + plot["status"] = "harvested" + plot["growth_level"] = 0 + elif valid_type == "Pass": + pass # nothing else happens + + # increment turn number + game_state["turn_number"] += 1 + game_state["active_player"] = "Solar Gardener" if player_symbol == "B" else "Lunar Gardener" + + # terminal checks + if self._check_terminal_conditions(): + return self.state.step() + + done, info = self.state.step() + return done, info + + # ---------------------------------------------------------------------- + def _check_terminal_conditions(self) -> bool: + """Check game end (turn limit or all plots empty/harvested) and set outcome.""" + game_state = self.state.game_state + if self.state.done: + return True + + plots = game_state["plots"] + all_passive = all(p["status"] in ["empty", "harvested"] for p in plots.values()) + if all_passive or game_state["turn_number"] >= game_state["max_turns"]: + ep = game_state["energy_points"] + if ep["A"] == ep["B"]: + self.state.set_draw(reason="Equal Energy Points. Draw.") + else: + winner = 0 if ep["A"] > ep["B"] else 1 + self.state.set_winner(player_id=winner, reason=f"Player {winner} had more Energy Points.") + return True + return False + + # ---------------------------------------------------------------------- + def get_observation(self) -> Tuple[int, List]: + """Return current player's observation tuple.""" + return self.state.current_player_id, self.state.observations + + def close(self) -> Tuple[Dict, Dict]: + """Finalize episode outputs.""" + return self.state.rewards, self.state.game_info +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2a116a7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +# pyproject.toml + +[project] +name = "game_20251121_082111" +version = "0.1.0" +description = "TURN-BASED GAME DESIGN DOCUMENT environment generated for TextArena." +dependencies = [ + "textarena>=0.7.3" +] + +[openverse] +entry_point = "env:StellarOrchardEnv" +tags = ["openverse", "generated"] +author = "Openverse"