吴昊品游戏核心算法 Round1 特别篇——吴昊整理至《电脑报》特刊——斗地主AI(智商比较低,但是方法值得借鉴:类似于词法分析地剥离每一种招数再利用主观性AI见招拆招)

本软件是基于android平台的斗地主AI,我们在源代码的基础之上,旨在改进AI的算法,使玩家具有更丰富的体验感,让NPC可以更为智能。

(一)玩法解析:

(1)发牌和叫牌:一副扑克54张,先为每个人发17张,剩下的3张作为底牌,玩家视自己手中的牌来确定自己是否叫牌。按顺序叫牌,谁出的分多谁就是地主,一般分数有1分,2分,3分。地主的底牌需要给其他玩家看过后才能拿到手中,最后地主20张牌,农民分别17张牌。

(2)出牌:地主先出牌,按照逆时针顺序依次进行,农民利用手中的牌组织地主继续出牌,并和同伴配合(这种配合的默契程度,之后会在算法中体现)尽快出完手中的牌。当一手牌在另外两家打不过的情况下,出牌的玩家继续出牌。

(3)牌型以及大小:单牌大小顺序为:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3,组合牌大小顺序为:火箭最大,炸弹其次,大过任何的牌型。

如果是同种牌型,则比较其主牌作为单牌时的顺序。下面是各种牌型的使用说明:

单牌:任何一张牌都是单牌,大小为:大王,小王,2,A,K,Q,J……

对牌:2张数值相同,花色不同的牌。

三张:3张数值相同,花色不同的牌。

单顺:5张或5张以上,数值连续的牌,2和双王不能列入其中。

双顺:3个或者3个以上数值连续的对牌。

三顺:2个或2个以上数值连续的三张。

飞机:三顺加数量相同的单牌或者对牌。

炸弹:4张数值相同的牌。

火箭:两个王。

四带二:炸弹+(两张单牌或者对牌)。

(二)UI布局省略:

(三)代码的详细解析:

游戏的工程目录如下:

 

整个游戏将近3000行代码,共由10个类组成,以下先将10个类的具体作用在表格中展示出来,再具体分析每个类的具体功能:

DDZ:入口Activity类

MenuView:菜单类

GameView:游戏类

Person:游戏中的玩家,其中两家是NPC(这里仅限于单机游戏,目的是要测试计算机的AI智商)

Desk:一个游戏桌位,存储着三个玩家的共有信息,和当前游戏的进度控制

Card:每一手牌都是一个Card类的实例

Poke:洗牌,比较大小等操作

PokeType:定义了牌型的接口,里面定义了十二种牌型常量和两种绘制牌的方向

AnalyzePoke:分析手中的所有牌,将各种牌型划分

(人工智能主要在Person类中NPC出牌的过程中展现)

UniqInt:一个存储不同数值的辅助工具类

  1. Desk(桌子) ,Person(玩家),Card(一手牌)

本游戏是一个单机游戏,所以桌子(Desk)只有一个,桌子上有3个玩家(Person),其中两个是NPC玩家,一个是用户玩家,每一个玩家都有一把牌,17张或者20张,每一把牌都可以分成很多手牌(Card)。

上面已经分析了Desk,Person,Card之间的关系,下面逐个逐个进行解析。

Desk是桌子类,它持有3个玩家的实例对象,负责为3个玩家分牌,控制出牌的顺序流程,当前分数的倍数等全局性信息。

Desk类地成员变量:


 1  public  static  int threePokes[] =  new  int[3]; //  三张底牌
 2 
 3  private  int threePokesPos[][] =  new  int[][] { { 170, 17 }, { 220, 17 },
 4 
 5 { 270, 17 } };
 6 
 7  private  int[][] rolePos = { { 60, 310 }, { 63, 19 }, { 396, 19 }, };
 8 
 9  public  static Person[] persons =  new Person[3]; //  三个玩家
10 
11  public  static  int[] deskPokes =  new  int[54]; //  一副扑克牌
12 
13  public  static  int currentScore = 3; //  当前分数
14 
15  public  static  int boss = 0; //  地主
16 
17  /**
18 
19  * -2:发牌<br>
20 
21  * -1:随机地主<br>
22 
23  * 0:游戏中 <br>
24 
25  * 1:游戏结束,重新来,活退出<br>
26 
27  */
28 
29  private  int op = -1; //  游戏的进度控制
30 
31  public  static  int currentPerson = 0; //  当前操作的人
32 
33  public  static  int currentCircle = 0; //  本轮次数
34 
35  public  static Card currentCard =  null; //  最新的一手牌
36 
37 控制游戏逻辑的代码:
38 
39  public  void gameLogic() {
40 
41  switch (op) {
42 
43  case -2:
44 
45  break;
46 
47  case -1:
48 
49 init();
50 
51 op = 0;
52 
53  break;
54 
55  case 0:
56 
57 gaming();
58 
59  break;
60 
61  case 1:
62 
63  break;
64 
65  case 2:
66 
67  break;
68 
69 }
70 
71 }

