Almost finished problems logic. TODO: showProblem() part
authorBenjamin Auder <benjamin.auder@somewhere>
Sat, 15 Dec 2018 17:22:14 +0000 (18:22 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Sat, 15 Dec 2018 17:22:14 +0000 (18:22 +0100)
14 files changed:
TODO [new file with mode: 0644]
public/javascripts/base_rules.js
public/javascripts/components/game.js
public/javascripts/components/problemSummary.js
public/javascripts/components/problems.js
public/javascripts/components/rules.js
public/javascripts/variants/Alice.js
public/javascripts/variants/Antiking.js
public/javascripts/variants/Checkered.js
public/javascripts/variants/Grand.js
public/javascripts/variants/Loser.js
public/javascripts/variants/Ultima.js
public/javascripts/variants/Wildebeest.js
routes/all.js

diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..e6641b4
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+Finish showProblem() in components/problemSummary.js (send event with infos, and pass message to game component)
+Add new mode in game component: "problem", in which we show description + hidden solution (reveal on click)
index 51c9dda..5763af7 100644 (file)
@@ -54,12 +54,15 @@ class ChessRules
        constructor(fen, moves)
        {
                this.moves = moves;
-               // Use fen string to initialize variables, flags and board
-               this.board = V.GetBoard(fen);
-               this.setFlags(fen);
+               // Use fen string to initialize variables, flags, turn and board
+               const fenParts = fen.split(" ");
+               this.board = V.GetBoard(fenParts[0]);
+               this.setFlags(fenParts[1]); //NOTE: fenParts[1] might be undefined
+               this.setTurn(fenParts[2]); //Same note
                this.initVariables(fen);
        }
 
+       // Some additional variables from FEN (variant dependant)
        initVariables(fen)
        {
                this.INIT_COL_KING = {'w':-1, 'b':-1};
@@ -95,7 +98,7 @@ class ChessRules
                                                        this.INIT_COL_ROOK['w'][1] = k;
                                                break;
                                        default:
-                                               let num = parseInt(position[i].charAt(j));
+                                               const num = parseInt(position[i].charAt(j));
                                                if (!isNaN(num))
                                                        k += (num-1);
                                }
@@ -106,18 +109,68 @@ class ChessRules
                this.epSquares = [ epSq ];
        }
 
+       // Check if FEN describe a position
+       static IsGoodFen(fen)
+       {
+               const fenParts = fen.split(" ");
+               if (fenParts.length== 0 || fenParts.length > 3)
+                       return false;
+               // 1) Check position
+               const position = fenParts[0];
+               const rows = position.split("/");
+               if (rows.length != V.size.x)
+                       return false;
+               for (let row of rows)
+               {
+                       let sumElts = 0;
+                       for (let i=0; i<row.length; i++)
+                       {
+                               if (V.PIECES.includes(row[i].toLowerCase()))
+                                       sumElts++;
+                               else
+                               {
+                                       const num = parseInt(row[i]);
+                                       if (isNaN(num))
+                                               return false;
+                                       sumElts += num;
+                               }
+                       }
+                       if (sumElts != V.size.y)
+                               return false;
+               }
+               // 2) Check flags (if present)
+               if (fenParts.length >= 2)
+               {
+                       if (!V.IsGoodFlags(fenParts[1]))
+                               return false;
+               }
+               // 3) Check turn (if present)
+               if (fenParts.length == 3)
+               {
+                       if (!["w","b"].includes(fenParts[2]))
+                               return false;
+               }
+               return true;
+       }
+
+       // For FEN checking
+       static IsGoodFlags(flags)
+       {
+               return !!flags.match(/^[01]{4,4}$/);
+       }
+
        // Turn diagram fen into double array ["wb","wp","bk",...]
        static GetBoard(fen)
        {
-               let rows = fen.split(" ")[0].split("/");
+               const rows = fen.split(" ")[0].split("/");
                let board = doubleArray(V.size.x, V.size.y, "");
                for (let i=0; i<rows.length; i++)
                {
                        let j = 0;
                        for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++)
                        {
-                               let character = rows[i][indexInRow];
-                               let num = parseInt(character);
+                               const character = rows[i][indexInRow];
+                               const num = parseInt(character);
                                if (!isNaN(num))
                                        j += num; //just shift j
                                else //something at position i,j
@@ -128,13 +181,20 @@ class ChessRules
        }
 
        // Extract (relevant) flags from fen
-       setFlags(fen)
+       setFlags(fenflags)
        {
                // white a-castle, h-castle, black a-castle, h-castle
-               this.castleFlags = {'w': new Array(2), 'b': new Array(2)};
-               let flags = fen.split(" ")[1]; //flags right after position
+               this.castleFlags = {'w': [true,true], 'b': [true,true]};
+               if (!fenflags)
+                       return;
                for (let i=0; i<4; i++)
-                       this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (flags.charAt(i) == '1');
+                       this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1');
+       }
+
+       // Initialize turn (white or black)
+       setTurn(turnflag)
+       {
+               this.turn = turnflag || "w";
        }
 
        ///////////////////
@@ -154,10 +214,6 @@ class ChessRules
                return (L>0 ? this.moves[L-1] : null);
        }
 
