65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的战争纸牌游戏模拟

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (9投票s)

2009年6月20日

CPOL

8分钟阅读

viewsIcon

87054

downloadIcon

3440

使用 LINQ 表达式和 Dictionary 对象创建经典纸牌游戏的 Windows Forms 应用程序

WarGameScreenSmall2.png

引言

(请注意,本文中的大写单词‘War’指的是游戏,而小写单词‘war’指的是对手卡牌数值相等的战斗。)

最近,我需要一种方法来生成数据,用于我正在学习的统计分析课程。在我与年幼的侄女玩“战争”(War)纸牌游戏时,我灵光一闪;战争是生成各种统计数据的完美游戏模拟。完成游戏所需的平均战斗次数是多少?一场战斗导致“战争”(平局)的几率是多少?关于双重或三重战争(连续平局)的几率呢?是否有方法可以预测结果?游戏规则的变化如何影响结果?

背景

在网上做了一些研究,我很清楚我不是第一个 undertaking this endeavor。由于战争的简单性,存在无数的计算机游戏模拟。此外,还有大量关于可应用于构建战争等游戏模拟的编程原则的文章,以及基于游戏结果可以进行的统计分析。在创建模拟之前,我将研究限制在纸牌游戏的规则上,而不是任何现有模拟的架构上。我想要创建工作模拟的挑战。

战争是一种被称为简单的累积型纸牌游戏。游戏的目标是获取(从对手那里赢得)一副标准的52 张牌盎格鲁-美式扑克牌中的所有牌。在战争游戏中,每张牌通常有十三种可能的预定值之一——从二(点2)到 A。在战争游戏中,牌的花色——红心、方块、黑桃或梅花,对牌的数值没有影响。

战争有很多变体。在此模拟中,我选择了最基本两人变体。规则如下:

  1. 一副标准的 52 张牌,洗牌并将牌发给每位玩家。每位玩家收到 26 张牌,背面朝下。
  2. 游戏由一系列“战斗”组成,每位玩家从牌堆顶部抽出一张牌,面朝上放在桌子上。如果两张牌的数值不同,则数值较高的牌的玩家赢得这场战斗。战斗的获胜者将桌上的所有牌收起,并以任意顺序放在牌堆底部。
  3. 两名玩家打出数值相等的牌的战斗被称为“战争”(因此得名)。在这种情况下,每位玩家将另外两张牌面朝下放在桌子上,然后放一张面朝上的牌。然后按照之前的规则,通过数值较高的牌来决定获胜者。如果这些牌碰巧也相同(“双重战争”),则过程会重复进行,直到确定获胜者。
  4. 最终获得所有 52 张牌,使对手没有牌的玩家即为获胜者。如果发生战争,如果一方没有足够的牌来进行战争(两张牌面朝下,一张牌面朝上),她/他将输掉这场战斗,从而输掉游戏。

Using the Code

此模拟构造为一个简单的 Windows Forms 应用程序,使用 C# 3.0 和 Visual Studio 2008 构建。用户界面由一个 Windows Form 组成,该窗体显示游戏的当前状态。此外,还有一系列包含游戏逻辑的类。我的主要关注点在于模拟的准确性、代码的构建以及数据的提取,而不是创建视觉效果丰富的 GUI。当然,可以对用户体验进行许多增强,以提高游戏的娱乐性。

该模拟允许游戏以两种不同的模式进行:手动和自动。您可以选择手动玩游戏,一次进行一场战斗并查看结果。或者,您可以选择让程序进行所有战斗,直到确定获胜者。

abstract 基类 CardGame 包含通用字段、属性和方法,可用于创建多种纸牌游戏。CardGame 类包含用于创建一副新的 52 张牌、洗牌(参见下面的代码)以及将牌发给每位玩家的方法。基类中的字段和属性包括用于存储玩家牌的 Dictionary<> 对象,以及玩家、战斗和游戏统计数据。

protected Dictionary ShuffleCards(Dictionary cardsToShuffle)
{
    protected Dictionary ShuffleCards(Dictionary cardsToShuffle)
    {
        Dictionary ShuffledCards = new Dictionary(cardsToShuffle);
        Random RandomNumber = new Random();
        ShuffledCards = ShuffledCards.Select(cards => cards)
            .OrderBy(cards => RandomNumber.Next())
            .ToDictionary(item => item.Key, item => item.Value);
        return ShuffledCards;
    }
}

WarCardGame 类继承自 CardGame 类,并添加了特定于战争纸牌游戏的附加字段、属性和方法。WarCardGame 类添加了用于开始游戏、执行战斗(参见下面的代码)以及确定战斗类型(单次、双次等)的方法。WarCardGame 类的字段存储诸如战争统计次数以及战争中要面朝下放置的牌数等值。有人可能会争辩说,WarCardGame 类的更多字段、属性和方法可以放在基类中,并在必要时进行重写。