关于发牌的代码:


  1  public  void fenpai( int[] pokes) {
  2 
  3  for ( int i = 0; i < 51;) {
  4 
  5 personPokes[i / 17][i % 17] = pokes[i++];
  6 
  7 }
  8 
  9 threePokes[0] = pokes[51];
 10 
 11 threePokes[1] = pokes[52];
 12 
 13 threePokes[2] = pokes[53];
 14 
 15 }
 16 
 17 personPokes是一个二维数组,存三堆17张的牌,threePokes存放最后三张。
 18 
 19 Desk类中随机生成地主的代码:
 20 
 21  //  随机地主,将三张底牌给地主
 22 
 23  private  void randDZ() {
 24 
 25 boss = Poke.getDZ();
 26 
 27 currentPerson = boss;
 28 
 29  int[] newPersonPokes =  new  int[20];
 30 
 31  for ( int i = 0; i < 17; i++) {
 32 
 33 newPersonPokes[i] = personPokes[boss][i];
 34 
 35 }
 36 
 37 newPersonPokes[17] = threePokes[0];
 38 
 39 newPersonPokes[18] = threePokes[1];
 40 
 41 newPersonPokes[19] = threePokes[2];
 42 
 43 personPokes[boss] = newPersonPokes;
 44 
 45 }
 46 
 47 利用getDZ()方法随机找到地主,然后,将当前人设为地主,newPersonPokes数组存放地主的牌。
 48 
 49 Desk类在游戏进行中的逻辑方法如下:
 50 
 51  //  存储当前一句的胜负得分信息
 52 
 53  int rs[] =  new  int[3];
 54 
 55  private  void gaming() {
 56 
 57  for ( int k = 0; k < 3; k++) {
 58 
 59  //  当三个人中其中一个人牌的数量为0,则游戏结束
 60 
 61  if (persons[k].pokes.length == 0) {
 62 
 63  //  切换到游戏结束状态
 64 
 65 op = 1;
 66 
 67  //  得到最先出去的人的id
 68 
 69 winId = k;
 70 
 71  //  判断哪方获胜
 72 
 73  if (boss == winId) {
 74 
 75  //  地主方获胜后的积分操作
 76 
 77  for ( int i = 0; i < 3; i++) {
 78 
 79  if (i == boss) {
 80 
 81  //  地主需要加两倍积分
 82 
 83 rs[i] = currentScore * 2;
 84 
 85 personScore[i] += currentScore * 2;
 86 
 87 }  else {
 88 
 89  //  农民方需要减分
 90 
 91 rs[i] = -currentScore;
 92 
 93 personScore[i] -= currentScore;
 94 
 95 }
 96 
 97 }
 98 
 99 }  else {
100 
101  //  如果农民方胜利
102 
103  for ( int i = 0; i < 3; i++) {
104 
105  if (i != boss) {
106 
107  //  农民方加分
108 
109 rs[i] = currentScore;
110 
111 personScore[i] += currentScore;
112 
113 }  else {
114 
115  //  地主方减分
116 
117 rs[i] = -currentScore * 2;
118 
119 personScore[i] -= currentScore * 2;
120 
121 }
122 
123 }
124 
125 }
126 
127  return;
128 
129 }
130 
131 }
132 
133  //  游戏没有结束,继续。
134 
135  //  如果本家ID是NPC,则执行语句中的操作
136 
137  if (currentPerson == 1 || currentPerson == 2) {
138 
139  if (timeLimite <= 300) {
140 
141  //  获取手中的牌中能够打过当前手牌
142 
143 Card tempcard = persons[currentPerson].chupaiAI(currentCard);
144 
145  if (tempcard !=  null) {
146 
147  //  手中有大过的牌,则出
148 
149 currentCircle++;
150 
151 currentCard = tempcard;
152 
153 nextPerson();
154 
155 }  else {
156 
157  //  没有打过的牌,则不要
158 
159 buyao();
160 
161 }
162 
163 }
164 
165 }
166 
167  //  时间倒计时
168 
169 timeLimite -= 2;
170 
171 }

这里调用了一个出牌人的AI函数,对于timeLimite的理解,感觉是让机器再多算一会儿,控制回溯的深度?这里有待考虑。

如果NPC没有大牌了,就选择不要牌,如下是对于不要牌的操作:


 1  // 不要牌的操作
 2 
 3  private  void buyao() {
 4 
 5  //  轮到下一个人
 6 
 7 currentCircle++;
 8 
 9  //  清空当前不要牌的人的最后一手牌
10 
11 persons[currentPerson].card =  null;
12 
13  //  定位下一个人的id
14 
15 nextPerson();
16 
17  //  如果已经转回来,则该人继续出牌,本轮清空,新一轮开始
18 
19  if (currentCard !=  null && currentPerson == currentCard.personID) {
20 
21 currentCircle = 0;
22 
23 currentCard =  null; //  转回到最大牌的那个人再出牌
24 
25 persons[currentPerson].card =  null;
26 
27 }
28 
29 }
30 
31  //  定位下一个人的id并重新倒计时
32 
33  private  void nextPerson() {
34 
35  switch (currentPerson) {
36 
37  case 0:
38 
39 currentPerson = 2;
40 
41  break;
42 
43  case 1:
44 
45 currentPerson = 0;
46 
47  break;
48 
49  case 2:
50 
51 currentPerson = 1;
52 
53  break;
54 
55 }
56 
57 timeLimite = 310;
58 
59 }
60 
61 }

nextPerson()函数用于定位下一个出牌的人(对于不要之后的轮转函数),而对于buyao()函数,这里有待进一步理解。

(如下的代码和UI有关,这里就不列出了)

对于Person类自己的一些见解:


  1  //  玩家手中的牌
  2 
  3  int[] pokes;
  4 
  5  //  玩家选中牌的标志
  6 
  7  boolean[] pokesFlag;
  8 
  9  //  玩家所在桌面上的坐标
 10 
 11  int top, left;
 12 
 13  //  玩家ID
 14 
 15  int id;
 16 
 17  //  玩家所在桌子的实例
 18 
 19 Desk desk;
 20 
 21  //  玩家最近一手牌
 22 
 23 Card card;
 24 
 25 DDZ ddz;
 26 
 27 以上为Person类的一些成员变量
 28 
 29 同样略去“美工代码”,Person类中NPC出牌的人工智能代码如下:
 30 
 31  //  判断出牌的人工智能
 32 
 33  public Card chupaiAI(Card card) {
 34 
 35  int[] pokeWanted =  null;
 36 
 37  if (card ==  null) {
 38 
 39  //  玩家随意出一手牌
 40 
 41 pokeWanted = Poke.outCardByItsself(pokes, last, next);
 42 
 43 }  else {
 44 
 45  //  玩家需要出一手比card大的牌
 46 
 47 pokeWanted = Poke.findTheRightCard(card, pokes, last, next);
 48 
 49 }
 50 
 51  //  如果不能出牌,则返回
 52 
 53  if (pokeWanted ==  null) {
 54 
 55  return  null;
 56 
 57 }
 58 
 59  //  以下为出牌的后续操作,将牌从玩家手中剔除
 60 
 61  int num = 0;
 62 
 63  for ( int i = 0; i < pokeWanted.length; i++) {
 64 
 65  for ( int j = 0; j < pokes.length; j++) {
 66 
 67  if (pokes[j] == pokeWanted[i]) {
 68 
 69 pokes[j] = -1;
 70 
 71 num++;
 72 
 73  break;
 74 
 75 }
 76 
 77 }
 78 
 79 }
 80 
 81  int[] newpokes =  new  int[0];
 82 
 83  if (pokes.length - pokeWanted.length > 0) {
 84 
 85 newpokes =  new  int[pokes.length - pokeWanted.length];
 86 
 87 }
 88 
 89  int j = 0;
 90 
 91  for ( int i = 0; i < pokes.length; i++) {
 92 
 93  if (pokes[i] != -1) {
 94 
 95 newpokes[j] = pokes[i];
 96 
 97 j++;
 98 
 99 }
100 
101 }
102 
103  this.pokes = newpokes;
104 
105 Card thiscard =  new Card(pokeWanted, pokeImage, id);
106 
107  //  更新桌子最近一手牌
108 
109 desk.currentCard = thiscard;
110 
111  this.card = thiscard;
112 
113  return thiscard;
114 
115 }

