c) Xác định vị trí các quân cờ
7.4.2 Sinh nƣớc đi (Hàm GEN)
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ó.
• 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.
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.
public struct MOVE {
public SByte from;
public SByte 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).
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ờ đó.
Kỹ thuật MAILBOX: 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 7.5: Chuyển tƣơng ứng bàn cờ từ bảng 9x10 sang bảng 13x14 để kiểm tra giới hạn ngoài
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.
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.
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.
Hình 7.6: Mặt nạ kiểm tra một quân cờ có đƣợc phép đến vị trí nào đó không? 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;j++;j<=8 ) // Số hướng đi tối đa
if (offset[p,j] = 0) break;
x=mailbox90[i]; // Chuyển sang bàn cờ mở rộng
if (p in [ROOK, CANNON]) n := 9 else n := 1;
for(t=1;t++,t<=n) // Số nước có thể đi theo một hướng
{
if((p=PAWN) & (side=LIGHT)) 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 t = y
else t = 90-y;
if ((y=-1) | // Ra ngoài lề ? ((legalmove[t] & maskpiece[p])=0)) // Được phép ở vị trí này không ?
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.