本软件是基于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:一个存储不同数值的辅助工具类
- 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,绘图,以及线程刷频等一些东西我就不拿出来了。