momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

PGN şi FEN (computer chess)

FEN | PGN | modelare obiectuală | replace() | spacer GIF
2010 nov

De regulă, interfeţele aplicaţiilor din domeniul Computer Chess mizează pe notaţia PGN pentru partida de şah şi pe notaţia FEN pentru o poziţie particulară (la un moment sau altul de pe parcursul jocului); este vorba de "interfaţa cu utilizatorul", fiindcă "cealaltă parte" a unei astfel de aplicaţii - partea care "joacă" efectiv, generând, analizând şi alegând mutările - implică reprezentări în memorie care de regulă, nu au de-a face cu notaţiile menţionate.

Referim aici aceste notaţii, împreună cu unele funcţii de prelucrare specifice (utilizate într-o formă sau alta în aplicaţiile pgn_dem şi chessJSengine).

Portable Game Notation (PGN)

Mai ales după 1990 (dar începând prin 1970), au fost create multe programe pentru jocul de şah (denumite generic chess engine); jucătorii de şah le folosesc pentru a analiza variante de joc în diverse poziţii şi în general - în procesul de pregătire proprie.

PGN răspunde necesităţii unui format standard pentru reprezentarea partidelor de şah - încât pe de o parte partida să poată fi uşor citită sau scrisă de către om, iar pe de altă parte să poată fi comunicată între calculatoare în cel mai simplu mod (indiferent de sistemul de operare) şi să poată fi cât mai uşor, analizată sau generată de "chess-engine" (indiferent de limbajele de programare folosite).

Un exemplu de PGN şi exerciţii Copy&Paste

Imediat dedesubt avem un element <textarea>, al cărui conţinut este un exemplu de reprezentare PGN a unei partide de şah. Click-dreapta în interior deschide un meniu din care puteţi alege Select All; după selectarea astfel a conţinutului <textarea>, un nou click-dreapta deschide un meniu din care puteţi alege Copy (operaţii valabile probabil în orice browser şi care au ca efect copierea în "clipboard" a conţinutului respectiv - putând apoi să-l "pastaţi" prin operaţii similare, într-un fişier deschis într-un editor de text, sau într-un element <textarea> dintr-o altă fereastră a browserului).

Iată un exerciţiu tipic, la îndemâna fiecăruia: selectaţi şi copiaţi conţinutul casetei; apoi deschideţi pgn_dem (într-un nou tab al browserului: click-dreapta pe link şi Open Link in New Tab) şi aici - faceţi click pe butonul marcat "PGN", selectaţi conţinutul casetei apărute şi folosiţi combinaţia de "pastare" CTRL + V; apoi, click pe butonul Load PGN de la baza casetei. Imaginea următoare prezintă ceea ce ar rezulta după aceste operaţii:

click "Play" va declanşa derularea mutărilor pe tablă, permiţând urmărirea jocului.
N.B. - pe parcursul anilor vor putea interveni modificări, faţă de imaginea redată mai sus.

Clik-ul pe Load PGN menţionat mai sus, a activat anumite funcţii prin care: s-a preluat conţinutul PGN din elementul <textarea> în care "pastasem" partida; s-a analizat acest conţinut şi s-au separat componentele (jucători, lista mutărilor, comentarii, etc.); s-au analizat rând pe rând mutările, stabilind "formatul lung" pentru fiecare (de exemplu, "1.d2-d4 Ng8-f6" - indicând şi câmpul de plecare şi pe cel de sosire - în loc de formatul scurt "1.d4 Nf6" utilizat în PGN).

În final, mutările în format lung rezultate au fost înscrise în tabelul din partea dreaptă a tablei de şah din imagine. Acestui tabel de mutări i s-a asociat şi un "handler de click" care determină poziţia corespunzătoare efectuării mutării indicate de utilizator prin click şi o reprezintă corespunzător pe tabla de şah din stânga tabelului mutărilor. Dacă mutarea curentă este însoţită în textul PGN de comentarii (sau de variante de joc), acestea vor fi reproduse dedesubtul diagramei (şi nu în tabelul mutărilor), în momentul când mutarea respectivă este jucată pe tablă.

