C++抽象编程——回溯算法(5)——Nim游戏代码及其反思

来源:互联网 发布:ubuntu snmpwalk 编辑:程序博客网 时间:2024/06/05 18:46

实话,距离上一篇博客发表完到现在,我为了这个代码写了一个多小时。里面的思维方式让我受益匪浅。很累,但是很开心。下面我就分享出来大家一起看看。我写的时候调试了几次,最后我把我几次调试的过程都写在了注释里面,产生的bug我会一一解释。强烈建议大家自己写一遍。下面的实现,主要的实现是相互递归。

Nim游戏代码

首先由于代码比较庞大,我们把它封装在一个类上,以Nim头文件命名:

Nim.h文件

#ifndef _Nim_h#define _Nim_h/**这个文件提供了Nim游戏的基本操作*//** 类型名: Player* ------------* 这个枚举类型用来区分电脑跟玩家 */enum Player { HUMAN, COMPUTER };/*封装nim的操作*/ class simpleNim{    public:        /*        *方法:play        *用法:game.play        *---------------        *用途:开始游戏,使得电脑跟人类对局        */        void play();        /*        * 方法: printInstructions        * 用法: game.printInstructions();        * -------------------------------        * 这个方法向用户解释游戏的规则         */        void printInstructions();        #include "Nimpriv.h" }; #endif

Nimpriv.h

/**这个文件存放的是Nim类的私有成员*/ private:    /*    *方法: getComputerMove    *用法: int nTaken = getComputerMove();    *-----------------------------------    *计算出什么样的举动对于电脑玩家来说是最好的,    *并返回所用的硬币数量。 该方法首先调用    *findGoodMove来查看是否存在获胜举动。     *如果没有,程序只拿走一枚硬币,使得人类玩家更多的机会犯错误。    */    int getComputerMove();    /*    *方法: findGoodMove    *用法: int nTaken = findGoodMove(nCoins);    * -----------------------------------------    *给定指定数量的硬币,该方法寻找在这堆硬币中寻找一个获胜的举动。 如果在该位    *置获胜,该方法返回该值; 如果没有,该方法返回常量NO_GOOD_MOVE。    *一个好的举动是让你的对手处于不利的位置,而糟糕的位置是不会有好的举动。    */    int findGoodMove(int nCoins);    /*    * 方法: isBadPosition    * 用法: if (isBadPosition(nCoins)) . . .    * ---------------------------------------    * 如果nCoins是不利的位置,则此方法返回true。一个不利的位置是没有好的举动。    * 剩下一个硬币显然是一个不好的位置,代表简单的递归的情况    */     bool isBadPosition(int nCoins);    /*    * 方法: getUserMove    * 用法: int nTaken = getUserMove();    * ----------------------------------    * 要求用户拿走并返回所用的硬币数量。    *如果拿取不合法,则要求用户重新进入有效的移动。    */     int getUserMove();    /*    * 方法: announceResult    * 用法: announceResult();    * ------------------------    * 这个方法宣布游戏的最终结果     */    void announceResult();    /*    * 方法: opponent    * 用法: Player other = opponent(player);    * ---------------------------------------    * 返回这个回合的玩家是谁     */    Player opponent(Player player);     /*实例化变量*/    int nCoins; /* 桌子上剩余的硬币数 */    Player whoseTurn; 

Nim.cpp

