refactoring in progress
authorBenjamin Auder <benjamin.auder@somewhere>
Sun, 3 May 2026 19:35:36 +0000 (21:35 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Sun, 3 May 2026 19:35:36 +0000 (21:35 +0200)
71 files changed:
README.md
css/base_pieces.css [moved from base_pieces.css with 100% similarity]
css/common.css [moved from common.css with 100% similarity]
css/iOS_fix.css [moved from iOS_fix.css with 100% similarity]
index.html
js/app.js [moved from app.js with 76% similarity]
js/base_rules.js [moved from base_rules.js with 99% similarity]
js/parameters.js [new file with mode: 0644]
js/parameters.js.dist [moved from parameters.js.dist with 100% similarity]
js/sanitize.js [new file with mode: 0644]
js/server.js [moved from server.js with 80% similarity]
js/variants.js [moved from variants.js with 100% similarity]
start.sh
variants/Absorption/style.css
variants/Alice/style.css
variants/Align4/style.css
variants/Allmate/style.css
variants/Ambiguous/style.css
variants/Antiking2/style.css [changed from symlink to file mode: 0644]
variants/Antimatter/style.css
variants/Apocalypse/style.css
variants/Arena/style.css
variants/Atomic/style.css
variants/Avalanche/style.css
variants/Balaklava/style.css
variants/Balanced/style.css
variants/Bario/style.css
variants/Baroque/style.css
variants/Benedict/style.css
variants/Berolina/style.css
variants/Bicolour/style.css
variants/Brotherhood/style.css
variants/Cannibal/style.css
variants/Capablanca/style.css
variants/Capture/style.css
variants/Chaining/style.css
variants/Chakart/style.css
variants/Checkered/class.js
variants/Checkered/style.css
variants/Checkless/style.css
variants/Chess960/style.css
variants/Circular/style.css
variants/Clorange/style.css
variants/Convert/style.css
variants/Copycat/style.css
variants/Coregal/style.css
variants/Coronation/style.css
variants/Crazyhouse/style.css
variants/Crossing/style.css
variants/Cwda/style.css
variants/Cylinder/style.css
variants/Dark/style.css
variants/Diamond/style.css
variants/Dice/style.css
variants/Discoduel/style.css
variants/Doublearmy/style.css
variants/Doublemove/style.css
variants/Dynamo/style.css
variants/Eightpieces/class.js
variants/Eightpieces/style.css
variants/Giveaway/style.css
variants/Madrasi/style.css
variants/Progressive/style.css
variants/Recycle/style.css
variants/Refusal/style.css
variants/Rifle/style.css
variants/Sleepy/style.css
variants/Suction/style.css
variants/Teleport/style.css
variants/Zen/style.css
variants/_Antiking/style.css

index 0b900c5..5555f0d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -9,10 +9,15 @@ PHP + Node.js + npm.
 
 ## Usage
 
-Initialisation (done once):
+Initialisation (done once): retrieve 'binaries'
+(files binary or not which never change).
 
 ```./initialize.sh```
 
-You may want to edit the parameters.js file. Then:
+Rename and edit the parameters.js.dist file:
+
+```cp js/parameters.js.dist js/parameters.js```
+
+Finally:
 
 ```./start.sh``` (and later, ```./stop.sh```)
similarity index 100%
rename from base_pieces.css
rename to css/base_pieces.css
similarity index 100%
rename from common.css
rename to css/common.css
similarity index 100%
rename from iOS_fix.css
rename to css/iOS_fix.css
index 669b406..5baa1bb 100644 (file)
@@ -7,13 +7,13 @@
     <meta name="viewport"
           content="width=device-width, initial-scale=1"/>
     <link id="_common_css"
-          rel="stylesheet" href="/common.css"/>
+          rel="stylesheet" href="/css/common.css"/>
     <script type="text/javascript">
       if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
         // Apple brightness in SVG workaround
         // TODO: remove that.
         let link = document.createElement("link");
-        link.href = "iOS_fix.css";
+        link.href = "/css/iOS_fix.css";
         link.type = "text/css";
         link.rel = "stylesheet";
         link.media = "screen,print";
       </div>
     </main>
 
-    <script src="/parameters.js"></script>
-    <script src="/variants.js"></script>
-    <script src="/app.js"></script>
+    <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>
 
 </html>
similarity index 76%
rename from app.js
rename to js/app.js
index c5d5c7f..4a92707 100644 (file)
--- a/app.js
+++ b/js/app.js
@@ -3,12 +3,12 @@ let $ = document; //shortcut
 ///////////////////
 // Initialisations
 
-// https://stackoverflow.com/a/27747377/12660887
-function generateId (len) {
-  const dec2hex = (dec) => dec.toString(16).padStart(2, "0");
-  let arr = new Uint8Array(len / 2); //len/2 because 2 chars per hex value
-  window.crypto.getRandomValues(arr); //fill with random integers
-  return Array.from(arr, dec2hex).join('');
+function generateId(len) {
+  const chars =
+    "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+  const arr = new Uint8Array(len);
+  window.crypto.getRandomValues(arr);
+  return Array.from(arr, (byte) => chars[byte % chars.length]).join('');
 }
 
 // Populate variants dropdown list
@@ -50,9 +50,30 @@ inputName.onfocus = () => setActive(true);
 /////////
 // Utils
 
+function h(tag, attrs, children) {
+  const el = document.createElement(tag);
+  if (attrs) {
+    Object.keys(attrs).forEach(k => {
+      // Special treatment for events (ex: onclick)
+      if (k.startsWith("on"))
+        el[k.toLowerCase()] = attrs[k];
+      else
+        el.setAttribute(k, attrs[k]);
+    });
+  }
+  if (children) {
+    if (Array.isArray(children))
+      children.forEach(c => c && el.append(c));
+    else
+      el.append(children);
+  }
+  return el;
+}
+
 function setName() {
   // 'onChange' event on name input text field [HTML]
-  localStorage.setItem("name", $.getElementById("myName").value);
+  const name = $.getElementById("myName").value;
+  localStorage.setItem("name", sanitize(name, 30));
 }
 
 // Turn a "tab" on, and "close" all others
@@ -117,17 +138,86 @@ function showNewGameForm() {
     });
   }
 }