În oricare moment, deasupra diagramei este redată codificarea FEN a poziţiei. FEN-ul poate fi copiat prin operaţiile obişnuite ale unui browser (la fel, comentariile sau lista mutărilor), putând fi utilizat de exemplu într-un program de şah (care să ofere o analiză a poziţiei).

Deasemenea, se poate face aici acest experiment: copiaţi FEN-ul pentru o poziţie mai simplă - de exemplu cel corespunzător mutării 39...Qd3-d7 (FEN-ul respectiv este: 6k1/p2q1b2/2n2Qp1/1p2p3/4P3/2P1B3/5R1K/8 w - - 1 40 şi poate fi copiat selectându-l cu mouse-ul şi tastând CTRL + C); deschideţi într-un alt tab al browserului aplicaţia chessJSengine şi aici - click pe butonul GET şi pastaţi FEN-ul respectiv; apoi click Go:

Click pe Go a declanşat funcţiile de analiză ale programului doChess, rezultând propunerea mutării 1.Bh6 în poziţia respectivă ("confirmată" şi în partidă - mutarea 40.Be3-h6, în "format lung").

Formatul PGN

Înregistrarea unei partide de şah în formatul PGN conţine două părţi: mai întâi, un antet în care trebuie specificate informaţii de identificare a partidei (concursul - sau turneul, meciul - în care s-a jucat partida, în ce localitate sau pe ce server s-a jucat, la ce dată, numele celor doi jucători, şi altele); apoi (separând eventual printr-un rând alb, cum se recomandă), textul partidei - adică lista numerotată a mutărilor efectuate, mutările fiind reprezentate în notaţia algebrică obişnuită (în format "scurt"); textul partidei trebuie încheiat (obligatoriu) de specificarea rezultatului ("1-0" sau "0-1" dacă a câştigat albul, respectiv negrul; "1/2-1/2" în cazul când partida s-a încheiat remiză; sau "*" dacă rezultatul este necunoscut la momentul respectiv). Mutările pot fi adnotate în textul partidei (respectând anumite reguli sintactice), prin comentarii şi/sau variante, eventual imbricate.

Sintaxa PGN poate fi descrisă - folosind BNF, ca de obicei la descrierea unui limbaj - prin:

<PGN-game> ::= <tag-section> <movetext-section>
    <tag-section> ::= <tag-pair> <tag-section>
        <tag-pair> ::= [ <tag-name> <tag-value> ]
            <tag-name> ::= <identifier>
            <tag-value> ::= <string>
    <movetext-section> ::= <move-sequence> <game-termination>
        <move-sequence> ::= <move> <move-sequence> <recursive-variation> <move-sequence>
            <move> ::= <move-number-indication> <SAN-move> <annotation-glyph>
            <recursive-variation> ::= ( <move-sequence> )
        <game-termination> ::= 1-0 | 0-1 | 1/2-1/2 | *

În descrierea de mai sus am folosit fireşte denumiri englezeşti ("move" = mutare, etc.); pe sursele publicate de programe "chess engine" se poate vedea obiceiul general de a folosi aceste denumiri (în engleză), indiferent de naţionalitatea autorului.

Astfel, PGN-ul exemplificat mai sus are <tag-section> (sau "antetul") format din şapte <tag-pair> consecutive. Un <tag-pair> începe cu caracterul "[", conţine un <tag-name> (în speţă Event, Site, Date, etc.) urmat de un caracter "spaţiu", apoi un <tag-value> (în speţă, şirurile de caractere "Dynarch Chess", etc.) şi în final, caracterul "]".

Elementul <move> corespunzător mutării "20.g4!" din textul partidei, are '20.' drept <move-number-indication>, 'g4' drept <SAN-move> şi '!' drept <annotation-glyph>.

