深度强化学习训练掼蛋AI:LSTM+DMC 架构实战与踩坑
掼蛋是近两年国内增长最快的牌类游戏,2024 年已被列为全国智力运动项目。但从AI角度看,掼蛋的决策难度远超大多数人的想象——它同时具备不完全信息、二人配合、复杂牌型、动态升级四大挑战。
本文分享我们用 LSTM + DMC 训练掼蛋AI的技术细节,包括难点分析、架构设计、训练过程和踩过的坑。
一、掼蛋AI的四大技术难点
| 难点 | 具体问题 | 对训练的影响 |
|---|---|---|
| 不完全信息 | 看不到队友和对手的手牌 | 需要从历史动作推断手牌分布 |
| 2v2 配合 | 必须和队友形成默契 | 不能只优化自己,要建模队友行为 |
| 牌型复杂 | 炸弹、同花顺、三连对、钢板等 | 动作空间巨大(~364种合法动作) |
| 升级机制 | 从2打到A,当前等级影响策略 | 状态空间增加一个维度 |
对比斗地主(DouZero 定义了 309 种动作),掼蛋的动作空间更大、配合需求更高。
二、架构设计
class GuandanAI(nn.Module):
"""掼蛋AI决策模型"""
def __init__(self):
super().__init__()
# 手牌编码:54张牌 × 4花色 one-hot
self.hand_encoder = nn.Sequential(
nn.Linear(216, 256),
nn.ReLU(),
nn.Linear(256, 128)
)
# 历史动作编码:LSTM 处理变长序列
self.action_lstm = nn.LSTM(
input_size=128, # 每步动作的编码维度
hidden_size=256,
num_layers=2,
batch_first=True
)
# 配合信号编码:队友近期动作的特征
self.teammate_encoder = nn.Linear(128, 64)
# 决策头:输出所有合法动作的概率
self.policy_head = nn.Sequential(
nn.Linear(256 + 128 + 64, 512),
nn.ReLU(),
nn.Linear(512, 364) # 掼蛋动作空间
)
def forward(self, hand, action_history, teammate_actions):
hand_feat = self.hand_encoder(hand)
_, (h_n, _) = self.action_lstm(action_history)
memory = h_n[-1] # 取最后一层的隐状态
team_feat = self.teammate_encoder(teammate_actions)
combined = torch.cat([memory, hand_feat, team_feat], dim=-1)
logits = self.policy_head(combined)
return logits
关键设计决策
1) 为什么单独编码队友行为?
掼蛋是 2v2,队友的出牌隐含了大量信号(比如队友出了一对 K,可能暗示手中有炸弹需要你配合接风)。把队友动作单独编码后拼接,比混在一般历史序列里效果好约 5%。
2) 动作空间裁剪
掼蛋理论上的牌型组合极多,但大部分在特定局面下不合法。我们在模型前加了一个规则引擎过滤器:
def get_legal_actions(hand, last_play, current_rank):
"""
根据手牌、上家出牌和当前等级,
枚举所有合法出牌组合
"""
legal = []
if last_play is None:
# 自由出牌:枚举所有牌型
legal = enumerate_all_plays(hand, current_rank)
else:
# 必须打过上家:枚举所有能压过的牌型
legal = enumerate_beats(hand, last_play, current_rank)
legal.append('pass') # 永远可以选择不出
return legal
# 模型只在合法动作上做 softmax
logits = model(hand, history, teammate)
mask = build_mask(legal_actions, action_space=364)
logits[~mask] = -float('inf')
probs = F.softmax(logits, dim=-1)
三、DMC 自对弈训练流程
初始化:随机策略模型 M_0
for episode in range(30_000_000): # 三千万局
# 1. 四个AI(2v2)用当前模型自对弈
trajectory = self_play_guandan(M_current, num_players=4)
# 2. 根据胜负标注每步价值
# 赢的一方所有步骤 reward=+1
# 输的一方所有步骤 reward=-1
rewards = label_rewards(trajectory)
# 3. 策略梯度更新
for step in trajectory:
log_p = log_prob(M_current(step.state), step.action)
loss = -step.reward * log_p
loss.backward()
optimizer.step()
# 4. 每 10000 局评估一次
if episode % 10000 == 0:
fitness = evaluate(M_current, baseline='greedy')
log(f"Episode {episode}: fitness={fitness:.3f}")
训练在 V100 GPU 上跑了约 3 周,期间适应度曲线:
Episode 0: fitness=0.35 (接近随机)
Episode 1,000,000: fitness=0.48
Episode 5,000,000: fitness=0.58
Episode 15,000,000: fitness=0.65
Episode 30,000,000: fitness=0.72 (当前水平)
四、踩坑记录
坑1:配合策略难收敛
最初没有单独编码队友行为,AI学出来的策略是"纯个人最优"——经常抢队友的牌权。加入 teammate_encoder 后,AI 开始学会"让牌""配合接风"等 2v2 配合策略。
坑2:炸弹使用时机
早期模型有两个极端:要么炸弹攒到最后全浪费,要么一开始就全扔完。
解决:在 reward 设计中加入了阶段性奖励——不仅看最终胜负,还奖励"关键时刻使用炸弹"(比如在对手即将出完牌时用炸弹拦截)。
坑3:升级等级带来的状态空间膨胀
掼蛋的升级机制意味着"当前打几"会影响哪些牌是万能牌(逢人配)。我们把当前等级作为一个额外的条件输入:
rank_embedding = nn.Embedding(13, 16) # 2-A共13个等级
rank_feat = rank_embedding(current_rank)
combined = torch.cat([memory, hand_feat, team_feat, rank_feat], dim=-1)
五、小结
掼蛋AI的核心挑战在于不完全信息 + 2v2配合 + 复杂动作空间的叠加。LSTM+DMC 的组合在处理时序推理和策略优化上表现不错,但队友配合的建模仍有提升空间。
下一步我们计划:
- 引入对手/队友风格分类(激进型/保守型),做针对性策略调整
- 探索 Attention 机制替换 LSTM,看能否进一步提升长序列建模
如果你也在做类似的多人博弈AI,欢迎交流。
作者团队:长沙赢麻哒文化传播 | malinguo.com