From 1a788978e3682ab54b77af3edfe38e0b371edbc4 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Mon, 17 Dec 2018 18:21:01 +0100 Subject: [PATCH] Save current state (unfinished, untested) --- TODO | 6 +- public/javascripts/base_rules.js | 657 ++++++++++++++------------ public/javascripts/components/game.js | 189 +++++--- public/stylesheets/variant.sass | 3 + 4 files changed, 493 insertions(+), 362 deletions(-) diff --git a/TODO b/TODO index e6641b48..4d99536b 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,4 @@ -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) +global lang cookie, + display (remember in each variant what is shown...) +translations (how ? switch on index page only, then find ideas...) +for each variant, adapt FEN (Crazyhouse, Grand, Loser, ...) +Improve style for various screen sizes diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js index 2a962a96..620b86f8 100644 --- a/public/javascripts/base_rules.js +++ b/public/javascripts/base_rules.js @@ -31,91 +31,33 @@ class Move // NOTE: x coords = top to bottom; y = left to right (from white player perspective) class ChessRules { + ////////////// + // MISC UTILS + // Path to pieces static getPpath(b) { return b; //usual pieces in pieces/ folder } + // Turn "wb" into "B" (for FEN) static board2fen(b) { return b[0]=='w' ? b[1].toUpperCase() : b[1]; } + // Turn "p" into "bp" (for board) static fen2board(f) { return f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f; } - ///////////////// - // INITIALIZATION - - // fen == "position [flags [turn]]" - constructor(fen, moves) - { - this.moves = moves; - // 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}; - this.INIT_COL_ROOK = {'w':[-1,-1], 'b':[-1,-1]}; - this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //squares of white and black king - const fenParts = fen.split(" "); - const position = fenParts[0].split("/"); - for (let i=0; i<position.length; i++) - { - let k = 0; //column index on board - for (let j=0; j<position[i].length; j++) - { - switch (position[i].charAt(j)) - { - case 'k': - this.kingPos['b'] = [i,k]; - this.INIT_COL_KING['b'] = k; - break; - case 'K': - this.kingPos['w'] = [i,k]; - this.INIT_COL_KING['w'] = k; - break; - case 'r': - if (this.INIT_COL_ROOK['b'][0] < 0) - this.INIT_COL_ROOK['b'][0] = k; - else - this.INIT_COL_ROOK['b'][1] = k; - break; - case 'R': - if (this.INIT_COL_ROOK['w'][0] < 0) - this.INIT_COL_ROOK['w'][0] = k; - else - this.INIT_COL_ROOK['w'][1] = k; - break; - default: - const num = parseInt(position[i].charAt(j)); - if (!isNaN(num)) - k += (num-1); - } - k++; - } - } - this.epSquares = [ this.getEpSquare(this.lastMove || fenParts[3]) ]; - } - // Check if FEN describe a position static IsGoodFen(fen) { - const fenParts = fen.split(" "); - if (fenParts.length== 0) - return false; + const fenParsed = V.ParseFen(fen); // 1) Check position - const position = fenParts[0]; + const position = fenParsed.position; const rows = position.split("/"); if (rows.length != V.size.x) return false; @@ -138,15 +80,16 @@ class ChessRules return false; } // 2) Check flags (if present) - if (fenParts.length >= 2) - { - if (!V.IsGoodFlags(fenParts[1])) - return false; - } + if (!!fenParsed.flags && !V.IsGoodFlags(fenParsed.flags)) + return false; // 3) Check turn (if present) - if (fenParts.length >= 3) + if (!!fenParsed.turn && !["w","b"].includes(fenParsed.turn)) + return false; + // 4) Check enpassant (if present) + if (!!fenParsed.enpassant) { - if (!["w","b"].includes(fenParts[2])) + const ep = V.SquareToCoords(fenParsed.enpassant); + if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x) return false; } return true; @@ -158,10 +101,225 @@ class ChessRules return !!flags.match(/^[01]{4,4}$/); } - // Turn diagram fen into double array ["wb","wp","bk",...] - static GetBoard(fen) + // a4 --> {x:3,y:0} + static SquareToCoords(sq) + { + return { + x: V.size.x - parseInt(sq.substr(1)), + y: sq[0].charCodeAt() - 97 + }; + } + + // {x:0,y:4} --> e8 + static CoordsToSquare(coords) + { + return String.fromCharCode(97 + coords.y) + (V.size.x - coords.x); + } + + // Aggregates flags into one object + aggregateFlags() + { + return this.castleFlags; + } + + // Reverse operation + disaggregateFlags(flags) + { + this.castleFlags = flags; + } + + // En-passant square, if any + getEpSquare(moveOrSquare) + { + if (!moveOrSquare) + return undefined; + if (typeof moveOrSquare === "string") + { + const square = moveOrSquare; + if (square == "-") + return undefined; + return { + x: square[0].charCodeAt()-97, + y: V.size.x-parseInt(square[1]) + }; + } + // Argument is a move: + const move = moveOrSquare; + const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; + if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) + { + return { + x: (sx + ex)/2, + y: sy + }; + } + return undefined; //default + } + + // Can thing on square1 take thing on square2 + canTake([x1,y1], [x2,y2]) + { + return this.getColor(x1,y1) !== this.getColor(x2,y2); + } + + // Is (x,y) on the chessboard? + static OnBoard(x,y) + { + return (x>=0 && x<V.size.x && y>=0 && y<V.size.y); + } + + // Used in interface: 'side' arg == player color + canIplay(side, [x,y]) + { + return (this.turn == side && this.getColor(x,y) == side); + } + + // On which squares is opponent under check after our move ? (for interface) + getCheckSquares(move) + { + this.play(move); + const color = this.turn; //opponent + let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) + ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate! + : []; + this.undo(move); + return res; + } + + ///////////// + // FEN UTILS + + // Setup the initial random (assymetric) position + static GenRandInitFen() + { + let pieces = { "w": new Array(8), "b": new Array(8) }; + // Shuffle pieces on first and last rank + for (let c of ["w","b"]) + { + let positions = _.range(8); + + // Get random squares for bishops + let randIndex = 2 * _.random(3); + let bishop1Pos = positions[randIndex]; + // The second bishop must be on a square of different color + let randIndex_tmp = 2 * _.random(3) + 1; + let bishop2Pos = positions[randIndex_tmp]; + // Remove chosen squares + positions.splice(Math.max(randIndex,randIndex_tmp), 1); + positions.splice(Math.min(randIndex,randIndex_tmp), 1); + + // Get random squares for knights + randIndex = _.random(5); + let knight1Pos = positions[randIndex]; + positions.splice(randIndex, 1); + randIndex = _.random(4); + let knight2Pos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Get random square for queen + randIndex = _.random(3); + let queenPos = positions[randIndex]; + positions.splice(randIndex, 1); + + // Rooks and king positions are now fixed, because of the ordering rook-king-rook + let rook1Pos = positions[0]; + let kingPos = positions[1]; + let rook2Pos = positions[2]; + + // Finally put the shuffled pieces in the board array + pieces[c][rook1Pos] = 'r'; + pieces[c][knight1Pos] = 'n'; + pieces[c][bishop1Pos] = 'b'; + pieces[c][queenPos] = 'q'; + pieces[c][kingPos] = 'k'; + pieces[c][bishop2Pos] = 'b'; + pieces[c][knight2Pos] = 'n'; + pieces[c][rook2Pos] = 'r'; + } + return pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + " w 1111 -"; //add turn + flags + enpassant + } + + // "Parse" FEN: just return untransformed string data + static ParseFen(fen) + { + const fenParts = fen.split(" "); + return { + position: fenParts[0], + turn: fenParts[1], + flags: fenParts[2], + enpassant: fenParts[3], + }; + } + + // Return current fen (game state) + getFen() + { + return this.getBaseFen() + " " + this.turn + " " + + this.getFlagsFen() + " " + this.getEnpassantFen(); + } + + // Position part of the FEN string + getBaseFen() + { + let position = ""; + for (let i=0; i<V.size.x; i++) + { + let emptyCount = 0; + for (let j=0; j<V.size.y; j++) + { + if (this.board[i][j] == V.EMPTY) + emptyCount++; + else + { + if (emptyCount > 0) + { + // Add empty squares in-between + position += emptyCount; + emptyCount = 0; + } + fen += V.board2fen(this.board[i][j]); + } + } + if (emptyCount > 0) + { + // "Flush remainder" + position += emptyCount; + } + if (i < V.size.x - 1) + position += "/"; //separate rows + } + return position; + } + + // Flags part of the FEN string + getFlagsFen() + { + let flags = ""; + // Add castling flags + for (let i of ['w','b']) + { + for (let j=0; j<2; j++) + flags += (this.castleFlags[i][j] ? '1' : '0'); + } + return flags; + } + + // Enpassant part of the FEN string + getEnpassantFen() + { + const L = this.epSquares.length; + if (L == 0) + return "-"; //no en-passant + return V.CoordsToSquare(this.epSquares[L-1]); + } + + // Turn position fen into double array ["wb","wp","bk",...] + static GetBoard(position) { - const rows = fen.split(" ")[0].split("/"); + const rows = position.split("/"); let board = doubleArray(V.size.x, V.size.y, ""); for (let i=0; i<rows.length; i++) { @@ -190,30 +348,101 @@ class ChessRules this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1'); } - // Initialize turn (white or black) - setTurn(turnflag) + ////////////////// + // INITIALIZATION + + // Fen string fully describes the game state + constructor(fen, moves) { - this.turn = turnflag || "w"; + this.moves = moves; + const fenParsed = V.ParseFen(fen); + this.board = V.GetBoard(fenParsed.position); + this.turn = (fenParsed.turn || "w"); + this.setOtherVariables(fen); } - /////////////////// - // GETTERS, SETTERS + // Some additional variables from FEN (variant dependant) + setOtherVariables(fen) + { + // Set flags and enpassant: + const parsedFen = V.ParseFen(fen); + this.setFlags(fenParsed.flags); + this.epSquares = [ V.SquareToCoords(parsedFen.enpassant) ]; + // Search for king and rooks positions: + this.INIT_COL_KING = {'w':-1, 'b':-1}; + this.INIT_COL_ROOK = {'w':[-1,-1], 'b':[-1,-1]}; + this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //squares of white and black king + const fenRows = parsedFen.position.split("/"); + for (let i=0; i<fenRows.length; i++) + { + let k = 0; //column index on board + for (let j=0; j<fenRows[i].length; j++) + { + switch (fenRows[i].charAt(j)) + { + case 'k': + this.kingPos['b'] = [i,k]; + this.INIT_COL_KING['b'] = k; + break; + case 'K': + this.kingPos['w'] = [i,k]; + this.INIT_COL_KING['w'] = k; + break; + case 'r': + if (this.INIT_COL_ROOK['b'][0] < 0) + this.INIT_COL_ROOK['b'][0] = k; + else + this.INIT_COL_ROOK['b'][1] = k; + break; + case 'R': + if (this.INIT_COL_ROOK['w'][0] < 0) + this.INIT_COL_ROOK['w'][0] = k; + else + this.INIT_COL_ROOK['w'][1] = k; + break; + default: + const num = parseInt(fenRows[i].charAt(j)); + if (!isNaN(num)) + k += (num-1); + } + k++; + } + } + } + + ///////////////////// + // GETTERS & SETTERS + + static get size() + { + return {x:8, y:8}; + } - static get size() { return {x:8, y:8}; } + // Color of thing on suqare (i,j). 'undefined' if square is empty + getColor(i,j) + { + return this.board[i][j].charAt(0); + } - // Two next functions return 'undefined' if called on empty square - getColor(i,j) { return this.board[i][j].charAt(0); } - getPiece(i,j) { return this.board[i][j].charAt(1); } + // Piece type on square (i,j). 'undefined' if square is empty + getPiece(i,j) + { + return this.board[i][j].charAt(1); + } - // Color - getOppCol(color) { return (color=="w" ? "b" : "w"); } + // Get opponent color + getOppCol(color) + { + return (color=="w" ? "b" : "w"); + } - get lastMove() { + get lastMove() + { const L = this.moves.length; return (L>0 ? this.moves[L-1] : null); } - // Pieces codes + // Pieces codes (for a clearer code) static get PAWN() { return 'p'; } static get ROOK() { return 'r'; } static get KNIGHT() { return 'n'; } @@ -222,15 +451,17 @@ class ChessRules static get KING() { return 'k'; } // For FEN checking: - static get PIECES() { + static get PIECES() + { return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING]; } // Empty square - static get EMPTY() { return ''; } + static get EMPTY() { return ""; } // Some pieces movements - static get steps() { + static get steps() + { return { 'r': [ [-1,0],[1,0],[0,-1],[0,1] ], 'n': [ [-1,-2],[-1,2],[1,-2],[1,2],[-2,-1],[-2,1],[2,-1],[2,1] ], @@ -238,50 +469,7 @@ class ChessRules }; } - // Aggregates flags into one object - get flags() { - return this.castleFlags; - } - - // Reverse operation - parseFlags(flags) - { - this.castleFlags = flags; - } - - // En-passant square, if any - getEpSquare(moveOrSquare) - { - if (typeof moveOrSquare === "string") - { - const square = moveOrSquare; - if (square == "-") - return undefined; - return { - x: square[0].charCodeAt()-97, - y: V.size.x-parseInt(square[1]) - }; - } - // Argument is a move: - const move = moveOrSquare; - const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x]; - if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2) - { - return { - x: (sx + ex)/2, - y: sy - }; - } - return undefined; //default - } - - // Can thing on square1 take thing on square2 - canTake([x1,y1], [x2,y2]) - { - return this.getColor(x1,y1) !== this.getColor(x2,y2); - } - - /////////////////// + //////////////////// // MOVES GENERATION // All possible moves from selected square (assumption: color is OK) @@ -341,12 +529,6 @@ class ChessRules return mv; } - // Is (x,y) on the chessboard? - static OnBoard(x,y) - { - return (x>=0 && x<V.size.x && y>=0 && y<V.size.y); - } - // Generic method to find possible moves of non-pawn pieces ("sliding or jumping") getSlideNJumpMoves([x,y], steps, oneStep) { @@ -550,14 +732,9 @@ class ChessRules return moves; } - /////////////////// + //////////////////// // MOVES VALIDATION - canIplay(side, [x,y]) - { - return (this.turn == side && this.getColor(x,y) == side); - } - getPossibleMovesFrom(sq) { // Assuming color is right (already checked) @@ -618,7 +795,7 @@ class ChessRules return false; } - // Check if pieces of color in array 'colors' are attacking square x,y + // Check if pieces of color in array 'colors' are attacking (king) on square x,y isAttacked(sq, colors) { return (this.isAttackedByPawn(sq, colors) @@ -714,17 +891,8 @@ class ChessRules return res; } - // On which squares is opponent under check after our move ? - getCheckSquares(move) - { - this.play(move); - const color = this.turn; //opponent - let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]) - ? [ JSON.parse(JSON.stringify(this.kingPos[color])) ] //need to duplicate! - : [ ]; - this.undo(move); - return res; - } + ///////////////// + // MOVES PLAYING // Apply a move on board static PlayOnBoard(board, move) @@ -774,8 +942,8 @@ class ChessRules } } - // After move is undo-ed, un-update variables (flags are reset) - // TODO: more symmetry, by storing flags increment in move... + // After move is undo-ed *and flags resetted*, un-update other variables + // TODO: more symmetry, by storing flags increment in move (?!) unupdateVariables(move) { // (Potentially) Reset king position @@ -784,22 +952,16 @@ class ChessRules this.kingPos[c] = [move.start.x, move.start.y]; } - // Hash of position+flags+turn after a move is played (to detect repetitions) - getHashState() - { - return hex_md5(this.getFen()); - } - play(move, ingame) { // DEBUG: // if (!this.states) this.states = []; -// if (!ingame) this.states.push(JSON.stringify(this.board)); +// if (!ingame) this.states.push(this.getFen()); if (!!ingame) move.notation = [this.getNotation(move), this.getLongNotation(move)]; - move.flags = JSON.stringify(this.flags); //save flags (for undo) + move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo) this.updateVariables(move); this.moves.push(move); this.epSquares.push( this.getEpSquare(move) ); @@ -807,7 +969,10 @@ class ChessRules V.PlayOnBoard(this.board, move); if (!!ingame) - move.hash = this.getHashState(); + { + // Hash of current game state *after move*, to detect repetitions + move.hash = hex_md5(this.getFen(); + } } undo(move) @@ -817,15 +982,15 @@ class ChessRules this.epSquares.pop(); this.moves.pop(); this.unupdateVariables(move); - this.parseFlags(JSON.parse(move.flags)); + this.disaggregateFlags(JSON.parse(move.flags)); // DEBUG: -// if (JSON.stringify(this.board) != this.states[this.states.length-1]) +// if (this.getFen() != this.states[this.states.length-1]) // debugger; // this.states.pop(); } - ////////////// + /////////////// // END OF GAME // Check for 3 repetitions (position + flags + turn) @@ -872,11 +1037,12 @@ class ChessRules return color == "w" ? "0-1" : "1-0"; } - //////// - //ENGINE + /////////////// + // ENGINE PLAY // Pieces values - static get VALUES() { + static get VALUES() + { return { 'p': 1, 'r': 5, @@ -887,18 +1053,14 @@ class ChessRules }; } - static get INFINITY() { - return 9999; //"checkmate" (unreachable eval) - } + // "Checkmate" (unreachable eval) + static get INFINITY() { return 9999; } - static get THRESHOLD_MATE() { - // At this value or above, the game is over - return V.INFINITY; - } + // At this value or above, the game is over + static get THRESHOLD_MATE() { return V.INFINITY; } - static get SEARCH_DEPTH() { - return 3; //2 for high branching factor, 4 for small (Loser chess) - } + // Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.) + static get SEARCH_DEPTH() { return 3; } // Assumption: at least one legal move // NOTE: works also for extinction chess because depth is 3... @@ -917,7 +1079,7 @@ class ChessRules let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE); if (!finish && !this.atLeastOneMove()) { - // Try mate (for other variants) + // Test mate (for other variants) const score = this.checkGameEnd(); if (score != "1/2") finish = true; @@ -946,7 +1108,7 @@ class ChessRules evalPos = this.evalPosition() else { - // Work with scores for Loser variant + // Working with scores is more accurate (necessary for Loser variant) const score = this.checkGameEnd(); evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval); } @@ -968,7 +1130,6 @@ class ChessRules this.undo(moves1[i]); } moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); }); - //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; })); let candidates = [0]; //indices of candidates moves for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++) @@ -1067,113 +1228,9 @@ class ChessRules return evaluation; } - //////////// - // FEN utils - - // Setup the initial random (assymetric) position - static GenRandInitFen() - { - let pieces = { "w": new Array(8), "b": new Array(8) }; - // Shuffle pieces on first and last rank - for (let c of ["w","b"]) - { - let positions = _.range(8); - - // Get random squares for bishops - let randIndex = 2 * _.random(3); - let bishop1Pos = positions[randIndex]; - // The second bishop must be on a square of different color - let randIndex_tmp = 2 * _.random(3) + 1; - let bishop2Pos = positions[randIndex_tmp]; - // Remove chosen squares - positions.splice(Math.max(randIndex,randIndex_tmp), 1); - positions.splice(Math.min(randIndex,randIndex_tmp), 1); - - // Get random squares for knights - randIndex = _.random(5); - let knight1Pos = positions[randIndex]; - positions.splice(randIndex, 1); - randIndex = _.random(4); - let knight2Pos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Get random square for queen - randIndex = _.random(3); - let queenPos = positions[randIndex]; - positions.splice(randIndex, 1); - - // Rooks and king positions are now fixed, because of the ordering rook-king-rook - let rook1Pos = positions[0]; - let kingPos = positions[1]; - let rook2Pos = positions[2]; - - // Finally put the shuffled pieces in the board array - pieces[c][rook1Pos] = 'r'; - pieces[c][knight1Pos] = 'n'; - pieces[c][bishop1Pos] = 'b'; - pieces[c][queenPos] = 'q'; - pieces[c][kingPos] = 'k'; - pieces[c][bishop2Pos] = 'b'; - pieces[c][knight2Pos] = 'n'; - pieces[c][rook2Pos] = 'r'; - } - return pieces["b"].join("") + - "/pppppppp/8/8/8/8/PPPPPPPP/" + - pieces["w"].join("").toUpperCase() + - " 1111 w"; //add flags + turn - } - - // Return current fen according to pieces+colors state - getFen() - { - return this.getBaseFen() + " " + this.getFlagsFen() + " " + this.turn; - } - - // Position part of the FEN string - getBaseFen() - { - let fen = ""; - for (let i=0; i<V.size.x; i++) - { - let emptyCount = 0; - for (let j=0; j<V.size.y; j++) - { - if (this.board[i][j] == V.EMPTY) - emptyCount++; - else - { - if (emptyCount > 0) - { - // Add empty squares in-between - fen += emptyCount; - emptyCount = 0; - } - fen += V.board2fen(this.board[i][j]); - } - } - if (emptyCount > 0) - { - // "Flush remainder" - fen += emptyCount; - } - if (i < V.size.x - 1) - fen += "/"; //separate rows - } - return fen; - } - - // Flags part of the FEN string - getFlagsFen() - { - let fen = ""; - // Add castling flags - for (let i of ['w','b']) - { - for (let j=0; j<2; j++) - fen += (this.castleFlags[i][j] ? '1' : '0'); - } - return fen; - } + ///////////////////////// + // MOVES + GAME NOTATION + ///////////////////////// // Context: just before move is played, turn hasn't changed getNotation(move) @@ -1182,7 +1239,7 @@ class ChessRules return (move.end.y < move.start.y ? "0-0-0" : "0-0"); // Translate final square - const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x); + const finalSquare = V.CoordsToSquare(move.end); const piece = this.getPiece(move.start.x, move.start.y); if (piece == V.PAWN) @@ -1213,10 +1270,8 @@ class ChessRules // Complete the usual notation, may be required for de-ambiguification getLongNotation(move) { - const startSquare = - String.fromCharCode(97 + move.start.y) + (V.size.x-move.start.x); - const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x); - return startSquare + finalSquare; //not encoding move. But short+long is enough + // Not encoding move. But short+long is enough + return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end); } // The score is already computed when calling this function diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 0ab12d62..9d7bb1fb 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -27,10 +27,7 @@ Vue.component('my-game', { watch: { problem: function(p, pp) { // 'problem' prop changed: update board state - // TODO: FEN + turn + flags + rappel instructions / solution on click sous l'échiquier - // TODO: trouver moyen de passer la situation des reserves pour Crazyhouse, - // et l'état des captures pour Grand... bref compléter le descriptif de l'état. - this.newGame("problem", p.fen, p.fen.split(" ")[2]); + this.newGame("problem", p.fen, V.ParseFen(p.fen).turn); }, }, render(h) { @@ -105,7 +102,7 @@ Vue.component('my-game', { : (smallScreen ? 31 : 37); if (this.mode == "human") { - let connectedIndic = h( + const connectedIndic = h( 'div', { "class": { @@ -122,7 +119,7 @@ Vue.component('my-game', { ); elementArray.push(connectedIndic); } - let turnIndic = h( + const turnIndic = h( 'div', { "class": { @@ -138,7 +135,7 @@ Vue.component('my-game', { } ); elementArray.push(turnIndic); - let settingsBtn = h( + const settingsBtn = h( 'button', { on: { click: this.showSettings }, @@ -157,7 +154,27 @@ Vue.component('my-game', { [h('i', { 'class': { "material-icons": true } }, "settings")] ); elementArray.push(settingsBtn); - let choices = h('div', + if (this.mode == "problem") + { + // Show problem instructions + elementArray.push( + h('div', + { + attrs: { id: "instructions-div" }, + "class": { "section-content": true }, + }, + [ + h('p', + { + attrs: { id: "problem-instructions" }, + domProps: { innerHTML: this.problem.instructions } + } + ) + ] + ) + ); + } + const choices = h('div', { attrs: { "id": "choices" }, 'class': { 'row': true }, @@ -195,7 +212,7 @@ Vue.component('my-game', { const lm = this.vr.lastMove; const showLight = this.hints && (this.mode!="idle" || this.cursor==this.vr.moves.length); - let gameDiv = h('div', + const gameDiv = h('div', { 'class': { 'game': true }, }, @@ -410,7 +427,6 @@ Vue.component('my-game', { ); elementArray.push(reserves); } - const eogMessage = this.getEndgameMessage(this.score); const modalEog = [ h('input', { @@ -437,7 +453,7 @@ Vue.component('my-game', { { attrs: { "id": "eogMessage" }, "class": { "section": true }, - domProps: { innerHTML: eogMessage }, + domProps: { innerHTML: this.endgameMessage }, } ) ] @@ -713,7 +729,7 @@ Vue.component('my-game', { actionArray ); elementArray.push(actions); - if (this.score != "*") + if (this.score != "*" && this.pgnTxt.length > 0) { elementArray.push( h('div', @@ -749,6 +765,32 @@ Vue.component('my-game', { } else if (this.mode != "idle") { + if (this.mode == "problem") + { + // Show problem solution (on click) + elementArray.push( + h('div', + { + attrs: { id: "solution-div" }, + "class": { "section-content": true }, + }, + [ + h('h3', + { + domProps: { innerHTML: "Show solution" }, + on: { click: "toggleShowSolution" } + } + ), + h('p', + { + attrs: { id: "problem-solution" }, + domProps: { innerHTML: this.problem.solution } + } + ) + ] + ) + ); + } // Show current FEN elementArray.push( h('div', @@ -760,7 +802,7 @@ Vue.component('my-game', { h('p', { attrs: { id: "fen-string" }, - domProps: { innerHTML: this.vr.getFen() } + domProps: { innerHTML: this.vr.getBaseFen() } } ) ] @@ -790,6 +832,24 @@ Vue.component('my-game', { elementArray ); }, + computed: { + endgameMessage: function() { + let eogMessage = "Unfinished"; + switch (this.score) + { + case "1-0": + eogMessage = "White win"; + break; + case "0-1": + eogMessage = "Black win"; + break; + case "1/2": + eogMessage = "Draw"; + break; + } + return eogMessage; + }, + }, created: function() { const url = socketUrl; const humanContinuation = (localStorage.getItem("variant") === variant); @@ -907,6 +967,12 @@ Vue.component('my-game', { }; }, methods: { + toggleShowSolution: function() { + let problemSolution = document.getElementById("problem-solution"); + problemSolution.style.display = problemSolution.style.display == "none" + ? "block" + : "none"; + }, download: function() { let content = document.getElementById("pgn-game").innerHTML; content = content.replace(/<br>/g, "\n"); @@ -917,35 +983,22 @@ Vue.component('my-game', { encodeURIComponent(content); downloadAnchor.click(); }, - endGame: function(score) { - this.score = score; + showScoreMsg: function() { let modalBox = document.getElementById("modal-eog"); modalBox.checked = true; + setTimeout(() => { modalBox.checked = false; }, 2000); + }, + endGame: function(score) { + this.score = score; + this.showScoreMsg(); // Variants may have special PGN structure (so next function isn't defined here) this.pgnTxt = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode); - setTimeout(() => { modalBox.checked = false; }, 2000); if (["human","computer"].includes(this.mode)) this.clearStorage(); this.mode = "idle"; this.cursor = this.vr.moves.length; //to navigate in finished game this.oppid = ""; }, - getEndgameMessage: function(score) { - let eogMessage = "Unfinished"; - switch (this.score) - { - case "1-0": - eogMessage = "White win"; - break; - case "0-1": - eogMessage = "Black win"; - break; - case "1/2": - eogMessage = "Draw"; - break; - } - return eogMessage; - }, setStorage: function() { if (this.mode=="human") { @@ -1042,18 +1095,30 @@ Vue.component('my-game', { if (!!storageVariant && storageVariant !== variant) return alert("Finish your " + storageVariant + " game first!"); // Send game request and wait.. - this.seek = true; try { this.conn.send(JSON.stringify({code:"newgame", fen:fen})); } catch (INVALID_STATE_ERR) { return; //nothing achieved } + this.seek = true; let modalBox = document.getElementById("modal-newgame"); modalBox.checked = true; setTimeout(() => { modalBox.checked = false; }, 2000); return; } - if (this.mode == "computer" && mode == "human") { } + if (mode == "computer" && !continuation) + { + const storageVariant = localStorage.getItem("comp-variant"); + if (!!storageVariant && storageVariant !== variant) + { + if (!confirm("Unfinished " + storageVariant + + " computer game will be erased")) + { + return; + } + } + } + if (this.mode == "computer" && mode == "human") { // Save current computer game to resume it later this.setStorage(); @@ -1062,45 +1127,42 @@ Vue.component('my-game', { this.score = "*"; this.pgnTxt = ""; //redundant with this.score = "*", but cleaner this.mode = mode; - this.incheck = continuation - ? this.vr - : []; - this.fenStart = (continuation ? localStorage.getItem("fenStart") : fen); + if (continuation && moves.length > 0) //NOTE: "continuation": redundant test + { + const lastMove = moves[moves.length-1]; + this.vr.undo(lastMove); + this.incheck = this.vr.getCheckSquares(lastMove); + this.vr.play(lastMove, "ingame"); + } + else + this.incheck = []; + if (continuation) + { + const prefix = (mode=="computer" ? "comp-" : ""); + this.fenStart = localStorage.getItem(prefix+"fenStart"); + } + else + this.fenStart = fen; if (mode=="human") { - - - -//TODO: refactor this. (for computer mode too), lastMove getCheckSquares... - - - - // Opponent found! if (!continuation) //not playing sound on game continuation { if (this.sound >= 1) - new Audio("/sounds/newgame.mp3").play().then(() => {}).catch(err => {}); + new Audio("/sounds/newgame.mp3").play().catch(err => {}); document.getElementById("modal-newgame").checked = false; } this.oppid = oppId; this.oppConnected = !continuation; this.mycolor = color; this.seek = false; - if (!!moves && moves.length > 0) //imply continuation - { - const lastMove = moves[moves.length-1]; - this.vr.undo(lastMove); - this.incheck = this.vr.getCheckSquares(lastMove); - this.vr.play(lastMove, "ingame"); - } this.setStorage(); //in case of interruptions } else if (mode == "computer") { this.mycolor = Math.random() < 0.5 ? 'w' : 'b'; if (this.mycolor == 'b') - setTimeout(this.playComputerMove, 500); + this.playComputerMove(); } //else: against a (IRL) friend or problem solving: nothing more to do }, @@ -1250,7 +1312,7 @@ Vue.component('my-game', { squares.item(i).style.zIndex = "auto"; movingPiece.style = {}; //required e.g. for 0-0 with KR swap this.play(move); - }, 200); + }, 250); }, play: function(move, programmatic) { if (!move) @@ -1269,7 +1331,7 @@ Vue.component('my-game', { if (this.mode == "human" && this.vr.turn == this.mycolor) this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid})); if (this.sound == 2) - new Audio("/sounds/chessmove1.mp3").play().then(() => {}).catch(err => {}); + new Audio("/sounds/chessmove1.mp3").play().catch(err => {}); if (this.mode != "idle") { this.incheck = this.vr.getCheckSquares(move); //is opponent in check? @@ -1286,10 +1348,19 @@ Vue.component('my-game', { { const eog = this.vr.checkGameOver(); if (eog != "*") - this.endGame(eog); + { + if (["human","computer"].includes(this.mode)) + this.endGame(eog); + else + { + // Just show score on screen (allow undo) + this.score = eog; + this.showScoreMsg(); + } + } } if (this.mode == "computer" && this.vr.turn != this.mycolor) - setTimeout(this.playComputerMove, 500); + this.playComputerMove; }, undo: function() { // Navigate after game is over diff --git a/public/stylesheets/variant.sass b/public/stylesheets/variant.sass index f8047560..6472e3d8 100644 --- a/public/stylesheets/variant.sass +++ b/public/stylesheets/variant.sass @@ -267,3 +267,6 @@ ul:not(.browser-default) > li .mistake-newproblem color: #663300 + +#problem-solution + display: none -- 2.44.0