「THUPC2021初赛」⿇将模拟器题解
历时三天,写代码时间四个半⼩时。重构⼀次,代码总长度约 30k,AC 代码 8.53kb。
另外吐槽⼀下这道题的出题⼈是不是不太会打⿇将(明明有更好理解的向听定义然⽽搞了个和牌距离弄了我半天。
⾸先开⼀个 class 类是传统艺能。因为构造函数不知道⼲什么就占了个位,析构函数⽐较好说,我们可以存下⼏个信息,即 isWon 是否有⼈和牌,wonWay 表⽰和牌⽅式。另外存下⼀个值 ope,这个东西可以多⽤,既可以表⽰当前是谁出牌(不是摸牌的原因是因为鸣牌会跳过摸牌),也可以表⽰在结束之时谁赢了。
根据这个写⼀下函数。⽤析构函数的原因是当过程结束这个函数会⾃动执⾏。
bool isWon=false;
int wonWay=0;
char gameEnding[3][10]={"","RON","SELFDRAWN"};
MahjongGame(){}
~MahjongGame()
{
if(!isWon) puts("DRAW");
else printf("%s %s\n%s WIN\n",playerName[ope],gameEnding[wonWay],playerName[ope]);
}
捋⼀遍过程。⾸先我们需要将牌⼭读下来。我们需要写⼀个函数完成牌的名称与编号的转化。⾄于不需要写编号转化成牌的函数,因为我们可以很简单的⽤字符串数组完成这个过程。
char brickName[40][10]=
{
"",
"1M","2M","3M","4M","5M","6M","7M","8M","9M",
"1P","2P","3P","4P","5P","6P","7P","8P","9P",
"1S","2S","3S","4S","5S","6S","7S","8S","9S",
"E","S","W","N","B","F","Z",
"DOUBLE","REVERSE","PASS"
};
int nameTrans(char *s)
{
int len=int(strlen(s));
if(len==1)
{
switch (s[0])
{
case 'E': return 28;
case 'S': return 29;
case 'W': return 30;
case 'N': return 31;
case 'B': return 32;
case 'F': return 33;
case 'Z': return 34;
}
}
else if(len==2)
{
int p=s[0]-'0';
switch (s[1])
{
case 'M': return p;
case 'P': return p+9;
case 'S': return p+18;
}
}
else
{
switch(s[0])
{
case 'D': return 35;
case 'R': return 36;
case 'P': return 37;
Loading [MathJax]/jax/output/HTML-CSS/jax.js
return -1;
}
考虑与牌有关的,需要保存下来的信息。⽆疑我们需要保存每个玩家的牌是什么,有多少张牌,每个玩家所拥有的每种牌的数量以及牌⼭。写⼀个读牌⼭的函数:
void Initialization()
{
for(int i=1;i<=148;++i)
{
char s[10];
scanf("%s",s);
brickHill[i]=nameTrans(s);
}
}
然后我们需要发牌。在这⾥⽐较好处理的⽅法是⼀开始就只给每个⼈发 13 张牌。对于玩家 A 发 14 张可以看成发了 13 张牌并且摸牌进⾏出牌操作。
同时我们需要写⼏个基本函数去辅助。⽐如调整当前某个玩家的牌(按顺序,实现就是⼀个 sort);得到牌以及扔掉牌,输出得到牌以及扔掉牌的信息(注意要包含 PASS 的特殊⽤法);下⼀个玩家是谁,上⼀个玩家是谁;调整可能出现错误的当前操作⽤户信息(⽐如
5→1,0→4)。定义两个变量 ope 与 rev(ope 意义如上,rev 表⽰当前的顺序是什么:1 代表正序,−1 代表反序。两个变量初始值皆为 1)
void adjustBricks(int who){sort(brick[who]+1,brick[who]+1+brickCnt[who]);}
void getBrick(int who,int wch)
{
++brickApp[who][wch];
++brickCnt[who];
brick[who][brickCnt[who]]=wch;
adjustBricks(who);
}
void outBrick(int who,int wch)
{
--brickApp[who][wch];
for(int i=1;i<=brickCnt[who];++i)
{
if(brick[who][i]==wch)
{
for(int j=i+1;j<=brickCnt[who];++j) brick[who][j-1]=brick[who][j];
brick[who][brickCnt[who]]=0;
--brickCnt[who];
return ;
}
}
}
int fixOpe(int wch)
{
if(wch==5) wch=1;
if(wch==0) wch=4;
return wch;
}
void outputGetBrick(int who,int wch){printf("%s IN %s\n",playerName[who],brickName[wch]);}
void outputOutBrick(int who,int wch)
{
printf("%s OUT %s",playerName[who],brickName[wch]);
if(wch==37) printf(" %s",playerName[fixOpe(who+rev)]);
puts("");
}
void dealBrick()
{
for(int i=1;i<=52;++i)
{
int who=i%4;
if(!who) who=4;
getBrick(who,brickHill[i]);
outputGetBrick(who,brickHill[i]);
int nxtPlayer(int who){return fixOpe(who+rev);}
int lasPlayer(int who){return fixOpe(who-rev);}
其他的东西⽐较复杂。我们先考虑怎么写我们的主要执⾏函数 void execute()。
⾸先肯定需要执⾏⼀次 Initialization() 与 dealBrick() 函数。然后枚举当前的牌⽤到哪⾥了。⽤⼀个 cnt 存下来。初始值设成 53,因为前⾯的52 张牌已经发出去了。
然后执⾏⼀次 getBrick(ope,brickHill[cnt]) 与 outputGetBrick(ope,brickHill[cnt]),表⽰发牌的过程。然后写⼀个函数 bool isTsumo(int who) 表⽰当前这个⼈⼿中的牌是否⾃摸了(因为涉及到听牌距离⼀概念暂且不谈,实际上判断是否⾃摸还有⼀个贪⼼做法),那么游戏结
束,isWon 置为 true 并且 wonWay 置为 2,退出程序。否则需要写⼀个函数 void outBricker(int who),表⽰对该玩家进⾏⼀次出牌操作。然后将玩家置为下⼀个玩家,并且将 cnt ⾃增 1。根据思路写出代码。
void execute()
{
Initialization();
dealBrick();
int cnt=53;
while(cnt<=148)
{
getBrick(ope,brickHill[cnt]);
outputGetBrick(ope,brickHill[cnt]);
if(isTsumo(ope))
{
isWon=true;
wonWay=2;
exit(0);
}
outBricker(ope);
ope=nxtPlayer(ope);
++cnt;
}
}
这⾥不把发牌放⼊ void outBricker(int) 函数中的原因是因为可能会出现鸣牌需要递归的情况,为了⽅便将摸牌操作拿出来,传参的原因也是因为能够⽅便递归。
然后考虑 void outBricker(int) 函数的实现。
根据题⽬所述,我们需要按 PASS、REVERSE、DOUBLE 的顺序去实现。根据 int nameTrans(char*) 内置的编号,分别为 37,36,35,以此判断即可。
对于 PASS,我们将当前执⾏的玩家置为下⼀个玩家,在返回到 execute() 函数中就会跳过应该被跳过
的玩家;
对于 REVERSE,我们将 rev 取反即可;
对于 DOUBLE,我们将当前执⾏的玩家置为上⼀个玩家,在返回到 execute() 函数中就会变成该玩家。
写⼀个函数 int decideThrowBrick(int*) 表⽰对于当前牌,在题⽬的说明下进⾏选择丢弃哪⼀张牌。因为这个东西涉及到的东西更复杂在后⾯再说。
然后就对当前执⾏的玩家将决定扔出的牌扔掉。考虑荣和的过程,相当于得到⼀张牌并且判断是否⾃摸即可。注意顺序判断。
如果有⼈荣和,游戏⽴刻结束,将 ope 置为荣和的⼈的编号,isWon 置为 true 并且 wonWay 置为 1,退出程序⾃动执⾏析构函数。
否则我们需要判断有没有⼈能够碰。如果能够碰,那我们就将当前的需要出牌的⼈置为这个⼈,然后再递归执⾏ outBricker 函数即可。
吃同理。不再赘述。根据思路写下代码。
void outBricker(int who)
{
if(brickApp[who][37])
{
outBrick(who,37);
outputOutBrick(who,37);
ope=nxtPlayer(ope);
return ;
}
if(brickApp[who][36])
{
outBrick(who,36);
outputOutBrick(who,36);
rev=-rev;
return ;
}
if(brickApp[who][35])
{
outBrick(who,35);
outputOutBrick(who,35);
ope=lasPlayer(ope);
return ;
}
int outBrk=decideThrowBrick(brick[who]);
outBrick(who,outBrk);
outputOutBrick(who,outBrk);
for(int i=nxtPlayer(ope);i!=ope;i=nxtPlayer(i))
{
getBrick(i,outBrk);
if(isTsumo(i))
{
ope=i;
isWon=true;
wonWay=1;
exit(0);
}
outBrick(i,outBrk);
}
for(int i=nxtPlayer(ope);i!=ope;i=nxtPlayer(i))
{
if(allowPong(i,outBrk))
{
ope=i;
Pong(i,outBrk);
outBricker(i);
return ;
}
}
int i=nxtPlayer(ope),type=allowChow(i,outBrk);
if(allowChow(i,outBrk))
{
ope=i;
Chow(i,outBrk,type);
outBricker(i);
}
}
于是我们成功的给⾃⼰挖了很多坑。按吃和碰⼜分别讲解。
⾸先我们要写⼀个计算和牌距离的函数以及需要扔掉哪张牌的函数,分别定义为 int calcXt(int*) 与 int decideThrowBrick(int*)。因为这个东西是最最复杂的所以⼜延后说。
注意,这⾥的和牌距离不等同于向听。如果当前并不是你出牌的回合,和牌距离是向听加⼀;否则就是向听数。听牌即为 0 向听。害死我这个⽇⿇玩家了(
假设这两个函数能够返回正确的值。我们需要实现判断是否能吃/碰的函数。怎么用printf输出bool函数值
先说碰 bool allowPong(int who,int brk)。⾸先如果这个⼈没有两张牌 brk,那么肯定是不可以的。计算⼀下当前的和牌距离(即调⽤calcXt(brick[who])),然后扔出去两张再算⼀下和牌距离。如果扔出去之后的听牌距离严格⼩于(即,向听数⼩于等于)之前的和牌距离,这个碰的动作就是允许的;否则禁⽌。注意将牌放回来。
输出碰这个动作(void Pong(int who,int wch))就⽐较 naive。直接来就⾏了。注意在这个函数中要执⾏副露的过程。
bool allowPong(int who,int brk)
{
if(brickApp[who][brk]<2) return false;
int nowXt=calcXt(brick[who]);
outBrick(who,brk);
outBrick(who,brk);
int presentXt=calcXt(brick[who]);
getBrick(who,brk);
getBrick(who,brk);
if(presentXt<nowXt) return true;
return false;
}
void Pong(int who,int wch)
{
printf("%s PONG %s %s %s\n",playerName[who],brickName[wch],brickName[wch],brickName[wch]);
outBrick(who,wch);
outBrick(who,wch);
}
然后是吃。因为吃有三种可能分别判断⼀下和牌距离即可。注意不要字牌吃字牌,万吃索这样的情况出现(预防就可以直接看有没有,会不会出去等的判断即可)。然后取最⼩值判断是否⽐之前的和牌距离⼩即可。做法⼀样。
注意的是⽅案具有优先级。这个时候返回⼀个⽅案即可。与执⾏吃这个操作的函数⼀起⽤就⾏了。
int allowChow(int who,int wch)
{
if(wch>27) return false;
int nowXt=calcXt(brick[who]);
int Xts[4];
memset(Xts,63,sizeof Xts);
if(wch!=8 && wch!=9 && wch!=17 && wch!=18 && wch!=26 && wch!=27 && brickApp[who][wch+1] && brickApp[who][wch+2])
{
outBrick(who,wch+1);
outBrick(who,wch+2);
Xts[3]=calcXt(brick[who]);
getBrick(who,wch+1);
getBrick(who,wch+2);
}
if(wch!=9 && wch!=1 && wch!=18 && wch!=10 && wch!=27 && wch!=19 && brickApp[who][wch+1] && brickApp[who][wch-1])
{
outBrick(who,wch-1);
outBrick(who,wch+1);
Xts[2]=calcXt(brick[who]);
getBrick(who,wch-1);
getBrick(who,wch+1);
}
if(wch!=1 && wch!=2 && wch!=10 && wch!=11 && wch!=19 && wch!=20 && brickApp[who][wch-1] && brickApp[who][wch-2])
{
outBrick(who,wch-2);
outBrick(who,wch-1);
Xts[1]=calcXt(brick[who]);
getBrick(who,wch-2);
getBrick(who,wch-1);
}
int minn=min({Xts[1],Xts[2],Xts[3]});
if(minn>=nowXt) return 0;
if(minn==Xts[1] && minn==Xts[2] && minn==Xts[3]) return 3;
if(minn==Xts[1] && minn==Xts[2] && minn!=Xts[3]) return 2;
if(minn==Xts[1] && minn!=Xts[2] && minn==Xts[3]) return 3;
if(minn!=Xts[1] && minn==Xts[2] && minn==Xts[3]) return 3;
if(minn!=Xts[1] && minn!=Xts[2] && minn==Xts[3]) return 3;
if(minn!=Xts[1] && minn==Xts[2] && minn!=Xts[3]) return 2;
if(minn==Xts[1] && minn!=Xts[2] && minn!=Xts[3]) return 1;
return -1;
}
void Chow(int who,int brk,int type)
{
if(type==3)
{
printf("%s CHOW %s %s %s\n",playerName[who],brickName[brk],brickName[brk+1],brickName[brk+2]);
outBrick(who,brk+1);
outBrick(who,brk+2);
return ;
}
if(type==2)
{