public bool Battle()
{
    //Fill temp decks with players cards
    tempPlayer1Deck = Player1Deck.Select(cards => cards).ToDictionary(item => 
					item.Key, item => item.Value);
    tempPlayer2Deck = Player2Deck.Select(cards => cards).ToDictionary(item => 
					item.Key, item => item.Value);

    if (Player1Deck.Count() > 0) { Player1CardKey = Player1Deck.ElementAt(0).Key; }
    if (Player2Deck.Count() > 0) { Player2CardKey = Player2Deck.ElementAt(0).Key; }

    if (tempPlayer1Deck.Count() == 0)
    {
        StatusMessage = "Player 2 wins the game!";
        OutcomeCode = 3;
        return false;
    }
    else if (tempPlayer2Deck.Count() == 0)
    {
        StatusMessage = "Player 1 wins the game!";
        OutcomeCode = 1;
        return false;
    }
    else if (tempPlayer1Deck.Count() < (2 + cardsPlacedFaceDown) && 
	tempPlayer1Deck.ElementAt(0).Value == tempPlayer2Deck.ElementAt(0).Value)
    {
        // Game ended in war (tied in battle)
        // Player 1 doesn't have enough cards for a war
        Player2TricksWon++;
        TotalTricksWon++;
        UpdateWarTotals(tempCardsOnTheTable.Count());
        StatusMessage = "Player 2 wins the game! Game ended in a war.";
        OutcomeCode = 4;
        return false;
    }
    else if (tempPlayer2Deck.Count() < (2 + cardsPlacedFaceDown) && 
	tempPlayer1Deck.ElementAt(0).Value == tempPlayer2Deck.ElementAt(0).Value)
    {
        Player1TricksWon++;
        TotalTricksWon++;
        UpdateWarTotals(tempCardsOnTheTable.Count());
        StatusMessage = "Player 1 wins the game! Game ended in a war.";
        OutcomeCode = 2;
        return false;
    }
    else // Game hasn't ended so continue
    {
        if (CardsOnTheTable != null) // If last battle ended in war, 
				// collect cards left on table
        {
            tempCardsOnTheTable = CardsOnTheTable.Select(cards => 
		cards).ToDictionary(item => item.Key, item => item.Value);
        }

        Player1CardValue = tempPlayer1Deck.ElementAt(0).Value;
        Player2CardValue = tempPlayer2Deck.ElementAt(0).Value;

        // Begin battle, each player lays down face-up card
        tempCardsOnTheTable.Add(tempPlayer1Deck.ElementAt(0).Key, 
				tempPlayer1Deck.ElementAt(0).Value);
        tempPlayer1Deck.Remove(tempPlayer1Deck.ElementAt(0).Key);
        tempCardsOnTheTable.Add(tempPlayer2Deck.ElementAt(0).Key, 
				tempPlayer2Deck.ElementAt(0).Value);
        tempPlayer2Deck.Remove(tempPlayer2Deck.ElementAt(0).Key);
        
        // Randomizes order of card on table before they are placed 
        // back onto the bottom of the winning player's deck
        tempCardsOnTheTable = ShuffleCards(tempCardsOnTheTable);

        if (Player1CardValue > Player2CardValue)
        {
            StatusMessage = "Player 1 wins the battle.";
            // Add cards on table to winning player's hand
            var playerDeck1LINQ = tempPlayer1Deck.Select(cards => 
		cards).Concat(tempCardsOnTheTable.Select(cards => cards));
            tempPlayer1Deck = playerDeck1LINQ.ToDictionary(item => 
					item.Key, item => item.Value);
            Player1TricksWon++;
            TotalTricksWon++;
            UpdateWarTotals(tempCardsOnTheTable.Count());
            tempCardsOnTheTable.Clear(); 	// There is a winner so clear 
					// the table of cards
        }
        else if (Player1CardValue < Player2CardValue)
        {
            StatusMessage = "Player 2 wins the battle.";
            // Add cards on table to winning player's hand
            var playerDeck2LINQ = tempPlayer2Deck.Select(cards => 
		cards).Concat(tempCardsOnTheTable.Select(cards => cards));
            tempPlayer2Deck = playerDeck2LINQ.ToDictionary(item => 
					item.Key, item => item.Value);
            Player2TricksWon++;
            TotalTricksWon++;
            UpdateWarTotals(tempCardsOnTheTable.Count());
            tempCardsOnTheTable.Clear();
        }
        else if (Player1CardValue == Player2CardValue) // Players cards are 
						// of equal value - war
        {
            StatusMessage = "It's a tie. Time for war.";
            // Lay card(s) face-down on table for war
            for (int counter = 0; counter < cardsPlacedFaceDown; counter++)
            {
                tempCardsOnTheTable.Add(tempPlayer1Deck.ElementAt(0).Key, 
					tempPlayer1Deck.ElementAt(0).Value);
                tempPlayer1Deck.Remove(tempPlayer1Deck.ElementAt(0).Key);
                tempCardsOnTheTable.Add(tempPlayer2Deck.ElementAt(0).Key, 
					tempPlayer2Deck.ElementAt(0).Value);
                tempPlayer2Deck.Remove(tempPlayer2Deck.ElementAt(0).Key);
            }
        }
        // Battle over, reassign cards in correct locations
        Player1Deck = tempPlayer1Deck.Select(cards => 
		cards).ToDictionary(item => item.Key, item => item.Value);
        Player2Deck = tempPlayer2Deck.Select(cards => 
		cards).ToDictionary(item => item.Key, item => item.Value);
        CardsOnTheTable = tempCardsOnTheTable.Select(cards => 
		cards).ToDictionary(item => item.Key, item => item.Value);
        return true; //Game is not over
    }
}

