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

Trunchierea tablei de şah

CSS | FEN | SAN | jQuery | widget
2019 dec

Vizualizări conjuncturale

Dacă ai de lămurit în scris (folosind HTML, sau LaTeX) diverse aspecte privitoare la un program de şah, nu ai nevoie neapărat să reprezinţi întreaga tablă de şah – poate fi suficient să vizualizezi doar o anumită porţiune compactă a acesteia:

În prima coloană am ilustrat faptul că SAN este o notaţie minimală. 1.axb6 desemnează mutarea pionului de pe coloana 'a' pe câmpul b6, cu efect de captură; câmpul iniţial nu poate fi decât a5, dar ce piesă este capturată nu se poate deduce decât din contextul în care este făcută mutarea: poate fi o figură neagră (un cal, un nebun, turn sau damă), un pion negru care se afla pe b6, sau în cazul când ultima mutare a negrului a fost b7-b5 - pionul b5 (capturat en-passant). Moda (mai în toate programele destinate amatorilor) este să adaugi pe diagramă şi săgeţi, cât mai frumos colorate (de la a5 la b6 şi de la b7 la b5) – ceea ce este chiar oribil (şi jignitor).

În a doua coloană, 1.Ne2 indică doar câmpul destinaţie şi deducem câmpul de plecare g1 numai ţinând seama de context (calul c3 este „legat”). Într-un program de şah există de obicei proceduri separate, pentru generarea mutărilor posibile într-o poziţie dată şi respectiv, pentru stabilirea legalităţii mutării (ambele mutări de cai sunt posibile, dar numai 1.Ng1-e2 este legală – iar notaţia SAN 'Ne2' implică acest aspect); se procedează aşa pentru a permite analiza pe o anumită adâncime a mutărilor şi a răspunsurilor posibile de fiecare dată – în general, proporţia mutărilor ilegale este mică şi amânând tratarea legalităţii, se sporeşte foarte mult viteza de generare a mutărilor posibile; după o primă ierarhizare valorică a mutărilor posibile (care implică şi o anumită reducere a arborelui mutărilor de analizat mai departe), urmează angajarea fiecăreia şi abia atunci se verifică şi legalitatea acesteia.

Iar în a treia coloană avem „rocada mare” – pentru care stabilirea legalităţii este o operaţie amplă: regele trebuie să nu se afle în şah, iar câmpurile dintre rege şi turn trebuie să nu se afle sub atacul unor piese adverse; de regulă, se consultă câmpul din FEN-ul poziţiei curente corespunzător drepturilor de rocadă ale părţilor şi dacă regele şi turnul respectiv încă nu au fost mutate de pe locurile iniţiale, atunci se înscrie rocada ca mutare posibilă, amânând stabilirea legalităţii acesteia.

Ce ne propunem

Vom defini un widget (folosind jQueryUI.Widget()), prin care să putem instanţia diagrame precum cele redate mai sus, pe baza specificării unui subset compact de linii şi coloane şi a unui 'FEN' adaptat poziţiei „trunchiate” respective. De exemplu, pentru diagrama redată mai sus sub 1.Ne2 am folosi:

    $('#td2').trcBrd( {rows:[4,1], cols:[2,7], tFen:"b5/1N4/PP2PP/1B1K1N"} );

unde trcBrd este numele widget-ului (instanţiat pe elementul identificat prin 'td2'), iar {...} este obiectul JS al opţiunilor prevăzute widget-ului.

Avem în vedere tabla normală – albul (linia 1) este jos, iar negrul (linia 8) este sus; liniile trebuie indicate ca în FEN: de sus în jos. De exemplu, pentru 'td2' s-au reţinut numai câmpurile din liniile 1..4 aflate pe coloanele b..g; formăm 'FEN'-ul analog FEN-ului obişnuit: cea mai de sus linie conţine un nebun negru şi 5 câmpuri libere – deci şablonul corespunzător în 'FEN' este b5/; linia 3 are şablonul /1N4/ (câmp liber, cal alb, apoi 4 câmpuri libere), ş.a.m.d. rezultând 'FEN'-ul "b5/1N4/PP2PP/1B1K1N".
Desigur, am putea avea în vedere şi un test de corectitudine: pe oricare câmp al acestui 'FEN', suma cifrelor (adică numărul de câmpuri libere de pe linia respectivă) plus numărul de piese indicate pe acel câmp – trebuie să fie egal cu numărul de coloane specificat pentru tabla trunchiată respectivă.

