dezvoltare inductiv-corelativă a unei aplicaţii Web
O aplicaţie Web exprimă un subiect dintr-un anumit domeniu, corelând limbaje specifice browserelor.
Domeniul ales aici este şahul; pentru a dezvolta o aplicaţie de şah - trebuie să cunoşti sau să înveţi, măcar regulile de bază ale acestui joc (la fel cum, dacă ţi se cere o aplicaţie pentru evidenţa contabilă a unei firme - trebuie să cunoşti sau să înveţi principiile de bază ale contabilităţii).
Subiectul exprimat aici este partida de şah furnizată într-un anumit format; "exprimarea" vizată este în fond cea obişnuită pentru un tabel de date: vrei să parcurgi înregistrările dintr-un punct sau din altul, într-un sens sau în altul; o "înregistrare" ar fi acum poziţia rezultată pe tabla de şah prin efectuarea mutării curente.
Aplicaţia ca atare, se realizează poate în două-trei săptămâni, se postează pe un site şi "gata" - cel mult, mai publici şi codul (comentat "profesionist" adică, în primul rând cât mai expeditiv) şi adaugi un "README" în care explici cum se folosesc butoanele aplicaţiei.
Dar aici prezentăm procesul de realizare treptată a aplicaţiei, evidenţiind extinderea progresivă a cunoştinţelor necesitată de constituirea ei.
Presupunem cunoştinţe de nivel elementar, de şah dar mai ales de programare (inclusiv, de HTML, CSS şi javaScript). Mai presupunem deprinderea de a folosi Google - necesară pentru a suplini defectul inerent al acestui studiu de a nu fi un manual.
Ceea ce urmărim este corelarea şi amplificarea treptată a acestor cunoştinţe, plecând de la conturarea problemelor care ar trebui rezolvate pentru construirea aplicaţiei.
"Corelarea şi amplificarea" vizează componentele specifice unui browser (HTML, CSS, javaScript, DOM) dar şi alte limbaje - de exemplu vom folosi "shell"-ul obişnuit (Bash), automatizând unele operaţii printr-un script corespunzător şi vom invoca unele programe utilitare, de exemplu pentru prelucrări de imagini; vom folosi jQuery, arătând cum construim aplicaţia sub forma unui "widget".
Pe de altă parte, "corelare şi amplificare" vizează câteva reprezentări devenite standard, pentru partidele şi poziţiile de şah, precum şi pentru tabla de şah (PGN, FEN, 0x88).
În realitate însă, acest studiu este ulterior constituirii aplicaţiei(1) pe care o vizăm pe parcurs, fiind vorba în fond de o regândire de la capăt a lucrurilor, punându-le acum şi într-o perspectivă didactică. Iar această "regândire de la capăt" a condus şi la simplificări importante ale codului existent (inclusiv, la structurarea finală a acestuia sub forma unui widget() jQuery).
Una dintre aceste "simplificări importante" vizează ideea de a folosi aceleaşi definiţii de poziţionare pentru toate seturile de piese. Fiind obligat să analizez chestiunea poziţionării unei imagini dintr-un sprite, am reuşit până la urmă o "contribuţie proprie" - această regulă neaşteptat de simplă: dacă imaginile care compun un sprite au toate aceeaşi lăţime, atunci poziţionarea imaginii de index K într-un <div> de aceeaşi lăţime se face prin background-position: -K*100%.
Vrând să exprimi printr-o aplicaţie proprie un anumit subiect, se cuvine să începi prin a investiga cât de cât ce realizări există deja pentru acel subiect, urmând eventual să-ţi constitui anumite repere.
Programele care modelează jocul de şah sunt circumscrise domeniului computer chess, categoria principală fiind programele care joacă şah; un astfel de program determină (folosind anumite structuri de date, metode de căutare şi de evaluare) cea mai promiţătoare mutare ca răspuns la mutarea curentă a utilizatorului (partenerul de joc). Subiectul nostru este într-o zonă particulară, ţinând doar de vizualizarea poziţiilor corespunzătoare mutărilor din textul final al partidei.
Punctăm aici câteva aspecte şi direcţii caracteristice pentru modelarea tablei de şah bazată pe HTML, CSS şi javaScript.
Cel mai obişnuit, tabla de şah este modelată printr-o structură HTML <table>. De exemplu, folosind Firebug putem vedea că pgn4web procedează astfel:
Elementele <td> corespunzătoare câmpurilor tablei au setate proprietăţile CSS width şi height şi sunt atributate cu valign="middle" align="center" - obţinând poziţionarea conţinutului <img> exact în centru (ceea ce este mai dificil fără <table>).
<img>-urile corespund pieselor existente pe câmpurile respective şi au specificată o clasă CSS pentru dimensiuni şi atributul src care indică browserului fişierul-imagine pe care va trebui să-l încarce (pentru fiecare piesă).
În plus, toate acestea au specificate atribute de identificare "id" şi handlere "onclick()" (care gestionează mutarea piesei). Pe figura de mai sus este marcat elementul <td id="tcol7row0" >, având drept conţinut un element <img id="img_tcol7trow0" > - interpretarea fiind: pe câmpul h8 se află un turn negru ("col7" este coloana "h", iar "row0" este linia 8 a tablei).
La urma urmei - văzând cum se identifică fiecare "celulă" şi cum se asociază fiecărei celule câte o "formulă" onclick() - am putea zice că se lucrează ca în Excel… Desigur (ca pentru orice tabel) - putem face un mic experiment: am decupat de sub Firebug textul HTML asociat tablei, l-am salvat cu extensia ".html" şi apoi l-am încărcat în Gnumeric (redăm alături rezultatul). Probabil ar fi doar o "chestiune muncitorească", să transformi (şi… să denaturezi) mai departe toată aplicaţia, în Excel; dar pentru noi a fost mult mai interesant textul HTML asociat tablei, decât ceea ce am putea obţine cu Excel.
Există şi modelări ale tablei de şah care nu folosesc <table>, ci numai elemente <div> cu anumite proprietăţi CSS. Astfel, în DHTML-chess tabla de şah propriu-zisă este (din 2011):
Setarea position: relative (împreună cu fixarea dimensiunilor), permite poziţionarea câmpurilor în raport cu marginile tablei. Această diviziune conţine întâi, 64 de <div> precum:
Având fiecare float: left şi dimensiunea comună de 45px, rezultă că ele se vor poziţiona una după cealaltă, pe 8 linii succesive (8*45px = 360px = lăţimea tablei, fixată anterior).
Imediat după aceste 64 de elemente de "marcare" a câmpurilor, se află maximum 32 de <div> în care se ambalează imaginile pieselor existente pe tablă:
Cu position: absolute, acestea vor fi poziţionate la distanţa indicată de left faţă de marginea stângă a tablei şi la distanţa dată de top faţă de marginea de sus. Este de observat că pentru realizarea operaţiei de "inversare" a tablei va fi necesar să se modifice valorile "top" şi "left" ale diviziunilor corespunzătoare pieselor.
Dar structurile CSS folosite s-ar putea corela mai bine cu unele necesităţi fireşti de operare asupra tablei de şah. Sunt deja câteva idei - furnizate în 2008 de chess.dynarch.com - care simplifică infrastructura HTML-CSS necesară şi măresc gradul de corelare cu dinamica tablei de şah.
Pentru facilitarea operaţiei de inversare a tablei se pot considera clase CSS care să corespundă valorilor "top" şi "left", pentru fiecare dintre cele două orientări posibile ale tablei. Grupând apoi într-o clasă "colectoare" acele dintre aceste clase care corespund unei aceleiaşi orientări, inversarea tablei va reveni la simpla adăugare sau ştergere a acestei clase în <div>-ul de bază al tablei.
În plus, folosind aceste clase (cu "top" şi "left") - apare imediat şi posibilitatea poziţionării directe a câmpurilor, fără a mai folosi "float: left" (şi astfel, cele 64 de <div> incluse mai sus doar pentru alinierea câmpurilor, nu mai sunt necesare).
De asemenea, imaginile celor 12 piese de şah pot fi comasate într-una singură (creând un sprite) - ceea ce este important dacă se vizează folosirea mai multor seturi de piese (browserul va avea de încărcat mai puţine imagini).
Despre toate acestea urmează să discutăm mai departe, într-o anumită logică inductivă, fără a ne feri de probleme şi poate de mici inovaţii. Sintetizăm principalele repere ale aplicaţiei noastre:
- creem o infrastructură DOM + CSS bazată pe poziţionare (a câmpurilor pe tabla de şah şi a pieselor dintr-un sprite pe câmpurile tablei), care să depindă cât mai puţin posibil de dimensiunea aleasă pentru câmp, de setul de piese ales şi de orientarea curentă a tablei;
- creem o a doua infrastructură (bazată pe reprezentarea 0x88), servind pentru parcurgerea internă prealabilă a partidei, în mod secvenţial - permiţând verificarea legalităţii mutărilor şi constituirea unui tabel în care o "înregistrare" este poziţia rezultată pe tabla de şah prin efectuarea mutării curente;
- asociem acestui tabel de "înregistrări" un mecanism de navigare, legând între ele cele două infrastructuri menţionate.
(1) Aplicaţia şahPGN a fost realizată (în prima ei formă) în 2008.
Să zicem că avem un "container" <div> şi vrem să "aşezăm" înăuntru un "pătrăţel" astfel încât primul colţ al acestuia să fie la un sfert din lăţimea şi respectiv, înălţimea tablei.
Realizăm aceasta setând proprietăţile position: relative pentru container şi position: absolute pentru elementul conţinut:
<!DOCTYPE html>
<head>
<style>
.container {
width: 160px; height: 160px;
position: relative;
border: 1px solid black; }
.field {
width: 20px; height: 20px;
position: absolute;
top: 25%; left: 25%;
background: black; }
</style>
</head>
<body>
<div class="container">
<div class="field"></div>
</div>
</body>
Textul-sursă HTML redat mai sus, împreună cu imaginea a ceea ce se obţine accesând în Firefox fişierul respectiv - constituie un alt exemplu (şi unul real) de poziţionare:
<div style="position:relative;"> <!-- container pentru <pre> şi <img> -->
<img src="rel-abs.png" style="position:absolute; top:35%; left:60%;" />
<pre class="html" style="width:60%;">
<!-- textul-sursă HTML redat mai sus -->
<pre>
</div>
class="html" serveşte aici pentru marcarea sintactică a textului-HTML conţinut de <pre>.
Dar mai mult, să zicem că avem nu doar unul, ci chiar 8 pătrăţele şi vrem să le poziţionăm unul lângă celălalt. Am putea repeta definiţiile ".field" de 8 ori, modificând doar valoarea pentru left (zero pentru primul, apoi mărind cu câte 160/8=20px pentru următoarele pătrăţele).
Dar avem o soluţie mai elastică: asociem pătrăţelelor o a doua clasă de proprietăţi ".Col-n" - conţinând numai left corespunzător fiecăruia (lăsând în ".field" numai "top", care are aceeaşi valoare pentru toate).
Pentru a putea distinge pătrăţelele, adăugăm un "border" în clasa .field; ca urmare, modificăm lăţimea containerului: 160 px + 8*(1px border-stânga + 1px border-dreapta) = 176px. Măsurăm valorile "left" în pixeli: 20px width + 1px border-stânga + 1 px border-dreapta = 22px, încât valorile sunt 0, 22px, 44px, etc. (în procente calculul devine incomod).
<head>
<style>
.container {
width: 176px; height: 176px; /* 160 + 8*2 (2px border la .field) */
position: relative;
border: 1px solid black; }
.field {
width: 20px; height: 20px;
position: absolute;
top: 25%; /* left: 25%; */
border:1px solid white;
background: black; }
.Col-1 { left: 0 }
.Col-2 { left: 22px } /* width + border_orizontal */
.Col-3 { left: 44px }
.Col-4 { left: 66px }
.Col-5 { left: 88px }
.Col-6 { left: 110px }
.Col-7 { left: 132px }
.Col-8 { left: 154px }
</style>
</head>
<body>
<div class="container"> <!-- position: relative -->
<!-- position: absolute
cu `top` de la definiţia .field
şi `left` de la definiţia .Col-n -->
<div class="field Col-1"></div>
<div class="field Col-2"></div>
<div class="field Col-3"></div>
<div class="field Col-4"></div>
<div class="field Col-5"></div>
<div class="field Col-6"></div>
<div class="field Col-7"></div>
<div class="field Col-8"></div>
</div>
</body>
În sfârşit - să zicem că avem de poziţionat nişte pătrăţele în interiorul containerului nu numai pe orizontală, dar şi pe verticală…
O tablă de şah are 64 de pătrăţele, pe 8 linii şi 8 coloane. Eliminăm "top" din clasa .field şi definim clasele "Row-n", similare cu "Col-n" (şi conţinând "top" pentru fiecare linie). Pătrăţelele tablei vor fi definite acum prin <div class="field Col-n Row-n">, unde .field setează "position: absolute" şi indică browserului dimensiunile pătrăţelului, iar .Col-n şi .Row-n precizează "left" şi "top".
Acum este clar avantajul separării proprietăţilor "top" şi "left": în loc de 64 definiţii .field care ar conţine şi setările pentru "top" şi "left", avem numai 17 definiţii: una comună pentru .field (setează position: absolute şi precizează dimensiunile câmpului), 8 definiţii "Col-n" (precizează "left" pentru fiecare din cele 8 coloane) şi 8 definiţii "Row-n" ("top" pentru fiecare rând al tablei).
Desigur, nu-i de loc de dorit să scrii manual (într-un fişier HTML) 64 de asemenea <div>… Putem obţine "automat" fişierul respectiv, folosind orice limbaj de nivel înalt; dar cel mai bine este să folosim javaScript, având în vedere că HTML este destinat unui browser, iar browserele moderne încorporează şi un interpretor de javaScript.
<!DOCTYPE html>
<head>
<style>
.container {
width: 176px; height: 176px;
position: relative;
border: 1px solid black; }
.field {
width: 20px; height: 20px;
position: absolute;
padding: 1px; /* în loc de `border` */ }
.WhiteField { background: white; }
.BlackField { background: lightgrey; }
.Col-1 { left: 0px } .Row-1 { top: 0px }
.Col-2 { left: 22px } .Row-2 { top: 22px }
.Col-3 { left: 44px } .Row-3 { top: 44px }
.Col-4 { left: 66px } .Row-4 { top: 66px }
.Col-5 { left: 88px } .Row-5 { top: 88px }
.Col-6 { left: 110px } .Row-6 { top: 110px }
.Col-7 { left: 132px } .Row-7 { top: 132px }
.Col-8 { left: 154px } .Row-8 { top: 154px }
</style>
</head>
<body>
<div class="container" id="tabla"></div>
<script>
function setChessTable() {
var html = [];
for (var row = 1; row <= 8; row++) {
for (var col = 1; col <= 8; col++) {
var fieldColor = (row + col) & 1 ?
"BlackField" : "WhiteField";
html.push("<div class='field",
" Row-", row,
" Col-", col,
" ", fieldColor,
"'></div>")
}
}
return html.join('');
}
document.getElementById("tabla")
.innerHTML = setChessTable();
</script>
</body>
Am adăugat "pătrăţelelor" şi clasa .BlackField, sau .WhiteField - asigurând alternanţa "background" a câmpurilor (câmpul fiind "negru" în cazul în care suma indicilor 1..8 de linie şi coloană este impară). Cu Firebug putem vedea că apelul setChessTable() a constituit în diviziunea indicată 64 de elemente <div>, fiecare având specificate câte patru clase de proprietăţi CSS:
Să observăm acum avantajul de a folosi direct javaScript (în contextul de aici), în loc de un alt limbaj de nivel înalt. Dacă foloseam C++, sau PHP, sau Python, etc. - puteam obţine un fişier static, având deja cele 64 de elemente <div>. În acest caz browserul nu avea nimic altceva de făcut decât să încarce şi să redea "mot-à-mot" conţinutul respectiv (chiar avantajos, dar numai temporar).
În schimb, funcţia javaScript setChessTable() scrisă în fişierul nostru a permis constituirea dinamică a diviziunilor respective: fişierul încărcat de browser conţinea doar <div class="container"> având conţinutul vid; dar imediat după ce l-a încărcat, browserul a trebuit să execute instrucţiunea din <script> document.getElementById("tabla").innerHTML = setChessTable(), ceea ce a determinat execuţia funcţiei setChessTable() şi inserarea în DOM a celor 64 de <div> vizualizate mai sus.
Dacă tabla de şah ar trebui doar afişată ca atare, atunci desigur că generarea statică a fişierului necesar este preferabilă (dacă nu şi mai simplu: folosim Word-ul…). Dar pe tabla de şah vor exista piese, iar acestea îşi vor schimba poziţia în timpul jocului - apărând eventual necesitatea regenerării tablei; ca urmare, generarea dinamică (folosind javaScript) este cea avantajoasă.
Desigur - pentru a avea o "tablă de şah" veritabilă, mai sunt câteva lucruri de considerat: trebuie adăugată o diviziune dedesubtul tablei pentru a nota coloanele (cu 'a', 'b', ..., 'h') şi una în stânga tablei pentru a nota liniile (cu 1..8); iar aceste două diviziuni pot fi şi ele poziţionate "absolute" faţă de tabla respectivă (şi pot refolosi definiţiile "Col-n" şi "Row-n").
Dar aspectul cel mai important ţine de dinamica tablei de şah: în orice moment, există nişte piese de şah care ocupă anumite câmpuri. Pe de altă parte, în orice moment (şi cât mai simplu) tabla de şah trebuie să poată fi "inversată" (albul jos - cu notaţia coloanelor "a..h" şi a liniilor "1..8" - sau albul sus, cu notaţia "h..a" şi "8..1"; poziţiile pieselor trebuie şi ele "răsturnate" odată cu notaţia).
setChessTable() constituia tabla de şah - într-un <div> cu proprietatea "position: relative", servind drept container - ca o succesiune de 64 elemente
<div class="field Row-1 Col-1 WhiteField"></div>
unde .field defineşte dimensiunile câmpului şi marchează diviziunea respectivă cu "position: absolute", iar Col-n şi Row-n (cu n = 1..8) definesc valorile "left" şi respectiv "top" pentru câmpul respectiv (proprietăţile CSS implicate astfel asigură alinierea câmpurilor în raport cu marginea stângă şi cu cea de sus a containerului).
Iniţial, toate aceste 64 de <div> sunt vide. Procedeul prin care vom înscrie o anumită piesă într-un anumit câmp al tablei este evident: identificăm acel <div> dintre cele 64 existente care corespunde câmpului şi îi înscriem drept conţinut un <div> care să conţină piesa.
Având de modelat termeni ca "piesă" şi însuşi procedeul tocmai descris, vor fi de conceput noi funcţii şi proprietăţi, astfel că este firesc să reformulăm codul precedent - mai întâi separând lucrurile:
<!DOCTYPE html>
<head>
<link href="brw-sah.css" rel="stylesheet" />
<script src="brw-sah.js"></script>
</head>
<body>
<div class="container" id="tabla"></div>
<script>
document.getElementById("tabla")
.innerHTML = setChessTable();
</script>
</body>
La încărcarea acestui fişier, browserul va căuta fişierele indicate în <head> ("brw-sah.css" ar conţine definiţiile .field, .Col-n, etc. iar "brw-sah.js" ar conţine funcţia setChessTable()) şi le va încărca şi pe acestea, apoi va crea elementul indicat în <body> şi va încheia procesul prin execuţia instrucţiunii din <script>-ul final (rezultatul fiind exact cel redat deja în (I)).
Iar pentru a ne uşura dezvoltarea aplicaţiei, alegem să folosim framework-ul jQuery şi adăugăm (în <head>, de obicei) <script>-ul necesar pentru includerea şi a acestei biblioteci.
Dar pentru "dezvoltarea aplicaţiei", trebuie mai întâi să ne lămurim: care aplicaţie? Încărcând într-un browser HTML-ul de mai sus obţii o tablă de şah şi atât… unde-i "aplicaţia"?
Să observăm însă o întrebare intermediară simplă: cine va "încărca într-un browser HTML-ul de mai sus"… Ca să putem vorbi de "aplicaţie" trebuie să mai avem în vedere un utilizator, imaginându-ne cât mai bine interesele acestuia - spre a le satisface prin aplicaţia respectivă.
De obicei, "utilizatorul" este considerat a fi o persoană; dar trebuie să avem în vedere şi browserul prin intermediul căruia persoana accesează aplicaţia - şi deja apare necesitatea unor precauţii, fiindcă există diferenţe de interpretare (pentru CSS, DOM, javaScript) între browsere (şi iată un avantaj important al folosirii unui framework; jQuery rezolvă "în spate" diversele probleme de compatibilitate între browserele moderne). Deasemenea, "utilizator" poate fi considerat şi vreun alt program, care poate şi el să acceseze o componentă sau alta din aplicaţia respectivă.
Ne propunem deocamdată acest scenariu: utilizatorul va transmite (sub o anumită formă) o poziţie de şah, aşteptând din partea aplicaţiei noastre să-i redea diagrama corespunzătoare. Aşadar, dorim să modelăm acest mecanism: "transmite poziţia - iată diagrama ei"; repetând după fiecare mutare - utilizatorul va putea urmări desfăşurarea întregii partide.
În prealabil, n-ar trebui să ratăm observaţia simplă că tabla ca atare - vidă - ar trebui creată o singură dată, la început; ulterior, fiecare poziţie transmisă trebuie eventual verificată (să fie o "poziţie de şah", nu altceva) şi înscrisă pe tabla deja existentă.
jQuery ne oferă posibilitatea de a dezvolta aplicaţia sub forma unui widget - o componentă Web independentă (reutilizabilă) care poate fi integrată (printr-o declaraţie foarte simplă) unuia sau mai multor elemente existente într-un document HTML.
Pentru aceasta, trebuie întâi să descărcăm componenta "Widget" de la jqueryui.com, obţinând fişierele "jquery.ui.core.min.js" şi "jquery.ui.widget.min.js". Constituim subdirectoare /js/ şi /css/ şi reformulăm fişierul HTML astfel:
<!DOCTYPE html>
<head>
<script src="js/jquery.min.js"></script>
<script src="js/jquery.ui.core.min.js"></script>
<script src="js/jquery.ui.widget.min.js"></script>
<link href="css/brw-sah.css" rel="stylesheet" />
<script src="js/brw-sah.js"></script>
</head>
<body>
<div class="container"></div>
<p class="container"></p>
<script>
$(function() {
// instanţiază widget-ul pe fiecare "container"
$('.container').fenBrowser();
});
</script>
unde widgetul "fenBrowser()" conţine metodele standard _create() şi _init(), precum şi metoda _setChessTable() (cu exact acelaşi conţinut ca şi funcţia "setChessTable()" scrisă anterior):
/* js/brw-sah.js */
(function ($) {
$.widget("brw.fenBrowser", {
// this.element reprezintă "containerul" pe care se instanţiază widget-ul
_create: function() {
this.element.html( this._setChessTable() );
},
_init: function() {
/* înscrie pe tablă poziţia transmisă */
},
_setChessTable: function() {
/* mutăm aici conţinutul funcţiei setChessTable() anterioare */
// var html = []; ...; return html.join('');
}
});
})(jQuery);
_create() - care apelează setChessTable() - va fi executată o singură dată, anume în momentul instanţierii widget-ului; deci avem deja (chiar gratis, datorită jQuery.widget()) ceea ce doream: tabla de şah vidă este creată o singură dată (şi nu după fiecare nouă transmitere de poziţie).
Rămâne să implicăm în _create() un element HTML - un <input> de exemplu - în care utilizatorul (presupunând că este o persoană) să poată furniza o poziţie de şah şi să completăm _init() cu un cod corespunzător pentru "citirea" acestei poziţii şi pentru înscrierea ei pe tabla creată.
Pentru o chestiune de genul "daţi mediile la obiectele şcolare - iată media generală" putem accepta că utilizatorul va introduce un tablou de valori: prima medie, a doua, ş.a.m.d., sau eventual un tablou-asociativ, conţinând perechi "obiect: medie".
Am putea proceda la fel, cerând utilizatorului un tablou POZ[64] în care POZ[k] să reprezinte piesa plasată pe câmpul de index k (sau zero dacă acest câmp este liber), sau un tablou-asociativ POZ = {câmp: piesă} în care ar apărea numai câmpurile care conţin piese.
Pentru exerciţiile din manualele uzuale, transmiterea datelor sub forma unui tablou de valori este ceva aproape indiscutabil, de la sine înţeles… Dar dacă s-ar pune problema arhivării datelor, sau comunicării lor între programe - atunci reprezentarea ca text simplu este clar preferabilă.
Pentru chestiunea mediilor şcolare am putea imagina o reprezentare textuală simplă a datelor: "a95/b10/c775" ar putea reprezenta foarte bine tabloul {'a': 9.5, 'b': 10, 'c': 7.75}. Am codificat fiecare obiect prin câte o literă; în reprezentarea textuală a mediei nu este necesar să apară şi '.' fiindcă mediile au valori între 1 şi 10 (deci 'c775' nu poate fi interpretat decât ca fiind 7.75); am prevăzut '/' pentru a separa obiectele (dar puteam şi renunţa la separator: "a95b10c775").
În mod analog putem imagina o reprezentare textuală pentru poziţiile de şah - dar deja FEN este formatul standard de reprezentare textuală a poziţiei de şah.
Cele 12 piese de şah sunt codificate prin câte o literă, folosind majuscule pentru piesele albe şi litere mici pentru piesele negre: p pentru pion, k pentru rege ("king"), q pentru damă ("queen"), r pentru turn ('rook'), b pentru nebun ('bishop') şi n pentru cal ('knight').
Fiecărei linii a tablei de şah i se asociază un şir de maximum 8 caractere, indicând piesele existente pe linia respectivă începând de la stânga spre dreapta şi folosind câte o cifră 1..8 pentru a indica eventual numărul de câmpuri libere aflate în dreapta. De exemplu, linia ['', 'P', '', '', 'k', 'p', '', ''] este reprezentată prin "1P2kp2"; iar o linie fără piese se reprezintă prin şirul "8".
Reprezentările celor 8 linii sunt separate prin '/' şi sunt concatenate în ordinea "de sus în jos" (în reprezentarea standard a tablei, albul ocupă liniile 1 şi 2 aflate în partea de jos a tablei, iar negrul ocupă liniile 7 şi 8 aflate în partea de sus).
De exemplu, poziţia iniţială are notaţia rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR.
În acest moment ne putem rezuma la această reprezentare a poziţiei. Mai târziu va trebui să implicăm şi celelalte porţiuni din reprezentarea FEN (destinate unor informaţii contextuale: cine este la mutare, ce drepturi de rocadă sunt valabile în poziţia respectivă, şi altele).
Acum, fişierul HTML trebuie să conţină un element <input> sau <textarea> (pentru ca utilizatorul să introducă FEN-ul) şi cel mai firesc este ca fenBrowser() să fie instanţiat chiar pe acest element:
în final va rezulta:
<body>
<textarea id="getFEN"></textarea>
<script>
$(function() {
$('#getFEN').fenBrowser();
});
</script>
</body>
Să ne amintim că setChessTable() returna un fragment HTML (şirul celor 64 de elemente <div> pentru câmpurile tablei) care trebuia înscris drept conţinut unui element cu proprietatea "position: relative". Fiindcă am scutit fişierul HTML de prevederea un astfel de element "container" (şi aşa se cuvine), revine metodei _create() să adauge acest element (înainte de a apela _setChessTable()).
Deasemenea, trebuie adăugat (conform uzanţelor) un buton "Load" cu handler-ul corespunzător: click pe "Load" va declanşa metoda _init() (care va "citi" ceea ce a tastat utilizatorul, va verifica dacă este un FEN valid şi dacă da, atunci va înscrie poziţia respectivă pe tablă).
/* js/brw-sah.js */
(function ($) {
$.widget("brw.fenBrowser", {
_create: function() {
var inputFEN = this.element, // <input> sau <textarea>
self = this; // salvează referinţa 'this' la instanţa curentă
// adaugă un buton 'Load', cu 'click()' pentru încărcarea poziţiei
$('<button></button>').text('Load')
.click(function() {
self._init(); // dar NU "this"._init()
return false;
})
.insertAfter( inputFEN );
// adaugă un "container" pentru tablă (cu 'position: relative')
$('<div class="container"></div>')
.insertAfter( inputFEN.next() )
// în care acum, inserează cele 64 de câmpuri
.html( this._setChessTable() );
},
_init: function() {
var fen = this.element.val();
// deocamdată, doar "alertează" conţinutul introdus
if(fen) alert(fen);
},
_setChessTable: function() {
/* mutăm aici conţinutul funcţiei setChessTable() anterioare */
// var html = []; ...; return html.join('');
}
});
})(jQuery);
Efectuând aceste modificări în fişierele anterioare şi încărcând într-un browser fişierul HTML, va rezulta ceea ce am redat deja în imaginea alăturată mai sus textului HTML. Dacă se va tasta ceva (în caseta corespunzătoare lui <textarea>), atunci click pe "Load" va afişa textul respectiv.
Obs. În interiorul unui obiect javaScript, this referă însuşi acest obiect. Obiectul $('<button/>') este creat în contextul instanţei curente a obiectului fenBrowser() (mai simplu spus: creat înăuntrul widget-ului); în interiorul butonului creat, "this" va referi acest button - încât this._init() ar însemna apelarea unei metode "_init()" proprii obiectului-buton respectiv (ori aici aveam nevoie să invocăm metoda _init() a widget-ului, nu a butonului). Rezolvarea tipică a situaţiei constă în salvarea prealabilă self = this a referinţei la instanţa curentă a widget-ului, urmând ca în interiorul codului butonului să folosim "self._init()" (vezi codul redat mai sus).
Să "finalizăm" lucrurile: trebuie "citit" şi validat FEN-ul introdus de utilizator şi trebuie înscrisă poziţia respectivă pe tabla de şah.
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR este reprezentarea FEN a poziţiei iniţiale (vizăm doar primul câmp din FEN şi nu formatul complet). Dacă am şterge caracterele / şi am înlocui 8 cu câte o secvenţă de 8 caractere spaţiu - mai general, dacă am înlocui cifrele din FEN cu câte o secvenţă de spaţii - atunci am obţine un şir de exact 64 caractere.
În şirul obţinut astfel, indexarea de la stânga la dreapta corespunde indexării tablei de şah de sus în jos şi de la stânga la dreapta; tot aceasta este şi ordinea în care au fost înscrise (la apelarea funcţiei setChessTable()) cele 64 de elemente <div> corespunzătoare câmpurilor tablei.
Prin urmare, n-avem decât să parcurgem aceste 64 <div>-câmpuri în ordinea în care sunt înscrise, să accesăm FEN-ul la indexul dat de această parcurgere şi să înscriem în câmpul respectiv piesa găsită astfel în FEN - anume, ca un <div class="piesa">piesa respectivă<div>.
Desigur, dacă "piesa" găsită astfel în FEN este spaţiu (în urma înlocuirii cifrelor prin secvenţe de spaţii) atunci va trebui să ştergem (dacă există) acest <div> intern câmpului, fiindcă el ar reprezenta o piesă "veche" (rămasă dintr-o înscriere anterioară).
Un lucru aparent important este validarea FEN-ului: unii utilizatori ar introduce "asdf23" drept FEN. În acest scop vom defini o funcţie separată (dar "vecină" definiţiei widget-ului, cu acces mai rapid decât dacă am defini-o global); procedăm tocmai aşa, pentru faptul că această funcţie va fi inutilă (şi o vom elimina ulterior): în realitate FEN-ul va fi sau preluat prin Copy&Paste de undeva, sau va fi transmis automat dintr-un alt program de şah - fiind implicit, deja validat.
Iar pe de altă parte, procedând "tocmai aşa", punem funcţia respectivă la dispoziţia oricărei instanţe a widget-ului; altfel, dacă ar fi plasată intern, atunci fiecare instanţă ar fi căpătat (inutil) câte o copie a funcţiei (ori evident, verificarea FEN nu depinde de instanţa care o solicită, ci doar de FEN).
Validarea FEN-ului ţine seama în principal, de faptul că el trebuie să conţină 8 secvenţe separate prin / (dar nu şi după ultima dintre acestea), fiecare conţinând sau piese în notaţia standard, sau cifre 1..8. Este drept că ar trebui să mai avem în vedere şi numărul de piese identice - de exemplu, trebuie să existe un singur "K" (există un rege alb şi numai unul), nu pot exista decât maximum 8 pioni de aceeaşi culoare, etc.; pentru simplitate, ignorăm aici aceste aspecte.
Rescriem fişierul "brw-sah.js", încorporând cele explicate mai sus; îl redăm aici complet:
/* js/brw-sah.js */
(function ($) {
/* variabile şi funcţii accesibile oricărei instanţe a widget-ului
(dar numai acestora) */
var FEN_STD =
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'; // poziţia iniţială
function isFEN(fen) { // validează (parţial) un FEN
var FENpattern =
/\s*([rnbqkpRNBQKP12345678]+\/){7}([rnbqkpRNBQKP12345678]+)\s*/;
if(! FENpattern.test(fen)) {
alert("FEN invalid");
return false;
}
else return true;
};
$.widget("brw.fenBrowser", {
_create: function() {
var inputFEN = this.element, // <input> sau <textarea>
self = this; // salvează referinţa 'this' la instanţa curentă
$('<button></button>').text('Load') // înscrie poziţia, la click()
.click(function() {
self._init(); // dar NU "this"._init()
return false;
})
.insertAfter( inputFEN );
// adaugă un "container" (cu 'position: relative')
// this.Tabla va referi intern acest container
this.Tabla = $('<div class="container"></div>')
.insertAfter( inputFEN.next() )
.html( this._setChessTable() ); // înscrie 64 <div>-câmpuri
},
_init: function() {
var fen = this.element.val() || FEN_STD;
if(isFEN(fen)) // validează FEN şi înscrie noua poziţie
this._setPosition(fen);
},
_setChessTable: function() {
var html = [];
for (var row = 1; row <= 8; row++) {
for (var col = 1; col <= 8; col++) {
var fieldColor = (row + col) & 1 ?
"BlackField" : "WhiteField";
html.push("<div class='field",
" Row-", row,
" Col-", col,
" ", fieldColor,
"'></div>")
}
}
return html.join('');
},
_setPosition: function(fen) {
/* elimină '/' şi înlocuieşte fiecare cifră
cu un număr corespunzător de spaţii */
var fen64 = fen.replace(/\x2f/g, "")
.replace(/[1-8]/g, function(nr_sp) {
return " ".substr(0, nr_sp);
});
/* parcurge în acelaşi sens cele 64 <div> şi FEN-ul
înscriind piesa din FEN, sau ştergând vechea "piesă" */
this.Tabla.children().each(function(index) {
// în interior, "this" indică obiectul "child" curent
var piece = fen64.charAt(index);
if (piece && piece != " ")
$(this).html("<div class="piece">" + piece + "</div>");
else
$(this).html(""); // şterge piesa veche, dacă există
});
}
});
})(jQuery);
Pentru exemplificare considerăm fişierul HTML redat mai jos. fenBrowser() este instanţiat şi pe un element <input> şi pe un <textarea>; la instanţiere se execută _create() (care, apelând setChessTable() produce "tabla de şah vidă") şi apoi (automat) se execută şi _init() (care iniţial, apelează setPosition(FEN_STD) - producând apariţia poziţiei iniţiale pe tabla creată).
După instanţiere, am introdus un FEN în <textarea>; handler-ul de click definit pe butonul "Load" a determinat execuţia _init(), care a apelat setPosition() pentru FEN-ul introdus - determinând înscrierea noii poziţii pe tabla deja existentă.
<!DOCTYPE html>
<head>
<script src="js/jquery-1.7.2.min.js"></script>
<script src="js/jquery.ui.core.min.js"></script>
<script src="js/jquery.ui.widget.min.js"></script>
<link href="css/brw-sah.css" rel="stylesheet" />
<script src="js/brw-sah.js"></script>
</head>
<body>
<p><input class='getFEN' style='width:200px;'></p>
<p><textarea class='getFEN'></textarea></p>
<script>
$(function() {
$('.getFEN').fenBrowser();
});
</script>
</body>
Dacă inspectăm sursa HTML (folosind "View Generated Source" în Firefox, sau poate mai simplu: folosind Firebug) putem vedea cum arată diviziunile care acum conţin şi piese:
<div class="field Row-1 Col-1 WhiteField">
<div class="piece">r</div>
</div>
Iar în "brw-sah.css" am adăugat definiţia:
.piece {
text-align: center; /* centrare orizontală */
line-height: 20px; /* asigură centrare verticală */
}
ceea ce a condus la "centrarea" literei (cum se vede în figura de mai sus).
Desigur, am vrea ca piesele să fie reprezentate nu prin literele preluate direct din FEN, ci prin imagini grafice. Ne vom ocupa ceva mai încolo de piese, ca imagini grafice. Acum doar arătăm cum putem înlocui literele cu reprezentări grafice pentru piese făcând câteva ajustări minore în fişierele redate mai sus.
Presupunem că avem un subdirector /images/20 care conţine imaginile pieselor, ca fişiere .png. Adăugăm în fişierul "brw-sah.js", imediat deasupra definiţiei widget-ului (în zona de variabile comune tuturor instanţelor) următorul obiect "PIECES":
var PIECES = { // piese cu dimensiunea 20x20 pixeli
'K': '<img src="/images/20/wk.png" alt="K" />',
'Q': '<img src="/images/20/wq.png" alt="Q" />',
'R': '<img src="/images/20/wr.png" alt="R" />',
'B': '<img src="/images/20/wb.png" alt="B" />',
'N': '<img src="/images/20/wn.png" alt="N" />',
'P': '<img src="/images/20/wp.png" alt="P" />',
'k': '<img src="/images/20/bk.png" alt="k" />',
'q': '<img src="/images/20/bq.png" alt="q" />',
'r': '<img src="/images/20/br.png" alt="r" />',
'b': '<img src="/images/20/bb.png" alt="b" />',
'n': '<img src="/images/20/bn.png" alt="n" />',
'p': '<img src="/images/20/bp.png" alt="p" />'
};
şi modificăm în _setPosition() definiţia diviziunii corespunzătoare piesei astfel:
$(this).html("<div class="piece">" + PIECES[piece] + "</div>");
Reîncărcând fişierul HTML în browser, obţinem pentru FEN-ul introdus mai înainte imaginea redată mai sus, de data aceasta literele fiind înlocuite cu imaginile pieselor.
Dar nu mai este mult până ce vom vedea că este neconvenabil să folosim reprezentări precum "PIECES" de mai sus (înscriind numele tuturor fişierelor sursă ale imaginilor pieselor): vom dori să putem folosi şi seturi de piese mai mari (desigur, mărind corespunzător şi pătrăţelele tablei).
Mai sus prevăzusem tabloul PIECES, care asocia fiecărei piese-literă din FEN, <img>-ul care specifică fişierul imagine corespunzător - de exemplu PIECES['K'] = '<img src="/images/20/wk.png" alt="K"/>'; iar în setPosition(), pentru piesa curentă din FEN se insera în câmpul corespunzător "<div class='piece'>" + PIECES[piece] + "</div>" - rezultând astfel diviziuni ca:
<div class="field Row-1 Col-1 WhiteField"> <!-- câmpul curent -->
<div class="piece"> <!-- piesa plasată pe acesta -->
<img alt="r" src="/images/20/br.png"> <!-- încarcă imaginea piesei -->
</div>
</div>
Această soluţie are două defecte: browserul va trebui să descarce şi să păstreze (în cache) 12 fişiere ".png" (pentru fiecare set de piese); iar dacă am vrea să creem posibilitatea folosirii mai multor seturi de piese, atunci ar trebui să prevedem câte un tablou-asociativ PIECES_n pentru fiecare.
Deocamdată considerăm un singur set de piese - reprezentat ca şi mai înainte prin cele 12 fişiere ".png" din subdirectorul images/20 - şi vizăm aici o altă soluţie, bazată pe CSS-sprites.
Un sprite combină mai multe imagini (de piese, aici) într-una singură; folosind proprietatea CSS background-position vom putea indica browserului ce parte din sprite (ce piesă) să poziţioneze într-un anumit container (pe câmpul respectiv al tablei de şah).
Un sprite se poate realiza şi manual - folosind de exemplu GIMP (de ce Photoshop? care este pentru Windows şi este Proprietary_software…). Dar este mult mai simplu să folosim o comandă de "montare" existentă în ImageMagick:
cd /images/20
montage -adjoin -tile 12x1 -geometry +0+0 -background none \
wp.png wk.png wq.png wr.png wb.png wn.png \
bn.png bb.png br.png bq.png bk.png bp.png \
men20.png
Imaginea obţinută "men20.png" concatenează cele 12 piese pe un singur rând (în ordinea indicată), fară spaţiu liber între ele şi are background-ul transparent (încât culoarea aleasă pentru "câmp alb", respectiv "negru" nu va fi alterată prin plasarea piesei pe câmpul respectiv).
Iată de exemplu, cum putem selecta într-un <div> grupul ultimelor două piese din dreapta:
<div style="width: 40px; height: 20px; border:1px dashed #666;
background:url('/images/20/men20.png');
background-position: 40px;">
</div>
"men20.png" (indicat în background: url()) a fost "deplasat" cu 2x20 = 40 pixeli spre dreapta (conform background-position), rezultând astfel grupul ultimelor două piese. Acelaşi rezultat îl puteam obţine şi "deplasând" în sens negativ (spre stânga), punând "background-position: -200px;" (caz în care se maschează primele 10 piese, măsurând 10x20 = 200 pixeli).
Bineînţeles, se poate greşi: de exemplu, cu background-position: 50px, am obţine
Nu are nici o importanţă ordinea în care concatenăm piesele, dar o anumită simetrie este utilă de exemplu în cazul în care am seta valorile background-position printr-o funcţie javaScript; în alegerea făcută aici, dacă deplasăm spre stânga piesele albe şi spre dreapta pe cele negre, atunci suma dintre poziţia unei piese negre şi a celei albe de acelaşi nume este constantă (= 20px).
Pentru a exploata sprite-ul realizat, adăugăm în fişierul css/brw-sah.css următoarele definiţii:
.Piece {
width: 20px; height: 20px;
background: url("../images/20/men20.png");
}
.white-pawn {background-position: 0;} .black-pawn {background-position: 20px;}
.white-king {background-position: -20px;} .black-king {background-position: 40px;}
.white-queen {background-position: -40px;} .black-queen {background-position: 60px;}
.white-rook {background-position: -60px;} .black-rook {background-position: 80px;}
.white-bishop {background-position: -80px;} .black-bishop {background-position: 100px;}
.white-knight {background-position: -100px;} .black-knight {background-position: 120px;}
urmând ca setPosition() să insereze piesele sub forma <div class="Piece black-rook"></div> (unde .Piece indică url-ul imaginii, iar .black-rook indică poziţia turnului negru în cadrul sprite-ului).
În fişierul js/brw-sah.js înlocuim variabila PIECES cu:
var PIECE_CLASS_NAMES = {
p: "pawn", n: "knight", b: "bishop", r: "rook", q: "queen", k: "king"
};
şi apoi rescriem metoda _setPosition() astfel:
_setPosition: function(fen) {
/* elimină '/' şi înlocuieşte fiecare cifră din FEN
cu un număr corespunzător de spaţii */
var fen64 = fen.replace(/\x2f/g, "")
.replace(/[1-8]/g, function(nr_sp) {
return " ".substr(0, nr_sp);
});
/* parcurge în acelaşi sens cele 64 <div> şi FEN-ul
înscriind piesa din FEN, sau ştergând vechea "piesă" */
this.Tabla.children().each(function(index) {
var piece = fen64.charAt(index);
if (piece && piece != " ") {
var piece_low = piece.toLowerCase(),
side = piece == piece_low ? "black" : "white";
pieceClass = "Piece " +
side + "-" + PIECE_CLASS_NAMES[piece_low];
$(this).html("<div class='" + pieceClass + "'></div>");
}
else $(this).html(""); // şterge piesa veche, dacă există
});
}
După aceste modificări, reîncărcând în browser fişierul HTML obţinem exact rezultatul redat anterior (în finalul din (IV)); dar acum, câmpurile tablei (înscrise de setPosition()) apar ca:
<div class="field Row-1 Col-1 WhiteField">
<div class="Piece black-rook"></div>
</div>
Mai târziu, vom reuşi să înlocuim - în cele 12 definiţii CSS ale poziţiilor pieselor (în .white-pawn, etc.) - măsura în pixeli cu una în procente, putând utiliza astfel aceleaşi 12 definiţii de poziţie pentru oricare set de piese (nu numai pentru setul 20x20, ca mai sus). Ceea ce va trebui schimbat de la un set la altul va fi doar definiţia pentru .Piece (precizând url-ul sprite-ului corespunzător acelui set, precum şi dimensiunile piesei).
Avem o imagine în care am reunit piesele de şah de o aceeaşi dimensiune n şi pe de altă parte, un câmp de dimensiune nxn. Problema este de a selecta din sprite-ul nostru o piesă şi a o poziţiona pe câmpul dat, încât definiţiile CSS necesare pentru aceasta să nu depindă de n. Investigăm cum să determinăm pentru acest caz, procentele necesare pentru background-position.
Proporţionăm din ce în ce mai fin procente pozitive, dar până la urmă ajungem la o soluţie neaşteptat de simplă, folosind procente negative!
Pentru background-position: X% nu am găsit decât iterări (şi exemplificări vizuale imediate) ale formulării din CSS 2.1, conform căreia imaginea va fi poziţionată în câmpul-destinaţie în aşa fel încât punctul din imagine de coordonate (X%, 50%) să coincidă cu punctul care în cadrul câmpului-destinaţie are coordonatele (X%, 50%).
Vom folosi pentru investigările necesare următorul sprite:
conţinând cele 12 piese de şah, de dimensiune NxN pixeli; pentru lizibilitate, am inclus piesele propriu-zise în câte un cadru de 1 pixel, încât aici N este cu 2 pixeli mai mare decât dimensiunea reală a piesei (ceea ce nu ar trebui să aibă importanţă, dat fiind că vom opera cu procente).
Pe de altă parte, considerăm undeva într-o pagină HTML, un element <div> (sau mai multe), având fixată aceeaşi dimensiune ca şi piesele, NxN pixeli.
Poziţionările :0% şi respectiv :100% sunt clare: marginea stângă (cu abscisa 0%) şi respectiv cea din dreapta imaginii (cu abscisa 100%) este poziţionată peste marginea stângă şi respectiv peste cea dreaptă a <div>-ului, aşezând aici atâta cât încape din imagine (începând de la marginea stângă, respectiv până la cea dreaptă a imaginii).
Abscisa 50% a imaginii "cade" pe linia care separă caii şi acesteia îi corespunde în <div>-ul nostru linia verticală (imaginară) dusă prin mijlocul acestuia. Prin urmare, background-position: 50% va înscrie în <div> jumătăţi ale cailor (de fapt, nu chiar "jumătăţi" fiindcă intervine şi grosimea liniei separatoare - care există şi ea, în imagine):
<!DOCTYPE html>
<head>
<style>
.test {
width: 34px; height: 34px; margin-right: 4px; float: left;
background: lightgrey url("/images/test-men32.png");
}
</style>
</head>
<body>
<div class="test" style="background-position:50%"></div>
<div class="test" style="background-position:0%"></div>
<div class="test" style="background-position:100%"></div>
<div style="clear:left;"></div>
</body>
Să vedem acum, cum am putea elimina jumătatea de cal negru apărută în <div> prin :50%, pentru a obţine în <div>-ul nostru calul alb "complet". Pentru ca în <div> să apară ambele jumătăţi ale calului alb, ar trebui să "suprapunem" verticala mediană a calului alb din cadrul imaginii, peste verticala mediană a <div>-ului.
Ce abscisă are verticala mediană a calului alb, în cadrul imaginii? Deoarece abscisa 50% indica marginea dreaptă a cadrului calului alb din imagine, rezultă că abscisa verticalei mediane a calului alb se obţine scăzând din 50% procentul corespunzător unei jumătăţi de piesă; o piesă din imagine are dimensiunea 100% / 12 ≅ 8.3333%, deci abscisa căutată este 50% - 8.3333%/2 ≅ 45.8333%.
Analog, adunând 8.3333% / 2 la 50%, obţinem abscisa care ar selecta calul negru: 54.1667%.
Nu-i sigur că este suficient de corect - şi n-ar fi vorba atât de erorile de aproximare, cât de faptul că totuşi, abscisei 45.8333% a imaginii nu-i corespunde chiar "verticala mediană" a <div>-ului, ci verticala de abscisă 45.8333% a <div>-ului! Dar adăugând aceste <div>-uri în fişierul HTML redat mai sus, constatăm că rezultatul este aproape mulţumitor:
<div class="test" style="background-position:45.8333%"></div>
<div class="test" style="background-position:54.1667%"></div>
Privind cu atenţie (se poate folosi eventual, "Zoom In" din meniul View al browserului) constatăm că în <div> calul alb este cu o nuanţă mai la stânga, iar cel negru este cu o nuanţă mai la dreapta faţă de caii corespunzători din sprite…
Iar acel 50% de la care am scăzut sau am adunat este nu marginea dreaptă sau stângă a vreunuia din caii din sprite, ci este abscisa verticalei imaginare care desparte cadrul calului alb de cadrul celui negru (amintim că am "împrejmuit" fiecare piesă din sprite cu un cadru de 1 pixel) - prin urmare "nuanţa" observată mai sus ar fi chiar mai mare, dacă vom renunţa (cum este firesc) la cadrele acum existente, în jurul pieselor.
Altfel spus, am scăzut prea mult (în cazul calului alb) şi respectiv, am adunat prea puţin (în cazul celui negru) faţă de abscisa considerată iniţial de 50%.
Să analizăm şi cazul regelui alb (a doua piesă din sprite). Abscisa cadrului stâng al acestei piese este 8.3333% (am văzut mai sus că fiecare piesă, deci şi prima, are lăţimea de 8.3333% din lăţimea întregii imagini) şi acest cadru va fi poziţionat în <div> la 8.3333% (din lăţimea <div>-ului) distanţă de marginea stângă a <div>-ului.
Cât este 8.3333% din lăţimea <div>-ului? Este 8.3333% din {8.3333% din lăţimea imaginii} - fiindcă <div>-ul are aceeaşi lăţime ca piesele din sprite - adică este 8.3333 * 8.3333 / 100 ≅ 0.6944% din lăţimea imaginii. Adunând 0.6944% la abscisa cadrului stâng vizată mai sus, rezultă poziţionarea "exactă" : 9.0277% în <div> a regelui alb:
<div class="test" style="background-position:8.3333%"></div>
<div class="test" style="background-position:9.0277%"></div>
În general, dacă vizăm piesa de index K = 1 (regele alb) până la K = 10 (regele negru), abscisa Q a cadrului stâng a acesteia este Q = K * 8.3333% din lăţimea sprite-ului şi pentru poziţionare "exactă" în <div> trebuie să-i adunăm (K * 8.3333% din 8.3333%).
Poziţionarea nu este chiar "exactă", fiindcă am măsurat ceea ce trebuie adunat abscisei Q, de la cadrul stâng şi nu de la marginea stângă propriu-zisă a piesei (deci am adunat ceva mai puţin decât trebuie). Şi vedem aceasta luând K = 10 (vizând regele negru); în acest caz, Q = 83.3333% şi trebuie să-i adunăm 10*0.69444 rezultând poziţionarea : 90.2777%:
<div class="test" style="background-position:90.2777%"></div>
Am mărit imaginea ca să vedem că încă n-am reuşit să scoatem complet, cadrul stâng. Noi vrem să plasăm în <div> piesa, dar am "măsurat" de la cadrul (inclusiv) care împrejmuieşte piesa (nu de la marginea-stângă propriu-zisă a piesei). Ca să eliminăm complet cadrul stâng, ar trebui să vedem ce procent din imagine ocupă un cadru, să raportăm acest procent la lăţimea <div>-ului şi să adunăm rezultatul (multiplicat cu factorul K) lui Q…
Nu mai redăm aici acest calcul (sincer, nici nu l-am mai făcut!) pentru că între timp, am mai experimentat o idee: cum stau lucrurile dacă folosim procente negative?
Bineînţeles, am început cu : -50%… dar apoi am avut inspiraţia de a proba : -100%, apoi : -200%, : -300% ş.a.m.d. - constatând neaşteptat, că aceste poziţionări rezolvă perfect problema de a obţine în <div> exact piesa dorită!
<div class="test" style="background-position:-50%"></div>
<div class="test" style="background-position:-100%"></div> <!-- a doua piesă -->
<div class="test" style="background-position:-200%"></div> <!-- a treia -->
<div class="test" style="background-position:-300%"></div>
<div class="test" style="background-position:-400%"></div>
<div class="test" style="background-position:-500%"></div>
<div class="test" style="background-position:-600%"></div>
<div class="test" style="background-position:-700%"></div>
<div class="test" style="background-position:-800%"></div>
<div class="test" style="background-position:-900%"></div>
<div class="test" style="background-position:-1000%"></div> <!-- penultima piesă -->
<div class="test" style="background-position:0%"></div> <!-- Prima piesă -->
<div class="test" style="background-position:100%"></div> <!-- Ultima piesă -->
Cu "Zoom In" vedem că fiecare <div> conţine nu numai piesa, dar şi cadrul de 1 pixel cu care am împrejmuit-o la construcţia sprite-ului (o ultimă dovadă că piesa este poziţionată "exact").
În final, putem formula mai general: dacă imaginile care compun un sprite au toate aceeaşi lăţime, atunci poziţionarea imaginii de index K într-un <div> de aceeaşi lăţime se face prin background-position: -K*100% (cu procent negativ).
Cum se explică acum, ceea ce am găsit mai sus graţie unui moment de inspiraţie? Cu alte cuvinte - cum se aplică regula generală "imaginea va fi poziţionată în câmpul-destinaţie în aşa fel încât punctul din imagine de coordonate (X%, 50%) să coincidă cu punctul care în cadrul câmpului-destinaţie are coordonatele (X%, 50%)" în cazul procentelor negative?
În cadrul imaginii, poziţionarea negativă (fie în procente, fie în pixeli) se calculează de la dreapta spre stânga imaginii (invers faţă de poziţionarea "pozitivă").
De exemplu, : -100% stabileşte ca origine marginea stângă a imaginii; dar… la fel şi pentru -200%, sau -300%, etc.! Poziţionarea : -K*100% stabileşte originea la marginea stângă a imaginii, indiferent cât este factorul K.
Putem zice că : -K*100% "nu are sens" referitor la imagine - va fixa originea imaginii la fel ca şi ": -100%". Dar regula evocată la început aplică ": -K*100" şi <div>-ului în care vrem să poziţionăm o porţiune din imagine, începând de la originea fixată acesteia (şi astfel, K "are sens").
Totdeauna poziţionarea în <div> (fie pozitivă, fie procentual negativă) se face relativ la marginea stângă a acestuia. Ca urmare, o abscisă procentuală negativă pointează în stânga <div>-ului (înafara lui, spre stânga).
Deci "-100%" de exemplu, înseamnă pentru <div> "deplasarea" fictivă a marginii stângi a sale spre stânga, pe o distanţă egală cu lăţimea lui ("creând" o zonă fictivă în stânga <div>-ului, de aceeaşi lăţime ca a acestuia). În sprite-ul nostru, : -100% fixează originea imaginii la marginea stângă a pionului alb şi la poziţionarea imaginii în <div>, pionul de la originea imaginii va fi "poziţionat" în zona fictivă de la stânga <div>-ului, iar piesa care urmează în imagine după pion va fi poziţionată în continuarea acestei zone fictive - deci regele alb va ajunge în <div>-ul nostru.
Analog, : -K*100% fixează originea imaginii pe marginea stângă a pionului alb şi "completează" <div>-ul spre stânga, adăugând fictiv K-1 "div"-uri identice lui; în primul dintre aceste "div"-uri fictive va intra pionul alb, în al doilea "div" va intra regele alb, ş.a.m.d. iar în final, piesa de index K din imagine va apărea în <div>-ul real.
Am considerat până acum un singur "set de piese" de şah şi am poziţionat câmpurile tablei şi imaginile pieselor prin valori determinate în pixeli, corespunzând exact acestui set.
Să spunem acum de unde putem obţine mai multe seturi şi să vedem cum realizăm printr-un program, sprite-urile corespunzătoare. Apoi, cum am putea face pentru ca poziţionările câmpurilor şi imaginilor să nu depindă totuşi, de setul curent folosit. Ulterior, vom vedea cum am putea minimiza acele definiţii CSS care depind inevitabil de setul curent.
Seturi de piese de şah în format PNG pot fi obţinute de la jin-piece-sets (Eric De Mund, sub licenţă Creative Commons Attribution-Share Alike 3.0 Unported). Am download-at "alpha.ead-02.zip" şi am dezarhivat în $HOME/Resources/alpha2; subdirectoare apărute astfel sunt '/20', '/21', etc. - numite după dimensiunea piesei - şi fiecare conţine fişierele ".png" corespunzătoare pieselor, astfel:
vb@vb:~/Resources/alpha2$ ls 20
bb.png bn.png bq.png wb.png wn.png wq.png
bk.png bp.png br.png wk.png wp.png wr.png
Prima literă ('b' sau 'w') din numele acestor fişiere reprezintă culoarea ('black' sau 'white'), iar a doua - numele piesei (conform standardului FEN).
Am văzut în (V) cum putem folosi comanda montage pentru a obţine un sprite corespunzător unui set de piese; acum ar trebui să iterăm "montage" pentru fiecare set şi este firesc să scriem un program Bash pentru aceasta.
Directorul în care lucrăm asupra widget-ului nostru este $HOME/brw-sah şi are structura:
vb@vb:~$ ls brw-sah/
brw-sah.html css images js
Să adăugăm un fişier executabil (pentru programul Bash anunţat):
vb@vb:~/brw-sah$ touch chessmen-sprite.sh ; chmod +x chessmen-sprite.sh
şi să-l edităm apoi, cu următorul conţinut:
#!/bin/bash
mkdir -p chessmen # creează $HOME/brw-sah/chessmen
cd chessmen # copiază seturile 20..32 în /chessmen
cp -r $HOME/Resources/alpha2/{20..32} .
for sz in $(ls) # pentru fiecare set din /chessmen
do
cd $sz # montează sprite-ul corespunzător
montage -adjoin -tile 12x1 -geometry +0+0 -background none \
wp.png wk.png wq.png wr.png wb.png wn.png \
bn.png bb.png br.png bq.png bk.png bp.png \
men$sz.png
cp men$sz.png ../../images/ # copiază sprite-ul în /images
cd .. # revine în /chessmen, pentru următorul set
done
Executând programul, obţinem în subdirectorul /images sprite-urile dorite ("men20.png", ş.a.m.d.); am optat numai pentru seturile 20..32 (nu mai mari) pentru că lucrăm la un widget pentru pagini Web (de montat alături de alte conţinuturi), nu la o aplicaţie independentă pentru desktop.
Definiţiile CSS din brw-sah/css/brw-sah.css (vezi (I) şi (V)) au avut în vedere numai piesele de 20 pixeli, încât ele corespund acum numai pentru "men20.png".
Am putea imagina iarăşi, un program Bash (sau în alt limbaj) care să furnizeze fişierul CSS necesar unui set dat - urmând să concatenăm fişierele respective (grupând CSS selectorii corespunzători unui set, pentru fiecare set)… Dar aceasta ar trebui să fie "ultima soluţie"; întâi ar fi de văzut cât de favorabil am putea specula faptul că proprietăţile CSS de poziţionare (top, left, background-position) pot avea ca valori nu numai pixeli, ci şi procente.
Dacă reuşim să indicăm valori procentuale adecvate, atunci vom putea imagina definiţii CSS comune pentru toate seturile (plus un număr minimal de definiţii particulare setului).
Pentru poziţionarea celor 8 coloane şi 8 linii ale tablei, trebuie să setăm left şi respectiv top; împărţind dimensiunea 100% a tablei la 8 obţinem (exact) 12.5% şi putem considera că lucrurile sunt simple: avansăm la următoarea coloană adunând 12.5% la left-ul celei precedente (la fel, pentru linii). Adică putem redefini selectorii CSS .Col-n şi .Row-n astfel:
.Col-1 { left: 0%; } .Row-1 { top: 0%; }
.Col-2 { left: 12.5%; } .Row-2 { top: 12.5%; }
.Col-3 { left: 25%; } .Row-3 { top: 25%; }
.Col-4 { left: 37.5%; } .Row-4 { top: 37.5%; }
.Col-5 { left: 50%; } .Row-5 { top: 50%; }
.Col-6 { left: 62.5%; } .Row-6 { top: 62.5%; }
.Col-7 { left: 75%; } .Row-7 { top: 75%; }
.Col-8 { left: 87.5%; } .Row-8 { top: 87.5%; }
Acum - cu aceste definiţii procentuale de poziţionare - construcţia tablei de şah este independentă de dimensiunea câmpului.
În schimb, pentru piese avem de folosit background-position şi lucrurile se complică. Am investigat deja chestiunea în Poziţionarea procentuală a unei piese:
rezultând că următoarele definiţii asigură poziţionarea independent de mărimea pieselor:
.white-pawn { background-position: 0; }
.white-king { background-position: -100%; }
.white-queen { background-position: -200%; }
.white-rook { background-position: -300%; }
.white-bishop { background-position: -400%; }
.white-knight { background-position: -500%; }
.black-knight { background-position: -600%; }
.black-bishop { background-position: -700%; }
.black-rook { background-position: -800%; }
.black-queen { background-position: -900%; }
.black-king { background-position: -1000%; }
.black-pawn { background-position: 100%; }
Privitor la "independenţă", ar mai fi un aspect de luat în seamă…
În poziţia normală, tabla de şah apare cu piesele albe în partea de jos şi cu cele negre în partea de sus a tablei. Este firesc să creem posibilitatea inversării tablei şi aceasta se face de obicei printr-o funcţie javaScript care inversează ordinea celor 64 de <div>-uri corespunzătoare câmpurilor tablei şi deasemenea, inversează notaţia liniilor şi respectiv, a coloanelor tablei.
În programul său de şah din 2008, dynarch a introdus o metodă mult mai simplă: se grupează selectorii de coloane şi de linii într-un nou selector CSS, dar inversând ordinea; funcţia javaScript necesară pentru inversarea tablei se poate astfel rezuma la adăugarea / ştergerea acestei noi proprietăţi în definiţia iniţială a tablei (într-un singur loc - nu este necesar pe fiecare câmp!).
Prin inversare, "Col-1" trebuie să devină (sub noul selector) "Col-8", "Row-8" trebuie să devină "Row-1", ş.a.m.d. iar această transformare asigură de la sine şi "inversarea" pieselor de pe tablă. Să rescriem deci definiţiile anterioare pentru poziţionarea liniilor şi coloanelor, astfel:
.Col-1, .revBoard .Col-8 { left: 0% } .Row-8, .revBoard .Row-1 { top: 0% }
.Col-2, .revBoard .Col-7 { left: 12.5% } .Row-7, .revBoard .Row-2 { top: 12.5% }
.Col-3, .revBoard .Col-6 { left: 25% } .Row-6, .revBoard .Row-3 { top: 25% }
.Col-4, .revBoard .Col-5 { left: 37.5% } .Row-5, .revBoard .Row-4 { top: 37.5% }
.Col-5, .revBoard .Col-4 { left: 50% } .Row-4, .revBoard .Row-5 { top: 50% }
.Col-6, .revBoard .Col-3 { left: 62.5% } .Row-3, .revBoard .Row-6 { top: 62.5% }
.Col-7, .revBoard .Col-2 { left: 75% } .Row-2, .revBoard .Row-7 { top: 75% }
.Col-8, .revBoard .Col-1 { left: 87.5% } .Row-1, .revBoard .Row-8 { top: 87.5% }
Modificând astfel "brw-sah.css", va fi suficient să se adauge în <script>-ul final din fişierul HTML (vezi eventual (IV)): $('div.container').addClass('revBoard'); pentru ca (la reîncărcarea HTML) tabla să-şi schimbe orientarea (piesele albe - sus, iar cele negre - jos).
În (VI) am reuşit să constituim în fişierul "brw-sah.css" proprietăţi CSS de poziţionare procentuală a liniilor şi coloanelor tablei şi respectiv a pieselor dintr-un sprite - cu meritul că aceste poziţionări sunt independente de setul curent de piese şi de orientarea tablei.
Dimensiunea tablei de şah (indicată anterior în proprietatea ".container"), dimensiunea câmpurilor ei (indicată în ".field") şi dimensiunea pieselor (în ".Piece") depind evident de setul de piese curent. Sunt posibile mai multe variante de lucru, pentru a ţine cont de setul curent NxN.
S-ar putea defini direct '.Piece-N', '.field-N', '.container-N' pentru fiecare N (amintim că am optat pentru N = 20..32 pixeli). Este adevărat că proprietăţile "directe" sunt mai uşor de aplicat (browserul nu are de căutat ascendenţi ai nodului pe care trebuie să aplice proprietatea); în schimb, va trebui mereu să adăugăm sufixul "-N", în cadrul funcţiilor care constituie câmpurile sau plasează piesele (setChessTable() şi setPosition()). Pe de altă parte… există cu siguranţă, metode mai elegante.
O idee tentantă ar fi specificarea "inline" a proprietăţilor respective, direct în metoda _create(). De exemplu, dacă variabila container este obiectul jQuery care corespunde tablei, atunci container .find('div') .css({'width': N +'px', 'height': N +'px', 'position': 'absolute'}); va aplica proprietăţile indicate tuturor celor 64 de elemente <div> corespunzătoare câmpurilor tablei. Dar am amesteca prea mult CSS-inline în funcţii javaScript, ar fi cel puţin incomod de făcut eventuale modificări, iar "eficienţa" poate lăsa de dorit.
Dusă la extrem, această idee "tentantă" ar însemna construirea unui widget pentru a cărui utilizare să fie suficient ca browserul să încarce fişierul javaScript care defineşte widget-ul; nu mai este necesară încărcarea vreunui fişier CSS specific, iar încărcarea altor biblioteci javascript (în cazul nostru jQuery) se poate lăsa în seama metodei _create() (se testează dacă jQuery există în cache; dacă nu, atunci se adaugă un element <script> pentru a forţa browserul să download-eze de undeva, biblioteca respectivă).
Dar soluţia cea mai firească este bazată pe imbricarea anumitor selectori. De exemplu, specificaţia .container-N .Piece va fi aplicată de către browser elementelor cu proprietatea ".Piece" pentru care există un ascendent cu proprietatea ".container-N"; efortul de a căuta în lanţul ascendenţilor înainte de a aplica proprietatea ".Piece" este totuşi, neglijabil.
Alegând "soluţia cea mai firească", va trebui să fim atenţi când vom scrie funcţiile necesare. De exemplu, dacă widget-ul este instanţiat pe mai multe elemente, o selecţie ca $('.Piece') va cuprinde "piesele" din toate aceste instanţe; corect va fi ca fiecare instanţă să păstreze o referinţă internă la propriul .container, urmând să-şi repereze "piesele" relativ la această referinţă.
Deasemenea, trebuie să prevedem că vor apărea şi dependenţe indirecte faţă de N; de exemplu, vom vrea să adăugăm o "bară de navigare" cu diverse butoane pentru parcurgerea partidei - ori dimensiunea acestei bare trebuie corelată cu dimensiunea tablei, fiind astfel dependentă de N.
Prin urmare, ar fi de dorit să specificăm "-N" nu în .container, ci mai "deasupra" - încât nu numai "tabla" şi elementele subordonate ei să fie sub incidenţa lui "-N", dar şi elemente nesubordonate direct tablei, dar ale căror proprietăţi CSS depind de N.
Având în vedere cele de mai sus, constituim schema ierarhică următoare:
Setând lăţimea diviziunii .chessmen-N în funcţie de N (să zicem, 9*N), ne asigurăm că lăţimea implicită a unor elemente subordonate (precum .gameInfo) nu va depăşi această valoare.
Setând "font-size" tot aici (în funcţie de N), fontul utilizat în notaţia liniilor şi coloanelor va putea fi proporţionat faţă de această valoare (şi în mod implicit, faţă de N).
Diviziunea .boardFields corespunde tablei de şah şi în raport cu ea vor fi poziţionate şi cele două bare pentru notaţia liniilor şi respectiv a coloanelor.
.boardFields va trebui să aibă dimensiunile de 8*N (plus eventual, 16*lăţimea borderului unui câmp .Field) şi desigur, .Field are dimensiunea NxN.
Având de calculat şi de setat aceste proprietăţi pentru N=20..32, vom imagina un program Bash pentru a obţine fragmentul de fişier CSS necesar; bineînţeles că întâi se cuvine să facem o probă, stabilind exact şi ceea ce va trebui să obţinem prin program pentru fiecare N.
Amânăm setarea "font-size" (vizată în schema de mai sus) până ce vom aborda şi chestiunea notaţiei liniilor şi coloanelor.
/* "brw-sah.css" */
.Col-1, .revBoard .Col-8 {left: 0%} .Row-8, .revBoard .Row-1 {top: 0%}
.Col-2, .revBoard .Col-7 {left: 12.5%} .Row-7, .revBoard .Row-2 {top: 12.5%}
.Col-3, .revBoard .Col-6 {left: 25%} .Row-6, .revBoard .Row-3 {top: 25%}
.Col-4, .revBoard .Col-5 {left: 37.5%} .Row-5, .revBoard .Row-4 {top: 37.5%}
.Col-5, .revBoard .Col-4 {left: 50%} .Row-4, .revBoard .Row-5 {top: 50%}
.Col-6, .revBoard .Col-3 {left: 62.5%} .Row-3, .revBoard .Row-6 {top: 62.5%}
.Col-7, .revBoard .Col-2 {left: 75%} .Row-2, .revBoard .Row-7 {top: 75%}
.Col-8, .revBoard .Col-1 {left: 87.5%} .Row-1, .revBoard .Row-8 {top: 87.5%}
.Wh-pawn {background-position:0%;} .Bl-pawn {background-position:100%;}
.Wh-king {background-position:-100%;} .Bl-king {background-position:-1000%;}
.Wh-queen {background-position:-200%;} .Bl-queen {background-position:-900%;}
.Wh-rook {background-position:-300%;} .Bl-rook {background-position:-800%;}
.Wh-bishop {background-position:-400%;} .Bl-bishop {background-position:-700%;}
.Wh-knight {background-position:-500%;} .Bl-knight {background-position:-600%;}
.WhField { background: white; }
.BlField { background: lightgrey; }
/* proprietăţi comune (pentru toate valorile -N) */
.boardFields {position: relative; border: 2px solid black;}
.Field {position: absolute; padding: 1px; /* 2px; pentru N > 23 ?*/}
/* proprietăţi specifice setului; exemplu pentru N = 24 */
.chessmen-24 {width: 248px;} /* 8*24 + 32(border) + 24 */
.chessmen-24 .boardFields {width: 224px; height: 224px;} /* 8*24 + 32(border) */
.chessmen-24 .Field {width: 24px; height: 24px; padding: 2px;}
.chessmen-24 .Piece {
width: 24px; height: 24px;
background: url('../images/men24.png'); /* GREŞIT! Revenim mai jos... */
}
În vechiul fişier "brw-sah.css" am modificat cum se vede mai sus unele denumiri (scurtându-le) şi am introdus proprietăţile din schema de mai sus, pentru N = 24. Pentru .Piese am folosit ca şi mai înainte, forma "scurtă" background: url() - ceea ce se dovedeşte în final, greşit…
Din fişierul "brw-sah.js" redăm numai porţiunile modificate faţă de conţinutul prezentat anterior:
/* "brw-sah.js" - modificările faţă de conţinutul prezentat anterior */
/* ... vezi (IV), (V) ... */
_create: function() {
/* ... ... */
this.Tabla = $('<div class="chessmen-24"></div>')
.insertAfter( inputFEN.next() )
.html( '<div class="boardFields">' + this._setChessTable() + "</div>" )
;//.addClass('revBoard');
this.boardFields = this.Tabla.find('div:first');
},
/* ... ... */
_setPosition: function(fen) {
/* ... ... */
this.boardFields.children().each(function(index) {
var piece = fen64.charAt(index);
if (piece && piece != " ") {
var piece_low = piece.toLowerCase(),
side = piece == piece_low ? "Bl-" : "Wh-";
pieceClass = side + PIECE_CLASS_NAMES[piece_low];
$(this).html("<div class='Piece " + pieceClass + "'></div>");
}
else $(this).html(""); // şterge piesa veche, dacă există
});
}
/* ... ... */
Amintim în sfârşit, fişierul "brw-sah.html"; încărcând în browser, vedem că în principiu lucrurile funcţionează (doar că piesele nu sunt găsite, cu excepţia pionului alb):
va rezulta (totuşi, greşit):
<!-- "brw-sah.html" (vezi (III)) -->
<body>
<textarea id="getFEN">
<script>
$(function() {
$('#getFEN').fenBrowser();
});
</script>
</body>
Dar de ce au fost reduse toate piesele la background-position: 0; (pionul alb)?
setPosition() a inserat în DOM aşa ceva: <div class="Piece black-rook"></div>. Browserul aplică proprietăţile de la dreapta la stânga, adică întâi "black-rook" şi apoi "Piece"; deci întâi stabileşte background-position: -800% corespunzător cu proprietatea .black-rook şi după aceeea, stabileşte din .Piece sursa imaginii background: url('...'). Ori pentru background-position avem precizarea:
If the value of this property is not set in a background shorthand property that is applied to the element after the background-position CSS property, the value of this property is then reset to its initial value by the shorthand property.
Aceasta explică de ce background-position a fost resetat la zero, în cazul nostru. În orice caz, nu ne convine să înscriem "-position" împreună cu "url", într-un "background" cu format scurt (definiţiile .black-rook, etc. ar deveni dependente de N - ori tocmai aceasta am vrut să evităm).
Pentru corectare, este suficient să evităm "forma scurtă" background: url() - rescriind .Piece astfel:
.chessmen-24 .Piece { width: 24px; height: 24px;
background-image: url('../images/men24.png');
}
deci folosind direct background-image, pentru a indica fişierul care conţine imaginea.
Obs. Poate că explicaţia pe care am găsit-o (plecând de la ordinea implicită "de la dreapta spre stânga") nu este cea "adevărată" (probabil am putea proba, schimbând ordinea…) - dar ea ne-a condus totuşi, la o soluţie bună şi simplă (ba chiar şi "logică").
Pentru fiecare set de piese NxN, avem de înscris într-un fişier cele patru proprietăţi CSS pe care le-am testat mai sus pe N = 24. Este uşor să scriem un program Bash pentru aceasta (dar desigur, putem folosi orice alt limbaj):
#!/bin/bash
css="20-32.css"; # CSS specific fiecărui set de piese
touch $css # creem fişierul, în directorul curent
padd='' # .Field are deja padding=1px
add_oriz=16 # (padding-left + padding-right)*8 = 16 pixeli
for set in $(seq 20 32)
do
if (($set > 23)) # pentru unele seturi, alege padding=2
then
padd='padding: 2px;'
let add_oriz=32 # (2 + 2)*8 = 32px de adăugat lăţimii
fi
let w1=$set*8+$add_oriz; # lăţime pentru .chessmen
let w2=$w1+$set; # lăţimea pentru .boardFields
# înscrie (adaugă) în fişier cele patru proprietăţi CSS
spec=".chessmen-$set" # .chessmen-24 etc.
echo "$spec {width: ${w2}px;}" >> $css
echo "$spec .boardFields {width: ${w1}px; height: ${w1}px;}" >> $css
echo "$spec .Field {width: ${set}px; height: ${set}px; $padd}" >> $css
echo -e "$spec .Piece {width: ${set}px; height: ${set}px;" \
"background-image: url('../images/men$set.png')}\n" >> $css
done
Desigur, puteam adăuga direct în fişierul nostru "brw-sah.css"; am folosit totuşi un fişier separat ca să putem verifica lucrurile şi nu rămâne decât să copiem conţinutul rezultat în "20-32.css" şi să-l adăugăm la sfârşitul fişierului "brw-sah.css".
Astfel constituit, "brw-sah.css" este complet, în sensul că permite instanţierea fenBrowser() cu oricare set 20..32 de piese de şah. Dar mai sus n-am făcut decât o probă, indicând direct setul de piese prin this.Tabla = $('<div class="chessmen-24"></div>'), în metoda _create().
Pentru a evita modificarea de la un set la altul, ar trebui creată o variabilă T care să indice setul dorit, folosind în _create() $('<div class="chessmen-" + T + '"></div>)'; T trebuie să fie externă metodei _create() şi ar trebui să poată fi setată dinafara widget-ului.
Pentru astfel de necesităţi, jQuery.widget() prevede obiectul options, conţinând perechi "cheie: valoare"; în metoda _create() avem de folosit aceste opţiuni, iar setarea valorilor acestora se poate face şi prin linia de instanţiere a widget-ului.
În "brw-sah.js" adăugăm options şi avem de modificat o singură linie în _create:
$.widget("brw.fenBrowser", {
options: {
field_size: 20 // setul de piese implicit ("men20.png")
},
_create: function() {
/* ... ... */
this.Tabla =
$('<div class="chessmen-' + this.options.field_size + '"></div>')
/* ... ... */
},
Amintim fişierul "brw-sah.html", dar acum prevedem două elemente <textarea>: pe primul instanţiem widget-ul cu setul implicit, iar pe celălalt - cu {field_size: 22}:
<!DOCTYPE html>
<head>
<script src="js/jquery-1.7.2.min.js"></script>
<script src="js/jquery.ui.core.min.js"></script>
<script src="js/jquery.ui.widget.min.js"></script>
<link href='css/brw-sah.css' rel='stylesheet' />
<script src='js/brw-sah.js'></script>
</head>
<body>
<textarea id='getFEN'></textarea>
<textarea id='getFEN1'></textarea>
<script>
$(function() {
$('#getFEN').fenBrowser();
$('#getFEN1').fenBrowser({field_size: 22});
});
</script>
</body>
Pe parcurs vom adăuga widget-ului şi alte opţiuni, în măsura în care ar apărea ca fiind utile.
Până acum am avut în vedere doar o poziţie individuală, furnizată ca şir FEN (într-un <textarea>) spre a fi "explicitată" pe tablă. Ceva mai târziu vom viza fireşte, o listă de mutări - furnizată spre a fi "rulată" pe tablă, mutare după mutare. Ori mutarea curentă trebuie să specifice piesa care trebuie mutată, câmpul de plecare şi câmpul de sosire.
Notaţia uzuală a mutărilor se bazează pe fixarea unei notaţii a câmpurilor tablei: coloanele sunt etichetate prin 'a'..'h', unde 'a' indică verticala din stânga albului; liniile sunt marcate prin 1..8, unde '1' indică prima linie a poziţiei iniţiale a albului. De exemplu, Ng1-f3 (sau scurt, Nf3) notează mutarea calului alb ('N', în notaţia FEN) de pe câmpul g1 (intersecţia coloanei 'g' cu linia '1') pe câmpul f3 (de intersecţie a coloanei 'f' cu linia '3').
Prin urmare (cel puţin pentru cazul când vom viza liste de mutări) este necesar să adăugăm tablei de şah constituite până acum, o "bară" orizontală dedesubt, pentru etichetarea coloanelor şi o "bară" verticală în stânga, pentru marcarea liniilor.
Aceste două bare trebuie să aibă aceeaşi dimensiune (lăţime într-un caz, înălţime în celălalt) ca tabla de şah (diviziunea .boardFields) - depinzând astfel, indirect, de setul de piese curent. Dar putem evita multiplicarea definiţiilor (câte două definiţii de bare, pentru fiecare set de piese), poziţionând absolut faţă de diviziunea .boardFields.
De exemplu, dacă adăugăm ca subdiviziune în .boardFields (care are position: relative)) o diviziune cu position: absolute; width: 100%, atunci aceasta va fi poziţionată faţă de .boardFields şi-i va moşteni lăţimea ("100%" se referă aici la diviziunea faţă de care este poziţionată).
Avem două variante de lucru: una care angajează "bara" ca atare (două <div>-uri, conţinând fiecare câte 8 subdiviziuni pentru etichetele de linii şi respectiv, de coloane) şi una care nu mai implică bare-container, angajând direct 16 <div>-uri pentru etichete.
Dar în prealabil (deşi, cum se poate bănui, am ajuns abia post-factum la această necesitate), ar fi de făcut o anumită îndreptare în codul anterior.
Prin setChessTable() au fost create 64 de diviziuni corespunzătoare câmpurilor tablei, iar în unele dintre acestea setPosition() constituia ulterior câte o diviziune pentru piesele existente în poziţia indicată de FEN - încât diviziunea .boardFields arată astfel:
<div class="boardFields">
<div class="Field Row-8 Col-1 BlackField"> <!-- câmpul a8 -->
<div class="Piece black-rook"></div>
</div>
<!-- ... etc. ... --> <!-- ... -->
<div class="Field Row-1 Col-8 BlackField"> <!-- câmpul h1 -->
<div class="Piece white-rook"></div>
</div>
</div>
Acum - ar urma să adăugăm în div.boardFields, după diviziunile menţionate, cel puţin încă 16 diviziuni pentru etichete.
Dar setPosition() (vezi (VII)) parcurgea diviziunile din .boardFields şi în paralel, caracterele din FEN - înserând o diviziune pentru piesă atunci când caracterul de index curent din FEN indica o piesă, sau ştergând conţinutul diviziunii curente dacă indexul curent din FEN indica alt caracter decât "piesă".
În mod implicit, era asumat că în momentul parcurgerii diviziunea .boardFields conţinea exact cele 64 de diviziuni corespunzătoare câmpurilor tablei; dar după adăugarea diviziunilor pentru etichete, parcurgerea descrisă mai sus se va extinde şi asupra acestora - rezultând pur şi simplu, ştergerea etichetelor respective (conform else $(this).html(""); din finalul metodei setPosition()).
Soluţia banală de a limita parcurgerea cu "if(index < 64)" este proastă ("if"-ul trebuind executat de 64 de ori). Soluţia firească şi într-adevăr bună, constă în prevederea imediat sub div.boardFields, a unui container <div class="flds64"> în care să ambalăm cele 64 de <div>-uri corespunzătoare câmpurilor tablei - separând astfel aceste diviziuni, faţă de cele corespunzătoare etichetelor; parcurgerea menţionată mai sus ar decurge apoi, pe diviziunile din div.flds64.
Necesitatea acestei îndreptări, precum şi însuşi faptul că avem (şi vom avea) de adăugat nişte noi diviziuni arată şi faptul că metoda _create() este formulată prea expeditiv (this.Tabla e constituit într-o singură linie) şi va trebui refăcută, încât să fie mai uşor de extins când va fi nevoie.
Adăugăm în div.boardFields, imediat după div.flds64, diviziunile .horBar şi .verBar pentru bara orizontală şi respectiv, cea verticală; ambele trebuie să aibă position: absolute (indicând "top" şi respectiv "left" faţă de div.boardFields) şi width: 100%, respectiv height: 100% (asigurând indirect, potrivirea exactă cu dimensiunea curentă a tablei, care depinde de setul curent de piese).
Fiecare bară conţine câte 8 <div>-uri; proprietatea .posAbs prevăzută fiecăruia dintre acestea serveşte pentru poziţionarea absolută în cadrul barei din care face parte <div>-ul respectiv. Iar .Row-N şi .Col-N precizează pentru fiecare dintre aceste <div>-uri "top" (în cazul barei verticale) şi "left" (în cazul celei orizontale) faţă de bara-container respectivă.
Adăugăm în fişierul "brw-sah.css" definiţiile minimale (cu setările strict necesare):
.horBar, .verBar, .pos-abs { position: absolute; }
.verBar { left: -6%; height: 100%; } /* faţă de div.boardFields */
.horBar { top: 102%; width: 100%; }
Rescriem metoda _create(), evidenţiind mai bine structura DOM care trebuie creată şi uşurând eventuala extindere a acesteia:
_create: function() {
var inputFEN = this.element, // <textarea> pe care se instanţiază
opts = this.options, // va referi mai rapid opţiunile
self = this; // salvează referinţa 'this' la instanţa curentă
$('<button></button>').text('Load') // înscrie poziţia, la click()
.click(function() {
self._init(); // dar NU "this"._init()
return false;
}).insertAfter( inputFEN );
var thtml = []; // elementele HTML de inserat ca noduri în DOM
thtml.push('<div class="chessmen-', opts.field_size, '">');
thtml.push('<div class="boardFields">');
thtml.push(this._setChessTable()); // 64 câmpuri, sub div.flds64
thtml.push(this._setLabels()); // cele două bare cu etichete
thtml.push('</div>'); // încheie div.boardFields
thtml.push('</div>'); // încheie div.chessmen-N
/* înscrie în DOM, după butonul Load, reţinând o referinţă 'Tabla' */
this.Tabla = $(thtml.join('')).insertAfter(inputFEN.next());
/* div.flds64 va fi vizată frecvent, deci instituie o referinţă */
this.flds64 = this.Tabla.find('.flds64:first');
},
Înscrie cele 64 de <div>-uri corespunzătoare câmpurilor, într-un container div.flds64:
_setChessTable: function() {
var thtml = ['<div class="flds64">'];
/* ... codul anterior al funcţiei ... */
thtml.push("</div>");
return thtml.join('');
},
Parcurge în ordinea 0..63 <div>-urile conţinute în div.flds64 (şi în paralel, caracterele din FEN - identificând câmpurile <div> pe care trebuie aşezate piesele):
_setPosition: function(fen) {
/* ... vezi codul anterior ... */
this.flds64.children().each(function(index) {
/* ... vezi codul anterior ...*/
});
},
În sfârşit, metoda pentru crearea celor două bare cu etichete:
_setLabels: function() {
var thtml = ['<div class="verBar">']; // bara verticală
for(var row = 1; row <= 8; row++)
thtml.push('<div class="pos-abs Row-', row, '">', row, '</div>');
thtml.push('</div>');
thtml.push('<div class="horBar">'); // bara orizontală
for(var col = 1; col <= 8; col++)
thtml.push('<div class="pos-abs Col-', col, '">',
String.fromCharCode(96 + col), // 1 -> 'a', 2 -> 'b', etc.
'</div>');
thtml.push('</div>');
return thtml.join('');
},
După aceste modificări în fişierul "brw-sah.js" putem încărca în browser fişierul "brw-sah.html" (redat de exemplu în (VII)).
Este de observat faptul că etichetele nu sunt "centrate", cum ar fi de dorit. Pentru a obţine şi centrarea, trebuie să adăugăm anumite proprietăţi CSS <div>-urilor care conţin etichetele şi anume, am putea adăuga aceste proprietăţi în definiţia lui .pos-abs…
Dar centrarea pe orizontală necesită anumite proprietăţi CSS, iar pe verticală altele; trebuind să adaptăm .pos-abs fiecărui caz - o eliminăm, înlocuind-o (în "brw-sah.css", dar apoi şi în cadrul metodei _setLabels()) de exemplu cu:
.pos-absHor { position: absolute; width: 12.5%; text-align: center; }
.pos-absVer { position: absolute; line-height: 3em; }
Într-adevăr, setând pe un <div> width şi text-align (obligatoriu ambele), conţinutul va fi aliniat în modul indicat (în particular, va fi centrat):
<div style="width:30px; text-align:center;
height: 35px; border:1px solid black;">B</div>
Centrarea verticală este complicată, dar aici avem un caz particular: este de centrat un singur rând, într-un <div> a cărui înălţime poate fi calculată. În acest caz este suficientă setarea line-height pe o valoare egală cu înălţimea diviziunii respective:
<div style="height:30px; line-height:30px;
width:35px; border:1px solid black;">1234</div>
În cazul nostru, înălţimea este de 12.5% din înălţimea barei verticale - numai că line-height nu poate fi setată astfel (şi am ales o valoare "medie", fiindcă încercarea de a seta "inline" - în cadrul metodei setLabels() - pe valoarea exactă "this.options.field_size" mi-a eşuat…).
În loc să creem cum am făcut mai sus, două bare (poziţionate absolut faţă de div.boardFields) şi apoi diviziuni pentru etichete în interiorul acestora (poziţionate faţă de bară) - am putea să creem direct (şi "de-a valma") cele 16 diviziuni pentru etichete, poziţionându-le nemijlocit faţă de div.boardFields (bazându-ne şi acum, pe .Row-N şi .Col-N):
.Notation-Row, .Notation-Col { /* pe fiecare din cele 16 DIV */
position: absolute; /* faţă de div.boardFields */
}
.Notation-Row { /* pentru fiecare DIV de pe verticală */
left: -6%; /* stânga faţă de div.boardFields */
height: 12.5%; /* egal cu pasul "top"-ului din .Row-N */
}
.Notation-Col { /* pentru fiecare DIV de pe orizontală */
top: 102%; /* jos faţă de div.boardFields */
width: 12.5%; /* pasul "left"-ului din .Col-N */
text-align:center; /* centrează orizontal eticheta */
}
setLabels() se scrie mai simplu faţă de varianta precedentă (cu un singur "for") şi acum reuşeşte şi setarea "inline" a proprietăţii line-height cu valoarea exactă (înălţimea câmpului = field_size + 2, unde "+2" reflectă "padding"-ul de 1 pixel prevăzut în .Field):
_setLabels: function() {
var fld = 2 + parseInt(this.options.field_size), thtml=[];
for (var N = 1; N <= 8; N++) {
var colID = String.fromCharCode(96 + N);
thtml.push("<div style='line-height:", fld, "px' ");
thtml.push("class='Notation-Row Row-", N, "'>", N, "</div>");
thtml.push("<div class='Notation-Col Col-", N, "'>", colID, "</div>");
}
return thtml.join('');
},
Amintindu-ne de proprietatea .revBoard (vezi "brw-sah.css") şi modificând în _create: thtml.push('<div class="boardFields revBoard">'); obţinem în final tabla redată alături - probând că obţinem centrarea dorită a etichetelor (plus inversarea tablei).
Desigur, pe diviziunile pentru etichete mai trebuie setate unele proprietăţi CSS, de exemplu: color: #98a; (nici chiar "black", ca în imaginea redată aici), font-family: monospace; font-size: 0.9em.
Poate că "font-size" ar trebui corelat cu mărimea diagramei (deci cu setul de piese curent)? În acest scop, am putea defini un "font-size" de bază pe containerul .chessmen-N; ca urmare, valorile definite (în unităţi em) pe sub-diviziuni vor fi proporţionate faţă de valoarea de bază menţionată (permiţând adaptarea mărimii etichetelor faţă de setul de piese).
Dar nu-i cazul să complicăm lucrurile: utilizatorul widget-ului va avea nevoie în pagina proprie de unu-două, poate trei seturi de piese şi va putea uşor să seteze "font-size" pentru etichete, în funcţie de contextul propriu.
Să ne imaginăm că avem o listă de "mutări" 1, 2, 3, etc.; nu luăm seama la mutările de şah, ci pur şi simplu reprezentăm mutarea doar prin numărul de ordine al ei. Vrem să completăm widget-ul cu elementele necesare pentru a asigura parcurgerea acestei liste de "mutări" - ceea ce ulterior, va servi pentru a reda o partidă de şah (cu mutări "reale").
Avem în vedere deci, numai o listă fictivă de "mutări"; în cazul unei partide reale, dată într-o notaţie standard - ar trebui rezolvate probleme de analiză (corectitudinea textului primit drept partidă de şah, legalitatea mutărilor conţinute) care în fond sunt "colaterale" (ar fi sarcina unui alt "widget") şi vor necesita oricum, o tratare specială.
În primul rând, va trebui să creem o diviziune pentru lista mutărilor, div.MoveList.
Parcurgere poate înseamna "de la prima mutare până la ultima", dar deasemenea - de la o anumită mutare înainte (eventual, "automat"), sau înapoi, revenire la prima, la ultima, sau la o anumită mutare din listă. În plus, în orice moment trebuie să fie posibilă inversarea tablei.
Prin urmare, vom crea o "bară de navigare" div.GameNav dedesubtul tablei, conţinând butoane (cu handlere corespunzătoare) pentru parcurgere (inclusiv, un buton pentru "inversare").
"Navigarea" ţine de lista mutărilor; deci fiecare mutare trebuie să fie "ambalată" într-un element <a> (HTMLAnchorElement), încât click pe mutare să aibă ca efect setarea poziţiei corespunzătoare efectuării mutării.
Se va putea face click pe oricare mutare din listă, deci poziţia care va trebui setată nu ţine neapărat, de poziţia curentă existentă pe tablă. Prin urmare, link-ul corespunzător mutării va trebui să conţină un atribut moveFEN care să indice poziţia care trebuie setată (fie direct FEN-ul rezultat după mutare, fie indirect, ca index într-o zonă de date care stochează FEN-urile poziţiilor).
În contextul fictiv pe care ni l-am propus aici, sau vom pune moveFEN = numărul mutării, sau poate (ceva mai interesant) vom indica nişte FEN-uri la întâmplare, pentru fiecare mutare.
Pentru funcţiile de navigare, va fi necesar să menţinem indexul mutării curente (aceea din listă care este ultima "efectuată") într-o anumită variabilă internă, this.LAST_MOVE.
Vom adăuga diviziunile prin metoda _create() (deci în momentul instanţierii widget-ului); însă lista mutărilor va trebui setată prin metoda _init() - încât "Load" să permită şi actualizarea ei, conform datelor existente în <textarea> (în orice moment ulterior instanţierii).
Pentru a obţine div.MoveList în dreapta tablei, cel mai firesc este să folosim float: left (în loc de position: absolute, ca în cazul anterior al etichetării tablei). Structura HTML care ar trebui creată în acest scop, are schema următoare:
<div>
<div style="float:left">
<div class="chessmen-" + N>
<div class="GameInfo"></div>
<div class="boardFields">
_setChessTable()
_setLabels()
</div>
</div>
<div class="GameNav"></div>
</div>
<div class="MoveList"></div> (cu float: left)
<eventual, alt DIV care trebuie adăugat în dreapta> (cu "float: left")
<div style="clear:left"></div>
</div>
şi o reflectăm în _create() - vezi sursa iniţială în (VIII) - prin următoarea secvenţă:
// elementele HTML de inserat ca noduri în DOM
var thtml = [
'<div>',
'<div style="float:left;">',
'<div class="chessmen-', opts.field_size, '">',
'<div class="GameInfo"></div>',
'<div class="boardFields">',
this._setChessTable(), // div.flds64 cu 64 câmpuri
this._setLabels(), // etichetarea tablei
'</div>', // încheie div.boardFields
'</div>', // încheie div.chessmen-N
'<div class="GameNav"></div>', // this._setGameNav(),
'</div>',
'<div class="MoveList"></div>', // cu "float: left"
'<div style="clear:left;"></div>',
'</div>'
];
/* înscrie în DOM, după butonul Load, reţinând o referinţă 'Tabla' */
this.Tabla = $(thtml.join('')).insertAfter(inputFEN.next());
/* fiindcă div.MoveList va fi vizată frecvent, constituie o referinţă internă */
this.moveList = this.Tabla.find('.MoveList');
În "brw-sah.css" specificăm proprietăţile corespunzătoare diviziunilor adăugate:
.MoveList {
float: left;
width: 7em; /* alege o lăţime convenabilă */
margin-left: 1em; /* distanţează faţă de DIV-ul din stânga */
/* height: 200px; */ /* trebuie adaptată înălţimii tablei */
overflow: auto;
font-family: monospace;
font-size: 0.9em; /* alege convenabil */
text-align: left;
}
.GameNav, .GameInfo { /* ... width: 90%; font-size: ... */ }
.GameNav {
margin-top: 1.2em; /* evită rândul cu etichetele coloanelor */
}
Pentru ca înălţimea părţii vizibile a div.MoveList să nu depăşească totuşi înălţimea totală a părţii din stânga, am prevăzut overflow: auto şi va mai trebui să setăm o valoare potrivită pentru height (ca urmare, dacă mutările nu încap pe înălţimea fixată, browserul va adăuga automat o bară de scroll); dar setarea pentru "height" va trebui făcută "inline", în _create() - după ce, inserând elementele în DOM, se va putea determina înălţimea curentă a "părţii din stânga".
În secvenţa din _create redată mai sus, am înscris direct '<div class="GameNav"></div>' - permiţând astfel o testare imediată în browser: încărcând fişierul "brw-sah.html" obţinem (dacă adăugăm şi câte o setare pentru border) imaginea reprodusă mai sus.
Dar div.GameNav trebuie să conţină butonele specifice navigării şi este firesc să adăugăm o metodă _setGameNav() care să furnizeze secvenţa HTML corespunzătoare (cum am mai procedat anterior, în cazul etichetării tablei prin _setLabels()).
Presupunem că în /images/ avem nişte fişiere .png pentru butoane de navigare uzuale; folosim caracterele "◒" şi "◓" pentru a sugera orientarea viitoare a tablei (după click pe "butonul" respectiv), precum şi caracterul "⊞" care să sugereze (prin "plus"-ul conţinut) parcurgerea automată a mai multor mutări consecutive.
Imaginăm div.GameNav ca fiind fomată dintr-un paragraf care conţine butoanele menţionate şi adăugăm în "brw-sah.js" (de exemplu, după _setLabels()) următoarea metodă:
_setGameNav: function() {
var ghtml = ["<div class='GameNav'>", "<p>",
"<span title='Reverse'>◓</span> ",
"<img src='/images/first.png' alt='' title='First'></img> ",
"<img src='/images/prev.png' alt='' title='Prev'></img> ",
"<img src='/images/next.png' alt='' title='Next'></img> ",
"<img src='/images/last.png' alt='' title='Last'></img> ",
"<span title='more Next'>⊞</span>",
"</p></div>"];
return ghtml.join('');
},
Următoarele definiţii CSS adăugate în "brw-sah.css", vor asigura într-o anumită măsură proporţionalitatea şi poziţionarea acceptabilă a elementelor adăugate de setGameNav():
.GameNav p { width: 90%; text-align:center;
height: 1.5em; line-height: 1.5em; /* centrează vertical conţinutul */
}
.GameNav span { font-size: 1.5em; color: black; cursor: pointer; }
.GameNav img { height: 1.2em; width: 1.3em;
border: 0; vertical-align: middle;
cursor: pointer; }
Pot fi de văzut unele cizelări şi reoganizări; de exemplu, în loc de a indica direct ("hard") numele fişierelor imagine - ar trebui adăugată o opţiune (în this.options) pentru a indica directorul care conţine imaginile respective (_setGameNav() urmând a fi rescrisă utilizând valoarea acestei opţiuni). Dar lucrul important este asigurarea funcţionalităţii corespunzătoare butoanelor adăugate.
Click pe un buton înseamnă în mod implicit şi click pe paragraful care conţine butoanele - prin urmare va fi suficient un singur handler, montat pe paragraful-container, pentru a putea sesiza click-ul pe unul dintre butoanele conţinute şi a acţiona corespunzător.
În jQuery dispunem pentru oricare element - după transformare în obiect jQuery - de metoda .click( handler(eventObject) ), care ataşează obiectului un "handler" de click - o funcţie care va fi executată de fiecare dată când obiectul sesizează un eveniment 'click'; parametrul "eventObject" este un obiect jQuery conţinând proprietăţi precum .target (referinţă la obiectul de pe care s-a iniţiat evenimentul) şi metode precum .stopPropagation() (care limitează propagarea "în sus" a evenimentului - scop în care putem folosi şi numai "return false" la sfârşit).
Adăugăm în metoda _create() (la sfârşit) un handler de click pentru paragraful din div.GameNav care conţine butoanele de navigare create mai sus:
this.REVERSE = false; // iniţial, tabla are orientarea obişnuită
this.Tabla.find('.GameNav').find('p').click(function(event) {
var dest = $(event.target); // obiectul acţionat prin click (butonul)
if(self.brwtimer) // sistează (eventual) parcurgerea automată
clearTimeout(self.brwtimer);
switch(dest.index()) { // 'self' referă instanţa curentă a widget-ului
case 1: self._firstMove(); break; // prima mutare
case 2: self._prevMove(); break; // precedenta
case 3: self._nextMove(); break; // următoarea
case 4: self._lastMove(); break; // ultima mutare
case 5: self._auto_nextMove(); break; // parcurge automat
case 0: // sau primul buton, sau înafara oricăruia (dar în 'p')
if(dest.is('span')) { // schimbă orientarea tablei
self.REVERSE = !self.REVERSE;
dest.html(self.REVERSE ? "◒": "◓"); // ◒ sau ◓
self.boardFields.toggleClass('revBoard');
}
}
return false; // evită propagarea click-ului înafara lui 'p'
});
}, // încheie _create()
"Inversarea" tablei este astfel, deja asigurată: am prevăzut this.REVERSE pentru a reţine orientarea curentă şi a fost suficient să adăugăm sau să eliminăm .revBoard pe div.boardFields (a vedea (VI)). Rămâne să elaborăm celelalte cinci metode invocate mai sus - _firstMove(), _nextMove(), etc.
Pentru a pune la punct mecanismele de navigare specificate mai sus, încă nu este necesar să avem în vedere conţinutul "real" al diviziunii .MoveList (care va trebui să fie lista mutărilor dintr-o partidă de şah, ambalate în link-uri de un anumit format).
Totuşi, a considera "mutare" ca fiind "pur şi simplu" (cum ziceam chiar la început) doar "numărul de ordine" al ei - este o exagerare inutilă ("pur şi simplu" neproductivă).
Să considerăm - temporar, pentru scopul urmărit aici - că "mutare" este un obiect cu două proprietăţi: .san care are ca valoare notaţia obişnuită a mutării şi .FEN, având ca valoare şirul-FEN al poziţiei rezultate prin efectuarea mutării indicate de "san". Deosebirea faţă de contextul viitor al unei partide "reale", constă în faptul că acum FEN-urile vor trebui introduse "manual" şi nu determinate în cursul analizei mutărilor din textul partidei furnizate din exteriorul widget-ului.
În modul cel mai firesc, creem aceste obiecte "mutări" în cadrul metodei _init():
_init: function() {
this.MOVES = [ // lista mutărilor (pe care vrem să "navigăm")
{san: 'd4', FEN: 'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR'},
{san: 'Nf6', FEN: 'rnbqkb1r/pppppppp/5n2/8/3P4/8/PPP1PPPP/RNBQKBNR'},
{san: 'Nf3', FEN: 'rnbqkb1r/pppppppp/5n2/8/3P4/5N2/PPP1PPPP/RNBQKB1R'},
{san: 'c5', FEN: 'rnbqkb1r/pp1ppppp/5n2/2p5/3P4/5N2/PPP1PPPP/RNBQKB1R'},
{san: 'Bg5', FEN: 'rnbqkb1r/pp1ppppp/5n2/2p3B1/3P4/5N2/PPP1PPPP/RN1QKB1R'},
{san: 'Ne4', FEN: 'rnbqkb1r/pp1ppppp/8/2p3B1/3Pn3/5N2/PPP1PPPP/RN1QKB1R'},
{san: 'Qb6', FEN: 'rnbqkb1r/pp1ppppp/8/2p5/3Pn3/4BN2/PPP1PPPP/RN1QKB1R'},
{san: 'b3', FEN: 'rnb1kb1r/pp1ppppp/1q6/2p5/3Pn3/4BN2/PPP1PPPP/RN1QKB1R'},
{san: 'd4', FEN: 'rnb1kb1r/pp1ppppp/1q6/2p5/3Pn3/1P2BN2/P1P1PPPP/RN1QKB1R'},
{san: 'Qb4', FEN: 'rnb1kb1r/pp1ppppp/8/2p5/1q1Pn3/1P2BN2/P1P1PPPP/RN1QKB1R'},
{san: 'Nfd2', FEN: 'rnb1kb1r/pp1ppppp/8/2p5/1q1Pn3/1P2B3/P1PNPPPP/RN1QKB1R'},
{san: 'cxd4', FEN: 'rnb1kb1r/pp1ppppp/8/8/1q1pn3/1P2B3/P1PNPPPP/RN1QKB1R'},
{san: 'a3', FEN: 'rnb1kb1r/pp1ppppp/8/8/1q1pn3/PP2B3/2PNPPPP/RN1QKB1R'},
{san: 'dxe3', FEN: 'rnb1kb1r/pp1ppppp/8/8/1q2n3/PP2p3/2PNPPPP/RN1QKB1R'},
{san: 'axb4', FEN: 'rnb1kb1r/pp1ppppp/8/8/1P2n3/1P2p3/2PNPPPP/RN1QKB1R'},
{san: 'exf2#', FEN: 'rnb1kb1r/pp1ppppp/8/8/1P2n3/1P6/2PNPpPP/RN1QKB1R'}
];
this.LAST_MOVE = null; // păstrează indexul ultimei mutări efectuate
this.brwtimer = null; // pentru a iniţia/stopa parcurgerea automată
this._setPosition(FEN_STD); // înscrie poziţia iniţială obişnuită
this._setMoveList(); // înscrie valorile '.san' în div.MoveList
},
Deocamdată, _setMoveList() doar înscrie mutările din .MOVES în div.MoveList, câte două pe linie (alb, respectiv negru), numerotând liniile:
_setMoveList: function() {
var MOVES = this.MOVES, mhtml = [], nr = 1;
for(var i = 0, n = MOVES.length; i < n; i++) {
if(i % 2 == 0) {
mhtml.push(nr, '. ', MOVES[i].san,
' ');
nr ++;
} else mhtml.push(MOVES[i].san, '<br>');
}
this.moveList.html(mhtml.join(''));
},
N-am avut în vedere aici nicio "decorare" specială - doar am adăugat line-height: 1.5em; în definiţia lui .MoveList din "brw-sah.css", pentru a mări puţin spaţierea rândurilor de mutări.
În contextul constituit mai sus, "navigare" va însemna parcurgerea tabloului .MOVES, actualizând .LAST_MOVE cu indexul curent şi aplicând _setPosition() pe FEN-ul de la acest index.
Prevedem o metodă ajutătoare _go_to_pos(): primind datele necesare (deocamdată, un index în limitele tabloului .MOVES), aceasta setează poziţia corespunzătoare şi actualizează .LAST_MOVE. Handlerele propriu-zise doar "calculează" indexul necesar (pentru următoarea mutare, precedenta, prima, sau ultima) şi apelează apoi _go_to_pos(index).
_go_to_pos: function(idx) { // "efectuează" mutarea indicată de 'idx'
var move = this.MOVES[idx];
this._setPosition(move.FEN);
this.LAST_MOVE = idx; // reţine indexul mutării efectuate
},
_nextMove: function() {
if (this.LAST_MOVE < this.MOVES.length - 1)
this._go_to_pos(this.LAST_MOVE + 1);
},
_auto_nextMove: function() { // apelează recursiv _nextMove(),
var self = this; // parcurgând automat o secvenţă de mutări
self._nextMove();
self.brwtimer = setTimeout(function() {
self._auto_nextMove();
}, 1000);
},
_prevMove: function() {
if (this.LAST_MOVE > 0)
this._go_to_pos(this.LAST_MOVE - 1);
},
_firstMove: function() {
this._go_to_pos(0);
},
_lastMove: function() {
this._go_to_pos(this.MOVES.length - 1);
},
Desigur, avem aici cam cea mai simplă formulare pentru aceste metode. Vor fi necesare în mod firesc, anumite completări: mutarea curent efectuată va trebui să fie evidenţiată (de exemplu, schimbând valoarea de 'background') şi în lista mutărilor, redată în div.MoveList; dacă mutarea curentă nu este în zona vizibilă a diviziunii .MoveList, atunci va trebui activat automat un mecanism de "scroll", pentru a derula lista până ce mutarea ajunge în zona vizibilă.
Tocmai în vederea unor astfel de completări, am instituit "helper"-ul _go_to_pos(): ele vor putea fi efectuate într-un singur loc, anume în cadrul acestei funcţii ajutătoare.
În final, încărcând în browser "brw-sah.html" se va putea testa parcurgerea partidei de 8 mutări, reprezentate mai sus în tabloul .MOVES. Fiindcă nu am implicat încă nicio componentă de analiză a legalităţii mutărilor, putem "extinde" partida - pentru a testa şi pe partide mai lungi - astfel: copiem conţinutul actual din tabloul .MOVES şi (după ce adăugăm , după ultima lui valoare) îl "pastăm" la sfârşitul tabloului - obţinând o "partidă" de două ori (analog, de patru ori, etc.) mai mare.
În precedentele nouă părţi ale acestui studiu elementar, ne-am ocupat de crearea infrastructurii DOM + CSS pentru un widget a cărui intenţie finală este aceea de a permite parcurgerea în browser a unei partide de şah. În părţile următoare ne ocupăm de reprezentarea standard pentru partidă de şah, abordând şi elementele necesare de modelare a jocului de şah.
Infrastructura unitară DOM + CSS creată anterior are la bază un set de sprite-uri de piese de şah. CSS defineşte dimensiunile specifice fiecărui sprite pentru piesă, câmp şi tabla de şah şi defineşte independent de sprite (folosind procente) criteriile de poziţionare pe tablă a câmpurilor şi pieselor. DOM-ul creat constă dintr-un număr de diviziuni - pentru tabla de şah cu cele 64 de câmpuri ale ei, pentru etichete de linii şi coloane, bara de navigare, lista mutărilor, etc. - şi totodată, un anumit număr de handlere asociate (pe butoanele de navigare, pe lista mutărilor, etc.).
Sprite-urile şi parţial, fişierul CSS - au putut fi create în mod automat (prin scripturi Bash). DOM-ul este constituit "inline" (în cursul metodei _create()), la instanţierea widget-ului.
Pentru a definitiva lucrurile (conform "intenţiei finale" tocmai amintite), trebuie acum să modelăm acest scenariu: preia (din elementul <textarea> pe care s-a instanţiat widget-ul) textul unei partide de şah; analizează acest text, extrăgând informaţiile despre partidă (cine cu cine joacă, la ce dată, cu ce rezultat, etc.) precum şi lista mutărilor efectuate.
Bineînţeles (în primul rând) că acest scenariu trebuie regizat după crearea infrastructurii DOM (operată de _create(), în momentul instanţierii widget-ului) - deci în cursul metodei _init().
Apoi (ca de obicei când se preiau date introduse de utilizator) trebuie verificat că input-ul respectiv este textul unei "partide de şah" şi nimic altceva. În scopul acestei verificări va trebui să avem în vedere un format standard pentru reprezentarea textuală a partidelor de şah - PGN.
În sfârşit, trebuie să avem în vedere ca, înainte de a înscrie mutările (preluate din textul partidei) în diviziunea constituită în DOM pentru "lista mutărilor" - să ne asigurăm că aceste mutări sunt legale.
Verificarea legalităţii mutărilor presupune neapărat să cunoaştem regulile jocului de şah (ceea ce n-a fost necesar în părţile anterioare). În principal este vorba de regulile de mutare specifice fiecărui tip de piesă - inclusiv mutarea "en-passant" şi mutările de tip "transformare" pentru pioni, mutările de tip "rocadă" - şi desigur, de reguli privitoare la poziţia de "şah", de "mat" sau de "remiză".
Mecanismul prin care se verifică legalitatea mutării curente - câtă vreme nu inventăm altceva - este unul tipic (fiind folosit în majoritatea programelor de şah): se generează o listă a tuturor mutărilor legale posibile în poziţia curentă, pentru partea care este la mutare; dacă mutarea indicată se găseşte în această listă, atunci rezultă că acea mutare este legală.
Pentru a fi eficient, acest mecanism implică o reprezentare specială (binară) a tablei de şah şi a poziţiei, dublând reprezentarea DOM constituită anterior (şi "dublând" reprezentarea textuală a mutărilor); ca urmare, vor fi necesare anumite secvenţe de cod pentru conversii şi legături între aceste reprezentări.
Vom folosi aici o astfel de "reprezentare specială" (vizând astfel şi "modelarea jocului de şah", cum specifică titlul comun al acestor prezentări). Dar de fapt, acest mecanism este absolut firesc pentru un chess engine, când se pune problema de a găsi o mutare cât mai bună în lista celor posibile; el se poate folosi şi în cadrul unui PGN-browser - fiind totuşi "prea tare" în acest caz, când se are în vedere doar mutarea curent citită (fără nicio sarcină de alegere) - încât generarea tuturor mutărilor este acum un mic lux, faţă de ce ar putea fi suficient pentru stabilirea legalităţii acelei mutări.
Formatul standard de reprezentare textuală a partidelor de şah este PGN (introdus de Steven J. Edwards în 1994). Pentru baze de date cu milioane de partide de şah şi numeroase criterii de organizare se folosesc şi formate de reprezentare binară (vezi ChessBase Database programs).
Formatul PGN conţine o secţiune informativă, urmată de lista mutărilor; se pot concatena mai multe partide într-un fişier text, având extensia standard .pgn.
Secţiunea informativă ar fi importantă dacă am avea de rezolvat cereri asupra partidelor dintr-un fişier .pgn (de exemplu, "lista partidelor care aparţin cutărui jucător"). Dar aici avem în vedere o singură partidă, iar antetul ei serveşte cel mult pentru a constitui conţinutul diviziunii .GameInfo (şi eventual, pentru a seta poziţia iniţială, dacă este specificată în antetul respectiv); este firesc atunci, să permitem şi cazul (incorect pentru PGN) când antetul este omis, permiţând "browsarea" şi pentru o partidă ad-hoc (care conţine numai lista mutărilor).
Antetul este constituit din "taguri PGN", cu sintaxa: [Cheie "Valoare"] (unde parantezele pătrate, spaţiul separator după Cheie şi ghilimelele care încadrează Valoare sunt toate, obligatorii):
[White "alexo 99"] [Black "vlad.bazon"] [Event "InstantChess"] [WhiteElo "1936"] [BlackElo "2038"] [Result "0-1"]
Să construim treptat o expresie regulată care să identifice în textul PGN toate tagurile (şi numai pe acestea). Primul caracter dintr-un tag PGN este "[" - deci şablonul trebuie să înceapă cu \[ (prefixul "\" schimbă semnificaţia unui anumit caracter: "[" are prestabilită semnificaţia de constructor de "clasă de caractere" şi aici, "\[" îi redă semnificaţia de caracter obişnuit).
Urmează numita mai sus Cheie, constând dintr-o secvenţă nevidă de caractere alfanumerice - deci adăugăm în şablon \w+ (acum "\" eludează semnificaţia de literă "w", rezultând semnificaţia de caracter posibil într-un "word" - adică literă, cifră sau "_"; iar + înseamnă "cel puţin o apariţie").
Urmează unul sau poate mai multe spaţii, pentru care şablonul este \s+. Până acum, şablonul este: /\[\w+\s+/ (unde slash-urile - iniţial şi final - joacă rolul de "constructor" de expresii regulate).
Mai departe, urmează ghilimele, Valoare, ghilimele şi în final ]. Aici, Valoare poate conţine orice caracter (nu numai \w), cu excepţia caracterului " încât şablonul potrivit pentru Valoare este [^"]* (unde * înseamnă zero sau mai multe apariţii, iar [^"] desemnează orice caracter diferit de "). Implicit, avem în vedere şi taguri "vide" (de forma Cheie "").
Prin urmare, şablonul căutat este \[\w+\s+"[^"]*"\]. El va permite extragerea primului tag din textul PGN; pentru a extinde căutarea pe întregul text PGN, trebuie să adăugăm constructorului specificatorul g (de la "global"), obţinând forma finală /\[\w+\s+"[^"]*"\]/g.
Extinzând "pe întregul text", vom găsi eventual şi secvenţe de tipul respectiv care de fapt nu sunt taguri PGN: astfel de secvenţe pot exista şi în interiorul unora dintre comentariile existente în lista mutărilor; dar acest caz este absolut netipic pentru PGN şi este preferabil să-l ignorăm.
Pentru o verificare expeditivă, alegem să folosim perl. Copiem un text PGN într-o construcţie heredoc pentru o variabilă $PGN, apoi folosim operatorul =~ pentru a găsi (în tabloul @tags) potrivirile între şirul $PGN şi şablonul nostru:
$PGN = <<PGN;
[White "alexo 99"]
[Black "vlad.bazon"]
[Event "InstantChess"]
[WhiteElo "1936"]
[BlackElo "2038"]
[Result "0-1"]
[ICCause "2"]
[ICEcause "3"]
1.d4 Nf6 2.Bf4 c5 {comentariu [xxx "xxx"]} 3.e3 Qb6
4.b3 cxd4 5.exd4 Nc6 6.Nf3 Nd5 7.Be3 Nxe3 8.fxe3 e5 9.Bc4 d5 10.Be2 Be7 11.O-O O-O
12.h3 Be6 13.c3 Rac8 14.Nbd2 e4 15.Nh2 Bg5 16.Ng4 f5 17.Nh2 Bxe3+ 18.Kh1 Nxd4
19.cxd4 Qxd4 20.Nhf3 exf3 21.Nxf3 Qf6 22.Bd3 f4 23.Nh2 Qg5 24.Ng4 Bc5 25.Qf3 h5
26.Nh2 Be3 27.Qe2 Qh6 28.Nf3 g5 29.Ne5 g4 30.h4 Bf5 31.Rad1 Qf6 32.Bxf5 Qxh4# 0-1
PGN
@tags = $PGN =~ /\[\w+\s+"[^"]*"\]/g;
print "@tags\n";
Executând programul, se afişează tagurile extrase din şirul $PGN prin şablonul respectiv:
vb@vb:~$ perl test.pl [White "alexo 99"] [Black "vlad.bazon"] [Event "InstantChess"] [WhiteElo "1936"] [BlackElo "2038"] [Result "0-1"] [ICCause "2"] [ICEcause "3"] [xxx "xxx"] vb@vb:~$
Observăm că s-a extras inclusiv "tagul" din comentariul artificial dinaintea mutării 3.
Faptul că am folosit Perl pentru acest mic experiment nu este întâmplător: javaScript, PHP, Python, etc. au adoptat pentru expresiile regulate sintaxa introdusă de Perl.
Instituim în _init() un obiect intern this.tags = {}; în care să păstrăm eventual, tagurile extrase din PGN - de exemplu, this.tags['White'] = 'alexo 99'.
Pentru a separa Cheie de Valoare în cadrul unui tag (extrăgând-ule separat), putem folosi şablonul /(\w+)\s+\"(.*)\"\]/; aici "()" grupează caracterele din Cheie şi respectiv pe cele din Valoare, iar grupele rezultate pot fi extrase folosind (în javaScript) RegExp.$N unde N este rangul grupului.
Următorul fişier HTML prezintă o funcţie extract_tags() care extrage tagurile din şirul PGN primit ca parametru (folosind în acest scop cele două şabloane precizate mai sus) şi returnează un "tablou asociativ" tags (cu tags[Key] = Val):
<!DOCTYPE html>
<head>
<script>
function extract_tags(PGN) {
var sablon_tag = /\[\w+\s+"[^"]*"\]/g,
sablon_KeyVal = /(\w+)\s+\"(.*)\"\]/, // grupează Key şi Val
tags = {}; // tags[Key] = Val
var matches = PGN.match(sablon_tag); // alert(matches.join('\n'));
for(var i=0, n=matches.length; i < n; i++) {
var tag = matches[i]
.match(sablon_KeyVal); //alert(RegExp.$1);
tags[RegExp.$1] = RegExp.$2;
}
return tags;
}
</script>
</head>
<body>
<script>
var PGN = '[White "alexo 99"][Black "vlad.bazon"][Event "InstantChess"]' +
'[WhiteElo "1936"][BlackElo "2038"][Result "*"][ICCause "2"][ICEcause "3"]' +
'1.d4 Nf6 2.Bf4 c5 {comentariu [xxx "xxx"]} 3.e3 Qb6' +
'4.b3 cxd4 5.exd4 Nc6 6.Nf3 Nd5 7.Be3 Nxe3 8.fxe3 e5 9.Bc4 d5 *';
var tags = extract_tags(PGN);
result = [];
for(var key in tags)
result.push(key + ": " + tags[key]);
alert(result.join('\n'));
</script>
</body>
N-avem decât să încorporăm funcţia redată aici în cadrul metodei _init() (rescriind-o eventual, ca metodă privată a widget-ului) şi… putem uita de-acum de "taguri PGN". Tagurile care sunt utile în contextul nostru sunt cele de chei White, Black, Result şi Date - valorile acestora servesc pentru a constitui (în cursul metodei _init) conţinutul diviziunii .GameInfo.
Un singur tag ar mai fi important: dacă antetul PGN conţine un tag FEN, atunci acesta trebuie să aibă ca valoare un şir FEN, reprezentând poziţia iniţială (alta decât cea standard) pentru partida respectivă.
"Putem uita" înseamnă că putem elimina antetul informativ din textul PGN al partidei. Dar această eliminare nu se poate face în interiorul funcţiei extract_tags(PGN) redate mai sus, fiindcă în javaScript şirurile (aici, şirul PGN) sunt transmise prin valoare şi nu prin referinţă.
Dar mai sus avem doar o ilustrare independentă a lucrurilor - de aceea am considerat ca parametru, şirul PGN. De fapt, şirul PGN este furnizat widget-ului nostru într-un element <textarea> (referit intern prin this.element); deci nu este necesar să-l precizăm ca parametru funcţiei extract_tags().
După extragerea tagurilor, PGN-ul fără partea de antet se va putea obţine prin var PGN = this.element.val().replace(/\[\w+\s+"[^"]*"\]/g, ''); (folosind iarăşi şablonul de tag).
Eliminând antetul, şirul PGN rămas poate să înceapă cu un comentariu introductiv, referitor la partida respectivă (sau la poziţia indicată în tagul FEN existent în antet). Extrăgând eventual şi acest comentariu - rămâne "lista mutărilor" (în care unele mutări pot fi şi acestea, comentate), de care ne vom ocupa în părţile următoare.
Notaţia liniilor prin 1..8 (începând dinspre partea albului), a coloanelor prin a..h (începând din stânga albului) şi notaţia algebrică derivată pentru mutările pieselor - SAN, adoptată de FIDE şi implicată în reprezentarea PGN - a fost introdusă de către Philipp Stamma, pe la 1750.
PGN poate avea două formate; "PGN Export format" este mai riguros - fiind folosit pentru arhivarea datelor PGN şi uşurând interschimbul acestora între programe - şi poate arăta astfel:
În acest format anumite taguri sunt obligatorii şi au o ordine prestabilită; nu este permisă scrierea mai multor taguri pe o aceeaşi linie; după "[" şi înainte de "]" nu trebuie lăsat spaţiu, iar între cheia şi valoarea tagului trebuie să existe un singur spaţiu. Antetul PGN trebuie separat de secţiunea mutărilor printr-o linie liberă. În plus, orice linie trebuie să conţină cel mult 80 de caractere.
Disecăm mai jos acest exemplu, evidenţiind elementele generale care trebuie luate în considerare de către o funcţie de analiză a textului PGN; dar în acest scop folosim formatul mai liber "PGN Import format", care elimină toate restricţiile menţionate mai sus - ideea fiind aceea de a putea folosi PGN-browserul (pe care-l dezvoltăm aici de ceva vreme) şi pentru PGN-uri (întâlnite şi acestea) care au anumite abateri de la standard.
Secţiunea informativă (antetul PGN, cu informaţii despre partidă)
Am păstrat numai unele taguri, relevante pentru conţinutul partidei: este vorba de un studiu pe tema "Turn contra Nebun", dintr-o carte celebră - Analyse du jeu des Échecs a lui Philidor.
Tagul FEN indică poziţia iniţială pentru lista de mutări (care urmează în PGN după antet) - vezi diagrama alăturată. Linia 8 corespunde primului câmp FEN 3k4/ (3 câmpuri libere, regele negru, 4 câmpuri libere); linia 7 are un turn negru, cu 4 câmpuri libere în stânga lui şi 3 în dreapta - deci se reprezintă prin /4r3/ în al doilea câmp FEN; ş.a.m.d.
Liniile 4, 3 şi 2 sunt libere, deci corespund câmpurilor FEN /8/8/8/. Prima linie are doar un turn alb pe câmpul f1, deci "ultimul" câmp din FEN este /5R2.
Câmpurile din FEN vizate mai sus reprezintă poziţia din diagramă, linie după linie. Dar FEN-ul mai conţine încă cinci "câmpuri" w - - 0 1 (separate prin spaţiu) - fiecare având o anumită semnificaţie pentru poziţia reprezentată în prima parte a FEN-ului; de exemplu, w marchează faptul că în poziţia respectivă albul ("white") este la mutare.
Comentariu introductiv (opţional; apare înainte de lista mutărilor)
Comentariile sunt texte cuprinse între acolade (şi nu pot fi imbricate). Un comentariu introductiv se referă eventual la întreaga partidă (nu la o anumită mutare din cursul partidei) şi trebuie să preceadă imediat, lista mutărilor; el va trebui reţinut separat, faţă de tabloul mutărilor.
Secţiunea mutărilor (a consulta eventual Rules of chess (Regulile şahului))
1. Rf8+ Re8 2. Rf7 Re2! (2... Rh8 3. Ra7 Rh6+ 4. Be6 {leads to mate.})
3. Rg7 $1 ; The black rook is forced to a less favourable square.
3... Re1 ({Weak is} 3... Re3 4. Rb7) 4. Rb7 Rc1 ({Another interesting variation is} 4... Kc8 5. Ra7 $1 Rb1 6. Rh7 $1 Kb8 (6... Rb6+ 7. Bc6) 7. Rh8+ Ka7 8. Ra8+ Kb6 9. Rb8+) 5. Bb3 $3 {This move would make no sense if the black rook should be on c2.} 5... Rc3 $1 {The rook moves to a bad rank.} ({White also wins in} 5... Kc8 6. Rb4 Kd8 7. Rf4 Re1 (7... Kc8 8. Bd5 Kb8 9. Ra4) 8. Ba4 Kc8 9. Bc6 Rd1+ 10. Bd5 Kb8 11. Ra4 ) 6. Be6 Rd3+ 7. Bd5 Rc3 (7... Kc8 8. Ra7 {loses at once.}) 8. Rd7+ $1 Kc8 ({Or } 8... Ke8 9. Rg7) 9. Rf7 Kb8 10. Rb7+ Kc8 11. Rb4 $1 Kd8 ({ The first point of the last white move is} 11... Rd3 12. Ra4)
12. Bc4 $3 ; The second pointe.
12... Kc8 13. Be6+ Kd8 14. Rb8+ Rc8 15. Rxc8# 1-0
Prima mutare 1. Rf8+ constituie reprezentarea SAN a mutării care s-ar descrie prin "ia turnul alb de pe câmpul f1 şi pune-l pe câmpul f8, apoi zi şah" (a vedea diagrama de mai sus).
SAN este minimală, fiindcă ţine seama de context; prefixul 1. (numărul de ordine al mutării) arată că albul va executa mutarea; R arată că piesa care trebuie mutată este un turn; câmpul de start nu este indicat, fiindcă pe câmpul destinaţie precizat f8 nu poate fi mutat (în contextul curent) decât un singur turn (cel de pe f1). Sufixul + indică un efect al mutării: negrul "este în şah" (iar sufixul # la mutarea 15 indică faptul că negrul este mat).
Unele mutări sunt adnotate cu un simbol care exprimă o apreciere de calitate a mutării: mutarea 2 a negrului Re2! este o mutare bună; sau sunt adnotate cu un index prefixat cu $, indicând o anumită intrare într-un tabel predefinit (denumit NAG) care conţine exprimări standard de apreciere a mutării sau a poziţiei: pentru 12. Bc4 $3 găsim pe locul NAG[3] aprecierea 'very good or brilliant move (traditional "!!")'.
Pentru unele mutări sunt indicate variante alternative de joc (posibil, cu sub-variante): o listă de mutări încadrată între paranteze rotunde. De exemplu, aici avem indicată o variantă de joc la mutarea a 2-a a negrului şi o variantă cu o sub-variantă la mutarea a 4-a a negrului.
La unele mutări (inclusiv, în cadrul variantelor) sunt oferite comentarii. De obicei, acestea sunt texte încadrate între acolade; dar este prevăzută şi forma "linie": se pune ; după mutarea respectivă şi atunci tot textul care urmează până la sfârşitul liniei este considerat comentariu - cum avem mai sus pe linia mutării 3 şi apoi, pe linia mutării 12.
La sfârşitul secţiunii mutărilor trebuie înscris rezultatul partidei - în cazul nostru 1-0, indicând că albul a câştigat; dar acesta nu va face parte din tabloul mutărilor.
Am văzut în (X) cum putem folosi expresii regulate pentru a extrage partea de antet a PGN-ului, constituind tabloul intern this.tags cu tagurile respective. Vom proceda analog asupra secţiunii mutărilor din PGN, constituind un obiect javaScript care să permită apoi PGN-browserului să acceseze uniform fiecare mutare SAN, împreună cu adnotările, variantele şi comentariile asociate acesteia (urmând să verifice legalitatea mutării şi să înscrie cele cuvenite în diviziunea mutărilor şi în cea a comentariilor).
Definim în afara widget-ului un obiect javaScript care să reprezinte o mutare din PGN:
function Move() {
this.SAN = ""; // Standard Algebraic Notation
this.FEN = ""; // the resulting FEN
this.mark = ""; // + (check), # (checkmate), ! (good move), etc.
this.NAG = 0; // Numeric Annotation Glyphs index
this.variant = ""; // variants for this move (but... as String?)
this.comment = "" // annotations
};
Poate că mai târziu vom reveni, punând this.variant = []; - având adică în vedere că o variantă este ea însăşi o "listă de mutări" (deci un Array() cu obiecte Move()).
Aşa cum pentru a păstra tagurile extrase din PGN, am instituit obiectul this.tags - adăugăm acum, la începutul metodei _init(), un tablou în care vom înregistra câte un obiect Move() pentru fiecare mutare din PGN şi deasemenea, un şir care să păstreze eventualul comentariu iniţial şi unul care să păstreze anumite mesaje generate în situaţia când parcurgând textul PGN, am întâlni o eroare de sintaxă (paranteze desperecheate, sau o mutare notată greşit, etc.):
_init: function() {
this.tags = {}; // from the 'tag pairs' section of the PGN=this.element.val()
this.moves = []; // Move() objects, for each move from the 'movetext' PGN-section
this.in_comm = ""; // the initial comment (that precede 'movetext')
this.errors = ""; // concatenate various error messages (when PGN is parsed)
// ... //
},
După eliminarea secţiunii informative şi a comentariului iniţial (de care ne-am ocupat anterior), PGN-ul s-a redus la secţiunea mutărilor. În primul rând să scăpăm de câmpul de "rezultat final" de la sfârşit şi să eliminăm eventualele spaţii de la început:
//strip the final result field, but assure that exist a Result tag
var result; // 1-0 | 0-1 | 1/2-1/2 | *
if ((result = PGN.match(/1\/2\s*-\s*1\/2\s*$/)) ||
(result = PGN.match(/0\s*-\s*1\s*$/)) ||
(result = PGN.match(/1\s*-\s*0\s*$/)) ||
(result = PGN.match(/\*\s*$/))) {
PGN = PGN.replace(result, "");
if (!this.tags['Result']) this.tags['Result'] = result;
}
else alert('a Result is needed at the end of PGN');
PGN = PGN.replace(/^\s+/, ""); // strip initial spaces
Ne-am asigurat astfel că PGN-ul rămas conţine doar mutări SAN numerotate (şi eventual marcate cu simboluri de apreciere), comentarii şi variante; parcurgând acest şir putem stabili începutul fiecăruia dintre aceste elemente şi folosind expresii regulate corespunzătoare vom putea extrage fiecare element (repetând apoi, asupra şirului rămas după "extragerea" elementului curent identificat). Algoritmul poate fi descris succint astfel:
Algoritm de analiză sintactică şi lexicală a şirului mutărilor PGN - pseudocod PGN-syntax-error = FALSE WHILE PGN este nevid { first_car = primul caracter din PGN IF first_car este o cifră: deci urmează "numărul mutării" DELETE(număr de mutare) va putea fi dedus din indexul lui MOVE() în this.moves[] IF first_car este caracter iniţial de mutare SAN: IF se respectă sintaxa SAN: EXTRACT(mutare SAN) în câmpul m.SAN al unui nou obiect m = MOVE() adaugă obiectul m în tabloul this.moves[] ELSE PGN-syntax-error = TRUE IF first_car este marcaj de apreciere sau este index NAG: EXTRACT(apreciere) în câmpul m.mark respectiv în m.NAG IF first_car este '{': început de comentariu IF acoladele se împerechează corect şi nu sunt imbricate EXTRACT(comentariu) în m.comment ELSE PGN-syntax-error = TRUE IF first_car este ';': început de comentariu-linie EXTRACT(restul liniei curente) în m.comment IF first_car este '(': variantă/sub-variantă IF parantezele se împerechează corect EXTRACT(variantă) adăugând-o în m.variant ELSE PGN-syntax-error = TRUE IF PGN-syntax-error este TRUE BREAK afişează mesaj de eroare şi încheie EXTRACT(spaţii iniţiale) actualizează first_car şi reia analiza }
Prezentăm mai întâi aspecte specifice mutărilor de şah, de care trebuie să ţină seama operaţia EXTRACT(mutare SAN); acestea vor fi vizate deasemenea, când vom aborda problema stabilirii legalităţii unei mutări. La sfârşit, vom schiţa şi o implementare a algoritmului de mai sus în cadrul widget-ului nostru (dacă n-am spus - întregul cod-sursă este postat pe github).
Sunt de văzut vreo trei-patru tipuri de mutări; le discutăm pe rând şi construim expresii regulate pentru fiecare tip, încercând să reunim (cât se poate) aceste şabloane parţiale într-o singură expresie regulată (permiţând extragerea unei game cât mai largi de mutări SAN).
Mutarea unei piese. "Piesă" înseamnă rege, damă, turn, nebun, sau cal (nu şi pionul!) - notate în mod standard prin K, Q, R, B, N (indiferent de culoare, în cadrul SAN).
În acest caz, primul caracter din SAN-ul mutării este unul din gama [KQRBN].
Urmează - cel mai frecvent - specificarea câmpului pe care se mută piesa, ceea ce revine la concatenarea notaţiei de coloană cu cea de linie: [a-h][1-8]; de exemplu, Rc8 înseamnă mutarea turnului pe câmpul c8.
Dar specificarea numai a destinaţiei ar fi insuficientă atunci când partea care trebuie să mute are mai multe piese de tipul indicat de primul caracter SAN care să poată muta legal pe câmpul destinaţie considerat. Astfel, în figura alăturată avem două turnuri care pot muta pe câmpul c8 şi (presupunând că ambele mutări sunt legale) notaţia "Rc8" nu poate de data aceasta, să indice unic o mutare. Pentru asemenea cazuri de excepţie, SAN implică şi o informaţie (dar minimală!) privind câmpul iniţial al piesei; pentru cazul redat în figură este suficient să se precizeze coloana câmpului iniţial: Rcc8. Pentru situaţia în care disocierea s-ar putea face şi prin precizarea liniei câmpului iniţial (adică aici, prin R6c8) - SAN prevede o regulă de prioritate: se foloseşte linia iniţială numai dacă disocierea nu se poate face prin coloană.
Vom vedea mai târziu că SAN prevede şi disocierea prin faptul că numai una dintre mutările posibile este şi legală. De exemplu, dacă ne imaginăm că în contextul figurii de mai sus, turnul din c6 este legat (adică nu poate muta din cauză că ar lăsa propriul rege în şah) - atunci este suficientă notaţia Rc8, fiindcă numai unul dintre turnuri (cel din a8) poate muta legal pe c8.
În situaţia (mai rară) în care totuşi, nu se poate disocia între două mutări posibile nici prin indicarea coloanei şi nici prin indicarea liniei de start - nu rămâne altă soluţie decât precizarea completă a câmpului iniţial al piesei. Astfel, în figura alăturată toţi cei patru cai (câte doi pe o aceeaşi linie şi respectiv, coloană) pot muta pe câmpul c2 şi nu avem cum disocia mutările decât precizând complet câmpul iniţial: Na1c2.
Dacă mai ţinem seama de faptul că pe câmpul destinaţie poate să se afle o piesă adversă, astfel că efectul mutării este capturarea acesteia - rezultă că şablonul unei mutări de piesă poate fi constituit prin concatenarea următoarelor elemente:
/^[KQRBN] obligatoriu, primul caracter din şir este o "piesă"
[a-h]? coloana câmpului iniţial, dar numai în caz de ambiguitate
[1-8]? linia câmpului iniţial, dar numai dacă ambiguitatea persistă
x? efectul de "captură", dar numai dacă este cazul
[a-h][1-8]/ (obligatoriu) câmpul pe care se mută piesa
unde: /^ înseamnă "caută o potrivire cu şablonul, începând de la primul caracter" al şirului PGN; ? înseamnă zero apariţii sau, o singură apariţie a caracterului din stânga; iar litera x este consacrată în SAN pentru efectul de "captură".
Mutări de pion. Şablonul constituit mai sus va corespunde şi mutărilor obişnuite de pion, imediat ce ignorăm primul câmp (cel care indică piesa mutată) - iar pentru aceasta este suficient să-l sufixăm cu ? (zero apariţii [KQRBN] - deci pion; sau, una apariţie - deci piesă).
Astfel, e4 reprezintă mutarea unui pion pe câmpul e4; câmpul de start este lăsat spre deducere din contextul curent al jocului (dacă negrul a făcut mutarea, atunci câmpul iniţial nu poate fi decât e5; dacă albul a făcut mutarea, atunci câmpul iniţial poate fi e2 (e2-e4), sau poate fi e3 - ceea ce iarăşi se deduce din context).
Dar de data aceasta, câmpul al doilea din şablonul de mai sus (coloana câmpului iniţial) nu mai este opţional, în cazul unei capturi: în figura alăturată, pionul e2 poate să captureze calul f3 - dar SAN nu acceptă notaţia xf3 (care începe cu simbolul pentru "captură") şi impune exf3 (specificând şi coloana iniţială, ca şi când ar fi vorba de o ambiguitate - precum în cazul când ar exista şi pe g2 un pion alb).
Pentru cazul specific pionilor, al mutărilor de transformare, şablonul de mai sus trebuie completat. În figura alăturată, pionul alb poate fi transformat în [QRBN] fie înaintând c8=Q, fie capturând în stânga: cxb8=Q. În SAN = indică o mutare de transformare şi trebuie urmat de precizarea piesei în care promovează pionul: =[QRBN] - dar acest şablon (cu două caractere) trebuie adăugat ca opţional în şablonul (deja destul de general) constituit mai înainte.
Pentru a-l face "opţional", trebuie să grupăm cele două caractere şi să sufixăm acest grup cu ? (zero sau una apariţie, a grupului): (=[QRBN])?. Trebuie să mai ţinem seama de faptul că pentru grupul creat astfel se creează automat şi o referinţă numerică suplimentară, care păstrează secvenţa respectivă (RegExp.$1, ceea ce am folosit anterior); pentru a evita aceasta, putem folosi construcţia (?:secvenţă) care grupează fără să şi capteze, secvenţa respectivă: (?:\=?[QRBN])? (am folosit totuşi "\=", fiindcă "=" are un rol prestabilit, în special în legătură cu "?" - iar aici avem nevoie de caracter şi nu de "rol"). În plus, am prevăzut "=" ca opţional - permiţând astfel şi reprezentări "directe" ca c8Q (întâlnită în diverse fişiere PGN!), în loc de (cum se pretinde în mod standard) c8=Q.
Prin urmare, din cele de mai sus am avea deja această expresie regulată unitară:
/^[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:\=?[QRBN])?/
care permite recunoaşterea mutărilor de piese şi de pioni (inclusiv a mutărilor de transformare).
"Am avea" - pentru că există totuşi un defect, rezultat din faptul al doilea câmp (cel de "coloana iniţială") a rămas opţional (ori el este obligatoriu în cazul "mutare de pion cu captură", cum am văzut mai sus); ca urmare, 3e4 va fi recunoscută ca mutare de pion valabilă (deşi începe cu "linie iniţială", ceea ce este incorect pentru SAN). Dar de ce să nu lăsăm rezolvarea unui asemenea defect chiar în seama funcţiei de parcurgere a PGN-ului? (în loc de a strica generalitatea şablonului construit mai sus, tratând separat cazul particular "mutare de pion cu efect de captură").
Un singur tip de mutare, nu se poate încadra în acest şablon: mutarea de tip rocadă. Dar expresia regulată pentru rocadă este foarte simplă: /^O-O-O|^0-0-0|^O-O|^0-0/ - ţinând seama că rocada mică, respectiv cea mare se notează prin O-O şi O-O-O (în SAN, cu litera majusculă "O"), sau 0-0 şi 0-0-0 (notaţie nestandard, dar întâlnită - folosind cifra "0").
De remarcat că am precizat întâi "O-O-O" şi abia apoi, "O-O"; dacă am folosi /^O-O|^O-O-O/ atunci pentru "O-O-O" funcţia match() ar da ca rezultat un tablou de două potriviri: [O-O, O-O-O].
Folosind aceste două şabloane de mutare putem formula acum operaţia EXTRACT(mutare SAN) din algoritmul de analiză PGN prezentat mai sus; dar desigur, prezentăm formularea acestei operaţii în contextul unei schiţe de implementare completă, a algoritmului menţionat:
_extract_pgn: function() {
var PGN = this.element.val();
/* ... */ // extrage tagurile PGN, în this.tags{}
/* ... */ // extrage "comentariu iniţial" în this.in_comm
/* ... */ // elimină câmpul "rezultat" din finalul PGN
// acum PGN are numai mutări (numerotate, marcate NAG), comentarii şi variante
// EXTRACT() mutări, comentarii, variante - verificând sintaxa PGN
var thm = 0, // indexul curent al obiectului Move() în tabloul this.moves[]
san = "", // mutarea tocmai extrasă din PGN
/*...*/ // alte variabile (pentru NAG, variantă, comentariu)
ERR = false; // sintaxă greşită (ERR = true), în cursul traversării PGN
// şablonul mutărilor SAN şi şablonul pentru rocadă (stabilite mai sus)
var rg_move = /^[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:\=?[QRBN])?/,
rg_castle = /^O-O-O|^0-0-0|^O-O|^0-0/;
var mtch, tmp, len_crt = PGN.length; // lungimea curentă a şirului PGN
while ((len_crt > 0) && !ERR) {
PGN = PGN.replace(/^[ |\t]+/, ''); // elimină spaţiile iniţiale
// PGN-curent începe cu "număr de mutare"? (EXTRACT număr-mutare)
if (mtch = PGN.match(/^\d+\.{1,3}\s*/)) {
PGN = PGN.replace(mtch, ""); // elimină "număr-mutare" (afectând PGN-curent)
if (PGN.length == 0) {
/* eroare: PGN nu se poate termina cu "număr-mutare" */
}
}
// PGN-curent se potriveşte cu şablonul principal de mutare? (EXTRACT move)
if (mtch = PGN.match(rg_move)) {
// rezolvă defectul de şablon menţionat pentru "mutare de pion şi captură"
if (/^\d/.test(mtch)) { // nu-i corect "3e4" şi nici "3xf4"
this.errors = " SAN notation error for move " + mtch + ". ";
ERR = true; break;
}
tmp = new Move(); // înscrie SAN într-un obiect Move()
tmp.SAN = mtch; // şi adaugă acest obiect în tabloul this.moves[]
this.moves[thm++] = tmp;
PGN = PGN.replace(mtch, ""); // avansează în PGN, peste mutarea curent extrasă
PGN = PGN.replace(/^\s+/, ""); // elimină spaţiile rămase la începutul PGN
}
// PGN-curent se potriveşte la început cu şablonul de rocadă?
if (mtch = PGN.match(rg_castle)) {
tmp = new Move();
tmp.SAN = mtch;
this.moves[thm++] = tmp;
PGN = PGN.replace(mtch, "").replace(/^\s+/, "");
}
/* IF-uri pentru celelalte potriviri posibile (NAG, comentariu, variantă) */
/* Dacă nu s-a reuşit să se avanseze în şirul PGN (ajungând aici, lungimea PGN-curent
a rămas aceeaşi cu cea iniţială) - înseamnă că pe locul curent analizat avem un
caracter care nu poate fi recunoscut nici ca mutare, nici ca variantă, etc. */
if (len_crt == PGN.length) {
/* caracter invalid PGN */
ERR = true; break;
}
len_crt = PGN.length; // lungimea şirului PGN rămas
}
if (ERR) alert(this.errors);
},
Apelând this._extract_pgn(); în cursul metodei _init(), obţinem tablourile interne pentru tagurile PGN şi pentru mutările SAN din partida transmisă - putând atunci constitui conţinuturile diverselor diviziuni create în momentul instanţierii widget-ului.
Este drept că încă nu am menţionat o diviziune destinată să păstreze comentariile şi variantele; ea poate fi implicată adăugând în _create() '<div class="AnnoList"></div>' (imediat după linia pentru diviziunea .MoveList) şi setând cam aceleaşi proprietăţi CSS ca şi pentru diviziunea mutărilor (în principal, float: left - încât să alăturăm div.AnnoList la dreapta div.MoveList) - vezi (IX).
În principiu, aproape am terminat: mai trebuie făcută o legătură între mutările din .MoveList (înscrise aici din this.moves[], constituit mai sus de către _extract_pgn()) şi handlerele de navigare introduse anterior - "legătură" care să asigure că la click pe o mutare din listă (sau pe un buton de navigare) să fie înscrisă pe tablă poziţia rezultată în urma mutării respective.
Dar această "poziţia rezultată" înseamnă un şir FEN; ca să obţii acest FEN, trebuie sau să prelucrezi FEN-ul precedent, sau (mult mai simplu) să faci mutarea pe o tablă auxiliară (actualizată cu fiecare mutare); mai trebuie ca FEN-ul obţinut să fie unul corect, deci trebuie să te asiguri că mutarea este legală în poziţia respectivă - iar _extract_pgn() a verificat doar corectitudinea sintactică a mutărilor SAN (mutarea Ra1a3 poate fi corectă sintactic, dar poate fi ilegală dacă pe a2 există un pion alb).
Ajungem astfel la concluzia că este obligatoriu să implicăm şi un mecanism de verificare a legalităţii unei mutări (deci… chiar nu am terminat).
Am văzut mai înainte că o mutare SAN (din secţiunea de mutări a textului PGN al partidei) cuprinde - explicit sau implicit - aceste informaţii: tipul mutării (piesă, pion, rocadă), efectul mutării (captură, sau transformare) şi câmpul final; numai când este necesar (pentru a asigura unicitatea mutării, dacă ar exista mai multe mutări legale de tipul respectiv care au acelaşi câmp final)- se mai prevede o indicaţie minimală asupra câmpului iniţial (coloana acestuia, sau linia, sau chiar câmpul întreg - în această ordine şi în măsura în care este suficient pentru disociere).
axb6 este o mutare de pion cu captură pe câmpul final b6. Ţinând cont de regulile de mutare a pionilor, putem deduce de pe ce câmp a plecat pionul (obligatoriu, de pe a5) - dar nu putem deduce dacă s-a capturat o piesă - şi în acest caz, ce piesă - sau s-a capturat pionul advers b6, sau dacă nu cumva este vorba de o captură "en-passant" (a pionului advers tocmai trecut de pe b7 pe b5); nu putem deduce acestea, decât dacă vedem acea poziţie în care se face mutarea menţionată:
Acest exemplu arată că SAN nu ne poate permite să reconstituim poziţia precedentă (măcar pentru piesele din contextul mutării), decât dacă dispunem de informaţii asupra contextului anterior mutării - de exemplu, dacă păstrăm FEN-urile poziţiilor succesive de pe parcursul partidei (şi pentru aceasta, am instituit câmpul .FEN în fiecare obiect Move() din tabloul this.moves); având toate FEN-urile, putem asigura parcurgerea partidei (folosind handlerele de navigare pe care le-am constituit anterior) înainte sau înapoi, eventual şi "pe sărite" (nu neapărat în ordinea consecutivă, a mutărilor).
În (XI) tocmai am completat acele câmpuri care ţin de notaţie, în fiecare Move() din this.moves (câmpul .SAN, în care am extras mutarea curentă din textul PGN al partidei; câmpul .variant pentru variantele de joc indicate în PGN pentru mutarea respectivă; etc.) - dar nu şi câmpul .FEN menit să conţină şirul FEN corespunzător după executarea mutării; cum obţinem acum şi aceste FEN-uri?
Oricare ar fi prima mutare în poziţia iniţială standard, FEN-ul poziţiei rezultate este simplu de stabilit; la fel - după răspunsul negrului şi la fel poate pentru încă vreo câteva mutări următoare.
1. Ne2
Dar există poziţii pentru care cunoaştem FEN-ul, însă (numai pe baza acestui fapt) mutarea SAN curentă nu ne permite să mai scriem FEN-ul care ar rezulta! Astfel în poziţia alăturată, cunoscând numai FEN-ul poziţiei şi mutarea menţionată - nu putem stabili FEN-ul poziţiei rezultate, pentru că pe e2 poate veni atât calul din g1, cât şi calul din c3 (deci am obţine două FEN-uri rezultat, distincte).
Este drept, calul din c3 nu poate muta (ar lăsa regele în şah, ceea ce este ilegal) şi tocmai acest aspect a fost avut în vedere de SAN pentru a nota minimal "Ne2" în loc de "Nge2" - numai că acest aspect (care ţine de legalitatea mutării) nu poate fi dedus cunoscând numai FEN-ul iniţial şi însăşi mutarea SAN menţionată.
Deci pentru a stabili FEN-ul poziţiei este necesar uneori şi un mecanism pentru verificarea legalităţii mutării SAN curente (putând astfel să eliminăm FEN-ul care ar corespunde mutării calului c3, în figura de mai sus). SAU… ar mai fi o soluţie - să renunţăm la SAN, adoptând în schimb o notaţie care să fie independentă de context: Smith Notation (dar marea comunitate şahistă nu poate renunţa la SAN, fiind deja constituită o cantitate imensă de date bazate pe SAN).
Angajăm o reprezentare internă (fără nicio legătură directă cu infrastructura DOM şi CSS pe care am creat-o anterior) - să-i zicem generic, BOARD - care să păstreze (într-o anumită codificare), poziţia curentă, atributele de joc necesare (cine este la mutare, ce drepturi de rocadă mai există, etc.) şi totodată, să furnizeze lista tuturor mutărilor legale în poziţia respectivă.
Această nouă infrastructură ne va permite să reflectăm una după alta mutările înscrise deja în this.moves[]; dacă mutarea curentă ("tradusă" în prealabil din SAN, în codificarea adoptată de BOARD) se găseşte în lista mutărilor legale din BOARD-ul curent, atunci actualizăm corespunzător BOARD-ul şi calculăm şirul FEN pentru poziţia rezultată:
BOARD = set_BOARD( FEN_iniţial ); for( Move in this.moves[] ) { Bmove = convert( Move.SAN ); if( Bmove in BOARD.legal_moves[] ) { actualizează BOARD; Move.FEN = get_FEN(BOARD); } }
Desigur, avem aici o schemă generală de lucru; în realitate, "convert"() ar implica în mod firesc şi verificarea legalităţii mutării (nu ca în schema improvizată aici).
Ca bază pentru infrastructura BOARD vom folosi reprezentarea 0x88. Aceasta înseamnă în primul rând un tablou de 2*64 = 128 întregi, pe care o să-l numim x88Board[] şi care poate fi imaginat alăturând două table de şah (una "reală" şi una "imaginară"):
Folosim indecşi hexazecimali 0..7 pentru linii şi 0..F pentru cele 16 coloane. Alipind un index de linie şi unul de coloană (în această ordine), obţinem indexul obişnuit 00..7F (în notaţie hexazecimală) al unuia dintre cele 128 de elemente ale tabloului; astfel, colţurile au indecşii 0016 (câmpul 'a1'), 0F16 (dreapta-jos), 7F16 (dreapta-sus) şi 7016 (câmpul 'a8'). Dacă a doua cifră este restrânsă la domeniul 0..7, atunci indexul respectiv corespunde unui câmp al tablei "reale" (şi mai indicăm colţurile 0716 pentru 'h1' şi 7716 pentru 'h8').
Dat un câmp al tablei "reale" - ce index îi corespunde? De exemplu, pentru câmpul e4: fiind situat pe a patra linie, este precedat în x88Board[] de 3 linii a câte 16 elemente şi încă de cele 4 câmpuri existente pe linia a patra în stânga sa - deci indexul este 3*16 + 4 = 3*(10)16 + 4 = (34)16.
Mai general, dat un câmp "real" în notaţia obişnuită [a-h][1-8] - indexul său în x88Board[] este 16*(câmp.charCodeAt(1) - 49) + (câmp.charCodeAt(0) - 97), unde .charCodeAt(rang) este o metodă a obiectelor javaScript String() care dă codul caracterului al cărui rang s-a specificat (şi ţinem seama că '1' are codul ASCII 49, iar 'a' are codul 97).
Vom prefera "operaţii pe biţi": înmulţirea cu 16 = 24 echivalează cu deplasarea spre stânga cu 4 biţi, iar adunarea - cu "OR" pe biţi; calculul indicat mai sus revine la (row << 4) | col unde row este linia redusă 0..7 şi col este coloana 0..7 (redusă faţă de codul lui 'a') a câmpului.
Rangul liniei pe care se află câmpul de index dat este câtul împărţirii indexului prin 16 (fiindcă o linie are 16 elemente), deci se poate folosi deplasarea spre dreapta cu 4 biţi: row = index >> 4. Rangul coloanei pe care se află un câmp pe tabla "reală" este dat de restul împărţirii prin 8 a indexului acestuia, deci poate fi obţinut prin col = index & 7 ("AND" cu 7, restul fiind cel mult 7).
În x88Board[] - şi anume pe partea "reală" a sa - vom înscrie valori întregi, reprezentând piesele existente în poziţia curentă (valoarea 0 indicând "câmp liber"); partea "imaginară" pentru servi şi pentru a păstra diverse alte valori, dar ea serveşte în principal pentru simplificarea căutării mutărilor posibile: un câmp al cărui index dă o valoare nenulă când este mascat cu 0x88 este deja în partea imaginară şi deci mutarea pe acest câmp este imposibilă.
Într-adevăr - dacă indexul are a doua cifră 0..7 (deci indică un câmp din partea "reală"), atunci index & 0x88 = 0; dacă a doua cifră este 8..F (vizând în partea "imaginară"), atunci index & 0x88 > 0.
Pentru codificarea pieselor în BOARD instituim (în exteriorul widget-ului) variabila:
var PIECE_COD = {
'P': 2, 'p': 3,
'N': 4, 'n': 5,
'K': 6, 'k': 7,
'B': 8, 'b': 9,
'R': 10, 'r': 11,
'Q': 12, 'q': 13
};
Codurile n-au fost alese chiar la întâmplare; piesele albe având coduri pare, iar cele negre - impare, distincţia rezultă simplu prin mascarea bitului de rang zero (cod & 1 este 0 pentru piesă albă şi este 1 pentru negru); codul piesei omonime de cealaltă culoare, rezultă prin setarea sau resetarea bitului zero (cod |= 1 setează bitul zero, cod &= ~1 îl resetează, iar cod ^= 1 îl comută).
Tabloul x88Board[] îl definim intern, în metoda _init():
_init: function() {
this.tags = {}; // from the 'tag pairs' section of PGN
this.moves = []; // Move() objects, for each move from the 'movetext' PGN-section
/* ... */
this.x88Board = new Array(128); // 0x88 reprezentation
this.sq_king = [0, 0]; // the square occupied by the white/black king
/* ... */
},
Am prevăzut şi un tablou intern de 2 întregi this.sq_king[], pe care vom păstra indecşii câmpurilor ocupate de cei doi regi (poziţia regilor fiind esenţială pentru stabilirea legalităţii unei mutări).
Pentru exemplu, poziţia iniţială se va codifica precum în imaginea alăturată, prin tabloul de valori this.x88Board = [10, 4, 8, 12, 6, 8, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ..., 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 11, 5, 9, 13, 7, 9, 5, 11, 0, 0, 0, 0, 0, 0, 0, 0].
Aici, this.sq_king = [4, 0x74].
Prima problemă care se pune este obţinerea reprezentării x88Board[], dintr-un FEN dat şi apoi invers: cum obţinem şirul FEN corespunzător unei reprezentări BOARD date. Dar problema esenţială (care justifică introducerea infrastructurii BOARD) va consta în obţinerea listei mutărilor legale într-o poziţie dată (şi validarea mutării SAN tocmai preluate din textul PGN al partidei).
Un şir FEN conţine şase câmpuri, separate prin câte un spaţiu şi var recs = FEN.split(/\s+/) ne dă un tablou recs[] conţinând câmpurile respective.
recs[0] codifică poziţia pieselor, ca o secvenţă de 8 subşiruri separate prin '/', fiecare reprezentând poziţia pieselor (de la stânga spre dreapta) de pe câte o linie a tablei, începând de la linia 8 (primul subşir) şi încheind cu linia 1 (ultimul subşir); ne-am referit deja la acest câmp, începând din (III).
Tabloul x88Board[] indexează câmpurile tablei tot de la stânga spre dreapta pe fiecare linie, dar începând de la linia 1 şi încheind cu linia 8 (am dat un exemplu mai sus); deci vom inversa subşirurile respective în recs[0], folosind poziţie = recs[0].split(/\x2f/).reverse().join(''): separăm la '/' (de cod 0x2F), inversăm tabloul rezultat şi realipim liniile într-un şir "poziţie".
recs[1] este litera 'w', sau litera 'b' - indicând partea (alb, respectiv negru) care este la mutare în poziţia respectivă. În contextul BOARD vom institui variabila internă .to_move, cu valoarea 0 pentru 'w' şi 1 pentru 'b'.
recs[2] este caracterul '-' dacă în poziţia respectivă nici una dintre părţi nu mai are dreptul de a face vreo rocadă, sau este o secvenţă de caractere [KQkq] indicând ce tip de rocadă poate face fiecare parte în poziţia respectivă (dacă ar fi la mutare): [Kk] vizează rocada mică, iar [Qq] - rocada mare (majusculele corespund albului). Vom înfiinţa corespunzător variabila internă .castle, reflectând drepturile de rocadă pe primii patru biţi ai acestui întreg.
recs[3] este '-' dacă poziţia nu a rezultat în urma unei mutări de avansare cu două câmpuri a unui pion; altfel (în urma avansării unui pion cu două câmpuri) recs[3] este câmpul peste care a trecut pionul, numit câmp "en-passant". De exemplu, după mutarea b7-b5 am avea recs[3] = 'b6'; dacă există un pion alb pe un câmp vecin cu b5 (pe c5, sau pe a5), atunci acesta va putea captura pionul negru pe câmpul b6 (cxb6, respectiv axb6. Vezi imaginea de la începutul articolului).
Pentru recs[3] înfiinţăm .en_pass, care va păstra indexul câmpului peste care a trecut pionul, sau - dacă nu este cazul "en-passant" - punându-i ca valoare -1.
recs[4] este '0' (cifra zero) dacă mutarea care a condus la poziţia respectivă este o mutare de pion, sau dacă este o mutare cu efect de capturare; altfel, recs[4] este incrementat după fiecare mutare (a albului, sau a negrului) care nu este mutare de pion şi nici nu are ca efect o captură. Justificarea considerării acestui contor este dată de "regula celor 50 de mutări": dacă pe parcursul a 50 de mutări consecutive ("mutare" însemnând aici o pereche: mutarea albului şi răspunsul negrului) nu s-a făcut nici o captură şi nu s-a mişcat nici un pion - atunci partida este declarată "remiză".
În sfârşit, recs[5] este '1' în cazul poziţiei iniţiale standard (dar poate fi setat '1' şi pentru o poziţie iniţială setată prin tagul [FEN] în textul PGN) şi este incrementat după fiecare răspuns al negrului - reflectând numărul de perechi de mutări efectuate până la obţinerea poziţiei respective.
Vom folosi .fifty şi .nr_move pentru recs[4] şi recs[5]. În baza celor de mai sus, transformarea de la FEN la infrastructura BOARD poate decurge astfel:
/* Dat FEN, setează tabloul x88Board[] şi setează caracteristicile poziţiei */
_setBOARD: function(fen) {
var BOARD = this.x88Board;
for (var i = 0; i < 128; i++)
BOARD[i] = 0; // iniţial, toate câmpurile sunt făcute "libere"
var recs = fen.split(/\s+/); // tabloul celor 6 câmpuri ale FEN-ului
var board = recs[0].split(/\x2f/) // [Linia8, Linia7, ..., Linia1] (separând la '/')
.reverse() // [Linia1, Linia2, ..., Linia8]
.join(''); // poziţia, ca şir compatibil cu indexarea 'a1'..'h8'
var ofs = 0; // indexul curent în şirul de 64 de câmpuri 'a1'..'h8' (pe tabla 8x8)
var self = this;
board.replace(/[prbnqk]|[1-8]/ig,
function(x) { // caracterul de la indexul ofs (piesă sau "număr câmpuri libere")
if (x <= '8') ofs += x - 0; // "sare" peste câmpurile libere
else { // calculează indexul câmpului în tabloul BOARD[] (8 linii, 16 coloane):
/* linia câmpului pe tabla 8x8 = ofs/8; înmulţim apoi cu 2^4, fiindcă pe tabla
8x16 o linie are 16 câmpuri; apoi adăugăm rangul coloanei câmpului, ofs & 7 */
var sq = ( (ofs >> 3) << 4 ) | (ofs & 7);
BOARD[sq] = PIECE_COD[x];
if (x == 'K' || x == 'k') // salvează indecşii câmpurilor regilor
self.sq_king[PIECE_COD[x] & 1] = sq;
ofs++ // vizează următorul caracter
}
}); // .replace() a fost folosit aici numai pentru "efectul lateral"
this.to_move = (/^w$/i.test(recs[1])) ? 0 : 1; // 0/1 alb/negru
this.castle = 0; // 1|2 alb O-O|O-O-O; 4|16 negru O-O|O-O-O
this.en_pass = -1; // câmpul de "en-passant" (sau -1)
var roq = recs[2].split('');
for (var i = 0, n = roq.length; i < n; i++) {
switch (roq[i]) {
case 'K': this.castle |= 1; break; // O-O alb
case 'Q': this.castle |= 2; break; // O-O-O alb
case 'k': this.castle |= 4; break; // O-O negru
case 'q': this.castle |= 8 // O-O-O negru
}
}
var ep = recs[3] || "-";
if (ep != "-")
this.en_pass = (ep.charCodeAt(0) - 97) + 16 * (ep.charCodeAt(1) - 49);
this.fifty = recs[4] || 0; // mutări consecutive fără pion sau captură
this.nr_move = recs[5] || 1; // numărul de perechi de mutări
},
Metoda _init va apela întâi ._extract_pgn() - obţinând tablourile .tags[] şi .moves[] pentru tagurile şi mutările extrase din textul PGN prezentat; apoi, va stabili .FEN_initial (sau şirul din tagul [FEN] dacă există, sau FEN-ul poziţiei iniţiale standard) şi va lansa ._setBOARD(.FEN_initial) - definind infrastructura BOARD care va servi pentru parcurgerea secvenţială a mutărilor din this.moves[] în vederea stabilirii valorilor FEN corespunzătoare fiecăreia (stabilind implicit, legalitatea mutării).
Pentru a obţine şirul FEN corespunzător unei reprezentări BOARD date, introducem metoda:
/* Returnează FEN pentru x88Board[] şi caracteristicile curente de joc */
_getFEN: function() {
var position = [], BOARD = this.x88Board;
var PIECE_CHAR = [, , // nu avem piese de cod 0 sau de cod 1
'P', 'p', 'N', 'n', 'K', 'k', 'B', 'b', 'R', 'r', 'Q', 'q'
]; // dă litera FEN a piesei, pe baza codului 2..13 din BOARD
/* parcurge BOARD de la linia 8 la linia 1 şi de la stânga la dreapta
reţinând în 'position' "număr câmpuri libere" şi piesele întâlnite */
for (var row = 7; row >= 0; row--) {
var str = "", empty = 0;
for (var col = 0; col < 8; col++) {
var pc = BOARD[(row << 4) | col];
if (pc > 0) {
if (empty) str += empty;
empty = 0;
str += PIECE_CHAR[pc]
} else empty++
}
if (empty) str += empty;
position.push(str);
}
var fen = [position.join("/")]; // primul câmp FEN (poziţia pieselor)
fen[1] = this.to_move ? "b": "w"; // al doilea câmp FEN
var q = "";
if (this.castle) {
if (this.castle & 1) q += 'K';
if (this.castle & 2) q += 'Q';
if (this.castle & 4) q += 'k';
if (this.castle & 8) q += 'q'
} else q = "-";
fen[2] = q; // drepturile de rocadă
fen[3] = this.en_pass > 0 ? // stabileşte indexul câmpului de trecere
(String.fromCharCode(97 + (this.en_pass & 7)) // coloana
+ ((this.en_pass >> 4) + 1)) // şi linia câmpului de trecere
: "-"; // câmpul de "en-passant"
fen[4] = this.fifty; // regula celor 50 de mutări
fen[5] = this.to_move ? +this.nr_move + 1 : this.nr_move; // numărul de mutări
return fen.join(" ");
},
Desigur că dacă BOARD ar corespunde unei poziţii ilegale, atunci FEN-ul rezultat va fi şi el ilegal; dar aceasta nu se va întâmpla: la crearea sa, BOARD corespunde lui .FEN_initial (iar FEN-ul iniţial este în mod firesc, corect) iar după aceea, BOARD va fi actualizat intern după fiecare mutare angajând "generatorul de mutări legale" (pe care urmează să-l elaborăm mai departe) - încât tot timpul BOARD va corespunde unor poziţii de joc legale.
Pentru a genera lista mutărilor legale trebuie să vedem întâi cum se mută piesele în reprezentarea 0x88 adoptată aici (aceeaşi problemă se pune pentru orice altă reprezentare posibilă). O aplicaţie imediată (şi importantă, pentru generarea mutărilor legale) constă în realizarea unei funcţii care să indice dacă un anumit câmp este sau nu, atacat de partea adversă.
Dacă sq este indexul unui câmp de pe tabla "reală" a tabloului x88Board[], atunci sq+1 este indexul câmpului din dreapta, iar sq-1 al celui din stânga; sq+16 este indexul câmpului de deasupra, iar sq-16 al celui dedesubt:
Putem avea ca excepţie câmpurile marginale; de exemplu, pentru sq = 7716 (indexul câmpului 'h8') avem sq+1 = 7816 şi câmpul vizat este în afara tablei "reale". Amintim că sq & 0x88 este zero numai dacă sq vizează un câmp "real".
De exemplu - fiindcă nebunul se poate deplasa numai pe diagonale, indecşii câmpurilor pe care poate muta un nebun se vor obţine adunând ±15 sau ±17 la indexul câmpului curent şi putem scrie astfel o funcţie care să returneze tabloul acestor câmpuri:
function moveBishop(square) {
var STEP = [15, -15, 17, -17], to_sq = [];
for(var i = 0, n = STEP.length; i < n; i++)
for(var sq = square; !(sq & 0x88); sq += STEP[i])
to_sq.push(sq); // sq.toString(16)
return to_sq;
}; // câmpurile accesibile nebunului de pe 'square'
alert( moveBishop(0x36) );
Alăturat, am redat (în hexazecimal) rezultatul apelului moveBishop(0x36) - mutările posibile ale nebunului aflat pe câmpul g4 (de coordonate: linia 3 şi coloana 6); '*' marchează prelungirile diagonalelor în afara tablei "reale" (cu indecşi care stopează ciclul "for" interior, în funcţia redată).
În mod analog se pot scrie funcţii pentru mutările posibile ale calului, turnului, etc. Să observăm însă că tabloul furnizat va conţine şi câmpul 'square' pe care se află piesa (şi încă de atâtea ori câte valori are tabloul STEP) - fiindcă to_sq.push(sq) se execută începând cu var sq = square; (reluând pentru fiecare valoare STEP, când la iteraţia curentă se depăşesc limitele tablei "reale"). O corectare banală ar implica un test prealabil if(sq != square) to_sq.push(sq), care însă ar fi executat pentru fiecare sq; corectarea preferabilă se bazează pe reformularea ciclului interior folosind "while", în loc de "for" (cum vom vedea în funcţia _isAttacked(), mai jos).
Ţinând seama de regula evidenţiată mai sus pentru calculul indexului câmpului vecin (în sus, în jos, la stânga, la dreapta, sau diagonal), instituim următoarele variabile (în exteriorul widget-ului), care ne vor servi pentru gestionarea mutărilor pieselor:
// deplasamente de index în x88Board[] pentru mutările pieselor
var STEP_r = [-16, -1, 1, 16]; // turn
var STEP_b = [-17, -15, 15, 17]; // nebun
var STEP_k = [-17, -16, -15, -1, 1, 15, 16, 17]; // rege
var STEP_n = [-33, -31, -18, -14, 14, 18, 31, 33]; // cal
Pentru cazul damei - putem reuni STEP_r cu STEP_b (fiindcă dama se mişcă şi ca turn şi ca nebun). Pentru pion - nu-i cazul să considerăm un tablou "STEP", fiindcă mişcarea pionului depinde de culoare (cel alb avansează cu +16, cel negru cu -16), după cum depinde şi de linia lui (de pe linia 2, respectiv de pe linia 7 - poate avansa cu +16 sau cu +32, respectiv cu -16 sau -32), iar în cazul unei capturi diagonale, depinde şi de contextul "en-passant".
Am văzut mai sus cum putem determina tabloul câmpurilor pe care ar putea muta o piesă - altfel spus, tabloul câmpurilor atacate de piesa respectivă - dar numai în contextul fictiv al unei table pe care s-ar afla numai piesa respectivă. De fapt, o piesă nu poate muta pe un câmp ocupat de o piesă proprie; ciclul interior din funcţia redată mai sus ar trebui stopat nu numai când se ating limitele tablei "reale", dar şi dacă sq-curent este ocupat de o piesă proprie.
Mai mult, poziţia regelui propriu în corelaţie cu poziţiile pieselor adverse, poate determina noi restricţii asupra câmpurilor pe care poate muta o piesă sau alta; dacă prin mutarea respectivă, regele ar rămâne în şah (câmpul pe care se află fiind atacat direct de o piesă adversă), atunci mutarea devine imposibilă. În plus, regele nu poate muta pe un câmp atacat (direct, fără a avea o piesă "acoperitoare" pe traiectorie) de o piesă adversă şi deasemenea, pentru a efectua o rocadă - regele nu poate trece peste câmpuri atacate de către adversar.
Prin urmare, dacă vrem să determinăm mutările legale ale unei piese (într-un context real de joc, nu cel fictiv al funcţiei de mai sus) - trebuie să implicăm starea curentă x88Board[] (absent în "contextul fictiv" evocat) şi ne-ar trebui în general, o funcţie care să testeze dacă un anumit câmp este sau nu atacat de o piesă adversă:
/* este câmpul de index SQ atacat de o piesă a adversarului 'side'? */
_isAttacked: function(SQ, side) { // side = 0 / 1, pentru alb / negru
var sq1, dir, BOARD = this.x88Board;
var knight = 4, rook = 10, queen = 12, bishop = 8, king = 6; // alb
/* este SQ atacat de către un Pion advers? */
if (!side) { // SQ atacat de pion alb: de pe SQ-15, sau de pe SQ-17
sq1 = SQ - 15;
if (!(sq1 & 0x88) && (BOARD[sq1] == 2))
return 1;
sq1 -= 2;
if (!(sq1 & 0x88) && (BOARD[sq1] == 2))
return 1
} else { // SQ atacat de pion negru: de pe SQ+15, sau de pe SQ+17
sq1 = SQ + 15;
if (!(sq1 & 0x88) && (BOARD[sq1] == 3))
return 1;
sq1 += 2;
if (!(sq1 & 0x88) && (BOARD[sq1] == 3))
return 1;
knight++; rook++; queen++; // 'side' fiind în acest caz Albul,
bishop++; king++; // comută pe negru piesele atacatoare
}
/* este SQ atacat de un cal advers, sau de regele advers? */
for (dir = 0; dir < 8; dir++) {
sq1 = SQ + STEP_n[dir];
if (!(sq1 & 0x88) && (BOARD[sq1] == knight))
return 1;
sq1 = SQ + STEP_k[dir];
if (!(sq1 & 0x88) && (BOARD[sq1] == king))
return 1
}
/* este SQ atacat de turn, damă sau nebun advers? */
for (dir = 0; dir < 4; dir++) {
var step = STEP_r[dir];
sq1 = SQ + step;
while (!(sq1 & 0x88)) {
var p = BOARD[sq1];
if (p > 0) {
if ((p == rook) || (p == queen)) return 1;
break
}
sq1 += step
}
step = STEP_b[dir];
sq1 = SQ + step;
while (!(sq1 & 0x88)) {
var p = BOARD[sq1];
if (p > 0) {
if ((p == bishop) || (p == queen)) return 1;
break
}
sq1 += step
}
}
return 0 // câmpul SQ nu este atacat de către adversar
},
Ca exemplu, să presupunem că albul este la mutare (adică, în termenii pe care i-am introdus, avem this.to_move == 0) şi că nu a pierdut dreptul de a face rocada mică (adică avem this.castle & 1 !== 0); această mutare - notată SAN prin O-O - este legală dacă:
- regele nu este în şah: !this._isAttacked(4, 1) ('e1', de index 0416, nu este atacat de negru);
- câmpurile 'f1' şi 'g1' sunt libere: !(this.x88Board[5] || this.x88Board[6]);
- câmpurile 'f1' şi 'g1' nu sunt atacate: !(this._isAttacked(5, 1) || this._isAttacked(6, 1)).
Analog pentru O-O-O albă şi deasemenea, pentru cazul rocadelor negrului.
Încă un exemplu de folosire a funcţiei _isAttacked(), în cadrul "generatorului de mutări" pe care urmează să-l scriem: regele părţii care se află la mutare poate să mute pe câmpul liber SQ1, dacă !this._isAttacked(SQ1,this.to_move ^ 1), unde ^ este operatorul "XOR" (permiţând aici indicarea părţii adverse celeia indicate de .to_move).
În cadrul unui program care joacă şah este esenţial să avem o codificare internă eficientă, pentru mutări - fiindcă pentru a alege mutarea pe care să o joace, programul procedează în general astfel: generează lista mutărilor posibile în poziţia respectivă (şi eventual face o primă ordonare a ei, după un anumit criteriu valoric); apoi, ia pe rând fiecare dintre aceste mutări, o efectuează temporar pe o tablă de joc internă şi pentru poziţia rezultată, generează lista mutărilor posibile (răspunsurile adversarului, la mutarea proprie considerată), ş.a.m.d.
Se repetă aceşti paşi, pe o "adâncime" prestabilită, evaluând poziţiile rezultate şi reordonând valoric listele (arborescente) de mutări - încât după terminarea investigaţiei pe adâncimea stabilită, să se poată furniza ca răspuns mutarea care a obţinut cea mai mare valoare.
Într-un limbaj C este absolut firesc (şi eficient) să codificăm mutările prin valori de tip int - de exemplu, folosind primul octet pentru a înregistra indexul câmpului de start, al doilea octet pentru indexul câmpului final, etc.; ulterior, informaţiile necesare vor putea fi extrase din codul unei mutări folosind operaţii de deplasare, sau alte operaţii "pe biţi".
Pentru JavaScript (JS) această idee poate să fie una improprie, numerele fiind reprezentate similar tipului double din C. Operaţiile cu numere "întregi" implică o conversie internă prealabilă (de la "double" la "int" pe 32 de biţi) şi apoi una finală (de la "int" intern, la "double"); corectitudinea acestui mecanism de operare cu întregi de 32 de biţi (implicând conversii "double" - "int") decurge din faptul că "double" asigură înregistrarea exactă a întregilor de 32 de biţi (fără pierderea preciziei, specifică reprezentării în "virgulă mobilă").
Totuşi chiar şi în aceste condiţii (neavând o modelare specială pentru "număr întreg"), browserele moderne reuşesc să facă în mod eficient, operaţiile specifice cu numere întregi (fiind important şi pentru browser: DOM angajează şi tablouri de obiecte, iar indecşii tablourilor sunt numere întregi).
Şi la urma urmelor, browserele chiar folosesc o codificare perfect analogă celeia pe care o vrem pentru mutările de şah: modelul RGB, care codifică o culoare printr-un întreg în care primul octet corespunde proporţiei de roşu, al doilea - pentru verde, iar al treilea pentru negru - punându-se probleme precum compunerea unei culori din componente date, sau extragerea unei componente.
Pentru mutări adoptăm următoarea codificare, ca întregi pe 32 de biţi:
31 | 30..28 | 27..24 | 23..16 | 15..8 | 7..4 | 3..0 bit 31..0 0 | xxx | SPECIAL | FROM | TO | PIECE | CAPTURED valoare înscrisă
Într-un întreg pe 32 de biţi, bitul 31 indică totdeauna semnul valorii (fiind 1 pentru număr negativ); în contextul nostru, îl vom fixăm totdeauna pe zero - evitând astfel unele particularităţi legate de bitul "de semn", ale operaţiilor de deplasare (de exemplu, deplasarea spre dreapta conservă bitul de semn: alert((0x80000000 >> 1).toString(16)); a proba şi înlocuind ">>" cu ">>>").
Ignorăm aici următorii 3 biţi (de ranguri 30..28); într-un "chess-engine" (program care joacă şah) ei ar putea fi utilizaţi pentru a păstra anumite informaţii despre mutare în procesul de căutare a acelei mutări pe care să o joace programul, când acesta ar fi la mutare.
Pe cei 4 biţi 27..24 înregistrăm informaţii asupra unor cazuri "speciale" de mutare:
Valoare Semnificaţie 1 O-O (rocadă mică) 2 O-O-O (rocadă mare) 3 mutare reversibilă: NU-pion, NU-captură, NU-rocadă (incrementează contorul pentru "regula celor 50 de mutări") 6 mutare de pion normală (NU-transformare, NU-cu 2 câmpuri) 7 mutare de piesă (NU pion) cu efect de captură 0x0E avansează pion cu 2 câmpuri 0x0F pion capturează pion advers "en-passant" 4, 5, 8..13 codul piesei în care se transformă pionul (regele este fireşte, exclus)
Pe următorii doi octeţi înregistrăm indexul câmpului de start (FROM) şi respectiv, al câmpului final (TO), iar pe primul octet al mutării înregistrăm codul piesei mutate (pe biţii 7..4) şi codul piesei capturate (pe biţii 3..0) - "codul piesei" fiind una dintre valorile 2..13 asociate de variabila PIECE_COD celor 12 piese (vezi (XII)), sau fiind eventual valoarea 0 (indicând "fără captură").
Fie FROM indexul câmpului de start al mutării, iar TO indexul câmpului final - cu valori 0x00..0x7F şi astfel încât mascarea cu 0x88 dă zero (altfel câmpul ar fi înafara tablei "reale" din x88Board[]). Fie PIECE şi CAPT codul piesei mutate, respectiv capturate (când este cazul).
Ca să construim codul de 32 biţi al mutării trebuie să ţinem seama de următorul aspect: fiecare dintre valorile considerate ocupă în reprezentarea ca întreg primul octet (biţii 8..31 fiind zero); de exemplu, FROM = 0x73 (indexul câmpului 'd8') are ca reprezentare întreagă 0x00000073. Deci va trebui să deplasăm spre stânga cu un anumit număr de biţi aceste reprezentări - pentru FROM, TO, etc. - (încât primul octet să ajungă în poziţia specificată de codificarea adoptată mai sus pentru mutare) şi apoi să le "adunăm" (folosind operatorul pe biţi "OR").
Exemplificăm pentru mutarea negrului Qd8-h4 (negrul mută dama de pe d8 pe h4):
FROM: 0x00000073 FROM <<= 16: 0x00730000 FROM trebuie să ocupe al treilea octet TO: 0x00000037 TO <<= 8: 0x00003700 TO trebuie să ocupe al doilea octet PIECE: 0x0D: 0x0000000D codul damei negre PIECE <<= 4: 0x000000D0 PIECE trebuie să ocupe prima jumătate a octetului SPECIAL: 0x03 mutare "reversibilă" SPECIAL <<= 24: 0x03000000 SPECIAL ocupă al patrulea octet din codul mutării codul mutării: SPECIAL | FROM | TO | PIECE = 0x037337D0
Putem scrie următoarea funcţie pentru a construi codul binar al unei mutări:
function cod_mutare( from, to, piece, spec, capt ) {
return piece << 4 | from << 16 | to << 8 | spec << 24 | capt;
}
alert( cod_mutare(0x73, 0x37, 0x0D, 3).toString(16) );
Aici, am pus 'capt' (codul piesei capturate) ca ultimul, în lista parametrilor de apel: dacă nu este precizat la apelarea funcţiei (ca în cazul indicat în "alert"), atunci el va fi automat convertit la 0 când trebuie implicat în operaţii cu întregi.
Puteam formula şi aşa: return (FROM << 8 | TO) << 8 | piece << 4 | spec << 24 | capt (FROM este deplasat în al doilea octet şi i se alipeşte la dreapta TO; apoi aceşti 16 biţi sunt deplasaţi spre stânga cu 8 poziţii, astfel că FROM ajunge în al treilea octet, iar TO în al doilea).
Putem obţine orice câmp de biţi mascându-i pe ceilalţi; de exemplu, pentru FROM trebuie selectaţi numai biţii din al treilea octet - deci folosim "masca" 0x00FF0000: 0x037337D0 & 0x00FF0000 ne dă 0x00730000 (în care este "vizibil" numai câmpul FROM).
Dar rezultatul obţinut - folosind & cu o anumită mască - trebuie "corectat", mutând câmpul izolat astfel în primul octet; nu 0x00730000 este indexul FROM dorit, ci 0x00000073! Pentru aceasta, trebuie să deplasăm rezultatul spre dreapta, cu un anumit număr de biţi.
Aceste două operaţii - mascarea şi deplasarea - pot fi şi inversate: întâi deplasăm codul mutării spre dreapta cu 16 biţi (câmpul FROM ajunge astfel în primul octet) şi apoi mascăm cu 0xFF.
Putem scrie o funcţie care să returneze câmpurile din codul unei mutări, astfel:
function move_fields( move_cod ) {
var fields = {
'FROM' : (move_cod >> 16) & 0xFF,
'TO' : (move_cod >> 8) & 0xFF,
'SPEC' : (move_cod >> 24) & 0x0F,
'PIECE': (move_cod & 0xF0) >> 4,
'CAPT' : move_cod & 0x0F
};
return fields;
}
var fld = move_fields(0x037337D0);
Imaginea pe care am alăturat-o redă inspecţia variabilei "fld", folosind instrumentele browserului Chromium (valorile fiind redate zecimal). Desigur, puteam "alerta" direct rezultatul, adăugând de exemplu alert( fld.FROM +'\n'+ fld['TO'] +'\n'+ fld.SPEC +'\n'+ fld.PIECE +'\n'+ fld.CAPT ).
În treacăt fie spus, deprinderea folosirii instrumentelor de investigare oferite de browserele moderne poate să ne uşureze pe parcurs, punerea la punct a unui program de şah (folosind JS).
Urmează mai departe să ne ocupăm de generarea listei mutărilor legale (în codificarea specificată mai sus), pentru partea aflată la mutare într-o poziţie dată.
În (XIII) am introdus patru variabile STEP_* conţinând deplasamentele indexului mutărilor de turn, nebun, cal, sau rege. Pentru cazul când piesa nu este nominalizată - fiind o valoare posibilă a unei anumite variabile, setate în cursul execuţiei - prevedem şi variabila:
var STEP = {
6 : [-17, -16, -15, -1, 1, 15, 16, 17], // STEP_k
12 : [-17, -16, -15, -1, 1, 15, 16, 17], // deplasamentele damei
10 : [-16, -1, 1, 16], // STEP_r
8 : [-17, -15, 15, 17], // STEP_b
4 : [-33, -31, -18, -14, 14, 18, 31, 33] // STEP_n
/* adaugă eventual şi pentru piesele negre (7, 13, 11, 9, 5) */
};
Am considerat aici numai piesele albe; dacă Piece este piesa vizată curent, atunci STEP[ Piece - this.to_move ] este tabloul deplasamentelor corespunzătoare ei (.to_move este 0 pentru alb şi 1 pentru negru). Desigur, operaţia suplimentară necesară pentru acces (reducerea la codul piesei albe) ar fi evitată, dacă am extinde STEP[] şi pentru piesele negre (dar "câştigul" ar fi nesemnificativ).
Un "generator de mutări" se scrie de obicei compunând funcţii separate: una pentru mutările de pion, altele pentru generarea mutărilor pieselor - ba chiar separând şi după culoare (câte un generator pentru diversele categorii de mutări ale albului, respectiv ale negrului).
În schimb, metoda _gen_moves() pe care o vom reda aici este una "monolitică" (vezi PGN-browser); procedând însă didactic-intuitiv (şi regândind lucrurile), o vom reda aici "pe bucăţi" semnificative (folosind indentarea, pentru a sugera legătura dintre bucăţile de cod succesive). Astfel redată - şi cu atâtea comentarii - ea poate părea foarte lungă; însă în forma obişnuită ea are sub 150 de linii (sub 200, dacă adăugăm şi liniile de la _isAttacked(), pe care o invocă în cazul mutărilor de rocadă) - ceea ce este chiar onorabil, pentru un generator de mutări bazat pe reprezentarea 0x88.
Adăugăm în widget-ul pe care l-am dezvoltat în părţile anterioare ale acestui studiu, metoda:
/* Lista mutărilor posibile pentru partea la mutare în poziţia BOARD[] curentă */
_gen_moves: function(moves) {
var ocsq = []; // câmpurile ocupate de piesele părţii aflate la mutare
var /* scurtături către unele variabile interne */
to_move = this.to_move, castle = this.castle,
BOARD = this.x88Board, en_pass = this.en_pass;
for (var i = 0; i < 120; i++)
if ( !(i & 0x88) && // câmp al tablei "reale"
BOARD[i] && // ocupat de o piesă
((BOARD[i] & 1) == to_move)) // aparţinând părţii aflate la mutare
ocsq.push(i);
Variabilele interne this.castle, etc. pentru care am creat "scurtături" mai sus, au fost înfiinţate la iniţializarea infrastructurii BOARD (vezi metoda _setBOARD(), (XII)).
ocsq[] conţine indecşii câmpurilor ocupate de piesele părţii aflate la mutare (indecşii 120..127 vizează câmpuri dinafara tablei "reale", încât am limitat "for" până la 120).
Condiţia ((BOARD[i] & 1) == to_move)) poate fi scrisă mai simplu !(BOARD[i] & 1 ^ to_move).
Mai departe, în cadrul metodei iniţiate mai sus urmează: o secvenţă de cod pentru generarea mutărilor "rocadă" (pentru alb şi respectiv, pentru negru - la execuţie fiind aleasă aceea care corespunde valorii curente .to_move); o secvenţă de cod pentru generarea mutărilor posibile pentru pioni (pentru alb şi respectiv, pentru negru) şi apoi, cea care generează mutările pieselor.
Mutările vor fi înscrise în tabloul extern referit de parametrul de apel moves. Pentru codificarea mutărilor (ca întregi pe 32 biţi) - trebuie văzut (XIV).
this.castle indică drepturile de rocadă rămase, rezervând pentru alb primii doi biţi şi pentru negru - următorii doi biţi. Deci o rocadă albă este posibilă dacă (în primul rând) rezultatul mascării valorii castle cu 112 = 3 este nenul; analog pentru rocada neagră, masca fiind 11002 = 0x0C.
În al doilea rând - trebuie verificat că regele nu se află în şah, iar câmpurile dintre rege şi turn sunt libere şi sunt neatacate de către vreo piesă adversă (metoda _isAttacked() este redată în (XIII)):
/* Generează mutările "rocadă" pentru alb, respectiv pentru negru */
if (!to_move) { // Albul este la mutare; E1 = 4, G1 = 6, C1 = 2
if ((castle & 3) && !this._isAttacked(4, 1)) { // albul poate roca
if ( (castle & 1) && // O-O este posibilă
!( BOARD[5] || BOARD[6] // câmpuri libere între rege şi turn
|| this._isAttacked(5, 1) // şi neatacate de negru
|| this._isAttacked(6, 1)))
moves.push(0x01040660); // O-O este legală (pentru alb)
if ((castle & 2) && !( BOARD[3] || BOARD[2] || BOARD[1]
|| this._isAttacked(3, 1)
|| this._isAttacked(2, 1)))
moves.push(0x02040260); // O-O-O este legală
}
} else { // Negrul la mutare; E8 = 0x74, G8 = 0x76, C8 = 0x71
if ((castle & 0x0C) && !this._isAttacked(0x74, 0)) { // negrul poate roca
if ((castle & 4) && !( BOARD[0x75] || BOARD[0x76]
|| this._isAttacked(0x75, 0)
|| this._isAttacked(0x76, 0)))
moves.push(0x01747670); // O-O este legală (pentru negru)
if ((castle & 8) && !( BOARD[0x73] || BOARD[0x72] || BOARD[0x71]
|| this._isAttacked(0x73, 0)
|| this._isAttacked(0x72, 0)))
moves.push(0x02747270); // O-O-O este legală
}
}
"Riguros" vorbind (dar numai pentru acest moment al dezvoltării) - ne-a scăpat ceva: am verificat că 'e1' (respectiv 'e8') este neatacat de vreo piesă adversă, dar… nu am verificat şi faptul că pe 'e1' (respectiv, pe 'e8') există ca piesă chiar regele alb (respectiv, cel negru)…
Însă această verificare este deja acoperită de castle, care trebuie să indice drepturile de rocadă rămase; în cursul metodei _makeMove() (de care ne vom ocupa mai târziu), imediat ce regele sau un turn este mutat (inclusiv, printr-o rocadă), urmează şi modificarea corespunzătoare a lui castle (resetând biţii de rocadă ai părţii respective).
În principiu, variabilele interne considerate (this.castle, this.en-pass, etc.) sunt definite şi iniţializate în _setBOARD(), sunt actualizate în _makeMove() şi servesc (prin valorile actuale) pentru stabilirea legalităţii mutărilor, în cursul metodei _gen_moves().
Aceste variabile (tradiţionale în programele de şah) ar putea fi "reunite" într-un singur întreg de 32 biţi (castle de exemplu, măsoară doar 4 biţi!), urmând să separăm după caz câmpurile necesare…
Mai departe în _gen_moves() urmează să generăm şi celelalte mutări posibile:
/* Generează mutările de pion, apoi şi mutările pieselor */
for (var i = 0, np = ocsq.length; i < np; i++) {
var sq = ocsq[i];
var p = BOARD[sq];
Anume, urmează să generăm mutările posibile ale piesei p, aflate pe câmpul de index sq - repetând aceasta de-a lungul întregului tablou ocsq (care vizează piesele părţii aflate la mutare).
Avem de subliniat că generăm aici mutările posibile (nu neapărat şi legale). Secvenţa redată mai sus pentru generarea rocadelor asigură ea însăşi (folosind _isAttacked()), că rocadele generate sunt legale - dar pentru mutările pe care urmează să le generăm nu vom mai verifica dacă nu cumva regele propriu rămâne în şah, după efectuarea mutării (deci de fapt, mutarea este ilegală).
Această verificare este lăsată în sarcina metodei _makeMove() şi este mai bine aşa: proporţia mutărilor care ar lăsa regele în şah este de obicei, foarte mică în raport cu numărul de mutări posibile - încât este firesc să generăm rapid (fără a mai apela _isAttaked()) mutările posibile, urmând ca testarea finală de legalitate să se facă în momentul când mutarea respectivă ar trebui eventual să fie şi efectuată pe tablă (ceea ce se petrece în _makeMove()).
Generăm mutările posibile ale pionului în această ordine: mutările de avansare, eventual cu efect de transformare; apoi, mutările cu efect de captură laterală (inclusiv, "en-passant").
Redăm întâi (şi mai detaliat) cazul când albul este la mutare, piesa curentă p fiind un pion.
Dacă pionul nu a ajuns pe linia a 7-a (deci indexul câmpului său nu depăşeşte 0x60), atunci el poate avansa pe câmpul din faţă (dacă acesta este liber); iar dacă pionul se află pe linia 2 (adică nu a făcut anterior nici o mutare), atunci el poate avansa şi cu două câmpuri (dacă sunt libere):
if (p == (2 | to_move)) { // 2 pentru pion Alb, 3 pentru Negru
/* Mutările de pion ale părţii aflate la mutare */
if (!to_move) { /* Albul este la mutare */
if (sq < 0x60) { // pionul nu este pe linia 7
if (!BOARD[sq + 16]) { // câmpul din faţă este liber
moves.push(
0x06000020 | (((sq << 8) | (sq + 16)) << 8)) // d3-d4
}
if ( (sq < 0x18) && // pionul este pe linia 2
!BOARD[sq + 16] && // câmpul din faţă este liber şi
!BOARD[sq + 32]) { // câmpul din faţa acestuia este liber
moves.push(
0x0E000020 | (((sq << 8) | (sq + 32)) << 8)) // d2-d4
}
}
Acum, dacă pionul este pe linia a 7-a şi câmpul din faţa sa este liber - atunci pionul poate avansa şi urmează să fie transformat în damă, rege, nebun sau cal (şi adăugăm 4 mutări posibile):
else { // pionul este pe linia 7; pe câmpul din faţă avem 4 transformări
if (!BOARD[sq + 16]) { // câmpul din faţă este liber
var frto = ((sq << 8) | (sq + 16)) << 8; // combină FROM şi TO
moves.push(0x0C000020 | frto, // d7-d8 = Q (transformă în damă)
0x0A000020 | frto, // d7-d8 = R
0x08000020 | frto, // d7-d8 = B
0x04000020 | frto) // d7-d8 = N
}
}
Pentru capturile cu pionul (spre stânga şi spre dreapta) trebuie să verificăm întâi dacă nu se iese înafara tablei (de exemplu, un pion alb de pe coloana 'h' nu poate captura la dreapta).
Apoi, trebuie să vedem dacă nu cumva câmpul pe care se face captura este exact cel indicat în momentul respectiv de this.en-pass - aceasta ar însemna că mutarea precedentă (a adversarului) a fost înaintarea cu două câmpuri a unui pion negru aflat pe coloana pe care se face acum captura, deci avem de înscris ca posibilă mutarea "en-passant" corespunzătoare - şi observăm că nu este necesar să mai verificăm că pionul alb este pe linia 5 (fiindcă numai de pe linia 5, pionul alb poate accesa câmpul care tocmai a fost înscris în "en-pass"). Gestionarea în acest fel a variabilei .en-pass este realizată iarăşi prin _makeMove().
Dacă nu este cazul "en-passant", atunci trebuie să vedem dacă pe câmpul de captură se află o piesă neagră şi dacă este aşa - atunci adăugăm ca posibilă mutarea de captură corespunzatoare; desigur, daca pionul alb este pe linia a 7-a, atunci avem de adăugat chiar 4 mutări de captură, incluzând şi transformările posibile:
for (var j = 15; j <= 17; j += 2) { // captură la stânga şi la dreapta
var sq1 = sq + j;
if (! (sq1 & 0x88)) { // Nu iese înafara tablei "reale"
var p1 = BOARD[sq1];
if (this.en_pass == sq1) { // captură "en-passant" (d5xf6 ep)
moves.push(
0x0F000023 | (((sq << 8) | sq1) << 8))
} else if (p1 & 1) { // piesă sau pion Negru
if (sq >= 0x60) { // captură cu transformare (pe linia 8)
var frto = ((sq << 8) | sq1) << 8;
moves.push(0x0C000020 | frto | p1, // d7 x c8 = Q
0x0A000020 | frto | p1, // d7 x c8 = R
0x08000020 | frto | p1, // d7 x c8 = B
0x04000020 | frto | p1) // d7 x c8 = N
} else { // captură obişnuită (d3 x e4)
moves.push(
0x06000020 | (((sq << 8) | sq1) << 8) | p1)
}
}
}
} /* încheie generarea capturilor cu pion alb */
} /* încheie generarea mutărilor pionilor pentru Alb */
Pentru cazul când negrul este la mutare (şi p este un pion negru) avem o tratare similară, inversând sensul avansării pionului (acum deplasamentele vor fi negative); redăm şi această secvenţă, acum într-un singur bloc de cod:
else { /* Negrul mută (pion negru, p = 3) */
if (sq >= 0x20) { // pionul nu este pe linia 2
if (!BOARD[sq - 16]) { // câmpul din faţă este liber
moves.push(
0x06000030 | (((sq << 8) | (sq - 16)) << 8)) // d7-d6
}
if ( (sq >= 0x60) && // pionul este pe linia 7
!BOARD[sq - 16] && // câmpul din faţă este liber şi
!BOARD[sq - 32]) { // câmpul din faţa acestuia este liber
moves.push(
0x0E000030 | (((sq << 8) | (sq - 32)) << 8)) // d7-d5
}
} else { // pionul este pe linia 2; pe câmpul din faţă avem 4 transformări
if (!BOARD[sq - 16]) { // câmpul din faţă este liber
var frto = ((sq << 8) | (sq - 16)) << 8;
moves.push(0x0D000030 | frto, // d2-d1 = Q (damă neagră)
0x0B000030 | frto, // d2-d1 = R
0x09000030 | frto, // d2-d1 = B
0x05000030 | frto) // d1-d1 = N
}
}
for (var j = 15; j <= 17; j += 2) { // captură la stânga şi la dreapta
var sq1 = sq - j;
if (! (sq1 & 0x88)) {
var p1 = BOARD[sq1];
if (en_pass == sq1) { // captură "en-passant" (d4xe3 ep)
moves.push(0x0F000032 | (((sq << 8) | sq1) << 8))
} else if ((p1 > 0) && !(p1 & 1)) {
if (sq < 0x20) { // captură şi transformare
var frto = ((sq << 8) | sq1) << 8;
moves.push(
0x0D000030 | frto | p1, // d2 x c1 = Q (damă neagră)
0x0B000030 | frto | p1, // d2 x c1 = R
0x09000030 | frto | p1, // d2 x c1 = B
0x05000030 | frto | p1) // d2 x c1 = N
} else { // captură obişnuită (d6 x e5)
moves.push(
0x06000030 | (((sq << 8) | sq1) << 8) | p1)
}
}
}
} /* încheie generarea mutărilor pionilor pentru Negru */
} /* încheie generarea mutărilor de pion pentru Alb şi pentru Negru */
Desigur, ar fi cazul să şi testăm aceste secvenţe de cod; nu mai indicăm aici, cum anume am putea testa lucrurile în acest moment - mai târziu vom aborda direct şi chestiunea testării globale a generatorului de mutări ("mai târziu" pentru că avem nevoie şi de _makeMove(), întrucât testarea vizează mutările legale).
În cazul pieselor, codul este unitar şi mai scurt (fiindcă deplasamentele pieselor nu depind de culoare, spre deosebire de cele pentru pioni). Pentru fiecare dintre deplasamentele indicate de STEP[] pentru piesa respectivă, se testează dacă la acel deplasament corespunde un câmp liber şi în acest caz se generează mutarea (exceptând cazul când p este regele şi câmpul pe care ar fi mutat este atacat de adversar); iar dacă pe câmpul vizat prin deplasamentul respectiv se află o piesă adversă, atunci se generează mutarea cu captură corespunzătoare.
Dar după generarea acestei mutări, se trece imediat la următorul deplasament numai în cazul când p este cal sau rege; celelalte piese (nebun, turn sau damă) pot glisa mai departe pe traiectoria dată de deplasamentul curent, încât sunt generate şi mutările care ar rezulta angajându-l din nou pe acesta (încheind şi abia atunci trecând la următorul deplasament, când se ajunge la un câmp ocupat de o piesă proprie, sau când traiectoria curentă iese de pe tabla "reală"):
else { /* Mutări ale pieselor (p este acum o piesă, nu pion) */
var ofdir = STEP[p - to_move]; // deplasamentele acelei piese
for (var d = 0, nd = ofdir.length; d < nd; d++) {
var sq1 = sq;
for (;;) { // se va ieşi din ciclu prin 'break'
sq1 += ofdir[d]; // va continua pe direcţia respectivă,
// pentru Nebun, Turn, sau Damă
if (sq1 & 0x88) break; // TO este înafara tablei "reale"
if ((p == (6 | to_move)) // se mută regele pe un câmp atacat?
&& this._isAttacked(sq1, to_move ^ 1)) break;
var p1 = BOARD[sq1];
if (!p1) { // mută pe un câmp liber
moves.push(
0x03000000 | (((sq << 8) | sq1) << 8) | (p << 4))
} else {
if ((!to_move && (p1 & 1))
|| (to_move && !(p1 & 1))) { // mutare cu captură
moves.push(
0x07000000 | (((sq << 8) | sq1) << 8)
| ((p << 4) | p1));
break
} else break
}
if (!(p & 8)) break // p nu este Nebun, Turn, sau Damă
}
}
} // încheie generarea mutărilor pieselor
} // încheie ciclul for (var i = 0, np = ocsq.length; i < np; i++) {
}, // încheie _gen_moves()
Dacă ne-am apuca să testăm - ar trebui să rezulte maximum 8 mutări pentru cal sau rege şi maximum 27 de mutări posibile pentru o damă. Dar chiar vom face aceasta mai departe - imaginând o funcţie care să folosească _gen_moves() pentru a furniza pentru fiecare piesă sau pion, tabele conţinând câmpurile pe care poate muta piesa respectivă plecând din fiecare câmp al tablei.
De exemplu, tabela mutărilor nebunului va trebui să arate cam aşa:
var Bishop_Moves = {
'a1': ['b2', 'c3', 'd4', 'e5', 'f6', 'g7', 'h8'], // unde poate muta nebunul din 'a1'
// ş.a.m.d.
'c3': ['a1', 'b2', 'd4', 'e5', 'f6', 'g7', 'h8', 'e1', 'd2', 'b4', 'a5'],
// ş.a.m.d.
'h8': ['a1', 'b2', 'c3', 'd4', 'e5', 'f6', 'g7'] // unde poate muta nebunul din 'h8'
};
Iar tabelele obţinute astfel vor servi ulterior pentru simplificarea conversiei de la formatul SAN al mutării (din textul PGN al partidei), la forma binară internă specifică infrastructurii BOARD; după această conversie, mutarea va fi pasată metodei _makeMove(), care va încheia verificarea legalităţii mutării (şi va efectua mutarea pe BOARD[], dacă ea este legală).
Conversia între notaţia obişnuită a câmpurilor şi indecşii corespunzători acestora în .x88Board[] poate fi uşurată, dacă am folosi un tabel TO_x88{} în care (de la bun început) vom fi înregistrat asocierile respective. Va fi util şi un tabel ALL_MOVES{} în care, pentru fiecare piesă şi pentru fiecare câmp [a-h][1-8] - să avem (tot în notaţie obişnuită) lista câmpurilor pe care ar putea muta acea piesă, plecând de pe câmpul indicat.
Să zicem că tocmai am preluat din textul PGN al partidei, mutarea albului Ne2. Ştim câmpul final 'e2' şi piesa care trebuie mutată - un cal alb. Nu ştim câmpul iniţial al acestui cal - în schimb (vezi sintaxa SAN în (XI) şi (XII)) mai ştim că numai unul singur dintre caii albi, poate muta legal pe e2; prin urmare, putem proceda ca în acest pseudocod:
/* Reducerea posibilităţilor FROM-TO la aceea care este legală */ piece = 2; // codul calului alb TO = 'e2'; // câmpul final, indicat de SAN (Ne2) table_cal = ALL_MOVES['N'][TO]; // unde poate muta calul de pe TO = 'e2' for (i = 0; i < table_cal.length; i++) { FROM = table_cal[i]; // de exemplu: FROM = 'g1', sau FROM = 'c3' if (BOARD[TO_x88[FROM]] == piece) { // există Cal pe câmpul FROM? dacă mutarea FROM-TO este legală: // Nc3-e2 lasă regele în şah? actualizează x88Board[], conform mutării BREAK // şi treci la următoarea mutare din textul PGN } }
În loc de a căuta pe toată tabla, care cai albi pot muta pe 'e2' - am căutat numai pe cele maximum 8 câmpuri preînregistrate în ALL_MOVES['N']['e2'] (câmpurile "TO" pe care poate sări calul din 'e2', sau totuna - câmpurile "FROM" de unde ar putea veni pe 'e2'). Şi am folosit TO_x88{} pentru a transforma FROM din notaţia obişnuită, în index de acces pe BOARD[].
Desigur, am profitat de faptul că mutările calului (şi la fel pentru celelalte piese) sunt reversibile (după mutarea de pe FROM pe TO este posibil şi invers: de pe TO pe FROM); pentru mutările pionului (care sunt ireversibile) va trebui să procedăm altfel decât în pseudocodul de mai sus.
TO_x88{} se poate obţine direct (fără widget-ul pe care îl tot dezvoltăm de ceva vreme aici), încărcând în browser fişierul următor:
<!DOCTYPE html>
<head>
<script type="text/javascript" src="js/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="js/json2.js"></script>
<title>Tabel de conversie: notaţia obişnuită -> index BOARD[]</title>
</head>
<body>
<div id="to_x88"></div>
<script>
function ah18_x88() {
var a_h = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
var ah_88 = {}; // câmp [a-h][1-8] ==> index în BOARD[]
for(var c = 0; c < 8; c++)
for(var r = 1; r <= 8; r++) {
field = a_h[c] + r; // notaţia obişnuită [a-h][1-8]
// asociază indexul în BOARD[] al câmpului
ah_88[field] = (field.charCodeAt(0) - 97) +
(field.charCodeAt(1) - 49) * 16;
}
return ah_88;
}
$(function() {
ah_88 = ah18_x88();
$('#to_x88').html("var TO_x88 = " + JSON.stringify(ah_88));
});
</script>
</body>
Vizualizăm sursa generată de browser prin execuţia <script>-ului (în Firefox avem un meniu "View Generated Source") şi copiem conţinutul diviziunii "to_x88" (după o mică reformatare) în fişierul JS al widget-ului nostru (îl numisem mai demult, "brw-sah.js"):
var TO_x88 = {
'a1': 0, 'a2': 16, 'a3': 32, 'a4': 48, 'a5': 64, 'a6': 80, 'a7': 96, 'a8': 112,
'b1': 1, 'b2': 17, 'b3': 33, 'b4': 49, 'b5': 65, 'b6': 81, 'b7': 97, 'b8': 113,
'c1': 2, 'c2': 18, 'c3': 34, 'c4': 50, 'c5': 66, 'c6': 82, 'c7': 98, 'c8': 114,
'd1': 3, 'd2': 19, 'd3': 35, 'd4': 51, 'd5': 67, 'd6': 83, 'd7': 99, 'd8': 115,
'e1': 4, 'e2': 20, 'e3': 36, 'e4': 52, 'e5': 68, 'e6': 84, 'e7': 100, 'e8': 116,
'f1': 5, 'f2': 21, 'f3': 37, 'f4': 53, 'f5': 69, 'f6': 85, 'f7': 101, 'f8': 117,
'g1': 6, 'g2': 22, 'g3': 38, 'g4': 54, 'g5': 70, 'g6': 86, 'g7': 102, 'g8': 118,
'h1': 7, 'h2': 23, 'h3': 39, 'h4': 55, 'h5': 71, 'h6': 87, 'h7': 103, 'h8': 119
};
În fişierul redat mai sus am folosit JSON, pentru a obţine imediat (scutindu-ne de a scrie codul prin care se adaugă apostrofurile, două-puncte, virgulele, etc.) reprezentarea textuală a obiectului ah_88 constituit prin funcţia ah18_x88() - invocând metoda .stringify() a obiectului JSON. De fapt, JSON.stringify() este deja disponibil în unele browsere moderne (şi în acest caz se poate elimina linia din <head> prin care se încarcă "json2.js" în documentul redat mai sus).
Pentru calculul acestui tabel folosim metoda _gen_moves(), făcând astfel şi o primă testare a generatorului de mutări (vezi (XV)).
Vom proceda astfel: adăugăm (temporar!) widget-ului nostru o metodă publică .allMovesTable(), care constituie obiectul JS conţinând toate mutările pieselor şi îl înscrie (după serializarea lui, cu JSON.stringify()) în diviziunea de document al cărei identificator i-a fost transmis la apelare.
În .allMovesTable() prevedem întâi, şablonul var allMoves = {'N': {}, ...} pentru obiectul JS care trebuie completat; de exemplu allMoves['N']['e2'] va trebui să fie tabloul ['c1', 'c3', 'd4', 'f4', 'g3', 'g1'], conţinând câmpurile pe care poate sări calul aflat pe 'e2' (sau, la fel de valabil: al câmpurilor de unde calul poate veni pe 'e2').
Apoi, se şterg de pe x88Board[] toate piesele care există eventual în momentul apelării metodei; pe parcurs, se va aşeza pe tablă o singură piesă şi se vor genera mutările posibile ale ei - după care piesa va fi ştearsă, reluând pentru altă poziţionare a ei, sau pentru altă piesă.
Culoarea piesei nu contează - încât se alege albul, pentru piese şi pentru partea aflată la mutare; deasemenea, rocadele nu au sens în acest context - încât se resetează this.castle.
Să observăm însă că anularea drepturilor de rocadă este chiar obligatorie: dacă în momentul apelării valoarea .castle ar fi de exemplu 2 (adică albul poate face încă, mutările O-O şi O-O-O), atunci în toate listele de mutări ale piesei se vor adăuga şi câmpurile "TO" ale rocadelor 'g1', 'c1' - dat fiind că _gen_moves() generează toate mutările posibile pentru piesele de pe tablă ale părţii aflate la mutare şi pe de altă parte, rocadele sunt generate fără testarea prealabilă a prezenţei regilor pe câmpurile cuvenite (aceasta rezultând implicit din valoarea curentă .castle).
Mai departe, pentru fiecare piesă (dintre cele cinci existente) şi pentru fiecare câmp al tablei - se generează lista mutărilor (în codificarea binară pe care am introdus-o anterior) şi apoi se reţine din fiecare mutare numai câmpul "TO", constituind cu aceste valori (convertite la notaţia obişnuită) tabloul care se adaugă corespunzător în obiectul allMoves:
allMovesTable: function(id_dest) {
var allMoves = {
'N': {}, 'K': {}, 'B': {}, 'R': {}, 'Q': {}
}; // allMoves['N']['e2'] = ['c1', 'c3', 'd4', 'f4', 'g3', 'g1']
var BOARD = this.x88Board; // vom avea o singură piesă (albă)
for (var i = 0; i < 128; i++) BOARD[i] = 0;
this.to_move = 0; // fixăm Albul la mutare
this.castle = 0; // excludem rocadele (important!)
for(var p = 4; p <= 12; p += 2) { // N, K, B, R, Q (piesele albe)
var piece = PIECE_CHAR[p];
for(var fld in TO_x88) { // 'a1', 'a2', ..., 'h8'
BOARD[TO_x88[fld]] = p; // pune piesa p pe câmpul fld
var moves = []; // codurile mutărilor posibile ale piesei
this._gen_moves(moves);
var to_arr = []; // câmpurile TO din codurile mutărilor
for(var i = 0, n = moves.length; i < n; i++) {
var TO = (moves[i] >> 8) & 0xFF; // extrage indexul TO
to_arr.push(// transformă TO în notaţie obişnuită
String.fromCharCode(97 + (TO & 7)) + ((TO >> 4) + 1));
}
allMoves[piece][fld] = to_arr;
BOARD[TO_x88[fld]] = 0; // şterge piesa curentă de pe fld
}
}
$('#' + id_dest).html( // inserează în DOM, la #id_dest
JSON.stringify(allMoves, null, 2));
},
Am adăugat .allMovesTable() în cadrul widget-ului nostru numai în scopul de a obţine tabelul ALL_MOVES{}, de care vom avea nevoie mai încolo (şi eventual, în scopul testării generatorului de mutări); după ce vom fi obţinut acest tabel - putem şterge metoda redată mai sus.
Rămâne să invocăm această metodă dintr-un fişier HTML care instanţiază widget-ul nostru - de exemplu (rezumând la strictul necesar):
<!DOCTYPE html>
<head>
<script src="js/jquery-1.7.2.min.js"></script>
<script src="js/jquery.ui.core.min.js"></script>
<script src="js/jquery.ui.widget.min.js"></script>
<link href="css/pgnbrw.css" rel="stylesheet" type="text/css" />
<script src="js/pgnbrw.js"></script>
<title>Tabelul tuturor mutărilor pieselor</title>
</head>
<body>
<textarea id="txtPGN"></textarea> <!-- pentru instanţierea widget-ului -->
<div id="allMoves"></div> <!-- aici vom obţine tabelul tuturor mutărilor -->
<script>
$(function() {
$('#txtPGN').pgnbrw({field_size: 20, show_PGN: false, with_FEN: false})
.pgnbrw('allMovesTable', 'allMoves');
});
</script>
</body>
Cum se vede mai sus - am făcut unele modificări de denumiri faţă de (VII) (de când n-am mai redat un HTML pentru instanţierea widget-ului): am renunţat la denumirile "brw-sah.js", "brw-sah.css" (de-acum vor fi "pgnbrw.*") şi am redenumit widget-ul prin .pgnbrw(), în loc de ".fenBrowser()".
Nu derularea unei partide de şah ne interesează, încât am instanţiat widget-ul folosind opţiuni de anulare a unor anumite zone; după instanţiere, .pgnbrw('allMovesTable', 'allMoves') invocă metoda publică definită mai sus, indicându-i (ca parametru) diviziunea "allMoves".
Încărcând în browser, obţinem ceea ce redăm parţial alături. Dedesubtul tablei de şah avem textul tabelului care ne interesează; îl copiem din fereastra browserului într-un editor de text - în gedit. Dacă dorim (mai ales, pentru a verifica uşor mutările generate) putem formata textul respectiv folosind meniul "Search/Replace" (căutăm "}, " şi înlocuim cu "},\n\n", etc.).
În final - înscriem tabelul în "pgnbrw.js" (în zona variabilelor exterioare widget-ului):
var ALL_MOVES = {
'N': {
'a1': ['b3','c2'],
'b1': ['a3','c3','d2'],
'c1': ['a2','b3','d3','e2'],
// etc.
'g8': ['f6','e7','h6'],
'h8': ['g6','f7']
},
'K': {
'a1': ['a2','b2','b1'],
'b1': ['a2','b2','c2','a1','c1'],
//etc.
'h8': ['g8', 'g7', 'h7']
},
'B': {
'a1': ['b2','c3','d4','e5','f6','g7','h8'],
'b1': ['c2','d3','e4','f5','g6','h7','a2'],
// etc.
'h8': ['a1','b2','c3','d4','e5','f6','g7']
},
'R': {
'a1': ['b1','c1','d1','e1','f1','g1','h1','a2','a3','a4','a5','a6','a7','a8'],
//etc.
},
'Q': {
'a1': ['b1','c1','d1','e1','f1','g1','h1','a2','a3','a4','a5','a6','a7','a8',
'b2','c3','d4','e5','f6','g7','h8'],
//etc.
}
};
Avem de folosit acest tabel (în maniera pseudocodului formulat la început) în cursul metodei de validare a mutării curente din textul PGN al partidei: mutarea SAN respectivă ne dă câmpul TO, iar tabelul de mai sus ne furnizează câmpurile FROM posibile pentru acest TO; aceste posibilităţi "FROM-TO" sunt apoi reduse folosind _gen_moves() şi _makeMove(), la aceea care este legală.
Mutarea SAN curentă (din textul PGN al partidei) precizează cel mai adesea, numai piesa şi câmpul TO pe care aceasta trebuie mutată. Din tabelul precalculat ALL_MOVES{} putem extrage câmpurile posibile FROM, de pe care poate veni pe TO piesa respectivă; reţinem numai acele tablouri [FROM, TO] pentru care pe .x88Board[] la indexul corespunzător lui FROM, există o piesă precum cea indicată în SAN. Pentru fiecare dintre aceste tablouri [FROM, TO] (până când întâlnim pe cel al mutării legale) constituim codul binar parţial (numai cu părţile FROM şi TO) al mutării.
Codul mutării este transmis metodei _makeMove(); aceasta simulează mutarea şi verifică dacă regele n-a rămas cumva în şah; dacă mutarea este legală - actualizează x88Board[] şi variabilele interne asociate (returnând "true").
Dar acest demers este dependent de concepţia metodei _makeMove(): aceasta poate accepta ca parametru codul binar parţial (deducând informaţiile care lipsesc, prin investigaţii suplimentare în x88Board[]), sau poate pretinde codul binar complet - generat deja de gen_moves() - al mutării.
Noi alegem varianta în care _makeMove() primeşte ca parametru codul binar complet al mutării. Deci înainte de a apela _makeMove(), trebuie să completăm câmpurile care lipsesc din codul parţial existent; câmpurile PIECE şi CAPTURED (v. (XIV)) sunt uşor de aflat (conţinuturile celulelor din x88Board[] corespunzătoare pentru FROM şi TO), dar câmpul SPECIAL poate pretinde analiza suplimentară a unor cazuri - şi în fond, n-am face decât să repetăm investigaţii deja efectuate în cursul lui _gen_moves() (în principiu, _gen_moves() este apelată înainte de _makeMove(): un program care joacă va alege răspunsul din lista mutărilor posibile, furnizată de _gen_moves()).
Pentru "completarea" codului parţial existent, vom proceda principial: apelăm _gen_moves(), obţinând lista tuturor mutărilor posibile în poziţia respectivă; unul singur dintre codurile binare din această listă se va putea potrivi pe câmpurile FROM şi TO, codului parţial existent (fiind posibilă o singură mutare de pe câmpul FROM pe câmpul TO) - exceptând cazul unei mutări de transformare (când pentru a identifica mutarea, trebuie adăugată codului parţial şi precizarea piesei în care se transformă pionul - ceea ce putem prelua imediat din SAN-ul mutării).
Să aplicăm mecanismul descris mai sus pentru poziţia alăturată, în care albul este la mutare şi SAN indică mutarea "Ne2".
Câmpurile FROM posibile sunt cele accesibile unui cal din TO = 'e2' şi sunt date de ALL_MOVES['N']['e2'] = ['c1', 'c3', 'd4', 'f4', 'g3', 'g1'] - iar pentru poziţia noastră, rămâne FROM = 'c3' sau FROM = 'g1'. Să considerăm pe rând cele două mutări posibile.
Pentru mutarea c3-e2, codul parţial este: 0x2214 (indexul 0x88 al lui FROM='c3' este 0x22; cel pentru TO='e2' este 0x14).
Apelăm _gen_moves(), obţinând lista tuturor mutărilor posibile ale albului în poziţia respectivă (listă în care va trebui să identificăm codul parţial 0x2214):
0: 0x3040360 03 04 03 60: Ke1-d1 (cod-rege = 6, index('e1') = 4, index('d1') = 3) 1: 0x3040560 Ke1-f1 2: 0x3041360 Ke1-d2 3: 0x3041460 4: 0x3041560 5: 0x3061440 Ng1-e2 (cod-cal = 4, index('g1') = 6) 6: 0x3062540 7: 0x3062740 8: 0x3220140 Nc3-b1 9: 0x3220340 Nc3-d1 10: 0x3221040 11: 0x3221440 codul complet care se potriveşte cu cel parţial (Nc3-e2) 12: 0x3223040 13: 0x3223440 14: 0x3224140 Nc3-b5 15: 0x3224340
Lista are 16 elemente, ceea ce este corect: sunt posibile 5 mutări de rege, 3 ale calului 'g1' şi 8 ale celui din 'c3'. Dintre acestea, aceea care se potriveşte codului parţial existent este mutarea de la indexul 11 al listei - deci codul complet al mutării c3-e2 este 0x03221440.
_makeMove(0x03221440) va returna însă false (regele rămâne în şah). Trecem atunci la cea de-a doua mutare, cu FROM = 'g1' şi repetăm paşii de mai sus pentru acest caz: codul parţial pentru g1-e2 este 0x0614 şi este identificat la indexul 5 al listei de mai sus; iar de data aceasta, _makeMove(0x03221440) va returna true (şi va actualiza .x88Board[] corespunzător mutării).
Desigur, "repetând" paşii de mai sus pentru g1-e2, generăm a doua oară exact lista celor 16 mutări din cazul c3-e2. Va trebui să căutăm o formulare a mecanismului descris şi exemplificat mai sus, astfel încât _gen_moves() să nu fie apelată de două ori pentru o aceeaşi poziţie.
Aşa cum este formulată în (XV) - metoda _gen_moves(moves) constituie lista mutărilor posibile în tabloul extern referit prin parametrul "moves" (alternativa era: nu primeşte nici un parametru şi returnează tabloul de mutări constituit); aici avem de folosit generatorul de mutări numai pentru poziţia curentă (nu şi pentru răspunsurile posibile ale adversarului la fiecare mutare, ca în cazul unui program care joacă).
Iar acest tablou "moves" nu prezintă interes pentru o instanţă sau alta, a widget-ului (caz în care l-am defini în interior, cu this.moves = []); prin urmare este firesc să adăugăm şi acest tablou ca variabilă "externă" widget-ului (accesibil din interiorul dar nu şi din afara funcţiei care defineşte widget-ul): var bin_moves = []. Mai mult - este firesc acum să eliminăm parametrul "moves" (postat iniţial de dragul generalităţii) din definiţia anterioară pentru _gen_moves():
_gen_moves: function() { // moves) {
var moves = bin_moves = []; /* reiniţializează tabloul mutărilor binare */
/* restul codului rămâne nemodificat */
},
Următoarea metodă implementează aproape literal, mecanismul de stabilire a legalităţii mutării FROM-TO (codificate parţial) descris mai sus:
_legal: function(m) { // m este [FROM, TO]: ['e1', 'g1'] sau ['a7', 'a8Q'] etc.
var from = TO_x88[m[0]];
var prom; // piesa în care promovează pionul
if (m[1].length == 3) { // 'a8Q' - pionul promovează în damă
prom = m[1].charAt(2);
prom = PIECE_COD[prom] | this.to_move;
m[1] = m[1].replace(/.$/, ""); // reţine câmpul TO
}
var to = TO_x88[m[1]];
var move = ((from << 8) | to) << 8; // codul binar parţial (deplasat) al mutării
// caută codul binar complet pentru `move`
for(var i = 0, n = bin_moves.length; i < n; i++) {
var m = bin_moves[i];
if ((m & 0x00FFFF00) == move && (!prom || ((m >> 24) == prom)))
if (this._makeMove(m))
return true; // mutarea este legală (şi x88Board[] actualizat)
}
return false; // mutarea este ilegală (şi x88Board[] rămâne nemodificat)
},
Dacă foloseam pentru "codul binar parţial" var move = (from << 8) | to; (0x2214 în exemplificarea de mai sus pentru Ne2, şi nu 0x221400 cum am avea acum, datorită adăugării deplasării finale << 8) atunci în "if" trebuia ((m >> 8) & 0xFFFF) == move - adică pentru fiecare cod din bin_moves[] ar fi fost de făcut câte o deplasare suplimentară.
Tabloul [FROM, TO] primit este transformat în cod binar parţial de mutare, iar acesta este apoi căutat în tabloul bin_moves[]; dacă este identificat aici, atunci codul binar "complet" al mutării este pasat metodei _makeMove() - aceasta va verifica legalitatea mutării respective şi dacă este cazul, va actualiza infrastructura BOARD corespunzător efectuării mutării.
Rămâne să modelăm determinarea valorilor [FROM-posibil, TO] cu care să apelăm metoda _legal(), plecând de la mutarea SAN extrasă din textul PGN al partidei.
În metoda _san_legal() pe care o descriem în continuare, mai întâi obţinem în tabloul "global" bin_moves[], lista mutărilor posibile în poziţia x88Board[] curentă:
/* mutare SAN --> coduri parţiale [FROM, TO] --> mutarea legală corespunzătoare */
_san_legal: function(SAN) {
this._gen_moves(); // bin_moves[] = lista mutărilor posibile (codificate binar)
var m = ['', '']; // câmpurile FROM-posibil şi TO
SAN = String(SAN);
Am convertit parametrul "SAN" (cu care este apelată metoda) la String(), fiindcă vom avea de folosit asupra valorii respective, metode proprii obiectelor String(). În fond, vom avea de apelat această metodă pentru câmpurile .SAN ale obiectelor Move() înregistrate în this.moves[] - vezi (XI) - iar apelul this._san_legal( movs[i].SAN ) - unde movs este o referinţă la this.moves[] - nu transmite valoarea proprietăţii .SAN (un String()), ci o referinţă la această variabilă.
Desigur, puteam evita conversia SAN = String(SAN), dacă presupuneam apelarea prin ._san_legal( '' + movs[i].SAN + '' ) - care converteşte în mod implicit variabila movs[i].SAN la valoare String().
Cazul când SAN reprezintă o rocadă este cel mai simplu: FROM este câmpul iniţial al regelui ('e1', respectiv 'e8'), iar TO este câmpul (specific tipului de rocadă) pe care trebuie mutat regele (desigur, turnul implicat în rocadă va fi "manevrat" de _makeMove(), dacă rocada este legală):
if ((SAN == 'O-O-O') || (SAN == '0-0-0'))
return this.to_move ?
this._legal(['e8', 'c8']) : this._legal(['e1', 'c1']);
if ((SAN == 'O-O') || (SAN == '0-0'))
return this.to_move ?
this._legal(['e8', 'g8']) : this._legal(['e1', 'g1']);
Dacă SAN nu este rocadă, atunci poate fi o mutare al cărei aspect este redat de aceste exemple:
a8=Q sau a8Q pionul promovează în damă (sau turn, nebun, cal)
bxa8=Q sau bxa8Q capturează şi apoi promovează pionul
f4 mutare de pion (sau de pe f3, sau de pe f2)
gxf6 mutare de pion (eventual, "en-passant") cu efect de captură
Ne2 sau Nxe2 mutare de piesă, eventual şi captură (un singur cal poate muta legal pe e2)
Nge2 sau Ngxe2 mutare de piesă, eventual şi captură (mai mulţi cai poate muta legal pe e2, dar mută cel de pe coloana 'g')
N2f4 sau N2xf4 mutare de piesă, eventual şi captură (doi cai aflaţi pe o aceeaşi coloană pot muta legal pe f4, dar mută cel aflat pe linia 2 a coloanei respective)
Ng6f4 sau Ng6xf4 (doi cai aflaţi pe o aceeaşi linie şi doi cai aflaţi pe o aceeaşi coloană pot muta legal pe f4; dezambiguizarea nu se poate face decât indicând complet câmpul de start)
Analizăm SAN începând de la sfârşit: dacă SAN se încheie cu [QRNB], precedat eventual de "=" - atunci avem o transformare de pion şi reţinem piesa în care promovează pionul, după care ştergem din SAN secvenţa respectivă (încât SAN rămâne "a8", sau "bxa8" în primele două exemple):
var prom, mtch, piece;
if (mtch = SAN.match(/=?[QRBN]$/)) { // dacă este mutare de transformare ("a8=Q")
prom = SAN.charAt(SAN.length - 1); // reţine piesa în care promovează pionul
SAN = SAN.replace(mtch, "") // rămâne "a8" (câmpul TO)
}
Acum (după eliminarea secvenţei specifice transformării) SAN se termină în toate variantele ilustrate mai sus cu câmpul TO al mutării - îl reţinem în m[1], ştergându-l totodată din SAN; prin această ştergere, dacă SAN este o mutare de captură atunci la sfârşit rămâne caracterul "x" - îl eliminăm şi pe acesta, reţinând într-o variabilă booleană că avem "efect de captură":
m[1] = SAN.substr(SAN.length - 2, 2); // reţine din SAN câmpul TO
SAN = SAN.replace(/..$/, ""); // elimină TO din SAN
var capt = false; // indică efectul de captură
if (mtch = SAN.match(/x$/i)) { // mutare cu efect de captură?
capt = true;
SAN = SAN.replace(mtch, ""); // elimină "x" din SAN
}
Să constituim în m[0], câmpul FROM-posibil pentru cazul unei mutări de transformare; coloana acestuia este caracterul rămas în SAN în cazul când există captură, sau primul caracter din m[1] (unde pusesem câmpul TO) dacă nu există şi captură; linia este 2 pentru negru şi 7 pentru alb:
if (prom && capt) { // promovare de pion, cu efect de captură ("bxa8=Q")
m[0] = SAN.charAt(0) + (this.to_move ? '2': '7'); // (FROM = "b7")
m[1] += prom; // (= "a8Q")
return this._legal(m); // încheie prin _makeMove() dacă mutarea este legală
} else {
if (prom) { // promovare de pion fără efect de captură ("a8=Q")
m[0] = m[1].charAt(0) + (this.to_move ? '2': '7'); // (FROM = "a7")
m[1] += prom; // (= "a8Q")
return this._legal(m); // încheie prin _makeMove() dacă mutarea este legală
}
}
În plus, am adăugat la sfârşitul valorii m[1] şi litera corespunzătoare piesei în care a promovat pionul - de exemplu pentru SAN = "a8Q" va rezulta m = ["a7", "a8Q"], urmând ca _makeMove() (invocată din this._legal()) să verifice dacă prin mutarea de transformare respectivă regele propriu rămâne sau nu în şah (adică dacă mutarea este sau nu, legală).
În acest moment avem în m[1] câmpul TO al mutării, iar SAN este: fie şirul vid (de exemplu, dacă iniţial SAN = "f4", atunci TO = "f4" şi după ştergere avem SAN = ''); fie un şir de un caracter, reprezentând o piesă (iniţial SAN = "Ne2"; după ştergerea lui TO rămâne SAN = "N"), sau o coloană ori o linie (SAN-iniţial = "gxf6" şi după ştergerile de "x" şi de TO, rămâne SAN = "g"); fie un şir de trei caractere reprezentând o piesă şi câmpul FROM al acesteia (pentru SAN-iniţial = Ng6xf4, după ştergerile consecutive menţionate rămâne SAN = "Ng6").
În cazul când SAN-ul rămas începe cu litera [KQRBN] asociată unei piese, determinăm valorile posibile ale câmpului FROM folosind tabloul ALL_MOVES[] (ceea ce am explicat de la început):
// în acest punct, m[1]=TO şi SAN conţine eventual piesa şi coloana/linia/FROM
var field, table_moves, poss_from, BOARD = this.x88Board;
if (/^[KQRBN]/.test(SAN)) { // mutare de piesă (nu pion)
piece = SAN.charAt(0);
table_moves = ALL_MOVES[piece]; // tabelul tuturor mutărilor piesei
SAN = SAN.replace(/^./, ""); // şterge piesa (primul caracter) din SAN
if (/^[a-h][1-8]/.test(SAN)) { // când SAN-iniţial ar fi "Nf4g6" (SAN-rămas="f4")
m[0] = SAN;
return this._legal(m);
}
if (this.to_move) piece = piece.toLowerCase();
piece = PIECE_COD[piece];
poss_from = table_moves[m[1]]; // FROM-posibile = ALL_MOVES[piece][TO]
for(var i = 0, n = poss_from.length; i < n; i++) {
field = poss_from[i]; // un FROM posibil pentru mutarea `piece` pe TO
if(BOARD[TO_x88[field]] == piece &&
(!SAN || SAN == field.charAt(0) || SAN == field.charAt(1))) {
m[0] = field;
if(this._legal(m)) return true;
}
}
}
Ne-a rămas un singur caz: mutările de pion obişnuite (fără transformări). Pentru cazul pionilor, constituim în prealabil (în zona variabilelor "globale" pentru widget-ul nostru) două tabele similare cu tabelele ALL_MOVES[] pe care le-am prevăzut pentru piese:
var WHITE_PAWN_FROM = {
'a3': ['a2', 'b2'], // pe 'a3' pionul vine sau din 'a2', sau din 'b2'
'b3': ['b2', 'a2', 'c2'], // SAN = "1.b3", sau SAN = "1.axb3", sau SAN = "1.cxb3"
// etc.
'h3': ['h2', 'g2'],
'a4': ['a2', 'a3', 'b3'],
'b4': ['b2', 'b3', 'a3', 'c3'],
// etc.
'g7': ['g6', 'f6', 'h6'],
'h7': ['h6', 'g6']
};
var BLACK_PAWN_FROM = {
'a6': ['a7', 'b7'],
'b6': ['b7', 'a7', 'c7'], // SAN = "1...b6", sau SAN = "1...axb6", sau SAN = "1...cxb6"
// etc.
'g2': ['g3', 'f3', 'h3'],
'h2': ['h3', 'g3']
};
Pentru fiecare câmp al tablei, care poate fi destinaţia TO a unei mutări de pion (exceptând ultima linie - fiindcă mutările de transformare au fost tratate separat mai sus) tabelele precizează pentru alb şi respectiv pentru negru, care ar putea fi câmpurile FROM de pe care pionul poate fi mutat pe TO.
Folosind aceste tabele precalculate, putem trata cazul rămas în mod asemănător cazului mutărilor de piese de mai sus (şi scăpăm aşa, de problemele sâcâitoare pe care le-ar pune o tratare directă: găseşte coloana din stânga pionului, din dreapta, dacă există pe tabla "reală", etc.):
poss_from = this.to_move ? BLACK_PAWN_FROM[m[1]]: WHITE_PAWN_FROM[m[1]];
piece = this.to_move ? 3 : 2;
var c = capt ? SAN.charAt(0) : m[1].charAt(0);
for(var i = 0, n = poss_from.length; i < n; i++) {
field = poss_from[i];
if (BOARD[TO_x88[field]] == piece && field.charAt(0) == c) {
m[0] = field;
if(this._legal(m)) return true;
}
}
return 0 // dacă nu s-a încheiat până aici (printr-un "return" anterior),
// atunci SAN-ul transmis este complet ilegal pentru contextul x88Board[]-curent
}, /* încheie metoda _san_legal() */
_extract_pgn() a constituit tabloul this.moves[], completând toate câmpurile prevăzute pentru mutări cu excepţia câmpului .moves[i].FEN. Acum putem înscrie şi .FEN în fiecare obiect-mutare din moves[] - apelând _san_legal(moves[i].SAN) şi folosind ._getFEN() pentru poziţia x88Board[] actualizată odată cu verificarea legalitaţii mutării, de către _makeMove().
Legalitatea rocadei a fost stabilită complet chiar în momentul generării mutării, în _gen_moves(). Pentru celelalte mutări posibile este necesară finalizarea verificării legalităţii: dacă prin efectuarea mutării regele propriu ar ajunge în şah, sau dacă regele advers nu mai există (fiind capturat prin mutarea tocmai efectuată) - atunci mutarea este ilegală.
Iar pentru cazul când mutarea este legală, rămân de făcut actualizările necesare în x88Board[] şi în variabilele interne asociate poziţiei ("flagurile" poziţiei) în care se efectuează mutarea.
Metoda _makeMove() primeşte codul binar complet al mutării (vezi (XIV)); întâi se verifică dacă mutarea indicată capturează regele advers - caz în care se încheie (mutarea este ilegală):
/* Primeşte codul binar complet al mutării (4 octeţi, bit-31 = 0):
move = | 0xxx SPECIAL | FROM | TO | PIECE CAPTURED |
simulează mutarea pe x88Board[]:
mută PIECE FROM-TO capturând CAPTERED, ţinând cont de SPECIAL
dacă regele nu rămâne în şah (mutarea este legală):
actualizează x88Board[], .castle, .en_pass, .fifty, .to_move */
_makeMove: function( move ) {
var BOARD = this.x88Board, // scurtează accesul la .x88Board[]
to_move = this.to_move,
castle = this.castle;
var spec = (move >> 24) & 0x0F, // câmpul SPECIAL (4 biţi)
from = (move >> 16) & 0xFF, // câmpurile FROM şi TO
to = (move >> 8) & 0xFF;
var p = BOARD[from], // if( (p != PIECE) || (p1 != CAPTURED)) return 0;
p1 = BOARD[to];
if ((p1 == 6) || (p1 == 7)) return 0; // s-ar captura chiar regele advers (ilegal)
N-ar fi greu să revenim la _gen_moves() şi să excludem de la generare mutările care capturează chiar regele advers; dar ar trebui adăugat câte un test (p1 == rege) în mai multe locuri, iar astfel de mutări sunt totuşi rare, într-o poziţie dată.
Câmpurile PIECE şi CAPTURED nu sunt necesare aici! Le-am păstrat (în _gen_moves() şi în _makeMove()) fiindcă utilitatea lor apare totuşi, în contextul mai general al unui program care joacă şah.
Mai departe, dacă mutarea nu este "rocadă" - se simulează efectuarea mutării indicate (făcând-o efectiv pe x88Board[] - dar fără a actualiza şi flagurile poziţiei - şi retrăgând-o la sfârşitul verificării) şi dacă regele propriu rămâne în şah - atunci nu se întreprinde nici o modificare:
// simulează mutarea verificând că regele nu rămâne în şah, dar
// exceptează rocadele (au fost simulate şi verificate în cursul gen_moves())
if ((spec != 0x01) && (spec != 0x02)) { // exclude rocadele
BOARD[to] = p; // mută piesa p de pe FROM pe TO
BOARD[from] = 0; // (ignoră piesa în care ar promova pionul, dacă este cazul)
if (spec == 0x0F) // elimină pionul luat "en=passant", dacă este cazul
BOARD[to + (to_move ? 16 : -16)] = 0;
// câmpul pe care se află regele (chiar TO, dacă p este regele)
var sqk = (p == (6 | to_move)) ? to: this.sq_king[to_move];
if (this._isAttacked(sqk, to_move ^ 1)) {
BOARD[from] = p; // dacă regele rămâne în şah după mutare,
BOARD[to] = p1; // atunci reconstituie poziţia anterioară mutării
if (spec == 0x0F) { // (pune înapoi pionul luat "en-passant")
if (to_move) BOARD[to + 16] = 2;
else BOARD[to - 16] = 3
}
return 0; // mutarea este ilegală - încheie complet procesul
}
// regele nu rămâne în şah; încheie simularea (retrăgând mutarea simulată)
BOARD[from] = p;
BOARD[to] = p1;
}
Ajungând aici, mutarea este legală; în acest caz - se actualizează x88Board[] şi flagurile interne asociate (.castle, .to_move, etc.), corespunzător efectuării propriu-zise a mutării.
În cadrul unui program care joacă, înainte de a actualiza poziţia, trebuie salvate valorile specifice poziţiei curente - pentru ca după ce se obţine o evaluare a şansele părţilor în noua poziţie, să se poată reconstitui poziţia precedentă (în care se va încerca o altă mutare dintre cele posibile, evaluând din nou poziţia rezultată, revenind la poziţia precedentă şi repetând - până ce se obţin "suficiente" informaţii pentru a decide care este cel mai "bun" răspuns).
Mai întâi, resetăm .en_pass (altfel, când se va apela _gen_moves() pentru următoarea poziţie, este posibil să se genereze şi o mutare "en-passant" - deşi mutarea din acest moment nu este neapărat o avansare cu două linii a unui pion):
if (this.en_pass > 0) this.en_pass = -1; // resetează "en_pass" pentru mutarea curentă
Urmează actualizările necesare, în funcţie de valoarea câmpului SPECIAL din codul mutării. Dacă aceasta este una dintre cele pe care le-am convenit pentru mutările de rocadă:
switch (spec) { /* actualizări în funcţie de valoarea câmpului SPECIAL */
case 0x01: // O-O
if (!to_move) { // pentru alb
BOARD[5] = 10; // turnul (cod=10) din 'h1' (index=7) vine pe 'f1'
BOARD[6] = 6; // regele (cod=6) vine din 'e1' (index=4) pe 'g1'
BOARD[7] = BOARD[4] = 0 // şterge piesele din vechile poziţii
} else { // pentru negru
BOARD[0x75] = 11; // turnul (cod=11) din 'h8' (index=0x77) vine pe 'f8'
BOARD[0x76] = 7; // regele (cod=7) vine din 'e8' (index=0x74) pe 'g8'
BOARD[0x77] = BOARD[0x74] = 0
}
this.fifty++; // încă o mutare fără captură şi care nu angajează pioni
break;
case 0x02: // O-O-O,
if (!to_move) { // la alb
BOARD[3] = 10;
BOARD[2] = 6;
BOARD[0] = BOARD[4] = 0
} else { // la negru
BOARD[0x73] = 11;
BOARD[0x72] = 7;
BOARD[0x70] = BOARD[0x74] = 0
}
this.fifty++;
break;
Pentru cazul mutărilor reversibile (mutările de piese - nu de pioni - fără capturi) trebuie incrementat şi contorul pentru "regula celor 50 de mutări":
case 0x03: // mutare fără captură, fără pion (şi fără rocadă)
BOARD[to] = p; // pune piesa pe TO, o şterge de pe FROM
BOARD[from] = 0;
this.fifty++; // actualizează contorul celor 50 de mutări
break;
Pentru mutările de piesă cu efect de captură şi pentru mutările normale de pion (şi în general, pentru mutarile ireversibile) contorul pentru "regula celor 50 de mutări" trebuie resetat:
case 0x07: // captură cu o piesă (nu pion)
case 0x06: // mutare normală de pion (NU-enPassant, NU-cu 2 linii, NU-promovare)
BOARD[to] = p;
BOARD[from] = 0;
this.fifty = 0; // resetează contorul celor 50 de mutări
break;
Pentru toate mutările de transformare de pion:
case 0x0C: // transformare de pion alb în Q (damă)
case 0x0A:
case 0x08:
case 0x04:
case 0x0D: // transformare de pion negru în Q (damă)
case 0x0B:
case 0x09:
case 0x05:
BOARD[to] = spec; // pune pe TO piesa în care s-a transformat pionul
BOARD[from] = 0;
this.fifty = 0; // resetează contorul celor 50 de mutări
break;
Dacă mutarea anterioară (ultima făcută de adversar) a fost o avansare cu două linii a unui pion, atunci pentru acea mutare s-a înscris indexul câmpului de "en-passant" în this.en_pass; ca urmare, pentru mutarea curentă _gen_moves() a găsit this.en_pass > 0 şi atunci a setat în codul mutării câmpul de biţi SPECIAL = 0x0F.
Pentru mutările "en-passant"şi pentru mutările de avansare cu două linii a unui pion:
case 0x0F: // captură "en-passant"
if (!to_move) BOARD[to - 16] = 0; // şterge pionul advers luat "en-passant"
else BOARD[to + 16] = 0;
BOARD[to] = p; // pune pionul pe TO,
BOARD[from] = 0; // ştergându-l de FROM
this.fifty = 0; // resetează contorul celor 50 de mutări
break;
case 0x0E: // avansare cu două linii a unui pion nemişcat anterior
BOARD[to] = p;
BOARD[from] = 0;
this.en_pass = to_move ? // setează .en_pass =
to + 16 : to - 16; // indexul câmpului sărit de pion
this.fifty = 0; // resetează contorul celor 50 de mutări
break
} /* încheie actualizările impuse imediat de valoarea SPECIAL */
Afară de actualizările directe operate mai sus în funcţie de valoarea câmpului SPECIAL, mai avem de operat actualizarea drepturilor de rocadă - şi avem două cazuri; partea care mută regele sau turnul (inclusiv, printr-o rocadă) pierde (total sau numai parţial) drepturile de rocadă (dacă mai există):
/* când mută regele sau un turn, pierde din "drepturile de rocadă rămase" */
switch (p) { // p este piesa mutată
case 6: // regele alb
if (castle & 3) castle &= 0x0C; // resetează biţii rocadei albe (bit_0, _1)
this.sq_king[0] = to; // păstrează noua poziţie a regelui
break;
case 7: // regele negru
if (castle & 0x0C) castle &= 3; // resetează biţii rocadei negre (bit_2, _3)
this.sq_king[1] = to;
break;
case 10: // turn alb
if ((castle & 1) && (from == 7))
castle ^= 1; // anulează bitul de rocadă mică albă
else {
if ((castle & 2) && (from == 0))
castle ^= 2 // anulează bitul de rocadă mare albă
}
break;
case 11: // turn negru
if ((castle & 4) && (from == 0x77))
castle ^= 4; // anulează bitul de rocadă mică neagră
else {
if ((castle & 8) && (from == 0x70))
castle ^= 8 // anulează bitul de rocadă mare neagră
}
break
}
Iar dacă este cazul unei capturi de turn, atunci adversarul pierde "drepturile de rocadă rămase" pentru rocada corespunzătoare turnului capturat:
/* capturarea unui turn afectează drepturile de rocadă ale adversarului */
switch (p1) { // p1 este piesa capturată
case 10: // negrul capturează turnul alb din 'h1' sau din 'a1'
if ((castle & 1) && (to == 7))
castle ^= 1; // anulează bitul de rocadă mică albă
else {
if ((castle & 2) && (to == 0))
castle ^= 2 // anulează bitul de rocadă mare albă
}
break;
case 11: // albul capturează turnul negru din 'h8' sau din 'a8'
if ((castle & 4) && (to == 0x77))
castle ^= 4; // anulează bitul de rocadă mică neagră
else {
if ((castle & 8) && (to == 0x70))
castle ^= 8 // anulează bitul de rocadă mare neagră
}
break
}
În final, trebuie incrementat contorul perechilor de mutări în cazul când mutarea curentă este efectuată de către negru şi trebui comutat .to_move, indicând că urmează la mutare cealaltă parte:
if (to_move) this.nr_move++; // când mută negrul, actualizează contorul de mutări
to_move ^= 1; // urmează să mute cealaltă parte
this.to_move = to_move; // păstrează intern flagurile constituite
this.castle = castle;
return 1; // mutarea este legală (şi x88Board[] a fost actualizat conform acesteia)
}, /* încheie _makeMove() */
Desigur că pentru construirea unei funcţii precum cea redată mai sus, sunt necesare pe parcurs diverse testări; adevărul este însă că _gen_moves() şi _makeMove() au fost preluate aici (în PGN-browser) dintr-un program care joacă şah - anume din şahEng - iar testarea lucrurilor n-a mai fost necesară şi acum (fiind testate anterior, în cursul constituirii programului care joacă).
Ne propunem totuşi să imaginăm o manieră de testare - spre a ne convinge că lucrurile funcţionează corect - şi pentru cazul de faţă (când generarea mutărilor se face numai pentru poziţia curentă, nu şi pentru răspunsurile posibile ale partenerului - ca în cazul unui program care joacă).
Prin "poziţia curentă" înţelegem conţinutul tabloului intern this.x88Board[], împreună cu valorile variabilelor interne .castle, .en_pass, etc. ("flagurile" poziţiei) - existente la momentul respectiv.
Pentru poziţia curentă, _gen_moves() ne dă lista tuturor mutărilor posibile (unele pot fi ilegale), în tabloul global bin_moves[]; apelând _makeMove(), constatăm dacă mutarea este legală - caz în care se face şi actualizarea poziţiei curente.
Aceste metode funcţionează corect, dacă pentru fiecare poziţie curentă - bin_moves[] include toate mutările legale pentru acea poziţie. Putem testa corectitudinea lor, aplicându-le pe un set de poziţii pentru care cunoaştem numărul de mutări legale ce trebuie obţinut; desigur, poziţiile alese trebuie să permită verificarea tuturor categoriilor de mutări.
De exemplu, în poziţia iniţială standard albul are 20 de mutări legale: 4 mutări de cai (cel din 'g1' poate muta pe 'f3', sau pe 'h3') şi câte 8 mutări de avansare cu o linie, respectiv cu două linii, a unui pion. Dacă bin_moves[] conţine 20 de elemente (de data aceasta, nu vor fi generate decât mutări legale) atunci putem fi siguri că mutările obişnuite de pion sunt generate corect; dacă ar fi mai mult de 20 de elemente, atunci o listare a mutărilor generate ne va putea indica ce anume nu funcţionează corect.
În (XVI) am adăugat temporar widget-ului din "pgnbrw.js" metoda allMovesTable() prin care am obţinut tabelele ALL_MOVES[]. Putem folosi aceeaşi manieră de lucru pentru a adăuga temporar metoda publică perft(), prin care să obţinem numărul de mutări legale care au fost generate în bin_moves[] - fie pentru poziţia al cărei FEN este indicat în textul PGN transmis widget-ului, fie pentru poziţia al cărei şir FEN este furnizat ca parametru de apel:
perft: function(id_dest, fen) {
var fen = fen || this.tags['FEN'],
N = 0; // numărul mutărilor legale generate
this._setBOARD(fen); // poziţia iniţială, conform FEN-ului dat
this._gen_moves(); // bin_moves[] = lista mutărilor posibile
for(var i = 0, n = bin_moves.length; i < n; i++) {
var m = bin_moves[i];
if (this._makeMove(m)) { // dacă mutarea este şi legală
N++; // o contorizează
this._setBOARD(fen); // şi revine la poziţia iniţială
} // mutările respinse de _makeMove() sunt ignorate
}
$('#' + id_dest).html('<p>' + (this.to_move ? 'Negrul': 'Albul') +
' are <b>' + N + '</b> mutări legale</p>');
},
Perft ("performance test") este o subrutină de depanare obişnuită în programele care joacă şah (sau alte jocuri bazate pe "mutări"); aici nu performanţele ca timp de generare ne interesează (fiindcă vizăm numai poziţia curentă, nu şi pe cele derivate în urma răspunsurilor posibile ale adversarului) şi mai sus avem o formă particulară de "perft", în termenii constituiţi în pgnbrw.
Pentru a invoca metoda introdusă putem folosi un fişier HTML similar celui din (XVI) (şi cu aceeaşi secţiune de <head>, pe care n-o mai redăm aici):
<body>
<textarea id="txtPGN">
[White "M. Bezzel, 1848"]
[FEN "8/2R5/3N4/6R1/3BBN2/1Q6/3K3k/8 w "]
</textarea>
<div id="nr-legale"></div>
<script>
$(function() {
$('#txtPGN')
.pgnbrw({show_PGN: false, with_FEN:false})
.pgnbrw('perft', 'nr-legale');
});
</script>
<!-- .pgnbrw('perft',
'nr-legale',
'R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1 w ') -->
</body>
În <textarea> am înscris numai tagul [FEN] (lipseşte secţiunea mutărilor); dar pgnbrw acceptă şi PGN-uri cu asemenea "defecte" - aceasta fiind mai mult un avantaj, decât un "defect". Am adăugat şi un tag [White], în care am înscris numele autorului poziţiei.
Max Bezzel (care a creat şi celebra problemă a damelor - Eight_queens_puzzle) a pus problema de a amplasa toate piesele albe (exceptând pionii) astfel încât numărul de mutări posibile să fie maxim şi a arătat că sunt numai două astfel de poziţii - cea reprezentată mai sus şi aceea care provine din aceasta punând turnul din 'g5' pe 'a5' - iar numărul de mutări în această poziţie este 100.
Încărcând în browser fişierul HTML obţinem ceea ce am redat în imaginea de mai sus; numărul de mutări generate în bin_moves[] este într-adevăr 100 - ceea ce ar înseamna că generarea mutărilor obişnuite ale pieselor (fără capturi, fiindcă poziţia nu acoperă acest caz) este corectă.
În comentariul adăugat înainte de </body> am indicat apelarea metodei perft() pentru un FEN dat ca parametru; în poziţia respectivă numărul de mutări legale este cunoscut: 218.
Imaginea alăturată corespunde reîncărcării fişierului HTML în browser, după înlocuirea în <textarea> cu parametrul menţionat; constatăm că în bin_moves[] au fost generate într-adevăr, 218 mutări (nu-i cazul nici aici, să avem mutări "posibile" dar "ilegale"); iar de această dată, avem generate corect şi nişte mutări cu efect de captură.
Desigur, această poziţie a fost creată artificial, urmărind crearea unei poziţii legale în care să fie posibil un număr cât mai mare de mutări. Se pune atunci problema (de obicei, nebanală) de a dovedi că poziţia creată este legală (adică poate fi obţinută plecând de la cea iniţială standard); se vede uşor că dacă regele alb ar fi fost pus pe 'b3' în loc de 'f1', atunci poziţia ar fi ilegală: care a fost în acest caz ultima mutare, a negrului? - nu există nici una, pe care negrul să o fi putut face şi să rezulte poziţia în care acum albul mută. Pe când în poziţia redată, ultima mutare a negrului a fost b3-b2 (acoperind şahul de la dama din 'e5'); nu putea fi c3xb2: lipsind numai pionii albi (promovaţi în dame), negrul nu avea ce să captureze.
Desigur, putem adăuga pe tiparul de mai sus şi alte exemple - poziţii potrivite pentru testarea generării corecte a mutărilor de transformare, a acelora de luare "en-passant", a acelora în care apare problema legalităţii mutărilor pieselor "legate", a acelora care corespund diverselor situaţii în care rocada este restricţionată, etc.
Pare îndoielnic că am putea găsi o raţiune pentru a păstra metoda perft() în cadrul widget-ului pgnbrw (noi o inclusesem "temporar", pentru verificarea generatorului de mutări), având în vedere că scopul asumat de acesta este de a permite parcurgerea unei partide de şah (pe cine ar interesa "numărul de mutări legale"?). Dar nu este rea, ideea de a prevedea o opţiune suplimentară pentru aceasta, sau un buton (similar celui care inversează tabla) care la click să apeleze perft() şi să afişeze undeva numărul de mutări legale în poziţia curentă.
Ne-a rămas de formulat metoda vizată deja în (IX) prin _setMoveList() şi pe care o redenumim acum _setListDivs(); ea trebuie să completeze câmpurile FEN în obiectele Move() din tabloul this.moves[] (apelând _san_legal(SAN), cu SAN = this.moves[i].SAN) şi în acelaşi timp, să insereze în diviziunile documentului rezervate listei mutărilor şi comentariilor, link-uri şi handlere corespunzătoare - încât "click" pe mutare să determine actualizarea diagramei afişate.
_setListDivs() leagă între ele cele două infrastructuri pe care le-am creat în cadrul widget-ului: DOM şi CSS pe de o parte (care creează şi "afişează" în document diagrama de şah, butoanele şi handlerele de navigare, diviziunile pentru lista mutărilor, etc.) şi infrastructura pe care am denumit-o generic BOARD (servind pentru verificarea legalităţii mutărilor şi pentru constituirea şirurilor FEN pe care se bazează mecanismul nostru de navigare).
DOM-ul este constituit în cursul metodei _create() (ţinând cont de specificaţiile "pgnbrw.css"), în momentul instanţierii widget-ului pgnbrw() pe elementul <textarea> indicat. În cursul metodei _init() (apelată automat imediat după _create(), sau ulterior prin handler-ul butonului "LOAD") se analizează textul PGN din <textarea> - folosind _extract_pgn() - obţinând tabloul moves[] cu obiecte Move() reprezentând mutările extrase din PGN; apoi, se constituie o infrastructură BOARD - folosind _setBOARD() - şi în final, se apelează _setListDivs() care validează mutările din moves[] (completând şi câmpurile FEN) şi le inserează ca link-uri în diviziunea div.MoveList:
_init: function() {
this.tags = {}; // păstrează tagurile extrase din PGN (tags['White'], etc.)
this.moves = []; // obiecte Move() pentru mutările partidei
this.in_comm = ""; // comentariul iniţial (precede lista mutărilor în PGN)
this.errors = ""; // mesaje cumulate pe parcursul analizei textului PGN
this.x88Board = new Array(128); // reprezentarea 0x88 a tablei
this.sq_king = [0, 0]; // păstrează indecşii câmpurilor ocupate de regi
this._extract_pgn(); // analizează PGN, extrăgând this.tags, this.moves, etc.
// alert( JSON.stringify(this.moves, null, 2) );
this.FEN_initial = this.tags['FEN'] || FEN_STD;
this._setBOARD(this.FEN_initial); // setează x88Board[] şi "flagurile" poziţiei
this._firstMove(); // "afişează" poziţia iniţială (apelând _setDiagram(fen))
this._setListDivs(); // validează mutările din moves[], completând moves[i].FEN
// şi înscrie link-uri identificatoare în div.MoveList
if (this.k_Info && this.tags['White']) {
// completează div.GameInfo
};
this.brwtimer = null; // utilizat de _auto_nextMove()
},
Linia alert(JSON.stringify(this.moves)) (marcată "comentariu", în codul de mai sus) ne aminteşte că înregistrările din .moves[] sunt obiecte cu proprietăţile "SAN", "FEN", etc. (conform definiţiei Move(), redată în (XI)); selectăm una dintre înregistrările alertate:
{ "SAN": ["Rc3"], "FEN": "", "mark": "!", "NAG": 0, "variant": " White also wins in 5... Kc8 6. Rb4 Kd8 7. Rf4 Re1 ... ", "comment": " The rook moves to a bad rank." },
A rămas de completat câmpul "FEN", cu şirul FEN corespunzător poziţiei rezultate după ce mutarea indicată în "SAN" ar fi efectuată în poziţia curentă. Pentru aceasta, _setListDivs() va invoca _san_legal(SAN), unde SAN este valoarea câmpului "SAN" din mutarea curentă.
Dar să observăm că valoarea câmpului "SAN" nu este un şir, ci este un tablou [] care conţine şirul dorit; aceasta explică (abia acum!) de ce apăruse obligaţia de a converti parametrul SAN în obiect String(), în debutul metodei _san_legal(SAN) - vezi (XVII).
Dar de ce este "SAN" un tablou, când ar fi trebuit să fie un simplu şir? Explicaţia o găsim în cuprinsul metodei _extract_pgn() - cea care a creat înregistrările Move() din tabloul .moves[]:
/* din (XI), _extract_pgn() */
// PGN-curent se potriveşte la început cu şablonul de rocadă?
if (mtch = PGN.match(rg_castle)) {
tmp = new Move();
tmp.SAN = mtch; // este tablou, NU şir tmp.SAN = mtch[0]
this.moves[thm++] = tmp;
PGN = PGN.replace(mtch, "").replace(/^\s+/, "");
}
Metoda .match(şablon) returnează un tablou, conţinând acele subşiruri găsite în obiectul String() din care este apelată care se potrivesc cu şablonul indicat ca parametru; prin urmare, tmp.SAN = mtch conduce la înregistrări precum cea redată mai sus ("SAN": ["Rc3"]).
Punând însă tmp.SAN = mtch[0], vom obţine "SAN" ca şir ("SAN": "Rc3"); avem de făcut această mică îndreptare în trei locuri din cuprinsul metodei _extract_pgn() (de două ori pentru "SAN" şi o dată pentru .mark = mtch[0];) - după care putem elimina conversia supărătoare (devenită acum inutilă) SAN = String(SAN) de la începutul metodei _san_legal().
<div class="MoveList"> (la care ne referim prin div.MoveList) va trebui să conţină link-uri pentru mutări, iar div.AnnoList va conţine "adnotările" din textul PGN (variantele şi comentariile):
_setListDivs: function() {
var movs = this.moves, // tabloul obiectelor Move()
errors = false,
html = [], // va cumula ceea ce trebuie înscris în div.MoveList
in_st = [], // va cumula ceea ce trebuie înscris în diviziunea adnotărilor
mdiv = this.k_moves; // elementul DIV.MoveList prevăzut pentru lista mutărilor
if (this.in_comm) // comentariul iniţial, dacă există (de înscris în DIV.AnnoList)
in_st.push('<div class="ann-init">', this.in_comm, '</div>');
if (this.to_move) // numerotează prima mutare cu "N...", dacă mută negrul
html.push("<span class='numberOfMove'>", this.nr_move, "...</span>");
În pgnbrw.css avem de prevăzut eventual, anumite definiţii pentru aspectul diviziunilor create (.ann-init, .numberOfMove, apoi .oneMove, etc.); acestea nu conţin nimic special - doar specificaţii de font, de margini, de culoare. Am ţinut cont (în al doilea "if") de faptul că PGN poate conţine un tag [FEN "fen"] definind poziţia iniţială cu negrul la mutare şi cu this.nr_move = N mutări anterioare - caz în care numerotăm prima mutare sub forma "N...".
Mai departe, se parcurg obiectele din this.moves[]; se verifică - prin _san_legal() - dacă mutarea este legală şi în acest caz se determină - prin _getFEN() - şirul FEN pentru poziţia BOARD rezultată:
// parcurge obiectele Move() din this.moves[] (în ordinea mutărilor)
for(var i = 0, n = movs.length; i < n; i++) {
var move = movs[i].SAN;
if(this._san_legal(move)) { // dacă mutarea este legală, determină
var fen = this._getFEN(), // şirul FEN al poziţiei rezultate
w_to_move = (/ w /.test(fen));
var mark = movs[i].mark, // scurtează accesul la proprietăţile din Move()
nag = movs[i].NAG,
variant = movs[i].variant,
comm = movs[i].comment;
movs[i].FEN = fen; // completează câmpul FEN din obiectul Move() curent
Pentru mutarea curentă - fie pentru exemplu i=5, cu SAN="Nge2" - creează un link de forma
<a href="" chessMoveId="5">Nge2</a>, în care atributul chessMoveId are ca valoare indexul obiectului Move() din this.moves[]:
// creează un link cu atributul "chessMoveId" = indexul mutării
if (!w_to_move) // precede mutările albului cu "numărul mutării"
html.push("<span class='numberOfMove'>", this.nr_move, ".</span>");
html.push("<a href='' class='oneMove' chessMoveId='", i, "'>",
move, "<span class='moveMark'>", mark, "</span></a>");
html.push((w_to_move ? "<br>": " "));
Dacă mutarea are "adnotări", acestea sunt înscrise într-un anumit format în div.AnnoList. După încheierea parcurgerii obiectelor Move(), sau după întâlnirea eventuală a unei mutări pentru care _san_legal(move) returnează "false" - se inserează în document elementele HTML create:
if (nag || variant || comm) { // există adnotări pentru mutarea curentă
var nrm = w_to_move ? (this.nr_move - 1 + '...')
: this.nr_move + '.';
in_st.push('<div class="ann-crt"><p>', nrm,
"<a href='' chessMoveId='", i, "'>",
move, mark, "</a></p>");
if (nag) in_st.push('<div class="ann-nag">', NAG[nag], '</div>');
if (variant) in_st.push('<div class="ann-variant">',
variant, '</div>');
if (comm) in_st.push('<div class="ann-comment">', comm, '</div>');
in_st.push('</div>');
}
} else { errors = true; break; }
} // încheie parcurgerea obiectelor din this.moves[]
mdiv.html(html.join('')); // inserează link-urile create în div.MoveList
this.k_annot.html(in_st.join('')); // inserează adnotările
În final, montăm un handler de click pentru div.MoveList şi unul pentru div.AnnoList, care identifică elementul <a> de unde s-a propagat evenimentul "click" şi îl transmite metodei _go_to_pos() - aceasta va identifica obiectul Move() corespunzător pe baza valorii atributului chessMoveId din link-ul primit şi va folosi câmpul .FEN din acest obiect pentru a actualiza afişarea diagramei:
// handlere de click pentru lista mutărilor şi lista adnotărilor
var self = this; // salvează o referinţă la instanţa curentă (pentru a putea
// invoca metodele proprii din cadrul handlerelor)
this.k_moves.click(function(event) { // click în div.MoveList
var tgt = $(event.target);
if (tgt.is('a'))
self._go_to_pos(tgt); // poziţia afişată va fi actualizată corespunzător
return false; // mutării din div.MoveList de unde provine click-ul
});
this.k_annot.click(function(event) { // click în div.AnnoList
var tgt = $(event.target);
if (tgt.is('a')) {
self.k_moves.scrollTop(0); // scroll-ează div.MoveList, la prima mutare
self._go_to_pos(tgt, true); // div.MoveList va fi derulată la mutarea curentă
}
return false;
});
if (errors) alert("Notaţie greşită la o mutare din PGN (vezi " +
this.nr_move + ")");
}, // încheie metoda _setListDivs()
Dacă se întâlneşte Move() pentru care _san_legal() returnează "false", atunci parcurgerea tabloului .moves[] se opreşte şi se alertează un mesaj de eroare, indicând numărul mutării respective.
Metoda _go_to_pos() (folosită de handlerele de navigare) a fost redată în (IX), dar în forma cea mai simplă; ea trebuie rescrisă pentru a implica şi derularea listei mutărilor şi a listei adnotărilor - încât mutarea curentă să fie vizibilă în toate situaţiile de provenienţă a "click"-ului (de pe butoanele de navigare, din lista mutărilor, sau a adnotărilor) care a determinat actualizarea afişării poziţiei.
vezi Cărţile mele (de programare)