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

Modelarea tablei şi jocului de şah (IV)

CSS | FEN | jQuery | javaScript | widget
2012 jun

Începe să fie clar ce înseamnă "dezvoltare inductivă: am ajuns tocmai la "partea a IV-a" şi încă n-am finalizat nimic!

Să "finalizăm" deocamdată ceea ce am început în (III): trebuie "citit" şi validat FEN-ul introdus de utilizator şi trebuie înscrisă poziţia respectivă pe tabla de şah.

Maparea FEN-ului 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).

vezi Cărţile mele (de programare)

docerpro | Prev | Next