通过创建黑白棋机器人来学习实现 Minimax 算法

介绍

黑白棋程序,我选择开发这个程序是因为在寻找黑白棋/黑白棋游戏时找不到具有我需要的功能的程序。

特征

  1. 支持模式 Human vs Human、Human vs Bot、Bot vs Bot
  2. 支持板编辑器
  3. 支持机器人创建者。您可以选择一个图像并自定义将用于评估棋盘的 AI 分数。
  4. 可以显示机器人的最后一步Minimax搜索树
  5. 可以更改个人资料图片Human Player1Human Player2
  6. 可以导航移动
  7. 支持深色模式

程序使用的文件扩展名

  1. .brd用作电路板信息。
  2. .but用作机器人信息。
  3. .rev用作游戏保存信息,游戏信息保留了移动的历史,这是它与棋盘游戏的区别,因此您可以通过导航控件进行导航。

怎么玩

游戏规则与普通黑白棋游戏完全相同。

这个概念

项目的文件结构

在本节中,我想谈谈负责渲染图形的类,但我不会提及与 Board Editor 或 Bot Creator 相关的任何内容。

游戏界面


该板还可以显示数字。

UI\PictureBoxBoard.cs

这是一个继承自PictureBox控件的类。
它负责显示黑白棋盘。此类本身由名为 的控件组成pictureBoxTable。控件将是从方法中呈现表格
的控件。 该方法从该类中读取棋盘信息, 绘制表格和磁盘。pictureBoxTablepictureBoxTable_Paint()
pictureBoxTable_Paint()

这就是画板的逻辑。

  1. 有一个由 64 个矩形组成的数组。我们创建这 64 个矩形来存储单元格在板上的位置。

  2. 此方法将调用:

  • DrawBoard()绘制板表
  • DrawIntersection()顾名思义
  • DrawDisk()只是绘制磁盘
  • DrawRedDot()显示哪个是最后的放置位置
  • DrawDiskBorder()显示可以放置哪个单元格
  • DrawNumber()- 此方法用于当我们需要显示单元格的分数时。
    此方法中的所有绘图仅使用基本的 GDI+ 方法,例如FillEllipse()DrawEllipse(),DrawRectangles()

没有用于电路板绘图的图像。这些是绘制符号的逻辑:

  1. 只需从 0 循环到 7。
  2. 使用DrawString()方法绘制string
    此控件具有PictureBoxBoard_Paint()呈现符号的方法。
C#
缩小▲   
public enum CellImageEnum
{
   WhiteCell,
   BlackCell,
   BlankCell,
   BlankCellWithRed  //To show which is the last cell that the disk was put
}
public enum BoardModeEnum
{
   PlayMode,
   EditMode
}
public Boolean IsSmallBoard { get; set; } = false; // Support display the board
                                                   // as small size
public Boolean IsHideLegent { get; set; } = false;
public Boolean IsDrawNotion { get; set; } = true;  // Can hide the notation (A1-H8)

