今天以黑白棋为例,开始给一个win32的小游戏设计,
这里打算分3部分介绍
1、棋盘,棋局的现实
2、玩家、AI的现实(且听下回分解)
3、游戏画面的现实(且听下下回分解)
其中第一部分为黑白棋游戏的主要逻辑:
1、棋盘,以及棋盘上的棋子的存储形式。这里用到了位图。
2、是否可以落子的判断(黑白棋是只有你落子的位置,在横竖斜八个方向中任意一个方向,能吃掉对方的子,你才可以落在该位置,八个方向都吃不掉对方子的位置是不能下的),以及吃子的逻辑(吃子的逻辑同样是八个方向,两个己方棋子之间夹住的对方棋子,可以被吃掉,翻转为己方棋子)。这里为了使得代码简介一点,使用了函数指针(不同方向上坐标的变化逻辑不一样)。
3、某一方下了一个子之后,交换手的判断(黑白棋中存在可能,一方下了一个子,并吃掉对方的子,之后对方无子可下,没有一个位置能使得对方能吃掉己方的棋子,所以黑白棋并不一定始终是一人一步来的,它可能存在一方连续落子的情况)。
4、记录玩家每一步下棋的步骤,使得可以悔棋(只悔玩家,不悔AI)。
5、游戏是否结束的判断(由于存在无子可下的情况,有可能双方都无子可以下,即一方将另一方全部吃光,所以黑白棋不一定是下满棋盘才分出胜负)。
第二部分主要为了写AI:
黑白棋的AI其实蛮复杂,有专门的研究黑白棋的AI的算法文章,这里只简单介绍一下,也涉及到几个概念:稳定子、行动力、等,具体在第二部分介绍。
第三部分主要是游戏画面的显示,涉及到windows消息机制,鼠标事件,键盘事件,菜单事件,定时器事件;以及简单的图形、文字绘制,涉及到画笔、画刷填充、绘图层HDC、画线、画圆、显示文字、双缓冲的位图拷贝。
阅读第三部分前,读者可以先行阅读《windows程序设计》一书打个基础。
也可以看完博文之后,再将涉及到的图形API,消息机制等windows程序设计中涉及到的点带回到书中去详细了解。
ps:只看1、3部分,同样可以实现一个黑白棋,只是没有AI,就只能人跟人下
黑白棋游戏在设计中需要注意的几点:
1、惯例,首先要定义好棋盘的坐标,定义为左上角那一格为(0,0),向右为x正方向,向下为y正方向,黑白棋棋盘是一个8*8的棋盘,所以定义两个常量表示:
const int REVERSI_MAX_ROW = 8; const int REVERSI_MAX_COLUMN = 8;
2、棋盘上棋子的类型分三种:黑子,白子,空白无子,枚举表示
enum EnumReversiPiecesType { enum_ReversiPieces_Null = 0x00, enum_ReversiPieces_Black = 0x01, enum_ReversiPieces_White = 0x02, };
这三种情况,其实用2位2进制即可表示,一行8个位置就是16位2进制,就是一个WORD就足够了,所以:
3、棋盘的表示,位图
TArray1<WORD, REVERSI_MAX_ROW> m_Map;
位图是8行,每行是一个WORD,这个TArray1是之前实现的一维数组模板直接用的
4、棋盘上一个位置的设计,因为这里涉及到位置(即坐标)的八方向移动的逻辑,因此将坐标位置单独抽象出来,实现坐标的上下左右以及斜的四方向的坐标变化,然后将其重定义为函数指针,使得后面在坐标变化时,不用switch...case八种情况,而是可以将方向当成参数。
typedef void (ReversiPoint::*ReversiPointForward)();
5、某一方的棋子,在某一坐标位置,向某一方向,是否可以吃掉对方的棋子的判断
bool ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward);是否可以吃子的伪代码:
定义一个坐标对象point,初值为当前点row_y, column_x 记录该方向上的搜索次数search,初值为0 point向forward方向移动 搜索次数search++ while (point是一个合法的坐标,不能移出棋盘外面去了) { 取point当前位置的棋子类型 (此时已经是forward移动一次之后的位置了,不是row_y, column_x了) if (当前位置有棋子) { if (当前位置的棋子类型等于传入参数type,type就是要下的棋子类型) { if (搜索次数search大于1次) { 说明找到的同色棋子与当前棋子坐标差超过1,point至少移动了2次 则两子之间夹有不同色的棋子 符合翻转规则,return true } else { 说明找到的同色棋子与当前棋子,两子是紧挨着的 该方向两子之间无子可以翻转 不符合翻转规则,return false } } else { 说明找到的是不同色的棋子,继续向下一个位置搜 point向forward方向移动 搜索次数search++ } } else { 一直找到空位也没找到,该方向没有同色棋子,无法翻转 } } 超出棋盘范围都没有找到同色棋子,该方向没有同色棋子,无法翻转
6、某一方的棋子,在某一坐标位置,向某一方向,吃掉对方的棋子
void DoReversi(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward);
伪代码实现
定义一个坐标对象point,初值为当前点row_y, column_x point向forward方向移动 while (point是一个合法的坐标,不能移出棋盘外面去了) { 取point当前位置的棋子类型 (此时已经是forward移动一次之后的位置了,不是row_y, column_x了) if (当前位置的棋子类型不等于传入参数type,type就是下的棋子类型) { 将该位置的棋子类型翻转为type一方的棋子 point向forward方向移动 因为在翻转之前做了ReversiCheck的判断 即这个方向肯定是符合翻转规则,有子可吃的 所以这里不再判断当前位置的棋子类型是否为空 } }
有了上面两个基本函数
7、判断某个位置是否可以落子
bool ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x);
则是分别调用上面的ReversiCheck,然后forward传入不同的方向
8、判断某一方是否可以落子
bool CanPlay(EnumReversiPiecesType type);即遍历棋盘每一个位置,任意一个位置可以落子,则该方可以落子
9、落一个子之后的吃子
void DoReversi(EnumReversiPiecesType type, char row_y, char column_x);则是分别调用上面的DoReversi,然后forward传入不同的方向
10、下棋的步骤,为了可以悔棋
typedef struct ReversiStep { ReversiBitBoard m_LastMap; EnumReversiPiecesType m_CurrType; ReversiStep& operator= (const ReversiStep& temp) { m_CurrType = temp.m_CurrType; m_LastMap = temp.m_LastMap; return *this; } }ReversiStep;
这里直接记录落子之前的棋盘状态,以及当前这一步是谁落得子就可以了,悔棋的时候就是恢复棋盘状态到这个人落子之前的状态,然后仍然由该玩家重新落子
落子的时候,填一下ReversiStep,存入链表,悔棋的时候,从链表尾退出一个节点,并将棋盘状态恢复为这个尾节点的状态,即实现了悔棋
一盘棋总共60步下满,这里用了一个对象池,一次性申请好60步所需内存,这样避免在频繁的落子悔棋的过程中,频繁的申请内存
11、最后,判断游戏是否结束的逻辑,即双方都无子可下,则游戏结束
先给个游戏截图吧
下面先贴出第一部分的代码
ReversiCommon.h
#ifndef _ReversiCommon_h_ #define _ReversiCommon_h_ //棋盘大小 const int REVERSI_MAX_ROW = 8; const int REVERSI_MAX_COLUMN = 8; enum EnumReversiPiecesType { enum_ReversiPieces_Null = 0x00, enum_ReversiPieces_Black = 0x01, enum_ReversiPieces_White = 0x02, }; enum EnumReversiResult { enum_Reversi_Playing = 0, enum_Reversi_Draw, enum_Reversi_Win_Black, enum_Reversi_Win_White, }; #endif
ReversiPoint.h
#ifndef _ReversiPoint_h_ #define _ReversiPoint_h_ #include "ReversiCommon.h" typedef struct ReversiPoint { char m_row_y; char m_column_x; ReversiPoint& operator= (const ReversiPoint& temp) { m_row_y = temp.m_row_y; m_column_x = temp.m_column_x; return *this; } bool operator!= (const ReversiPoint& temp) { if (m_row_y == temp.m_row_y && m_column_x == temp.m_column_x) { return false; } else { return true; } } bool IsValid() { if (0 <= m_row_y && 0 <= m_column_x && m_row_y < REVERSI_MAX_ROW && m_column_x < REVERSI_MAX_COLUMN) { return true; } else { return false; } } void UL() { m_row_y--; m_column_x--; } void U() { m_row_y--; } void UR() { m_row_y--; m_column_x++; } void L() { m_column_x--; } void R() { m_column_x++; } void DL() { m_row_y++; m_column_x--; } void D() { m_row_y++; } void DR() { m_row_y++; m_column_x++; } }ReversiPoint; typedef void (ReversiPoint::*ReversiPointForward)(); #endif
ReversiBitBoard.h
#ifndef _ReversiBitBoard_h_ #define _ReversiBitBoard_h_ #include <Windows.h> #include "TArray.h" #include "ReversiCommon.h" class ReversiBitBoard { public: ReversiBitBoard(); ~ReversiBitBoard(); ReversiBitBoard& operator= (const ReversiBitBoard& temp); void SetPieces(EnumReversiPiecesType type, char row_y, char column_x); EnumReversiPiecesType GetPieces(char row_y, char column_x); BYTE GetBlackCount(); BYTE GetWhiteCount(); private: TArray1<WORD, REVERSI_MAX_ROW> m_Map; BYTE m_BlackCount; BYTE m_WhiteCount; }; #endif
ReversiBitBoard.cpp
#include "ReversiBitBoard.h" ReversiBitBoard::ReversiBitBoard() { for (int i = 0; i < REVERSI_MAX_ROW; i++) { m_Map[i] = 0; } m_BlackCount = 0; m_WhiteCount = 0; } ReversiBitBoard::~ReversiBitBoard() { } ReversiBitBoard& ReversiBitBoard::operator=(const ReversiBitBoard& temp) { m_Map = temp.m_Map; m_BlackCount = temp.m_BlackCount; m_WhiteCount = temp.m_WhiteCount; return *this; } void ReversiBitBoard::SetPieces(EnumReversiPiecesType type, char row_y, char column_x) { WORD oldtype = (m_Map[row_y] & (0x0003 << (column_x * 2))) >> (column_x * 2); if (enum_ReversiPieces_Black == oldtype) { m_BlackCount--; } else if (enum_ReversiPieces_White == oldtype) { m_WhiteCount--; } m_Map[row_y] = m_Map[row_y] & (~(0x0003 << (column_x * 2))); m_Map[row_y] = m_Map[row_y] | (type << (column_x * 2)); if (enum_ReversiPieces_Black == type) { m_BlackCount++; } else if (enum_ReversiPieces_White == type) { m_WhiteCount++; } } EnumReversiPiecesType ReversiBitBoard::GetPieces(char row_y, char column_x) { WORD value = m_Map[row_y] & (0x0003 << (column_x * 2)); value = value >> (column_x * 2); EnumReversiPiecesType type = static_cast<EnumReversiPiecesType>(value); return type; } BYTE ReversiBitBoard::GetBlackCount() { return m_BlackCount; } BYTE ReversiBitBoard::GetWhiteCount() { return m_WhiteCount; }
Reversi.h
#ifndef _Reversi_ #define _Reversi_ #include "TBDLinkList.h" #include "TObjectPool.h" #include "ReversiCommon.h" #include "ReversiBitBoard.h" #include "ReversiPoint.h" typedef struct ReversiStep { ReversiBitBoard m_LastMap; EnumReversiPiecesType m_CurrType; ReversiStep& operator= (const ReversiStep& temp) { m_CurrType = temp.m_CurrType; m_LastMap = temp.m_LastMap; return *this; } }ReversiStep; class Reversi { public: Reversi(); ~Reversi(); void Init(); bool CanPlay(EnumReversiPiecesType type); bool CanPlay(EnumReversiPiecesType type, char row_y, char column_x); void DoReversi(EnumReversiPiecesType type, char row_y, char column_x); bool ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x); void AddReversiStep(); void Cancel(); void SwapPlayer(); EnumReversiResult IsGameOver(); EnumReversiPiecesType GetCurrType(); ReversiBitBoard& GetMap(); private: void DoReversi(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward); bool ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward); EnumReversiPiecesType m_CurrType; ReversiBitBoard m_ReversiMap; TBDLinkList<ReversiStep> m_ReversiStepList; TObjectPool<TBDLinker<ReversiStep>> m_ReversiStepPool; }; #endif
Reversi.cpp
#include "Reversi.h" Reversi::Reversi() { } Reversi::~Reversi() { } void Reversi::Init() { m_CurrType = enum_ReversiPieces_Black;//规定黑先 m_ReversiStepList.Init(enum_DisableLock); m_ReversiStepPool.Init(REVERSI_MAX_ROW * REVERSI_MAX_COLUMN, 0, enum_DisableLock_ObjPool, enum_DisableAssign_ObjPool); for (int i = 0; i < REVERSI_MAX_ROW; i++) { for (int j = 0; j < REVERSI_MAX_COLUMN; j++) { m_ReversiMap.SetPieces(enum_ReversiPieces_Null, i, j); } } m_ReversiMap.SetPieces(enum_ReversiPieces_Black, 3, 3); m_ReversiMap.SetPieces(enum_ReversiPieces_White, 3, 4); m_ReversiMap.SetPieces(enum_ReversiPieces_White, 4, 3); m_ReversiMap.SetPieces(enum_ReversiPieces_Black, 4, 4); } bool Reversi::CanPlay(EnumReversiPiecesType type) { for (int i = 0; i < REVERSI_MAX_ROW; i++) { for (int j = 0; j < REVERSI_MAX_COLUMN; j++) { if (CanPlay(type, i, j)) { return true; } } } return false; } bool Reversi::CanPlay(EnumReversiPiecesType type, char row_y, char column_x) { if (enum_ReversiPieces_Null == m_ReversiMap.GetPieces(row_y, column_x)) { if (ReversiCheck(type, row_y, column_x)) { return true; } } return false; } bool Reversi::ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x) { if (ReversiCheck(type, row_y, column_x, &ReversiPoint::UL) || ReversiCheck(type, row_y, column_x, &ReversiPoint::U) || ReversiCheck(type, row_y, column_x, &ReversiPoint::UR) || ReversiCheck(type, row_y, column_x, &ReversiPoint::L) || ReversiCheck(type, row_y, column_x, &ReversiPoint::R) || ReversiCheck(type, row_y, column_x, &ReversiPoint::DL) || ReversiCheck(type, row_y, column_x, &ReversiPoint::D) || ReversiCheck(type, row_y, column_x, &ReversiPoint::DR)) { return true; } return false; } bool Reversi::ReversiCheck(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward) { ReversiPoint point = {row_y, column_x}; EnumReversiPiecesType currType; int search = 0; (point.*forward)();//向某方向搜寻 search++; while(point.IsValid()) { currType = m_ReversiMap.GetPieces(point.m_row_y, point.m_column_x); if (enum_ReversiPieces_Null != currType) { if (type == currType) { if (search > 1) { //找到的同色棋子与当前棋子坐标差超过1,则两子之间夹有不同色的棋子 return true; } else { //否则两子是紧挨着的,该方向两子之间无子可以翻转 return false; } } else { //找到的是不同色的棋子,继续 (point.*forward)(); search++; } } else { //一直找到空位也没找到,该方向没有同色棋子,无法翻转 return false; } } //超出棋盘范围都没有找到同色棋子,该方向没有同色棋子,无法翻转 return false; } void Reversi::DoReversi(EnumReversiPiecesType type, char row_y, char column_x) { if (ReversiCheck(type, row_y, column_x, &ReversiPoint::UL)) { DoReversi(type, row_y, column_x, &ReversiPoint::UL); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::U)) { DoReversi(type, row_y, column_x, &ReversiPoint::U); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::UR)) { DoReversi(type, row_y, column_x, &ReversiPoint::UR); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::L)) { DoReversi(type, row_y, column_x, &ReversiPoint::L); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::R)) { DoReversi(type, row_y, column_x, &ReversiPoint::R); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::DL)) { DoReversi(type, row_y, column_x, &ReversiPoint::DL); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::D)) { DoReversi(type, row_y, column_x, &ReversiPoint::D); } if (ReversiCheck(type, row_y, column_x, &ReversiPoint::DR)) { DoReversi(type, row_y, column_x, &ReversiPoint::DR); } } void Reversi::DoReversi(EnumReversiPiecesType type, char row_y, char column_x, ReversiPointForward forward) { ReversiPoint point = {row_y, column_x}; (point.*forward)(); while(point.IsValid()) { if (type != m_ReversiMap.GetPieces(point.m_row_y, point.m_column_x)) { m_ReversiMap.SetPieces(type, point.m_row_y, point.m_column_x); (point.*forward)(); } else { break; } } } void Reversi::AddReversiStep() { TBDLinker<ReversiStep> *pLinker = m_ReversiStepPool.Malloc(); if (NULL != pLinker) { pLinker->m_Value.m_LastMap = m_ReversiMap; pLinker->m_Value.m_CurrType = m_CurrType; pLinker->m_pLinkList = NULL; m_ReversiStepList.PushTail(pLinker); } } void Reversi::Cancel() { TBDLinker<ReversiStep> *pLinker = m_ReversiStepList.PopTail(); if (NULL != pLinker) { m_ReversiMap = pLinker->m_Value.m_LastMap; m_CurrType = pLinker->m_Value.m_CurrType; m_ReversiStepPool.Free(pLinker); } } void Reversi::SwapPlayer() { EnumReversiPiecesType nexttype; if (enum_ReversiPieces_Black == m_CurrType) { nexttype = enum_ReversiPieces_White; } else { nexttype = enum_ReversiPieces_Black; } if (CanPlay(nexttype)) { m_CurrType = nexttype; } } EnumReversiResult Reversi::IsGameOver() { if (!CanPlay(enum_ReversiPieces_Black) && !CanPlay(enum_ReversiPieces_White)) { BYTE black = m_ReversiMap.GetBlackCount(); BYTE white = m_ReversiMap.GetWhiteCount(); if (black > white) { return enum_Reversi_Win_Black; } else if (black < white) { return enum_Reversi_Win_White; } else { return enum_Reversi_Draw; } } else { return enum_Reversi_Playing; } } EnumReversiPiecesType Reversi::GetCurrType() { return m_CurrType; } ReversiBitBoard& Reversi::GetMap() { return m_ReversiMap; }