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