public event PictureBoxCellClick CellClick;
private void PictureBox1_MouseDown(object sender, MouseEventArgs e)
{
   //Must be a left button
   if (e.Button != MouseButtons.Left)
   {
       return;
   }

   int X = e.X;
   int Y = e.Y;
   int RowClick = Y / CellSize;
   int ColClick = X / CellSize;
   if (RowClick >= NoofRow ||
       ColClick >= NoofRow ||
       RowClick < 0 ||
       ColClick < 0)
   {
       //Row and Column click must be in range.
       return;
   }

   if (CellClick != null)
   {
       //Raise event.
       CellClick(this, new Position(RowClick, ColClick));
   }
}
private void DrawNumber(Graphics g, int Number, Rectangle rec)
{
    /*This method is for drawing a number in case of
      showing a score value from evaluate function.
    */
    Rectangle r = rec;
    r.Height -= 10;
    r.Width -= 10;
    r.X += 10;
    r.Y += 15;

    Font SegoeUIFont = new Font("Segoe UI", 14, FontStyle.Bold);
    Color NumberColor = Color.FromArgb(150, 180, 180);
    String NumberText = "+" + Number;
    if(Number < 0)
    {
      //Negative number does not need "+" prefix.
      NumberText =  Number.ToString ();
      NumberColor = Color.FromArgb(255, 199, 79);
    }
    //Create Brush and other objects
    PointF pointF = new PointF(r.X, r.Y);
    SolidBrush solidBrush = new SolidBrush(NumberColor );
    //Draw text using DrawString
    g.DrawString(NumberText ,
      SegoeUIFont,
      solidBrush, pointF );

    solidBrush.Dispose();
}
private void DrawDisk(Graphics g, CellImageEnum CellImage, Rectangle rec)
{
    if (CellImage == CellImageEnum.BlankCell)
    {
      return;
    }
    Rectangle r = rec;
    //Reduce the size of Rectangle
    r.Height -= 10;
    r.Width -= 10;
    r.X += 5;
    r.Y += 5;
    DrawDiskBorder(g, r);

    Color colorDisk = Color.White;
    if (CellImage != CellImageEnum.WhiteCell)
    {
      colorDisk = Color.Black;
    }

    using (Brush brushDisk = new SolidBrush(colorDisk))
    {
      g.FillEllipse(brushDisk, r);
    }

    Color diskBorderColor = Color.Black;

    //Uncomment this line in case you would like white disk to have white color border
    //diskBorderColor = colorDisk
    using (Pen penDisk = new Pen(new SolidBrush(diskBorderColor)))
    {
     g.DrawEllipse(penDisk, r);
    }
}
private void DrawDiskBorder(Graphics g, Rectangle rec)
{
    //Reduce the size of Rectangle
    Rectangle r = rec;
    r.Height -= 10;
    r.Width -= 10;
    r.X += 5;
    r.Y += 5;
    using (Pen penDisk = new Pen(DiskBorderColor))
    {
      g.DrawEllipse(penDisk, r);
    }
}
private void DrawBoard(Graphics g, Rectangle[] arrRac)
{
    //This method just draw 64 array of Rectangle objects.

    g.Clear(BoardColor);
    g.DrawRectangles(PenBorder, arrRac);
    Rectangle rectangleBorder = new Rectangle(0, 0, CellSize * 8, CellSize * 8);
    using(Pen penBigBorder=new Pen ( Color.Black, 4))
    {
      g.DrawRectangle(penBigBorder, rectangleBorder);
    }
}

棋盘信息

棋盘信息将显示玩家的图片和姓名以及每一方的磁盘数量。它们只是 上的一组控件FormGame,将在ReverseUI课堂上使用。

导航器控件与移动

这些只是FormGame将在ReverseUI 课堂上使用的控件。
Navigator控件由四个按钮组成,移动历史使用了一个容器tableLayoutPanel ,内含Link按钮

Theme.cs

这个类只包含控件颜色信息。

C#
public class Theme
{
    public Color ButtonBackColor { get; set; }
    public Color ButtonForeColor { get; set; }
    public Color LabelForeColor { get; set; }
    public Color FormBackColor { get; set; }
    public Color InputBoxBackColor { get; set; }
    public Color LinkLabelForeColor { get; set; }
    public Color LinkLabelActiveForeColor { get; set; }
    public Boolean IsFormCaptionDarkMode { get; set; } = true;

    public Color MenuBackColor { get; set; }
    public Color MenuHoverBackColor { get; set; }
    public Color MenuForeColor { get; set; }
}

这个程序支持DarkMode,我们使用Global.LightTheme

DarkTheme<br />
Then

,当加载每个表单时,它将访问Global.CurrentTheme,然后用于themeUtil
设置Controls的外观。

这是一个使用themUtil 对象的例子。

C#
themeUtil.SetLabelColor(theme, lblNumberofBlackDisk,
                        lblNumberofWhiteDisk,
                        lblPlayer1Name,
                        lblPlayer2Name)
         .SetButtonColor(theme, btnFirst,
                        btnNext,
                        btnPrevious,
                        btnLast)
         .SetMenu (this.menuStrip1)
         .SetForm(this);

FormGame.cs

这个类是主窗体,它包含移动PictureBoxBoard Navigator 控件和历史记录。

UI.Dialog.cs

显示对话框的所有代码都属于此处。

!!!主程序!!!

AI.Board.cs

这是保存板信息的类。

字段和属性

  • int[,] boardMatrix 我们使用简单的二维数组存储,Black =-1, Blank =0, White =1
  • BoardPhase具有三个阶段:BeginningMiddleEndGame。这个值将被 AI 使用,它将确定它需要哪一组分数值来评估棋盘分数。
  • NumberofLastFlipCell每次我们翻转单元格时,我们都会保留被翻转的单元格的数量
  • CurrentTurn 当前回合
  • generateMoves() 生成可用的移动
  • PutValue()只需输入单元格值
  • IsLegalMove()检查位置是否有效
  • SwitchTurn()只需切换转弯
  • IsTherePlaceToPut()false-如果位置已被占用;否则,它将返回true

