"""Minimal Vibe Duels client in Python. pip install websockets python vibeduels.py --token vd_... --vs volley --rounds 8 [--server https://vibeduels.com] python vibeduels.py --token vd_... --mode lobby --room friends # play another entrant, unranked Replace `tick(state)` with your bot. The state and the action are exactly the objects in https://vibeduels.com/kit/ARENA.md; the protocol is in ../../PROTOCOL.md. """ import argparse import asyncio import json import math import websockets BOT = {"name": "Snake", "motto": "Written in Python", "stats": {"armor": 3, "engine": 3, "gun": 2, "reload": 2}} def init(info): pass def tick(state): me, enemy = state["me"], state["enemy"] want = math.atan2(enemy["y"] - me["y"], enemy["x"] - me["x"]) diff = math.atan2(math.sin(want - me["turret"]), math.cos(want - me["turret"])) return {"throttle": 0, "turn": 0, "turretTurn": max(-1, min(1, diff * 4)), "fire": abs(diff) < 0.1} async def play(server, token, mode, vs, rounds, seed, room=None): url = server.replace("http", "ws", 1) + "/ws" async with websockets.connect(url, max_size=4 * 1024 * 1024) as ws: hello = {"type": "hello", "token": token, "bot": BOT, "mode": mode} if mode != "lobby": hello["vs"] = vs if mode in ("practice", "lobby"): hello["rounds"] = rounds if seed is not None: hello["seed"] = seed if mode == "lobby" and room: hello["room"] = room await ws.send(json.dumps(hello)) async for raw in ws: m = json.loads(raw) t = m["type"] if t == "waiting": print(m["message"], "...") elif t == "welcome": print(f'{m["you"]["name"]} vs {m["opponent"]["name"]} ({mode}, {m["rounds"]} rounds) {m["replayUrl"]}') elif t == "init": init(m["info"]) elif t == "tick": view = m["view"] await ws.send(json.dumps({"type": "action", "tick": view["tick"], "action": tick(view)})) elif t == "roundOver": r = "DRAW" if m["draw"] else ("WIN " if m["youWon"] else "LOSS") print(f' round {m["index"] + 1:2d} · {r} · {m["time"]:5.1f} s · HP {m["hp"]["you"]} vs {m["hp"]["enemy"]} · margin {m["margin"]:+.3f}') elif t == "notice": print(" !", m["message"]) elif t == "matchOver": s = m["score"] print(f'\n{s["wins"]} wins, {s["losses"]} losses, {s["draws"]} draws · rate {m["rate"]:.3f} · margin {m["margin"]:+.3f}') print("replay:", m["replayUrl"]) elif t == "error": raise SystemExit(f'{m["code"]}: {m["message"]}') if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("--server", default="https://vibeduels.com") p.add_argument("--token", required=True) p.add_argument("--mode", default="practice", choices=["practice", "ranked", "lobby"]) p.add_argument("--vs", default="volley") p.add_argument("--rounds", type=int, default=8) p.add_argument("--seed", type=int, default=None) p.add_argument("--room", default=None, help="lobby only: pair with whoever uses the same code") a = p.parse_args() asyncio.run(play(a.server, a.token, a.mode, a.vs, a.rounds, a.seed, a.room))