The sum of all bids may never equal the number of cards each player holds in the round; the last bidder has to adjust their bid. This was an optional "not equal" lobby setting and is now a fixed rule of the game engine, so it applies to humans, AI players and training alike. - Remove GameOptions.not_equal, the set_options field, the lobby toggle and the --not-equal training flag - Explain the rule in the lobby and show the blocked bid while bidding - Drop the option from the AI observation encoding - Tests check that no round ends with bids summing to the cards in hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.4 KiB
Python
83 lines
3.4 KiB
Python
"""Command line entry point.
|
|
|
|
python -m wizard_ai serve # production: connect to the game server
|
|
python -m wizard_ai train --episodes 5000 # self-play training
|
|
python -m wizard_ai evaluate --agents model,heuristic,random,random
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from wizard_game import GameOptions
|
|
|
|
from .agents import AGENT_NAMES, make_agent
|
|
from .config import Config
|
|
|
|
|
|
def main() -> None:
|
|
config = Config()
|
|
parser = argparse.ArgumentParser(prog="wizard_ai")
|
|
parser.add_argument("--log-level", default=config.log_level)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
|
|
serve = commands.add_parser("serve", help="serve AI decisions to the game server")
|
|
serve.add_argument("--server-url", default=config.server_url)
|
|
serve.add_argument("--agent", default=config.agent, choices=AGENT_NAMES)
|
|
serve.add_argument("--model-path", default=config.model_path)
|
|
|
|
train = commands.add_parser("train", help="train the policy with self-play")
|
|
train.add_argument("--episodes", type=int, default=1000)
|
|
train.add_argument("--players", default="3,4,5,6", help="comma separated player counts")
|
|
train.add_argument("--opponents", default="self", choices=("self", "heuristic", "random"))
|
|
train.add_argument("--hidden-bids", action="store_true")
|
|
train.add_argument("--resume", action="store_true", help="continue from --out if it exists")
|
|
train.add_argument("--out", default=config.model_path, help="checkpoint directory")
|
|
train.add_argument("--seed", type=int)
|
|
|
|
evaluation = commands.add_parser("evaluate", help="let agents play against each other")
|
|
evaluation.add_argument("--agents", default="model,heuristic,random,random",
|
|
help=f"comma separated, 3-6 of {AGENT_NAMES}")
|
|
evaluation.add_argument("--games", type=int, default=200)
|
|
evaluation.add_argument("--model-path", default=config.model_path)
|
|
evaluation.add_argument("--seed", type=int)
|
|
|
|
args = parser.parse_args()
|
|
logging.basicConfig(level=args.log_level.upper(), format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
|
|
if args.command == "serve":
|
|
from .service import AIService
|
|
|
|
agent = make_agent(args.agent, args.model_path)
|
|
asyncio.run(AIService(args.server_url, agent, config.secret).run_forever())
|
|
|
|
elif args.command == "train":
|
|
from .model import UniformPolicy, checkpoint_exists, load_policy
|
|
from .training import SelfPlayTrainer, TrainingConfig
|
|
|
|
out = Path(args.out)
|
|
# TODO: replace UniformPolicy with the real model once it is implemented.
|
|
policy = load_policy(out) if args.resume and checkpoint_exists(out) else UniformPolicy(args.seed)
|
|
training_config = TrainingConfig(
|
|
episodes=args.episodes,
|
|
player_counts=tuple(int(n) for n in args.players.split(",")),
|
|
options=GameOptions(hidden_bids=args.hidden_bids),
|
|
opponents=args.opponents,
|
|
checkpoint_dir=out,
|
|
seed=args.seed,
|
|
)
|
|
SelfPlayTrainer(policy, training_config).train()
|
|
|
|
elif args.command == "evaluate":
|
|
from .training import evaluate
|
|
|
|
agents = [make_agent(name.strip(), args.model_path) for name in args.agents.split(",")]
|
|
print(evaluate(agents, args.games, seed=args.seed))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|