这些是Board构造函数。

C#
public Board()
{
    boardMatrix = new int[8, 8];
    SetCell(3, 3, CellValue.White);
    SetCell(3, 4, CellValue.Black);
    SetCell(4, 3, CellValue.Black);
    SetCell(4, 4, CellValue.White);
    listPutPosition.Clear();
}

public Board(Board OriginalBoard)
{
    boardMatrix = new int[8, 8];
    Array.Copy(OriginalBoard.boardMatrix, this.boardMatrix,
               this.boardMatrix.Length);
    this.CurrentTurn = OriginalBoard.CurrentTurn;
    if (OriginalBoard.LastPutPosition != null)
    {
        this.SetLastPutPosition(OriginalBoard.LastPutPosition.Clone());
    }
}

AI.BoardValue.cs

此类包含板信息,当我们想要保存板或创建自定义板时。我们将值存储在此类中,然后将其序列化。

ReverseUI.cs

这个类实现了 IUI 接口,所以它必须有这些方法:

C#
缩小▲   
void RenderHistory();
    void RenderNumberofDisk(int WhiteDisk, int BlackDisk);
    void ShowBoardAtTurn(int NumberofTurn);
    void MoveBoardToNextTurn();
    void MoveBoardToPreviousTurn();
    void RenderBoard();
    void SetGame(KReversiGame game);
    void RemoveGame();
    void BlackPutCellAt(Position position);
    void WhitePutCellAt(Position position);
    void Initial();
    void ReleaseUIResource(); //To make sure there is no event leak
    void InformPlayer1NeedtoPass();
    void InformPlayer2NeedtoPass();
    void InformGameResult(KReversiGame.GameResultEnum result);

// Any event the begin with MoveBoard relate to navigation.
    event EventHandler MoveBoardToNextTurnClick;
    event EventHandler MoveBoardToPreviousTurnClick;
    event EventHandler MoveBoardToFirstTurnClick;
    event EventHandler MoveBoardToLastTurnClick;
    event PictureBoxBoard.PictureBoxCellClick CellClick;
    event EventInt MoveBoardToSpecificTurnClick;
    event EventHandler ContinuePlayingClick;

KReversiGame.cs

此类包含UIU对象和板对象。它充当这两个对象之间的粘合剂。
这意味着当我们点击一​​个单元格时,UI 部分将不会知道任何关于游戏数据的信息。
它不会检查此单元格是否为空或是否为有效位置,它只会通知游戏此特定单元格已被单击,然后游戏对象将决定下一步要做什么。