-       get turn() {
-               return (this.moves.length%2==0 ? 'w' : 'b');
-       }
-
        // Pieces codes
        static get PAWN() { return 'p'; }
        static get ROOK() { return 'r'; }
@@ -166,6 +222,11 @@ class ChessRules
        static get QUEEN() { return 'q'; }
        static get KING() { return 'k'; }
 
+       // For FEN checking:
+       static get PIECES() {
+               return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING];
+       }
+
        // Empty square
        static get EMPTY() { return ''; }
 
@@ -483,9 +544,7 @@ class ChessRules
 
        canIplay(side, [x,y])
        {
-               return ((side=='w' && this.moves.length%2==0)
-                               || (side=='b' && this.moves.length%2==1))
-                       && this.getColor(x,y) == side;
+               return (this.turn == side && this.getColor(x,y) == side);
        }
 
        getPossibleMovesFrom(sq)
@@ -717,7 +776,7 @@ class ChessRules
        // Hash of position+flags+turn after a move is played (to detect repetitions)
        getHashState()
        {
-               return hex_md5(this.getFen() + " " + this.turn);
+               return hex_md5(this.getFen());
        }
 
        play(move, ingame)
@@ -1054,7 +1113,7 @@ class ChessRules
        // Return current fen according to pieces+colors state
        getFen()
        {
-               return this.getBaseFen() + " " + this.getFlagsFen();
+               return this.getBaseFen() + " " + this.getFlagsFen() + " " + this.turn;
        }
 
        // Position part of the FEN string
