【日常练习】猪国杀【大大大大大模拟】

前言

由于久仰大名+一时兴起+最近考试总是打挂细节,于是终于对这道题下手了.jpg
实际上调试过程比想象中要好点,再加上有LOJ数据帮忙,码基础函数花了4h左右,然后从第一次提交到AC也就花了3h。
算是退役前,了却一个心愿吧。

正文

关于题目内容大家可以自己去看,我就不说了。我在这里主要阐述一下打这道题代码的核心思路。

众所周知,打这种大模拟题必须思路清晰,并且要有一些基础函数的帮忙,才能在保证代码量和调试难度的基础上AC。

那么那道题,我的基础思路是这样的:

1、猪的身份、是否表现、装备、HP ---->结构体
2、摸牌阶段、出牌阶段、字符转数字 ----->switch、for、if
3、锦囊牌、基本牌——函数实现

这是一个最基础的大体架构,即我们打算以怎样的复杂度来实现。尽管模拟题一般都不会卡你的复杂度,但是追求一种更优秀的方法总是好的。

于是在敲定这一点后,我就开始动手打代码了。

一、基础部分

我本来一开始就打算写锦囊牌的,但是写到一半就发现要实现的很多东西都会在别的地方重复实现,于是我就先开了个叫Basic的namespace来实现一些基本的操作。
基本的操作有哪些呢?
抽牌,询问某玩家有多少张某种牌,扣除某玩家的某数量的某种牌,将手牌向左靠拢(填补空缺)。这些是与手牌相关的。
是否死亡的判定,死亡之后的奖惩机制,游戏结束。这是与死亡相关的。
除此之外,还有用来判断a对b的看法的recognize函数,字符数字相互转换的get/reget id函数。由于我采用的变量名算得上是浅显易懂,所以直接看代码应该也没问题,这里就不多阐述了:

struct Pig{
	int HP,num,hand[1002];
	//生命值,手牌数,手牌
	int type,equ,show,re;
	//身份,是否装备,是否跳忠/反/类反,是否需要洗手牌
	//type=1/2/3分别对应主公,忠臣,反贼
	//show=0/1/2/3/4分别对应未知,主公,跳忠,跳反,类反
	//re是因为在装备/杀人摸牌之后优先使用最左侧的能用的牌
	//因此需要重新从最左边开始扫
	int l,r;
	//顺时针的人,逆时针的人
}pig[13];

namespace Basic
{
	int card[2010],card_num,num_fan;
	namespace Trans{//数字和字符的转换
		iint get_id(char c)
		{
			switch(c){
				case 'K':return 1;break;
				case 'D':return 2;break; 
				case 'P':return 3;break;
				case 'F':return 4;break;
				case 'N':return 5;break;
				case 'W':return 6;break; 		
				case 'J':return 7;break;
				case 'Z':return 8;break; 		
			}
		}
	
		iint reget_id(int x)
		{
			switch(x){
				case 1:return 'K';break;
				case 2:return 'D';break; 
				case 3:return 'P';break;
				case 4:return 'F';break; 
				case 5:return 'N';break;
				case 6:return 'W';break; 		
				case 7:return 'J';break;
				case 8:return 'Z';break; 		
			}
		}
	}using namespace Trans;
		
 	namespace Card{//手牌相关
		ivoid get_card(int id,int num)
		{
			for(rint i=1;i<=num;i++){
				if(!card_num)card_num++;
				pig[id].hand[++pig[id].num]=card[card_num--];
			}
			
		}
	
		ivoid refresh(int id)
		{
			int *p=pig[id].hand,pos=0;
			for(rint i=1;i<=pig[id].num;i++)
			if(p[i])p[++pos]=p[i];
			pig[id].num=pos;
		}
		
		iint query(int id,int type)
		{
			int *p=pig[id].hand,num=0;
			for(rint i=1;i<=pig[id].num;i++)
			if(p[i]==type)num++;
			return num;
		}
	