-function backToNormalSeek() {
+function backToNormalSeek() { //TODO: index.html......
   toggleVisible("newGame");
 }
 
-function toggleStyle(event, obj) {
-  const word = obj.innerHTML;
-  options[word] = !options[word];
-  event.target.classList.toggle("highlight-word");
+let options;
+function prepareOptions() {
+  options = {};
+  const container = $.getElementById("gameOptions");
+  container.innerHTML = "";
+  if (V.Options.select) {
+    V.Options.select.forEach(select => {
+      const selectEl = h('select', {
+        id: `var_${select.variable}`,
+        onchange: (e) => { options[select.variable] = e.target.value; }
+      }, select.options.map(opt =>
+        h('option', {
+          value: opt.value,
+          selected: opt.value == select.defaut
+        }, opt.label)
+      ));
+      container.append(
+        h('div', { class: 'option-select' }, [
+          h('label', { for: `var_${select.variable}` }, select.label),
+          h('div', { class: 'select' }, [
+            selectEl,
+            h('span', { class: 'focus' })
+          ])
+        ])
+      );
+    });
+  }
+  if (V.Options.input) {
+    V.Options.input.forEach(input => {
+      const inputAttrs = {
+        id: `var_${input.variable}`,
+        type: input.type,
+        onchange: (e) => {
+          options[input.variable] =
+            (input.type == "checkbox" ? e.target.checked : e.target.value);
+        }
+      };
+      if (input.type == "checkbox" && input.defaut)
+        inputAttrs.checked = true;
+      else if (input.defaut)
+        inputAttrs.value = input.defaut;
+      container.append(
+        h('div', { class: 'option-input' }, [
+          h('label', { class: 'input' }, [
+            h('input', inputAttrs),
+            h('span', { class: 'spacer' }),
+            h('span', { textContent: input.label })
+          ])
+        ])
+      );
+    });
+  }
+  if (V.Options.styles) {
+    const wordsDiv = h('div', { class: 'words' });
+    let i = 0;
+    while (i < V.Options.styles.length) {
+      const row = h('div', { class: 'row' });
+      for (let j = i; j < i + 4 && j < V.Options.styles.length; j++) {
+        const styleName = V.Options.styles[j];
+        row.append(
+          h('span', {
+            textContent: styleName,
+            onclick: (e) => {
+              options[styleName] = !options[styleName];
+              e.target.classList.toggle("highlight-word");
+            }
+          })
+        );
+      }
+      wordsDiv.append(row);
+      i += 4;
+    }
+    container.append(wordsDiv);
+  }
 }
 
-let options;
 function prepareOptions() {
   options = {};
   let optHtml = "";
@@ -210,6 +300,64 @@ function getGameLink() {
   });
 }
 
+
+
+
+function fillGameInfos(gameInfos, oppIndex) {
+  fetch(`/variants/${gameInfos.vname}/rules.html`)
+    .then(res => res.text())
+    .then(txt => {
+      const container = $.getElementById("gameInfos");
+      container.innerHTML = ""; // Nettoyage initial
+
+      // 1. Infos Joueurs
+      const playerDiv = h('div', { class: 'players-info' }, [
+        h('p', null, [
+          h('span', { class: 'bold', textContent: gameInfos.vdisp }),
+          h('span', { textContent: ` vs. ${gameInfos.players[oppIndex].name}` })
+        ])
+      ]);
+
+      // 2. Traitement des Options (Filtrage + Groupement par 4)
+      const optionsInfos = h('div', { class: 'options-info' });
+      const activeOptions = Object.entries(gameInfos.options).filter(opt => !!opt[1]);
+      
+      let i = 0;
+      while (i < activeOptions.length) {
+        const row = h('div', { class: 'row' });
+        for (let j = i; j < i + 4 && j < activeOptions.length; j++) {
+          const [key, val] = activeOptions[j];
+          const label = (val === true ? key : `${key}:${val}`);
+          row.append(h('span', { class: 'option', textContent: label + " " }));
+        }
+        optionsInfos.append(row);
+        i += 4;
+      }
+
+      // 3. Règles (on garde innerHTML ici car le HTML vient de ton fichier local rules.html)
+      const rulesDiv = h('div', { class: 'rules' });
+      rulesDiv.innerHTML = txt;
+
+      // 4. Bouton de retour
+      const btnWrap = h('div', { class: 'btn-wrap' }, [
+        h('button', { 
+          onclick: toggleGameInfos, 
+          textContent: "Back to game" 
+        })
+      ]);
+
+      // Assemblage final
+      container.append(
+        playerDiv, 
+        activeOptions.length > 0 ? optionsInfos : null, 
+        rulesDiv, 
+        btnWrap
+      );
+    });
+}
+
+
+
 function fillGameInfos(gameInfos, oppIndex) {
   fetch(`/variants/${gameInfos.vname}/rules.html`)
   .then(res => res.text())
@@ -221,16 +369,16 @@ function fillGameInfos(gameInfos, oppIndex) {
           <span>vs. ${gameInfos.players[oppIndex].name}</span>
         </p>
       </div>`;
-    const options = Object.entries(gameInfos.options);
-    if (options.length > 0) {
+    const _options = Object.entries(gameInfos.options);
+    if (_options.length > 0) {
       htmlContent += '<div class="options-info">';
       let i = 0;
-      while (i < options.length) {
+      while (i < _options.length) {
         htmlContent += '<div class="row">';
         for (let j=i; j<i+4; j++) {
-          if (j == options.length)
+          if (j == _options.length)
             break;
-          const opt = options[j];
+          const opt = _options[j];
           if (!opt[1]) //includes 0 and false (lighter display)
             continue;
           htmlContent +=
@@ -372,6 +520,7 @@ const messageCenter = (msg) => {
         break;
       if (document.hidden)
         notifyMe("move");
+      // TODO: moves not sanitized (most likely: won't "fix"...)
       vr.playReceivedMove(obj.moves, () => {
         if (vr.getCurrentScore(obj.moves) != "*") {
           localStorage.removeItem("gid");
@@ -492,6 +641,30 @@ const afterPlay = (move_s, newTurn, ops) => {
   }
 };
 
+
+
+
+
+
+
+function initializeGame(obj) {
+  const container = $.getElementById("boardContainer");
+  container.innerHTML = ""; // Nettoyage
+
+  // Créer les boutons de contrôle proprement
+  const infoBtn = createSVGButton("upLeftInfos", toggleGameInfos);
+  const stopBtn = createSVGButton("upRightStop", confirmStopGame);
+  const board = $.createElement("div");
+  board.className = "chessboard";
+
+  container.append(infoBtn, stopBtn, board);
+
+
+
+
+
+
+
 let vr = null, playerColor, lastVname = undefined;
 function initializeGame(obj) {
   const options = obj.options || {};
similarity index 99%
rename from base_rules.js
rename to js/base_rules.js
index 3866aa3..84cf1ea 100644 (file)
@@ -1482,7 +1482,7 @@ export default class ChessRules {
         // Zero step but non-zero interval => impossible
         (!Number.isFinite(rx) && !Number.isNaN(rx)) ||
         (!Number.isFinite(ry) && !Number.isNaN(ry)) ||
-        // Negative number of step (impossible)
+        // Negative number of steps (impossible)
         (rx < 0 || ry < 0) ||
         // Not the same number of steps in both directions:
         (!Number.isNaN(rx) && !Number.isNaN(ry) && Math.abs(rx - ry) > epsilon)
diff --git a/js/parameters.js b/js/parameters.js
new file mode 100644 (file)
index 0000000..15c987a
--- /dev/null
@@ -0,0 +1,16 @@
+const Params =
+{
+  // Usage on client:
+  http_server: "http://localhost:8000", //https://xogo.casa
+  socket_server: "ws://localhost:8080", //wss://xogo.casa
+
+  // Usage on (socket) server:
+  socket_port: 8080, //...
+  dev: true,
+
+  // Usage on both:
+  socket_path: "/ws",
+};
+
+// Next line for usage on server (Node.js)
+if (typeof window === 'undefined') module.exports = Params;
similarity index 100%
rename from parameters.js.dist
rename to js/parameters.js.dist
diff --git a/js/sanitize.js b/js/sanitize.js
new file mode 100644 (file)
index 0000000..b1aa8d8
--- /dev/null
@@ -0,0 +1,18 @@
+const sanitize = function(str, maxLength = 100)
+{
+  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;'
+  };
+  return cleaned.replace(/[&<>"']/g, m => map[m]);
+}
+
+// Next line for usage on server (Node.js)
+if (typeof window === 'undefined') module.exports = sanitize;
similarity index 80%
rename from server.js
rename to js/server.js
index 06d9866..0199fdb 100644 (file)
--- a/server.js
@@ -1,4 +1,5 @@
-const params = require("./parameters.js");
+const params = require("./js/parameters.js");
+const sanitize = require("./js/sanitize.js");
 const WebSocket = require("ws");
 const wss = new WebSocket.Server({
   port: params.socket_port,
@@ -17,16 +18,16 @@ function send(sid, code, data) {
   // If a player deletes local infos and then tries to resume a game,
   // sockets[oppSid] will probably not exist anymore:
   if (socket)
-    socket.send(JSON.stringify(Object.assign({code: code}, data)));
+    socket.send(JSON.stringify({code, ...data}));
 }
 
 function initializeGame(vname, players, options) {
   const gid =
     Crypto.randomBytes(randstrSize).toString("hex").slice(0, randstrSize);
   games[gid] = {
-    vname: vname,
-    players: players,
-    options: options,
+    vname,
+    players,
+    options,
     time: Date.now(),
     moveHash: {} //set of moves hashes seen so far
   };
@@ -35,11 +36,12 @@ function initializeGame(vname, players, options) {
 
 // Provide seed in case of, so that both players initialize with same FEN
 function launchGame(gid) {
-  const gameInfo = Object.assign(
-    {seed: Math.floor(Math.random() * 19840), gid: gid},
-    games[gid]
-  );
-  // players array is supposed to be full:
+  const gameInfo = {
+    seed: Math.floor(Math.random() * 19840),
+    gid,
+    ...games[gid]
+  };
+  // Players array is supposed to be full:
   for (const p of games[gid].players)
     send(p.sid, "gamestart", gameInfo);
 }
@@ -50,20 +52,59 @@ function getRandomVariant() {
   return variants[index].name;
 }
 
+if (params.dev) {
+  const chokidar = require("chokidar");
+  const watcher = chokidar.watch(
+    ["*.js", "*.css", "utils/", "variants/"],
+    { persistent: true }
+  );
+
+  // When a file changes: notify each connected client
+  watcher.on("change", path => {
+    console.log(`File changed: ${path}. Notifying clients...`);
+    wss.clients.forEach(socket => {
+      if (socket.readyState === WebSocket.OPEN) {
+        socket.send(JSON.stringify({
+          code: "filechange",
+          path
+        }));
+      }
+    });
+  });
+}
+
 wss.on("connection", (socket, req) => {
   const sid = req.url.split("=")[1]; //...?sid=...
+  if (sockets[sid]) {
+    // If SID is already taken, connexion is refused:
+    socket.send(JSON.stringify({ code: "ECONNREFUSED" }));
+    setTimeout(() => socket.terminate(), 100);
+    return;
+  }
   sockets[sid] = socket;
   socket.isAlive = true;
   socket.on("pong", () => socket.isAlive = true);
-  if (params.dev == true) {
-    const chokidar = require("chokidar");
-    const watcher = chokidar.watch(
-      ["*.js", "*.css", "utils/", "variants/"],
-      {persistent: true});
-    watcher.on("change", path => send(sid, "filechange", {path: path}));
-  }
   socket.on("message", (msg) => {
-    const obj = JSON.parse(msg);
+    let obj;
+    try {
+      obj = JSON.parse(msg);
+    } catch (e) {
+      return; //ignore les messages JSON malformés
+    }
+
+    // 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))
+        return; //unknown variant name
+    }
+    if (obj.name)
+      obj.name = sanitize(obj.name, 30);
+    if (obj.fen)
+      obj.fen = sanitize(obj.fen, 500);
+    if (obj.gid)
+      obj.gid = sanitize(obj.gid, 20);
+
     switch (obj.code) {
       // Send challenge (may trigger game creation)
       case "seekgame": {
@@ -96,7 +137,7 @@ wss.on("connection", (socket, req) => {
           // Launch game
           let players = [
             {sid: sid, name: obj.name, randvar: randvar},
-            Object.assign({}, challenges[oppIndex])
+            {...challenges[oppIndex]}
           ];
           delete challenges[oppIndex];
           if (Math.random() < 0.5)
@@ -141,7 +182,7 @@ wss.on("connection", (socket, req) => {
               vname = getRandomVariant();
             games[obj.gid].players.forEach(p => p.randvar = allrand);
             const gid = initializeGame(vname,
-                                       games[obj.gid].players.reverse(),
+                                       [...games[obj.gid].players].reverse(),
                                        games[obj.gid].options);
             launchGame(gid);
           }
@@ -196,6 +237,14 @@ wss.on("connection", (socket, req) => {
         break;
       // Relay a move + update games object
       case "newmove":
+        // Basic sanitizing on moves sent:
+        if (
+          !games[obj.gid] ||
+          !Array.isArray(obj.moves) ||
+          obj.moves.length > 20
+        ) {
+          return;
+        }
         // NOTE: still potential racing issues, but... fingers crossed
         const hash = Crypto.createHash("md5")
                      .update(JSON.stringify(obj.fen))
@@ -229,7 +278,7 @@ wss.on("connection", (socket, req) => {
         break; //only one challenge per player
       }
     }
-    for (let g of Object.values(games)) {
+    for (const g of Object.values(games)) {
       const myIndex = g.players.findIndex(p => p && p.sid == sid);
       if (myIndex >= 0) {
         if (g.rematch && g.rematch[myIndex] > 0) g.rematch[myIndex] = 0;
similarity index 100%
rename from variants.js
rename to js/variants.js
index 53517f8..fc6d664 100755 (executable)
--- a/start.sh
+++ b/start.sh
@@ -1,5 +1,5 @@
 #!/bin/sh
 
-nodemon -w server.js &
+nodemon -w js/server.js &
 echo $! > .pid
 php -S localhost:8000 &
index 14714ab..abce63c 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.amazon {
   background-image: url('/pieces/black_amazon.svg');
index 2683462..fc82364 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.alice-pawn {
   background-image: url('/pieces/yellow_pawn.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index f8dac72..a8ca5e6 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.target {
   background-image: url('/pieces/Ambiguous/yellow_target.svg');
deleted file mode 120000 (symlink)
index aee569aa46a071d7533878d9d4ddd2d83b23d8d7..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1 +0,0 @@
-../_Antiking/style.css
\ No newline at end of file
new file mode 100644 (file)
index 0000000000000000000000000000000000000000..f84086d2e09f93e9de08d832017a394d60b54562
--- /dev/null
@@ -0,0 +1,8 @@
+@import url("/css/css/base_pieces.css");
+
+piece.black.antiking {
+  background-image: url('/pieces/_Antiking/black_antiking.svg');
+}
+piece.white.antiking {
+  background-image: url('/pieces/_Antiking/white_antiking.svg');
+}
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index daca9e4..c4caf0a 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 div.illegal-text {
   position: relative;
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index f44a5b6..fb1ce5e 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.mammoth {
   background-image: url('/pieces/Balaklava/white_mammoth.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 76aab16..fb534b1 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.undefined {
   background-image: url('/pieces/white_mystery.svg');
index d35931f..4ec9ba8 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 @import url("/variants/_SpecialCaptures/style.css");
 
 piece.white.immobilizer {
index 1cb0afd..8062b94 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.cleopatra {
   background-image: url('/pieces/Benedict/black_cleopatra.svg');
index 9250552..e7a5bad 100644 (file)
@@ -1,2 +1,2 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 @import url("/variants/_Berolina/style.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 10d9eac..88c715d 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.empress {
   background-image: url('/pieces/black_empress.svg');
index 290a6f4..fd44963 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css")
+@import url("/css/base_pieces.css")
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 6f45bed..7f26838 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.egg {
   background-image: url('/pieces/Chakart/egg.svg');
index f73ed0e..72c7f3f 100644 (file)
@@ -165,7 +165,8 @@ export default class CheckeredRules extends ChessRules {
     super.setOtherVariables(fenParsed);
     // Non-capturing last checkered move (if any)
     const cmove = fenParsed.cmove;
-    if (cmove == "-") this.cmove = null;
+    if (cmove == "-")
+      this.cmove = null;
     else {
       this.cmove = {
         start: C.SquareToCoords(cmove.substr(0, 2)),
index 0dec143..7ad8f79 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.checkered.pawn {
   background-image: url('/pieces/Checkered/checkered_pawn.svg');
index 290a6f4..fd44963 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css")
+@import url("/css/base_pieces.css")
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 290a6f4..fd44963 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css")
+@import url("/css/base_pieces.css")
index 98324a9..3b8d5e7 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.nv-pawn {
   background-image: url('/pieces/yellow_pawn.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 668e543..6cba3f6 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.royal_queen {
   background-image: url('/pieces/Coregal/black_royal_queen.svg');
index 290a6f4..fd44963 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css")
+@import url("/css/base_pieces.css")
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index f9f7679..4f4a5f4 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.c_rook {
   background-image: url('/pieces/Cwda/c_white_rook.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index ad11010..7f13777 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 @font-face {
   font-family: chess-font;
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index e50c2f4..01cddb0 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.commoner {
   background-image: url('/pieces/black_commoner.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index f836570..80a6681 100644 (file)
@@ -67,14 +67,31 @@ export default class EightpiecesRules extends ChessRules {
     return ['c', 'd', 'e', 'f', 'g', 'h', 'm', 'o'];
   }
 
+  // obj == "-", {-1,-1} or ["]{x,y}["]
+  static convertPush(obj) {
+    if (typeof obj === "string")
+      // Reading from FEN
+      return obj == "-" ? {x: -1, y: -1} : JSON.parse(obj);
+    // Sending to FEN
+    return obj.x < 0 ? "-" : JSON.stringify(obj);
+  }
+
+  getPartFen(o) {
+    return Object.assign(
+      {
+        pushFrom: o.init ? "-" : V.convertPush(this.pushFrom),
+        pushedTo: o.init ? "-" : V.convertPush(this.pushedTo)
+      },
+      super.getPartFen(o)
+    );
+  }
+
   setOtherVariables(fenParsed) {
     super.setOtherVariables(fenParsed);
-    //this.pushFrom = 
-    //this.afterPush = 
+    this.pushFrom = V.convertPush(fenParsed.pushFrom);
+    this.pushedTo = V.convertPush(fenParsed.pushedTo);
   }
 
-  // TODO: FEN utils pushFrom et afterPush
-
   pieces(color, x, y) {
     const mirror = (this.playerColor == 'b');
     return Object.assign({
@@ -147,6 +164,16 @@ export default class EightpiecesRules extends ChessRules {
     return this.board[i][j] == "" || (V.LANCERS.includes(p) && c == colIJ);
   }
 
+  canIplay(x, y) {
+    if (
+      this.pushFrom.x == x && this.pushFrom.y == y &&
+      this.getColor(x, y) != this.playerColor
+    ) {
+      return true;
+    }
+    return super.canIplay(x, y);
+  }
+
   isImmobilized([x, y]) {
     const color = this.getColor(x, y);
     const oppCol = C.GetOppTurn(color);
@@ -188,35 +215,81 @@ export default class EightpiecesRules extends ChessRules {
     return res;
   }
 
-
-// TODO: finish lancers
-  // http://ftp.chessvariants.com/rules/8-piece-chess
-
-
   getPotentialMovesFrom([x, y], color) {
-    if (!this.pushFrom) {
-      return this.getPassMoves(x, y).concat(
-        super.getPotentialMovesFrom([x, y], color) );
+    if (this.pushFrom.x < 0 || this.pushedTo.x >= 0) {
+      let smoves = super.getPotentialMovesFrom([x, y], color);
+      // Forbid direction x,y --> pushFrom if x,y == pushedTo
+      if (x == this.pushedTo.x && y == this.pushedTo.y) {
+        smoves = smoves.filter(m => {
+          return ( !super.compatibleStep(
+            [x, y], [m.end.x, m.end.y],
+            [this.pushFrom.x - x, this.pushFrom.y - y]
+          ) );
+        });
+      }
+      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
-    return []; //TODO
+    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)
+    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}) );
+    });
+    
+console.log(pmoves);
+
+    return pmoves;
+
+
+
+  }
+
+  postPlay(move) {
+    if (
+      move.vanish.length > 0 &&
+      move.vanish[0].p == 's' &&
+      move.appear[0].c != move.vanish[0].c
+    ) {
+      // Sentry push ("capturing" part)
+      this.pushFrom = {x: move.end.x, y: move.end.y};
+      this.pushedTo = {x: -1, y: -1};
+    }
+    else if (move.vanish.length > 0 && move.vanish[0].c != this.turn)
+      this.pushedTo = {x: move.end.x, y: move.end.y};
+    else {
+      // All other cases: just reset both push variables
+      this.pushFrom = {x: -1, y: -1};
+      this.pushedTo = {x: -1, y: -1};
+    }
+    super.postPlay(move);
   }
 
-  getSentryPushes(x, y) {
-    // TODO: return all squares piece on x, y can be pushed to
-    return [{x: x+1, y: y-1}];
+  isLastMove(move) {
+    if (move.vanish[0].p == 's' && move.appear[0].c != move.vanish[0].c)
+      return false;
+    return super.isLastMove(move);
   }
 
-  // Post-process sentry pushes (if any), regular lancer moves, lancer drops..
   postProcessPotentialMoves(moves) {
     moves = super.postProcessPotentialMoves(moves);
     let finalMoves = [];
     for (const m of moves) {
-      //if (m.vanish.length == 0 && ... TODO: drop
-      if (m.vanish.length > 0 && V.LANCERS.includes(m.vanish[0].p)) {
-        // TODO: how to know it's regular? (not sentry push)
+      // Reorient a lancer after drop or regular move
+      if (
+        (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.pushFrom.x || m.start.y != this.pushFrom.y)
+        )
+      ) {
         this.getLancerOptions(m.end.x, m.end.y).forEach(o => {
           finalMoves.push( new Move({
             appear: [new PiPo({x:m.end.x,y:m.end.y,c:m.appear[0].c,p:o})],
@@ -227,16 +300,14 @@ export default class EightpiecesRules extends ChessRules {
       else if (m.vanish.length == m.appear.length || m.vanish[0].p != 's')
         finalMoves.push(m);
       else {
-        // Sentry "capture" --> turn into pushes
+        // Sentry "capture" --> remove sentry from final square (TODO: blink?)
         const [x, y] = [m.end.x, m.end.y]
         const p = this.getPiece(x, y);
         const c = this.getColor(x, y);
-        this.getSentryPushes(x, y).forEach(sq => {
-          finalMoves.push( new Move({
-            appear: m.appear.concat(new PiPo({x:sq.x, y:sq.y, p:p, c:c})),
-            vanish: m.vanish
-          }) );
-        });
+        finalMoves.push( new Move({
+          appear: [new PiPo({x:m.end.x,y:m.end.y,c:c,p:p})],
+          vanish: [ m.vanish[0] ]
+        }) );
       }
     }
     return finalMoves;
@@ -262,7 +333,8 @@ export default class EightpiecesRules extends ChessRules {
 
   updateReserve(color, piece, count) {
     if (V.LANCERS.includes(piece))
-      piece = 'c'; //TODO: orientation, or new drawing?
+      // Show only one lancer orientation, and reorient when drop:
+      piece = color == 'w' ? 'c' : 'g';
     super.updateReserve(color, piece, count);
   }
 
index 0ff0c7a..a35f14f 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.jailer {
   background-image: url('/pieces/Eightpieces/white_jailer.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index d614763..4ff0c39 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.white.sleepy-pawn {
   background-image: url('/pieces/yellow_pawn.svg');
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index a3550bc..95e35b2 100644 (file)
@@ -1 +1 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
index 6614062..017c413 100644 (file)
@@ -1,4 +1,4 @@
-@import url("/base_pieces.css");
+@import url("/css/base_pieces.css");
 
 piece.black.antiking {
   background-image: url('/pieces/_Antiking/black_antiking.svg');