这里,AI考虑当玩家没有任何“威胁”的情况下,任意出了一手牌,其实按照习惯,应该出一手玩家认为在当前时刻最为迫切出的牌,所以,这里的AI有待补完。

PokeType类,也就是其中的对于牌型的定义:


 1  package com.peiandsky;
 2 
 3  public  interface PokeType {
 4 
 5  int danpai=1;
 6 
 7  int duipai=2;
 8 
 9  int sanzhang=3;
10 
11  int sandaiyi=4;
12 
13  int danshun=5;
14 
15  int shuangshun=6;
16 
17  int sanshun=7;
18 
19  int feiji=8;
20 
21  int sidaier=9;
22 
23  int zhadan=10;
24 
25  int huojian=11;
26 
27  int error=12; // 错误类型
28 
29  int dirH=0; // 绘制方向为横向
30 
31  int dirV=1; // 绘制方向为竖向
32 
33 }

Poke获取牌型信息:

Poke定义了一些关于扑克牌的核心操作,例如洗牌,获取牌型,出牌等。作为一个关于扑克操作的工具栏,Poke中的所有方法全部是静态方法,可以直接通过类名调用,不实例化Poke类。

关于洗牌的操作,实际上,根据经验可知,在物理上,洗牌六次可以达到最大的混乱度,但是,这里采用了一个随机算法,也就是让54张牌分别和随机生成的牌发生交换,这种方法不置可否,应该没有常规洗牌那么“随机性”,不过,作为置乱,也已经足够随机了。


  1  //  0-53表示54张牌
  2 
  3  public  static  void shuffle( int[] pokes) {
  4 
  5  int len = pokes.length;
  6 
  7  //  对于54张牌中的任何一张,都随机找一张和它互换,将牌顺序打乱。
  8 
  9  for ( int l = 0; l < len; l++) {
 10 
 11  int des = rand.nextInt(54);
 12 
 13  int temp = pokes[l];
 14 
 15 pokes[l] = pokes[des];
 16 
 17 pokes[des] = temp;
 18 
 19 }
 20 
 21 }
 22 
 23 利用冒泡排序方法,对pokes进行从大到小的排序:
 24 
 25  //  对pokes进行从大到小排序,采用冒泡排序
 26 
 27  public  static  void sort( int[] pokes) {
 28 
 29  for ( int i = 0; i < pokes.length; i++) {
 30 
 31  for ( int j = i + 1; j < pokes.length; j++) {
 32 
 33  if (pokes[i] < pokes[j]) {
 34 
 35  int temp = pokes[i];
 36 
 37 pokes[i] = pokes[j];
 38 
 39 pokes[j] = temp;
 40 
 41 }
 42 
 43 }
 44 
 45 }
 46 
 47 }
 48 
 49 给出一张牌的索引值,可以返回它真实的可以比较大小的值(称为getPokeValue)
 50 
 51  /**
 52 
 53  * 16小王,17大王
 54 
 55  */
 56 
 57  public  static  int getPokeValue( int poke) {
 58 
 59  //  当扑克值为52时,是小王
 60 
 61  if (poke == 52) {
 62 
 63  return 16;
 64 
 65 }
 66 
 67  //  当扑克值为53时,是大王
 68 
 69  if (poke == 53) {
 70 
 71  return 17;
 72 
 73 }
 74 
 75  //  其它情况下返回相应的值(3,4,5,6,7,8,9,10,11(J),12(Q),13(K),14(A),15(2))
 76 
 77  return poke / 4 + 3;
 78 
 79 }
 80 
 81 判断牌型的办法:
 82 
 83 首先是两个辅助函数:
 84 
 85 (1)统计一手牌中同值的牌出现的次数:
 86 
 87  //  统计一手牌中同值的牌出现的次数来判断是对牌,三顺,三带一,炸弹,四代二等
 88 
 89  public  static  int getPokeCount( int[] pokes,  int poke) {
 90 
 91  int count = 0;
 92 
 93  for ( int i = 0; i < pokes.length; i++) {
 94 
 95  if (getPokeValue(pokes[i]) == getPokeValue(poke)) {
 96 
 97 count++;
 98 
 99 }
100 
101 }
102 
103  return count;
104 
105 }
106 
107 (2)判断一组牌的值是不是连续的:
108 
109  /**
110 
111  * 判断是不是顺子
112 
113  *
114 
115  @param  pokes
116 
117  @return
118 
119  */
120 
121  public  static  boolean shunzi( int[] pokes) {
122 
123  int start = getPokeValue(pokes[0]);
124 
125  //  顺子中不能包含2,king
126 
127  if (start >= 15) {
128 
129  return  false;
130 
131 }
132 
133  int next;
134 
135  for ( int i = 1; i < pokes.length; i++) {
136 
137 next = getPokeValue(pokes[i]);
138 
139  if (start - next != 1) {
140 
141  return  false;
142 
143 } // 如果有一个不等于1的都是不行的
144 
145 start = next;
146 
147 }
148 
149  return  true;
150 
151 }
152 
153 利用这两个辅助函数(或者称为方法),可以适当判断牌型:
154 
155  /**
156 
157  * pokes中的牌的顺序要按照牌的值排列,顺牌中不包含2
158 
159  *
160 
161  @param  pokes
162 
163  @return
164 
165  */
166 
167  public  static  int getPokeType( int[] pokes) {
168 
169  int len = pokes.length;
170 
171  //  当牌数量为1时,单牌
172 
173  if (len == 1) {
174 
175  return PokeType.danpai;
176 
177 }
178 
179  //  当牌数量为2时,可能是对牌和火箭
180 
181  if (len == 2) {
182 
183  if (pokes[0] == 53 && pokes[1] == 52) {
184 
185  return PokeType.huojian;
186 
187 }
188 
189  if (getPokeValue(pokes[0]) == getPokeValue(pokes[1])) {
190 
191  return PokeType.duipai;
192 
193 }
194 
195 }
196 
197  //  当牌数为3时,只可能是三顺
198 
199  if (len == 3) {
200 
201  if (getPokeValue(pokes[0]) == getPokeValue(pokes[1])
202 
203 && getPokeValue(pokes[2]) == getPokeValue(pokes[1])) {
204 
205  return PokeType.sanzhang;
206 
207 }
208 
209 }
210 
211  //  当牌数为4时,可能是三带一或炸弹
212 
213  if (len == 4) {
214 
215  int firstCount = getPokeCount(pokes, pokes[0]);
216 
217  if (firstCount == 3 || getPokeCount(pokes, pokes[1]) == 3) {
218 
219  return PokeType.sandaiyi;
220 
221 }
222 
223  if (firstCount == 4) {
224 
225  return PokeType.zhadan;
226 
227 }
228 
229 }
230 
231  //  当牌数大于5时,判断是不是单顺
232 
233  if (len >= 5) {
234 
235  if (shunzi(pokes)) {
236 
237  return PokeType.danshun;
238 
239 }
240 
241 }
242 
243  //  当牌数为6时,四带二
244 
245  if (len == 6) {
246 
247  boolean have4 =  false;
248 
249  boolean have1 =  false;
250 
251  for ( int i = 0; i < len; i++) {
252 
253  int c = getPokeCount(pokes, pokes[i]);
254 
255  if (c == 4) {
256 
257 have4 =  true;
258 
259 }
260 
261  if (c == 1) {
262 
263 have1 =  true;
264 
265 }
266 
267 }
268 
269  if (have4 && have1) {
270 
271  return PokeType.sidaier;
272 
273 }
274 
275 }
276 
277  //  当牌数大于等于6时,先检测是不是双顺和三顺
278 
279  if (len >= 6) {
280 
281  //  双顺
282 
283  boolean shuangshunflag =  true;
284 
285  for ( int i = 0; i < len; i++) {
286 
287  if (getPokeCount(pokes, pokes[i]) != 2) {
288 
289 shuangshunflag =  false;
290 
291  break;
292 
293 }
294 
295 }
296 
297  if (shuangshunflag) {
298 
299  int[] tempPokes =  new  int[len / 2];
300 
301  for ( int i = 0; i < len / 2; i++) {
302 
303 tempPokes[i] = pokes[i * 2];
304 
305 }
306 
307  if (shunzi(tempPokes)) {
308 
309  return PokeType.shuangshun;
310 
311 }
312 
313 }
314 
315 System.out.println("shuangshun:" + shuangshunflag);
316 
317  //  三顺
318 
319  boolean sanshunflag =  true;
320 
321  for ( int i = 0; i < len; i++) {
322 
323  if (getPokeCount(pokes, pokes[i]) != 3) {
324 
325 sanshunflag =  false;
326 
327  break;
328 
329 }
330 
331 }
332 
333  if (sanshunflag) {
334 
335  int[] tempPokes =  new  int[len / 3];
336 
337  for ( int i = 0; i < len / 3; i++) {
338 
339 tempPokes[i] = pokes[i * 3];
340 
341 }
342 
343  if (shunzi(tempPokes)) {
344 
345  return PokeType.sanshun;
346 
347 }
348 
349 }
350 
351 }
352 
353  //  当牌数大于等于8,且能够被4整除时,判断是不是飞机
354 
355  if (len >= 8 && len % 4 == 0) {
356 
357 UniqInt ui =  new UniqInt();
358 
359  int have1 = 0;
360 
361  for ( int i = 0; i < pokes.length; i++) {
362 
363  int c = getPokeCount(pokes, pokes[i]);
364 
365  if (c == 3) {
366 
367 ui.addInt(pokes[i]);
368 
369 }  else  if (c == 1) {
370 
371 have1++;
372 
373 }
374 
375 }
376 
377  if (ui.size() == have1) {
378 
379  int[] tempArray = ui.getArray();
380 
381 sort(tempArray);
382 
383  if (shunzi(tempArray)) {
384 
385  return PokeType.feiji;
386 
387 }
388 
389 }
390 
391 }
392 
393  //  如果不是可知牌型,返回错误型
394 
395  return PokeType.error;
396 
397 }
398 
399  // 对于牌型的解析,有点类似于编译原理中的词法分析,但是,对于每种牌型解析的方法都不太一样,总是,一步一步走即可(AI的过程则有点类似于语法分析的过程)
400 
401 以下列出判断牌型的有限自动机:

 

  

