一、钓鱼纸牌规则与博弈特征
钓鱼纸牌(Go Fish)是全球流行的经典纸牌游戏,核心规则如下:
- 一副牌(不含大小王)均分给所有玩家,剩余为”鱼塘”
- 玩家轮流向其他玩家索要特定点数的牌(如”给我所有的7″)
- 如果对方有该点数的牌,必须全部交出;否则说”去钓鱼”,索要者从鱼塘抽一张
- 当玩家集齐4张同点数时,组成一套并亮出计分
- 手牌用完者从鱼塘补牌,鱼塘空时游戏结束,套数最多者获胜
钓鱼纸牌是典型的不完全信息博弈:
– 每位玩家只能看到自己的手牌和已亮出的套
– 需要通过对手的行为推断其手牌分布
– 记忆能力直接决定胜率
二、不完全信息博弈建模
2.1 信息集定义
在钓鱼纸牌中,玩家 i 的信息集包含:
– 私有信息:自己的手牌
– 公共信息:已亮出的所有套、每位玩家索要/交出的历史
– 推断信息:对手手牌的概率分布(通过贝叶斯更新)
2.2 贝叶斯信念更新
当玩家A向玩家B索要点数 r 时:
– B交出k张:确认B手中该点数剩余为0,更新信念
– B说”去钓鱼”:确认B手中没有该点数,A从鱼塘抽一张(所有人看到A抽牌但不知道是什么)
三、核心算法实现
3.1 增强版卡牌与套管理
import java.util.*;
public class GoFishCard {
public enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES }
private final Suit suit;
private final int rank; // 1-13
public GoFishCard(Suit suit, int rank) {
this.suit = suit;
this.rank = rank;
}
public int getRank() { return rank; }
public Suit getSuit() { return suit; }
@Override
public String toString() {
String[] r = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
return r[rank-1] + suit.toString().charAt(0);
}
}
public class CardDeck {
private List<GoFishCard> cards;
private Random rand;
public CardDeck() {
cards = new ArrayList<>();
rand = new Random();
for (GoFishCard.Suit s : GoFishCard.Suit.values()) {
for (int r = 1; r <= 13; r++) cards.add(new GoFishCard(s, r));
}
}
public void shuffle() { Collections.shuffle(cards, rand); }
public GoFishCard draw() { return cards.isEmpty() ? null : cards.remove(cards.size()-1); }
public boolean isEmpty() { return cards.isEmpty(); }
public int size() { return cards.size(); }
}
3.2 记忆模型:对手手牌信念追踪
public class BeliefModel {
// beliefs[rank][player] = 该玩家持有该点数的期望张数
private double[][] beliefs;
private int numPlayers;
private int numRanks = 13;
private Random random;
public BeliefModel(int numPlayers) {
this.numPlayers = numPlayers;
this.beliefs = new double[numRanks][numPlayers];
this.random = new Random();
// 初始均匀分布:每人每张点数期望 4/numPlayers
for (int r = 0; r < numRanks; r++) {
for (int p = 0; p < numPlayers; p++) {
beliefs[r][p] = 4.0 / numPlayers;
}
}
}
/**
* 更新信念:玩家p交出了k张rank点数的牌给asker
*/
public void updateGiven(int rank, int player, int countGiven, int asker) {
int r = rank - 1;
// 确认player手中该点数剩余极少
beliefs[r][player] = Math.max(0, beliefs[r][player] - countGiven);
// asker获得了牌,期望值增加
beliefs[r][asker] += countGiven;
normalize(r);
}
/**
* 更新信念:player被索要rank但说"Go Fish"
* 确认player没有该点数
*/
public void updateGoFish(int rank, int player) {
int r = rank - 1;
beliefs[r][player] = 0;
normalize(r);
}
/**
* 玩家亮出一套(4张),清空所有玩家对该点数的信念
*/
public void updateCompleteSet(int rank) {
int r = rank - 1;
for (int p = 0; p < numPlayers; p++) {
beliefs[r][p] = 0;
}
}
private void normalize(int r) {
double sum = 0;
for (int p = 0; p < numPlayers; p++) sum += beliefs[r][p];
if (sum > 0) {
for (int p = 0; p < numPlayers; p++) {
beliefs[r][p] = Math.min(4, beliefs[r][p] * (4.0 / sum));
}
}
}
/**
* 获取某玩家持有某点数的期望张数
*/
public double getExpectedCount(int rank, int player) {
return beliefs[rank-1][player];
}
/**
* 选择索要目标:找期望值最高的对手和点数
*/
public int[] chooseBestAsk(int myId, List<Integer> myRanks) {
double bestScore = -1;
int bestRank = -1;
int bestTarget = -1;
for (int rank : myRanks) {
for (int p = 0; p < numPlayers; p++) {
if (p == myId) continue;
double expected = beliefs[rank-1][p];
// 综合期望张数和完成套的概率
double score = expected * (1 + expected / 4.0);
if (score > bestScore) {
bestScore = score;
bestRank = rank;
bestTarget = p;
}
}
}
return new int[]{bestRank, bestTarget};
}
}
3.3 AI玩家:记忆策略实现
public class GoFishAI {
private String name;
private List<GoFishCard> hand;
private int score; // 完成的套数
private BeliefModel belief;
private int id;
private Random random;
// 精确记忆:记录已知在谁手中的牌
private Map<Integer, Set<Integer>> knownCards; // rank -> set of player ids
public GoFishAI(String name, int id, int numPlayers) {
this.name = name;
this.id = id;
this.hand = new ArrayList<>();
this.score = 0;
this.belief = new BeliefModel(numPlayers);
this.random = new Random();
this.knownCards = new HashMap<>();
for (int r = 1; r <= 13; r++) knownCards.put(r, new HashSet<>());
}
public String getName() { return name; }
public int getId() { return id; }
public int getScore() { return score; }
public int getHandSize() { return hand.size(); }
public boolean isHandEmpty() { return hand.isEmpty(); }
public void receiveCard(GoFishCard card) {
if (card != null) hand.add(card);
}
public void receiveCards(List<GoFishCard> cards) {
hand.addAll(cards);
}
/**
* 检查是否能组成套并亮出
*/
public List<Integer> checkCompleteSets() {
Map<Integer, List<GoFishCard>> byRank = new HashMap<>();
for (GoFishCard c : hand) {
byRank.computeIfAbsent(c.getRank(), k -> new ArrayList<>()).add(c);
}
List<Integer> completed = new ArrayList<>();
for (Map.Entry<Integer, List<GoFishCard>> entry : byRank.entrySet()) {
if (entry.getValue().size() >= 4) {
completed.add(entry.getKey());
// 移除这4张牌
hand.removeAll(entry.getValue().subList(0, 4));
score++;
belief.updateCompleteSet(entry.getKey());
}
}
return completed;
}
/**
* 核心决策:向谁索要什么点数的牌
*/
public AskDecision makeDecision() {
// 策略1:优先向已知有该点数的玩家索要(精确记忆)
Set<Integer> myRanks = new HashSet<>();
for (GoFishCard c : hand) myRanks.add(c.getRank());
for (int rank : myRanks) {
Set<Integer> knownHolders = knownCards.get(rank);
for (int holder : knownHolders) {
if (holder != id && belief.getExpectedCount(rank, holder) > 0.5) {
return new AskDecision(rank, holder, "known_memory");
}
}
}
// 策略2:使用信念模型选择最优期望目标
int[] best = belief.chooseBestAsk(id, new ArrayList<>(myRanks));
if (best[0] > 0) {
return new AskDecision(best[0], best[1], "belief_model");
}
// 策略3:随机选择(保底)
int rank = new ArrayList<>(myRanks).get(random.nextInt(myRanks.size()));
int target;
do { target = random.nextInt(belief.numPlayers); } while (target == id);
return new AskDecision(rank, target, "random");
}
/**
* 记录观察到的行为
*/
public void observeAsk(int asker, int target, int rank, boolean success, int count) {
if (success) {
belief.updateGiven(rank, target, count, asker);
knownCards.get(rank).add(target);
// 目标交出了牌,asker现在肯定有该点数
knownCards.get(rank).add(asker);
} else {
belief.updateGoFish(rank, target);
// 目标没有该点数
knownCards.get(rank).remove(target);
}
}
/**
* 给出指定点数的所有牌
*/
public List<GoFishCard> giveCards(int rank) {
List<GoFishCard> toGive = new ArrayList<>();
Iterator<GoFishCard> it = hand.iterator();
while (it.hasNext()) {
GoFishCard c = it.next();
if (c.getRank() == rank) {
toGive.add(c);
it.remove();
}
}
return toGive;
}
public boolean hasRank(int rank) {
return hand.stream().anyMatch(c -> c.getRank() == rank);
}
@Override
public String toString() {
return String.format("%s[套:%d 手牌:%d]", name, score, hand.size());
}
}
class AskDecision {
int rank;
int target;
String strategy;
AskDecision(int rank, int target, String strategy) {
this.rank = rank;
this.target = target;
this.strategy = strategy;
}
}
3.4 游戏主引擎
public class GoFishGame {
private List<GoFishAI> players;
private CardDeck pond;
private Random random;
private int currentPlayer;
public GoFishGame(int numPlayers) {
this.players = new ArrayList<>();
this.random = new Random();
String[] names = {"记忆大师", "概率专家", "随机玩家"};
for (int i = 0; i < numPlayers && i < names.length; i++) {
players.add(new GoFishAI(names[i], i, numPlayers));
}
}
public void initialize() {
pond = new CardDeck();
pond.shuffle();
// 每人发5张牌(标准规则)
int initialCards = 5;
for (int i = 0; i < initialCards; i++) {
for (GoFishAI p : players) {
p.receiveCard(pond.draw());
}
}
// 初始检查套
for (GoFishAI p : players) p.checkCompleteSets();
currentPlayer = 0;
}
public void playRound() {
GoFishAI player = players.get(currentPlayer);
if (player.isHandEmpty() && !pond.isEmpty()) {
player.receiveCard(pond.draw());
System.out.println(player.getName() + " 从鱼塘补牌");
}
if (player.isHandEmpty()) {
currentPlayer = (currentPlayer + 1) % players.size();
return;
}
AskDecision decision = player.makeDecision();
GoFishAI target = players.get(decision.target);
System.out.printf("%s -> %s: 给我所有的%s (策略:%s)\n",
player.getName(), target.getName(),
rankToString(decision.rank), decision.strategy);
boolean success = target.hasRank(decision.rank);
List<GoFishCard> received;
if (success) {
received = target.giveCards(decision.rank);
player.receiveCards(received);
System.out.println(" 成功!获得 " + received.size() + " 张");
// 通知所有玩家观察
for (GoFishAI p : players) {
p.observeAsk(player.getId(), target.getId(), decision.rank, true, received.size());
}
} else {
System.out.println(" " + target.getName() + ": Go Fish!");
GoFishCard drawn = pond.draw();
if (drawn != null) {
player.receiveCard(drawn);
System.out.println(" 从鱼塘抽到: " + drawn);
}
for (GoFishAI p : players) {
p.observeAsk(player.getId(), target.getId(), decision.rank, false, 0);
}
currentPlayer = (currentPlayer + 1) % players.size();
}
// 检查是否完成套
List<Integer> completed = player.checkCompleteSets();
if (!completed.isEmpty()) {
System.out.println(" *** " + player.getName() + " 亮出套: " + completed);
}
}
private String rankToString(int rank) {
String[] r = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
return r[rank-1];
}
public boolean isGameOver() {
if (!pond.isEmpty()) return false;
for (GoFishAI p : players) {
if (!p.isHandEmpty()) return false;
}
return true;
}
public void play() {
initialize();
System.out.println("=== 钓鱼纸牌开始 ===\n");
int rounds = 0;
while (!isGameOver() && rounds < 200) {
playRound();
rounds++;
}
System.out.println("\n=== 最终结果 ===");
players.sort((a, b) -> b.getScore() - a.getScore());
for (GoFishAI p : players) {
System.out.println(p);
}
}
public static void main(String[] args) {
GoFishGame game = new GoFishGame(3);
game.play();
}
}
四、策略对比实验
public class StrategyComparison {
public static void main(String[] args) {
int iterations = 1000;
int[] wins = new int[3];
for (int i = 0; i < iterations; i++) {
GoFishGame game = new GoFishGame(3);
game.initialize();
int rounds = 0;
while (!game.isGameOver() && rounds < 200) {
game.playRound();
rounds++;
}
// 记录胜者
int best = 0;
for (int j = 1; j < 3; j++) {
if (game.players.get(j).getScore() > game.players.get(best).getScore()) {
best = j;
}
}
wins[best]++;
}
System.out.println("=== 1000局统计 ===");
String[] names = {"记忆大师", "概率专家", "随机玩家"};
for (int i = 0; i < 3; i++) {
System.out.printf("%s: %d胜 (%.1f%%)\n", names[i], wins[i], 100.0 * wins[i] / iterations);
}
}
}
五、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 信念更新 | O(P) | O(P*R) | P玩家数,R点数种类 |
| 最优决策 | O(P*R) | O(1) | 遍历所有对手和点数 |
| 套检测 | O(H) | O(R) | H为手牌数 |
| 整局游戏 | O(TPR) | O(P*R) | T为回合数 |
六、扩展与优化
- 深度记忆:不仅记忆”谁有什么”,还记忆”谁索要什么”推断其手牌
- 欺骗策略:故意索要自己没有的点数,误导对手信念
- 联盟检测:识别两个玩家是否互相配合
- 强化学习:使用Q-Learning训练更优的索要策略
七、总结
钓鱼纸牌是不完全信息博弈的经典范例。通过贝叶斯信念更新追踪对手手牌分布、记忆模型记录确定性信息、期望收益计算选择最优索要目标,AI玩家可以显著优于随机策略。这些技术同样适用于桥牌、德州扑克等更复杂的纸牌博弈场景。