WarCardGame 对象的实例在游戏过程中管理三组牌:玩家一的牌、玩家二的牌以及“桌上的牌”(玩家在战斗中打出的牌)。牌组存储在 Dictionary<string, int> 对象字段中。DictionaryKeyValuePair 元素代表单独的牌。Key 表示为 string 类型的牌,例如“9 ♥”。Value 表示为整数类型的牌值,例如“9”。

在战斗过程中,牌组使用 LINQ 表达式(System.LINQ 类)复制到三个临时 Dictionary 中的一个。在战斗期间,牌在三个临时 Dictionary 之间移动。战斗结束后,再次使用 LINQ 将牌复制回相应的 Dictionary 字段。LINQ 还用于反转牌的顺序以及连接 Dictionary 的内容。临时 Dictionary 代表战斗期间的牌组状态,而原始 Dictionary 字段代表战斗之前和之后的牌组状态。我认为使用这种方法有助于更容易地维护游戏“状态”。

在每场战斗之后以及游戏结束时,用于跟踪游戏状态的字段和属性都会更新,以反映当前的游戏状态——获得牌数、玩家获胜次数、战争次数等。统计数据通过 DisplayResults() 从 UI 返回。每副牌的内容显示为逗号分隔的 string,使用 DisplayDeck(Dictionary< string, int >)。可选的 DataToClipboard() 可以调用,以制表符分隔的 string 返回游戏统计数据,以便轻松导出到剪贴板并粘贴到 Excel 中进行测试和统计分析。

关注点

牌组权重

完成模拟后,我阅读了 Jacob Haqq-Misra 在《Science Creative Quarterly》上发表的在线文章《战争游戏的预测性》。阅读 Jocob 的文章后,我决定将他描述的牌组加权方法添加到模拟中。玩家初始 26 张牌的牌组的权重范围将在 +84 到 -84 之间。具有理想最大权重 +84 的牌组必须仅包含最高值的牌:4 张 A、4 张 K、4 张 Q 等,直到包含 8。权重理论认为,玩家牌组的权重越高,该玩家赢得比赛的几率就越高。

有趣的是(并且符合逻辑),对手的牌组(原始牌组的一半)的权重将始终与对手的牌组权重成反比,因为整个 52 张牌的牌组的总权重始终为 0(零)。如果玩家一的权重为 +25,则玩家二的牌组权重始终为 -25。即使牌被洗过,使得每位玩家的牌组完全随机,此反比规则也成立。

在模拟中,DeckWeight(Dictionary<string, int>) 方法接受一组牌,并根据牌的面值(参见下面的代码)为每张牌分配 -8 到 +8 之间的权重。DeckWeight(Dictionary<string, int>) 方法返回一个整数,表示牌组的权重——所有单张牌权重的总和。

public int CalculateDeckWeight(Dictionary deckToWeigh)
{
    int theDecksWeight = 0;
    foreach (KeyValuePair kvp in deckToWeigh)
    {
        theDecksWeight += kvp.Value - 8;
    }
    return theDecksWeight;
}

顺便说一句,在测试了几千场比赛后,我未能获得大于 +48 的玩家牌组权重。我理论上认为,模拟中的 52 张牌是通过一种方式“洗牌”的,这种方式产生的牌的随机排序比手工洗牌更充分。手工洗牌在确保牌的完全随机化方面效率不高。通过手工洗牌获得明显更高或更低权重牌组的几率似乎更大。

统计

此程序的下一个更新将包含 5000 场比赛的统计结果。使用源代码中包含的 GenerateGameData(int)DataToClipboard() 方法可以快速轻松地生成数据。可以将其粘贴到 Excel 中进行分析。

历史

  • 2009 年 6 月 20 日 - 版本 1.0
    • 初始版本
  • 2009 年 6 月 22 日 - 版本 1.1
    • 修复了 UpdateWarTotals(int) 方法中的一个小错误
    • 改进了游戏完成后重置属性的方式。影响了 StartGame() 方法
    • 重写了 ShuffleCards(Dictionary<string, int>)DealCards(Dictionary<string, int>, int) 方法,以更好地利用 LINQ
© . All rights reserved.