实话,距离上一篇博客发表完到现在,我为了这个代码写了一个多小时。里面的思维方式让我受益匪浅。很累,但是很开心。下面我就分享出来大家一起看看。我写的时候调试了几次,最后我把我几次调试的过程都写在了注释里面,产生的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;
}
运行结果:
反思
- 第一个错误,是我的自己语法错误,在写头文件的时候忘记了加#endif,在编译的时候报错,这个错误还是容易找的。
- 第二个错误,是我第一次运行的时候,whoseTurn = opponent(whoseTurn); 括号里面填写了STARTING_PLAYER,这很明显会造成运行出错,因为STARTING_PLAYER是常量,它的值永远不会变,所以导致的结果就是一直是计算机在自己跟自己玩游戏。whoseTurn是变量,它的初始值是由STARTING_PLAYER赋值给它的,所以它的值可以改变。
- 第三个错误,是我在实现getUserMove()函数的时候,把while(true)函数忘写了,导致的结果就是在我输入1到3以外的数,它虽然会报错,但是它仍然视为你已经拿了硬币,只不过是不计入总数而已,相当于你进入了假的回合,这属于作弊行为。比如你一直这样,那么计算机总会拿到最后一个硬币
- 第四个错误,是我在实现findGoodMove函数的时候把nTaken <= limit,写成了把nTaken < =limit,这个时候相当于计算机只能考虑到至多两种情况,这就可能导致它不可能拿走3个硬币,这就导致了它一直都是只拿走一个硬币,因为它找不到最佳的解决方案。而我们定义的getComputerMove()是没有最佳方案的时候就拿一个。
- 第五个易错点是play函数中的这一句,if(whoseTurn == HUMAN),很容易写成if(whoseTurn =HUMAN),这个时候相当于赋值语句,那么出现的情况就会是只有计算机自己跟自己玩的情况.
- 这里我们还有一点值得关注,就是为什么程序的一些数据,我们用的是const?这个规则也就是一些数字,我们直接输入数字代替一些大写的字母不是更好?没错,确实如此。但是如果我的游戏规则有所修改呢?如果我输入过程中有错误呢?const能让我们只用修改值,就能修改涉及的其他代码。这也是一种值得学习的设计模式。
以上是我自己在写这个游戏的时候的一些调试过程以及个人感悟,希望对大家有帮助