#!/usr/bin/env python3 """Example client for the playgd.party bot API (docs/BOT_API.md). Needs only `pip install websockets`. export GD_BOT_KEY=gd_... # create one at https://playgd.party/keys (account -> API keys) python examples/bot_client.py --hands 20 python examples/bot_client.py --opponent adb:v18 --partner self --hands 200 --name mybot Plays a RANDOM legal move: replace `choose(state)` below with your own bot. The key comes from $GD_BOT_KEY (or --key); it is never printed. Server pings ({"type": "ping"}) are answered with a pong; errors and close frames are printed with their code and reason. """ from __future__ import annotations import argparse import json import os import random import sys import time from websockets.exceptions import ConnectionClosed from websockets.sync.client import connect # ====================================================================================================================== # REPLACE THIS with your bot. `state` is the "state" object of a decide message: # state["seat"] your seat 0..3 (seats 0+2 and 1+3 are the teams) # state["hand"] your cards, e.g. ["S2", "HA", "SB", "HR"] (suit S/H/C/D + rank 2-9/T/J/Q/K/A; SB / HR = jokers) # state["level"] the level rank ("2" in this API); state["wild"] the wild card ("H2": the heart of the level) # state["cards_left"] cards per seat 0..3; state["finished"] the seats already out, in order # state["to_beat"] the play you must beat {"seat", "kind", "rank", "cards"} or null when you lead # state["history"] every action of this hand so far [{"seat", "kind", "rank", "cards"}] (passes included) # state["legal"] your legal moves [{"index", "kind", "rank", "cards"}]; return one of their "index" values # ====================================================================================================================== def choose(state: dict) -> int: return random.choice(state["legal"])["index"] def main(): ap = argparse.ArgumentParser(description="plays hands against the playgd.party bots over the bot API") ap.add_argument("--url", default="wss://playgd.party/ws/bot") ap.add_argument("--key", default=None, help="API key (default: $GD_BOT_KEY)") ap.add_argument("--name", default="example-random") ap.add_argument("--opponent", default="adb:v18") ap.add_argument("--partner", default="self", help='"self" (you play both seats of your team) or an agent spec') ap.add_argument("--hands", type=int, default=20) ap.add_argument("--no-duplicate", action="store_true", help="do not play each deal twice with the teams swapped") ap.add_argument("--seed", type=int, default=None) ap.add_argument("--quiet", action="store_true") args = ap.parse_args() key = args.key or os.environ.get("GD_BOT_KEY") if not key: sys.exit("no key: set GD_BOT_KEY (create a key at https://playgd.party/keys)") hello = {"type": "hello", "key": key, "name": args.name, "opponent": args.opponent, "partner": args.partner, "hands": args.hands, "duplicate": not args.no_duplicate} if args.seed is not None: hello["seed"] = args.seed t0 = time.time(); decisions = 0; rc = 0 try: with connect(args.url, max_size=4 * 1024 * 1024, open_timeout=20) as ws: ws.send(json.dumps(hello)) while True: m = json.loads(ws.recv()) t = m.get("type") if t == "ready": print(f"ready: session {m['session']} as {m['name']} (key {m['label']}), {m['hands']} hands vs " f"{m['opponent_name']}, partner {m['partner']}, duplicate {m['duplicate']}, seed {m['seed']}") print(f" {m['convention']}") elif t == "decide": ws.send(json.dumps({"type": "play", "req": m["req"], "index": choose(m["state"])})) decisions += 1 elif t == "hand_end": r = m["running"] if not args.quiet: print(f"hand {m['hand'] + 1:>4}: seats {m['your_seats']} score {m['score']:+d} finish {m['finish_order']}" f" running mean {r['mean']:+.3f} over {r['hands']} hands") elif t == "ping": ws.send(json.dumps({"type": "pong"})) elif t == "pong": pass elif t == "error": print(f"error [{m['code']}] {m['message']} (strikes {m['strikes']}{', fatal' if m.get('fatal') else ''})", file=sys.stderr) if m.get("fatal"): rc = 1 elif t == "done": s = m["summary"] print(f"done ({m['reason']}): {s['hands']} hands, mean {s['mean']} +- {s['se']} per hand, win rate {s['win_rate']}," f" {s['hands_per_min']} hands/min, {s['timeouts']} timeouts, {s['strikes']} strikes") if s.get("paired"): p = s["paired"] print(f" duplicate: {p['n']} pairs, paired mean {p['mean']} +- {p['se']}") else: print(f"unknown frame {m}", file=sys.stderr) except ConnectionClosed as e: code = e.rcvd.code if e.rcvd else "?"; reason = e.rcvd.reason if e.rcvd else "" if code != 1000: print(f"connection closed: code {code} {reason}", file=sys.stderr); rc = 1 el = time.time() - t0 if el > 0: print(f"{decisions} decisions in {el:.1f} s ({decisions / el:.2f}/s)") sys.exit(rc) if __name__ == "__main__": main()