diff --git a/GoBang.cpp b/GoBang.cpp new file mode 100644 index 0000000..ed93f48 --- /dev/null +++ b/GoBang.cpp @@ -0,0 +1,94 @@ + +// GoBang.cpp: 定义应用程序的类行为。 +// + +#include "pch.h" +#include "framework.h" +#include "GoBang.h" +#include "GoBangDlg.h" + +#ifdef _DEBUG +#define new DEBUG_NEW +#endif +#include + + +// CGoBangApp + +BEGIN_MESSAGE_MAP(CGoBangApp, CWinApp) + ON_COMMAND(ID_HELP, &CWinApp::OnHelp) +END_MESSAGE_MAP() + + +// CGoBangApp 构造 + +CGoBangApp::CGoBangApp() +{ + // TODO: 在此处添加构造代码, + // 将所有重要的初始化放置在 InitInstance 中 +} + + +// 唯一的 CGoBangApp 对象 + +CGoBangApp theApp; + + +// CGoBangApp 初始化 + +BOOL CGoBangApp::InitInstance() +{ + // 如果一个运行在 Windows XP 上的应用程序清单指定要 + // 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式, + //则需要 InitCommonControlsEx()。 否则,将无法创建窗口。 + INITCOMMONCONTROLSEX InitCtrls; + InitCtrls.dwSize = sizeof(InitCtrls); + // 将它设置为包括所有要在应用程序中使用的 + // 公共控件类。 + InitCtrls.dwICC = ICC_WIN95_CLASSES; + InitCommonControlsEx(&InitCtrls); + + CWinApp::InitInstance(); + + + AfxEnableControlContainer(); + + // 创建 shell 管理器,以防对话框包含 + // 任何 shell 树视图控件或 shell 列表视图控件。 + CShellManager *pShellManager = new CShellManager; + + // 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题 + CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows)); + + // 标准初始化 + // 如果未使用这些功能并希望减小 + // 最终可执行文件的大小,则应移除下列 + // 不需要的特定初始化例程 + // 更改用于存储设置的注册表项 + // TODO: 应适当修改该字符串, + // 例如修改为公司或组织名 + SetRegistryKey(_T("应用程序向导生成的本地应用程序")); + + CGoBangDlg dlg; + m_pMainWnd = &dlg; + INT_PTR nResponse = dlg.DoModal(); + if (nResponse == -1) + { + TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n"); + TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n"); + } + + // 删除上面创建的 shell 管理器。 + if (pShellManager != nullptr) + { + delete pShellManager; + } + +#if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS) + ControlBarCleanUp(); +#endif + + // 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序, + // 而不是启动应用程序的消息泵。 + return FALSE; +} \ No newline at end of file diff --git a/GoBang.h b/GoBang.h new file mode 100644 index 0000000..8d77e77 --- /dev/null +++ b/GoBang.h @@ -0,0 +1,31 @@ + +// GoBang.h: PROJECT_NAME 应用程序的主头文件 +// + +#pragma once + +#ifndef __AFXWIN_H__ + #error "在包含此文件之前包含 'pch.h' 以生成 PCH" +#endif + +#include "resource.h" // 主符号 + +// CGoBangApp: +// 有关此类的实现,请参阅 GoBang.cpp +// + +class CGoBangApp : public CWinApp +{ +public: + CGoBangApp(); + +// 重写 +public: + virtual BOOL InitInstance(); + +// 实现 + + DECLARE_MESSAGE_MAP() +}; + +extern CGoBangApp theApp; diff --git a/GoBang.rc b/GoBang.rc new file mode 100644 index 0000000..a200bef Binary files /dev/null and b/GoBang.rc differ diff --git a/GoBang.sln b/GoBang.sln new file mode 100644 index 0000000..d999c9a --- /dev/null +++ b/GoBang.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GoBang", "GoBang.vcxproj", "{90B8E532-0248-4484-B41C-7D82226F81E6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {90B8E532-0248-4484-B41C-7D82226F81E6}.Debug|x64.ActiveCfg = Debug|x64 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Debug|x64.Build.0 = Debug|x64 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Debug|x86.ActiveCfg = Debug|Win32 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Debug|x86.Build.0 = Debug|Win32 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Release|x64.ActiveCfg = Release|x64 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Release|x64.Build.0 = Release|x64 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Release|x86.ActiveCfg = Release|Win32 + {90B8E532-0248-4484-B41C-7D82226F81E6}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {385878C0-F06E-40E2-A6C9-98BBA25F8B03} + EndGlobalSection +EndGlobal diff --git a/GoBang.vcxproj b/GoBang.vcxproj new file mode 100644 index 0000000..ff46a12 --- /dev/null +++ b/GoBang.vcxproj @@ -0,0 +1,231 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {90B8E532-0248-4484-B41C-7D82226F81E6} + MFCProj + GoBang + 10.0 + + + + Application + true + v143 + Unicode + Dynamic + + + Application + false + v143 + true + Unicode + Dynamic + + + Application + true + v143 + Unicode + Dynamic + + + Application + false + v143 + true + Unicode + Dynamic + + + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + Use + Level3 + true + WIN32;_WINDOWS;_DEBUG;%(PreprocessorDefinitions) + pch.h + + + Windows + + + false + true + _DEBUG;%(PreprocessorDefinitions) + + + 0x0804 + _DEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + + + Use + Level3 + true + _WINDOWS;_DEBUG;%(PreprocessorDefinitions) + pch.h + + + Windows + + + false + true + _DEBUG;%(PreprocessorDefinitions) + + + 0x0804 + _DEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + + + Use + Level3 + true + true + true + WIN32;_WINDOWS;NDEBUG;%(PreprocessorDefinitions) + pch.h + + + Windows + true + true + + + false + true + NDEBUG;%(PreprocessorDefinitions) + + + 0x0804 + NDEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + + + Use + Level3 + true + true + true + _WINDOWS;NDEBUG;%(PreprocessorDefinitions) + pch.h + + + Windows + true + true + + + false + true + NDEBUG;%(PreprocessorDefinitions) + + + 0x0804 + NDEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + + + + + + + + + + + + + NotUsing + NotUsing + + + + Create + + + NotUsing + NotUsing + + + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GoBang.vcxproj.filters b/GoBang.vcxproj.filters new file mode 100644 index 0000000..647e209 --- /dev/null +++ b/GoBang.vcxproj.filters @@ -0,0 +1,85 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + + + 资源文件 + + + + + 资源文件 + + + 资源文件 + + + 资源文件 + + + + + + 资源文件 + + + 资源文件 + + + \ No newline at end of file diff --git a/GoBang.vcxproj.user b/GoBang.vcxproj.user new file mode 100644 index 0000000..f44c018 --- /dev/null +++ b/GoBang.vcxproj.user @@ -0,0 +1,7 @@ + + + + GoBang.rc + false + + \ No newline at end of file diff --git a/GoBangDlg.cpp b/GoBangDlg.cpp new file mode 100644 index 0000000..77ccf63 --- /dev/null +++ b/GoBangDlg.cpp @@ -0,0 +1,563 @@ + +// GoBangDlg.cpp: 实现文件 +// + +#include "pch.h" +#include "framework.h" +#include "GoBang.h" +#include "GoBangDlg.h" +#include "afxdialogex.h" +#include"resource.h" +#include + + + +#include +#include + +#ifdef _DEBUG +#define new DEBUG_NEW +#endif + + + + +// CGoBangDlg 对话框 + + +CGoBangDlg::CGoBangDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_GOBANG_DIALOG, pParent) +{ + m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); +} + +void CGoBangDlg::DoDataExchange(CDataExchange* pDX) +{ + IsPlaying = false; + for (int i = 0; i < SIZE; i++) + { + for (int j = 0; j < SIZE; j++) + { + ChessBoard[i][j] = -1; + } + }//初始化棋盘 + CDialogEx::DoDataExchange(pDX); +} + +BEGIN_MESSAGE_MAP(CGoBangDlg, CDialogEx) + ON_WM_PAINT() + ON_WM_QUERYDRAGICON() + ON_BN_CLICKED(IDC_START, &CGoBangDlg::OnBnClickedStart) + ON_BN_CLICKED(IDC_QUIT, &CGoBangDlg::OnBnClickedQuit) + ON_WM_LBUTTONUP() + ON_WM_SETCURSOR() + ON_WM_CLOSE() + ON_BN_CLICKED(IDC_ENDGAME, &CGoBangDlg::OnBnClickedEndgame) + ON_BN_CLICKED(IDC_REPENTANCE, &CGoBangDlg::OnBnClickedRepentance) + ON_BN_CLICKED(IDC_SAVE, &CGoBangDlg::OnBnClickedSave) + ON_BN_CLICKED(IDC_OPEN, &CGoBangDlg::OnBnClickedOpen) + ON_BN_CLICKED(IDC_BUTTON_AI, &CGoBangDlg::OnBnClickedButtonAi) +END_MESSAGE_MAP() + + +// CGoBangDlg 消息处理程序 + +BOOL CGoBangDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + + // 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动 + // 执行此操作 + SetIcon(m_hIcon, TRUE); // 设置大图标 + SetIcon(m_hIcon, FALSE); // 设置小图标 + + // TODO: 在此添加额外的初始化代码 + SetBackgroundImage(IDB_BACKGROUNDIMAGE); + CString filename = AfxGetApp()->m_lpCmdLine; + if (filename == L"") + return TRUE; + filename.Remove('\"'); + if (filename.Mid(filename.ReverseFind('.')) == ".gob") + OpenFile(filename); + return TRUE; // 除非将焦点设置到控件,否则返回 TRUE +} + +// 如果向对话框添加最小化按钮,则需要下面的代码 +// 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序, +// 这将由框架自动完成。 + +void CGoBangDlg::OnPaint() +{ + if (IsIconic()) + { + CPaintDC dc(this); // 用于绘制的设备上下文 + + SendMessage(WM_ICONERASEBKGND, reinterpret_cast(dc.GetSafeHdc()), 0); + + // 使图标在工作区矩形中居中 + int cxIcon = GetSystemMetrics(SM_CXICON); + int cyIcon = GetSystemMetrics(SM_CYICON); + CRect rect; + GetClientRect(&rect); + int x = (rect.Width() - cxIcon + 1) / 2; + int y = (rect.Height() - cyIcon + 1) / 2; + + // 绘制图标 + dc.DrawIcon(x, y, m_hIcon); + } + else + { + CPaintDC dc(this); + CPen pen(PS_SOLID, 2, RGB(0, 0, 0)); + dc.SelectObject(pen); + for (int i = 0; i < SIZE; i++) + { + dc.MoveTo(50, 50 + i * 50); + dc.LineTo(750, 50 + i * 50); + }//绘制棋盘横线 + for (int i = 0; i < SIZE; i++) + { + dc.MoveTo(50 + i * 50, 50); + dc.LineTo(50 + i * 50, 750); + }//绘制棋盘竖线 + for (int nx = 0; nx < SIZE; nx++) + { + for (int ny = 0; ny < SIZE; ny++) + { + + int color = GetChessBoardColor(nx, ny); + if (color == 0)//白棋 + { + CBrush brush_w(RGB(255, 255, 255)); + const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心 + dc.SelectObject(brush_w); + dc.Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15); + } + else if (color == 1)//黑棋 + { + CBrush brush_b(RGB(0, 0, 0)); + const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心 + dc.SelectObject(brush_b); + dc.Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15); + } + } + } + } +} + +//当用户拖动最小化窗口时系统调用此函数取得光标 +//显示。 +HCURSOR CGoBangDlg::OnQueryDragIcon() +{ + return static_cast(m_hIcon); +} + + + +void CGoBangDlg::OnBnClickedStart() +{ + GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE); + if (IsPlaying && MessageBoxW(L"确定要重玩吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDNO) + return; + GetDlgItem(IDC_START)->SetWindowTextW(L"重玩"); + if (IsPlaying) { + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(FALSE); + IsPlaying = true; + AIPlaying = false; + NowColor = 1;//黑先 + index = -1; + g.Reset(); + } + else if (AIPlaying) { + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(FALSE); + IsPlaying = false; + AIPlaying = true; + NowColor = 1;//黑先 + index = -1; + g.Reset(); + } + else { + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(FALSE); + IsPlaying = true; + AIPlaying = false; + NowColor = 1;//黑先 + index = -1; + g.Reset(); + } + + + + GetDlgItem(IDC_ENDGAME)->EnableWindow(TRUE); + GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE); + GetDlgItem(IDC_SAVE)->EnableWindow(TRUE); + CleanChessBoard(); +} + + +void CGoBangDlg::OnBnClickedQuit() +{ + g.Reset(); + if (!IsPlaying || MessageBoxW(L"正在游戏中,确定要退出吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES) + EndDialog(0); +} + +bool CGoBangDlg::AI_step() { + GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE); + int aiMove = ai.Search(&g); + int y = aiMove % 15; + int x = aiMove / 15; + if (GetChessBoardColor(x, y) != -1)//如果已有棋子 + { + return false; + } + SetChessBoardColor(x, y, NowColor); + index++; + order[index].x = x; + order[index].y = y; + g.PutChess(x * 15 + y); + + SendMessage(WM_SETCURSOR); + int winner = GetWinner(); + if (winner != -1 || index == (SIZE * SIZE - 1)) + { + if (winner == 0) + MessageBoxW(L"AI胜利!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + else if (winner == 1) + MessageBoxW(L"你赢了!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + else + MessageBoxW(L"平局!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + EndGame(); + return false; + } + NowColor = !NowColor; + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1); + return true; +} + +bool CGoBangDlg::Human_step(CPoint point) { + int x = int(round(point.x / 50.0) - 1); + int y = int(round(point.y / 50.0) - 1); + //将鼠标坐标转为数组下标 + if (GetChessBoardColor(x, y) != -1)//如果已有棋子 + return false; + SetChessBoardColor(x, y, NowColor); + index++; + order[index].x = x; + order[index].y = y; + g.PutChess(x * 15 + y); + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1); + //放置棋子 + SendMessage(WM_SETCURSOR); + int winner = GetWinner(); + if (winner != -1 || index == (SIZE * SIZE - 1)) + { + if (winner == 0) + MessageBoxW(L"白棋胜利!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + else if (winner == 1) + MessageBoxW(L"黑棋胜利!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + else + MessageBoxW(L"平局!", L"双人五子棋", MB_OK | MB_ICONINFORMATION); + EndGame(); + return false; + } + NowColor = (!NowColor); + return true; +} + +void CGoBangDlg::OnLButtonUp(UINT nFlags, CPoint point)// 鼠标事件相应 +{ + if (!(IsPlaying||AIPlaying) || point.x < 40 || point.x>760 || point.y < 40 || point.y>760) + return; + else if(IsPlaying){ + if (!Human_step(point))return; + } + else if(AIPlaying) { + if (!Human_step(point))return; + if (!AI_step())return; + //return; + } + +} + +int CGoBangDlg::GetChessBoardColor(int nx, int ny) +{ + return ChessBoard[ny][nx]; +} + +void CGoBangDlg::SetChessBoardColor(int nx, int ny, int color) +{ + ChessBoard[ny][nx] = color; + CDC* dc = this->GetDC(); + CPen pen(PS_SOLID, 2, RGB(0, 0, 0)); + dc->SelectObject(pen); + if (color == 0)//白棋 + { + CBrush brush_w(RGB(255, 255, 255)); + const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心 + dc->SelectObject(brush_w); + dc->Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15); + } + else if (color == 1)//黑棋 + { + CBrush brush_b(RGB(0, 0, 0)); + const CPoint o(50 * nx + 50, 50 * ny + 50);//圆心 + dc->SelectObject(brush_b); + dc->Ellipse(o.x - 15, o.y - 15, o.x + 15, o.y + 15); + } + else//清除该坐标棋子,需要重绘,用于悔棋 + { + Invalidate(); + } +} + +void CGoBangDlg::EndGame() +{ + CleanChessBoard(); + IsPlaying = false; + AIPlaying = false; + index = -1; + GetDlgItem(IDC_START)->SetWindowTextW(L"双人对战"); + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(TRUE); + GetDlgItem(IDC_ENDGAME)->EnableWindow(FALSE); + GetDlgItem(IDC_REPENTANCE)->EnableWindow(FALSE); + GetDlgItem(IDC_SAVE)->EnableWindow(FALSE); + +} + +void CGoBangDlg::CleanChessBoard() +{ + for (int i = 0; i < SIZE; i++) + { + for (int j = 0; j < SIZE; j++) + { + ChessBoard[i][j] = -1; + } + } + Invalidate(); +} + +void CGoBangDlg::OpenFile(CString filename) +{ + std::ifstream infile; + infile.open(CStringA(filename)); + if (!infile) + { + MessageBoxW(L"打开失败!", L"双人五子棋", MB_OK | MB_ICONERROR); + return; + } + for (int y = 0; y < 15; y++) + { + for (int x = 0; x < 15; x++) + { + int t; + infile >> t; + infile.seekg(infile.tellg().operator+(1)); + ChessBoard[y][x] = t; + } + } + Invalidate();//绘制棋盘和棋子 + infile >> NowColor; + infile.seekg(infile.tellg().operator+(1)); + infile >> index; + for (int i = 0; i <= index; i++) + { + infile.seekg(infile.tellg().operator+(1)); + infile >> order[i].x; + infile.seekg(infile.tellg().operator+(1)); + infile >> order[i].y; + } + infile.close(); + GetDlgItem(IDC_START)->SetWindowTextW(L"重玩"); + GetDlgItem(IDC_ENDGAME)->EnableWindow(TRUE); + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > 0); + GetDlgItem(IDC_SAVE)->EnableWindow(TRUE); + IsPlaying = true; +} + + + +int CGoBangDlg::GetChessCount(int nx, int ny) +{ + int color = GetChessBoardColor(nx, ny); + if (color == -1) + return -1; + int x = nx, y = ny; + int m_max, count; + while (--y >= 0 && GetChessBoardColor(x, y) == color); + y++; + for (count = 1; (++y < SIZE) && (GetChessBoardColor(x, y) == color); count++); + m_max = count; + x = nx, y = ny; + while (--x >= 0 && GetChessBoardColor(x, y) == color); + x++; + for (count = 1; ++x < SIZE && GetChessBoardColor(x, y) == color; count++); + if (m_max < count) + m_max = count; + x = nx, y = ny; + while (x - 1 >= 0 && y - 1 >= 0 && GetChessBoardColor(x - 1, y - 1) == color) + x--, y--; + for (count = 1; x + 1 < SIZE && y + 1 < SIZE && GetChessBoardColor(x + 1, y + 1) == color; count++) + x++, y++; + if (m_max < count) + m_max = count; + x = nx, y = ny; + while (x - 1 >= 0 && y + 1 < SIZE && GetChessBoardColor(x - 1, y + 1) == color) + x--, y++; + for (count = 1; x + 1 < SIZE && y - 1 >= 0 && GetChessBoardColor(x + 1, y - 1) == color; count++) + x++, y--; + if (m_max < count) + m_max = count; + return m_max; +} + + + +int CGoBangDlg::GetWinner() +{ + if (GetChessCount(order[index].x, order[index].y) >= 5) + return NowColor; + return -1; +} + + +BOOL CGoBangDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) +{ + POINT point; + GetCursorPos(&point); + ScreenToClient(&point); + if (!(IsPlaying || AIPlaying) || point.x < 40 || point.x>760 || point.y < 40 || point.y>760) + return CDialogEx::OnSetCursor(pWnd, nHitTest, message); + if (NowColor == 1)//黑棋 + SetCursor(LoadCursorW(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_BLACK))); + else + SetCursor(LoadCursorW(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_WHITE))); + return TRUE; +} + + +void CGoBangDlg::OnClose() +{ + if (!IsPlaying || MessageBoxW(L"正在游戏中,确定要退出吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES) + CDialogEx::OnClose(); + +} + + +void CGoBangDlg::OnBnClickedEndgame() +{ + if (MessageBoxW(L"确定要结束本局吗?", L"双人五子棋", MB_YESNO | MB_ICONQUESTION) == IDYES) + { + EndGame(); + GetDlgItem(IDC_START)->EnableWindow(TRUE); + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(TRUE); + g.Reset(); + } + +} + + +void CGoBangDlg::OnBnClickedRepentance() +{ + if (IsPlaying) { + g.Regret(1); + SetChessBoardColor(order[index].x, order[index].y, -1); + index--; + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1); + NowColor = (!NowColor); + } + if (AIPlaying) { + g.Regret(2); + SetChessBoardColor(order[index].x, order[index].y, -1); + index--; + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1); + NowColor = (!NowColor); + SetChessBoardColor(order[index].x, order[index].y, -1); + index--; + GetDlgItem(IDC_REPENTANCE)->EnableWindow(index > -1); + NowColor = (!NowColor); + } +} + + +void CGoBangDlg::OnBnClickedSave() +{ + CFileDialog filedlg(FALSE); + filedlg.m_ofn.lpstrFilter = L"五子棋文件(*.gob)\0*.gob\0\0"; + if (filedlg.DoModal() != IDOK) + return; + CString filename = filedlg.GetPathName(); + if (filedlg.GetFileExt() == L"") + filename += ".gob"; + std::ofstream outfile; + outfile.open(CStringA(filename)); + if(!outfile) + { + MessageBoxW(L"保存失败!", L"双人五子棋", MB_OK | MB_ICONERROR); + return; + } + for (int y = 0; y < 15; y++) + { + for (int x = 0; x < 15; x++) + { + outfile << GetChessBoardColor(x, y) << '\0'; + } + outfile << '\r'; + } + //输出ChessBoard数组 + outfile <EnableWindow(FALSE); + GetDlgItem(IDC_START)->SetWindowTextW(L"重玩"); + GetDlgItem(IDC_ENDGAME)->EnableWindow(TRUE); + GetDlgItem(IDC_SAVE)->EnableWindow(TRUE); + GetDlgItem(IDC_BUTTON_AI)->EnableWindow(FALSE); + IsPlaying = false; + AIPlaying = true; + NowColor = 1;//黑先 + index = -1; + g.Reset(); + srand((unsigned)time(NULL)); + + CleanChessBoard(); +} diff --git a/GoBangDlg.h b/GoBangDlg.h new file mode 100644 index 0000000..a7ee4e5 --- /dev/null +++ b/GoBangDlg.h @@ -0,0 +1,65 @@ + +// GoBangDlg.h: 头文件 +// + +#pragma once + +#define SIZE 15 + +#include "MCTS.h" +#include "game.h" + +// CGoBangDlg 对话框 +class CGoBangDlg : public CDialogEx +{ +// 构造 +public: + CGoBangDlg(CWnd* pParent = nullptr); // 标准构造函数 + +// 对话框数据 +#ifdef AFX_DESIGN_TIME + enum { IDD = IDD_GOBANG_DIALOG }; +#endif + +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 + bool IsPlaying = false; + bool AIPlaying = false; + bool NowColor; + int ChessBoard[SIZE][SIZE];//棋盘,-1为空,0为白,1为黑 + int index; + CPoint order[SIZE * SIZE]; + int GetChessBoardColor(int ,int); + void SetChessBoardColor(int ,int,int); + void EndGame(); + void CleanChessBoard(); + void OpenFile(CString filename); + int GetChessCount(int,int); + int GetWinner();//获取赢家,-1无,0白,1黑 + bool AI_step(); + bool Human_step(CPoint point); + MCTS ai; + Game g; + +// 实现 +protected: + HICON m_hIcon; + + // 生成的消息映射函数 + virtual BOOL OnInitDialog(); + afx_msg void OnPaint(); + afx_msg HCURSOR OnQueryDragIcon(); + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnBnClickedStart(); + afx_msg void OnBnClickedQuit(); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); + afx_msg void OnClose(); + afx_msg void OnBnClickedEndgame(); + afx_msg void OnBnClickedRepentance(); + afx_msg void OnBnClickedSave(); + afx_msg void OnBnClickedOpen(); + afx_msg void OnStartAIButtonClicked(); + afx_msg void OnBnClickedButtonAi(); +}; diff --git a/MCTS.h b/MCTS.h new file mode 100644 index 0000000..3685e84 --- /dev/null +++ b/MCTS.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include "game.h" + +const int THREAD_NUM_MAX = 32; + +class TreeNode +{ +public: + TreeNode(TreeNode *p); + void Clear(); + + int visit; + float value; + float winRate; + float expandFactor; + int validGridCount; + int gridLevel; + GameBase *game; + + TreeNode *parent; + list children; + array validGrids; +}; + +class MCTS +{ +public: + MCTS(int mode = 0); + ~MCTS(); + int Search(Game *state); + +private: + static void SearchThread(int id, int seed, MCTS *mcts, clock_t startTime); + + // standard MCTS process + TreeNode* TreePolicy(TreeNode *node); + TreeNode* ExpandTree(TreeNode *node); + TreeNode* BestChild(TreeNode *node, float c); + float DefaultPolicy(TreeNode *node, int id); + void UpdateValue(TreeNode *node, float value); + + // custom optimization + bool PreExpandTree(TreeNode *node); + + int CheckBook(GameBase *state); + + void ClearNodes(TreeNode *node); + float CalcScore(const TreeNode *node, float expandFactorParent_c); + + TreeNode* NewTreeNode(TreeNode *parent); + void RecycleTreeNode(TreeNode *node); + void ClearPool(); + + int maxDepth, fastStopSteps, fastStopCount; + GameBase gameCache[THREAD_NUM_MAX]; + list pool; + TreeNode *root; + int mode; +}; diff --git a/framework.h b/framework.h new file mode 100644 index 0000000..fac8b6c --- /dev/null +++ b/framework.h @@ -0,0 +1,49 @@ +#pragma once + +#ifndef VC_EXTRALEAN +#define VC_EXTRALEAN // 从 Windows 头中排除极少使用的资料 +#endif + +#include "targetver.h" + +#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // 某些 CString 构造函数将是显式的 + +// 关闭 MFC 的一些常见且经常可放心忽略的隐藏警告消息 +#define _AFX_ALL_WARNINGS + +#include // MFC 核心组件和标准组件 +#include // MFC 扩展 + + +#include // MFC 自动化类 + + + +#ifndef _AFX_NO_OLE_SUPPORT +#include // MFC 对 Internet Explorer 4 公共控件的支持 +#endif +#ifndef _AFX_NO_AFXCMN_SUPPORT +#include // MFC 对 Windows 公共控件的支持 +#endif // _AFX_NO_AFXCMN_SUPPORT + +#include // MFC 支持功能区和控制条 + + + + + + + + + +#ifdef _UNICODE +#if defined _M_IX86 +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") +#elif defined _M_X64 +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") +#else +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") +#endif +#endif + + diff --git a/game.cpp b/game.cpp new file mode 100644 index 0000000..f23b1ef --- /dev/null +++ b/game.cpp @@ -0,0 +1,1124 @@ +#include "game.h" +#include +#include +#include + + +#define max(a, b) ((a > b) ? a : b) +#define min(a, b) ((a < b) ? a : b) + + +#define USE_BEAUTIFUL_BOARD 1 +#define OUTPUT_LINE_SCORE_DICT 0 +#define OUTPUT_RESTRICTED_SCORE 0 +#define ENABLE_KEY_OPTIMIZATION 1 + +bool Board::RestrictedMoveRule = false; + +bool Board::isLineScoreDictReady = false; +array Board::lineScoreDict; + +bool Board::isKeyInfoOriginReady = false; +array, 4> Board::keyInfoOrigin; + +Board::Board() +{ + if (!isLineScoreDictReady) + InitLineScoreDict(); + + Clear(); +} + +void Board::Clear() +{ + + grids.fill(E_EMPTY); + scoreInfo[0].fill(0); + scoreInfo[1].fill(0); + gridCheckStatus.fill(E_PRIORITY_MAX); + + if (ENABLE_KEY_OPTIMIZATION) + { + InitKeyInfo(); + } +} + + +char Board::GetGrid(int row, int col) +{ + if (!IsValidCoord(row, col)) + return E_INVALID; + + return grids[Board::Coord2Id(row, col)]; +} + +bool Board::SetGrid(int row, int col, char value) +{ + if (!IsValidCoord(row, col)) + return false; + + grids[Board::Coord2Id(row, col)] = value; + return true; +} + +bool Board::IsWin(int id) +{ + int side = grids[id]; + int i0 = (side == E_BLACK) ? 0 : 1; + int score0 = scoreInfo[i0][id]; + + if (score0 >= FIVE_SCORE) + { + if (Board::RestrictedMoveRule && side == E_BLACK && IsRestrictedMove(score0)) + return false; + + return true; + } + + return false; +} + +bool Board::IsLose(int id) +{ + if (Board::RestrictedMoveRule && grids[id] == E_BLACK) + { + int score0 = scoreInfo[0][id]; + if (IsRestrictedMove(score0)) + return true; + } + return false; +} + +int Board::CalcBoardScore(int side) +{ + int i0 = (side == E_BLACK) ? 0 : 1; + + int boardScore = 0; + for (int i = 0; i < GRID_NUM; ++i) + { + if (grids[i] == E_EMPTY) + { + if (RestrictedMoveRule && side == E_BLACK) + { + if (Board::IsRestrictedMove(scoreInfo[i0][i])) + continue; + } + boardScore += scoreInfo[i0][i]; + } + } + return boardScore; +} + +void Board::InitLineScoreDict() +{ + FILE *fp; + + if (OUTPUT_RESTRICTED_SCORE) + { + fopen_s(&fp, "restricted_score.log", "w"); + for (int i = 0; i < 10000; ++i) + { + if (Board::IsRestrictedMove(i)) + { + fprintf(fp, "%d\n", i); + } + } + fclose(fp); + } + + char strmap[4] = { ' ', '@', 'O', 'X' }; + + if (OUTPUT_LINE_SCORE_DICT) + fopen_s(&fp, "line_dict.log", "w"); + + int maxId = pow(4, 9); + + for (int i = 0; i < maxId; ++i) + { + array line; + char lineStr[12]; + bool isValid = true; + + int key = i; + for (int j = 0; j < 9; ++j) + { + line[j] = key % 4; + key = key >> 2; + lineStr[j + 1] = strmap[line[j]]; + + if (j == 4 && (line[j] == E_INVALID || line[j] == E_EMPTY)) + { + isValid = false; + break; + } + } + lineStr[0] = lineStr[10] = '|'; + lineStr[11] = 0; + + int leftCount = 0; + for (int j = 0; j < 4; ++j) + { + if (line[j] != E_INVALID) + ++leftCount; + + if (line[j] == E_INVALID && leftCount > 0) + { + isValid = false; + break; + } + } + + int rightCount = 0; + for (int j = 8; j > 4; --j) + { + if (line[j] != E_INVALID) + ++rightCount; + + if (line[j] == E_INVALID && rightCount > 0) + { + isValid = false; + break; + } + } + + if (isValid) + { + short score = CalcLineScore(line); + lineScoreDict[i] = score; + + if (OUTPUT_LINE_SCORE_DICT && score > 0) + { + fprintf(fp, "%8d, %5d, %s\n", i, score, lineStr); + } + } + } + + if (OUTPUT_LINE_SCORE_DICT) + fclose(fp); + + isLineScoreDictReady = true; +} + +short Board::CalcLineScore(array line) +{ + int side = line[4]; + int otherSide = 3 - side; + + // continuous grids + int num1 = 0; + for (int i = 3; i >= 0; --i) + { + if (line[i] != side) + break; + + ++num1; + } + + int num2 = 0; + for (int i = 5; i <= 8; ++i) + { + if (line[i] != side) + break; + + ++num2; + } + + // continuous grid with 1 empty + bool isValid = false; + bool foundEmpty = false; + int num3 = 0; + for (int i = 3; i >= 0; --i) + { + if (line[i] == otherSide || line[i] == E_INVALID) + break; + + if (!foundEmpty) + { + if (line[i] == E_EMPTY) + foundEmpty = true; + } + else + { + if (line[i] == E_EMPTY) + break; + + if (line[i] == side) + isValid = true; + } + + ++num3; + } + if (!isValid) + num3 = 0; + + isValid = false; + foundEmpty = false; + int num4 = 0; + for (int i = 5; i <= 8; ++i) + { + if (line[i] == otherSide || line[i] == E_INVALID) + break; + + if (!foundEmpty) + { + if (line[i] == E_EMPTY) + foundEmpty = true; + } + else + { + if (line[i] == E_EMPTY) + break; + + if (line[i] == side) + isValid = true; + } + + ++num4; + } + if (!isValid) + num4 = 0; + + + // continuous grid or empty + int max1 = 0; + for (int i = 3; i >= 0; --i) + { + if (line[i] == otherSide || line[i] == E_INVALID) + break; + + ++max1; + } + + int max2 = 0; + for (int i = 5; i <= 8; ++i) + { + if (line[i] == otherSide || line[i] == E_INVALID) + break; + + ++max2; + } + + // calculate line score + int total1 = num1 + num2 + 1; + int total2 = max1 + max2 + 1; + + bool isOpen1 = max1 > num1; + bool isOpen2 = max2 > num2; + bool isTotalOpen = total2 > WIN_COUNT; + + int total3 = num1 + num4 + 1; + int total4 = num2 + num3 + 1; + int total5 = max(total3, total4); + + bool isOpen3 = max1 > num3; + bool isOpen4 = max2 > num4; + + int score1 = 0, score2 = 0; + + if (total2 >= WIN_COUNT) + { + // continuous situations + if (total1 >= WIN_COUNT) // continuous 5 + { + score1 = FIVE_SCORE; + + if (Board::RestrictedMoveRule && side == E_BLACK && total1 > WIN_COUNT) + return RESTRICTED_SCORE; + } + else if (total1 == WIN_COUNT - 1) + { + if (isOpen1 && isOpen2 && isTotalOpen) // open 4 + { + score1 = OPEN_FOUR_SCORE; + } + else // half-open 4 + { + score1 = CLOSE_FOUR_SCORE; + } + } + else if (total1 == WIN_COUNT - 2) + { + if (isOpen1 && isOpen2 && isTotalOpen) // open 3 + { + score1 = OPEN_THREE_SCORE; + } + else // half-open 3 + { + score1 = OTHER_SCORE; + } + } + else if (total1 == WIN_COUNT - 3) + { + if (isOpen1 && isOpen2 && isTotalOpen) // open 2 + { + score1 = OTHER_SCORE; + } + } + + // jump situations + if (total5 >= WIN_COUNT) + { + if (total3 >= WIN_COUNT && total4 >= WIN_COUNT) // 2 jump 4 + { + score2 = OPEN_FOUR_SCORE; + + if (Board::RestrictedMoveRule && side == E_BLACK) + return RESTRICTED_SCORE; + } + else // jump 4 + { + score2 = CLOSE_FOUR_SCORE; + } + } + else if (total5 == WIN_COUNT - 1) + { + if (total3 >= WIN_COUNT - 2 && isOpen1 && isOpen4 && isTotalOpen) + { + score2 = OPEN_THREE_SCORE; // jump 3 + } + else if (total4 >= WIN_COUNT - 2 && isOpen2 && isOpen3 && isTotalOpen) + { + score2 = OPEN_THREE_SCORE; // jump 3 + } + else + { + score2 = OTHER_SCORE; // half-open jump 3 + } + } + else if (total5 == WIN_COUNT - 2 && isTotalOpen) + { + if (total3 == WIN_COUNT - 2 && isOpen1 && isOpen4) + { + score2 = OTHER_SCORE; // jump 2 + } + else if (total4 == WIN_COUNT - 2 && isOpen2 && isOpen3) + { + score2 = OTHER_SCORE; // jump 2 + } + } + } + int score = max(score1, score2); + + return score; +} + + +void Board::UpdatScoreInfo(int id, int turn) +{ + int row0, col0; + Board::Id2Coord(id, row0, col0); + + int side = GetGrid(row0, col0); + int otherSide = 3 - side; + + int i0 = (side == E_BLACK) ? 0 : 1; // this side + int i1 = 1 - i0; // other side + + UpdateKeyInfo(row0, col0); + + for (int j = 0 ; j < 8; ++j) + { + int dx, dy; + Board::Direction2DxDy((ChessDirection)j, dx, dy); + + bool needUpdate0 = true, needUpdate1 = true; + int row = row0, col = col0; + for (int k = 1; k <= 4; ++k) + { + row += dy; col += dx; + + int chess = GetGrid(row, col); + + if (chess == E_EMPTY) + { + if (needUpdate0) + UpdateScore(row, col, row0, col0, (ChessDirection)j, side); + + if (needUpdate1) + UpdateScore(row, col, row0, col0, (ChessDirection)j, otherSide); + } + else if (chess == side) + { + needUpdate1 = false; // no need to update other side + } + else if (chess == otherSide) + { + needUpdate0 = false; // no need to udpate this side + } + + if (!needUpdate0 && !needUpdate1) + break; + } + } + + keyInfo[0] = keyInfo[1]; + + UpdateGridsInfo(i1); // grids info for next turn +} + +void Board::InitKeyInfo() +{ + if (!isKeyInfoOriginReady) + { + for (int i = 0; i < 4; ++i) + { + Board::keyInfoOrigin[i].fill(0); + } + + for (int i = 0; i < GRID_NUM; ++i) + { + int row, col; + Board::Id2Coord(i, row, col); + + for (int j = 0; j < 4; ++j) + { + int key = 0; + + int dx, dy; + Board::Direction2DxDy((ChessDirection)j, dx, dy); + + int row1 = row, col1 = col; + for (int k = 1; k <= 4; ++k) + { + row1 += dy; col1 += dx; + + int shift = 4 + k; + int value = GetGrid(row1, col1) << (shift * 2); + key += value; + } + + row1 = row; col1 = col; + for (int k = 1; k <= 4; ++k) + { + row1 -= dy; col1 -= dx; + + int shift = 4 - k; + int value = GetGrid(row1, col1) << (shift * 2); + key += value; + } + Board::keyInfoOrigin[j][i] = key; + } + } + isKeyInfoOriginReady = true; + } + keyInfo[0] = Board::keyInfoOrigin; + keyInfo[1] = Board::keyInfoOrigin; +} + +__declspec(noinline) +void Board::UpdateKeyInfo(int row, int col) +{ + int side = GetGrid(row, col); + + for (int j = 0; j < 8; ++j) + { + int keyGroup = (j < 4) ? j : 7 - j; + keyInfo[1][keyGroup][Board::Coord2Id(row, col)] += side << (4 * 2); + + int dx, dy; + Board::Direction2DxDy((ChessDirection)j, dx, dy); + + int row1 = row, col1 = col; + for (int k = 1; k <= 4; ++k) + { + row1 += dy; col1 += dx; + + if (Board::IsValidCoord(row1, col1)) + { + int shift = (j < 4) ? (4 - k) : (4 + k); + int value = side << (shift * 2); + keyInfo[1][keyGroup][Board::Coord2Id(row1, col1)] += value; + } + } + } +} + +void Board::UpdateScoreOpt(int row, int col, ChessDirection direction, int side) +{ + int id = Board::Coord2Id(row, col); + int keyGroup = (direction < 4) ? direction : 7 - direction; + int key = keyInfo[1][keyGroup][id] + (side << (4 * 2)); + int key0 = keyInfo[0][keyGroup][id] + (side << (4 * 2)); + + int lineScore = lineScoreDict[key]; + int lineScore0 = lineScoreDict[key0]; + + int i0 = (side == E_BLACK) ? 0 : 1; // this side + scoreInfo[i0][Board::Coord2Id(row, col)] += lineScore - lineScore0; +} + +__declspec(noinline) +void Board::UpdateScore(int row, int col, int rowX, int colX, ChessDirection direction, int side) +{ + if (ENABLE_KEY_OPTIMIZATION) + { + UpdateScoreOpt(row, col, direction, side); + return; + } + + int key = side << (4 * 2); + int key0 = key; + + int dx, dy; + direction = ChessDirection(7 - direction); + Board::Direction2DxDy(direction, dx, dy); + + int row1 = row, col1 = col; + for (int i = 1; i <= 4; ++i) + { + row1 -= dy; col1 -= dx; + + int value = GetGrid(row1, col1) << ((4 - i) * 2); + key += value; + + if (!(row1 == rowX && col1 == colX)) + key0 += value; + } + + row1 = row, col1 = col; + for (int i = 1; i <= 4; ++i) + { + row1 += dy; col1 += dx; + + int value = GetGrid(row1, col1) << ((4 + i) * 2); + key += value; + + if (!(row1 == rowX && col1 == colX)) + key0 += value; + } + + int lineScore0 = lineScoreDict[key0]; + int lineScore = lineScoreDict[key]; + + int i0 = (side == E_BLACK) ? 0 : 1; // this side + scoreInfo[i0][Board::Coord2Id(row, col)] += lineScore - lineScore0; +} + +void Board::FindOtherGrids(int i0, int id, GridType type) +{ + int side = (i0 == 0) ? E_BLACK : E_WHITE; + int otherSide = 3 - side; + + int row, col; + Board::Id2Coord(id, row, col); + + for (int d = 0; d < 4; ++d) + { + int dx, dy; + Board::Direction2DxDy((Board::ChessDirection)d, dx, dy); + + // calc origin key & line score + int key = side << ( 4 * 2); + int row1 = row, col1 = col; + for (int i = 0; i < 4; ++i) + { + row1 -= dy; col1 -= dx; + + int value = GetGrid(row1, col1) << ((3 - i) * 2); + key += value; + } + + row1 = row, col1 = col; + for (int i = 0; i < 4; ++i) + { + row1 += dy; col1 += dx; + + int value = GetGrid(row1, col1) << ((5 + i) * 2); + key += value; + } + int lineScore = lineScoreDict[key]; + + // check valid grids + row1 = row, col1 = col; + for (int i = 0; i < 4; ++i) + { + row1 -= dy; col1 -= dx; + + int chess = GetGrid(row1, col1); + + if (chess == E_INVALID || chess == otherSide) + break; + + if (chess == E_EMPTY) + { + int value = otherSide << ((3 - i) * 2); + int key1 = key + value; + int lineScore1 = lineScoreDict[key1]; + int newScore = scoreInfo[i0][id] + lineScore1 - lineScore; + + if (newScore < THREE_THREE_SCORE) + { + int id1 = Board::Coord2Id(row1, col1); + gridCheckStatus[id1] = min(gridCheckStatus[id1], type); + } + } + } + + row1 = row, col1 = col; + for (int i = 0; i < 4; ++i) + { + row1 += dy; col1 += dx; + + int chess = GetGrid(row1, col1); + + if (chess == E_INVALID || chess == otherSide) + break; + + if (chess == E_EMPTY) + { + int value = otherSide << ((5 + i) * 2); + int key1 = key + value; + int lineScore1 = lineScoreDict[key1]; + int newScore = scoreInfo[i0][id] + lineScore1 - lineScore; + + if (newScore < THREE_THREE_SCORE) + { + int id1 = Board::Coord2Id(row1, col1); + gridCheckStatus[id1] = min(gridCheckStatus[id1], type); + } + } + } + } +} + +__declspec(noinline) +void Board::UpdateGridsInfo(int i0) +{ + int i1 = 1 - i0; + + gridCheckStatus.fill(E_GRID_TYPE_MAX); + hasGridType.fill(false); + + for (int i = 0; i < GRID_NUM; ++i) + { + if (grids[i] != E_EMPTY) + continue; + + int score0 = scoreInfo[i0][i]; + int score1 = scoreInfo[i1][i]; + + if (Board::RestrictedMoveRule) + { + if (i1 == 0 && score1 >= THREE_THREE_SCORE && IsRestrictedMove(score1)) + { + score1 = 0; + + if (score0 < THREE_THREE_SCORE) // leave opponent's restricted move untouched if not neccessary + { + gridCheckStatus[i] = E_OTHER; + continue; + } + } + + if (i0 == 0 && score0 >= THREE_THREE_SCORE && IsRestrictedMove(score0)) + { + gridCheckStatus[i] = E_RESTRICTED; + continue; + } + } + + if (score0 >= THREE_THREE_SCORE || score1 >= THREE_THREE_SCORE) + { + if (score0 >= FIVE_SCORE) + { + gridCheckStatus[i] = E_FIVE; + hasGridType[E_FIVE] = true; + } + else if (score1 >= FIVE_SCORE) + { + gridCheckStatus[i] = E_COUNTER_FIVE; + hasGridType[E_COUNTER_FIVE] = true; + } + else if (score0 >= OPEN_FOUR_SCORE) + { + gridCheckStatus[i] = E_OPEN_FOUR; + hasGridType[E_OPEN_FOUR] = true; + } + else if (score0 >= FOUR_THREE_SCORE) + { + gridCheckStatus[i] = E_FOUR_THREE; + hasGridType[E_FOUR_THREE] = true; + } + else if (score0 >= CLOSE_FOUR_SCORE) + { + gridCheckStatus[i] = E_CLOSE_FOUR; + hasGridType[E_CLOSE_FOUR] = true; + } + else if (score1 >= OPEN_FOUR_SCORE) + { + gridCheckStatus[i] = E_COUNTER_OPEN_FOUR; + hasGridType[E_COUNTER_OPEN_FOUR] = true; + + FindOtherGrids(i1, i, E_COUNTER_OPEN_FOUR); // find other possible counter moves + } + else if (score1 >= FOUR_THREE_SCORE) + { + gridCheckStatus[i] = E_COUNTER_FOUR_THREE; + hasGridType[E_COUNTER_FOUR_THREE] = true; + + FindOtherGrids(i1, i, E_COUNTER_FOUR_THREE); // find other possible counter moves + } + else if (score0 >= THREE_THREE_SCORE) + { + gridCheckStatus[i] = E_THREE_THREE; + hasGridType[E_THREE_THREE] = true; + } + else //if (score1 >= THREE_THREE_SCORE) + { + gridCheckStatus[i] = E_COUNTER_THREE_THREE; + hasGridType[E_COUNTER_THREE_THREE] = true; + + FindOtherGrids(i1, i, E_COUNTER_THREE_THREE); // find other possible counter moves + } + } + else + { + if (score0 >= TWO_TWO_SCORE || score1 >= TWO_TWO_SCORE) + { + if (score0 >= OPEN_THREE_SCORE) + { + gridCheckStatus[i] = min(gridCheckStatus[i], E_OPEN_THREE); + } + else if (score1 >= OPEN_THREE_SCORE) + { + gridCheckStatus[i] = min(gridCheckStatus[i], E_COUNTER_OPEN_THREE); + } + else //if (score0 >= TWO_TWO_SCORE || score1 >= TWO_TWO_SCORE) + { + gridCheckStatus[i] = min(gridCheckStatus[i], E_TWO_TWO); + } + } + else + { + if (score0 > 0) + { + gridCheckStatus[i] = min(gridCheckStatus[i], E_OPEN_TWO); + } + else + { + gridCheckStatus[i] = min(gridCheckStatus[i], E_OTHER); + } + } + } + } + + int bestType = E_GRID_TYPE_MAX; + for (int i = 0; i < E_GRID_TYPE_MAX; ++i) + { + if (hasGridType[i]) + { + bestType = i; + break; + } + } + + keyGrid = 0xff; + hasPriority.fill(false); + + for (int i = 0; i < GRID_NUM; ++i) + { + int priority = E_PRIORITY_MAX; + + switch (gridCheckStatus[i]) + { + case E_FIVE: + case E_COUNTER_FIVE: + case E_OPEN_FOUR: + case E_FOUR_THREE: + if (bestType == gridCheckStatus[i]) + { + priority = E_HIGHEST; + keyGrid = i; + } + else + { + priority = E_HIGH; + } + break; + case E_CLOSE_FOUR: + priority = (bestType <= E_COUNTER_THREE_THREE) ? E_HIGH : E_MIDDLE; // try to win before opponent + break; + case E_COUNTER_OPEN_FOUR: + case E_COUNTER_FOUR_THREE: + priority = E_HIGH; + break; + case E_THREE_THREE: + case E_COUNTER_THREE_THREE: + priority = (bestType <= E_COUNTER_FOUR_THREE) ? E_MIDDLE : E_HIGH; // counter 4 + 3 first + break; + case E_OPEN_THREE: + priority = (bestType == E_COUNTER_THREE_THREE) ? E_HIGH : E_MIDDLE; // try to win before opponent + break; + case E_COUNTER_OPEN_THREE: + case E_TWO_TWO: + priority = E_LOW; + break; + case E_OPEN_TWO: + priority = E_LOW; + break; + case E_OTHER: + case E_RESTRICTED: + priority = E_LOWEST; + break; + } + + gridCheckStatus[i] = priority; + hasPriority[priority] = true; + } +} + +__declspec(noinline) +bool Board::IsRestrictedMove(int score) +{ + if (score >= RESTRICTED_SCORE) + { + if (score % RESTRICTED_SCORE >= FIVE_SCORE) + return false; // five has higher priority than restricted move + else + return true; + } + + if (score >= FIVE_SCORE) // five has higher priority than restricted move + return false; + + if (score >= THREE_THREE_SCORE) + { + if (score >= CLOSE_FOUR_SCORE + OPEN_THREE_SCORE && score < CLOSE_FOUR_SCORE * 2) // 4 + 3 + return false; + + if (score >= OPEN_FOUR_SCORE && score < OPEN_FOUR_SCORE + CLOSE_FOUR_SCORE) // 4 or 4 + 3 + return false; + + return true; + } + + return false; +} + +void Board::GetGridsByPriority(ChessPriority priority, array &result, int &count) +{ + count = 0; + for (int i = 0; i < GRID_NUM; ++i) + { + if (gridCheckStatus[i] == priority) + { + result[count++] = i; + } + } +} + +int Board::Coord2Id(int row, int col) +{ + return row * BOARD_SIZE + col; +} + +void Board::Id2Coord(int id, int &row, int &col) +{ + row = id / BOARD_SIZE; + col = id % BOARD_SIZE; +} + +bool Board::IsValidCoord(int row, int col) +{ + return (0 <= row && row < BOARD_SIZE && 0 <= col && col < BOARD_SIZE); +} + +void Board::Direction2DxDy(ChessDirection direction, int &dx, int &dy) +{ + dx = dy = 0; + + switch (direction) + { + case E_LEFT: + dx = -1; + break; + case E_RIGHT: + dx = 1; + break; + case E_UP: + dy = -1; + break; + case E_DOWN: + dy = 1; + break; + case E_UP_LEFT: + dx = dy = -1; + break; + case E_DOWN_RIGHT: + dx = dy = 1; + break; + case E_UP_RIGHT: + dx = 1; dy = -1; + break; + case E_DOWN_LEFT: + dx = -1; dy = 1; + break; + } +} + +int Board::CalcDistance(int id1, int id2) +{ + int row1, col1, row2, col2; + Board::Id2Coord(id1, row1, col1); + Board::Id2Coord(id2, row2, col2); + + int dx = abs(col1 - col2); + int dy = abs(row1 - row2); + return max(dx, dy); +} +/////////////////////////////////////////////////////////////////////// + +GameBase::GameBase() +{ + Init(); +} + +void GameBase::Init() +{ + turn = 1; + lastMove = -1; + board.Clear(); + state = E_NORMAL; + + validGridCount = GRID_NUM; + for (int i = 0; i < GRID_NUM; ++i) + { + validGrids[i] = i; + } +} + +bool GameBase::PutChess(int id) +{ + if (state != E_NORMAL || board.grids[id] != Board::E_EMPTY) + return false; + + int side = GetSide(); + board.grids[id] = side; + board.UpdatScoreInfo(id, turn); + + lastMove = id; + + UpdateValidGrids(); + ++turn; + + if (board.IsWin(lastMove)) + state = (side == Board::E_BLACK) ? E_BLACK_WIN : E_WHITE_WIN; + + if (side == Board::E_BLACK && board.IsLose(lastMove)) // lose due to restricted move + state = E_WHITE_WIN; + + if (turn > GRID_NUM) + state = E_DRAW_; + + return true; +} + +void GameBase::UpdateValidGrids() +{ + if (board.keyGrid != 0xff) + { + validGrids[0] = board.keyGrid; + validGridCount = 1; + return; + } + + for (int i = Board::E_HIGH; i < Board::E_PRIORITY_MAX; ++i) + { + if (board.hasPriority[i]) + { + board.GetGridsByPriority((Board::ChessPriority)i, validGrids, validGridCount); + break; + } + } +} + +bool GameBase::UpdateValidGridsExtra() +{ + if (board.keyGrid == 0xff) + { + if (!board.hasPriority[Board::E_HIGH] && board.hasPriority[Board::E_MIDDLE] && board.hasPriority[Board::E_LOW]) + { + board.GetGridsByPriority(Board::E_LOW, validGrids, validGridCount); + return true; + } + } + return false; +} + +int GameBase::GetSide() +{ + return (turn % 2 == 1) ? Board::E_BLACK : Board::E_WHITE; +} + +int GameBase::GetNextMove() +{ + int id = rand() % validGridCount; + return validGrids[id]; +} + +int GameBase::CalcBetterSide() +{ + int otherSide = 3 - GetSide(); + int score0 = board.CalcBoardScore(GetSide()); + int score1 = board.CalcBoardScore(otherSide); + + return (score0 > score1) ? GetSide() : otherSide; +} + + +bool Game::PutChess(int Id) +{ + if (GameBase::PutChess(Id)) + { + record.push_back(lastMove); + return true; + } + return false; +} + +void Game::Regret(int step) +{ + while (!record.empty() && --step >= 0) + { + record.pop_back(); + } + RebuildBoard(); +} + +void Game::Reset() +{ + GameBase::Init(); + record.clear(); +} + +void Game::RebuildBoard() +{ + GameBase::Init(); + + for (int i = 0; i < record.size(); ++i) + { + GameBase::PutChess(record[i]); + } +} + + + +int Game::Str2Id(const string &str) +{ + int col = str[0] - 'A'; + int row = str[1] <= '9' ? str[1] - '1' : str[1] - 'a' + 9; + if (!Board::IsValidCoord(row, col)) + return -1; + + int id = Board::Coord2Id(row, col); + return id; +} + +string Game::Id2Str(int id) +{ + int row, col; + Board::Id2Coord(id, row, col); + string result(1, col + 'A'); + result += (row < 9) ? row + '1' : row + 'a' - 9; + return result; +} diff --git a/game.h b/game.h new file mode 100644 index 0000000..3b39bd3 --- /dev/null +++ b/game.h @@ -0,0 +1,185 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#pragma warning (disable:4244) +#pragma warning (disable:4018) + +using namespace std; + +const int BOARD_SIZE = 15; +const int WIN_COUNT = 5; +const int GRID_NUM = BOARD_SIZE * BOARD_SIZE; + +const int FIVE_SCORE = 1000; // continuous 5 +const int OPEN_FOUR_SCORE = 250; // open 4 +const int CLOSE_FOUR_SCORE = 60; // half-open 4, jump 4 +const int OPEN_THREE_SCORE = 50; // open 3, jump 3 +const int OTHER_SCORE = 3; // half-open 3 or open 2 or jump 2 +const int RESTRICTED_SCORE = 5000; // restricted moves + +const int FOUR_THREE_SCORE = OPEN_THREE_SCORE + CLOSE_FOUR_SCORE; // 3 + 4, 4 + 4 +const int THREE_THREE_SCORE = OPEN_THREE_SCORE * 2; // 3 + 3, 3 + 4, 4 + 4 +const int TWO_TWO_SCORE = OTHER_SCORE * 2; + +const int LINE_ID_MAX = 262144; // 4 ^ 9 + +class Board +{ +public: + enum Chess + { + E_EMPTY = 0, + E_BLACK, + E_WHITE, + E_INVALID, + }; + + enum ChessDirection + { + E_LEFT = 0, + E_UP, + E_UP_LEFT, + E_UP_RIGHT, + E_DOWN_LEFT, + E_DOWN_RIGHT, + E_DOWN, + E_RIGHT, + }; + + enum GridType + { + E_RESTRICTED = -1, + E_FIVE = 0, + E_COUNTER_FIVE, + E_OPEN_FOUR, + E_FOUR_THREE, + E_CLOSE_FOUR, + E_COUNTER_OPEN_FOUR, + E_COUNTER_FOUR_THREE, + E_THREE_THREE, + E_COUNTER_THREE_THREE, + E_OPEN_THREE, + E_COUNTER_OPEN_THREE, + E_TWO_TWO, + E_OPEN_TWO, + E_OTHER, + E_GRID_TYPE_MAX, + }; + + enum ChessPriority + { + E_HIGHEST = 0, + E_HIGH = 1, + E_MIDDLE = 2, + E_LOW = 3, + E_LOWEST = 4, + E_PRIORITY_MAX, + }; + + Board(); + + void Clear(); + + + bool IsWin(int id); + bool IsLose(int id); + void GetGridsByPriority(ChessPriority priority, array &result, int &count); + int CalcBoardScore(int side); + + void UpdatScoreInfo(int id, int turn); + + static int Coord2Id(int row, int col); + static void Id2Coord(int id, int &row, int &col); + static bool IsValidCoord(int row, int col); + static void Direction2DxDy(ChessDirection direction, int &dx, int &dy); + static int CalcDistance(int id1, int id2); + + static int hitCount; + static int totalCount; + + uint8_t keyGrid; + array grids; + array scoreInfo[2]; + array, 4> keyInfo[2]; + array gridCheckStatus; + array hasGridType; + array hasPriority; + +private: + char GetGrid(int row, int col); + bool SetGrid(int row, int col, char value); + + void InitKeyInfo(); + void UpdateKeyInfo(int row, int col); + void UpdateScore(int row, int col, int rowX, int colX, ChessDirection direction, int side); + void UpdateScoreOpt(int row, int col, ChessDirection direction, int side); + void UpdateGridsInfo(int i0); + void FindOtherGrids(int i0, int id, GridType type); + + static bool RestrictedMoveRule; + static bool IsRestrictedMove(int id); + + static void InitLineScoreDict(); + static short CalcLineScore(array line); + + static array lineScoreDict; + static bool isLineScoreDictReady; + + static array, 4> keyInfoOrigin; + static bool isKeyInfoOriginReady; +}; + +class GameBase +{ +public: + enum State + { + E_NORMAL, + E_BLACK_WIN, + E_WHITE_WIN, + E_DRAW_, + }; + + GameBase(); + void Init(); + bool PutChess(int id); + int GetSide(); + void UpdateValidGrids(); + bool UpdateValidGridsExtra(); + int GetNextMove(); + int CalcBetterSide(); + + Board board; + int state; + int turn; + int lastMove; + int validGridCount; + array validGrids; +}; + +class Game : private GameBase +{ +public: + + int GetState() { return state; } + int GetTurn() { return turn; } + + bool PutChess(int id); + void Regret(int step = 2); + void Reset(); + + const vector& GetRecord() { return record; } + static int Str2Id(const string &str); + static string Id2Str(int id); + +private: + void RebuildBoard(); + + vector record; +}; + diff --git a/img/alg1.png b/img/alg1.png new file mode 100644 index 0000000..ba38b1e Binary files /dev/null and b/img/alg1.png differ diff --git a/img/alg2.png b/img/alg2.png new file mode 100644 index 0000000..08be23a Binary files /dev/null and b/img/alg2.png differ diff --git a/img/drawboard.png b/img/drawboard.png new file mode 100644 index 0000000..b93d7aa Binary files /dev/null and b/img/drawboard.png differ diff --git a/mcts.cpp b/mcts.cpp new file mode 100644 index 0000000..7aa7290 --- /dev/null +++ b/mcts.cpp @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include +#include "mcts.h" + + +const float Cp = 2.0f; // Cp in UCT +const float SEARCH_TIME = 1.0f; // Maximum searching time +const int EXPAND_THRESHOLD = 3; // How low the time that a node is visited to be expanded +const bool ENABLE_MULTI_THREAD = true; // whether use multithread +const float FAST_STOP_THRESHOLD = 0.1f; +const float FAST_STOP_BRANCH_FACTOR = 0.01f; +// if visited too many times, try more nodes. +const bool ENABLE_TRY_MORE_NODE = true; +const int TRY_MORE_NODE_THRESHOLD = 1000; + +TreeNode::TreeNode(TreeNode *p) +{ + visit = 0; + value = 0; + winRate = 0; + expandFactor = 0; + validGridCount = 0; + gridLevel = 0; + game = NULL; + parent = p; +} + +FILE *fp; + +MCTS::MCTS(int mode) +{ + this->mode = mode; + + root = NULL; + +} + +MCTS::~MCTS() +{ + ClearPool(); +} + +mutex mtx; + +void MCTS::SearchThread(int id, int seed, MCTS *mcts, clock_t startTime) +{ + srand(seed); + float elapsedTime = 0; + + while (1) + { + mtx.lock(); + TreeNode *node = mcts->TreePolicy(mcts->root); + mtx.unlock(); + + float value = mcts->DefaultPolicy(node, id); + + mtx.lock(); + mcts->UpdateValue(node, value); + mtx.unlock(); + + elapsedTime = float(clock() - startTime) / 1000; + if (elapsedTime > SEARCH_TIME) + { + mtx.lock(); + TreeNode *mostVisit = *max_element(mcts->root->children.begin(), mcts->root->children.end(), [](const TreeNode *a, const TreeNode *b) + { + return a->visit < b->visit; + }); + + TreeNode *bestScore = mcts->BestChild(mcts->root, 0); + mtx.unlock(); + + if (mostVisit == bestScore) + break; + } + } +} + +int MCTS::Search(Game *state) +{ + int move = CheckBook((GameBase*)state); + if (move != -1) + { + return move; + } + + fastStopSteps = 0; + fastStopCount = 0; + + root = NewTreeNode(NULL); + *(root->game) = *((GameBase*)state); + root->validGridCount = root->game->validGridCount; + root->validGrids = root->game->validGrids; + + clock_t startTime = clock(); + + thread threads[THREAD_NUM_MAX]; + int thread_num = ENABLE_MULTI_THREAD ? thread::hardware_concurrency() : 1; + + for (int i = 0; i < thread_num; ++i) + threads[i] = thread(SearchThread, i, rand(), this, startTime); + + for (int i = 0; i < thread_num; ++i) + threads[i].join(); + + TreeNode *best = BestChild(root, 0); + move = best->game->lastMove; + + maxDepth = 0; + + + ClearNodes(root); + + //printf("the final move is %d .\n", move); + return move; +} + +TreeNode* MCTS::TreePolicy(TreeNode *node) +{ + while (node->game->state == GameBase::E_NORMAL) + { + if (node->visit < EXPAND_THRESHOLD) + return node; + + if (PreExpandTree(node)) + return ExpandTree(node); + else + node = BestChild(node, Cp); + } + return node; +} + +bool MCTS::PreExpandTree(TreeNode *node) +{ + if (node->validGridCount > 0) + { + int id = rand() % node->validGridCount; + swap(node->validGrids[id], node->validGrids[node->validGridCount - 1]); + } + else + { + // try grids with lower priority after certain visits + if (ENABLE_TRY_MORE_NODE && node->gridLevel == 0 && node->visit > TRY_MORE_NODE_THRESHOLD * node->children.size()) + { + if (node->game->UpdateValidGridsExtra()) + { + node->gridLevel++; + node->validGrids = node->game->validGrids; + node->validGridCount = node->game->validGridCount; + + int id = rand() % node->validGridCount; + swap(node->validGrids[id], node->validGrids[node->validGridCount - 1]); + } + } + } + return node->validGridCount > 0; +} + +TreeNode* MCTS::ExpandTree(TreeNode *node) +{ + int move = node->validGrids[node->validGridCount - 1]; + --(node->validGridCount); + + TreeNode *newNode = NewTreeNode(node); + node->children.push_back(newNode); + *(newNode->game) = *(node->game); + newNode->game->PutChess(move); + newNode->validGridCount = newNode->game->validGridCount; + newNode->validGrids = newNode->game->validGrids; + + return newNode; +} + +TreeNode* MCTS::BestChild(TreeNode *node, float c) +{ + TreeNode *result = NULL; + float bestScore = -1; + float expandFactorParent_c = sqrtf(logf(node->visit)) * c; + + for (auto child : node->children) + { + float score = CalcScore(child, expandFactorParent_c); + if (score > bestScore) + { + bestScore = score; + result = child; + } + } + return result; +} + + +float MCTS::CalcScore(const TreeNode *node, float expandFactorParent_c) +{ + return node->winRate + node->expandFactor * expandFactorParent_c; +} + +float MCTS::DefaultPolicy(TreeNode *node, int id) +{ + gameCache[id] = *(node->game); + + float weight = 1.0f; + while (gameCache[id].state == GameBase::E_NORMAL) + { + float factor = (1 - FAST_STOP_BRANCH_FACTOR * gameCache[id].validGridCount); + weight *= max(factor, 0.5f); + + int move = gameCache[id].GetNextMove(); + gameCache[id].PutChess(move); + + if (weight < FAST_STOP_THRESHOLD) + { + fastStopCount++; + fastStopSteps += gameCache[id].turn - node->game->turn; + + int betterSide = gameCache[id].CalcBetterSide(); + gameCache[id].state = betterSide; // let better side win + } + } + float value = (gameCache[id].state == root->game->GetSide()) ? 1.f : 0; + value = (value - 0.5f) * weight + 0.5f; + + return value; +} + +void MCTS::UpdateValue(TreeNode *node, float value) +{ + while (node != NULL) + { + node->visit++; + node->value += value; + + node->expandFactor = sqrtf(1.f / node->visit); + node->winRate = node->value / node->visit; + + if (node->game->GetSide() == root->game->GetSide()) // win rate of opponent + node->winRate = 1 - node->winRate; + + node = node->parent; + } +} + +void MCTS::ClearNodes(TreeNode *node) +{ + if (node != NULL) + { + for (auto child : node->children) + { + ClearNodes(child); + } + + RecycleTreeNode(node); + } +} + +TreeNode* MCTS::NewTreeNode(TreeNode *parent) +{ + if (pool.empty()) + { + TreeNode *node = new TreeNode(parent); + node->game = new GameBase(); + return node; + } + + TreeNode *node = pool.back(); + node->parent = parent; + pool.pop_back(); + + return node; +} + +void MCTS::RecycleTreeNode(TreeNode *node) +{ + node->parent = NULL; + node->visit = 0; + node->value = 0; + node->winRate = 0; + node->expandFactor = 0; + node->validGridCount = 0; + node->gridLevel = 0; + node->children.clear(); + + pool.push_back(node); +} + +void MCTS::ClearPool() +{ + for (auto node : pool) + { + delete node->game; + delete node; + } +} + + +int MCTS::CheckBook(GameBase *state) +{ + int centerId = Game::Str2Id("H8"); + + if (state->turn == 1) + return centerId; + + if (state->turn == 2 && Board::CalcDistance(state->lastMove, centerId) <= 3) + { + int id = state->GetNextMove(); + while (Board::CalcDistance(state->lastMove, id) > 1) + id = state->GetNextMove(); + + return id; + } + + return -1; +} diff --git a/pch.cpp b/pch.cpp new file mode 100644 index 0000000..db1a479 --- /dev/null +++ b/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: 与预编译标头对应的源文件 + +#include "pch.h" + +// 当使用预编译的头时,需要使用此源文件,编译才能成功。 diff --git a/pch.h b/pch.h new file mode 100644 index 0000000..aa4549e --- /dev/null +++ b/pch.h @@ -0,0 +1,13 @@ +// pch.h: 这是预编译标头文件。 +// 下方列出的文件仅编译一次,提高了将来生成的生成性能。 +// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。 +// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。 +// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。 + +#ifndef PCH_H +#define PCH_H + +// 添加要在此处预编译的标头 +#include "framework.h" + +#endif //PCH_H diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fc3320f --- /dev/null +++ b/readme.md @@ -0,0 +1,386 @@ +# 项目说明 + +## 成员及分工 +王思图:后端,接口设计,联合调试 + +张镕州:前端 + +## 程序功能 +本程序实现了基于MFC的图形化五子棋程序,支持人人对弈和人机对弈,同时有保存棋局的功能。 + + +# 具体实现 +## 游戏基础知识 +参见 http://game.onegreen.net/wzq/HTML/142336.html + +根据blog中所写,将上述棋型的得分设置如下 +```cpp +const int FIVE_SCORE = 1000; // continuous 5 +const int OPEN_FOUR_SCORE = 250; // open 4 +const int CLOSE_FOUR_SCORE = 60; // half-open 4, jump 4 +const int OPEN_THREE_SCORE = 50; // open 3, jump 3 +const int OTHER_SCORE = 3; // half-open 3 or open 2 or jump 2 +``` +## 算法原理 + +### MCTS + + 蒙特卡洛树搜索(Monte Carlo tree search;MCTS)是一种用于某些决策过程的启发式搜索算法,最引人注目的是在游戏中的使用。一个主要例子是电脑围棋程序,它也用于其他棋盘游戏、即时电子游戏以及不确定性游戏。 + +![pic](img/alg1.png) + +- 选择(selection):根据当前获得所有子步骤的统计结果,选择一个最优的子步骤。从根结点 R 开始,选择连续的子结点向下至叶子结点 L 。一般而言,让游戏树向最优的方向扩展,这是蒙特卡洛树搜索的精要所在。 +- 扩展(expansion):在当前获得的统计结果不足以计算出下一个步骤时,随机选择一个子步骤。除非任意一方的输赢使得游戏在 L 结束,否则创建一个或多个子结点并选取其中一个结点 C。 +- 模拟(simulation):模拟游戏,进入下一步。在从结点C开始,用随机策略进行游戏,又称为playout或者rollout。 +- 反向传播(Back-Propagation):根据游戏结束的结果,计算对应路径上统计记录的值。使用随机游戏的结果,更新从C到R的路径上的结点信息。 +- 决策(decision):当到了一定的迭代次数或者时间之后结束,选择根节点下最好的子节点作为本次决策的结果。 + + +在开始阶段,搜索树只有一个节点,也就是我们需要决策的局面。 + +搜索树中的每一个节点包含了三个基本信息:代表的局面,被访问的次数,累计评分。 + +#### 选择 + +在选择阶段,需要从根节点,也就是要做决策的局面 R 出发向下选择出一个最急迫需要被拓展的节点 N,局面 R 是每一次迭代中第一个被检查的节点; + +对于被检查的局面而言,他可能有三种可能: + +1. 该节点所有可行动作(即所有子节点)都已经被拓展过 +2. 该节点有可行动作(还有子节点)还未被拓展过 +3. 这个节点游戏已经结束了(例如已经连成五子的局面) + +对于这三种可能: + +1. 如果所有可行动作都已经被拓展过,即所有子节点都有了战绩,那么我们将使用 UCB 公式计算该节点所有子节点的 UCB 值,并找到值最大的一个子节点继续向下迭代。 +2. 如果被检查的节点 A 依然存在没有被拓展的子节点 B (也即还有战绩为 0/0 的节点),那么我们认为 A 节点就是本次迭代的的目标节点,紧接着对 A 进行扩展。 +3. 如果被检查到的节点是一个游戏已经结束的节点。那么从该节点直接记录战绩,并且反向传播。 + +##### 优化(!!!重要) +使用**游戏基础知识**中所述的局面评估方法,以及棋子位置等信息计算局面的评估值,排序可选操作,调节某个节点被选择的概率。 + +如果不加优化,仅适用纯MCTS,会导致结果极差,看起来像乱下。推测原因是15*15的棋盘导致搜索空间奇大,如果仅按UCB选择每个操作基本只能模拟6-7次,非常劣。因此引入手搓的局面评估函数获得先验概率后,再按照先验概率结合UCB进行选择。此方法取自AlpahGo的做法,只不过AlphaGo的评估函数是炼丹炼的,这个是手搓的。 + +![pic](img/alg2.png) + +Board类的茫茫多方法都和计算这个局面评估值相关。函数实现翻译了Git上一个项目的Python代码。 + +https://github.com/marblexu/PythonGobang + +#### 扩展 + +在选择阶段结束时候,我们查找到了一个最迫切被拓展的节点 N,以及他一个尚未拓展的动作 A。在搜索树中创建一个新的节点 $N_A$ 作为N的一个新子节点,$N_A$ 的局面就是节点 N 在执行了动作 A 之后的局面。 + + +#### 模拟 + +为了让 $N_A$ 得到评分,我们从 $N_A$ 开始,让游戏随机进行,直到得到一个游戏结局,这个结局将作为 $N_A$ 的初始战绩,采用 $\frac{胜场}{总次数}$来记录。 + +#### 反向传播 + + 在 $N_A$ 的模拟结束之后,它的父节点 n 以及从根节点到 N 的路径上的所有节点都会根据本次模拟的结果来添加自己的累计评分,注意评分具有交替性。如果在选择阶段直接造成了游戏结局,则跳过模拟,根据该结局来更新评分。 + +#### 决策 + + 每一次迭代都会拓展搜索树,随着迭代次数的增加,搜索树的规模也不断增加。当到了一定的迭代次数或者时间之后结束,选择根节点下最好的子节点作为本次决策的结果。本项目采用持续模拟1秒的方法。 + + + +### 多线程 +采用多线程搜索 +参考 https://blog.csdn.net/QLeelq/article/details/115747717 + +https://blog.csdn.net/gcs_20210916/article/details/128411700 + +## 基于对话框的MFC程序 +### 创建对话框 + 在资源视图中依此单击“+”号,展开各个相关,找到Dialog。 + + + 修改给定的对话框: + 在对话框属性中找到描述文字,修改为“五子棋游戏”。 + 在工具箱中找到Button控件,根据游戏功能需求拖入合适位置,调整间距,大小,修改名称为“双人对战”、“AI对战”、“保存棋局”、“打开棋局”、“悔棋”、“结束本局”、“退出”。 + + 如下图所示: +![pic](img/drawboard.png) +### 生成消息响应函数 +双击各个控件,在GoBandDlg.h和GoBandDlg.cpp中会自动生成可编辑的函数。 + + +## 代码结构 +### GoBandDlg +实现了MFC对话框 + +1. CGoBangDlg类:对话框类,在程序开始时自动实例化 + +- afx_msg void OnPaint():绘图函数,实现棋盘绘制和黑白棋子的绘制。 + +- afx_msg void OnBnClickedStart():与双人对战控件对应,点击后开始游戏,在游戏进行时为重开按键 + +- afx_msg void OnBnClickedQuit():与退出控件对应,点击后退出游戏,且在对局未结束时加入提醒。 + +- afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message):设置光标函数,在不同轮次中显示相应的黑白光标 + +- afx_msg void OnBnClickedRepentance():与悔棋控件对应,点击后返回上一步的棋盘状态。 + +- afx_msg void OnBnClickedSave():与保存棋局控件对应,可将未结束的对局以gob文件的形式保存下来。 + +- afx_msg void OnBnClickedOpen():与打开棋局控件对应,加载保存下来的棋局继续对战。 +```cpp +#define SIZE 15 + +#include "MCTS.h" +#include "game.h" + +class CGoBangDlg : public CDialogEx +{ +public: + CGoBangDlg(CWnd* pParent = nullptr); + +#ifdef AFX_DESIGN_TIME + enum { IDD = IDD_GOBANG_DIALOG }; +#endif + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + bool IsPlaying = false; + bool AIPlaying = false; + bool NowColor; + int ChessBoard[SIZE][SIZE]; + int index; + CPoint order[SIZE * SIZE]; + int GetChessBoardColor(int ,int); + void SetChessBoardColor(int ,int,int); + void EndGame(); + void CleanChessBoard(); + void OpenFile(CString filename); + int GetChessCount(int,int); + int GetWinner(); + bool AI_step(); + bool Human_step(CPoint point); + MCTS ai; + Game g; + +protected: + HICON m_hIcon; + + virtual BOOL OnInitDialog(); + afx_msg void OnPaint(); + afx_msg HCURSOR OnQueryDragIcon(); + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnBnClickedStart(); + afx_msg void OnBnClickedQuit(); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); + afx_msg void OnClose(); + afx_msg void OnBnClickedEndgame(); + afx_msg void OnBnClickedRepentance(); + afx_msg void OnBnClickedSave(); + afx_msg void OnBnClickedOpen(); + afx_msg void OnStartAIButtonClicked(); + afx_msg void OnBnClickedButtonAi(); +}; +``` + + +### game +实现了游戏逻辑相关部分 + +1. Board类,存局面等信息,提供判断局面方法,一些自定义格式的转换方法,同时有算分相关方法。 + +2. GameBase类,最基础的完整游戏逻辑 + +3. Game类,能悔棋,能记录,能放置,功能完善,傻瓜式一键使用的游戏逻辑接口,前后端用了都说好。 +```cpp +class Board +{ +public: + enum Chess; + enum ChessDirection; + enum GridType; + + enum ChessPriority; + Board(); + + void Clear(); + + bool IsWin(int id); + bool IsLose(int id); + void GetGridsByPriority(ChessPriority priority, array &result, int &count); + int CalcBoardScore(int side); + + void UpdatScoreInfo(int id, int turn); + + static int Coord2Id(int row, int col); + static void Id2Coord(int id, int &row, int &col); + static bool IsValidCoord(int row, int col); + static void Direction2DxDy(ChessDirection direction, int &dx, int &dy); + static int CalcDistance(int id1, int id2); + + static int hitCount; + static int totalCount; + + uint8_t keyGrid; + array grids; + array scoreInfo[2]; + array, 4> keyInfo[2]; + array gridCheckStatus; + array hasGridType; + array hasPriority; + +private: + char GetGrid(int row, int col); + bool SetGrid(int row, int col, char value); + + void InitKeyInfo(); + void UpdateKeyInfo(int row, int col); + void UpdateScore(int row, int col, int rowX, int colX, ChessDirection direction, int side); + void UpdateScoreOpt(int row, int col, ChessDirection direction, int side); + void UpdateGridsInfo(int i0); + void FindOtherGrids(int i0, int id, GridType type); + static bool RestrictedMoveRule; + static bool IsRestrictedMove(int id); + static void InitLineScoreDict(); + static short CalcLineScore(array line); + static array lineScoreDict; + static bool isLineScoreDictReady; + static array, 4> keyInfoOrigin; + static bool isKeyInfoOriginReady; +}; + +class GameBase +{ +public: + enum State; + + GameBase(); + void Init(); + bool PutChess(int id); + int GetSide(); + void UpdateValidGrids(); + bool UpdateValidGridsExtra(); + int GetNextMove(); + int CalcBetterSide(); + + Board board; + int state; + int turn; + int lastMove; + int validGridCount; + array validGrids; +}; + +class Game : private GameBase +{ +public: + + int GetState() { return state; } + int GetTurn() { return turn; } + + bool PutChess(int id); + void Regret(int step = 2); // 默认为2,方便人悔棋顺便把AI下的删了 + void Reset(); + + const vector& GetRecord() { return record; } + static int Str2Id(const string &str); + static string Id2Str(int id); + +private: + void RebuildBoard(); + vector record; +}; +``` + +### MCTS +实现了改良版蒙特卡洛树搜索算法 + +1. TreeNode:树节点,维护分数相关信息 + +2. MCTS:搜索树 +```cpp +class TreeNode +{ +public: + TreeNode(TreeNode *p); + void Clear(); + + int visit; + float value; + float winRate; + float expandFactor; + int validGridCount; + int gridLevel; + GameBase *game; + + TreeNode *parent; + list children; + array validGrids; +}; + +class MCTS +{ +public: + MCTS(int mode = 0); + ~MCTS(); + int Search(Game *state); + +private: + static void SearchThread(int id, int seed, MCTS *mcts, clock_t startTime); + + // 标准MCTS + TreeNode* TreePolicy(TreeNode *node); + TreeNode* ExpandTree(TreeNode *node); + TreeNode* BestChild(TreeNode *node, float c); + float DefaultPolicy(TreeNode *node, int id); + void UpdateValue(TreeNode *node, float value); + + // 魔改优化 + bool PreExpandTree(TreeNode *node); + int CheckBook(GameBase *state); + void ClearNodes(TreeNode *node); + float CalcScore(const TreeNode *node, float expandFactorParent_c); + TreeNode* NewTreeNode(TreeNode *parent); + void RecycleTreeNode(TreeNode *node); + void ClearPool(); + + int maxDepth, fastStopSteps, fastStopCount; + GameBase gameCache[THREAD_NUM_MAX]; + list pool; + TreeNode *root; + int mode; +}; +``` + +可供调整的参数(在MCTS.cpp中) + +```cpp +const float Cp = 2.0f; // UCB值中的Cp +const float SEARCH_TIME = 1.0f; // 搜索时间上限 +const int EXPAND_THRESHOLD = 3; // expand的阈值 +const bool ENABLE_MULTI_THREAD = true; // 是否使用多线程,建议开启 +const float FAST_STOP_THRESHOLD = 0.1f; // 剪枝 +const float FAST_STOP_BRANCH_FACTOR = 0.01f; +const bool ENABLE_TRY_MORE_NODE = true; // 如果模拟太多,换一个点模拟 +const int TRY_MORE_NODE_THRESHOLD = 1000; +``` + + + +# 测试部分 +## 人人对弈 +使用分支测试,测试了按钮、局面类型的所有可能组合,程序能够正确运行。 + +又经过自己瞎点了几十局,没有发现任何错误,可以合理推测程序能够在所有局面上正常运行。 +## 人机对弈 +经过亲自测试,发现AI写的过强,下不过,在几十场对局中人工队取得了$0\%$的好成绩。 + +# 可能的改进 +AI选择难度:通过调整搜索次数,以及局面评估值与模拟值之间的加权比例实现。 + +选择AI先后手:感觉加入后,从文件加载局面的交互会比较复杂,所以没有加,但AI支持选择先后手,可以通过手动调整代码来选择先后手(bushi)。 + +# 参考资料 +https://zhuanlan.zhihu.com/p/26335999 + +https://www.nature.com/articles/nature16961/ (校园网免费下载,非常好功能) diff --git a/res/GoBang.ico b/res/GoBang.ico new file mode 100644 index 0000000..12981a8 Binary files /dev/null and b/res/GoBang.ico differ diff --git a/res/GoBang.rc2 b/res/GoBang.rc2 new file mode 100644 index 0000000..db3a648 Binary files /dev/null and b/res/GoBang.rc2 differ diff --git a/res/GoBang0.ico b/res/GoBang0.ico new file mode 100644 index 0000000..d56fbcd Binary files /dev/null and b/res/GoBang0.ico differ diff --git a/res/black.cur b/res/black.cur new file mode 100644 index 0000000..255ece2 Binary files /dev/null and b/res/black.cur differ diff --git a/res/white.cur b/res/white.cur new file mode 100644 index 0000000..5b761e3 Binary files /dev/null and b/res/white.cur differ diff --git "a/res/\350\203\214\346\231\257\345\233\276.bmp" "b/res/\350\203\214\346\231\257\345\233\276.bmp" new file mode 100644 index 0000000..7aa71b9 Binary files /dev/null and "b/res/\350\203\214\346\231\257\345\233\276.bmp" differ diff --git a/resource.h b/resource.h new file mode 100644 index 0000000..dc896ba --- /dev/null +++ b/resource.h @@ -0,0 +1,28 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ 生成的包含文件。 +// 供 GoBang.rc 使用 +// +#define IDD_GOBANG_DIALOG 102 +#define IDR_MAINFRAME 128 +#define IDC_BLACK 130 +#define IDC_WHITE 131 +#define IDB_BACKGROUNDIMAGE 138 +#define IDC_START 1001 +#define IDC_QUIT 1002 +#define IDC_ENDGAME 1003 +#define IDC_REPENTANCE 1004 +#define IDC_SAVE 1005 +#define IDC_OPEN 1006 +#define IDC_BUTTON1 1011 +#define IDC_BUTTON_AI 1011 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 145 +#define _APS_NEXT_COMMAND_VALUE 32771 +#define _APS_NEXT_CONTROL_VALUE 1014 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/targetver.h b/targetver.h new file mode 100644 index 0000000..d7bf688 --- /dev/null +++ b/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 + +// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将 +// 将 _WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。 + +#include