Finish 8-pieces implementation
authorBenjamin Auder <benjamin.auder@somewhere>
Fri, 8 May 2026 22:21:25 +0000 (00:21 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Fri, 8 May 2026 22:21:25 +0000 (00:21 +0200)
12 files changed:
index.html
js/app.js
js/base_rules.js
js/sanitize.js [deleted file]
js/server.js
variants/Allmate/class.js
variants/Ambiguous/class.js
variants/Atomic/class.js
variants/Chaining/class.js
variants/Convert/class.js
variants/Dynamo/class.js
variants/Eightpieces/class.js

index 5baa1bb..5b4c35f 100644 (file)
     </main>
 
     <script src="/js/parameters.js"></script>
-    <script src="/js/sanitize.js"></script>
     <script src="/js/variants.js"></script>
     <script src="/js/app.js"></script>
   </body>
index 016a1c3..950d2d3 100644 (file)
--- a/js/app.js
+++ b/js/app.js
@@ -90,8 +90,7 @@ function h(tag, attrs, children) {
 
 function setName() {
   // 'onChange' event on name input text field [HTML]
-  const name = $.getElementById("myName").value;
-  localStorage.setItem("name", sanitize(name, 30));
+  localStorage.setItem("name", $.getElementById("myName").value);
 }
 
 // Turn a "tab" on, and "close" all others
@@ -573,7 +572,8 @@ async function initializeGame(obj) {
   const options = obj.options || {};
 
   // 1. Dynamic loading of variant js module
-  const module = await import('/' + await manifest(`variants/${obj.vname}/class.js`));
+  const module =
+    await import('/' + await manifest(`variants/${obj.vname}/class.js`));
   window.V = module.default;
 
   // Export aliases in global scope (used by variants classes)
@@ -605,7 +605,11 @@ async function initializeGame(obj) {
 
   // Create SVG icons with a string, inserted securely.
   const infoIcon = h('div', { id: 'upLeftInfos', onclick: toggleGameInfos });
-  infoIcon.innerHTML = `<svg viewBox="0.5 0.5 100 100"><path d="M50.5,0.5c-27.614,0-50,22.386-50,50c0,27.614,22.386,50,50,50s50-22.386,50-50C100.5,22.886,78.114,0.5,50.5,0.5z M60.5,85.5h-20v-40h20V85.5z M50.5,35.5c-5.523,0-10-4.477-10-10s4.477-10,10-10c5.522,0,10,4.477,10,10S56.022,35.5,50.5,35.5z"/></svg>`;
+  infoIcon.innerHTML = `<svg viewBox="0.5 0.5 100 100">\
+<path d="M50.5,0.5c-27.614,0-50,22.386-50,50c0,27.614,22.386,50,50,50s50-\
+22.386,50-50C100.5,22.886,78.114,0.5,50.5,0.5z M60.5,85.5h-20v-40h20V85.5z \
+M50.5,35.5c-5.523,0-10-4.477-10-10s4.477-10,10-10c5.522,0,10,4.477,10,\
+10S56.022,35.5,50.5,35.5z"/></svg>`;
 
   const stopIcon = h('div', { id: 'upRightStop', onclick: confirmStopGame });
   stopIcon.innerHTML = `<svg viewBox="0 0 533.333 533.333" xmlns="http://www.w3.org/2000/svg">
index 4fc46ac..5ff92f8 100644 (file)
@@ -156,7 +156,7 @@ export default class ChessRules {
         ],
         vanish: []
       });
-      res.drag = {c: this.captured.c, p: this.captured.p};
+//      res.drag = {c: this.captured.c, p: this.captured.p}; //TODO?
       return res;
     }
     return null;
@@ -909,9 +909,14 @@ export default class ChessRules {
       pieceWidth = this.getPieceWidth(r.width);
       const cd = this.idToCoords(e.target.id);
       if (cd) {
-        const move = this.doClick(cd);
-        if (move)
-          this.buildMoveStack(move, r);
+        const move_s = this.doClick(cd);
+        if (move_s) {
+          if (Array.isArray(move_s)) //8-pieces (at least.. only?)
+            this.showChoices(move_s, r);
+          else
+            // Usual case, single move
+            this.buildMoveStack(move_s, r);
+        }
         else if (!this.clickOnly) {
           const [x, y] = Object.values(cd);
           if (typeof x != "number")
@@ -1535,7 +1540,7 @@ export default class ChessRules {
 
   // All possible moves from selected square
   // TODO: generalize usage if arg "color" (e.g. Checkered)
-  getPotentialMovesFrom([x, y], color) {
+  getPotentialMovesFrom([x, y], color, noPP) {
     if (this.subTurnTeleport == 2)
       return [];
     if (typeof x == "string")
@@ -1548,7 +1553,9 @@ export default class ChessRules {
       Array.prototype.push.apply(moves, this.getEnpassantCaptures([x, y]));
     if (this.isKing(0, 0, piece) && this.hasCastle)
       Array.prototype.push.apply(moves, this.getCastleMoves([x, y]));
-    return this.postProcessPotentialMoves(moves);
+    if (!noPP)
+      moves = this.postProcessPotentialMoves(moves);
+    return moves;
   }
 
   postProcessPotentialMoves(moves) {
diff --git a/js/sanitize.js b/js/sanitize.js
deleted file mode 100644 (file)
index fe553f8..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-const sanitize = function(str, maxLength = 100, relax = false)
-{
-  if (typeof str !== 'string') return "";
-  // 1. Cut string to avoid memory overload
-  let cleaned = str.substring(0, maxLength);
-  // 2. Replace special characters by HTML entities
-  const map = {
-    '&': '&amp;',
-    '<': '&lt;',
-    '>': '&gt;',
-    '"': '&quot;',
-    "'": '&#039;'
-  };
-  const regexp = relax ? /[<>]/g : /[&<>"']/g
-  return cleaned.replace(regexp, m => map[m]);
-}
-
-// Next line for usage on server (Node.js)
-if (typeof window === 'undefined') module.exports = sanitize;
index 85e6066..2c0211e 100644 (file)
@@ -1,5 +1,4 @@
 const params = require("./parameters.js");
-const sanitize = require("./sanitize.js");
 const WebSocket = require("ws");
 const wss = new WebSocket.Server({
   port: params.socket_port,
@@ -93,17 +92,19 @@ wss.on("connection", (socket, req) => {
     }
 
     // Basic security on recurrent fields
-    if (obj.vname) {
-      obj.vname = sanitize(obj.vname, 50);
-      if (obj.vname != "_random" && !variants.find(v => v.name == obj.vname))
+    if (
+      obj.vname &&
+      obj.vname != "_random" &&
+      !variants.find(v => v.name == obj.vname)
+    ) {
         return; //unknown variant name
     }
-    if (obj.name) //TODO: probably overkill..
-      obj.name = sanitize(obj.name, 30);
+    if (obj.name)
+      obj.name = obj.name.substring(0, 32);
     if (obj.fen)
-      obj.fen = sanitize(obj.fen, 500, true);
+      obj.fen = obj.fen.substring(0, 1024);
     if (obj.gid)
-      obj.gid = sanitize(obj.gid, 20);
+      obj.gid = obj.gid.substring(0, 16);
 
     switch (obj.code) {
       // Send challenge (may trigger game creation)
@@ -241,7 +242,7 @@ wss.on("connection", (socket, req) => {
         if (
           !games[obj.gid] ||
           !Array.isArray(obj.moves) ||
-          obj.moves.length > 20
+          obj.moves.length > 32
         ) {
           return;
         }
index eb435e8..1de9327 100644 (file)
@@ -64,13 +64,13 @@ export default class AllmateRules extends ChessRules {
     for (let i=0; i<this.size.x; i++) {
       for (let j=0; j<this.size.y; j++) {
         if (this.getColor(i, j) == myColor) {
-          const movesIJ = super.getPotentialMovesFrom([i, j], myColor);
+          const movesIJ = super.getPotentialMovesFrom([i, j]); //, myColor);
           for (let move of movesIJ) {
             this.playOnBoard(move);
             let testSquare = [x, y];
-            if (i == x && j == y) {
+            if (i == x && j == y)
               // The mated-candidate has moved itself
-              testSquare = [move.end.x, move.end.y]; }
+              testSquare = [move.end.x, move.end.y];
             const res = this.underAttack(testSquare, [color]);
             this.undoOnBoard(move);
             if (!res)
index 9d73242..5420856 100644 (file)
@@ -92,7 +92,7 @@ export default class AmbiguousRules extends ChessRules {
         }
       }
     }
-    const moves = super.getPotentialMovesFrom([x, y], oppCol);
+    const moves = super.getPotentialMovesFrom([x, y]); //, oppCol);
     return moves.filter(m => m.end.x == target.x && m.end.y == target.y);
   }
 
index 83f2a2b..d8d64f8 100644 (file)
@@ -30,7 +30,7 @@ export default class AtomicRules extends ChessRules {
     return super.canIplay(x, y);
   }
 
-  getPotentialMovesFrom([x, y], color) {
+  getPotentialMovesFrom([x, y]) {
     if (this.options["rempawn"] && this.movesCount == 0) {
       if ([1, 6].includes(x)) {
         const c = this.getColor(x, y);
@@ -52,7 +52,7 @@ export default class AtomicRules extends ChessRules {
       }
       return [];
     }
-    return super.getPotentialMovesFrom([x, y], color);
+    return super.getPotentialMovesFrom([x, y]);
   }
 
   doClick(square) {
index f4794a2..7aaec70 100644 (file)
@@ -86,7 +86,7 @@ export default class ChainingRules extends ChessRules {
     return super.getPiece(x, y);
   }
 
-  getPotentialMovesFrom([x, y], color) {
+  getPotentialMovesFrom([x, y]) {
     const L = this.lastMoveEnd.length;
     if (
       L >= 1 &&
@@ -95,7 +95,7 @@ export default class ChainingRules extends ChessRules {
       // A self-capture was played: wrong square
       return [];
     }
-    return super.getPotentialMovesFrom([x, y], color);
+    return super.getPotentialMovesFrom([x, y]);
   }
 
   isLastMove(move) {
index ecaa684..f2a0b13 100644 (file)
@@ -87,7 +87,7 @@ export default class ConvertRules extends ChessRules {
     return super.getPiece(x, y);
   }
 
-  getPotentialMovesFrom([x, y], color) {
+  getPotentialMovesFrom([x, y]) {
     const L = this.lastMoveEnd.length;
     if (
       L >= 1 &&
@@ -96,7 +96,7 @@ export default class ConvertRules extends ChessRules {
       // A capture was played: wrong square
       return [];
     }
-    return super.getPotentialMovesFrom([x, y], color);
+    return super.getPotentialMovesFrom([x, y]);
   }
 
   underAttack_aux([x, y], color, explored) {
index de36887..3ec2c3f 100644 (file)
@@ -205,7 +205,7 @@ export default class DynamoRules extends ChessRules {
       // Free to play any move (if piece of my color):
       let moves = [];
       if (sqCol == color) {
-        moves = super.getPotentialMovesFrom([x, y])
+        moves = super.getPotentialMovesFrom([x, y]);
         if (this.canReachBorder(x, y))
           this.addExitMove(moves, [x, y], kp);
       }
index 62e718f..46d66d2 100644 (file)
@@ -23,18 +23,18 @@ export default class EightpiecesRules extends ChessRules {
       options.push('m');
     if (y < this.size.y)
       options.push('e');
-    if (x < this.size.x) {
+    if (x < this.size.x - 1) {
       options.push('g');
       if (y > 0)
         options.push('h');
-      if (y < this.size.y)
+      if (y < this.size.y - 1)
         options.push('f');
     }
     if (x > 0) {
       options.push('c');
       if (y > 0)
         options.push('o');
-      if (y < this.size.y)
+      if (y < this.size.y - 1)
         options.push('d');
     }
     return options;
@@ -62,7 +62,7 @@ export default class EightpiecesRules extends ChessRules {
       "/pppppppp/8/8/8/8/PPPPPPPP/" +
       s.w.join("").replace('l', random > 0 ? 'c' : 'd').toUpperCase();
     return {
-      fen: 'jfs1kb1r/1P3ppp/3p1q2/p7/2P5/8/P2PPPPP/J1SQKBNR', //     fen,
+      fen,
       o: {flags: s.flags}
     };
   }
@@ -94,7 +94,7 @@ export default class EightpiecesRules extends ChessRules {
 
   pieces(color, x, y) {
     const mirror = (this.playerColor == 'b');
-    return Object.assign({
+    return {
       'j': {
         "class": "jailer",
         moves: [
@@ -156,7 +156,8 @@ export default class EightpiecesRules extends ChessRules {
           {steps: [[-1, -1]]}
         ]
       },
-    }, super.pieces(color, x, y));
+      ...super.pieces(color, x, y)
+    };
   }
 
   canIplay(x, y) {
@@ -215,16 +216,49 @@ export default class EightpiecesRules extends ChessRules {
     return res;
   }
 
-// TODO: sentry nudge
-
-  // after pushedTo, if lancer : allow reorient + move, or just reorient (move to king) if stuck
-
-  // later, if stuck, allow reorient only (just click)
- // doClick(coords) { TODO
+  // Reorient immobilized or stuck lancer
+  doClick(coords) {
+    if (typeof coords.x != "number")
+      return null; //click on reserves
+    if (this.board[coords.x][coords.y] != "") {
+      const p = this.getPiece(coords.x, coords.y),
+            c = this.getColor(coords.x, coords.y);
+      const lopts = this.getLancerOptions(coords.x, coords.y);
+      if (
+        V.LANCERS.includes(p) &&
+        (
+          this.pushedTo.x < 0 && //not just pushed
+          !lopts.includes(p) //can't move
+        )
+        ||
+        (
+          this.pieces()['j'].moves[0].steps.some(s => {
+            const [i, j] = [coords.x + s[0], coords.y + s[1]];
+            return (
+              this.onBoard(i, j) &&
+              this.getPiece(i, j) == 'j' &&
+              this.getColor(i, j) != c
+            );
+          })
+        )
+      ) {
+        return lopts.filter(o => o != p).map(o => {
+          return new Move({
+            appear: [ new PiPo({x: coords.x, y: coords.y, p: o, c: c}) ],
+            vanish: [ new PiPo({x: coords.x, y: coords.y, p: p, c: c}) ]
+          });
+        });
+      }
+    }
+    return null;
+  }
 
-  getPotentialMovesFrom([x, y], color) {
+  getPotentialMovesFrom([x, y]) {
+    const p = this.getPiece(x, y);
+    let color = this.getColor(x, y);
+    let oppCol = C.GetOppTurn(color)
     if (this.pushFrom.x < 0 || this.pushedTo.x >= 0) {
-      let smoves = super.getPotentialMovesFrom([x, y], color);
+      let smoves = super.getPotentialMovesFrom([x, y]);
       // Forbid direction x,y --> pushFrom if x,y == pushedTo
       if (x == this.pushedTo.x && y == this.pushedTo.y) {
         smoves = smoves.filter(m => {
@@ -238,39 +272,111 @@ export default class EightpiecesRules extends ChessRules {
             [sx / divideBy, sy / divideBy]
           ) );
         });
+        if (V.LANCERS.includes(p)) {
+          // Allow all other directions without reorient
+          const ls = this.pieces()[p].both[0].steps[0];
+          for (const lCode of V.LANCERS) {
+            const s = this.pieces()[lCode].both[0].steps[0];
+            if (s[0] != ls[0] || s[1] != ls[1]) {
+              this.board[x][y] = color + lCode;
+              super.findDestSquares([x, y], {}).forEach(r => {
+                let mv = new Move({
+                  appear: [
+                    new PiPo({x: r.sq[0], y: r.sq[1], p: lCode, c: color})],
+                  vanish: [
+                    new PiPo({x: x, y: y, p: p, c: color})],
+                  noReorient: true
+                });
+                if (this.board[r.sq[0]][r.sq[1]] != "") {
+                  mv.vanish.push(
+                    new PiPo({
+                      x: r.sq[0],
+                      y: r.sq[1],
+                      p: this.getPiece(r.sq[0], r.sq[1]),
+                      c: oppCol
+                    })
+                  );
+                }
+                smoves.push(mv);
+              });
+            }
+          }
+          this.board[x][y] = color + p;
+          // Add reorient-only moves, if stuck:
+          const lopts = this.getLancerOptions(x, y);
+          if (!lopts.includes(p)) {
+            const kp = this.searchKingPos(color)[0];
+            Array.prototype.push.apply(smoves, lopts.map(o => {
+              return new Move({
+                appear: [ new PiPo({x: x, y: y, p: o, c: color}) ],
+                vanish: [ new PiPo({x: x, y: y, p: p, c: color}) ],
+                end: {x: kp[0], y: kp[1]}
+              });
+            }) );
+          }
+        }
       }
-
-      // TODO: lancer special case, move as a queen after push
-
       return smoves.concat(this.getPassMoves(x, y));
     }
     // pushFrom.x >= 0 && pushedTo.x < 0
     if (x != this.pushFrom.x || y != this.pushFrom.y)
       return [];
     // After sentry "attack": move enemy as if it was ours
-    const p = this.getPiece(x, y);
-    this.board[x][y] = this.turn + p;
-    let pmoves = super.getPotentialMovesFrom([x, y], this.turn);
-    const oppCol = C.GetOppTurn(this.turn)
+    [color, oppCol] = [oppCol, color];
+    this.board[x][y] = color + p;
+    let pmoves = super.getPotentialMovesFrom([x, y], color, true)
+      .filter(m => m.appear.length > 0); //exclude sentry "captures"
+    if (V.LANCERS.includes(p)) {
+      pmoves.forEach(m => m.noReorient = true);
+      // Allow all other steps by 1 square (nudge)
+      const ls = this.pieces()[p].both[0].steps[0];
+      let nextP = p;
+      for (const lCode of V.LANCERS) {
+        const s = this.pieces()[lCode].both[0].steps[0];
+        if (
+          (s[0] != ls[0] || s[1] != ls[1]) &&
+          this.onBoard(x + s[0], y + s[1]) &&
+          this.board[x + s[0]][y + s[1]] == ""
+        ) {
+          let mv = new Move({
+            appear: [new PiPo({x: x + s[0], y: y + s[1], p: lCode, c: color})],
+            vanish: [new PiPo({x: x, y: y, p: p, c: color})]
+          });
+          mv.noReorient = true;
+          pmoves.push(mv);
+        }
+      }
+    }
     this.board[x][y] = oppCol + p;
     pmoves.forEach(m => {
       m.appear[0].c = m.vanish[0].c = oppCol;
       m.appear.push( new PiPo({x:x, y:y, p:'s', c:this.turn}) );
     });
-    return pmoves;
+    return this.postProcessPotentialMoves(pmoves);
   }
 
   postProcessPotentialMoves(moves) {
     moves = super.postProcessPotentialMoves(moves);
     let finalMoves = [];
     for (const m of moves) {
+      // Drop lancers "self captures" (not from sentry push):
+      if (
+        !m.noReorient &&
+        m.vanish.length == 2 &&
+        m.vanish[0].c == m.vanish[1].c
+      ) {
+        continue;
+      }
       // Reorient a lancer after drop or regular move
       if (
-        (m.vanish.length == 0 && ['c', 'g'].includes(m.appear[0].p)) ||
+        !m.noReorient &&
         (
-          (m.vanish.length > 0 && V.LANCERS.includes(m.vanish[0].p)) &&
-          // Next line test checks that the lancer wasn't just pushed away
-          (m.start.x != this.pushedTo.x || m.start.y != this.pushedTo.y)
+          (m.vanish.length == 0 && ['c', 'g'].includes(m.appear[0].p)) ||
+          (
+            (m.vanish.length > 0 && V.LANCERS.includes(m.vanish[0].p)) &&
+            // Next line test checks that the lancer wasn't just pushed away
+            (m.start.x != this.pushedTo.x || m.start.y != this.pushedTo.y)
+          )
         )
       ) {
         this.getLancerOptions(m.end.x, m.end.y).forEach(o => {
@@ -323,8 +429,8 @@ export default class EightpiecesRules extends ChessRules {
   underAttack([x, y], oppCols) {
     if (super.underAttack([x, y], oppCols))
       return true;
-    // TODO: check enemy sentry(ies), for each, check all of our own pieces which attack the square (if belonging to opponent!). Then, call :
     const oppCol = oppCols[0];
+    const color = C.GetOppTurn(oppCol);
     for (let i=0; i < this.size.x; i++) {
       for (let j=0; j < this.size.y; j++) {
         if (
@@ -332,12 +438,26 @@ export default class EightpiecesRules extends ChessRules {
           this.getPiece(i, j) == 's' &&
           this.getColor(i, j) == oppCol
         ) {
-          this.pieces()['s'].both[0].steps.forEach(s => {
-            let ii = i + s[0];
-            // TODO.........
-            if (true)
-              return true;
-          });
+          this.board[i][j] = oppCol + 'b';
+          // Find enemy sentries "attacks":
+          const rs = super.findDestSquares([i, j], {attackOnly: true});
+          this.board[i][j] = oppCol + 's';
+          // Can any of these pieces capture our king?
+          for (const r of rs) {
+            const p = this.getPiece(r.sq[0], r.sq[1]);
+            if (['j', 's'].includes(p))
+              continue;
+            const specs = this.pieces(oppCol, r.sq[0], r.sq[1])[p];
+            const steps = (specs.both || specs.attack)[0].steps;
+            let res = false;
+            for (const s of steps) {
+              let [ii, jj] = [r.sq[0] + s[0], r.sq[1] + s[1]];
+              while (this.onBoard(ii, jj) && this.board[ii][jj] == "")
+                [ii, jj] = [ii + s[0], jj + s[1]];
+              if (ii == x && jj == y)
+                return true;
+            }
+          }
         }
       }
     }