Her resmî kuralı uygulayan, dünyanın en küçük satranç oyunu.
2,7 KB'tan 1.815 bayta kadar inen farklı arayüz varyantlarıyla.
Tarayıcıda çalışır, JavaScript ile yazılmıştır; kütüphane, kurulum ve sunucu istemez.
İndirdiğiniz tek bir HTML dosyası neredeyse her akıllı cihazda açılır.
Başlangıç pozisyonunun tamamı: 64 onaltılık
(0–9 ve a–f ile yazılan, 16 tabanlı sayı) basamak, a1'den h8'e
sırayla. Her basamak bir kare, her renk bir taş türü. Motorun
tahta temsili bundan ibaret.
boş 0Fil 2/3Kale 4/5Vezir 6/7Piyon 8/9Şah a/bAt c/d
“Mükemmelliğe, ekleyecek bir şey kalmadığında değil,
çıkarılacak bir şey kalmadığında ulaşılır.”Antoine de Saint-Exupéry
a84♜56
b8c♞57
c82♝58
d86♛59
e8a♚60
f82♝61
g8c♞62
h84♜63
a78♟48
b78♟49
c78♟50
d78♟51
e78♟52
f78♟53
g78♟54
h78♟55
a6040
b6041
c6042
d6043
e6044
f6045
g6046
h6047
a5032
b5033
c5034
d5035
e5036
f5037
g5038
h5039
a4024
b4025
c4026
d4027
e4028
f4029
g4030
h4031
a3016
b3017
c3018
d3019
e3020
f3021
g3022
h3023
a29♟8
b29♟9
c29♟10
d29♟11
e29♟12
f29♟13
g29♟14
h29♟15
a15♜0
b1d♞1
c13♝2
d17♛3
e1b♚4
f13♝5
g1d♞6
h15♜7
Her karede dört şey var: kare adı, taşın onaltılık kodu, glifi
(taşın Unicode simgesi — ♞, ♛),
ve motorun kullandığı indeks.
İndeks a1'de sıfırdan başlar, soldan sağa artar, h1'den sonra bir
üst yatayın a sütununa geçer ve h8'de 63'te biter. Bu düzenin motora ne kazandırdığı
motorun kendisi sekmesinde.
L3 numerical sürümde kullanıcı sezgisine daha uygun olsun
diye indeksler +1 artırılmıştır, yani indeksin bir fazlası: a1 = 01,
e4 = 29, h8 = 64.
Golfstack
Kodun kendisi, değeri ve Golfstack sanatı
2,7 KB'lık (bir dosyanın diskte kapladığı yerin ölçüsü;
1 KB = 1024 bayt) tek bir HTML dosyasına, turnuva standartlarında eksiksiz bir
satranç hakemi sığdırıldı. Golfstack konseptiyle geliştirilen bu proje, kod
optimizasyonunun sınırlarını zorlayarak en küçük boyutta en çok işlevi sunmayı
hedefleyen bir mühendislik çalışması.
İki oyuncunun aynı cihaz üzerinden karşılıklı oynadığı bu motor, harici hiçbir
kütüphaneye ihtiyaç duymadan tam donanımlı bir satranç deneyimi sağlar.
Kural tarafında eksik yok. FIDE kitabının tamamı uygulanıyor. Hangi maddenin nasıl
işlediğini Resmî oyun kuralları sekmesinde madde madde
bulabilirsiniz. Terk ve süre bitiminin yetersiz materyalle karşılaştığı o çetrefil
senaryolarda bile karar 5.1.2 ve 6.9'a birebir uyuyor. 5.2.2'nin kilitli pozisyon
dalında motorun verdiği her karar doğru: ölü dediği pozisyon gerçekten ölüdür. Ölçülen
kapsama %93; kaçan %7, içinde fil bulunan uç örnekler. Tanımadığı her durumda
motor oyunu sürdürüyor, yani hata payı hep güvenli tarafta kalıyor.
Teknik özellikler
Modern siteye yakın arayüz.L3(tahtanın tıklanarak oynandığı sürüm) sürümünde tahta tıklanabilir,
seçili kare ve legal hedefler işaretlenir, terfi seçici açılır, şah göstergesi durum
satırında, beraberlik teklifi beraberlik düğmesinde görünür. Hamleden sonra tahta sıradaki oyuncunun
perspektifine döner — bu döndürme L3 input ve L3 prompt
sürümlerinde de var. L3'un taş glifleri, saat işareti ve göstergeleri
Unicode'dan geliyor — tek bir görsel dosyası, font indirmesi ya da CDN isteği
olmadan. Öbür sürümler tahtayı ASCII ya da tam genişlik karakterlerle çiziyor;
körleme sürümüyle L3 numerical hiç çizmiyor.
Fischer zaman kontrolü ve hoş saat gösterimi. Tempo 10+5: on dakika, artı her
hamlede oyuncunun süresine eklenen beş saniye. İki saat ekranda emojisiyle birlikte
DAKİKA : SANİYE biçiminde işler, artış her hamlede görünür şekilde biner. Bayrak
düştüğü anda sonuç kendiliğinden doğar.
Hamle üretimi. Motor satranç mantığı bakımından tuhaflık yapmaz: legal
olmayan hamle geçmez, legal olan reddedilmez, sonuç kodu yanlış pozisyonda doğmaz.
İddia ölçüme dayanıyor — bilinen satranç programlama testleri
makul derinliklere kadar hatasız geçildi: CPW pozisyonları, van Kervinck'in zor
listesi, 6.838 pozisyonluk Vajolet külliyatı.
Kural doğruluğu ve oyun sonları. Oyun bittiğinde “berabere” deyip geçmez;
on beş sonucun hangisi geldiyse onu adıyla bildirir. Kodlar iki
karakterlik olmasına rağmen mnemonik — hangi harfin neye karşılık geldiği bir bakışta
oturuyor — ve on beşi FIDE kitapçığındaki maddelerle birebir örtüşüyor. Pat mı, 3'lü tekrar mı, ölü pozisyon mu: belirsiz kalmaz.
Beraberliğin zor yarısı. Bir hakemin, sıradan hamle üretiminin dışında
cevaplaması gereken iki soru var: bu pozisyonda mat artık hiç kurulabilir mi? ve
bayrak düştüğünde ya da bir taraf çekildiğinde rakip mat edebilecek durumda mıydı?
İkisinin de ucuz bir yarısı var, bir de zor yarısı. Ucuz yarı materyali saymak: tahtada
mat etmeye yetecek taş kaldı mı. Yaygın olan bu, ve motor da yapıyor.
Zor yarı taşları saymakla çıkmıyor. Bir pozisyon, materyal fazlasıyla
yeterliyken de ölü olabilir: piyonlar birbirini kilitlemiştir, hiçbir taş oynayamaz, iki
taraf iş birliği yapsa bile mat kurulamaz. Bayrak düştüğünde de asıl soru “rakibin yeterli
taşı var mı” değil, “bu tahtada bir mat pozisyonu kurulabiliyor mu”dur — kaybeden taraf
kendi şahının kaçış karelerini kendi taşlarıyla kapatarak yardım edebildiği için savunanın
taşları da hesaba giriyor. Motor iki yarıyı da uyguluyor.
Ve zor yarının maliyeti sanıldığı kadar yüksek değil.Kilitli pozisyon hamle araması istemiyor; tahtanın statik resminden
çıkıyor — piyonların ve şahların ulaşabildiği kareler doyana kadar büyütülüyor, hiçbiri
büyümüyorsa pozisyon ölüdür. Ölçülen kapsama %93, ve kaçırdıklarının tamamı fil
içeren uç örnekler; motor tanımadığı hiçbir pozisyona “ölü” demiyor, yalnızca bazılarını
görmüyor. Yani pratikte karşılaşılan kilitli pozisyonların neredeyse tamamı, çok küçük bir
maliyetle yakalanabiliyor (1 KB daha vererek filli pozisyonların da
neredeyse tamamını dahil etmek, yani %93'ü %99,94'e çıkarmak mümkün; çizgiyi şimdilik
burada çekmeye karar verdim).
Bozulmazlık. Ne bug ne geçersiz girdi ne de kasıt motoru yoldan çıkarabiliyor.
L3'da hatalı girdi zaten doğmuyor; hamle tıklayarak yapılıyor ve yalnızca
legal kareler işaretleniyor. Metin girişli sürümlerde motor kabul etmediği her şeyi
reddedip aynı soruyu tekrar soruyor — boş, saçma ya da kasten bozuk girdiler dahil.
Oyun kilitlenmiyor, durum bozulmuyor; notasyonu doğru ama terfi harfi geçersiz bir
girişte taş otomatik vezire terfi ediyor.
Modüler yapı. Parçalar birbirine yapışık değil: kural katmanı bir yanda
durur, arayüzün yaptığı işler öbür yanda. Beraberlik teklifini, saati ya da terk
düğmesini çıkarmak istersen çıkarırsın, geri kalan aynen çalışmaya devam eder; 5'li
tekrar ve 75 hamle gibi otomatik beraberlik sınırları için de aynısı geçerli. Başlangıç
dizilişi — istersen bir FEN'den — süre, hatta tekrar sayacının eşiği bile ayarlanabilir.
Code golf satrançlarında bu ayrım genellikle yoktur: şah kontrolü ayrı bir sınama
değildir, hamle üretimiyle değerlendirme iç içe geçer, ve kuralın nerede bitip oyun
oynamanın nerede başladığı okunamaz hâle gelir.
Okunabilirlik ve mnemonik. Kaynak okunmak için değil koşmak için yazıldı:
tek harfli isimler, tek satır, gömülmüş fonksiyonlar. Yine de isimlendirme rastgele
değil — bayt bütçesi izin verdiği sürece her değişken ve fonksiyon adı İngilizce
karşılığını çağrıştıracak şekilde seçildi. Bunun için gerçekten özen gösterildi:
büyük-küçük harf ayrımına kadar, golfstack'in izin verdiği her yerde okunabilirlik
gözetildi. Ne olup bittiği satır satır ayrı bir bölümde
açılıyor.
Hız ve derinlik. Hakemin işinin büyük kısmı tek bir yarım hamlede biter:
sıradaki oyuncunun legal hamleleri ve pozisyonun bitip bitmediği. Beraberliğin zor yarısı
bunun dışında kalıyor — orada motor gerçekten ileriye bakıyor, üstelik testte değil,
oyunun içinde, bayrak düştüğü anda. Hız bu yüzden süs değil. İkinci yer test: perft
(bir pozisyondan çıkan bütün hamle dizilerini sayan standart doğruluk
sınaması) milyonlarca düğüm sayar, orada saniyeler dakikaya döner.
engine_4x ikisi için de var — aynı kuralları uygulayan,
48 bayt daha uzun ve mantıksal olarak birebir aynı bir varyant; hamle üretiminde iş yüküne
göre 2,4× ile 9,7× arası, hüküm katmanında çok daha fazlası. Baytın nereye gittiği ve
ölçümler ayrı bir başlıkta.
Bot ayrı bir katman. Ana sürümde oyunu iki insan oynuyor; motor hamle
önermiyor, oyunu yönetiyor. İddia bayt başına kural yoğunluğunda olduğu için bot şart
değil — iki insanın oynadığı yapı, rastgele hamle seçen bir bottan bile ucuza çıkıyor.
Kural katmanı ayrı durduğu için üstüne arama koymak da çekirdeği bozmuyor: saati ve
oyuncular arası etkileşimleri dışarıda bırakıp kural tarafını yerinde tutan, botlu ayrı
bir sürüm var. Ölçülen gücü 2400 civarında; dayanakları, testleri ve başka bot
varyantları ayrıca gelecek. Sıradaki soru, kurallar ve arayüz dahil 10 KB'lık tek bir
HTML'e makul bir işlemci bütçesiyle kaç elo sığdığı — hedef 3000, sonuç ne çıkarsa
ayrıntısıyla yayımlanacak.
Asıl iddia tek tek bu maddeler değil, oranın kendisi: hizmet başına düşen bayt.
Yukarıdakilerin tamamı, indirilebilir tek bir 2,7 KB'lık dosyanın içinden çıkıyor.
Motor geliştirme aşamasında uç durumlar dahil binlerce kez sınandı ve bilinen
hataların tamamı ayıklandı. Yine de oyun sırasında gözünüze çarpan bir şey olursa
bildirin; geri bildirim motoru doğrudan iyileştirir.
Oyna
Sürümü seç, başlat
Her sürüm kendi penceresinde çalışır, saatler ancak başlattığında işlemeye
başlar. Sürüm değiştirmek çalışan oyunu kapatır.
L3 ve L3 input sürümlerinde tahta küçük geliyorsa
tarayıcının yakınlaştırmasını kullan — Ctrl+, macOS'ta
⌘+. Oyun pencerenin içinde birlikte ölçeklenir;
Ctrl0 eski hâline döndürür.
Sekiz sürümden ikisi — L3 prompt ve L3 numerical — hamleleri
prompt() penceresiyle alıyor ve oyun sonucunu alert() ile
bildiriyor. Tarayıcın bu pencereleri engelliyorsa — çoğu tarayıcı ikinciden sonra
“bu sayfanın iletişim kutularını engelle” seçeneği sunar — oyun sessizce durur.
Engellediysen sayfayı yenile ve kutuları geri aç. Kalan altı sürüm hiç iletişim
kutusu kullanmaz.
Seçili sürüm L3. Oyunu başlat ya da kodunu göster.
Code golf, bir programı işlevinden, kurallarından ve mantıksal doğruluğundan
hiçbir ödün vermeden mümkün olan en az karakterle yazma sanatı. Kısıt tek: davranış
aynı kalacak, hacim küçülecek.
Bu proje baştan sona tek bir sorunun peşindeydi:
Baştan sona hiçbir kuralı atlamayan, %100 FIDE uyumlu
eksiksiz bir satranç maçı en az kaç bayt olabilir?
Cevabı aşağıda. Ama bu soruyu sorabilmek için önce “eksiksiz” kelimesinin nerede
başladığına karar vermek gerekiyordu.
Neyi çıkarırsak hâlâ satranç kalır
Motoru yazarken kendimi bir anda Theseus'un Gemisi paradoksunun ortasında buldum:
bir satranç oyunundan neleri söküp atarsak, o oyun hâlâ satranç kalmaya devam eder?
Burada kaçınılmaz bir kimlik sorusu çıkıyor. On beşinci yüzyıl öncesinin satrancını
başlangıç noktası almadım — orada fil yalnızca iki kare atlıyor, vezir tek kare çapraz
gidiyor, rok ve piyonun çift adımı yok. Aynı adı taşısalar da o oyunla bugün oynadığımız
oyun temelde iki ayrı kimlik; onu taban kabul etmek, sorunun cevabını başka bir oyun
için vermek olurdu.
Başlangıç noktası olarak şunu aldım: bugünkü taş hareketleri, şah, mat ve pat,
piyonun tek kare ilerlemesi, otomatik vezir terfisi. Çıkarılabilecek ne varsa
çıkarıldığında geriye kalan çekirdek bu. Buradan bir madde daha eksiltmek oyunu satranç
olmaktan çıkarıyor.
Çekirdek eldeyken tek yön kalmıştı: eklemek.
Çekirdekten tam hakemliğe
Motor tek seferde doğmadı, üst üste binen turlarla büyüdü.
Önce seçimli terfi geldi; piyon vezire mahkûm olmaktan çıktı, at, fil ve kale de
birer seçenek oldu. Ardından piyonun iki kare ilerlemesi ve onunla birlikte gelmek
zorunda olan geçerken alma — ikisi ayrılamaz, çünkü ikincisi birincisinin açtığı
boşluğu kapatır. Sonra rok. Bu noktada tahtada olup biten artık bugünkü satrancın
kendisiydi.
Sonrasında tahtanın dışına çıktım. Oyunun sonsuza kadar sürmesini engelleyen iki
teorik sınır, 50 hamle kuralı ve saat, motora bu aşamada girdi.
Normalde orada bırakacaktım. Ama bu kadar ilerlemişken kural kitabının geri kalanını
da tamamlamak istedim: pozisyon tekrarı, yetersiz materyal, beraberlik teklifi ve
iddiası, bayrak düşmesi, oyundan çekilme. Hakemin tahtaya hiç bakmadan verdiği kararlar
da böylece işin içine girdi.
Bu yolun düz bir çizgi olduğunu söyleyemem. Sık sık, daha önce eklediğim bir kuralın
FIDE'ye tam uymadığını ya da belirli bir pozisyonda yanlış davrandığını fark edip geri
dönmem gerekti. Bazen yeni bir keşif, o ana kadar yazdığım çözümün tamamını çöpe
attırdı. Her geri dönüş yapısal bir revizyon, arkasından da yeni bir bayt optimizasyonu
demekti; kod büyüdü, sonra yeniden küçüldü.
Uzun ve öğretici bir süreç oldu, hem kodlama hem satranç mantığı tarafında. Hangi
kuralın neyi kırdığını, hangi keşfin neyi baştan yazdırdığını ayrı bir “Yapım süreci”
bölümünde adım adım ve çok daha ayrıntılı anlatacağım.
Yontma nerede biter
Michelangelo'ya heykeli nasıl yaptığını sorduklarında verdiği meşhur cevap şudur:
Heykel zaten mermer bloğun içinde tamamlanmış hâldedir. Ben sadece
gereksiz kısımları yontup atıyorum.Michelangelo
Golfstack'te yaptığım da buydu. Kuralların ne olduğu baştan belliydi; iş, o kuralları
anlatan her fazlalığı yontmaktı.
Claude Shannon'ın bilgi teorisi bu yontmanın nerede biteceğini söylüyor: her mesajın
kayıpsız sıkıştırılabileceği bir alt sınır var, ve o sınırın altındaki tasarruf artık
tasarruf değil, bilgi kaybıdır. Code golf tam olarak o sınıra doğru yürümek. Buradaki
fark, sıkıştırmanın kayıpsız olmak zorunda olması: kuralların gerektirdiği mantıksal
veriden tek bir madde bile düşerse elde kalan şey daha küçük bir satranç olmaz, satranç
olmayan bir şey olur.
Sorunun cevabı bu yüzden tek bir sayı: 1.815 bayt.numerical_packed.html, iki insan oyuncu arasındaki resmî bir maçı baştan
sona yönetiyor.
Neden tarayıcı ve JavaScript
Akla gelen ilk sorulardan biri şu: neden tarayıcı, neden JavaScript?
Cevabın ilk yarısı erişimle ilgili. Hangi cihazda, hangi işletim sisteminde, hangi
ortamda açılabildiğine bakılırsa tarayıcı açık ara birinci sırada. Derleyici yok,
çalışma zamanı kurulumu yok, paket yöneticisi yok; indirilen dosyaya çift tıklamak
yetiyor. Üstelik HTML görsel katmanı neredeyse bedavaya veriyor — tahta, saat ve
göstergeler için ayrı bir arayüz altyapısı yazmak gerekmiyor, dahası
<html>, <head>, <body> hiç
yazılmasa bile dosya açılıyor. Motorun oyuncuya ulaşmak için kat etmesi gereken mesafe
bu kadar kısa olmasaydı, 2,7 KB'lık iddianın pratikte bir karşılığı olmazdı.
İkinci yarısı dilin kendisiyle ilgili. JavaScript golfstack'e beklenenden çok daha
elverişli: değişken bildirim anahtar kelimesi istemiyor, giriş noktası ve içe aktarma
gibi zorunlu bir iskeleti yok, örtük dönüşüm sayesinde tahtanın sayı kodlaması tek bir
tip çevrimi yazdırmıyor. Ok fonksiyonu, üçlü operatör ve virgül operatörü birlikte
çalışınca bütün bir fonksiyon süslü parantez ve return olmadan tek ifadeye
sığıyor. Kendi kendini açan paketleyiciler de bu dile özgü; motorun paketlenmiş hâli
doğrudan buna dayanıyor.
Buradan bir itiraz doğuyor: tarayıcı yüzlerce megabayt, JavaScript motorunu da
çalışma zamanını da o veriyor. Öyleyse 1.815 bayt kelime oyunu değil mi?
Sıfır taban diye bir şey yok. C ile yazsam libc ve bir işletim sistemi gerekirdi,
assembly ile yazsam bir komut kümesi ve bir makine. Çalışma zamanını da sayan ölçünün
duracağı bir yer yok; öyle ölçülürse hiçbir dilde boyut iddiası kurulamaz. Code golf
bu yüzden çizgiyi hep aynı yere çeker: yazılan kod ile dışarıdan eklenen kütüphaneler
sayılır, ortam ve taban sayılmaz. Tarayıcı da işletim sistemi de işlemci de kimsenin
hanesine yazılmaz. Diller arası karşılaştırma bu ölçüyle yapılır, ve her rekor denemesi
istenen dile açıktır.
Son kullanıcının gözünden bakıp daha sıkı bir ölçü isteyen de haklıdır: aynı ortam,
aynı dil, sıfır kütüphane. İkisi de meşru, ve ikisinde de çizginin bu yanında tek bir
dosya duruyor — indirilen, paketlenen, eklenen hiçbir şey yok.
“Dünyanın en küçüğü” iddiası bu ölçüye dayanıyor. Uzunca bir süredir arıyorum ve
her resmî kuralı uygulayan daha küçüğüne rastlamadım, ne JavaScript'te ne başka bir
yerde. İddia çürütülebilir, öyle de olmalı: biri daha küçüğünü çıkarırsa düşer.
Çıkana kadar duruyor.
Kural katmanı ayrı, bot ayrı
İkinci soru daha sert: neden ana sürümde bir yapay zekâ rakip yok?
Alışılmış hat belli. Ekrana bir tahta çizilir, taşlar dizilir, oyuncu rastgeleden
biraz daha iyi oynayan bir botla kapışır; bugüne kadarki denemelerin büyük çoğunluğu bu
çizgide ilerledi. Code golf dünyasında birkaç yüz bayta sığdırılmış satranç botları var,
ama yer açmak için rok, geçerken alma, 50 hamle, 3'lü tekrar gibi oyunun belkemiğini
oluşturan kuralların neredeyse hepsini feda ediyorlar. Bot mantığı kodu ciddi biçimde
şişirir; iki insanın oynadığı yapı bayt bütçesi açısından çok daha ucuz. Üstelik bir bot
eklemek FIDE uyumluluğuna hiçbir şey katmıyor.
Asıl mesele ölçülebilirlik. Botun tanımı belirsiz: 100 baytın altına sığan, rastgele
ama legal hamle yapan şey bot mudur? Materyal sayan bot mudur, yoksa eşik daha yukarıda
mı? Bunu kim söyleyecek? Kurallar öyle değil, kesin ve net. Bir kodun kuralı uygulayıp
uygulamadığı bakıldığında görünüyor, tartışmaya yer bırakmıyor. Süreç boyunca ben de bot
yazmayı denedim; sonra bütün enerjimi ölçülebilir olan tarafa verdim.
İkisi kodda da ayrı duruyor, ve bu tesadüf değil. Motor çekirdeği sürücüden hiçbir şey
bilmiyor; üstüne bir arama koymak kural tarafına dokunmuyor. Botlu sürüm tam olarak böyle
çıktı: aynı kural katmanı, saat ve oyuncular arası etkileşimler dışarıda, üstünde arama.
Ölçülen gücü 2400 civarında. Bitmiş saymıyorum, çünkü bir botu buraya taşımak için
kendime koyduğum ölçüt hakem motorununkiyle aynı: bayt/kalite oranında şaheser olması. Bu
daha uzun sürecek bir iş, ama çok keyif aldığım bir hobi olduğu için o tarafta da somut
bir rekor denemesi yapacağım.
Geriye ölçülebilir tarafın ne satın aldığı kalıyor: 1.815 baytlık bu yapı,
beraberliğin zor yarısında büyük platformların ilerisinde. Yaygın olan, saf FIDE
uygulayan yerlerde bile, materyal testiyle yetinmek: tahtada mat etmeye yetecek taş
var mı. Pozisyonun kendisine bakan taraf — bayrak düştüğünde mat pozisyonunun kurulup
kurulamayacağı, ve kilitli pozisyonların tespiti — yaygın değil.
Bunun bir de ölçüsü var: CHA-Solver
büyük bir açık oyun veritabanını baştan sona tarayıp 200 bin civarında yanlış
sonuçlanmış oyun saydı — 50 bini kilitli pozisyonlardan, kalan 150 bini
bayrak düşmesinde sorulmayan sorudan. Chess.com bazı maddelerde
FIDE-USCF hibriti uyguluyor; eksiklik değil tercih, iki
federasyonun kurallarını bilerek harmanlıyorlar. Buradaki tercih başka: tek kitap, tek
ölçü. Aynı maddelerde motor bu yüzden FIDE'ye daha sıkı bağlı.
Baytların ötesinde
Code golf dünyasındaki satranç projeleri genellikle 50 hamle sınırından, 3'lü tekrardan, yetersiz materyalden, çoğu zaman rok ve geçerken almadan taviz verip yalnızca
temel çekirdeğe odaklanıyor. Ben kimsenin pek uğramadığı tarafa baktım; projenin
özgünlüğü de galiba tam olarak oradan doğdu. Bildiğim kadarıyla Golfstack, %100 FIDE
standardını bu kadar dar bir bayt alanına sığdırabilen dünyanın en küçük tam hakem
motoru.
Ticari bir ürün değil, öyle bir iddiası da yok. Ama satrancın bütün kurallarını bu
şekilde katmanlandırıp en az bayta sığdırmak, kod tarafında sınır zorlayan katıksız bir
mühendislik işi.
Bu noktaya gelirken Claude ve Gemini gibi modellerden ciddi destek aldım. Normalde
aylar sürecek hata ayıklama, kod optimizasyonu ve tarayıcı davranışlarını didikleme
işini bu araçlarla inanılmaz kısalttım. Kararlar bana ait kaldı, kazandığım şey hız
oldu.
Çehov'un dediği gibi: uzun yazmak kolaydır, asıl zor olan kısa yazmaktır. Ben de bu
projede satrancın o devasa dünyasını, işlevinden hiçbir şey kaybetmeyen en kısa hâline
dönüştürmeye çalıştım.
FideLite — adın ve logonun anlamı
Ad iki parçadan: FIDE + Lite. Birleşince Fransızca
fidélité okunuşuna düşer — sadakat, bağlılık. Anlattığı da bu:
dosya küçülürken kural kitabına bağlılığın küçülmemesi. “Lite” hafiflik,
“Fide” federasyon; ikisi bir arada, hafiflik uğruna hiçbir maddenin atılmadığı
bir hakem.
Logo tek bir karenin etrafına kurulu. Ortadaki kare
başlangıç, geri kalan yirmi dört kare o kareden hareket eden taşların
gidebileceği yerler.
Başlangıç — ortadaki tek kare
At — sekiz nötr kare, L sıçramaları
Kale — sekiz turkuaz kare, dört ışın
Fil — sekiz leylak kare, iki köşegen
Vezir — kale ve fil karelerinin tümü
Şah — bordo çerçeve, bir adımlık erim
Piyon — tek karelik ilerlemesinin düştüğü kare: FIDElite yazısı
Kurallar
Motorun uyguladığı kurallar
1. Taş hareketleri, geçerlilik ve şah güvenliği
Geometrik hatlar. Kale, fil ve vezir kendi doğrultularında ilerler; şah
her yöne bir kare; piyon dikey ileri gider, çapraz alır; at “L” geometrisiyle atlar.
Yolun açıklığı. At dışındaki tüm taşlar için hat üzerinde başka taş
bulunmamalıdır.
Geçerlilik. Hedef kare boş olmalı ya da rakip taş barındırmalıdır; kendi
taşının bulunduğu kareye hamle yapılamaz.
Şah güvenliği. Hiçbir hamle kendi şahını tehdit altında bırakamaz.
2. Özel hamleler
Rok. Şah ve kale hiç hareket etmemiş, aralarındaki kareler boş, şah şahta
değil ve geçtiği kare tehdit altında değilse yapılan çift taş hamlesidir. Hareket
eden kalenin rok hakkı gider; şah hareket etmişse iki kalenin de hakkı gider.
Geçerken alma. İki kare çıkan rakip piyonun hemen ardından, tek kare
çıkmış gibi çaprazlama alınmasıdır. Yalnızca o hamlelik geçerlidir.
Terfi. Son yataya ulaşan piyon, şah ve piyon dışındaki kendi taşlarından
birine — vezir, kale, fil veya at — dönüştürülür.
3. Mat ve pat
Mat. Şah tehdit altındadır ve tehdidi kaldıracak hiçbir geçerli hamle
yoktur. Mat eden taraf kazanır.
Pat. Sırası gelen tarafın şahı tehdit altında değildir, ama hiçbir
geçerli hamlesi kalmamıştır. Oyun berabere biter.
4. Terk, süre bitimi ve yetersiz materyal
Terk ve süre bitimi. Oyundan çekilen veya süresi biten taraf kural olarak
kaybeder.
Ölü pozisyon. Geçerli hamle dizilimleriyle matın geometrik olarak imkânsız
olduğu durumda oyun anında berabere biter: şah–şah, şah–şah+fil, şah–şah+at, ve bütün
filleri aynı renk karelerde olan şah+fil–şah+fil. Fil sayısının burada bir etkisi yok:
iki tarafta kaç fil olursa olsun, hepsi tek renk karedeyse mat kurulamaz. Buna ek olarak
yalnız şah ve piyon kalmış kilitli pozisyonlar da tespit ediliyor.
Mat imkânsızken terk veya süre bitimi. Çekilen ya da süresi biten taraf
normalde kaybeder; ancak rakibinin geçerli hamlelerle mat etmesi geometrik olarak
tümüyle imkânsızsa oyun berabere biter. Süre bitimi için 6.9, çekilme için
5.1.2 — ikincisi 2023'te eklendi.
Bu istisna “rakip mat etmeye zorlayabilir mi” diye sormaz; “bu materyalle
mat pozisyonu kurulabilir mi” diye sorar. Kaybeden taraf kendi taşlarıyla
şahının kaçış karelerini kapatarak yardım edebileceği için, savunan tarafın taşları
da hesaba girer — mekanizma boğma mattakinin aynısı.
Materyal
Karar
Çıplak şah
Yetersiz
Tek fil
Yetersiz
Tek renk karelerde filler
Yetersiz
Tek at
Yetersiz
İki renkte filler
Yeterli
Fil + at
Yeterli
İki at
Yeterli
Herhangi bir kale, vezir veya piyon
Yeterli
Tek atla mat ancak savunan taraf kendini duvara kapatacak bir taşa sahipse
kurulabilir — fil, at, piyon, hatta kale. Savunan vezir bu işi göremez, çünkü köşedeki kaçışı kapatabileceği her
kareden aynı zamanda mat eden atı tehdit eder. Savunan piyonu yeterli kılan asıl şey
terfi: madde herhangi bir geçerli hamle dizisine izin verdiği için piyon, sıkıştırmayı
yapacak taşa dönüşebilir.
Mat eden
Savunan
Mat
Kurulabilen pozisyon
Ş
Ş + herhangi bir taş
Yok
kurulamaz
Ş + Fil
Ş
Yok
kurulamaz
Ş + Fil
Ş + Fil aynı renk
Yok
kurulamaz
Ş + Fil
Ş + Kale
Yok
kurulamaz
Ş + Fil
Ş + Vezir
Yok
kurulamaz
Ş + At
Ş
Yok
kurulamaz
Ş + At
Ş + Vezir
Yok
kurulamaz
Ş + At
Ş + Kale
Var
♚♜♞♚
Ş + At
Ş + Fil
Var
♚♝♚♞
Ş + At
Ş + At
Var
♚♞♞♚
Ş + At
Ş + Piyon
Var
Terfiden sonra
Ş + Fil
Ş + At
Var
♚♞♝♚
Ş + Fil
Ş + Fil zıt renk
Var
♚♝♚♝
Ş + Fil
Ş + Piyon
Var
Terfiden sonra
Ş + At + At
Ş
Var
♞♚♞♚
Bunlar kurulabilen pozisyonlar, zorlanabilen sonuçlar değil. Hiçbiri kazanma
yöntemi değil; yalnızca “bu materyalle mat diye bir şey var mı” sorusunun cevabı,
ve 6.9 ile 5.1.2 tam olarak bunu sorar. Materyale dayanmayan ölü pozisyonlar —
kilitlenmiş bir piyon zinciri gibi — şah ve piyon kaldığı sürece ayrıca tespit ediliyor;
onu görmek tarama değil arama gerektiriyordu.
5.2.2'nin ikinci yarısı: kilitli pozisyonlar
FIDE'nin 5.2.2 maddesi şöyle: hiçbir oyuncunun herhangi bir geçerli hamle
dizisiyle rakibin şahını mat edemeyeceği bir pozisyon ortaya çıktığında oyun
berabere biter. Buna ölü pozisyon denir.
Bu koşulun yetersiz materyalden doğan hâli tamamen uygulanıyor; yukarıdaki iki tablo
tam olarak onu anlatıyor. İkinci hâli, yani materyalin yeterli olduğu ama matın yine de
imkânsız olduğu kilitli pozisyonlar, artık uygulanıyor — kısmen. Tahtada yalnız şah
ve piyon kaldığında motor pozisyonun kilitli olup olmadığına karar veriyor. Ölçülen
kapsama %93; kaçan %7’nin tamamı fil içeriyor.
Yönü de söylemek gerek: kaçırılan beraberlik var, uydurulan beraberlik yok. Motor ölü
dediği her pozisyonda haklı. Tanımadığı her durumda oyunu sürdürüyor, yani hata payı hep
güvenli tarafta.
♚♝♟♟♟♟♟♟♟♟♚♝
Materyal fazlasıyla yeterli, mat imkânsız. Beyazın piyonları 4.
yatayın bütün açık karelerinde, siyahınkiler 5. yatayın bütün koyu karelerinde:
açık kareli beyaz fil 4. yatayı, koyu kareli siyah fil 5. yatayı hiçbir zaman
geçemez. Geriye kalan kareleri de karşı piyonlar tutuyor, o yüzden şahlar da kendi
yarısında kalır. Hiçbir piyon oynayamaz, hiçbir taş alınamaz.
Bu pozisyonda tahtada iki fil ve sekiz piyon var; hiçbir materyal testi buna
yetersiz demez. Yine de taraflar iş birliği yapsa bile mat eden bir hamle dizisi
kurulamaz, yani 5.2.2 anlamında ölü bir pozisyondur. Motor bunu göremez ve oyun
sayaçlar dolana kadar sürer — kilitli dedektörünün kapsamı şah ve piyonla sınırlı,
tahtadaki iki fil onu devre dışı bırakıyor.
Teşhis şuydu: kilitli pozisyon tahta görüntüsünden ya da durum değişkenlerinden
herhangi bir çıkarımla belli olmuyor. Motorun geri kalanı böyle değil — diğer her şey
geometri ve aritmetikle, hem her teşhisin doğru olduğu hem de hiçbir vakanın kaçmadığı
biçimde çözülebiliyor. Burada tarama yetmiyordu, arama gerekiyordu.
Arama yazıldı. Sekiz bitboard maskesi piyonların ve şahların ulaşabileceği kareleri
adım adım büyütüyor; hiçbir maske büyümediğinde pozisyon kilitlidir. Altmış dört kare ve
sekiz maske, en kötü ihtimalle 512 adım demek, döngü 600’de duruyor — yani
sınır ispatlı, arama erken kesilmiyor. Toplam 488 bayt.
Bu arama yapıldı.
CHA-Solver bir
pozisyonun kazanılabilir olup olmadığına matematiksel kesinlikle karar veriyor;
algoritması hakemli bir makaleyle yayımlandı, açık kaynak, ve Lichess veritabanının
tamamını taradı. Bu yazının yazıldığı 08.08.2026 itibarıyla 200 bin civarında yanlış
sonuçlanmış oyun tespit edilmiş durumda; bunun 50 bini kilitli pozisyonlardan
geliyor, kalan 150 bini bayrak düşmesinde sorulmayan sorudan. Yani sağlamlık çözülmüş bir problem — verdiği cevap yanlış olmuyor.
Çözülmemiş olan tamlık, ve buradaki fark ince değil. Aracın kendi belgesi
açıkça yazıyor: bilinen her legal pozisyonda — gerçek ya da kurgu — tam, ama teoride
tamlığı kanıtlanmış değil; çözemediği bir pozisyon bulmaya davet ediyor. Bu bir kusur
değil, problemin cinsi. Kanıt değil tümevarım: CHA da tamlık iddiasından sonra gelen
karşıt örneklerle uygulamasını sürekli iyileştirdi, kaçırdığı pozisyonlar geldikçe
kapattı. Kitabın öbür maddelerinde sınır ispatla çizili; burada ölçümle çizili.
Filli vakaların neredeyse tamamını kapsayan bir genişletme tasarlandı ve ölçüldü:
kapsamayı %93'ten %99,94'e çıkarıyordu. Golf edilmiş hâli bile düz metinde 1.000
bayta yakın tuttu, ve askıya alındı.
Aritmetik şöyle. Yerindeki dedektör 488 baytla 93 puan alıyor; genişletme 1.000
baytla 7 puan daha. Bayt başına getiri 27 kat düşüyor. Karşılığında kapatılan
şey de zaten nadir görülen bir pozisyon sınıfının içindeki bir alt küme — istisnanın
istisnası. 1.935 baytlık bir motorda 1.000 bayt, programın yarısından fazlası demek;
paketlenmiş numerical_packed.html 1.815'ten 2.600 bayta çıkardı.
Üstelik %99,94 da %100 değil. Ölçülen bir sayı, kanıtlanmış bir sınır değil — 1.000 bayt
tamlık satın almıyor, yalnızca eksikliği ondalık basamaklara itiyor.
Kural bu yüzden askıda; kaybolmuş değil. Boyut ve kesinliğin ölçüsü
motorun kendisi sekmesinde.
Kaçırmanın pratikte ne kadar sürdüğü de ölçüldü. Aşağıdaki pozisyonda her iki tarafın
da tek legal hamlesi var — beyaz şah a1 ile b1 arasında, siyah şah a8 ile b8 arasında gidip
geliyor, filler kendi piyonlarıyla tıkalı, piyonların hiçbiri oynayamıyor:
k1b5/1p1p4/1P1P4/8/8/1p1p4/1P1P4/K1B5 w
Motor bunu ölü göremiyor, çünkü tahtada fil var. Ama oyun on altıncı yarım hamlede
beş tekrarla bitiyor: dört yarım hamlede bir aynı pozisyona dönülüyor. Sonuç kodu
DP yerine 5R çıkıyor, ve ikisi de beraberlik. Tespit edilemeyen bir
kilitli pozisyonun bedeli yüz elli yarım hamle değil, çoğu zaman on altı — çünkü kilitli
olmak zaten hareketliliğin bitmesi demek, tekrar da o yüzden erken gelir.
Hedef %100 değildi, olamazdı da. Pratikte karşılaşılan kilitli pozisyonların
%90'ından fazlasını kapsamak ve ne kapsandığını açıkça söylemekti; ölçülen sonuç %93.
Motor artık beraberliği yalnız emin olduğu yerde ilan ediyor. Büyük platformlarda yaygın
olan ise, federasyondan bağımsız olanlarda bile, materyal testiyle yetinmek.
Sapma bundan ibaret. Mat imkânsızlığının çekilme ve süre bitimindeki iki
istisnası — 5.1.2 ve 6.9 — eksiksiz uygulanıyor, ve ikisi de
kendi sonucuyla ayrı ayrı bildiriliyor.
k1b5/1p1p4/1P1P4/8/8/1p1p4/1P1P4/K1B5 w
5. Hamle ve tekrar sayaçları
Bir oyuncunun hamlesi bir yarım hamle, iki tarafın karşılıklı hamlesi bir tam
hamledir.
Pozisyon tekrarı. İki pozisyonun aynı sayılması için tahta görüntüsü,
hamle sırası, rok hakları ve geçerli geçerken alma imkânının tamamı birebir aynı
olmalıdır. Biri bile farklıysa pozisyon benzersizdir.
50 ve 75 hamle sayacı. Piyon hareketi yapılmadığı ve taş alınmadığı sürece
ilerler; ikisinden biri olduğunda sıfırlanır.
İddia edilenler. 3'lü tekrar ve 50 tam hamle (100 yarım hamle) iddia
edilirse beraberlik verilir.
Otomatik olanlar. 5'li tekrar ve 75 tam hamle (150 yarım hamle) iddia
beklemeden ilan edilir.
♜♚♟♟♚
Siyah az önce g7–g5 oynadı ve geçerken alma karesi g6. Ama f5'teki
beyaz piyon, f8'deki kaleye karşı şahına bağlı: fxg6 oynanırsa f dikeyi açılır ve
beyaz şah tehdit altında kalır. Alma geçerli olmadığı için imkân da kurulmaz.
Tekrar anahtarındaki “geçerli geçerken alma imkânı” şartı, o imkânın
gerçekten geçerli olmasını istiyor. Motor kareyi ancak alma oynanabilir
durumdaysa kurar; iki kare sürülen her piyonda otomatik olarak değil.
Ayrım kritik, çünkü hamleyi en sonda şah kontrolünde reddetmek yeterli değil.
Geçerken almanın kendisi açısından ikisi aynı sonucu verir — hamle her hâlükârda
oynanamaz. Fark tekrar sayacında ortaya çıkar: imkân, pozisyon anahtarının bir
parçasıdır. Geçersizken kurulursa FIDE'nin aynı saydığı iki pozisyon farklı anahtar
üretir, üçüncü tekrar geç tetiklenir ya da hiç tetiklenmez.
Sonuç, berabere bitmesi gereken bir maçın mağlubiyetle bitmesi olabilir. Motor
hamleye izin vermese bile bu bir kural ihlalidir; istenmeyen kenar durum bu yüzden
yok. Kontrol geçerli hamle üretecinin kendisiyle yapıldığından, almanın kendi şahını
açığa çıkardığı durumlar da aynı testte kapanır.
6. Beraberlik iddiası ve anlaşma
Mevcut durumu iddia. Eşik tahtada zaten dolmuşsa hamle yapmadan talep
edilir.
Hamleyle iddia. Oyuncu hamlesini bildirir ve o hamle oynandığında kuralın
sağlanacağını beyan eder.
Anlaşmalı beraberlik. Sırası gelen oyuncu hamlesini yaptıktan sonra
rakibine teklif sunar; teklif kabul edilirse oyun berabere biter.
Teklifin düşmesi. Teklif, rakip onu kabul etmeyen bir hamle oynadığında
geçerliliğini yitirir.
Matın üstünlüğü. Bir hamle mat ile sonuçlanıyorsa, aynı hamlede 75 hamle
kuralı veya 5'li tekrar gibi otomatik beraberlik koşulları oluşsa dahi
mat geçerlidir. Aynısı iddia için de geçerli: hamlenin sonuna konan beraberlik eki,
hamle mat ediyorsa yok sayılır. Mat hamlesi yapıldığı an oyun galibiyetle biter.
Federasyon farkı: FIDE ve USCF
Oyunun sonlanma türleri federasyona göre kayda değer biçimde değişir. USCF'de
çekilmek her zaman kayıptır, materyale bakılmaz. Otomatik beraberlik diye bir şey
yoktur: 75 hamle ve 5'li tekrar sonuç doğurmaz, yalnızca 50 hamle veya 3'lü tekrar iddia edilirse beraberlik verilir.
Asıl fark bayrak düşmesiyle materyalin karşılaştığı yerde ortaya çıkar, ve iki
federasyon farklı soru sorar. FIDE'nin sorusu şu: rakip en kötü hamleleri oynasa
bile mat mümkün mü? Yani yardımlı mat, geometrik imkân. Bu sorunun cevabında kesinlik
ve tamlık vardır; tek boyutlu bir tahta dizisinden bile çıkarılabilir — bu kodda
yapılan tam olarak budur. USCF'nin sorusu ise başka: bayrağı düşen tarafın rakibinin
zorlama matı var mı?
Belirli bir çizgiye kadar ikisi hemen hemen aynı sonucu verir. Ama USCF tek ata,
tek file, hatta bazen iki ata doğrudan yetersiz materyal diyerek istenmeyen kenar
durumlar üretebilir. Chess.com bu maddede tersini yapar: bayrağı düşen tarafın
rakibinde şah ve iki at varsa sonuç beraberlik değil mağlubiyettir. Masa başında
zorlama mat tespiti kolaydır; dijital bir algoritmada aynı doğruluğu sağlamak için
arama gerekir. Aksi hâlde tek atla yapılan mat gibi durumlar tespit edilemez.
♟♚♞♚
Beyaz Af1 oynadı. Siyahın tek hamlesi h3–h2, ardından Ag3 mat: şahı
köşeye kilitleyen taş kendi piyonu.
Aynı tahtada üç soru, iki sonuç. Yandaki pozisyonda FIDE ile USCF hemfikir: mat
hem mümkün hem zorlama, siyahın bayrağı düşerse beyaz kazanır. Chess.com üçüncü bir
soru soruyor — bu materyal çıplak bir şahı mat edebilir mi — ve tek at
edemediği için
beraberlik veriyor.
Pozisyondan bağımsız, sabit maliyetli, her partide aynı cevabı veren bir test bu;
sunucu ölçeğinde savunulabilir bir tercih, üstelik yardımlı mat hükmünü acemi
oyuncuya izah etme yükünü de kaldırıyor. Bedeli şu: kaybı gören taraf saati akıtarak
beraberliğe ulaşabilir. Buradaki motor takası ters yönde yapıyor — aynı kararı
64'lük diziden, arama yapmadan veriyor.
Sürümler
Sekiz önyüz, tek motor
Sekizi de aynı çekirdeği kullanıyor. Fark giriş biçiminde, tahtanın gösterilip
gösterilmemesinde ve saatin nasıl işlediğinde. Boyutlar bayt olarak, gönderilen
dosyanın tamamı: <script> sarmalayıcısı, L3 input
ailesinin 28 baytlık işaretleme öneki ve glif taşıyan dosyalardaki BOM dahil.
Kaynağı ekrandan kopyalayıp bir metin dosyasına yapıştıracaksan tek bir şeye
dikkat: L3 ailesi ve L3 prompt taş glifleri taşıyor, yani
dosyanın UTF-8 olarak kaydedilmesi gerekiyor. Not Defteri'nde kaydetme penceresindeki
açılır listeden UTF-8 seçmek yeterli — BOM'lu olanı seçersen kodlama dosyanın
kendisinden okunur ve tarayıcının tahmin etmesi hiç gerekmez. L3 input,
L3 input_blindfold, L3 numerical ve üç .js tamamen
ASCII; onlarda hangi kodlamayla kaydettiğinin bir önemi yok.
Beşinde de ölçü aynı. Fark yuvarlamada: L3 kalan süreyi yukarı
yuvarlıyor, bu yüzden 0:00 ancak bayrak düştüğünde görünüyor; saniye
gösteren öbürleri aşağı kırpıyor. L3 input ailesinde iki saat ayrı
basıldığı için aynı yuvarlama 10 bayt tutuyordu.
Terfi, terk ve beraberlik
Bir hamle dışındaki her şeyin nasıl yapıldığı sürüme göre değişiyor. DOM'da
hepsi ayrı bir denetim; kalanlarda hepsi aynı metin alanından geçiyor.
Sürüm
Terfi
Terk
Beraberlik
L3
Panel açılır, taş seçilene kadar hamle tamamlanmaz
Resign düğmesi
Beraberlik radiosu
UCI üçlüsü
Hamlenin 5. karakteri: r b n, diğer her şey vezir
r, L3 prompt'ta ayrıca Cancel
Hamlenin sonuna =; tek başına = iddia veya kabul
L3 numerical
Hamlenin 5. karakteri: 0 fil, 1 kale, 2 at, diğer her şey vezir
Cancel, Esc, boş enter, sekmeyle çıkış
Sayısal olmayan karakter içeren her giriş
Göstergeler
Motor hiçbir sürümde durumu anlatmaz; ne gösterileceği tamamen sürücünün
tercihidir, ve bayt bütçesi arttıkça gösterge artar. Beş sürüm durumu yazıyla
bildiriyor, ikisi neredeyse hiçbir şey söylemiyor.
Sürüm
Koordinat
D?
C!
M=R=
Saat
Son hamle
L3
a8-h8 / h1-a1tahtanın yönü
—yerine radyoda sarı çerçeve
Var
Var
⏱ 10:00canlı · alt sıradaki
Yeşil çerçevevarış karesinde
L3 input
Yatay rakamı + dikey harfiyönle döner
Var
Var
Var
W: 600scanlı · alt sıradaki
—
L3 input_blindfold
—
—
—
—
W:600s B:600scanlı · sabit sıra
[e2e4]
L3 prompt
a8 / h1yalnız köşe karesi
Var
Var
Var
W: 600shamlede · alt sıradaki
—
L3 numerical
—
—
—
—
900000 900000hamlede · ham ms, etiketsiz
Ham girdi
L2
—
—
Var
Var
—
Yeşil çerçevevarış karesinde
L2_aybars_2400
—
—
Var
Var
—
Yeşil çerçevevarış karesinde
Gösterge taşıyan sürümlerde satır aynı sırayla kurulur. L3'un durum
satırı a8-h8 C! M=0 R=1 biçimindedir; L3 input ve
L3 prompt aynı alanları tahtanın üstüne yazar.
M= yarım hamle sayacı — FEN ve motor dilindeki adıyla halfmove clock. Taş alımında ve piyon hamlesinde
sıfırlanır. M=100'de 50 hamle beraberliği iddia
edilebilir, M=150'de iddia beklemeden ilan edilir. Pratik okuması
şu: ekranda M=99 görüyorsan, oynayacağın hamle taş almıyor ve piyon
değilse eşik onunla dolar — UCI'deki b2b3= tam olarak bunun için var.
R= tekrar sayısı. Mevcut pozisyonun kaçıncı kez görüldüğü.
R=3 3'lü tekrar iddiasını açar, R=5 otomatik beraberlik.
Geri sayım değil, her hamleden sonra yeniden hesaplanan bir değer: pozisyon
benzersizleştiğinde R=1'e düşer.
İki sayının da iki eşiği var, ve hangisinin işlediği sürüme bağlı.
L2'de beraberlik isteyecek bir düğme yok, dolayısıyla iddiaya
bağlı olan R=3 ile M=100 orada hiçbir şey tetiklemez;
oyun yalnızca kendiliğinden gelen R=5 ve M=150'de biter.
L2_aybars_2400'ün de oyuncu katmanı yok ama o hâlâ R=3 ve
M=100'de bitiyor, bilerek: botun araması bu iki eşiği kendi içinde de
okuyor, oyunu yukarı çekip aramayı olduğu yerde bırakmak ikisini birbirinden
koparırdı. Açık duran bir uç.
C! sırası gelen tarafın şahta olduğunu söyler, yani kendi
ekranında her zaman kendi şahını. D? ise açık bir beraberlik
teklifinin varlığı. L3 input ve L3 prompt'ta ikisi aynı yuvayı
paylaşır ve D? öne geçer; L3 onu hiç yazmaz — orada aynı bilgiyi
beraberlik radyosundaki sarı çerçeve bedavaya taşıyor.
Oyun bittiğinde L3 durum satırının tamamını sonuç koduyla değiştirir,
L3 input yalnızca bu yuvaya yazar — M= ve R=
yerinde kalır.
D? teklif bitlerinin sıfırdan farklı olması demektir, kimin
teklif ettiği değil. Normal akışta gördüğün teklif rakibinkidir, ama hamlesiz
= yazarsan kendi teklifini de D? olarak görürsün.
Ayrımı yalnızca L3 yapar, ve orada tek işaret de budur: beraberlik radyosu
sarı çerçeveyi rakibin açık teklifi varken alır.
Göstergesiz sürümlerde ne yapacaksın. İkisinde de şah, sayaçlar ve tekrar
tamamen sende; motor hiçbir şey hatırlatmaz. Geriye kalan tek kanal son hamle logu
ve saat, ve ikisi de sandığından fazlasını taşır.
L3 input_blindfold'da beraberlik eki loga giriyor: rakip e7e5=
oynadıysa köşeli parantezte [e7e5=] görürsün, yani teklif ya da iddia
ettiği oradan okunur. Terfi de öyle — e7e8n loga [e7e8N]
olarak düşer, gerçekten neye terfi ettiğini tahtayı kafanda tutmadan
doğrulayabilirsin. Sıra bilgisi verilmiyor; hangi saatin işlediğine bakarsın.
L3 numerical'da log ham girdinin kendisi, o yüzden rakibin beraberlik
isteği oradaki sayısal olmayan karakterlerden okunur: 1229asd hem
geçerli bir hamledir hem taleptir. Hamlesiz talep iz bırakmaz. Saat iki etiketsiz
milisaniye sayısı — soldaki beyaz, sağdaki siyah — ve hangisinin azaldığı sırayı
söyler. Saat yalnızca pencere açıldığında yenilendiği için, geçerli
ama oynanamaz bir sayısal giriş teklifi tetiklemeden ekranı tazelemenin tek
yoludur.
Notasyon
Hamle nasıl girilir
Tıklama — L3
Her şey tıklanarak yapılır. Taşa, sonra varış karesine basılır. Terfi karesine
ulaşıldığında panel açılır ve taş seçilene kadar hamle tamamlanmış olmaz; saat bu
sırada işlemeye devam eder. Taşı seçmezsen terfiyi iptal edip başka bir hamle
oynayabilirsin.
Beraberlik radiosu hamleden bağımsız çalışır. Tahtada eşik zaten dolmuşsa
işaretlemek beraberliği doğrudan tetikler; rakip teklif göndermişse işaretlemek
teklifi kabul eder; ikisi de yoksa işaret kalır ve hamleyi oynadığında değerlendirilir
— hamle eşiği sağlıyorsa iddia, sağlamıyorsa rakibe teklif olur.
UCI — L3 input, L3 input_blindfold, L3 prompt
Biçim <başlangıç><varış><terfi><beraberlik>.
Büyük küçük harf duyarsız: E2E4, e2e4 ve e2E4
aynı hamledir.
Terfi harfi r, b veya n. Bu üçü dışındaki
her harf — q dahil — ve harf yokluğu vezire terfi eder, yani
e7e8 yeter. Renk motorun kendi meselesi, sen yalnızca türü seçiyorsun.
Rok, şahın gideceği kareyle yazılır: beyaz için e1g1 ve
e1c1, siyah için e8g8 ve e8c8. Şahın kaleyi
aldığı biçim yok. Geçerken alma ise piyonun gerçekten varacağı kareyle yazılır, o
yüzden ayrı bir kural gerektirmez.
Beraberlik hamlenin sonuna gelen = ile. b2b3= demek:
bu hamleyi oynadığımda eşik dolacaksa iddia ediyorum, dolmayacaksa rakibe teklif
ediyorum. Motor kararı hamle oynandıktan sonraki duruma bakarak verir; hamle
sayacı sıfırlıyorsa ya da pozisyonu benzersizleştiriyorsa beraberlik kurulmaz.
Tek başına = ise mevcut durumu iddia eder veya bekleyen teklifi kabul eder.
Terk r. L3 prompt'ta pencereyi iptal etmek de terk sayılır; boş enter sayılmaz, geçersiz hamledir ve
saat işlemeye devam eder.
e7e8=N girişinde = beşinci karakter, yani terfi
yuvasında; r b n olmadığı için sonuç vezir olur. Beraberlik eki
dizginin sonunda aranır, orada N durduğu için beraberlik de istenmiş
olmaz. SAN alışkanlığı burada iki bakımdan birden ters çalışır. Ata terfi etmek için
e7e8n, ata terfi edip beraberlik istemek için e7e8n=.
Bir hamle mat ediyorsa sonundaki = yok sayılır. Mat, otomatik
beraberlik koşullarının hepsini geçtiği gibi iddiayı da geçer.
Sayısal — L3 numerical
Aynı dizilim, kareler iki basamaklı sayı. Numaralandırma bilerek 0–63 yerine
1–64: a1 = 01, h8 = 64.
Kare
UCI
Sayısal
Motor içi
a1
a1
01
0
h1
h1
08
7
a2
a2
09
8
e4
e4
29
28
a8
a8
57
56
h8
h8
64
63
Terfi girdisi rakam: 0 fil, 1 kale, 2 at,
3 ve diğer her şey vezir. Harf yok — bu sürümde hiçbir yerde harf yok.
Rok yine şahın kareleriyle: beyaz 0507 ve 0503, siyah
6163 ve 6159.
Beraberliğin ayrı bir düğmesi yok; kanal girdinin kendisine bağlı.
Sayısal olmayan her giriş bir beraberlik isteğidir. İstek o anda geçerli bir
iddiaya denk düşüyorsa — 50 hamle ya da üçlü tekrar — oyunu orada bitirir; düşmüyorsa
rakibe teklif olarak geçer. Ortaya dört durum çıkıyor:
Hamleyle birlikte istek.5664asd — hamle normal oynanır, isteği
de yanına alır.
Hamlesiz istek.asd, hatta tek karakterlik g —
tahtanın o anki durumu için doğrudan iddia, geçerli değilse teklif.
Bekleyen teklifin kabulü. Rakip de sayısal olmayan bir şey yazar; iki bit
birden yandığı anda oyun karşılıklı anlaşmayla biter.
Sayısal ama geçersiz giriş.6500 — ne iddia ne teklif; hamle
reddedilir, aynı soru yeniden sorulur.
İsteğin hamleye iliştirilebilmesi keyfi bir kolaylık değil, kitapçığın gereği.
“Sayaç 99 yarım hamlede ama bu hamleyi oynarsam 100 olacak” ya da “pozisyon iki kez
görüldü, bu hamle üçüncüsünü getirecek” durumlarında iddia oynanacak hamle
üzerinden kurulur. Kanalın hamleyle birlikte gidebilmesinin sebebi bu; davranış 9.2 ve
9.3'e birebir uyuyor.
Dördüncü maddenin bir yan faydası var. Bu sürümde saat canlı değil, yalnızca
prompt() penceresinin başlığında duruyor. Sayısal ama geçersiz bir giriş
hiçbir şey tetiklemeden pencereyi yeniden açtığı için, kalan süreyi görmenin tek yolu
da bu.
Terk ayrı bir kanal: İptal, Esc, boş enter ve sekmeden çıkmak terk eder.
0, 00 ve boşluk etmez — üçü de sayısal olarak sıfır, yani
yalnızca geçersiz hamle. r de terk değil; harf içerdiği için beraberlik
kanalına düşer.
Bu arayüzün rahatsız ediciliği kasıtlıdır. Sürüm bayt için yazıldı, konfor için
değil.
Sonuç kodları
On beş sonuç
Oyunun bitebileceği on beş yol var; altısı sonuçlu, dokuzu berabere. Motorun iki
kodlaması var ve ikisi aynı on beş sonucu adlandırıyor: L3 ve UCI
üçlüsü iki harfli kısaltmayı döndürür, L3 numerical ve
engine.js tam sayıyı. Her hücrede ikisi birlikte.
W#1
Beyaz mat etti
B#2
Siyah mat etti
SM3
Pat
WT10
Beyaz süreden kazandı
BT11
Siyah süreden kazandı
TM14
Süre × mat imkânsız
WR12
Beyaz, çekilmeden kazandı
BR13
Siyah, çekilmeden kazandı
RM15
Çekilme × mat imkânsız
754
75 hamle kuralı
5R5
5'li tekrar
DP6
Ölü pozisyon
3R8
3'lü tekrar iddiası
507
50 hamle iddiası
DA9
Anlaşmalı beraberlik
Beyaz kazanırSiyah kazanırBerabere
Kısaltmalar kendini okutuyor: W ile başlayanlarda beyaz,
B ile başlayanlarda siyah kazanmıştır. D ile başlayanlar,
M ile bitenler ve içinde rakam geçenler beraberedir. On beş kodun
on beşi de bu kalıplara giriyor; dışarıda kalan yok.
Satırlar da anlamlı: birinci satır mat ve pat, ikincisi süre bitimi, üçüncüsü
çekilme ile ilgili. Dördüncü satır kendiliğinden tetiklenen beraberlikler, beşincisi
oyuncunun istemesiyle gelenlerdir.
Bir koddaki isim tercihi ayrıca anlatmaya değer. DP maddenin kendi
adını taşıyor: 5.2.2 ölü pozisyonun hem yetersiz materyalden doğan hâlini hem de
kilitli pozisyonları kapsıyor, motor da ikisini birden veriyor — kilitli tarafta
ölçülen kapsama %93. Kodu dar olana göre adlandırmak kolay yoldu; maddeye göre
adlandırmak ileriyi açık bıraktı, ve kilitli pozisyonlar
eklendiğinde gerçekten aynı koddan çıktılar: liste on beş kaldı.
Bu on beş kod FIDE'ye göredir. Başka bir federasyonun kuralları başka bir liste
üretir. Örneğin USCF'de 75, 5R ve RM oyun
sonları yoktur, TM ise oldukça farklı çalışır. Motoru USCF'ye uyarlamak
mümkündür.
Motor
engine.js — arayüzsüz çekirdek
Tahta nasıl duruyor
Tahta 64 elemanlı düz bir dizi. Her eleman bir kare, değeri o karedeki taşın kodu.
İndeks a1'de sıfırdan başlıyor, h8'de 63'te bitiyor; i>>3 yatayı,
i%8 dikeyi veriyor.
Taş kodu tür*2+renk. Sıfırıncı bit renk — 1 beyaz, 0 siyah —
geri kalanı tür. Boş kare 0.
Tür
P
Kod · siyah
Kod · beyaz
Fil
1
2
3
Kale
2
4
5
Vezir
3
6
7
Piyon
4
8
9
Şah
5
10
11
At
6
12
13
Tür numaralandırması keyfi değil. Üst üçlü — piyon, şah, at — G'nin
P>3, P>4, P>5 merdivenini besliyor. Alt
üçlüde ise vezir, filin ve kalenin bit birleşimi: 3 = 1|2. Bu yüzden
çapraz sınama h==v&P fil ile veziri birlikte geçiriyor, düz sınama
P>1 kale ile veziri. Vezir için ayrı bir dal hiç yazılmamış.
Sınıflandırma kare merkezli: bilgi kareye asılı, taşa değil. Karşı kutup
taş merkezli temsiller — bitboard'da bilgi taş türüne göre ayrılmış bit kümelerine
dağılır, "bu karede ne var" sorusu pahalılaşır, küme işlemleri ucuzlar. Burada tersi:
kare sorgusu bir dizi erişimi, b[i], dört karakter.
Ve düz: kenar dolgusu yok, 0x88 yok, gözcü karesi yok. Dizi tam 64, fazlası
yok. Bunların hepsi denendi ve elendi; gerekçeler
ayrı bir bölümde.
Önyüzü olmayan hâli. Saf durum ve fonksiyonlar; DOM yok, bağımlılık yok, derleme
adımı yok. Tek satır, 1.935 bayt. Aşağıdaki kaynak builds/ dizinindeki
dosyanın kendisi, sayfa açılırken okunuyor.
4x anahtarı ikinci bir kaynağa geçiriyor: engine_4x.js, aynı
kuralları uygulayan hızlandırılmış varyant. 48 bayt daha uzun, mantıksal olarak
birebir aynı — her derinlikte aynı hamleler, aynı düğüm sayıları, aynı sonuç kodları.
Farkı nereden çıkardığı ve ölçümleri ayrı bir başlıkta.
perft anahtarı üçüncüsünü açıyor: engine_onlyMoveGenerator.js,
727 bayt. Motorun yalnızca hamle üreten yarısı — taş hareketleri, rok, geçerken
alma, terfi ve şah güvenliği. Geri kalan her şey yok; burada sıra bile durum değil, onu
sürücü taşıyor.
Bu dosya süitin tamamını geçiyor: CPW pozisyonları, van
Kervinck'in listesi, Vajolet külliyatı, Stockfish'e karşı hamle listesi karşılaştırması.
Sürpriz değil, asıl mesele bu — hiçbiri oyunun bitip bitmediğini sormuyor.
engine.js'in bu dosyanın ötesinde taşıdığı 1.219 bayt — sayaçlar, tekrar
tablosu, materyal testi, hüküm — o bir buçuk milyar düğümün hiçbirinde çağrılmıyor.
Testleri geçmek hamle üretiminin doğruluğunu kanıtlıyor, kural setinin tamamını değil;
geri kalanını kural metninin dikkatli okunması kapatıyor. Süiti bu seviyede koşmak için
ENGINE=./engine_onlyMoveGenerator.js yeterli.
Dosyanın tamamı otuz altı isimden ibaret: on beş durum değişkeni, on beş fonksiyon,
altı scratch. Geri kalan her harf fonksiyon parametresi, yani sürücünün yeniden
kullanmasına açık. Hepsi aşağıda.
İsimlendirme rastgele değil. Çoğu ad İngilizce bir kelimenin baş harfi —
A apply, G geometry, V vulnerable,
M move, d distance, i initial, f final —
ve sayfanın İngilizce sürümünde her harfin hangi kelimeden geldiği tablolarda kalın olarak
işaretli. Birkaçı başka bir mantığa uyuyor: n saymanın harfi, k
vektörlerde geleneksel olarak kullanılan harf, q ile Q ℚ'dan
geliyor, x ile y koordinat, z ile Z ise
oyunun sonunu taşıdıkları için alfabenin sonunda duruyor. O aradığı sıfıra
benziyor, $ de bir hash'e.
Büyük-küçük harf kendi başına bir düzen taşıyor: fonksiyonlar genelde büyük harfi, durum
değişkenleriyle parametreler küçüğü alıyor. Kural değil eğilim — l ve
a küçük harfli birer fonksiyon, R büyük harfli bir tablo — ama
çıplak bir harf çoğu zaman hangi cinsten bir adla karşı karşıya olduğunuzu söylüyor. Bir
harfin iki hâli birden kullanımdaysa, çoğu kez aynı fikrin iki kademesini adlandırıyorlar:
p taş kodu ve P onun türü, l şah testi ve
L legal liste, c rok hakları ve C onları kırpan
fonksiyon, z saklanan sonuç ve Z onu üreten fonksiyon.
engine.js—
Sürücü yazmak için altı çağrı
Bir sürücünün satranç bilmesi gerekmiyor. Tıklamayı ya da metin girişini
L ile doğrula, A ile işle, ekrana bas. Kalan dördü oyunun
tahta dışında bitebildiği hâller için.
L(kare)
Kare bir 0–63 indeksi. O karedeki taşın legal varış karelerinin dizisi — şahı açıkta bırakan hamleler zaten elenmiş hâlde. Boş dizi: o taş oynayamıyor.
A(başlangıç, varış, terfi)
Hamleyi gerçekten oynar: rok haklarını günceller, sırayı çevirir, tekrar tablosuna yazar ve sonuç kodunu döndürür. Terfi türü 1 fil, 2 kale, 3 vezir, 6 at; verilmezse vezir.
Z()
Yalnızca pozisyondan doğan sonuç: mat, pat, ölü pozisyon, 5'li tekrar, 75 hamle. Oyun sürerken 0. A zaten dönüşünde bunu çağırır; ayrıca çağırmak gerekmez.
D(taraf)
O taraf beraberlik teklif eder ya da bekleyen teklifi kabul eder; iddiaya bağlı beraberlikleri de burada döner. Argümansız D() teklif bitlerine dokunmadan yalnızca değerlendirir.
F(taraf, tür)
Tür 0: o tarafın bayrağı düştü. Tür 1: o taraf çekildi. Rakibin mat edecek materyali yoksa sonucu berabereye çevirir — FIDE 6.9.
l(taraf)
O taraf şahta mı? Yalnızca gösterge için; legallik L'nin işi.
Taraf her yerde taş kodunun renk biti. Saat sürücünün işi —
motor saymaz, yalnızca U ve N alanlarını taşır; her vuruşta
azalt, sıfıra ulaşınca F(taraf,0) çağır.
Üç şeyi daha bilmek gerekiyor. Motor kendini kuruyor: dosya yüklendiği anda tahta,
sıra, rok hakları ve sayaçlar başlangıç pozisyonunda; ayrı bir kurulum çağrısı yok.
Geri alma yok:A tek yönlü. L kendi legallik denemesi
için anlık kopya alıp geri koyuyor ama bunu dışarıya açmıyor; geri alma isteyen sürücü
b, e, n, t, c,
R ve $'ı kendisi yedekler — R unutulursa
sayaç geri döner ama tablodaki kayıt kalır, sonraki hamle yanlış
$ üretir. FEN ve PGN yok: pozisyon kurmak isteyen
b dizisine doğrudan yazar.
Durum değişkenleri
Sürücünün ekrana basmak için okuyacağı her şey burada. Yazması gereken tek şey
saatler.
Ad
Ne tutar
Ayrıntı
b
Tahta
64 elemanlı sayı dizisi — temsil yukarıda. Sürücü doğrudan okuyabilir; yazması gerekmez.
t
Sıra
1 beyaz, 0 siyah. A her hamlede çevirir.
z
Oyun sonucu
Sonuç kodu; oyun sürerken 0. Motor kendi yazmaz — sürücü A, D ya da F'nin dönüşünü buraya koyar.
n
Yarım hamle sayacı
Taş alımında ve piyon hamlesinde sıfırlanır. n>99 50 hamle iddia edilebilir, n>149 otomatik.
$
Tekrar sayısı
Mevcut pozisyon kaçıncı kez görüldü. >2 3'lü tekrar iddia edilebilir, >4 otomatik.
o
Teklif bitleri
Bit 0 beyazın, bit 1 siyahın açık teklifi. İkisi de yanınca beraberlik anlaşması doğar.
c
Rok hakları
Dört bitlik maske: 1 beyaz kısa, 2 beyaz uzun, 4 siyah kısa, 8 siyah uzun.
e
Geçerken alma karesi
Yoksa -1. Yalnızca komşu piyon gerçekten alabiliyorsa yazılır — tekrar anahtarının doğruluğu buna bağlı.
UN
Beyaz ve siyah saat
Saniye — ama dört sürümde değil: L3 ile L3 input ailesinde U iki elemanlı bir dizi, artı ilk yoklamada doğan U[2] zaman damgası ve N saat bile değil — L3 ailesinde 'innerHTML' takma adı, yani durum değil yardımcı; L3 input ailesinde hiç okunmuyor. L3 numerical'da ikisi de milisaniye. Ayrıntısı burada. Motor okumaz, azaltmaz; tamamen sürücünün alanı.
R
Tekrar tablosu
Pozisyon anahtarı → kaç kez görüldüğü. Anahtar b+t+e+c.
Xm
Arama bütçesi
6.9 aramasının deneyebileceği hamle sayısı. F her çağrıda 2e4'e kuruyor, X her denemede bir düşürüyor. Sıfıra inince arama kesiliyor ve mat mümkün diyor — kanıtlanamayan imkânsızlık beraberlik saymıyor.
Xs
Arama tablosu
Aranmış pozisyon → hangi derinliğe kadar arandığı; satranç programlamada bunun adı transposition table. Boolean değil derinlik tutuyor; böylece sığ bir sonuç derin bir sorguyu yanlışlıkla karşılamıyor. Anahtar b+t+e.
Y
Yük taşımayan harf
e=Y=-1 ile başlatılıyor ve bir daha hiçbir yerde okunmuyor ya da yazılmıyor. Bildirim zincirindeki yeri tamamen imza amaçlı. Aynı harf J'nin parametresi olarak da geçiyor, ama o globali gölgeleyen ayrı bir bağlayıcı: Z1, 6.9 araması 0 geçiyor.
Q
'indexOf' takma adı
Üç arama çağrısında geçiyor: l ile H içinde b[Q](…), M içinde L(x)[Q](…) — sonuncusu tahtada değil, L'nin dönüşünde arıyor. Aynı harf G ile Z'de parametre adı olarak da geçiyor; orada genel değer gölgeleniyor ve hiç okunmuyor. Satrançla ilgisi olmayan iki kısaltmadan biri; ikizi a fonksiyonlar tablosunda, kaynakta ikisi yan yana duruyor.
Fonksiyonlar
Altısını yukarıda gördünüz. Kalan dokuzu iç mekanizma; sürücünün çağırması
gerekmez ama motoru okumak için gerekli.
Ad
İmza
İşi
G
G(i, f, T)
Geometri. i'deki taş f'ye gidebilir mi — şah güvenliğine bakmadan. T tehdit modu: açıkken piyonun yalnızca alma hamlelerini sayar.
V
V(u, s)
u karesi s'nin rakibi tarafından tehdit ediliyor mu.
l
l(g)
Şah tehdit altında mı — V'yi şahın karesiyle çağırır.
L
L(i)
Legal varış listesi. Her adayı M ile oynayıp l ile bakar, sonra geri alır.
M
M(i, f, u)
Ham hamle. Taşı taşır, rok kalesini oynatır, geçerken alınanı siler, terfi eder. Rok hakkına, sıraya ve tekrar tablosuna dokunmaz — çünkü L'nin deneme döngüsü içinde de koşar.
C
C(i)
Kare → hangi rok hakkını düşürdüğü. Şah ve kale kareleri dışında 0.
A
A(i, f, u)
Gerçek hamle: C ile hakları kırpar, M'yi çağırır, sırayı çevirir, R'ye yazar, Z döndürür.
I
I(g)
Yetersiz materyal: 5.2.2'nin materyal yarısı, kilitli yarısını J taşıyor. g>1 ile çağrılırsa iki tarafa birden bakar; H de onu bu yüzden iki kez çağırıyor.
H
H(g)
FIDE 6.9'un materyal yarısı: g bayrağı düşen ya da çekilen taraf; rakibinin elindeki taşlarla mat kurması imkânsız mı. X'in ilk kapısı — doğru dönerse arama hiç başlamaz.
X
X(g, d)
6.9'un aramalı yarısı. g yardım etse bile rakibi d yarım hamle içinde bir mat pozisyonu kurabiliyor mu; doğru dönüş kuramıyor demek. Zorlanabilir mat değil yardımlı mat arar, çünkü madde de onu sorar. Mat testini ayrı bir dal olarak yazmaz — üçüncü parametresinin varsayılanına katlar.
J
J(Y)
Ölü pozisyon, 5.2.2'nin kilitli yarısı. Sekiz bitboard maskesiyle piyonların ve şahların ulaşabildiği kareleri doyana kadar büyütür; hiçbiri büyümüyorsa pozisyon ölüdür. Kapsamı şah ve piyon — tahtada başka taş varsa hiç koşmaz.
Z
Z()
Pozisyon sonucu. Önce legal hamle var mı diye bakar; yoksa mat mı pat mı ayırır. Varsa I ile J'ye danışıp ölü pozisyonu, ardından tekrar ve hamle sayaçlarını yoklar.
D
D(g)
Beraberlik teklifi, kabulü ve iddiası.
F
F(g, k)
Süre ve terk. Xm ile Xs'i kurup X'e danışır, sonucu ona göre yazar.
a
a
Math.abs takma adı. Satrançla ilgisi olmayan ikinci kısaltma; ikizi Q durum tablosunda. Paketlenmiş sürümde açılır, orada yoktur.
Geçici değerler ve parametreler
Altı harf global scratch — bir fonksiyonun içinde doğar, çağrı bitince değeri
anlamsızdır: m fil kare-rengi maskesi, W materyal ağırlığı,
BH'nin ikinci I çağrısı boyunca sakladığı
g fil renkleri, r rok
kalesinin karesi, OH'nin yokluk testi, sA'nın kurduğu pozisyon anahtarı.
Kalan on altı harfin hepsi fonksiyon-lokaldir; sürücü aynı adları global olarak
güvenle kullanabilir. Nerede sütunu, o harfin engine.js'te hangi
fonksiyonların içinde geçtiğini gösteriyor.
Ad
Nerede
Rol
i
G V L C M I Z A
Başlangıç karesi, ya da tahtayı tarayan döngünün indeksi.
f
G L M A
Hedef kare.
p
V L M I Z
Taş kodu — b üzerinden okunan ham değer.
P
G M
Taş türü (p>>1). M'de doğrudan "piyon mu" testine dönüşür.
g
G l L I H D F
Taraf: 1 beyaz, 0 siyah. Taş kodunun renk bitiyle aynı.
q
L M
L'de biriken varış listesi; M'de iki karenin ortası — rok kalesinin ve geçerken alma karesinin hesabı.
u
V M H A
M ve A'da terfi türü; V'de tehdit altında olup olmadığı sorulan kare; H'de O'nun yokluğunu sorduğu taş kodu.
T
G
Tehdit modu bayrağı. Açıkken piyonun yalnızca çapraz hamleleri sayılır.
k
G F
G'de yön vektörü — bir adımda indeksin ne kadar değiştiği. F'de sonlanma sebebi: 0 süre, 1 terk.
d
G M
G'de kaç adım atılacağı; M'de indeks mesafesi — rok (2) ve iki adımlık piyon (16) bununla tanınır.
h
G
Yatay mesafe, sütun farkının mutlak değeri.
v
G
Dikey mesafe, satır farkının mutlak değeri.
y
G
Başlangıç karesinin satırı (i>>3).
S
G
Işın tarayıcı. Kayan taşın yolunu adım adım yürüyüp araya taş girip girmediğine bakar; kendini çağırır.
x
M
Komşu piyon karesi — geçerken alma hakkının gerçekten doğup doğmadığı burada sınanır.
Q
G Z
G içinde ışın tarayıcının parametresi; Z'de hiç kullanılmayan parametre. İkisi de aynı adlı genel takma adı gölgeliyor — ne S ne Z onu okuduğu için harf iki işi birden görüyor.
engine.js'te identifier olarak hiç doğmayan harfler: Ej_. Yedi dosyanın tamamı aşağıda.
Akış
engine.js — satır satır ne oluyor
Harflerin ne anlama geldiği yukarıdaki tablolarda. Burada anlatılan, o harflerin
hangi sırayla ne yaptığı: hangi ifade hangi kuralı uyguluyor, hangi operatör
neyi neye çeviriyor, bir hamle tahtaya yazılırken hangi fonksiyonlar hangi sırayla
koşuyor.
Dosyanın tamamı yirmi üç üst düzey bildirim: yedisi başlangıç durumu, biri takma
ad — Q, bir dizgi — on beşi fonksiyon; on beşincisi a, o da
Math.abs'ın takma adı. Yukarıdaki tablonun saydığı on beşin aynısı.
Sıraları keyfî değil — her fonksiyon neredeyse yalnızca kendinden öncekilere
yaslanıyor. Üç yerde ileri bakıyor: G hem V'ye hem
C'ye, L de M'ye. İkisi gerçek karşılıklı
özyineleme, üçüncüsü yalnızca sabit bir tabloya erişim; ayrıntısı hemen aşağıda.
Kat haritası
Motor beş kat. En altta saf geometri: bir taş şu kareden şu kareye
gidebilir mi — hedefte ne olduğunu ve şahın açıkta kalıp kalmadığını hiç sormadan.
Üstünde tehdit: bir kare dövülüyor mu. Sonra legallik: hamleyi
gerçekten yapıp şahı sınayan, sonra tahtayı geri alan kat. Sonra hüküm:
pozisyon bitti mi, bittiyse nasıl. En üstte işlem: hamleyi kalıcı hâle
getiren, hakları düşüren, sayaçları yürüten ve sonucu döndüren üç fonksiyon. Sürücü
yalnızca bu son kata dokunur.
Düz oklar kat sırası: her kat yalnızca kendinden aşağıdakini çağırır.
Kesikli altın oklar iki istisna — ikisi de satranç kuralının kendisinden doğuyor.
Bağımlılık neredeyse tamamen tek yönlü. İki yerde kırılıyor:
G → V. Geometri katı rokta bir üst kata
uzanıyor: şahın geçtiği karenin dövülüp dövülmediğini sormadan rokun
geçerliliğine karar veremez. Bu yüzden G ile V karşılıklı
özyinelemeli — V her taş için G'yi çağırırken
G de rok dalında V'yi çağırıyor.
M → L. Hamleyi tahtaya yazan M,
geçerken alma karesini kurarken hamle üretimine geri dönüyor: kareyi ancak komşu
piyon o almayı gerçekten legal olarak yapabiliyorsa yazıyor. Tekrar
anahtarının doğruluğu buna bağlı.
Üçüncü bir özyineleme daha var ama katı aşmıyor: G, rok dalında
kalenin yolunun boş olup olmadığını sınamak için kendini çağırıyor.
Üç adım. Şablon literali 64 karakterlik bir dizgi üretiyor; ortadaki 40
karakter kaynakta yazılı değil, 10n**40n-10n**32n ile çalışma anında
doğuyor. Çıkarma aslında 10n**32n * (10n**8n - 1n), yani sekiz adet 9 ve
arkasından otuz iki adet 0 — tam 40 basamak. BigInt olması şart: aynı sayı sıradan
bir Number ile tutulsaydı çoktan kayan noktaya düşmüş ve basamakları
bozulmuş olurdu.
Yayma operatörü dizgiyi 64 tek karaktere bölüyor, ve
map her karakteri sayıya çeviriyor. Buradaki oyun öncelikte:
+ ile - aynı öncelikte ve soldan sağa işliyor, yani önce
'0x'+u birleşiyor — 'd' için '0xd' — sonra
-0 o dizgiyi sayıya zorluyor ve JavaScript 0x önekini
onaltılık okuyup 13 veriyor. +0 yazılsaydı birleştirme sürer ve
ortaya '0xd0' çıkardı; çevirmenin bu kadar kısa başka yolu yok.
Ortaya çıkan dizide indeks 0 a1, indeks 63 h8.
Kalan başlangıç durumu yedi bildirim: c=15 ile dört rok hakkının
hepsi açık, e=-1 ile geçerken alma karesi yok, t=1 ile sıra
beyazda, z=n=o=0 ile sonuç, 50 hamle sayacı ve teklif bitleri sıfır.
U=N=600 saatleri kuruyor ama motor bunlara hiç dokunmuyor; sürücünün
malı. Y ise bir daha hiç okunmuyor — yalnızca imzayı tamamlamak için
orada. a=Math.abs üç kullanımıyla 10 bayt kazandırıyor; Q='indexOf'
aynı işi dizi aramasında görüyor, yine üç kullanım, 3 bayt. İkisi de satrançtan bağımsız
takma adlar — başlangıç durumunun parçası değiller.
Son bildirim tekrar tablosunu başlangıç pozisyonu bir kez görülmüş hâlde
kuruyor:
R={[b+t+e+c]:$=1}
Anahtar dört parçadan: dizinin kendisi metne çevrilip — altmış dört sayı,
virgülle ayrılmış — arkasına sıra, geçerken alma karesi ve rok maskesi
ayraçsız ekleniyor. Ayraç olmaması ilk bakışta çakışma riski gibi duruyor,
ama riski kapatan şey satranç kuralının kendisi: e ya -1
ya da 16–23 / 40–47 aralığında bir kare, yani her zaman iki karakter. Sıra tek
karakter olduğuna göre anahtar daima "bir karakter sıra, iki karakter kare, kalanı
maske" diye okunur. Geçerken alma karesinin ancak üçüncü ya da altıncı yatayda
doğabilmesi, bu ayrıştırmayı garantileyen tek şey.
Geometri — G
G(i,f,T) tek bir soruya cevap verir: i karesindeki taş
f karesine tahtanın geometrisi açısından gidebilir mi? Hedefte
kendi taşının durup durmadığına bakmaz, hamlenin şahı açıkta bırakıp bırakmadığına
da bakmaz. İkisi de üst katların işi. Motorun en uzun fonksiyonu: 270 bayt, yani
dosyanın neredeyse dörtte biri.
Üçüncü argüman Ttehdit kipi. Açıkken üç şey değişir: rok
kapanır (rok bir tehdit değildir), piyonun çapraz karesi üzerinde taş olmasa bile
sayılır (piyon o kareyi döver), ve piyonun düz itişi kapanır (itiş kimseyi
tehdit etmez). Yani G(i,u,1) "şu taş şu kareyi dövüyor mu" demek.
Parametre listesi bir hesap tablosu
Varsayılan parametreler gövdeden önce ve soldan sağa değerlendirildiği için
G'nin başlığı, anahtar kelime harcamadan bir let bloğu
işi görüyor. Gövde çalışmaya başladığında geometrinin tamamı çıkarılmış oluyor:
P taş türü, g rengi, y çıkış yatayı,
h ile v yatay ve dikey mesafenin mutlak değeri. İlginç
olan üçü:
d=h|v. Toplama değil, veya. Anlamı olan bütün
geometrilerde ikisinden biri sıfırdır (düz hamle) ya da ikisi eşittir (çapraz);
her iki durumda da bit düzeyinde veya, büyük olanı verir. Yani d
hamlenin kaç kare sürdüğü.
k=(f-i)/d. Toplam yer değiştirmeyi kare sayısına bölmek, tek
karelik adım vektörünü veriyor: yatayda ±1, dikeyde
±8, çaprazlarda ±7 ve ±9. Yön tablosu yok,
yön dizisi yok — bir bölme.
S. Işın yürüyücüsü. i'yi k kadar
ilerletir; f'e vardıysa doğru, varmadıysa o karenin boş olması
(b[i]<1) şartıyla kendini çağırır. Aradaki bütün karelerin boş
olması bu tek satırla sınanıyor.
At için d ve k anlamsız çıkar — 1|2 üçtür
ve üçe bölmek saçma bir vektör verir — ama at dalı ikisini de hiç okumaz. Aynı şey,
geometrisi hiçbir taşa uymayan kare çiftleri için de geçerli: S kesirli ya da
tahta dışı bir indekse düştüğünde b[i]undefined olur,
undefined<1 yanlıştır ve yürüyüş anında durur. Sonsuz döngü
yazılmadan engellenmiş değil, mümkün olmadığı için yok.
Dört dal, altı taş
Gövde tek bir üçlü zincir. Sıralama tür numarasına göre büyükten küçüğe iniyor,
ve zincirin son dalı üç taşı birden topluyor.
At.h*v==2 — mesafelerin çarpımı iki ise hamle bir L'dir,
çünkü ikiyi veren tek tam sayı çifti 1×2 ve 2×1. Sekiz yönlü bir tablo yerine tek
çarpma.
Şah. İki dal. Birincisi d<2: her yöne bir kare. İkincisi
rok, ve rokun bütün şartları tek zincirde. T|v|h^2 üç şartı tek ifadede topluyor: tehdit kipindeysek,
yatay değiştiyse ya da h ikiden farklıysa rok yok — o zaman sıradan şah
hamlesi d<2 işler. Sonra kale karesi hesaplanıyor:
r = i + 3.5*k - .5 k=+1 → i+3 (kısa rok, h1)k=-1 → i-4 (uzun rok, a1)
Kale kısa tarafta üç kare ötede, uzun tarafta dört kare ötede duruyor —
simetrik olmayan bu farkı üçlü operatör yerine 3,5 ve 0,5 kapatıyor. Ardından
c&C(r) o kalenin hakkının hâlâ maskede olduğunu, G(r,i)
kalenin şahın karesine kadar boş bir yol bulduğunu — özyineleme, ve uzun rokta
b1'in de boş olması bu sayede sınanıyor — !V(i,g) şahın şu
an şahta olmadığını, !V(i+k,g) ise geçtiği karenin dövülmediğini
sınıyor. Şahın vardığı kare burada yok, çünkü onu zaten
L'nin legallik süzgeci yakalıyor: hamleyi yapıp şahı sorduğunda
dövülen bir varış karesi kendiliğinden eleniyor.
Piyon. Önce yön: f<i^g. <, ^'dan sıkı
bağladığı için ifade (f<i)^g diye ayrışır — beyaz (g=1) indeksi
büyütmek zorunda, yani f<i yanlış olmalı ve XOR bir verir; siyahta tersi. Sonra iki dal, ayıracı h: dikey değiştiyse
alma, değişmediyse itiş.
Alma.h*v==1 tam bir çapraz adım demek. Ardından
T|b[f]|f==e üç kabulden birini arıyor: tehdit kipindeyiz, hedefte bir
taş var, ya da hedef geçerken alma karesi. Üçünün veya ile bağlanması,
geçerken almayı ayrı bir dal açmadan sıradan bir almaya çeviriyor.
İtiş.T|b[f] iki şartı birden eliyor: tehdit kipindeysek ya da hedefte taş
varsa itiş yok. Sonra ya tek kare (v<2) ya da çift kare:
v<3 mesafe, !b[i+f>>1] aradaki karenin boşluğu —
iki karenin ortası her zaman indekslerinin ortalaması — ve y%5==1
başlangıç yatayı. Beşe bölümünden kalan bir olan iki yatay vardır: 1 ve 6,
yani beyazın ve siyahın piyon sıraları. Tek bir modülo iki rengi birden
kapsıyor.
Kayan taşlar. Zincirin son dalı fil, kale ve veziri birlikte ele alıyor,
ve bunu tür numarasını bir yetenek bit alanı gibi okuyarak yapıyor. Fil 1,
kale 2, vezir 3 — yani ikilik tabanda 01, 10,
11. Sıfırıncı bit "çapraz gidebilir", birinci bit "düz gidebilir"
demek.
h*v ? h==v&Pçapraz denemesi: eşit mesafe, ve P'nin 0. biti
: P>1düz deneme: P'nin 1. bitini okumanın en kısa hâli
h==v&P ifadesinde karşılaştırma &'den önce
işlediği için sonuç (h==v)&P olur: fil ve vezir geçer, kale
1&2=0 ile düşer. Düz denemede P>1 kaleyi ve veziri
alır, fili bırakır. İki taşın hangi yöne gidebildiği hiçbir yerde yazılı değil —
numaralarının kendisinde duruyor.
Dalın sonundaki & S() yolu açtırıyor. Geometri uygun değilse sol
taraf sıfır olur ve çarpımın sonucu S() ne dönerse dönsün sıfır kalır;
hüküm değişmez.
Tehdit — V ve l
Geometri katı tek bir taşı biliyor. Tehdit katı bütün tahtaya soruyor.
V(u,s) şunu sorar: u karesi, s'in
karşısındaki taraf tarafından dövülüyor mu? Süzgeç p%2^s —
taşın renk biti ile s'in özel veyası, yani renkler farklıysa bir.
Ardından G(i,u,1), tehdit kipiyle. some ilk saldırganı
bulduğu anda duruyor; tam bir tarama yalnızca kare gerçekten güvenliyse
yapılıyor.
Boş kareler süzgeçten sızar — 0%2 sıfırdır ve s=1 iken
özel veya bir verir. Zararsız oluşunun sebebi bir kat aşağıda: boş karede
P sıfır olduğu için G son dala düşer,
h*v?h==v&0:0>1 her koşulda sıfırdır ve sonuç değişmez.
l(g) ise tek satırlık bir sarmalayıcı: şahı b[Q](10+g)
ile buluyor — tür 5, yani kod 10+renk — ve o kareyi V'ye
soruyor. Şahın yeri hiçbir yerde saklanmıyor; her çağrıda yeniden aranıyor. Bir
önbellek her hamlede güncellenmek zorunda kalırdı, ve o güncelleme
L'nin geri aldığı hamlelerde de doğru olmak zorunda kalırdı. Aramak
daha ucuz.
Legallik — L ve M
L — üret, dene, geri al
Geometri bir hamlenin şekline bakar, legallik sonucuna. Aradaki fark tek bir
kural: kendi şahını açıkta bırakan hamle oynanamaz. Bunu statik olarak sınamanın
kısa bir yolu yok, o yüzden L hamleyi gerçekten yapar, şahı
sorar ve tahtayı geri alır.
L=(i,q=[],g=b[i]&1)=>(b.map((p,f)=>
süzgeç !p|p&1^g && G(i,f) && (
saklap=[b,e,n],
klonla b=[...b],
oyna M(i,f),
sına l(g)||q.push(f),
geri al [b,e,n]=p)),q)
Biriktirici q varsayılan parametre olarak doğuyor, yani bir
let daha harcanmıyor. Süzgeç !p|p&1^g hedefin boş
olmasını ya da renginin farklı olmasını istiyor — &,
^'dan, o da |'dan önce işlediği için ifade
(!p)|((p&1)^g) diye ayrışıyor. Bu, geometri hiç çalışmadan kendi
taşının üzerine gitmeyi ve f'in i'ye eşit olduğu durumu
birlikte eliyor.
Asıl incelik yedekte. Durum [b,e,n] üçlüsü olarak
p parametresinin üzerine yazılıyor — az önce hedef karenin
içeriğini tutan, işi bitmiş olan harfin üzerine. Global bir yedek burada bozulurdu,
çünkü M geçerken alma karesini kurarken L'yi geri
çağırıyor ve o iç çağrı aynı globale yazardı. Yedeğin çağrı yığınında,
parametrenin içinde durması bir zarafet değil, zorunluluk.
Tahta yayılarak klonlanıyor; e ve n sayı oldukları için
değerleriyle saklanmaları yetiyor. M(i,f) terfi türünü belirtmeden
çağrılıyor, yani varsayılan vezir kullanılıyor — legallik açısından fark etmez,
çünkü şahın açıkta kalıp kalmadığı varış karesinin dolu olmasına bağlı,
üzerindeki taşın türüne değil.
M — beş adım, sırası değişmez
M hamleyi tahtaya yazar ve başka hiçbir şey yapmaz: sırayı
çevirmez, rok hakkına dokunmaz, tekrar tablosuna yazmaz. Sebebi
L'nin onu deneme amaçlı çağırması — geri alınacak bir hamlenin kalıcı
iz bırakmaması gerekiyor. Kalıcı olan her şey bir kat üstte, A'da.
50 hamle sayacı.P taşın piyon olduğunu,
b[f] hedefte bir taş bulunduğunu söyler; ikisinden biri doğruysa sayaç
sıfırlanır, değilse artar. Kuralın tamamı bir üçlü operatör.
Varış karesi ve terfi.f%56<8 son yatay testidir: 0–7 ve
56–63 kareleri bu koşulu sağlar, aradaki her şey sağlamaz. Piyon oraya vardıysa
seçilen tür yazılır — u*2+p%2, yani tür ve taşın kendi rengi;
seçim rengi değiştiremez.
Çıkış karesi ve geçerken alınan piyon. Zincirleme atama sağdan sola
işler: önce b[X]=0, sonra b[i]=0. Sıradan hamlede
X zaten i'dir ve aynı kare iki kez sıfırlanır. Geçerken
almada ise Xf^8 olur, ve bu ifade yönü kendiliğinden
buluyor: geçerken alma karesi ya 16–23 ya da 40–47 aralığındadır, birincisinde
üçüncü bit sıfırdır ve özel veya bir yatay ileri gider, ikincisinde bir olduğu için
bir yatay geri gider. Alınan piyon her iki durumda da doğru karede.
Rok kalesi.d==2&p>9 ikilisi rokun tek imzasıdır:
p>9 taşın şah ya da at olduğunu söyler, ama atın hiçbir hamlesinde
indeks farkı iki değildir — at yalnızca 6, 10, 15 ve 17 kadar yer değiştirir.
Kalenin varış karesi q, yani şahın çıkış ve varış karelerinin ortası.
Çıkış karesi burada üçlü operatörle bulunuyor; G'de aynı hesap
i+3.5*k-.5 ile yazılıydı. İkisi de 11 karakter — fark bayt değil,
kapsam: M'de adım vektörü k hesaplanmıyor.
Geçerken alma karesi. Şart d>9, çünkü piyonun diğer bütün
hamlelerinde fark 7, 8 ya da 9’dur; 16 yalnızca çift adımda çıkar. Kare önce
e=q ile geçici olarak yazılır — ve bu atamanın testten önce
olması zorunludur, çünkü aşağıdaki L(x) çağrısı içinde
G'nin piyon dalı tam olarak f==e karşılaştırmasını yapar.
Sonra varış karesinin iki komşusuna bakılır: 17-p renk çevirmesidir
(beyaz piyon 9 → siyah piyon 8, ve tersi), ve ~L(x)[Q](e) o
piyonun almayı gerçekten legal olarak yapabildiğini sorar. Yapamıyorsa kare
-1'e döner.
Beşinci adımın bu kadar titiz olmasının sebebi tekrar tablosu. Anahtar
e'yi içerdiği için, kimsenin kullanamayacağı bir geçerken alma karesi
yazılsaydı aynı pozisyon farklı iki anahtara düşer ve üç kat tekrar iddiası sessizce
kaçırılırdı. FIDE'nin "aynı pozisyon" tanımı hamle imkânlarını da kapsar; bu satır
o tanımın harfi harfine karşılığı.
Komşu araması f-1 ve f+1 ile yapıldığı için dikey
kenarlarda tahtanın öbür ucuna taşabilir. Zararsız: taşan kare a ya da h dikeyinin
karşı ucundadır, ve L(x) oradan e'ye giden bir hamle asla
üretmez — G'nin piyon dalı yedi karelik bir yatay farkı çapraz alma
saymaz.
Rok hakkı — C
Dört rok hakkı c'de dört bit. Hangi karenin hangi biti düşürdüğünü
söyleyen fonksiyon 27 bayt:
C=i=>'20003001'[i%56]<<i/28
İki adım. i%56 birinci ve sekizinci yatayları aynı sekiz indekse
katlıyor: a1 ve a8 sıfıra, e1 ve e8 dörde, h1 ve h8 yediye. Aradaki 48 kare
8–55 aralığına düşüyor, yani 8 karakterlik dizginin dışına — orada
undefined okunuyor. Dizginin kendisi bir tablo: sıfırıncı karakter
'2', dördüncü '3', yedinci '1', kalan beşi
'0'.
Sonra <<i/28. Kaydırma operatörü iki tarafını da tam sayıya
zorluyor: '2' ikiye, '0' sıfıra, undefined sıfıra
dönüyor; sağ taraf ise birinci yatayda 0, sekizinci yatayda 2 çıkıyor. Sonuç dört
bit:
Kare
i%56
Karakter
Kaydırma
Bit
Hak
h1
7
1
0
1
Beyaz kısa
a1
0
2
0
2
Beyaz uzun
h8
7
1
2
4
Siyah kısa
a8
0
2
2
8
Siyah uzun
e1
4
3
0
3
Beyazın ikisi
e8
4
3
2
12
Siyahın ikisi
diğer 58
—
'0' / yok
—
0
—
Şah karesinin tek seferde iki bit düşürmesi, dizgideki '3''ün
tamamı. Ve bütün hak düşürme işi A'da tek satır:
c &= ~C(i) & ~C(f)
Çıkış karesi şahın ya da kalenin oynadığını, varış karesi ise bir kalenin kendi
köşesinde alındığını anlatıyor. İki ayrı kural, tek ifade. İlgisiz kareler
sıfır döndürdüğü için tersleri -1 olur ve maskeyi hiç değiştirmez —
koşul yazmaya gerek kalmıyor.
Materyal — I ve H
I(g) tahtayı bir kez tarayıp iki scratch dolduruyor ve tek bir soruya
cevap veriyor: bu materyalle mat kurulabilir mi?
I=g=>(m=W=0,b.map((p,i)=>p&&g>1|p%2==g&&(
p<4 ? m|=(i/8^i)%2+1fil: kare rengini maskeye yaz
: W+=p<10?9:p>11)), ağır 9, at 1, şah 0W*2+m<3)
g>1 ile çağrıldığında renk süzgeci düşer ve iki taraf birden
sayılır; Z onu böyle kullanıyor. Filler ayrı tutuluyor çünkü sayıları
değil kare renkleri önemli: (i/8^i)%2 yatay ile indeksin özel
veyasının son bitini alıyor, yani karenin rengini; +1 ile bu 1 ya da 2
oluyor ve m'ye veya'lanıyor. Aynı renkteki beş fil de tek bit yakar,
iki renkte birer fil ise m'yi 3 yapar.
Geri kalan her taş W'ye puan yazıyor, ve eşik W*2+m<3
bütün FIDE ayrımlarını tek karşılaştırmaya sığdırıyor:
Materyal
W
m
W*2+m
Sonuç
Yalnız şahlar
0
0
0
Yetersiz
Tek at
1
0
2
Yetersiz
Aynı renkte filler
0
1
1
Yetersiz
Zıt renkte filler
0
3
3
Yeterli
İki at
2
0
4
Yeterli
Fil ve at
1
1
3
Yeterli
Bir piyon, kale ya da vezir
9+
—
18+
Yeterli
Atın bire, filin kare rengi başına bire, ağır taşın dokuza yazılması keyfî
görünüyor ama tek amacı bu tablonun doğru yerden kesilmesi. Eşik yalnızca üç şeyi
ayırıyor: W'nin sıfır, bir, ya da birden büyük olması. Ağır taşın
karşılaması gereken tek şart da bu — tek başına iki attan hafif olmamak, yani en az
iki. İkiden dokuza kadar her rakam aynı hükümleri veriyor ve hepsi tek karakter;
dokuz seçilmesinin sebebi vezirin alışıldık değeri olması, böylece W
bir sayaç değil materyal ağırlığı gibi okunuyor.
I bittiğinde m ile W okunabilir hâlde
kalıyor, ve H tam olarak bunu kullanıyor:
O(u) "g tarafında u/2 türünden taş yok"
demek — O(4) kale, O(8) piyon, O(12) at.
Sonra I iki kez koşuyor: birincisi yalnızca yan etkisi için, çünkü
B=m ile g'nin fil renklerini saklamak gerekiyor;
ikincisinin hem dönüş değeri hem yan etkisi kullanılıyor. İkinci çağrıdan sonra
m ile W artık rakibin materyalini anlatıyor, ve
koşulun geri kalanı o iki harfi öyle okuyor.
Sorulan şey rakibin mat edebilmesi değil, mat etmesinin mümkün
olması — yani g en kötü hamleleri oynasa bile ortaya çıkabilecek bir
mat var mı. Üç alt testin karşılığı sırasıyla şu: rakip çıplak şahsa hiçbir dizilim
mat vermez; g'nin atı ya da piyonu varsa mat kurulabilir, çünkü o taş
kendi şahının kaçış karesini kapatabilir; ve g'nin fili yalnızca
rakibin filiyle aynı renkteyse iki taraf da diğerinin karelerine hiç
dokunamaz. Kuralın kendisi ve vaka vaka dökümü
kurallar bölümünde.
Mat imkânsızlığı — X
Materyal testi "bu taşlarla mat diye bir şey var mı" diye soruyor. 6.9 daha dar bir
şey soruyor: bu pozisyondan mat kurulabilir mi. Taşlar fazlasıyla yeterli olup
pozisyonun buna hiç izin vermemesi mümkün. X, o farkı kapatan arama.
Sorduğu soru tam olarak maddenin sorduğu soru: bayrağı düşen ya da çekilen taraf,
hiçbir geçerli hamle dizisiyle rakibi tarafından mat edilemiyor mu. Zorlanabilir
mat değil yardımlı mat arıyor — kaybeden taraf yardım etse bile bir mat pozisyonu
kurulabiliyor mu.
X=(g,d,v=t^g|!l(g))=>
H(g) || Xs[s=b+t+e]>=d || J(0) üç kapı
|| (Xs[s]=d, b.every((p,i)=>!p|p%2^t
|| L(i).every(f=> v = n>14975 hamle dolduysa nasılsa berabere
|| d && Xm-->0derinlik ve bütçe
&& [3,2,1,6].every((u,x,w)=>
x && p>>1^4|f%56>7 terfi değilse tek tur
|| (w=[b,e,n,t], b=[...b],
M(i,f,u), t^=1,
X(g,d-1)|([b,e,n,t]=w)))))) özyinele, sonra geri al
&& v) yaprak: g mat edilmiş mi
Üç kapı var. H(g) materyal zaten yetmiyorsa hemen çıkıyor.
Xs pozisyonun daha önce en az bu derinlikte arandığını söylüyorsa çıkıyor —
tabloda doğru/yanlış değil derinlik duruyor, yoksa sığ bir sonuç derin bir sorguyu
karşılar ve arama sessizce yanlışlanırdı. J(0) pozisyon kilitliyse çıkıyor.
En zarif yeri üçüncü parametre. v=t^g|!l(g) bir yaprak testi: sıra
g'de değilse ya da g şahta değilse burada mat yoktur. İkisi de
yanlışsa ve tahtada hiç legal hamle üretilmemişse v varsayılanında kalıp
sıfır oluyor, mat bulunmuş oluyor. Mat tespiti ayrı bir dal olarak hiç yazılmamış,
parametre varsayılanına katlanmış. Aynı satır patı da ayırıyor: hamle yoksa ama
g şahta değilse cevap doğru, yani bu dalda mat yok.
Geri alma da ifadenin içinde. X(g,d-1)|([b,e,n,t]=w) — dizi ataması
| operandı olarak NaN'a düşüp sıfır katkı veriyor, özyineleme sonucu
bozulmadan geçiyor.
Arama yalnız matı değil, matın önüne geçen her bitişi görüyor. Zorunlu bir alım
materyali yetersiz bırakıyorsa bir sonraki düğümde H dalı kapatıyor. Yarım
hamle sayacı arama sırasında dolarsa n>149 devreye giriyor. Tekrar için
ayrı bir sayaç yok, ama Xs aynı pozisyona dönen dalı kesiyor — zorunlu
tekrarın sonucu böyle doğuyor. Bütçe biterse v sıfırlanıyor ve cevap
mat mümkün oluyor: kanıtlanamayan imkânsızlık beraberlik saymıyor.
Görünür sonucu şu. Vezir sizi köşeye sıkıştırıp şah çekmiş, süreniz bitmiş, ama tek
legal hamleniz veziri almak ve sonrasında iki şah kalıyor — kaybetmiyorsunuz,
TM alıyorsunuz. Tersi daha da şaşırtıcı:
8/6pp/7k/6q1/7K/4Q1PP/8/8 w beyazın süresi bitti · White's flag falls
Beyaz şahta ve tek legal hamlesi var: Ve3xg5, ve o hamle mat. Ama
hamleyi oynamaya vakit kalmadı. Siyahın veziri duruyor, yani H materyale
bakıp bir şey diyemiyor. X tek düğümde karar veriyor: beyazın tek hamlesi
siyahı mat ettiği için siyah hiç sıra alamıyor, dolayısıyla beyazı hiçbir dizilimle mat
edemez. Sonuç TM — berabere.
6.9 "rakip mat edebilir mi" diye sorar, "ben edebilir miyim" diye değil. Bayrak
düştüğü anda oyun bitmiştir ve kazanacak hamle oynanmamıştır.
Ölü pozisyon — J
I materyale bakıyor, X aramaya. Arada üçüncü bir soru var:
taşlar yeterli, arama da bir şey bulamıyor olabilir — çünkü pozisyon kilitli.
J onu tarıyor.
J=(Y, f=2n**64n-1n, y=f^f/255n, x=y>>1n, …)=>
~e|n>1&Y ? 0 : kapı: geri dönülemez bir şey oldu mu
( b.map(p=>(p && (p-8>>2 ? w=1 piyon ve şah dışı → kapsam dışı
: p>9 ? p&1?K=V=u:L=v=u şahlar
: p&1?W|=u:B|=u), u*=2n)), piyonlar
C=P=W, Z=Q=B,
!w && V*v && (X(600), doyana kadar büyüt
!( C>>56n | Z&255n terfi kaldı mı
| S(C)<<8n&(Z|v) | S(C^W)<<8n&L
| S(Z)>>8n&(C|V) | S(Z^B)>>8n&K alma kaldı mı
)))
Kapı iki şey soruyor: şu an geçerken alma hakkı var mı, ve son iki yarım hamlede geri
dönülemez bir şey oldu mu. İkisinin de gerekçesi satranç, kod değil.
Birincisi bir susturma. Geçerken alma hakkı açıkken pozisyon ölü olamaz:
alan piyon rakibin az önce boşalttığı dosyaya iniyor ve orada önü kalıcı olarak açık
kalıyor — terfi mümkün, mat mümkün. Dedektörün modeli bu çapraz sıçramayı göremiyor, çünkü
ilerleyiş kümesi yalnız dikey büyüyor; o yüzden hüküm vermesine hiç izin verilmiyor.
“Geçerken alma varsa kilit yoktur” bir tahmin değil, ispatlanabilir bir önerme.
İkincisi bir tetikleyici. Bir pozisyon canlıdan ölüye ancak geri dönülemez bir
hamleyle geçebilir: ya bir piyon ilerlemiştir ya bir taş alınmıştır. Şah hamlesi geri
alınabilir, hiçbir imkânı kalıcı olarak yok edemez — şah geldiği kareye dönebilir. Piyon
hamlesi ile alım ise tam olarak n'i sıfırlayan iki şey. Yani doğal tetikleyici
n=0.
Ama tek başına yetmiyor, ve sebebi birinci maddede. Geçerken alma karesi bir
çift adımdan doğuyor, çift adım da bir piyon hamlesi — yani n'in
sıfırlandığı ply, tam da susturmanın devrede olduğu ply. Tetikleyicinin ateşlemesi gereken
an, dedektörün konuşmasının yasak olduğu an. Hüküm bir ply ertelenmek zorunda: geçerken
alma hakkı düştüğünde tekrar bakılacak. O anda nbir. Yani “ep düşünce
de bak” kuralı ayrı bir bit istemiyor; n=0 penceresini bir ply genişletmek
yetiyor. İkisi birlikte n<2 ediyor.
Aynı gerekçe şah için de geçerli. Kapsam şah ve piyonla sınırlı olduğu için tahtada
kayan taş yok; kayan taş yoksa keşif şahı yok, şah da kendisi şah çekemez. Geriye tek
ihtimal kalıyor: şahı bir piyon hamlesi vermiştir. O da n'i sıfırlar, kaçış
hamlesi n=1 verir. Yine pencerenin içinde. Alternatifi bir önceki ply'ın anlık
görüntüsünü taşımaktı — şah var mıydı, geçerken alma karesi neydi — ve o bilginin tamamı
zaten n'in içinde duruyor.
J'nin parametresi bir zorlama anahtarı. Z gerçek bir
hamleden sonra 1 geçiyor ve pencereyi devreye sokuyor;
6.9 araması0 geçerek “bu pozisyonu olduğu gibi
yargıla” diyor, çünkü aramanın içinde n oyunun sayacı değil aramanın kendi
hamlelerini sayıyor ve pencerenin bir anlamı kalmıyor. Susturma her iki çağrıda da
yerinde. Parametre Y adını taşıyor ve aynı adlı globali gölgeliyor — o
global yalnızca imza için var, hiçbir yerde okunmuyor.
Kapsamı w çiziyor. p-8>>2 piyon ve şah kodlarının
dışında kalan her şeyde yanıyor, !w koşulu da aramayı hiç başlatmıyor. Fil
bulunan kilitli pozisyonların kaçmasının tek satırlık sebebi bu.
Doldurma renge değil yöne bağlanıyor: W artan indekse giden
piyonlar, B azalana. Ters tahtalı sürümde bu yüzden yalnız bağlamayı takas
etmek yetiyor, gövdeye hiç dokunulmuyor.
Döngü dört ayna çifti büyütüyor. P/Q tıkalı piyonlar,
C/Z piyonların ilerleyebildiği kareler, K/L
şahların ulaşabildiği kareler, r/q iki tarafın denetlediği alan.
Her tur bunları birbirinden besliyor: şah tıkalı piyona da rakip piyonun aldığı kareye de
giremiyor, piyon rakip piyonun tuttuğu kareye ilerleyemiyor. Hiçbir maske büyümediğinde
denge kurulmuş olur.
Sekiz maske ve 64 kare, en kötü ihtimalle 512 adım demek; döngü 600’de duruyor. Yani X'in aksine burada kesme hiç gerçekleşmiyor —
sınır ölçülmüş değil, ispatlı.
Çıkışta altı soru var. İkisi terfi: beyaz sekizinci yatağa, siyah birinciye ulaşabiliyor
mu. Dördü alma: ilerleyiş karelerinin çapraz komşuları rakip piyona ya da rakip şahın
alanına değiyor mu. Altısı da hayırsa pozisyon nefes alamıyor demektir.
Hüküm — Z
Bütün oyun sonu kuralları tek bir soruya asılı: sırası gelenin oynayabileceği
bir hamle var mı? Cevap evetse oyun yalnızca otomatik beraberliklerle bitebilir;
hayırsa mat ya da pat olmuştur. Z tam olarak bu ayrımın şekli.
Ölü pozisyonun iki yarısı burada tek bir | ile birleşiyor:
I materyalden bakıyor, J yapıdan. İkisinden biri doğru dönerse
kod aynı — 6, çünkü madde de aynı: 5.2.2.
Z=Q=>b.some((p,i)=>p%2==t&&L(i)+'')
? I(2)|J(Y)?6 : $>4?5 : n>149?4 : 0 hamle var → yalnızca otomatik beraberlikler
: l(t)?t+1:3 hamle yok → mat ya da pat
L(i)+'' boş dizi sınamasının en kısa hâli: boş dizi metne
çevrildiğinde '' olur ve yanlış sayılır, dolu dizi
'28,24' gibi bir şey verir ve doğru sayılır. .length yedi
karakter, bu üç.
Mat kodu t+1. Sıra siyahtaysa (t=0) mat eden beyazdır ve
kod 1 çıkar; sıra beyazdaysa 2. Aynı toplama iki sonucu birden adlandırıyor.
Dal sırasının kendisi bir kural. FIDE 9.6, 5'li tekrar ve 75 hamlenin oyunu
berabere bitirdiğini söyler — son hamle mat etmediyse. Burada mat, "hamle
yok" dalının içinde yaşadığı için o istisna hiç yazılmamış: 75 hamle sayacı dolu bir
matta motor sayaca bakmaz bile, çünkü o dala hiç girmez. Öncelik kodlanmış değil,
şeklin sonucu.
Ayrımın maliyeti de asimetrik. "Oyun sürüyor" demek ilk legal hamleyi bulunca
biter; "mat" demek bütün hamleleri deneyip hiçbirinin olmadığını kanıtlamayı
gerektirir. Ölçüldüğünde fark bu: bir mat hamlesi Z içinde
48 kare için hamle ürettiriyor; aşağıdaki e2-e4 izinde sayaç 33'te durdu, çünkü siyahın oynayabilen ilk taşı bulunur bulunmaz tarama bitti.
I(2)'deki iki, renk süzgecini düşüren argüman — yetersiz materyal
iki tarafa birden bakılarak karara bağlanıyor. Süzgeç p%2==t boş kareleri
de geçiriyor, ama L boş kareden boş dizi döndürdüğü için hüküm
değişmiyor.
İşlem — A, D, F
A — beş adım, her biri bir öncekini okur
A=(i,f,u)=>(
1 c&=~C(i)&~C(f), rok haklarını düşür2 M(i,f,u), tahtayı yaz3 t^=1, sırayı çevir4 $=R[s=b+t+e+c]=-~R[s], yeni pozisyonu say5 Z()) hükmü döndür
Birinci adım iki ayrı kuralı tek ifadede topluyor. C(i) çıkış
karesini soruyor: şah ya da kale kendi köşesinden ayrıldıysa hakkı düşer.
C(f) varış karesini soruyor: bir kale kendi köşesinde alındıysa
hakkı yine düşer. Rokta bunların ikisi de tek karede toplanıyor —
C(e1) üç döndürdüğü için beyazın iki hakkı birden gider ve kalenin
kendi karesine hiç bakılmaz.
Üçüncü adımın dördüncüden önce gelmesi zorunlu, çünkü tekrar anahtarı
hamleden sonraki pozisyonu anlatmalı — sırası gelen taraf da o pozisyonun
parçası. Dördüncü adımda atamanın sol tarafı önce değerlendiği için
s yeni anahtara kurulmuş olur ve -~R[s] onu okur.
~undefined eksi birdir, işareti çevrilince bir: ilk kez görülen pozisyon
varsayılan yazmadan doğrudan 1'e oturuyor.
A'nın yapmadığı iki şey de tanımının parçası: saatlere
dokunmaz ve beraberlik teklifini temizlemez. İkisi de sürücü politikası, kural
değil.
D — iddia ve teklif
D=g=>(o^=2-g,n>99?7:$>2?8:o>2?9:Z())
Teklif bitleri o'da: beyaz sıfırıncı bit, siyah birinci.
2-g bunu tek çıkarmayla seçiyor. Argümansız çağrıldığında
2-undefinedNaN olur, o^=NaNo'yu
olduğu gibi bırakır — aynı fonksiyon böylece bitleri bozmadan yalnızca hüküm
okumak için de kullanılabiliyor.
Eşiklerin Z'dekilerden farklı olması tesadüf değil. 50 hamle ve 3'lü tekrar iddia edilir, 75 hamle ve 5'li tekrar kendiliğinden
olur. FIDE bu ikisini ayrı maddelere koyuyor; motor da ayrı fonksiyonlara. İsteğe
bağlı olan D'de, zorunlu olan Z'de.
F — bayrak ve terk
F=(g,k)=>(Xm=2e4,Xs={},X(g,15))?14+k:10+2*k+g
g bayrağı düşen ya da çekilen taraf, k ise sıfırken
süre, birken terk. X(g,15) doğruysa rakibin mat etmesi imkânsızdır ve sonuç
berabere olur — 14 ya da 15. Değilse rakip kazanır ve tek bir aritmetik dört sonucu
birden adlandırır: k süreyi terkten, g tarafı seçer. 10 ve
11 süreden, 12 ve 13 terkten.
Bir hamlenin tam izi
Aşağıdaki sayılar motoru koşturarak ölçüldü — her fonksiyona sayaç takılıp tek bir
A çağrısı boyunca kaç kez girildiğine bakıldı.
Sıradan bir hamle — e2-e4
Sürücü önce tıklanan ya da yazılan kareyi L(12) ile doğrular, hedef
listede varsa A(12,28) çağırır. İçeride beş adım koşar: rok maskesi
değişmez (C iki kareye de sıfır döner), M piyonu 28'e yazar
ve 50 hamle sayacını sıfırlar, sıra siyaha geçer, yeni pozisyon tabloya bir kere
işlenir, Z sıfır döndürür — oyun sürüyor.
Geçerken alma karesi yazılmaz. Çift adım olmasına rağmen 27 ve 29
karelerinde alacak siyah piyon bulunmadığı için e eksi birde kalır. Aynı
hamle, yanında bir siyah piyon varken kareyi yazardı.
Fonksiyon
G
L
M
V
l
C
I
Z
A(12,28) boyunca
1.616
33
3
2
2
2
1
1
Dağılım motorun şeklini özetliyor. Hamlenin kendisi neredeyse bedava — bir
M, iki C. Bin altı yüz geometri sorusunun neredeyse tamamı
son adımdan geliyor: Z, oyunun bitmediğini kanıtlamak için siyahın
oynayabilecek bir taşı olduğunu göstermek zorunda, ve bunu bulana kadar otuz üç kare
için hamle üretiyor. Motor hamleyi yapmak için değil, hakemlik etmek için
çalışıyor.
Kısa rok
Yol açıldıktan sonra L(4) şahın hamlelerini
[5, 6, 12] diye döndürür — f1, g1 ve e2. g1 listede sıradan bir
hedef gibi duruyor; sürücü onun rok olduğunu hiç öğrenmez, sadece bir kareye
tıklar. Rokun bütün şartları G'nin içinde, hedef listesine çıkmadan
sınanmış olur.
O tek L çağrısında C bir kez, V beş kez
koşuyor. Beşin ikisi doğrudan G'nin rok dalından geliyor — şahın şu an
şahta olmadığı ve geçtiği karenin dövülmediği. Uzun rok hiç sorulmuyor bile: c1'de
hâlâ fil durduğu için L'nin süzgeci hedefi geometriye götürmeden eliyor,
ve C bu yüzden ikinci kez çağrılmıyor.
Ardından A(4,6): C(4) üç döndürür, maske 15'ten
12'ye iner — beyazın iki hakkı da tek adımda gider. M'nin
dördüncü adımı ateşlenir, kale h1'den f1'e taşınır ve h1 boşalır. Ölçülen son durum:
f1'de beyaz kale, g1'de beyaz şah, h1 boş.
Geçerken alma
1.e4 a6 2.e5 d5 dizisinde asıl iş siyahın çift adımında oluyor.
M'nin beşinci adımı ateşlenir: fark 16’dır, kare geçici olarak
e=43 (d6) yazılır, sonra varış karesinin iki komşusuna bakılır. 36'da
beyaz piyon vardır — 17-8 tam olarak onu arıyor — ve
L(36) gerçekten 43'ü üretir. Bu üretim ancak e zaten
yazılmış olduğu için mümkün; G'nin piyon dalı f==e
karşılaştırmasını yapıyor. Sonuç: e=43 kalır.
Aynı hamle, e5'te beyaz piyon yokken e=-1 ile biter. İkisini de
ölçtüm. Fark tekrar tablosuna gider: kimsenin kullanamayacağı bir kare yazılsaydı
aynı pozisyon iki farklı anahtara düşer ve üç kat tekrar iddiası sessizce
kaçırılırdı.
Beyaz A(36,43) ile aldığında M'nin üçüncü adımı devreye
girer: f==e olduğu için silinecek kare 43^8, yani 35 — d5.
Ölçülen sonuç d5 boş, d6'da beyaz piyon, e5 boş. Ve bu hamle çift adım olmadığı için
beşinci adım ateşlenmez, e eksi bire döner.
Sürücü sınırı
Dört metin sürücüsü A'yı olduğu gibi çağırıyor:
A(i,f,terfi) ve dönen kodu z'ye yazıyor. Tahta basan sürümler
A'yı yeniden tanımlayıp beş adımı kendi gövdesine gömüyor;
L3'da sebebi, saat artışını üçüncü adımın tam ortasına — sıranın
çevrildiği ana — yerleştirmek. Motorun kural mantığı aynen duruyor; araya
giren tek şey U[t^=1]+=5.
Kısa devre merdiveni
Beş ayrı test aynı sonuca çıkıyor. H, I, J,
tekrar sayacı ve yarım hamle sayacı — hepsi oyunun berabere bittiğini söylüyor. Farkları
ne kadar erken ve ne kadar ucuz söyledikleri.
En dipteki ağ 75 hamle kuralı: kilitli her pozisyon eninde sonunda oraya düşer.
Beş tekrar onun kısa devresi, çünkü kilitlilik hareketliliği düşürür ve tekrar daha erken
gelir. I ile J tekrarın kısa devresi, pozisyonu ilk hamlede
tanır. H de X'in kısa devresi, materyal yetmiyorsa aramayı hiç
başlatmaz.
İlkesel olarak X hepsini kapsayabilirdi. Sınırsız derinlik ve bütçeyle
ulaşılabilir durum uzayını tüketir ve imkânsızlığı kendisi ispatlardı. Ama bunun ölçüsü
var, ve iyi haber vermiyor.
k4b2/8/8/p1p1p1p1/P1P1P1P1/8/8/K4B2 w kilitli · blocked
Bu pozisyonda hiçbir taş karşıya geçemiyor. Piyonlar karşılıklı tıkalı, çapraz komşuları
boş; iki fil de kendi yarısındaki karelerde hapis; her iki şah da rakip piyonların tuttuğu
yatayı aşamıyor. Ölü bir pozisyon. Ama tahtada fil olduğu için J devrede değil,
materyal fazlasıyla yeterli olduğu için H da bir şey demiyor. Geriye aramanın
kendisi kalıyor.
Derinlik
Cevap
Düğüm
9
mat mümkün
10
15
mat mümkün
19
23
mat mümkün
31
35
mat mümkün
43
Düğüm sayısı derinlikle doğrusal artıyor, üstel değil — ve cevap hiç değişmiyor. Sebebi
X'in tasarımında: every ilk yanlışta kırılıyor, derinlik bitince
yaprakta v sıfırlanıp "mat mümkün" diyor. Arama tek bir hattı dibe kadar iniyor,
orada pes ediyor, kardeş dalları hiç açmıyor.
Yani X imkânsızlığı ancak bütün hatlar derinlik dolmadan kapanırsa
ispatlayabiliyor. Şahlar serbestse hatlar hiç kapanmaz. Gereken şey birkaç kat bütçe değil,
ulaşılabilir durum uzayının çapı kadar derinlik — şah ve fil konumlarının çarpımı, on
binlerce durum. Kısa devreler tam da bu yüzden var: J aynı kararı taramayla,
arama hiç açılmadan veriyor.
Filli vakaların neredeyse tamamını kapsayan bir genişletme tasarlandı ve ölçüldü:
kapsamayı %93'ten %99,94'e çıkarıyordu. Golf edilmiş hâli bile düz metinde bin bayta
yakın tuttu. 1.935 baytlık bir motorda bu, programın yarısından fazlası; paketlenmiş
numerical_packed.html 1.815'ten 2.600 bayta çıkardı. Bayt başına getiri 27 kat düşüyor.
Üstelik %99,94 da %100 değil. Eklesem bile tamlıktan hiçbir zaman emin
olamazdım — ölçülen bir sayı, kanıtlanmış bir sınır değil. Emin olamayacağım bir şeyi
hakem motoruna koymak, motorun geri kalanının verdiği garantiyi zayıflatır;
testlerin ölçtüğü şey tam olarak o garanti. Kural bu yüzden askıda,
kaybolmuş değil.
Doğrulama
Geçtiği testler
Motorun kural iddiaları perft ile sınandı: bir pozisyondan belirli bir derinliğe
kadar üretilen geçerli hamle dizilerinin sayısı, yayımlanmış referans değerlerle
karşılaştırıldı. Tek bir düğümlük sapma bile kural setinde bir hata demektir, o
yüzden perft bir hakem motorunun en sert testidir — hamle üretiminin tamamını, şah
güvenliğini, rok haklarını ve geçerken alma imkânını aynı anda yoklar.
Karşılaştırma dört kaynaktan yapılıyor: Chess Programming Wiki'nin yayımlanmış
düğüm sayıları, van Kervinck'in zor pozisyon listesi, 6.838 pozisyonluk Vajolet
külliyatı ve Stockfish. Yayımlanmış değerin bulunmadığı sığ derinliklerde ise motorla
hiç kod paylaşmayan, kurallardan doğrudan yazılmış naif bir 0x88 uygulaması
karşılaştırma sağlıyor.
Aşağıdaki derinlikler birleştirilmiştir: bir pozisyon birden fazla koşuya
bölünmüşse — önce d5'e, sonra ayrı bir koşuda d6 ve d7'ye — tabloda ulaşılan en derin
katmanla tek satırda duruyor. Her pozisyon d1'den o katmana kadar bütün ara
derinliklerde de doğrulandı.
CPW standart pozisyonları
Yedi pozisyon, dördüncünün renk aynası dahil. Ayna tesadüfi değil: renk
simetrisini bozan bir hata yalnızca orada görünür.
Pozisyon
Derinlik
Düğüm
Karşılaştırma
Başlangıç
d6
119.060.324
CPW yayımlanmış değerleri
Kiwipete
d5
193.690.690
CPW yayımlanmış değerleri
Pozisyon 3
d7
178.633.661
CPW yayımlanmış değerleri
Pozisyon 4
d5
15.833.292
CPW yayımlanmış değerleri
Pozisyon 4 aynası
d5
15.833.292
CPW yayımlanmış değerleri
Pozisyon 5
d5
89.941.194
CPW yayımlanmış değerleri
Pozisyon 6
d5
164.075.551
CPW yayımlanmış değerleri
van Kervinck zor pozisyonları
Geçersiz geçerken alma, şah altında rok, geçerken almanın açtığı keşif şahı, şah
çeken terfi ve alt terfi, pat–mat ayrımı, fil bağından kaçınma. On beş pozisyonun
onunda yayımlanmış derinliğin ötesine geçildi; o katmanlarda karşılaştırılacak
yayımlanmış değer olmadığı için doğrulama Stockfish'e karşı yapıldı.
Pozisyon
Yayımlanmış
Ulaşılan
Doğrulama
Geçersiz geçerken alma #1
d6 · 1.134.888
d6
Yayımlanmış değer, Stockfish teyitli
Geçersiz geçerken alma #2
d6 · 1.015.133
d6
Yayımlanmış değer, Stockfish teyitli
Geçerken alma rakibe şah çekiyor
d6 · 1.440.467
d6
Yayımlanmış değer, Stockfish teyitli
Kısa rok şah çekiyor
d6 · 661.072
d7
Stockfish
Uzun rok şah çekiyor
d6 · 803.711
d7
Stockfish
Rok hakları
d4 · 1.274.206
d5
Stockfish
Rok engellenmiş
d4 · 1.720.476
d5
Stockfish
Şahtan terfiyle çıkış
d6 · 3.821.001
d6
Yayımlanmış değer, Stockfish teyitli
Keşif şahı
d5 · 1.004.658
d6
Stockfish
Şah çeken terfi
d6 · 217.342
d8
Stockfish
Şah çeken alt terfi
d6 · 92.683
d8
Stockfish
Kendi kendine pat
d6 · 2.217
d8
Stockfish
Pat ve mat ayrımı I
d7 · 567.584
d8
Stockfish
Pat ve mat ayrımı II
d4 · 23.527
d4
Yayımlanmış değer, Stockfish teyitli
Fil bağından kaçınma
d5 · 1.063.513
d6
Stockfish
Toplu koşular
Tek tek seçilmiş pozisyonlar kenar durumları yakalar; toplu koşular sıradan
oyunda ortaya çıkanı yakalar.
Koşu
Kapsam
Yöntem
Vajolet külliyatı gerçek oyun pozisyonları
100 pozisyon × d4 kapsama için, ilk 10 pozisyon × d5 derinlik için — 450 perft kontrolü
Külliyatın kendi referans değerleri
Rastgele oyun perft değil
Kiwipete'ten 20 deterministik-rastgele oyun, oyun başına en çok 80 yarım hamle
Her pozisyonda geçerli hamle listesinin tamamı, sıralı hâlde Stockfish ile birebir
Rastgele oyun koşusu düğüm toplamına bakmaz, listeyi karşılaştırır. Toplamın
tutması iki hatanın birbirini götürmesiyle de mümkündür; liste karşılaştırması tam da
bunu elediği için var.
Testler öntanımlı olarak engine_4x.js üzerinde
koşuyor. engine.js dosyasını bayt birebir sınamak için
ENGINE=./engine.js yeterli.
Kendiniz koşun
Yukarıdaki sayıların hepsi tek bir iddia: motor şu pozisyonlarda şu düğümleri
üretiyor. Kontrol etmek için bana güvenmeniz gerekmiyor.
Repo herkese açık:
github.com/cuneytinann/FideLite.
Test klasörünün tamamını tek dosyada indirmek için
test.zip · 236 KB — içinde süit, üç motorun
kopyası, Vajolet pozisyon listesi ve sekiz çift tıklanabilir .bat var,
başka bir şey indirmeniz gerekmiyor. Windows'ta dosyaya çift tıklamak yetiyor; başka
platformda node test.js <komut>.
Sekiz koşunun beşi yalnızca Node istiyor. CPW standart pozisyonları, Vajolet
süiti ve derin tek pozisyon koşuları yayımlanmış değerlerle karşılaştırıyor; dış bir
motora ihtiyaç yok. En hızlısı 1-sanity-test.bat.
İkisi Stockfish gerektiriyor: rastgele oyun simülasyonu ve yayımlanmış derinliğin
ötesine geçen doğrulamalar. Biri de isteğe bağlı kullanıyor — zor pozisyonlar koşusu
Stockfish varsa çapraz kontrol ekliyor, yoksa referans değerlerle yetiniyor.
Stockfish pakette yok, tek başına 100 megabaytı aşıyor. İsteyen kendi ikili
dosyasını test klasörüne koyar ya da PATH'e ekler; süit sırayla STOCKFISH
değişkenine, Windows'ta stockfish.exe'ye, Linux ve macOS'ta olağan kurulum
yollarına bakıyor.
Yayımlanmış değerin de Stockfish'in de olmadığı yerler için pakette
ref.js var: kurallardan doğrudan yazılmış, motorla tek satır kod
paylaşmayan naif bir referans. İkisinin aynı sayıya varması ortak bir hatanın değil,
gerçek bir mutabakatın kanıtı. Tahtası 0x88 — motorda bayt
yüzünden elenen mimari, baytın umurunda olmadığı yerde en okunaklısı.
Hız
engine_4x.js — aynı kurallar, +48 bayt, iki ilâ on kat hız
İki motor mantıksal olarak aynıdır: her derinlikte aynı hamleler, aynı düğüm
sayıları, aynı sonuç kodları. Başlangıç pozisyonundan perft d1–d4 ikisinde de
20 / 400 / 8.902 / 197.281 veriyor. Fark yalnızca aynı cevaba kaç işlemle
varıldığında.
13 baytın tamamı dört değişikliğe gidiyor. Aşağıdaki kazanç sütunu her
değişikliğin tek başına ölçülmüş etkisi — motorun en sıcak yolu olan Z()
çağrısı üzerinden, başlangıç pozisyonunda, iki sıra için ortalama alınarak.
Değişiklik
Ne yapıyor
Bayt
Kazanç
p=b[i] önbelleği
G taş kodunu iki kez okuyordu (P=b[i]>>1 ve g=b[i]&1); artık bir kez okunup ikisi ondan türüyor.
+1
1,07×
& → &&
G'de iki yer: rok satırındaki !V(i,g)&!V(i+k,g) ve son satırdaki (…)&S(). & kısa devre yapmaz, yani sol taraf yanlış olsa bile sağ taraf koşuyordu — birincisinde gereksiz bir tehdit taraması, ikincisinde her kayan taş denemesinde tam bir ışın yürüyüşü.
+2
1,58×
p&& ön koşulu
V ve Z'nin tarama koşulları boş kareyi de kabul ediyordu. Ayrıntısı aşağıda.
+6
6,34×
map → forEach
L her çağrıda 64 elemanlık bir sonuç dizisi ayırıp atıyordu.
+4
1,02×
Dördü engine.js'e sırayla uygulanınca çıkan dosya
engine_4x.js ile bayt bayt aynıdır; ara bir şey yok.
Beşinci bir değişiklik daha vardı ve listede değil, çünkü artık ikisinde birden
duruyor: L önce pahalı G'yi çağırıp sonra "hedef zaten kendi
taşım mı" diye bakıyordu, sıra ters çevrildi. Bu takas bayt nötr —
G(i,f)&&!p|p&1^g&& ile
!p|p&1^g&&G(i,f)&& ikisi de 18 karakter, ve tek
başına 1,49× getiriyor. Gönderilen bütün sürümlerde uygulanmış durumda. Kaynakta bedava
olması paketlenmiş çıktıda bedava olmasını gerektirmiyor: RegPack'in gördüğü tekrar
dizileri değiştiği için o zamanki paketlenmiş dosyalarda toplam 3 bayt
tutmuştu.
Üçüncü satır listenin en ilginci, çünkü düzelttiği şey bir yavaşlık değil bir
asimetri. Boş karenin kodu 0, ve engine.js'te tarama
koşulları p%2==t ile p%2^s. Sıfırın ikiye bölümünden kalan
sıfır olduğu için bu koşullar boş kareleri siyahın taşı sayıyor. Sonuç:
Z() siyahın sırasındayken tahtadaki her boş kare için tam bir legal hamle
üretimi koşuyor, beyazın sırasındayken koşmuyor.
Ölçüldüğünde fark bu kadar keskin:
Başlangıç pozisyonunda Z()
Beyaz sırada
Siyah sırada
Oran
engine.js
174 µs
1.393 µs
8,0×
engine_4x.js
97 µs
102 µs
1,1×
Bu yüzden "iki kat" tek bir sayı değil, iş yüküne bağlı bir aralık.
Başlangıç pozisyonundan perft d4 — düğümlerinin büyük çoğunluğunda beyaz sırada
olduğu için asimetrinin ucuz tarafı — 2,4× veriyor. Aynı perft d3, çoğunluğu
siyah sırada bittiği için 9,7×. Sürümün adındaki iki, kötü durumdaki değil
iyi durumdaki rakam; yani mütevazı bir isim.
13 bayt ancak motorun sıcak yolunun sürekli açık olduğu bir kullanımda karşılığını
buluyor. Hamlesini insandan alan bir sürücüde motorun mikrosaniyeleri kullanıcının düşünme
süresinin yanında görünmüyor; ölçüm de bu yüzden sürücüsüz koşuluyor.
engine_4x.js sürücülerin hiçbirine gömülü değil; kaynağı builds/
altında, bir kopyası da test paketinde.
13 bayt gökten seçilmedi
Yukarıdaki dört değişiklik bir tahmin değil. Aday listesi ölçüldü, dördü kaldı,
birkaçı elendi. Kalanların ne olduğu yukarıda tek tek yazılı; burada elenenler ve
neden elendikleri var.
Önce ölçünün kendisi
Bu başlıktaki her sayı yönteme bağlı, ve hız ölçmenin iki kolay hatası var —
ikisi de kazancı büyütür.
Birincisi en iyi koşuyu almak. Aynı kodu 15 kez koşturup en hızlısını
yazarsan gürültünün en cömert anını ölçmüş olursun, ve o cömertlik her sürümde aynı
değil. İkincisi yapay tezgâh: sabit birkaç pozisyonda hamle üretimini döngüye
sokmak, gerçek bir derin sayımda yanında koşan durum yönetimini dışarıda bırakır.
İkisi birden yapıldığında sonuç sayısal olarak kaymıyor, işaret değiştiriyor.
Aşağıdaki unmake satırı sentetik tezgâhta ve en iyi koşuyla ölçüldüğünde %11 kazanç
gösteriyordu; medyan ve gerçek perft ile %13 kayıp. Tablo bu yüzden başlangıç
pozisyonundan perft d3 (8.902 düğüm), 15 tekrarın medyanı, her sürüm
ayrı süreçte.
Fonksiyon fonksiyon kazanç
Toplam oran tek bir sayı değil, çünkü kazanç işlevden işleve çok farklı. Aşağıdaki
ölçüm her fonksiyonu ayrı ayrı, 7 tekrarın medyanıyla yoklar.
Bir şeyi baştan söylemek gerek: perft X ile J'ye hiç
uğramaz. Perft düğüm sayar, hüküm vermez; J'yi Z,
X'i de F ancak oyun bittiğinde çağırır. Yukarıdaki perft
tablosuyla aşağıdaki tablo bu yüzden farklı şeyleri ölçüyor — biri hamle üretecinin,
öteki hakem katmanının hızı.
Fonksiyon
Ne ölçüldü
engine.js
engine_4x.js
Kazanç
L
Başlangıç pozisyonunda tüm legal hamleler, 2.000 tur
2.050 ms
885 ms
2,3×
X
6.9 araması, açık pozisyon, 3.000 tur
5.398 ms
2.039 ms
2,6×
J
Kilitli piyon duvarı, 20.000 çağrı
9.419 ms
255 ms
37×
J'deki uçurumun sebebi tek bir satır. 1x sürüm doğrulukla ilgili bir
gerekçeyle sabit 600 tur dönüyor — sınırın ispatlı olması için. 4x sürüm bunun
yerine maskelerin toplamını izliyor ve hiçbiri büyümediği anda duruyor. Kilitli
bir pozisyonda denge çoğu zaman 15-20 turda kuruluyor, yani 4x sürüm işin
%99’unu hiç yapmıyor. 24 baytın karşılığı bu.
Kazancın L ile X'te iki buçuk kat civarında kalması da
aynı sebebin tersi: orada yapılan iş gerçekten yapılması gereken iş, kısaltılacak
boşluk yok.
Elenenler
Süreler ölü pozisyon ile mat imkânsızlığı aramasından
önceki nesilde ölçüldü; bayt sütunu ise bugünkü motorlara göre yazıldı, çünkü dört
denemenin hiçbiri o iki katmana dokunmuyor — maliyetleri hangi nesle eklenirse eklensin
aynı. Dördü de elendi, ve elenme gerekçeleri o günden bu yana değişmedi.
Sürüm
Bayt
Medyan
4x'e göre
engine.js
1.935
12.385 ms
0,10×
engine_4x.js
1.983
1.177 ms
1,00×
+S dışarı alındı
2.005
1.091 ms
1,08×
+ hedef bekçisi
2.008
1.035 ms
1,14×
+ ikisi birden
2.030
1.012 ms
1,16×
+ gerçek unmake
2.068
1.349 ms
0,87×
S kapanışını parametre listesinden çıkarmak.G'nin
son varsayılan parametresi kendine referans veren bir kapanış, ve her çağrıda yeniden
yaratılıyor — gövdedeki ilk karşılaştırma koşmadan önce. Teori, kapanışın
G'nin yerellerini her çağrıda yığına zorladığıydı. +22 bayt, %8.
V8 kapanışı teorinin varsaydığından iyi idare ediyor.
Hedef taramasını sınırlamak. Kayan olmayan taşlar indeks olarak on yediden
uzağa gidemiyor — piyon en fazla 16, şah 9, at 17. L'ye tek bekçi koyup
kayan olmayan taşlarda taramayı bu aralıkla sınırlamak +25 bayt, %14. Listenin
en iyi oranı, ve doğruluğu taş kodlarından ispatlanabilen yerel
bir sınır.
Gerçek unmake.L'nin referans takaslı geri alması yerine
yalnızca M'nin dokunduğu kareleri geri koymak; şah hamlelerinde kopya
yolu korunuyor, çünkü rok kalesi iki kare daha yazıyor. +85 bayt, ve yavaş.
Aday başına 5 elemanlı bir dizi ayırıyor; [...b] ise 64 elemanlık
paketlenmiş dizide tek bir toplu kopya. Ölçümün en oynak sürümü de bu — maksimumu
1.806 ms'ye çıkıyor, yani çöp toplayıcıyı besliyor.
Üstelik doğruluğu M'nin tam olarak hangi karelere yazdığını bilmeye
bağlı. İlk yazımında geri koyma karesi bir globalde tutuluyordu ve
M'nin geçerken alma dalı içeriden L'yi tekrar çağırdığı için
iç çağrı o globali eziyordu: dış çağrı yanlış kareyi geri koyuyor, tahta sessizce
bozuluyordu. M'ye eklenecek her yeni yazma aynı hatayı yeniden doğurur.
Aynı ders map → forEach değişikliğinin bir adım ötesinde
de çıktı: forEach'i elle yazılmış bir for döngüsüyle
değiştirmek +19 bayt ve %10 yavaş. C'den taşınan sezgiler — toplu kopya
pahalıdır, elle döngü hızlıdır — JavaScript'te geçerli değil.
Sıra da bir değişken
Unmake'in ölçüsü bir zamanlar bundan çok daha iyiydi, ve o ölçüm de yanlış değildi.
V'nin boş kare taraması düzeltilmeden önce — yukarıdaki
p&& ön koşulu, tek başına 6,34× — tahta kopyaları çok daha büyük
bir toplamın çok daha büyük bir payıydı. Aynı düzenleme o düzeltmeden önce ve sonra
ölçüldüğünde iki ayrı optimizasyon gibi görünüyor.
Bir kazancı bir değişikliğe yazarken sıra önemli: ölçtüğün şey değişikliğin kendisi
değil, o andaki darboğazdaki payı.
Neden burada durduk
Oran uçurumu tek satırda görünüyor. engine.js'ten
engine_4x'e 13 bayt 10 kat getiriyor. Sonrasındaki en iyi aday
47 bayt karşılığında %16. Bayt başına 100 kat fark.
Sebebini profil söylüyor. Açılış pozisyonunda tam bir tarama 768 şekil sondası
yapıyor ve bunların %97'si ret — hepsi de ilk karşılaştırmada. Reddedilenlerin
dağılımı %50 piyon, %31 kayan, %12,5 at, %6,3 şah; yani tam olarak materyal sayımı. On
hamle sonra tekrar ölçüldüğünde aynı, çünkü tarama tekdüze: her taş, türü ne olursa
olsun aynı sayıda hedef sondası alıyor. Belirli bir taşı hızlandırmanın hiçbir
pozisyonda sürpriz kazancı yok.
Kırpılacak bir şey kalmıyor. Buradan sonrası tarama uzayını daraltmak demek — taş
başına hedef listeleri, yani hamle tabloları — ve o yol
zaten ölçülüp elenmişti, kilobaytlar tutuyor.
Amaç golfstack'i bozmadan hızlı bir motordu. engine_4x o. Sonrası ne
yeterince hızlandırıyor ne de baytı hak ediyor.
35 bayt daha — X ve J
Yukarıdaki 13 bayt motorun tabanına ait ve olduğu gibi duruyor.
Ölü pozisyon ile mat imkânsızlığı araması gelince fark 48 bayta
çıktı; aradaki 35 bayt tamamen o iki fonksiyonun hızlı varyantlarından geliyor.
Katman
engine.js
engine_4x.js
Fark
Taban
1.175
1.188
+13
X
272
283
+11
J
488
512
+24
Toplam
1.935
1.983
+48
X — 11 bayt, dört kalem
Şah testi yaprağa erteleniyor. 1x sürümde yaprak testi üçüncü parametrenin
varsayılanı: v=t^g|!l(g). Varsayılan her çağrıda koşuyor —
H, Xs ya da J kapısından hemen çıkan düğümlerde
bile. l(g) ise tahtayı tarayan bir test. 4x sürüm v=0 yazıp
bedava geçiyor ve ifadeyi sona, &&(v||t^g|!l(g)) içine alıyor. Erken
çıkan her düğümde şah testi hiç koşmuyor. +5 bayt.
Boş kare kısa devreye alınıyor.!p|p%2^t yerine
!p||p%2^t. Kare boşsa renk karşılaştırması hiç yapılmıyor. +1 bayt.
Terfi testi ucuzdan başlıyor.p>>1^4|f%56>7 yerine
(f%56>7||p>>1^4). Hedefin son yatayda olmaması hem daha ucuz hem
çok daha sık doğru, o yüzden öne alınıp || ile bağlanıyor. +3 bayt.
Geri alma sayı döndürüyor.([b,e,n,t]=w) yerine
([b,e,n,t]=w,0). 1x sürüm dizinin | altında NaN'a düşmesine
güveniyordu; bu her düğümde diziyi metne çevirip sayıya dönüştürmek demek. Sabit
0 aynı işi bedavaya görüyor. +2 bayt.
J — 24 bayt, 37 kat
Sayaçlı döngü sabit noktaya dönüyor. Bu, motordaki en büyük tek kazanç.
1x sürüm X=k=>k&&(…,X(k-1)) ile 600 tur dönüyor; sınırın
ispatlı olması için. 4x sürüm bir yerel daha alıp — o=W — her turun sonunda
bütün maskelerin toplamını karşılaştırıyor:
X=_=>(o^(o=C+Z+K+L+r+q-P-Q))&&(…,X()). Toplam değişmediği anda
duruyor. Kilitli bir pozisyonda denge çoğu zaman 15-20 turda kuruluyor, yani 4x
sürüm işin neredeyse tamamını hiç yapmıyor. Ölçülen kazanç 37 kat.
Doldurma dizi yaratmıyor.b.map yerine b.every.
map her çağrıda 64 elemanlık yeni bir dizi ayırıyor ve sonucu
atılıyor; every hiçbir şey ayırmıyor.
24 baytın karşılığı bu. Kural metninin geri kalanı iki sürümde de bayt bayt
aynı — değişen yalnız ne zaman duracağı.
Alternatifler
Denenip elenen yollar
Motorun tahta temsili düz 64 elemanlı, kare merkezli bir
dizi. Dolgu yok, 0x88 yok, bitboard yok. Bu bir varsayılan değil — geriye kalan.
Aşağıdakilerin hepsi ya yazıldı ya ölçüldü, ve hepsi elendi.
Öklid uzaklığı — ilk tasarım, iki karede çöktü
İlk fikir hareketi mutlak uzaklıkla tanımlamaktı: kesikli düzlemde bir kareden
ötekine Pisagor uzaklığı. Boş bir 8×8 tahta çizip seçilen kareye göre bütün
uzaklıkları ölçen küçük bir simülasyon yazıldı, ve formüller ilk bakışta kusursuz
duruyordu — at hep √5, şah 1 ile √2 arası, kale tam sayılar {1…7}, fil √2'nin katları
{1√2…7√2}, vezir ikisinin birleşimi.
At ve şah gerçekten temiz. Sekiz kareli uzayda d²=5 veren tek şekil (1,2); at √5'i
tek başına sahipleniyor. d²≤2 veren şekiller (0,1), (1,0), (1,1) — şahın da rakibi
yok.
Kale ve fil değil. Bütün uzaklık uzayında tam iki çarpışma var:
Uzaklık
Şekil
Çarpışan şekil
5
(0,5) — beş kare kale
(3,4) — hiçbir taşın hamlesi değil
5√2
(5,5) — beş kare fil
(1,7) — hiçbir taşın hamlesi değil
3-4-5 üçgeni kalenin beşinci karesini, (1,7) filin beşinci karesini taklit ediyor.
Vezir ikisini birden miras alıyor. Uzaklık tek başına yasal hamleyi tanımlayamıyor.
İkinci sorun daha ölümcül: uzaklık yönsüz. Karekök işareti yutuyor, piyon ise
tahtanın tek yönlü taşı. Öklid yaklaşımı piyonu hiçbir yamayla kurtaramazdı.
Çözüm karekökü atmak oldu: yatay ve dikey farkı ayrı tutmak. Motorun bugünkü
h ve v'si buradan geliyor, ve tek hamlede
iki sorun birden kapandı — çarpışma, çünkü (3,4) ile (0,5) artık farklı iki çift; yön,
çünkü f<i^g işaretli bir karşılaştırma taşıyor. Vazgeçilen yaklaşımın kalıntısı
hâlâ görünür: h*v==2 at, h|v<2 şah, h==v fil,
h*v==0 kale. Uzaklık gitti, şekil kaldı.
Dolgulu tahta ve indeks farkı — aynı karar
Uzaklık şekle dönüştükten sonraki soru şuydu: şekil testi de gidebilir mi? Yasallığı
i ile f arasındaki ham indeks farkından, ya da taş başına
hamle vektörü tablolarından okumak denendi.
İki formülasyon arasındaki fark yalnızca hız değil, cinsi ayrı. Seçilen yol
gezinir: kaynak kare için 64 hedefi tek tek dolaşıp her birine "bu şekil yasal
mı" diye sorar. Delta yolu üretir: taş türü kendi yön listesini taşır ve
f = i + offset ile hedefi doğurur. Üretilen her hedefin gerçekten tahtada
olup olmadığı ise ayrı bir sorudur — at h2'deyken +10 seni tahtanın öbür
ucuna atar.
Düz 64'te bu soru cevapsız kalıyor. Delta kenarda belirsiz: +1 bir sağa
demek ama h dikeyinde bir üst yatayın a'sına sarıyor, çaprazların +7 /
+9'u ve atın +6/+10/+15/+17'si aynı şekilde. Belirsizliği
kaldırmak için |i%8-f%8| eklemek gerekiyor, o da zaten h;
yani delta yolu düz tahtada şekil testine geri düşüyor. Delta ancak dolgulu bir
tahtada anlam kazanıyor: 0x88'in 128 gözü ya da 10×12 mailbox'ın gözcü kenarı sarmayı
emiyor ve fark tek anlamlı hâle geliyor. İkisi bu yüzden birlikte denendi.
İkisi de belirgin şekilde büyük çıktı, ve sebep basit: bu motorda dolgunun
çözeceği problem yok.G from-to çalışıyor, offset üretmiyor —
L hedefleri b.map ile geziyor, yani f üretilmiş
değil, zaten var olan bir kare. Tahtadan çıkacak aritmetik olmadığı için sarma da
imkânsız; açılış pozisyonunda tam bir tarama boyunca G'ye giden tahta dışı
indeks sayısı sıfır. Tek maruziyet S'nin ışın yürüyüşü ve o da
bedava kapanıyor: b[i]<1,
undefined için yanlış.
Dahası dolgu bu şemada bir test kaldırmıyor, ekliyor. 0x88'de dizi 128 göze
çıkar, L 128 hedef gezer ve yarısını ayrıca elemek zorunda kalır — düz
64'te hiç var olmayan bir denetim. Başlangıç literali de iki katına çıkar. Dolgunun
sattığı bedava kenar testi ancak offset üreten bir hamle üreticisinde alıcı bulur;
burada üretilen offset yok.
Taramanın bedeli de sanıldığı kadar ağır değil, çünkü ayrı bir döngü değil:
L hamle listesini kurarken b'yi nasılsa geziyor,
b.map aynı anda hem taş kodunu okuyor hem hedefi veriyor. Açılışta tek bir
Z() çağrısı G'yi 192 kez çalıştırıyor — teorik tavan 4096,
aradaki farkı erken çıkış kapatıyor.
Takas ayrıca eksik kalırdı. Delta tablosu yalnızca G'nin şekil yarısını
değiştirebilirdi; menzilli taşların önünün kapalı olup olmadığını yine yürüyerek
sınamak gerekiyor, çünkü blokaj temsilden bağımsız bir soru. Yani dolgunun ve tablonun
bedeli ödenecek, S yine kalacaktı. Kalan yapı bu yüzden düz 64 +
G şekil testi + S doğrusallık yürüyüşü.
Tersi doğru değil: dolgulu tahta vektör tablosu gerektirmiyor, 0x88 üstünde de
h / v koşturulabilirdi. Ama dolgunun maliyetini ödemenin tek
sebebi delta kullanmak. İkisi ayrı ayrı elenmedi, birlikte elendi.
Tahtayı sayı olarak tutmak — iki deneme, iki eksen
Bu bölümün tezi iki kelimeye dayanıyor: kare merkezli, ve dizi. Denenen iki
alternatif bunlardan birini reddediyor.
Bitboard kare merkezliliği bırakır: bilgi taş türüne göre ayrılmış 64 bitlik
kelimelere dağılır, her bit "bu karede beyaz piyon var mı" der. Küme işlemleri ucuzlar
— bütün beyaz piyonların saldırısını tek maskede alırsın. Karşılığında "12. karede ne
var" sorusu pahalılaşır; her kelimeyi ayrı sınaman gerekir. Paketlenmiş tamsayı
ise kare merkezliliği korur, yalnızca kabı değiştirir: aynı 64 basamak, dizi yerine tek
bir sayının içinde. Veri modeli birebir aynı, erişim biçimi farklı.
İkisi de aynı yere mecbur: BigInt. 64 kare × 4 bit 256 bit eder,
Number'ın 53 bitlik tam sayı hassasiyetinin çok ötesi. Yani her iki yol da
JS'in BigInt bedelini ödüyor — her literale n eki, karışık her ifadeye açık
dönüşüm, ve BigInt ile Number'ı + ile
karıştırdığın anda TypeError.
Bedelin nerede biriktiği ölçülebilir. engine.js tahtaya
b[…] biçiminde on sekiz yerde dokunuyor — on üçü okuma, beşi atama — ve
ayrıca map, some ve indexOf
ile altı kez geziyor. Dizi hâlinde b[i] dört karakter; paketlenmiş hâlde
aynı okuma kaydırma ve maske ister, kabaca 25. Yalnız okumalarda 200 baytı
aşan fark, ve gezinmelerin doğrudan karşılığı yok — altısı da elle döngüye dönüşür.
Bitboard'a bunun üstüne saldırı tabloları eklenir.
C ya da Assembly'de bu modelleri ucuz kılan şey işlemci seviyesinde bedava olan 64
bitlik aritmetik. JavaScript'te o zemin yok; kazanç bir yana, iki deneme de yüzlerce
bayt ekledi. Dahası ikisi de motorun tüm iskeletini değiştirecek, karşılığında
yönetilmesi zor bir algoritmik bilinmezlik yığını getirecekti — hızı boyuta takas eden
bir projede yanlış yönde bir takas.
İroni şu ki BigInt motorda kaldı, sadece bambaşka bir işte. Başlangıç
tahtası 10n**40n-10n**32n ile kuruluyor: ifade 40 basamaklı bir sayı üretiyor
— sekiz adet 9, sonra otuz iki adet 0 — yani beyaz piyon sırası ve boş orta dört yatay.
Kaynakta 20 karakter, ürettiği dizi 40. 20 bayt kazanç. BigInt tahta temsili
olarak elendi, literal sıkıştırıcı olarak kaldı.
Hayalet en passant — e'yi tahtaya taşımak
Geçerken alma karesi motorda ayrı bir global: e, bir kare indeksi, yokken
-1. Onu ayrı tutmak yerine tahtanın kendisinde işaretlemek denendi. Fikir
temiz: pozisyon tek doğruluk kaynağı oluyor, tekrar anahtarı b+t+e+c yerine
b+t+c diye kısalıyor, ve bir harf tamamen serbest kalıyor.
Yarım nibble kodlamasında üç değer boşta: 1,
14, 15. Ucuz olan tek slot 1, ve sebebi
G'nin dallanma zinciri. Kod taş tipini p>>1 ile ayırıyor:
1>>1 sıfır veriyor, yani boş karenin zaten düştüğü son dala düşüyor ve
orada ifade daima yanlış. 14 ile 15 ise 7 veriyor —
zincirin ilk dalına, at testine. Hayalet at gibi tehdit ediyor, tahtadaki şaha sekiz
kareden hayalet şah çekiyor; üstelik glif dizgisinde yedinci karakter yok.
1 ise dört ayrı yerde bedavaya geliyor: G'de tehdit etmiyor, glif
tablosunda boşluğa düşüyor, ölü pozisyon taramasını kendiliğinden
susturuyor, ve materyal sayımını bozmuyor — hayalet tahtadaysa piyon da tahtadadır, sayaç
zaten yeterli materyal görüyor.
Fikrin en güzel tarafı bu. Ama tek güzel tarafı. Kaybettiren şey tek cümleye sığıyor:
e bir değişkenken ep karesinin nerede olduğu bedavaydı; tahtaya
gömünce her hamlede aramak gerekiyor. Kazançlar dağınık ve küçük — her biri
e harfinin bir kullanımını silmekten ibaret. Maliyet ise iki kalemde
toplanıyor.
Kalem
Bayt
e'nin başlangıç değeri
−7
Tekrar anahtarı, iki yerde
−4
Taramayı susturan muhafız
−3
G'nin piyon çaprazındaki f==e
−5
Legallik denemesinin yedeği, iki yerde
−4
Ep karesini kuran satır
−5
Kazanç
−28
Zorlama bitini kurtaran satır
+9
Temizleme zinciri ve b[f]==1 testi
+19
Sayaç sıfırlamasının muhafızı
+2
Boşluk testi !p → p<2
+1
Taş seçme muhafızı
+2
Maliyet
+33
Net
+5
+19 — eski hayaleti silmek.e varken temizlik tek bir atamaydı.
Hayalet varken tahtayı taramak gerekiyor:
b[i]=b[b.indexOf(1)]=b[P&b[f]==1?f^8:i]=0
Ayrıca hedef kareye yazan atama aşağı taşınmak zorunda: orijinalde hedef,
b[f]==1 testinden önce yazılıyordu, sıra düzeltilmezse ep alışı kendi
taşını okuyup yanlış kareyi siliyor. Zincirin çalışma sebebi ince — JavaScript atama
hedeflerini soldan sağa çözüp sağdan sola atıyor, yani indexOf ve
b[f] hâlâ eski değerleri görüyor. Temizliğin ham hamlenin içinde olması
da şart: kareyi dışarıdan verip bir üst katmanda bir kez arayan varyant 2 bayt daha
kazandırıyor, ama ham hamleyi o katmandan geçmeden çağıran her yol — ki legallik denemesi
tam olarak öyle çağırıyor — bayat hayalet bırakıyor.
+9 — zorlama bitini kurtarmak. Ölü pozisyon taramasının tetikleyicisi üç iş
birden yapıyor: ep açıkken taramayı sustur, ep düştükten sonra zorla, şah
düştükten sonra zorla. Birincisi hayaletle bedava geliyor — tahtada
1 varken tarama zaten kendi ilk kapısından çıkıyor, ayrı bir muhafız yazmaya
gerek kalmıyor, 3 bayt kâr. Ama ikincisi e'nin içinde saklıydı ve yerine bir
arama koymak gerekiyor, 9 bayt zarar. Aynı iki satırda net +6.
Susturmanın neden gerekli olduğu ayrıca ilginç: tarama piyonların ilerleyebildiği
kareleri yalnızca dikey büyütüyor, piyonun çapraz sıçrayıp komşu dosyadan terfi etmesini
göremiyor. Ep açıkken alan piyon rakibin az önce boşalttığı dosyaya iniyor ve orada
önü kalıcı olarak açık kalıyor — terfi mümkün, mat mümkün, pozisyon ölü değil. Yani
“ep varsa kilit yoktur” muhafazakâr bir tahmin değil ispatlanabilir bir önerme, ve susturma
taramanın modelindeki bu tek körlüğü kapatıyor. Susturma olmazsa klasik bir kilitli duvar
yanlışlıkla ölü ilan ediliyor: zincirin kenarında bekleyen tek bir çift adım, geçerken alma
yoluyla açılan dosyadan vezire gidiyor.
Kod tabanı
Ep yöntemi
Bayt
Fark
Mat ve patölü pozisyon yok
e indeksi
1.598
—
hayalet
1.600
+2
Sayaçlar ve ölü pozisyon
e indeksi
2.324
—
hayalet
2.329
+5
hayalet + indexOf alias'ı
2.326
+2
Mat/pat sürümünde hayaletin kazanabileceği kalemlerin çoğu zaten yok — tekrar anahtarı
yok, susturma yok, zorlama biti yok — ama temizleme zinciri aynen duruyor. Buna karşılık
orada ep mekanizması tamamen kayboluyor: G'nin piyon çaprazında ayrı bir
f==e kuralı gerekmiyor, hayaletin kendisi zaten alınabilir bir hedef gibi
okunuyor. Fikrin en temiz göründüğü yer burası, ve en net kaybettiği yer de burası.
Alias'ın hikâyesi ayrıca öğretici.indexOf için takma ad kurmak
12 bayt, her kullanımda 5 bayt kazandırıyor — yani başabaş noktası üç kullanım. Gönderilen
iki sürümde indexOf tek bir yerde geçiyor, orada alias 7 bayt zarar.
Hayalet üçüncü kullanımı kendisi yaratıyor: şah karesini bulan arama, zorlama bitini
kurtaran arama, ve temizleme zincirindeki arama. Eşiği geçen tek dosya o, ve alias orada 3 bayt kazandırıyor. Yani bu bağımsız bir kazanç değil, hayaletin kendi zararını kısmen
geri alması — ve aracın gerçek kuralını gösteriyor: takma ad bir kaldıraç değil,
kullanım sayısına bağlı bir eşik.
Sürüm
indexOf sayısı
Alias'ın etkisi
Mat/pat, e indeksi
1
+7
Sayaçlı, e indeksi
1
+7
Mat/pat, hayalet
2
+2
Sayaçlı, hayalet
3
−3
Bir de hız bedeli var. Başlangıç pozisyonundan perft d3, hayalet sürümlerde
%25 daha yavaş. Kaynağı iki yerde: her ham hamlede fazladan bir tahta taraması, ve
hayalet tahtadayken ölü pozisyon taramasının 64 kareyi boşuna gezip ilk kapıdan çıkması.
Arayüzde ölçülemez — bir tıklama zaten mikrosaniyeler sürüyor — ama testte görünüyor, ve
“bedava” olmadığını söylüyor.
Alınmadı. Üç kod tabanında da ya başabaş ya zararda, ve motoru yavaşlatıyor. Son
ironi de şurada: hayalet e ile f slotlarını serbest bırakıyor ama
ikisi de >>1 = 7 olduğu için G'nin en sıcak ifadesine
muhafız eklemeden kullanılamıyor. Yani bu kodlamada ucuz olan tek serbest slot
1'di, hayalet onu harcayıp yerine kullanılamayan iki slot veriyor.
Doğruluğu tartışmalı değil: yedi pozisyonda perft sayıları indeksli sürümlerinkiyle
birebir aynı çıkıyor — başlangıç, dört standart CPW pozisyonu, ve özellikle ep yüklü iki
pozisyon. Elenme sebebi hata değil, fiyat.
Yukarıdaki rakamlar, ölü pozisyon taramasının tetikleyicisi
n<2 penceresine bağlanmadan önceki kod tabanına ait;
hayalet o nesle karşı ölçüldü. Yeni tetikleyici hayaletin son sığınağını da kapatıyor:
alias'ı kârlı yapan üçüncüindexOf kullanımı, tam da tetikleyiciyle
birlikte silinen satırdı. Onsuz hayaletin kullanımı ikiye düşüyor ve alias yine eşiğin
altında kalıyor.
Boşta kalan slotlarla iki deneme daha
Hayalet fikri “tahtada boşta duran değerler işe yarar mı” sorusunu açıyor. Aynı soruyla
iki şey daha denendi, ikisi de aynı duvara çarptı.
Boş kareleri tehdit durumuna göre sınıflandırmak.0,
1, 14, 15 ile boş kareler üzerinde iki bitlik bir
tehdit haritası tutulup şah güvenliği ve rok kontrolü kısaltılabilir mi? Üç ayrı sebeple
hayır. Birincisi ve öldürücü olanı: bayrak hamleden önceki pozisyonu anlatıyor, oysa
şahın kendi gövdesi ışını kesiyor — “şah, şah çeken taşın ışında geri kaçamaz” kuralı
önbellekte çöküyor. Ölçüldü: aday şah hamlelerinin %0,2’sinde harita hatalı izin veriyor. Mevcut kod aynı işi legallik denemesinin yap-dene-geri al'ı sayesinde bedavaya ve
doğru yapıyor. İkincisi: şahın kendi karesi dolu, yani şah testinin ve rokun ihtiyaç
duyduğu kareler haritanın kapsadığı kareler değil. Üçüncüsü: boş kare artık
0 olmayınca koddaki sekiz ayrı boşluk testi kırılıyor — harita bakım kodu yazılmadan önce 16 bayt.
Rok haklarını tahtaya taşımak. Aynı fikrin c için hâli. Burada
duvar daha erken: >>1 = 2, yani kaleyle aynı dala düşen serbest bir değer
yok. Kalan yol kale karelerini işaretlemek olurdu, ama hakları kırpan fonksiyon zaten
yirmi yedi bayt ve o kadar sıkı ki dizginin kendisi tablo görevi görüyor — sınırın dışına
düşen kareler undefined → NaN → 0 yoluyla bedavaya sıfır dönüyor. Tahtaya
taşımak o bedava davranışı kaybettiriyor.
Piyon yönü için işaretli değişken — denendi, kazandırmadı
Renk başına ±1'lik bir yön değişkeni tutup piyon hamlesini o çarpanla yazmak
denendi. Kazandırmadı. Sebebi seçilen temsilin daha ucuz olması değil, daha çok
işli olması: renk bilgisi bir bit olarak duruyor ve motor onu aynı anda doğruluk
değeri (p%2==g), XOR operandı (t^=1), toplanan
(l(t)?t+1:3 mat kodunu, 10+2*k+g sonuç kodunu verir), indeks
('WB'[t]) ve çarpan (t*49+7 render çevirmesi) olarak
kullanıyor. ±1 bunlardan yalnızca sonuncusunu yapabiliyor, üstelik işaretli yön
yalnızca tek taşta anlamlı. Piyon bugün ayrı bir değişken taşımıyor:
f<i^g varış indeksinin kaynaktan küçük olup olmadığını sorup bunu renk
bitiyle XOR'luyor, tek ifadede hem yön hem renk.
a8=0 indeksleme — render için seçildi, motor için bırakıldı
Tahta bir dönem sekizinci yataydan başlıyordu: siyah üstte, a8=0,
b8=1, … h1=63. Sebebi render'dı — bu düzende diziyi baştan
sona basmak doğrudan beyaz bakışıyla bir tahta veriyor, ilk döndürme için bedel
ödenmiyordu.
Ama bu motorda aranan şey yalnızca kısalık değildi; anlaşılır ve evrensel olması da
öncelikti. Üstelik her sürümde tahta yok — biri körleme, biri sayısal. Render kaygısı
yedi dosyanın hepsini bağlamıyor.
Ölçüldüğünde terazi zaten boş çıktı. Motor tarafında a8=0'ın maliyeti
tam sıfır; sürücülerde kimi dosyada sıfır, kimi dosyada bir iki bayt kazandırıyor. Ama
başlangıç tahtasının sıkıştırılabilir bloğu bu düzende yer değiştiriyor ve sayı
sıkıştırması aynı kadarını geri alıyor. Render'da kazanılan literalde kaybediliyor.
Geriye tek gerekçe kalıyor, ve o bedelsiz: a1=0 standart satranç
indekslemesi. Yatay ve dikey, ikisi de beyaz yönünde arttığı
için motorun her ifadesi doğal okunuyor, altmış dört karenin tamamında. Motorun evrenselliği sıfır bayt farkla tercih edildi. İddia render'da değil
motorda.
Paketleme
Paketlenmiş iki dosya
Paketlenmiş sürüm iki tane: dom_packed.html 2.728 bayt,
numerical_packed.html 1.815. İkisi de kendi kendini açan RegPack
çıktısı — dosyanın gövdesi bir açma döngüsü, son adımı eval(_).
eval(_) yerine bir atama koyulduğunda dosya kendi kaynağını geri
veriyor:
vm.runInContext(scriptBody.replace(/eval\(_\)$/, '__out=_'), c);
// c.__out → paketlenen kaynak
Geri gelen kaynak plain dosyanın kendisi değil. RegPack'in aradığı kısalık değil
tekrar, o yüzden paketlenen kaynak plain'den daha uzun: L3 numerical'da
2.145 bayt yerine 2.238, L3'da 2.796 yerine 3.368. Aradaki fark üç başlıkta
toplanıyor — açılan alias'lar, gömülen fonksiyonlar, uzatılan operatörler — ve
L3'da bir dördüncüsü var: eskimiş işaretleme.
Alias'lar ve gömülen fonksiyonlar
Dosya
Açılan alias
Gömülen fonksiyon
Fonksiyon kalan
numerical_packed.html
a=Math.abs3 yerQ='indexOf'4 yer
VZADF
GlLCMIHXJ
dom_packed.html
a=Math.abs3 yerQ='indexOf'5 yerN='innerText' açılmayan tek alias
ZH
GVlLCMIXJAFSd
X ile J iki dosyada da fonksiyon kalıyor; ikisi de özyinelemeli,
yani gömülmeleri mümkün değil. V ile l ise ayrışıyor. l'nin gövdesi
tek bir V çağrısı, yani birini gömmek ötekinin çağrı yerlerini çoğaltıyor:
L3'da ikisi de gömülmüyor: l 7 çağrı yerine çıkıyor,
V 3 çağrı yeriyle kalıyor. L3 numerical'da tercih öbür yöne
gidiyor — l fonksiyon kalıyor, düz kaynakta 3 çağrı yeri olan
V gömülüyor.
Operatörler doğruluk değeri beklenen yerlerde uzatılıyor:
L3 numerical'da && 25'ten 37'ye, || 10'dan 14'e;
L3'da 28'den 42'ye ve 14'ten 19'a. Motor tarafında renk çıkarma &1 yerine
%2 yazılıyor. Adlandırma korunuyor — reassignVars kapalı, iki
dosyada da globaller plain ile aynı harflerde.
Bir alias'ı açmanın kazandırması Math.abs'e özel; genel bir kural değil.
L3 numerical'da h, v, d,
y, P ve g alias'larının 64 alt kümesinin tamamı
denendi: hepsi paketi büyüttü. En ucuz açma tabana bir bayt bindiriyor, en
pahalısı yirmi altı. Ters yön de aynı kapıya çıkıyor — Math.abs'i geri
alias'lamak +3 bayt, l'yi açmak +11, C'yi açmak +21,
b.indexOf'u alias'lamak +15.
Sebebi token ekonomisi. Elle yazılmış bir alias bedava bir kısaltma: kaynakta zaten
kısa duruyor ve paketleyicinin tek karakterlik token havuzundan hiçbir şey yemiyor. Onu
açtığında ise kaynağa uzun bir metin giriyor, paketleyici o metni sıkıştırmak için bir
token harcıyor, ve harcanan token başka bir kalıptan eksiliyor.
Math.abs istisna, çünkü yalnızca üç yerde geçiyor ve açılmış hâli komşu
metinle birleşip — Math.abs( — tek başına üreteceğinden daha uzun bir ortak
kalıp veriyor.
Kural şu: bir alias ancak açılmış hâli komşu metinle birleşip daha uzun bir ortak
kalıp üretiyorsa açılır. Farklı bağlamlarda çok kez geçen saf değer alias'ları zaten
optimaldir, onlara dokunmak her seferinde kaybettirir.
L3 istisnası — eskimiş işaretleme
dom_packed.html, bugünkü L3.html'in paketlenmiş hâli değil;
L3'un daha eski bir neslinden geliyor. Saat ve
imza ikisinde de aynı — açılan kaynak
c=15,U=[600,600],N='innerText',e=Y=-1,t=1 ile başlıyor ve süreyi U[2]
damgasıyla, new Date ile ölçüyor. Fark işaretlemede: paketlenmiş sürüm
tahtayı ve çevresini document.write ile basıyor, ve bunu bugün düz sürümde
bulunmayan yöntemlerle yapıyor.
Konu
L3.html
dom_packed.html
İşaretleme
Statik HTML; tablo gövdesi T[N]= ile
Tamamı document.write şablonuyla
Zemin
body{background:#ccc}
<body bgcolor=#ccc>
Ortalama
#T{margin:auto}
<center>
Hücre aralığı
*{border-spacing:0}
<table cellspacing=0>
Hücre
<td>, td{width:54px}
<th width=54>
Kare rengi
E.background=
p.bgColor=
Kenar boşluğu
margin-left:36px
margin-left:36birimsiz — quirks
Metin yazma
innerHTML, N alias'ı üzerinden
innerText, yine N alias'ı üzerinden
Tablo
id=T taşıyor
id yok; gövde şablonun içinde
Yazı tipi ailesi
_
a
Globaller eşleşse de birkaç yerel ad ayrışıyor. Render fonksiyonu blok yerine ifade
biçiminde ve parametresi _ değil p; L ile
I'nin b.map indeksleri w'de birleşiyor;
G'nin ışın tarayıcısı S=Q=> yerine S=T=>;
render'ın J, E ve s geçicileri yok, hepsi
p üzerinden yürüyor.
İki gölgeleme var. S=T=>G'nin kendi T
parametresini örtüyor. d=p=> ise terfi paneli elementini render'ın tüm
gövdesi boyunca örtüyor, üstelik içerideki b.map((p,w)=>(p=this[w], … ))
aynı harfi bir kez daha alıyor. İkisi de zararsız: örtülen değer o kapsamda hiç
okunmuyor — panel yalnızca A ve S içinde geçiyor.
RegPack ayarları
Sürüm v5.0.4, GitHub tag'inden. Paketi npm'den almak yetmiyor: orada iş 5.0.1'de
durmuş ve o sürümün iki eksiği var. tokenCost yok, yani kaçış gerektiren bir
token'ın regexp sınıfında iki bayt tuttuğu hesaba girmiyor; ve modern sözdizimini
kaldıramıyor — 5.0.0'ın çıktısı ||= üzerinde
Unexpected token '=' ile patlıyor. Kaynağı depodan almak zorunlu.
5.0.4'ün bir huyu var: şablon literalinin içindeki boşluğu kırpıyor. Bu tek başına
sürümü elemek için sebepti; kaçış olarak yazılan bir boşlukla
çözüldüğü için artık değil.
Ayar
numerical_packed.html
dom_packed.html
reassignVars
false
crushGainFactor
0
3
crushLengthFactor
0
1
crushCopiesFactor
0
0
crushTiebreakerFactor
0
Kazanan aşama
stage 1 — packToRegexpCharClass
Sarmalayıcı değişkeni
Gikisinde de takas yok
BOM
yok
EF BB BF
Gövde / dosya
1.798 / 1.815
2.708 / 2.728
Paketlenmiş iki dosya, kaynağı ekrandan kopyalanabilecek dosyalar değil.
Sözlük anahtarları \x01–\x1f aralığındaki denetim
karakterleri; panoya, metin kutusuna ya da bir editörün satır sonu düzeltmesine
uğradıkları anda sessizce kaybolur ya da başka bir şeye dönerler, ve dosya bir daha
açılmaz. Bu ikisi için tek doğru yol indirmek. Düz sürümlerde böyle bir kısıt yok.
gain = count*(len - tokenCost) - len - 2*tokenCost // tokenCost: \ için 2, diğerleri 1
score = gainFactor*gain + lengthFactor*len + copiesFactor*count
Üç katsayı yalnızca oran olarak anlamlı; hepsini aynı pozitif sayıyla çarpmak
sonucu değiştirmiyor. Varsayılan oran (2/1/0, eşitlik bozucu 1)
iki dosyada da optimal değil, ve optimum dosyaya göre kayıyor. Tarama bunu gösteriyor:
L3 numerical'da bütün katsayıları sıfırlamak kazandırıyor, ama
L3'ta uzunluk katsayısı devrede kalmalı.
Sarmalayıcı satırı bir zorunluluktan geliyor: RegPack açma döngüsünün değişkenini
kendi kaynağında G diye sabit kodluyor, seçenekle değiştirilemiyor, ve
G bizde geometri fonksiyonu. Yine de iki dosyada da harfi takas etmeye gerek
kalmıyor: açma döngüsü eval(_)'den önce bitiyor, açılan kod aynı harfe kendi
değerini yazıyor, ve arada G hiç okunmuyor. Çakışma değil, sıra meselesi.
Bu ayarlarla açılan kaynak v5.0.4 ile yeniden paketlendiğinde iki dosya da
gönderilen hâlinin bayt bayt aynısı çıkıyor — sözlük anahtar sınıfı, aday sıralaması
ve açılan kaynak dahil. Paketleme bu haliyle yeniden üretilebilir bir adım.
gain / length / copies / tiebreak
numerical
dom
2 / 1 / 0 / 1RegPack varsayılanı
1.800
2.713
0 / 0 / 0 / 0
1.798
2.722
1 / 1 / 0 / 0
1.800
2.714
2 / 1 / 0 / 0
1.800
2.713
3 / 1 / 0 / 0
1.799
2.708
0 / 1 / 0 / 0
1.980
2.944
Kazanç dom'da 3/1'de bir platoya oturuyor: dört, beş ve altı
da aynı 2.708'i veriyor, o yüzden platoya ulaşan en küçük tam sayı seçildi. Uzunluk
katsayısını tek başına açmak (0/1) iki dosyada da felakete dönüyor —
kazancı hiç hesaba katmadan uzun kalıpları kovalamak demek.
reassignVars'ın kapalı olması bir tercihten fazlası. Açıkken RegPack tek
harfli isimleri sıklığa göre yeniden dağıtıyor ve hiç okunmayan Y'yi boşta
harf sanıp kaydırıyor — kaynağa gömülü imzayı bozarak. Kapalıyken
açılan kaynak girdiyle bayt bayt aynı kalıyor, hiçbir tanımlayıcı yerinden oynamıyor.
Bedeli ölçüldü: yeniden adlandırmaya izin verilseydi numerical 1.789,
dom 2.694 olurdu. Sadakatin bedeli 9 ve 14 bayt.
Seçeneğin adı varsNotReassigned. varsNotReassignedRaw
geçirilirse RegPack dönüşüm bloğunu atlayıp seçeneği sessizce yok sayıyor. Komut satırında
da bir tuzak var: --reassignVars false yazmak işe yaramıyor, çünkü
minimist değeri "false"metni olarak veriyor ve RegPack
onu doğru diye okuyor. Yeniden adlandırma açık kalıyor, imza sessizce uçuyor, ve dosya
birkaç bayt küçük çıktığı için fark edilmesi de kolay olmuyor. Ya
--reassignVars= yazmak ya da seçenekleri API üzerinden gerçek bir boolean
olarak geçirmek gerekiyor.
Kaynağa uygulanan zorunlu üç şey
Literal içindeki satır sonları \n kaçışına çevrilmiş olmalı.
RegPack girdiyi temizlerken [\r\n]+\s* deseniyle siliyor ve satır sonunun bir
literalin içinde olup olmadığına bakmıyor. Atlanırsa hata çıkmaz, çıktı geçerli JS kalır —
yalnızca tahta çizen sürümlerin render'ı kayar.
Şablon literalindeki anlamlı boşluk \x20 kaçışıyla yazılmalı.
Aynı temizleme, dom kaynağındaki şu boşluğu da yiyor:
onclick=S(id) id=${w} → onclick=S(id)id=${w}. Tırnaksız
onclick değeri S(id)id=0 hâline gelip id
özniteliğini yutuyor; altmış dört karenin hiçbirinde id kalmıyor,
this[w] tanımsız oluyor ve tahta hiç çizilmiyor. Node tarafındaki testlerde
document.write sahte olduğu için hiç görünmüyor — yalnızca tarayıcıda
patlıyor. Çözüm boşluğu kaynağa kaçış olarak yazmak:
<th width=54 onclick=S(id)\x20id=${w}>. RegPack kaçış dizisini boşluk
saymıyor. Kaynağı üç karakter uzatıyor, pakete birkaç bayt yansıyor, ve üretilen HTML
düz sürümdekiyle birebir aynı kalıyor. reassignVars'ın açık ya da kapalı
olması fark etmiyor; düzeltme her hâlükârda gerekli.
İşaretleme öneki paketlemenin dışında kalmalı.<script>'ten önce gelen statik HTML paketleyiciye girmiyor; girerse
açılan kaynak eval edilebilir bir JS olmaktan çıkıyor.
Token karakterleri
RegPack token'larını kaynakta hiç geçmeyen karakterlerden seçiyor. Kontrol
karakterleri neredeyse bedava, çünkü regexp sınıfında tek bir
\u0001-\u001f aralığına sığıyorlar; yazdırılabilir olanlar tek tek
sayılıyor. Kaynağa giren her yeni karakter havuzdan bir aday eksiltiyor, çıkan her
karakter bir aday ekliyor — L3'da render'ın parametresinin ve yazı tipi
ailesinin _'dan çıkması _'yi bu yüzden token yapıyor.
Ters yönün canlı örneği dom'da duruyor.
Şablon literaline yazılan \x20 kaynağa bir ters bölü
sokuyor, ve \ o anda havuzdan düşüyor — zaten kaçış gerektirdiği için
regexp sınıfında iki bayt tutan, yani en pahalı adaydı. Üç karakterlik bir düzeltmenin
bedeli baytla bitmiyor: token alfabesinden de bir harf götürüyor.
Dosya
Kaynakta geçmeyen yazdırılabilirler
Token sınıfı
numerical_packed.html
" # @ A F \ _ j
[\u0001-\u001f @ A j _ F #]sekizden altısı
dom_packed.html
" @ E H _
[\u0001-\u001f _ H E @]beşten dördü
Paketten sonra ne doğrulanıyor
Paketleme kaynağı okunamaz hâle getirdiği için, çıktının doğru olduğuna gözle karar
vermek mümkün değil. Her iki dosya için koşulan sınamalar şunlar:
Açılma.eval(_) yerine bir yakalama konularak açılmış kaynak
çıkarıldı ve paketleyiciye verilen girdiyle karşılaştırıldı: bayt bayt aynı.
İmza. Kaynağa gömülü c U N e Y t altılısının altısı da yerinde,
hiçbir tanımlayıcı kaymamış.
perft. Beş standart konum — başlangıç, kiwipete ve üç, dört, beş numaralı
konumlar — dördüncü derinliğe kadar
(perft: bir pozisyondan çıkan bütün hamle dizilerini sayan standart
doğruluk sınaması), tamamı geçildi.
Kilit adımlı denklik. Yeni paketle bir önceki nesle aynı girdi ve tıklama
dizisi koşturuldu, ve her yarım hamlede b, e, c,
t, n, $, o, z — dom'da
ayrıca terfi panelinin hidden durumu — karşılaştırıldı.
numerical'da 1.452, dom'da 3.501 adım: 0 ayrılma.
Üretilen işaretleme.dom'un document.write çıktısı
bir önceki nesille birebir aynı — <th width=54 onclick=S(id) id=0>
içindeki boşluk dahil.
Yeniden paketleyecek olan üçünü mutlaka koşsun: imza, üretilen işaretleme
ve bir önceki paketle kilit adımlı denklik. Boyut düşmüş olması tek başına hiçbir
şey söylemiyor — imzası uçmuş bir dosya da birkaç bayt küçük çıkar.
Uyumluluk
Eskimiş teknikler ve quirks modu
Motor tarafı yeni: BigInt literalleri, template literalleri, mantıksal atama
||=, varsayılan parametreler. Eski olan tek yer paketlenmiş
L3, ve orada da sebep tek kelimeyle bayt — HTML'in terk edilmiş sunum
nitelikleri CSS karşılıklarından kısa, ve tarayıcılar onları bırakmakta özgür değil:
spesifikasyon bugün de nasıl işleneceklerini tanımlıyor, çünkü webin büyük bölümü
onlara bağlı. Eskimiş olmak çalışmamak demek değil; doğrulayıcının şikâyet etmesi
demek.
Yükün tamamı dom_packed.html'de. Sekiz düz sürümün hiçbirinde tek
bir eskimiş öge veya nitelik yok; tahta basanlar da işaretlemesini statik yazıp ölçüyü
<style>'a bırakıyor. Aşağıdaki tablonun “modern karşılığı” sütunu bugünkü L3.html'in fiilen
yaptığı şeyin dökümü; “kaynakta” sütunu ise
ondan önceki neslin.
Eskimiş olanlar
Ne
Kaynakta
Durumu
Modern karşılığı
Bedeli
<center>
<center>
Eskimiş öge
text-align:center, * kuralına
+10
bgcolor
<body bgcolor=#ccc>
Eskimiş nitelik
body{background:#ccc}
+2
cellspacing
<table cellspacing=0>
Eskimiş nitelik
border-spacing:0, * kuralına
+3
width
<th width=54>
Eskimiş nitelik
width:54px, td kuralına
+2
.bgColor
p.bgColor='#c91'
Eskimiş DOM özelliği
E.background
+1
document.write()
tüm işaretlemenin basımı
Deprecated
Statik işaretleme + innerHTML
—
with
yalnızca packed, RegPack açıcısı
Strict mode'da yasak
yok
—
Bedel sütunu kaynağa yazılacak en kısa karşılığın maliyeti, paketleme öncesi —
toplamı 18 bayt. Pakete yansıması doğrusal değil: değişiklik hem paketleyicinin
gördüğü tekrar dizilerini bozuyor, hem de kaynağa giren her yeni karakter
token havuzundan bir yuva eksiltiyor. Bugünkü düz
L3.html'in ölçülen en ucuz paketlenmiş hâli 2.891 bayt2.470 paketlenmiş script + 404 statik işaretleme + 17 etiket; gönderilen
eskimiş nesil 2.728'de duruyor, ve <!DOCTYPE html>’in 15 baytı
paketin dışında kaldığı için doğrudan bunun üstüne biniyor.
Eskimiş yöntem kullanmak yine de bir tercih değil, yalnızca yer kalmadığında
başvurulan bir şey — yer açılır açılmaz düz sürüm altısını da bıraktı.
document.write, with ve eval
document.write() üçünün içinde en tartışmalısı. MDN doğrudan deprecated
işaretliyor ve spesifikasyonun kendisi de uyarı kutusu taşıyor: metot ayrıştırıcı
çalışırken belgenin durumunu değiştirebiliyor, kaynağa karşılık gelmeyen bir DOM
üretebiliyor, ve sayfa yüklendikten sonra çağrılırsa belgeyi siliyor. Chrome'un 2016'dan
beri uyguladığı müdahale ise dar kapsamlı: yalnızca yavaş bağlantıdaki kullanıcıda, üst
düzey belgede, document.write() ile enjekte edilmiş ayrıştırıcıyı
bloklayan <script> ögelerini çalıştırmıyor.
dom_packed.html script değil işaretleme basıyor ve bunu ayrıştırma sırasında
yapıyor, yani müdahalenin dışında kalıyor. Yine de ayrıştırıcıyı bloklamaya devam ediyor.
Bugünkü L3.html ise document.write'ı hiç çağırmıyor:
işaretlemesi statik, yalnızca tablo gövdesi innerHTML ile yazılıyor.
with iki paketlenmiş dosyanın ikisinde de var, ama bizim yazdığımız kod
değil: RegPack'in açma döngüsü with(_.split(…)) kalıbını kullanıyor.
Pratik sonucu şu — with strict mode'da sözdizimi hatası olduğu için
paketlenmiş dosyalar ES modülü olarak yüklenemez ve başlarına
'use strict' konamaz. Plain sürümlerde böyle bir kısıt yok.
eval eskimiş değil, ama açma döngüsünün son adımı. Sonucu bir uyumluluk
sınırı: unsafe-eval içermeyen bir Content Security Policy altında
paketlenmiş sürümler çalışmaz. Bu sayfadaki başlatıcı bunu varsaymak yerine sınıyor —
iframe aynı kaynaktan yüklendiği için aynı politikayı alıyor, dolayısıyla sayfanın kendi
testinin cevabı iframe'in de cevabı.
Quirks modu
Dokuz HTML dosyasının hiçbirinde <!DOCTYPE html> yok, dolayısıyla
hepsi quirks modunda açılıyor. Bu bir gözden kaçma değil, dosya başına 15 baytlık bir
tercih. Üç JavaScript dosyası — engine.js, engine_4x.js ve
engine_onlyMoveGenerator.js — için ne DOCTYPE'ın ne de render modunun bir karşılığı
var.
Yaygın bir yanılgıyı burada ayırmak gerekiyor: eskimiş sunum nitelikleri quirks
moduna ait değil.bgcolor, width ve
cellspacing, spesifikasyonun sunum ipucu olarak tanımladığı niteliklerdir ve
standart modda birebir aynı çalışırlar. <!DOCTYPE html> eklemek
yukarıdaki tablonun hiçbir satırını düzeltmez; ikisi bağımsız konular.
Sık anılan iki klasik quirks davranışı da burada hiçbir şeyi değiştirmiyor.
Birincisi kutu modeli: eski IE modelinin quirks'te hâlâ yürürlükte olduğu
yaygın bir inanış, ama modern tarayıcılarda tablo hücresi iki modda da içerik
kutusu. Ölçtük: <th width=54> ve 1px varsayılan dolguyla hücre
56×56 çıkıyor — yani 54 içerik artı iki yandan birer piksel, tam olarak
standart modun vereceği sayı. İkincisi tabloların gövdeden yazı tipi miras almaması;
o da etkisiz, çünkü dosyaların hepsi evrensel seçiciyle başlayıp hücrelere ayrıca
kendi kuralını veriyor.
L3.html quirks modunda açılıyor ama ona dayanmıyor. Dosyada
birimsiz uzunluk yok, #'siz renk yok, sunum niteliği yok; iki mod arasındaki
tek fark yok: yukarıda ölçülen 56×56 iki modda da aynı çıkıyor, telafi edilecek bir
rakam bile kalmıyor. <!DOCTYPE html> istenirse 15 bayt
karşılığında bugün eklenebilir, tek bir pikseli bile oynatmadan.
Quirks'e gerçekten dayanan tek dosya dom_packed.html.
Eskimiş sanılanlar
Denetimlerde sık sık işaretlenen ama aslında güncel olan kullanımlar da var; listeye
almamızın sebebi tekrar sorulmasın.
innerHTML, setInterval, charCodeAt. Üçü
de eskimiş değil. codePointAtcharCodeAt'in yerine geçen değil,
yanına gelen bir metot; vekil çiftleriyle uğraşmayan bir kodda charCodeAt
doğru seçim.
V.key. Klavye olaylarında eskimiş olanlar
keyCode ve which; ikisi de hiçbir sürücüde geçmiyor. Bu
tesadüf değil, ödenmiş bir fiyat: onkeyup=V=>V.which!=13 yazmak
V.key!='Enter' yazmaktan dosya başına 3 bayt kısa. Kanca
yalnızca L3 input ailesinde olduğu için 6 bayt masada bırakıldı —
L3 prompt ile L3 numerical girdiyi bloklayan bir
pencereden aldığı için olay kancasına hiç ihtiyaç duymuyor.
Named access. Element id'sinin global değişkene dönüşmesi
spesifikasyonda tanımlı, ama açıkça "uyumluluk için" kaydıyla. Kaldırılmayacak, çünkü
kaldırmak sayısız sayfayı kırar. Burada bilinçli bir kaldıraç: L3'da
this[w], metin sürücülerinde T ve x.
innerHTML ve innerText. İkisi de 1997'de IE4'ün
tescilli uzantısı olarak doğdu ve sonradan standarda alındı — innerText on
sekiz yıl standart dışı kaldıktan sonra, 2016'da. Bugün ikisi de güncel; hiçbiri eskimiş
değil. Baştan beri standart olan textContent ise kullanılmayan tek seçenek,
çünkü 2 bayt daha pahalı. Denetimler innerHTML'i refleksle işaretler,
haklı olarak: işaretleme ayrıştırır, yani güvenilmeyen metin oraya ulaşırsa XSS olur.
Burada ulaşmıyor. Kullanıcının yazdığını ekrana geri basan tek sürücü
L3 input_blindfold, ve o zaten ayrıştırmayan innerText'i
kullanıyor. L3 yalnızca tıklama alıyor; L3 input'un render
şablonuna da girdi metni girmiyor.
DOM0 olay atama.onclick= nitelikleri ve
onkeyup= ataması addEventListener'ın öncülü, ama eskimiş
değil. Tek kısıtı tek dinleyiciye izin vermesi — ikinci bir atama birincinin üzerine
yazar.
Ayrıştırma
Sürücü hamleyi nasıl okur
Motor altı çağrı sunuyor ve hepsi kare indeksiyle konuşuyor:
A(i,f,u) için i ve f 0–63 aralığında birer sayı
olmalı. Kullanıcı ise harf, rakam ya da tıklama veriyor. Aradaki dönüşüm sürücünün işi ve
iki yönlü: içeri hamle, dışarı sonuç. Giriş biçimlerinin anlatıldığı
yerde yalnızca kullanıcı tarafı var; burada kodun kendisi.
Üç ayrı ayrıştırma var, üçü de tek satır. Ortak noktaları şu: hiçbiri doğrulama yapmıyor.
Geçersiz bir giriş bir hata dalına değil, L(i) listesinde bulunmayan bir sayıya
dönüşüyor — ve orada sessizce düşüyor.
UCI üçlüsü — k
L3 input ailesi ile L3 prompt, üçü de aynı ayrıştırıcıyı
taşıyor. Bir kare iki karakter: dikey harfi ve yatay rakamı.
k = V => K.charCodeAt(V) + 8 * K[++V] - 105
Üç numara üst üste binmiş. Birincisi tür karışımı.K.charCodeAt(V) sayı döndürüyor, K[++V] ise tek karakterlik bir
string. 8* ile çarpılınca JavaScript o string'i sayıya zorluyor — ayrı bir
parseInt gerekmiyor, ve çarpanın kendisi zaten yatayın indeksteki ağırlığı.
İkincisi yan etki.++V parametreyi yerinde artırıyor, böylece tek
fonksiyon çağrısı iki karakteri birden okuyor. Çağıran taraf yalnızca çiftin başlangıcını
veriyor: k(0) kaynak kare, k(2) hedef kare.
Üçüncüsü sabit.105 gökten inmedi: 'a' karakterinin kodu
97, birinci yatayın katkısı 8*1. İkisinin toplamı a1'i sıfıra oturtuyor, ve
a1=0 indekslemesi zaten motorun düzeni.
e2e4
charCodeAt
8 * rakam
−105
Kare
k(0) → e2
'e' = 101
8*'2' = 16
−105
12
k(2) → e4
'e' = 101
8*'4' = 32
−105
28
Girdi önce toLowerCase()'den geçtiği için E2E4 ile
e2e4 aynı kareyi veriyor. Büyük harf duyarsızlığı ayrı bir kural değil, tek
bir çağrının yan ürünü.
İki basamaklı sayı — L3 numerical
L3 numerical'ın k'si yok, çünkü ihtiyacı yok. Kareler zaten
1–64 arası sayı, ve dönüşüm satır içinde bitiyor.
i = K[0] + K[1] - 1
f = K[2] + K[3] - 1
+ burada toplama değil birleştirme: K[0] ve
K[1] tek karakterlik string'ler, '5'+'6''56' ediyor.
Hemen ardındaki -1 iki iş birden yapıyor — string'i sayıya zorluyor ve 1–64
aralığını 0–63'e kaydırıyor. Aynı ifadede önce string, sonra sayı davranışı.
Numaralandırmanın 0–63 yerine 1–64 seçilmesinin bedeli tam olarak
bu iki karakter. Karşılığında kullanıcı sıfırdan saymak zorunda kalmıyor.
Tıklama — iki fazlı seçim
L3'da ayrıştırılacak metin yok; onun yerine bir durum makinesi var.
Altmış dört karenin her biri onclick=S(id) taşıyor ve
named access sayesinde kendi id'sini geçiriyor.
S = u => z || p.hidden && (u ^= K,
~i && ~L(i)[Q](u)
? (f = u, b[i]>>1 == 4 & u%56 < 8 ? p.hidden = 0 : A())
: b[u] && b[u]%2 == t && d(i = f = u))
u ^= K ekran karesini tahta karesine çeviriyor. Render tahtayı her
zaman sırası gelen tarafa göre basıyor, yani ekrandaki sıfırıncı hücre siyahın sırasındayken
a1 değil. K = t*49+7 maskesi bunu geri alıyor: beyaz için ^7
dikeyleri, siyah için ^56 yatayları çeviriyor. Tıklama handler'ının ilk işi bu,
çünkü sonraki her şey tahta indeksiyle konuşuyor.
~i seçim bayrağı. Seçim yokken i-1, ve
~(-1) sıfır — yani ayrı bir bayrak değişkeni yok, i'nin kendisi
hem seçili kareyi hem "seçim var mı" sorusunu taşıyor.
İki faz. Seçim varsa ve tıklanan kare L(i) listesindeyse hamle
yapılıyor. Değilse — ya da hiç seçim yoksa — tıklanan karede sırası gelen tarafın taşı varsa
seçim oraya taşınıyor. Yani yanlış bir kareye tıklamak hata vermiyor, sessizce seçimi
değiştiriyor ya da hiçbir şey yapmıyor.
Seçim i = f = u ile kuruluyor, ikisi birden. Sebebi render: yeşil
çerçeve f'yi işaretliyor, dolayısıyla seçim anında seçilen kareyi, hamleden
sonra ise varış karesini gösteriyor. Tek değişken iki farklı anlam taşıyor ve ikisi de
doğru.
Legal hedeflerin gösterilmesi
Bir taş seçilince gidebileceği kareler altın rengine dönüyor. Bunu ayrı bir mekanizma
yapmıyor — render zaten L'yi çağırıyor.
s = L(i) altmış dörtlük döngünün dışında. İçeride bırakılsaydı her
kare için baştan hesaplanırdı — motorun en sıcak yolu altmış dört kez.
Dışarı alınması s harfini motor–sürücü sınırına taşıyan tek sebep, ve
gölgeleme tablosunda bu yüzden görünüyor.
Hiçbir şey seçili değilken i-1, ve L(-1) boş dizi
dönüyor: b[-1]undefined, taş tipi sıfır, G hiçbir
hedefte geçmiyor. Yani "seçim yok" durumu için ayrı bir dal gerekmiyor, boş liste zaten
hiçbir kareyi boyamıyor.
Üç görsel kanal üst üste biniyor ve üçü ayrı soruya cevap veriyor: altın gidilebilir
kareler, yeşil çerçeve seçili ya da son varış karesi, beyaz metin rengi taşın
rengi. Hiçbiri diğerini ezmiyor çünkü farklı özelliklere yazıyorlar —
bgColor, outline ve color.
Terfi panelinin araya girmesi
Hamle legal olduğunda S doğrudan A() çağırmıyor, önce bir soru
soruyor: b[i]>>1 == 4 & u%56 < 8 — seçili taş piyon mu ve varış karesi
birinci ya da sekizinci yatayda mı. Öyleyse p.hidden = 0 ile panel açılıyor ve
hamle orada askıya alınıyor. f zaten yazılmış durumda; panel düğmeleri
A(3), A(2), A(1), A(6) ile taş türünü
getirip hamleyi tamamlıyor.
Askıya alma ayrı bir durum değişkeni gerektirmiyor ama tahtayı kilitlemeyi gerektiriyor,
ve z || p.hidden && guard'ı bunun için duruyor. Guard olmadan panel
açıkken kendi taşlarından birine tıklamak S'nin ikinci dalını çalıştırır,
i = f = u olur ve panel hâlâ açıktır; düğmeye basıldığında
M(i,f)i == f ile koşar, b[f] = p'nin hemen ardından
b[i] = 0 aynı kareyi siler — taş buharlaşır ve sıra karşıya geçer. İkisi de
satrançta yok. p.hidden normalde truthy, yalnızca panel açıkken sıfır olduğu
için guard tam o aralıkta kapatıyor. Terfi kararı verilene kadar hamle yarım kalıyor —
notasyon bölümünde anlatılan davranışın kaynağı bu satır.
Panelin ikinci bir kapısı var: terfi düğmeleri A'yı S'den
geçmeden çağırıyor, yani S'nin guard'ı onları kapsamıyor. Panel açıkken oyun
biterse — teslim, iddiaya bağlı beraberlik, ya da bayrak — F veya zamanlayıcı
z'yi yazar, panel açık kalır (p.hidden'ı yalnızca A
kapatıyor), ve düğmeye basılınca Az'yi tazeden hesaplayıp
üzerine yazar: oyun bitmişken kaldığı yerden devam eder. A = T => z || d(…)
dört giriş noktasını — S, A, F ve zamanlayıcı —
aynı kapıya bağlıyor.
Beraberlik radyosu — tek eleman, iki bit
L3'un beraberlik denetimi bir radyo düğmesi, ve tek başına
teklif bitlerinin ikisini birden gösteriyor.
F = T => z || d(z = T ? j('R') : D(t), x.checked = o & 2-t)
x.style.outline = o & t+1 && 'solid #ff0'
… x.checked ? D() : Z(o = 0) // A içinde
o'nun sıfırıncı biti beyazın açık teklifi, birinci biti siyahınki. Radyo
ikisini iki ayrı kanaldan okuyor:
İşaretli olması senin teklifin.o & 2-t — sıra beyazdayken
2-1 = 1, yani sıfırıncı bit; siyahtayken 2, yani birinci bit. Her
zaman sırası gelen tarafın biti.
Sarı çerçeve rakibin teklifi.o & t+1 — sıra beyazdayken
2, yani siyahın biti. Tam tersi. Aynı eleman, aynı o, iki farklı
maske: biri "ben teklif ettim", öteki "bana teklif edildi".
Hamleyle birlikte gönderiliyor. Radyo işaretliyken oynanan hamle
x.checked ? D() : Z(o=0) dalına düşüyor. D()argümansız çağrıldığı için bitlere dokunmuyor, yalnızca
değerlendiriyor — teklif zaten F() tarafından yakılmıştı. İşaretli değilse
o = 0 bütün teklifleri söndürüyor: hamle yapmak açık teklifi geri çekiyor.
Aynı fonksiyon terk düğmesine de bakıyor: F(1) argümanlı çağrı,
F() argümansız. Bir harf iki kontrolü birden taşıyor.
Terfi: üç kodlama, tek varsayılan
Üç sürücü terfi türünü üç ayrı biçimde alıyor, ama üçü de aynı yere varıyor:
M'nin üçüncü parametresine, yani taş tipine.
Sürüm
İfade
Vezir
Kale
Fil
At
UCI ailesi
{r:2,b:1,n:6}[K[4]]
—
r
b
n
L3 numerical
'126'[K[4]]
—
1
0
2
L3
A(3)A(2)A(1)A(6)
♕
♖
♗
♘
Vezir sütunu boş, ve boş olması kasıtlı. İlk iki ifade tanımadığı bir karakterde
undefined döndürüyor — nesne aramada karşılığı yok, dizgide indeks aralık
dışı. undefined bir argüman olarak geçirildiğinde JavaScript
varsayılan parametreyi devreye sokuyor, ve M'nin varsayılanı
u=3: vezir.
Yani "terfi harfi yazmazsan vezir olur" davranışı bir denetimden değil, bir dil
kuralından geliyor. Üç sürücüde de aynı, ve üçünde de sıfır bayt tutuyor.
L3'un düğmeleri istisna: orada bir varsayılan yok çünkü kullanıcı zaten
dördünden birini seçmek zorunda. Panel açıldığında hamle askıda kalıyor, yani
undefined yolu hiç kurulmuyor.
Ters yön — sonucu yazmak
Motor bir sayı döndürüyor: 1–15 arası bir kod. Ekrana ne yazılacağı
sürücünün işi, ve burada da üç ayrı yol var.
L3 numerical hiç çevirmiyor — alert(z) rakamın kendisini basıyor.
Baştan sona sayısal kalan bir arayüzde kodu harfe çevirmek tutarsızlık olurdu.
L3 ise hiç sayı üretmiyor: Z, D ve sonuç kurucusunu
kendi sürümüyle değiştiriyor, dizgi doğrudan çıkıyor. Çeviren yalnızca üç UCI sürümü, ve
tek satırda.
w = z => z && ' WBS75D53D'[z] + ' ##M5RP0RA'[z]
İki dizgi paralel duruyor ve aynı indeksle okunuyor. z'inci karakterler yan
yana gelince iki harfli kod çıkıyor — ayrı bir tablo, nesne ya da dal yok.
z
1
2
3
4
5
6
7
8
9
' WBS75D53D'
W
B
S
7
5
D
5
3
D
' ##M5RP0RA'
#
#
M
5
R
P
0
R
A
Sonuç
W#
B#
SM
75
5R
DP
50
3R
DA
Sıfırıncı karakter ikisinde de boşluk, ama oraya hiç varılmıyor: z &&
kısa devre yapıyor ve oyun sürerken w sıfır döndürüyor. "Devam ediyor" hâli için
ayrı bir dal harcanmıyor.
Dizgiler 10 karakter, yani kapsam 1–9 ile sınırlı. Süre düşmesi ve terk sonuçları
buradan hiç geçmiyor; onları sürücünün kendi
F=k=>(Xm=2e4,Xs={},X(t,15))?k+'M':'WB'[t]+k fonksiyonu kuruyor — 'T' ya da
'R' alıp matın imkânsız olup olmamasına göre önek veya
sonek ekliyor. İki ayrı mekanizma, çünkü ikisinin girdisi ayrı: biri kod, öteki harf.
Dokuz sayısı da tesadüf değil. Motorda anlaşmalı beraberlik 15, bu üç sürümde
9. On beşe kadar uzanan bir dizgi çifti on iki bayt daha tutardı; dokuz ise 75 hamle
kuralından sonraki ilk boş yuva. İki kodlamanın ayrışması bir tasarım
tercihi değil, bu tabloya sığma zorunluluğundan doğuyor.
Motor tarafındaki yuva dağıtımı da aynı sıkışmanın izini taşıyor: literal kodlar tek
basamağa sığsın diye düşük yuvalar onlara bırakıldı, iki basamağa taşan tek kod en seyrek
görülen anlaşmalı beraberlik oldu. Kullanıcıya görünen çıktı üç şemada da aynı; fark
yalnızca iç katmanda.
Üç döngü biçimi
Ayrıştırıcıyı kimin çağırdığı da sürümden sürüme değişiyor, ve bu seçim saatin nasıl
tutulacağını doğrudan belirliyor.
Sürüm
Yapı
Saat
L3 prompt L3 numerical
Bloklayan döngü — for(;!z;), her tur bir prompt()
Pencere kapanınca ölçülüyor: new Date - d. Sayaç işlemiyor, geçen süre hesaplanıyor
L3 input ailesi
Olay güdümlü — onkeyup, Enter tuşunda
Damgadan besleniyor: U[2] son yoklamanın Date'i, aradaki fark saate işlenir; setInterval saniyede 10 kez yokluyor
L3
Kare başına onclick, artı terfi ve beraberlik düğmeleri
Damgadan besleniyor: U[2] son yoklamanın Date'i, aradaki fark saate işlenir; setInterval saniyede 10 kez yoklar ve render'ı tetikler
Bloklayan sürümlerde saat gerçekten durmuyor, hiç işlemiyor — bir modal pencere
açıkken zamanlayıcı zaten çalışamaz. Onun yerine pencere kapanınca aradaki fark ölçülüp
düşülüyor. Sonuç aynı, ama L3 numerical'ın saatinin milisaniye tutmasının sebebi
bu: ölçüm doğrudan Date farkı olduğu için saniyeye yuvarlamanın anlamı yok.
engine.js'in kullanmadığı harfler sürücüye
kalıyor. Aşağıdaki
tablolar o harflerin beş sürücüde fiilen ne iş yaptığını, hangi adların
gölgelendiğini ve hangilerinin gerçekten boşta olduğunu veriyor. Oyuncu katmanı
olmayan L2 ile L2_aybars_2400, ve beraberlik kuralları hiç
bulunmayan L1 bu tabloların dışında. Bir harfi yeniden adlandırmadan önce
bakılacak tablolar burada; işin nasıl yapılacağı
Tuzaklar'da.
Dosya düzeni
Sekiz düz sürümün ve üç motor dosyasının hepsi aynı dört blok hâlinde, aynı
sırayla okunuyor. Önce durum: c, saatler, e,
t, tahta, sayaçlar ve tekrar tablosu — oyunun o andaki hâlini tarif
eden her şey. Sonra yardımcı: Q='indexOf' ile
a=Math.abs, satrançla ilgisi olmayan, yalnızca JavaScript olarak işe
yarayan kısaltmalar. Sonra motor: G V l L C M I H X J Z A D F,
kuralların tamamı — ne girdiden ne çıktıdan haberi var. En sonda
sürücü: ayrıştırma, biçimlendirme, çizim, etkileşim,
kanca ve başlatma.
engine.js üçüncü bloktan sonra duruyor. “Arayüzsüz”
ifadesinin somut karşılığı bu.
Blok sınırı iki yerde içeriğe göre kayıyor. Bir harf ne tuttuğuna göre bloğunu
buluyor: N saat olduğu dört sürümde durumda, 'innerHTML'
olduğu L3 ailesinde yardımcıda. Ve fonksiyonlarını gömen sürümlerde
motor bloğu erken kapanıyor — L3'da motorun A'sı arayüz
handler'ının içine katlanmış, L2 ile L2_aybars_2400'de
blok M'de bitiyor. L2_aybars_2400'ün bot katmanı sürücüden
sonra, beşinci bir blok gibi duruyor.
Paketlenmiş iki dosya bu düzenin dışında: alias'ları açık
yazıldığı için yardımcı blokları boş, fonksiyonlarının bir kısmı gömüldüğü için motor
bloklarında boşluklar var.
Sürücü globalleri
Yukarıdaki 15 harfi ve üç iki karakterli adı beş sürücü fiilen kullanıyor, ve aynı harf sürümden
sürüme farklı iş yapıyor. Aşağıdaki üç tablo plain sürümleri anlatır;
paketlenmiş sürümlerin adlandırması ayrı bir konu.
En sert ayrım L3 ile metin sürücüleri arasında. L3
işaretlemeye yazdığı için harflerinin çoğu element tutuyor; metin sürücülerinde aynı
harfler ayrıştırma ve zamanlama işi görüyor. K bunun en net örneği:
dört metin sürümünde ham girdi metni, L3'da tahtanın yön maskesi.
Harf
L3
L3 input
L3 input_blindfold
L3 prompt
L3 numerical
if
Seçili ve varış karesii=-1 seçim yok · f varış karesinde kalır
İki harf tek bir sürümde yaşıyor: u ve S, ikisi de
L3'da. E ile p ikişer sürümde, y
ikisinde. onkeyup yalnızca L3 input ailesinde —
sebebi döngü biçiminde.
L3 numerical'ın iki boşluğu tesadüf değil. J yok, çünkü
kareler iki basamaklı sayı olduğu için K[0]+K[1]-1 ile satır içinde
çözülüyor; w yok, çünkü bu sürüm sonuç kodunu sayı olarak döndürüyor ve
çevrim tablosuna hiç girmiyor. İkisi birlikte, dosyanın neden en küçük sürüm
olduğunun bir parçası.
x'in iki sürümdeki birimi aynı değil: L3 prompt geçen süreyi
saniye olarak tutar, L3 numerical ham milisaniye olarak — saati de zaten
milisaniye tuttuğu için.
Element id'leri
İşaretleme taşıyan üç dosya var, ve hepsi elementlerine JavaScript'ten
named access ile ulaşıyor: bir id, aynı adlı bir global gibi
okunabiliyor. Bu bir kaldıraç — getElementById yazmamak dosya başına
onlarca bayt.
64 kare; render this[w] ile dolaşır, tıklama S(id) ile kendi id'sini geçirir
input.html input_blindfold.html
T
Render hedefi, <pre>
input.html input_blindfold.html
x
Hamle girişi, <input>
prompt.html numerical.html
—
İşaretleme yok; ikisi de yalnızca <script>
L3 input ailesinin ikisi 28 baytlık işaretleme önekinin tamamını
oluşturuyor: <pre id=T></pre><input id=x>.
L3'un öneki 185 bayt ve o da statik; yalnızca 64 karenin
id'si sonradan doğuyor, T[N]= ile yazılan bir şablon
literalinden. Aynı altmış dört iddom_packed.html'de de kullanılıyor.
Bu tablodaki bir harfi yeniden adlandırırken işaretlemedeki
id= de taşınmalıdır — taşınmazsa named-access bağı kopar ve global
undefined kalır. Nasıl yapılacağı Tuzaklar'da.
Gölgelemeler
Aynı harfin iki ayrı şey olduğu on yedi yer var, ve hiçbiri hata değil. Sebebi
basit: motor DOM'a hiç dokunmuyor, gölgeleyen taraf da gölgelediğini hiç okumuyor.
İkisi aynı anda canlı değil — biri değerini kurduğunda öteki çoktan işini bitirmiş
oluyor.
Harf
Bir tarafta
Öbür tarafta
g
Motorun renk parametresi — G l L I H D F
L3'da alt saat elementi; dört metin sürücüsünde hamleyi yapan taraf
Her sürümde 'indexOf' alias'ı — motorda da öyle, o yüzden burada yeniden kullanım yok
S
G'nin ışın tarayıcısı
L3'da kare tıklama handler'ı
p
Motorun taş kodu — V L M I Z
L3'da terfi paneli elementi ve markup map'inin parametresi; L3 input_blindfold'da taş kodu cache'i
x
M'de komşu piyon karesi
L3'da beraberlik radyosu; L3 input ailesinde girdi kutusu; L3 prompt ve L3 numerical'da geçen süre
T
G'de tehdit modu bayrağı
L3 input ailesinde render elementi; L3'da A ve F'nin parametresi
u
Motorun V M A parametresi
L3'da render temp'i ve S'nin parametresi
d
G ve M'de mesafe parametresi
L3 input, L3 input_blindfold, L3'da render fonksiyonu; kalan ikisinde zaman damgası
K
Sürücünün ham girdi metni
L3 input render'ında replace offset'i; L3'da tahta yönü maskesi
E
L3 input_blindfold'da beraberlik = bayrağı
L3'da style temp'i ve saat map'inin parametresi
CO
Rok biti fonksiyonu / H'nin yokluk testi
L3 input ve L3 prompt tahta render'ındaki b.map((C,O)=>)
V
Motorun tehdit fonksiyonu
J içinde beyaz şahın karesi; Sk'nin ve onkeyup'ın parametresi
z
Sonuç globali
w'nin parametresi
s
A'nın kurduğu pozisyon anahtarı
V'nin renk parametresi; L3'un render'ında legal hedef listesi — s=L(i)
N
Motorun siyah saat alanı
L3 ailesinde 'innerHTML' takma adı — durum değil yardımcı; L3 input ailesinde hiç okunmuyor
z motorun kendi içinde kalıyor, sürücüye hiç çıkmaz.
s için bu artık geçerli değil: L3'un render'ı
L(i)'yi döngünün dışına alıp bu harfte tutuyor — içeride bıraksaydı
altmış dört karenin her biri için yeniden hesaplanırdı. Geri kalanı motor–sürücü
sınırında duruyor, ve büyük çoğunluğu yalnızca L3'da doğuyor —
işaretleme basan tek sürüm o olduğu için.
Dosyadaki en yoğun gölgeleme tek bir fonksiyonun içinde. J parametre
listesinde on sekiz yerel tanımlıyor ve çoğu motor adlarının üstünde duruyor —
W, B, V, K, L,
C, Z, P, Q, N,
S, X. Hiçbiri dışarıdan okunmuyor; kural tamamen kendi
bitboard'ları üzerinde çalışıyor, ve yeni global harfe hiç ihtiyaç duymamasının sebebi
tam olarak bu gölgeleme.
Güvenliğin şartı zamanlama: örneğin u, L3'un render
döngüsünde kare indeksi tutarken motorun M'si aynı harfi terfi türü
olarak kullanıyor. İkisi çakışmıyor çünkü render motor çağrısı bitmeden başlamıyor.
Bu yüzden gölgeleme listesi bir muafiyet değil, bir kısıt: yeni bir sürücü
globali eklerken önce bu tabloya bakılmalı.
Boşta harfler
Yukarıdaki dört tablo hangi harfin kullanıldığını anlatıyor. Bu tablo tersini:
her dosyada identifier olarak hiç doğmayan harfler. Yedi plain dosyanın kaynağı
parse edilip tek harflik bütün identifier'lar toplandı; aşağıdakiler geriye
kalanlar.
Dosya
Boşta harfler
Sayı
engine.js
Ej_
3
engine_4x.js
Ej
2
L3 numerical
Ej_
3
L3 prompt
Ej_
3
L3 input
Ej
2
L3 input_blindfold
j
1
L3
j
1
Sayı motorsuz sürücüden tam sürücüye doğru düşüyor: engine.js'in
arayüzü yok, üç harfi boşta; L3'da tek harf kalıyor. Yedi dosyanın hepsinde
ortak boşta olan tek harf var: j.
O harf de doğrudan kullanılabilir değil, ve sebebi
paketleyicide. RegPack açma döngüsünün sarmalayıcısı için
bir harf istiyor, ama token karakterlerini de kaynakta hiç geçmeyen
karakterlerden seçiyor. Yani iki ayrı test var: harf identifier olarak boşta
olacak, ve dosyada karakter olarak da hiç bulunmayacak. İkisi aynı şey
değil.
Harf
Kaç dosyada boşta
Boşta olduğu dosyalarda karakter olarak
j
7
Üçünde var — join çağrısı: L3, L3 input, L3 prompt
E
5
Üçünde var — engine_4x, L3 input, L3 numerical
_
3
Hiçbirinde yok
İki testi birden geçen harf yok. j yedi dosyanın hepsinde identifier
olarak boşta ama ikinci testten düşüyor: L3, L3 input ve
L3 prompt'ta join çağrısı harfi kaynağa sokuyor.
_ ikinci testi geçiyor — boşta olduğu üç dosyada tek bir karakter olarak
bile bulunmuyor — ama yalnızca o üç dosyada kullanılabilir; yedisi için tek bir ad
isteniyorsa ele gelmiyor. E daha da dar: beş dosyada boşta, üçünde
karakter olarak var.
Boşluk her zaman bu kadar dar değildi. X yedi dosyanın hepsinde boştaydı
ve tam bu yüzden ayrılmış tutuluyordu; 6.9 araması gelince o harfi
aldı. Aynı turda Jölü pozisyon taramasına,
K ile w de bu iki fonksiyonun yerellerine gitti. Bir zamanlar uç
örnek olan k — G'de yön vektörü olduğu için hiçbir dosyada boşta
değil, karakter olarak yedisinde birden geçiyor — artık istisna değil kural:
X, J, K ve w de iki testi birden
kaybediyor.
Tuzaklar
Tuzaklar ve dikkat edilmesi gerekenler
Golfed kodun her satırı bir taviz, ve tavizlerin çoğu görünmüyor. Aşağıdakiler
motorda ya da sürücülerde değişiklik yapacak birinin bilmesi gereken yerler:
dokunulduğunda sessizce bozulan, ya da ilk bakışta yanlış okunan noktalar.
Motorun kırılgan yerleri
S'de b[i]<1 şarttır, !b[i]
değil. Işın tarayıcı S=Q=>(i+=k)==f||b[i]<1&&S()
biçiminde. Boş kare için ikisi de aynı sonucu veriyor, ama tahtanın dışına
çıkıldığında b[i]undefined oluyor:
undefined<1 yanlış, !undefined doğru. İkincisi yazılırsa
tarayıcı hizalı olmayan her aday hamlede sonsuza kadar kendini çağırır.
M yan etkisizdir ve öyle kalmalıdır. Rok haklarına, sıraya ve
tekrar tablosuna dokunmaz, çünkü L'nin legallik denemesinin
içinde de koşuyor — L her adayı M ile oynayıp
l ile bakıp geri alıyor. Gerçek hamle için sürücü daima A
çağırır; hakları kırpan, sırayı çeviren ve R'ye yazan odur.
L'nin yedeği fonksiyon-lokal olmak zorunda. Her adayda
tahtayı, geçerken alma karesini ve yarım hamle sayacını p=[b,e,n] ile
yedekliyor, ve p map callback'inin parametresi — her iterasyonda yeni.
Global bir yedek çalışmaz, çünkü L dolaylı olarak kendini çağırıyor:
L → M → geçerken alma kontrolünde yine L.
İçteki çağrı dıştakinin yedeğini ezer ve tahta geri alınamaz.
Z'nin varlık sınaması L(i)+'' olmak zorunda,
L(i)[0] değil. İkisi de 3 karakter; takasın bayt kazancı yok,
yalnızca kaybı var. L hedefleri b.map ile artan sırayla
eklediği için a1 listede varsa daima ilk elemandır — ve a1 sıfırıncı
karedir, yani yanlış sayılır. L(i)[0] o taşı "hamlesi yok" diye okur.
Sırası gelenin oynayabilen bütün taşları a1'e gidebiliyorsa some yanlış
döner: motor, taraf çekte değilse hayalet pat, çekteyse hayalet
mat ilan eder. Perft bu hatayı görmez — hamle üretimi
kusursuz çalışıyor, yanlış okuyan sonlanma katmanı.
.length doğrudur ama 4 karakter pahalı.
İlk bakışta yanlış okunan yerler
Argümansız D() teklif bitlerine dokunmaz. Fonksiyon
D=g=>(o^=2-g,…) ile başlıyor; g verilmezse
2-undefinedNaN oluyor, o^=NaN ise
o^0, yani o. Sonuç "bitleri bozmadan yalnızca değerlendir"
demek ve kasıtlı: L3 hamle sırasında
x.checked?D():Z(o=0) ile bunu kullanıyor.
O'nun argümanı taş kodu, tür değil.H içinde
O=u=>!~b[Q](u+g) — "tahtada u+g kodlu taş yok"
demek. Kod tür*2+renk olduğu için O(4) kale,
O(8) piyon, O(12) at yokluğunu sınıyor. Vezir ve fil bu
testten hiç geçmiyor; ikisi I'nin kurduğu W materyal
ağırlığı ve m fil kare-rengi maskesi üzerinden değerlendiriliyor.
s tek ifadede hem anahtar hem sayaç.A'nın son
işi $=R[s=b+t+e+c]=-~R[s]. s pozisyon anahtarını kuruyor,
R[s] görülme sayısını taşıyor, -~x ise x+1 ile
aynı uzunlukta ama undefined üzerinde 1 verdiği için ilk
görülüşteki tablo girişini de aynı ifade yapıyor.
[f-1,f+1] dikey taşması hata değil, aynı satırın ikinci
koşulunda kapanıyor.M'nin geçerken alma satırı varış karesinin iki yatay
komşusuna bakıyor; f a ya da h dikeyindeyse komşulardan biri komşu
sıranın karşı ucuna taşıyor — f=24 (a4) için f-1=23 (h3).
Kenar denetimi eklemek gereksiz: taşan karede doğru renkte bir piyon dursa bile
~L(x)[Q](e) onu eliyor, çünkü yedi karelik yatay fark
G'nin piyon dalındaki h*v==1 şartını hiçbir zaman
sağlamıyor. Güvenlik komşu hesabında değil ardındaki legallik sınamasında; kenar
kontrolü ekleyen yalnızca bayt kaybeder.
Durum değişkenleri sürüme göre değişir
Durum tablosu motorun genel hâlini anlatıyor. Dört değişken
bunun dışına çıkıyor, ve bir sürücüden ötekine kod taşırken ilk kırılan yer burası.
Oyuncu katmanı olmayan iki sürümde teklif eden de kimse kalmadığı için 3'lü tekrar ve
50 hamle, iddia beklenmeden hükme bağlanıyor.
Ad
Genel hâli
Sapma
UN
Beyaz ve siyah saat, saniye — U=N=600engine.js · engine_4x.js · prompt
L3: U iki elemanlı bir dizi, [600,600], artı ilk yoklamada doğan U[2] zaman damgası; N saat bile değil, 'innerHTML' sabiti. L3 input ve L3 input_blindfold: U=[600,600], damga yine U[2]'de; N ise N=e=Y=-1 zincirinde, yani Y gibi hiç okunmayan bir imza harfi. L3 numerical: milisaniye, U=N=9E5. L2 ve L2_aybars_2400: saat yok, U hiç doğmuyor
o
Teklif bitleri
L2 ve L2_aybars_2400: teklif edecek kimse olmadığı için harf boşta
z
Oyun sonucu
engine, engine_4x ve L3 numerical'da sayı kodu; kalan bütün sürümlerde iki harfli string
L3'un dizisinde indeks renk bitinin tersi: U[0] beyaz,
U[1] siyah. Sürücü işleyen saati U[t^1] ile azaltıyor,
artışı U[t^=1]+=5 ile veriyor — sıra çevrildikten sonra hamleyi yapan
tarafa denk geliyor.
Bayrak testi de bu birimlere bağlı. İki prompt sürücüsü de bayrağı iki saatin
çarpımında arıyor, ama farklı eşiklerle: L3 numericalU*N<1, L3 promptU*N<=0 kullanıyor. Bu bir
üslup meselesi değil. L3 numerical saatini tam milisaniye tutuyor, yani
çarpım ya sıfır ve altında ya da en az bir; arası yok, <1 hem bir bayt
kısa hem güvenli. L3 prompt'un saati ise kesirli saniye; orada
<1 yazılsaydı 0,9 ve 1,0 saniye kalan bir
karambolde çarpım 0,90 çıkar ve iki bayrak da düşmeden süreden galibiyet
ilan edilirdi. Yani <1 yalnızca tam sayılı saatte doğru; birimi bir gün
değiştirirsen bayrak sessizce erken düşmeye başlar.
Terfi paneli üçüncü bir giriş yolu
L3'da tahtaya iki yoldan girildiği sanılır: kareye tıklamak ve
düğmeye basmak. Üçüncü bir yol daha var, ve gözden kaçması kolay. Terfi panelinin
dört düğmesi işaretlemede doğrudan A'ya bağlı —
<button onclick=A(3)> — yani S'nin içinden geçmiyorlar.
S'ye konan hiçbir bariyer onları tutmaz.
Bunun açtığı delik şuydu. Piyon son yatayı seçtiğinde panel açılıyor ve oyun
terfi kararını bekliyor. O aralıkta oyun başka bir yoldan bitebilir: süre
düşer, terk düğmesine basılır, ya da beraberlik radyosu açık haldeyken teklif kabul
edilir. Sonra oyuncu vezire tıklar. A korumasız olsaydı hamleyi tahtaya
yazar ve z'ye yeni hükmü basardı — bitmiş oyunun sonucunun
üstüne. Durum satırındaki sonuç siliniyor, oyun kaldığı yerden devam ediyordu.
Bariyer A=T=>z||d(…), 3 bayt. Aynı 3 bayt
F'de de duruyor: onsuz, oyun bittikten sonra terk ya da beraberlik
düğmesine tekrar basmak sonucu değiştiriyor.
Panelin ikinci hediyesi daha da beteri. Panel açıkken başka bir taşa tıklanırsa
seçim oraya kayar ama panel açık kalır; ardından vezire basılınca A
artık i ile f'nin aynı kare olduğu bir hamle koşar, ve
M'nin içindeki b[f]=… ile b[i]=0 sırası
yüzünden taş buharlaşır. Bunu tutan da S'deki
p.hidden&&: panel açıkken kare tıklaması hiç işlenmiyor.
L2 aynı deliği başka türlü kapatıyor, ve daha ucuza.
p.hidden=0 yerine p.hidden=z-- yazıyor:
z sıfırken z-- sıfır döndürüp z'yi
-1 yapar, yani panel açılırken oyun aynı anda “bitmiş”
işaretlenir ve S=u=>z||(…) kendini kilitler. Bedava, çünkü orada
saat de düğme de yok — z'yi A'dan başka kuran yok. O yüzden
L2'de bu tuzağın yalnızca ikinci yarısı görülebiliyordu.
L3'da aynı hile işlemezdi: z'yi kuran üç ayrı yol
olduğu için -1 onların sonucuyla çakışırdı.
L3.html: BOM ve kırılmaz boşluk
ASCII dışı glif taşıyan dosyalar var: tahta basanlar satranç taşlarını,
prompt.html tam genişlikli harfleri kullanıyor. Hepsinin başında BOM
duruyor ve gerekli — dosyalar <meta charset> taşımıyor, kodlamayı
bildiren tek şey o.
İkinci tuzak daha ince. Tahta basan sürümlerin render dizisinde boş karenin
karakteri normal boşluk değil, kırılmaz boşluk — ' ♝♜♛♟♚♞'
dizisinin ilk karakteri U+00A0. 0x20'ye çevrilirse işaretlemede
daraltılabilir boşluğa dönüşür, hücre boş sayılır ve tahta çöker. Bir editörün
"gereksiz karakteri temizlemesi" bunu tek başına yapar.
Y ve imza
Tahtayı kuran b'den önceki altı atamanın sırası tesadüf değil. Baş
harfleri okununca kaynağın altına atılmış bir isim çıkıyor — büyük-küçük harf
okunuşu değiştirmiyor:
c=15, U=N=600, e=Y=-1, t=1, b=[...], ...
// c U N e Y t → cUNeYt
Sıra saati olan bütün sürümlerde korunuyor — engine.js,
engine_4x.js ve beş önyüz — ve iki paketlenmiş dosya açıldığında da
aynen çıkıyor. Saati olmayan bir sürücüde okunuş doğmuyor — U ile
Y orada bulunmuyor.
Y imzanın içindeki tek ölü harf. -1 ile başlatılıyor
ve bir daha hiçbir yerde okunmuyor da yazılmıyor da; tek işlevi akrostişin okunuşunu
tamamlamak. Bu bir israf değil, akrostişin bedeli: beş harf zaten iş yapıyor, altıncısı
için iki bayt ödeniyor.
Aynı harf J'nin parametresi olarak da geçiyor, ama o globali gölgeleyen
ayrı bir bağlayıcı — zorlama anahtarı. İkisi çakışmıyor, çünkü akrostiş
bildirimin metnine bağlı, çalışma zamanına değil. Pratik sonucu şu: bir sürücü
Y'yi kendi işine alabilir, imza yine okunur.
Y'yi kaldırmak isteyen, i ve f'nin
başlatılmasını da taşımak zorunda: e=Y=-1,…,i=f=Y yerine
e=i=f=-1 zincirlemesi gerekiyor. İki bayt kazanılır, imza kaybedilir.
Zincirleme kurulmazsa fundefined kalır. Render
u^f ile "bu kare varış karesi mi" diye baktığı için ifade
u^0'a düşer, sıfırıncı kare de açılışta yeşil çerçeveli görünür.
Sessiz ve kozmetik bir hata; testten geçer, ekranda durur.
Yeniden adlandırma
Bir harfi değiştirmek göründüğü kadar mekanik değil; kör bir arama-değiştirme üç
yerde birden bozar.
String literal'ler. Harf gibi görünen ama identifier olmayan yerler:
input.html'deki 'a b c d e f g h' ve
'h g f e d c b a' sütun etiketleri; L3, L3 input
ve L3 prompt'taki 'C!' göstergesi ve son ikisindeki
'D?';
L3 prompt ve L3 input ailelerindeki 'T' süre kodu ve
${…}s saniye eki.
İşaretlemedeki id=.Element id tablosundaki her harf için markup da taşınmalı;
taşınmazsa named-access bağı kopar ve global undefined kalır.
Gölgelenen harfler. Aynı harf iki ayrı iş yapıyor olabilir —
gölgeleme tablosu hangi harfin nerede ne olduğunu veriyor.
Doğru yöntem kaynağı acorn ile parse edip yalnızca Identifier
düğümlerini değiştirmek; id= taşımasını ayrıca elle yapmak. Literal ve
template parçaları böylece hiç görülmemiş olur.
Değişiklikten sonra
Perft koşularının tamamı dış ölçüye bakar: ölçü FIDE'nin
kendisi, motorun geçmişi ilgisiz. Kaynağa dokunulduğunda ise soru değişir — kural doğru
mu değil, bu değişiklik bir şeyi bozdu mu? Ölçü artık bir önceki sürüm. Beş kontrol
bunun için:
Bayt nötrlüğü. Yeniden adlandırma sonrası dosya boyutu değişmemeli.
Yapısal eşdeğerlik. acorn ile parse edilip düğüm yapısı karşılaştırılır; tek
fark Identifier adları olmalı, hiçbir Literal
değişmemeli.
İç diferansiyel. Eski ve yeni sürüm ayrı vm context'lerinde
rastgele oyun oynar; her ply'da b c e t n o $ ve L(i)
karşılaştırılır.
Render doğrulaması. jsdom'da altmış dört hücrenin metni, rengi ve
outline'ı, durum satırı ve iki saat.
Tersinirlik. Paketlenmiş çıktı açıldığında pipeline kaynağını bayt bayt geri
vermeli.
Perft bu listede yok: hamle üretimine dokunulmadıysa gerekmiyor, dokunulduysa zaten
yukarıdaki koşuların tamamı baştan çalışır.