Chương 2 : PHÂN TÍCH VÀÀ̀ LẬP TRÌNH GAME
2.2 Xây dựng chương trình
2.2.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 tốn để chọn nước đi có lợi nhất cho nó. Các u cầầ̀u chính đối với một thủủ̉ tup̣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 tốn. Đồng thờầ̀i chương trình có thể bịp̣ 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 tố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ịp̣ trí khống chế củủ̉a đối phương). Ta hình dung ngay thủủ̉ tup̣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ủủ̉ tup̣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ủủ̉ tup̣c phức tạp và dễ sai nhất.
Một nước đi có hai giá trịp̣ 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 { Địp̣nh nghĩa cấu trúú́c nước đi } from, dest: byte;
Kiểm tra giới hạn bàn cờ
Ví dup̣ có một qn 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ịp̣ 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ủủ̉ tup̣c Gen_push. Thủủ̉ tup̣c này có hai tham số là vịp̣ 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 qn Xe, trong đó x là vịp̣ trí củủ̉a qn 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ó qn đối phương) then
gen_push(vịp̣ trí củủ̉a Xe, vịp̣ 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ịp̣ 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í dup̣ để 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ịp̣ trí củủ̉a Xe, vịp̣ 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
...
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ư) . Mup̣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ịp̣ -1, tức là các giá trịp̣ 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 ngồ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í dup̣, nếu vịp̣ 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ịp̣ 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ịp̣ 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ịp̣ 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ịp̣ 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ịp̣ 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í dup̣ như Tướng và Sĩ khơng thể đi ra ngồi cung, Tượng chỉ được phéú́p ở 7 điểm cố địp̣nh phía bên mình, Tốt chỉ được phéú́p tung hồnh trên đất đối phương, còầ̀n phía bên mình cũã̃ng bịp̣ 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ịp̣ 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ì qn cờầ̀ đó được phéú́p đến ơ đấy.
Ví dup̣, ơ số 3 có giá trịp̣ legalmove[3] = 5 (chuyển thành số nhịp̣ 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ịp̣ 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ịp̣ 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ịp̣ trí này khơng ? }
then break; { Thố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ịp̣ 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ủủ̉ tup̣c sinh nước Gen. Tuy nhiên đơn giản hơn, ta đặt nó trong thủủ̉ tup̣c gen_push, nếu hở mặt Tướng thủủ̉ tup̣c này sẽã̃ khơng đưa nước đi đó