吴昊品游戏核心算法 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,绘图,以及线程刷频等一些东西我就不拿出来了。

posted on 2013-02-27 20:20  吴昊系列  阅读(2197)  评论(0编辑  收藏  举报

导航