获取牌型之后,通过判断两手牌的大小,需要知道一手牌的牌值:


  1  //  通过给给出的一手牌,来返回它的牌值大小,pokes中的顺序是排列好的
  2 
  3  public  static  int getPokeTypeValue( int[] pokes,  int pokeType) {
  4 
  5  //  这几种类型直接返回第一个值
  6 
  7  if (pokeType == PokeType.danpai || pokeType == PokeType.duipai
  8 
  9 || pokeType == PokeType.danshun || pokeType == PokeType.sanshun
 10 
 11 || pokeType == PokeType.shuangshun
 12 
 13 || pokeType == PokeType.sanzhang || pokeType == PokeType.zhadan) {
 14 
 15  return getPokeValue(pokes[0]);
 16 
 17 }
 18 
 19  //  三带一和飞机返回数量为3的牌的最大牌值
 20 
 21  if (pokeType == PokeType.sandaiyi || pokeType == PokeType.feiji) {
 22 
 23  for ( int i = 0; i <= pokes.length - 3; i++) {
 24 
 25  if (getPokeValue(pokes[i]) == getPokeValue(pokes[i + 1])
 26 
 27 && getPokeValue(pokes[i + 1]) == getPokeValue(pokes[i + 2])) {
 28 
 29  return getPokeValue(pokes[i]);
 30 
 31 }
 32 
 33 }
 34 
 35 }
 36 
 37  //  四带二返回数量为4的牌值
 38 
 39  if (pokeType == PokeType.sidaier) {
 40 
 41  for ( int i = 0; i < pokes.length - 3; i++) {
 42 
 43  if (getPokeValue(pokes[i]) == getPokeValue(pokes[i + 1])
 44 
 45 && getPokeValue(pokes[i + 1]) == getPokeValue(pokes[i + 2])
 46 
 47 && getPokeValue(pokes[i + 2]) == getPokeValue(pokes[i + 3])) {
 48 
 49  return getPokeValue(pokes[i]);
 50 
 51 }
 52 
 53 }
 54 
 55 }
 56 
 57  return 0;
 58 
 59 }
 60 
 61 三带一,飞机,以及四带二,只能通过其主键值(key-value)来作为其关键牌来比较大小。
 62 
 63 下面,我们就可以比较两手牌的大小了!
 64 
 65  /**
 66 
 67  * true 第一个大
 68 
 69  *
 70 
 71  @param  f
 72 
 73  @param  s
 74 
 75  @return
 76 
 77  */
 78 
 79  public  static  boolean compare(Card f, Card s) {
 80 
 81  //  当两种牌型相同时
 82 
 83  if (f.pokeType == s.pokeType) {
 84 
 85  //  两手牌牌型相同时,数量不同将无法比较,默认为第二个大,使s不能出牌
 86 
 87  if (f.pokes.length != s.pokes.length)
 88 
 89  return  false;
 90 
 91  //  牌型相同,数量相同时,比较牌值
 92 
 93  return f.value > s.value;
 94 
 95 }
 96 
 97  //  在牌型不同的时候,如果f的牌型是火箭,则返回true
 98 
 99  if (f.pokeType == PokeType.huojian) {
100 
101  return  true;
102 
103 }
104 
105  if (s.pokeType == PokeType.huojian) {
106 
107  return  false;
108 
109 }
110 
111  //  排除火箭的类型,炸弹最大
112 
113  if (f.pokeType == PokeType.zhadan) {
114 
115  return  true;
116 
117 }
118 
119  if (s.pokeType == PokeType.zhadan) {
120 
121  return  false;
122 
123 }
124 
125  //  无法比较的情况,默认为s大于f
126 
127  return  false;
128 
129 }

