업데이트 : 저는 프로그래밍 퍼즐, 체스 포지션, 허프만 코딩을 썼습니다 . 이 글을 읽어 보면 완전한 게임 상태를 저장하는 유일한 방법은 전체 동작 목록을 저장하는 것이라고 판단했습니다 . 이유는 계속 읽어보십시오. 그래서 저는 조각 레이아웃에 대해 약간 단순화 된 버전의 문제를 사용합니다.
문제
이 이미지는 시작 체스 위치를 보여줍니다. 체스는 8x8 보드에서 발생하며 각 플레이어는 8 개의 폰, 2 개의 루크, 2 개의 기사, 2 개의 비숍, 1 개의 퀸, 1 개의 킹으로 구성된 동일한 16 개 조각 세트로 시작합니다.
위치는 일반적으로 열의 문자와 행의 숫자로 기록되므로 White의 여왕은 d1에 있습니다. 이동은 대부분의 경우 대수 표기법으로 저장되며 , 이는 모호하지 않으며 일반적으로 필요한 최소한의 정보 만 지정합니다. 이 오프닝을 고려하십시오.
- e4 e5
- Nf3 Nc6
- …
다음으로 번역됩니다.
- White는 king 's pawn을 e2에서 e4로 이동합니다 (e4에 도달 할 수있는 유일한 조각이므로 "e4").
- Black은 왕의 폰을 e7에서 e5로 이동합니다.
- 흰색은 기사 (N)를 f3으로 이동합니다.
- 검은 색은 기사를 c6로 이동합니다.
- …
보드는 다음과 같습니다.
모든 프로그래머에게 중요한 능력 은 문제 를 정확하고 명확하게 지정할 수 있다는 것 입니다.
그래서 무엇이 누락되었거나 모호합니까? 밝혀진대로 많이.
보드 상태 대 게임 상태
가장 먼저 결정해야 할 것은 게임의 상태를 저장할 것인지 아니면 보드에있는 조각의 위치를 저장할 것인지입니다. 단순히 조각의 위치를 인코딩하는 것은 하나이지만 문제는 "모든 후속 법적 이동"이라고 말합니다. 문제는 또한이 시점까지의 움직임을 아는 것에 대해 아무것도 말하지 않습니다. 제가 설명 하겠지만 실제로 문제입니다.
캐슬 링
게임은 다음과 같이 진행되었습니다.
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
보드는 다음과 같이 보입니다.
흰색에는 캐슬 링 옵션이 있습니다. 이것에 대한 요구 사항 중 일부는 왕과 관련 루크가 결코 움직일 수 없기 때문에 왕이나 양쪽의 루크가 이동했는지 여부를 저장해야한다는 것입니다. 분명히 그들이 그들의 시작 위치에 있지 않다면, 그들은 움직이지 않았다면 그것을 지정해야합니다.
이 문제를 처리하는 데 사용할 수있는 몇 가지 전략이 있습니다.
첫째, 그 조각이 움직 였는지 여부를 나타 내기 위해 6 비트의 추가 정보 (각 루크와 킹에 대해 1 개)를 저장할 수 있습니다. 올바른 조각이있는 경우 6 개의 사각형 중 하나에 대해 약간만 저장하여이를 간소화 할 수 있습니다. 또는 움직이지 않은 각 조각을 다른 조각 유형으로 취급 할 수 있으므로 각면에 6 개의 조각 유형 (폰, 루크, 나이트, 비숍, 퀸 및 킹) 대신 8 개 (움직이지 않은 루크 및 움직이지 않은 킹 추가)가 있습니다.
En Passant
Chess에서 독특하고 자주 무시되는 또 다른 규칙은 En Passant 입니다.
게임이 진행되었습니다.
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
- OO b5
- Bb3 b4
- c4
b4에있는 Black의 폰은 이제 b4에있는 그의 폰을 c4에있는 White 폰을 c3으로 옮기는 옵션이 있습니다. 이것은 첫 번째 기회에서만 발생합니다. 즉, Black이 옵션을 통과하면 다음 이동을 수행 할 수 없습니다. 그래서 우리는 이것을 저장해야합니다.
이전 움직임을 알고 있다면 En Passant가 가능한지 확실히 대답 할 수 있습니다. 또는 4 순위에있는 각 폰이 두 번 앞으로 이동하여 방금 이동했는지 여부를 저장할 수 있습니다. 또는 우리는 보드에서 가능한 각 En Passant 위치를보고 가능한지 여부를 나타내는 플래그를 가질 수 있습니다.
프로모션
화이트의 움직임입니다. White가 자신의 폰을 h7에서 h8로 옮기면 다른 말로 승격 될 수 있습니다 (왕은 아님). 99 %의 경우 여왕으로 승진되지만 때로는 그렇지 않은 경우도 있습니다. 일반적으로 그렇지 않으면 승리 할 때 교착 상태가 될 수 있기 때문입니다. 이것은 다음과 같이 작성됩니다.
- h8 = Q
이것은 우리가 각면에 고정 된 수의 조각이 있다는 것을 믿을 수 없다는 것을 의미하기 때문에 우리 문제에서 중요합니다. 8 개의 폰이 모두 승격되면 한 쪽이 9 명의 퀸, 10 명의 루크, 10 명의 비숍 또는 10 명의 기사로 끝날 가능성은 전적으로 가능합니다.
수가 막히게 하다
당신이 이길 수없는 위치에있을 때 당신의 최선의 전술은 교착 상태 를 시도하는 것 입니다. 가장 가능성이 높은 변형은 합법적 인 이동을 할 수없는 경우입니다 (일반적으로 킹을 체크 할 때 이동하기 때문에). 이 경우 무승부를 청구 할 수 있습니다. 이것은 음식을 제공하기 쉽습니다.
두 번째 변형은 3 중 반복 입니다. 같은 보드 위치가 게임에서 세 번 발생하면 (또는 다음 이동에서 세 번 발생) 무승부가 청구될 수 있습니다. 위치는 특정 순서로 발생할 필요가 없습니다 (즉, 동일한 순서의 동작을 세 번 반복 할 필요가 없음). 이것은 이전의 모든 보드 위치를 기억해야하기 때문에 문제를 크게 복잡하게 만듭니다. 이것이 문제의 요구 사항 인 경우 문제에 대한 유일한 해결책은 이전의 모든 이동을 저장하는 것입니다.
마지막으로 50 개의 이동 규칙이 있습니다. 플레이어는 폰이 움직이지 않았고 이전 50 개 연속 이동에서 조각을 가져 오지 않은 경우 무승부를 청구 할 수 있으므로 폰이 이동 된 이후 또는 조각을 가져온 이후에 몇 개의 움직임을 저장해야합니다 (두 개 중 가장 최근의 것). 6 비트 (0-63).
누구의 차례인가?
물론 우리는 누구의 차례인지 알 필요가 있으며 이것은 하나의 정보입니다.
두 가지 문제
교착 상태로 인해 게임 상태를 저장하는 유일하고 현명한 방법은이 위치로 이어진 모든 동작을 저장하는 것입니다. 그 한 가지 문제를 해결하겠습니다. 보드 상태 문제는 다음과 같이 단순화됩니다. 캐슬 링, en passant, 교착 상태 및 차례를 무시하고 보드에있는 모든 조각의 현재 위치를 저장합니다 .
조각 레이아웃은 각 사각형의 내용을 저장하거나 각 조각의 위치를 저장하는 두 가지 방법 중 하나로 광범위하게 처리 할 수 있습니다.
간단한 내용
6 가지 유형 (폰, 루크, 나이트, 비숍, 퀸, 킹)이 있습니다. 각 조각은 흰색 또는 검은 색일 수 있으므로 사각형에는 12 개의 가능한 조각 중 하나가 포함될 수 있거나 비어있어 13 개의 가능성이 있습니다. 13은 4 비트 (0-15)로 저장 될 수 있으므로 가장 간단한 해결책은 각 제곱에 대해 4 비트를 64 제곱 또는 256 비트 정보로 저장하는 것입니다.
이 방법의 장점은 조작이 매우 쉽고 빠르다는 것입니다. 저장 공간 요구 사항을 늘리지 않고 3 개의 가능성을 더 추가하여 확장 할 수도 있습니다. 이전에 언급 된 문제의
하지만 우리는 더 잘할 수 있습니다.
Base 13 인코딩
이사회 위치를 매우 많은 수로 생각하면 도움이됩니다. 이것은 종종 컴퓨터 과학에서 수행됩니다. 예를 들어, 중지 문제 는 컴퓨터 프로그램을 (올바르게) 많은 수로 취급합니다.
첫 번째 솔루션은 위치를 64 자리 16 진수로 처리하지만이 정보에 중복성이 있으므로 ( "숫자"당 사용되지 않은 3 가지 가능성) 숫자 공간을 64 진수 13 자리로 줄일 수 있습니다. 물론 이것은 기본 16만큼 효율적으로 수행 할 수 없지만 저장 요구 사항을 절약 할 수 있습니다 (저장 공간 최소화가 목표입니다).
베이스 (10)의 수 (234)는 2 × 동등 2 + 3 × 10 1 + 4 × 10 0 .
16 진법에서 숫자 0xA50은 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (십진수)과 같습니다.
따라서 위치를 p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 으로 인코딩 할 수 있습니다. 여기서 p i 는 제곱 i 의 내용을 나타냅니다 .
2 256 등호 약 1.16e77. 13 64는 약 1.96e71과 같으며 237 비트의 저장 공간이 필요합니다. 7.5 % 만 절약 하면 조작 비용 이 크게 증가합니다.
가변베이스 인코딩
법적 게시판에서는 특정 조각이 특정 사각형에 나타날 수 없습니다. 예를 들어, 폰은 첫 번째 또는 여덟 번째 랭크에서 발생할 수 없으므로 해당 사각형의 가능성을 11로 줄입니다. 그러면 가능한 보드가 11 16 x 13 48 = 1.35e70 (대략)으로 줄어들어 233 비트의 저장 공간이 필요합니다.
실제로 이러한 값을 십진수 (또는 이진수)로 인코딩하고 디코딩하는 것은 좀 더 복잡하지만 안정적으로 수행 할 수 있으며 독자에게 연습으로 남겨 둡니다.
가변 폭 알파벳
앞의 두 가지 방법은 모두 고정 너비 알파벳 인코딩 으로 설명 할 수 있습니다 . 알파벳의 11, 13 또는 16 멤버 각각은 다른 값으로 대체됩니다. 각 "문자"는 너비가 같지만 각 문자의 가능성이 같지 않다는 점을 고려하면 효율성이 향상 될 수 있습니다.
모스 부호를 고려하십시오 (위 그림 참조). 메시지의 문자는 일련의 대시와 점으로 인코딩됩니다. 이러한 대시와 점은 라디오 (일반적으로)를 통해 전송되며 그 사이에 일시 중지되어 구분됩니다.
문자 E ( 영어에서 가장 일반적인 문자 )가 단일 점, 가능한 가장 짧은 시퀀스 인 반면 Z (최소 빈도)는 대시 2 개와 경고음 2 개입니다.
이러한 방식은 예상되는 메시지 의 크기를 크게 줄일 수 있지만 무작위 문자 시퀀스의 크기를 증가시키는 비용이 발생합니다.
모스 코드에는 또 다른 내장 기능이 있다는 점에 유의해야합니다. 대시는 점 3 개만큼 길기 때문에 대시 사용을 최소화하기 위해이를 염두에두고 생성 된 코드입니다. 1과 0 (빌딩 블록)에는이 문제가 없기 때문에 복제해야 할 기능이 아닙니다.
마지막으로 모스 부호에는 두 가지 종류의 쉼표가 있습니다. 짧은 쉼표 (점의 길이)는 점과 대시를 구분하는 데 사용됩니다. 더 긴 간격 (대시 길이)은 문자를 구분하는 데 사용됩니다.
그렇다면 이것이 우리 문제에 어떻게 적용됩니까?
허프만 코딩
Huffman 코딩 이라는 가변 길이 코드를 처리하는 알고리즘이 있습니다 . Huffman 코딩은 일반적으로 기호의 예상 빈도를 사용하여 더 일반적인 기호에 더 짧은 값을 할당하는 가변 길이 코드 대체를 생성합니다.
위의 트리에서 문자 E는 000 (또는 left-left-left)으로 인코딩되고 S는 1011입니다.이 인코딩 체계는 명확해야 합니다.
이것은 모스 부호와의 중요한 차이점입니다. 모스 부호에는 문자 구분 기호가 있으므로 모호한 대체를 수행 할 수 있지만 (예 : 4 개의 점은 H 또는 2 개의 Is 일 수 있음) 1과 0 만 있으므로 대신 명확한 대체를 선택합니다.
다음은 간단한 구현입니다.
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; }
}
정적 데이터 사용 :
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);
}
과:
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);
}
}
}
가능한 출력은 다음과 같습니다.
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
시작 위치의 경우 이것은 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 비트와 같습니다.
국가 차이
또 다른 가능한 접근 방식은 첫 번째 접근 방식을 Huffman 코딩과 결합하는 것입니다. 이것은 (무작위로 생성 된 것이 아니라) 대부분의 예상 체스 보드가 적어도 부분적으로는 시작 위치와 비슷하지 않을 가능성이 더 높다는 가정을 기반으로합니다.
그래서 여러분이하는 일은 256 비트 시작 위치로 256 비트 현재 보드 위치를 XOR 한 다음이를 인코딩합니다 (Huffman 코딩 또는 실행 길이 인코딩 방법 사용 ). 분명히 이것은 (64 비트에 해당하는 64 0s) 시작하는 데 매우 효율적이지만 게임이 진행됨에 따라 필요한 저장 공간이 증가합니다.
조각 위치
앞서 언급했듯이이 문제를 해결하는 또 다른 방법은 플레이어가 가지고있는 각 조각의 위치를 저장하는 것입니다. 이것은 대부분의 사각형이 비어있는 엔드 게임 위치에서 특히 잘 작동합니다 (그러나 Huffman 코딩 방식에서는 빈 사각형이 어쨌든 1 비트 만 사용합니다).
각면에는 킹과 0-15 개의 다른 조각이 있습니다. 승진으로 인해 이러한 조각의 정확한 구성은 시작 위치를 기반으로 한 숫자가 최대라고 가정 할 수 없을 정도로 다양 할 수 있습니다.
이것을 분할하는 논리적 방법은 두 개의면 (흰색과 검은 색)으로 구성된 위치를 저장하는 것입니다. 각 측면에는 다음이 있습니다.
- 킹 : 위치에 대해 6 비트;
- 폰 있음 : 1 (예), 0 (아니오)
- 그렇다면 폰 수 : 3 비트 (0-7 + 1 = 1-8);
- 그렇다면, 각 폰의 위치는 다음과 같이 인코딩됩니다 : 45 비트 (아래 참조);
- 비 폰의 수 : 4 비트 (0-15);
- 각 조각 : 유형 (퀸, 루크, 나이트, 비숍의 경우 2 비트) 및 위치 (6 비트)
폰 위치와 관련하여 폰은 48 개의 가능한 사각형에있을 수 있습니다 (다른 것과 같은 64 개가 아닌). 따라서 폰당 6 비트를 사용하는 경우 사용할 추가 16 개의 값을 낭비하지 않는 것이 좋습니다. 따라서 폰이 8 개인 경우 28,179,280,429,056에 해당 하는 48 8 개의 가능성 이 있습니다 . 많은 값을 인코딩하려면 45 비트가 필요합니다.
이는 측면 당 105 비트 또는 총 210 비트입니다. 시작 위치는이 방법의 경우 최악의 경우이며 조각을 제거할수록 훨씬 좋아집니다.
폰이 모두 같은 사각형에있을 수 없기 때문에 48 8 개 미만의 가능성 이 있다는 점을 지적해야합니다 . 첫 번째는 48 개, 두 번째는 47 개 등입니다. 48 x 47 x… x 41 = 1.52e13 = 44 비트 저장.
다른 조각 (다른면 포함)이 차지하는 사각형을 제거하여이를 더욱 개선 할 수 있으므로 먼저 흰색 비폰을 배치 한 다음 검정색 비폰을 배치 한 다음 흰색 폰을 배치하고 마지막으로 검은 폰을 배치 할 수 있습니다. 시작 위치에서 이것은 저장 요구 사항을 흰색의 경우 44 비트, 검은 색의 경우 42 비트로 줄입니다.
결합 된 접근 방식
또 다른 가능한 최적화는 이러한 접근 방식 각각에 강점과 약점이 있다는 것입니다. 예를 들어, 가장 좋은 4 개를 선택한 다음 처음 2 비트에서 체계 선택기를 인코딩 한 다음 그 이후에 체계 별 저장소를 인코딩 할 수 있습니다.
오버 헤드가 너무 작기 때문에 이것이 최고의 접근 방식이 될 것입니다.
게임 상태
나는 위치 보다는 게임 을 저장하는 문제로 돌아 간다 . 3 중 반복 때문에이 지점까지 발생한 동작 목록을 저장해야합니다.
주석
결정해야 할 한 가지는 단순히 동작 목록을 저장하는 것입니까, 아니면 게임에 주석을 달고 있습니까? 체스 게임에는 종종 주석이 추가됩니다. 예를 들면 다음과 같습니다.
- Bb5 !! Nc4?
White의 움직임은 두 개의 느낌표가 훌륭하다고 표시되는 반면 Black은 실수로 간주됩니다. Chess 구두점을 참조하십시오 .
또한 이동이 설명 된대로 자유 텍스트를 저장해야 할 수도 있습니다.
나는 움직임이 충분하여 주석이 없을 것이라고 가정합니다.
대수 표기법
여기에 이동 텍스트 (“e4”,“Bxb5”등)를 간단히 저장할 수 있습니다. 종료 바이트를 포함하여 이동 당 약 6 바이트 (48 비트)를보고 있습니다 (최악의 경우). 특히 효율적이지 않습니다.
두 번째 시도는 시작 위치 (6 비트)와 끝 위치 (6 비트)를 저장하여 이동 당 12 비트를 저장하는 것입니다. 훨씬 낫습니다.
또는 우리가 선택한 예측 가능하고 결정적인 방식과 상태로 현재 위치에서 모든 법적 이동을 결정할 수 있습니다. 그런 다음 위에서 언급 한 가변 기본 인코딩으로 돌아갑니다. 화이트와 블랙은 첫 번째 이동에서 각각 20 개의 이동이 가능하고 두 번째 이동에서 더 많은 이동이 가능합니다.
결론
이 질문에 절대적으로 정답은 없습니다. 위의 몇 가지 가능한 접근 방식이 많이 있습니다.
이 문제와 유사한 문제에 대해 내가 좋아하는 점은 사용 패턴을 고려하고 요구 사항을 정확하게 결정하고 코너 케이스에 대해 생각하는 것과 같이 모든 프로그래머에게 중요한 능력이 필요하다는 것입니다.
Chess Position Trainer 에서 스크린 샷으로 찍은 체스 위치 .