Tải bản đầy đủ (.doc) (25 trang)

Báo cáo đồ án minh họa thuật toán anpha – beta bằng chương trình chơi cờ tướng

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (476.05 KB, 25 trang )

KHOA CÔNG NGHỆ THÔNG TIN
BỘ MÔN KHOA HỌC MÁY TÍNH
B O C O Á Á ĐỒ ÁN TRÍ TUỆ NHÂN TẠO
Đề bài : Minh họa thuật toán Anpha – Beta bằng chương trình chơi cờ tướng
I. Giới thiệu
Trò chơi Cờ Tướng 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 . 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 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 cũng hết sức đơn giản
Hinh 2.1
II. Viết chương trình chơi cờ
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:
BYTE piece[10,9];
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:
BYTE piece[90];
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ờ Kí hiệu Giá trị
Tốt (Chốt) PAWN 0
Sĩ BISHOP 1


Tượng ELEPHANT 2
Mã KNIGHT 3
Pháo CANNON 4
Xe ROOK 5
Tướng KING 6
Trống EMPTY 7

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[BOARD_SIZE]= {
5, 3, 2, 1, 6, 1, 2, 3, 5,
7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 4, 7, 7, 7, 7, 7, 4, 7,
0, 7, 0, 7, 0, 7, 0, 7, 0,
7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7,
0, 7, 0, 7, 0, 7, 0, 7, 0,
7, 4, 7, 7, 7, 7, 7, 4, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7,
5, 3, 2, 1, 6, 1, 2, 3, 5};
Đế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 Kí hiệu Giá trị
Đen DARK 0

Trắng LIGHT 1
Trống EMPTY 7
Ta lại có thông tin về bên được khai báo khởi đầu tương tự:
BYTE color[BOARD_SIZE]= {
0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 0, 7, 7, 7, 7, 7, 0, 7,
0, 7, 0, 7, 0, 7, 0, 7, 0,
7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7,
1, 7, 1, 7, 1, 7, 1, 7, 1,
7, 1, 7, 7, 7, 7, 7, 1, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7,
1, 1, 1, 1, 1, 1, 1, 1, 1}
Để biết bên nào tới lượt đi, ta dùng một biến toàn cục m_side chứa một trong hai giá trị
LIGHT và DARK. Một biến toàn cục khác m_xside sẽ có sẵn giá trị ngược với side để
tiện tính toán (ví dụ nếu m_side = LIGHT thì m_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):
m_side := m_xside; m_xside := 3 - m_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.
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.
Struct MOVE { short from,dest; };


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, 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ờ Kí hiệu Điểm
Tốt PAWN 1 (2 nếu đã qua sông)
Sĩ BISHOP 2
Tượng ELEPHANT 2
Mã KNIGHT 4
Pháo CANNON 4.5
Xe ROOK 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):
short CBoard::Eval()
{
short i, s = 0;
m_evalcount +=1;
for(i=0; i<BOARD_SIZE; i++)

{
if (m_color[i]==DARK) s += pointtable[m_piece[i]][DARK][i];
else if (m_color[i]==LIGHT) s -= pointtable[m_piece[i]][LIGHT][i];
}
if (m_side==LIGHT) s = -s;
return s + Bonous();
}
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
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.

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à 2x40^2 - 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à 40^3 + 40^(5/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.
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.
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 đã thấy chương trình mẫu của thủ tục AlphaBeta. Hàm AlphaBeta của ta 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
m_newmove. Máy sẽ chọn đi theo nước này.
6. 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
m_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.
7. Hiện thông tin
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 m_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.
Để 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
m_gen_end[m_ply] – m_gen_begin[m_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 m_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.
// biến dùng để đo hệ số phân nhánh
long m_brandtotal;
long m_gencount;
// biến dùng để đo số nút lượng giá
long m_evalcount;
BOOL Gen()
{

m_brandtotal = m_brandtotal + m_gen_end[m_ply] - m_gen_begin[m_ply];
m_gencount += 1;
}
short Eval()
{

evalcount += 1;

}
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.
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ờ).

×