大数跨境
0
0

如何用SQL写一个棋局并优化【1】

如何用SQL写一个棋局并优化【1】 点融黑帮
2016-04-26
2
导读:世界上最远的距离,不是天涯海角,而是我站在你面前,你却只顾着玩我写的棋。

前段时间,人工智能阿尔法围棋以总比分4比1战胜人类代表,李世石与AlphaGo 的人机大战把人工智能推成了热门话题。AI 可以在围棋上战胜人类顶尖棋手的时代已经到来。


可以预见,人工智能和机器人将会在更多领域做到比人力更高效、准确、安全。所以未来,掌握编程技能显得更加重要。


笔者是做DBA的,平时除了专业安装数据库三十年,唯一的乐趣就是写写sql了,所以就来说说怎么用sql来写一个最简单棋类游戏:

Tic Tac Toe(又叫井字棋)。


两个玩家,一个打圈(O),一个打叉(X),轮流在3乘3的井字格上打自己的符号,最先以任意一行、一列或对角线连成一线则为胜。规定X先手。

一个终局棋谱(MOVES)指的是从开始下子到一方获胜或者下完9个子出现平局,从头到尾的下子情况。一方获胜后,本局即终止。不得提前认输。格子从上到下,从左到右,依次编号1-9

MOVES的第一位表示第一子位置,第二位表示第二子位置,......如果一方获胜,MOVES的长度有可能<9。

局面(BOARD)表示棋盘上呈现的局面,也是按照从上到下,从左到右排列。用X和0填入相应的格子。减号“-” 表示空位。

