用c#一行编写SharpMoku的Gomoku/Five

目录

介绍

概述

特征

用户界面

UI\IUI.cs

FormSharpMoku.cs

UI\PictureBoxGoMoku.cs

UI\LabelCustomPaint\GoMokuPaint.cs

Gomoku游戏

Board.cs

Game.cs

人工智能

AI\Minimax.cs

AI\EvaluateV1.cs

AI\EvaluateV2.cs

AI\EvaluateV3.cs

测试

我们能做些什么来改进

引用


介绍

这是一款用C#编写的Gomoku/Five in a row游戏。我之所以创建这个游戏,是因为我在学校电脑上玩它的美好回忆。

概述

Gomoku是一款棋盘游戏,两名玩家轮流在9x915x15的棋盘大小上放置棋子。
这个游戏的目标是成为第一个在水平、垂直或对角线上形成一排五个连续棋子的人。

获胜者是首先成功做到这一点的玩家。

特征

  • 电路板的尺寸可以是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控件,由用于表示石材和板线的标签集合组成。

  1. 9x9板上共有81个标签,15x15板上共有225个标签。
  2. 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表示beginHeightEH表示endHeightBW表示beginWidthEW表示endWidth。在beginHeighendHeight之间是middleHeight 和它也是 middleWidth  

例如,具有该Center位置的标签需要绘制两条线。

  1. beginWidthendWidthmiddleHeight的水平线。
  2. beginHeightendHeightmiddleWidth的垂直线。

PaintNeighbour()——我们不使用此方法,但如果您希望它显示邻居以进行调试,您可以取消注释它。

这张图片显示了黑色和白色石头的相邻单元格。
相邻位置的目的将在AI部分讨论。

用户单击时的时序图。

这是用户单击时的序列图。我省略了(流程)部分中的细节。
您可以在Game.cs部分查看机器人工作原理的序列图。

Gomoku游戏

UI已经完成,现在我们将讨论游戏组件,暂时不包括AI

Board.cs

我们将石头的值存储在名为Matrix2D数组中,我们还使用dicWhiteStonedicBlackStonedicNeighbor来存储黑白石头及其邻居的位置。我们存储重复的数据以减少搜索时间,假设我们需要知道邻居的位置,我们不需要搜索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

这些事情会发生:

  1. 设置Matrix[0,0]-1
  2. 使用position(0,0)值增加dicBlackStone
  3. 添加position(0,0)listHistory(程序将此值用于Undo())
  4. 使用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后,如果游戏结果没有确定,则会

  1. 切换转弯,因为现在轮到机器人了。
  2. 引发BotThinking事件以告知UI将光标更改为沙漏并阻止用户的输入。
  3. 在该调用BotMove()方法之后,此方法将使用该Minimax函数来查找一个好的位置。
  4. 引发BotFinishedThinking事件以告知UI将光标更改回正常状态。
  5. IUI调用MoveCursorTo将鼠标定位到机器人所需的位置。
  6. 引发HasFinishedMouveCursor游戏对象,所以它可以自己PutStone()
  7. 告诉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个方块。这是没有用的,因为我们的图案被对手的两边的石头挡住了

这个评估算法是可以的。我可以玩它,但是当我增加机器人的深度时,游戏太慢了。
我发现此模式存在两个问题:

  1. 我们检查的单元格数量太大,大约有900 + 225 + 450(水平、垂直、对角线的2个方向)的单元格。
  2. 与XXXX-O相比,该算法对像XXX-XO这样的模式没有给出太多分数,两种模式都可以通过放置一块石头来赢得,但对于第一种模式,该算法仅将其视为2个连续石头的2个模式。
    XXX-XO和XXX------XO的分数相同,尽管第一个可以让我们以一块石头获胜。

AI\EvaluateV3.cs

由于EvaluateV2还不够好,我试图寻找另一种解决方案,我找到了Anton Midrenok https://codepen.io/mudrenok/pen/gpMXgg\
EvaluateV3JavaScript 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-XXXOXX-XXOXXX-XOXXXX-,-XXXXOX-XXXOXX-XXOXXX-XO

