2048 A.I. 在 stackoverflow 上有个讨论:http://stackoverflow.com/questions/22342854/what-is-the-optimal-algorithm-for-the-game-2048

得票最高的回答是基于 Min-Max-Tree + alpha beta 剪枝,启发函数的设计很优秀。

其实也可以不用设计启发函数就写出 A.I. 的,我用的方法是围棋 A.I. 领域的经典算法——Monte Carlo 局面评估 + UCT 搜索。

算法的介绍见我几年前写的一篇博文:http://www.cnblogs.com/qswang/archive/2011/08/28/2360489.html

简而言之就两点:

  1. 通过随机游戏评估给定局面的得分;
  2. 从博弈树的父节点往下选择子节点时,综合考虑子节点的历史得分与尝试次数。

针对2048游戏,我对算法做了一个改动——把 Minx-Max-Tree 改为 Random-Max-Tree,因为增加数字是随机的,而不是理性的博弈方,所以猜想 Min-Max-Tree 容易倾向过分保守的博弈策略,而不敢追求更大的成果。

UCT搜索的代码:

Orientation UctPlayer::NextMove(const FullBoard& full_board) const {
int mc_count = ;
while (mc_count < kMonteCarloGameCount) {
FullBoard current_node;
Orientation orientation = MaxUcbMove(full_board);
current_node.Copy(full_board);
current_node.PlayMovingMove(orientation);
NewProfit(&current_node, &mc_count);
} return BestChild(full_board);
}

NewProfit函数用于更新该节点到某叶子节点的记录,是递归实现的:

float UctPlayer::NewProfit(board::FullBoard *node,
int* mc_count) const {
float result;
HashKey hash_key = node->ZobristHash();
auto iterator = transposition_table_.find(hash_key);
if (iterator == transposition_table_.end()) {
FullBoard copied_node;
copied_node.Copy(*node);
MonteCarloGame game(move(copied_node)); if (!HasGameEnded(*node)) game.Run(); result = GetProfit(game.GetFullBoard());
++(*mc_count);
NodeRecord node_record(, result);
transposition_table_.insert(make_pair(hash_key, node_record));
} else {
NodeRecord *node_record = &(iterator->second);
int visited_times = node_record->VisitedTimes();
if (HasGameEnded(*node)) {
++(*mc_count);
result = node_record->AverageProfit();
} else {
AddingNumberRandomlyPlayer player;
AddingNumberMove move = player.NextMove(*node);
node->PlayAddingNumberMove(move);
Orientation max_ucb_move = MaxUcbMove(*node);
node->PlayMovingMove(max_ucb_move);
result = NewProfit(node, mc_count);
float previous_profit = node_record->AverageProfit();
float average_profit = (previous_profit * visited_times + result) /
(visited_times + );
node_record->SetAverageProfit(average_profit);
} node_record->SetVisitedTimes(visited_times + );
} return result;
}

起初用结局的最大数字作为得分,后来发现当跑到512后,Monte Carlo棋局的结果并不会出现更大的数字,各个节点变得没有区别。于是作了改进,把移动次数作为得分,大为改善。

整个程序的设计分为 board、player、game 三大模块,board 负责棋盘逻辑,player 负责移动或增加数字的逻辑,game把board和player连起来。

Game类的声明如下:

class Game {
public:
typedef std::unique_ptr<player::AddingNumberPlayer>
AddingNumberPlayerUniquePtr;
typedef std::unique_ptr<player::MovingPlayer> MovingPlayerUniquePtr; Game(Game &&game) = default; virtual ~Game(); const board::FullBoard& GetFullBoard() const {
return full_board_;
} void Run(); protected:
Game(board::FullBoard &&full_board,
AddingNumberPlayerUniquePtr &&adding_number_player,
MovingPlayerUniquePtr &&moving_player); virtual void BeforeAddNumber() const {
} virtual void BeforeMove() const {
} private:
board::FullBoard full_board_;
AddingNumberPlayerUniquePtr adding_number_player_unique_ptr_;
MovingPlayerUniquePtr moving_player_unique_ptr_; DISALLOW_COPY_AND_ASSIGN(Game);
};