例如,游戏对象会检查它是否是一个有效的位置,然后它会调用一个棋盘对象来更新值,然后它会通知 UI 让他们知道它需要更新或者它会调用 UI 来更新自己直接。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是基于pygame实现黑白棋游戏AI代码,使用了Min-max算法和Alpha-Beta剪枝实现人工智能对手。代码中包含了游戏界面的绘制、游戏规则的判断、AI对手的实现等。 ```python import pygame import sys import time import random import copy # 初始化pygame pygame.init() # 设置游戏界面大小 size = width, height = 640, 640 # 设置棋盘格子大小 grid_size = 80 # 设置棋盘大小 board_size = 8 # 设置棋子半径 piece_radius = int(grid_size / 2 - 5) # 设置棋盘边缘留白大小 margin = int(grid_size / 2) # 设置棋盘颜色 board_color = (0, 128, 0) # 设置棋子颜色 black_color = (0, 0, 0) white_color = (255, 255, 255) # 设置字体 font = pygame.font.Font(None, 32) # 设置AI搜索深度 search_depth = 3 # 定义棋盘类 class Board: def __init__(self): self.board = [[0 for i in range(board_size)] for j in range(board_size)] self.board[3][3] = self.board[4][4] = 1 self.board[3][4] = self.board[4][3] = -1 # 获取当前棋盘状态 def get_board(self): return self.board # 获取当前棋盘上黑白棋子的数量 def get_piece_count(self): black_count = 0 white_count = 0 for i in range(board_size): for j in range(board_size): if self.board[i][j] == 1: black_count += 1 elif self.board[i][j] == -1: white_count += 1 return black_count, white_count # 判断当前位置是否可以落子 def is_valid_move(self, x, y, color): if self.board[x][y] != 0: return False directions = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)] for d in directions: if self.is_valid_direction(x, y, color, d): return True return False # 判断当前位置在某个方向上是否可以落子 def is_valid_direction(self, x, y, color, d): dx, dy = d x += dx y += dy count = 0 while x >= 0 and x < board_size and y >= 0 and y < board_size: if self.board[x][y] == -color: count += 1 x += dx y += dy elif self.board[x][y] == color: if count > 0: return True else: return False else: return False return False # 落子 def make_move(self, x, y, color): self.board[x][y] = color directions = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)] for d in directions: self.make_flips(x, y, color, d) # 在某个方向上翻转棋子 def make_flips(self, x, y, color, d): dx, dy = d x += dx y += dy count = 0 while x >= 0 and x < board_size and y >= 0 and y < board_size: if self.board[x][y] == -color: count += 1 x += dx y += dy elif self.board[x][y] == color: for i in range(count): x -= dx y -= dy self.board[x][y] = color break else: break # 获取当前可以落子的位置列表 def get_valid_moves(self, color): valid_moves = [] for i in range(board_size): for j in range(board_size): if self.is_valid_move(i, j, color): valid_moves.append((i, j)) return valid_moves # 判断游戏是否结束 def is_game_over(self): black_count, white_count = self.get_piece_count() if black_count == 0 or white_count == 0 or black_count + white_count == board_size * board_size: return True if len(self.get_valid_moves(1)) == 0 and len(self.get_valid_moves(-1)) == 0: return True return False # 获取当前棋盘上黑白双方的得分 def get_score(self): black_score = 0 white_score = 0 for i in range(board_size): for j in range(board_size): if self.board[i][j] == 1: black_score += 1 elif self.board[i][j] == -1: white_score += 1 return black_score, white_score # 定义游戏类 class Game: def __init__(self): self.board = Board() self.current_player = 1 self.game_over = False # 切换当前玩家 def switch_player(self): self.current_player = -self.current_player # 处理鼠标点击事件 def handle_click(self, x, y): if self.current_player == 1: if self.board.is_valid_move(x, y, self.current_player): self.board.make_move(x, y, self.current_player) self.switch_player() self.check_game_over() # 处理AI落子 def ai_move(self): valid_moves = self.board.get_valid_moves(self.current_player) if len(valid_moves) > 0: best_move = self.get_best_move() self.board.make_move(best_move[0], best_move[1], self.current_player) self.switch_player() self.check_game_over() # 获取最佳落子位置 def get_best_move(self): return self.minimax(self.board, search_depth, self.current_player) # Min-max算法 def minimax(self, board, depth, color): if depth == 0 or board.is_game_over(): return None, self.evaluate(board, color) if color == 1: best_score = -float('inf') best_move = None for move in board.get_valid_moves(color): new_board = copy.deepcopy(board) new_board.make_move(move[0], move[1], color) score = self.minimax(new_board, depth - 1, -color)[1] if score > best_score: best_score = score best_move = move else: best_score = float('inf') best_move = None for move in board.get_valid_moves(color): new_board = copy.deepcopy(board) new_board.make_move(move[0], move[1], color) score = self.minimax(new_board, depth - 1, -color)[1] if score < best_score: best_score = score best_move = move return best_move, best_score # Alpha-Beta剪枝 def alphabeta(self, board, depth, color, alpha, beta): if depth == 0 or board.is_game_over(): return None, self.evaluate(board, color) if color == 1: best_score = -float('inf') best_move = None for move in board.get_valid_moves(color): new_board = copy.deepcopy(board) new_board.make_move(move[0], move[1], color) score = self.alphabeta(new_board, depth - 1, -color, alpha, beta)[1] if score > best_score: best_score = score best_move = move alpha = max(alpha, best_score) if alpha >= beta: break else: best_score = float('inf') best_move = None for move in board.get_valid_moves(color): new_board = copy.deepcopy(board) new_board.make_move(move[0], move[1], color) score = self.alphabeta(new_board, depth - 1, -color, alpha, beta)[1] if score < best_score: best_score = score best_move = move beta = min(beta, best_score) if alpha >= beta: break return best_move, best_score # 评估当前棋盘状态 def evaluate(self, board, color): black_score, white_score = board.get_score() if color == 1: return black_score - white_score else: return white_score - black_score # 检查游戏是否结束 def check_game_over(self): if self.board.is_game_over(): self.game_over = True # 绘制游戏界面 def draw(self, screen): screen.fill(board_color

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

StubbornZorro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值