目录
UI\LabelCustomPaint\GoMokuPaint.cs
介绍
这是一款用C#编写的Gomoku/Five in a row游戏。我之所以创建这个游戏,是因为我在学校电脑上玩它的美好回忆。
概述
Gomoku是一款棋盘游戏,两名玩家轮流在9x9或15x15的棋盘大小上放置棋子。
这个游戏的目标是成为第一个在水平、垂直或对角线上形成一排五个连续棋子的人。
获胜者是首先成功做到这一点的玩家。
特征
- 电路板的尺寸可以是9x9或15x15。
- 模式:人与人、人与机器人、机器人与人类
- 机器人级别正常,困难
- 它支持石头和木板的图像或颜色。
- 支持各种类型的主题
用户界面
这是一个简单的Gomoku程序,因此我们只需要一个包含一个组件的表单来渲染板。
UI\IUI.cs
此接口定义方法和事件。我们使用FormSharpMoku实现这个接口。
public interface IUI
{
// Raise event CellClicked so the game object will handle the operation
event Board.CellClickHandler CellClicked;
void RenderUI();
void MoveCursorTo(Position position);
//To tell the game object that the bot has finished
//moving a cursor to the position to put.
event EventHandler HasFinishedMoveCursor;
/*
These three methods will be triggered from the Game Object.
Game_GameFinished : UI will display the result.
Game_BotThinking : UI will change the cursor to an hourglass.
Game_BotFinishedThinking : UI will change the cursor back to the
default cursor and allow the user to input
*/
void Game_GameFinished(object sender, EventArgs e);
void Game_BotThinking(object sender, EventArgs e);
void Game_BotFinishedThinking(object sender, EventArgs e);
}
FormSharpMoku.cs
该表单实现了IUI接口,它由PictureBoxGoMoku控制组成。
UI\PictureBoxGoMoku.cs
此类继承自PictureBox控件,由用于表示石材和板线的标签集合组成。
- 9x9板上共有81个标签,15x15板上共有225个标签。
- 在PictureBoxGoMoKu Paint中,它只负责渲染符号和背景图像,电路板的其余部分将由标签本身渲染。该板的表示法可以是Gomoku表示法(行为A-O,列为1-15)和数组索引位置(行和列均为0-14),我们使用后者进行调试。
UI\LabelCustomPaint\GoMokuPaint.cs
为了支持各种主题,我们使用了多个类来渲染标签,对于GoMoku主题,我们使用GoMokuPaint类来渲染。
- PaintStone()——此方法将渲染石头,它还支持渲染图像,或仅渲染石头颜色。
- PaintBorder()——此方法将在标签上画一条线以制作边框,它还会绘制一个交点。
该方法处理不同类型的绘图位置:
-
- TopLeftCorner
- TopBorder
- TopRightCorner
- LeftBorder
- Center
- RightBorder
- BottomLeftCorder
- BottomBorder
- BottomRightCorner
在此图像中,蓝色是一种标签,绿色是其位置为Center的标签,但也有一个交点。
这是仅显示Center位置大小写的代码,每个位置将绘制成2条线,如果是交叉点,则中心会有一个小圆圈。
private void PaintBorder(Graphics g, ExtendLabel pLabel)
{
Point fromPointX = Point.Empty;
Point toPointX = Point.Empty;
Point fromPointY = Point.Empty;
Point toPointY = Point.Empty;
int beginWidth = 0;
int middleWidth = pLabel.Width / 2;
int endWidth = pLabel.Width;
int beginHeight = 0;
int middleHeight = pLabel.Height / 2;
int endHeight = pLabel.Height;
switch (pLabel.CellAttribute.GoboardPosition)
{
case GoBoardPositionEnum.Center:
fromPointY = new Point(middleWidth, beginHeight);
toPointY = new Point(middleWidth, endHeight);
fromPointX = new Point(beginWidth, middleHeight);
toPointX = new Point(endWidth, middleHeight);
break;
//Omit the code for other position
}
g.DrawLine(penTable, fromPointY, toPointY);
g.DrawLine(penTable, fromPointX, toPointX);
g.CompositingMode = CompositingMode.SourceOver;
if (pLabel.CellAttribute.IsIntersection)
{
RectangleF RecCircleIntersecton =
new RectangleF(middleWidth - 4, middleHeight - 4, 8, 8);
g.FillEllipse(ShareGraphicObject.SolidBrush(penTable.Color),
RecCircleIntersecton);
}
}
在这张图中,蓝色是标签的面积,BH表示beginHeight,EH表示endHeight,BW表示beginWidth,EW表示endWidth。在beginHeigh和endHeight之间是middleHeight 和它也是 middleWidth。
例如,具有该Center位置的标签需要绘制两条线。
- 从beginWidth到endWidth处middleHeight的水平线。
- 从beginHeight到endHeight处middleWidth的垂直线。
PaintNeighbour()——我们不使用此方法,但如果您希望它显示邻居以进行调试,您可以取消注释它。
这张图片显示了黑色和白色石头的相邻单元格。
相邻位置的目的将在AI部分讨论。
用户单击时的时序图。
这是用户单击时的序列图。我省略了(流程)部分中的细节。
您可以在Game.cs部分查看机器人工作原理的序列图。
Gomoku游戏
UI已经完成,现在我们将讨论游戏组件,暂时不包括AI。
Board.cs
我们将石头的值存储在名为Matrix的2D数组中,我们还使用dicWhiteStone、dicBlackStone和dicNeighbor来存储黑白石头及其邻居的位置。我们存储重复的数据以减少搜索时间,假设我们需要知道邻居的位置,我们不需要搜索Matrix中的所有位置来计算邻居位置。
//Some part of the Board.cs
[Serializable]
public class Board
{
public delegate void CellClickHandler
(object sender, PositionEventArgs positionClick);
// 2d array to store cell value
public int[,] Matrix;
/*
* dicWhiteStone store WhiteStone position
* dicBlackStone store WhiteStone position
* dicNieghbor store position of the stone next to both white and black stone
*/
public Dictionary<String, SharpMoku.Position> dicWhiteStone
{ get; private set; } = new Dictionary<String, SharpMoku.Position>();
public Dictionary<String, SharpMoku.Position> dicBlackStone
{ get; private set; } = new Dictionary<String, SharpMoku.Position>();
public Dictionary<String, SharpMoku.Position> dicNeighbor
{ get; private set; } = new Dictionary<string, Position>();
public int BoardSize { get; private set; }
public enum WinStatus
{
BlackWon=-1,
NotDecidedYet=0,
WhiteWon=1,
Draw=2
}
// Represent the value of the cell in the board
public enum CellValue
{
Black=-1,
Empty=0,
White=1,
}
public enum Turn
{
Black=-1,
White=1
}
public Turn CurrentTurn { get; private set; } = Turn.Black;
public CellValue CurrentTurnCellValue
{
get
{
if(CurrentTurn == Turn.Black)
{
return CellValue.Black;
}
return CellValue.White;
}
}
public Board (int boardSize)
{
if(boardSize != 9 &&
boardSize != 15)
{
throw new ArgumentException($"Board size is invalid {boardSize},
program only accept 9 and 15 as valid value");
}
this.BoardSize = boardSize;
Matrix = new int[this.BoardSize, this.BoardSize];
}
public Board(Board board)
{
Matrix = new int[board.Matrix.GetLength(0), board.Matrix.GetLength(1)];
dicWhiteStone = new Dictionary<string, Position>();
dicBlackStone = new Dictionary<string, Position>();
dicNeighbor = new Dictionary<string, Position>();
this.Matrix = board.Matrix.Clone() as int[,];
this.dicWhiteStone = new Dictionary<string,
Position>(board.dicWhiteStone);
this.dicBlackStone = new Dictionary<string,
Position>(board.dicBlackStone);
this.dicNeighbor = new Dictionary<string,
Position>(board.dicNeighbor);
this.listHistory = new List<Position>(board.listHistory);
this.BoardSize = board.BoardSize;
this.CurrentTurn = board.CurrentTurn;
}
public void PutStone(int pRow, int pCol, CellValue cellValue)
{
/* 1.Assign value into the matrix
* 2.Add the postion value into Hash
* 3.Add postion into history
* 4.Add Empty neighbor
*/
Matrix[pRow, pCol] = (int)cellValue;
SharpMoku.Position newPosition = new Position(pRow, pCol);
GetHshByCellValue(cellValue).Add
(newPosition.PositionString(), newPosition);
listHistory.Add(newPosition);
AddEmptyNeighborOf(newPosition);
}
如果我们用PutStone(0,0, -1)把黑石放到第0行第0列
这些事情会发生:
- 设置Matrix[0,0]为-1。
- 使用position(0,0)值增加dicBlackStone。
- 添加position(0,0)到listHistory(程序将此值用于Undo())
- 使用position(1,0)和。position(0,1)增加dicNeighbor
当我们调用Undo()时,程序还需要调整邻居位置。
Game.cs
这个类的角色是UI和棋盘对象之间的中间人,UI只会引发一个事件,然后游戏对象会决定下一步该做什么,所有的业务逻辑都在游戏和棋盘中,UI只知道如何渲染图形。
当用户点击棋盘时,它会向该UI_CellClicked()方法引发一个事件,在UI_CellClicked()中,程序将检查游戏和棋盘的状态,然后继续执行PutStone(positionClick.Value, board.CurrentTurnCellValue)。
public class Game
{
public Board board = null;
private UI.IUI UI = null;
public enum GameStateEnum
{
NotBegin,
Playing,
End
}
public enum GameModeEnum
{
PlayerVsBot = 0,
BotVsPlayer = 1,
PlayerVsPlayer = 2,
}
public Board.WinStatus WinResult { get; private set; } =
Board.WinStatus.NotDecidedYet;
public Board.Turn TheWinner { get; private set; }
public ILog log = null;
public GameModeEnum GameMode { get; private set; } = GameModeEnum.PlayerVsBot;
public GameStateEnum GameState { get; private set; } = GameStateEnum.NotBegin;
/*
* GameFinished event to tell the UI to display the result
* BotThinking to tell the UI to change the cursor to an hourglass
* BotFinishedThinking to tell the UI to move the cursor to the position
* that Bot needs to put the stone
*/
public event EventHandler GameFinished;
public event EventHandler BotThinking;
public event EventHandler BotFinishedThinking;
private void ExplicitConstructor(UI.IUI ui,
Board board,
int boardSize,
IEvaluate pbot,
int botSearchDepth,
GameModeEnum gameMode)
{
this.UI = ui;
this.GameMode = gameMode;
this.BotSearchDepth = botSearchDepth;
if (pbot != null)
{
bot = pbot;
}
WinResult = Board.WinStatus.NotDecidedYet;
this.UI.CellClicked -= UI_CellClicked;
this.UI.HasFinishedMoveCursor -= UI_HasFinishedMoveCursor;
this.GameFinished -= UI.Game_GameFinished;
this.BotThinking -= UI.Game_BotThinking;
this.BotFinishedThinking -= UI.Game_BotFinishedThinking;
this.UI.CellClicked += UI_CellClicked;
this.UI.HasFinishedMoveCursor += UI_HasFinishedMoveCursor;
this.board = (board != null)
? board
: new Board(boardSize);
this.GameFinished += UI.Game_GameFinished;
this.BotThinking += UI.Game_BotThinking;
this.BotFinishedThinking += UI.Game_BotFinishedThinking;
}
public Game(UI.IUI ui, Board board,
IEvaluate pbot,
int botSearchDepth,
GameModeEnum gameMode
)
{
ExplicitConstructor(ui, board, 0, pbot, botSearchDepth, gameMode);
}
public Game(UI.IUI ui, int boardSize,
IEvaluate pbot,
int botSearchDepth,
GameModeEnum gameMode
)
{
ExplicitConstructor(ui, null, boardSize, pbot, botSearchDepth, gameMode);
}
public bool CanUndo => board == null
? false
: board.CanUndo;
private void UI_HasFinishedMoveCursor(object sender, EventArgs e)
{
PutStone(botMoveToPostion, (Board.CellValue)board.CurrentTurn);
}
public void PutStone(Position position)
{
PutStone(position, this.board.CurrentTurnCellValue);
}
public void PutStone(Position position, Board.CellValue turn)
{
board.PutStone(position, turn);
this.UI.RenderUI();
WinResult = board.CheckWinStatus();
if (WinResult == Board.WinStatus.NotDecidedYet)
{
this.board.SwitchTurn();
bool IsBotTurn = (GameMode == GameModeEnum.PlayerVsBot && !IsPlayer1Turn) ||
(GameMode == GameModeEnum.BotVsPlayer && IsPlayer1Turn);
if (IsBotTurn)
{
BotThinking?.Invoke(this, null);
BotMove();
}
return;
}
this.GameState = GameStateEnum.End;
WinStatusEventArgs statusEvent = new WinStatusEventArgs(WinResult);
GameFinished?.Invoke(this, statusEvent);
}
public void NewGame()
{
this.GameState = GameStateEnum.Playing;
if (this.GameMode == GameModeEnum.BotVsPlayer)
{
System.Threading.Thread.Sleep(20);
BotMove();
}
}
// This method is being used by humans only.
private void UI_CellClicked(object o, Board.PositionEventArgs positionClick)
{
Boolean isPlayerClickDespiteItisBotTurn =
(this.GameMode == GameModeEnum.PlayerVsBot &&
this.board.CurrentTurn != Board.Turn.Black) ||
(this.GameMode == GameModeEnum.BotVsPlayer &&
this.board.CurrentTurn != Board.Turn.White);
Boolean isClickedOnNonEmptyCell = board.Matrix[positionClick.Value.Row,
positionClick.Value.Col] != (int)Board.CellValue.Empty;
Boolean isClickedOInValidPosition = !board.IsValidPosition(positionClick.Value);
if (GameState != GameStateEnum.Playing
|| isClickedOInValidPosition
|| isPlayerClickDespiteItisBotTurn
|| isClickedOnNonEmptyCell)
{
return;
}
PutStone(positionClick.Value, board.CurrentTurnCellValue);
}
public int BotSearchDepth { get; private set; } = 2;
private Position botMoveToPostion;
private IEvaluate bot = new EvaluateV3();
private void BotMove()
{
SharpMoku.Board cloneBoard = new Board(this.board);
Minimax miniMax = new Minimax(cloneBoard, bot, this.log);
botMoveToPostion = miniMax.calculateNextMove(BotSearchDepth);
BotFinishedThinking?.Invoke(this, null);
UI.MoveCursorTo(botMoveToPostion);
}
}
此序列图解释了第一个图中省略的细节部分。游戏对象调用PutStone后,如果游戏结果没有确定,则会
- 切换转弯,因为现在轮到机器人了。
- 引发BotThinking事件以告知UI将光标更改为沙漏并阻止用户的输入。
- 在该调用BotMove()方法之后,此方法将使用该Minimax函数来查找一个好的位置。
- 引发BotFinishedThinking事件以告知UI将光标更改回正常状态。
- IUI调用MoveCursorTo将鼠标定位到机器人所需的位置。
- 引发HasFinishedMouveCursor游戏对象,所以它可以自己PutStone()。
- 告诉UI渲染看板。
人工智能
现在来到AI部分,对于搜索功能,我使用标准的Minimax Alpha-Beta修剪。对于程序搜索的节点,程序不会搜索不是邻居的节点,因为Gomoku板太大了。对于15x15的板子大小,第一级有200多个位置,第二级将有40,000多个位置因此,我们的目标是通过将搜索限制为每级大约30个相邻位置来最小化节点数量。
我们可以这样做,因为在Gomoku中,要赢得比赛,您放置的位置必须紧挨着现有的石头位置。
AI\Minimax.cs
board.generateNeighboreMoves()是获取邻居位置的方法。
如果是第一级,我们允许从现有石头位置获取半径为 2 的邻居。
此类允许您注入赋值器对象,然后调用evaluator.evaluateBoard()。
我们将赋值器对象与Minimax类分开,因为我们希望允许Minimax在各种赋值器函数之间切换。
截至目前,我们只使用EvaluateV3.cs但我想提及EvaluateV2.cs用于教育目的。
private MoveScore minimaxSearchAlphaBeta
(int depth, SharpMoku.Board board, Boolean IsMax,
double AlphaValue, double BetaValue)
{
NumberOfNodes++;
NumberOfNodeInEachLevel[depth]++;
// Last depth (terminal node), evaluate the current board score.
String tabString = GetTab(depth);
MoveScore movescore = new MoveScore();
Log($"{tabString}depth{depth}");
if (depth == 0)
{
movescore = new MoveScore(evaluator.evaluateBoard(board, !IsMax));
Log($"{tabString}Evaluate happens here");
Log($"{tabString}Score::{movescore.Score}");
return movescore;
}
/*If it is first level, the radiusNeighbor can be 2
* because it will not have too much node.
*/
int radiusNeighbour = (depth == FirstLevelDepth)
? 2
: 1;
List<Position> allNeighborPossibleMoves = null;
if (radiusNeighbour == 2)
{
allNeighborPossibleMoves = board.generateNeighboreMoves(radiusNeighbour);
if (allNeighborPossibleMoves.Count > 30)
{
allNeighborPossibleMoves = board.generateNeighboreMoves(1);
}
}
else
{
allNeighborPossibleMoves = board.generateNeighboreMoves(1);
}
// If there is no possible move left,
// treat this node as a terminal node and return the score.
bool IsNothingLeftToSearch = (allNeighborPossibleMoves.Count == 0);
if (IsNothingLeftToSearch)
{
movescore = new MoveScore(evaluator.evaluateBoard(board, !IsMax));
return movescore;
}
/*If we reach this stage it means
* There are valid moves
*/
MoveScore bestMove = new MoveScore();
int depthChild = 0;
Boolean isMaxChild = false;
depthChild = depth - 1;
isMaxChild = !IsMax;
bestMove.Row = allNeighborPossibleMoves[0].Row;
bestMove.Col = allNeighborPossibleMoves[0].Col;
bestMove.Score = IsMax
? int.MinValue
: int.MaxValue;
int iCountMove = 0;
Log($"{tabString}No of neighbor::{allNeighborPossibleMoves.Count }");
foreach (Position move in allNeighborPossibleMoves)
{
iCountMove++;
Log($"{tabString}{iCountMove}. move::{move.PositionString()}");
board.PutStoneAndSwitchTurn(move);
movescore = minimaxSearchAlphaBeta
(depthChild, board, isMaxChild, AlphaValue, BetaValue);
movescore.Row = move.Row;
movescore.Col = move.Col;
Log($"{tabString}Score::{movescore.Score }");
// board.Undo();
if (board.IsFull)
{
Log("{tabString}board.IsFull");
return movescore;
}
board.Undo();
if (IsMax)
{
AlphaValue = Math.Max(movescore.Score, AlphaValue);
if (movescore.Score >= BetaValue)
{
Log($"{tabString}moveScoe >= Beta");
return movescore;
}
bestMove = MoveScore.Max(bestMove, movescore);
}
else
{
BetaValue = Math.Min(movescore.Score, BetaValue);
if (movescore.Score > AlphaValue)
{
Log($"{tabString}moveScore > Alpha");
return movescore;
}
bestMove = MoveScore.Min(bestMove, movescore);
}
}
return bestMove;
}
AI\EvaluateV1.cs
这个类只是选择一个随机位置。
AI\EvaluateV2.cs
这就是流行的Gomoku赋值器函数,这个赋值器函数的思想是我们尝试搜索整个棋盘以找到水平、垂直和对角线方向的模式。
对于15x15尺寸的电路板,将有88行。水平15行。
垂直15行。
对角线两个方向的58条线,每个方向有29条线。
在每一行中,我们搜索以找到图案,我们拥有的连续石头越多,我们得到的分数就越高,除非我们的图案被对手的石头挡住了。
要给出分数,谁是当前也是需要考虑的,例如,如果我们连续有4块石头,现在轮到我们了,我们可以保证我们会赢,但如果当前回合是对手,我们不会从这种模式中获得太多好处,因为对手可以阻止我们获得获胜的位置。
这3条线连续有4块石头,区别在于:
- #15线没有对手的阻挡。
- #13线在对手的左侧有一个格挡。
- #11线在对手的左右两侧都有2个格挡。
我将用
X来代表我们的石头,
-代表空白,
O代表对手的石头。
此表中的分数不是提取值,它们只是一个可以调整的想法。
该函数只需要Pattern,轮到它作为参数,
我在此表上显示块列数只是为了便于查看。
模式 | 块数 | 轮到谁了 | 得分 | 描述 |
XXXXX | 0 | (N/A) | 50000000 | 连续5个。我们可以赢得比赛 |
-XXXX- | 0 | 轮到我们了 | 1000000 | 连续4个没有阻挡,可以确认获胜,因为它已经有4个石头,现在轮到我了 |
OXXXX- | 1 | 轮到我们了 | 1000000 | 连续4个,左边有一个方块,可以确认获胜,因为它已经有4个石头了,现在轮到我了 |
OXXXX- | 1 | 对手回合 | 1000 | 连续4个用一个阻挡,但这是轮到对手的,所以我们的对手可以阻挡我们 |
-XXX- | 0 | 轮到我们了 | 200 | 连续3个没有阻挡,这还不错,它有获胜的潜力 |
OXXXXO | 2 | (N/A) | 0 | 连续4个,左右各有2个方块。这是没有用的,因为我们的图案被对手的两边的石头挡住了 |
这个评估算法是可以的。我可以玩它,但是当我增加机器人的深度时,游戏太慢了。
我发现此模式存在两个问题:
- 我们检查的单元格数量太大,大约有900 + 225 + 450(水平、垂直、对角线的2个方向)的单元格。
- 与XXXX-O相比,该算法对像XXX-XO这样的模式没有给出太多分数,两种模式都可以通过放置一块石头来赢得,但对于第一种模式,该算法仅将其视为2个连续石头的2个模式。
XXX-XO和XXX------XO的分数相同,尽管第一个可以让我们以一块石头获胜。
AI\EvaluateV3.cs
由于EvaluateV2还不够好,我试图寻找另一种解决方案,我找到了Anton Midrenok https://codepen.io/mudrenok/pen/gpMXgg\
EvaluateV3的JavaScript GoMoku程序这是C#的端口,在代码的某些部分进行了反射。
这个功能的想法是,当程序评估时,它不需要扫描整个板子,它只需要从它想要放置石头的位置搜索36个单元格。
假设我们想知道位置7,7的分数。
这些是它将搜索的位置。
有四个方向:
- 从北到南表示垂直
- 从西到东表示水平
- 东北到西南和西北到东南两条对角线
每个方向将仅搜索9个单元格、位置本身和行中的其他8个单元格。在每一行中,我们搜索这些类型的模式。
这些是模式和分数的示例。
- 一些分数值是“取决于多少和另一个因素”,请查看getScoreByPattern()以获得更多的理解。
模式名称 | 图案样本 | 得分 |
Stone5 | XXXXX公司 | 1000000000 |
Stone4WithNoBlock | -XXXX- | 100000000 |
Stone3WithNoBlock | -XXX--,--XXX-,-X-XX-,-XX-X- | 10000000 |
Stone2WithNoBlock | --XX--,-X-X--,--X-X-,-XX---,---XX-,-X--X- | 取决于数量和另一个因素 |
Stone4WithBlock | OX-XXX,OXX-XX,OXXX-X,OXXXX-,-XXXXO,X-XXXO,XX-XXO,XXX-XO, | 取决于数量和另一个因素 |
Stone3WithBlock | OXXX--,OXX-X-,OX-XX-,--XXXO,-X-XXO,-XX-XO, | 取决于数量和另一个因素 |
- GetListAllDirection()——此函数将从4个方向获取模式列表。
- GetCellValueInDirection()——此函数将从put position(positionCheck)中获取模式。
假设您需要检查位置行0第6列上的模式。自西向东方向。有三个步骤:
-
- 首先循环检查4个单元格 [0,5],[0,4],[0,3],[0,2],然后插入listCell。
- 将0,6单元格值添加到列表中。
- 第二个循环检查4个单元格[0,7][0,8],[0,9],[0,10],然后加入listCell。
我们在0位置插入第一个循环的原因是
我们希望得到这样的
数据2,3,4,5,6,7,8,9,10
第一个循环的顺序是5,4,3,2,但我们需要得到2,3,4,5
,所以我们在0位置插入它,这样我们就可以得到2,3,4,5。
对于第二个循环,它的顺序是6、7、8、9、10,这已经是我们想要的了。
此图显示了需要检查的单元格的位置。
- getScoreByPattern()——此函数将通过给出模式计算分数。
public List<List<int>> GetListAllDirection
(SharpMoku.Board board, Position checkPosition, SharpMoku.Board.CellValue cellValue)
{
Position positionDeltaNorthSouth = new Position(1, 0);
Position positionDeltaWestEast = new Position(0, 1);
Position positionDeltaNorthWest = new Position(1, 1);
Position positionDeltaNorthEast = new Position(1, -1);
/*
*Prepare to go though all 8 directions
* 4 have 4 lists of News because each list go both way
* For example NorthSouth mean from the position to north
* and from the postion to south
*/
List<int> listNorthSouth =
GetCellValueInDirection(board.Matrix, cellValue,
checkPosition, positionDeltaNorthSouth);
List<int> listWestEast =
GetCellValueInDirection(board.Matrix, cellValue,
checkPosition, positionDeltaWestEast);
List<int> listNorthWest =
GetCellValueInDirection(board.Matrix, cellValue,
checkPosition, positionDeltaNorthWest);
List<int> listNorthEast =
GetCellValueInDirection(board.Matrix, cellValue,
checkPosition, positionDeltaNorthEast);
List<List<int>> listAllDirection = new List<List<int>>()
{
listNorthSouth ,
listWestEast ,
listNorthWest ,
listNorthEast
};
return listAllDirection;
}
public List<int> GetCellValueInDirection(int[,] matrix,
SharpMoku.Board.CellValue cellValue,
Position positionCheck, Position positionDelta)
{
int i;
List<int> listCell = new List<int>();
bool IsCheckPostionIsNotmatchWithCellValue =
matrix[positionCheck.Row, positionCheck.Col] != (int)cellValue;
HashSet<String> hshCellInaRow = new HashSet<string>();
if (IsCheckPostionIsNotmatchWithCellValue)
{
return listCell;
}
int opponentCellvalue = -(int)cellValue;
//First loop Insert cell #1
for (i = 1; i < 5; i++)
{
Position nextPosition =
new Position(positionCheck.Row - positionDelta.Row * i,
positionCheck.Col - positionDelta.Col * i);
if (nextPosition.Row < 0 ||
nextPosition.Row >= matrix.GetLength(0) ||
nextPosition.Col < 0 ||
nextPosition.Col >= matrix.GetLength(0))
{
break;
}
var nextValue = matrix[nextPosition.Row, nextPosition.Col];
if(!hshCellInaRow.Contains ( nextPosition.PositionString()))
{
listCell.Insert(0, nextValue); //We insert at the 0 position
}
if ((int)nextValue == opponentCellvalue)
{
break;
}
}
listCell.Add((int)cellValue); //The cell itself #2
//Add #3
for (i = 1; i < 5; i++)
{
Position nextPosition =
new Position(positionCheck.Row + positionDelta.Row * i,
positionCheck.Col + positionDelta.Col * i);
if (nextPosition.Row < 0 ||
nextPosition.Row >= matrix.GetLength(0) ||
nextPosition.Col < 0 ||
nextPosition.Col >= matrix.GetLength(0))
{
break;
}
var nextValue = matrix[nextPosition.Row, nextPosition.Col];
if (!hshCellInaRow.Contains(nextPosition.PositionString()))
{
listCell.Add(nextValue);//We add it to the last position
}
if ((int)nextValue == opponentCellvalue)
{
// listCell.Insert(0, nextValue);
break;
}
//listCell.Insert(0, nextValue);
}
return listCell;
}
public int getScoreByPattern(NumberofScorePattern numberofPattern)
{
if (numberofPattern.Winning > 0)
{
return CONST_winScore * numberofPattern.Winning;
}
if (numberofPattern.Stone4 > 0)
{
return CONST_winGuarantee;
}
if (numberofPattern.BlockStone4 > 1)
{
return CONST_winGuarantee / 10;
}
if (numberofPattern.Stone3 > 0
&& numberofPattern.BlockStone4 > 0)
{
return CONST_winGuarantee / 100;
}
if (numberofPattern.Stone3 > 1)
{
return CONST_winGuarantee / 1000;
}
if (numberofPattern.Stone3 == 1)
{
switch (numberofPattern.Stone2)
{
case 3: return 40000;
case 2: return 38000;
case 1: return 35000;
default: return 3450;
}
}
if (numberofPattern.BlockStone4 == 1)
{
switch (numberofPattern.Stone2)
{
case 3: return 4500;
case 2: return 4200;
case 1: return 4100;
default: return 4050;
}
}
switch (numberofPattern.BlockStone3)
{
case 3:
if (numberofPattern.Stone2 == 1) return 2800;
break;
case 2:
switch (numberofPattern.Stone2)
{
case 2: return 3000;
case 1: return 2900;
}
break;
case 1:
switch (numberofPattern.Stone2)
{
case 3: return 3400;
case 2: return 3300;
case 1: return 3100;
}
break;
}
switch (numberofPattern.Stone2)
{
case 4: return 2700;
case 3: return 2500;
case 2: return 2000;
case 1: return 1000;
}
return 0;
}
这个评估功能非常强大,它解决了Evaluate2存在的两个问题。
- 我们检查的单元格数量不再太大。
- 该算法更擅长处理像这样的模式XXX-XO
测试
只需从Visual Studio运行脚本即可。
我们能做些什么来改进
对于UI,如果我需要再次重写棋盘对象,我可能会考虑不使用标签数组来渲染棋盘和石头。
如果我们在黑板上看到的所有对象都是由一个picturebox对象绘制的,那就更好了。
对于AI来说,它已经足够强大了,但如果我们实现一些开放算法并使用Zobrist哈希,它可能会更强大。
引用
- https://en.wikipedia.org/wiki/Gomoku
- Minimax for Gomoku (Connect Five)
- https://codepen.io/mudrenok/pen/gpMXgg
https://www.codeproject.com/Articles/5375122/SharpMoku-a-Gomoku-Five-in-a-Row-Written-in-Csharp