		ivoid dec_some(int id,int type,int num)
		{
			int *p=pig[id].hand;
			for(rint i=1;i<=pig[id].num;i++)
			if(p[i]==type&&num)num--,p[i]=0;
		}
	}using namespace Card;
	
	namespace GG{//游戏结束
	
		ivoid game_over()
		{
			if(!pig[1].HP)cout<<"FP"<<endl;
			//如果是主公死了输出FP
			else cout<<"MP"<<endl;
			//否则就是主公赢了
			for(rint i=1;i<=n;i++){
				if(pig[i].HP<=0)cout<<"DEAD"<<endl;
				else{
					refresh(i);
					for(rint j=1;j<=pig[i].num;j++)
					cout<<(char)reget_id(pig[i].hand[j])<<" ";
                    cout<<endl;
                }
			}
			exit(0);
		}
	}using namespace GG;
	
	namespace Die{//死亡相关
		ibool if_alive(int id)
		{
			if(pig[id].HP>0)return 1;
			int num=query(id,3);
			if(pig[id].HP+num>0){dec_some(id,3,1-pig[id].HP);pig[id].HP=1;return 1;}
			return 0;
		}
	
		ivoid judge(int id1,int id2)
		{
			pig[pig[id2].l].r=pig[id2].r;
			pig[pig[id2].r].l=pig[id2].l;
			if(pig[id2].type==1)game_over();
			if(pig[id2].type==3&&!(--num_fan))game_over();
			//要是反贼死完了||主公挂了可以直接GG
			if(pig[id2].type==3)
			{get_card(id1,3);pig[id1].re=1;return;}
			//一旦通过杀反贼摸了牌就要重新从左边开始考虑.jpg
			if(pig[id1].type==1&&pig[id2].type==2)
			{pig[id1].num=0;pig[id1].equ=0;return;}
		}
	}using namespace Die;

	namespace Think{
		iint recognize(int id1,int id2){
			//0是未知,1是友军,2是敌人
			if(pig[id1].type==1&&pig[id2].show==4)return 2;
			//特判类反贼
			if(!pig[id2].show||pig[id2].show==4)return 0;
			if(pig[id1].type!=3&&pig[id2].show!=3)return 1;
			if(pig[id1].type==3&&pig[id2].show==3)return 1;
			return 2;
		}
	}using namespace Think;
	
	namespace Prevent{
		inline bool prevent(int id1,int id2,int type){
			int pos=id1;
			do{
				if(recognize(pos,id2)==1+(type^1)){
					if(query(pos,7)){
						dec_some(pos,7,1);
						pig[pos].show=pig[pos].type;
						return 1^prevent(pos,id2,type^1);
					}
				}
				pos=pig[pos].r;
			}while(pos!=id1);
			return 0;
		}
	}using namespace Prevent;
}
using namespace Basic;

比较特殊的是我把无懈可击也给写到了basic里面,实际上它应该归到锦囊牌里面,但由于这个牌太特殊了且对于所有别的锦囊牌都有影响,我就把他提上来写了。

二、锦囊牌
1、无懈可击

这张牌在游戏里算是最特殊的,首先无懈可击可以对所有的锦囊牌(包括无懈可击)使用,其次只有使用无懈可击才可以献殷勤。
那么首先,对于一张锦囊牌,我们要判断这对它的生效者是有害还是有利。如果有害,就是与生效者为友军的人使用无懈可击,否则就是他的敌人使用无懈可击。这一点我们可以利用递归,生效与否和利害关系则可以通过01异或来解决。

inline bool prevent(int id1,int id2,int type){//type为1是有害
	int pos=id1;//从出牌人开始逆时针
	do{
		if(recognize(pos,id2)==1+(type^1)){//判断友军出还是敌军出
			if(query(pos,7)){//如果手上有
				dec_some(pos,7,1);
				pig[pos].show=pig[pos].type;
				return 1^prevent(pos,id2,type^1);//递归
			}
		}
		pos=pig[pos].r;
		}while(pos!=id1);
	return 0;
}
2、决斗