Schema de lucru

Creem întâi această structură de fişiere:

    trwdg
    ├── CSS
    │   └── tbrw.css
    ├── IMG
    │   └── men20.png  /* sprite pentru piesele de şah (20x20px) */
    ├── JS
    │   ├── jquery1124.js  /* jQuery (v. 1.12) */
    │   ├── jquery.ui.widget.min.js  /* componenta Widget de la jQueryUI */
    │   └── tbrw.js
    └── trunc.html

Vom dezvolta aplicaţia în fişierul ”tbrw.js”, în forma (obişnuită pentru plugin-urile jQuery) IIFE (Immediately Invoked Function Expression), plecând de la următoarea schemă:

/*  fişierul "tbrw.js"  */
(function ($) {
/* variabile şi funcţii ajutătoare (partajate de instanţele widget-ului) */
    const PIECE_NAME = { p: "pawn", n: "knight", b: "bishop",
        r: "rook", q: "queen",  k: "king"
    };
    function strFEN(fen) {
    // transformă FEN în şir cu şablonul  /[pnbrqk ]/i (piese şi spaţii)
    };
    function limToSeq(lims) {
      // transformă [7,5] în [7,6,5] şi [2,4] în [2,3,4]
    };
    function strFldLab(rows, cols) { 
      // şirul HTML al diviziunilor pt. câmpuri şi etichete
    };
/* definiţia widget-ului */
    $.widget("brw.trcBrd", {
        options: {
            rows: [8, 1],  // liniile, de sus în jos; ex. [7,5]
            cols: [1, 8],  // coloanele, de ex. [2,4]
            tFen: ""       // "FEN" adaptat sub-tablei: "2K/1R1/q2"
        },
        _create: function() {
          // instanţiază "tabla", conform opţiunilor rows şi cols
        },
        _init: function() {
          // înscrie piesele, conform opţiunii tFen
        }
    });
})(jQuery);

Un fişier "trunc.html" minimal, în care să exploatăm widget-ul, va fi de forma:

<!DOCTYPE html>
<head> <meta charset="utf-8" />
    <link href="CSS/tbrw.css" rel="stylesheet">
    <script src="JS/jquery1124.js"></script>
    <script src="JS/jquery.ui.widget.min.js"></script>
    <script src="JS/tbrw.js"></script>
</head>
<body>
    <div id="brw1"></div>
<script>
    $(function() { 
        $('#brw1').trcBrd({rows:[4,1], cols:[2,7], tFen:"b5/1N4/PP2PP/1B1K1N"});
    });
</script>
</body>
</html>

trcBrd() va trebui să preia în options opţiunile transmise şi să folosească funcţiile ajutătoare şi metodele proprii, precum şi specificaţiile CSS din "tbrw.css", pentru a construi diagrama corespunzătoare.

Funcţiile ajutătoare

În "FEN"-ul primit, eliminăm separatorul de linii '/' şi înlocuim fiecare cifră cu un număr corespunzător de caractere ' ' (spaţiu):

function strFEN(fen) {
    return fen.replace(/\x2f/g, "")  // şterge '/' din FEN
              .replace(/[1-8]/g, n_sp => " ".repeat(n_sp));
};

Am folosit aici specificaţia de „funcţie săgeată” (introdusă de ECMAScript 6), în loc de formularea obişnuită (mai lungă) „function(n_sp){return " ".repeat(n_sp);}”.