index 278e078..49db618 100644 (file)
@@ -1024,10 +1024,7 @@ Vue.component('my-game', {
                        {
                                const storageVariant = localStorage.getItem("variant");
                                if (!!storageVariant && storageVariant !== variant)
-                               {
-                                       alert("Finish your " + storageVariant + " game first!");
-                                       return;
-                               }
+                                       return alert("Finish your " + storageVariant + " game first!");
                                // Send game request and wait..
                                localStorage["newgame"] = variant;
                                this.seek = true;
index 6003f08..48a4253 100644 (file)
@@ -2,16 +2,33 @@
 Vue.component('my-problem-summary', {
        props: ['prob'],
        template: `
-               <div class="problem col-sm-12">
-                       <div class="diagram">
-                               {{ getDiagram(prob.fen) }}
-                       </div>
-                       <div class="problem-instructions">
-                               {{ prob.instructions.substr(0,32) }}
-                       </div>
-                       <div class="problem-time">
-                               {{ prob.added }}
-                       </div>
+               <div class="problem col-sm-12" @click="showProblem()">
+                       <div class="diagram" v-html="getDiagram(prob.fen)"></div>
+                       <div class="problem-instructions" v-html="prob.instructions.substr(0,32)"></div>
+                       <div class="problem-time">{{ timestamp2datetime(prob.added) }}</div>
                </div>
        `,
+       methods: {
+               getDiagram: function(fen) {
+                       const fenParts = fen.split(" ");
+                       return getDiagram({
+                               position: fenParts[0],
+                               // No need for flags here
+                               turn: fenParts[2],
+                       });
+               },
+               timestamp2datetime(ts) {
+                       // TODO
+                       return ts;
+               },
+               showProblem: function() {
+                       alert("show problem");
+                       //..........
+                       //TODO: send event with object prob.fen, prob.instructions, prob.solution
+                       //Event should propagate to game, which set mode=="problem" + other variables
+                       //click on a problem ==> land on variant page with mode==friend, FEN prefilled... ok
+                       // click on problem ==> masque problems, affiche game tab, launch new game Friend with
+                       //   FEN + turn + flags + rappel instructions / solution on click sous l'échiquier
+               },
+       },
 })
index 594d6e6..dbd8340 100644 (file)
@@ -6,14 +6,11 @@ Vue.component('my-problems', {
        },
        template: `
                <div>
-                       <button>Previous</button>
-                       <button>Next</button>
+                       <button @click="fetchProblems('backward')">Previous</button>
+                       <button @click="fetchProblems('forward')">Next</button>
                        <button @click="showNewproblemModal">New</button>
                        <my-problem-summary
-                               v-for="(p,idx) in sortedProblems",
-                               v-bind:prob="p",
-                               v-bind:key="idx",
-                               @click="showProblem(p)"
+                               v-for="(p,idx) in sortedProblems" v-bind:prob="p" v-bind:key="idx">
                        </my-problem-summary>
                        <input type="checkbox" id="modal-newproblem" class="modal">
                        <div role="dialog" aria-labelledby="newProblemTxt">
@@ -23,7 +20,8 @@ Vue.component('my-problems', {
                                        <form @submit.prevent="postNewProblem">
                                                <fieldset>
                                                        <label for="newpbFen">Fen</label>
-                                                       <input type="text" id="newpbFen" placeholder="Position [+ flags [+ turn]]"/>
+                                                       <input type="text" id="newpbFen"
+                                                               placeholder="Position [+ flags [+ turn]]"/>
                                                </fieldset>
                                                <fieldset>
                                                        <p class="emphasis">
@@ -47,8 +45,9 @@ Vue.component('my-problems', {
        `,
        computed: {
                sortedProblems: function() {
+                       console.log("call");
                        // Newest problem first
-                       return problems.sort((p1,p2) => { return p2.added - p1.added; });
+                       return this.problems.sort((p1,p2) => { return p2.added - p1.added; });
                },
                mailErrProblem: function() {
                        return "mailto:contact@vchess.club?subject=[" + variant + " problems] error";
@@ -56,26 +55,43 @@ Vue.component('my-problems', {
        },
        methods: {
                fetchProblems: function(direction) {
-                       // TODO: ajax call return list of max 10 problems
-                       // Do not do anything if no older problems (and store this result in cache!)
-                       // TODO: ajax call return list of max 10 problems
-                       // Do not do anything if no newer problems
-               },
-               showProblem: function(prob) {
-                       //TODO: send event with object prob.fen, prob.instructions, prob.solution
-                       //Event should propagate to game, which set mode=="problem" + other variables
-                       //click on a problem ==> land on variant page with mode==friend, FEN prefilled... ok
-                       // click on problem ==> masque problems, affiche game tab, launch new game Friend with
-                       //   FEN + turn + flags + rappel instructions / solution on click sous l'échiquier
+                       return; //TODO: re-activate after server side is implemented (see routes/all.js)
+                       if (this.problems.length == 0)
+                               return; //what could we do?!
+                       // Search for newest date (or oldest)
+                       let last_dt = this.problems[0].added;
+                       for (let i=0; i<this.problems.length; i++)
+                       {
+                               if ((direction == "forward" && this.problems[i].added > last_dt) ||
+                                       (direction == "backward" && this.problems[i].added < last_dt))
+                               {
+                                       last_dt = this.problems[i].added;
+                               }
+                       }
+                       ajax("/problems/" + variant, "GET", {
+                               direction: direction,
+                               last_dt: last_dt,
+                       }, response => {
+                               if (response.problems.length > 0)
+                                       this.problems = response.problems;
+                       });
                },
                showNewproblemModal: function() {
                        document.getElementById("modal-newproblem").checked = true;
                },
                postNewProblem: function() {
                        const fen = document.getElementById("newpbFen").value;
+                       if (!V.IsGoodFen(fen))
+                               return alert("Bad FEN string");
                        const instructions = document.getElementById("newpbInstructions").value;
                        const solution = document.getElementById("newpbSolution").value;
-                       
+                       ajax("/problems/" + variant, "POST", {
+                               fen: fen,
+                               instructions: instructions,
+                               solution: solution,
+                       }, response => {
+                               document.getElementById("modal-newproblem").checked = false;
+                       });
                },
        },
 })
index 660f0be..829bf3b 100644 (file)
@@ -8,11 +8,11 @@ Vue.component('my-rules', {
                // AJAX request to get rules content (plain text, HTML)
                ajax("/rules/" + variant, "GET", response => {
                        let replaceByDiag = (match, p1, p2) => {
-                               const args = self.parseFen(p2);
+                               const args = this.parseFen(p2);
                                return getDiagram(args);
                        };
-                       self.content = response.replace(/(fen:)([^:]*):/g, replaceByDiag);
-               }
+                       this.content = response.replace(/(fen:)([^:]*):/g, replaceByDiag);
+               });
        },
        methods: {
                parseFen(fen) {
index 5e10a05..ece59cd 100644 (file)
@@ -29,6 +29,10 @@ class AliceRules extends ChessRules
                return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
        }
 
+       static get PIECES() {
+               return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES));
+       }
+
        initVariables(fen)
        {
                super.initVariables(fen);
index e741168..2821f3f 100644 (file)
@@ -6,7 +6,11 @@ class AntikingRules extends ChessRules
        }
 
        static get ANTIKING() { return 'a'; }
-       
+
+       static get PIECES() {
+               return ChessRules.PIECES.concat([V.ANTIKING]);
+       }
+
        initVariables(fen)
        {
                super.initVariables(fen);
index 2d62d4b..585d20f 100644 (file)
@@ -19,18 +19,34 @@ class CheckeredRules extends ChessRules
        }
        static fen2board(f)
        {
+               // Tolerate upper-case versions of checkered pieces (why not?)
                const checkered_pieces = {
                        's': 'p',
+                       'S': 'p',
                        't': 'q',
+                       'T': 'q',
                        'u': 'r',
+                       'U': 'r',
                        'c': 'b',
+                       'C': 'b',
                        'o': 'n',
+                       'O': 'n',
                };
                if (Object.keys(checkered_pieces).includes(f))
                        return 'c'+checkered_pieces[f];
                return ChessRules.fen2board(f);
        }
 
+       static get PIECES() {
+               return ChessRules.PIECES.concat(['s','t','u','c','o']);
+       }
+
+       static IsGoodFlags(flags)
+       {
+               // 4 for castle + 16 for pawns
+               return !!flags.match(/^[01]{20,20}$/);
+       }
+
        setFlags(fen)
        {
                super.setFlags(fen); //castleFlags
@@ -108,9 +124,7 @@ class CheckeredRules extends ChessRules
 
        canIplay(side, [x,y])
        {
-               return ((side=='w' && this.moves.length%2==0)
-                               || (side=='b' && this.moves.length%2==1))
-                       && [side,'c'].includes(this.getColor(x,y));
+               return (side == this.turn && [side,'c'].includes(this.getColor(x,y)));
        }
 
        // Does m2 un-do m1 ? (to disallow undoing checkered moves)
@@ -194,11 +208,12 @@ class CheckeredRules extends ChessRules
        checkGameEnd()
        {
                const color = this.turn;
-               this.moves.length++; //artifically change turn, for checkered pawns (TODO)
+               // Artifically change turn, for checkered pawns
+               this.turn = this.getOppCol(this.turn);
                const res = this.isAttacked(this.kingPos[color], [this.getOppCol(color),'c'])
                        ? (color == "w" ? "0-1" : "1-0")
                        : "1/2";
-               this.moves.length--;
+               this.turn = this.getOppCol(this.turn);
                return res;
        }
 
index f5ae065..cafdada 100644 (file)
@@ -18,6 +18,10 @@ class GrandRules extends ChessRules
        static get MARSHALL() { return 'm'; } //rook+knight
        static get CARDINAL() { return 'c'; } //bishop+knight
 
+       static get PIECES() {
+               return ChessRules.PIECES.concat([V.MARSHALL,V.CARDINAL]);
+       }
+
        // En-passant after 2-sq or 3-sq jumps
        getEpSquare(move)
        {
index 3def40a..98bd944 100644 (file)
@@ -6,6 +6,11 @@ class LoserRules extends ChessRules
                this.epSquares = [ epSq ];
        }
 
+       static IsGoodFlags(flags)
+       {
+               return true; //anything is good: no flags
+       }
+
        setFlags(fen)
        {
                // No castling, hence no flags; but flags defined for compatibility
index 04ef29b..d6fecb7 100644 (file)
@@ -7,6 +7,15 @@ class UltimaRules extends ChessRules
                return b; //usual piece
        }
 
+       static get PIECES() {
+               return ChessRules.PIECES.concat([V.IMMOBILIZER]);
+       }
+
+       static IsGoodFlags(flags)
+       {
+               return true; //anything is good: no flags
+       }
+
        initVariables(fen)
        {
                this.kingPos = {'w':[-1,-1], 'b':[-1,-1]};
index bb478cc..dcf2d58 100644 (file)
@@ -10,6 +10,10 @@ class WildebeestRules extends ChessRules
        static get CAMEL() { return 'c'; }
        static get WILDEBEEST() { return 'w'; }
 
+       static get PIECES() {
+               return ChessRules.PIECES.concat([V.CAMEL,V.WILDEBEEST]);
+       }
+
        static get steps() {
                return Object.assign(
                        ChessRules.steps, //add camel moves:
index f3e184e..b1e0fda 100644 (file)
@@ -28,6 +28,7 @@ router.get("/:vname([a-zA-Z0-9]+)", (req,res,next) => {
                                return next(err);
                        if (!variant || variant.length==0)
                                return next(createError(404));
+                       // TODO (later...) get only n=100(?) most recent problems
                        db.all("SELECT * FROM Problems WHERE variant='" + vname + "'",
                                (err2,problems) => {
                                        if (!!err2)
@@ -55,6 +56,9 @@ router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
        if (!req.xhr)
                return res.json({errmsg: "Unauthorized access"});
        // TODO: next or previous: in params + timedate (of current oldest or newest)
+       db.serialize(function() {
+               //TODO
+       });
 });
 
 // Upload a problem (AJAX)
@@ -62,10 +66,13 @@ router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
        if (!req.xhr)
                return res.json({errmsg: "Unauthorized access"});
        const vname = req.params["variant"];
-       
-       // TODO: get parameters and sanitize them
-       sanitizeHtml(req.body["fen"]); // [/a-z0-9 ]*
-       sanitizeHtml(req.body["instructions"]);
+       const timestamp = Date.now();
+       // Sanitize them
+       const fen = req.body["fen"];
+       if (!fen.match(/^[a-zA-Z0-9 /]*$/))
+               return res.json({errmsg: "Bad characters in FEN string"});
+       const instructions = sanitizeHtml(req.body["instructions"]);
+       const solution = sanitizeHtml(req.body["solution"]);
        db.serialize(function() {
                let stmt = db.prepare("INSERT INTO Problems VALUES (?,?,?,?,?)");
                stmt.run(timestamp, vname, fen, instructions, solution);
@@ -74,5 +81,4 @@ router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
   res.json({});
 });
 
-
 module.exports = router;