Obs. Dacă este cazul de a traduce - putem folosi desigur, translate.google. Dacă traducerea cerută nu există şi vrem totuşi să ne lămurim sensul termenului, putem încerca o traducere în altă limbă; de exemplu, pentru termenul "glyph" nu există traducere în română, în schimb (cerând traducerea în rusă) găsim символический знак - deci sensul pentru "glyph" este acela de "un semn care simbolizează ceva" (în cazul de aici, '!' adăugat unei mutări înseamnă - conform unor convenţii asumate de multă vreme în şah - "mutare bună, nebanală").

Precizăm totuşi că descrierea BNF de mai sus nu este completă; de exemplu, în loc de "annotation-glyph" - mai corect era eventual "-sufix" - putem avea mai general, "numeric-annotation-glyph" (prescurtat "NAG" în documentaţii) - reprezentat prin caracterul "$" urmat de un număr 0..255 care are o semnificaţia prestabilită (de exemplu, 20.g4 $1 corespunde mutării "20.g4!"; 20.g4 $13 ar fi însemnat "unclear position", etc.). Deasemenea, lipseşte descrierea necesară pentru "comentarii": în PGN un comentariu este un text oarecare încadrat între acolade (şi există anumite reguli suplimentare, care complică descrierea BNF completă).

FEN-ul poziţiei curente

FEN descrie poziţii particulare de pe parcursul unei partide de şah; "poziţie" înseamnă mai mult decât "poziţia pieselor" pe tablă, implicând şi informaţii contextuale: cine este la mutare în poziţia respectivă, ce drepturi de rocadă există în acea poziţie, etc.

Sunt asumate în mod implicit următoarele elemente: liniile tablei de şah sunt numerotate de la 1 la 8, iar coloanele sunt indexate prin literele 'a'..'h', astfel încât în poziţia iniţială, albului să-i corespundă liniile 1, 2 iar negrului liniile 7, 8 şi - văzând de la stânga spre dreapta, din partea albului - coloana 'a' să fie prima, iar coloana 'h' ultima.

Piesele sunt notate prin câte o literă provenită de la denumirile englezeşti, folosind majuscule numai pentru piesele albe - K pentru regele alb şi k pentru cel negru (denumirea fiind king = rege); B pentru nebun alb ("bishop"); N pentru cal (de la "kNight"); R pentru turn ("rook"); Q pentru damă ("queen"); P pentru pion ("pawn").

FEN-ul poziţiei iniţiale este: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

FEN-ul conţine maximum şase câmpuri, separate între ele printr-un caracter "spaţiu". Primul câmp indică amplasarea pieselor în momentul respectiv, începând de la linia 8 şi încheind cu linia 1, iar pe fiecare linie - de la coloana 'a' spre coloana 'h'; reprezentările liniilor sunt separate între ele în cadrul câmpului, printr-un caracter "/". Câmpurile libere (neocupate de vreo piesă) sunt indicate prin câte o cifră 1..8, indicând numărul de câmpuri libere consecutive care urmează pe linia respectivă. De exemplu, /8/ indică o linie pe care nu se află nici o piesă; linia /P2ppK2/ conţine un pion alb, două câmpuri libere, apoi doi pioni negri, regele alb şi două câmpuri libere.

Al doilea câmp indică printr-un caracter - sau "w", sau "b" - partea care urmează să mute în poziţia respectivă (white, respectiv black).

Al treilea câmp vizează drepturile de rocadă ale părţilor; conţine literele care desemnează regele şi dama (deci "K" şi "Q", în cazul albului), dacă partea respectivă n-a pierdut până la acel moment, dreptul de a roca în partea regelui, respectiv pe flancul damei. Dacă ambele părţi au pierdut acest drept (fie au făcut anterior mutarea "0-0" - rocada mică - sau rocada mare "0-0-0", fie au mutat între timp regele sau turnurile, pierzând astfel dreptul la rocadă) - atunci câmpul conţine caracterul "-".

Butoanele "FEN" şi "GET" de pe chessJSengine permit vizualizarea FEN-ului curent şi respectiv, iniţializarea jocului din FEN-ul indicat de utilizator.

Conversia FEN-ului într-o diagramă de şah

