赛博斗地主——使用大语言模型扮演Agent智能体玩牌类游戏。

news/2025/3/17 13:57:49/文章来源:https://www.cnblogs.com/gmmy/p/18233297

通过大模型来实现多个智能体进行游戏对局这个想对已经比较成熟了无论是去年惊艳的斯坦福小镇还是比如metaGPT或者类似的框架都是使用智能体技术让大模型来操控,从而让大模型跳出自身“预测下一个token”的文字功能去探索更多的应用落地可能性。不过一直没有真正操作过,直到前段时间看到一个新闻《和GPT-4这些大模型玩狼人杀,人类因太蠢被票死,真·反向图灵测试》决定自己来玩一下。

斗地主是一款国人比较熟悉的棋牌游戏,考虑到这个游戏受众群体,所以基础大模型使用国产的通义千问提供的API接口(GPT4太贵用不起)。通过阿里云百炼大模型平台即可简单注册并申请使用:https://bailian.console.aliyun.com/

接着就是整体框架设计,其实整个游戏设计比较简单,随机发牌->随机定义一个玩家作为地主并发出尾牌(由于主要是模拟大模型使用Agent的玩牌所以这里就不加入抢地主环节了)->从地主开始玩家轮流出牌->谁的牌出完根据其角色决定是地主胜利还是农民胜利。

游戏整体使用c#编程,游戏主要的处理逻辑就是检测AI出牌的合法性,包括AI出牌是否是当前智能体的持有的手牌、牌型是否正确(单排/连子/对子/顺子/三带一/炸弹),出的牌是否可以压住上一轮玩家的牌等等逻辑。核心的部分如下:

