Import source: Shayan1/Prod/main
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
|
||||
from nash import solver, visualize
|
||||
from nash.game import Game
|
||||
from nash.simulation import Simulation, EvolutionarySimulation
|
||||
|
||||
# ANSI color codes — no external dependencies needed
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
CYAN = "\033[96m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def build_arg_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Solve and simulate Nash equilibria for a normal-form game."
|
||||
)
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument("--game", help="Path to a game JSON file")
|
||||
source.add_argument(
|
||||
"--describe",
|
||||
help="Plain-English scenario description; translated into a game JSON via AI",
|
||||
)
|
||||
parser.add_argument("--rounds", type=int, default=50)
|
||||
parser.add_argument("--memory", choices=["on", "off"], default="on")
|
||||
parser.add_argument(
|
||||
"--out", default="strategy_usage.png", help="Path to save the usage chart"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save-game",
|
||||
default="games/generated.json",
|
||||
help="Where to save an AI-translated game (only used with --describe)",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument(
|
||||
"--player",
|
||||
choices=["best_response", "regret_matching", "softmax", "q_learning", "llm"],
|
||||
default="best_response",
|
||||
help="Player decision model for simulation (default: best_response)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inspect",
|
||||
action="store_true",
|
||||
help="Print game details, payoff matrix, and dominant strategies, then exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Print per-round debug data during simulation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sim-type",
|
||||
choices=["agents", "evolutionary"],
|
||||
default="agents",
|
||||
help="Type of simulation: 'agents' (default) or 'evolutionary'",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reduce",
|
||||
action="store_true",
|
||||
help="Iteratively eliminate strictly dominated strategies before simulation or inspection",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def load_game(args):
|
||||
try:
|
||||
if args.describe:
|
||||
from nash import translate
|
||||
|
||||
print(f"Translating description via AI: {args.describe!r}")
|
||||
data = translate.describe_to_json(args.describe)
|
||||
translate.save_json(data, args.save_game)
|
||||
print(f"Generated game JSON (saved to {args.save_game}):")
|
||||
print(json.dumps(data, indent=2))
|
||||
return Game(data["players"], data["strategies"], data["payoffs"])
|
||||
return Game.load_from_json(args.game)
|
||||
except ValueError as e:
|
||||
print(f"{RED}{BOLD}Error:{RESET} {RED}{e}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def print_inspection(game):
|
||||
"""Print a formatted game inspection: strategies, payoff matrix, and dominant strategies."""
|
||||
header = f"{BOLD}{CYAN}Game Inspection{RESET}"
|
||||
rule = f"{DIM}{'─' * 60}{RESET}"
|
||||
print(f"\n{rule}\n {header}\n{rule}")
|
||||
|
||||
# --- Players & strategies ---
|
||||
print(f"\n {BOLD}Players & Strategies{RESET}")
|
||||
for i, name in enumerate(game.players):
|
||||
strats = ", ".join(game.strategies[i])
|
||||
print(f" {CYAN}{name}{RESET} → [{strats}]")
|
||||
|
||||
# --- Payoff matrix ---
|
||||
print(f"\n {BOLD}Payoff Matrix{RESET}")
|
||||
header_parts = [f"{CYAN}{p}{RESET}" for p in game.players]
|
||||
print(f" {' │ '.join(header_parts)}")
|
||||
|
||||
for profile in itertools.product(*(range(n) for n in game.strategy_counts())):
|
||||
labels = " │ ".join(
|
||||
f"{game.strategy_name(i, s):>12}" for i, s in enumerate(profile)
|
||||
)
|
||||
payoffs = ", ".join(
|
||||
_color_payoff(game.payoff(i, profile)) for i in range(game.num_players)
|
||||
)
|
||||
print(f" {labels} │ ({payoffs})")
|
||||
|
||||
# --- Dominant strategies ---
|
||||
print(f"\n {BOLD}Dominant Strategies{RESET}")
|
||||
try:
|
||||
dominant = solver.find_dominant_strategies(game)
|
||||
except AttributeError:
|
||||
print(f" {YELLOW}(not available — solver.find_dominant_strategies not yet implemented){RESET}")
|
||||
dominant = None
|
||||
|
||||
if dominant is not None:
|
||||
has_any = False
|
||||
for i, name in enumerate(game.players):
|
||||
strat_idx = dominant.get(i)
|
||||
if strat_idx is not None:
|
||||
strat_name = game.strategy_name(i, strat_idx)
|
||||
print(f" {CYAN}{name}{RESET}: {GREEN}{BOLD}{strat_name}{RESET}")
|
||||
has_any = True
|
||||
else:
|
||||
print(f" {CYAN}{name}{RESET}: {DIM}(none){RESET}")
|
||||
if not has_any:
|
||||
print(f" {YELLOW}No dominant strategies found for any player.{RESET}")
|
||||
|
||||
print(f"\n{rule}")
|
||||
|
||||
|
||||
def _color_payoff(value):
|
||||
"""Color a payoff value: green for positive, red for negative, plain for zero."""
|
||||
formatted = f"{value:+.2f}" if value != 0 else " 0.00"
|
||||
if value > 0:
|
||||
return f"{GREEN}{formatted}{RESET}"
|
||||
elif value < 0:
|
||||
return f"{RED}{formatted}{RESET}"
|
||||
return formatted
|
||||
|
||||
|
||||
|
||||
def print_solution(game):
|
||||
print("\n=== Theoretical solution ===")
|
||||
pure_eq = solver.find_pure_nash_equilibria(game)
|
||||
if pure_eq:
|
||||
for profile in pure_eq:
|
||||
named = [game.strategy_name(i, s) for i, s in enumerate(profile)]
|
||||
print(f"Pure Nash equilibrium: {named}")
|
||||
else:
|
||||
print("No pure Nash equilibrium found.")
|
||||
|
||||
mixed_eq = []
|
||||
if game.num_players == 2:
|
||||
mixed_eq = solver.find_mixed_nash_equilibria(game)
|
||||
for x, y in mixed_eq:
|
||||
print(
|
||||
f"Mixed Nash equilibrium: {game.players[0]}={x.round(3).tolist()} "
|
||||
f"{game.players[1]}={y.round(3).tolist()}"
|
||||
)
|
||||
return pure_eq, mixed_eq
|
||||
|
||||
|
||||
def print_comparison(game, simulation, pure_eq):
|
||||
print("\n=== Comparison ===")
|
||||
converged_round = simulation.converged_round()
|
||||
|
||||
if converged_round is not None:
|
||||
profile = simulation.converged_profile()
|
||||
named = [game.strategy_name(i, s) for i, s in enumerate(profile)]
|
||||
matches_pure = any(profile == eq for eq in pure_eq)
|
||||
if matches_pure:
|
||||
print(
|
||||
f"Simulation converged at round {converged_round + 1} to {named} "
|
||||
"-- a true Nash equilibrium."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Play held at {named} for {simulation.convergence_window}+ rounds, "
|
||||
"but that is NOT a Nash equilibrium of this game. This is expected "
|
||||
"when there's no pure equilibrium: fictitious play produces streaks "
|
||||
"that eventually flip as history accumulates. Only the long-run "
|
||||
"frequencies below reflect the actual (mixed) equilibrium."
|
||||
)
|
||||
else:
|
||||
print("Play never held a single repeated profile within the given rounds.")
|
||||
|
||||
frequencies = simulation.strategy_frequencies()
|
||||
print("Long-run strategy usage frequencies (final round):")
|
||||
for i, name in enumerate(game.players):
|
||||
freqs = {
|
||||
strat: round(frequencies[i][s][-1], 3)
|
||||
for s, strat in enumerate(game.strategies[i])
|
||||
}
|
||||
print(f" {name}: {freqs}")
|
||||
|
||||
|
||||
def main():
|
||||
args = build_arg_parser().parse_args()
|
||||
rng = random.Random(args.seed)
|
||||
|
||||
game = load_game(args)
|
||||
|
||||
if args.reduce:
|
||||
game, logs = solver.reduce_game(game)
|
||||
for log in logs:
|
||||
print(log)
|
||||
|
||||
# --inspect mode: print game details and exit
|
||||
if args.inspect:
|
||||
print_inspection(game)
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Game: {', '.join(game.players)}")
|
||||
for i, name in enumerate(game.players):
|
||||
print(f" {name} strategies: {game.strategies[i]}")
|
||||
|
||||
pure_eq, _ = print_solution(game)
|
||||
|
||||
if args.sim_type == "evolutionary":
|
||||
print(f"\n=== Evolutionary Simulation ({args.rounds} rounds) ===")
|
||||
simulation = EvolutionarySimulation(game, args.rounds)
|
||||
simulation.run()
|
||||
|
||||
print("Final population distribution:")
|
||||
for i, name in enumerate(game.players):
|
||||
freqs = {
|
||||
strat: round(float(simulation.populations[i][s]), 3)
|
||||
for s, strat in enumerate(game.strategies[i])
|
||||
}
|
||||
print(f" {name}: {freqs}")
|
||||
|
||||
chart_path = visualize.plot_evolutionary_trajectories(game, simulation, args.out)
|
||||
print(f"\nStrategy usage chart saved to: {chart_path}")
|
||||
else:
|
||||
memory = args.memory == "on"
|
||||
print(f"\n=== Simulation ({args.rounds} rounds, memory={args.memory}, player={args.player}) ===")
|
||||
|
||||
# Build Simulation kwargs
|
||||
sim_kwargs = dict(
|
||||
memory=memory,
|
||||
rng=rng,
|
||||
player_kind=args.player,
|
||||
)
|
||||
|
||||
# --verbose: wire a debug logger
|
||||
if args.verbose:
|
||||
debug_logger = lambda player_index, msg: print(
|
||||
f"[Round {len(simulation.history) + 1}] Player {player_index}: {msg}"
|
||||
)
|
||||
sim_kwargs["debug_logger"] = debug_logger
|
||||
|
||||
simulation = Simulation(game, args.rounds, **sim_kwargs)
|
||||
simulation.run()
|
||||
visualize.print_history(game, simulation)
|
||||
|
||||
print_comparison(game, simulation, pure_eq)
|
||||
|
||||
chart_path = visualize.plot_strategy_frequencies(game, simulation, args.out)
|
||||
print(f"\nStrategy usage chart saved to: {chart_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user