Dintre funcţionalităţile care trebuie asigurate în contextul prezentat mai sus, cel mai uşor de modelat este vizualizarea tablei de şah conţinând poziţia reprezentată de un FEN dat. Mica aplicaţie pe care o expunem mai jos pleacă de la următoarea schemă: oferim utilizatorului un element <input> în care să introducă un FEN (tastându-l, sau "pastând" din altă parte); creem o funcţie care să verifice (cât de cât) corectitudinea primului câmp din FEN-ul introdus şi să constituie o diagramă de şah pentru poziţia respectivă (într-adevăr - aici nu interesează decât primul câmp din FEN, dat fiind că ne-am propus numai vizualizarea poziţiei, nu şi prelucrări referitoare la mutările posibile în acea poziţie).

Modelări incipiente

Într-o primă formulare (şi… liberă):

<script>
        function is_valid( FEN ) { 
            // verificare sintactică pentru primul câmp din FEN 
        }

        function get_diagram( FEN-id, dest-id ) { 
            // dacă is_valid(FEN), produce diagrama corespunzătoare în locul dest-id
        }         
<script>

introduceţi FEN: <input id="fen-id" />

<button onclick="get_diagram('fen-id', 'diagram-id');"> Submit <button/> 

Diagrama corespunzătoare FEN-ului introdus:
<div id="diagram-id"> </div>

Formularea de mai sus anunţă o modelare tradiţională: un set de funcţii şi variabile disparate (deşi conturate într-un scop comun), care mai şi amestecă lucrurile (ceea ce este "tradiţional", deasemenea) - solicitând parametrii "-id" din exterior (de pe documentul HTML care încorporează scriptul).

Reformulăm, angrenând totul într-un obiect (dar în modul cel mai elementar) - ceea ce pe de o parte înlesneşte extinderea cu noi proprietăţi şi metode, iar pe de altă parte permite o integrare uşoară în diverse contexte, a funcţionalităţii aferente (prin simpla instanţiere a obiectului, în noul context):

function fenObj(fen) {
    this.fen = fen; // FEN-ul primit devine "proprietate internă" a obiectului
    this.validate = function() {
        // metodă a obiectului, pentru testarea proprietăţii sale 'fen'
    };
    this.dumpDiagram = function() {
        // returnează un şir care poate fi alert()-at ca diagramă (litere, pentru piese)
    };
    this.diagram = function() {
        // returnează un şir HTML mai complex (cu imagini ale pieselor)
    };
}

Să observăm că acum nu mai sunt implicate elemente HTML (precum ID-urile din prima formulare) - deci obiectul "fenObj" va putea fi utilizat şi în alte scopuri (de exemplu, pentru menţinerea selectivă a unei liste de FEN-uri, pe parcursul desfăşurării jocului - asigurând posibilitatea reluării jocului dintr-o anumită poziţie din această listă).

"Instanţierea" şi folosirea obiectului decurg după schema (valabilă în mai toate limbajele):

    var fen1 = new fenObj( "8/8/8/8/8/kn1b4/2P5/K7 w - - 0 0" );
    alert( fen1.validate() && fen1.dumpDiagram() );

Operatorul new constituie într-o zonă de memorie liberă, o copie fidelă a elementelor specificate în definiţia obiectului şi returnează o referinţă (salvată mai sus în variabila "fen1") la zona respectivă. Operatorul "punct" . asigură apoi, accesarea proprietăţilor sau metodelor obiectului tocmai creat.

this este totdeauna, o referinţă la obiectul (fen1 mai sus) căruia i se accesează proprietatea sau metoda respectivă. Şi atunci, fiindcă în definiţia de mai sus avem this.validate = function(){...} - rezultă că fiecare dintre obiectele instanţiate prin "new" va conţine codul funcţiei respective (deşi este vorba de unul şi acelaşi cod de funcţie); ar fi de dorit ca instanţele create să "partajeze" acest cod, doar referind zona unică respectivă, în loc de a o copia efectiv în propria zonă de memorie.

