Phân tích bài toán

Một phần của tài liệu GIẢI THUẬT TÌM KIẾM MINIMAX VÀ ỨNG DỤNG TRONG CÁC TRÒ CHƠI CÓ TỔNG BẰNG KHÔNG (Trang 51)

3.1.1 Trò chơi

Trò chơi N quân hậu bắt nguồn từ bài toán rất nổi tiếng là Bài toán 8 con hậu:

đặt lần lượt 8 con hậu lên bàn cờ quốc tế (kích thước 8x8) sao cho không có 2 con hậu nào khống chế lẫn nhau. Do vậy, người ta có khá nhiều cách định nghĩa về trò chơi N quân hậu, điển hình là 3 cách sau:

1) Cho bàn cờ quốc tế có kích thước N x N, 2 người chơi lần lượt đặt từng quân hậu lên bàn cờ sao cho không có 2 quân hậu nào khống chế nhau. Người chơi nào không đặt được quân hậu lên bàn cờ nữa là người thua cuộc.

2) Cho bàn cờ quốc tế có kích thước N x N, 2 người chơi lần lượt đặt từng quân hậu lên bàn cờ sao cho không có 2 quân hậu nào khống chế nhau cho đến khi không đặt được nữa. Mỗi quân hậu đặt lên bàn cờ sẽ khống chế các ô nằm trong 8 hướng đi của nó nếu ô đó còn chưa bị khống chế. Ai khống chế được nhiều ô hơn là người thắng cuộc.

3) Cho bàn cờ quốc tế kích thước N x N. Người đi đầu tiên sẽ đặt a quân hậu lên bàn cờ. Người chơi thứ 2 tiếp tục đặt b con hậu lên bàn cờ (a > b và a + b ≤ N) sao cho không có 2 quân hậu nào khống chế lẫn nhau. Sau đó người chơi thứ nhất sẽ di chuyển các con hậu của mình để ăn các con hậu của đối phương, người chơi thứ 2 có nhiệm vụ phòng thủ, di chuyển các con hậu của mình để đối phương không bắt được. Sau k nước đi nếu người chơi thứ nhất ăn được hết b con hậu của đối phương thì người chơi thứ nhất thắng, ngược lại người chơi thứ 2 thắng.

Sau khi cân nhắc, cách phát biểu trò chơi thứ 2 được chọn để tiến hành viết ứng dụng cho luận văn. Tuy nhiên, để cho trò chơi hấp dẫn hơn và công bằng hơn chúng ta bổ sung thêm một số luật chơi.

Phát biểu trò chơi (bài toán) như sau:

Cho bàn cờ có kích thước N x N (3 ≤ N ≤ 15), trên đó có sẵn m ô đá ( hay có thể hiểu là chướng ngại vật) (0 ≤ m ≤ (N - 2)2) nằm ở các vị trí ngẫu nhiên, các ô còn lại là ô trống.

Hai người chơi lần lượt đặt từng con hậu lên bàn cờ sao cho không có 2 con hậu nào khống chế lẫn nhau. Hai con hậu khống chế nhau nếu chúng nằm trên đường đi của nhau và không có ô đá nào nằm trên đường đi ấy.

Mỗi con hậu đặt lên bàn cờ sẽ khống chế các ô nằm trên 8 đường đi của nó nếu ô đó còn trống, chưa bị khống chế bởi con hậu nào khác.

Trò chơi kết thúc khi không đối thủ nào đặt được quân hậu lên bàn cờ nữa. Người nào khống chế được nhiều ô hơn là người thắng cuộc.

Hình 3.1 dưới đây minh họa hai trường hợp: Hai con hậu không khống chế nhau và hai con hậu khống chế nhau:

Hình 3.1

(a) Hai con Hậu không khống chế nhau (b) Hai con Hậu khống chế nhau

Trong hình (a) Con Hậu xanh ở vị trí (2,3) đặt trước do đó nó khống chế được 5 ô, con Hậu màu vàng đặt sau nên chỉ khống chế được 3 ô.

3.1.2 Cơ sở lý thuyết

