Apocalypse chess is better but still slightly buggy
authorBenjamin Auder <benjamin.auder@somewhere>
Fri, 27 Mar 2020 00:56:00 +0000 (01:56 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Fri, 27 Mar 2020 00:56:00 +0000 (01:56 +0100)
client/public/images/pieces/Apocalypse/empty.svg [new file with mode: 0644]
client/src/components/Board.vue
client/src/translations/rules/Apocalypse/en.pug
client/src/variants/Apocalypse.js

diff --git a/client/public/images/pieces/Apocalypse/empty.svg b/client/public/images/pieces/Apocalypse/empty.svg
new file mode 100644 (file)
index 0000000..08ec906
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
\ No newline at end of file
index 0856096..5f88880 100644 (file)
@@ -87,7 +87,7 @@ export default {
     const gameDiv = h(
       "div",
       {
-        class: {
+        "class": {
           game: true,
           clearer: true
         }
@@ -97,7 +97,7 @@ export default {
         return h(
           "div",
           {
-            class: {
+            "class": {
               row: true
             },
             style: { opacity: this.choices.length > 0 ? "0.5" : "1" }
@@ -108,7 +108,7 @@ export default {
             if (showPiece(ci, cj)) {
               elems.push(
                 h("img", {
-                  class: {
+                  "class": {
                     piece: true,
                     ghost:
                       !!this.selectedPiece &&
@@ -131,7 +131,7 @@ export default {
             if (this.settings.hints && hintSquares[ci][cj]) {
               elems.push(
                 h("img", {
-                  class: {
+                  "class": {
                     "mark-square": true
                   },
                   attrs: {
@@ -144,7 +144,7 @@ export default {
             return h(
               "div",
               {
-                class: {
+                "class": {
                   board: true,
                   ["board" + sizeY]: true,
                   "light-square": lightSquare,
@@ -176,13 +176,13 @@ export default {
           h(
             "div",
             {
-              class: { board: true, ["board" + sizeY]: true },
+              "class": { board: true, ["board" + sizeY]: true },
               attrs: { id: getSquareId({ x: sizeX + shiftIdx, y: i }) },
               style: { opacity: qty > 0 ? 1 : 0.35 }
             },
             [
               h("img", {
-                class: { piece: true, reserve: true },
+                "class": { piece: true, reserve: true },
                 attrs: {
                   src:
                     "/images/pieces/" +
@@ -190,7 +190,7 @@ export default {
                     ".svg"
                 }
               }),
-              h("sup", { class: { "reserve-count": true } }, [ qty ])
+              h("sup", { "class": { "reserve-count": true } }, [ qty ])
             ]
           )
         );
@@ -203,13 +203,13 @@ export default {
           h(
             "div",
             {
-              class: { board: true, ["board" + sizeY]: true },
+              "class": { board: true, ["board" + sizeY]: true },
               attrs: { id: getSquareId({ x: sizeX + (1 - shiftIdx), y: i }) },
               style: { opacity: qty > 0 ? 1 : 0.35 }
             },
             [
               h("img", {
-                class: { piece: true, reserve: true },
+                "class": { piece: true, reserve: true },
                 attrs: {
                   src:
                     "/images/pieces/" +
@@ -217,7 +217,7 @@ export default {
                     ".svg"
                 }
               }),
-              h("sup", { class: { "reserve-count": true } }, [ qty ])
+              h("sup", { "class": { "reserve-count": true } }, [ qty ])
             ]
           )
         );
@@ -233,7 +233,7 @@ export default {
         h(
           "div",
           {
-            class: {
+            "class": {
               game: true,
               "reserve-div": true
             },
@@ -245,7 +245,7 @@ export default {
             h(
               "div",
               {
-                class: {
+                "class": {
                   row: true,
                   "reserve-row": true
                 }
@@ -258,7 +258,7 @@ export default {
         h(
           "div",
           {
-            class: {
+            "class": {
               game: true,
               "reserve-div": true
             },
@@ -270,7 +270,7 @@ export default {
             h(
               "div",
               {
-                class: {
+                "class": {
                   row: true,
                   "reserve-row": true
                 }
@@ -300,7 +300,7 @@ export default {
         "div",
         {
           attrs: { id: "choices" },
-          class: { row: true },
+          "class": { row: true },
           style: {
             top: topOffset + "px",
             left:
@@ -313,7 +313,9 @@ export default {
         },
         [ h(
           "div",
-          { },
+          {
+            "class": { "full-width": true }
+          },
           this.choices.map(m => {
             // A "choice" is a move
             const applyMove = (e) => {
@@ -331,7 +333,7 @@ export default {
             return h(
               "div",
               {
-                class: {
+                "class": {
                   board: true,
                   ["board" + sizeY]: true
                 },
@@ -349,7 +351,7 @@ export default {
                       this.vr.getPPpath(m, this.orientation) +
                       V.IMAGE_EXTENSION
                   },
-                  class: { "choice-piece": true },
+                  "class": { "choice-piece": true },
                   on: onClick
                 })
               ]
@@ -488,16 +490,16 @@ export default {
 </script>
 
 <style lang="sass" scoped>
+// NOTE: no variants with reserve of size != 8
 .game.reserve-div
   margin-bottom: 18px
-
 .reserve-count
   padding-left: 40%
-
 .reserve-row
   margin-bottom: 15px
 
-// NOTE: no variants with reserve of size != 8
+.full-width
+  width: 100%
 
 .game
   user-select: none
index 3a0ef74..eaeab80 100644 (file)
@@ -2,11 +2,16 @@ p.boxed
   | Both players play a move "at the same time".
   | The goal is to eliminate all pawns.
 
+figure.diagram-container
+  .diagram
+    | fen:npppn/p3p/5/P3P/NPPPN:
+  figcaption Initial position.
+
 p
   | This variant is inspired by the 
   a(href="https://en.wikipedia.org/wiki/Four_Horsemen_of_the_Apocalypse")
-    | &nbsp;Four Horsemen of the Apocalypse
-  | mythology. Knights are horsemen, and pawns are footmen.
+    | Four Horsemen of the Apocalypse
+  | &nbsp;mythology. Knights are horsemen, and pawns are footmen.
   | The goal is to eliminate all enemy footmen,
   | most likely with the help of your horsemen.
   | If all footmen die, the other side wins.
@@ -24,13 +29,14 @@ ul
   li.
     If both moves are illegal none are played.
     If one is illegal, the other is played.
-  li.
-    If both moves arrive on the same square, both pieces disappear except
-    if one is a horseman and the other a footman.
-    In this case only the horseman remains.
   li.
     If a capture was intended but the target moved, the move is still played
     but doesn't capture anything.
+  li.
+    If both moves arrive on the same square: the illegal move prevails,
+    if the other was legal (higher risk => reward).
+    If both moves are legal or illegal, then a horseman wins over a footman.
+    Finally, at same risk level and same piece type, both disappear.
 
 figure.diagram-container
   .diagram
@@ -43,6 +49,8 @@ p.
   Pawns automatically promote in a knight, except if the player already
   have two horsemen on the board. In this case the footman is relocated on
   any free square which is not on last rank.
+  Even in this last case, pawn promotions may appear possible by
+  anticipation of a knight capture. This is risky but playable.
 
 h3 End of the game
 
@@ -50,7 +58,8 @@ p.
   As stated previously, losing all pawns lose the game, so promoting your
   last pawn loses. It may be the only legal move.
   If however both footmen armies vanish at the same time, it's a draw.
-  It can happen if the two last pawns decide to advance to the same square.
+  It can happen if the two last pawns decide to advance to the same square
+  for example.
 
 h3 Source
 
index 96431c3..b84fb5b 100644 (file)
@@ -22,13 +22,19 @@ export class ApocalypseRules extends ChessRules {
   }
 
   static get CanAnalyze() {
-    return false;
+    return true; //false;
   }
 
   static get ShowMoves() {
     return "byrow";
   }
 
+  getPPpath(m) {
+    // Show the piece taken, if any, and not multiple pawns:
+    if (m.vanish.length == 1) return "Apocalypse/empty";
+    return m.vanish[1].c + m.vanish[1].p;
+  }
+
   static get PIECES() {
     return [V.PAWN, V.KNIGHT];
   }
@@ -112,7 +118,9 @@ export class ApocalypseRules extends ChessRules {
   }
 
   getFlagsFen() {
-    return this.penaltyFlags.join("");
+    return (
+      this.penaltyFlags['w'].toString() + this.penaltyFlags['b'].toString()
+    );
   }
 
   setOtherVariables(fen) {
@@ -126,7 +134,10 @@ export class ApocalypseRules extends ChessRules {
   }
 
   setFlags(fenflags) {
-    this.penaltyFlags = [0, 1].map(i => parseInt(fenflags[i]));
+    this.penaltyFlags = {
+      'w': parseInt(fenflags[0]),
+      'b': parseInt(fenflags[1])
+    };
   }
 
   getWhitemoveFen() {
@@ -149,13 +160,17 @@ export class ApocalypseRules extends ChessRules {
     this.turn = V.GetOppCol(color);
     const oppMoves = super.getAllValidMoves();
     this.turn = color;
-    // For each opponent's move, generate valid moves [from sq]
+    // For each opponent's move, generate valid moves [from sq if same color]
     let speculations = [];
     oppMoves.forEach(m => {
       V.PlayOnBoard(this.board, m);
       const newValidMoves =
         !!sq
-          ? super.getPotentialMovesFrom(sq)
+          ? (
+            this.getColor(sq[0], sq[1]) == color
+              ? super.getPotentialMovesFrom(sq)
+              : []
+            )
           : super.getAllValidMoves();
       newValidMoves.forEach(vm => {
         const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y;
@@ -190,8 +205,8 @@ export class ApocalypseRules extends ChessRules {
       // If 0 or 1 horsemen, promote in knight
       let knightCounter = 0;
       let emptySquares = [];
-      for (let i=0; i<V.size.x; i++) {
-        for (let j=0; j<V.size.y; j++) {
+      for (let i = 0; i < V.size.x; i++) {
+        for (let j = 0; j < V.size.y; j++) {
           if (this.board[i][j] == V.EMPTY) emptySquares.push([i, j]);
           else if (
             this.getColor(i, j) == color &&
@@ -203,10 +218,22 @@ export class ApocalypseRules extends ChessRules {
       }
       if (knightCounter <= 1) finalPieces = [V.KNIGHT];
       else {
-        // Generate all possible landings
+        // Generate all possible landings, maybe capturing something on the way
+        let capture = undefined;
+        if (this.board[x2][y2] != V.EMPTY) {
+          capture = JSON.parse(JSON.stringify({
+            x: x2,
+            y: y2,
+            c: this.getColor(x2, y2),
+            p: this.getPiece(x2, y2)
+          }));
+        }
         emptySquares.forEach(sq => {
-          if (sq[0] != lastRank)
-            moves.push(this.getBasicMove([x1, y1], [sq[0], sq[1]]));
+          if (sq[0] != lastRank) {
+            let newMove = this.getBasicMove([x1, y1], [sq[0], sq[1]]);
+            if (!!capture) newMove.vanish.push(capture);
+            moves.push(newMove);
+          }
         });
         return;
       }
@@ -233,41 +260,66 @@ export class ApocalypseRules extends ChessRules {
 
   // White and black (partial) moves were played: merge
   resolveSynchroneMove(move) {
-    let m = [this.whiteMove, move];
-    for (let i of [0, 1]) {
-      if (!!m[i].illegal) {
-        // Either an anticipated capture of something which didn't move
-        // (or not to the right square), or a push through blocus.
-        if (
-          (
-            // Push attempt
-            m[i].start.y == m[i].end.y &&
-            (m[1-i].start.x != m[i].end.x || m[1-i].start.y != m[i].end.y)
-          )
-          ||
-          (
-            // Capture attempt
-            Math.abs(m[i].start.y - m[i].end.y) == 1 &&
-            (m[1-i].end.x != m[i].end.x || m[1-i].end.y != m[i].end.y)
-          )
-        ) {
-          // Just discard the move, and add a penalty point
-          this.penaltyFlags[m[i].vanish[0].c]++;
-          m[i] = null;
-        }
-      }
+    let m1 = this.whiteMove;
+    let m2 = move;
+    const movingLikeCapture = (m) => {
+      const shift = (m.vanish[0].c == 'w' ? -1 : 1);
+      return (
+        m.start.x + shift == m.end.x &&
+        Math.abs(m.end.y - m.start.y) == 1
+      );
+    };
+    const isPossible = (m, other) => {
+      return (
+        (
+          m.vanish[0].p == V.KNIGHT &&
+          (m.vanish.length == 1 || m.vanish[1].c != m.vanish[0].c)
+        )
+        ||
+        (
+          // Promotion attempt
+          m.end.x == (m.vanish[0].c == "w" ? 0 : V.size.x - 1) &&
+          other.vanish.length == 2 &&
+          other.vanish[1].p == V.KNIGHT &&
+          other.vanish[1].c == m.vanish[0].c
+        )
+        ||
+        (
+          // Moving attempt
+          !movingLikeCapture(m) &&
+          other.start.x == m.end.x &&
+          other.start.y == m.end.y
+        )
+        ||
+        (
+          // Capture attempt
+          movingLikeCapture(m) &&
+          other.end.x == m.end.x &&
+          other.end.y == m.end.y
+        )
+      );
+    };
+    if (!!m1.illegal && !isPossible(m1, m2)) {
+      // Either an anticipated capture of something which didn't move
+      // (or not to the right square), or a push through blocus.
+      // ==> Just discard the move, and add a penalty point
+      this.penaltyFlags[m1.vanish[0].c]++;
+      m1.isNull = true;
     }
-
+    if (!!m2.illegal && !isPossible(m2, m1)) {
+      this.penaltyFlags[m2.vanish[0].c]++;
+      m2.isNull = true;
+    }
+    if (!!m1.isNull) m1 = null;
+    if (!!m2.isNull) m2 = null;
+    // If one move is illegal, just execute the other
+    if (!m1 && !!m2) return m2;
+    if (!m2 && !!m1) return m1;
     // For PlayOnBoard (no need for start / end, irrelevant)
     let smove = {
       appear: [],
       vanish: []
     };
-    const m1 = m[0],
-          m2 = m[1];
-    // If one move is illegal, just execute the other
-    if (!m1 && !!m2) return m2;
-    if (!m2 && !!m1) return m1;
     if (!m1 && !m2) return smove;
     // Both move are now legal:
     smove.vanish.push(m1.vanish[0]);
@@ -296,15 +348,27 @@ export class ApocalypseRules extends ChessRules {
         smove.vanish.push(m2.vanish[1]);
       }
     } else {
-      // Collision: both disappear except if different kinds (knight remains)
+      // Collision: priority to the anticipated capture, if any.
+      // If ex-aequo: knight wins (higher risk), or both disappears.
+      // Then, priority to the knight vs pawn: remains.
+      // Finally: both disappears.
+      let remain = null;
       const p1 = m1.vanish[0].p;
       const p2 = m2.vanish[0].p;
-      if ([p1, p2].includes(V.KNIGHT) && [p1, p2].includes(V.PAWN)) {
+      if (!!m1.illegal && !m2.illegal) remain = { c: 'w', p: p1 };
+      else if (!!m2.illegal && !m1.illegal) remain = { c: 'b', p: p2 };
+      if (!remain) {
+        // Either both are illegal or both are legal
+        if (p1 == V.KNIGHT && p2 == V.PAWN) remain = { c: 'w', p: p1 };
+        else if (p2 == V.KNIGHT && p1 == V.PAWN) remain = { c: 'b', p: p2 };
+        // If remain is still null: same type same risk, both disappear
+      }
+      if (!!remain) {
         smove.appear.push({
           x: m1.end.x,
           y: m1.end.y,
-          p: V.KNIGHT,
-          c: (p1 == V.KNIGHT ? 'w' : 'b')
+          p: remain.p,
+          c: remain.c
         });
       }
     }
@@ -312,6 +376,10 @@ export class ApocalypseRules extends ChessRules {
   }
 
   play(move) {
+    if (!this.states) this.states = [];
+    const stateFen = this.getFen();
+    this.states.push(stateFen);
+
     // Do not play on board (would reveal the move...)
     move.flags = JSON.stringify(this.aggregateFlags());
     this.turn = V.GetOppCol(this.turn);
@@ -325,7 +393,6 @@ export class ApocalypseRules extends ChessRules {
       this.whiteMove = move;
       return;
     }
-
     // A full turn just ended:
     const smove = this.resolveSynchroneMove(move);
     V.PlayOnBoard(this.board, smove);
@@ -342,6 +409,10 @@ export class ApocalypseRules extends ChessRules {
     this.turn = V.GetOppCol(this.turn);
     this.movesCount--;
     this.postUndo(move);
+
+    const stateFen = this.getFen();
+    if (stateFen != this.states[this.states.length-1]) debugger;
+    this.states.pop();
   }
 
   postUndo(move) {
@@ -373,9 +444,9 @@ export class ApocalypseRules extends ChessRules {
       return "1-0"; //fmCount['b'] == 0
     }
     // Check penaltyFlags: if a side has 2 or more, it loses
-    if (this.penaltyFlags.every(f => f == 2)) return "1/2";
-    if (this.penaltyFlags[0] == 2) return "0-1";
-    if (this.penaltyFlags[1] == 2) return "1-0";
+    if (Object.values(this.penaltyFlags).every(v => v == 2)) return "1/2";
+    if (this.penaltyFlags['w'] == 2) return "0-1";
+    if (this.penaltyFlags['b'] == 2) return "1-0";
     if (!this.atLeastOneMove('w') || !this.atLeastOneMove('b'))
       // Stalemate (should be very rare)
       return "1/2";