这里只对于“绝对优势”的牌做出了比较,比如:火箭和炸弹。

下 面继续人工智能,Poke类中给出了这样一个方法,可以直接从牌中选中能够打过card值得牌,如果没有则返回false;(其中运用了C++--STL 中的vector作为存储容器,这里还有一个小亮点,利用must值来判断紧迫性,如果neesZd为true,则需要炸弹了配合,也就是到了一些关键关 头,另外,在选择拆牌的时候,考虑到“火箭”和“4个2”的牌是拥有绝对实力的,这样的牌都不能拆开。在检查炸弹的时候,也根据紧迫性几率出牌,如果下家 是和自己一伙的,则顺延给下家)


  1  //  从pokes数组中找到比card大的一手牌
  2 
  3  public  static  int[] findBigThanCardSimple2(Card card,  int pokes[],  int must) {
  4 
  5  try {
  6 
  7  //  获取card的信息,牌值,牌型
  8 
  9  int[] cardPokes = card.pokes;
 10 
 11  int cardValue = card.value;
 12 
 13  int cardType = card.pokeType;
 14 
 15  int cardLength = cardPokes.length;
 16 
 17  //  使用AnalyzePoke来对牌进行分析
 18 
 19 AnalyzePoke analyz = AnalyzePoke.getInstance();
 20 
 21 analyz.setPokes(pokes);
 22 
 23 Vector< int[]> temp;
 24 
 25  int size = 0;
 26 
 27  //  根据适当牌型选取适当牌
 28 
 29  switch (cardType) {
 30 
 31  case PokeType.danpai:
 32 
 33 temp = analyz.getCard_danpai();
 34 
 35 size = temp.size();
 36 
 37  for ( int i = 0; i < size; i++) {
 38 
 39  int[] cardArray = temp.get(i);
 40 
 41  int v = Poke.getPokeValue(cardArray[0]);
 42 
 43  if (v > cardValue) {
 44 
 45  return cardArray;
 46 
 47 }
 48 
 49 }
 50 
 51  //  如果单牌中没有,则选择现有牌型中除火箭和4个2后的最大一个
 52 
 53  int st = 0;
 54 
 55  if (analyz.getCountWang() == 2) {
 56 
 57 st += 2;
 58 
 59 }
 60 
 61  if (analyz.getCount2() == 4) {
 62 
 63 st += 4;
 64 
 65 }
 66 
 67  if (Poke.getPokeValue(pokes[st]) > cardValue)
 68 
 69  return  new  int[] { pokes[st] };
 70 
 71  //  检查炸弹,根据紧迫性几率出牌,如果下家是和自己一伙的则顺延给下家
 72 
 73  break;
 74 
 75  case PokeType.duipai:
 76 
 77 temp = analyz.getCard_duipai();
 78 
 79 size = temp.size();
 80 
 81  for ( int i = 0; i < size; i++) {
 82 
 83  int[] cardArray = temp.get(i);
 84 
 85  int v = Poke.getPokeValue(cardArray[0]);
 86 
 87  if (v > cardValue) {
 88 
 89  return cardArray;
 90 
 91 }
 92 
 93 }
 94 
 95  //  如果对子中没有,则需要检查双顺
 96 
 97 temp = analyz.getCard_shuangshun();
 98 
 99 size = temp.size();
100 
101  for ( int i = 0; i < size; i++) {
102 
103  int[] cardArray = temp.get(i);
104 
105  for ( int j = cardArray.length - 1; j > 0; j--) {
106 
107  int v = Poke.getPokeValue(cardArray[j]);
108 
109  if (v > cardValue) {
110 
111  return  new  int[] { cardArray[j], cardArray[j - 1] };
112 
113 }
114 
115 }
116 
117 }
118 
119  //  如果双顺中没有,则需要检查三张
120 
121 temp = analyz.getCard_sanzhang();
122 
123 size = temp.size();
124 
125  for ( int i = 0; i < size; i++) {
126 
127  int[] cardArray = temp.get(i);
128 
129  int v = Poke.getPokeValue(cardArray[0]);
130 
131  if (v > cardValue) {
132 
133  return  new  int[] { cardArray[0], cardArray[1] };
134 
135 }
136 
137 }
138 
139  //  如果三张中没有,则就考虑炸弹,下家也可以顺牌
140 
141  break;
142 
143  case PokeType.sanzhang:
144 
145 temp = analyz.getCard_sanzhang();
146 
147 size = temp.size();
148 
149  for ( int i = 0; i < size; i++) {
150 
151  int[] cardArray = temp.get(i);
152 
153  int v = Poke.getPokeValue(cardArray[0]);
154 
155  if (v > cardValue) {
156 
157  return cardArray;
158 
159 }
160 
161 }
162 
163  break;
164 
165  case PokeType.sandaiyi:
166 
167  if (pokes.length < 4) {
168 
169  break;
170 
171 }
172 
173  boolean find =  false;
174 
175  int[] sandaiyi =  new  int[4];
176 
177 temp = analyz.getCard_sanzhang();
178 
179 size = temp.size();
180 
181  for ( int i = 0; i < size; i++) {
182 
183  int[] cardArray = temp.get(i);
184 
185  int v = Poke.getPokeValue(cardArray[0]);
186 
187  if (v > cardValue) {
188 
189  for ( int j = 0; j < cardArray.length; j++) {
190 
191 sandaiyi[j] = cardArray[j];
192 
193 find =  true;
194 
195 }
196 
197 }
198 
199 }
200 
201  //  没有三张满足条件
202 
203  if (!find) {
204 
205  break;
206 
207 }
208 
209  //  再找一张组合成三带一
210 
211 temp = analyz.getCard_danpai();
212 
213 size = temp.size();
214 
215  if (size > 0) {
216 
217  int[] t = temp.get(0);
218 
219 sandaiyi[3] = t[0];
220 
221 }  else {
222 
223 temp = analyz.getCard_danshun();
224 
225 size = temp.size();
226 
227  for ( int i = 0; i < size; i++) {
228 
229  int[] danshun = temp.get(i);
230 
231  if (danshun.length >= 6) {
232 
233 sandaiyi[3] = danshun[0];
234 
235 }
236 
237 }
238 
239 }
240 
241  //  从中随便找一个最小的
242 
243  if (sandaiyi[3] == 0) {
244 
245  for ( int i = pokes.length - 1; i >= 0; i--) {
246 
247  if (Poke.getPokeValue(pokes[i]) != Poke
248 
249 .getPokeValue(sandaiyi[0])) {
250 
251 sandaiyi[3] = pokes[i];
252 
253 }
254 
255 }
256 
257 }
258 
259  if (sandaiyi[3] != 0) {
260 
261 Poke.sort(sandaiyi);
262 
263  return sandaiyi;
264 
265 }
266 
267  break;
268 
269  case PokeType.danshun: //  还值得优化
270 
271 temp = analyz.getCard_danshun();
272 
273 size = temp.size();
274 
275  for ( int i = 0; i < size; i++) {
276 
277  int[] danshun = temp.get(i);
278 
279  if (danshun.length == cardLength) {
280 
281  if (cardValue < Poke.getPokeValue(danshun[0])) {
282 
283  return danshun;
284 
285 }
286 
287 }
288 
289 }
290 
291  for ( int i = 0; i < size; i++) {
292 
293  int[] danshun = temp.get(i);
294 
295  if (danshun.length > cardLength) {
296 
297  if (danshun.length < cardLength
298 
299 || danshun.length - cardLength >= 3) {
300 
301  if (rand.nextInt(100) < must) {
302 
303  if (cardValue >= Poke.getPokeValue(danshun[0])) {
304 
305  continue;
306 
307 }
308 
309  int index = 0;
310 
311  for ( int k = 0; k < danshun.length; k++) {
312 
313  if (cardValue < Poke
314 
315 .getPokeValue(danshun[k])) {
316 
317 index = k;
318 
319 }  else {
320 
321  break;
322 
323 }
324 
325 }
326 
327  if (index + cardLength > danshun.length) {
328 
329 index = danshun.length - cardLength;
330 
331 }
332 
333  int[] newArray =  new  int[cardLength];
334 
335  int n = 0;
336 
337  for ( int m = index; m < danshun.length; m++) {
338 
339 newArray[n++] = danshun[m];
340 
341 }
342 
343  return newArray;
344 
345 }
346 
347  break;
348 
349 }
350 
351  if (cardValue >= Poke.getPokeValue(danshun[0])) {
352 
353  continue;
354 
355 }
356 
357  int start = 0;
358 
359  int end = 0;
360 
361  if (danshun.length - cardLength == 1) {
362 
363  if (cardValue < Poke.getPokeValue(danshun[1])) {
364 
365 start = 1;
366 
367 }  else {
368 
369 start = 0;
370 
371 }
372 
373 }  else  if (danshun.length - cardLength == 2) {
374 
375  if (cardValue < Poke.getPokeValue(danshun[2])) {
376 
377 start = 2;
378 
379 }  else  if (cardValue < Poke
380 
381 .getPokeValue(danshun[1])) {
382 
383 start = 1;
384 
385 }  else {
386 
387 start = 0;
388 
389 }
390 
391 }
392 
393  int[] dan =  new  int[cardLength];
394 
395  int m = 0;
396 
397  for ( int k = start; k < danshun.length; k++) {
398 
399 dan[m++] = danshun[k];
400 
401 }
402 
403  return dan;
404 
405 }
406 
407 }
408 
409  break;
410 
411  case PokeType.shuangshun:
412 
413 temp = analyz.getCard_shuangshun();
414 
415 size = temp.size();
416 
417  for ( int i = size - 1; i >= 0; i--) {
418 
419  int cardArray[] = temp.get(i);
420 
421  if (cardArray.length < cardLength) {
422 
423  continue;
424 
425 }
426 
427  if (cardValue < Poke.getPokeValue(cardArray[0])) {
428 
429  if (cardArray.length == cardLength) {
430 
431  return cardArray;
432 
433 }  else {
434 
435  int d = (cardArray.length - cardLength) / 2;
436 
437  int index = 0;
438 
439  for ( int j = cardArray.length - 1; j >= 0; j--) {
440 
441  if (cardValue < Poke.getPokeValue(cardArray[j])) {
442 
443 index = j / 2;
444 
445  break;
446 
447 }
448 
449 }
450 
451  int total = cardArray.length / 2;
452 
453  int cardTotal = cardLength / 2;
454 
455  if (index + cardTotal > total) {
456 
457 index = total - cardTotal;
458 
459 }
460 
461  int shuangshun[] =  new  int[cardLength];
462 
463  int m = 0;
464 
465  for ( int k = index * 2; k < cardArray.length; k++) {
466 
467 shuangshun[m++] = cardArray[k];
468 
469 }
470 
471  return shuangshun;
472 
473 }
474 
475 }
476 
477 }
478 
479  break;
480 
481  case PokeType.sanshun:
482 
483 temp = analyz.getCard_sanshun();
484 
485 size = temp.size();
486 
487  for ( int i = size - 1; i >= 0; i--) {
488 
489  int[] cardArray = temp.get(i);
490 
491  if (cardLength > cardArray.length) {
492 
493  continue;
494 
495 }
496 
497  if (cardValue < Poke.getPokeValue(cardArray[0])) {
498 
499  if (cardLength == cardArray.length) {
500 
501  return cardArray;
502 
503 }  else {
504 
505  int[] newArray =  new  int[cardLength];
506 
507  for ( int k = 0; k < cardLength; k++) {
508 
509 newArray[k] = cardArray[k];
510 
511 }
512 
513  return newArray;
514 
515 }
516 
517 }
518 
519 }
520 
521  break;
522 
523  case PokeType.feiji:
524 
525  //  暂时不处理
526 
527  break;
528 
529  case PokeType.zhadan:
530 
531 temp = analyz.getCard_zhadan();
532 
533 size = temp.size();
534 
535  int zd[] =  null;
536 
537  if (size > 0) {
538 
539  for ( int i = 0; i < size; i++) {
540 
541 zd = temp.elementAt(i);
542 
543  if (cardValue < Poke.getPokeValue(zd[0])) {
544 
545  return zd;
546 
547 }
548 
549 }
550 
551 }
552 
553  break;
554 
555  case PokeType.huojian:
556 
557  return  null;
558 
559  case PokeType.sidaier:
560 
561  //  暂时不处理,留待读者完成
562 
563  break;
564 
565 }
566 
567  //  TODO 如果可以一次性出完,无论如何都要,留待读者完成
568 
569  //  根据must的值来判断要牌的必要性
570 
571  boolean needZd =  false;
572 
573  if (must < 90) {
574 
575 must *= 0.2;
576 
577  if (rand.nextInt(100) < must) {
578 
579 needZd =  true;
580 
581 }
582 
583 }  else {
584 
585 needZd =  true;
586 
587 }
588 
589  if (needZd) {
590 
591 temp = analyz.getCard_zhadan();
592 
593 size = temp.size();
594 
595  if (size > 0) {
596 
597  return temp.elementAt(size - 1);
598 
599 }
600 
601 }
602 
603 }  catch (Exception e) {
604 
605 e.printStackTrace();
606 
607 }
608 
609  return  null;
610 
611 }