Run函数的实现:

void Game::Run() {
while (!HasGameEnded(full_board_)) {
if (full_board_.LastForce() == Force::kMoving) {
BeforeAddNumber(); AddingNumberMove
move = adding_number_player_unique_ptr_->NextMove(full_board_);
full_board_.PlayAddingNumberMove(move);
} else {
BeforeMove(); Orientation orientation =
moving_player_unique_ptr_->NextMove(full_board_);
full_board_.PlayMovingMove(orientation);
}
}
}

这样就可以通过继承 Game 类,实现不同的构造函数,组合出不同的 Game,比如 MonteCarloGame 的构造函数:

MonteCarloGame::MonteCarloGame(FullBoard &&full_board) :
Game(move(full_board),
std::move(Game::AddingNumberPlayerUniquePtr(
new AddingNumberRandomlyPlayer)),
std::move(Game::MovingPlayerUniquePtr(new MovingRandomlyPlayer))) {}

一个新的2048棋局,会先放上两个数字,新棋局应该能方便地build。默认应该随机地增加两个数字,builder 类可以这么写:

template<class G>
class NewGameBuilder {
public:
NewGameBuilder();
~NewGameBuilder() = default; NewGameBuilder& SetLastForce(board::Force last_force); NewGameBuilder& SetAddingNumberPlayer(game::Game::AddingNumberPlayerUniquePtr
&&initialization_player); G Build() const; private:
game::Game::AddingNumberPlayerUniquePtr initialization_player_;
}; template<class G>
NewGameBuilder<G>::NewGameBuilder() :
initialization_player_(game::Game::AddingNumberPlayerUniquePtr(
new player::AddingNumberRandomlyPlayer)) {
} template<class G>
NewGameBuilder<G>& NewGameBuilder<G>::SetAddingNumberPlayer(
game::Game::AddingNumberPlayerUniquePtr &&initialization_player) {
initialization_player_ = std::move(initialization_player);
return *this;
} template<class G>
G NewGameBuilder<G>::Build() const {
board::FullBoard full_board; for (int i = ; i < ; ++i) {
board::AddingNumberMove move = initialization_player_->NextMove(full_board);
full_board.PlayAddingNumberMove(move);
} return G(std::move(full_board));
}

很久以前,高效的 C++ 代码不提倡在函数中 return 静态分配内存的对象,现在有了右值引用就方便多了。

main 函数:

int main() {
InitLogConfig();
AutoGame game = NewGameBuilder<AutoGame>().Build();
game.Run();
}

./fool2048:

这个A.I.的移动不像基于人为设置启发函数的A.I.那么有规则,不会把最大的数字固定在角落,但最后也能有相对不错的结果,游戏过程更具观赏性~

项目地址:https://github.com/chncwang/fool2048

最后发个招聘链接:http://www.kujiale.com/about/join

我这块的工作主要是站内搜索、推荐算法等,欢迎牛人投简历到hr邮箱~