Prin urmare… reformulăm definiţia de mai sus (pentru a salva spaţiu de memorie şi timp de construcţie, la instanţierea obiectelor) şi anume, preferăm aici astfel:

function FenObj(fen_string) {
    this.fen = fen_string; 
    this.validate = validate; // referinţă la zona codului funcţiei 
    this.dumpDiagram = dumpDiagram;
    this.diagram = diagram;
};
function validate() {
    // codul funcţiei este "partajat" (prin referinţă), de toate instanţele create
};
function dumpDiagram() { 
    alert(this.fen); // this referă obiectul din care se va invoca metoda
};
function diagram() { };

Rămâne să completăm rând pe rând, corpurile funcţiilor specificate mai sus. Dar nici aceasta nu decurge cum s-ar vrea (sau cum se obişnuieşte în manualele uzuale), de sus în jos şi de la stânga la dreapta… Este firesc să ignorăm - pentru început - funcţia de validare a FEN-ului, rezolvând întâi problema obţinerii diagramelor.

Obţinerea diagramei literale

Presupunem că FEN-ul transmis la construcţia obiectului este unul corect şi ne ocupăm de realizarea funcţiei dumpDiagram().

Câmpurile FEN-ului sunt separate printr-un caracter "spaţiu" şi fiindcă ne interesează numai primul câmp, putem "şterge" secvenţa care începe la primul spaţiu şi continuă până la sfârşit. Şirurile de caractere sunt parcurse de regulă de la stânga spre dreapta, astfel că secvenţa pe care vrem să o eliminăm poate fi indicată prin expresia regulată / .*/ (primul caracter "spaţiu" care apare în şir, urmat de oricâte caractere; * = zero sau mai multe caractere, iar . ţine loc de un caracter oarecare).

// şterge de la primul spaţiu (inclusiv) până la sfârşit:
    var position = this.fen.replace(/ .*/, ''); 

this.fen este un şir de caractere (altfel, funcţia de validare va trebui să-l respingă), având atunci proprietăţile şi metodele obiectului javascript predefinit String(); de exemplu, există proprietatea this.fen.length (având ca valoare lungimea şirului this.fen) şi deasemenea, am putut folosi metoda replace() a obiectului String() pentru a înlocui secvenţa indicată mai sus, prin şirul vid ''.

Obs. this.fen nu este modificat: replace() lucrează pe o copie a şirului dat, returnând rezultatul.

Variabila position conţine opt subşiruri (pentru cele opt linii ale tablei de şah) separate prin caracterul / (vezi mai sus, constituirea primului câmp din FEN). Dar aceste subşiruri au lungimi diferite, datorită prezenţei câmpurilor libere; de exemplu, /kn1b4/2P5/ conţine două subşiruri (încadrate de '/') de lungimi 5 şi 3 (primul subşir reprezintă o linie pe care se află regele negru, cal negru, un câmp liber, nebun negru şi apoi patru câmpuri libere).

Dacă vrem să obţinem o reprezentare tabelară obişnuită, atunci trebuie să completăm toate subşirurile din position, până la lungimea de opt "pătrăţele". Aceasta revine la a înlocui peste tot cifrele care apar (amintim că acestea indică un număr de câmpuri libere consecutive pe linia respectivă) printr-o secvenţă de caractere "spaţiu" cu lungimea egală cu cifra înlocuită:

    var empty_sq = "        ";   // o linie are maxim 8 câmpuri libere consecutive
    position = position.replace( /([1-8])/g, 
                                 function(nr_sp) {
                                     return empty_sq.substr(0, nr_sp);
                                 }
                               );

Cifra [1-8] este căutată în tot şirul (/g specifică "global"), este "captată" (datorită includerii în "paranteza de grupare" ()) pentru a fi utilizată ca parametru "nr_sp" de funcţia anonimă adăugată ca parametru la invocarea metodei replace() (această funcţie extrage un substring de lungime "nr_sp" din şirul empty_sq, iar "replace" va înlocui cifra curentă din şir cu acest substring).

