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

Modelarea tablei şi jocului de şah (XX)

FEN | JSON | PGN | SAN | handler | jQuery | javaScript | widget
2012 aug

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

O îndreptare conjuncturală

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().

Construcţia listei mutărilor

<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>": "&nbsp;&nbsp;"));

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)

docerpro | Prev | Next