基于Monte Carlo方法的2048 A.I.的更多相关文章

  1. 蒙特卡罗(Monte Carlo)方法简介

    蒙特卡罗(Monte Carlo)方法,也称为计算机随机模拟方法,是一种基于"随机数"的计算方法. 二 解决问题的基本思路 Monte Carlo方法的基本思想很早以前就被人们所发 ...

  2. Monte Carlo方法简介(转载)

    Monte Carlo方法简介(转载)       今天向大家介绍一下我现在主要做的这个东东. Monte Carlo方法又称为随机抽样技巧或统计实验方法,属于计算数学的一个分支,它是在上世纪四十年代 ...

  3. 利用蒙特卡洛(Monte Carlo)方法计算π值[ 转载]

    部分转载自:https://blog.csdn.net/daniel960601/article/details/79121055 圆周率π是一个无理数,没有任何一个精确公式能够计算π值,π的计算只能 ...

  4. 蒙特卡罗方法、蒙特卡洛树搜索(Monte Carlo Tree Search,MCTS)初探

    1. 蒙特卡罗方法(Monte Carlo method) 0x1:从布丰投针实验说起 - 只要实验次数够多,我就能直到上帝的意图 18世纪,布丰提出以下问题:设我们有一个以平行且等距木纹铺成的地板( ...

  5. 增强学习(四) ----- 蒙特卡罗方法(Monte Carlo Methods)

    1. 蒙特卡罗方法的基本思想 蒙特卡罗方法又叫统计模拟方法,它使用随机数(或伪随机数)来解决计算的问题,是一类重要的数值计算方法.该方法的名字来源于世界著名的赌城蒙特卡罗,而蒙特卡罗方法正是以概率为基 ...

  6. Monte carlo

    转载 http://blog.sciencenet.cn/blog-324394-292355.html 蒙特卡罗(Monte Carlo)方法,也称为计算机随机模拟方法,是一种基于"随机数 ...

  7. [Bayes] MCMC (Markov Chain Monte Carlo)

    不错的文章:LDA-math-MCMC 和 Gibbs Sampling 可作为精进MCMC抽样方法的学习材料. 简单概率分布的模拟 Box-Muller变换原理详解 本质上来说,计算机只能生产符合均 ...

  8. Monte Carlo与TD算法

    RL 博客:http://blog.sciencenet.cn/home.php?mod=space&uid=3189881&do=blog&view=me&from= ...

  9. 简析Monte Carlo与TD算法的相关问题

    Monte Carlo算法是否能够做到一步更新,即在线学习? 答案显然是不能,如果可以的话,TD算法还有何存在的意义?MC算法必须要等到episode结束后才可以进行值估计的主要原因在于对Return ...

随机推荐

  1. 【codeforces 779A】Pupils Redistribution

    [题目链接]:http://codeforces.com/contest/779/problem/A [题意] 让你把两个组的5个人的数目都变成一样的. 支持交换操作; 问你最少需要交换几次. [题解 ...

  2. 链表与哈希表基本概念及Java常用集合

    -链表- 是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点包括两个 ...

  3. 一题多解(五) —— topK(数组中第 k 大/小的数)

    根据对称性,第 k 大和第 k 小,在实现上,是一致的,我们就以第 k 小为例,进行说明: 法 1 直接排序(sort(A, A+N)),当使用一般时间复杂度的排序算法时,其时间复杂度为 O(N2) ...

  4. 【】【】Pocket Cube

    Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others) Total Submission(s) ...

  5. spring boot打包后在tomcat无法访问静态资源问题

    我的spring boot项目中前端页面的资源引用 我的静态文件夹是 我的application.yml中资源路径配置了 同时我在WebMvcConfig中配置了addResourceHandlers ...

  6. linux 网卡配置文件详解

    配置文件位置:/etc/sysconfig/network-scripts/ifcfg-eth0 1. DEVICE=eth0 网卡的名字 2. HWADDR=00:0c:29:90:89:d9 HW ...

  7. 【16.05%】【codeforces 664B】Rebus

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  8. 【Python注意事项】如何理解python中间generator functions和yield表情

    本篇记录自己的笔记Python的generator functions和yield理解表达式. 1. Generator Functions Python支持的generator functions语 ...

  9. java项目采用exe4j打包成exe档

    java项目采用exe4j打包成exe档 前言:我们都知道java是平台无关性.能够打包成jar文件,到不论什么操作系统有jre环境的电脑都能够同意!可是我们打包成exe文件就相当于舍弃了这一大优势, ...

  10. 性能测试以及对redis

    Ignite性能测试以及对redis的对比   测试方法 为了对Ignite做一个基本了解,做了一个性能测试,测试方法也比较简单主要是针对client模式,因为这种方法和使用redis的方式特别像.测 ...