斗地主AI算法——第九章の被动出牌(3)


上一章已经说明了被动出牌算法基本的出牌思路,且以单牌为例给出了具体的代码。

对牌、三不带牌型实现方法与单牌基本类似。改动的地方主要是枚举牌类型,出牌时value_nPutCardList的处理,回溯时value_aHandCardList和nHandCardCount的变化等几个方面。下面给出完整代码:

/对牌类型
	else if (clsGameSituation.uctNowCardGroup.cgType == cgDOUBLE)
	{
		//剪枝:如果能出去最后一手牌直接出
		CardGroupData SurCardGroupData = ins_SurCardsType(clsHandCardData.value_aHandCardList);
		if (SurCardGroupData.cgType != cgERROR)
		{
			if (SurCardGroupData.cgType == cgDOUBLE&&SurCardGroupData.nMaxCard>clsGameSituation.uctNowCardGroup.nMaxCard)
			{
				Put_All_SurCards(clsGameSituation, clsHandCardData, SurCardGroupData);
				return;
			}
			else if (SurCardGroupData.cgType == cgBOMB_CARD || SurCardGroupData.cgType == cgKING_CARD)
			{
				Put_All_SurCards(clsGameSituation, clsHandCardData, SurCardGroupData);
				return;
			}
		}
		//-------------------------------------------对牌-------------------------------------------

		//暂存最佳的价值
		HandCardValue BestHandCardValue = get_HandCardValue(clsHandCardData);

		//我们认为不出牌的话会让对手一个轮次,即加一轮(权值减少7)便于后续的对比参考。
		BestHandCardValue.NeedRound += 1;

		//暂存最佳的牌号
		int BestMaxCard = 0;
		//是否出牌的标志
		bool PutCards = false;

		for (int i = clsGameSituation.uctNowCardGroup.nMaxCard + 1; i < 18; i++)
		{
			if (clsHandCardData.value_aHandCardList[i] > 1)
			{
				//尝试打出一对牌,估算剩余手牌价值
				clsHandCardData.value_aHandCardList[i]-=2;
				clsHandCardData.nHandCardCount-=2;
				HandCardValue tmpHandCardValue = get_HandCardValue(clsHandCardData);
				clsHandCardData.value_aHandCardList[i]+=2;
				clsHandCardData.nHandCardCount+=2;

				//选取总权值-轮次*7值最高的策略  因为我们认为剩余的手牌需要n次控手的机会才能出完,若轮次牌型很大(如炸弹) 则其-7的价值也会为正
				if ((BestHandCardValue.SumValue - (BestHandCardValue.NeedRound * 7)) <= (tmpHandCardValue.SumValue - (tmpHandCardValue.NeedRound * 7)))
				{
					BestHandCardValue = tmpHandCardValue;
					BestMaxCard = i;
					PutCards = true;
				}

			}
		}
		if (PutCards)
		{
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgDOUBLE, BestMaxCard, 2);
			return;
		}


		//-------------------------------------------炸弹-------------------------------------------

		for (int i = 3; i < 16; i++)
		{
			if (clsHandCardData.value_aHandCardList[i] ==4)
			{

				//尝试打出炸弹,估算剩余手牌价值,因为炸弹可以参与顺子,不能因为影响顺子而任意出炸
				clsHandCardData.value_aHandCardList[i] -= 4;
				clsHandCardData.nHandCardCount -= 4;
				HandCardValue tmpHandCardValue = get_HandCardValue(clsHandCardData);
				clsHandCardData.value_aHandCardList[i] += 4;
				clsHandCardData.nHandCardCount += 4;

				//选取总权值-轮次*7值最高的策略  因为我们认为剩余的手牌需要n次控手的机会才能出完,若轮次牌型很大(如炸弹) 则其-7的价值也会为正
				if ((BestHandCardValue.SumValue - (BestHandCardValue.NeedRound * 7)) <= (tmpHandCardValue.SumValue - (tmpHandCardValue.NeedRound * 7))
				//如果剩余手牌价值为正,证明出去的几率很大, 那么可以用炸获得先手
					|| tmpHandCardValue.SumValue > 0)
				{
					BestHandCardValue = tmpHandCardValue;
					BestMaxCard = i;
					PutCards = true;
				}

			}
		}
		if (PutCards)
		{
		    clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgBOMB_CARD, BestMaxCard, 4);
			return;
		}

		//王炸
		if (clsHandCardData.value_aHandCardList[17] > 0 && clsHandCardData.value_aHandCardList[16] > 0)
		{
			//如果剩余手牌价值为正,证明出去的几率很大,那么可以用炸获得先手,王炸20分
			if (BestHandCardValue.SumValue > 20)
			{
				clsHandCardData.value_nPutCardList.push_back(17);
				clsHandCardData.value_nPutCardList.push_back(16);
				clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgKING_CARD, 17, 2);
				return;
			}
		}

		//管不上
		clsHandCardData.uctPutCardType = get_GroupData(cgZERO, 0, 0);
		return;
	}
	//三牌类型
	else if (clsGameSituation.uctNowCardGroup.cgType == cgTHREE)
	{
		//剪枝:如果能出去最后一手牌直接出
		CardGroupData SurCardGroupData = ins_SurCardsType(clsHandCardData.value_aHandCardList);
		if (SurCardGroupData.cgType != cgERROR)
		{
			if (SurCardGroupData.cgType == cgTHREE&&SurCardGroupData.nMaxCard>clsGameSituation.uctNowCardGroup.nMaxCard)
			{
				Put_All_SurCards(clsGameSituation, clsHandCardData, SurCardGroupData);
				return;
			}
			else if (SurCardGroupData.cgType == cgBOMB_CARD || SurCardGroupData.cgType == cgKING_CARD)
			{
				Put_All_SurCards(clsGameSituation, clsHandCardData, SurCardGroupData);
				return;
			}
		}
		//-------------------------------------------三牌-------------------------------------------

		//暂存最佳的价值
		HandCardValue BestHandCardValue = get_HandCardValue(clsHandCardData);


		//我们认为不出牌的话会让对手一个轮次,即加一轮(权值减少7)便于后续的对比参考。
		BestHandCardValue.NeedRound += 1;

		//暂存最佳的牌号
		int BestMaxCard = 0;
		//是否出牌的标志
		bool PutCards = false;

		for (int i = clsGameSituation.uctNowCardGroup.nMaxCard + 1; i < 18; i++)
		{
			if (clsHandCardData.value_aHandCardList[i] > 2)
			{
				//尝试打出一对牌,估算剩余手牌价值
				clsHandCardData.value_aHandCardList[i] -= 3;
				clsHandCardData.nHandCardCount -= 3;
				HandCardValue tmpHandCardValue = get_HandCardValue(clsHandCardData);
				clsHandCardData.value_aHandCardList[i] += 3;
				clsHandCardData.nHandCardCount += 3;

				//选取总权值-轮次*7值最高的策略  因为我们认为剩余的手牌需要n次控手的机会才能出完,若轮次牌型很大(如炸弹) 则其-7的价值也会为正
				if ((BestHandCardValue.SumValue - (BestHandCardValue.NeedRound * 7)) <= (tmpHandCardValue.SumValue - (tmpHandCardValue.NeedRound * 7)))
				{
					BestHandCardValue = tmpHandCardValue;
					BestMaxCard = i;
					PutCards = true;
				}

			}
		}
		if (PutCards)
		{
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgTHREE, BestMaxCard, 3);
			return;
		}


		//-------------------------------------------炸弹-------------------------------------------

		for (int i = 3; i < 16; i++)
		{
			if (clsHandCardData.value_aHandCardList[i] == 4)
			{

				//尝试打出炸弹,估算剩余手牌价值,因为炸弹可以参与顺子,不能因为影响顺子而任意出炸
				clsHandCardData.value_aHandCardList[i] -= 4;
				clsHandCardData.nHandCardCount -= 4;
				HandCardValue tmpHandCardValue = get_HandCardValue(clsHandCardData);
				clsHandCardData.value_aHandCardList[i] += 4;
				clsHandCardData.nHandCardCount += 4;

				//选取总权值-轮次*7值最高的策略  因为我们认为剩余的手牌需要n次控手的机会才能出完,若轮次牌型很大(如炸弹) 则其-7的价值也会为正
				if ((BestHandCardValue.SumValue - (BestHandCardValue.NeedRound * 7)) <= (tmpHandCardValue.SumValue - (tmpHandCardValue.NeedRound * 7))
					//如果剩余手牌价值为正,证明出去的几率很大, 那么可以用炸获得先手
					|| tmpHandCardValue.SumValue > 0)
				{
					BestHandCardValue = tmpHandCardValue;
					BestMaxCard = i;
					PutCards = true;
				}

			}
		}
		if (PutCards)
		{
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.value_nPutCardList.push_back(BestMaxCard);
			clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgBOMB_CARD, BestMaxCard, 4);
			return;
		}

		//王炸
		if (clsHandCardData.value_aHandCardList[17] > 0 && clsHandCardData.value_aHandCardList[16] > 0)
		{
			//如果剩余手牌价值为正,证明出去的几率很大,那么可以用炸获得先手,王炸20分
			if (BestHandCardValue.SumValue > 20)
			{
				clsHandCardData.value_nPutCardList.push_back(17);
				clsHandCardData.value_nPutCardList.push_back(16);
				clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgKING_CARD, 17, 2);
				return;
			}
		}

		//管不上
		clsHandCardData.uctPutCardType = get_GroupData(cgZERO, 0, 0);
		return;
	}

