当我们想写一个棋类游戏的时候,不难发现,很多棋类游戏的棋盘都可以用一个二维数组表示,比如:
井字棋(3*3的二维数组)、黑白棋(8*8的二维数组)、五子棋(15*15的二维数组)等等
使用二维数组表示棋盘,数组的下标就是棋子的坐标,数组中的值就是棋子的状态。
好处就是数据访问比较直观,可直接根据下标快速找到某个位置的棋子的状态。
但缺点也是很明显的
比如:
首先是遍历棋盘需要用双重循环处理横坐标跟纵坐标;
其次是判断棋子状态,比如以上所说的三种棋子,需要判断行、列以及斜线8个方向上的棋子状态,因为根据行、列和斜线的下标变化特点,加上判断的算法不统一,需要用多套不同的方法来处理。
针对这种情况,我来给大家介绍一种方法,正如标题所示,用一维数组来表示棋盘状态,那么用一维数组该怎样才能表示一个二维数组呢?
我们用井字棋做为例子来说:
首先来看看用二维数组表示井字棋棋盘:
0 1 2
0 x x x
1 x x x
2 x x x
其中X就是代表棋盘的每个位置,0、1、2就是每个位置的坐标,比如第二个棋子的坐标是(0,1),第六个棋子的坐标是(1,2)
假如起点在第一个棋子,即(x = 0,y = 0)这个位置,那向下走就需要执行一次x + 1, y + 0,才能向下走一格。其他方向同理,都需要处理两个值。
好了,用二维数组表示的就不多说了,相信大家都知道怎么做的,下面来讲讲这次的主题:用一维数组表示,用一维数组表示的有两种方法:
第一种:
0 1 2
3 4 5
6 7 8
直接有多少个棋子就开多大的数组,例如:井字棋有3*3 = 9个,所以用长度为9的一维数组表示棋盘,这样在二维数组中的第一行编号为0 1 2,第二行编号为3 4 5,第三行编号为6 7 8.。即一维数组中的6个元素表示二维数组中的第3行第1个。
这样虽然损失了数据访问的直观性,但它处理数据更加简洁,只需一层遍历就能搜索整个棋盘,不需要关注两个下标。那它是怎么向各个方向走的呢?
其实看上面那个图就不难找到个规律,
向左步进量为1,
向下步进量为3,
向右下步进量为4,
向左下步进量为2,
跟以上各自相反方向的步进量则取负;
这样我们就可以用一个一维数组来表示步进量:
int dir[4] = {1,3,4,2};
但是用这种一维数组存储方法有一个缺点,就是比较难判断是否越界了,比如:在3这个位置,减一变成2,在数组里存在,所以程序会判断出不越界,但是,减1的操作是向左走一步,即3这个位置再向左走一步就已经越界了。
所以给大家介绍第二种表示方法:
dddd
dxxx
dxxx
dxxx
ddddd
这是Warren Smith提出的一种建模方法:http://www.radagast.se/othello/endgame.c
如上表所示,一个井字棋棋盘用长度为21的一维数组表示,其中所有的d为标志位,x为棋盘中棋子状态。看到这个棋盘,有些人会觉得右边没有用标志位d包围整个棋盘,不是跟前面那种方法一样不能判断越界,其实并不会,这样就已经能保证任意一个棋子位置向8个方向走都能遇到标志位。我们把字母改为数字来表示就明白了。
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
16 17 18 19 20
其中一维数组中下标为0、1、2、3、4、8、12、16、17、18、19、20的都表示标志位,我们可以用值-1表示,或任何一个不跟棋子状态一样的数值表示,剩下的位置都是表示棋盘。
这样可以看出,
向左步进量为1,
向下步进量为4,
向右下步进量为5,
向左下步进量为3,
跟以上各自相反方向的步进量则取负;
现在我们再来看看越界这个问题,加入13这个位置,向左走一步,即减1操作,变成12,而12这个位置是标志位,所以越界结束搜索。
任意一个棋位向各个方向走都能遇到标志位,大家可以自己算算,这里就不多说了。
下面,来说说这些下标是怎么从二维数组下标中转换过来的:
以下的x都表示纵向方向,y都表示横向方向
先说第一种方法:
0 1 2
3 4 5
6 7 8
棋盘有r(3)行c(3)列
第一个位置便是0 * 3 + 0 = 0
第二个位置是0 * 3 + 1 = 1
…
第四个位置是1 * 3 + 0 = 3
…
最后一个是2 * 3 + 2 = 8
按照这个规律不难推出一条公式:p = c * x + y,其中p为一维数组下标,c为棋盘有多少列,x为二维数组的x坐标,y为二维数组的y坐标。即一维数组中的p可以表示二维数组中的(x, y)这个位置。
3*3棋盘各个方向的步进量刚才已经写出,为{1,3,4,2}
那如果是4 * 4、5*5甚至更多呢?别急,下面来算算就知道了:
4*4棋盘:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
5*5棋盘:
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
不难看出4*4的步进量为{1,4,5,3}
5*5的步进量为{1,5,6,4}
看出规律了把?,没错,向左右的步进量都不变为1,向下都为列数,向右下都为列数 + 1,向左下都为列数– 1;
所以可以推出一条公式:dir[4] = {1,c,c + 1, c - 1};
接下来是从一维下标推出二维下标,根据p = c * x + y,这条公式,
可以看出
y = p % c
x = (p – y) / c;
第二种方法:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
16 17 18 19 20
同样棋盘有r(3)行c(3)列
棋盘为是5 6 7 9 10 11 13 14 15,那怎么从二维中转过来呢?可以看出:
5 = 3 + 2 + 0 + 0 * 4
6 = 3 + 2 + 1 + 0 * 4
…
9 = 3 + 2 + 0 + 1 * 4
…
15 = 3 + 2 + 2 + 2 * 4
同样可以推出一条规律p = c + 2 + y + x * (c + 1) ,其中p为一维数组下标,c为棋盘有多少列,x为二维数组的x坐标,y为二维数组的y坐标。即一维数组中的p可以表示二维数组中的(x,y)这个位置。
接下来看看步进量问题(大家可以自己画出来算算):
3 * 3的为{1,4,5,3}
4 * 4的为{1,5,6,4}
5 * 5的为{1,6,7,5}
同样不难看出其规律,dir[4] = {1, c + 1, c + 2, c},c为棋盘列数
接下来是从一维下标推出二维下标,根据p = c + 2 + y + x * (c + 1),这条公式,
同样可以看出
y = (p – (c + 2)) % (c + 1),
x = (p – (c + 2) - y) / (c + 1)
好了,今天说的就这么多,下面来看看我做的一个例子,是我在写AI五子棋时用到的:
/** * 设置先手落子的玩家ID * * @param playerid 玩家ID */ public void InitGameState(int playerid) { this.m_playerId = playerid; this.winner = playerid; this.m_board = new int[15 * 15 + 34 + 15]; this.nullPos = new HashSet<>(); this.whitePos = new HashSet<>(); this.blackPos = new HashSet<>(); this.canPutPos = new ArrayList<>(); for (int i = 0; i < 15; ++i) { for (int j = 0; j < 15; ++j) { m_board[17 + i + j * 16] = -1; } } this.len = 0; this.is = false; }
我在初始化中把标志位都设为0,把空的棋盘都设为-1,游戏中设定黑棋为1,白棋为2
下面以一个判断是否五子连珠为例子:
//方向步进量 private static int f[] = {1, 16, 17, 15}; /** * 检查是否有形成五子连珠 * * @param dis 下棋位置 * @param playid 玩家id,即白方还是黑方 */ private void check(int dis, int playid) { for (int i = 0; i < f.length; ++i) { int c = 1; int d = dis + f[i]; //向各个方向步进,直到遇到该位置是标志位或已经有对方棋子结束 while (m_board[d] == playid) { c++; d += f[i]; } d = dis - f[i]; Log.d("d", d + ""); //向相反方向步进,直到遇到该位置是标志位或已经有对方棋子结束 while (m_board[d] == playid) { c++; d -= f[i]; } if (c >= 5) { winner = playid; Log.d("over", winner + "赢"); is = true; break; } } }
下面是用一维数组思路做出的二维矩阵旋转的算法(c++):
#include<iostream> #include<string.h> using namespace std; int result[1000]; int main() { int n; while (cin >> n) { int num = 1; int i = 0, j = 0; memset(result, 0, sizeof(result)); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { result[n + 2 + i + j * (n + 1)] = -1; } } int k = n + 1, f = 1; int dis[4] = { 1, n + 1, -1, -(n + 1) }; while (num <= n * n) { for (int i = 0; i < 4; ++i) { while (result[k + dis[i]] == -1) { result[k += dis[i]] = num++; } } } int pos = 0; for (int i = n + 2; i <= (n + 2) * n; ++i) { if (result[i]) { printf("%5d", result[i]); pos++; if (pos % n == 0) { cout << endl; } } } } return 0; }
下面附上我写的AI五子棋
Java版AI五子棋http://blog.csdn.net/a249900679/article/details/51052103(用二维数组表示棋盘)
Android版AI五子棋框架http://git.oschina.net/ysx_xx/Chess-frame(用一维数组表示棋盘)