取决于数量和另一个因素

Stone3WithBlock

OXXX--,OXX-X-OX-XX-,--XXXO-X-XXO-XX-XO

取决于数量和另一个因素

  • GetListAllDirection()——此函数将从4个方向获取模式列表。
  • GetCellValueInDirection()——此函数将从put position(positionCheck)中获取模式。

假设您需要检查位置行06列上的模式。自西向东方向。有三个步骤:

    1. 首先循环检查4个单元格 [0,5],[0,4],[0,3],[0,2],然后插入listCell
    2. 将0,6单元格值添加到列表中。
    3. 第二个循环检查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://www.codeproject.com/Articles/5375122/SharpMoku-a-Gomoku-Five-in-a-Row-Written-in-Csharp

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的人机对战五子棋小游戏的 C# 代码示例: ```csharp using System; namespace Gomoku { class Program { static void Main(string[] args) { // 初始化棋盘 int[,] board = new int[15, 15]; // 游戏循环 while (true) { // 人类玩家下棋 Console.WriteLine("请您输入您下棋的坐标(格式:x,y):"); string input = Console.ReadLine(); string[] inputArray = input.Split(','); int x = int.Parse(inputArray[0]); int y = int.Parse(inputArray[1]); board[x, y] = 1; // 判断人类玩家是否胜利 if (IsWin(board, x, y, 1)) { Console.WriteLine("您赢了!"); break; } // 电脑下棋 int[] computerMove = GetNextMove(board); board[computerMove[0], computerMove[1]] = 2; // 判断电脑是否胜利 if (IsWin(board, computerMove[0], computerMove[1], 2)) { Console.WriteLine("电脑赢了!"); break; } // 打印当前棋盘状态 PrintBoard(board); } } // 判断是否胜利 private static bool IsWin(int[,] board, int x, int y, int player) { int count = 1; // 横向判断 for (int i = x - 1; i >= 0; i--) { if (board[i, y] == player) { count++; } else { break; } } for (int i = x + 1; i < 15; i++) { if (board[i, y] == player) { count++; } else { break; } } if (count >= 5) { return true; } // 竖向判断 count = 1; for (int j = y - 1; j >= 0; j--) { if (board[x, j] == player) { count++; } else { break; } } for (int j = y + 1; j < 15; j++) { if (board[x, j] == player) { count++; } else { break; } } if (count >= 5) { return true; } // 斜向判断 count = 1; for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) { if (board[i, j] == player) { count++; } else { break; } } for (int i = x + 1, j = y + 1; i < 15 && j < 15; i++, j++) { if (board[i, j] == player) { count++; } else { break; } } if (count >= 5) { return true; } // 反斜向判断 count = 1; for (int i = x - 1, j = y + 1; i >= 0 && j < 15; i--, j++) { if (board[i, j] == player) { count++; } else { break; } } for (int i = x + 1, j = y - 1; i < 15 && j >= 0; i++, j--) { if (board[i, j] == player) { count++; } else { break; } } if (count >= 5) { return true; } return false; } // 获取电脑下棋的位置 private static int[] GetNextMove(int[,] board) { int[] move = new int[2]; // 随机生成下棋位置 Random random = new Random(); while (true) { int x = random.Next(15); int y = random.Next(15); if (board[x, y] == 0) { move[0] = x; move[1] = y; break; } } return move; } // 打印当前棋盘状态 private static void PrintBoard(int[,] board) { Console.WriteLine(" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14"); for (int i = 0; i < 15; i++) { Console.Write(i + " "); for (int j = 0; j < 15; j++) { if (board[i, j] == 0) { Console.Write("┼ "); } else if (board[i, j] == 1) { Console.Write("● "); } else { Console.Write("○ "); } } Console.WriteLine(); } } } } ``` 这个示例程序实现了一个简单的人机对战五子棋小游戏,其中 1 表示玩家下的棋子,2 表示电脑下的棋子,0 表示空位。您可以在此基础上进行修改和扩展,实现更复杂的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值