前言:
  算是"long long ago"的事了, 某著名互联网公司在我校举行了一次"lengend code"的比赛, 其中有一题就是"智能俄罗斯方块". 本着一向甘做分母, 闪耀分子的绿叶精神, 着着实实地打了一份酱油. 这次借学习H5的机会, 再来重温下俄罗斯方块的AI编写.
  本系列的文章链接如下:
  1). 需求分析和目标创新
  2). 游戏的基本框架和实现
  这些博文和代码基本是同步的, 并不确定需求是否会改变, 进度是否搁置, 但期翼自己能坚持和实现.

演示&下载:
  该版本依旧较为简陋, 效果如图所示:
  
  其代码下载地址为: http://pan.baidu.com/s/1sjyY7FJ
  下载解压目录结构如下所示:
  
  点击tetris.html, 在浏览器上运行(由于HTML5程序, 最好在Chrome/Firefox上运行).

算法分析:
  核心算法参考了如下博文:
  • 传统规则俄罗斯方块AI技术介绍 
  • 控制台彩色版带AI的『俄罗斯方块』
  本程序也采用改进的Pierre Dellacherie算法(只考虑当前方块).

  其对局面的评估, 采用6项指标:
  1). Landing Height(下落高度): The height where the piece is put (= the height of the column + (the height of the piece / 2))
  2). Rows eliminated(消行数): The number of rows eliminated.
  3). Row Transitions(行变换): The total number of row transitions. A row transition occurs when an empty cell is adjacent to a filled cell on the same row and vice versa.
  4). Column Transitions(列变换): The total number of column transitions. A column transition occurs when an empty cell is adjacent to a filled cell on the same column and vice versa.
  5). Number of Holes(空洞数): A hole is an empty cell that has at least one filled cell above it in the same column.
  6). Well Sums(井数): A well is a succession of empty cells such that their left cells and right cells are both filled.

  对各个指标进行加权求和, 权重系数取自经验值:

  1. 1   -4.500158825082766
  2. 2   3.4181268101392694
  3. 3   -3.2178882868487753
  4. 4   -9.348695305445199
  5. 5   -7.899265427351652
  6. 6   -3.3855972247263626

