Cập nhật: Tôi thích chủ đề này rất nhiều, tôi đã viết Câu đố lập trình, Vị trí cờ vua và Mã hóa Huffman . Nếu bạn đọc qua phần này, tôi đã xác định rằng cách duy nhất để lưu trữ trạng thái trò chơi hoàn chỉnh là lưu trữ một danh sách đầy đủ các bước di chuyển. Đọc tiếp để biết lý do tại sao. Vì vậy, tôi sử dụng một phiên bản đơn giản hóa của vấn đề cho bố cục mảnh.
Vấn đề
Hình ảnh này minh họa vị trí Cờ vua bắt đầu. Cờ vua diễn ra trên bàn cờ 8x8 với mỗi người chơi bắt đầu với một bộ 16 quân giống hệt nhau bao gồm 8 quân tốt, 2 quân, 2 kỵ sĩ, 2 quân, 1 quân hậu và 1 quân vương như minh họa ở đây:
Vị trí thường được ghi lại dưới dạng một chữ cái cho cột theo sau là số cho hàng để quân hậu của Trắng ở d1. Các chuyển động thường được lưu trữ dưới dạng ký hiệu đại số , không rõ ràng và thường chỉ xác định thông tin tối thiểu cần thiết. Hãy xem xét phần mở đầu này:
- e4 e5
- Nf3 Nc6
- …
dịch thành:
- Trắng di chuyển con tốt của vua từ e2 sang e4 (nó là quân duy nhất có thể đến e4 do đó “e4”);
- Đen chuyển quân của vua từ e7 sang e5;
- Trắng di chuyển quân (N) đến f3;
- Màu đen di chuyển kỵ sĩ đến c6.
- …
Bảng trông như thế này:
Một khả năng quan trọng đối với bất kỳ lập trình viên nào là có thể chỉ định vấn đề một cách chính xác và rõ ràng .
Vậy có gì thiếu sót hoặc mơ hồ? Rất nhiều như nó bật ra.
Bang hội đồng vs Bang trò chơi
Điều đầu tiên bạn cần xác định là bạn đang lưu trữ trạng thái của trò chơi hay vị trí của các quân cờ trên bàn cờ. Mã hóa đơn giản vị trí của các mảnh là một chuyện nhưng vấn đề nói lên “tất cả các động thái pháp lý tiếp theo”. Vấn đề cũng không nói gì về việc biết các động thái cho đến thời điểm này. Đó thực sự là một vấn đề như tôi sẽ giải thích.
Castling
Trò chơi đã diễn ra như sau:
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
Bảng trông như sau:
Màu trắng có tùy chọn nhập thành . Một phần của các yêu cầu đối với việc này là vua và quân có liên quan không bao giờ được di chuyển, vì vậy dù vua hay một trong hai quân của mỗi bên đã di chuyển đều cần được lưu trữ. Rõ ràng là nếu họ không ở vị trí xuất phát của họ, họ đã di chuyển nếu không thì cần phải xác định rõ.
Có một số chiến lược có thể được sử dụng để giải quyết vấn đề này.
Đầu tiên, chúng ta có thể lưu trữ thêm 6 bit thông tin (1 cho mỗi quân và quân) để cho biết quân đó đã di chuyển hay chưa. Chúng ta có thể sắp xếp hợp lý điều này bằng cách chỉ lưu trữ một chút cho một trong sáu hình vuông này nếu mảnh đúng nằm trong đó. Ngoài ra, chúng ta có thể coi mỗi quân không di chuyển như một loại quân khác, vì vậy thay vì 6 loại quân ở mỗi bên (quân tốt, quân, kỵ sĩ, giám mục, nữ hoàng và vua) thì có 8 (thêm quân không di chuyển và vua không di chuyển).
En Passant
Một quy tắc đặc biệt khác và thường bị bỏ qua trong Cờ vua là En Passant .
Trò chơi đã tiến triển.
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
- OO b5
- Bb3 b4
- c4
Con tốt của Đen ở b4 bây giờ có tùy chọn di chuyển con của mình ở b4 đến c3 và cầm quân của Trắng ở c4. Điều này chỉ xảy ra ở cơ hội đầu tiên có nghĩa là nếu Đen bỏ qua lựa chọn bây giờ anh ta không thể thực hiện bước tiếp theo. Vì vậy, chúng ta cần lưu trữ cái này.
Nếu chúng ta biết động thái trước đó, chúng ta chắc chắn có thể trả lời nếu En Passant có thể thực hiện được. Ngoài ra, chúng tôi có thể lưu trữ xem mỗi con tốt ở hạng 4 của nó có vừa di chuyển đến đó hay không với một lần di chuyển kép về phía trước. Hoặc chúng ta có thể xem xét từng vị trí En Passant có thể có trên bảng và có một lá cờ để chỉ ra liệu nó có thể có hay không.
Khuyến mại
Đó là nước đi của White. Nếu Trắng di chuyển quân của mình ở h7 đến h8, quân đó có thể được thăng lên bất kỳ quân nào khác (nhưng không phải quân vua). 99% trường hợp nó được thăng lên thành Nữ hoàng nhưng đôi khi không phải vậy, thường là vì điều đó có thể gây ra bế tắc khi nếu không thì bạn sẽ thắng. Điều này được viết là:
- h8 = Q
Điều này rất quan trọng trong vấn đề của chúng ta bởi vì nó có nghĩa là chúng ta không thể tin rằng có một số lượng cố định ở mỗi bên. Hoàn toàn có thể xảy ra (nhưng cực kỳ khó xảy ra) cho một bên kết thúc với 9 quân hậu, 10 quân, 10 giám mục hoặc 10 hiệp sĩ nếu cả 8 con tốt đều được thăng cấp.
Bế tắc
Khi ở một vị trí mà bạn không thể giành chiến thắng, chiến thuật tốt nhất của mình là cố gắng khai thông thế bế tắc . Biến thể có khả năng xảy ra nhất là bạn không thể thực hiện một nước đi hợp pháp (thường là vì bất kỳ nước đi nào khi đưa vua của bạn vào vòng kiểm soát). Trong trường hợp này, bạn có thể yêu cầu một trận hòa. Điều này là dễ dàng để phục vụ cho.
Biến thể thứ hai là lặp lại ba lần . Nếu cùng một vị trí bàn cờ xảy ra ba lần trong một trò chơi (hoặc sẽ xảy ra lần thứ ba ở nước đi tiếp theo), một trận hòa có thể được xác nhận. Các vị trí không cần phải xảy ra theo bất kỳ thứ tự cụ thể nào (có nghĩa là nó không phải lặp lại cùng một chuỗi các bước di chuyển ba lần). Điều này làm phức tạp vấn đề vì bạn phải nhớ mọi vị trí bảng trước đó. Nếu đây là một yêu cầu của vấn đề, giải pháp khả thi duy nhất cho vấn đề là lưu trữ mọi động thái trước đó.
Cuối cùng, có quy tắc di chuyển năm mươi . Một người chơi có thể yêu cầu hòa nếu không có con tốt nào di chuyển và không có quân cờ nào được lấy trong năm mươi nước đi liên tiếp trước đó, vì vậy chúng tôi sẽ cần lưu trữ bao nhiêu nước đi kể từ khi một con tốt được di chuyển hoặc một quân cờ bị 6 bit (0-63).
Đến lượt của ai?
Tất nhiên chúng ta cũng cần biết đó là lượt của ai và đây là một chút thông tin.
Hai vấn đề
Vì trường hợp bế tắc, cách duy nhất khả thi hoặc hợp lý để lưu trữ trạng thái trò chơi là lưu trữ tất cả các nước đi đã dẫn đến vị trí này. Tôi sẽ giải quyết một vấn đề đó. Bài toán trạng thái bàn cờ sẽ được đơn giản hóa thành: lưu trữ vị trí hiện tại của tất cả các quân cờ trên bàn cờ bỏ qua các điều kiện nhập thành, bị động, bế tắc và đến lượt của ai .
Bố cục mảnh có thể được xử lý rộng rãi theo một trong hai cách: bằng cách lưu trữ nội dung của mỗi ô vuông hoặc bằng cách lưu trữ vị trí của mỗi mảnh.
Nội dung đơn giản
Có sáu loại quân cờ (cầm đồ, quân xe, hiệp sĩ, giám mục, nữ hoàng và vua). Mỗi mảnh có thể là Trắng hoặc Đen nên một hình vuông có thể chứa một trong 12 mảnh có thể hoặc nó có thể trống để có 13 khả năng. 13 có thể được lưu trữ trong 4 bit (0-15) Vì vậy giải pháp đơn giản nhất là lưu trữ 4 bit cho mỗi hình vuông nhân với 64 hình vuông hoặc 256 bit thông tin.
Ưu điểm của phương pháp này là thao tác cực kỳ dễ dàng và nhanh chóng. Điều này thậm chí có thể được mở rộng bằng cách thêm 3 khả năng khác mà không cần tăng yêu cầu lưu trữ: một con tốt đã di chuyển 2 khoảng trống ở lượt cuối cùng, một quân vua không di chuyển và một quân xe chưa di chuyển, sẽ phục vụ cho rất nhiều của các vấn đề đã đề cập trước đó.
Nhưng chúng ta có thể làm tốt hơn.
Mã hóa cơ sở 13
Thường sẽ hữu ích khi nghĩ về vị trí hội đồng quản trị là một con số rất lớn. Điều này thường được thực hiện trong khoa học máy tính. Ví dụ: vấn đề tạm dừng coi một chương trình máy tính (đúng ra) là một số lớn.
Giải pháp đầu tiên coi vị trí là một số cơ số 16 gồm 64 chữ số nhưng như đã chứng minh là có sự dư thừa trong thông tin này (là 3 khả năng không sử dụng cho mỗi “chữ số”) nên chúng ta có thể giảm không gian số xuống còn 64 chữ số cơ số 13. Tất nhiên điều này không thể được thực hiện hiệu quả như cơ sở 16 nhưng nó sẽ tiết kiệm yêu cầu lưu trữ (và giảm thiểu không gian lưu trữ là mục tiêu của chúng tôi).
Trong cơ số 10, số 234 tương đương với 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .
Trong cơ số 16, số 0xA50 tương đương với 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (thập phân).
Vì vậy, chúng ta có thể mã hóa vị trí của chúng ta là p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 trong đó p i đại diện cho nội dung của hình vuông i .
2 256 bằng khoảng 1,16e77. 13 64 bằng khoảng 1,96e71, yêu cầu không gian lưu trữ 237 bit. Tiết kiệm chỉ 7,5% đi kèm với chi phí chế tác tăng lên đáng kể .
Mã hóa cơ sở biến
Trong bảng luật nhất định các quân cờ không thể xuất hiện trong các ô vuông nhất định. Ví dụ: các con tốt không thể xảy ra ở hàng thứ nhất hoặc thứ tám, làm giảm khả năng cho các ô vuông đó xuống còn 11. Điều đó làm giảm các bảng có thể có xuống 11 16 x 13 48 = 1,35e70 (xấp xỉ), yêu cầu 233 bit không gian lưu trữ.
Trên thực tế, mã hóa và giải mã các giá trị như vậy sang và từ thập phân (hoặc nhị phân) phức tạp hơn một chút nhưng nó có thể được thực hiện một cách đáng tin cậy và được để lại như một bài tập cho người đọc.
Bảng chữ cái có chiều rộng biến đổi
Cả hai phương pháp trước đều có thể được mô tả là mã hóa chữ cái có độ rộng cố định . Mỗi thành viên trong số 11, 13 hoặc 16 của bảng chữ cái được thay thế cho một giá trị khác. Mỗi "ký tự" có cùng chiều rộng nhưng hiệu quả có thể được cải thiện khi bạn xem xét rằng mỗi ký tự không có khả năng như nhau.
Hãy xem xét mã Morse (hình trên). Các ký tự trong tin nhắn được mã hóa dưới dạng một chuỗi dấu gạch ngang và dấu chấm. Những dấu gạch ngang và dấu chấm đó được chuyển qua radio (thông thường) với một khoảng dừng giữa chúng để phân tách chúng.
Lưu ý rằng chữ E ( chữ cái phổ biến nhất trong tiếng Anh ) là một dấu chấm đơn, chuỗi ngắn nhất có thể, trong khi chữ Z (ít thường xuyên nhất) là hai dấu gạch ngang và hai tiếng bíp.
Một sơ đồ như vậy có thể làm giảm đáng kể kích thước của một thông báo mong đợi nhưng phải trả giá bằng việc tăng kích thước của một chuỗi ký tự ngẫu nhiên.
Cần lưu ý rằng mã Morse có một tính năng sẵn có khác: dấu gạch ngang dài bằng dấu ba chấm vì vậy đoạn mã trên được tạo ra nhằm mục đích giảm thiểu việc sử dụng dấu gạch ngang. Vì 1s và 0s (khối xây dựng của chúng tôi) không có vấn đề này, nó không phải là một tính năng mà chúng tôi cần tái tạo.
Cuối cùng, có hai loại phần còn lại trong mã Morse. Phần còn lại ngắn (độ dài của dấu chấm) được dùng để phân biệt giữa dấu chấm và dấu gạch ngang. Khoảng cách dài hơn (độ dài của dấu gạch ngang) được sử dụng để phân cách các ký tự.
Vậy điều này áp dụng cho vấn đề của chúng ta như thế nào?
Mã hóa Huffman
Có một thuật toán để xử lý các mã có độ dài thay đổi được gọi là mã hóa Huffman . Mã hóa Huffman tạo ra sự thay thế mã có độ dài thay đổi, thường sử dụng tần suất dự kiến của các ký hiệu để gán các giá trị ngắn hơn cho các ký hiệu phổ biến hơn.
Trong cây trên, chữ E được mã hóa là 000 (hoặc trái-trái-trái) và S là 1011. Cần rõ rằng lược đồ mã hóa này là rõ ràng .
Đây là một điểm khác biệt quan trọng với mã Morse. Mã Morse có dấu phân tách ký tự để nó có thể thay thế không rõ ràng (ví dụ: 4 dấu chấm có thể là H hoặc 2 Is) nhưng chúng ta chỉ có 1s và 0 nên chúng ta chọn một sự thay thế rõ ràng để thay thế.
Dưới đây là một cách thực hiện đơn giản:
private static class Node {
private final Node left;
private final Node right;
private final String label;
private final int weight;
private Node(String label, int weight) {
this.left = null;
this.right = null;
this.label = label;
this.weight = weight;
}
public Node(Node left, Node right) {
this.left = left;
this.right = right;
label = "";
weight = left.weight + right.weight;
}
public boolean isLeaf() { return left == null && right == null; }
public Node getLeft() { return left; }
public Node getRight() { return right; }
public String getLabel() { return label; }
public int getWeight() { return weight; }
}
với dữ liệu tĩnh:
private final static List<string> COLOURS;
private final static Map<string, integer> WEIGHTS;
static {
List<string> list = new ArrayList<string>();
list.add("White");
list.add("Black");
COLOURS = Collections.unmodifiableList(list);
Map<string, integer> map = new HashMap<string, integer>();
for (String colour : COLOURS) {
map.put(colour + " " + "King", 1);
map.put(colour + " " + "Queen";, 1);
map.put(colour + " " + "Rook", 2);
map.put(colour + " " + "Knight", 2);
map.put(colour + " " + "Bishop";, 2);
map.put(colour + " " + "Pawn", 8);
}
map.put("Empty", 32);
WEIGHTS = Collections.unmodifiableMap(map);
}
và:
private static class WeightComparator implements Comparator<node> {
@Override
public int compare(Node o1, Node o2) {
if (o1.getWeight() == o2.getWeight()) {
return 0;
} else {
return o1.getWeight() < o2.getWeight() ? -1 : 1;
}
}
}
private static class PathComparator implements Comparator<string> {
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return o2 == null ? 0 : -1;
} else if (o2 == null) {
return 1;
} else {
int length1 = o1.length();
int length2 = o2.length();
if (length1 == length2) {
return o1.compareTo(o2);
} else {
return length1 < length2 ? -1 : 1;
}
}
}
}
public static void main(String args[]) {
PriorityQueue<node> queue = new PriorityQueue<node>(WEIGHTS.size(),
new WeightComparator());
for (Map.Entry<string, integer> entry : WEIGHTS.entrySet()) {
queue.add(new Node(entry.getKey(), entry.getValue()));
}
while (queue.size() > 1) {
Node first = queue.poll();
Node second = queue.poll();
queue.add(new Node(first, second));
}
Map<string, node> nodes = new TreeMap<string, node>(new PathComparator());
addLeaves(nodes, queue.peek(), "");
for (Map.Entry<string, node> entry : nodes.entrySet()) {
System.out.printf("%s %s%n", entry.getKey(), entry.getValue().getLabel());
}
}
public static void addLeaves(Map<string, node> nodes, Node node, String prefix) {
if (node != null) {
addLeaves(nodes, node.getLeft(), prefix + "0");
addLeaves(nodes, node.getRight(), prefix + "1");
if (node.isLeaf()) {
nodes.put(prefix, node);
}
}
}
Một đầu ra có thể là:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Đối với vị trí bắt đầu, điều này tương đương với 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 bit.
Sự khác biệt của trạng thái
Một cách tiếp cận khả thi khác là kết hợp cách tiếp cận đầu tiên với mã hóa Huffman. Điều này dựa trên giả định rằng các Bàn cờ được mong đợi nhất (chứ không phải là những bàn cờ được tạo ngẫu nhiên) có nhiều khả năng không giống như vị trí ban đầu.
Vì vậy, những gì bạn làm là XOR vị trí bảng hiện tại 256 bit với vị trí bắt đầu 256 bit và sau đó mã hóa vị trí đó (sử dụng mã hóa Huffman hoặc, giả sử, một số phương pháp mã hóa độ dài chạy ). Rõ ràng điều này sẽ rất hiệu quả khi bắt đầu (64 0 có thể tương ứng với 64 bit) nhưng tăng dung lượng lưu trữ cần thiết khi trò chơi tiến triển.
Vị trí mảnh
Như đã đề cập, một cách khác để tấn công vấn đề này là thay vào đó lưu trữ vị trí của từng quân cờ mà người chơi có. Điều này đặc biệt hiệu quả với các vị trí cuối trò chơi nơi hầu hết các ô vuông sẽ trống (nhưng trong phương pháp mã hóa Huffman, các ô trống chỉ sử dụng 1 bit dù sao).
Mỗi bên sẽ có một quân vua và 0-15 quân cờ khác. Do quảng cáo, cấu tạo chính xác của những mảnh đó có thể khác nhau đến mức bạn không thể giả định các con số dựa trên vị trí bắt đầu là cực đại.
Cách hợp lý để phân chia điều này là lưu trữ một Vị trí bao gồm hai Mặt (Trắng và Đen). Mỗi bên có:
- A vua: 6 bit cho vị trí;
- Có các con tốt: 1 (có), 0 (không);
- Nếu có, số con tốt: 3 bit (0-7 + 1 = 1-8);
- Nếu có, vị trí của mỗi con tốt được mã hóa: 45 bit (xem bên dưới);
- Số lượng không phải con tốt: 4 bit (0-15);
- Đối với mỗi phần: nhập (2 bit cho quân hậu, quân, kỵ sĩ, giám mục) và vị trí (6 bit)
Đối với vị trí cầm đồ, các con tốt chỉ có thể nằm trên 48 ô vuông khả thi (không phải 64 ô như những ô khác). Do đó, tốt hơn là không nên lãng phí 16 giá trị bổ sung mà việc sử dụng 6 bit cho mỗi con tốt sẽ sử dụng. Vì vậy, nếu bạn có 8 con tốt, thì có 48 8 khả năng, bằng 28.179.280.429.056. Bạn cần 45 bit để mã hóa nhiều giá trị đó.
Đó là 105 bit mỗi bên hoặc tổng số 210 bit. Tuy nhiên, vị trí bắt đầu là trường hợp xấu nhất đối với phương pháp này và nó sẽ tốt hơn đáng kể khi bạn loại bỏ các mảnh.
Cần chỉ ra rằng có ít hơn 48 8 khả năng bởi vì tất cả các con tốt không thể nằm trong cùng một hình vuông. Đầu tiên có 48 khả năng, thứ hai 47, v.v. 48 x 47 x… x 41 = 1,52e13 = 44 bit lưu trữ.
Bạn có thể cải thiện điều này hơn nữa bằng cách loại bỏ các ô vuông bị chiếm bởi các quân khác (bao gồm cả mặt còn lại) để trước tiên bạn có thể đặt các con tốt không trắng rồi đến các con không đen, sau đó là các con trắng và cuối cùng là các con đen. Ở vị trí bắt đầu, điều này làm giảm yêu cầu lưu trữ xuống 44 bit cho Trắng và 42 bit cho Đen.
Các phương pháp kết hợp
Một cách tối ưu khác có thể xảy ra là mỗi cách tiếp cận này đều có điểm mạnh và điểm yếu. Chẳng hạn, bạn có thể chọn 4 tốt nhất và sau đó mã hóa bộ chọn lược đồ trong hai bit đầu tiên và sau đó là bộ lưu trữ dành riêng cho lược đồ sau đó.
Với chi phí nhỏ như vậy, đây sẽ là cách tiếp cận tốt nhất.
Trạng thái trò chơi
Tôi quay lại vấn đề lưu trữ trò chơi hơn là vị trí . Bởi vì sự lặp lại ba lần, chúng tôi phải lưu trữ danh sách các nước đi đã xảy ra cho đến thời điểm này.
Chú thích
Một điều bạn phải xác định là bạn đang lưu trữ một danh sách các nước đi hay bạn đang chú thích trò chơi? Các trò chơi cờ vua thường được chú thích, ví dụ:
- Bb5 !! Nc4?
Nước đi của Trắng được đánh dấu bằng hai dấu chấm than là tuyệt vời trong khi của Đen được coi là một sai lầm. Xem Dấu câu cờ vua .
Ngoài ra, bạn cũng có thể cần lưu trữ văn bản miễn phí khi các bước di chuyển được mô tả.
Tôi giả định rằng các bước di chuyển là đủ nên sẽ không có chú thích.
Ký hiệu đại số
Chúng tôi chỉ cần lưu trữ văn bản của việc di chuyển ở đây (“e4”, “Bxb5”, v.v.). Bao gồm một byte kết thúc mà bạn đang nhìn vào khoảng 6 byte (48 bit) mỗi lần di chuyển (trường hợp xấu nhất). Điều đó không đặc biệt hiệu quả.
Điều thứ hai cần thử là lưu trữ vị trí bắt đầu (6 bit) và vị trí kết thúc (6 bit) sao cho 12 bit mỗi lần di chuyển. Điều đó tốt hơn đáng kể.
Ngoài ra, chúng tôi có thể xác định tất cả các động thái hợp pháp từ vị trí hiện tại theo cách và trạng thái có thể dự đoán và xác định được mà chúng tôi đã chọn. Điều này sau đó quay trở lại mã hóa cơ sở biến được đề cập ở trên. Trắng và Đen có 20 nước đi có thể có ở mỗi nước đi đầu tiên, nhiều hơn ở nước đi thứ hai, v.v.
Phần kết luận
Không có câu trả lời hoàn toàn đúng cho câu hỏi này. Có nhiều cách tiếp cận khả thi, trong đó những cách trên chỉ là một số.
Điều tôi thích về vấn đề này và các vấn đề tương tự là nó đòi hỏi những khả năng quan trọng đối với bất kỳ lập trình viên nào như xem xét kiểu sử dụng, xác định chính xác các yêu cầu và suy nghĩ về các trường hợp góc.
Vị trí cờ vua được chụp dưới dạng ảnh chụp màn hình từ Nhà huấn luyện vị trí cờ vua .