相比起无懈可击,决斗的思维难度会小很多,大致分为以下几个步骤:
1、判定是否有人出无懈可击,有则返回
2、检索双方手牌中杀的数量,并结合谁是发起者进行判定。具体的讲,如果发起者杀的数量>=接受者数量,那么是接受者受伤,否则是发起者受伤。
3、受伤之后一系列关于死亡的判定就顺便更新啦。
4、特别判断题目中给出的特殊情况,即主公对忠臣使用决斗时,忠臣不会出牌这一点。
另外,这些单体攻击的牌都会造成身份的更新,为了避免遗漏我是写在一开始的地方的~

ivoid Fight(int id1,int id2)
{
	pig[id1].show=pig[id1].type;//跳忠/反
	if(pig[id1].type==1&&pig[id2].type==2){//特判:忠臣被主公diss
		pig[id2].HP--;
		if(!if_alive(id2))judge(id1,id2);
		return;
	}
	if(prevent(id1,id2,1))return;
	rint num1,num2,tmp=0;
	num1=query(id1,1);num2=query(id2,1); 
	if(num1<num2){
		dec_some(id1,1,num1);dec_some(id2,1,num1+1);
		pig[id1].HP--;
		if(!if_alive(id1))judge(id2,id1);
	}
	else{
		dec_some(id1,1,num2);dec_some(id2,1,num2);
		pig[id2].HP--;
		if(!if_alive(id2))judge(id1,id2);
	}
}
3、南蛮入侵,万箭齐发

这两张锦囊牌本质上讲是一样的,只是要求出的牌不同。流程大概如下:
1、对每一个人依次进行判定
2、从发起者(这是重点!)开始逆时针判断是否有人出无懈可击
3、若无,判断手牌是否有闪,有则打出
4、若没有闪,HP–,判定死亡。如果受伤者是主公且发起者的身份未知,还要更新发起者身份为类反

ivoid Loktar_Ogar(int id1)//南(shou)蛮(ren)入侵
{	
	int pos=pig[id1].r;
	while(pos!=id1){
		if(prevent(id1,pos,1)){pos=pig[pos].r;continue;}
		if(query(pos,1))dec_some(pos,1,1);
		else{
			pig[pos].HP--;
			if(!if_alive(pos))judge(id1,pos);
			if(pos==1&&!pig[id1].show)pig[id1].show=4;
		}
		pos=pig[pos].r;
	}
}
	
ivoid Xiu_xiu_xiu(int id1)//万箭齐发
{
	int pos=pig[id1].r;
	while(pos!=id1){
		if(prevent(id1,pos,1)){pos=pig[pos].r;continue;}
		if(query(pos,2))dec_some(pos,2,1);
		else{
			pig[pos].HP--;
			if(!if_alive(pos))judge(id1,pos);
			if(pos==1&&!pig[id1].show)pig[id1].show=4;
		}
		pos=pig[pos].r;
	}
}
三、其他牌&流程实现
1、杀

这个倒没什么好说的,具体的判定其实很上文类似,这里不再多提

ivoid Kill(int id1,int id2)
{
	pig[id1].show=pig[id1].type;
	if(query(id2,2)){
	dec_some(id2,2,1);}
	else {
		pig[id2].HP--;
		if(!if_alive(id2))judge(id1,id2);
	}
}
2、诸葛连弩

写个equip函数即可。(不知道说什么.jpg)

ivoid equip(int id){if(!pig[id].equ)pig[id].re=1;pig[id].equ=1;return;}
//注意更新re为1,有可能前面有很多杀
3、出牌

主要是对要出的牌进行判定,能否打出&对谁打出。

