游戏开发论坛

 找回密码
 立即注册
搜索
查看: 15748|回复: 0

H5版俄罗斯方块游戏开发:游戏的AI算法

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2016-3-28 16:11:39 | 显示全部楼层 |阅读模式
QQ截图20160328161218_副本.jpg

  GameRes游资网授权发布 文/mumuxinfei

  算是"long long ago"的事了, 某著名互联网公司在我校举行了一次"lengend code"的比赛, 其中有一题就是"智能俄罗斯方块"。 本着一向甘做分母, 闪耀分子的绿叶精神, 着着实实地打了一份酱油。 这次借学习H5的机会, 再来重温下俄罗斯方块的AI编写。

  演示&下载:

  该版本依旧较为简陋, 效果如图所示:

052022119821877.gif

  其代码下载地址为: http://pan.baidu.com/s/1sjyY7FJ

  下载解压目录结构如下所示:

052030370928429.png

  点击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. -4.500158825082766
  2. 2   3.4181268101392694
  3. 3   -3.2178882868487753
  4. 4   -9.348695305445199
  5. 5   -7.899265427351652
  6. 6   -3.3855972247263626
复制代码

  源码解读:

  代码文件结构如图所示:

052155191076678.png

  • 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. MoveGenerator.prototype.generate = function(tetrisUnit, shape) {

  7.     var keymapFunc = function(x, y, idx) {
  8.         return "" + x + ":" + y + ":" + idx;
  9.     }

  10.     var moveMapFunc = function(step) {
  11.         return {x:step.x, y:step.y, idx:step.idx};
  12.     }

  13.     var results = [];

  14.     var boards = tetrisUnit.boards;
  15.     var rownum = tetrisUnit.row;
  16.     var colnum = tetrisUnit.col;
  17.     var shapeArrs = shape.shapes;

  18.     var occupy = {}

  19.     var actionQueues = [];
  20.     actionQueues.push({x:shape.x, y:shape.y, idx:shape.idx, prev:-1});
  21.     occupy[keymapFunc(shape.x, shape.y, shape.idx)] = true;

  22.     var head = 0;
  23.     while ( head < actionQueues.length )  {
  24.         var step = actionQueues[head];

  25.         // 1). 向左移动一步
  26.         var tx = step.x - 1;
  27.         var ty = step.y;
  28.         var tidx = step.idx;
  29.         if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  30.             var key = keymapFunc(tx, ty, tidx);
  31.             if ( !occupy.hasOwnProperty(key) ) {
  32.                 actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  33.                 occupy[key] = true;
  34.             }
  35.         }

  36.         // 2). 向右移动一步
  37.         tx = step.x + 1;
  38.         ty = step.y;
  39.         tidx = step.idx;
  40.         if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  41.             var key = keymapFunc(tx, ty, tidx);
  42.             if ( !occupy.hasOwnProperty(key) ) {
  43.                 actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  44.                 occupy[key] = true;
  45.             }
  46.         }

  47.         // 3). 旋转一步
  48.         tx = step.x;
  49.         ty = step.y;
  50.         tidx = (step.idx + 1) % 4;
  51.         if ( tetrisUnit.checkAvailable(tx, ty, shapeArrs[tidx]) ) {
  52.             var key = keymapFunc(tx, ty, tidx);
  53.             if ( !occupy.hasOwnProperty(key) ) {
  54.                 actionQueues.push({x:tx, y:ty, idx:tidx, prev:head});
  55.                 occupy[key] = true;
  56.             }
  57.         }

  58.         // 4). 向下移动一步
  59.         tx = step.x;
  60.         ty = step.y + 1;
  61.         tidx = step.idx;
  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.         } else {

  69.             // *) 若不能向下了, 则为方块的一个终结节点.
  70.             var tmpMoves = [];
  71.             tmpMoves.push(moveMapFunc(step));
  72.             var tprev = step.prev;
  73.             while ( tprev != -1 ) {
  74.                 tmpMoves.push(moveMapFunc(actionQueues[tprev]));
  75.                 tprev = actionQueues[tprev].prev;
  76.             }
  77.             tmpMoves.reverse();

  78.             results.push({last:step, moves:tmpMoves});
  79.         }
  80.         head++;
  81.     }
  82.     return results;

  83. }
复制代码

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

  1. function Evaluator() {
  2. }

  3. Evaluator.prototype.evaluate = function(boards) {
  4. }

  5. function PierreDellacherieEvaluator() {
  6. }

  7. PierreDellacherieEvaluator.prototype = new Evaluator();
  8. PierreDellacherieEvaluator.prototype.constructor = PierreDellacherieEvaluator;

  9. PierreDellacherieEvaluator.prototype.evaluate = function(boards, shape) {
  10.     return (-4.500158825082766) * landingHeight(boards, shape)          // 下落高度
  11.             + (3.4181268101392694) * rowsEliminated(boards, shape)      // 消行个数
  12.             + (-3.2178882868487753) * rowTransitions(boards)            // 行变换
  13.             + (-9.348695305445199) * colTransitions(boards)             // 列变化
  14.             + (-7.899265427351652) * emptyHoles(boards)                 // 空洞个数
  15.             + (-3.3855972247263626) * wellNums(boards);                 // 井数
  16. }
复制代码

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

  1. function AIStrategy() {
  2.   this.generator = new MoveGenerator();
  3.   this.evalutor = new PierreDellacherieEvaluator();
  4. }

  5. /*
  6. * @brief 作出最优的策略
  7. * @return  {dest:{x:{x}, y:{y}, idx:{idx}}, [{action_list}]}
  8. */
  9. AIStrategy.prototype.makeBestDecision = function(tetrisUnit, shape) {

  10.     var bestMove = null;
  11.     var bestScore = -1000000;

  12.     // 1) 生成所有可行的落点, 以及对应的路径线路
  13.     var allMoves = this.generator.generate(tetrisUnit, shape);

  14.     // 2) 遍历每个可行的落点, 选取最优的局面落点
  15.     for ( var i = 0; i < allMoves.length; i++ ) {
  16.         var step = allMoves[i].last;

  17.         var shapeArrs = shape.shapes;
  18.         var bkBoards = tetrisUnit.applyAction2Data(step.x, step.y, shapeArrs[step.idx]);

  19.         // 2.1) 对每个潜在局面进行评估
  20.         var tscore = this.evalutor.evaluate(bkBoards, {x:step.x, y:step.y, shapeArr:shapeArrs[step.idx]});

  21.         // 2.2) 选取更新最好的落点和路径线路
  22.         if ( bestMove === null || tscore > bestScore ) {
  23.             bestScore = tscore;
  24.             bestMove = allMoves[i].moves;
  25.         }
  26.     }

  27.     // 3) 返回最优可行落点, 及其路径线路
  28.     return {score:bestScore, action_moves:bestMove};

  29. }
复制代码

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

  效果评估:

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

  总结:

  该算法的权重系数采用了经验值。 当然了, 也可以借助模拟退火算法来学习参数, 不过由于游戏本身的不确定性/偶然性影响, 收敛的效果并非如预期那么好。 有机会再讲讲。

  无论怎么样, 该AI可以扮演一个合格的"麻烦制造者", 让游戏充满趣味和挑战性。 勿忘初心, let's go!!!

  写在最后:

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

  相关阅读H5版俄罗斯方块游戏开发:需求分析和框架实现

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-5-20 08:53

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表