2K FIDE PromptChess
Welcome Hoş Geldiniz
This is a complete FIDE-compliant chess game that fits in 2048 bytes of HTML. Both players are human — there is no AI. The interface is the browser's prompt() dialog: the board is drawn with text, you type your move, and the game responds.
Open index.html in any browser. A dialog appears with the board and a clock. Type a move in coordinate notation (e.g., e2e4) and press OK.
2048 byte HTML'e sığan tam FIDE uyumlu bir satranç oyunudur. Her iki oyuncu da insandır — yapay zeka yoktur. Arayüz tarayıcının prompt() penceresi: tahta metinle çizilir, hamleyi yazarsınız, oyun cevap verir.
index.html'i herhangi bir tarayıcıda açın. Tahta ve sayaç içeren bir pencere açılır. Hamlenizi koordinat notasyonuyla yazın (örn. e2e4) ve Tamam'a basın.
The Notation in One Sentence Notasyon Tek Cümlede
Notation is extremely simple. Every move is always written the same way: starting square followed by target square. That's it. No piece letter at the front, no capture marker, no check sign.
Want to move the pawn on e2 to e4? Type e2e4. Want to bring out the knight from g1 to f3? Type g1f3. Capturing works the same way — you don't need a special symbol; just write where the piece is and where it goes. e4d5 captures the piece on d5 just as naturally as it would move to an empty square.
The one and only exception is pawn promotion (covered in §06). When your pawn reaches the last rank, you can optionally append a piece letter to choose what it promotes to. Everything else — castling, en passant, the dramatic checkmating attack — uses the same plain four-character form.
Notasyon son derece basittir. Her hamle her zaman aynı şekilde yazılır: başlangıç karesi, ardından hedef kare. Bu kadar. Başta taş harfi yok, alma işareti yok, şah çekme sembolü yok.
e2'deki piyonu e4'e götürmek mi istiyorsun? e2e4 yaz. g1'deki atı f3'e çıkarmak mı? g1f3 yaz. Almak da aynı şekilde çalışır — özel bir sembol gerekmez; sadece taşın nerede olduğunu ve nereye gittiğini yaz. e4d5, d5'teki taşı tıpkı boş bir kareye gitmek kadar doğal bir şekilde alır.
Tek ve yegane istisna piyon terfisidir (§06'da anlatılıyor). Piyonun son sıraya ulaştığında, neye terfi edeceğini seçmek için isteğe bağlı olarak sonuna bir taş harfi ekleyebilirsin. Geri kalan her şey — rok, geçerken alma, dramatik mat saldırısı — aynı sade dört karakterli formu kullanır.
Coordinates Koordinatlar
Squares are labeled by file (a–h, columns) and rank (1–8, rows). White starts at the bottom (rank 1). Black starts at the top (rank 8).
Kareler sütun (a–h) ve satır (1–8) ile etiketlenir. Beyaz alttan başlar (satır 1). Siyah üstten başlar (satır 8).
A move is two squares: from + to. The famous opening move e2e4 moves a white pawn from e2 (highlighted) to e4.
Bir hamle iki karedir: çıkış + varış. Ünlü açılış hamlesi e2e4, beyaz piyonu e2'den (işaretli) e4'e götürür.
Piece Symbols Taş Sembolleri
The board uses Latin letters. Uppercase = White, lowercase = Black. Empty squares show as _ (or _ in the deployed interface).
Tahta Latin harfleri kullanır. Büyük harf = Beyaz, küçük harf = Siyah. Boş kareler _ olarak görünür (yayınlanan arayüzde _).
| WhiteBeyaz | BlackSiyah | PieceTaş |
|---|---|---|
| K | k | KingŞah |
| Q | q | QueenVezir |
| R | r | RookKale |
| B | b | BishopFil |
| N | n | KnightAt |
| P | p | PawnPiyon |
How Pieces Move Taşlar Nasıl Hareket Eder
Standard FIDE chess rules apply. Below is a quick reference. Each diagram shows the piece (red) and the squares it can move to (pink).
Standart FIDE satranç kuralları geçerlidir. Aşağıda hızlı referans var. Her diyagram taşı (kırmızı) ve gidebileceği kareleri (pembe) gösterir.
e2e4.e2e4.g1f3.g1f3.f1c4.f1c4.a1a4.a1a4.d1h5.d1h5.e1e2.e1e2.Special Moves Özel Hamleler
e1g1 (white)(beyaz)e8g8 (black)(siyah)
e1c1 (white)(beyaz)e8c8 (black)(siyah)
e5d6
Q, R, B, or N. If you omit the letter and just write the regular four-character move, the pawn promotes to a queen automatically — no extra typing needed.Q, R, B veya N. Eğer harfi atlar ve sadece normal dört karakterli hamleyi yazarsanız, piyon otomatik olarak vezire terfi eder — ekstra tuşa gerek yok.a7a8 → queen (default)→ vezir (varsayılan)a7a8Q → queen (explicit)→ vezir (açık)h2h1N → knight→ at
Result Codes Sonuç Kodları
When the game ends, an alert displays one of three codes:
Oyun bittiğinde, bir uyarı penceresi üç koddan birini gösterir:
The Clock Saat
Each side starts with 900 seconds (15 minutes). The clock counts down during your turn — the seconds you spend in the prompt dialog are deducted from your total. The top of the board displays both clocks:
Her taraf 900 saniye (15 dakika) ile başlar. Sıra sizdeyken saat geri sayar — prompt penceresinde harcadığınız saniyeler toplamınızdan düşer. Tahtanın üstünde her iki saat görünür:
If either side reaches 0, the other side wins by time forfeit (W# or B#).
Eğer iki taraftan biri 0'a ulaşırsa, diğer taraf süreyi aşma ile kazanır (W# veya B#).
Input Behavior Girdi Davranışı
The engine silently rejects any input that isn't a legal move. The prompt simply reopens — your clock keeps ticking while you think.
- Empty input → prompt reopens
- Invalid format (e.g.,
xyz) → prompt reopens - Illegal move (e.g., your pawn jumping three squares) → prompt reopens
- Wrong side's move → prompt reopens
- Pressing Cancel → game exits
Promotion accepts any of Q, R, B, N (any case). Anything else (e.g., X, 5) silently falls back to Queen.
Motor, yasal hamle olmayan herhangi bir girdiyi sessizce reddeder. Prompt yeniden açılır — siz düşünürken saat işlemeye devam eder.
- Boş girdi → prompt yeniden açılır
- Geçersiz format (örn.
xyz) → prompt yeniden açılır - Yasak hamle (örn. piyonun üç kare zıplaması) → prompt yeniden açılır
- Yanlış tarafın hamlesi → prompt yeniden açılır
- İptal'e basmak → oyundan çıkar
Terfi Q, R, B, N harflerinden birini kabul eder (büyük veya küçük). Başka herhangi bir karakter (örn. X, 5) sessizce Vezir'e döner.
Sample Game — Scholar's Mate Örnek Oyun — Çoban Matı
A classic four-move checkmate. Type each move in order:
Klasik dört hamlelik mat. Her hamleyi sırayla yazın:
After the fourth move, the alert displays W# — White wins by checkmate.
Dördüncü hamleden sonra, uyarı penceresi W# gösterir — Beyaz mat ile kazandı.
What You'll See Ne Göreceksiniz
The prompt dialog displays the timer at the top and the board below. In the deployed index.html, fullwidth glyphs keep everything aligned regardless of your browser's font:
Prompt penceresi üstte sayacı, altında tahtayı gösterir. Yayınlanan index.html'de fullwidth karakterler, tarayıcı fontundan bağımsız olarak her şeyi hizalı tutar:
How we wrote chess
in 2048 bytes
Satrancı 2048 byte'a
nasıl yazdık
The starting problem Başlangıç sorunu
Chess has eleven separate rules in the FIDE Laws of Chess. Every legal move falls under one of them — how each piece moves, when castling is allowed, what counts as en passant, when a draw is automatic. Implementations that get all eleven right exist in many programming languages, in many sizes, but the vast majority weigh somewhere between five and fifty kilobytes. The question we set ourselves was simpler than it sounds: how small can a complete one get, in plain JavaScript, with no external libraries, no build step, and no game-engine framework?
The answer, after months of iteration, is 2048 bytes — including the visual layer that makes the board align in any browser. The bare engine, without the visual polish, is 1996 bytes. This essay is the story of how the byte count came down, what we sacrificed, what we kept, and what we tried that didn't work.
It is also, in places, an argument: that some choices that look like obvious wins (a bitboard, a 0x88 mailbox, a clever input parser) cost more bytes than they save in JavaScript, and that some choices that look like obvious losses (text input, a serif font, an alert box for results) turn out to be free.
Satrancın FIDE Satranç Kuralları'nda on bir ayrı kuralı vardır. Her yasal hamle bunlardan birine girer — her taşın nasıl hareket ettiği, rokun ne zaman caiz olduğu, geçerken alma sayılan şey, beraberliğin ne zaman otomatik olduğu. On birinin hepsini doğru yapan implementasyonlar pek çok programlama dilinde, pek çok boyutta vardır, ama büyük çoğunluğu beş ila elli kilobayt arasında bir yer tutar. Kendimize sorduğumuz soru, kulağa geldiğinden daha basitti: dış kütüphane olmadan, build adımı olmadan, oyun motoru framework'ü olmadan, sade JavaScript ile, tam bir tanesi en küçük ne kadar olabilir?
Cevap, aylarca süren yinelemelerin ardından, 2048 byte — tarayıcıda tahtayı hizalı tutan görsel katman dahil. Görsel cila olmadan çıplak motor 1996 byte. Bu deneme, byte sayısının nasıl düştüğünün, neyi feda ettiğimizin, neyi koruduğumuzun ve neyi denedik ama işe yaramadığının hikayesidir.
Aynı zamanda bir tartışma da içeriyor: bazı tercihler bariz kazançmış gibi görünüyor (bitboard, 0x88 mailbox, akıllı bir girdi ayrıştırıcı) ama JavaScript'te kazandırdıklarından daha fazla byte'a mal oluyor; ve bazı tercihler bariz kayıpmış gibi görünüyor (metin girişi, serif font, sonuç için alert kutusu) ama bedavaya çıkıyor.
Why prompt() and not a canvas Neden prompt(), canvas değil
The cheapest user interface in a browser is the one already there. The prompt() dialog needs no HTML, no CSS, no event listeners, no DOM access at all — you call it with a string, the browser puts up a modal, and when the user clicks OK you get a string back. The entire interaction is two function calls: one to show the board (which is also the input dialog), one to display the result (alert).
Compare this to the alternative. A <canvas> board would need getContext('2d'), calls to fillRect per square, event listeners for clicks, coordinate-to-square math, and a redraw routine after every move. Even at its most aggressive golfing, that overhead is several hundred bytes. A <table> board (the path Toledo took) is cheaper than canvas but still asks for innerHTML manipulation, onclick handlers, and a render function that walks 64 cells.
The prompt() route trades visual elegance for zero cost. The board renders as text inside the dialog. Input is typed, not clicked. Everything blocks while the dialog is open — including the page itself, which is why the clock cannot tick visibly between moves. We accepted that trade.
A side benefit emerged after the fact: prompt() works everywhere. Every smartphone, every smart TV with a browser, every e-reader. There is no canvas to fail to render, no touch-versus-click ambiguity. You type your move and press OK.
Tarayıcıdaki en ucuz kullanıcı arayüzü zaten orada olandır. prompt() penceresi HTML, CSS, event listener, DOM erişimi gerektirmez — onu bir string ile çağırırsın, tarayıcı bir modal açar, kullanıcı Tamam'a bastığında geri bir string alırsın. Tüm etkileşim iki fonksiyon çağrısıdır: biri tahtayı göstermek için (aynı zamanda girdi penceresi), biri sonucu göstermek için (alert).
Alternatifle karşılaştır. Bir <canvas> tahtası getContext('2d'), her kare için fillRect çağrıları, tıklamalar için event listener'lar, koordinat-kare matematiği ve her hamleden sonra yeniden çizim rutini ister. En agresif golf'lemede bile bu overhead yüzlerce byte. Bir <table> tahtası (Toledo'nun seçtiği yol) canvas'tan ucuz ama hâlâ innerHTML manipülasyonu, onclick handler'ları ve 64 hücreyi gezen bir render fonksiyonu ister.
prompt() yolu görsel zarafeti sıfır maliyete satıyor. Tahta dialog içinde metin olarak render edilir. Girdi yazılır, tıklanmaz. Dialog açıkken her şey bloklanır — sayfa dahil, bu yüzden saat hamleler arasında görünür şekilde ilerleyemiyor. Bu takasta razı olduk.
Sonradan ortaya çıkan bir yan fayda: prompt() her yerde çalışıyor. Her akıllı telefonda, tarayıcısı olan her akıllı TV'de, her e-kitap okuyucuda. Render edemeyen bir canvas yok, dokunma-tıklama belirsizliği yok. Hamleyi yazıp Tamam'a basıyorsun.
Why human vs. human, no AI Neden insan vs. insan, yapay zeka yok
Toledo's famous JS chess implementations all play against you. So do most of the well-known size-records: nanochess, micromax, the various 1K demos. They include a search routine — usually alpha-beta minimax to a shallow depth — that plays one side automatically. This adds value but it also adds bytes, often several hundred of them.
We made a different decision early on. The rules of chess are interesting in their own right; the question of whether this engine can mate that position is a separate, much larger problem. By focusing on the rules alone — making sure every legal move is accepted, every illegal one rejected, every termination correctly recognized — we kept the engine's purpose narrow. Two humans take turns. The program is the referee.
This freed somewhere between 500 and 1000 bytes, depending on how aggressively an AI is golfed. The trade is the obvious one: you need a friend, or you play both sides yourself. In return, every byte of the engine is in service of FIDE compliance, not in service of "what move would a computer play here."
Toledo'nun ünlü JS satranç implementasyonlarının hepsi sana karşı oynar. İyi bilinen boyut rekorlarının çoğu da öyle: nanochess, micromax, çeşitli 1K demolar. Bir taraf için otomatik oynayan bir arama rutini içerirler — genellikle sığ derinlikte alpha-beta minimax. Bu değer katar ama aynı zamanda byte da katar, çoğu zaman birkaç yüzü.
Erken bir aşamada farklı bir karar verdik. Satranç kuralları kendi başına ilginçtir; bu motorun şu pozisyonu mat edip edemeyeceği sorusu ayrı, çok daha büyük bir problemdir. Sadece kurallara odaklanarak — her yasal hamlenin kabul edildiğinden, her yasaklı olanın reddedildiğinden, her sonlanmanın doğru tanındığından emin olarak — motorun amacını dar tuttuk. İki insan sırayla oynar. Program hakemdir.
Bu, bir yapay zekanın ne kadar agresif golf'lendiğine bağlı olarak 500 ile 1000 byte arası bir yer açtı. Takas bariz: bir arkadaşa ihtiyacın var ya da iki tarafı kendin oynarsın. Karşılığında motorun her byte'ı FIDE uyumluluğunun hizmetinde, "burada bir bilgisayar hangi hamleyi oynar" hizmetinde değil.
The board as a string of characters Tahta, bir karakter dizisi olarak
The board lives in a single variable, s, which is an array of 64 single-character strings. Index 0 is a8, index 7 is h8, index 56 is a1, index 63 is h1. A white queen is 'Q', a black knight is 'n', an empty square is '_'. Case carries color: uppercase white, lowercase black.
The initial setup is built from a template literal:
The spread operator turns the 64-character string into an array of 64 single characters. The ${_.repeat(32)} in the middle is the 32 empty squares between the pawn ranks. The variable _ is set to the literal string '_' just above; that one-character variable name will be reused throughout the engine to mean "empty square."
This choice has consequences. Coordinates work out as x = i % 8, y = i >> 3. Bounds checks become natural 0 ≤ i < 64. To test color, p < 'a' tells us if a piece is white (uppercase letters all sort below lowercase in ASCII). To convert to uppercase for piece-type lookup, p.toUpperCase().
A more computer science-flavored alternative would be a 0x88 mailbox — a 128-element array where the high nibble encodes the rank and the low nibble encodes the file, with a single bit-test (i & 0x88) to check if you've walked off the board. This is what serious chess programs in C use, and the technique is genuinely elegant.
We tried it. The result was +795 bytes. Why? Because 0x88 wins by enabling fast directional iteration (walk a bishop diagonally until you hit the edge or a piece), and our move generator doesn't iterate directionally. It scans the whole board every time. So we pay the cost of the larger array, the cost of every (i & 0x88) guard, and the cost of explicit direction arrays — without using the speed advantage that justifies all of that. The technique is right for C and asm. It is wrong for our JavaScript.
1n << 8n instead of 1 << 8. Every operation costs roughly twice the bytes. Magic bitboards for sliding-piece attacks would add hundreds of bytes of precomputed tables. In a sub-2KB budget, this is a non-starter.
Tahta tek bir değişkende, s'de yaşar — 64 tane tek karakterli string'ten oluşan bir dizidir. İndex 0 a8'dir, indeks 7 h8'dir, indeks 56 a1'dir, indeks 63 h1'dir. Beyaz vezir 'Q', siyah at 'n', boş kare '_'. Büyük-küçük harf rengi taşır: büyük beyaz, küçük siyah.
Başlangıç düzeni bir template literal'den inşa edilir:
Spread operatörü 64 karakterli string'i 64 tane tek karakterden oluşan bir diziye çevirir. Ortadaki ${_.repeat(32)}, piyon sıraları arasındaki 32 boş karedir. Değişken _ hemen yukarısında literal string '_''a atanır; bu tek karakterli değişken adı motor boyunca "boş kare" anlamında yeniden kullanılacak.
Bu tercih sonuçlar getiriyor. Koordinatlar x = i % 8, y = i >> 3 olarak çıkıyor. Sınır kontrolleri doğal olarak 0 ≤ i < 64. Rengi test etmek için p < 'a' bize taşın beyaz olup olmadığını söyler (büyük harfler ASCII'de küçük harflerin altında sıralanır). Taş türü için büyük harfe dönüştürmek p.toUpperCase().
Daha bilgisayar bilimi tadında bir alternatif 0x88 mailbox olurdu — yüksek nibble'ın satırı, alçak nibble'ın sütunu kodladığı 128 elemanlı bir dizi, tahtanın dışına çıkıp çıkmadığını kontrol etmek için tek bir bit testi (i & 0x88) ile. Bu, ciddi C satranç programlarının kullandığı şey ve teknik gerçekten zarif.
Denedik. Sonuç +795 byte. Neden? Çünkü 0x88 hızlı yönlü iterasyonla kazanır (bir fili köşeye veya taşa ulaşana kadar çapraz yürüt), bizim hamle üreticimiz yönlü iterasyon yapmıyor. Tüm tahtayı her seferinde tarıyor. Yani daha büyük dizinin bedelini, her (i & 0x88) koruyucusunun bedelini ve açık yön dizilerinin bedelini ödüyoruz — tüm bunu meşrulaştıran hız avantajını kullanmadan. Teknik C ve asm için doğru. Bizim JavaScript için yanlış.
1 << 8 yerine 1n << 8n. Her işlem kabaca iki kat byte'a mal oluyor. Sliding-taş saldırıları için magic bitboard'lar yüzlerce byte önceden hesaplanmış tablo ekler. 2 KB altı bir bütçede başlamadan ölü.
Move generation: scan everything, ask later Hamle üretimi: her şeyi tara, sonra sor
The move generator G(d) returns the list of squares the piece on square d can legally move to. It works by looking at every other square on the board and asking, for each one, "can this piece reach there?" Sixty-three questions per move generation. It is, by the standards of serious chess engines, criminally slow.
It is also criminally short. The entire function fits in around 350 bytes, which is less than half of what a directional generator would cost in JavaScript. The reason is that all six piece types share the same outer loop and the same target-square evaluation. The logic of what makes a move legal differs only in a single chained ternary expression at the heart of the loop:
D and H are the absolute differences in file and rank. A knight legal-move check is "the product of those differences is 2" (because a knight's move is always 1×2 or 2×1). A king's is "both differences are less than 2." A bishop is "both differences are equal and the path is clear." A rook is "one of the differences is zero and the path is clear." A queen is "either of those." A pawn has its own clause, where direction matters.
The C(d, i) function — "is the path from d to i clear of obstructions?" — is itself just six lines, walking the line one step at a time and checking each intermediate square.
Speed doesn't matter here. The generator is called only when a human enters a move; there is no search tree, no millions-of-positions-per-second requirement. A move is generated, validated, and applied in well under a millisecond, and the player is the bottleneck anyway. Trading speed for size is the entire point.
Hamle üretici G(d), d karesindeki taşın yasal olarak gidebileceği karelerin listesini döndürür. Tahtadaki diğer her kareye bakarak ve her biri için "bu taş oraya ulaşabilir mi?" diye sorarak çalışır. Hamle üretimi başına altmış üç soru. Ciddi satranç motorları standartlarına göre, suç işleyecek kadar yavaş.
Aynı zamanda suç işleyecek kadar kısa. Tüm fonksiyon yaklaşık 350 byte'a sığıyor; bu, JavaScript'te yönlü bir üreticinin maliyetinin yarısından az. Sebep, altı taş türünün de aynı dış döngüyü ve aynı hedef-kare değerlendirmesini paylaşmasıdır. Bir hamleyi yasal yapan mantık yalnızca döngünün kalbindeki tek bir zincirleme ternary ifadesinde farklılık gösterir:
D ve H sütun ve satır farklarının mutlak değeridir. Bir at için yasal hamle kontrolü "bu farkların çarpımı 2" (çünkü at hamlesi her zaman 1×2 veya 2×1'dir). Şah için "iki fark da 2'den küçük." Fil için "iki fark eşit ve yol açık." Kale için "farklardan biri sıfır ve yol açık." Vezir için "ikisinden biri." Piyon kendi cümlesine sahip, yön orada önemli.
C(d, i) fonksiyonu — "d'den i'ye yol engelsiz mi?" — kendi başına sadece altı satır, çizgiyi adım adım yürütüp her ara kareyi kontrol ediyor.
Burada hız önemli değil. Üretici sadece bir insan hamle girdiğinde çağrılır; arama ağacı yok, saniyede milyonlarca pozisyon gereksinimi yok. Bir hamle üretilir, doğrulanır ve bir milisaniyenin altında uygulanır; oyuncu zaten darboğazdır. Hızı boyutla takas etmek tüm meselenin kendisi.
Detecting check without naming it İsim koymadan şahı algılamak
Check, checkmate, and stalemate all share a single primitive: is a given square attacked by a given color? The function that answers it is called L, and it is the only function in the engine longer than a screenful. It returns 1 if the answer is yes.
From here, everything else is composition. J(W) — "is the side W's king in check?" — finds the king (s.indexOf("kK"[+W])) and asks L if it's attacked by the other color. That's the entire check detector. Two function calls.
Checkmate is "the king is in check and the side has no legal moves." Stalemate is "the king is not in check and the side has no legal moves." We test these together with a single scan over the 64 squares: walk every square, ask the move generator if the piece there (if it belongs to the side to move) has any legal moves. If after the full scan no legal moves were found, the position is terminal — and then a single call to J decides whether it's mate or stalemate. The whole thing fits in one line.
The move generator does not return "pseudo-legal" moves; it returns only moves that leave the moving side's king out of check. It enforces this by simulating each candidate move on a temporary copy of the board, calling J, and discarding the candidate if the king ends up in check. This is, again, slow by competitive standards. Generating 20 candidates means cloning the board 20 times. But the clone is cheap (a single s.slice()), and at human reaction time scales it is invisible.
Şah, mat ve pat hepsi tek bir ilkellik paylaşır: verilen bir kare verilen bir renk tarafından saldırı altında mı? Bu soruyu cevaplayan fonksiyon L adını taşır ve motorda bir ekran dolusu sayfadan uzun olan tek fonksiyondur. Cevap evet ise 1 döner.
Buradan itibaren, geri kalan her şey kompozisyon. J(W) — "W tarafının şahı çekilmiş mi?" — şahı bulur (s.indexOf("kK"[+W])) ve L'ye karşı renk tarafından saldırı altında olup olmadığını sorar. İşte tüm şah dedektörü. İki fonksiyon çağrısı.
Mat "şah çekildi ve tarafın yasal hamlesi yok"tur. Pat "şah çekilmedi ve tarafın yasal hamlesi yok"tur. Bunları 64 kare üzerinde tek bir taramayla beraber test ediyoruz: her kareyi yürüt, oradaki taşın (eğer hamle sırasının tarafına aitse) yasal hamlesi olup olmadığını üreticiye sor. Tam tarama sonunda yasal hamle bulunmazsa pozisyon sondur — ve sonra J'a tek bir çağrı bunun mat mı yoksa pat mı olduğunu belirler. Hepsi tek bir satıra sığıyor.
Hamle üretici "pseudo-legal" (yarı yasal) hamleler döndürmez; sadece hamle yapan tarafın şahını çekme altında bırakmayan hamleleri döndürür. Bunu her aday hamleyi tahtanın geçici bir kopyasında simüle ederek, J'yi çağırarak ve şah çekme altında kalırsa adayı atarak uygular. Rekabetçi standartlarla yine yavaş. 20 aday üretmek tahtayı 20 kez klonlamak demek. Ama klon ucuz (tek bir s.slice()) ve insan reaksiyon zamanı ölçeğinde görünmez.
One function for every kind of move Her tür hamle için tek bir fonksiyon
The function M(f, t, p='Q') applies a move from square f to square t, with an optional promotion piece p. It handles regular moves, captures, en passant, castling, promotion, and all the bookkeeping that updates castling rights, the en passant target square, and the halfmove counter — in roughly twelve lines.
The trick is sequencing. Capture-and-place runs first as the common case: s[t] = s[f]; s[f] = '_'. Then five small clauses handle the special cases, each guarded by a check on the moving piece's type:
Notice the order: the en passant capture-removal happens before the piece moves into the en passant square, because we need the captured pawn's position relative to the source. The castling rights update happens after the king move, because we need to know who moved.
The R object is a tiny lookup table from rook starting square to the castling-rights bit it represents. When a rook moves or is captured, the relevant bit clears. The whole table is four entries: {0:8, 7:4, 56:2, 63:1}. Four entries, four bytes of meaning each.
Promotion deserves a note. When the input is a four-character move (a7a8), there's no promotion character, and p defaults to 'Q'. When the input is five characters (a7a8N), the fifth character is used as the promotion type — but only if it matches Q, R, B, or N (any case). The matching is done by a regex (/[QRBN]/i) on the way in; anything else silently becomes Q. So a7a8K and a7a85 both become a queen promotion. We made this choice for input forgiveness, but it has a side effect: an old bug where players in a hurry typed garbage characters became invisible. The game keeps going.
M(f, t, p='Q') fonksiyonu, f karesinden t karesine, isteğe bağlı bir terfi taşı p ile bir hamleyi uygular. Normal hamleleri, almaları, geçerken almayı, rok'u, terfiyi ve rok haklarını, geçerken alma hedef karesini ve halfmove sayacını güncelleyen tüm muhasebeyi yaklaşık on iki satırda halleder.
Numara sıralamada. Al-ve-koy ortak durum olarak önce çalışır: s[t] = s[f]; s[f] = '_'. Sonra beş küçük cümle özel durumları ele alır, her biri hamle yapan taşın türünde bir kontrolle korunuyor:
Sıraya dikkat: geçerken alma sırasında yenilen piyon kaldırma önce olur, çünkü taşın geçerken alma karesine hareket etmesinden önce alınan piyonun konumuna kaynağa göre ihtiyacımız var. Rok hakkı güncellemesi sonra olur, çünkü kimin hareket ettiğini bilmemiz gerek.
R nesnesi, kale başlangıç karesinden temsil ettiği rok-hakkı bit'ine küçük bir lookup tablosudur. Bir kale hareket ettiğinde veya yenildiğinde, ilgili bit temizlenir. Tüm tablo dört girişten oluşuyor: {0:8, 7:4, 56:2, 63:1}. Dört giriş, her birinde dört byte anlam.
Terfi bir not hak ediyor. Girdi dört karakterli bir hamle olduğunda (a7a8), terfi karakteri yoktur ve p varsayılan olarak 'Q''dur. Girdi beş karakter olduğunda (a7a8N), beşinci karakter terfi türü olarak kullanılır — ama yalnızca Q, R, B veya N ile eşleşirse (büyük veya küçük). Eşleşme girişte bir regex ile yapılır (/[QRBN]/i); başka her şey sessizce Q'ya döner. Yani a7a8K ve a7a85 ikisi de vezir terfisi olur. Bu tercihi girdi affedişi için yaptık ama bir yan etkisi var: acelesi olan oyuncuların çöp karakterler yazdığı eski bir bug görünmez oldu. Oyun devam eder.
Three kinds of draw, three different tricks Üç tür beraberlik, üç farklı numara
The FIDE rules give three automatic draw conditions beyond stalemate: the 50-move rule, threefold repetition, and dead position (insufficient mating material). Each of them is implemented with a different golf trick.
The 50-move rule is a single integer, n, that counts halfmoves since the last pawn move or capture. It's incremented in M on every move that doesn't reset it, and the main loop checks n > 99 before each prompt. When the counter exceeds 99 (i.e., 50 full moves without progress), the result is set to D! and the loop breaks. The bookkeeping is two characters per relevant decision.
Threefold repetition needs a position hash, and a way to count how often each hash has been seen. We use an object, P, with the joined board string plus the side-to-move, en passant target, and castling rights as the key. The trick is the increment expression:
B is the board joined into a string. The +=[t,e,o] appends the comma-joined string version of the three state variables (template literals would have worked but cost more bytes). -~x is JavaScript golf shorthand for x + 1, with the convenient property that -~undefined is 1 — so the first time we see a position the count starts correctly. The right side of the assignment is the new count; we compare to 2 because three occurrences means we've seen it twice before this one.
Dead position is the trickiest of the three because FIDE's definition is awkward: K vs K, K+minor vs K, and K+B vs K+B with same-colored bishops are dead. We test this with two regular expressions over the joined board string:
The first half catches "only kings and bishops, all bishops on the same color." The bishop color is computed from its position with (k ^ k>>3) & 1 — an old trick that gives 0 or 1 depending on the square's color parity. The second half catches "only kings and at most one knight." This is not every dead position FIDE recognizes (K+B vs K+N can be dead in some configurations, for example), but it covers the common cases without bloating the engine.
FIDE kuralları pat dışında üç otomatik beraberlik koşulu verir: 50 hamle kuralı, üçlü tekrar ve ölü pozisyon (mat etmeye yetersiz materyal). Her biri farklı bir golf numarasıyla uygulanır.
50 hamle kuralı tek bir tam sayıdır, n; son piyon hamlesinden veya almadan beri halfmove sayar. M içinde, onu sıfırlamayan her hamlede artırılır ve ana döngü her prompt'tan önce n > 99 kontrolü yapar. Sayaç 99'u aştığında (yani ilerlemesiz 50 tam hamle), sonuç D!'ye ayarlanır ve döngü kırılır. Muhasebe her ilgili karar için iki karakter.
Üçlü tekrar bir pozisyon hash'ine ve her hash'in ne sıklıkta görüldüğünü saymaya bir yola ihtiyaç duyar. Bir nesne, P, kullanıyoruz; anahtar olarak birleştirilmiş tahta string'i artı hamle sırasındaki taraf, geçerken alma hedefi ve rok hakları. Numara, artırma ifadesinde:
B string'e birleştirilmiş tahtadır. +=[t,e,o], üç durum değişkeninin virgülle birleştirilmiş string sürümünü ekler (template literal'ler çalışırdı ama daha fazla byte'a mal olurdu). -~x JavaScript golf'ünde x + 1'in kısaltmasıdır ve şu kullanışlı özelliği vardır: -~undefined, 1'dir — yani bir pozisyonu ilk gördüğümüzde sayı doğru başlar. Atamanın sağ tarafı yeni sayıdır; 2 ile karşılaştırıyoruz çünkü üç oluşum, bundan önce iki kez gördüğümüz anlamına gelir.
Ölü pozisyon üçünün en zorudur çünkü FIDE'nin tanımı tuhaftır: K vs K, K+küçük taş vs K ve aynı renkli filler ile K+B vs K+B ölüdür. Bunu birleştirilmiş tahta string'i üzerinde iki regex ile test ediyoruz:
İlk yarı "yalnızca şahlar ve filler, tüm filler aynı renkte" durumlarını yakalar. Fil rengi, konumundan (k ^ k>>3) & 1 ile hesaplanır — kare renk paritesine göre 0 veya 1 veren eski bir numara. İkinci yarı "yalnızca şahlar ve en fazla bir at" durumlarını yakalar. Bu, FIDE'nin tanıdığı her ölü pozisyon değildir (örneğin K+B vs K+N bazı yapılandırmalarda ölü olabilir), ama yaygın durumları motoru şişirmeden kapsar.
The clock — and why we floor, not round Saat — ve neden yuvarlamak yerine kesiyoruz
Each side starts with 900 seconds. The clock is two integers, u and v, decremented after each move by the number of seconds spent in the prompt dialog. The dialog blocks JavaScript execution, so the time the dialog was open is the time the player took to think.
Originally we computed elapsed time with (new Date - d + 500) / 1e3 | 0 — the +500 rounds to the nearest second. After a Gemini-led optimization pass, we dropped the +500: (new Date - d) / 1e3 | 0. This floors instead of rounding. Each move now consumes at most one second less than it used to.
Two reasons. First, FIDE specifies floor truncation for displayed clocks anyway, not round-to-nearest. Second, the saving (four bytes per occurrence, and the function is used twice) was real. The behavioral change is tiny and biased in the player's favor: across a long game, a player might recover something like ten seconds total, which is within the noise of real chess timekeeping.
The clock also doesn't tick visibly. The prompt blocks the JavaScript thread, so any setInterval-driven UI would queue up updates and only flush them when the prompt closed. The clock value shown at the top of each prompt is the value at the moment the prompt opened; the elapsed time of your current think is subtracted on submit. Functionally equivalent to a wall clock; visually less satisfying. We accepted that.
Her taraf 900 saniyeyle başlar. Saat iki tam sayıdır, u ve v; her hamleden sonra prompt penceresinde geçirilen saniye sayısı kadar azaltılır. Pencere JavaScript çalışmasını bloklar, dolayısıyla pencerenin açık olduğu süre oyuncunun düşünme süresidir.
Başlangıçta geçen süreyi (new Date - d + 500) / 1e3 | 0 ile hesaplıyorduk — +500 en yakın saniyeye yuvarlıyordu. Gemini liderliğindeki bir optimizasyon turundan sonra +500'ü düşürdük: (new Date - d) / 1e3 | 0. Bu yuvarlama yerine kesiyor. Her hamle artık eskisinden en fazla bir saniye daha az tüketiyor.
İki sebep. Birincisi, FIDE zaten gösterilen saatler için kesme belirtir, en yakına yuvarlama değil. İkincisi, tasarruf (her geçişte dört byte ve fonksiyon iki kez kullanılır) gerçekti. Davranışsal değişiklik küçük ve oyuncu lehine: uzun bir oyun boyunca bir oyuncu yaklaşık on saniye kazanabilir, bu gerçek satranç zaman tutmanın gürültü seviyesinde.
Saat ayrıca görünür şekilde ilerlemiyor. Prompt JavaScript thread'ini bloklar, böylece herhangi bir setInterval-yönelimli UI güncellemeleri sıraya koyar ve sadece prompt kapandığında temizler. Her prompt'un üstünde gösterilen saat değeri, prompt'un açıldığı andaki değerdir; mevcut düşünmenizin geçen süresi gönderildiğinde çıkarılır. Bir duvar saati ile işlevsel olarak eşdeğer; görsel olarak daha az tatmin edici. Bunu kabul ettik.
The fullwidth flourish: 52 bytes for legibility Fullwidth dokunuş: okunabilirlik için 52 byte
The bare engine renders the board with ordinary ASCII letters. In a typical browser's prompt dialog, those letters are drawn in the system UI font — which is proportional. Narrow letters like r and i take less horizontal space than wide ones like Q and M. Columns drift. The board looks like a misaligned grid.
The fix is one of the small joys of Unicode. The range U+FF21 to U+FF5A is a parallel ASCII alphabet of "fullwidth" letters: R N B Q K instead of R N B Q K. They're designed to occupy the same horizontal space as a CJK character — which means, on systems with the standard CJK font fallback (i.e., basically every modern browser), they render monospaced.
The trick is that every ASCII printable character has a fullwidth counterpart at exactly code + 65248. We don't need a lookup table:
The whole UI upgrade is this one line, plus changing W:${u} B:${v} to W:${u}s B:${v}s (the s suffix telling the user the unit is seconds). The total cost is 52 bytes. The result is a board that lines up in every browser the engine runs in.
The deployed page index.html uses the fullwidth UI layer. In the catalog, every variant comes in both UI states — the file with no _U suffix uses plain ASCII rendering, the one with _U uses fullwidth glyphs. The plain-ASCII versions are what gets compared in size against other golf chess implementations; the UI-on versions exist for anyone who wants the cleanest browser experience at a 50-byte premium.
Çıplak motor tahtayı sıradan ASCII harflerle render eder. Tipik bir tarayıcı prompt penceresinde, bu harfler sistem UI fontunda çizilir — ki bu proportional'dır. r ve i gibi dar harfler Q ve M gibi geniş olanlardan daha az yatay alan kaplar. Sütunlar kayar. Tahta yanlış hizalanmış bir ızgaraya benzer.
Düzeltme Unicode'un küçük sevinçlerinden biri. U+FF21 ile U+FF5A arasındaki aralık "fullwidth" harflerin paralel bir ASCII alfabesidir: R N B Q K yerine R N B Q K. Bir CJK karakteriyle aynı yatay alanı kaplayacak şekilde tasarlanmışlardır — yani standart CJK font fallback'ine sahip sistemlerde (yani temelde her modern tarayıcı) monospaced render edilirler.
Numara şu: her yazdırılabilir ASCII karakterinin tam olarak kod + 65248 konumunda bir fullwidth karşılığı vardır. Bir lookup tablosuna ihtiyacımız yok:
Tüm UI yükseltmesi bu tek satır, artı W:${u} B:${v}'yi W:${u}s B:${v}s'a değiştirmek (s eki kullanıcıya birimin saniye olduğunu söyler). Toplam maliyet 52 byte. Sonuç, motorun çalıştığı her tarayıcıda hizalanan bir tahta.
Yayınlanan sayfa index.html fullwidth UI katmanını kullanır. Katalogda, her varyant iki UI durumunda da gelir — _U eki olmayan dosya düz ASCII render kullanır, _U eki olan fullwidth karakterler kullanır. Düz ASCII versiyonları diğer golf satranç implementasyonlarıyla boyut olarak karşılaştırılan şeydir; UI-açık versiyonlar, 50 byte ek bedele en temiz tarayıcı deneyimini isteyenler için vardır.
The thirty-six-variant catalog Otuz altı varyantlı katalog
Six independent feature flags, each present or absent. Three describe the engine's capabilities: T (the clock and 50-move counter), M (the FIDE move set: castling, en passant, two-square pawn advance), P (choosable promotion). One more, D, adds the two early-claim draw detectors (threefold repetition and dead position) — but only on top of the full T+M+P stack. Then two presentation flags: S (showON/showOFF — whether the terminal result is alerted) and U (uiON/uiOFF — whether the board uses fullwidth glyphs and the timer gets a s suffix).
The naming on disk follows the pattern index_[L2-flags][_D][_S][_U].html. Examples: index_TMP is the full move-set engine, silent, plain ASCII. index_TMP_D_SU is the full FIDE engine with the terminal alert and the fullwidth UI — identical in content to the deployed index.html. The most primitive variant — no L2 features, no S, no U — is the lone file labeled index_L1.html; all other L1-level files (with only S/U flags) drop the L1 prefix entirely, becoming just index_S, index_U, index_SU.
Materializing every legal combination as a separate file gives thirty-six variants. Not every combination is interesting; some are useful only as intermediate proof points (what does adding the timer alone cost? what does adding promotion choice without the rest of the move set cost?). But all thirty-six are tested, and the catalog lets a reader see exactly how each FIDE rule and each presentation layer adds bytes by comparing same-flag pairs across the table.
For example: index_TM is 1673 bytes; index_TMP is 1716 bytes. The difference is just the promotion-choice handling: 43 bytes. index_L1 is 1072 bytes; index_TMP is 1716. The 644-byte difference is the cost of clock + full FIDE move handling on top of the bare engine. index_TMP_D_S is 1996; index_TMP_D_SU is 2048. The 52-byte difference is the entire UI polish layer.
The catalog is generated by a single HTML file, version_generator.html, which contains a parameterized build function. This is how we make sure the generator stays the canonical source: any optimization gets applied once to the generator and propagates to all thirty-six variants at the next regen.
Altı bağımsız özellik bayrağı, her biri var veya yok. Üçü motorun yeteneklerini tanımlıyor: T (saat ve 50 hamle sayacı), M (FIDE hamle seti: rok, en passant, piyon iki kare ilerleme), P (seçilebilir terfi). Bir tane daha, D, iki erken-iddia beraberlik dedektörünü ekler (üçlü tekrar ve ölü pozisyon) — ama yalnızca tam T+M+P stack'i üzerinde. Sonra iki sunum bayrağı: S (showON/showOFF — sonlanma uyarısı var mı) ve U (uiON/uiOFF — tahta fullwidth karakter kullanıyor mu ve sayaca s eki var mı).
Diskteki adlandırma index_[L2-flags][_D][_S][_U].html desenini izler. Örnekler: index_TMP tam hamle-seti motoru, sessiz, düz ASCII. index_TMP_D_SU tam FIDE motoru, sonlanma uyarısı ve fullwidth UI ile — yayınlanan index.html ile içerik olarak birebir aynı. En ilkel varyant — L2 özelliği yok, S yok, U yok — yalnız index_L1.html etiketli dosyadır; diğer tüm L1 seviyesindeki dosyalar (sadece S/U bayraklarıyla) L1 önekini tamamen düşürür, sadece index_S, index_U, index_SU olur.
Her yasal kombinasyonu ayrı bir dosya olarak materyalize etmek otuz altı varyant verir. Her kombinasyon ilginç değildir; bazıları yalnızca ara kanıt noktaları olarak yararlıdır (sadece saat eklemenin maliyeti nedir? hamle setinin geri kalanı olmadan sadece terfi seçimi eklemenin maliyeti nedir?). Ama otuz altısının hepsi test edildi ve katalog, bir okuyucunun her FIDE kuralının ve her sunum katmanının kaç byte eklediğini tabloyu aynı bayraklı çiftler arasında karşılaştırarak tam olarak görmesini sağlar.
Örneğin: index_TM 1673 byte; index_TMP 1716 byte. Fark sadece terfi seçimi işlemesi: 43 byte. index_L1 1072 byte; index_TMP 1716. 644 byte'lık fark, çıplak motorun üzerine saat + tam FIDE hamle işlemesinin maliyetidir. index_TMP_D_S 1996; index_TMP_D_SU 2048. 52 byte'lık fark, tüm UI cilası katmanıdır.
Katalog tek bir HTML dosyası tarafından üretilir, version_generator.html; parametreli bir derleme fonksiyonu içerir. Üreticiyi kanonik kaynak tutmamızın yolu bu: herhangi bir optimizasyon üreticiye bir kez uygulanır ve bir sonraki yeniden üretimde otuz altı varyanta da yayılır.
The optimization journey Optimizasyon yolculuğu
The first working version was around 3,000 bytes. Most of the bulk was readable code: meaningful variable names, explicit destructuring, comments. The shrinkage proceeded in passes, each finding a different category of waste.
Single-character variables came first, the easy win in any code-golf project. board became s; turn became t; halfMoveCount became n. Functions were declared with arrow syntax and default parameters: w=p=>p<'a' instead of a function declaration. Math methods were aliased: A=Math.abs, S=Math.sign. Each alias is worth it after about three uses.
Then came the structural choices: combining make and unmake by snapshot-and-restore instead of inverse moves; consolidating six piece-type clauses into one chained ternary; collapsing the input parser into the same arithmetic step (63 - 8 * i[1] + i.charCodeAt() % 32, a clever map from "a8"-style notation to square index, courtesy of an early Gemini suggestion).
The final round was a six-trick batch suggested in collaboration with Gemini. Each trick was tiny on its own and all but invisible in isolation, but they totaled −312 bytes across the eighteen variants:
- A coordinate parser function
K=c=>63-8*i[c+1]+i.charCodeAt(c)%32replacing two near-duplicate parsing expressions. - Floor instead of round for the clock decrement:
+500removed. - The board-render regex
/(. ){8}/gtightened to/.{16}/g(both match the same thing for our board, but the second is shorter). - Castling direction check rewritten from
X-x<-1tox-X>1. - Promotion edge check rewritten from
!(Y%7)toY%7<1. - En passant target check rewritten from
A(Y-y)==2toA(Y-y)>1(safe because the move generator already restricts pawn advances to ≤2).
None of these changed the engine's behavior in any way that matters. All thirty-six variants regenerate cleanly, and the test suite (414 deep tests, 36 perft, 90 crash, 5 mirror-parity) still passes 100%.
İlk çalışan sürüm yaklaşık 3.000 byte'tı. Hacmin çoğu okunabilir koddu: anlamlı değişken adları, açık destructuring, yorumlar. Küçülme turlarda ilerledi, her tur farklı bir israf kategorisi buldu.
Tek karakterli değişkenler önce geldi, herhangi bir kod-golf projesindeki kolay kazanç. board, s oldu; turn, t oldu; halfMoveCount, n oldu. Fonksiyonlar arrow syntax ve varsayılan parametrelerle bildirildi: bir function bildirimi yerine w=p=>p<'a'. Math metotlarına alias verildi: A=Math.abs, S=Math.sign. Her alias yaklaşık üç kullanımdan sonra değer.
Sonra yapısal tercihler geldi: ters hamleler yerine snapshot-and-restore ile make ve unmake'i birleştirmek; altı taş-türü cümlesini tek zincirleme ternary'ye birleştirmek; girdi ayrıştırıcıyı aynı aritmetik adıma çökertmek (63 - 8 * i[1] + i.charCodeAt() % 32, "a8"-tarzı notasyondan kare indeksine zekice bir eşleme, erken bir Gemini önerisi).
Son tur, Gemini ile işbirliğinde önerilen altı numaralı bir batch'ti. Her numara tek başına minik ve izolasyonda neredeyse görünmezdi, ama on sekiz varyant boyunca toplamda −312 byte:
- İki yarı-yinelenmiş ayrıştırma ifadesinin yerini alan bir koordinat ayrıştırıcı fonksiyonu
K=c=>63-8*i[c+1]+i.charCodeAt(c)%32. - Saat azaltma için yuvarlama yerine kesme:
+500kaldırıldı. - Tahta-render regex'i
/(. ){8}/g,/.{16}/g'ya sıkıştırıldı (her ikisi de tahtamız için aynı şeyi eşleştiriyor ama ikincisi daha kısa). - Rok yön kontrolü
X-x<-1'denx-X>1'a yeniden yazıldı. - Terfi kenar kontrolü
!(Y%7)'denY%7<1'a yeniden yazıldı. - Geçerken alma hedef kontrolü
A(Y-y)==2'denA(Y-y)>1'a yeniden yazıldı (güvenli çünkü hamle üretici zaten piyon ilerlemelerini ≤2 ile sınırlıyor).
Bunların hiçbiri motorun davranışını önemli bir şekilde değiştirmedi. Otuz altı varyantın hepsi temiz şekilde yeniden üretilir ve test paketi (414 derin test, 36 perft, 90 crash, 5 mirror-parity) hâlâ %100 geçer.
Things we tried and abandoned Denedik ve vazgeçtik
Two ideas looked promising and turned out wrong; we record them here because the wrong-headedness is part of the story.
0x88 mailbox. Discussed in §04. We wrote a complete 0x88 engine as an experiment, expecting savings of 30–80 bytes. It came in at 2791 bytes, nearly 800 over the existing one. The technique is right for languages that make array literals and tight loops cheap (C, assembly). JavaScript pays for both. The experiment is preserved in the repository as a teaching artifact.
Bitboards with BigInt. Briefly considered, never implemented. The cost analysis was enough to kill it: every 1n << 8n in place of 1 << 8, every BigInt literal carrying an n suffix, every comparison needing parentheses around the BigInt arithmetic. The precomputed attack tables alone would add several hundred bytes. A representation that's a clear win in C ends up a clear loss in JS.
Direct alphabets. We tried Mathematical Bold (𝐊 𝐐 𝐑), Mathematical Sans-Serif (𝖪 𝖰 𝖱), Math Sans-Serif Bold (𝗞 𝗤 𝗥), and Math Monospace (𝙺 𝚀 𝚁) as alternatives to fullwidth. They all worked, but at +94 bytes each instead of fullwidth's +78, because they're 4-byte UTF-8 surrogate pairs and need extra spread-and-index handling. Fullwidth's BMP encoding made it the right choice.
A random-move AI. An afternoon's experiment: drop in Math.random() over the legal moves list and have the engine play one side. About 137 extra bytes. It worked, but it played at the level of someone who'd just learned the rules — perhaps 250 ELO. Toledo's actual AI plays around 1200. The point of an AI is that it should be better than blind chance, and we couldn't afford alpha-beta. We pulled it.
İki fikir umut verici göründü ve yanlış çıktı; yanlışlığı hikayenin parçası olduğu için onları burada kaydediyoruz.
0x88 mailbox. §04'te tartışıldı. Bir deney olarak tam bir 0x88 motoru yazdık, 30-80 byte tasarruf bekliyorduk. 2791 byte olarak geldi, mevcut olanın yaklaşık 800 üstünde. Teknik, dizi literal'lerini ve sıkı döngüleri ucuz yapan diller (C, asm) için doğru. JavaScript ikisinin de bedelini öder. Deney, öğretici bir eserdir olarak repoda korunuyor.
BigInt ile bitboard'lar. Kısaca düşünüldü, asla uygulanmadı. Maliyet analizi öldürmek için yeterliydi: 1 << 8 yerine her 1n << 8n, n ekiyle her BigInt literal'i, BigInt aritmetiği etrafında parantez gerektiren her karşılaştırma. Yalnızca önceden hesaplanmış saldırı tabloları yüzlerce byte ekler. C'de açık bir kazanım olan bir gösterim, JS'te açık bir kayba dönüşür.
Doğrudan alfabeler. Fullwidth'e alternatif olarak Mathematical Bold (𝐊 𝐐 𝐑), Mathematical Sans-Serif (𝖪 𝖰 𝖱), Math Sans-Serif Bold (𝗞 𝗤 𝗥) ve Math Monospace (𝙺 𝚀 𝚁) denedik. Hepsi çalıştı, ama fullwidth'in +78'i yerine her biri +94 byte, çünkü 4-byte UTF-8 surrogate pair'leri ve ek spread-and-index işleme gerektiriyorlar. Fullwidth'in BMP kodlaması onu doğru seçim yaptı.
Rastgele-hamle yapay zekası. Bir öğleden sonraki deney: yasal hamleler listesi üzerine Math.random() bırak ve motorun bir tarafı oynamasını sağla. Yaklaşık 137 ekstra byte. Çalıştı ama kuralları yeni öğrenmiş birinin seviyesinde oynadı — belki 250 ELO. Toledo'nun gerçek yapay zekası 1200 civarında oynar. Bir yapay zekanın amacı kör şanstan daha iyi olması gerektiğidir ve alpha-beta'yı karşılayamadık. Çıkardık.
A mirror for testing Test için bir ayna
Golfed code is easy to break. A single misplaced operator can turn a legal move into a crash, or worse, into a wrong-but-plausible behavior that passes casual play but corrupts a tournament. We knew from the start that we needed a way to verify the engine systematically.
The solution is test_helper.html, which contains two independent implementations of the rules and runs them in parallel. The first is a reference engine: a clean, readable, deliberately uncompressed implementation of FIDE rules, designed to be obviously correct. The second is a mirror engine: a structural port of the golfed engine that maintains the same control flow but uses readable variable names. The mirror is supposed to behave identically to the golfed motor on every input.
The test suite plays full games through both engines and asserts after every move that their states agree: piece positions, castling rights, en passant target, halfmove counter, repetition map. If they ever disagree, something in the golfed engine is wrong.
This is supplemented by category-specific tests: 414 deep tests across four motor profiles covering each FIDE rule in detail; 36 perft tests at depth 3 across all variants; 90 crash tests with adversarial input; 5 mirror-parity tests for the 50-move and threefold draw edge cases. The result is unambiguous: every release passes all 545 tests.
Golf'lenmiş kodu kırmak kolay. Yanlış yerleştirilmiş tek bir operatör yasal bir hamleyi çökmeye veya daha kötüsü, gündelik oyunda geçen ama bir turnuvayı bozan yanlış-ama-makul bir davranışa çevirebilir. Motoru sistematik olarak doğrulamamız gerektiğini en başından biliyorduk.
Çözüm test_helper.html'dir, kuralların iki bağımsız uygulamasını içerir ve onları paralel olarak çalıştırır. Birincisi bir referans motor: FIDE kurallarının temiz, okunabilir, kasten sıkıştırılmamış bir uygulaması, açıkça doğru olacak şekilde tasarlandı. İkincisi bir ayna motor: golf'lenmiş motorun yapısal bir port'u, aynı kontrol akışını korur ama okunabilir değişken adları kullanır. Ayna her girdide golf'lenmiş motorla aynı şekilde davranmalıdır.
Test paketi her iki motor aracılığıyla tam oyunlar oynar ve her hamleden sonra durumlarının aynı olduğunu iddia eder: taş konumları, rok hakları, geçerken alma hedefi, halfmove sayacı, tekrar haritası. Eğer herhangi bir noktada anlaşmazlarsa, golf'lenmiş motorda bir şey yanlıştır.
Bu, kategori-özel testlerle desteklenir: her FIDE kuralını ayrıntıda kapsayan dört motor profili boyunca 414 derin test; tüm varyantlarda derinlik 3'te 36 perft testi; düşmanca girdiyle 90 crash testi; 50 hamle ve üçlü beraberlik edge case'leri için 5 mirror-parity testi. Sonuç belirsiz değil: her sürüm 545 testin hepsini geçer.
What would beat us Bizi ne geçer
In the current state of the field, no public JavaScript chess implementation handles all eleven FIDE rules in fewer bytes. Toledo's various JS versions are between 1024 and 2299 bytes; the smaller ones omit castling, en passant, or promotion choice; the larger ones omit threefold repetition and dead position. Our 1996-byte bare engine handles all eleven, and the 2048-byte deployed page does so with a properly aligned visual layer.
This is a small claim, defended by a narrow niche. If Toledo (or someone of comparable skill) refactored their AI-based engine to be a human-vs-human prompt-based engine with all eleven FIDE rules, the result would probably land around 1500 bytes — well below us. The savings would come from sharing the move-generator code between move execution and AI search in clever ways we don't need to consider.
We are aware of this and we accept it. Our claim is not "no one can beat this." It's "no one has, and the gap to anyone who might is at least one substantial refactor away." Whether anyone takes that on is up to them.
The other axis where we might lose: bytes are not the only thing. A Toledo engine plays. We don't. For someone whose primary use case is "I want to play chess against a computer at lunch," we are the wrong tool. For someone whose primary use case is "I want to look at a complete, audited, FIDE-compliant chess implementation that's small enough to read in one sitting," we are, at present, the smallest such thing.
Alanın mevcut durumunda, on bir FIDE kuralının hepsini daha az byte'ta işleyen herkese açık bir JavaScript satranç implementasyonu yok. Toledo'nun çeşitli JS sürümleri 1024 ile 2299 byte arasında; daha küçük olanlar rok, geçerken alma veya terfi seçimini atlar; daha büyük olanlar üçlü tekrar ve ölü pozisyonu atlar. Bizim 1996 byte'lık çıplak motorumuz on birinin hepsini ele alır ve 2048 byte'lık yayınlanan sayfa bunu düzgün hizalanmış bir görsel katmanla yapar.
Bu küçük bir iddiadır, dar bir niş tarafından savunulur. Eğer Toledo (veya benzer beceriye sahip biri) yapay zeka tabanlı motorunu, tüm on bir FIDE kuralına sahip insan-vs-insan prompt tabanlı bir motora dönüştürürse, sonuç muhtemelen 1500 byte civarında olurdu — bizden epey aşağıda. Tasarruf, hamle üreticisi kodunu hamle yürütme ve yapay zeka araması arasında düşünmemiz gerekmeyen akıllı yollarla paylaşmaktan gelirdi.
Bunun farkındayız ve bunu kabul ediyoruz. İddiamız "kimse bunu geçemez" değil. "Kimse geçmedi ve geçebilecek herkesle aramızdaki açık en az bir ciddi refactor uzakta" şeklinde. Bunu birinin üstlenip üstlenmemesi ona kalmış.
Kaybedebileceğimiz diğer eksen: byte tek şey değildir. Bir Toledo motoru oynar. Biz oynamayız. Birincil kullanım durumu "öğle yemeğinde bilgisayara karşı satranç oynamak istiyorum" olan biri için yanlış araçız. Birincil kullanım durumu "tek oturuşta okuyacak kadar küçük olan tam, denetlenmiş, FIDE uyumlu bir satranç implementasyonuna bakmak istiyorum" olan biri için, şu anda, böyle olan en küçük şeyiz.
Closing notes Kapanış notları
Three things we did not anticipate at the start, and learned along the way.
One. Optimization choices that look like they're about bytes are often about behavior. Removing +500 from the clock saved four bytes per use, but it also changed when the clock rounded — and the new behavior turned out to match FIDE's specification better than the old one. The byte-savers were often hiding correctness improvements.
Two. JavaScript is unusually well-suited to compact rule-based programs and unusually poorly suited to compact arithmetic-heavy ones. The string-array board, the regex draw detector, the template-literal init — these are all idioms native to JavaScript that have no direct equivalent in C. They each save dozens of bytes. Whereas the bitboards and 0x88 mailboxes that are the gold standard in C cost more bytes in JavaScript, not fewer. Language-aware design matters.
Three. The niche we ended up occupying — fully FIDE-compliant, human-vs-human, prompt-based chess in plain JavaScript — was not the niche we set out for. We set out to write a small chess engine. The niche emerged from the cumulative weight of small honest decisions: prompt because it was free, two-player because we didn't need an AI, eleven rules because we'd come this far. None of it was strategic. All of it became strategic in retrospect.
What we have, at the end, is a 2048-byte single HTML file that plays a complete FIDE-legal game of chess in any browser, with no dependencies, no build step, and a worked test suite that catches every regression. It is not the most impressive piece of software anyone has ever written. It is, in its small and specific way, exactly the piece of software we wanted to write.
Open index.html · Read the rules in manual.html · Browse the catalog in the repository.
Başlangıçta beklemediğimiz ve yol boyunca öğrendiğimiz üç şey.
Bir. Byte hakkında gibi görünen optimizasyon tercihleri çoğu zaman davranışla ilgilidir. Saatten +500'ü kaldırmak her kullanımda dört byte tasarruf etti, ama saatin nasıl yuvarladığını da değiştirdi — ve yeni davranış FIDE'nin spesifikasyonuyla eskisinden daha iyi eşleşti. Byte-tasarrufçular sık sık doğruluk iyileştirmelerini saklıyorlardı.
İki. JavaScript, sıkıştırılmış kural-tabanlı programlar için olağandışı şekilde uygun ve sıkıştırılmış aritmetik-ağırlıklı olanlar için olağandışı şekilde uygunsuz. String-array tahta, regex beraberlik dedektörü, template-literal başlatma — bunların hepsi JavaScript'e özgü ve C'de doğrudan karşılığı olmayan deyimlerdir. Her biri onlarca byte tasarruf eder. Oysa C'de altın standart olan bitboard'lar ve 0x88 mailbox'lar JavaScript'te daha çok byte'a mal olur, daha az değil. Dile özgü tasarım önemli.
Üç. Sonunda işgal ettiğimiz niş — düz JavaScript'te tam FIDE uyumlu, insan-vs-insan, prompt tabanlı satranç — yola çıktığımız niş değildi. Küçük bir satranç motoru yazmak için yola çıktık. Niş, küçük dürüst kararların kümülatif ağırlığından ortaya çıktı: prompt çünkü ücretsizdi, iki-oyuncu çünkü yapay zekaya ihtiyacımız yoktu, on bir kural çünkü buraya kadar gelmiştik. Hiçbiri stratejik değildi. Tamamı geriye dönük olarak stratejik oldu.
Sonunda elimizde olan şey, herhangi bir tarayıcıda tam FIDE yasal bir satranç oyununu oynayan, bağımlılığı olmayan, build adımı olmayan, her regresyonu yakalayan işlenmiş bir test paketine sahip 2048 byte'lık tek bir HTML dosyasıdır. Şimdiye kadar yazılmış en etkileyici yazılım parçası değildir. Kendi küçük ve özel yolunda, yazmak istediğimiz tam olarak o yazılım parçasıdır.
index.html'i aç · Kuralları manual.html'de oku · Kataloğa repository'den göz at.
golfchess test helper
prompt-engine parity verifier prompt-motor eşlik doğrulayıcıNotation Notasyon
golfchess — version generator golfchess — versiyon üreteci
L1 is always included. Pick from L2 features. D (Draw detection) is only active when T+M+P are all on. L1 her zaman dahil. L2 özelliklerinden seçim yap. D (Draw detection) sadece T+M+P hepsi açıkken aktif olur.
L2 features: L2 özellikleri:
L3 (Draw detection): L3 (Draw detection):
Result announcement (S): Sonuç bildirimi (S):
Interface (U): Arayüz (U):