50 0 584KB
TỰ VIẾT CHƯƠNG TRÌNH CỜ TƯỚNG Tác giả: Phạm Mục lục Chương 1 Hồng Nguyên
Lời nói đầu (11/2002) Nhằm giúp các bạn yêu thích cờ Tướng máy tính có thêm tài liệu nghiên cứu, học tập, tôi xuất bản quyển sách này ở dạng miễn phí. So với phiên bản ban đầu (năm 1998) nó có một số sửa đổi và chỉnh lý, cắt bỏ phần dậy viết chương trình cờ Vua và các phụ lục (phụ lục chỉ là các chương trình mẫu). Một số phần của tài liệu đã được trích dẫn và đăng tải trên tạp chí PCWorld VN. Bạn có thể dùng (đọc, download, lưu trữ) tài liệu này cho mục đích sử dụng của cá nhân hoặc sao chép (copy) cho người khác nhưng không được phép: sửa đổi (bất cứ câu chữ, hình vẽ nào), kinh doanh (dưới bất cứ hình thức nào), đăng tải lại ở trang web khác (trích dẫn là được phép và phải tuân theo các qui định về trích dẫn), xuất bản lại ở bất cứ dạng nào. (Một số ngoại lệ có thể được xem xét riêng). Hi vọng, với các điều khoản dễ và “fair” này sẽ được các bạn tuân thủ nghiêm chỉnh. Một điều lưu ý nữa là tài liệu này được cung cấp ở dạng AS IT (có thế nào xin dùng thế ấy) và sẽ không được kèm thêm các bài giảng hay các trợ giúp. Tôi có cung cấp địa chỉ email, nhưng chủ yếu là để nhận những phản hồi có ích cho mọi người như những góp ý, phê bình - xin đừng lạm dụng nó. Những yêu cầu cá nhân như xin chương trình (bạn hãy tự download lấy hoặc xin bạn bè), xin giảng thêm về thuật toán hoặc giải thích riêng cho một chi tiết kỹ thuật nào đó nói chung sẽ không được đáp ứng. Bạn cần phải tự suy nghĩ, nhờ bạn bè, tìm kiếm câu trả lời có sẵn hoặc hỏi ở các diễn đàn, bạn bè. Mặc dù tôi cung cấp các chương trình mẫu và quyển sách này ở dạng miễn phí nhưng điều đó không có nghĩa là toàn bộ quĩ thời gian (vốn đã rất eo hẹp) và sức lực của tôi chỉ dành cho chúng - các bạn cần hiểu và thông cảm. Phạm Hồng Nguyên
Lời nói đầu (1998) Cờ Tướng và cờ Vua là các môn thể thao trí tuệ hấp dẫn và phổ biến trên thế giới. Tuy nhiên việc tìm hiểu và viết chương trình máy tính có thể chơi được các loại cờ này vẫn là các thách thức lớn, nhất là đối với các bạn trẻ. Đã có khá nhiều chương trình nguồn của cờ Vua cho tự do (trên Internet). Hầu hết các chương trình này chỉ được cho sau khi các tác giả đã phát triển chúng trong nhiều năm ròng. Ví dụ, bộ GNU Chess cho sau khi phát triển hơn 20 năm, bộ CRAFTY sau khoảng 10 năm. Nhiều chương trình trong số đó chơi rất hay, đạt thứ hạng cao trong làng cờ máy (ví dụ GNU có hệ số ELO 2350-2400, CRAFTY 2500-2700). Nhưng cũng vì vậy các chương trình
Chương 1 TRÒ CHƠI ĐỐI KHÁNG VÀ CÁC PHƯƠNG PHÁP TÌM KIẾM
I. Dạng trò chơi Trong phần này, ta sẽ xem cách một chương trình máy tính có thể chơi được các trò chơi đấu trí như các trò chơi cờ Vua, cờ Tướng, cờ vây, cờ caro (go-moku), go, checker... như thế nào. Các trò này còn gọi là các trò chơi đối kháng, diễn ra giữa hai đấu thủ. Nói chung, các trò chơi đó đều có thể chuyển về một dạng bài toán tìm kiếm đặc biệt: tìm đường đi đến các điểm cao nhất giữa hai đấu thủ. Đặc điểm của các trò chơi trên như sau: • • •
Có hai đấu thủ, mỗi người chỉ đi một nước khi tới lượt. Các đấu thủ đều biết mọi thông tin về tình trạng trận đấu. Trận đấu không kéo dài vô tận, phải diễn ra hòa, hoặc một bên thắng và bên kia thua.
Thông thường ta hay gọi các trò chơi này là các loại cờ. Đôi khi ta gọi đây là các trò chơi Minimax (dựa trên tên của thuật toán tìm kiếm cơ bản áp dụng cho chúng). Hình 1.1 là ví dụ về một số trò chơi nói trên. Các trò chơi như chơi bài, dò mìn, xúc sắc... không thuộc lớp trò chơi này.
II. Cây trò chơi Các trạng thái bàn cờ khác nhau (hay còn gọi là một thế cờ, tình huống cờ) trong quá trình chơi có thể biểu diễn thành một cây tìm kiếm (được gọi là cây trò chơi - hình 1.2) và ta sẽ tiến hành tìm kiếm trên cây để tìm được nước đi tốt nhất. Cây trò chơi có các nút của cây là các tình huống khác nhau của bàn cờ, các nhánh nối giữa các nút sẽ cho biết từ một tình huống bàn cờ chuyển sang tình huống khác thông qua chỉ một nước đi đơn nào đó. Dĩ nhiên, các nước đi này diễn ra theo cặp do hai đấu thủ lần lượt tiến hành. Độ sâu của cây trò chơi ply là số tầng của cây (chính là độ sâu d của cây). Thuật ngữ “nước đi” trong sách được thống nhất chỉ bao gồm một lần đi của một đấu thủ hoặc một lần đi phản ứng lại của đối thủ bên kia. Chú ý nó khác với thói quen dùng trong thực tế một nước đi bao gồm lần đi của ta và một lần đi của đối thủ. Nói cách khác, nước đi ở đây thực chất chỉ là "nửa nước" theo cách hiểu của làng cờ.
III. Vét cạn Dùng một thuật toán vét cạn để tìm kiếm trên cây trò chơi dường như là một ý tưởng đơn giản. Ta chỉ cần chọn nhánh cây sẽ dẫn tới nước thắng để đi quân là đảm bảo thắng lợi. Nếu đúng vậy, các loại cờ sẽ trở thành các trò chơi buồn tẻ, sẽ chẳng còn đâu những bí quyết huyền ảo thần kì và bàn cờ sẽ chẳng khác gì bàn... tính. Rất tiếc (hoặc rất may) rằng, cách làm này lại không thể thực hiện nổi do cái gọi là bùng nổ tổ hợp. Ví dụ, nếu từ một thế cờ, trung bình có khả năng đi được 16 nước đi khác nhau (ta gọi đó là hệ số nhánh con tại mỗi nút là b = 16). Như vậy, sau một tầng ta sẽ có 16 nút con, mỗi nút này lại có thể có 16 con nữa. Tổng số nút con ở độ sâu thứ hai là 16x16 = b2. Cứ như vậy ở độ sâu d sẽ có bd nút. Nếu giả sử độ sâu của cây là 100 (hệ số nhánh 16 và độ sâu 100 đều là những con số còn nhỏ hơn con số thường gặp trong trò chơi cờ), thì số nhánh phải duyệt lên đến 16100 hay xấp xỉ 10120 - một con số lớn khủng khiếp. Để hình dung số đó lớn thế nào, ta giả sử tất cả các nguyên tử trong vũ trụ đều trở thành máy tính để tính nước đi với tốc độ một giây tính được cỡ 1010 (10 tỷ) nước đi, và nếu chúng hoạt động cật lực từ thời vụ nổ lớn đến nay (theo một số lý thuyết, thì thế giới này hình thành sau một vụ nổ gọi là vụ nổ lớn bigbang, trước đây cỡ 15 tỷ năm) thì đến bây giờ mới có thể đi được nước đi đầu tiên. Vì số các khả năng tăng quá nhanh, chỉ có một số ít những vấn đề đơn giản là thích hợp với kiểu tìm kiếm vét hết mọi khả năng này (kiểu tìm kiếm vét cạn đòi hỏi phải kiểm tra
tất cả các đỉnh). Do đó, các phương pháp tìm kiếm khác đã ra đời và phát triển. Ngược lại, nếu có một phương pháp luôn luôn chính xác nhằm đánh giá một thế cờ này là tốt hay kém so với thế kia, thì trò chơi trở thành đơn giản bằng cách chọn nước đi dẫn tới thế cờ tốt nhất. Do đó sẽ không cần phải tìm kiếm gì nữa. Rất tiếc, các thủ tục như vậy không hề có. Ta cần có chiến lược tìm kiếm trong trò chơi.
IV. Chiến lược tìm kiếm trong trò chơi Một chiến lược thường được cả người lẫn máy dùng là phân tích thế cờ chỉ sau một số nước đi nào đó của cả hai bên. Sau khi "nhìn xa" xem bàn cờ có những khả năng biến đổi như thế nào sau một số nước, ta sẽ đánh giá độ xấu tốt của các thế cờ nhận được. Tiếp theo, ta sẽ chọn nước đi sẽ dẫn tới một thế cờ tốt nhất trong số đó có cân nhắc đến cách đi của cả hai bên. Với máy, thế cờ này được đánh giá là tốt hơn thế cờ kia nhờ so sánh điểm của thế đó do bộ lượng giá trả lại. Chúng ta chỉ có khả năng xét trước một số hữu hạn các nước (ví dụ đại kiện tướng chơi cờ vua có thể xét trước 8-10 nước đi, người thường chỉ 24 nước đi). Rõ ràng là nếu xét càng sâu thì chơi càng giỏi. Nhưng không thể thực hiện điều này với độ sâu quá lớn được do số nút ở độ sâu đó có thể trở nên lớn khủng khiếp và không đủ thời gian để phân tích. Nếu dừng ở một độ sâu hợp lý thì bộ phân tích có thể hoàn thành việc tính toán trong một thời gian hạn định.
V. Thủ tục Minimax[1] Giả sử chúng ta có một bộ phân tích thế cờ có thể áp dụng tất cả các luật, các phương pháp đánh cờ khác nhau vào từng thế cờ và chuyển đổi chúng thành một con số đại diện (cho điểm thế cờ). Mặt khác, ta giả sử con số đó là dương khi áp dụng cho thế cờ của một đấu thủ (được gọi là người chơi cực đại - maximizer), và là âm khi áp dụng cho đấu thủ bên kia (được gọi là người chơi cực tiểu - minimizer). Quá trình tính toán cho điểm thế
cờ được gọi là lượng giá tĩnh (static evaluation). Hàm thực hiện việc tính toán được gọi là một bộ lượng giá tĩnh, và giá trị nhận được gọi là điểm lượng giá tĩnh. Cả hai đấu thủ đều cố gắng đi như thế nào đó để đạt được điểm tuyệt đối lớn nhất. Người chơi cực đại sẽ tìm những nước đi dẫn đến điểm của mình trở nên lớn hơn (hay cao nhất có thể được) hay điểm của đối thủ bớt âm hơn (nhỏ hơn về giá trị tuyệt đối). Còn đấu thủ của anh ta, người chơi cực tiểu, lại ra sức phản kháng lại, để dẫn tới điểm âm của anh ta âm hơn hay điểm dương của đối thủ nhỏ đi (hình 1.4).
Ví dụ một phần cây trò chơi trong hình 1.5.
Người chơi cực đại hi vọng chọn nước đi bên phải để đạt được điểm 8. Thế nhưng nếu đi như vậy thì khi đến lượt đi của người chơi cực tiểu, anh ta sẽ cố gắng không cho người chơi cực đại đạt được điểm này bằng cách chọn nước đi nhánh bên trái và như vậy, người chơi cực đại chỉ được có 1 điểm thay vì 8. Ngược lại, nếu người chơi cực đại chọn nước đi bên trái, thì trong tình huống xấu nhất anh ta vẫn còn được 2 điểm, lớn hơn là chọn nước đi bên phải. Nói chung, người chơi cực đại sẽ phải tìm cách nhận ra các nước đi của đối phương tiếp theo làm cho điểm giảm xuống. Và tương tự như vậy, người chơi cực tiểu phải nhận biết được nước đi của người chơi cực đại cố gắng làm tăng điểm lên. Thủ tục tìm nước đi tốt nhất trên cây trò chơi như trên được gọi là thủ tục Minimax do điểm ở mỗi nút có thể là điểm cực đại hoặc có thể là điểm cực tiểu và có thuật toán như sau:
Thuật toán Minimax •
•
•
Nếu như đạt đến giới hạn tìm kiếm (đến tầng dưới cùng của cây tìm kiếm), tính giá trị tĩnh của thế cờ hiện tại ứng với người chơi ở đó. Ghi nhớ kết quả Nếu như mức đang xét là của người chơi cực tiểu, áp dụng thủ tục Minimax này cho các con của nó. Ghi nhớ kết quả nhỏ nhất Nếu như mức đang xét là của người chơi cực đại, áp dụng thủ tục Minimax này cho các con của nó. Ghi nhớ kết quả lớn nhất.
Viết chương trình cho thuật toán Minimax Bây giờ, ta thử dựa vào phát biểu trên để viết chương trình cho thuật toán này bằng ngôn ngữ tựa Pascal. Đây là một hàm có tên là Minimax và sẽ là loại đệ qui. Trước hết, để hàm này biết đã đạt đến giới hạn tìm kiếm chưa, ta cần cung cấp cho nó một tham số về độ sâu tìm kiếm depth (để biết phải tìm đến đâu), đồng thời ta cũng phải cho biết thế cờ hiện tại pos để nó từ đó nó biết cách tính tiếp. Giá trị trả về của hàm chính là điểm của thế cờ (bàn cờ) pos. Vậy hàm sẽ có khai báo dạng: function Minimax (pos, depth): integer; Mỗi khi Minimax được gọi, nó sẽ càng gần đến giới hạn tìm kiếm, do đó ta sẽ gọi hàm này với độ sâu bằng độ sâu cũ trừ đi một. Đạt đến độ sâu giới hạn chính là khi depth = 0. Khi đạt độ sâu này ta sẽ gọi hàm lượng giá Eval để đánh giá chất lượng của thế cờ pos hiện tại (thực hiện điều một của thuật toán). Như vậy bước đầu hàm này có dạng sau:
function Minimax (pos, depth): integer; begin if depth = 0 then { Đã đạt đến giới hạn } Minimax := Eval (pos) { Tính giá trị thế cờ pos } else begin ... Minimax (pos, depth - 1); { Gọi đệ qui với độ sâu giản dần} ... end; end; Ở trên, Minimax được gọi với độ sâu giảm đi một. Đó là độ sâu của các thế cờ là con. Các thế cờ con pos' đó là các thế cờ được tạo ra từ pos bằng cách đi một nước đi hợp lệ m nào đó. Do đó ta phải có các lệnh thực hiện đi quân để đến các thế cờ mới. Để biết từ thế cờ pos có thể đi được những nước nào, ta dùng một thủ tục Gen có tham số là thế cờ cha
pos. Thủ tục này sẽ cất các thế cờ con pos' đó vào bộ nhớ (dạng danh sách). Việc tiếp theo là ta lấy từng thế cờ đó ra và áp dụng tiếp thủ tục Minimax cho nó để tính điểm value của nó. Vậy hàm Minimax bây giờ có dạng: function Minimax (pos, depth): integer; begin if depth = 0 then Minimax := Eval (pos) { Tính giá trị thế cờ pos } else begin Gen (pos); { Sinh ra mọi nước đi từ thế cờ pos } while còn lấy được một nước đi m do begin pos := Tính thế cờ mới nhờ đi m; value := Minimax (pos, depth-1); { Tính điểm của pos } ... end; ... end; end; Theo phát biểu của thuật toán, ta thấy các điều 2 và 3 chỉ khác nhau ở cách chọn kết quả tốt nhất best phụ thuộc vào người chơi đang là người chơi cực đại hay cực tiểu. Cuối cùng thuật toán sẽ trả về điểm tốt nhất đạt được. Vậy hàm này được phát triển tiếp thành: function Minimax (pos, depth): integer; begin if depth = 0 then Minimax := Eval (pos) { Tính giá trị thế cờ pos } else begin Gen (pos); { Sinh ra mọi nước đi từ thế cờ pos } while còn lấy được một nước đi m do begin pos := Tính thế cờ mới nhờ đi m; value := Minimax (pos, depth-1); { Tính điểm của pos } { Chọn điểm tốt nhất tuỳ thuộc theo người chơi } if người chơi là người cực đại then begin if best < value then best := value; end else begin if best > value then best := value; end end; Minimax := best; { Trả về giá trị tốt nhất }
end; end; Thông thường để cho tiện (và cũng rất gần sự thực) ta coi cả hai người chơi (hai bên) có cùng cách đánh giá về một thế cờ. Có điều thế cờ này là tốt với một người thì phải được đánh giá là tồi với người kia và ngược lại. Trong máy tính cách thể hiện tốt nhất là ta cho điểm một thế cờ có thêm dấu âm dương: dấu âm dành cho người chơi cực đại và dấu âm cho người chơi cực tiểu. Với người chơi cực đại sẽ mong muốn điểm này càng dương càng tốt, còn người chơi cực tiểu lại mong muốn điểm này càng âm càng tốt. Do đó để dễ xử lí ta sẽ tuỳ theo mức người chơi mà đổi dấu giá trị đánh giá thế cờ pos. Chú ý rằng, thay đổi độ sâu là chuyển sang đối phương nên phải đổi dấu. Chương trình thực hiện đổi dấu như sau: value := -Minimax (pos, depth-1); { Tính điểm của pos } Cũng do dùng cùng hàm lượng giá nên khi đến lượt người chơi cực đại và cực tiểu có cùng cái nhìn như nhau về một thế cờ. Điều này dẫn đến có thể dùng cùng cách chọn nước đi tốt nhất cho họ (gộp được điều 2 và 3 lại với nhau được). Giá trị best cần được khởi đầu rất nhỏ để đảm bảo không vượt mọi giá trị value, tốt nhất là giá trị -vô cùng: function Minimax (pos, depth): integer; begin if depth = 0 then Minimax := Eval (pos) { Tính giá trị thế cờ pos } else begin best := -INFINITY; Gen (pos); { Sinh ra mọi nước đi từ thế cờ pos } while còn lấy được một nước đi m do begin pos := Tính thế cờ mới nhờ đi m; value := -Minimax (pos, depth - 1); if value > best then best := value; end; Minimax := best; end; end; Thông thường, bàn cờ được biểu diễn bằng các biến toàn cục. Do đó thay cho truyền tham số là một bàn cờ mới pos vào thủ thục Minimax thì người ta biến đổi luôn biến toàn cục này nhờ thực hiện nước đi "thử" (nước đi dẫn đến bàn cờ mới pos). Sau khi Minimax thực hiện việc tính toán dựa vào bàn cờ lưu ở biến toàn cục thì thuật toán sẽ dùng một số thủ tục để loại bỏ nước đi này. Như vậy Minimax bỏ các tham số pos như sau: function Minimax (depth): integer; begin if depth = 0 then Minimax := Eval { Tính thế cờ pos trong biến toàn cục }
else begin best := -INFINITY; Gen; { Sinh ra mọi nước đi từ thế cờ pos } while còn lấy được một nước đi m 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 best := value; end; Minimax := best; end; end; Thuật toán Minimax với việc đảo dấu mỗi khi thay đổi độ sâu như trên đôi khi được gọi là thuật toán Negamax.
Đánh giá thuật toán Minimax Nếu hệ số nhánh trung bình của cây là b và ta thực hiện tìm kiếm đến độ sâu d thì số nút phải lượng giá ở đáy cây như ta đã biết là bd. Đây chính là số đo độ phức tạp của thuật toán. Nếu b = 40, d = 4 (các con số thường gặp trong trò chơi cờ) thì số nút phải lượng giá là 404 = 2560000 (trên 2 triệu rưỡi nút). Còn với b = 40, d = 5 thì số nút phải lượng giá sẽ tăng 40 lần nữa thành 405 = 102400000 (trên 102 triệu nút). Lưu ý: toàn bộ ý tưởng của thuật toán này là dựa trên việc chuyển đổi mỗi thế cờ thành một con số để đánh giá. Rất tiếc là các con số này thường không tốt và không đủ để đánh giá hết mọi điều. Mặt khác, thuật toán này có thể rất tốn kém (chạy chậm) do việc sinh các nước đi và lượng giá rất tốn thời gian tính toán, do vậy độ sâu của cây trò chơi cũng bị hạn chế nhiều. Ta cần có thêm những cải tiến để cải thiện tình hình.
VI. Thủ tục AlphaBeta Thủ tục AlphaBeta là một cải tiến thuật toán Minimax nhằm tỉa bớt nhánh của cây trò chơi, làm giảm số lượng nút phải sinh và lượng giá, do đó có thể tăng độ sâu của cây tìm kiếm. Giả sử hình 1.6 là một thế cờ mà hai nút đầu tiên đã được lượng giá. Nếu thực hiện thủ tục Minimax đối với các nút đó sẽ cho thấy người chơi cực đại đã được đảm bảo nếu đi nước bên trái sẽ được ít nhất là 2 điểm dù là các lượng giá của các nút khác cho kết quả như thế nào đi nữa.
Bây giờ, ta lại giả sử nút tiếp theo được lượng giá và cho kết quả là 1. Nếu đi vào nhánh này thì đối phương sẽ đảm bảo làm điểm của người chơi cực đại không thể vượt quá được giá trị 1 dù là các lượng giá của các nút khác cho kết quả như thế nào đi nữa. Do đó đến đây, nước đi tốt nhất là chọn nước đi bên trái với đảm bảo là ít nhất đạt được 2 điểm. Và do đó, hoàn toàn không cần thiết phải lượng giá nút còn lại.
Nguyên tắc Alpha-Beta Nếu biết điều đó thật sự tồi thì đừng mất thời gian tìm hiểu nó sẽ tồi tệ đến đâu Ý tưởng này được gọi là nguyên tắc Alpha-Beta do nó dùng trong thủ tục AlphaBeta (ta sẽ xét dưới đây). Hai tham số của thủ tục này (theo các đặt tên truyền thống) được gọi là alpha và beta và dùng để theo dõi các triển vọng - chúng cho biết các giá trị nằm ngoài khoảng [alpha, beta] là các điểm "thật sự tồi" và không cần phải xem xét nữa. Khoảng [alpha, beta] còn được gọi là cửa sổ alpha, beta. Trong ngữ cảnh của các trò chơi, nguyên tắc Alpha-Beta nói rằng, mỗi khi xem xét một nút bất kì, nên kiểm tra các thông tin đã biết về các nút cha, ông của nó. Rất có thể do có đủ thông tin từ cha, ông nên không cần phải làm bất cứ việc gì nữa cho nút này. Cũng vậy, nguyên tắc này cũng giúp chỉnh sửa
hoặc xác định chính xác giá trị tại nút cha, ông nó. Như trên nói, một cách để tiện theo dõi quá trình tính toán là dùng các tham số alpha và beta để ghi lại các thông tin theo dõi cần thiết. Thủ tục AlphaBeta được bắt đầu tại nút gốc với giá trị của alpha là -vôcùng và beta là +vôcùng. Thủ tục sẽ tự gọi đệ quy chính nó với khoảng cách giữa các giá trị alpha và beta ngày càng hẹp hơn.
Thuật toán AlphaBeta * Nếu mức đang xét là đỉnh (gốc cây), đặt giá trị của alpha là -vôcùng và beta là +vôcùng * Nếu như đạt đến giới hạn tìm kiếm (đến tầng dưới cùng của cây tìm kiếm), tính giá trị tĩnh của thế cờ hiện tại ứng với người chơi ở đó. Ghi lại kết quả * Nếu như mức đang xét là của người chơi cực tiểu, • Thực hiện các công việc sau cho đến khi tất cả các con của nó đã được xét với thủ tục AlphaBeta hoặc cho đến khi alpha là bằng hoặc lớn hơn beta. - Áp dụng thủ tục AlphaBeta với giá trị alpha và beta hiện tại cho một con. Ghi nhớ lại kết quả. - So sánh giá trị ghi nhớ với giá trị beta, nếu giá trị đó nhỏ hơn thì đặt beta bằng giá trị mới này. • Ghi nhớ lại beta * Nếu như mức đang xét là của người chơi cực đại, •
Thực hiện các công việc sau cho đến khi tất cả các con của nó đã được xét với thủ tục AlphaBeta hoặc cho đến khi alpha là bằng hoặc lớn hơn beta. - Áp dụng thủ tục AlphaBeta với giá trị alpha và beta hiện tại cho một con. Ghi nhớ lại kết quả. - So sánh giá trị ghi nhớ với giá trị alpha, nếu giá trị đó lớn hơn thì đặt alpha bằng giá trị mới này.
•
Ghi nhớ lại alpha.
Viết chương trình cho thuật toán AlphaBeta Từ phát biểu trên ta sẽ xây dựng hàm AlphaBeta bằng ngôn ngữ tựa Pascal. Hàm này sẽ có dạng khai báo như dưới, trong đó depth là độ sâu tìm kiếm, INFINITY là giá trị vô cùng, thuật toán tính toán dựa trên thế cờ hiện tại pos là các biến toàn cục:
function AlphaBeta(alpha, beta, depth): integer; begin if depth = 0 then AlphaBeta := Eval { Tính giá trị thế cờ pos } else begin best := -INFINITY; Gen; { Sinh ra mọi nước đi từ vị trí pos } while (còn lấy được một nước đi m) 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 best := value; end; AlphaBeta := best; end; end; Lời gọi thủ tục AlphaBeta đầu tiên với độ sâu tìm kiếm 4 và thế cờ hiện tại pos có dạng như sau: AlphaBeta(-INFINITY, +INFINITY, 4); Cũng tương tự như thuật toán Minimax ta đã gộp hai mục 2 và 3 làm một nhờ việc đổi dấu thích hợp. So với thuật toán Minimax thì trong thuật toán AlphaBeta đã đưa thêm hai biến alpha, beta làm hai mức ngưỡng. Ta thấy cứ mỗi khi best >= beta thì thuật toán không thực hiện tiếp vòng lặp, có nghĩa là nó không chịu mở rộng tiếp những nhánh còn lại nữa. Các nhánh đó đã bị cắt bỏ - và do đó ta sẽ tiết kiệm được thời gian. Việc cắt bỏ này hoàn toàn an toàn với những lí do ta đã xét ở trên. Ta thấy rằng mỗi lần hàm này được gọi thì chỉ có tham số beta được dùng để so sánh cắt bỏ, còn tham số alpha không được dùng. Tuy nhiên khi áp dụng cùng thuật toán cho cây con thì ta đã hoán vị hai giá trị alpha, beta cho nhau (và đảo cả dấu), do đó alpha sẽ có tác dụng trong độ sâu sau, rồi độ sâu sau nữa lại đến lượt beta... Nói cách khác, một giá trị chỉ luôn ảnh hưởng đến người chơi cực đại, còn giá trị kia lại luôn ảnh hưởng đến người chơi cực tiểu. Chúng là các ngưỡng của họ (ngưỡng giữa các nước đi được chấp nhận và không chấp nhận). Những nước đi cần quan tâm phải nằm lọt giữa hai giá trị này. Dần dần khoảng cách giữa hai giá trị alpha - beta càng ngày càng thu hẹp và dẫn đến các nhánh cây có giá trị nằm ngoài khoảng này nhanh chóng bị cắt bỏ (hình 1.7).
Đánh giá thuật toán AlphaBeta Trong điều kiện lí tưởng, thuật toán AlphaBeta chỉ phải xét số nút theo công thức: với d chẵn với d lẻ
= =
Với b = 40 và d = 4 ta có số nút phải xét là 2x402 - 1 = 3199. Như vậy trong điều kiện lí tưởng thì số nút phải xét nhờ AlphaBeta (chỉ khoảng 3 nghìn nút) ít hơn thuật toán Minimax (hơn 2,5 triệu nút) là 2560000 / 3199 khoảng 800 lần. Còn với b = 40 và d = 5 ta có số nút phải xét là 403 + 405/2 - 1 = 64000+10119-1 = 74118. Số nút phải xét nhờ AlphaBeta ít hơn thuật toán Minimax (hơn 102 triệu nút) là 102400000/74118 = 1382 lần. Dưới đây là bảng so sánh số nút phải xét giữa hai thuật toán Minimax và AlphaBeta. Minimax Độ sâu 1 2 3 4 5 6 7
Số nút 40 1600 64000 2560000 102400000 4096000000 163840000000
AlphaBeta Số lần tăng 40 40 40 40 40 40
Số nút 40 79 1852 3199 74118 127999 2964770
Tỉ lệ số nút
Số lần tăng 1 1.9 23.2 1.7 23.2 1.7 23.2
Minimax / AlphaBeta 20 34 800 1381 32000 55262
8
6553600000000
40
5120000
1.7
1280000
Ta có thể nhận xét như sau: •
•
Số lần tăng số nút khi tăng độ sâu của Minimax luôn là hệ số phân nhánh b, trong trường hợp này là 40. Số lần tăng của AlphaBeta ít hơn nhiều: chỉ cỡ 1.7 lần khi tăng từ d lẻ sang d chẵn và 23.2 lần khi từ d chẵn sang lẻ - trung bình chỉ tăng khoảng hơn 6 lần khi tăng d Số nút của AlphaBeta tăng chậm hơn rất nhiều lần so với Minimax. Tỉ số nút phải xét giữa hai thuật toán này càng cao khi d càng lớn.
Công thức tính số nút cho thấy số nút phải xét khi dùng AlphaBeta ít hơn nhiều so với Minimax nhưng vẫn là hàm số mũ và vẫn dẫn tới bùng nổ tổ hợp. Thuật toán AlphaBeta hoàn toàn không chống được bùng nổ tổ hợp mà chỉ làm giảm tốc độ bùng nổ. Tuy trong thực tế số nút phải xét (lượng giá) thường nhiều hơn trong điều kiện lí tưởng nhưng nó vẫn đủ để tiết kiệm khá nhiều thời gian. Trong cùng một khoảng thời gian, thuật toán AlphaBeta có thể tìm đến độ sâu gấp hai lần độ sâu tìm kiếm bằng Minimax. Hình 1.8 là đồ thị so sánh giữa hai thuật toán này.
Ví dụ: Ta sẽ xem xét thuật toán AlphaBeta hoạt động như thế nào đối với cây trò chơi như trong hình 1.9.
Cây này có độ sâu bằng 3 và hệ số phân nhánh bằng 3. Các thứ tự kết luận (các con số bên trái) được đưa ra như sau: 1-2
Tìm kiếm đi xuống dưới theo nhánh trái cho đến lá. Ở đây giá trị tĩnh thu được là 8. Giá trị đầu tiên này do người chơi cực đại được phép chọn trong ba giá trị ở nhánh này đã đảm bảo rằng là kết quả thu được sẽ ít nhất là bằng 8. Điều lưu ý này được bước 2 ghi lại. 3-5 Để chắc chắn không còn có điểm nào cao hơn 8, người chơi cực đại phải xét cả hai thế cờ còn lại và thu được các giá trị 7 và 2. Do đó đến đây đã kết luận chính xác điểm cao nhất có thể đạt được ở cây con là đúng bằng 8. 6. Leo lên một tầng cây. Đây là các nước đi của người chơi cực tiểu. Ta không hi vọng anh ta cho người chơi cực đại được nhiều điểm nên có thể tạm kết luận ở mức này là sẽ đạt được nhiều nhất là 8 điểm. 7-8. Để xem người chơi cực tiểu còn lựa chọn nào tốt hơn (và tồi tệ hơn cho người chơi cực đại) ta phải xem xét cả hai nước đi còn lại. Nước đi còn lại đầu tiên dẫn đến giá trị lượng giá tĩnh là 9 - một giá trị lớn hơn 8. Như vậy nhánh giữa là tồi tệ hơn cho người chơi cực tiểu. Đến đây việc cắt bỏ được thực hiện - đừng hòng người chơi cực đại với tới được điểm đó khi đã có sẵn lựa chọn thấp hơn cho anh ta (là 8). Điều này cũng dẫn đến không cần thiết phải xét hai nút còn lại - đằng nào nhánh giữa cũng đủ tồi tệ rồi và người chơi cực tiểu sẽ không chọn nó để đi. 9-14. Người chơi cực tiểu cần phải khảo sát tiếp lựa chọn cuối cùng. Cách làm tương tự như phần trên. Ở đây phải lượng giá cả ba nút cây và kết luận cuối cùng được đưa ra là người chơi cực đại đi giỏi lắm thì chỉ đạt được 4 điểm.
15.
Như vậy nhờ việc khảo sát nhánh cây bên phải người chơi cực tiểu thấy rằng nếu chọn đi theo nhánh này thì người chơi cực đại chỉ được có 4 điểm thay cho 8. 16. Bây giờ ta có thể kết luận ở mức trên cùng. Mức này là của người chơi cực đại. Anh ta thấy rằng nếu chọn đi theo nhánh trái thì được 4 điểm. Như vậy anh ta đã chắc chắn điểm của mình sẽ ít nhất là 4 rồi. Để xem liệu có thể đạt được điểm cao hơn nữa hay không cần phải xem xét hai nhánh còn lại. 17-30. Tương tự như phần trên, ta kết luận nhánh giữa sẽ mang lại cho người chơi cực đại 5 điểm. 31. Cũng tương tự như kết luận 16, ở đây ta kết luận khả quan hơn là người chơi cực đại đã cầm chắc 5 điểm và có thể còn cao hơn. 32-38 Ta kết luận được rất nhanh là cây con bên phải chỉ cho "thu hoạch" nhiều nhất là 3 điểm - một điểm số quá kém do đó thuật toán không buồn xem xét các trường hợp còn lại nữa. Do đó đã tiết kiệm được 6 nút không cần phải lượng giá và cũng không phải sinh nước đi cho hai trường hợp. 39. Kết luận cuối cùng là điểm cao nhất mà người chơi cực đại có thể thu được là 5 điểm nhờ chọn đi theo nhánh giữa.
VII. Hướng cải thiện việc tỉa nhánh của thuật toán AlphaBeta Thuật toán AlphaBeta nói chung giúp chúng ta tiết kiệm nhiều thời gian so với Minimax mà vẫn đảm bảo kết quả tìm kiếm chính xác. Tuy nhiên lượng tiết kiệm này không ổn định - phụ thuộc vào số nút mà nó cắt bỏ. Trong trường hợp xấu nhất thuật toán không cắt được một nhánh nào và phải xét số nút đúng bằng Minimax. Ta cần đẩy mạnh việc cắt bỏ nhờ đẩy nhanh sự thu hẹp của cửa sổ tìm kiếm alpha - beta. 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 (các công thức về số nút phải lượng giá trong điều kiện lí tưởng ở trên tính được với trật tự là tốt nhất). Ta sẽ trở lại phần này trong một chương riêng.
Tổng kết chương 1 Chương này trình bầy những kiến thức chung về trò chơi cờ, các định nghĩa và thế nào là cây trò chơi. Do bùng nổ tổ hợp quá lớn của cây trò chơi mà cả người và máy không thể (và không bao giờ) có thể tìm kiếm vét cạn (hết mọi khả năng). Do đó phương pháp tìm kiếm duy nhất là chỉ tìm kiếm đến một độ sâu giới hạn nào đó và chọn nước đi dẫn đến một thế cờ có lợi nhất cho mình. Do phải tính cả khả năng chống trả của đối phương nên ta không dùng được các thuật toán tìm kiếm thông thường. Phải dùng một thuật toán tìm kiếm riêng cho cây trò chơi. Đó là thuật toán Minimax và cải tiến của nó là AlphaBeta.
Tuy cả hai thuật toán đều không tránh được bùng nổ tổ hợp nhưng AlphaBeta làm chậm bùng nổ tổ hợp hơn nên được dùng nhiều trong các trò chơi cờ.
Bài đọc SƠ LƯỢC VỀ LỊCH SỬ CÁC CHƯƠNG TRÌNH CHƠI CỜ Vào năm 1950, Alan Turing - một nhà nghiên cứu người Anh đi tiên phong trong lĩnh vực máy tính số, đã viết chương trình chơi cờ đầu tiên. Vào lúc đó, Turing phải viết và chạy chương trình của ông bằng... bút chì và giấy. Chương trình đó, cũng như chủ nhân của nó, chơi cờ rất tồi, nhưng đạt được mục đích: cho thấy máy tính có thể chơi được cờ. Cũng vào năm đó, Claude Shannon đã vạch ra một chiến lược cho máy tính chơi cờ tốt. Nhưng vào những năm 1950 tốc độ máy tính rất chậm nên không ai dám tiên đoán liệu máy tính có thể thắng con người được không, dù trong các trò chơi đơn giản như trò Checker. Năm 1958, một chương trình chơi cờ đã lần đầu tiên hạ được đối phương là con người. Người thua là một cô thư kí của chính đội lập trình ra nó, cô chưa bao giờ chơi cờ trước đó và được dậy chơi cờ chỉ một giờ trước cuộc đấu. Đối với ngày nay chiến công này thật nhỏ nhoi, nhưng nó cho thấy tri thức có thể được đưa vào trong một chương trình chơi cờ. Lượng tri thức này được đo chính xác bằng một giờ học chơi. Sau chiến thắng đó, một số người trong nhóm lập trình cờ đầu tiên đã tiên đoán rằng vào những năm 60 sẽ có chương trình chơi cờ được liệt vào hàng ngũ kiện tướng thế giới. Vào những năm cuối của thập kỷ 60, Spassky đã trở thành kiện tướng cờ thế giới và các chương trình chơi cờ đã chiếm được những thứ hạng cao trong hàng ngũ những người chơi cao cấp. Nhưng nhiều người cho rằng máy tính sẽ không bao giờ có thể giải quyết được những nhiệm vụ thông minh, không thể đạt được chức Vô địch cờ thế giới. Lời tiên đoán này được nhắc lại một lần nữa vào những năm 70, liên quan đến một cuộc đánh cược giữa David Levy, một kiện tướng quốc tế người Anh (theo phân loại của Liên đoàn cờ quốc tế các đẳng cấp cao bao gồm: Kiện tướng quốc tế, Đại kiện tướng và Vô địch thế giới) và John McCarthy, một nhà nghiên cứu trong lĩnh vực trí tuệ nhân tạo. Lời thách đấu được đưa ra vào năm 1978. Trận đấu đã được diễn ra và chương trình cờ tốt nhất thời đó, CHESS 4.7 đã bị Levy hạ trong trận đấu có năm ván tại Toronto với thành tích ba ván người thắng, một hoà và một máy thắng. Levy không chỉ chiến thắng mà còn đút túi số tiền đánh cược 1000 bảng. Nếu như mục đích của cuộc đánh cược là làm cho những nhà nghiên cứu phải nghĩ kĩ trước khi tiên đoán đến ngày thắng lợi, thì lần đánh cược này cho thấy: mặc dù tiên đoán sai trong những năm 1958-1968 và 1968-1978, các chuyên gia chương trình cờ lại tiếp tục tiên đoán tiếp rằng máy tính sẽ đạt đến vô địch cờ thế giới trong thập kỉ tiếp theo. Nhưng một lần nữa, vào năm 1988, Vô địch cờ thế giới vẫn là con người.
Trong năm tiếp theo, Deep Thought, một chương trình cờ mạnh nhất từ xưa đến nay đã chiến thắng một cách dễ dàng Kiện tướng Quốc tế Levy. Bộ não của Deep Thought có 250 chip và hai bộ xử lí trong một bảng mạch đơn, nó có khả năng xét 750.000 thế cờ trong một giây và tìm trước được đến 10 nước. Cũng trong năm đó, nó là máy tính đầu tiên hạ được một Đại kiện tướng (Bent Larsen). Deep Thought đã trở thành một trong một trăm người chơi cờ mạnh nhất thế giới. Nhưng trong trận đấu diễn ra vào năm 1989 giữa nhà Vô địch thế giới Garry Kasparov và Deep Thought thì nó đã bị nhà vô địch đè bẹp. Các lời tiên đoán lại đến như các lần trước. Đã ba lần các nhà nghiên cứu tiên đoán: 'trong thập kỉ tới'. Nhưng lần này họ lại sửa lại là: 'trong 3 năm tới'... Trong năm 1993, Deep Thought đã hạ Judit Polgar - lúc đó là Đại kiện tướng trẻ nhất trong lịch sử và là người phụ nữ chơi hay nhất thế giới, trong trận đấu 2 ván. Trong năm 1996, Deep Blue (tên mới của Deep Thought và lúc này nó thuộc hãng IBM) là một máy tính song song có 32 bộ xử lí với 256 mạch tích hợp cỡ lớn, khả năng xét từ 2 đến 400 triệu nước đi mỗi giây) đã thắng Gary Kasparov trong ván đầu tiên của trận đấu 6 ván, nhưng lại thua trong toàn trận (với tỉ số máy thắng 1, hoà 2 và thua 3). Cuối cùng đích mà mọi người chờ đợi đã tới, nhưng sau 9 năm từ lời tiên đoán cuối và 39 năm từ lúc có chương trình chơi cờ đầu tiên, Deep Blue đã chiến thắng nhà đương kim Vô địch thế giới Garry Kasparov vào tháng 5/1997 trong một cuộc chiến dài đầy khó khăn, với tỷ số sát nút 2 thắng, 1 thua và 3 hoà. Phạm Hồng Nguyên, sưu tầm, tổng hợp và dịch từ Internet
[1] Để "ngấm" được các thuật toán Minimax và AlphaBeta có thể cần nhiều thời gian. Nếu cảm thấy khó, các bạn có thể xem nhanh phần này để hiểu ý tưởng rồi chuyển sang chương sau viết chương trình chơi cờ. Sau đó quay lại chương này để hiểu kĩ hơn về các thuật toán đó. Copyright 2002, Phạm Hồng Nguyên TỰ VIẾT CHƯƠNG TRÌNH CỜ TƯỚNG Tác giả: Phạm Hồng Mục lục Chương 1 Nguyên
Chương 3
Chương 2 CỜ TƯỚNG - PHIÊN BẢN ĐẦU TIÊN (VERY SIMPLE CHINESE CHESS PROGRAM - VSCCP 1.0)
I. Giới thiệu Trò chơi Cờ Tướng (tên phiên âm Trung Quốc XiangQi, tên tiếng Anh Chinese Chess) là một minh hoạ rất tốt cho bài toán tìm kiếm trên cây trò chơi và áp dụng thuật toán AlphaBeta trên cây này như thế nào. Đây là một trò chơi thú vị và tương đối phổ biến ở Việt nam, châu Á cũng như trên toàn thế giới. Nó tạo cảm giác dường như máy tính có thể suy nghĩ và đọ sức với con người (thực tế cho đến nay nó vẫn chỉ tính toán mà thôi). Cờ Tướng là loại cờ có độ phức tạp và rất nhiều mặt tương đương với cờ Vua. Trong phần này, chúng tôi sẽ giới thiệu với bạn những kiến thức cơ bản nhất về một chương trình chơi cờ phải như thế nào. Các chương trình mẫu VSCCP (Very Simple Chinese Chess Program - Chương trình Cờ Tướng rất đơn giản) cũng hết sức đơn giản và có đầy đủ trong các phụ lục.
II. Viết chương trình chơi cờ VSCCP 1.0 1. Biểu diễn bàn cờ và quân cờ Bàn cờ trong trò chơi cờ Tướng là một bảng chữ nhật bao gồm 9 đường dọc và 10 đường ngang. Các quân cờ chia làm hai bên đứng tại các giao điểm của các đường. Bàn cờ và vị trí khởi đầu các quân cờ như hình 2.1. Cách đơn giản nhất để biểu diễn một bàn cờ trong máy tính là ta dùng một mảng hai chiều, kích thước 9 x 10: piece: array[1..10, 1..9] of byte
Mảng trên hoạt động tốt nhưng có cái bất tiện là ta phải dùng tới hai chỉ số để truy cập vào mảng (ví dụ vị trí quân Pháo góc trên bên trái (cột 2, dòng 3) là piece[3, 2]). Một cải tiến nhỏ là ta dùng mảng một chiều như sau: piece: array[1..90] of byte Truy nhập đến vị trí quân Pháo góc trên bên trái lúc này là piece[20].
Các ô của mảng sẽ chứa những giá trị khác nhau cho biết đó là quân cờ gì. Mỗi quân cờ sẽ được gán một mã số khác nhau như bảng dưới đây. Các chỗ trống (không có quân cờ) sẽ được điền kí hiệu trống (EMPTY): Quân cờ Tốt (Chốt) Sĩ Tượng Mã Pháo Xe Tướng Trống
Kí hiệu PAWN BISHOP ELEPHANT KNIGHT CANNON ROOK KING EMPTY
Giá trị 1 2 3 4 5 6 7 0
Ngoài mục đích gán mỗi quân cờ một mã số để phân biệt, mã này còn giúp ta ước lượng sơ bộ tầm quan trọng của quân cờ đó. Như vậy, lúc khởi đầu, các ô trong mảng sẽ được gán các giá trị quân cờ nhờ khai báo const (trong Pascal) như dưới, trong đó BOARD_SIZE (kích thước bàn cờ = 90) là một hằng số đã được định nghĩa trước đó: piece: array[1..BOARD_SIZE] of byte = ( 6, 4, 3, 2, 7, 2, 3, 4, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 5, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 4, 3, 2, 7, 2, 3, 4, 6); Đến đây, bàn cờ còn chưa có thông tin để phân biệt một quân cờ là của bên nào. Ta có thể cải tiến thay cho kiểu byte của mảng piece là một bản ghi nhằm lưu thêm các thông tin này. Chúng tôi xin giới thiệu một phương pháp đơn giản là dùng thêm một mảng nữa mảng color, để lưu các thông tin về bên. Hai bên được gán kí hiệu và mã như bảng dưới. Những chỗ trống sẽ dùng cùng kí hiệu trống EMPTY. Bên Đen Trắng Trống
Kí hiệu DARK LIGHT EMPTY
Giá trị 1 2 0
Ta lại có thông tin về bên được khai báo khởi đầu tương tự: color: array[1..BOARD_SIZE] of byte = (1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2); Để biết bên nào tới lượt đi, ta dùng một biến toàn cục side chứa một trong hai giá trị LIGHT và DARK. Một biến toàn cục khác xside sẽ có sẵn giá trị ngược với side để tiện tính toán (ví dụ nếu side = LIGHT thì xside = DARK). Khi tới phiên đối phương đi, ta
cần đảo ngược giá trị trong cả side và xside bằng các lệnh như sau (chú ý là LIGHT+DARK = 3): side := xside; xside := 3 - xside; Ngoài ra, ta còn khai báo biến computerside cũng chỉ có một trong hai giá trị LIGHT và DARK nhằm cho biết bên nào là máy. Để hiện bàn cờ, phương pháp đơn giản nhất là hiện ở chế độ text (văn bản). Tuy cách này trông xấu và thường không dùng trong các chương trình có trên thị trường nhưng nó có các ưu điểm nổi trội sau: • •
•
•
•
Vẫn hiện được bàn cờ rất đầy đủ, trực quan, dễ hiểu, cho phép theo dõi và thi đấu bình thường. Rất đơn giản và tin cậy. Bạn sẽ đỡ nhiều thời gian tìm hiểu và phát triển giao diện, dồn sức cho việc quan trọng hơn - làm chương trình chơi hay hơn. Giao diện đồ hoạ chỉ cần thiết lúc hoàn chỉnh - lúc bạn "đóng gói" chương trình. Việc hiện thêm các thông tin để kiểm tra, tìm lỗi rất dễ dàng. Rõ ràng việc dùng hai thủ tục có sẵn của Pascal là gotoxy và write thì dễ, nhanh và đảm bảo hơn các hàm đồ hoạ. Dễ chuyển đổi hệ máy và hệ điều hành. Có thể bạn sẽ muốn thử chạy trên những máy tính có nền tảng khác như máy mini, macintosh, hệ điều hành Windows 3.1, Win95, Win98, WinNT, System7, UNIX... Bạn có thể tự phát triển giao diện đồ hoạ và cài chuột theo ý mình.
Quân cờ được biểu diễn bằng một chữ cái viết hoa đứng đầu tên của nó. Quân hai bên sẽ có mầu khác nhau. Do quân Tướng và Tượng có cùng chữ cái đầu T nên để tránh nhầm lẫn ta dùng chữ V (Voi) biểu diễn quân Tượng. Tuy quân Tốt và Tướng cũng có cùng chữ cái đầu nhưng ta không sợ nhầm do Tốt không thể nhập cung bên mình được. Nó chỉ có thể "tiếp chuyện" Tướng đối phương, nhưng lúc này hai bên lại phân biệt được nhờ mầu (các phương pháp khác là dùng chữ cái đầu tên tiếng Anh: P-Tốt, E-Tượng, N-Mã, B-Sĩ, R-Xe, C-Pháo, K-Tướng; hoặc theo qui định của Liên đoàn cờ Việt Nam: Tg-Tướng, SSĩ, T-Tượng, X-Xe, P-Pháo, M-Mã, C-Chốt). Ta sẽ hiện bàn cờ ở dạng như hình 2.3. Chú ý cách đánh kí hiệu các vị trí trong bàn cờ bằng chữ và số giống như bàn cờ Vua. Để hiện một quân cờ, ta viết một thủ tục DrawCell với tham số là vị trí bàn cờ. Ngoài ra, nó còn một tham số nữa cho biết sẽ hiện mầu nền của quân cờ với mầu bình thường NORMAL (sẽ có nền đen) hay mầu khác SELECT (sẽ có nền xanh sẫm) để thể hiện quân cờ đó được chọn (phục vụ cho chọn và đi quân của người chơi). procedure DrawCell(pos, mode: byte); Như vậy, để hiện toàn bộ bàn cờ ta chỉ cần gọi một vòng lặp for là xong: for i := 1 to BOARD_SIZE do DrawCell(i, NORMAL);
2. Sinh nước đi Một trong những việc quan trọng nhất để máy tính có thể chơi được cờ là phải chỉ cho nó biết mọi nước đi có thể đi được từ một thế cờ. Máy sẽ tính toán để chọn nước đi có lợi nhất cho nó. Các yêu cầu chính đối với một thủ tục sinh nước đi là: •
• •
Chính xác (rất quan trọng). Một nước đi sai sẽ làm hỏng mọi tính toán. Đồng thời chương trình có thể bị trọng tài xử thua luôn. Do số lượng nước đi sinh ra lớn, luật đi quân nhiều và phức tạp nên việc kiểm tra tính đúng đắn tương đối khó. Đầy đủ (rất quan trọng). Sinh được mọi nước đi có thể có từ một thế cờ. Nhanh. Do chức năng này phải sinh được hàng triệu nước đi mỗi khi máy đến lượt nên giảm thời gian sinh nước đi có ý nghĩa rất lớn.
Sinh nước đi là một thuật toán vét cạn. Máy sẽ tìm mọi nước đi hợp lệ có thể có. Máy phải sinh được từ những nước đi rất hay cho đến những nước đi rất ngớ ngẩn (như đẩy Tướng vào vị trí khống chế của đối phương). Ta hình dung ngay thủ tục sinh nước đi Gen sẽ có đầy những vòng lặp for, các câu lệnh kiểm tra if và rẽ nhánh case, trong đó các phép tính kiểm tra giới hạn chiếm một phần đáng kể. Thủ tục này luôn sinh nước cho bên đang tới lượt chơi căn cứ vào nội dung của biến side. Đây là một trong những thủ tục phức tạp và dễ sai nhất. Một nước đi có hai giá trị cần quan tâm. Đó là điểm xuất phát (from) và điểm đến (dest). Ta sẽ khai báo một cấu trúc move như sau để dùng những nơi cần đến dữ liệu nước đi. type move = record { Định nghĩa cấu trúc nước đi }
from, dest: byte; end;
Kiểm tra giới hạn bàn cờ Ví dụ có một quân Xe nằm ở ô số 84 (trong mảng piece). Bây giờ ta sẽ sinh thử một nước đi sang trái một ô cho nó. Nước đi sang trái một ô được chuyển thành ô số 83 là một vị trí cùng hàng với ô xuất phát nên được chấp nhận. Một nước đi khác cần phải xem xét là sang trái ba ô - ô 81. Ô 81 tuy có trong bàn cờ nhưng khác hàng nên không được chấp nhận. Như vậy, muốn biết ô của nước đi sang trái có được phép không ta phải kiểm tra xem nó có cùng hàng với ô xuất phát không. Việc kiểm tra thực hiện bằng cách chia cho 9 (kích thước của một dòng) và lấy phần nguyên (trước đó lại phải chuyển thứ tự ô về gốc là 0 bằng cách trừ đi 1). Ta thấy ((83-1) div 9) = ((84-1) div 9) nên ô 83 được chấp nhận; trong khi đó do ((81-1) div 9) ((84-1) div 9) nên kết luận là nước đi đến ô 81 không hợp lệ (hình 2.5).
Các nước đi vừa sinh ra sẽ được đưa vào danh sách nước đi nhờ gọi thủ tục Gen_push. Thủ tục này có hai tham số là vị trí xuất phát của quân cờ sẽ đi và nơi đến dest của quân cờ đó. Dưới đây là đoạn chương trình dùng để sinh những nước đi sang trái của một quân Xe, trong đó x là vị trí của quân Xe này . i := x - 1; { Nước sang trái đầu tiên } while ((i-1) div 9) = ((x-1) div 9) do begin if (ô thứ i là trống) or (ô thứ i có quân đối phương) then gen_push(vị trí của Xe, vị trí ô đang xét); if ô thứ i không trống then break; { Kết thúc quá trình sinh nước đi sang trái } i := i - 1; { Xét tiếp vị trí bên trái } end; Việc sinh những nước đi theo chiều khác cũng tương tự (ví dụ để sinh nước đi theo chiều đứng ta chỉ cần cộng hoặc trừ một bội số của 9) nhưng điều kiện kiểm tra sẽ khác nhau. Như vậy, chương trình sinh nước đi Gen có dạng như sau: for Xét lần lượt từng quân cờ bên hiện tại do case quâncờ of Xe: begin while Xét lần lượt tất cả các ô từ bên phải Xe cho đến lề trái bàn cờ do begin if (ô đang xét là ô trống) or (ô đang xét chứa quân đối phương) then gen_push(vị trí của Xe, vị trí ô đang xét) if ô đang xét không trống then break; end; while Xét lần lượt tất cả các ô từ bên trái Xe cho đến lề phải bàn cờ do ... while Xét lần lượt tất cả các ô từ bên trên Xe cho đến lề trên bàn cờ do ... while Xét lần lượt tất cả các ô từ bên dưới Xe cho đến lề dưới bàn cờ do ... end; { Xong quân Xe } Pháo: ... Phương pháp này có nhược điểm là chương trình phải viết phức tạp, cồng kềnh, khó tìm lỗi nhưng khá nhanh.
Trong chương trình mẫu VSCCP, chúng tôi giới thiệu một kĩ thuật khác có tên gọi là "hộp thư" (mail box - do các bảng của nó có dạng các hộp phân thư). Mục đích chính của kĩ thuật này là nhằm giảm bớt các phép kiểm tra vượt giới hạn bàn cờ và làm đơn giản chương trình. Mấu chốt là thay cho bàn cờ có kích thước bình thường 9x10 = 90, ta dùng một bàn cờ mở rộng, mỗi chiều có thêm 2 đường nữa (bàn cờ mới có kích thước 13x14 = 182). Các ô ứng với các đường bao mở rộng đều có giá trị -1, tức là các giá trị báo vượt biên. Các nước đi trên bàn cờ 9x10 được chuyển tương ứng sang bàn cờ này. Nếu một nước đi được sinh ra lại rơi vào một trong hai đường bao thì có nghĩa nó đã rơi ra ngoài bàn cờ rồi, phải bỏ đi và ngừng sinh nước về phía đó. Sở dĩ có đến hai đường biên (chứ không phải một) do quân Mã và Tượng có thể nhẩy xa đến hai ô (hình 2.6).
Việc chuyển đổi toạ độ thực hiện nhờ hai mảng. Mảng mailbox90 dùng để chuyển từ toạ độ bàn cờ thường sang toạ độ bàn cờ mới. Mảng ứng với bàn cờ mới - mailbox182 dùng để kiểm tra các nước đi vượt quá đường biên và dữ liệu để chuyển trở lại toạ độ bình thường. Ví dụ, nếu vị trí quân Xe nằm ở ô số 84 (trong mảng piece) như hình 2.5, thì khi đổi sang bàn cờ mở rộng sẽ thành vị trí mailbox90[84] = 148. Có nghĩa là nó ứng với ô thứ 148 của mảng mailbox182. Bây giờ ta sẽ sinh thử một nước đi sang trái một ô cho quân Xe này. Việc sinh và kiểm tra sẽ được thực hiện trong bàn cờ mở rộng, nước đi mới là ô số 148 - 1 = 147. Do mailbox182[147] = 83 ¹ -1 nên nước đi này được chấp nhận. Số 83 cho biết kết quả sang trái một ô là vị trí 83 trong bàn cờ bình thường. Tuy nhiên, nước đi sang trái ba ô, có số 148 - 3 = 145 và mailbox182[145] = -1 cho biết đó là một nước đi không hợp lệ. Chương trình cũng cần kiểm tra số nước đi tối đa theo một hướng của quân cờ đang xét. Chỉ có quân Pháo và Xe có thể đi đến 9 nước đi, còn các quân khác có nhiều nhất là 1. Việc đi một quân sang vị trí mới thực chất là ta đã thay đổi toạ độ của nó bằng cách cộng với một hằng số (dương hoặc âm). Ở trên ta đã thấy để quân Xe sang trái một nước ta đã phải cộng vị trí hiện tại với -1. Để sinh một ô mới theo hàng dọc, ta phải cộng với +13 hoặc -13 (chú ý, việc sinh nước và kiểm tra hợp lệ đều dựa vào mảng mailbox182 nên giá trị 13 là kích thước một dòng của mảng này). Để sinh các nước đi chéo ta phải cộng trừ với một hằng số khác. Ta nên lưu các hằng số này vào mảng offset có 2 chiều. Một chiều dựa vào loại quân cờ nên chỉ số được đánh từ 1 đến 7. Chiều còn lại là số hướng đi tối đa
của một quân cờ. Quân cờ có nhiều hướng nhất là quân Mã có tới 8 hướng nên chiều này được khai báo chỉ số từ 1 đến 8. Các quân cờ có số hướng ít hơn sẽ được điền 0 vào phần thừa. Chú ý là nước đi quân Tốt của hai bên khác nhau hướng tiến. Để tiết kiệm ta chỉ lưu nước tiến của Tốt đen, còn nước tiến của Tốt trắng chỉ đơn giản lấy ngược dấu.
Kiểm tra vị trí được phép đến Việc chuyển toạ độ từ bàn cờ thường sang bàn cờ mở rộng chỉ giúp ta loại bỏ được các nước vượt quá biên. Ta còn phải kiểm tra một số giới hạn khác. Ví dụ như Tướng và Sĩ không thể đi ra ngoài cung, Tượng chỉ được phép ở 7 điểm cố định phía bên mình, Tốt chỉ được phép tung hoành trên đất đối phương, còn phía bên mình cũng bị giới hạn ngặt nghèo một số ô... Để thực hiện những phép kiểm tra này, ta dùng một mảng là legalmove ứng với từng ô trên bàn cờ sẽ cung cấp các thông tin này. Để kiểm tra một quân cờ có được phép ở đó không, ta dùng một mặt nạ bít Maskpiece mà tuỳ theo từng quân sẽ có giá trị khác nhau. Nếu ở ô cần kiểm tra có bit trùng với mặt nạ này được đặt là 1 thì quân cờ đó được phép đến ô đấy.
Ví dụ, ô số 3 có giá trị legalmove[3] = 5 (chuyển thành số nhị phân là 00000101) cho biết các quân cờ được phép đi đến đó là Tượng, Xe, Pháo, Mã. Ngoài ra, ta còn phải kiểm tra nước bị cản (đối với Tượng và Mã) và xử lí cách ăn quân của quân Pháo như một trường hợp đặc biệt. Như vậy, tổng thể sinh các nước đi cho một quân cờ có dạng như sau: p := piece[i]; { Sinh nước đi cho quân cờ p ở vị trí i } for j := 1 to 8 do begin { Số hướng đi tối đa } if offset[p,j] = 0 then break; x:=mailbox90[i]; { Chuyển sang bàn cờ mở rộng} if p in [ROOK, CANNON] then n := 9 else n := 1; for t:=1 to n do { Số nước có thể đi theo một hướng} begin
if (p=PAWN) and (side=LIGHT) then x := x - offset[p, j] else x := x + offset[p, j]; { Nước đi mới } y := mailbox182[x]; { Chuyển về toạ độ bình thường } if side = DARK then t := y else t := 91-y; if (y=-1) or { Ra ngoài lề ? } ((legalmove[t] and maskpiece[p])=0) { Được phép ở vị trí này không ? } then break; { Thoát nếu nước đi không hợp lệ } { Kiểm tra nước cản với Tượng, Mã và xử lí Pháo ở đây } ... Một vấn đề nữa là luật hai Tướng không được đối mặt trực tiếp với nhau. Việc kiểm tra được thực hiện nhờ hàm kingface. Hàm sẽ trả lại giá trị true nếu nước vừa đi gây hở mặt Tướng. Hàm này có thể được đặt trong thủ tục sinh nước Gen. Tuy nhiên đơn giản hơn, ta đặt nó trong thủ tục gen_push, nếu hở mặt Tướng thủ tục này sẽ không đưa nước đi đó vào danh sách các nước đi.
3. Đánh giá một thế cờ Đánh giá một thế cờ là một trong những nhiệm vụ quyết định chương trình chơi cờ của bạn có là "cao thủ" hay không. Căn cứ vào một thế cờ máy sẽ gán cho nó một điểm số (lượng giá tĩnh) để đánh giá độ tốt - xấu. Nhờ điểm này máy mới có thể so sánh các thế cờ với nhau và biết chọn nước đi tốt nhất. Đây là một nhiệm vụ rất khó khăn và phức tạp do không tồn tại một thuật toán tổng quát và thống nhất nào để tính điểm cả. Điểm của một thế cờ dựa trên rất nhiều yếu tố mà khó có thể số hoá hết được như phụ thuộc vào số lượng và giá trị các quân cờ hiện tại, phụ thuộc vào tính hãm, tính biến, thế công, thế thủ của từng quân cờ cũng như cả cục diện trận đấu. Ví dụ, một cặp Mã giao chân, có thể sát cánh tiến quân và tựa lưng phòng thủ thường có giá hơn hai Mã đứng rời. Nhưng cũng có lúc hai Mã đứng rời lại tốt hơn hai Mã giao chân khi Mã này lại cản Mã kia trong một thế trận nào đó. Ta cũng biết rằng, "lạc nước hai Xe đành bỏ phí, gặp thời một Tốt cũng thành công", thế nhưng số hoá điều này qua hàm lượng giá quả là một điều khó quá sức. Chúng ta bắt đầu với việc công nhận các giả thuyết sau: 1. Ta có thể biểu diễn chất lượng một thế cờ bằng một con số. Ví dụ, con số đó có thể là đánh giá của ta về xác suất chiến thắng, nhưng đối với đa số chương trình thì con số đó không có gì đặc biệt, nó chỉ là con số mà mục đích chính là so sánh được với nhau. 2. Chúng ta nên đo chất lượng của một thế cờ tương tự như phép đo của đối phương (do đó, nếu ta coi là đạt được một thế tốt thì đối phương của ta phải thấy đó là thế xấu đối với họ và ngược lại). Điều này tuy không thật đúng lắm, nhưng nó giúp cho thuật toán tìm kiếm của ta làm việc tốt và trong thực tế cũng khá gần với sự thật.
Trong chương này chúng ta chỉ cài đặt phương pháp đơn giản nhưng cơ bản nhất: lượng giá dựa trên cơ sở giá trị của từng quân cờ. Cách tính này sẽ lấy tổng giá trị các quân cờ hiện có của bên mình trừ đi tổng giá trị các quân cờ hiện có của đối phương. Do đó, một thế cờ này hơn thế cờ kia ở chỗ nó còn nhiều quân bên mình hơn, nhiều quân giá trị cao hơn cũng như có bắt được nhiều quân và quân giá trị cao của đối phương hơn không. Cách làm này đã bỏ qua mất những nghệ thuật, chiến lược chơi cờ (mà đó là những điểm để đánh giá một người là giỏi cờ hay không). Các quân cờ được triển khai không theo một chiến lược chung nào hết (vô định). Nó còn tạo cảm giác như cờ "vồ", nghĩa là chỉ nhằm nhằm sơ hở của đối phương là "ăn" ngay mà không cần biết đến hậu quả (điều này không hẳn đúng, lí do sẽ trình bầy dưới). Tuy nhiên phương pháp tính điểm này có những ưu điểm sau: •
•
Là cách tính điểm cơ bản nhất, đóng vai trò chủ đạo trong điểm của một thế cờ. Nó là cơ sở của đa số hàm đánh giá. Ta cũng có thể nhận thấy trong phần lớn thời gian diễn ra trận đấu, hai bên đều tìm cách tiêu diệt quân của nhau. Các phương án, chiến lược thường chỉ được tính như những điểm cộng thêm vào và đóng góp một tỷ lệ nhỏ trong tổng số điểm của một thế cờ. Việc chỉ nhằm "vồ" quân của đối phương nhưng sau khi suy nghĩ sâu nhiều nước cũng đã trở thành "cao cờ" rồi đấy. Nói cho cùng, mục đích chính của chúng ta cũng là "vồ" bằng được quân có giá trị cao nhất - Tướng đối phương. Là cách tính đơn giản nhất và nhanh nhất. Do tính nhanh, ta có thể tăng thêm độ sâu tìm kiếm. Việc tăng thêm độ sâu lại giúp máy có cái nhìn xa hơn, "cao cờ" hơn và nhiều khi khắc phục được nhược điểm của cách tính đơn giản.
Điểm các quân cờ được đánh giá theo kinh nghiệm và cho biết sự tương quan giữa các quân cờ. Sau đây là điểm từng quân mà mọi người thường chấp nhận: Quân cờ Tốt Sĩ Tượng Mã Pháo Xe
Kí hiệu PAWN BISHOP ELEPHANT KNIGHT CANNON ROOK
Điểm 1 (2 nếu đã qua sông) 2 2 4 4.5 9
Bạn cũng có thể theo bất kì thang điểm nào khác tuỳ theo hiểu biết và sở thích của mình. Ví dụ như trong làng cờ Việt nam thường cho điểm các quân hơi khác (xem bài đọc dưới): Tốt - 1 (2 nếu đã qua sông), Sĩ - 2, Tượng - 2.5, Mã - 4.5, Pháo - 5, Xe - 10. Trong chương trình cờ của chúng ta, điểm cụ thể của từng quân cờ là các số nguyên 10, 20, 20, 40, 45 và 90. Ta dùng một mảng piecevalue để lưu điểm từng quân cờ. Cho điểm như vậy không những vẫn giữ nguyên được tương quan và tránh dùng số thực (tính chậm) mà ta còn có một khoảng cách hợp lí giữa các điểm để sau này dễ dàng thêm những điểm bổ xung "thưởng" hoặc "phạt", tức là những điểm căn cứ vào những yếu tố
khác (ví dụ cùng là Pháo nhưng lúc chỉ được 40 điểm do ở vị trí "bí", lúc khác lại có thể đến 85 do ở vị trí Pháo trống và đang đe doạ Pháo lồng). Trong bàn cờ, quân Tướng là quân quan trọng nhất, dù mất mọi quân hoặc đạt được thế cờ nào đi nữa đều không được mất Tướng vì nó dẫn đến thua cờ. Do đó, Tướng thường được cho một điểm rất cao, cách biệt nhiều lần so với các quân khác, đảm bảo điểm tổng của các quân còn lại cùng đủ mọi loại "thưởng", "thêm" đều không bằng được Tướng. Trong chương trình, ta cho nó 1000 điểm. Hàm lượng giá thật đơn giản như sau (chú ý là ta chưa tăng điểm cho Tốt đã qua sông): function Eval: integer; const piecevalue:array[1..7]of integer=(10,20,20,40,45,90,1000); var i, s: integer; begin s := 0; for i:=1 to BOARD_SIZE do begin if color[i] = side then s := s + piecevalue[piece[i]] else if color[i] = xside then s := s - piecevalue[piece[i]]; end; Eval := s; end; Vấn đề cải tiến hàm lượng giá sẽ được bàn riêng trong một chương phần sau. Tham khảo kiến thức cờ Tướng GIÁ TRỊ CỦA CÁC QUÂN CỜ Khái niệm giá trị của các quân cờ không phải là mới đối với những người chơi cờ từ các thế kỷ trước. Từ lâu, làng cờ đã có câu nhận định sức mạnh của các quân chủ lực là "Xe 10, Pháo 7, Mã 3" hoặc đánh giá Xe là quân chủ lực mạnh nhất bằng câu "Nhất Xa sát vạn" hay đánh giá một Tốt qua hà, sức mạnh bằng nửa con Xe (nhất Tốt độ hà, bán Xa chi lực). Thế nhưng sự đánh giá nhận định này về sức mạnh của các quân không chính xác và cũng không đầy đủ. Do đó khái niệm giá trị của các quân cần được xác định rõ và hoàn chỉnh hơn. Thông thường, theo thời gian một ván cờ được chia thành ba giai đoạn: khai cuộc, trung cuộc và tàn cuộc. Trong ba giai đoạn này thì trung cuộc chiếm nhiều thời gian nhất và có ý nghĩa đến thắng thua nhiều nhất. Do đó các định giá quân cờ thường là cho giai đoạn này. Chúng ta đã biết mỗi loại quân cờ hay mỗi binh chủng có đặc điểm, tính năng và tác dụng khác nhau nên sức mạnh của chúng cũng khác nhau, giá trị của chúng cũng không giống nhau. Ngay bản thân mỗi quân cờ cũng không phải lúc nào sức mạnh cũng giữ nguyên như cũ. Khi đứng ở vị trí tốt nó phát huy tính năng tác dụng tối đa khác hẳn với khi nó đứng ở vị trí xấu, không thể phát huy tính năng tác dụng được, hay chỉ phát huy ở mức rất hạn chế. Vậy điều trước tiên chúng ta phải thấy mỗi quân cờ có hai giá trị: giá trị vốn có và giá trị biến động. Giá trị vốn có thường được gọi là giá trị cơ bản (giá trị tĩnh), còn giá
trị biến động được gọi là giá trị tương đối. Các nhà nghiên cứu lí luận về cờ đã trao đổi thống nhất với nhau bảng giá trị cơ bản của các quân như sau: Nếu lấy Tốt chưa qua sông làm chuẩn để xác định giá trị thì nó là quân kém năng lực nhất trên bàn cờ. Khi chưa qua sông nó chỉ kiểm soát hay khống chế mỗi một điểm trước mặt nó nên tạm cho nó có giá trị bằng 1. Khi Tốt qua sông, nó khống chế đến 2 hay 3 điểm trước mặt và bên cạnh (Tốt biên chỉ khống chế được 2). Bây giờ nó mang giá trị tương đối, phải thấy nó mạnh hơn lúc chưa qua sông nên giá trị lúc này của nó phải bằng 2. Đối với Sĩ, Tượng là loại binh chủng phòng ngự, có nhiệm vụ chính là bảo vệ an toàn cho Tướng, đôi lúc chúng cũng trợ giúp các quân khác tấn công. Trong giai đoạn trung cuộc, Tốt đối phương qua sông đổi được một Sĩ hoặc một Tượng. Do đó người ta đánh giá Sĩ hoặc Tượng có giá trị bằng một Tốt đã qua sông, tức là bằng 2. Nhưng vì so sánh năng lực giữa Tượng và Sĩ thì thấy Tượng có phần đắc lực hơn nên định giá trị Sĩ là 2 thì Tượng phải là 2.5 mới công bằng. Quân Mã trên bàn cờ có khả năng khống chế tối đa 8 điểm. Mỗi bước nhẩy của nó vượt được hai tuyến đường ngang hay dọc. So ra thì Mã cũng mạnh đấy nhưng tác chiến ở tầm cự ly ngắn mà thôi. Nếu so với hai Tốt đã qua sông cặp kè nhau thì năng lực của Mã không hơn bao nhiêu, do đó định giá trị cơ bản của Mã 4.5 là vừa. Nên nhớ hai Tốt cặp kè nhau khi qua sông phải có giá trị hơn 4. Pháo là binh chủng tác chiến tầm xa rất có hiệu quả. Trên đường dọc, Pháo khống chế tối đa chỉ 8 điểm nhưng nó còn khống chế được cả đường ngang. Mặt khác Pháo là quân có tính cơ động cao, nó có thể đi một bước vượt đến 9 tuyến đường. Nhưng ở cự li gần, nhiều khi Pháo bất lực, không làm gì được để khống chế đối phương. Do đó so với Mã thì phải thấy mỗi quân có một sở trường riêng, sức mạnh của chúng coi như tương đương nhau. Thế nhưng trong giai đoạn trung cuộc quân số của hai bên tương đối còn nhiều, Mã đi lại dễ bị cản trở, còn Pháo thì nhanh nhẹn hơn, dễ phát huy năng lực hơn nên giá trị cơ bản của Pháo phải hơn Mã một chút và bằng 5. Đối với Xe thì rõ ràng là một binh chủng cơ động mạnh nhất, nó khống chế ngang, dọc tối đa đến 17 điểm. Nếu đem Xe đổi lấy một Pháo và một Mã của đối phương thì thấy có phần thiệt thòi một chút, còn đối với hai Pháo thì có thể coi là cân bằng. Do đó giá trị cơ bản của Xe là 10. Còn Tướng, bản thân nó không có sức mạnh gì nhiều dù đôi khi nó cũng làm được nhiệm vụ trợ giúp cho các quân phe nó tấn công có kết quả. Thế nhưng giá trị cơ bản của nó thì vô cùng to lớn, không một quân nào so sánh được. Tất cả các quân đều có thể đổi, có thể hi sinh nhưng riêng quân Tướng không thể đánh đổi mà cũng không thể hi sinh, vì mất Tướng là thua cờ. Do đó cũng không nên định giá trị cơ bản của Tướng vì định cũng chẳng có ý nghĩa gì.
(Sưu tầm)
4. Xử lí một nước đi "thử" Trong quá trình tính toán tìm nước đi tốt nhất cho mình, máy thường phải đi "thử" một nước rồi lại tính tiếp. Sau đó nó sẽ phục hồi lại nước đi này. Do quá trình thử - phục hồi diễn ra rất nhanh và không hiện ra màn hình nên máy có thể thử hàng chục triệu lần mỗi khi đến lượt. Việc đi thử được thực hiện nhờ gọi hàm Makemove. Việc khôi phục nước đi này nhờ gọi thủ tục UnMakemove. Như vậy, chương trình thường xuyên thực hiện các câu lệnh dạng như sau: while tất cả các nước đi có thể có từ một thế cờ do begin Makemove(một_nước_đi); { Đi thử một nước } best := Tính điểm của nước đi thử đó; UnMakemove; { phục hồi nước đi này} Lưu nước đi có điểm cao nhất; end; Để tính được điểm của nước đi vừa thực hiện, nhiều khi máy lại phải thử đi tiếp một số nước nữa. Cứ như vậy các hàm Makemove và UnMakemove có thể được gọi lồng nhau rất sâu. Khi đi thử, hàm Makemove phải lưu lại các thông tin cần thiết cho việc khôi phục lại sau này. Cách đơn giản nhất - lưu lại cả bàn cờ, không dùng được do các thông tin phải lưu quá nhiều. Thực chất, ta chỉ cần lưu các thông tin bao gồm: điểm xuất phát của quân cờ, điểm đến và loại quân cờ mà nó bắt được (nếu có). Để lưu các thông tin này, hàm sử dụng một mảng hist_dat các bản ghi chứa thông tin đó và con trỏ hdp được tổ chức như một ngăn xếp, nghĩa là nước đi sau cùng sẽ được phục hồi trước nhất. Khi máy đi "thử" cho một bên một nước, thì nước sau sẽ đến lượt đối phương. Do đó hàm này cũng phải thay đổi giá trị các biến toàn cục về bên đi và đối thủ side và xside (việc thay đổi này là rất cần thiết để hàm Gen sinh nước đi cho đúng bên tới lượt). Nó cũng tăng biến ply (độ sâu) thêm 1 do đã dấn sâu thêm một bước trên cây tìm kiếm. Hàm khôi phục nước đi thử UnMakemove có nhiệm vụ là làm mọi thứ ngược lại với hàm Makemove. Một vấn đề nẩy sinh ở đây là nếu một quân Tướng bị bắt thì có cần phải đi "thử" nữa không. Nếu mất một Tướng, ván cờ sẽ kết thúc ngay lập tức, do đó không cần phải đi thử tiếp và nhánh cây trò chơi bị cắt tại đây dù vẫn chưa đạt đến độ sâu tìm kiếm tối đa. Còn nếu để máy đi thử tiếp, máy sẽ coi Tướng như một quân bình thường (dù có giá trị rất
cao) và có thể đổi quân được - nó sẵn sàng để mất Tướng miễn là ngay sau đó ăn lại được Tướng đối phương (hình 2.8). Ta buộc phải cắt nhánh ở những nơi Tướng đã bị mất (hình 2.9).
Để xử lí những trường hợp này, ta cần chuyển Makemove thành một hàm boolean, trả lại giá trị true nếu nước đi thử này lại ăn được Tướng đối phương. Nếu ăn được, hiển nhiên điểm sẽ rất cao (được 1000 điểm) và không cần phải tính điểm cho những nhánh con nữa. Phần chương trình đi thử có dạng như sau:
while tất cả các nước đi có thể có từ một thế cờ do begin if Makemove(một_nước_đi) then { Đi thử một nước. Hàm trả về giá trị true nếu ăn được Tướng đối phương } best := 1000 - ply { Điểm của nước cờ nếu ăn được Tướng đối phương} else best := Tính điểm của nước đi thử; UnMakemove; { phục hồi nước đi này} Lưu nước đi có điểm cao nhất end; Trong các câu lệnh trên có một chỗ hơi khó hiểu: tại sao ta lại cho nước ăn Tướng là 1000 - ply mà không phải đúng 1000? Biến ply là biến chỉ độ sâu tìm kiếm. Nếu không có biến này và trong trường hợp Tướng (bên máy tính) sắp bị bắt dù đi bất cứ nước nào thì mọi nhánh cây đều trả về điểm -1000. Do đó máy sẽ chọn nhánh đầu tiên để đi. Nước đi này có thể không hay do chấp nhận thua quá sớm như minh hoạ trên hình 2.10. Máy nên chọn nước thua lâu nhất để hi vọng đối phương đi "nhầm" (còn nước còn tát) và đúng tinh thần chiến đấu hơn.
5. Tìm kiếm AlphaBeta Ta đã thấy chương trình mẫu của thủ tục AlphaBeta trong chương 1. Hàm AlphaBeta của VSCCP về cơ bản là giống như vậy, chỉ bỏ đi các tham số pos (thế cờ hiện tại - ở đây là giá trị của hai mảng piece và color) ở một số hàm do đã biến đổi trực tiếp vào các mảng toàn cục piece và color (nhờ các hàm Makemove và UnMakemove mà ta đã đề cập đến ở trên) nên chúng chính là thế cờ đang xét.
function AlphaBeta(alpha, beta: integer; depth: byte): integer; var i, best, value: integer; begin if depth = 0 then AlphaBeta := Eval else begin Gen; best := -INFINITY; i := gen_begin[ply]; { Khởi đầu để lặp tất cả các nước } while (i < gen_end[ply]) and (best < beta) do begin if best > alpha then alpha := best; if MakeMove(gen_dat[i].m) then value := 1000-ply else value := -AlphaBeta(-beta, -alpha, depth-1); UnMakemove; if value > best then begin best := value; if ply = 0 then newmove := gen_dat[i].m; end; inc (i); end; { while } AlphaBeta := best; end; end; Nước đi tốt nhất (điểm cao nhất) ở độ sâu 0 (ply = 0) được lưu vào một biến toàn cục newmove. Máy sẽ chọn đi theo nước này. Thủ tục gọi hàm AlphaBeta chỉ có các lệnh đơn giản như dưới. Nó gọi hàm này với các giá trị khởi đầu thích hợp để bắt đầu tìm kiếm. (***** THINK - MÁY TÍNH TÍNH NƯỚC ĐI *****) procedure ComputerThink; { Tìm nước đi tốt nhất cho máy } begin AlphaBeta (-INFINITY, INFINITY, MAX_PLY); end;
6. Xử lí điều khiển của người chơi Người chơi dùng bàn phím để điều khiển các quân bên mình khi đến lượt: dùng các phím mũi tên điều khiển con trỏ màn hình chạy đến quân cờ thích hợp, bấm phím Space hoặc Enter để chỉ mình sẽ đi quân này, chuyển con trỏ đến chỗ mới và vẫn bấm phím Space hoặc Enter để chỉ đó là chỗ đến. Người chơi có thể ngừng chơi giữa chừng bằng cách bấm phím ESC. Hàm xử lí điều khiển của người chơi GetHumanMove phải làm nhiệm
vụ xử lí các phím điều khiển và trả về nước đi được chọn trong biến toàn cục newmove. Hàm cũng xử lí hai biến toàn cục x, y là toạ độ của con trỏ màn hình. Một chương trình chơi cờ tốt còn phải giúp đỡ người chơi không đi sai luật. Các nước đi không hợp lệ (dù cố ý hay vô tình) đều cần được phát hiện và ngăn cấm. Cách làm thì có nhiều, ở đây chúng tôi giới thiệu phương pháp đơn giản nhất sử dụng hàm sẵn có: hàm Gen. Đầu tiên, ta gọi nó để sinh ra tất cả những nước đi có thể có từ thế cờ hiện tại với bên đi là người - đó chính là các nước đi hợp lệ. Mỗi khi người chơi chọn một nước, máy sẽ kiểm tra xem nó có trong danh sách các nước đi hợp lệ không. Nếu có, máy sẽ chấp nhận nước đi đó và thoát khỏi hàm GetHumanMove. Nếu không nó sẽ đòi người chơi phải chọn lại. function GetHumanMove: boolean; const x: byte = 5; y: byte = 5; var ch: char; selecting, from, t, i: integer; begin Gen; { Dùng để kiểm tra nước đi hợp lệ của người chơi } GetHumanMove := false; selecting := 0; while true do begin MoveTo(x, y); ch := ReadKey; case ch of #13, ' ': begin { Đánh dấu/chọn một nước đi } t := x + (y-1)*SIZE_X; if selecting=0 then begin { Người chơi đánh dấu } if color[t]=side then begin selecting:=1; from := t; DrawCell(t,SELECT); end; end else begin { Người chơi chỉ ra nước đi đến } if t from then DrawCell(from, NORMAL); if color[t]=side then begin from := t; DrawCell(t, SELECT); end else begin newmove.from := from; newmove.dest := t; { Kiểm tra xem nước đi vừa chọn có trong danh sách các nước đi hợp lệ không. Nếu có sẽ thoát khỏi hàm này để đến lượt máy } for i := gen_begin[ply] to gen_end[ply]-1 do if (gen_dat[i].m.from = from) and (gen_dat[i].m.dest = t)then exit; { Nếu nước đó không hợp lệ, hiện lại quân xuất phát, để người chơi chọn nước đi khác }
DrawCell(from, SELECT); end; end; end; #27: { Xử lí các phím khác ở đây } ...
7. Cập nhật một nước đi Sau khi "suy nghĩ" xong, đã đến lúc máy hoặc người phải thực hiện một nước đi thực sự. Chương trình phải cập nhật tất cả các thông tin cần thiết liên quan đến nước đi và hiện hình những thay đổi. Việc này được thực hiện nhờ gọi hàm UpdateNewMove. Nó có khá nhiều chỗ giống như hàm Makemove nhưng đơn giản hơn. Tham số quan trọng nhất mà nó quan tâm là nước đi được người hoặc máy chọn đi (đặt trong biến toàn cục newmove). Hàm cũng đồng thời sẽ kiểm tra tình trạng thắng cờ của bên vừa đi (ăn được Tướng đối phương) và trả về giá trị true nếu có một bên thắng. Vòng lặp chính sẽ căn cứ vào giá trị chân lí này để ngừng chơi.
8. Vòng lặp chính xử lí trò chơi Sau khi có các hàm và thủ tục cần thiết, đã đến lúc ta liên kết chúng lại thành một trò chơi hấp dẫn. Trong phần thân chương trình chính, ta sẽ bắt gặp vòng lặp repeat. Vòng lặp này sẽ lặp mãi cho đến khi người chơi bấm phím ESC hoặc một bên thắng cờ. Tuỳ theo bên tới lượt nó sẽ gọi hàm xử lí thích hợp cho người hoặc máy. Sau khi nhận kết quả nó lo cập nhật các số liệu cần thiết, chuyển đối phương thành người đến lượt chơi. repeat if side=computerside then ComputerThink else if GetHumanMove then break;{ Thoát nếu ESC được bấm} side := xside; xside := 3 - xside; { Đổi bên tới lượt chơi } until UpdateNewMove;
9. Hiện thông tin Đến đây, chương trình đã sẵn sàng đọ sức với bạn rồi đấy. Nhưng có thể bạn muốn biết thêm một vài số liệu như: thời gian mỗi lần máy nghĩ, hệ số phân nhánh của cây trò chơi, số thế cờ nó phải tính giá trị, khả năng xét thế cờ trung bình của máy là bao nhiêu cũng như nhiều số liệu khác. Việc quan sát các số liệu này còn giúp ta biết các cải tiến, sửa đổi về sau có thực sự hoạt động tốt không. Cách lấy số liệu rất đơn giản, ta khai báo thêm một số biến toàn cục, thêm một số lệnh và
cuối cùng in chúng ra màn hình ở chỗ thích hợp. Một số thông tin rất khó lấy chính xác, ta sẽ tạm bằng lòng với những số liệu gần đúng vậy. Để biết mỗi lần nghĩ, máy đã lượng giá bao nhiêu thế cờ - một thông tin vô cùng quan trọng, ta khai báo một biến toàn cục evalcount và mỗi khi hàm Eval được gọi ta lại tăng giá trị của nó thêm một. Ta cũng cần biết máy đã nghĩ trong bao lâu bằng cách khai báo các biến để tính thời gian. Thời gian được đo căn cứ vào số xung đồng hồ ở vị trí tuyệt đối $40:$6c trong bộ nhớ. Từ đây ta tính được thời gian tính bằng giây nhờ chia cho 18.23 - số xung trong mỗi giây. Khi biết được thời gian đã tiêu tốn có thể ước tính tốc độ lượng giá của máy bằng cách chia tổng số nút đã lượng giá cho thời gian tiêu tốn. Tất nhiên thời gian tiêu tốn này ngoài thời gian tiêu cho hàm lượng giá còn phải chi cho nhiều thứ khác như chạy các lệnh của hàm Gen, hàm AlphaBeta... nên cách làm này chỉ cho biết con số ước lượng mà thôi. Để có được hệ số phân nhánh chính xác ta phải thống kê qua một số dữ liệu lớn. Chú ý rằng, số nhánh con được sinh ra mỗi lần gọi hàm Gen được đo bằng hiệu gen_end[ply]gen_begin[ply]. Do đó hệ số phân nhánh được đo bằng cách lấy tổng các nhánh con này rồi chia cho số lần gọi hàm Gen (đo bằng biến gencount). Càng chơi lâu, các dữ liệu đo được càng lớn, kết quả sẽ càng chính xác. Ngoài ra, bạn có thể còn muốn hiện chú giải nước vừa đi của máy để dễ theo dõi và xem xét điểm đạt được. Hãy chú ý cách tính các con số này (chú ý số 65 là mã của chữ cái A, 9 là số cột của một dòng bàn cờ). Sau đây là những chỗ cần bổ xung để bạn có được các thông tin cần thiết. Những chỗ thêm, sửa từ nay về sau đều được đánh dấu bằng một mũi tên trắng cho dễ tìm. const ... { Các biến dùng để đo hệ số phân nhánh } brandtotal: longint = 0; gencount: longint = 0; ... Var evalcount: longint; systicks: longint absolute $40:$6C; { Biến để đo thời gian } ... procedure Gen; ... brandtotal := brandtotal + gen_end[ply] - gen_begin[ply]; inc(gencount); end; function Eval: integer; var i, s: integer; begin inc(evalcount);
... procedure ComputerThink; { Tìm nước đi và hiện thông tin } var best: integer; tickstart, tickend: longint; begin evalcount := 0; tickstart := systicks; best := AlphaBeta(-INFINITY, INFINITY, MAX_PLY); { Phục vụ hiện các thông tin theo dõi } tickend := systicks; textcolor(7); gotoxy(50, 4); write('Do sau : ', MAX_PLY); gotoxy(50, 5); write('So nut luong gia: ', evalcount, ' '); gotoxy(50, 6); write('He so phan nhanh: ', brandtotal/gencount:3:2, ' '); gotoxy(50, 7); write('Thoi gian (giay): ', (tickend-tickstart)/18.23:0:2, ' '); gotoxy(50, 8); write('Toc do xet nut : ', evalcount*18.23/(tickend-tickstart+1):0:0, ' '); gotoxy(50, 9); write('Diem dat duoc : ', best, ' '); gotoxy(50,11); write('May tinh di : ', chr(((newmove.from-1) mod SIZE_X)+65), SIZE_X -(newmove.from-1) div SIZE_X, chr(((newmove.dest-1) mod SIZE_X)+65), SIZE_X-(newmove.dest-1)div SIZE_X,' '); end;
10. Chạy thử Bây giờ bạn hãy dịch và chạy thử chương trình trên (toàn bộ nguồn trong Phụ lục A). Cẩn thận nhé, ta chưa cài chức năng cho phép hoãn đi. Nếu đi nhầm bạn không thể đi lại. Chương trình máy tính lại không "nhầm lẫn" và với độ sâu ngầm định 4 (tức là nghĩ trước tới 4 nước đi) nó có khả năng đánh bại những người chơi có trình độ trung bình và chơi ẩu. Bạn hãy chịu khó suy nghĩ kĩ một chút, cẩn thận một chút thì có thể thắng được. Hình 2.11 là thông tin in ra màn hình sau khi máy đi nước đầu tiên. Chương trình được chạy trên máy tính Pentium 166MHZ cài hệ điều hành Windows 98 và tìm kiếm đến độ sâu bằng 4. Các số liệu cho ta biết cây trò chơi có hệ số phân nhánh khoảng 41. Nếu lấy tròn là 40 thì ta có bd = 404 = 2560000 (tức là số nút lá của cây trò chơi có khoảng trên hai triệu rưởi nút). Đó chính là số nút ta phải xét nếu dùng phương pháp tìm kiếm Minimax. Với tốc độ trung bình xét được 55985 nút mỗi giây thì thời gian để xét hết bằng phương pháp này sẽ mất khoảng 2560000/55985 » 46 giây. Ở đây, ta đang dùng phương pháp tìm kiếm AlphaBeta, số nút phải xét chỉ còn 150480, nghĩa là ít hơn 17 lần và tổng thời gian phải bỏ ra chỉ 2.63 giây thay cho 46 giây. Nước đi Mã vào góc không được hay lắm - nhưng không sao, đây mới chỉ là phiên bản đầu tiên và độ sâu tìm kiếm còn hạn chế.
Độ sâu
Số nút lượng giá
Thời gian (giây)
Hệ số phân nhánh
Điểm
Nước đi
Số lần tăng số nút lượng giá
1
44
0.00
44.00
40
B7B0
-
2
877
0.00
43.78
-5
B7B0
19.9
3
24411
0.38
41.73
35
B7B0
27.8
4
150480
2.63
41.75
-5
B9A7
6.2
5
5574794
90.73
40.40
10
B7C7
37.0
6
62929742
1031.32
40.15
-5
B7C7
11.3
7
723007130
13355.24
39.86
10
B7C7
11.5
Hình 2.11 Nếu bạn chưa hề thực hiện một cải tiến nào mà chỉ nhập nguyên vẹn chương trình mẫu từ Phụ lục A và dịch chạy thì các giá trị về độ sâu, số nút lượng giá, hệ số phân nhánh, điểm đạt được, máy tính đi (trừ những thông số liên quan đến thời gian do tuỳ thuộc loại máy bạn có) phải y hệt như hình 2.11. Nếu có điều gì khác có nghĩa là đã có sai sót trong khi nhập chương trình nguồn, bạn cần thận trọng kiểm tra chương trình. Tuỳ theo sức mạnh máy tính (và cả khả năng chờ đợi), bạn hãy thử tăng độ sâu tìm kiếm bằng cách sửa khai báo hằng số MAX_PLY từ 4 lên 5 hoặc 6. Đừng ham tăng nhiều. Do bùng nổ tổ hợp, nếu tăng nhiều bạn sẽ không chờ nổi đâu. Càng tăng độ sâu, bạn sẽ thấy chương trình chơi càng bớt hớ hênh và càng khó thắng đấy. Bạn hãy thử kiểm nghiệm với độ sâu 5 xem sao. Các con số đều tăng khủng khiếp. Trên máy tính của tôi, thời gian xét nước đầu tiên đã tăng từ 2.63 tới 88 giây (tăng 33 lần). Số nút phải lượng giá là 5574794 nút (tăng 37 lần). Nếu máy tính của bạn là loại tốt, hãy thử tiếp với độ sâu 6. Trên máy tính của tôi, thời gian máy "nghĩ" nước đầu tiên đã mất tới 1031 giây (hơn 17 phút, gần gấp 12 lần). Còn số nút phải lượng giá là 62929742 (gấp 11 lần). Bạn có thể thấy các con số cũng tăng rất nhiều nhưng dù sao chỉ tăng cỡ 11 lần. Hệ số phân nhánh được thống kê qua số lượng nhiều hơn đã tụt xuống còn 40.15. Nước được chọn đi hơi khác là B7C7 (có lẽ là nước tốt hơn). Còn độ sâu 7 tôi chỉ dám thử một lần vì thời gian tiêu tốn cho lần nghĩ nước đầu tiên mất đến... 3.7 giờ (trong khi qui định tổng cộng tất cả các nước của một đấu thủ chỉ được trong 1 giờ khi thi đấu). Nếu ta cho rằng tăng một độ sâu thời gian tính toán chỉ dài thêm trung bình 10 lần thì chỉ cần đến độ sâu 11, các máy tính cỡ Pentium 166 MHZ sẽ mất đến... 4 năm để nghĩ nước đi đầu tiên (còn đến độ sâu 14 thì mất... 4223 năm).
Ta hãy thử ước mơ nếu có được chiếc máy tính mạnh và chuyên dụng (cho cờ) như Deep Blue thì tính được đến đâu. Loại máy lần đầu tiên thắng được Kasparov (năm 1997) có khả năng xét được trung bình 200 triệu (đôi khi lên đến 400 triệu) nước đi mỗi giây. Với độ sâu 7 (mà máy tính của tôi phải ỳ ạch trong 3.7 tiếng đồng hồ) sẽ được máy chiếu cố trong khoảng... 3 giây. Nếu được nghĩ một nước trong 3 phút (thời gian được phép trung bình cho một nước) thì độ sâu có thể đến được sẽ khoảng từ 8 đến 10 (nếu không có cải tiến đặc biệt). Đến đây ta cũng có thể thấy, các chương trình cờ thuộc loại tốt nhất chạy trên các máy tính thông thường (loại PC và không gắn các mạch chuyên dụng) cũng chỉ tìm kiếm được đầy đủ đến độ sâu từ 5 đến 8 mà thôi (với giới hạn thời gian thi đấu bình thường). Dù vậy các cố gắng cải tiến bỏ ra cũng phải rất nhiều và rất lâu mới đạt được đến các độ sâu như vậy (bạn sẽ được nghiên cứu một vài cải tiến nhằm nâng độ sâu trong thời gian chơi hợp lí). Chú ý là thông báo về tốc độ xét nước có thể rất khác nhau khi thay đổi độ sâu cực đại do hàm lượng giá dùng ở đây quá đơn giản, tỷ trọng thời gian không lớn so với các hàm khác và thời gian đo lại bao gồm tất cả mọi thứ nên các con số bị ảnh hưởng nhiều. Tuy nhiên, nếu với cùng độ sâu thì tốc độ hiện tương đối ổn định. Toàn bộ mã nguồn chương trình cờ Tướng VSCCP phiên bản 1.0 cho trong phần Phụ lục A.
Tổng kết chương 2 Chương này đi sâu vào cách viết một chương trình cờ Tướng đơn giản. Những điểm quan trọng trong chương là các khai báo các loại dữ liệu khác nhau, cách biểu diễn một bàn cờ, các thủ tục và hàm cơ bản. Chú ý là các cấu trúc dữ liệu và các khai báo trong chương trình mẫu rất đơn giản nhưng cũng rất hiệu quả. Thủ tục Gen là lằng nhằng, phức tạp và dễ sai nhất. Thuật toán AlphaBeta tuy giống như đã đề cập đến trong chương 1 nhưng đến chương này bạn cần phải hiểu nó thật sự. Bạn cũng cần nắm vững cách lấy số liệu cũng như hiểu các số liệu này để tiếp tục theo dõi các cải tiến chương trình trong các chương sau.
GỢI Ý PHÁT TRIỂN 1. Thay cho mảng một chiều biểu diễn bàn cờ, bạn hãy thử dùng mảng hai chiều (dạng piece[1..10, 1..9] of byte). Tự mình khám phá các ưu, nhược điểm của cách làm này.
2. Một byte là quá đủ để lưu thông tin về quân cờ và bên chơi. Nói cách khác, mảng color không thật cần thiết. Bạn hãy sửa lại chương trình sao cho vẫn giữa nguyên khai báo của mảng piece, nhưng lại bỏ được mảng color. 3. Tại sao bạn lại phải chuyển sang bàn cờ mở rộng kích thước 13´14 (và để rồi lại phải chuyển về bàn cờ thường 9´10) để làm mỗi nhiệm vụ kiểm tra việc vượt quá biên cho nước cờ đang được sinh? Bạn có thể dùng trực tiếp bàn cờ mở rộng này để chơi được không (dùng trực tiếp bàn cờ 13x14)? 4. Bạn hãy tự sửa chương trình cho phép cả hai bên đều là máy (hoặc đều là người) chơi. Nghĩa là cho máy có thể tự đấu. 5. Thử để máy đấu cả hai bên với độ sâu giống và khác nhau (ví dụ như 3-3, 3-4, 44, 3-5 hoặc 4-5) và thay đổi lượt đi trước của các bên. Bên nào thắng? 6. Bạn hãy tự cải tiến hàm lượng giá theo ý mình. Ví dụ hãy thử cho điểm các quân cờ tuỳ ý. 7. Hãy thi đấu nhiều với máy. Chắc chắn trong quá trình chơi máy sẽ đi nhiều nước khá lạ lùng (trên quan điểm của người chơi bình thường). Hãy ghi lại những nước đi đó và hãy thử lí giải vì sao máy lại đi như vậy. Khảo sát kĩ và hãy thử cải tiến hàm lượng giá nếu máy phạm sai lầm nhiều. 8. Bạn hãy cài thêm một chức năng nữa cho người chơi: chức năng "hoãn", nghĩa là bỏ nước vừa đi (hoặc một loạt nước vừa đi) và cho người chơi đi lại nước khác. Chú ý: hãy tận dụng tối đa những thủ tục có sẵn như UnMakemove. 9. Bạn hãy cài chức năng "gợi ý" (hay "mách nước") cho người chơi biết nên đi nước nào bằng cách chỉ ra các nước đi chống cự tốt nhất. Bạn nên tận dụng những kết quả có sẵn trong lần máy nghĩ trước đó mà không cần phải tính lại. 10. Hãy cho phép người chơi ghi lại ván cờ đang chơi dở và lúc khác có thể nạp vào để chơi tiếp. 11. Cho phép người chơi được bầy cờ thế. Bầy xong mới thi đấu. 12. Hãy tự cải tiến giao diện: dùng đồ hoạ, cài thêm chuột, thêm menu, phím gõ tắt... Hãy tham khảo các chương trình cờ có trên thị trường để làm cho tốt. 13. Bình luận: Theo dõi một trận đấu bất kì, mỗi khi một đấu thủ đi một nước thì cố gắng giải thích lí do chọn nước đó. Ví dụ giải thích nước đi đó dẫn tới thế cờ hay (nhờ điểm cao), sẽ ăn được quân nào đó của đối phương hoặc tránh được mất quân, hoặc dẫn tới thắng hoặc tránh thua sau bao nhiêu nước... 14. Ghi nước đi: Ta đã làm quen với cách ghi nước đi đơn giản ở trên. Ví dụ như A1A6 nghĩa là quân cờ ở vị trí A1 sẽ chuyển đến vị trí A6. Cách này xử lí thật đơn giản. Tuy vậy, đây không phải là cách ghi nước đi thường dùng trong "làng cờ". Nếu bạn đưa bản ghi một ván cờ như thế cho một người chơi cờ bình thường thì họ không hiểu. Ta cần ghi một ván cờ theo các qui định ghi chép biên bản ván đấu (do Liên đoàn cờ Việt nam đưa ra). Các qui định này như sau: •
Bàn cờ được tính theo cột (hình 2.12). Bên nào tính theo cột bên đó.
• • •
Kí hiệu quân cờ: Tg - Tướng; S - Sĩ; T - Tượng; X - Xe; P - Pháo; M - Mã; B - Tốt (Binh). Để phân biệt các quân cùng loại đứng trước, sau hay ở giữa thì dùng các chữ viết tắt: t - trước, s - sau, g - giữa. Kí hiệu đi quân:
+Tấn (tiến): (.) dấu chấm. Chú ý là nước tấn của Xe, Pháo, Tốt được ghi hơi khác với Mã, Tượng. Ví dụ: X2.6 có nghĩa Xe ở cột 2 tiến 6 ô. Còn M 2.3 có nghĩa là quân Mã đang ở cột 2 tiến sang cột 3). +Thoái (lui): (/) gạch chéo. Ngược với tấn. Ví dụ M7/8 nghĩa là Mã cột 7 thoái lui sang cột 8. +Bình (sang ngang): (-) dấu ngang (P2-5 nghĩa là Pháo 2 sang ngang đến cột 5). Ví dụ: Các nước đi dẫn đến thế cờ như hình 2.12 (thế này có tên dài dòng là: Pháo đầu Xe tuần hà đối bình phong Mã cổ điển) được ghi như sau: 1. P2-5 M8.7 2. M2.3 C3.1 3. X1-2 X9-8 4. X2.4 M2.3
Bài đọc VÀI NÉT VỀ NGUỒN GỐC CỜ TƯỚNG Cờ Tướng ra đời ở đâu? Ở thời đại nào? Có liên quan gì đến cờ Vua? Đó là những câu hỏi mà người hâm mộ rất muốn tìm hiểu. Nguồn gốc cờ Tướng là vấn đề rất thú vị, hiện nay vẫn còn đang tranh luận và tiếp tục được tìm tòi, khảo cứu. Các nhà nghiên cứu đã bỏ ra nhiều công sức để tìm ra câu trả lời. Tuy vậy cho tới nay, có một điều phần đông chuyên gia thừa nhận là: Cờ Tướng xuất xứ từ Trung Quốc, trong khi cờ Vua lại có nguồn gốc từ một loại cờ cổ của Ấn Độ (xuất hiện vào khoảng thế kỉ thứ 6, được truyền bá sang Iran rồi sang châu Âu và phát triển thành cờ Vua như ngày nay). Hai loại cờ này có nguồn gốc khác nhau nhưng lại có những điểm tương đồng như cách đi của một số quân. Dĩ nhiên không loại trừ trường hợp đã từng có những mô hình tượng trưng cho chiến trường cổ đại cùng được hình thành từ nhiều miền đất khác nhau và mang những đặc điểm riêng biệt. Trong quá trình tiến hoá của lịch sử, do sự hoà nhập, giao lưu đã hình thành hai loại cờ Tướng của phương Đông và cờ Vua của phương Tây cùng có những điểm giống nhau. Cờ Tướng theo Hán văn gọi là Tượng kỳ. Tượng có nghĩa là hình tượng, tượng trưng chứ không phải là quân voi (Tượng) trên bàn cờ. Theo các văn kiện lịch sử và các văn vật được phát hiện thì cờ Tướng xuất hiện từ thời Chiến Quốc (từ năm 403 đến 221 trước công nguyên) mà tiền thân là "Lục bác kỳ", một loại cờ rất thịnh hành ở thời kì đó. Nếu đúng như vậy thì cờ Tướng đã có trên 2000 năm lịch sử. Nhưng có thể khẳng định rằng cờ Tướng lúc đó mới có mô hình sơ khai chứ chưa phải loại cờ Tướng mà chúng ta chơi ngày nay. Đặc biệt cờ Tướng cổ đại không có quân Pháo. Các nhà nghiên cứu đều nhất trí là quân Pháo được bổ xung từ thời nhà Đường (sau năm 618) là quân cờ ra đời muộn nhất trên bàn cờ Tướng bởi cho tới lúc đó con người mới tìm ra vũ khí "Pháo" sử dụng trong chiến tranh - đó là các loại máy móc thô sơ dùng để bắn các viên đá to. Trong một thời gian dài, quân Pháo trong chữ Hán viết với bộ "thạch". Cho tới đời Tống (năm 960 1276) khi phát minh ra loại Pháo mới mang thuốc nổ thì quân Pháo mới được viết lại với bộ "hoả". Điều lí thú nhất là theo các tài liệu lịch sử cờ Tướng ở thời Đường được gọi là Tượng hý (du hý - trò chơi) có đặc điểm là quân cờ lập thể, bàn cờ có 8 ´ 8 = 64 ô vuông xen kẽ hai mầu trắng đen, giống hệt bàn cờ Vua hiện nay. Loại bàn cờ này đã được để lại trên các bức tranh dệt "Cầm, Kỳ, Thi, Hoạ" thời Đường. Tại Uyên Ương trì, Vĩnh Xương, tỉnh Cam Túc (Trung Quốc) người ta cũng phát hiện dạng bàn cờ này trên các vật dụng bằng sứ cổ đại với 64 ô. Nhiều ý kiến cho rằng loại bàn cờ này rất hợp với các con số mà nhiều học thuyết thuộc nền văn minh Trung hoa thường đề cập đến như "Thái Cực, Lưỡng Nghi, Âm dương, Tứ Tượng, Bát quái, Lục thập tứ ngao"... Trong khi đó, theo Bách khoa toàn thư của nước Anh thì trước thế kỉ 13, người châu Âu sử dụng loại bàn cờ 64 ô cùng mầu. Như vậy lịch sử cờ Tướng, cờ Vua ra sao? Cho tới nay các nhà nghiên cứu chưa có đồng luận điểm. Nhưng có một điều có thể gọi là chung:
Cờ Tướng hiện đại được hoàn chỉnh vào đời Tống. Các quy định về bàn cờ, quân cờ rất hợp với cơ chế quân sự thời đó: Tướng soái ở trong dinh chỉ huy, có vệ sĩ túc trực, binh chốt có 5 quân đúng với luật "ngũ nhân vi ngũ" (năm người một đội ngũ). Điều này được ghi chép lại trong nhiều tác phẩm đời Tống như "Quảng tượng kì đồ" của Triều Vô Cửu, "Đả mã đồ kinh" của Lý Thanh Chiếu, bài thơ "Tượng dịch" của thi sĩ Lưu Khắc Trang. Cuốn kỳ phổ đầu tiên về cờ Tướng hiện đại là "Sự Lâm Quảng ký" của Trần Nguyên Tĩnh ở thời kì cuối đời Tống (cách đây hơn 700 năm) trong khi tác phẩm đầu tiên về cờ Vua được xuất bản tại Tây Ban Nha vào năm 1495.
CÂU CHUYỆN VỀ HÌNH CÁC QUÂN CỜ TƯỚNG Người châu Âu (và cả nhiều người châu Á) học chơi cờ Tướng rất khó khăn do không nhớ nổi mặt chữ của quân cờ. Đơn thuần chỉ là các chữ Hán, không có hình tượng rõ ràng, cụ thể như ở cờ Vua. Các chữ tượng hình này trông khá lằng nhằng rắc rối (tôi chắc bạn không thể đảm bảo vẽ lại được toàn bộ các quân cờ mà không nhìn mẫu). Biết thế nhưng tại sao người châu Á chúng ta (và cả Việt nam) lại không thay đổi mà vẫn cứ dùng mãi những quân cờ chữ truyền thống đó cho tới tận hôm nay? Thực ra, đã nhiều lần người ta muốn đổi và thử đổi. Mấy năm gần đây ở một số giải đấu người ta đã công bố các quân cờ có dạng biểu tượng như Mã hình đầu ngựa, Pháo có hình khẩu thần công... (bạn có thể xem lại các quân cờ biểu tượng ở hình 2.1, quân cờ lập thể ở hình dưới) và thậm chí đã có nơi sản xuất các quân cờ theo kiểu cờ Vua cải biên rồi. Thế nhưng "đâu lại hoàn đấy": các giải cờ Tướng trong từng quốc gia cũng như các giải thế giới toàn chơi bằng các quân cờ truyền thống dùng các chữ Hán quen thuộc. Vì sao vậy? Có lẽ thói quen đã ăn sâu vào đầu óc những người chơi cờ, khiến người ta khó tiếp nhận một sự cải tiến hoặc cải cách. Cũng có thể đối với người Á Đông cách suy nghĩ có phần sâu sắc hơn: đã cải cách thì ở nội dung là chính chứ không phải là hình thức. Nếu chỉ cải cách hình thức mà không cải cách nội dung thì cải cách đó cũng không cần thiết lắm. Quân cờ truyền thống có cái tiện của nó: không cồng kềnh như cờ Vua, rất giản dị, dễ làm, dễ mang đi, hỏng dễ thay... bình dân và gần gũi với mọi người. Nhưng cũng có thể đó là sự tự hào sâu xa của một nền văn hoá phương Đông: muốn có cái gì đó mang dấu ấn của riêng mình, không muốn có sự lai căng bắt chước từ bên ngoài. Bởi xét cho cùng cờ Tướng cũng hay, cũng hấp dẫn không kém cờ Vua. (Trích từ báo Người Chơi Cờ )
Copyright 2002, Pham Hong Nguyen
Tớ xin tiếp tục chủ đề này nhé Bài 1 Cây tìm kiếm là một giải thuật chính trong các game cờ. Chúng ta tiến hành tìm kiếm tất cả các nước đi có thể có của game, và đưa lên một cây. Lá của cây là nước đi cuối cùng chúng ta có thể kiểm soát được tính từ thời điểm hiện tại. Để tìm ra các nước đi tốt nhất, chúng ta tiến hành tìm kiếm trên cây, đánh giá các nút lá. Điều khó khăn nhất là các cây tìm kiếm thường rất lớn, do đó tìm kiếm nước đi tiêu tốn lượng thời gian lớn, ngay cả đối với các máy tính nhanh nhất. Thuật toán đơn giản nhất là MiniMax, được môt tả cách đây 60 năm. MiniMax được mô tả như sau: Chúng ta giả sử rằng có một cách nào đó định giá game bằng các con số, và có 2 đấu thủ Max và Min. Giá trị của game là dương dành cho Max, âm dành cho Min, trị tuyệt đối các giả trị này chỉ mức độ thuận lợi. Max luôn těm mọi cách để làm tăng mức giá trị của game Min luôn těm mọi cách để làm giảm mức giá trị của game Giả cả hai đấu thủ đều chơi rất hoàn hảo, nghĩa là không hề mắc sai lầm trong qua trình chơi Chúng ta hãy xét tình huống sau: Max Move Min Move Evaluation A C 12 A D -2 BC5 BD6 Max gải sử rằng Min chơi rất hoàn hảo, do đó anh ta biết rằng, nếu anh ta đi nước A, đối thủ của anh ta sẽ đi D, và kết quả cuối cùng là -2. Còn nếu anh ta đi B, đối thủ sẽ đi C, kết quả cuối là 5. Do đó trong thuật toán MiniMax, Max luôn luôn chọn đi B. Độ phức tạp: giả thiết có B nước đi cho mỗi đối thủ, và chúng ta tìm kiếm đến độ sâu n, thì thuật toán phải tìm khoảng O(B^n). Đoạn mã C như sau: int NegaMax (pos, depth) { if (depth == 0) return Evaluate(pos); best = -INFINITY; succ = Successors(pos); while (not Empty(succ)) { pos = RemoveOne(succ); value = -NegaMax(pos, depth-1); if (value > best) best = value; } return best; } trong đó Pos Một vị trí của game. Depth Độ sâu tìm kiếm. Evaluate Hàm lương giá. Có giá trị trong khoảng -INFINITY và +INFINITY. Best Giá trị tốt nhất. Successors Một hàm sinh ra các nước đi hợp lệ. Succ Tập hợp các nước đi hợp lệ.
AlphaBeta: Làm cho MiniMax khả thi hơn: Giả sử rằng chúng ta tìm kiếm nước đi tốt nhất cho Max, do đó chúng ta biết rằng nước đi tốt nhất là 5. giả sử rằng chúng ta tìm kiếm tại A, và đi theo đường A-D. Đường đi này đạt -2 điểm. Với Max, điều này thật tồi tệ: nếu anh ta đi tại A, anh ta sẽ thu được -2 điểm, bởi vì Min không bao giờ phạm sai lầm, nghĩa là Min sẽ không bao giờ đi theo A-C. Vậy, không cần thiết phải tìm kiếm A-C, hay bất cứ nhánh nào xuất phát từ A, và Max buộc phải chọn B. Như vậy, mỗi khi chúng ta tìm được một nước đi tốt hơn, chúng ta sẽ loại bỏ các nước đi không tốt bằng. Đoạn mã C như sau: int AlphaBeta (pos, depth, alpha, beta) { if (depth == 0) return Evaluate(pos); best = -INFINITY; succ = Successors(pos); while (not Empty(succ) && best < beta) { pos = RemoveOne(succ); if (best > alpha) alpha = best; value = -AlphaBeta(pos, depth-1, -beta, -alpha); if (value > best) best = value; } return best; } Độ phức tạp: trong trường hợp tốt nhất, chúng ta sẽ tìm O(B^(n/2)) thay vì O(B^n). Tuy nhiên, với AlphaBeta, chúng ta phải đưa ra sách lược tìm kiếm sao cho giảm được nhiều nhất các nhanh con. Nếu trong trường hợp tồi nhất, giá trị của trò chơi tăng dần, nghĩa là các nước đi sau tốt hơn các nước đi trước, thì AlphaBeta chẳng thể cắt bỏ được nhánh nào, và độ phức tạp của nó bằng với MiniMax. (Bài sau: thuật toán NegaScout và MTD(f)) Cấu trúc dữ liệu trong game cờ: Để có một game cờ hay, ngoài giải thuật tìm kiếm tốt, còn phải có các cấu trúc dữ liệu phù hợp với các giải thuật đó. Biểu diễn bàn cờ: Có 2 cách để biểu diễn bàn cờ Cách thông thường: dùng một mảng để biểu diễn bàn cờ và các quân cờ tương ứng. Chẳng hạn với cờ vua, dùng mảng 8x8, chứa các số nguyên tương ứng với các quân cờ. Với cách này, một kỹ thuật hay dùng nhất, đó là phần tử lính canh: bàn cờ 8x8 được bao phủ thêm 2 vòng ngoài cùng thành 12x12 để tránh trường hợp kiểm tra xem một nước đi có năm trong bàn cờ hay không (ví dụ đối với quân mã) Dùng mảng bit: Chúng ta dùng các mảng 64 bit. Mỗi bit là một giá trị cho biết tại vị trí đó có quân cờ hay không. Vì vậy, trạng thái bàn cờ (vua) được biểu diễn bởi 12 mảng bit tương ứng với 12 loại quân cờ: vua trắng-đen, hậu trắng-đen, tượng trắng-đen, mã trắng-đen, xe trắng-đen, tốt trắng-đen. Có thể có thêm 2 mảng bit nữa, mỗi mảng biểu diễn tất cả các quân cờ của mỗi bên. Chúng ta sẽ so sánh ưu điểm của hai cách biểu diễn trên. Giả sử cần xét hậu trắng có đang "chiếu" vua đen không. Với cách biểu diễn thông thường: · Tìm vị trí của hậu trắng, bằng phép tìm tuyến tính, mất trung bình 32 chu kỳ máy · Kiểm tra tất cả các ô mà hậu trắng có thể di chuyển được (8 hướng), cho đến khi tìm thấy vua đen, hoặc kết thúc tìm kiếm. Quá trình này luôn tiêu tốn thời gian, càng nhiều khi quá trình tìm kiếm càng về cuối, và trong đa số trường hợp là không tìm thấy. Với mảng bit, cách làm như sau: · Nạp mảng bit biểu thị vị trí hậu trắng · Nạp mảng các vị trí tấn công tương ứng của hậu trắng · AND với mảng bit biểu thị vị trí vua đen.
Nếu kết quả khác 0, thì hậu trắng đang chiếu vua đen. Toàn bộ quá trình trên chỉ mất khoảng 3-4 chu kì máy. Bảng hoán vị Trong cờ, có nhiều cách đi để đạt được cùng một trạng thái. Chẳng hạn, không phụ thuộc việc bạn đi 1.P-K4, 2.P-Q4 hay 1.P-Q4, 2.P-K4, thì trạng thái cuối cùng của bàn cờ là như nhau. Việc lưu trữ trạng thái cuối của bàn cờ gọi là bảng hoán vị. Như vậy, nếu bạn muốn tìm kiếm kết quả nước đi 1.P-Q4, 2.P-K4 thì bạn có thể lấy kết quả lưu sẵn trong 1.P-K4, 2.P-Q4. Mỗi bảng hoán vị được lưu nhờ các hàm băm (hash function). Khi một vị trí được tìm kiếm, kết quả tìm kiếm được lưu vào bảng hoán vị. Với mỗi đầu vào mới, chúng ta sẽ tìm trên bảng hoán vị trước. Nếu thấy, sẽ sử dụng kết quả trong bảng, và vuợt qua nút tìm kiếm này. Hàm băm Một phương pháp để lưu bảng hoán vị là dùng hàm băm. Hàm băm được mô tả như sau: · Sinh ra một bảng12x64 số N-bit. Mỗi số tương ứng với một vị trí trên bàn cờ. · Xuất phát từ 0 · Với mỗi vị trí, XOR với giá trị tương ứng của bảng ngẫu nhiên, hoặc với 0 nếu vị trí trên bàn cờ là trống Thực hiện tương tự như trên với một bảng ngẫu nhiên khác để tạo khoá. Như vậy mỗi trang thái của bàn cờ được biểu diên bởi 2 số. Hai trạng thái của bàn cờ là giống nhau nếu cả hai số trên là như nhau.
Dịch từ History of computer chess (http://www.x3dchess.com/press/historyofcomputerchess.htm), do Frederic Friedel viết. Sơ lược lịch sử máy tính trong cờ Vua Chiếc máy đánh cờ đầu tiên Năm 1769 kỹ sư người Hungary Baron Wolfgang von Kempelen thiết kế một chiếc máy chơi cờ để làm vui cho nữ hoàng Áo Maria Theresia. Đây là một cỗ máy cơ khí hoàn toàn, có hình dáng giống như một người Thổ. Tất nhiên là sức mạnh nổi bật của nó là nhờ một kiện tướng được khéo léo giấu bên trong nó. Chiếc máy này là đồ giả mạo :) Chiếc "máy giấy" của Turing Một điều đáng kinh ngạc là chương trình chơi cờ đầu tiên được viết trước khi chiếc máy tính đầu tiên được phát minh. Nó được viết bởi một người nhìn xa trông rộng, biết rằng máy tính có thể lập trình được sắp ra đời và một khi nó được phát minh ra, nó có thể chơi cờ được. Người đó là Alan Turing, một trong những nhà toán học lớn của thời kỳ đó. Turing đứng đầu nhóm phá mã bí mật "Enigma" của Đức, có ảnh hưởng lớn đến kết cục của chiến tranh thế giới lần thứ 2. Ông rất thích chơi cờ nhưng mặc dù rất cực kỳ thông minh và giành rất nhiều công sức để học cờ nhưng ông vẫn chỉ là một người chơi tương đối yếu. Sau chiến tranh, ông viết những lệnh hướng dẫn để máy tính có thể chơi cờ được. Vào thời điểm đó chưa có chiếc máy tính nào có thể chạy được các lệnh nên chính ông thực hiện các lệnh đó, đóng vai bộ xử lý trung tâm và cần khoảng nửa tiếng cho một nước đi. Một ván cờ được ghi lại, trong đó chiếc "paper machine" của Turing thua một đồng nghiệp. Đây là ván cờ lịch sử: Turing's paper machine – Alick Glennie, Manchester 1952: 1.e4 e5 2.Nc3 Nf6 3.d4 Bb4 4.Nf3 d6 5.Bd2 Nc6 6.d5 Nd4 7.h4 Bg4 8.a4 Nxf3+ 9.gxf3 Bh5 10.Bb5+ c6 11.dxc6 0-0 12.cxb7 Rb8 13.Ba6 Qa5 14.Qe2 Nd7 15.Rg1 Nc5 16.Rg5 Bg6 17.Bb5 Nxb7 18.0-0-0 Nc5 19.Bc6 Rfc8 20.Bd5 Bxc3 21.Bxc3 Qxa4 22.Kd2? [22.h5 would have trapped the bishop] 22...Ne6 23.Rg4 Nd4? [23...Rxb2! 24.Bxb2 Rxc2+] 24.Qd3 Nb5 25.Bb3 Qa6 26.Bc4 Bh5 27.Rg3 Qa4 28.Bxb5 Qxb5 29.Qxd6 Rd8 0-1. Chiến lược của Shanon Cũng vào cùng thời với Turing, một nhà toán học lớn khác, Claude Shanon của Bell Laboratorié cũng nghĩ tới việc dạy máy tính chơi cờ. Ông nhận ra rằng vấn đề là ở chỗ có quá nhiều khả năng tiếp diễn sau một nước đi. Do đó ông phân biệt giữa "chiến lược A", tìm kiếm tất cả những nước tiếp theo, và "chiến lược B", bỏ những đường không cần thiết. Ngày nay chúng ta phân biệt các chương trình theo loại "cục súc" (brute force) hay "lựa chọn" mặc dù tất cả các chương trình mạnh đều ít nhiều thuộc về loại "cục súc". Cờ thay vì bom nguyên tử Trong những năm chiến tranh, Mĩ xây dựng một phòng thí nghiệm khổng lồ ở Los Alamos trong sa mạc của bang New Mexico. Mục đích của nó là nghiên cứu chế tạo bom nguyên tử. Để tìm ra dạng cấu tạo của phần kích nổ để có thể tạo thành phản ứng dây chuyền đòi hỏi rất nhiều tính toán. Năm 1946, nhà toán học Hungary/Mỹ John von Neumann được giao nhiệm vụ thiết kế một chiếc máy tính để thực hiện công việc này nhanh hơn. Năm 1950, một chiếc máy khổng lồ được goi là MANIAC I được chế tạo. Nó có hàng nghìn bóng chân không và công tắc và có thể thực hiện 10000 lệnh trong một giây. Nó cũng có thể được lập trình.
Thay vì ngay lập tức bắt tay vào việc chế tạo bom, các nhà khoa học bắt đầu thí nghiệm với chiếc máy. Một trong những điều đầu tiên họ làm là viết một chương trình chơi cờ. Nó chơi trên một bàn cờ thu nhỏ 6x6 và không có Tượng. Mặc dù vậy chương trình này vẫn cần 12 phút để tìm kiếm trước 4 ply (ply là nửa nước đi, ví dụ e4 hay ...d5; 1.e4 e5 là một nước đi) (với Tượng trên bàn cờ nó sẽ cần khoảng 3 tiếng). Chương trình này chơi ba ván cờ trong những năm 50. Ván đầu tiên thi đấu với chính nó (Trắng thắng), ván thứ hai với một người chơi hay, chấp nó một hậu. Ván cờ kéo dài 10 tiếng và người thắng. Cuối cùng nó chơi với một phụ nữ trẻ, mới học chơi cờ tuần trước. Chương trình thắng trong vòng 23 nước. Đó là lần đầu tiên con người thua một chiếc máy tính trong một trò chơi trí tuệ. Đây là ván cờ lịch sử thứ hai (bàn cờ 6x6, không tượng, tốt không được phép đi hai ô trong nước đầu tiên, không được nhập thành) MANIAC 1 – Human, Los Alamos 1956: 1.d3 b4 2.Nf3 d4 3.b3 e4 4.Ne1 a4 5.bxa4? [5.Nd2 and 6.Nd2-c4+ Nbcxc4 7.b3xc4 with a good game] 5...Nxa4 6.Kd2? Nc3 7.Nxc3 bxc3+ 8.Kd1 f4 9.a3 Rb6 10.a4 Ra6 11.a5 Kd5 12.Qa3 Qb5 13.Qa2+ Ke5 14.Rb1 Rxa5 15.Rxb5 Rxa2 16.Rb1 [to prevent 16...Ra1 mate!] 16...Ra5 17.f3 Ra4 18.fxe4 c4 19.Nf3+ Kd6 20.e5+ Kd5 21.exf6Q Nc5 22.Qf6xd4+ Kc6 23.Nf3-e5 mate. Cờ và toán học Vấn đề chính với các chương trình chơi cờ là số lượng lớn các nước phải tính toán. Một ví trí trung bình sẽ có 40 nươc đi hợp lệ. Nếu bạn tính tất cả các nước đi đối phương trả lời bạn sẽ có 40x40 = 1600 vị trí. Điều này có nghĩa là sau hai ply, được coi là một nước đi trong cờ Vua, 16000 vị trí có thể xảy ra. Sau hai nước nó là 2.5 triệu vị trí và sau ba nước là 4.1 tỷ. Trung bình một ván cờ kéo dài khoảng 40 nước. Số vị trí cần tính là khoảng 10 mũ 128, lớn hơn cả số nguyên tử có trong vũ trụ (chỉ khoảng 10 mũ 80) Một điều dễ nhận thấy là không có chiếc máy tính hay loại máy nào có thể chơi cờ bằng cách tìm ra tất cả các khả năng. Nhưng con người cũng không phải là hoàn hảo. Câu hỏi là máy cần tìm kiếm tới độ sâu nào (trước bao nhiêu nước) để có thể đối chọi được với khả năng chiến lược của con người. Những chiếc máy tính thời đầu có thể tạo và đánh giá khoảng 500 vị trí trong một giây hay 90000 vị trí trong ba phút, thời gian bạn có để đi một nước trong các cuộc thi đấu. Điều này có nghĩa là nó chỉ có thể tìm kiếm trước 3 ply (một nước đi rưỡi). Điều này có nghĩa là nó chơi rất kém - chỉ ngang một người mới tập chơi. Để tìm kiếm sâu hơn nữa nó cần giải quyết được 15000 vị trí trong một giây, nhanh hơn gấp 30 lần. Nhưng tính toán trước 4 ply cũng chưa đủ sâu. Do đó máy tính dường như không bao giờ có thể chơi ở trình độ kiện tướng trong cờ Vua. Alpha-beta Bước nhẩy vọt đầu tiên là năm 1958 khi ba nhà khoa học của đại học Carnegie-Mellon University tại Pittsburgh (Newell, Shaw và Simon) tìm ra một phát hiện quan trọng. Bạn có thể bỏ một phần lớn của cây tìm kiếm mà không ảnh hưởng tới kết quả. Họ gọi đó là thuật toán alpha-beta. Một điểm cần nhớ là đây là một kỹ thuật toán học thuần tuý. Đây là sơ lược thuật toán alpha-beta trong cờ Vua: giả sử máy tính đã kết thúc ước lượng một nước đi và bắt đầu tính toán nước thứ hai. Ngay khi có một đường chứng tỏ nó sẽ có giá trị thấp hơn nước đầu tiên chúng ta có thể bỏ đường tìm kiếm này. Chúng ta không cần biết chính xác là nước đi thứ hai tệ hơn bao nhiêu so với nước đi đầu tiên nhưng chắc chắn là chúng ta muốn nước đi đầu tiên hơn. Thuật toán alpha-beta có kết quả giống như một tìm kiếm đầy đủ trong khi chỉ phải đi qua căn bậc hai số vị trí mà tìm kiếm đầy đủ cần. Đột nhiên những chiếc máy tính thời đầu có thể tìm kiếm trước 5 hoặc 6 ply. Vào thập kỷ 70, chiếc máy tính nhanh nhất (CDC Cyber series) có thể tìm trước tới 7 ply và đạt được khả năng chơi đáng nể. Nhưng kể cả với alpha-beta, bạn vẫn cần tốc độ gấp 5 lần để có thể tìm thêm một ply nữa. Số mũ của số phải tìm kiếm một lần nữa đuổi kịp các nhà lập trình. Chiếc máy Belle Ken Thompson là một nhà khoa học không thể chờ đợi những chiếc siêu máy tính giá hàng triệu đô trở nên 5 hay 25 lần nhanh hơn để có thể chơi cờ tốt hơn. Ông và một đồng nghiệp ở Bell Laboratories quyết định chế tạo một chiếc máy chỉ chuyên để chơi cờ, sử dụng hàng trăm con chip và giá khoảng 20 nghìn đô la. Họ gọi chiếc máy đó là "Belle" và nó chỉ có thể chơi cờ. Nhưng nó có thể tìm kiếm tới 180 nghìn vị trí trong một giây (siêu máy tính vào thời đó chỉ có thể tìm được 5000 vị trí) Belle có thể tìm trước 8 hay 9 ply trong các cuộc thi đấu, giúp nó có thể được chơi trong hàng kiện tướng. Nó thắng giải vô địch thế giới máy tính chơi cờ đầu tiên và tất cả những giải đấu khác từ 1980 đến 1983 cho đến khi nó bị chiếc máy khổng lồ Cray X-MPs, đắt hơn nó một nghìn lần, qua mặt. Những con chip để chơi cờ Vào giữa những năm 80, giáo sư Hans Berliner, một nhà khoa học máy điện toán ở đại học Carnegie-Mellon tiếp tục công việc của Ken Thompson. Berliner, đã từng là phóng viên báo chỉ ở giải vô địch cờ vua thế giới, chế tạo một chiếc máy tính có phần cứng đặc biệt để chơi cờ, gọi là HiTech. Ông và sinh viên Carl Ebeling chế tạo một con chip để tính các nước đi. Với 64 chip chạy song song, HiTech suýt nữa đạt được danh hiệu vô địch máy tính đánh cờ vua thế giới vào năm 1986 (một chiếc Cray thắng giải này). Sau đó các sinh viên của Berliner như Feng-hsiung Hsu, Murray Campbell và những người khác tự phát triển một chiếc máy tính khác, được gọi là ChipTest và sau đó Deep Thought. Giá của nó khoảng 5000 đô la và có thể tính toán được 500 000 vị trí trong một giây. Sau đó Hsu và Campbell cắt đứt với các thầy và gia nhậm IBM. Cùng vời Joe Hoane họ chế tạo ra Deep Blue. Deep Blue Garry Kasparov thi đấu với Deep blue tại Philadelphia và New York. Nó gồm có một máy chủ IBM SP/2 với một số lớn các con chip đặc biệt để tính toán nhanh. Mỗi con chip có thể xử lý hai đến ba triệu vị trí một giây. Với việc sử dụng hơn 200 con chip này, tốc độ tổng cộng của chương trình có thể tăng lên tới 200 triệu vị trí trong một giây. Độ sâu tìm kiếm và khả năng chơi cờ Xử lý 200 triệu vị trí trong một giây có nghĩa gì với một chiếc máy tính đánh cờ ? Ken Thompson, cha đẻ của Belle (cũng như Unix và ngôn ngữ lập trình C) tiến hành một số thí nghiệm thú vị trong những năm 80 cho thấy tương quan giữa độ sâu tìm kiếm và khả năng chơi cờ.
Thompson chơi Belle với chính nó với một bên được tính toán sâu hơn. Trung bình tính trước được thêm một ply ngang bằng với khoảng 200 điểm ELO. Với 4 ply Belle ở khoảng 1230 và với 9 ply nó đạt tới 2328 điểm ELO. Bằng cách tiếp tục tăng độ sâu tìm kiếm (càng về sau ELO càng tăng chậm) ta có thể kết luận là cần tính trước được 14 ply để có thể đạt đến trình độ vô địch thế giới (2800) Kết luận của các chuyên gia: bạn cần chế tạo một chiếc máy tính có thể xử lý một tỷ vị trí trong một giây (và tính trước 14 ply) nếu bạn muốn thách đấu với nhà vô địch cờ vua thế giới. Deep Blue đã tiến khá gần, nhưng chưa đạt tới điểm này. Những chiếc máy nhỏ bé Tất nhiên là chất lượng lập trình cũng có vị trí rất quan trọng. Những chương trình chơi cờ trên PC ngày này như Fritz hay Junior có thể xử lý 5 triệu hoặc hơn vị trí trong một giây. Chúng đều đạt đến sức mạnh khoảng 2700 ELO và là đối thủ cho bất kỳ ai trong nhóm 100 kỳ thủ đứng đầu thế giới. Trong cờ nhanh chỉ có khoảng 10 kỳ thủ đứng đầu có thể cạnh tranh với nó và nếu chơi blitz có lẽ chỉ hai hoặc ba người có thể sống sót. Tấn công trên cả hai mặt trận Một trong những điểm quan trọng trong sức mạnh của máy tính là nó có khả năng chơi theo các sách khai cuộc. Những kiến thức và kinh nghiệm qua bao đời của các kiện tướng có thể dễ dàng được lưu trữ trên đĩa cứng và máy tính có thể truy cập nó trong khi chơi khai cuộc. Ngay cả những chương trình trên PC cũng biết khoảng 10 triệu vị trí khai cuộc và có thể truy cập đầy đủ thống kê về chúng (những nước nào đã được đi, kết quả như thế nào, thứ hạng của người chơi v.v...). Thường thì máy tính sẽ chơi mười lăm hoặc hai mươi nước trước khi nó phải tính toán nước đầu tiên. Nếu không có được lợi thế từ kiến thức của con người trong khai cuộc, máy tính sẽ yếu đi nhiều Máy tính không chỉ có lợi từ khối lượng kiến thức khổng lồ trong khai cuộc từ lịch sử cờ Vua mà nó còn có lợi thế từ những nghiên cứu về cờ tàn. Cơ sở dữ liệu cờ tàn Một lần nữa chúng ta lại gặp lại Ken Thompson, người tiên phong trong lĩnh vực này. Trong những năm 80, ông bắt đầu tạo và ghi lại tất cả những vị trí tàn cuộc với bốn và năm quân trên bàn cờ. Một thế cờ tàn cuộc bình thường với 5 quân, ví dụ như một Vua với hai Tượng và một Vua với một Mã, sẽ có khoảng 121 triệu vị trí. Với một con tốt, do nó đi không đều (nước đầu tiên có thể đi hai ô), số vị trí tăng lên 335 triệu. Thompson viết một chương trình tính toán tất cả các vị trí hợp lệ và tìm ra tất cả các đường bắt buộc dẫn trong mỗi tàn cuộc. Ông cũng nén dữ liệu và có thể lưu trữ khoảng 20 loại tàn cuộc trong một đĩa CD-ROM chuẩn. Sử dụng cơ sở dữ liệu này, máy tính sẽ chơi mỗi tàn cuộc với độ chính xác tuyệt đối ("như là chúa trời"). Cho bất kỳ thế tàn cuộc nào, nó biết ngay lập tức đó là một trận thắng, hoà hay thua và trong bao nhiêu nước. Thường thì nó sẽ thông báo chiến thắng hay chiếu hết trước khoảng 50 nước. Khi ở bên thua nó sẽ chơi theo đường tốt nhất. Deep Blue sử dụng cơ sở dữ liệu tàn cuộc của Thompson và ngay cả chương trình cho PC Fritz bây giờ cũng sử dụng nó trong cây tìm kiếm của mình. Điều này sẽ ảnh hưởng đến sức mạnh của nó như thế nào sẽ cần thời gian để trả lời. Một số tàn cuộc với 5 quân nổi tiếng là khó hoặc không thể cho con người có thể làm chủ. Một ví dụ điển hình là Hậu chống lại Hậu và Tốt, trong trường hợp này con người không có cơ hội nào có thể thắng được máy tính. Nhưng những tàn cuộc với 5 quân chỉ là tic-tac-toe (một trò chơi rất đơn giản) so với những tàn cuộc với 6 quân mà Thompson đang tạo ra. Trong một số tàn cuộc với 6 quân, bạn cần đi chính xác 200 nước đi để có thể dành chiến thắng. Thường thì ngay cả những kỳ thủ mạnh nhất trên thế giới cũng không thể biết được mình đã tiến đến đâu sau 100 nước đi mà máy tính nói với chúng ta là bắt buộc. Sự phát triển về công nghệ phần cứng cũng làm tăng lợi thế của máy tinhs. Các thế tàn cuộc với 6 quân của Thompson, với 8 đến 20 tỷ vị trí mỗi loại, có thể được nén và lưu trữ trên một chiếc DVD. May mắn thay tàn cuộc với 7 quân, gồm khoảng 500 nghìn tỷ vị trí cho mỗi loại, vẫn là một tương lai xa. Và may mắn hơn nữa là hai đầu - nghiên cứu khai cuộc và cơ sở dữ liệu về tàn cuộc - sẽ không bao giờ gặp nhau. Có lẽ bạn sẽ không bao giờ thấy máy tính đi 1.e4 và thông báo sẽ chiếu hết ở nước thứ 40. Nhưng hầu như chỉ còn là vấn đề thời gian, vài năm hay một thập kỷ, trước khi máy tính có thể liên tục đánh bại nhà vô địch cờ Vua thế giới. Frederic Friedel Edit: đã sửa đoạn bác Turing :) xin cảm ơn bác Hưng đã góp ý :)