小游戏中的计算机的人工智能

软件世界

什么是AI?AI是一组旨在使计算机模拟人类推理的技术。总的来说,AI编程就是要把人脑思维的过程逻辑化、程式化。举个例子,一个人要出门看见外面下雨,就会去拿伞。在编程中这一过程可表述为:IF外面下雨AND我要出门THEN去拿伞。这就是一个简单的AI例子。前两期我们所制作的游戏,基本上无须太多的人工智能(以下简称AI),因为这仅仅是用户单方面在操作。当游戏中机器要与玩家作对等操作时,机器就像另一个玩家在与真人玩家对战,此时AI就显得非常重要了。
博弈(棋)类游戏的是小游戏中AI编程水平的典范,事实上战略游戏、RPG游戏等大型游戏都带着较大的博弈类游戏成分(回合制战略游戏尤为突出),只不过棋子变成了坦克、飞机、人物等。
我们先介绍井字棋(英语叫Tic-Tac-Toe),相信不少人都玩过在3×3的方格内双方轮流下子。如果一方的棋子有三个连成一线(横行、竖行、对角线均可)就为赢;如果到最后一步都没有人赢则算和棋。你认为很简单?事实上我们这几期介绍的游戏都比较简单,可是代码量并不少。顺便说一下,我们之所以介绍比较简单的游戏,是为了让大家领会一种思维方法和制作方法,从而开发自己的小游戏,不是做一个算一个了事。
井字棋可以说是微缩版的五子棋,对制作其他博弈游戏有触类旁通的效果。网上的一些算法过于繁复,这里我们编写的算法较为简单。

一、控件设置

包含9个PictureBox的控件数组Pic,分布如(图1)。记住把AutoRedraw属性都设为True否则无法用Line和Circle方法绘图。DrawWidth设为3;四个Option:OptFirstComp(Caption为“计算机先”),OptFirstHuman(Caption为“你先”)为一组放入Frame1;OptHigh(Caption为“高级”),OptJunior(Caption为“初级”)为一组放入Frame2。还有一个按钮Command1(Caption为“开始”)。

图1
图1

二、游戏中全局变量的意义

I,J为循环变量;Counter记录已经进行的步数,FirstMoved记录先下子的玩家(0为计算机,1为真人),Rt用于暂存函数的返回值;Grid(9)用于存储对应的9个PictureBox的走子情况(0为未走,1为真人玩家已在此格下子,2为计算机已在此格下子)。
在编写代码前,回忆一下自己和别人玩时是怎么下的:轮到自己下子时,如果对方有两个子已经连线,而这条直线上还没有己方的棋子,那么在这两个子的直线上的空位下子以封堵(如(图2),画X的一方必须在格8下子,这样通过己方下子堵住对方棋路防止对方下一步获胜的过程称为封堵);如果己方已有两个子连线而未被封堵则直接在直线上的空位下子获胜(如(图3),画O的一方在格0下子即可获胜);否则,按照如下优先级在空位下子:中心、四角、边。好!现在让计算机去模拟。

图2
图2
图3
图3

三、几个关键性的函数和过程