Theo như phát biểu trò chơi ở trên, ta có thể thấy trò chơi N quân hậu thuộc lớp các trò chơi đối kháng giữa hai người chơi. Cụ thể trò chơi này thuộc dạng trò chơi có tổng bằng không với hai người chơi (Two players, Zero-sum-game). Vì thế ta có thể áp dụng thuật toán tìm kiếm Minimax và thuật toán Alpha-beta trong trò chơi này.

Mặt khác nếu viết chương trình để người chơi với người thì chỉ cần xây dựng các tập luật chơi để người chơi không phạm quy và kết thúc khi có người thắng. Tuy nhiên như vậy lại không ứng dụng và kiểm tra được thuật toán trong luận văn này. Vì vậy chúng ta sẽ viết chương trình cho Người chơi với Máy, trong trường hợp này ta phải tính toán như thế nào để khả năng máy tính thắng cao hơn người.

Máy tính có lợi thế là khả năng tính toán nhanh, khả năng nhớ tốt gấp nhiều lần so với con người, vậy để máy tính thắng thì thật dễ, vấn đề là làm sao để máy có thể nghĩ được như người? Và có thể tính trước được ít nhất là 4 đến 5 nước. Vậy phải có thuật toán để máy có thể quét qua các phương án đi, và chọn phương án cuối cùng là tốt nhất sao cho máy tính có thể thắng. Trong ứng dụng này ta chọn thuật toán Minimax và thuật toán cải tiến Alpha-beta (chương 2) để cài đặt cho máy tính chơi.

3.2 Cài đặt chương trình

Chương trình được cài đặt bằng ngôn ngữ C# (.NET Framework 2.0). Ngoài ra còn sử dụng thêm một số phầm mềm như: PowerCHM, Photoshop và phần mềm tạo file icon.

3.2.1 Cấu trúc chương trình và mối quan hệ giữa các lớp chính

Chương trình được thiết kế theo mô hình Hướng đối tượng(Object-Oriented) để tiện cho việc cải tiến về sau. Do chương trình mới được phát triển nên cấu trúc còn đơn giản, chỉ có 3 lớp chính là các lớp Form1 (tên lớp mặc định do C# đặt cho

form), lớp CBoard và lớp gameAI. Trong đó lớp gameAI là lớp được áp dụng thuật toán trong chương 2 của chúng ta.

Mối quan hệ giữa 3 lớp thể hiện qua sơ đồ các lớp trong hình 3.2 như sau:

Hình 3.2: Sơ đồ thể hiện mối liên quan giữa 3 lớp chính. Trong đó:

- Lớp Form1 có thuộc tính mb thuộc lớp CBoard.

- Lớp CBoard có thuộc tính form thuộc lớp Form1.

- Lớp CBoard có thuộc tính machine thuộc lớp GameAI. - Lớp GameAI có thuộc tính ownBoard thuộc lớp CBoard. (adsbygoogle = window.adsbygoogle || []).push({});

Các lớp tương tác với nhau thông qua các bước sau:

1. Lớp Form1 truyền thông tin nhận được từ người chơi cho lớp CBoard, lớp

CBoard lấy được thông tin thông qua thuộc tính form.

2. Lớp Cboard xử lý dữ liệu thu được từ lớp Form1 và truyền cho lớp GameAI, lớp

GameAI thu được thông qua thuộc tính ownBoard .

3. Lớp GameAI sau khi nhận được thông tin sẽ xử lý và truyền lại cho lớp Cboard, lớp Cboard nhận được thông qua thuộc tính machine.

1

2

4. Lớp Cboard nhận được thông tin từ lớp GameAI, xử lý và truyền lại cho lớp

Form1 thông qua thuộc tính mb. Lớp form1 xử lý lại thông tin và điều chỉnh giao

diện của form.

Sau đây chúng ta sẽ xem xét cấu trúc và nhiệm vụ của 3 lớp trên.

3.2.2 Lớp Form1

Lớp này thể hiện giao diện của trò chơi, làm nhiệm vụ giao tiếp với người chơi. Các thao tác chính của lớp này là:

- Nhận thông tin khởi tạo một ván chơi từ người sử dụng như: kích thước bàn cờ, trình độ của máy, số ô đá (hay vật cản) sử dụng, ai là người đi trước để cung cấp thông tin cho lớp CBoard khởi tạo một ván chơi mới.

- Ghi nhận thông tin về nước đi của người chơi mỗi khi người chơi nhấp chuột lên một ô trống rồi truyền cho lớp CBoard xử lý.

- Cập nhật nước đi của người và máy lên màn hình.

- Hiển thị các thông tin về trận đấu như điểm của các đối thủ, các nước đã đi, ai là người đang đi…

- Thể hiện 1 số hiệu ứng khi người dùng di chuyển chuột trên bàn cờ để tiện cho người chơi suy nghĩ, đánh giá.

- Ngoài ra còn thực hiện xuất các kết quả của các ván chơi ra một file Excel để tiện theo dõi lại sau nhiều ván chơi.

Trong mục 3.3 chúng ta sẽ thấy được các thành phần cụ thể của lớp Form1.

3.2.3 Lớp CBoard

Lớp CBoard đóng vai trò như một bàn cờ trong thực tế. Lớp này chứa các thông tin về bàn cờ:

- Một mảng 2 chiều lưu trữ trạng thái từng ô của bàn cờ.

- Kích thước bàn cờ, người đi trước, số ô đá trên bàn cờ, thời gian và trình độ suy nghĩ của máy, điểm của 2 đối thủ.

- Lưu trữ thông tin về nước đi hiện tại: ai đang đi, là nước đi thứ mấy… Các phương thức chính của lớp Cboard như sau:

- Khởi tạo một ván chơi mới.

- Kiểm tra một nước đi có hợp lệ hay không? - Ghi nhận một nước đi.

- Thực hiện nước đi của người chơi. - Yêu cầu máy thực hiện nước đi.

3.2.4 Lớp gameAI (adsbygoogle = window.adsbygoogle || []).push({});

Có thể nói lớp gameAI là lớp trung tâm của các chương trình trò chơi đối kháng. Lớp này chứa các phương thức phục vụ cho việc quyết định đi một nước của máy. Sau đây chúng ta sẽ xem xét chi tiết các thuộc tính và phương thức của lớp này.

Hình 3.3: Cấu trúc lớp gameAI

Trong lớp gameAI có các thuộc tính đáng chú ý là:

- Mảng 2 chiều board[,] chứa trạng thái của bàn cờ hiện tại. o board[i, j] = -100 nếu ô đó là ô đá.

o board[i, j] = -x tức là ô (i, j) chứa con hậu được đặt ở nước thứ x. o board[i, j] = x nếu ô (i, j) bị khống chế bởi con hậu đặt ở nước thứ x.

- Mảng listCell[] chứa danh sách các ô sẽ bị khống chế nếu ta đặt con hậu ở một ô nào đó

- 2 mảng dx, dy thể hiện 8 hướng đi có thể của quân hậu.

- ownBoard là một thể hiện( đối tượng) của lớp cBoard, chứa bàn cờ chúng ta đang chơi và các thông tin về nó như kích cỡ, trình độ máy v.v…..

- ownedCell: ghi nhận số ô đã bị khống chế trên bàn cờ

- totalCell: tổng số ô trống trên bàn cờ lúc đầu = tổng số ô – số ô đá. - startTime: ghi nhận thời gian bắt đầu suy nghĩ của máy.

- resX, resY: ghi lại nước đi máy sẽ đi.

- bestValue ghi nhận giá trị tốt nhất nếu thực hiện nước đi (resX, resY).

Các phương thức của lớp gameAI

- Lớp chỉ có một phương thức có thuộc tính public là phương thức requestMove(). Phương thức này có đầu vào là một trạng thái của bàn cờ, trả lại giá trị (resX, resY) là ô mà máy sẽ đặt quân hậu. Để tìm kiếm được nước đi tốt nhất, phương thức requestMove sử dụng các phương thức hỗ trợ là:

- AlphaBeta: thực hiện tìm kiếm theo thuật toán Alpha-beta. - Minimax: thực hiện tìm kiếm theo thuật toán Minimax. - Hàm eval: lượng giá thế cờ hiện tại.