接下来我们说一下顺子的处理方法。以单顺为例:

首先在第一阶段判断是否同类型牌里要额外增加一个条件,即顺子长度要一致

if (SurCardGroupData.cgType == cgSINGLE_LINE&&SurCardGroupData.nMaxCard>clsGameSituation.uctNowCardGroup.nMaxCard
				&&SurCardGroupData.nCount== clsGameSituation.uctNowCardGroup.nCount)
			{
				Put_All_SurCards(clsGameSituation, clsHandCardData, SurCardGroupData);
				return;
			}

然后除了设置BestHandCardValue等变量外,我们需要额外设置几个关于顺子的标志

		//验证顺子的标志
		int prov = 0;
		//顺子起点
		int start_i = 0;
		//顺子终点
		int end_i = 0;
		//顺子长度
		int length = clsGameSituation.uctNowCardGroup.nCount;

遍历顺子的方法有点类似于 最大子段和问题,大家可以参考下我以前的博客 http://blog.youkuaiyun.com/sm9sun/article/details/53240992

解决思路就是如果出现某张牌个数为0,那么必然不存在经过他的顺子,此时就把计数器置零,如果计数器长度大于等于length,即可以组成顺子,我们以当前下标i为最高标志构造出(i-length+1)~i的顺子。


举个例子:对方牌型为34567,我从4遍历至8,若满足,此时end_i=8,即45678,继续走到9,若还满足,end_i=9。即56789,若没有10,则prov归零,下一次循环若11存在,则prov=1。


		for (int i = clsGameSituation.uctNowCardGroup.nMaxCard - length + 2; i < 15; i++)
		{
			if (clsHandCardData.value_aHandCardList[i] > 0)
			{
				prov++;
			}
			else
			{
				prov = 0;
			}
			if (prov >= length)
			{
				end_i = i;
				start_i = i - length + 1;

				for (int j = start_i; j <= end_i; j++)
				{
					clsHandCardData.value_aHandCardList[j] --;
				}
				clsHandCardData.nHandCardCount -= clsGameSituation.uctNowCardGroup.nCount;
				HandCardValue tmpHandCardValue = get_HandCardValue(clsHandCardData);
				for (int j = start_i; j <= end_i; j++)
				{
					clsHandCardData.value_aHandCardList[j] ++;
				}
				clsHandCardData.nHandCardCount += clsGameSituation.uctNowCardGroup.nCount;

				//选取总权值-轮次*7值最高的策略  因为我们认为剩余的手牌需要n次控手的机会才能出完,若轮次牌型很大(如炸弹) 则其-7的价值也会为正
				if ((BestHandCardValue.SumValue - (BestHandCardValue.NeedRound * 7)) <= (tmpHandCardValue.SumValue - (tmpHandCardValue.NeedRound * 7)))
				{
					BestHandCardValue = tmpHandCardValue;
					BestMaxCard = end_i;
					PutCards = true;
				}

			}
		}

最后打出顺子的话在start_i和 end_i区间内依次减一即可。

		if (PutCards)
		{
			for (int j = start_i; j <= end_i; j++)
			{
				clsHandCardData.value_nPutCardList.push_back(j);
			}		
			clsHandCardData.uctPutCardType = clsGameSituation.uctNowCardGroup = get_GroupData(cgSINGLE_LINE, BestMaxCard, clsGameSituation.uctNowCardGroup.nCount);
			return;
		}

以上就是单顺的处理方法,下一章我们继续填充其他牌型的出牌方法。


敬请关注下一章:斗地主AI算法——第十章の被动出牌(4)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值