函数LineWin用于计算指定一方(是人还是计算机,通过参数Piece决定)的走子是否有两个或三个连线,如果有两个已连线且该条线上有空位(即如果那两个棋子是真人的,计算机在该位置上下子可以封堵;是计算机的在该位置上下子可以直接获胜),则返回空位号码(0到8),如果有三个已连线则根据参数Piece返回10(真人获胜)或11(计算机获胜)。如果到最终都没有找到符合条件的棋子则返回号码9表示没有位置需要封堵(以后会用到)。可以看出,该函数的原理是逐个检查所有行、列、对角线,统计该条线上要统计的棋子号为Piece的棋子的个数。我们来分析一下其代码:
Function LineWin(Piece As Integer) As Integer
Dim V(3), H(3), LR, RL As Integer
参数Piece中1表示要计算真人的棋子,2表示要计算计算机的。V(3)、H(3)、LR、RL分别记录三个纵行、三个横行、左上-右下对角线、左下-右上对角线上有几个棋子号等于Piece的棋子,如果有2个或3个则加以判断。
LineWin = 9 '初始值为9,如果下面的计算找到符合条件的格子则LineWin值被改变
For I = 0 To 8 Step 4
If Grid(I) = Piece Then LR = LR + 1
Next I '检查左上-右下对角线。
观察这一条线上的格子(0,4,8)发现都是4的倍数故采用Step 4步长值。每找到一个要统计的棋子则LR加1。下面类似。
For I = 2 To 6 Step 2
If Grid(I) = Piece Then RL = RL + 1
Next I '检查右上-左下对角线格子2、4、6。
If LR = 3 Or RL = 3 Then '如果有一方获胜则返回10或11并退出函数
LineWin = 9 + Piece
Exit Function
End If
If LR = 2 Then '如果在左上-右下一线有两个已连线
For I = 0 To 8 Step 4
If Grid(I) = 0 Then '寻找并返回左上-右下线上的空位
LineWin = I
Exit Function
End If
Next I
End If
If RL = 2 Then '寻找并返回右上-左下一线的空位
For I = 2 To 6 Step 2
If Grid(I) = 0 Then
LineWin = I
Exit Function
End If
Next I
End If
For I = 0 To 2
For J = 0 To 2
If Grid(J * 3 + I) = Piece Then V(I) = V(I) + 1 '检查纵列,统计该列棋子号为Piece的棋子数(当I=0时表示是第0列,此时随着J的变化检查第0列即格0,3,6,依此类推)
If Grid(I * 3 + J) = Piece Then H(I) = H(I) + 1 '检查横行(I=0表第0行,检查第0行即0,1,2格)
Next J, I
For I = 0 To 2 '检查是否有一方已经获胜
If V(I) = 3 or H(I) = 3 Then
LineWin = 9 + Piece
Exit Function
End If
Next I
For I = 0 To 2 '遍历所有横行、纵列
If V(I) = 2 Then '如果有纵列两棋子连线
For J = 0 To 2 '寻找该列是否有空格,有则返回。下面检查横行的程序类似。
If Grid(J * 3 + I) = 0 Then
LineWin = J * 3 + I
Exit Function
End If
Next J
End If
If H(I) = 2 Then
For J = 0 To 2
If Grid(I * 3 + J) = 0 Then
LineWin = I * 3 + J
Exit Function
End If
Next J
End If
Next I
End Function
我们之所以把检查是否有一方获胜放在计算落子空格的前面并使用Exit Function退出函数,这涉及到优先级的问题。道理很简单,一旦有一方已经获胜,就无须再计算落子。这就是说计算获胜的优先级较高。优先级思想在后面“高级”模式下计算有重要应用。
下面看看电脑走子的程序。根据OptJunior和OptHigh的值决定是采用初级(被动防守)模式──调用Defend函数来计算,抑或高级(主动进攻)模式──调用Attempt函数计算。计算的结果为要走的格的号码(参见(图1))传给ToMove。如果电脑先走(FirstMove=0)那么电脑走子时通过过程DrawO在ToMove对应的格子里画圈,否则通过DrawX画叉。
Sub COMPUTERMove()
Dim ToMove As Integer
If OptJunior.Value = True Then ToMove = Defend Else ToMove = Attempt
Counter = Counter + 1
If Firstmoved = 0 Then
DrawO Pic(ToMove)
Else
DrawX Pic(ToMove)
End If
Grid(ToMove) = 2 '在Grid中记录这一步
If LineWin(2) = 11 Then '检验是否电脑已经获胜
MsgBox "您输了!"
Ini
Exit Sub
End If
If Counter = 9 Then
MsgBox "和棋"
Ini
End If
End Sub
被动防守计算模式:如果LineWin(1)(参数1表示要分析真人落子的情况)返回一个小于9的值则表示有位置需要封堵,此时根据返回值封堵;如果无位置可封堵或游戏刚刚开始,则利用RndMove函数找一个空的格子随机走一步。
Function Defend() As Integer
If Counter = 9 Then Exit Function
Rt = LineWin(1)
If Counter <= 1 Or Rt = 9 Then
Defend = RndMove
Else
Defend = Rt
End If
End Function
随机走一步的函数:为防止随机数进入死循环,即有时需要计算很长时间才能找到一个空的格子,加入RndCounter变量,如果运算100次仍然没找到则按Grid索引的顺序查找第一个空位。
Function RndMove() As Integer
Dim RndCounter, Rd As Integer
Do
Rd = Int(Rnd * 8 + 1)
RndCounter = RndCounter + 1
If RndCounter > 100 Then GoTo 50
Loop Until Grid(Rd) = 0 '检验查找的位置是否是空位
RndMove = Rd
Exit Function
50 For I = 0 To 8
If Grid(I) = 0 Then
Rd = I
Exit For
End If
Next I
RndMove = Rd
End Function
高级模式的计算函数(按如下优先级进行查找空格的运算:能直接获胜,需要封堵,中心,角,边)。
Function Attempt() As Integer
Dim MBCount As Integer
Rt = LineWin(2) '指定参数2,统计计算机的走子情况
If Rt < 9 Then '返回有效空位(0到8之间)表示计算机已经有两个棋子连成一线且未被封堵,直接在该空位落子获胜
Attempt = Rt
Exit Function
End If
Rt = LineWin(1) '统计真人走子情况
If Rt < 9 Then '返回有效空位表示需要封堵
Attempt = Rt
Exit Function
End If
If Grid(4) = 0 Then '判断中心位置是否可以落子
Attempt = 4
Exit Function
End If
For I = 0 To 8 Step 2 '判断角上的位置(0,2,6,8)可否落子
If Grid(I) = 0 Then
Attempt = I
Exit Function
End If
Next I
For I = 1 To 7 Step 2 '寻找一个可以落子的边上的空位(1,3,5,7)
If Grid(I) = 0 Then
Attempt = I
Exit Function
End If
Next I
End Function
在判断角上位置可否落子运算的过程中包括了中心的位置4,但不能把上面判断中心可否落子的过程省略,否则一旦在0或2格上找到空位就会放弃判断中心空格,从而无法体现中心空格优先级高于角上空格。
下面的就是一些声明、显示输出、初始化、对用户操作的响应代码,比较简单,不再分析。
Option Explicit '声明
Dim I, J, Counter, Firstmoved, Rt As Integer
Dim Grid(9) As Integer
Sub DrawX(pict As PictureBox) '画叉
pict.Cls
pict.Line (pict.ScaleWidth * 0.1, pict.ScaleHeight * 0.1)-(pict.ScaleWidth * 0.9,pict.ScaleHeight * 0.9)
pict.Line (pict.ScaleWidth * 0.9, pict.ScaleHeight * 0.1)-(pict.ScaleWidth * 0.1,pict.ScaleHeight * 0.9)
End Sub
Sub DrawO(pict As PictureBox) '画圆圈
pict.Cls
pict.Circle (pict.ScaleWidth/2, pict.ScaleHeight/2), pict.ScaleWidth * 0.4
End Sub
Sub Ini() '初始化
For I = 0 To 8
Pic(I).Cls
Pic(I).Enabled = False
Pic(I).BackColor = Me.BackColor
Grid(I) = 0
Next I
Counter = 0
If OptFirstComp.Value = True Then
Firstmoved = 0 '计算机先走
Else
Firstmoved = 1 '玩家先走
End If
End Sub
Private Sub Command1_Click()
Ini
For I = 0 To 8
Pic(I).Enabled = True
Pic(I).BackColor = vbWhite
Next I
If Firstmoved = 0 Then COMPUTERMove
End Sub
Private Sub Pic_Click(Index As Integer)
If Grid(Index) <> 0 Then Exit Sub '如果用户单击了一个已经落子的格则无效
Counter = Counter + 1
If Firstmoved = 0 Then '如果计算机先走(用户后走)则用户落子时画叉否则画圈
DrawX Pic(Index)
Else
DrawO Pic(Index)
End If
Grid(Index) = 1 '记录用户落子
If LineWin(1) = 10 Then '检查用户是否已经获胜
MsgBox "您赢了!"
Ini
Exit Sub
End If
If Counter = 9 Then
MsgBox "和棋"
Ini
Exit Sub
End If
COMPUTERMove '计算机相应地走出一步
End Sub
Private Sub Form_Load()
Ini
End Sub
将上述代码连起来即可。记住声明部分要放在程序最前。类似的分析方法、函数、过程可以用于五子棋等的编程。
小结:在进行人工智能编程时,要从如果自己是计算机该怎么办分析入手,把自己的思维用程序语言表述出来。编写博弈类软件时,要注意各落子方法间的关系和优先级,严格按优先级编程。同时尽可能地使程序逻辑严密,思路清晰,利用计算机可以大量进行穷举等运算的优势尽量使程序无懈可击。