În acest moment, fiecare linie (dintre cele opt separate prin "/", din position) conţine exact opt caractere - fie literele care reprezintă piesele, fie câte un caracter "spaţiu" pentru fiecare câmp liber.

Pentru a ajunge la o reprezentare tabelară, mai trebuie să "bordăm" fiecare caracter din şirul position - pentru a imagina un "pătrăţel" al tablei de şah, ocupat de… caracterul respectiv. În acest scop, putem folosi caracterele "liniare" obişnuite: |, - şi (pentru marcarea "colţurilor") +.

Mai întâi, să prefixăm fiecare caracter (literă, sau spaţiu) cu '|' (nu-i necesar - chiar ar fi greşit - să bordăm caracterul şi la stânga şi la dreapta: bordura la stânga a celui de-al doilea caracter devine "la dreapta" pentru caracterul anterior, ş.a.m.d.):

    position = position.replace( /(\w| )/g,  
                                 function(letter_or_space) {
                                     return "|" + letter_or_space;
                                 }
                               );

Să observăm că numai ultimul caracter, de pe fiecare dintre cele opt linii separate prin '/' - a rămas nebordat la dreapta. Deci înlocuim separatorul '/' cu un separator "tabelar" - constituit din bordura '|' plus caracterul de "trecere pe următoarea linie", \n; dar această "următoarea" linie ar trebui totuşi, să reprezinte "bordura inferioară" (dedesubt) pentru pătrăţelele liniei tocmai încheiate, aşa că adăugăm o secvenţă de bare orizontale şi "colţuri":

    position = position.replace(/\//g, '|\n|-+-+-+-+-+-+-+-|\n');

Caracterul '/' trebuie "escapat" (prin prefixare cu backslash), având alte utilizări decât un caracter obişnuit; mai bine este de folosit direct codul ASCII corespunzător: replace(/\x2f/g, ...).

Să observăm în sfârşit, că - fiindcă am bordat pe orizontală numai dedesubtul liniilor de date şi fiindcă aceasta s-a făcut prin înlocuirea caracterului separator iniţial, '/' - ultimul rând de date (de sus în josul tabelului constituit) a rămas nebordat inferior (fiindcă ultima linie dintre cele opt nu are '/' ca separator faţă de "următoarea" linie, căci nu există linia 9); deasemenea - primei linii din tabel (şi numai ei) i-ar trebui adăugată o bordură deasupra.

Prin urmare, în final position trebuie prefixat şi sufixat cu un şir de bordare corespunzător.

Sintetizând şi înlănţuind acum prelucrările stabilite mai sus, putem constitui următoarea funcţie:

function dumpDiagram(br_nl) {  // '<br>' sau '\n'
    var empty_sq = "        "; // 8 spaţii
    var border_bottom = '|' + br_nl + '+-+-+-+-+-+-+-+-+' + br_nl; // va înlocui '/'
    var border_0 = "+-+-+-+-+-+-+-+-+" + br_nl;  // înaintea primei linii de date
    var border_8 = '|' + br_nl + "+-+-+-+-+-+-+-+-+"; // după ultima linie de date

    var position = this.fen
        .replace( / .*/, '' )
        .replace( /([1-8])/g, function(nr_sp) {
                                 return empty_sq.substr(0, nr_sp);
                              } )
        .replace( /(\w| )/g,  function(p_or_s) {
                                 return "|" + p_or_s;
                              } )
        .replace( /\//g, border_bottom );

    return border_0 + position + border_8;
};

Bineînţeles că nu am inclus "mot-a-mot", formulările anterioare; am prevăzut la început, variabile pentru "borduri" (dacă ulterior am vrea să modificăm stilul de bordare, atunci este suficient să modificăm aceste variabile iniţiale); apoi, am înlănţuit operaţiile "replace" (fiecare "lucrează" pe rezultatul returnat de precedentul "replace").

În plus, am "parametrizat" funcţia (introducând parametrul "br_nl"), prevăzând astfel două posibilităţi de utilizare a ei: dacă vrem doar să "alertăm" diagrama rezultată - alert(fen1.dumpDiagram('\n')); dar dacă vrem să înscriem diagrama în documentul HTML, atunci '\n' trebuie schimbat cu "<br>".

Pentru testarea funcţiei realizate folosim desigur un fişier HTML, de exemplu:

<head>
    <style type="text/css">
    body {
        font-family: Courier;
        font-size: 1.5em;
        font-weight: bold;
        letter-spacing: 0.3em;
        line-height: 0.85em;
    }
    </style>
    <script src="fenob.js"></script>
</head>
<body>
    <script type="text/javascript">
        var fen1 = new FenObj("8/8/8/8/8/kn1b4/2P5/K7 w - - 0 0"); 
        document.write(fen1.dumpDiagram('<br>'));
    </script>
</body>

Încărcându-l în browser, va rezulta diagrama alăturată.

Om fi reuşit ce ne propusesem - dar… poate servi la ceva, această funcţie? (ar fi chiar incomod, să joci şah pe o asemenea diagramă literală şi cu toate câmpurile de aceeaşi culoare)

Poate servi, fiindcă până să reuşeşti un program care "să joace şah" (măcar corect) - sunt de pus la punct multe lucruri…

Concret, nouă ne-a servit la un moment dat, pentru a depista unde greşeşte funcţia mea de generare a listei mutărilor; afişând asemenea diagrame, corespunzător poziţiei curente de pe parcursul generării mutărilor la o anumită adâncime a arborelui mutărilor - am putut deduce mai uşor ce greşisem.

Există "poziţii de test" publice (între care şi cea redată în diagramă), pentru care s-a stabilit numărul de mutări legale până la o anumită adâncime de joc. De exemplu, pe diagrama redată aici albul - aflat în şah - are numai două mutări posibile (Ka1-b1, sau Pc2xb3); la nivelul de mutare următor, negrul va putea răspunde (în total - fie la prima dintre mutările albului, fie la a doua) în 31 de moduri - la care apoi, albul va avea posibile un număr total de 58 de mutări, ş.a.m.d. (pe următorul nivel - 758 de mutări ale negrului, apoi la adâncimea cinci - 2669 mutări ale albului, etc.).

Obţinerea diagramei de şah uzuale

Diagrama de şah obişnuită nu conţine litere, ci piese (imagini standardizate pentru rege, cal, etc.) şi în plus, este bicoloră (două câmpuri vecine au "background" diferit - sau "alb", sau "negru"; câmpul h1 trebuie să fie "alb"; culorile "alb" şi "negru" pot fi alese).

Am putea încerca să folosim exact funcţia de mai sus - renunţând la "borduri" (de oricare fel) şi indicând în al doilea "replace" (cel de parametru "p_or_s") figurinele corespunzătoare literelor… Găsim uşor colecţii publice de imagini (.png, sau .gif) pentru piesele de şah; dar problema subtilă care apare nu de piese ţine, ci de… "spaţiu".

Browserele colapsează caracterele "spaţiu" consecutive (adică, pe diagrama noastră, câmpurile libere alăturate pe linie): când încarcă un fişier HTML, browserul analizează textul respectiv (asociind tagurilor HTML obiectele de memorie predefinite corespunzătoare "afişării" pe ecran, rezultând "modelul obiectual al documentului", DOM), iar când întâlneşte în textul parcurs mai multe caractere "spaţiu" obişnuite consecutive, le reduce la unul singur.

Funcţia reuşită mai sus a mascat problema - prin faptul că au fost implicate "borduri" (la stânga şi dedesubt, pentru fiecare câmp) şi în plus, a fost specificat un font monospaţial ("font-family: Courier"); aceasta "merge" pentru text, dar acum am avea şi text (caracterele "spaţiu") şi imagini.

Câmpurile libere (care în diagrama "literală" erau înlocuite cu caracter "spaţiu") trebuie să apară ca spaţii "pătrate" de o aceeaşi dimensiune, ori "spaţiu" este un caracter, nu o imagine (dimensiunea de caracter e una, dimensiunea imaginii e alta; pot apărea probleme de corelare a dimensiunilor).

În acest context, soluţia pe care o vom adopta aici pentru reprezentarea caracterului "spaţiu" este una veche (dinainte să se fi impus CSS), dar simplă şi încă frecvent utilizată într-o formă sau alta: folosim pentru "spaţiu" o imagine - anume, un PNG transparent de 1 pixel (un spacer) - setându-i atributele hspace şi vspace corespunzător dimensiunii dorite a "pătrăţelului" (aceste atribute sunt dezaprobate deja, dar sunt recunoscute dacă tipul documentului încărcat (DOCTYPE) nu este prea restrictiv).

Metoda cea mai simplă pentru a obţine diagrama dorită implică un element <table>:

function diagram() {
// obţine primul câmp din FEN şi înlocuieşte cifrele cu spaţii
    var empty_sq = "        "; 
    var position = this.fen.replace(/ .*/, '')
        .replace( /([1-8])/g, function(nr_sp) { return empty_sq.substr(0, nr_sp); });

// tagurile <img> care vor înlocui literele (sau spaţiul)
    var pieces = {
        ' ': '<img src="spacer.png" hspace="16px" vspace="16px" />',
        'K': '<img src="wking.png" />', //  
        'Q': '<img src="wqueen.png" />', // 
        'R': '<img src="wrook.png" />', // 
        'B': '<img src="wbishop.png" />', // 
        'N': '<img src="wknight.png" />', // 
        'P': '<img src="wpawn.png" />', // 
        'k': '<img src="bking.png" />', // 
        'q': '<img src="bqueen.png" />', // 
        'r': '<img src="brook.png" />', // 
        'b': '<img src="bbishop.png" />', // 
        'n': '<img src="bknight.png" />', // 
        'p': '<img src="bpawn.png" />', // 
    };

// parcurge position înlocuind caracterele cu <img> şi alternând "background"-ul
    var html = '<table><tr>';
    var qolor = ['white', '#eee']; // background de alternat pe câmpurile vecine
    for(var p = 0, n = position.length; p < n; p++) {
        var piece = position.charAt(p); // literă sau spaţiu sau terminatorul '/'
        switch(piece) {
            case '/': html += '</tr><tr>'; break;
            default:  html += '<td style="background:' + qolor[p % 2] + '">' 
                              + pieces[piece] + '</td>'; break; 
        }
    }
    html += '</tr></table>';

    return html; // fragmentul HTML (un <table>) corespunzător diagramei poziţiei
};

Obs. Am presupus - asamblând doar un "exerciţiu didactic" - că fişierele "*.png" indicate în atributele src se află într-un acelaşi director cu celelalte fişiere implicate (".js" şi ".html").

Pentru testare putem refolosi fişierul HTML anterior; dar putem rescrie <body> mai "creativ":

<body>
<div id="diagrama1"></div>
<div id="diagrama2"></div>
        
<script type="text/javascript">
var fen1 = new FenObj("8/8/8/8/8/kn1b4/2P5/K7 w - - 0 0"),
    fen2 = new FenObj(
           "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -"); 
        
alert( fen1.dumpDiagram('\n') );
        
document.getElementById('diagrama1').innerHTML = fen1.diagram() + '<br>'
                                                 + fen1.fen + '<hr>';
document.getElementById('diagrama2').innerHTML = fen2.diagram() + '<br>'
                                                 + fen2.fen + '<hr>';
</script>
</body>

S-au rezervat în document, diviziuni pentru două diagrame; analizând fişierul la încărcarea sa, browserul le va "transforma" în obiecte subordonate obiectului predefinit document (şi care au între multe altele şi proprietatea .innerHTML, care poate fi setată - într-un <script> - cu un fragment HTML, cum este aici rezultatul returnat de .diagram()). S-a completat fragmentul corespunzător diagramei, adăugând (după elementul <br>) valoarea proprietăţii .fen corespunzătoare şi apoi un element de separare ("horizontal line") faţă de următoarea diagramă.

vezi Cărţile mele (de programare)

docerpro | Prev | Next