源码解读:
  代码文件结构如图所示:
  
  • tetris_base.js: 公共的数据结构, 包括方块定义和方块池等
  • tetris_ai.js: 具体定义了AI的核心算法和数据结构.
  • tetris_game.js: 是整个程序的展示和驱动.
  这边主要讲讲tetris_ai.js这个代码文件, 里面有三个重要的类, MoveGenerator, Evaluator, AIStrategy.
  MoveGenerator用于生成各个可行落点以及对应的路径线路:

  1.     /*
  2.   * @brief 走法生成器
  3.   */
  4.     function MoveGenerator() {
  5.     }
  6.  
  7.   MoveGenerator.prototype.generate = function(tetrisUnit, shape) {
  8.  
  9. var keymapFunc = function(x, y, idx) {
  10. return "" + x + ":" + y + ":" + idx;
  11. }
  12.  
  13. var moveMapFunc = function(step) {
  14. return {x:step.x, y:step.y, idx:step.idx};
  15. }
  16.  
  17. var results = [];
  18.  
  19. var boards = tetrisUnit.boards;
  20. var rownum = tetrisUnit.row;
  21. var colnum = tetrisUnit.col;
  22. var shapeArrs = shape.shapes;
  23.  
  24. var occupy = {}
  25.  
  26. var actionQueues = [];
  27. actionQueues.push({x:shape.x, y:shape.y, idx:shape.idx, prev:-1});
  28. occupy[keymapFunc(shape.x, shape.y, shape.idx)] = true;
  29.  
  30. var head = 0;
  31. while ( head < actionQueues.length ) {
  32. var step = actionQueues[head];
  33.  
  34. // 1). 向左移动一步
  35. var tx = step.x - 1;
  36. var ty = step.y;
  37. var tidx = step.idx;
  38. if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  39. var key = keymapFunc(tx, ty, tidx);
  40. if ( !occupy.hasOwnProperty(key) ) {
  41. actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  42. occupy[key] = true;
  43. }
  44. }
  45.  
  46. // 2). 向右移动一步
  47. tx = step.x + 1;
  48. ty = step.y;
  49. tidx = step.idx;
  50. if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  51. var key = keymapFunc(tx, ty, tidx);
  52. if ( !occupy.hasOwnProperty(key) ) {
  53. actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  54. occupy[key] = true;
  55. }
  56. }
  57.  
  58. // 3). 旋转一步
  59. tx = step.x;
  60. ty = step.y;
  61. tidx = (step.idx + 1) % 4;
  62. if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  63. var key = keymapFunc(tx, ty, tidx);
  64. if ( !occupy.hasOwnProperty(key) ) {
  65. actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  66. occupy[key] = true;
  67. }
  68. }
  69.  
  70. // 4). 向下移动一步
  71. tx = step.x;
  72. ty = step.y + 1;
  73. tidx = step.idx;
  74. if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  75. var key = keymapFunc(tx, ty, tidx);
  76. if ( !occupy.hasOwnProperty(key) ) {
  77. actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  78. occupy[key] = true;
  79. }
  80. } else {
  81.  
  82. // *) 若不能向下了, 则为方块的一个终结节点.
  83. var tmpMoves = [];
  84. tmpMoves.push(moveMapFunc(step));
  85. var tprev = step.prev;
  86. while ( tprev != -1 ) {
  87. tmpMoves.push(moveMapFunc(actionQueues[tprev]));
  88. tprev = actionQueues[tprev].prev;
  89. }
  90. tmpMoves.reverse();
  91.  
  92. results.push({last:step, moves:tmpMoves});
  93. }
  94. head++;
  95. }
  96. return results;
  97.  
  98. }

   Evaluator类, 则把之前的评估因素整合起来:

  1. function Evaluator() {
  2. }
  3.  
  4. Evaluator.prototype.evaluate = function(boards) {
  5. }
  6.  
  7. function PierreDellacherieEvaluator() {
  8. }
  9.  
  10. PierreDellacherieEvaluator.prototype = new Evaluator();
  11. PierreDellacherieEvaluator.prototype.constructor = PierreDellacherieEvaluator;
  12.  
  13. PierreDellacherieEvaluator.prototype.evaluate = function(boards, shape) {
  14. return (-4.500158825082766) * landingHeight(boards, shape) // 下落高度
  15. + (3.4181268101392694) * rowsEliminated(boards, shape) // 消行个数
  16. + (-3.2178882868487753) * rowTransitions(boards) // 行变换
  17. + (-9.348695305445199) * colTransitions(boards) // 列变化
  18. + (-7.899265427351652) * emptyHoles(boards) // 空洞个数
  19. + (-3.3855972247263626) * wellNums(boards); // 井数
  20. }

  AIStrategy整合了落地生成器评估函数, 用于具体决策最优的那个落地点, 以及行动路线.

  1.     function AIStrategy() {
  2.       this.generator = new MoveGenerator();
  3.       this.evalutor = new PierreDellacherieEvaluator();
  4.     }
  5.  
  6. /*
  7. * @brief 作出最优的策略
  8. * @return {dest:{x:{x}, y:{y}, idx:{idx}}, [{action_list}]}
  9. */
  10. AIStrategy.prototype.makeBestDecision = function(tetrisUnit, shape) {
  11.  
  12. var bestMove = null;
  13. var bestScore = -1000000;
  14.  
  15. // 1) 生成所有可行的落点, 以及对应的路径线路
  16. var allMoves = this.generator.generate(tetrisUnit, shape);
  17.  
  18. // 2) 遍历每个可行的落点, 选取最优的局面落点
  19. for ( var i = 0; i < allMoves.length; i++ ) {
  20. var step = allMoves[i].last;
  21.  
  22. var shapeArrs = shape.shapes;
  23. var bkBoards = tetrisUnit.applyAction2Data(step.x, step.y, shapeArrs[step.idx]);
  24.  
  25. // 2.1) 对每个潜在局面进行评估
  26. var tscore = this.evalutor.evaluate(bkBoards, {x:step.x, y:step.y, shapeArr:shapeArrs[step.idx]});
  27.  
  28. // 2.2) 选取更新最好的落点和路径线路
  29. if ( bestMove === null || tscore > bestScore ) {
  30. bestScore = tscore;
  31. bestMove = allMoves[i].moves;
  32. }
  33. }
  34.  
  35. // 3) 返回最优可行落点, 及其路径线路
  36. return {score:bestScore, action_moves:bestMove};
  37.  
  38. }

  注: 该代码注释, 诠释了决策函数的整个流程.

效果评估:
  该AI算法的效果不错, 在演示模式下, 跑了一晚上, 依旧好好的活着. 这也满足了之前想要的需求和功能.

总结:
  该算法的权重系数采用了经验值. 当然了, 也可以借助模拟退火算法来学习参数, 不过由于游戏本身的不确定性/偶然性影响, 收敛的效果并非如预期那么好. 有机会再讲讲.
  无论怎么样, 该AI可以扮演一个合格的"麻烦制造者", 让游戏充满趣味和挑战性. 勿忘初心, let's go!!!

写在最后:
  
如果你觉得这篇文章对你有帮助, 请小小打赏下. 其实我想试试, 看看写博客能否给自己带来一点小小的收益. 无论多少, 都是对楼主一种由衷的肯定.

  

