在写三国杀摸牌模拟器,并让AI也写了一份,在很多地方都比我的代码更加简洁,故记录。目前是使用C#控制台写一个简易版,还有许多功能未完成。之后考虑使用Unity做一个比较像样的。

其中最基本的就是卡牌类和几种牌堆。卡牌类定义如下:

class Card
    {
        public string Name { get; set; }  //牌名
        public string Suit { get; set; }  //花色:各花色使用英文首字母代替
        public string Point { get; set; } //点数

        public Card(string name, string suit, string point)
        {
            Name = name;
            Suit = suit;
            Point = point;
        }

        public override string ToString()  //重写System.Object的ToString()函数
        {
            return $"{Suit}{Point} {Name}";
        }
    }

之后,在Game类中定义牌堆和其他相关函数。这里只记录本文需要的部分:

class Game
    {
        private List<Card> deck;  //牌堆
        private List<Card> hand;  //手牌(堆)
        private List<Card> discardPile;  //弃牌堆
        private Random random;

        public Game()
        {
            deck = new List<Card>();
            hand = new List<Card>();
            discardPile = new List<Card>();
            random = new Random();
            InitializeDeck();
        }

        private void ShuffleDeck()  //洗牌
        {
            deck = deck.OrderBy(x => random.Next()).ToList();
        }
        //其他实现...
    }

一、OrderBy()函数实现洗牌

可以看到,洗牌功能只用了一行。以下是对这一行代码中关键部分的解释:

函数原型:OrderBy<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector),其中

  • TSource:集合中的元素类型(这里是 Card)。
  • TKey:排序依据的键类型(这里是 int,因为 random.Next() 返回整数)。

并使用Lambda 表达式 x => random.Next()给出了参数:

  • x:代表 deck 中的每个元素(即每张 Card)。
  • random.Next():返回int类型的随机数参与排序。

最终使用ToList()将排序后的IOrderedEnumerable<Card>类型转换回List<Card>类型,才可以赋值给deck。

而我原本的代码使用了Fisher-Yates洗牌算法,基本步骤为:

从数组的最后一个元素开始,对于每一个元素 i,生成一个从 0 到 i 范围内的随机数 j,然后将 i 位置的元素与 j 位置的元素交换。依次向前直到数组的第一个元素。代码如下:

private void ShuffleDeck() //Fisher-Yates 洗牌算法
{
    int n = deck.Count;
    for (int i = n - 1; i > 0; i--)
    {
        int j = random.Next(0, i + 1); // 随机选择 0 到 i 的索引
        Card temp = deck[i];
        deck[i] = deck[j];
        deck[j] = temp; // 交换
    }
}

对于这两种代码,效率是不同的。

时间上,洗牌算法由于只需一次遍历(n-1),每次遍历中复杂度为O(1),故时间复杂度为O(n);而OrderBy()使用的是快速排序,时间复杂度为O(nlogn)。

空间上,洗牌算法在原列表上修改,空间复杂度为O(1);OrderBy()和之后的ToList()各需要额外的O(n)大小的空间,最终空间复杂度为O(n)。

可见,虽然洗牌算法多了几行代码,但效率上还是比使用OrderBy()更胜一筹。结合实际情况,由于三国杀军争牌堆为160张,这个数字并不算大,所以实际使用这两种算法不会产生很明显的效率差异。

二、重写ToString()函数与ToString()函数的隐式调用

卡牌类Card自动继承了System.Object中的ToString()默认实现,会返回this.GetType().ToString()即该类类型。而对于Card类这并没有意义,故重写为更加实用的实现:

public override string ToString()  //重写System.Object的ToString()函数
{   return $"{Suit}{Point} {Name}"; }

此ToString()函数会输出[花色][点数] [牌名],并会在需要string类型时自动调用,十分方便。如:

Console.WriteLine(new Card("杀","H","8"));  //输出 H8 杀

如果没有重写ToString(),则输出:SanGuoShaCardSimulator.Card

或者:string str = "一张手牌" + new Card("杀", "H", "8");  //也会自动调用ToString()

三、string.Join()函数使用

在这个程序中,经常需要展示当前手牌(List<Card>类型),最容易想到的就是用foreach遍历输出。但AI直接给了一行代码实现:

Console.WriteLine(string.Join(", ", hand));

其中hand是List<Card>类型,表示手牌堆,比如是如下内容:

hand = new List<Card>
{
    new Card("杀", "S", "7"),
    new Card("桃", "H", "4"),
    new Card("顺手牵羊", "D", "3")
};

string.Join将这些字符串用分隔符", "连接起来并输出,结果为:S7 杀, H4 桃, D3 顺手牵羊。十分方便地实现了展示手牌功能,比foreach简洁很多。如果要换行输出也很简单,将第一个参数", "改为"\n"即可。

在string.Join执行的过程中也隐式调用了ToString(),因为string.Join需要字符串。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