这里,对于不同的人工智能解析,会有不同的版本,这里的版本暂时设定为Simple2。

以下是出牌智能,上述的解析函数运用在出牌智能中。

该出牌智能基于如下一些原则:

对于出牌者:

玩家只剩下一手牌的时候,此时无论如何也应该要出牌。

对于要牌者(这里理解为挑战者):

当玩家为BOSS时,要牌的紧迫程度随着牌数量的减少而线性减少------- int must=pokeLength*100/17;

当pokeLength<=2时,迫切程度达到100。

当玩家非地主时,如果是地主出的牌,则和BOSS的紧迫性原则差不多,但是,如果是自己家的牌的话,遵循以下的一些原则:

(1) 如果我很可能一次性或者几乎一次性地出完牌(c<=3),那么我就出牌。

(2) 如果我手中的牌的大小大于一定的值(这个值可以经过商议之后微调,不过,这里暂时限定为card.value=12),否则,我可以顺延一个。

这里,有一个很好的想法是“紧迫程度原则”,但是,是否就用must变量来处理,有待进一步分析。

最后一个类为AnalyzePoke类,进行牌型的分析,与牌型的分析不同,这里是对一副牌的分析,上面是对一手牌的分析。AnalyzePoke类本来应该也放置在Poke类中,但是为了减少Poke类中的代码量,于是就将牌型的分析这个模块分离出来,