H5版俄罗斯方块(3)---游戏的AI算法的更多相关文章

  1. H5版俄罗斯方块(2)---游戏的基本框架和实现

    前言: 上文中谈到了H5版俄罗斯方块的需求和目标, 这次要实现一个可玩的版本. 但饭要一口一口吃, 很多东西并非一蹴而就. 本文将简单实现一个可玩的俄罗斯方块版本. 下一步会引入AI, 最终采用coc ...

  2. H5版俄罗斯方块(4)---火拼对战的雏形

    前言: 勿忘初心, 本系列的目标是实现一款类似QQ"火拼系列"的人机对战版俄罗斯方块. 在完成了基本游戏框架和AI的算法探索后, 让我们来尝试一下人机大战雏形编写. 本系列的文章链 ...

  3. H5版俄罗斯方块(5)---需求演进和产品迭代

    前言: 产品的形态是不断迭代的, 从粗糙到精致, 从简易到立体. 有了最初的技术积累和时间思考后, 终于明确了该游戏的方向. 我想说的是: 技术不是重点, 产品/用户体验才是核心议题. 结合朋友的游戏 ...

  4. H5版俄罗斯方块(1)---需求分析和目标创新

    前言: 俄罗斯方块和五子棋一样, 规则简单, 上手容易. 几乎每个开发者, 都会在其青春年华时, 签下"xx到此一游". 犹记得大一老师在布置大程作业的时候提过: "什么 ...

  5. 游戏人工智能 读书笔记 (四) AI算法简介——Ad-Hoc 行为编程

    本文内容包含以下章节: Chapter 2 AI Methods Chapter 2.1 General Notes 本书英文版: Artificial Intelligence and Games ...

  6. Java五子棋小游戏(控制台纯Ai算法)

    Java五子棋小游戏(控制台纯Ai算法) 继续之前的那个五子棋程序 修复了一些已知的小Bug 这里是之前的五子棋程序 原文链接 修复了一些算法缺陷 本次增加了AI算法 可以人机对战 也可以Ai对Ai看 ...

  7. 浅析初等贪吃蛇AI算法

    作为小学期程序设计训练大作业的一部分,也是自己之前思考过的一个问题,终于利用小学期完成了贪吃蛇AI的一次尝试,下作一总结. 背景介绍: 首先,我针对贪吃蛇AI这一关键词在百度和google上尽心了检索 ...

  8. Android版俄罗斯方块的实现

    学习Android的基本开发也有一段时间了,可是由于没有常常使用Android渐渐的也就忘记了. Android编程学的不深,不过为了对付逆向,可是有时还是会感到力不从心的.毕竟不是一个计算机专业毕业 ...

  9. 怎样制作一个横版格斗过关游戏 Cocos2d-x 2.0.4

     本文实践自 Allen Tan 的文章<How To Make A Side-Scrolling Beat 'Em Up Game Like Scott Pilgrim with Coco ...

随机推荐

  1. Python 基础练习

    今天接触了python,了解了一下 python 的基础语法,于是想着手训练一下,在本习题集中,参考代码为提供的参考答案,前面的代码为自己思考的代码,最后每道题给出练习的时间. Python 基础练习 ...

  2. 通过HWND获得CWnd指针

    cwnd 又为计算机网络中拥塞窗口(congestion window)的简写.拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化.发送方让自己的发送窗口还可能小于拥塞窗口. CWnd是MFC窗口类 ...

  3. mongodb中的副本集搭建实践

    准备运行1个主节点,2个从节点,从节点中其中是一个是仲裁节点(Arb). --oplogSize --oplogSize --oplogSize 其中application是副本集的名称,节点必须相同 ...

  4. openurl 跳转

    1.拨打电话: [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"tel://68979"]]; ...

  5. SPSS常用基础操作(1)——变量分组

    有时我们需要对数据资料按照某个规则进行归组,如 在上述资料中,想按照年龄进行分组,30岁以下为组1,30-40岁为组2,40岁以上为组3 有两种方法可以实现: 1.使用计算变量功能 <1> ...

  6. 1017作业:配置java环境,学习流程图

  7. SAP NWBC for HTML and Desktop configuration steps[From sdn]

    Summary :- This dcoumnenst conatin the information about requirement , hardware , configuration step ...

  8. hdu 1008 注意同层情况

    #include<iostream> using namespace std; int main() { int n; ]; while(cin>>n) { ,m=; ) br ...

  9. [原]Wpf应用Path路径绘制圆弧

    1. 移动指令:Move Command(M):M 起始点  或者:m 起始点比如:M 100,240或m 100,240使用大写M时,表示绝对值; 使用小写m时; 表示相对于前一点的值,如果前一点没 ...

  10. Hibernate createCriteria查询详解

    本文转载自 : http://penghao122.javaeye.com/blog/80794 1.创建一个Criteria实例 net.sf.hibernate.Criteria这个接口代表对一个 ...