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

Modelarea tablei şi jocului de şah (IX)

CSS | FEN | handler | jQuery | javaScript
2012 jun

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ă.

Principiile integrării listei de mutări

Î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.

Crearea diviziunii mutărilor şi a barei de navigare

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".

Crearea butoanelor de navigare

Î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'>&#9683;</span>&nbsp;&nbsp;&nbsp;&nbsp;",
"<img src='images/first.png' alt='' title='First'></img>&nbsp;&nbsp;",
"<img src='images/prev.png' alt='' title='Prev'></img>&nbsp;&nbsp;",
"<img src='images/next.png' alt='' title='Next'></img>&nbsp;&nbsp;",
"<img src='images/last.png' alt='' title='Last'></img>&nbsp;&nbsp;&nbsp;&nbsp;",
"<span title='more Next'>&#8862;</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.

Crearea unui handler pentru navigare

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 ? "&#9682;": "&#9683;"); // ◒ 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.

Constituirea temporară a unei liste de mutări

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, 
                       '&nbsp;&nbsp;');
            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.

Conturarea metodelor de navigare

Î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.

vezi Cărţile mele (de programare)

docerpro | Prev | Next