De exemplu, strFEN("2/1p/1n/P1") produce şirul de 8 caractere "␣␣␣p␣nP␣"; vom obţine „diagrama HTML” corespunzătoare, înlocuind spaţiile "" prin "<div class='Field'></div>" şi piesele prin "<div class=>'Field'><div class='Piece'></div></div>" – mizând pe nişte definiţii CSS, din "tbrw.css", pentru div.Field şi div.Piece.

Funcţia limToSeq() primeşte un tablou cu două numere întregi distincte şi returnează (ca tablou) secvenţa numerelor intermediare:

function limToSeq(lims) {
    let L = lims[0], R = lims[1], sq = [];
    if(L < R) { for(let i=L; i <= R; i++) sq.push(i); }  // coloane (stânga..dreapta)
    else { for(let i=L; i >= R; i--) sq.push(i); }  // linii (de sus în jos)
    return sq;
};

Vom apela limToSeq() pentru a obţine secvenţa descrescătoare a liniilor şi pe cea crescătoare a coloanelor, din tablourile de limite options.rows şi options.cols, indicate la instanţierea widget-ului.

Funcţia strFldLab() produce ca şir HTML, div.trFlds conţinând diviziunile asociate câmpurilor tablei „trunchiate” şi totodată, diviziunile asociate etichetelor de linii şi coloane. Ambalând câmpurile în div.trFlds (separându-le astfel de etichetele care urmează acesteia), ne asigurăm posibilitatea de a le parcurge (în _init()) în vederea înscrierii pieselor din 'FEN', folosind $('div.trFlds').children().each().

Această funcţie este probabil, mai complicată decât celelalte – dar ideea este simplă: se translatează tabla trunchiată (după options.rows şi options.cols) în colţul stânga-sus al tablei 8\(\times\)8 normale, beneficiind astfel de proprietăţile CSS de poziţionare ale câmpurilor acesteia:

    function strFldLab(rows, cols) { 
        let html = ['<div class="trFlds">']; 
        let sq_row = limToSeq(rows), sq_col = limToSeq(cols), R, K;
        R = 8;
        for(let row of sq_row) { 
            K = 1;
            for(let col of sq_col) {
                let fldCl = (row + col) & 1 ? "whFld" : "blFld";
                html.push(`<div class="Field Row${R} Col${K} ${fldCl}"></div>`);
                K ++;
            }
            R --;
        }
        html.push('</div>');
        R = 8;
        for(let row of sq_row) { 
            html.push(`<div class="tagRow Row${R}">${row}</div>`); 
            R --;
        }
        K = 1;
        for(let col of sq_col) {
            let colID = String.fromCharCode(96 + col);
            html.push(`<div class="tagCol Col${K}">${colID}</div>`);
            K ++;
        }
        return html.join('');
    };