ivoid Use(int id,int type)
{
	switch(type){
		case 1:{
			if(!use_kill[id]||pig[id].equ)
			//use_kill是判断本回合是否打出过杀
			if(recognize(id,pig[id].r)==2){
				dec_some(id,type,1);
				Kill(id,pig[id].r);use_kill[id]=1;
			}
			break;
		}
		case 3:
			if(pig[id].HP<4){dec_some(id,type,1);pig[id].HP++;}
			break;
		case 4:{
			if(pig[id].type==3){dec_some(id,type,1);Fight(id,1);}
			else{
				int pos=id;
				while((pos=pig[pos].r)!=id){
					if(recognize(id,pos)==2){
						dec_some(id,type,1);
						Fight(id,pos);
						break;
					}
				}
			}	
			break;
		}
		case 5:{
			dec_some(id,type,1);
			Loktar_Ogar(id);
			pig[id].re=1;
			break;
		}
		case 6:{
			dec_some(id,type,1);
			Xiu_xiu_xiu(id);
			pig[id].re=1;
			break;
		}
		case 8:{
			dec_some(id,type,1);
			equip(id);
			break;
		}
	}
} 
4、主流程

这一部分就是实现一个回合,即从主公开始逆时针一圈依次出牌的过程。
需要特别注意:因为决斗可以对自己造成伤害,所以存在打牌打到一半自己没了的情况,这需要特殊判断。

ivoid Use_cards()
{
	int pos=1;
	do{
		refresh(pos);use_kill[pos]=0;get_card(pos,2);
		//洗手牌,标记归零,摸牌
		for(rint i=1;i<=pig[pos].num;i++){
			int now=pig[pos].hand[i];
			if(!now)continue;
			//因为中途没有洗手牌,所以中间的某一张可能为空
			Use(pos,now);if(!pig[pos].HP)break;
			if(pig[pos].re){pig[pos].re=0;i=0;}
			//要是摸到了新牌,要从左边开始重新扫
		}
		refresh(pos);pos=pig[pos].r;
		//结束的时候再洗一次手牌,为了降低扫描时的复杂度
	}while(pos^1);
}
5、主函数

这题的主函数其实没什么可讲的,就是普通的读入而已,然后在主流程的外面套个while(1)即可。

char hand[N];
signed main()
{
	n=rad();m=rad();
	for(rint i=1;i<=n;i++){
		scanf("%s",hand+1);
		pig[i].HP=4;
		if(hand[1]=='M')pig[i].type=pig[i].show=1;
		if(hand[1]=='Z')pig[i].type=2;
		if(hand[1]=='F')pig[i].type=3,num_fan++;//记录反贼数量
		pig[i-1].r=i;pig[i].l=i-1;//一开始的顺序
		for(rint j=1;j<=4;j++){//读入每个人的手牌
			scanf("%s",hand+1);
			pig[i].num++;
			pig[i].hand[pig[i].num]=get_id(hand[1]);
		}
	}
	pig[1].l=n;pig[n].r=1;
	for(rint i=1;i<=m;i++){//读入牌库
		scanf("%s",hand+1);
		card_num++;card[m-card_num+1]=get_id(hand[1]);
	}	
	while(1)Use_cards();
	return 0;
}

最后再说几句:写这道题其实并不会锻炼调试能力,因为这题样例不好出,要发现问题多半是靠数据;但是,此题对思维能力和实现能力有很大帮助,因为这题会要求你想的尽可能的周全,而想好了之后,在实现上有些地方还可以继续简化。

要退役了.jpg,把这道题调完也算是了却了自己的心愿吧~

另附:调试本题一直出事的同学,可以去loj下载数据,然后借助luogu讨论区的战场模拟器(没错就是输出每一步发生了啥)来核查每一步。如果是TLE几个点的可以把cerr给关了再试试。没错我就是因为没关cerr,T了半天还不知道为啥QAQ

发布了44 篇原创文章 · 获赞 16 · 访问量 7249

猜你喜欢

转载自blog.csdn.net/Cyan_rose/article/details/102954359