Minisweeper (của Windows) có phần giao diện chính là một bảng các ô vuông xếp liền nhau tạo thành một hình chữ nhật có chiều rộng và dài tối thiểu là 9 ô (đơn vị là ô vuông) và số mìn tối thiểu là 10.
Trong bảng này sẽ có các ô được đặt mìn ngẫu nhiên và nhiệm vụ của người chơi là mở tất cả các ô không có mìn bằng cách click chuột trái vào các ô đó, khi chỉ còn các ô có mìn còn lại thì kết thúc màn chơi.
Các trạng thái của một ô
Tùy theo quá trình khởi tạo và thao tác của người dùng mà các ô trong bảng có thể có một hoặc vài trạng thái trong các trạng thái sau:
- Có mìn: được đặt ngẫu nhiên lúc khởi tạo
- Đã mở: Khi người dùng nhấn chuột trái vào ô
- Được cắm cờ: Khi người dùng nhấn phải vào ô
- Được đánh dấu: Khi nhấn phải vào ô đã được “cắm cờ”
- Bình thường: không có tất cả các trạng thái trên
Các trường hợp khi mở một ô
Khi mở một ô X nào đó, có 3 trường hợp có thể xảy ra:
X có mìn: hiện tất cả mìn trong bảng ra và ‘game over’.
X không có mìn nhưng 8 ô xung quanh có mìn: hiện số mìn xung quanh vào X.
X không có mìn và xung quanh cũng không có mìn: mở lần lượt các ô xung quanh X cho đến khi gặp các trường hợp 1 và 2.
Cắm cờ và đánh dấu
Minisweeper cho phép bạn đánh dấu các ô nghi ngờ có mìn bằng cách “cắm cờ” và “đánh dấu”. Khi bạn “cắm cờ”, tức là bạn xác định rằng ô đó có mìn và ô đó được hiển thị là một lá cờ. Bạn không thể mở ô đó bằng chuột trái được. Bạn chỉ có thể hủy bỏ trạng thái “cắm cờ” bằng cách click chuột phải, tùy theo thiết lập mà ô đó sẽ chuyển sang trại thái “đánh dấu” hoặc “bình thường”.
Khi bạn “đánh dấu”, tức là bạn đoán rằng ô đó có thể có mìn nhưng không chắc chắn.
Xây dựng chương trình
Như các project trước tôi đã làm, ta sẽ tách các phần logic và UI và ra để dễ nâng cấp khi cần thiết.
Phần Bussineses
Phần này tôi thiết kế một lớp Cell và lớp MinesBoard đại diện cho ô và bảng mìn trong trò chơi.
Lớp Cell dựa trên các trạng thái của một ô, bạn có thể sử dụng enum thay cho class, trong bài này tôi sử dụng class để người đọc dễ hiểu. Lớp này chỉ bao gồm các field (bạn có thể dùng Property cho “máy móc”) sau:
Mã:
public class Cell{ public bool IsMine = false; public bool IsOpened = false; public bool IsFlag = false; public bool IsMarked = false; public int MinesAround = 0;}
Lớp MinesBoard sẽ chứa một mảng hai chiều các đối tượng kiểu Cell, ngoài ra còn chứa dữ liệu cần thiết khác như số dòng, số cột, số mìn, số lá cờ được cắm, số ô đã được mở và hai trạng thái thắng, thua.
Note: Trong bài này, tôi dùng Rows và Cols thay cho Height và Width để tránh nhầm lẫn khi làm việc với mảng hai chiều. Bạn có thể hiểu Rows là Height và Cols là Width.
Việc khởi tạo MinesBoard bạn sẽ lặp vào random đánh dấu trạng thái IsMine của các Cell là true:
Mã:
private void InitBoard(){ // […] while (count < _MinesCount) { int index = rnd.Next(_CellsCount); int r = index / _Cols; int c = index % _Cols; if (!_cells[r, c].IsMine) { _cells[r, c].IsMine = true; count++; } }}
Tiếp đến ta cần viết một phương thức để “mở” một ô trong bảng, như đã giới thiệu trong phần luật chơi, bạn phải kiểm tra các trạng thái của ô đó và quyết định sẽ làm gì. Trong trường hợp thứ 3 bạn phải dùng đệ quy để duyệt và mở các ô xung quanh. Nhưng trước khi viết phương thức này, ta hãy xem một chút về phương pháp duyệt các ô để đếm số mìn quanh một ô.
Mã:
private int CountAroundMines(int row, int col){ int count = 0; int r1 = row == 0 ? 0 : -1; int c1 = col == 0 ? 0 : -1; int r2 = row == _Rows-1 ? 1 : 2; int c2 = col == _Cols-1 ? 1 : 2; for (; r1 < r2; r1++) for (int j=c1; j < c2; j++) { if (_cells[row + r1, col + j].IsMine) count++; } return count;}
Như bạn thấy trong đoạn mã trên thì r1 và c1 tương ứng cho giá trị của dòng và cột đầu tiên, r2 và c2 cho dòng và cột cuối cùng ta sẽ lặp qua. Đây chỉ là vị trí tương đối và nếu ô đang xét không nằm ở biên của bảng thì r1 và c1 sẽ có giá trị là -1, r2 và c2 có giá trị là 2. Tức là bạn có thể lặp tối đa từ -1 đến 1, cộng các giá trị này với vị trí row và col để duyệt tất cả 9 ô xung quanh ô hiện tại.
(Ở đây bạn có thể thêm phần kiểm tra nếu r1=0, j=0 (trong đoạn mã trên) tức là ô duyệt đến là ô hiện tại, bạn có thể bỏ qua không xét ô này)
Bạn đã biết cách cách lặp qua các ô xung quanh một ô, bây giờ là phần viết lệnh để mở một ô. Hãy xem hình minh họa sau để hiểu cách thức mà mã lệnh của chúng ta sẽ thực hiện:
Mã:
/// <summary>/// /// </summary>/// <param name="row"></param>/// <param name="col"></param>/// <returns>true nếu trúng mìn</returns>public bool OpenCell(int row, int col){ if (_cells[row, col].IsOpened || _cells[row, col].IsFlag) return false; _cells[row, col].IsOpened = true; if (_cells[row, col].IsMine) { return true; } _OpenedCellsCount++; // Đếm số mìn xung quanh và kiểm tra các trường hợp int count = CountAroundMines(row, col); if (count > 0) { _cells[row, col].MinesAround = count; } else { int r1 = row == 0 ? 0 : -1; int c1 = col == 0 ? 0 : -1; int r2 = row == _Rows - 1 ? 1 : 2; int c2 = col == _Cols - 1 ? 1 : 2; for (; r1 < r2; r1++) for (int j = c1; j < c2; j++) { OpenCell(row + r1, col + j); } } return false;}
Đó là những vấn đề chính của phần bussiness này, bạn có áp dụng dùng nó để tạo nên phần “nhân” cho một WindowsForms App hoặc Console App bất kì của trò chơi này.
Phần Presentation
Thông thường tôi sẽ tạo một UserControl để “tạo hình” cho lớp MinesBoard trên. Bạn có thể hiểu lớp MinesBoard trên là “hồn” và cần gắn vào một cái “xác” để nó có thể hoạt động được. Ở đây tôi đặt tên lớp này là MinesBoardUI. Tôi đặt tên giống nhau như vậy để bạn hiểu rằng không nên thiết kế và gắn thêm quá nhiều thứ vào lớp này ngoài những chức năng mà MinesBoard đã có sẵn.
Vậy ta chỉ cần đơn giản là tạo lớp này để nó vẽ ra một cái bảng đồng thời cập nhật những trạng thái của các ô thành hình ảnh để người dùng có thể thấy và thao tác được. Nếu bạn gắn thêm những thứ khác như button, label để thực thi lệnh gì đó hay để hiển thị điểm thì bạn phải thiết kế lại UserControl này mỗi lần muốn nâng cấp chương trình. Giả sử nó là một DLL ngoài để cho người khác dùng thì sẽ rất bất tiện.
Vẽ giao diện
Khi thiết kế lớp này, bạn chỉ cần tập trung vào hai phần chính: Hiển thị và xử lý thao tác của người dùng. Hai sự kiện tương ứng mà tôi chọn là Paint và MouseDown. Hãy override các sự phương thức tương ứng của hai sự kiện trên. Các đoạn code sau sẽ thay phần giải thích của tôi, bạn có thể thấy hơi dài và rối, tuy nhiên nó không phức tạp mà chỉ đơn giản là kiểm tra từng trạng thái của các ô nên rất dễ hiểu.
Mã:
protected override void OnPaint(PaintEventArgs e){ e.Graphics.FillRectangle(Brushes.LightGray, 0, 0, this.Width, this.Height); for (int i = 0; i < _board._Rows; i++) { int y = CELL_SIZE * i; for (int j = 0; j < _board._Cols; j++) { int x = CELL_SIZE * j; if (_board[i, j].IsOpened) { if (_board[i, j].IsMine) { e.Graphics.FillRectangle(Brushes.Red, x, y, CELL_SIZE, CELL_SIZE); e.Graphics.DrawImage(_imgBomb, x, y); } else if (_board[i, j].MinesAround > 0) { string s = _board[i, j].MinesAround.ToString(); SizeF size = e.Graphics.MeasureString(s, this.Font); e.Graphics.DrawString(s, this.Font, new SolidBrush(_foreColors[_board[i, j].MinesAround - 1]), x + (CELL_SIZE - size.Width) / 2, y + (CELL_SIZE - size.Height) / 2); } } else e.Graphics.DrawImage(_imgCell, x, y); if (_board._IsLost) { if (_board[i, j].IsMine) e.Graphics.DrawImage(_imgBomb, x, y); } if (_board[i, j].IsFlag) { e.Graphics.DrawImage(_imgFlag, x, y); } else if (_board[i, j].IsMarked) { string s = "?"; SizeF size = e.Graphics.MeasureString(s, this.Font); e.Graphics.DrawString(s, this.Font, Brushes.Black, x + (CELL_SIZE - size.Width) / 2, y + (CELL_SIZE - size.Height) / 2); } // vertical if(i==0) e.Graphics.DrawLine(Pens.Gray, x, 0, x, this.Height); } // hoz e.Graphics.DrawLine(Pens.Gray, 0, y, this.Width, y); } base.OnPaint(e);}
Có một phương thức của lớp Graphisc có thể bạn thắc mắc là MeasureString(). Đây là phương thức để lấy về kích thước của một chuỗi dựa trên Font dùng để viết chuỗi đó. Tôi dùng phương thức này để tính toán và vẽ để chuỗi hiển thị chính giữa ô.
Bạn có thể thấy là trong Minesweeper, mỗi một con số có giá trị khác nhau hiển thị trong ô vuông sẽ có màu sắc khác nhau. Ta làm việc này đơn giản bằng cách tạo một mảng các đối tượng Color, và truy xuất dựa theo giá trị của số:
Mã:
Color[] _foreColors = { Color.Blue,Color.Green,Color.Red,Color.Purple,Color.Peru, Color.PaleGreen,Color.Orchid,Color.Olive};
Để tạo một Brush để vẽ dựa theo màu, bạn dùng đối tượng SolidBrush. Như trong đoạn mã trên:
Mã:
e.Graphics.DrawString(s, this.Font, new SolidBrush(_foreColors[ số mìn của ô đang xét - 1]), x + (CELL_SIZE - size.Width) / 2, y + (CELL_SIZE - size.Height) / 2);
Xử lý thao tác người dùng
Mã:
protected override void OnMouseDown(MouseEventArgs e){ if (!_board._IsLost && !_board._IsFinish) { int c = e.X / CELL_SIZE; int r = e.Y / CELL_SIZE; if (_board[r, c].IsOpened) return; if (!_board[r, c].IsFlag && e.Button == MouseButtons.Left) { _board[r, c].IsMarked = false; if (_board.OpenCell(r, c)) { _board._IsLost = true; pictureBox1.Left = e.X - pictureBox1.Width / 2; pictureBox1.Top = e.Y - pictureBox1.Height / 2; pictureBox1.Visible = true; timer1.Enabled = true; // Dùng cho event OnMinesExplode(); } else { // Win if (RemainCellsCount == MinesCount) { _board._IsFinish = true; // Cắm cờ tất cả ô còn lại for (int i = 0; i < Rows; i++) { for (int j = 0; j < Cols; j++) { if (!_board[i, j].IsOpened) _board[i, j].IsFlag = true; } } } } Invalidate(); } else if (e.Button == MouseButtons.Right) { if (_board[r, c].IsMarked) { _board[r, c].IsMarked = false; } else { _board[r, c].IsFlag = !_board[r, c].IsFlag; _board[r, c].IsMarked = !_board[r, c].IsFlag; if (_board[r, c].IsFlag) _board._FlagsCount++; else _board._FlagsCount--; } Invalidate(); } // Dùng cho event OnCellClick(); } base.OnMouseDown(e);}
Thêm các Event
Để hoàn tất UserControl này, bạn cần thêm các event để từ Form ta có thể xử lý trong những trường hợp như đạp trúng mìn, mở một ô,... Đây là đoạn mã lệnh tương ứng để tạo các event cho lớp này. Bạn có thể thấy chúng được kích hoạt trong đoạn mã OnMouseDown trên:
Mã:
public event EventHandler CellClick;public event EventHandler MinesExplode; #region CustomEvent private void OnCellClick(){ if (CellClick != null) CellClick(this, null);}protected void OnMinesExplode(){ if (MinesExplode != null) MinesExplode(this, null);} #endregion
<font size="3"><font color="DarkOrange">Bài tập cho người đọc
Pallet nhựa Long An đã trở thành một trong những lựa chọn phổ biến cho nhu cầu vận chuyển và lưu trữ hàng hóa trong nhiều ngành công nghiệp. Với đặc tính nhẹ nhàng, chắc chắn và dễ vận chuyển, các...
Thanh lý pallet nhựa Long An giá rẻ