AnalyzePoke最主要的方法就是分析方法,它将一副牌中所有的牌型全部分析出来(按照牌的威力的依次下降逐层逐层分析的),放置到各自的Vector容器中,这样就可以在需要的时候从各自的牌型中取得,或者进行组合得到。


  1  //  分析几大主要牌型
  2 
  3  private  void analyze() {
  4 
  5  //  初始化牌型容器
  6 
  7 init();
  8 
  9  //  分析王,2,普通牌的数量
 10 
 11  for ( int i = 0; i < pokes.length; i++) {
 12 
 13  int v = Poke.getPokeValue(pokes[i]);
 14 
 15  if (v == 16 || v == 17) {
 16 
 17 countWang++;
 18 
 19 }  else  if (v == 15) {
 20 
 21 count2++;
 22 
 23 }  else {
 24 
 25 countPokes[v - 3]++;
 26 
 27 }
 28 
 29 }
 30 
 31  //  分析三顺牌型
 32 
 33  int start = -1;
 34 
 35  int end = -1;
 36 
 37  for ( int i = 0; i <= countPokes.length - 1; i++) {
 38 
 39  if (countPokes[i] == 3) {
 40 
 41  if (start == -1) {
 42 
 43 start = i;
 44 
 45 }  else {
 46 
 47 end = i;
 48 
 49 }
 50 
 51 }  else {
 52 
 53  if (end != -1 && start != -1) {
 54 
 55  int dur = end - start + 1;
 56 
 57  int[] ss =  new  int[dur * 3];
 58 
 59  int m = 0;
 60 
 61  for ( int j = 0; j < pokes.length; j++) {
 62 
 63  int v = Poke.getPokeValue(pokes[j]) - 3;
 64 
 65  if (v >= start && v <= end) {
 66 
 67 ss[m++] = pokes[j];
 68 
 69 }
 70 
 71 }
 72 
 73  if (m == dur * 3 - 1) {
 74 
 75 System.out.println("sanshun is over!!!");
 76 
 77 }  else {
 78 
 79 System.out.println("sanshun error!!!");
 80 
 81 }
 82 
 83 card_sanshun.addElement(ss);
 84 
 85  for ( int s = start; s <= end; s++) {
 86 
 87 countPokes[s] = -1;
 88 
 89 }
 90 
 91 start = end = -1;
 92 
 93  continue;
 94 
 95 }  else {
 96 
 97 start = end = -1;
 98 
 99 }
100 
101 }
102 
103 }
104 
105  //  分析双顺牌型
106 
107  int sstart = -1;
108 
109  int send = -1;
110 
111  for ( int i = 0; i < countPokes.length; i++) {
112 
113  if (countPokes[i] == 2) {
114 
115  if (sstart == -1) {
116 
117 sstart = i;
118 
119 }  else {
120 
121 send = i;
122 
123 }
124 
125 }  else {
126 
127  if (sstart != -1 && send != -1) {
128 
129  int dur = send - sstart + 1;
130 
131  if (dur < 3) {
132 
133 sstart = send = -1;
134 
135  continue;
136 
137 }  else {
138 
139  int shuangshun[] =  new  int[dur * 2];
140 
141  int m = 0;
142 
143  for ( int j = 0; j < pokes.length; j++) {
144 
145  int v = Poke.getPokeValue(pokes[j]) - 3;
146 
147  if (v >= sstart && v <= send) {
148 
149 shuangshun[m++] = pokes[j];
150 
151 }
152 
153 }
154 
155 card_shuangshun.addElement(shuangshun);
156 
157  for ( int s = sstart; s <= send; s++) {
158 
159 countPokes[s] = -1;
160 
161 }
162 
163 sstart = send = -1;
164 
165  continue;
166 
167 }
168 
169 }  else {
170 
171 sstart = send = -1;
172 
173 }
174 
175 }
176 
177 }
178 
179  //  分析单顺牌型
180 
181  int dstart = -1;
182 
183  int dend = -1;
184 
185  for ( int i = 0; i < countPokes.length; i++) {
186 
187  if (countPokes[i] >= 1) {
188 
189  if (dstart == -1) {
190 
191 dstart = i;
192 
193 }  else {
194 
195 dend = i;
196 
197 }
198 
199 }  else {
200 
201  if (dstart != -1 && dend != -1) {
202 
203  int dur = dend - dstart + 1;
204 
205  if (dur >= 5) {
206 
207  int m = 0;
208 
209  int[] danshun =  new  int[dur];
210 
211  for ( int j = 0; j < pokes.length; j++) {
212 
213  int v = Poke.getPokeValue(pokes[j]) - 3;
214 
215  if (v == dend) {
216 
217 danshun[m++] = pokes[j];
218 
219 countPokes[dend]--;
220 
221 dend--;
222 
223 }
224 
225  if (dend == dstart - 1) {
226 
227  break;
228 
229 }
230 
231 }
232 
233 card_danshun.addElement(danshun);
234 
235 }
236 
237 dstart = dend = -1;
238 
239 }  else {
240 
241 dstart = dend = -1;
242 
243 }
244 
245 }
246 
247 }
248 
249  //  分析三张牌型
250 
251  for ( int i = 0; i < countPokes.length; i++) {
252 
253  if (countPokes[i] == 3) {
254 
255 countPokes[i] = -1;
256 
257  int[] sanzhang =  new  int[3];
258 
259  int m = 0;
260 
261  for ( int j = 0; j < pokes.length; j++) {
262 
263  int v = Poke.getPokeValue(pokes[j]) - 3;
264 
265  if (v == i) {
266 
267 sanzhang[m++] = pokes[j];
268 
269 }
270 
271 }
272 
273 card_sanzhang.addElement(sanzhang);
274 
275 }
276 
277 }
278 
279  //  分析对牌
280 
281  for ( int i = 0; i < countPokes.length; i++) {
282 
283  if (countPokes[i] == 2) {
284 
285  int[] duipai =  new  int[2];
286 
287  for ( int j = 0; j < pokes.length; j++) {
288 
289  int v = Poke.getPokeValue(pokes[j]) - 3;
290 
291  if (v == i) {
292 
293 duipai[0] = pokes[j];
294 
295 duipai[1] = pokes[j + 1];
296 
297 card_duipai.addElement(duipai);
298 
299  break;
300 
301 }
302 
303 }
304 
305 countPokes[i] = -1;
306 
307 }
308 
309 }
310 
311  //  分析单牌
312 
313  for ( int i = 0; i < countPokes.length; i++) {
314 
315  if (countPokes[i] == 1) {
316 
317  for ( int j = 0; j < pokes.length; j++) {
318 
319  int v = Poke.getPokeValue(pokes[j]) - 3;
320 
321  if (v == i) {
322 
323 card_danpai.addElement( new  int[] { pokes[j] });
324 
325 countPokes[i] = -1;
326 
327  break;
328 
329 }
330 
331 }
332 
333 }
334 
335 }
336 
337  //  根据2的数量进行分析
338 
339  switch (count2) {
340 
341  case 4:
342 
343 card_zhadan.addElement( new  int[] { pokes[countWang],
344 
345 pokes[countWang + 1], pokes[countWang + 2],
346 
347 pokes[countWang + 3] });
348 
349  break;
350 
351  case 3:
352 
353 card_sanzhang.addElement( new  int[] { pokes[countWang],
354 
355 pokes[countWang + 1], pokes[countWang + 2] });
356 
357  break;
358 
359  case 2:
360 
361 card_duipai.addElement( new  int[] { pokes[countWang],
362 
363 pokes[countWang + 1] });
364 
365  break;
366 
367  case 1:
368 
369 card_danpai.addElement( new  int[] { pokes[countWang] });
370 
371  break;
372 
373 }
374 
375  //  分析炸弹
376 
377  for ( int i = 0; i < countPokes.length - 1; i++) {
378 
379  if (countPokes[i] == 4) {
380 
381 card_zhadan.addElement( new  int[] { i * 4 + 3, i * 4 + 2,
382 
383 i * 4 + 1, i * 4 });
384 
385 countPokes[i] = -1;
386 
387 }
388 
389 }
390 
391  //  分析火箭
392 
393  if (countWang == 1) {
394 
395 card_danpai.addElement( new  int[] { pokes[0] });
396 
397 }  else  if (countWang == 2) {
398 
399 card_zhadan.addElement( new  int[] { pokes[0], pokes[1] });
400 
401 }
402 
403 }
404 
405 }

 


(这样,可以初步解析出一副牌,以及分析出牌的“综合实力”了,需要改进的地方还有很多,比如,有些时候,牌的“威力”并不是那么固定的)

以 上,就是所有的主要代码,电脑NPC的智商还是有些偏低,这里没有用到一些人工智能的算法(利用博弈树,DFS,BFS,A*)等等,貌似用到了回溯,但 是,其中有些亮点值得注意,这里并没有纯粹客观地利用搜索的深度来逐渐扩大NPC的“智商”,因为,这样会引入一些比较大的时间复杂度,而这在作为玩家的 游戏中时不可取的,我们应该在加强NPC的聪明程度的同时考虑到玩家的耐心程度,斗地主AI的优秀程度,也就是这两个方面的利弊权衡吧!

以上就是我对那3000行代码的一些见解,关于UI,绘图,以及线程刷频等一些东西我就不拿出来了。

你可能感兴趣的:(round)