diff --git a/Shayan1/Prod/docs/DOCS-2.md b/Shayan1/Prod/docs/DOCS-2.md new file mode 100644 index 0000000..01934da --- /dev/null +++ b/Shayan1/Prod/docs/DOCS-2.md @@ -0,0 +1,337 @@ +# Repository Guidelines + +> Onboarding and operating guide for AI assistants working in this repository. Everything here is derived from the actual source (`nash/`, `main.py`, `tests/`, `games/`) and is intended to be authoritative. When the code and this document disagree, trust the code and update this file. + +## Project Overview + +This is a **small, dependency-light Python 3.13 command-line tool** that **solves and simulates Nash equilibria for normal-form (simultaneous-move) games**. + +- **Input:** a game is *data*, not code — a JSON file describing players, their strategies, and a payoff tensor per player (`games/*.json`). +- **Solve:** find all pure-strategy Nash equilibria (brute force, any number of players) and, for 2-player games, mixed-strategy equilibria (support enumeration). +- **Simulate:** run dynamic simulations for N rounds to observe convergence. Supports discrete agent models (best response/fictitious play, regret matching, softmax, Q-learning, or LLM decisions) and population-level evolutionary dynamics (replicator dynamics). +- **Inspect & Reduce:** inspect game structures, dominant strategies, and iteratively eliminate strictly dominated strategies (IESDS) to find the rationalizable core. +- **Report:** print per-round play, cross-check convergence against the true equilibria, and chart long-run strategy usage or population trajectories to a PNG. + +The design goal is a reusable library (`nash/`) plus a thin CLI (`main.py`). The optional AI translation layer (`nash/translate.py`) is the only networked component and is fully decoupled from solving/simulation. + +**What it is NOT:** there is no web server, no database, no build step, no package publishing, no async, and no framework. It is plain scripts executed through a local virtualenv. + +## Architecture & Data Flow + +``` + main.py (argparse CLI + orchestration) + │ + ┌───────────────────────────┴───────────────────────────┐ + │ load_game(args) │ + │ --game → Game.load_from_json(path) │ + │ --describe → translate.describe_to_json(text)│ ← only network call + │ (validates by constructing Game)│ + └───────────────────────────┬───────────────────────────┘ + ▼ + Game (nash/game.py) + payoff tensors: one np.ndarray per player + │ + ┌───────────────────────────┴───────────────────────────┐ + │ print_solution(game) │ + │ solver.find_pure_nash_equilibria(game) [any N] │ + │ solver.find_mixed_nash_equilibria(game) [2-player] │ + └───────────────────────────┬───────────────────────────┘ + ▼ + Simulation(game, rounds, memory, rng).run() (nash/simulation.py) + drives BestResponsePlayer per player (nash/players.py) + → self.history, self.payoff_history + │ + ┌───────────────────────────┴───────────────────────────┐ + │ print_comparison(game, simulation, pure_eq) │ + │ converged_profile() cross-checked vs pure equilibria │ + │ long-run strategy_frequencies() printed │ + │ visualize.plot_strategy_frequencies(...) → PNG │ + └───────────────────────────────────────────────────────┘ +``` + +### Stage details + +1. **Load & Reduce.** `Game.__init__` validates that `len(players) == len(strategies) == len(payoffs)`, converts payoffs to numpy arrays, and checks dimensions. If `--reduce` is passed, `solver.reduce_game(game)` iteratively deletes strictly dominated strategies, updating the payoff tensors and returning a reduced game + reduction logs. +2. **Inspect.** If `--inspect` is passed, the CLI prints the game details, payoffs (colored by polarity), and strictly dominant strategies (if any) via `solver.find_dominant_strategies`, then exits. +3. **Solve (theoretical).** Pure equilibria: brute-force search over all profiles. Mixed equilibria (2-player only): support enumeration via indifference linear systems. +4. **Simulate (dynamic / evolutionary).** + - **Agents mode:** runs `Simulation` where each player (selected via `--player`) implements a decision model (`choose(history)`): + - `best_response`: fictitious play (memory=on) or best response to previous round (memory=off). + - `regret_matching`: chooses strategies based on cumulative positive regrets. + - `softmax`: probabilistic best response weighted by temperature. + - `q_learning`: uses temporal-difference learning on action-values. + - `llm`: queries the Anthropic Claude API for reasoning and strategy index choice. + - **Evolutionary mode:** runs `EvolutionarySimulation` simulating replicator dynamics on a population strategy distribution over time. +5. **Report.** Agent simulations check for convergence windows and compare convergence to actual Nash equilibria. Evolutionary simulations show final population distributions. Frequencies/trajectories are plotted to a PNG. +### Core invariant + +**Games are data, not code.** Adding a 3+ player game requires *no code changes* — only a JSON file whose payoff tensors have the matching shape. The tensor representation (`payoffs[i][profile]`) is what makes the pure solver and the simulation generalize to any N. Do not hardcode scenario details inside `nash/`. + +## Key Directories + +| Path | Purpose | +|------|---------| +| `nash/` | The importable core library (the package). | +| `nash/game.py` | `Game` — load/validate games, payoff-tensor indexing, iteration, and utilities. | +| `nash/players.py` | `BasePlayer` & implementations (`BestResponsePlayer`, `SoftmaxBestResponsePlayer`, `RegretMatchingPlayer`, `QLearningPlayer`, `LLMPlayer`), plus the `make_player` factory. | +| `nash/simulation.py` | `Simulation` (agent-based) and `EvolutionarySimulation` (replicator dynamics). | +| `nash/solver.py` | Pure/mixed equilibrium solvers, dominant strategy finder (`find_dominant_strategies`), and game reduction (`reduce_game`). | +| `nash/translate.py` | `describe_to_json` — the **only** network/API call (anthropic SDK). Decoupled by design. | +| `nash/visualize.py` | Prints CLI outputs, plots agent frequencies (`plot_strategy_frequencies`), and plots evolutionary trajectories (`plot_evolutionary_trajectories`). | +| `nash/__init__.py` | Package marker. Imports/exports are performed explicitly in `main.py` (`from nash import solver, visualize`, etc.). | +| `main.py` | CLI entry point and full pipeline orchestration. | +| `games/` | Sample game JSON files. This is where new scenarios live. | +| `tests/` | `pytest` tests for the solver, players, logging, and simulations. | +| `.venv/` | Local virtualenv (system Python is externally managed). Ignored by git. | +| `requirements.txt` | Runtime + dev dependencies. | +| `CLAUDE.md` | Human-facing guidance from the original author; complements this file. | + + +## Development Commands + +### Environment setup + +Dependencies are installed into a local venv because the system Python is externally managed: + +```bash +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +``` + +`requirements.txt` currently lists: `numpy`, `matplotlib`, `pytest`, and `anthropic` (the last is **only** needed for `--describe`). + +### Running the tool + +```bash +.venv/bin/python main.py --game games/pricing.json --rounds 30 --memory on --seed 1 +``` + +Exactly one of `--game` / `--describe` is required (they are a mutually exclusive, required group). See **CLI Reference** below for all flags. + +### Running tests + +```bash +.venv/bin/python -m pytest tests/ # all tests +.venv/bin/python -m pytest tests/test_solver.py::test_pricing_has_one_pure_equilibrium_low_low +``` + +There is **no build, lint, or format step** in this project. Do not introduce one unless explicitly requested. + +## Code Conventions & Common Patterns + +- **Naming & style:** PEP 8. `snake_case` for functions/attributes, `CamelCase` for classes. **Type hints are not used anywhere in the codebase** — match this bare style; do not sprinkle annotations into existing files inconsistently. +- **Output channel:** user-facing output uses `print` (in `main.py` and `nash/visualize.py`). There is no `logging` configured; do not introduce it without reason. +- **Error model:** library code raises standard exceptions and lets them propagate to the CLI. There is no custom exception hierarchy. + - `Game.__init__` → `ValueError` on length mismatch or payoff-shape mismatch. + - `find_mixed_nash_equilibria` → `NotImplementedError` when `num_players != 2` (intentional, see gotchas). + - `translate._extract_json` → `ValueError` when no JSON object is found in the model response. + - `numpy.linalg.LinAlgError` inside mixed solving is caught internally and converted to "no equilibrium on this support" (`None`). +- **RNG reproducibility (critical):** randomness is *injected*, never global. `main.py` creates exactly one `random.Random(args.seed)` and passes it into `Simulation(rng=...)`, which forwards the **same** instance to every `BestResponsePlayer`. Tie-breaking among equally-good best responses uses `rng.choice`, so a fixed `--seed` makes a run fully deterministic end to end. **Always thread `rng`/`random.Random` down the call chain; never call `random.*` module functions directly in library code.** +- **N-player support is a hard constraint:** the pure solver and the simulation must keep working for any N players. The mixed solver is **2-player only by deliberate design** (support enumeration; there is no generalized Lemke-Howson past 2 players). Do not "fix" the `NotImplementedError` by silently degrading or returning partial results. +- **Memory switch:** `BestResponsePlayer(memory=True)` (default) = fictitious play against the full history; `memory=False` = best response to only the prior round and can cycle forever (e.g. Rock-Paper-Scissors, Matching Pennies). Keep this semantics if you touch `choose`. +- **Tie tolerance:** `_best_response` uses `tol=1e-9` when collecting equally-optimal strategies; mixed solving uses `tol=1e-7`. Preserve these tolerances when modifying equilibrium math. +- **AI-layer decoupling:** `translate.describe_to_json` is reachable *only* through `main.py --describe`. Before returning, it constructs a `Game` from the parsed JSON, so a malformed translation fails visibly at parse time rather than producing a silently wrong simulation. It never imports `solver` or `simulation`. +- **Module structure:** `main.py` wires everything; `nash/` modules expose functions/classes and avoid side effects at import. `visualize.py` sets `matplotlib.use("Agg")` at import so it works headless. + +## Important Files & Public API + +### `nash/game.py` — `Game` + +```python +Game(players: list[str], strategies: list[list[str]], payoffs: list) # raises ValueError on mismatch +Game.load_from_json(path: str) -> Game +game.players # list[str] +game.strategies # list[list[str]] +game.num_players # int +game.payoffs # list[np.ndarray]; payoffs[i].shape == tuple(strategy counts) +game.strategy_counts() -> list[int] +game.payoff(player_index: int, profile: tuple[int, ...]) -> float # payoffs[player_index][profile] +game.all_profiles() -> itertools.product # all strategy-index tuples +game.strategy_name(player_index: int, strategy_index: int) -> str +``` + +### `nash/players.py` — Players & Models + +```python +BestResponsePlayer(game: Game, player_index: int, memory: bool = True, rng: random.Random | None = None) +SoftmaxBestResponsePlayer(game: Game, player_index: int, memory: bool = True, rng: random.Random | None = None, temperature: float = 0.1) +RegretMatchingPlayer(game: Game, player_index: int, memory: bool = True, rng: random.Random | None = None) +QLearningPlayer(game: Game, player_index: int, memory: bool = True, rng: random.Random | None = None, alpha: float = 0.1, gamma: float = 0.9, epsilon: float = 0.1) +LLMPlayer(game: Game, player_index: int, rng: random.Random | None = None, debug_logger: callable | None = None, model: str = "claude-3-5-sonnet-20240620") + +# All players support the common interface: +player.choose(history: list[tuple[int, ...]]) -> int # Choose strategy index for this round +player.update(profile: tuple[int, ...], payoffs: tuple[float, ...]) # Update internal state (e.g. Q-table, regret) based on outcomes +``` + +### `nash/simulation.py` — `Simulation` & `EvolutionarySimulation` + +```python +Simulation(game: Game, num_rounds: int, memory: bool = True, + rng: random.Random | None = None, convergence_window: int = 5, + player_kind: str = "best_response", debug_logger: callable | None = None) +sim.run() -> list[tuple[int, ...]] # populates .history and .payoff_history +sim.history # list[tuple[int, ...]] +sim.payoff_history # list[tuple[float, ...]] +sim.converged_round() -> int | None # first index of a window-long stable streak +sim.converged_profile() -> tuple[int, ...] | None +sim.strategy_frequencies() -> list[list[list[float]]] # [player][strategy][round] running frequency + +EvolutionarySimulation(game: Game, num_rounds: int) +ev_sim.run() -> list[list[np.ndarray]] # runs replicator dynamics, populates .history +ev_sim.populations # list[np.ndarray] representing the population fraction of each strategy per player +ev_sim.history # list[list[np.ndarray]] of population trajectories +``` + +### `nash/solver.py` + +```python +find_pure_nash_equilibria(game: Game) -> list[tuple[int, ...]] # brute force, any N +find_mixed_nash_equilibria(game: Game, tol: float = 1e-7) -> list[tuple[np.ndarray, np.ndarray]] + # raises NotImplementedError if game.num_players != 2 +find_dominant_strategies(game: Game) -> list[int | None] # returns dominant strategy index (or None) for each player +reduce_game(game: Game) -> tuple[Game, list[str]] # iteratively eliminates strictly dominated strategies (IESDS), returns (reduced_game, logs) +``` + +### `nash/translate.py` + +```python +describe_to_json(description: str, model: str = "claude-sonnet-5") -> dict # calls anthropic +save_json(data: dict, path: str) -> None +``` +`describe_to_json` returns the raw `dict` (the same shape `Game` expects). It is only invoked from `main.load_game` when `--describe` is set. + +### `nash/visualize.py` + +```python +print_round(round_num, game, profile, payoffs) -> None +print_history(game, simulation) -> None +plot_strategy_frequencies(game, simulation, save_path) -> str # matplotlib Agg, returns save_path +plot_evolutionary_trajectories(game, evo_sim, save_path) -> str # matplotlib Agg, returns save_path +``` + +### `main.py` (entry point) + +```python +build_arg_parser() -> argparse.ArgumentParser +load_game(args) -> Game +print_solution(game) -> tuple[list, list] # (pure_eq, mixed_eq) +print_comparison(game, simulation, pure_eq) -> None +main() # __main__ entry +``` + +## Runtime / Tooling Preferences + +- **Python:** 3.13 (from `.venv`). Run everything via `.venv/bin/python` / `.venv/bin/pip`. Never use the system interpreter (externally managed). +- **No Node/Bun, no Rust, no build system, no `pyproject.toml`/`setup.cfg`/`tox.ini`.** Plain scripts + venv. +- **Dependencies:** `numpy` (payoff math/tensors), `matplotlib` (charts, Agg backend), `pytest` (tests), `anthropic` (only for `--describe`). +- **`--describe` prerequisites:** `anthropic` installed **and** `ANTHROPIC_API_KEY` set in the environment. Default model is `"claude-sonnet-5"`. +- **Charts:** written headlessly to a PNG (default `strategy_usage.png`, override with `--out`). No interactive display. + +## Testing & QA + +- **Framework:** `pytest`, executed as `.venv/bin/python -m pytest tests/`. No `conftest.py`, no markers, no coverage config. +- **Conventions:** + - Tests are module-level `test_*` functions. + - They load **real JSON fixtures** via `Game.load_from_json("games/.json")` (paths are relative to the repo root / CWD). + - Determinism is achieved by injecting `random.Random()` into `Simulation(rng=...)` — **mirror this; never rely on global `random`** in tests. + - Assertions use exact tuple/profile equality for equilibria and `numpy.allclose(..., atol=...)` for mixed probabilities. +- **Current coverage:** + - `tests/test_solver.py`: + - pure NE brute force: 2-player PD (`pricing`) → `[(1,1)]`; zero-sum with none (`matching_pennies`) → `[]`; 3-player coordination → `(0,0,0)` present. + - mixed NE == pure (PD): `(0,1)`, `(0,1)` within `1e-6`. + - mixed NE is 50/50 (Matching Pennies) within `1e-6`. + - mixed solver rejects >2 players with `pytest.raises(NotImplementedError)`. + - `tests/test_simulation.py`: + - `pricing` converges to `(Low, Low)` under `memory=True` (seed 1, 10 rounds). + - `matching_pennies` long-run frequencies trend toward 0.5 for both players under `memory=True` (seed 7, 300 rounds, tolerance 0.15). +- **Gaps to cover when extending:** + - No tests for `memory=False` cycling behavior. + - No tests for tie-breaking among equally good best responses (depends on seeded `rng`). + - No tests for Rock-Paper-Scissors dynamics or its mixed result. + - No tests for `print_comparison`'s convergence cross-check / fallback messaging. + - `translate.describe_to_json` needs network + API key; do **not** hit it in CI. If you need coverage, test the internal `_extract_json` parser (pure, offline) instead. +- **Expectation:** when you change solver/simulation/player behavior, add or update a `test_*` that asserts the *observable contract* (equilibrium profiles, final frequencies within a tolerance, or the raised exception) — not a snapshot of current printed output. + +## JSON Game Schema + +Every game is a JSON object with three top-level keys. The shape of `payoffs` is the only part that must be computed carefully. + +```json +{ + "players": ["Company A", "Company B"], + "strategies": [["High", "Low"], ["High", "Low"]], + "payoffs": [ + [[8, 2], [10, 5]], + [[8, 10], [2, 5]] + ] +} +``` + +### Rules + +- `players`: ordered list of player-name strings. Its length is `num_players`. +- `strategies`: parallel list; `strategies[i]` is the ordered list of player `i`'s strategy names. Order defines the integer indices used everywhere else. +- `payoffs`: parallel list; `payoffs[i]` is player `i`'s payoff tensor. For N players it is nested lists with **one dimension per player**, each dimension sized to that player's strategy count. So `payoffs[i].shape == (len(strategies[0]), len(strategies[1]), ...)`. +- **Indexing convention (2-player):** `payoffs[i][a][b]` = payoff to player `i` when player 0 plays strategy index `a` and player 1 plays strategy index `b` (rows = player 0, columns = player 1). +- A profile is a `tuple` of strategy indices, one per player, used directly to index the tensor: `payoffs[i][profile]`. +- `Game.__init__` raises `ValueError` if the lengths don't align or any payoff tensor's shape is wrong — so validate by just constructing the `Game`. + +### 3-player example (proves the "data, not code" invariant) + +```json +{ + "players": ["A", "B", "C"], + "strategies": [["x", "y"], ["x", "y"], ["x", "y"]], + "payoffs": [ + [[[1,0],[0,0]], [[0,0],[0,0]]], + [[[1,0],[0,0]], [[0,0],[0,0]]], + [[[1,0],[0,0]], [[0,0],[0,0]]] + ] +} +``` +Each payoff tensor is shape `(2,2,2)`; `payoffs[0][0][0][0]` is player A's payoff when all three play `x`. No code change is needed to support this — only the file. + +## CLI Reference + +| Flag | Type | Default | Notes | +|------|------|---------|-------| +| `--game ` | str | — | Path to a game JSON file. Mutually exclusive with `--describe`; one of the two is **required**. | +| `--describe ` | str | — | Plain-English scenario; translated to JSON via the anthropic API. Requires `anthropic` + `ANTHROPIC_API_KEY`. Mutually exclusive with `--game`. | +| `--rounds ` | int | `50` | Number of simulation rounds. | +| `--memory ` | choice | `on` | `on` = fictitious play (full history); `off` = best response to previous round only. | +| `--seed ` | int | `None` | Seeds the `random.Random` used for the whole run (reproducibility). `None` → non-deterministic. | +| `--out ` | str | `strategy_usage.png` | Where the strategy-usage chart PNG is saved. | +| `--save-game ` | str | `games/generated.json` | Output path for the AI-translated JSON; only used with `--describe`. | +| `--inspect` | flag | — | Print game details, payoff matrix, and dominant strategies, then exit. | +| `--reduce` | flag | — | Iteratively eliminate strictly dominated strategies (IESDS matrix reduction) before simulation or inspection. | +| `--verbose` | flag | — | Print per-round debug data (including agent reasoning and Q-learning updates) during simulation. | +| `--player ` | choice | `best_response` | Player decision model for simulation (default: `best_response`). | +| `--sim-type ` | choice | `agents` | Type of simulation: `agents` (default, multi-agent model simulation) or `evolutionary` (replicator dynamics). | + +Example runs: + +```bash +.venv/bin/python main.py --game games/rock_paper_scissors.json --rounds 100 --memory on --seed 42 +.venv/bin/python main.py --describe "Two firms choose High or Low price" --save-game games/my_game.json +``` + +## Extension Recipes + +**Add a new game (no code change):** create `games/.json` following the schema above, then run `main.py --game games/.json`. The pure solver and simulation work for any number of players automatically. + +**Add a new player decision rule:** implement a class with the same `choose(history) -> int` interface as `BestResponsePlayer` and pass instances into a `Simulation` — but note `Simulation` currently constructs its own `BestResponsePlayer`s internally, so you would extend `Simulation.__init__` (or its player-factory) to accept pluggable players. Preserve the `rng` injection pattern. + +**Add a new equilibrium algorithm:** put it in `nash/solver.py` as a free function taking a `Game` and returning equilibria in the same shapes as the existing solvers (`list[tuple[int,...]]` for pure, `list[tuple[np.ndarray, np.ndarray]]` for mixed). Wire it into `main.print_solution` if it should be reported. Keep mixed solvers 2-player-only unless you implement a real N-player method. + +**Change reporting/charts:** edit `nash/visualize.py` (print helpers + `plot_strategy_frequencies`) and `main.print_comparison`. Keep the convergence cross-check logic intact — it exists to compensate for the heuristic's inability to detect "fake" convergence in games with no pure equilibrium. + +## Known Limitations & Gotchas + +- **Mixed solver is 2-player only by design.** Calling `find_mixed_nash_equilibria` on `num_players != 2` raises `NotImplementedError`. This is intentional, not a TODO. +- **Mixed solver assumes non-degenerate games** (only tries equal-size supports for both players). Degenerate games with unequal support sizes are a known, unhandled limitation. +- **Convergence detection is a heuristic.** `converged_round()` only looks for `convergence_window` consecutive identical profiles. In games with no pure equilibrium (Matching Pennies, RPS under `memory=on`), fictitious play can show long streaks that later flip. `main.print_comparison` is the safety net: it cross-checks against the true pure equilibria and, on mismatch, reports long-run frequencies instead of claiming a false equilibrium. +- **`memory=off` can cycle forever** and may never produce a stable window; for such games `converged_round()` returns `None`. +- **No type hints, no linter, no formatter.** Consistency comes from human/AI discipline, not tooling. Match the existing style. +- **Tests load fixtures by relative path** (`games/...`), so run pytest from the repo root. +- **`--describe` requires network + API key + the `anthropic` package**; it is the only code path that makes an external call, and its output is always printed and saved before any simulation runs.