Güncelleme: Bu konuyu o kadar sevdim ki Programlama Bulmacaları, Satranç Pozisyonları ve Huffman Kodlaması yazdım . Bunu okursanız, tam bir oyun durumunu saklamanın tek yolunun tam bir hamle listesi depolamak olduğunu belirledim . Nedenini okuyun. Bu yüzden parça düzeni için problemin biraz basitleştirilmiş bir versiyonunu kullanıyorum.
Sorun
Bu resim, satranç başlangıç pozisyonunu göstermektedir. Satranç 8x8'lik bir tahtada, burada gösterildiği gibi 8 piyon, 2 kale, 2 at, 2 fil, 1 kraliçe ve 1 kraldan oluşan aynı 16 taştan oluşan bir setle başlayan her oyuncuyla gerçekleşir:
Pozisyonlar genellikle sütun için bir harf olarak ve ardından sıra numarası olarak kaydedilir, böylece Beyaz'ın veziri d1'dedir. Hareketler çoğunlukla belirsiz olmayan ve genellikle sadece gerekli minimum bilgileri belirten cebirsel gösterimde saklanır . Bu açılışı düşünün:
- e4 e5
- Af3 Ac6
- …
bu da şu anlama gelir:
- Beyaz şahın piyonunu e2'den e4'e hareket ettirir (e4'e ulaşabilen tek taş dolayısıyla “e4 ”'tür);
- Siyah şahın piyonunu e7'den e5'e hareket ettirir;
- Beyaz, atı (N) f3'e hareket ettirir;
- Siyah, atı c6'ya taşır.
- …
Yönetim kurulu şuna benzer:
Herhangi bir programcı için önemli bir yetenek , sorunu doğru ve net bir şekilde belirleyebilmektir .
Öyleyse eksik veya belirsiz olan nedir? Görünüşe göre çok fazla.
Board State vs Game State
Belirlemeniz gereken ilk şey, bir oyunun durumunu mu yoksa tahtadaki taşların konumunu mu depoladığınızdır. Sadece parçaların konumlarını kodlamak bir şeydir, ancak sorun "sonraki tüm yasal hamleler" diyor. Sorun, bu noktaya kadar olan hareketleri bilmek hakkında da hiçbir şey söylemiyor. Bu aslında açıklayacağım gibi bir problem.
Castling
Oyun şu şekilde ilerledi:
- e4 e5
- Af3 Ac6
- Fb5 a6
- Fa4 Fc5
Yönetim kurulu aşağıdaki gibi görünür:
Beyazın rok yapma seçeneği vardır . Bunun gerekliliklerinden bir kısmı, şah ve ilgili kalenin asla hareket edemeyeceğidir, bu nedenle şahın veya her iki tarafın kalesinin hareket etmiş olup olmadığının depolanması gerekecek. Açıkçası başlangıç pozisyonlarında değillerse, hareket etmişlerdir, aksi takdirde belirtilmesi gerekir.
Bu sorunu çözmek için kullanılabilecek birkaç strateji vardır.
İlk olarak, bu taşın hareket edip etmediğini belirtmek için fazladan 6 bit bilgi (her kale ve şah için 1) depolayabiliriz. Doğru parça varsa, bu altı kareden biri için yalnızca biraz depolayarak bunu kolaylaştırabiliriz. Alternatif olarak, her bir taşınmamış taşı başka bir taş türü olarak ele alabiliriz, böylece her iki taraftaki 6 taş türü (piyon, kale, at, fil, kraliçe ve şah) yerine 8 tane vardır (hareketsiz kale ve hareketsiz şah ekleyerek).
En Passant
Satrançta tuhaf ve sıklıkla ihmal edilen bir başka kural da En Passant'tır .
Oyun ilerledi.
- e4 e5
- Af3 Ac6
- Fb5 a6
- Fa4 Fc5
- OO b5
- Fb3 b4
- c4
Siyahın b4'teki piyonu artık b4'teki piyonunu c4'teki Beyaz piyonu alarak c3'e taşıma seçeneğine sahiptir. Bu sadece ilk fırsatta olur, yani Siyah seçeneği geçerse bir sonraki hamleyi yapamaz. Yani bunu saklamamız gerekiyor.
Önceki hamleyi bilirsek, Geçer Tutma mümkün olup olmadığını kesinlikle cevaplayabiliriz. Alternatif olarak, 4. seviyedeki her bir piyonun ileriye doğru çift hareketle oraya yeni mi hareket ettiğini kaydedebiliriz. Ya da tahtadaki her olası En Passant pozisyonuna bakabilir ve bunun mümkün olup olmadığını gösteren bir bayrağa sahip olabiliriz.
Promosyon
Beyazın hamlesi. Beyaz h7'deki piyonunu h8'e hareket ettirirse, başka bir taşa terfi edebilir (ancak şaha değil). Zamanın% 99'u bir Kraliçe olarak terfi ettirilir, ancak bazen değildir, çünkü tipik olarak bu, aksi takdirde kazanırsanız bir çıkmaza neden olabilir. Bu şu şekilde yazılır:
- h8 = Q
Bu bizim sorunumuz için önemlidir çünkü bu, her iki tarafta da sabit sayıda parça olduğuna güvenemeyeceğimiz anlamına gelir. 8 piyonun da terfi etmesi halinde, bir tarafın 9 vezir, 10 kale, 10 fil veya 10 atla sonuçlanması tamamen mümkündür (ama inanılmaz derecede olası değildir).
Çıkmaz
Kazanamayacağınız bir pozisyondayken en iyi taktik, bir çıkmaza girmeye çalışmaktır . En olası varyant, yasal bir hamle yapamayacağınız yerdir (genellikle şahınızı kontrol altına aldığınızda herhangi bir hamle). Bu durumda bir çekiliş talep edebilirsiniz. Bunun karşılanması kolaydır.
İkinci varyant, üç kat tekrarlamadır . Bir oyunda aynı tahta pozisyonu üç kez meydana gelirse (veya bir sonraki hamlede üçüncü kez meydana gelirse), bir beraberlik talep edilebilir. Pozisyonların belirli bir sırada gerçekleşmesi gerekmez (yani, üç kez tekrarlanan aynı hamle dizisine sahip olmak zorunda değildir). Bu, sorunu büyük ölçüde karmaşıklaştırır çünkü önceki her tahta konumunu hatırlamanız gerekir. Bu, sorunun bir gereğiyse, sorunun olası tek çözümü, önceki her hareketi saklamaktır.
Son olarak, elli hamle kuralı var . Bir oyuncu, önceki elli ardışık hamlede hiç piyon hareket etmediyse ve hiçbir taş alınmadıysa, bir piyon hareket ettiğinden veya bir taş alındığından beri (ikisinin en sonuncusu) kaç hamle kaydetmemiz gerekir. 6 bit (0-63).
Kimin sırası?
Elbette sıra kimin olduğunu da bilmemiz gerekiyor ve bu ufak bir bilgi.
İki Sorun
Çıkmaz durum nedeniyle, oyun durumunu saklamanın tek uygulanabilir veya mantıklı yolu, bu konuma yol açan tüm hareketleri saklamaktır. Ben bu sorunu çözeceğim. Tahtanın durumu problemi şuna basitleştirilecektir: tüm parçaların mevcut konumunu tahtadaki rok, geçerken, çıkmaz koşulları ve kimin sırası olduğunu göz ardı ederek saklayın .
Parça düzeni iki yoldan biriyle genel olarak ele alınabilir: her karenin içeriğini saklayarak veya her bir parçanın konumunu saklayarak.
Basit İçerikler
Altı taş türü vardır (piyon, kale, at, fil, kraliçe ve şah). Her parça Beyaz veya Siyah olabilir, bu nedenle bir kare 12 olası parçadan birini içerebilir veya boş olabilir, bu nedenle 13 olasılık vardır. 13, 4 bitte (0-15) saklanabilir. Dolayısıyla en basit çözüm, her bir kare çarpı 64 kare veya 256 bit bilgi için 4 bit depolamaktır.
Bu yöntemin avantajı, manipülasyonun inanılmaz derecede kolay ve hızlı olmasıdır. Bu, depolama gereksinimlerini artırmadan 3 olasılık daha ekleyerek daha da uzatılabilir: son turda 2 boşluk hareket eden bir piyon, hareket etmemiş bir şah ve hareket etmemiş bir kale, çok şey sağlayacak daha önce bahsedilen sorunlardan.
Ama daha iyisini yapabiliriz.
Base 13 Kodlama
Yönetim kurulu pozisyonunu çok büyük bir sayı olarak düşünmek genellikle yararlıdır. Bu genellikle bilgisayar bilimlerinde yapılır. Örneğin, durdurma sorunu bir bilgisayar programını (haklı olarak) büyük bir sayı olarak ele alır.
İlk çözüm, konumu 64 basamaklı 16 tabanlı bir sayı olarak ele alır, ancak gösterildiği gibi, bu bilgide fazlalık vardır ("basamak" başına 3 kullanılmayan olasılıktır), böylece sayı alanını 64 taban 13 basamağa indirebiliriz. Elbette bu, taban 16'nın yapabileceği kadar verimli bir şekilde yapılamaz, ancak depolama gereksinimlerinden tasarruf sağlayacaktır (ve depolama alanını en aza indirmek hedefimizdir).
10 tabanında 234 sayısı 2 x 10 2 + 3 x 10 1 + 4 x 10 0'a eşdeğerdir .
16 tabanında 0xA50 sayısı 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640'a (ondalık) eşdeğerdir .
Biz p konumumuzu kodlamak Yani 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 p ı kare içeriğini temsil i .
2 256 yaklaşık olarak 1.16e77'ye eşittir. 13 64 yaklaşık 1.96e71'e eşittir ve bu da 237 bit depolama alanı gerektirir. Yalnızca% 7,5'lik bu tasarruf, önemli ölçüde artan manipülasyon maliyetlerine neden olur.
Değişken Temel Kodlama
Yasal kurullarda belirli parçalar belirli karelerde görünmez. Örneğin, piyonlar birinci veya sekizinci sırada oluşamaz ve bu kareler için olasılıkları 11'e düşürür. Bu, olası tahtaları 11 16 x 13 48 = 1.35e70'e (yaklaşık olarak) düşürür ve 233 bit depolama alanı gerektirir.
Aslında, bu tür değerleri ondalık (veya ikili) arasında kodlamak ve kodunu çözmek biraz daha kıvrımlıdır, ancak güvenilir bir şekilde yapılabilir ve okuyucuya bir alıştırma olarak bırakılır.
Değişken Genişlikli Alfabeler
Önceki iki yöntemin her ikisi de sabit genişlikli alfabetik kodlama olarak tanımlanabilir . Alfabenin 11, 13 veya 16 üyesinden her biri başka bir değerle ikame edilir. Her "karakter" aynı genişliktedir ancak her karakterin eşit olasılıklı olmadığını düşündüğünüzde verimlilik artırılabilir.
Mors alfabesini düşünün (yukarıda resmedilmiştir). Bir mesajdaki karakterler, bir dizi çizgi ve nokta olarak kodlanır. Bu çizgiler ve noktalar, onları sınırlandırmak için aralarında bir duraklama ile (tipik olarak) radyo üzerinden aktarılır.
E harfinin (İngilizce'deki en yaygın harf ) tek nokta, mümkün olan en kısa sıra, Z'nin (en az sıklıkta) iki çizgi ve iki bip sesi olduğuna dikkat edin.
Böyle bir şema, beklenen bir mesajın boyutunu önemli ölçüde azaltabilir, ancak rastgele bir karakter dizisinin boyutunu artırma maliyetine sahiptir.
Mors kodunun başka bir dahili özelliğe sahip olduğuna dikkat edilmelidir: kısa çizgiler üç nokta kadardır, bu nedenle yukarıdaki kod, tire kullanımını en aza indirmek için bu akılda tutularak oluşturulur. 1'ler ve 0'lar (yapı taşlarımız) bu soruna sahip olmadığından, kopyalamamız gereken bir özellik değil.
Son olarak, Mors alfabesinde iki tür dinlenme vardır. Noktaları ve tireleri birbirinden ayırmak için kısa bir dinlenme (bir noktanın uzunluğu) kullanılır. Karakterleri sınırlandırmak için daha uzun bir boşluk (tire uzunluğu) kullanılır.
Peki bu bizim sorunumuza nasıl uygulanabilir?
Huffman Kodlama
Huffman kodlaması adı verilen değişken uzunluklu kodlarla uğraşmak için bir algoritma var . Huffman kodlaması, daha yaygın sembollere daha kısa değerler atamak için tipik olarak sembollerin beklenen frekansını kullanan değişken uzunluklu bir kod ikamesi oluşturur.
Yukarıdaki ağaçta, E harfi 000 olarak kodlanmıştır (veya sol-sol-sol) ve S 1011'dir. Bu kodlama şemasının açık olduğu açık olmalıdır .
Bu, Mors alfabesinden önemli bir ayrımdır. Mors kodu karakter ayırıcısına sahiptir, bu nedenle aksi takdirde belirsiz ikameler yapabilir (örneğin 4 nokta H veya 2 Is olabilir) ancak sadece 1'ler ve 0'lara sahibiz, bu yüzden bunun yerine kesin bir ikame seçeriz.
Aşağıda basit bir uygulama verilmiştir:
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; }
}
statik verilerle:
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);
}
ve:
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);
}
}
}
Olası bir çıktı:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Bir başlangıç konumu için bu, 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 bite eşittir.
Durum Farkı
Başka bir olası yaklaşım, ilk yaklaşımı Huffman kodlamasıyla birleştirmektir. Bu, en çok beklenen satranç tahtalarının (rastgele oluşturulmuş olanlar yerine), en azından kısmen bir başlangıç pozisyonuna benzememekten daha muhtemel olduğu varsayımına dayanmaktadır.
Yani yaptığınız şey, 256 bitlik bir başlangıç konumu ile 256 bit mevcut kart konumunu XOR ve sonra bunu kodlamaktır (Huffman kodlaması veya, örneğin, bazı çalışma uzunluğu kodlama yöntemlerini kullanarak ). Açıkçası bu, başlamak için çok verimli olacaktır (64 0s muhtemelen 64 bite karşılık gelir), ancak oyun ilerledikçe gereken depolama alanı artar.
Parça Konumu
Bahsedildiği gibi, bu soruna saldırmanın başka bir yolu, oyuncunun sahip olduğu her parçanın konumunu saklamaktır. Bu, çoğu karenin boş olacağı oyun sonu konumlarında özellikle iyi çalışır (ancak Huffman kodlama yaklaşımında boş kareler zaten sadece 1 bit kullanır).
Her iki tarafta bir şah ve 0-15 diğer taş bulunur. Terfi nedeniyle, bu parçaların tam yapısı, başlangıç pozisyonlarına göre sayıların maksimum olduğunu varsayamayacak kadar değişebilir.
Bunu bölmenin mantıklı yolu, iki Taraftan (Beyaz ve Siyah) oluşan bir Pozisyon kaydetmektir. Her Tarafta:
- Bir kral: Konum için 6 bit;
- Piyonları var: 1 (evet), 0 (hayır);
- Evet ise, piyon sayısı: 3 bit (0-7 + 1 = 1-8);
- Evet ise, her bir piyonun yeri kodlanmıştır: 45 bit (aşağıya bakın);
- Piyon olmayanların sayısı: 4 bit (0-15);
- Her taş için: tür (kraliçe, kale, at, fil için 2 bit) ve konum (6 bit)
Piyon konumuna gelince, piyonlar sadece 48 olası karede olabilir (diğerleri gibi 64 değil). Bu nedenle, piyon başına 6 bit kullanmanın kullanacağı ekstra 16 değeri israf etmemek daha iyidir. Yani 8 piyonunuz varsa, 28.179.280.429.056'ya eşit 48 8 olasılık vardır. Bu kadar çok değeri kodlamak için 45 bite ihtiyacınız var.
Bu, taraf başına 105 bit veya toplam 210 bittir. Başlangıç pozisyonu, bu yöntem için en kötü durumdur ve parçaları kaldırdıkça önemli ölçüde daha iyi hale gelecektir.
Bu 48'den az olduğunu sivri dışarı olmalıdır 8 piyon hepsi ilk 48 olanaklarını, ikinci 47 ve benzeri bulunur aynı meydanda olamaz çünkü olasılıklar. 48 x 47 x… x 41 = 1.52e13 = 44 bit depolama.
Bunu, diğer taşların (diğer taraf dahil) işgal ettiği kareleri ortadan kaldırarak daha da geliştirebilirsiniz, böylece önce beyaz olmayan piyonları, sonra siyah olmayanları, sonra beyaz piyonları ve son olarak da siyah piyonları yerleştirebilirsiniz. Başlangıç konumunda bu, depolama gereksinimlerini Beyaz için 44 bit ve Siyah için 42 bit'e düşürür.
Kombine Yaklaşımlar
Diğer bir olası optimizasyon, bu yaklaşımların her birinin kendi güçlü ve zayıf yönlerine sahip olmasıdır. Örneğin, en iyi 4'ü seçebilir ve ardından ilk iki bitte bir şema seçiciyi ve ardından şemaya özgü depolamayı kodlayabilirsiniz.
Bu kadar küçük ek yük ile, bu açık ara en iyi yaklaşım olacaktır.
Oyun Durumu
Bir pozisyondan çok oyun saklama sorununa dönüyorum . Üçlü tekrar nedeniyle, bu noktaya kadar meydana gelen hareketlerin listesini saklamamız gerekiyor.
Ek açıklamalar
Belirlemeniz gereken bir şey, sadece bir hamle listesi mi depoluyorsunuz yoksa oyuna açıklama mı veriyorsunuz? Satranç oyunlarına genellikle açıklama eklenir, örneğin:
- Fb5 !! Ac4?
Beyazın hareketi parlak olarak iki ünlem işareti ile işaretlenirken, Siyahın hareketi bir hata olarak görülüyor. Satranç noktalama işaretlerine bakın .
Ek olarak, hareketler açıklandıkça serbest metin de kaydetmeniz gerekebilir.
Hareketlerin yeterli olduğunu varsayıyorum, bu yüzden ek açıklama olmayacak.
Cebirsel Gösterim
Burada taşıma metnini basitçe saklayabiliriz ("e4", "Fxb5", vb.). Sonlandırıcı bir bayt dahil olmak üzere, hareket başına yaklaşık 6 bayta (48 bit) bakıyorsunuz (en kötü durum). Bu özellikle verimli değil.
Denenecek ikinci şey, başlangıç konumunu (6 bit) ve bitiş konumunu (6 bit) saklamaktır, böylece hareket başına 12 bit. Bu önemli ölçüde daha iyi.
Alternatif olarak, mevcut konumdan tüm yasal hamleleri öngörülebilir ve belirleyici bir şekilde ve seçtiğimiz durumda belirleyebiliriz. Bu daha sonra yukarıda bahsedilen değişken temel kodlamaya geri döner. Beyaz ve Siyahın her birinin ilk hareketlerinde 20 olası hamlesi vardır, ikinci hamlede daha fazla vb.
Sonuç
Bu sorunun kesinlikle doğru bir cevabı yok. Yukarıdakilerin sadece birkaçı olduğu birçok olası yaklaşım vardır.
Bu ve benzeri sorunlardan hoşlandığım şey, herhangi bir programcı için kullanım modelini dikkate alma, gereksinimleri doğru bir şekilde belirleme ve köşe durumlarını düşünme gibi önemli yetenekler talep etmesidir.
Satranç Pozisyonu Antrenöründen ekran görüntüsü olarak alınan satranç pozisyonları .