public (CardsType, int[]) GetCardsType(string[] Cards)
{try{if (Cards.Length == 1)return (CardsType.单牌, GetCardsNumber(Cards));else if (Cards.Length == 2){if (Cards.OrderBy(x => x).SequenceEqual(new List<string>() { "小王", "大王" }.OrderBy(x => x)))return (CardsType.炸弹, GetCardsNumber(Cards));if (Cards.Select(ReplaceColor).Distinct().Count() == 1)return (CardsType.对子, GetCardsNumber(Cards));throw new Exception("");}else if (Cards.Length == 4){var groupCards = Cards.Select(ReplaceColor).GroupBy(x => x).OrderByDescending(x => x.Count()).ToList();//三带一if (groupCards.Count == 2 && groupCards[0].Count() == 3)return (CardsType.三带一, GetCardsNumber(groupCards[0].ToArray()));//三带一只需要看三张牌的大小即可//炸弹if (groupCards.Count == 1)return (CardsType.炸弹, GetCardsNumber(Cards));throw new Exception("");}else if (Cards.Length >= 5){//检测是否是顺子if (Cards.Length == 6){var groupCards = Cards.Select(ReplaceColor).GroupBy(x => x).OrderByDescending(x => x.Count()).ToList();if (groupCards.Count == 3 && groupCards.All(x => x.Count() == 2))return (CardsType.顺子, GetCardsNumber(groupCards[0].ToArray()));}var cardsnumber = GetCardsNumber(Cards);int? currItem = null;foreach (var item in cardsnumber){if (currItem == null)currItem = item;else if (currItem + 1 != item)throw new Exception("");}return (CardsType.连子, cardsnumber);}throw new Exception("");}catch (Exception e){throw new Exception($"当所选牌型无效,牌型只能是[{string.Join(",", Enum.GetNames(typeof(CardsType)))}],请检查你的牌型");}
}

以及玩牌部分的核心逻辑:

public void Play(string[] Cards)
{var currPlayer = GetCurrnetPlayer();if (Cards == null || Cards.Length == 0){if (!GameRecords.Any(x => x.Player != null)){throw new Exception("当前你是地主,必须进行出牌");}else if (GameRecords.Last(x => x.Player != null && x.Cards.Any()).Player.Name == currPlayer.Name){throw new Exception("上一轮你出牌后其他玩家都过了,本轮该你进行出牌(可以考虑出小牌)");}GameRecords.Add(new GameRecordInfo() { Player = currPlayer, GameRecordText = $"玩家名:{currPlayer.Name},角色:{currPlayer.Role},本轮没有出牌", CardsType = null, Cards = Cards });return;}//首先检查出牌是否在手牌中if (IsSubsetWithFrequency(Cards, currPlayer.HandCards.ToArray(), out var missingCards)){//检查最后一个牌组的情况if (GameRecords.Any(x => x.Player != null)){var last = GameRecords.Last(x => x.Player != null && x.Cards.Any());var lastcardstype = GetCardsType(last.Cards);var cardstype = GetCardsType(Cards);if (last.Player.Name != currPlayer.Name){if (lastcardstype.Item1 != cardstype.Item1 && cardstype.Item1 != CardsType.炸弹){throw new Exception($"无效出牌,上一轮的牌型是{lastcardstype.Item1},你必须使用相同牌型出牌");}//相同牌型则检测大小if (cardstype.Item1 == CardsType.单牌 || cardstype.Item1 == CardsType.对子 || cardstype.Item1 == CardsType.顺子 || cardstype.Item1 == CardsType.炸弹){if (lastcardstype.Item2[0] >= cardstype.Item2[0])throw new Exception($"无效出牌,你的出牌:[{string.Join(",", Cards)}]必须比上一轮出牌:[{string.Join(",", last.Cards)}]更大才行");}else{//连子的情况需要检测两个牌张数一致和最小长大于对方if (lastcardstype.Item2.Length != cardstype.Item2.Length)throw new Exception($"无效出牌,由于本轮出牌是连子所以你的出牌数:[{Cards.Length}]必须和一轮出牌数:[{last.Cards.Length}]一致");if (lastcardstype.Item2[0] >= cardstype.Item2[0])throw new Exception($"无效出牌,你的出牌:[{string.Join(",", Cards)}]必须比上一轮出牌:[{string.Join(",", last.Cards)}]更大才行");}}}}else{throw new Exception($"无效出牌,原因:{missingCards}。请重新出牌");}GameRecords.Add(new GameRecordInfo() { Player = currPlayer, GameRecordText = $"玩家名:{currPlayer.Name},角色:{currPlayer.Role},本轮出牌:[{string.Join(",", Cards)}],牌型:{GetCardsType(Cards).Item1}", CardsType = GetCardsType(Cards).Item1, Cards = Cards });Players[CurrnetPlayerIndex].HandCards.RemoveAll(x => Cards.Select(x => x.ToLower()).Contains(x.ToLower()));
}

接着就是一些游戏状态管理,包括初始化牌组、分派给三个玩家手牌,玩家自身的手牌管理等等这里就不一一赘述了,这里主要讲一下基于阿里千问大模型如何设计Agent代理的部分。在阿里百炼上,可以查看模型的调用示例,这里我们选择阿里目前最大的千亿参数大模型千问-MAX,进入调用示例就可以看到类似如下示例代码(如果你喜欢SDK则可以选择python和java的包。如果是其他语言则只有自己手写http请求调用):

 调用的部分比较简单,就是一个httpclient的封装,以及对调用入参和出参DTO的实体定义:

public class ApiClient
{private readonly HttpClient _httpClient;public ApiClient(string baseUrl, string apiKey){_httpClient = new HttpClient{BaseAddress = new Uri(baseUrl)};// 配置HttpClient实例_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));}public async Task<ApiResponse> PostAsync(string resource, TextGenerationRequest request){var jsonData = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });using var content = new StringContent(jsonData.ToLower(), Encoding.UTF8, "application/json");var response = await _httpClient.PostAsync(resource, content);if (response.IsSuccessStatusCode){var responseContent = await response.Content.ReadAsStringAsync();var options = new JsonSerializerOptions{PropertyNameCaseInsensitive = true  // 忽略大小写
            };return JsonSerializer.Deserialize<ApiResponse>(responseContent, options);}else{// 错误处理var errorContent = await response.Content.ReadAsStringAsync();throw new Exception($"API 请求失败: {response.StatusCode}, {errorContent}");}}}

接着就是比较关键的部分,即入参的定义,这决定了大模型如何调用智能体的关键,这里面其实主要还是编写特定的prompt让大模型知道自己要干嘛。由于是斗地主游戏,所以这里我们需要在系统提示词中编写一些关于斗地主的基本游戏规则、不同角色可以采取的常规游戏策略,游戏当前的对局情况。接着在用户提示词中需要告知大模型扮演智能体的角色、持有的手牌,可以调取的游戏函数。其中游戏函数比较关键,这也是大模型唯一可以让游戏“动起来”的方式。以下是我定义的关于斗地主游戏的请求入参:

TextGenerationRequest GetNowReq()
{var userprompt = "";if (game.GameRecords.Where(x => x.Player != null).Count() == 0){userprompt = $"现在是第一轮,你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}请先出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}else if (game.GameRecords.Any(x => x.Player != null) && game.GameRecords.Last(x => x.Player != null && x.Cards.Any()).Player.Name == game.GetCurrnetPlayer().Name){userprompt = $"你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}。上一轮其他玩家都过了你的牌,请你出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}else{userprompt = $"你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}。请出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌),或者选择本轮不出牌(当你的手牌都小于最后的出牌或者上一轮出牌的玩家是同组玩家时可以不出牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}return new TextGenerationRequest{Model = "qwen-max",Input = new InputData{Messages = new List<Message>{new Message { Role = "system", Content = $"""
                    你正在参与一场斗地主游戏,#游戏规则参与游戏的玩家由一个地主和两个农民组成,如果你是地主,你需要出掉所有的牌才能获得胜利。如果你是农民,你和你的队友任意一人出完所有的牌即可获胜。可以出单牌、【对子】(两张相同数字的牌,如:["♥3","♣3"])、【连子】(从小到大顺序数字的牌5张起出,如:["♥4","♣5","♦6","♣7","♥8"])、【顺子】(三个连起来的对子,如:["♥9","♣9","♦10","♣10","♣j","♥j"])、【三带一】(三张相同数字的牌+一张任意单牌,如:["♥4","♣4","♦4","♣6"])、【炸弹】(四个相同数字的牌或者双王,如:["♥4","♣4","♦4","♣4"]或者["小王","大王"]),牌从小到大顺序:3,4,5,6,7,8,9,10,j,q,k,a,2,小王,大王。每一轮出牌必须【大于】对方的出牌,并且必须和对方牌型一致[{string.Join(",",Enum.GetNames(typeof(CardsType)))}]##关于炸弹的特别规则如果当前手牌里有【炸弹】同时手牌里没有【大于】对方的出牌时,可以根据使用炸弹,炸弹可以最大程度的确保你拥有下一轮次的出牌权除非对手有比你更大的炸弹。所以尽可能的不要将炸弹的牌拆成对子、连子、顺子、三带一,如手牌是:["♥7","♣9","♦10","♣10","♣10","♥10"]尽可能不要拆成["♥7","♦10","♣10","♣10"]或者["♣10","♣10"]出牌注意双王是最大炸弹,四个2是第二大的炸弹,请谨慎使用。##斗地主常见出牌策略参考:#地主的策略快速出牌:地主的首要策略是尽可能快地出牌,减少农民合作的机会。地主手中有更多的牌,可以更灵活地控制游戏节奏。控制大牌:保留关键的大牌(如2、王等)来在关键时刻打破农民的配合或结束游戏。分割农民的牌:尝试通过出牌强迫农民拆散他们的对子或连牌,破坏他们的出牌计划。压制对手:地主可以通过连续出牌来压制农民,尤其是当发现农民手牌较少时,增加出牌速度,迫使他们出掉保留的大牌。记牌:地主需要注意记住已出的关键牌,尤其是农民已经出过的高牌,以合理规划自己的出牌策略。#农民的策略配合与合作:两名农民需要通过默契的配合来阻挡地主,比如其中一个尝试出小牌逼地主出大牌,另一个则保留大牌来后期制胜。堵牌:注意地主可能会形成的牌型,比如顺子、对子等,并尝试通过出相同类型的牌来堵截地主的出牌。牺牲策略:有时候,一名农民可能需要牺牲自己的一些好牌,以帮助另一名农民形成更强的牌型或打断地主的出牌计划。保存关键牌:农民应保存一些关键牌,如单张的王或2,用来在关键时刻打断地主的连胜。记牌与推算:农民需要密切注意牌局的走向和地主的出牌习惯,推算出地主可能保留的牌,合理规划自己的出牌策略。#所有玩家策略在斗地主中,观察和记牌是所有玩家的重要技能。无论是地主还是农民,合理利用手中的牌,观察对手的出牌习惯,以及与队友或自己的牌进行策略性的搭配,都是赢得游戏的关键因素。##游戏已进行的历史{game.GetGameRecordsHistory()}""" },new Message { Role = "user", Content =userprompt }}},Parameters = new InputParametersData(){Tools = new List<Tool>{new Tool{Type = "function",Function = new FunctionDetail{Name = "send_cards",Description = "出牌函数,用于本轮游戏出牌。你的出牌必须包含在你的手持牌组中",Parameters = new List<FunctionDetailParameter>(){new FunctionDetailParameter(){properties=new{Cards=new{type="string[]",description= "选择你要出的牌组,使用逗号\",\"分割,每一个牌必须使用\"\"包裹"}}}}}}}}};
}

接下来就是游戏的运行主要部分逻辑,定义一个游戏实例,通过一个死循环检测是否已经有玩家手牌出尽来判断游戏是否已经达到结局,没有出尽则依次让大模型调用智能体通过函数玩游戏,并且当模型出牌不符合规则时通过函数回调告知模型出错的逻辑指导模型重新进行对应的出牌:

Console.OutputEncoding = System.Text.Encoding.UTF8;Game game = new Game();var apiKey = "通过百炼模型平台申请你的API-KEY";
var baseUrl = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
var apiClient = new ApiClient(baseUrl, apiKey);var request = GetNowReq();
try
{var rollindex = 0;var rollbigindex = 1;while (!game.Players.Any(x=>x.HandCards.Count==0)){if (!game.GameRecords.Any()){game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮开始" });}ApiResponse response = default;try{response = await apiClient.PostAsync("", request);}catch (Exception e){var jsonData = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });File.WriteAllText("errdata.json", jsonData.ToLower());throw new Exception("接口请求异常,原始信息:" + e.Message);}if (response.Output.Choices[0].Message.Tool_Calls != null && response.Output.Choices[0].Message.Tool_Calls.Any()){try{var argument = JsonSerializer.Deserialize<CardsDto>(response.Output.Choices[0].Message.Tool_Calls.First().Function.Arguments);game.Play(argument.cards ?? Array.Empty<string>());var last = game.GameRecords.LastOrDefault(x => x.Player != null);Console.ForegroundColor = ConsoleColor.Green;if (Console.CursorLeft != 0)Console.WriteLine($"");Console.Write($"({last.Player.Role})玩家:{last.Player.Name}:");Console.ForegroundColor = ConsoleColor.Red;Console.Write((last.Cards == null || last.Cards.Length == 0) ? "" : game.GetCardsNumberText(last.Cards));Console.ResetColor();var messageContent = response.Output.Choices[0].Message.Content;if (!string.IsNullOrWhiteSpace(messageContent)){messageContent = messageContent.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");}Console.WriteLine($"({messageContent},余牌:{game.GetCurrnetPlayerHandCards()})");rollindex++;if (rollindex == 3){rollindex = 0;game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮结束,进入下一轮" });rollbigindex++;}game.MoveNextPlayer();request = GetNowReq();}catch(JsonException je){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = "传递了错误的函数调用字符串,无法转化成标准的json格式,原始字符串:" + response.Output.Choices[0].Message.Tool_Calls.First().Function.Arguments });}catch (Exception e){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = e.Message });}}else{try{game.Play(Array.Empty<string>());//不进行出牌var last = game.GameRecords.LastOrDefault(x => x.Player != null);Console.ForegroundColor = ConsoleColor.Green;if (Console.CursorLeft != 0)Console.WriteLine($"");Console.Write($"({last.Player.Role})玩家:{last.Player.Name}:");Console.ForegroundColor = ConsoleColor.Red;Console.Write((last.Cards == null || last.Cards.Length == 0) ? "" : game.GetCardsNumberText(last.Cards));Console.ResetColor();var messageContent = response.Output.Choices[0].Message.Content;if (!string.IsNullOrWhiteSpace(messageContent)){messageContent = messageContent.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");}Console.WriteLine($"({messageContent},余牌:{game.GetCurrnetPlayerHandCards()})");rollindex++;if (rollindex == 3){rollindex = 0;game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮结束,进入下一轮" });rollbigindex++;}game.MoveNextPlayer();request = GetNowReq();}catch (Exception e){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = e.Message });}}}Console.WriteLine($"游戏结束,{(game.Players.Any(x => x.Role == "地主" && x.HandCards.Any()) ? "农民胜利" : "地主胜利")}");
}
catch (Exception ex)
{Console.WriteLine(ex.Message);Console.WriteLine(ex.StackTrace);
}

以上内容基本就是主要的部分,演示的内容如下:

 可以看到模型的表现还是比较“蠢”,这是因为斗地主是一个典型的信息不完全(信息不透明)的游戏。这意味着在游戏过程中不是所有的信息都是对所有玩家开放的。策略的多样性和不确定性让玩家在游戏中必须基于有限的信息做出决策,比如是否抢地主(本示例没有)、如何出牌以及如何配合或对抗其他玩家。玩家的策略不仅受到手牌的限制,还受到对其他玩家策略的猜测和解读的影响。加之当前大模型对于数学的理解能力较差和逻辑短板导致其表现的比较“智障”。一般的斗地主AI主要依赖搜索算法+剪枝策略或者基于神经网络+强化学习+搜索算法来实现比如典型的棋牌类AI比如Pluribus和AlphaGo都是依赖类似的技术来实现,而大模型本身主要并非转向基于游戏决策做过训练,所以这里也就不展开了。本作主要还是想讨论大模型在智能体应用上有哪些可能的落地方式。

完整的代码如下,有兴趣的朋友可以自行申请百炼的千问API接口进行尝试(没有依赖任何包,所以可以创建一个控制台程序直接粘贴到program.cs即可运行):

using System.Collections;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;Console.OutputEncoding = System.Text.Encoding.UTF8;Game game = new Game();var apiKey = "通过百炼模型平台申请你的API-KEY";
var baseUrl = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
var apiClient = new ApiClient(baseUrl, apiKey);var request = GetNowReq();
try
{var rollindex = 0;var rollbigindex = 1;while (!game.Players.Any(x=>x.HandCards.Count==0)){if (!game.GameRecords.Any()){game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮开始" });}ApiResponse response = default;try{response = await apiClient.PostAsync("", request);}catch (Exception e){var jsonData = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });File.WriteAllText("errdata.json", jsonData.ToLower());throw new Exception("接口请求异常,原始信息:" + e.Message);}if (response.Output.Choices[0].Message.Tool_Calls != null && response.Output.Choices[0].Message.Tool_Calls.Any()){try{var argument = JsonSerializer.Deserialize<CardsDto>(response.Output.Choices[0].Message.Tool_Calls.First().Function.Arguments);game.Play(argument.cards ?? Array.Empty<string>());var last = game.GameRecords.LastOrDefault(x => x.Player != null);Console.ForegroundColor = ConsoleColor.Green;if (Console.CursorLeft != 0)Console.WriteLine($"");Console.Write($"({last.Player.Role})玩家:{last.Player.Name}:");Console.ForegroundColor = ConsoleColor.Red;Console.Write((last.Cards == null || last.Cards.Length == 0) ? "" : game.GetCardsNumberText(last.Cards));Console.ResetColor();var messageContent = response.Output.Choices[0].Message.Content;if (!string.IsNullOrWhiteSpace(messageContent)){messageContent = messageContent.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");}Console.WriteLine($"({messageContent},余牌:{game.GetCurrnetPlayerHandCards()})");rollindex++;if (rollindex == 3){rollindex = 0;game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮结束,进入下一轮" });rollbigindex++;}game.MoveNextPlayer();request = GetNowReq();}catch(JsonException je){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = "传递了错误的函数调用字符串,无法转化成标准的json格式,原始字符串:" + response.Output.Choices[0].Message.Tool_Calls.First().Function.Arguments });}catch (Exception e){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = e.Message });}}else{try{game.Play(Array.Empty<string>());//不进行出牌var last = game.GameRecords.LastOrDefault(x => x.Player != null);Console.ForegroundColor = ConsoleColor.Green;if (Console.CursorLeft != 0)Console.WriteLine($"");Console.Write($"({last.Player.Role})玩家:{last.Player.Name}:");Console.ForegroundColor = ConsoleColor.Red;Console.Write((last.Cards == null || last.Cards.Length == 0) ? "" : game.GetCardsNumberText(last.Cards));Console.ResetColor();var messageContent = response.Output.Choices[0].Message.Content;if (!string.IsNullOrWhiteSpace(messageContent)){messageContent = messageContent.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");}Console.WriteLine($"({messageContent},余牌:{game.GetCurrnetPlayerHandCards()})");rollindex++;if (rollindex == 3){rollindex = 0;game.GameRecords.Add(new GameRecordInfo() { GameRecordText = $"第{rollbigindex}轮结束,进入下一轮" });rollbigindex++;}game.MoveNextPlayer();request = GetNowReq();}catch (Exception e){var last = game.GetCurrnetPlayer();request = GetNowReq();request.Input.Messages.Add(response.Output.Choices[0].Message);request.Input.Messages.Add(new Message() { Role = "tool", Name = "send_cards", Content = e.Message });}}}Console.WriteLine($"游戏结束,{(game.Players.Any(x => x.Role == "地主" && x.HandCards.Any()) ? "农民胜利" : "地主胜利")}");
}
catch (Exception ex)
{Console.WriteLine(ex.Message);Console.WriteLine(ex.StackTrace);
}
void readerrdatatest()
{var json = File.ReadAllText("errdata.json");var obj = JsonSerializer.Deserialize<TextGenerationRequest>(json,new JsonSerializerOptions
{PropertyNameCaseInsensitive = true
});
}
TextGenerationRequest GetNowReq()
{var userprompt = "";if (game.GameRecords.Where(x => x.Player != null).Count() == 0){userprompt = $"现在是第一轮,你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}请先出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}else if (game.GameRecords.Any(x => x.Player != null) && game.GameRecords.Last(x => x.Player != null && x.Cards.Any()).Player.Name == game.GetCurrnetPlayer().Name){userprompt = $"你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}。上一轮其他玩家都过了你的牌,请你出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}else{userprompt = $"你是{game.GetCurrnetPlayer().Name},你的角色是:{game.GetCurrnetPlayer().Role}。请出牌(如果单牌较少可以考虑尽可能出顺子、连子、三带一或者对子,如果单牌较多则优先考虑出单牌),或者选择本轮不出牌(当你的手牌都小于最后的出牌或者上一轮出牌的玩家是同组玩家时可以不出牌)\r\n手持牌组:{game.GetCurrnetPlayerHandCards()}";}return new TextGenerationRequest{Model = "qwen-max",Input = new InputData{Messages = new List<Message>{new Message { Role = "system", Content = $"""
                    你正在参与一场斗地主游戏,#游戏规则参与游戏的玩家由一个地主和两个农民组成,如果你是地主,你需要出掉所有的牌才能获得胜利。如果你是农民,你和你的队友任意一人出完所有的牌即可获胜。可以出单牌、【对子】(两张相同数字的牌,如:["♥3","♣3"])、【连子】(从小到大顺序数字的牌5张起出,如:["♥4","♣5","♦6","♣7","♥8"])、【顺子】(三个连起来的对子,如:["♥9","♣9","♦10","♣10","♣j","♥j"])、【三带一】(三张相同数字的牌+一张任意单牌,如:["♥4","♣4","♦4","♣6"])、【炸弹】(四个相同数字的牌或者双王,如:["♥4","♣4","♦4","♣4"]或者["小王","大王"]),牌从小到大顺序:3,4,5,6,7,8,9,10,j,q,k,a,2,小王,大王。每一轮出牌必须【大于】对方的出牌,并且必须和对方牌型一致[{string.Join(",",Enum.GetNames(typeof(CardsType)))}]##关于炸弹的特别规则如果当前手牌里有【炸弹】同时手牌里没有【大于】对方的出牌时,可以根据使用炸弹,炸弹可以最大程度的确保你拥有下一轮次的出牌权除非对手有比你更大的炸弹。所以尽可能的不要将炸弹的牌拆成对子、连子、顺子、三带一,如手牌是:["♥7","♣9","♦10","♣10","♣10","♥10"]尽可能不要拆成["♥7","♦10","♣10","♣10"]或者["♣10","♣10"]出牌注意双王是最大炸弹,四个2是第二大的炸弹,请谨慎使用。##斗地主常见出牌策略参考:#地主的策略快速出牌:地主的首要策略是尽可能快地出牌,减少农民合作的机会。地主手中有更多的牌,可以更灵活地控制游戏节奏。控制大牌:保留关键的大牌(如2、王等)来在关键时刻打破农民的配合或结束游戏。分割农民的牌:尝试通过出牌强迫农民拆散他们的对子或连牌,破坏他们的出牌计划。压制对手:地主可以通过连续出牌来压制农民,尤其是当发现农民手牌较少时,增加出牌速度,迫使他们出掉保留的大牌。记牌:地主需要注意记住已出的关键牌,尤其是农民已经出过的高牌,以合理规划自己的出牌策略。#农民的策略配合与合作:两名农民需要通过默契的配合来阻挡地主,比如其中一个尝试出小牌逼地主出大牌,另一个则保留大牌来后期制胜。堵牌:注意地主可能会形成的牌型,比如顺子、对子等,并尝试通过出相同类型的牌来堵截地主的出牌。牺牲策略:有时候,一名农民可能需要牺牲自己的一些好牌,以帮助另一名农民形成更强的牌型或打断地主的出牌计划。保存关键牌:农民应保存一些关键牌,如单张的王或2,用来在关键时刻打断地主的连胜。记牌与推算:农民需要密切注意牌局的走向和地主的出牌习惯,推算出地主可能保留的牌,合理规划自己的出牌策略。#所有玩家策略在斗地主中,观察和记牌是所有玩家的重要技能。无论是地主还是农民,合理利用手中的牌,观察对手的出牌习惯,以及与队友或自己的牌进行策略性的搭配,都是赢得游戏的关键因素。##游戏已进行的历史{game.GetGameRecordsHistory()}""" },new Message { Role = "user", Content =userprompt }}},Parameters = new InputParametersData(){Tools = new List<Tool>{new Tool{Type = "function",Function = new FunctionDetail{Name = "send_cards",Description = "出牌函数,用于本轮游戏出牌。你的出牌必须包含在你的手持牌组中",Parameters = new List<FunctionDetailParameter>(){new FunctionDetailParameter(){properties=new{Cards=new{type="string[]",description= "选择你要出的牌组,使用逗号\",\"分割,每一个牌必须使用\"\"包裹"}}}}}}}}};
}
public class ApiClient
{private readonly HttpClient _httpClient;public ApiClient(string baseUrl, string apiKey){_httpClient = new HttpClient{BaseAddress = new Uri(baseUrl)};// 配置HttpClient实例_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));}public async Task<ApiResponse> PostAsync(string resource, TextGenerationRequest request){var jsonData = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });using var content = new StringContent(jsonData.ToLower(), Encoding.UTF8, "application/json");var response = await _httpClient.PostAsync(resource, content);if (response.IsSuccessStatusCode){var responseContent = await response.Content.ReadAsStringAsync();var options = new JsonSerializerOptions{PropertyNameCaseInsensitive = true  // 忽略大小写
            };return JsonSerializer.Deserialize<ApiResponse>(responseContent, options);}else{// 错误处理var errorContent = await response.Content.ReadAsStringAsync();throw new Exception($"API 请求失败: {response.StatusCode}, {errorContent}");}}}
public class TextGenerationRequest
{public string Model { get; set; }public InputData Input { get; set; }public InputParametersData Parameters { get; set; }
}
public class InputParametersData
{public string Result_Format { get; set; } = "message";public List<Tool> Tools { get; set; }
}
public class InputData
{public List<Message> Messages { get; set; }
}public class Message
{public string Name { get; set; }public string Role { get; set; }public List<ToolCall> Tool_Calls { get; set; }public string Content { get; set; }
}
public class Tool
{public string Type { get; set; }public FunctionDetail Function { get; set; }
}
public class ToolCall
{public FunctionCall Function { get; set; }public string Id { get; set; }public string Type { get; set; }
}public class FunctionCall
{public string Name { get; set; }public string Arguments { get; set; }
}public class FunctionDetail
{public string Name { get; set; }public string Description { get; set; }public List<FunctionDetailParameter> Parameters { get; set; }
}
public class FunctionDetailParameter
{public string type { get; set; } = "object";public object properties { get; set; }
}
public class ApiResponse
{public OutputResponse Output { get; set; }public UsageData Usage { get; set; }public string RequestId { get; set; }
}public class OutputResponse
{public List<Choice> Choices { get; set; }
}public class Choice
{public string FinishReason { get; set; }public Message Message { get; set; }
}public class UsageData
{public int TotalTokens { get; set; }public int OutputTokens { get; set; }public int InputTokens { get; set; }
}
public class CardsDto
{public string[] cards { get; set; }
}
public class Game
{string[] array = ["3", "4", "5", "6", "7", "8", "9", "10", "j", "q", "k", "a", "2", "小王", "大王"];public List<string> Deck { get; private set; }public List<Player> Players { get; private set; }public List<string> BottomCards { get; private set; }public Player Landlord { get; private set; }public int CurrnetPlayerIndex = 0;public List<GameRecordInfo> GameRecords { get; set; }public Game(){Players = new List<Player> { new Player("Player 1"), new Player("Player 2"), new Player("Player 3") };GameRecords = new List<GameRecordInfo>();Deck = GenerateDeck();ShuffleDeck();DealCards();ChooseLandlord();}public string GetGameRecordsHistory(){return string.Join("\r\n", GameRecords.Select(x => x.GameRecordText));}public string GetCurrnetPlayerHandCards(int? index = null){return string.Join(",", Players[index ?? CurrnetPlayerIndex].HandCards.OrderBy(x => Array.IndexOf(array, ReplaceColor(x))));}public Player GetCurrnetPlayer(){return Players[CurrnetPlayerIndex];}public int[] GetCardsNumber(string[] Cards){var cardsnumber = Cards.Select(x =>{if (x == "小王" || x == "大王")return Array.IndexOf(array, x);else{var num = ReplaceColor(x);return Array.IndexOf(array, num);}}).ToArray();return cardsnumber.Order().ToArray();}string ReplaceColor(string card) => card.Replace("", "").Replace("", "").Replace("", "").Replace("", "").ToLower();public string GetCardsNumberText(string[] Cards){return string.Join(",", Cards);}public (CardsType, int[]) GetCardsType(string[] Cards){try{if (Cards.Length == 1)return (CardsType.单牌, GetCardsNumber(Cards));else if (Cards.Length == 2){if (Cards.OrderBy(x => x).SequenceEqual(new List<string>() { "小王", "大王" }.OrderBy(x => x)))return (CardsType.炸弹, GetCardsNumber(Cards));if (Cards.Select(ReplaceColor).Distinct().Count() == 1)return (CardsType.对子, GetCardsNumber(Cards));throw new Exception("");}else if (Cards.Length == 4){var groupCards = Cards.Select(ReplaceColor).GroupBy(x => x).OrderByDescending(x => x.Count()).ToList();//三带一if (groupCards.Count == 2 && groupCards[0].Count() == 3)return (CardsType.三带一, GetCardsNumber(groupCards[0].ToArray()));//三带一只需要看三张牌的大小即可//炸弹if (groupCards.Count == 1)return (CardsType.炸弹, GetCardsNumber(Cards));throw new Exception("");}else if (Cards.Length >= 5){//检测是否是顺子if (Cards.Length == 6){var groupCards = Cards.Select(ReplaceColor).GroupBy(x => x).OrderByDescending(x => x.Count()).ToList();if (groupCards.Count == 3 && groupCards.All(x => x.Count() == 2))return (CardsType.顺子, GetCardsNumber(groupCards[0].ToArray()));}var cardsnumber = GetCardsNumber(Cards);int? currItem = null;foreach (var item in cardsnumber){if (currItem == null)currItem = item;else if (currItem + 1 != item)throw new Exception("");}return (CardsType.连子, cardsnumber);}throw new Exception("");}catch (Exception e){throw new Exception($"当所选牌型无效,牌型只能是[{string.Join(",", Enum.GetNames(typeof(CardsType)))}],请检查你的牌型");}}public void MoveNextPlayer(){CurrnetPlayerIndex++;if (CurrnetPlayerIndex == Players.Count)CurrnetPlayerIndex = 0;}public void Play(string[] Cards){var currPlayer = GetCurrnetPlayer();if (Cards == null || Cards.Length == 0){if (!GameRecords.Any(x => x.Player != null)){throw new Exception("当前你是地主,必须进行出牌");}else if (GameRecords.Last(x => x.Player != null && x.Cards.Any()).Player.Name == currPlayer.Name){throw new Exception("上一轮你出牌后其他玩家都过了,本轮该你进行出牌(可以考虑出小牌)");}GameRecords.Add(new GameRecordInfo() { Player = currPlayer, GameRecordText = $"玩家名:{currPlayer.Name},角色:{currPlayer.Role},本轮没有出牌", CardsType = null, Cards = Cards });return;}//首先检查出牌是否在手牌中if (IsSubsetWithFrequency(Cards, currPlayer.HandCards.ToArray(), out var missingCards)){//检查最后一个牌组的情况if (GameRecords.Any(x => x.Player != null)){var last = GameRecords.Last(x => x.Player != null && x.Cards.Any());var lastcardstype = GetCardsType(last.Cards);var cardstype = GetCardsType(Cards);if (last.Player.Name != currPlayer.Name){if (lastcardstype.Item1 != cardstype.Item1 && cardstype.Item1 != CardsType.炸弹){throw new Exception($"无效出牌,上一轮的牌型是{lastcardstype.Item1},你必须使用相同牌型出牌");}//相同牌型则检测大小if (cardstype.Item1 == CardsType.单牌 || cardstype.Item1 == CardsType.对子 || cardstype.Item1 == CardsType.顺子 || cardstype.Item1 == CardsType.炸弹){if (lastcardstype.Item2[0] >= cardstype.Item2[0])throw new Exception($"无效出牌,你的出牌:[{string.Join(",", Cards)}]必须比上一轮出牌:[{string.Join(",", last.Cards)}]更大才行");}else{//连子的情况需要检测两个牌张数一致和最小长大于对方if (lastcardstype.Item2.Length != cardstype.Item2.Length)throw new Exception($"无效出牌,由于本轮出牌是连子所以你的出牌数:[{Cards.Length}]必须和一轮出牌数:[{last.Cards.Length}]一致");if (lastcardstype.Item2[0] >= cardstype.Item2[0])throw new Exception($"无效出牌,你的出牌:[{string.Join(",", Cards)}]必须比上一轮出牌:[{string.Join(",", last.Cards)}]更大才行");}}}}else{throw new Exception($"无效出牌,原因:{missingCards}。请重新出牌");}GameRecords.Add(new GameRecordInfo() { Player = currPlayer, GameRecordText = $"玩家名:{currPlayer.Name},角色:{currPlayer.Role},本轮出牌:[{string.Join(",", Cards)}],牌型:{GetCardsType(Cards).Item1}", CardsType = GetCardsType(Cards).Item1, Cards = Cards });Players[CurrnetPlayerIndex].HandCards.RemoveAll(x => Cards.Select(x => x.ToLower()).Contains(x.ToLower()));}private bool IsSubsetWithFrequency(string[] smallList, string[] bigList, out string missingElements){var bigCount = bigList.GroupBy(x => x).ToDictionary(g => g.Key.ToLower(), g => g.Count());var smallCount = smallList.GroupBy(x => x).ToDictionary(g => g.Key.ToLower(), g => g.Count());var missingList = new List<string>();foreach (var num in smallList.Select(x => x.ToLower())){if (!bigCount.ContainsKey(num) || bigCount[num] == 0){missingList.Add(num);}else{bigCount[num]--;}}bigCount = bigList.GroupBy(x => x).ToDictionary(g => g.Key.ToLower(), g => g.Count());StringBuilder sb = new StringBuilder();foreach (var item in missingList.Distinct().ToArray()){var smallval = smallCount[item];//检测一下其他色号var num = ReplaceColor(item);Func<string, bool> check = x => ReplaceColor(x) == num;if (!bigCount.ContainsKey(item)){if (bigList.Any(check)){var all = bigList.Where(check).ToList();sb.AppendLine($"你所选的牌{item},不在你的手牌中,可以选择手牌中同数字不同花色的牌:{string.Join(",", all)}");}else{sb.AppendLine($"你所选的牌{item},不在你的手牌中");}}else{if (bigList.Any(check)){var all = bigList.Where(check).ToList();sb.AppendLine($"你选了{smallval}张{item},但是你的手牌中只有{bigCount[item]}张{item}, 可以选择手牌中同数字不同花色的牌:{string.Join(",", all.Where(x => x != item))}");}else{sb.AppendLine($"你选了{smallval}张{item},但是你的手牌中只有{bigCount[item]}张{item}");}}}missingElements = sb.ToString();return missingList.Distinct().ToArray().Length == 0;}private List<string> GenerateDeck(){string[] suits = { "", "", "", "" };string[] ranks = { "3", "4", "5", "6", "7", "8", "9", "10", "j", "q", "k", "a", "2" };List<string> deck = new List<string>();foreach (var suit in suits){foreach (var rank in ranks){deck.Add($"{suit}{rank}");}}deck.Add("小王");deck.Add("大王");return deck;}private void ShuffleDeck(){Random rng = new Random();int n = Deck.Count;while (n > 1){n--;int k = rng.Next(n + 1);var value = Deck[k];Deck[k] = Deck[n];Deck[n] = value;}}private void DealCards(){for (int i = 0; i < 17; i++){foreach (var player in Players){player.HandCards.Add(Deck.First());Deck.RemoveAt(0);}}BottomCards = Deck.ToList();Deck.Clear();}private void ChooseLandlord(){int landlordIndex = new Random().Next(Players.Count);Landlord = Players[landlordIndex];CurrnetPlayerIndex = landlordIndex;Console.WriteLine($"{Landlord.Name} 是候选地主。");Landlord.HandCards.AddRange(BottomCards);Landlord.Role = "地主";Console.WriteLine($"{Landlord.Name} 成为地主,获得底牌。");}
}public class Player
{public string Role { get; set; }public string Name { get; private set; }public List<string> HandCards { get; private set; }public Player(string name){Name = name;Role = "农民";HandCards = new List<string>();}
}
public class GameRecordInfo
{public Player Player { get; set; }public string GameRecordText { get; set; }public CardsType? CardsType { get; set; }public int[] CardsNumber { get; set; }public string[] Cards { get; set; }
}
public enum CardsType
{单牌, 对子, 连子, 顺子, 三带一, 炸弹
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/720391.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ollama,在centos7宿主机上,curl http://宿主机IP:11434 提示拒绝访问 ,但是curl http://localhost:11434 正常

Ollama,在centos7宿主机上,curl http://宿主机IP:11434 提示拒绝访问 ,但是curl http://localhost:11434 正常1.使用 netstat 或 ss 命令在宿主机上检查 11434 端口的状态[root@localhost ~]# netstat -tulnp | grep 11434 tcp 0 0 127.0.0.1:11434 0.…

NFS,smb和数据库文件

nfs的搭建网上有很多,可自行查看 Windows Server2012 R2搭建NFS服务器 - 知乎 (zhihu.com) 其中Windows10家庭版不支持NFS客户端,目前Windows上的协议是V3版本,防火墙上有NFS的选项,端口2049udp和tcp 在客户端上的访问和smb一样,都可映射网络驱动器,使用\\ip\目录方式访问…

JavaDoc生成文档

javaDoc命令是用来生成自己API文档的参数信息:@auchor:作者名 @version:版本号 @since:指明需要最早使用的jdk版本 @param:参数名 @return:返回值情况 @throws:异常抛出情况//主要生成的参数 如:/*** @author 林伟填* @version 1.0* @since 1.8*/public class doc {Str…

心诺安 x TapData:快速搭建云中数仓,助力电商企业实施“以用户为中心的”精细化运营

电商企业如何充分整合并利用自身数据资源,实现高效的数据管理和业务流程优化,迎接用户导向的电商精细化运营时代的挑战。使用 TapData,化繁为简,摆脱手动搭建、维护数据管道的诸多烦扰,轻量代替 OGG、DSG 等同步工具,「CDC + 流处理 + 数据集成」组合拳,加速仓内数据流转…

Docker安装使用教程

Docker安装使用教程Docker是什么Docker是一个容器化开源平台,它使开发者可以再容器中封装应用程序,以及其依赖的所有组件,包括操作系统、库文件、环境变量等,并以轻量级、可移植的方式进行交付和部署.Docker的三大核心概念是:镜像(Image):镜像是Docker的基本构建块,它是一…

未来5年,只有这种产品团队才能开启上帝视角【玩转IPD】

一家企业如何在波涛汹涌的市场浪潮中站稳脚跟?一个团队如何快速识别风险发现机遇,成为行业的标杆?一家企业如何在波涛汹涌的市场浪潮中站稳脚跟?一个团队如何快速识别风险发现机遇,成为行业的标杆?市场瞬息万变,如何准确地响应市场动向,紧跟用户需求?这些问题,已成为…

Linux 部署 MinIO(远程服务器)

1. 下载安装 进入 Linux 内# 我习惯放在local下 cd /usr/local/# 新建目录 mkdir minio # 进入目录 cd minio下载路径:# 下载地址 wget https://dl.min.io/server/minio/release/linux-amd64/minio授权:# 授权 chmod +x minio 2. 自定义配置 自定义账号与登录密码,直接在本…

STM32H743 ADC+DMA

1.** STM32CubeMX 配置如下:** ADC: 配置通道配置ADC的工作模式,这里用到了DMA使用ADC中断DMA2的通道4对应ADC2GPIO的配置,没有配置上下拉DMA配置:用STM32H743用DMA传输ADC的数据会有一个需要注意的点,需要将传输数据的buf配置在固定的某一段RAM中。 #define ADC_CONVERTE…

裁剪的3种方式,CSS 如何隐藏移动端的滚动条?

在移动端开发中,经常会碰到需要横向滚动的场景,例如这样的但很多时候是不需要展示这个滚动条的,也就是这样的效果,如下你可能想到直接设置滚动条样式就可以了,就像这样::-webkit-scrollbar {display: none; }目前来看好像没什么问题,但在某些版本的 iOS 上却无效(具体待…

golang使用OpenCC繁简转换

https://github.com/longbridgeapp/opencc main.go package mainimport ("fmt""log""github.com/longbridgeapp/opencc" )func main() {s2t, err := opencc.New("s2t")if err != nil {log.Fatal(err)}in := `我来测试一下简转繁`out, …

visual studio 插件开发 - 项目介绍

1.项目结构 创建步骤: 1.创建名为 xxxx 的 VSIX 项目。 可以通过搜索“vsix”在“新建项目”对话框中找到 VSIX 项目模板。2.项目打开时,添加名为 FirstCommand 的自定义命令项模板。创建好一个 vsix 项目后最简单的结构:XXXXPackage.cs 称为 Package 类。 Visual Studio 调…

ASP.NET Core应用程序9:使用内置的标签助手

ASP.NET Core 提供了一组内置的标签助手,可以应用最常用的元素转换。使用了内置的标签助手,就不必像前一章一样自己创建自定义标签助手。本章描述了基本的内置标签助手,并解释了它们是如何用于转换锚、链接、脚本和图像元素的。还解释了如何缓存内容部分以及如何根据应用程序…