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

实话,距离上一篇博客发表完到现在,我为了这个代码写了一个多小时。里面的思维方式让我受益匪浅。很累,但是很开心。下面我就分享出来大家一起看看。我写的时候调试了几次,最后我把我几次调试的过程都写在了注释里面,产生的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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值