——维基百科(http://www.gpedia.com/zh/m/gpedia/井字遊戲)


举个栗子:

这里表示出的是:

MOVES=3175968,

BOARD=O-X-OOXXX,

WINNER=X


我给自己设了三个难度:

  1. 如何用一条sql列出所有可能终局棋谱和相应的局面

  2. 使用pl/sql写出一个可以陪自己下棋的ai

  3. 给定一个局面用一条SQL判断出哪一方有必胜策略,以及获胜方最多再下几子必定会获胜。并试着把井字棋的3 3 3 变成nmk


本期我们主要讨论——

如何用一条SQL列出所有可能终局棋谱和相应的局面


这里有必要解释一下:

  • 要求生成所有可能的终局棋谱,只要符合规则即可,哪怕其中有些走法可能看起来很愚蠢,也得包含进去。还没下完的棋谱不要列入。

  • 如果两个终局的局面(BOARD)相同,但是其下子顺序(MOVES)不同,则视为不同棋谱,两个都必须出现在结果中。

  • 如果两个棋谱的MOVES不同,但是其终局局面(BOARD)经过旋转、翻转后重合,仍然被视为不同棋谱,两个都必须出现在结果中


做这道题有两个方向:

  1. 先计算 moves,通过 moves 确定 board 和 winner

    这是通常的思路,优点是编程容易,因为 moves 能唯一确定 board,只要判断 moves 是否合法就够了,合法的条件很 简单:只要 X 和 O 轮流下,边下边判断,有一方取胜就停止,下完 9 个格也停止,结果可能是 X 胜也可能是平局。缺点是运行速度慢,因为 moves 的基数比 board 大得多 (255168 vs 958),运算量也大。


  2. 先计算 board,通过 board 确定 moves 和 winner

    这是反常的思路,编程麻烦,因为一个一个合法的终局 board 可能对应多个合法的棋谱 moves,如何演变出合法的棋 步,考虑因素多两个,包括:判断 board 合法性(第一种方向也要做,只不过那里可以改为判断 moves 是否有一方胜),确定所有排列,从中去掉不合法的。但优点是运 行速度快。


先从第一个思路入手:

最自然的解题思路,就是模拟真人下棋的情况,而SQL的优势在于它不会放过每个可能下子的位置,最终自然会得到所有的结局。从空棋盘开始,尝试在每个空位下子,每下一子就查看是否出现胜局,如果胜负已分则不再继续。所有空位已满也不再继续。用sql的劣势也是你不能像高级语言一样定义变量,自定义循环。


我们先创建一张表来存可能的结果,也放便最后去检查是不是做对了




先排列组合出来所有的前4子的情况,并且用两列来存储有顺序的落子顺序(放便后面的like)在落第五子的时候,也就是有一方可以获胜时 用两方的分别去like可以赢的局面,如果已经有人胜出,则剪枝。最后如果每个都落了子还没有人赢则取差集为平棋。



不过这条sql在我的ssd上都要跑近13s这对笔者来说是不可忍受的(虽然这台虚拟机只有800m内存),所以来优化一下吧:


计算机是采用的二进制,因此它极适合逻辑运算,二进制只有两个数码,和逻辑代数中的“真”“假”相吻合,那就试着用二进制来解一下吧:


第一步先来生成棋盘布局,按要求把棋盘的位置编号1-9, 同时算出这个位子的二进制表示,及其行列坐标,用如下的子查询实现:

WITH

cells AS (SELECT LEVEL pos,POWER(2,LEVEL-1) b,CEIL(LEVEL/3) r,MOD(LEVEL-1,3)+1 c FROM DUAL CONNECT BY LEVEL<=9)


获胜情况总共才八种(三横三纵,加上两个对角线),可以用枚举的方法写出来。如果想要做得灵活一点,以便扩展到M,N,K的情况,可以用如下递归思路:

  • 选任意一个棋子作为起点;


  • 从当前棋子向四个方向各走一步,新加入的棋子的二进制位也加入到总和;


  • 走三步(得到三个棋子)则停止。


  • 用递归WITH实现如下:

    ,w (win_bits,cnt,r,c,direction) as (


  • win_bits表示三个连续棋子的二进制之和; cnt表示累计几个棋子,满三个即停;r,c为行列坐标,direction表示哪个方向

    SELECT b,1,r,c,direction FROM cells,(SELECT LEVEL direction FROM DUAL CONNECT BY LEVEL<=4) ----从棋盘m任选一点开始,和四种方向做笛卡尔积

    UNION ALL

    SELECT w.win_bits+cells.b,w.cnt+1,cells.r,cells.c ,w.direction

    FROM w,cells

    WHERE w.cnt<3 ----满三个即停止


  • 下面从四个方向来加入棋子。并不是每个起点都能得到连续三个棋子,下面的坐标关系自然会过滤掉不合格的

          AND (direction=1 AND cells.c=w.c AND cells.r=w.r+1 -----如果方向1, 则加入同列、下一行的位子

               OR direction=2 AND cells.r=w.r AND cells.c=w.c+1 -----如果方向2, 则加入同行、下一列的位子

               OR direction=3 AND cells.r=w.r+1 AND cells.c=w.c+1 -----如果方向3, 则加入下一行、下一列的位子(右下方)

               OR direction=4 AND cells.r=w.r+1 AND cells.c=w.c-1 -----如果方向4, 则加入下一行、上一列的位子(左下方)

    ,win as (select win_bits from w where cnt=3) ---- 选出那些走满三步的记录,这里有8个二进制数字,代表了8种获胜布局


  • 下面开始用递归WITH来模拟所有可能的棋局。所需要的数据结构如下:

    ,t(board  ----- 到目前为止所有X,O的分布

    ,x ------- X下过了哪些位置,用二进制之和表示

    ,o ------- O下过了哪些位置,用二进制之和表示

    ,step ----- 当前走了几步(双方一共下了几子)

    ,winner ----- 如果胜负已分,在这里标出

    ,moves ----- 到目前为止所有下子顺序,即棋谱

    AS (


  • 递归的起点是从X下第一子开始

    SELECT CAST(RPAD('-',pos-1,'-')||'X'||RPAD('-',9-pos,'-') AS VARCHAR2(9)) board  ----- 把X的位置拼入空棋盘

         ,b X

         ,0 O

         ,1

         ,CAST(NULL AS VARCHAR2(1))

         ,CAST(pos AS VARCHAR2(10))

    FROM cells

    UNION ALL


  • 下面是递归的部分,在这里算出所有的变化

    SELECT SUBSTR(t.board,1,cells.pos-1)  ------ 把选中的下一个棋子按照其位置拼到棋盘中去。其位置即m.pos表示的1-9的整数

    ||CASE WHEN MOD(t.step,2)=0 THEN 'X' ELSE 'O' END ----- 根据当前步数算出下一个子应该是X或者O

           ||SUBSTR(t.board,cells.pos+1) 

          ,CASE WHEN MOD(t.step,2)=0 THEN t.x+cells.b ELSE t.x END ---- 如果下一子是X, 把其二进制叠加到t.x,否则保持原样

          ,CASE WHEN MOD(t.step,2)=1 THEN t.o+cells.b ELSE t.o END ---- 对O的处理,方法同上

          ,t.step+1 

          ,CASE WHEN MOD(t.step,2)=0 

                THEN (SELECT 'X' FROM win WHERE

    BITAND(t.x+cells.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 如果轮到X走子,用BITAND判断走后是否出现胜局

    ELSE (SELECT 'O' FROM win WHERE BITAND(t.o+cells.b,win.win_bits)=win.win_bits AND ROWNUM=1) ---- 判断O是否获胜,方法同X

    END,moves||cells.pos ---- 把下一个子的落子情况拼到棋谱中

    FROM t JOIN cells ON BITAND(t.x+t.o,cells.b)=0 ----- 将t和9个棋位连接,只要该位子还没有棋子就可以走, BITAND为零表示该位置为空

    WHERE t.winner IS NULL ----- 胜负未分

    )


  • 递归结束,找出所有叶子节点(胜负已分,或者和棋)

    --SELECT WINNER,COUNT(*) FROM (

    SELECT moves

          ,board

          ,NVL(winner,'D') AS WINNER

    FROM T

    WHERE WINNER IS NOT NULL OR STEP=9

    ) GROUP BY WINNER;

    SELECT WINNER,COUNT(*) FROM T WHERE WINNER IS NOT NULL OR STEP=9 GROUP BY WINNER;


以上就是以最直接思路实现的版本,它确实能够找出所有的棋局,但还有优化余地,因为有很多的棋局是彼此对称的,可以把一个棋局进行各种旋转翻转变换得到其他棋局,而不需要真正去“下完”那些棋局。




本文作者:冯帅(点融黑帮),目前就职于点融网devops部门,担任DBA,业余喜欢弹吉他,研究美食电影等。



随着点融网新一轮融资,点融网即将开始大规模的扩张,需要各种优秀人才的加入,如果你觉得自己够优秀,欢迎加入我们!获取更多职位信息,请关注点融黑帮。

【声明】内容源于网络
0
0
点融黑帮
点融黑帮——一个充满激情和梦想的技术团队,吸引了来自金融及信息科技领域的顶尖人才。我们正在用技术创新改变传统金融。
内容 374
粉丝 0
点融黑帮 点融黑帮——一个充满激情和梦想的技术团队,吸引了来自金融及信息科技领域的顶尖人才。我们正在用技术创新改变传统金融。
总阅读649
粉丝0
内容374