#include <iostream>#include <string>#include "Nim.h"using namespace std;/*定义常数*/const int N_COINS = 13; //初始化硬币的数量const int MAX_MOVE = 3; //一次最多拿走3个const int NO_GOOD_MOVE = -1; //标记没有好的移动方案const Player STARTING_PLAYER = HUMAN; // 用于游戏由谁开始/*利用条件运算符决定下一个回合的是谁,opponent  对手*/ Player simpleNim::opponent(Player player) {    return (player == HUMAN) ? COMPUTER : HUMAN;} /*开始游戏*/void simpleNim::play(){    nCoins = N_COINS;    whoseTurn = STARTING_PLAYER;    while(nCoins > 1){        cout << "这里有" << nCoins << "个硬币在桌面上" << endl;        if(whoseTurn == HUMAN){ //不能写=            nCoins -= getUserMove();         }else{            int nTaken = getComputerMove();            cout << "我将拿走" << nTaken << "个硬币" << endl;            nCoins -= getComputerMove();         }        whoseTurn = opponent(whoseTurn); //注意这里不能填STARTING_PLAYER    }    announceResult(); } /*实现打印规则*/ void simpleNim::printInstructions(){    cout << "欢迎来到Nim游戏" << endl;    cout << "在这个游戏里,我们桌子有一堆含有" << N_COINS << "个硬币";    cout << endl;    cout <<"每个回个你和我将从这里取走介于1跟";    cout << MAX_MOVE << " 个硬币." << endl;    cout << "谁拿到最后一个硬币,谁就算输" << endl << endl;}/*实现计算机该拿走的数量*/ int simpleNim::getComputerMove(){    int nTaken = findGoodMove(nCoins);    return (nTaken == NO_GOOD_MOVE) ? 1 : nTaken; }/*实现寻找好的策略*/int simpleNim::findGoodMove(int nCoins){    int limit = (nCoins < MAX_MOVE) ? nCoins : MAX_MOVE;    /*在循环中,如果说我们把nTaken < limit,那么结果会如何?*/     for(int nTaken = 1; nTaken <= limit; nTaken++){        /*如果拿走nTaken个硬币后,剩下的处境为坏,那么就拿走nTaken个        *这个时候留给对手的始终是坏的处境,对于计算机来说这就是good move        */         if(isBadPosition(nCoins - nTaken)) return nTaken;     }    return NO_GOOD_MOVE;} /*判断是否处于不利的处境*/bool simpleNim::isBadPosition(int nCoins){    if(nCoins == 1) return true;    return findGoodMove(nCoins) == NO_GOOD_MOVE;} /*获取用户拿走的硬币数*/int simpleNim::getUserMove(){    while(true){ //试想一下,如果没有while(true)程序会怎么运行?     int nTaken;     cout << "你想拿走多少个硬币? ";    cin >> nTaken;    int limit = (nCoins < MAX_MOVE) ? nCoins : MAX_MOVE;    if(nTaken > 0 && nTaken <= MAX_MOVE) return nTaken;    cout << "输入不合法,请输入1到" << limit << "之间的数" << endl;    cout << "这里有 " << nCoins << " 个硬币" << endl;    }} /*宣布结果*/void simpleNim::announceResult(){    if(nCoins == 0){        cout << "你拿了最后一个硬币,你输了" << endl;    }else{        cout << "这里只剩下一个硬币" << endl;        if(whoseTurn == HUMAN){            cout << "你输了" << endl;        } else {            cout << "你赢了" << endl;         }    }} 

测试代码

#include <iostream>#include "Nim.h"using namespace std;int main(){    simpleNim game;    game.printInstructions();    game.play();    return 0;} 

运行结果:

反思

  1. 第一个错误,是我的自己语法错误,在写头文件的时候忘记了加#endif,在编译的时候报错,这个错误还是容易找的。
  2. 第二个错误,是我第一次运行的时候,whoseTurn = opponent(whoseTurn); 括号里面填写了STARTING_PLAYER,这很明显会造成运行出错,因为STARTING_PLAYER是常量,它的值永远不会变,所以导致的结果就是一直是计算机在自己跟自己玩游戏。whoseTurn是变量,它的初始值是由STARTING_PLAYER赋值给它的,所以它的值可以改变。
  3. 第三个错误,是我在实现getUserMove()函数的时候,把while(true)函数忘写了,导致的结果就是在我输入1到3以外的数,它虽然会报错,但是它仍然视为你已经拿了硬币,只不过是不计入总数而已,相当于你进入了假的回合,这属于作弊行为。比如你一直这样,那么计算机总会拿到最后一个硬币
  4. 第四个错误,是我在实现findGoodMove函数的时候把nTaken <= limit,写成了把nTaken < =limit,这个时候相当于计算机只能考虑到至多两种情况,这就可能导致它不可能拿走3个硬币,这就导致了它一直都是只拿走一个硬币,因为它找不到最佳的解决方案。而我们定义的getComputerMove()是没有最佳方案的时候就拿一个。
  5. 第五个易错点是play函数中的这一句,if(whoseTurn == HUMAN),很容易写成if(whoseTurn =HUMAN),这个时候相当于赋值语句,那么出现的情况就会是只有计算机自己跟自己玩的情况.
  6. 这里我们还有一点值得关注,就是为什么程序的一些数据,我们用的是const?这个规则也就是一些数字,我们直接输入数字代替一些大写的字母不是更好?没错,确实如此。但是如果我的游戏规则有所修改呢?如果我输入过程中有错误呢?const能让我们只用修改值,就能修改涉及的其他代码。这也是一种值得学习的设计模式。

以上是我自己在写这个游戏的时候的一些调试过程以及个人感悟,希望对大家有帮助

0 0
原创粉丝点击