- Phương thức heuristicGenerateMove: sinh heuristic các nước đi có thể.

- Phương thức doMove: thử thực hiện một nước đi được sinh bởi phương thức heuristicGenerateMove.

- Phương thức remove: bỏ thực hiện nước đi đã thử (như đã nói trong thuật toán ở chương 2).

Sau đây chúng ta sẽ xem xét các phương thức của lớp gameAI một cách chi tiết.

Function Minimax(depth): integer; Begin

If (đã quá thời gian suy nghĩ) then

return –INFINITY; {dừng không duyệt nữa}

if (depth=0) or (không thể đi được nước nào nữa) then

return eval(depth); {lượng giá ngay thế cờ hiện tại và kết thúc}

best = -INFINITY;

pMove = heuristicGenerateMove; {sinh các nước đi có thể} while (còn lấy được nước đi m trong pMove)do

begin

Thực hiện nước đi m;

value = -Minimax(depth - 1); Bỏ thực hiện nước đi m; if (value > best) then

begin (adsbygoogle = window.adsbygoogle || []).push({});

best := value;

if (đây là nước đi đầu tiên của máy) then

cập nhật nước đi resX, resY;

end;

end; return best; End;

Phương thức Alphabeta

Function AlphaBeta(Alpha, beta, depth): integer; Begin

If (đã quá thời gian suy nghĩ) then

return –INFINITY; {dừng không duyệt nữa}

if (depth=0) or (không thể đi được nước nào nữa) then

return eval(depth); {lượng giá ngay thế cờ hiện tại và kết thúc}

best = -INFINITY;

pMove = heuristicGenerateMove; {sinh các nước đi có thể} while (còn lấy được nước đi m trong pMove) and (best < beta) do begin

if (best > Alpha) then Alpha := best; Thực hiện nước đi m;

value = -AlphaBeta(-beta, -Alpha, depth - 1); Bỏ thực hiện nước đi m;

if (value > best) then

begin

best := value;

if (đây là nước đi đầu tiên của máy) then

cập nhật nước đi resX, resY;

end;

end; return best; End;

Phương thức sinh nước đi heuristicGenerateMove

Trong chương 2 khi đánh giá thuật toán Alpha-beta ta đã nhận xét: thuật toán hoạt động càng hiệu quả khi sự thu hẹp cửa sổ Alpha và Beta càng nhanh. Cửa sổ này được thu hẹp một bước khi gặp một giá trị mới tốt hơn giá trị cũ. Khi gặp giá trị tốt nhất thì cửa sổ này thu hẹp nhất. Do đó nếu càng sớm gặp giá trị tốt nhất thì cửa sổ càng chóng thu hẹp. Như vậy phải làm sao cho các nút ở lá được sắp xếp theo trật tự từ cao xuống thấp. Trật tự này càng tốt bao nhiêu thì thuật toán chạy càng nhanh bấy nhiêu.

Tuy vậy, do giới hạn về thời gian tính toán và sự bùng nổ tổ hợp, ta không thể liệt kê hết các nút lá để sắp xếp được. Do đó, ta chỉ áp dụng cách sinh các nước đi theo một tiêu chuẩn mà ta cho là tốt, phù hợp với đặc điểm trò chơi đang xét như sau:

- Ta thấy, nếu không có ô đá thì nước đi đầu tiên vào ô trung tâm bao giờ cũng chiếm được nhiều ô trống nhất. Do đó, ta sẽ ưu tiên xét các ô theo thứ tự từ ô trung tâm tỏa ra các ô xung quanh (tất nhiên là trừ ô đá!)

- Một cách cảm tính, ta có thể nhận thấy đi vào các ô có số ô trống trong 8 ô xung quanh nó lớn thì có vẻ có lợi hơn.

Như vậy ta có thể sinh các nước đi sắp tới theo hướng heuristic như sau: tìm tất cả các ô còn trống. với mỗi ô, đếm số ô trống xung quanh nó, cho tất cả vào một danh sách. Tiến hành sắp xếp danh sách theo thứ tự giảm dần của số ô trống xung quanh. Nếu hai ô có cùng số ô trống xung quanh thì ưu tiên ô gần trung tâm hơn. Giải thuật được cài đặt như sau:

Procedure generateHeuristicMove(var pMove); begin

for i := 1 to boardSize do for j := 1 to boardSize do (adsbygoogle = window.adsbygoogle || []).push({});

if board[i, j] = 0 then {nếu ô (i, j) còn là ô trống} begin

k := số ô trống xung quanh ô (i, j); đặt bộ 3(i, j, k) vào mảng pMove; end;

sắp xếp mảng pMove theo tiêu chí ở trên; End;

Hàm đếm số ô trống xung quanh ô (i, j) được thực hiện đơn giản, chỉ cần kiểm tra 8 ô xung quanh nó. Lưu ý trường hợp ô (i, j) ở biên của bàn cờ, ta phải sử dụng kỹ thuật lính canh: đặt bên ngoài biên của bàn cờ là các ô đá để tránh trường hợp tràn mảng.

Phương thức lượng giá eval

Do đặc điểm của trò chơi, hàm lượng giá được thực hiện như sau: Ta chỉ việc tính hiệu số giữa số ô người chơi chiếm giữ và số ô máy chiếm giữ. Tham số depth được đưa vào để biết được đối thủ nào gọi hàm lượng giá này.

Function eval(depth): integer; begin

for i := 1 to boardSize do for j := 1 to boardSize do

if ô (i,j) bị người chơi chiếm then tăng số ô người chiếm lên 1

else

if ô (i,j) bị máy chiếm then tăng số ô máy chiếm lên 1;

if (depth mod 2 = 0) {nếu người chơi gọi hàm lượng giá} return (số ô máy chiếm – số ô người chiếm)

else return (số ô người chiếm – số ô máy chiếm); End;

Phương thức doMove

Phương thức doMove nhận tham số đầu vào là (i, j): chỉ số ô sẽ đi và x là số thứ tự của nước đi. Phương thức thực hiện như sau: đánh dấu ô sẽ đi là –x. Từ ô (i, j) ta đi lần lượt theo 8 hướng có thể đến khi nào ra biên hoặc gặp ô đá. Với mỗi ô trên đường đi, nếu còn là ô trống thì đánh dấu ô đó mang giá trị x. Trong quá trình đó, cập nhật lại giá trị số ô đã bị chiếm.

Phương thức remove

Phương thức remove có tham số đầu vào là x: số thứ tự của nước đi. Phương thức thực hiện bỏ nước đi được tạo bởi hàm doMOVE. Phương thức đơn giản chỉ quét các ô trên bàn cờ, nếu giá trị tuyệt đối của ô đó bằng x thì ta gán lại giá trị bằng 0, đồng thời giảm giá trị số ô bị chiếm đi 1.

Mã nguồn lớp gameAI: using System; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Collections; namespace n_queens {

// xay dung lop diem moi dua vao lop point da co class myPoint

{

public Point pt; int val;

public myPoint(int x, int y, int startVal) {

pt = new Point(x, y); val = startVal;

}

//Tính khoang cách giữa điểm a và điểm trung tâm static double distance(myPoint a, double mid) {

return Math.Sqrt(Math.Pow(a.pt.X - mid, 2)+Math.Pow(a.pt.Y - mid, 2));

}

// Phương thức tĩnh so sánh khoang cach giua hai diem voi o trung tam

public static int compare(myPoint a, myPoint b, float mid) {

if (a.val > b.val) return -1; else if (a.val < b.val) return 1; else (adsbygoogle = window.adsbygoogle || []).push({});

{

double d1 = distance(a, mid); double d2 = distance(b, mid); if (d1 < d2) return -1; else if (d1 > d2) return 1; else return 0; } } }

class CompareInv : IComparer// lop Icomparer co 1 phuong thuc compare

Một phần của tài liệu GIẢI THUẬT TÌM KIẾM MINIMAX VÀ ỨNG DỤNG TRONG CÁC TRÒ CHƠI CÓ TỔNG BẰNG KHÔNG (Trang 51)