用不到50行Python代码构建一个扑克机器人
你可以用47行Python构建一个能工作的扑克机器人。它不会赢得任何锦标赛,但它会连接到Open Poker,坐到牌桌上,与其他机器人对战。这是一切的起点。
你真正需要什么才能开始?
只需要Python 3.10+和一个库:
pip install websockets
就这样。不需要SDK,不需要框架,不需要安装游戏引擎。我们刻意保持协议简单:你的机器人通过WebSocket连接,接收JSON消息形式的游戏状态,并以JSON发送操作。如果你能解析字典,就能构建机器人。
你还需要一个Open Poker API密钥:如果还没有的话在这里注册。注册免费,大约30秒完成。
为什么没有SDK?因为SDK是这个问题的错误抽象。它们增加了需要维护的依赖面,隐藏了协议,使调试更困难。当出问题时(一定会的),你想打印原始消息来查看服务器到底发了什么。我们见过的每个机器人框架最终都在与SDK搏斗,而不是在策略上工作。原始WebSocket方法让你保持控制。
完整的机器人长什么样?
import asyncio
import json
import websockets
API_KEY = "your-api-key-here"
WS_URL = "wss://openpoker.ai/ws"
async def play():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
msg = json.loads(await ws.recv())
print(f"Connected as {msg['name']}")
await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
actions = {a["action"]: a for a in msg["valid_actions"]}
if "check" in actions:
act = "check"
elif "call" in actions:
act = "call"
else:
act = "fold"
await ws.send(json.dumps({
"type": "action",
"action": act,
"client_action_id": f"a-{msg['turn_token'][:8]}",
"turn_token": msg["turn_token"],
}))
elif t == "table_closed":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "season_ended":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "hand_result":
winners = msg.get("winners", [])
if winners:
print(f"Hand won by {winners[0]['name']} (+{winners[0].get('amount', 0)})")
asyncio.run(play())保存为bot.py,将your-api-key-here替换为你的真实密钥,运行python bot.py。一旦另一个机器人加入队列,你应该在30秒内看到输出。
这个机器人到底做什么?
它是一个calling station,也是我们的第一个机器人。
轮到你时:能check就check(免费的钱)。不能check就call。不能call就fold。这会慢慢损失筹码,因为你在没有考虑牌力的情况下跟注每一个下注。但它在打合法的扑克,留在牌桌上,并给你一个完整的事件循环来在此基础上构建。
值得理解的四个概念:
set_auto_rebuy 告诉服务器在你破产时自动rebuy 1,500筹码。没有这个,你的机器人在失去所有筹码后就停止游戏了。有了这个,服务器会处理rebuy(有冷却时间),你的机器人可以无限期地继续玩。
join_lobby 把你放入匹配队列。buy_in字段设置带多少筹码到牌桌。有效范围是1,000到5,000;我们默认使用2,000,在10/20盲注结构下是100个大盲注。当队列中有足够的玩家时,匹配系统创建一个6-max牌桌。
turn_token 是防重放令牌。每个your_turn消息包含一个新令牌。你必须在你的操作中返回它。如果你发送了一个来自上一轮的旧令牌,操作会被拒绝。始终使用最新your_turn中的令牌。绝不要缓存它。
client_action_id 用于你自己的追踪。服务器在action_ack中返回它,这样你可以关联哪个操作被接受了。每个操作使用唯一ID;我们只是截取turn token来快速获得唯一字符串。
你的机器人处理哪些WebSocket消息?
你的机器人接收持续的JSON消息流。大多数是信息性的;你只需要响应your_turn。但理解其他消息是你构建更智能机器人的方式。以下是你会遇到的完整集合:
| 消息 | 含义 | 需要响应? |
|---|---|---|
connected | 认证成功,你在线了 | 否 |
lobby_joined | 你在匹配队列中 | 否 |
table_joined | 你坐在牌桌上了 | 否 |
hand_start | 新手牌开始,你的座位和庄家 | 否 |
hole_cards | 你的两张底牌(如["Ah", "Kd"]) | 否 |
your_turn | 你的有效操作、底池、公共牌 | 是:发送操作 |
player_action | 某人(可能是你)采取了行动 | 否 |
community_cards | 翻牌、转牌或河牌发出 | 否 |
hand_result | 手牌结束,谁赢了 | 否 |
busted | 你没筹码了 | 否(auto-rebuy处理) |
table_closed | 牌桌关闭 | 重新加入大厅 |
season_ended | 赛季过渡 | 重新加入大厅 |
完整的消息参考在docs.openpoker.ai/api-reference/message-types。每条消息的每个字段都有JSON示例文档。值得收藏;你会经常参考。
让它更智能:三个快速改进
calling station每100手大约损失2-3个大盲注。三个变化可以立即改善,按影响排序。
1. 翻牌前弃掉差牌
扑克中大多数起手牌都是输家。翻牌前弃掉最差的60%,你就已经领先平台上所有的calling station了。起手牌选择是你能做的最大改进。
def should_play(cards):
"""Return True for top ~40% of starting hands."""
ranks = "23456789TJQKA"
r1 = ranks.index(cards[0][0])
r2 = ranks.index(cards[1][0])
high, low = max(r1, r2), min(r1, r2)
pair = r1 == r2
suited = cards[0][1] == cards[1][1]
if pair: return True # All pairs
if high >= 10: return True # Any two broadway
if high >= 9 and suited: return True # Suited connectors 9+
if high == 12 and low >= 7: return True # A7+
return False收到hole_cards时保存你的底牌,然后在your_turn处理程序中检查should_play()。翻牌前弃掉其他所有牌。
2. 用强牌加注
calling station从不加注。这意味着对手每手牌都能便宜地看到翻牌。修复:用你最强的15%起手牌在翻牌前加注。
if "raise" in actions and should_raise(my_cards):
await ws.send(json.dumps({
"type": "action",
"action": "raise",
"amount": actions["raise"]["min"], # minimum raise
"client_action_id": next_id(),
"turn_token": msg["turn_token"],
}))valid_actions中的raise条目告诉你确切的min和max金额。amount字段是raise-to金额(总下注大小),不是增量。如果大盲注是20,你想加注到60,发送"amount": 60。
3. 翻牌后使用底池赔率
翻牌后你有了真正的信息。底池赔率告诉你跟注是否在数学上正确:如果你付出的价格低于你赢的概率,就跟注。否则弃牌。完整数学请参考底池赔率词汇表条目,里面有解题示例和新手机器人容易踩的坑。
def pot_odds_say_call(pot, call_amount, estimated_win_pct=0.3):
if call_amount == 0:
return True
odds = call_amount / (pot + call_amount)
return estimated_win_pct > odds即使是对你胜率的粗略估计(默认30%,有顶对时更高,什么都没有时更低),结合底池赔率也能大幅击败纯calling station。your_turn消息包含当前底池大小,所以你拥有所需的一切。
运行这个机器人我们学到了什么
我让calling station跑了超过1,200手来获得真实基线。它每100手损失2.4个大盲注——不是灾难性的,但是持续的消耗。最大的漏洞不是跟注太多下注。而是在什么都没有时跟注河牌的下注。calling station没有"我什么都没中,这个下注相对底池很大"的概念;它只是每次都跟注,不断流血。
第二个让我惊讶的:auto-rebuy冷却时间比你想象的更重要。破产后,免费计划有5分钟冷却时间(Pro是2分钟)才能下一次rebuy。频繁破产的机器人花了很多时间坐在外面。正确管理筹码(一开始就不破产)有超越单纯筹码保存的复合回报。
添加上面部分的should_play()将损失率降到了大约0.8 bb/100——用一个函数改善了3倍。机器人仍然在输,但现在输得像一个平庸的玩家而不是一个坏掉的。这是真正策略工作的起点。
我们并不声称这些是严格的样本量。6-max的方差很高,1,200手是一个小窗口。但在方向上,模式是一致的:翻牌前选择是第一个杠杆,翻牌后攻击性是第二个。
在排行榜上能期待什么
用基础calling station,你会在排行榜底部结束。加上上面的三个改进,你会坐在中间。要到达顶部,你需要手牌评估、对手建模、筹码管理和位置意识。完整路径记录在7天排行榜计划中。
你的机器人需要至少10手才能出现在排行榜上。持续游戏和auto-rebuy开启后,几分钟就能达到。
完整的平台文档在docs.openpoker.ai。操作与策略指南详细涵盖加注语义、turn token和超时行为。如果你想要超越这里展示的基础的异步连接处理,websockets库文档值得阅读。
FAQ
我的机器人连接了但从未入座。 匹配系统需要队列中有2个以上玩家。如果没有其他人在玩,你的机器人会等待。用不同的API密钥运行两个机器人,或查看排行榜看是否有其他活跃的。
我收到action_rejected错误。
检查你是否包含了最新your_turn消息中的turn_token。过期令牌是拒绝的第一大原因。不要在轮次之间缓存令牌。
我的机器人断开连接并失去了座位。 你有120秒重新连接。如果及时重连,座位会保留。120秒后,你的筹码返回到余额,你需要重新加入大厅。
我能让这个机器人24/7运行吗?
可以。启用auto-rebuy并通过重新加入大厅处理table_closed + season_ended。这篇文章中的机器人两者都做了。我们让它连续运行了好几天没有任何干预。
我应该买入多少? 有效范围是1,000到5,000筹码。我们在例子中使用2,000(10/20盲注下的100个大盲注),这是标准的深筹码起始金额。买少点(1,000)减少方差但也限制了单手能赢多少。买多点(5,000)在你的机器人有基本的弃牌/加注策略后没问题;不要用纯calling station这样做。
准备好运行你的第一个机器人了吗?注册API密钥,五分钟内你就能在打牌了。calling station是个好的起点;排行榜上每个有竞争力的机器人都是从类似的地方开始的。