Precizăm că ne-am folosit aici de „şabloane literale” (introduse de ES6): în cadrul secvenţelor de caractere delimitate de ` (backtick, în loc de apostrof sau ghilimele), construcţiile de forma "${variabilă}" vor fi înlocuite prin conţinutul curent al variabilei. De exemplu, prin strFldLab([8,6], [1,2]) obţinem următoarea structură HTML (pentru cele 6 câmpuri şi cele 3 etichete de linii şi 2 etichete de coloane):

<div class="trFlds">
    <div class="Field Row8 Col1 whFld"></div>
    <div class="Field Row8 Col2 blFld"></div>
    <div class="Field Row7 Col1 blFld"></div>
    <div class="Field Row7 Col2 whFld"></div>
    <div class="Field Row6 Col1 whFld"></div>
    <div class="Field Row6 Col2 blFld"></div>
<div class="tagRow Row8">8</div>
<div class="tagRow Row7">7</div>
<div class="tagRow Row6">6</div>
<div class="tagCol Col1">a</div>
<div class="tagCol Col2">b</div>

Proprietăţile .Row*, .Col*, tagRow şi tagCol setate astfel (urmând să le specificăm corespunzător în "tbrw.css") servesc pentru poziţionarea diviziunilor respective (în timp ce .Field asigură o anumită dimensionare a câmpurilor, iar .whFld şi .blFld servesc pentru colorarea alternativă a acestora).

Metodele interne ale widget-ului

Metoda _create() („constructorul” widget-ului, lansat automat la invocarea acestuia) inserează în DOM, după nodul care corespunde elementului HTML pe care este instanţiat widget-ul, diviziunea div.boardFields care ambalează structura pe care o returnează funcţia strFldLab():

        _create: function() {
            let Elem = this.element,  // cui se ataşează widget-ul
                trw = this.options.rows,
                trk = this.options.cols;
            let R = trw[0] - trw[1] + 1,  // nr. linii
                C = trk[1] - trk[0] + 1;  // nr. coloane
            this.Schema = 
                $(`<div class="boardFields" 
                        style="width:${C*22}px;height:${R*22}px"></div>`)
                .insertAfter(Elem)
                .html(`${strFldLab(trw, trk)}`);
            this.Tabla = this.Schema.find('div.trFlds');
        },

Pentru câmpuri am considerat <div>-uri cu dimensiunea 20\(\times\)20px, cu padding de câte 1px – încât lăţimea şi înălţimea tablei trunchiate div.boardFields sunt produsul dintre 22 şi numărul de coloane, respectiv numărul de linii.

Metoda _init() (lansată automat, imediat după _create()) parcurge „în paralel” <div>-urile (corespunzătoare câmpurilor) din div.trFlds şi caracterele din "FEN"-ul produs de strFEN(); când caracterul curent reprezintă o piesă, inserează în <div>-ul curent o diviziune corespunzătoare acelei piese:

        _init: function() {
            let fen = strFEN(this.options.tFen);
            this.Tabla.children().each(function(index) {
                let piece = fen.charAt(index);
                if (piece && piece != " ") {
                    var piece_low = piece.toLowerCase(),
                    side = piece == piece_low ? "bl-": "wh-";
                    pieceClass = side + PIECE_NAME[piece_low];
                    $(this).html(`<div class="Piece ${pieceClass}"></div>`);
                }
                else $(this).html("");
            });
        }

De exemplu, în cazul opţiunilor {rows:[8,5], cols:[1,2], tFen:"2/1p/1n/P1"} – pentru care, pe linia 7 în coloana 2 se află un pion negru – vom obţine:

<div class="Field Row7 Col2 whFld">
    <div class="Piece bl-pawn"></div>
</div>

şi analog, pentru calul negru aflat pe linia 6 în coloana 2 şi respectiv, pentru pionul alb de pe linia 5, coloana 1.

Poziţionarea CSS a câmpurilor şi pieselor

Definirea tablei de şah (împreună cu piesele existente şi cu orientarea acesteia) utilizând selectori CSS imbricaţi şi speculând proprietatea CSS position a fost introdusă prin 2007, de către Mihai Bazon (https://github.com/mishoo).

Logica de bază decurge din aceste aspecte: browser-ul aşază conţinuturile elementelor <div> unul sub celălalt (în ordinea în care apar în fişierul HTML încărcat) – exceptând însă cazul când este setată proprietatea position. O diviziune declarată cu position:relative;left:50px va fi aşezată cu 50 pixeli mai la dreapta poziţiei obişnuite; o diviziune cu position:absolute;top:20px;left:20px va fi aşezată mai jos şi mai la dreapta cu câte 20px, faţă de prima diviziune care o încorporează şi care are position:relative (dacă există una, altfel – faţă de marginile ferestrei browser-ului).

În cazul nostru, div.boardFields subordonează diviziunile corespunzătoare câmpurilor şi deasemenea, pe cele care conţin etichetele de linii şi coloane; setăm (în "tbrw.css") position:relative pe div.boardFields şi position:absolute pe diviziunile subordonate:

.boardFields {position: relative; border: 1px solid;}
.Field {position: absolute; padding: 1px;}
.tagRow, .tagCol {  /* pe fiecare DIV-etichetă */
    position: absolute;  /* faţă de div.boardFields */
    font-family: monospace; color:#98a;
}

Trebuie apoi să indicăm la ce distanţă ("left" şi "top") se află câmpurile şi etichetele, faţă de marginile lui div.boardFields. Mai întâi, să precizăm dimensiunile pentru câmpuri şi piese:

.Field, .Piece {width: 20px; height: 20px}

Poziţionăm etichetele de linii cu 10px mai la stânga faţă de div.boardFields (având grijă să micşorăm puţin mărimea fontului), fixăm înălţimea lor la 22px (am adăugat 2px pentru "padding"-ul indicat în .Field) şi setăm deasemenea, "line-height" (pentru a asigura centrarea pe verticală a conţinutului). Procedăm analog, pentru etichetele coloanelor (montându-le dedesubtul tablei, la 102% faţă de marginea de sus a acesteia):

.tagRow {  /* pentru fiecare DIV-etichetă de pe verticală */
    left: -10px;  /* stânga faţă de div.boardFields */
    height: 22px;
    font-size: 0.9em;
    line-height:22px;  /* asigură centrarea pe verticală a etichetei */
}   
.tagCol {  /* pentru fiecare DIV-etichetă de pe orizontală */
    top: 102%;    /* jos faţă de div.boardFields */
    width: 22px;
    text-align:center; font-size: 0.95em;
} 

Valorile 'left' şi 'top' corespunzătoare câmpurilor tablei trebuie crescute (începând de la 0px, spre dreapta şi respectiv, de sus în josul tablei) cu câte 22px şi le fixăm prin definiţiile ".Col*" şi respectiv, ".Row*":

.Col1 {left:0px}     .Row8 {top:0px}
.Col2 {left:22px}    .Row7 {top:22px}
.Col3 {left:44px}    .Row6 {top:44px}
.Col4 {left:66px}    .Row5 {top:66px}
.Col5 {left:88px}    .Row4 {top:88px}
.Col6 {left:110px}   .Row3 {top:110px}
.Col7 {left:132px}   .Row2 {top:132px}
.Col8 {left:154px}   .Row1 {top:154px}
.whFld { background: #f8f0ff; }
.blFld { background: #D3D3D3; }

Am adăugat şi o specificaţie de culoare pentru câmpurile „albe”, respectiv, „negre”.

Piesele de şah vor fi montate pe câmpurile cuvenite extrăgându-le dintr-un sprite (indicat în definiţia .Piece) şi urmând această regulă: 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% (regulă pe care am stabilit-o mai demult, în Poziţionarea procentuală a unei piese dintr-un sprite):

.Piece {
    background-image:url('../IMG/men20.png')
}
.wh-pawn   {background-position:0%}      
.wh-king   {background-position:-100%}
.wh-queen  {background-position:-200%}
.wh-rook   {background-position:-300%}
.wh-bishop {background-position:-400%}
.wh-knight {background-position:-500%}
.bl-knight {background-position:-600%}
.bl-bishop {background-position:-700%}
.bl-rook   {background-position:-800%}
.bl-queen  {background-position:-900%}
.bl-king   {background-position:-1000%}
.bl-pawn   {background-position:100%}

Enunţul regulii de mai sus trebuie amendat puţin: ":-K*100" pentru imaginile interioare sprite-ului (K=1..10); pentru imaginile de la capete (pionul alb şi respectiv, cel negru), în background-position trebuie folosit :0% şi respectiv :100%.

Widget-ul pe care ni l-am propus este acum complet constituit; l-am folosit efectiv într-un studiu mai amplu (aflat „în curs de apariţie”) asupra modelării jocului de şah, pentru a ilustra legătura dintre notaţiile SAN şi FEN pe parcursul analizei unui fişier PGN, precum şi aspectele referitoare la generarea mutărilor posibile şi la stabilirea legalităţii mutării.

vezi Cărţile mele (de programare)

docerpro | Prev | Next