更新:私はこのトピックがとても好きだったので、プログラミングパズル、チェスの位置、ハフマンコーディングを書きました。これを一読すると、完全なゲーム状態を保存する唯一の方法は、ムーブの完全なリストを保存することであると判断しました。その理由を読んでください。そこで、ピースレイアウトの問題を少し簡略化したバージョンを使用します。
問題
この画像は、チェスの開始位置を示しています。チェスは8x8のボードで行われ、各プレーヤーは、8つのポーン、2つのルーク、2つのナイト、2つのビショップ、1つのクイーン、1つのキングで構成される16個のピースの同じセットから始まります。
通常、位置は列の文字として記録され、その後に行の番号が続くため、ホワイトのクイーンはd1にあります。ほとんどの場合、移動は代数表記で保存されます。これは明確であり、通常、必要な最小限の情報のみを指定します。この開口部を検討してください:
- e4 e5
- Nf3 Nc6
- …
これは次のように変換されます。
- 白はキングのポーンをe2からe4に移動します(これはe4に到達できる唯一のピースなので、「e4」)。
- 黒はキングのポーンをe7からe5に移動します。
- 白は騎士(N)をf3に移動します。
- 黒は騎士をc6に移動します。
- …
ボードは次のようになります。
プログラマにとって重要な機能は、問題を正確かつ明確に特定できることです。
では、何が欠けていたり、あいまいであったりしますか?結局のところ、たくさん。
ボードの状態とゲームの状態
最初に決定する必要があるのは、ゲームの状態を保存しているか、ボード上の駒の位置を保存しているかです。ピースの位置を単純にエンコードすることは1つのことですが、問題は「その後のすべての合法的な動き」を示しています。問題は、この時点までの動きを知ることについても何も言っていません。私が説明するように、それは実際には問題です。
キャスリング
ゲームは次のように進行しました:
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
ボードは次のようになります。
ホワイトはキャスリングのオプションがあります。これに対する要件の一部は、キングと関連するルークが決して移動できないことです。そのため、キングまたは各サイドのルークが移動したかどうかを保存する必要があります。明らかに、それらが開始位置にない場合、それらは移動しているので、指定する必要があります。
この問題に対処するために使用できるいくつかの戦略があります。
まず、追加の6ビットの情報(ルークとキングごとに1つ)を保存して、駒が移動したかどうかを示すことができます。正しいピースがたまたまある場合、これらの6つの正方形の1つにビットを格納するだけでこれを合理化できます。別の方法として、動かない各ピースを別のピースタイプとして扱うこともできます。つまり、6つのピースタイプ(ポーン、ルーク、ナイト、ビショップ、クイーン、キング)の代わりに、8(動かないルークと動かないキングを追加)があります。
En Passant
チェスのもう1つの独特でしばしば無視されがちなルールはEn Passantです。
ゲームが進行しました。
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
- OO b5
- Bb3 b4
- c4
b4の黒のポーンは、b4の彼のポーンをc3に移動して、c4の白のポーンを取るオプションがあります。これは最初の機会でのみ発生します。つまり、ブラックがオプションを渡した場合、彼は次の行動を取ることができなくなります。これを保存する必要があります。
前の動きがわかっていれば、En Passantが可能かどうか間違いなく答えることができます。別の方法として、4番目のランクの各ポーンが2倍前進してそこに移動したかどうかを保存することもできます。または、ボード上で可能なすべてのEn Passantの位置を確認し、可能かどうかを示すフラグを設定できます。
昇進
ホワイトの動きです。ホワイトがh7からh8にポーンを移動する場合、他の駒に昇格できます(ただし、キングは不可)。99%の確率でクイーンに昇格しますが、そうでない場合もあります。通常は、そうしないと勝つと、行き詰まってしまうからです。これは次のように書かれます:
- h8 = Q
これは私たちの問題にとって重要です。なぜなら、それは、それぞれの側に一定数のピースがあることに頼ることができないことを意味するからです。8つのポーンすべてが昇格した場合、片方がクイーン9、ルーク10、ビショップ10、またはナイト10になる可能性はまったくありません(信じられないほどありません)。
行き詰まり
あなたが最善の戦術を勝つことができない立場にいるときは、行き詰まりを試すことです。最も可能性の高いバリアントは、法的な移動ができない場所です(通常、キングをチェックしたときに移動するため)。この場合、あなたは引き分けを主張することができます。これは簡単に対応できます。
2番目のバリアントは、3回の繰り返しによるものです。ゲームで同じボードの位置が3回発生した場合(または次の手で3回目に発生した場合)、ドローを要求できます。位置は特定の順序で発生する必要はありません(つまり、同じ順序で3回繰り返す必要はありません)。以前のボードの位置をすべて覚えておく必要があるため、これは問題を非常に複雑にします。これが問題の要件である場合、問題の唯一の可能な解決策は、以前のすべての移動を保存することです。
最後に、50の移動ルールがあります。ポーンが移動せず、前の50回の連続した移動で駒が1つも取られなかった場合、プレーヤーはドローを要求できます。そのため、ポーンが移動した後の移動数または駒(2つのうち最新のもの)を保存する必要があります。 6ビット(0-63)。
誰の番ですか?
もちろん、それが誰であるかを知る必要もあります。これはほんの少しの情報です。
2つの問題
行き詰まったケースのため、ゲームの状態を保存する唯一の実行可能なまたは賢明な方法は、この位置に至るすべての動きを保存することです。その1つの問題に取り組みます。ボードの状態の問題はこれに単純化されます:キャスティング、エンパッサント、ステイルメイトの条件とその順番を無視して、ボード上のすべてのピースの現在の位置を保存します。
ピースのレイアウトは、各正方形のコンテンツを格納するか、各ピースの位置を格納するという2つの方法のいずれかで広く処理できます。
シンプルなコンテンツ
駒のタイプは6つあります(ポーン、ルーク、ナイト、ビショップ、クイーン、キング)。各ピースは白または黒にすることができるので、正方形には12の可能なピースの1つが含まれるか、または空である可能性があるため、13の可能性があります。13は4ビット(0〜15)で格納できます。したがって、最も簡単な解決策は、各平方に64ビットの256ビットまたは256ビットの情報を乗算して4ビットを格納することです。
この方法の利点は、操作が非常に簡単で高速であることです。これは、ストレージ要件を増やすことなく、さらに3つの可能性を追加することで拡張できます。最後のターンに2スペース移動したポーン、移動していないキング、移動していないルーク。前述の問題の。
しかし、私たちはもっとうまくやることができます。
Base 13エンコーディング
多くの場合、ボードの位置を非常に大きな数として考えると役立ちます。これはコンピュータサイエンスでよく行われます。たとえば、停止の問題は、コンピュータプログラムを(正しく)大きな数として扱います。
最初のソリューションは位置を64桁のベース16番号として扱いますが、実証されているように、この情報には冗長性があり(「桁」ごとに3つの未使用の可能性があるため)、番号スペースを64桁の13桁に減らすことができます。もちろん、これはbase 16ほど効率的に行うことはできませんが、ストレージ要件を節約できます(そして、ストレージスペースを最小限に抑えることが目標です)。
10進数では、234は2 x 10 2 + 3 x 10 1 + 4 x 10 0に相当します。
基数16では、数値0xA50は10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640(10進数)に相当します。
したがって、位置を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%の節約は、操作コストの大幅な増加という犠牲を伴います。
可変ベースエンコーディング
法定掲示板では、特定のマスが特定の正方形に表示されない場合があります。たとえば、第1ランクまたは第8ランクではポーンが発生せず、これらの正方形の可能性が11に減少します。これにより、可能なボードが11 16 x 13 48 = 1.35e70(約)に減少し、233ビットのストレージスペースが必要になります。
実際にそのような値を10進数(または2進数)にエンコードおよびデコードすることは少し複雑ですが、確実に実行でき、読者への課題として残されています。
可変幅のアルファベット
前の2つの方法はどちらも、固定幅のアルファベットエンコーディングとして説明できます。アルファベットの11、13、または16の各メンバーは、別の値に置き換えられます。各「文字」の幅は同じですが、各文字の可能性が同じではないと考えると、効率が向上します。
モールス符号を考えてみてください(上図)。メッセージ内の文字は、ダッシュとドットのシーケンスとしてエンコードされます。それらのダッシュとドットは、それらを区切るためにそれらの間に一時停止を置いて(通常)無線で転送されます。
文字E(英語で最も一般的な文字)が単一のドット、つまり最短のシーケンスであるのに対し、Z(頻度が最も低い)は2つのダッシュと2つのビープ音です。
このようなスキームでは、予想されるメッセージのサイズを大幅に縮小できますが、ランダムな文字シーケンスのサイズを大きくするという犠牲が伴います。
モールス符号には別の組み込み機能があることに注意してください。ダッシュは3つのドットと同じ長さなので、ダッシュの使用を最小限に抑えるために上記のコードはこれを念頭に置いて作成されます。1と0(ビルディングブロック)にはこの問題がないため、これを複製する必要のある機能ではありません。
最後に、モールス符号には2種類の休符があります。短い休符(ドットの長さ)は、ドットとダッシュを区別するために使用されます。長いギャップ(ダッシュの長さ)は、文字を区切るために使用されます。
では、これは私たちの問題にどのように当てはまりますか?
ハフマンコーディング
ハフマンコーディングと呼ばれる可変長コードを処理するためのアルゴリズムがあります。ハフマンコーディングは、可変長のコード置換を作成します。通常、シンボルの予想頻度を使用して、より一般的なシンボルに短い値を割り当てます。
上記のツリーでは、文字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ビットに相当します。
状態の違い
別の可能なアプローチは、最初のアプローチをハフマンコーディングと組み合わせることです。これは、(ランダムに生成されたものではなく)最も期待されるチェス盤は、少なくとも部分的には、開始位置に似ている可能性が高いという仮定に基づいています。
したがって、実行するのは、256ビットの現在のボード位置と256ビットの開始位置をXORしてから、それをエンコードします(ハフマンコーディングまたは、たとえば、ランレングスエンコーディングのいくつかの方法を使用します)。明らかに、これは(64ビットに対応する64 0)から始めるのは非常に効率的ですが、ゲームが進むにつれて必要なストレージが増加します。
ピースポジション
前述のように、この問題を攻撃する別の方法は、代わりにプレイヤーが持っている各駒の位置を保存することです。これは、ほとんどの正方形が空になるエンドゲームの位置で特にうまく機能します(ただし、ハフマンコーディングアプローチでは、空の正方形はとにかく1ビットしか使用しません)。
各サイドには、キングと0〜15個の他のピースがあります。プロモーションのため、これらのピースの正確な構成は、開始位置に基づく数が最大であると想定できないほど十分に異なる場合があります。
これを分割する論理的な方法は、2つの面(白と黒)で構成される位置を保存することです。両サイドには以下があります:
- キング:ロケーションの6ビット。
- ポーンあり:1(はい)、0(いいえ)。
- はいの場合、ポーンの数:3ビット(0-7 + 1 = 1-8)。
- はいの場合、各ポーンの位置がエンコードされます:45ビット(以下を参照)。
- 非ポーンの数:4ビット(0-15)。
- 各ピース:タイプ(クイーン、ルーク、ナイト、ビショップの2ビット)と場所(6ビット)
ポーンの場所については、ポーンは48の可能な四角形にしか配置できません(他の64のようにはできません)。そのため、ポーンごとに6ビットを使用する場合に使用される余分な16値を無駄にしない方がよいでしょう。したがって、ポーンが8つある場合、8つの可能性は48あり、28,179,280,429,056に相当します。その多くの値をエンコードするには45ビットが必要です。
これは、片側あたり105ビットまたは合計210ビットです。開始位置はこの方法の最悪のケースですが、ピースを取り除くと大幅に改善されます。
48の未満があることが指摘されるべきである8ポーンは、すべて最初の48個の可能性、第47などを有する同じ正方形であることができないので、可能性が。48 x 47 x…x 41 = 1.52e13 = 44ビットストレージ。
これをさらに改善するには、他の部分(反対側を含む)が占める正方形を削除して、最初に白の非ポーン、次に黒の非ポーン、次に白のポーン、最後に黒のポーンを配置します。開始位置では、これにより、ストレージ要件が白では44ビットに、黒では42ビットに削減されます。
組み合わせたアプローチ
別の可能な最適化は、これらのアプローチのそれぞれに長所と短所があることです。たとえば、最高の4を選択し、最初の2ビットでスキームセレクターをエンコードし、その後にスキーム固有のストレージをエンコードすることができます。
オーバーヘッドが小さいため、これは断然最善のアプローチです。
ゲームの状態
ポジションではなく、ゲームを保存する問題に戻ります。3回繰り返されているため、この時点までに発生した移動のリストを保存する必要があります。
注釈
決定しなければならないことの1つは、単に移動のリストを保存しているのか、それともゲームに注釈を付けているのかということです。チェスのゲームには、多くの場合、注釈が付けられています。
- Bb5 !! NC4?
ホワイトの動きは2つの感嘆符で見事にマークされていますが、ブラックの動きは間違いと見なされています。チェスの句読点を参照してください。
さらに、動きが説明されているので、フリーテキストを保存する必要がある場合もあります。
私は移動が十分であると想定しているため、注釈はありません。
代数表記
ここに移動のテキスト(「e4」、「Bxb5」など)を格納するだけで済みます。移動ごとに約6バイト(48ビット)である終了バイトを含める(最悪の場合)。それは特に効率的ではありません。
2番目に試すことは、開始位置(6ビット)と終了位置(6ビット)を保存して、1移動あたり12ビットにすることです。それはかなり良いです。
または、現在の位置からのすべての合法的な動きを、予測可能かつ確定的な方法で決定し、選択した状態を示すこともできます。その後、上記の可変ベースエンコーディングに戻ります。白と黒は、最初の手でそれぞれ20の可能な手があります。
結論
この質問に対する絶対的に正しい答えはありません。上記のほんの一部である多くの可能なアプローチがあります。
この問題と同様の問題について私が気に入っているのは、使用パターンの検討、要件の正確な決定、コーナーケースの検討など、プログラマーにとって重要な能力を必要とすることです。
Chess Position Trainerからスクリーンショットとして撮ったチェスの位置。