From 9b76053854ec41e9f4d52aa5ddc51ea4c4c33ab6 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 5 Jul 2022 13:31:39 +0200
Subject: [PATCH] Add Avalam

---
 README.md                    |   3 +
 base_rules.js                |  20 ++--
 pieces/Avalam/.gitignore     |   1 +
 pieces/Avalam/generateSVG.py |  41 ++++++++
 variants.js                  |   2 +-
 variants/Avalam/class.js     | 195 +++++++++++++++++++++++++++++++++++
 variants/Avalam/rules.html   |   1 +
 variants/Avalam/style.css    |  35 +++++++
 8 files changed, 286 insertions(+), 12 deletions(-)
 create mode 100644 pieces/Avalam/.gitignore
 create mode 100755 pieces/Avalam/generateSVG.py
 create mode 100644 variants/Avalam/class.js
 create mode 100644 variants/Avalam/rules.html
 create mode 100644 variants/Avalam/style.css

diff --git a/README.md b/README.md
index 8c47cc7..4846e80 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,9 @@ A static web server like "php -S localhost:8000".
 Rename parameters.js.dist &rarr; parameters.js, and edit file. <br>
 ```npm i```
 
+Generate some pieces: <br>
+```python generateSVG.py``` in pieces/Avalam
+
 ```./start.sh``` (edit 'php -S ...' line if necessary) <br>
 ... <br>
 ```./stop.sh```
diff --git a/base_rules.js b/base_rules.js
index 0b7d6ed..01a2c24 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -1,5 +1,5 @@
-import { Random } from "/utils/alea.js";
-import { ArrayFun } from "/utils/array.js";
+import {Random} from "/utils/alea.js";
+import {ArrayFun} from "/utils/array.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
@@ -528,20 +528,16 @@ export default class ChessRules {
       (oldV,newV) => oldV + (this.reserve[c][newV] > 0 ? 1 : 0), 0);
   }
 
-  static AddClass_es(piece, class_es) {
+  static AddClass_es(elt, class_es) {
     if (!Array.isArray(class_es))
       class_es = [class_es];
-    class_es.forEach(cl => {
-      piece.classList.add(cl);
-    });
+    class_es.forEach(cl => elt.classList.add(cl));
   }
 
-  static RemoveClass_es(piece, class_es) {
+  static RemoveClass_es(elt, class_es) {
     if (!Array.isArray(class_es))
       class_es = [class_es];
-    class_es.forEach(cl => {
-      piece.classList.remove(cl);
-    });
+    class_es.forEach(cl => elt.classList.remove(cl));
   }
 
   // Generally light square bottom-right
@@ -629,6 +625,8 @@ export default class ChessRules {
         class="chessboard_SVG">`;
     for (let i=0; i < this.size.x; i++) {
       for (let j=0; j < this.size.y; j++) {
+        if (!this.onBoard(i, j))
+          continue;
         const ii = (flipped ? this.size.x - 1 - i : i);
         const jj = (flipped ? this.size.y - 1 - j : j);
         let classes = this.getSquareColorClass(ii, jj);
@@ -1494,7 +1492,7 @@ export default class ChessRules {
     let moves = [];
     for (let i=0; i<this.size.x; i++) {
       for (let j=0; j<this.size.y; j++) {
-        if (this.canDrop([c, p], [i, j])) {
+        if (this.onBoard(i, j) && this.canDrop([c, p], [i, j])) {
           let mv = new Move({
             start: {x: c, y: p},
             end: {x: i, y: j},
diff --git a/pieces/Avalam/.gitignore b/pieces/Avalam/.gitignore
new file mode 100644
index 0000000..756b22f
--- /dev/null
+++ b/pieces/Avalam/.gitignore
@@ -0,0 +1 @@
+*.svg
diff --git a/pieces/Avalam/generateSVG.py b/pieces/Avalam/generateSVG.py
new file mode 100755
index 0000000..43e1e9d
--- /dev/null
+++ b/pieces/Avalam/generateSVG.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# Compose each piece SVG with numbers
+# https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
+# https://developer.mozilla.org/fr/docs/Web/SVG/Tutoriel/Paths
+
+preamble = """<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">"""
+
+black = '<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>'
+white = '<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>'
+
+digits = [
+    # 1 (unused here)
+    '<path d="M130,85 v60"',
+    # 2
+    '<path d="M100,85 h30 v30 h-30 v30 h30"',
+    # 3
+    '<path d="M100,85 h30 v30 h-30 M130,115 v30 h-30"',
+    # 4
+    '<path d="M100,85 v30 h30 v30 M130,85 v30"',
+    # 5
+    '<path d="M130,85 h-30 v30 h30 v30 h-30"'
+]
+
+final = "</svg>"
+
+for color in ["white", "black"]:
+    for number in range(5):
+        filename = color + "_stack" + (str(number + 1) if number >= 1 else "") + ".svg"
+        f = open(filename, "w")
+        f.write(preamble)
+        f.write("\n")
+        f.write(white if color == "white" else black)
+        f.write("\n")
+        if number >= 1:
+            f.write(digits[number] + ' fill="none" stroke-width="5" stroke="black"/>')
+            f.write("\n")
+        f.write(final)
+        f.close()
diff --git a/variants.js b/variants.js
index b3cf120..8662316 100644
--- a/variants.js
+++ b/variants.js
@@ -12,7 +12,7 @@ const variants = [
   {name: 'Arena', desc: 'Middle battle'},
   {name: 'Atarigo', desc: 'First capture wins', disp: 'Atari-Go'},
   {name: 'Atomic', desc: 'Explosive captures'},
-//  {name: 'Avalam', desc: 'Build towers'},
+  {name: 'Avalam', desc: 'Build towers'},
 //  {name: 'Avalanche', desc: 'Pawnfalls'},
 //  {name: 'Ball', desc: 'Score a goal'},
 //  {name: 'Balaklava', desc: 'Meet the Mammoth'},
diff --git a/variants/Avalam/class.js b/variants/Avalam/class.js
new file mode 100644
index 0000000..f35df83
--- /dev/null
+++ b/variants/Avalam/class.js
@@ -0,0 +1,195 @@
+import ChessRules from "/base_rules.js";
+import {Random} from "/utils/alea.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class AvalamRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: [{
+        label: "Randomness",
+        variable: "randomness",
+        defaut: 1,
+        options: [
+          {label: "Deterministic", value: 0},
+          {label: "Random", value: 1},
+        ]
+      }],
+      input: [
+        {
+          label: "Free placement",
+          variable: "freefill",
+          type: "checkbox",
+          defaut: false
+        }
+      ]
+    };
+  }
+
+  get hasFlags() {
+    return false;
+  }
+  get hasEnpassant() {
+    return false;
+  }
+
+  pieces(color, x, y) {
+    const steps = [
+      [1, 0], [0, 1], [-1, 0], [0, -1],
+      [1, 1], [1, -1], [-1, 1], [-1, -1]
+    ];
+    return {
+      'b': {
+        "class": "stack",
+        moves: [{steps: steps, range: 1}]
+      },
+      'c': {
+        "class": "stack2",
+        moveas: 'b'
+      },
+      'd': {
+        "class": "stack3",
+        moveas: 'b'
+      },
+      'e': {
+        "class": "stack4",
+        moveas: 'b'
+      },
+      'f': {
+        "class": "stack5",
+        moveas: 'b'
+      }
+    };
+  }
+
+  genRandInitBaseFen() {
+    let fen = "";
+    if (this.freefill)
+      fen = "9/".repeat(8) + "9 w 0";
+    else if (this.options["randomness"] == 0) {
+      fen = "2Bb5/1BbBb4/1bBbBbB2/1BbBbBbBb/BbBb1bBbB/" +
+            "bBbBbBbB1/2BbBbBb1/4bBbB1/5bB2 w 0";
+    }
+    else {
+      const pieces = ('B'.repeat(24) + 'b'.repeat(24)).split("");
+      const a = Random.shuffle(pieces, 48).join("");
+      fen = (
+        "2" + a.substr(0, 2) + "5/1" + a.substr(2, 4) +
+        "4/1" + a.substr(6, 6) + "2/1" + a.substr(12, 8) +
+        "/" + a.substr(20, 4) + "1" + a.substr(24, 4) +
+        "/" + a.substr(28, 8) + "1/2" + a.substr(36, 6) +
+        "1/4" + a.substr(42, 4) + "1/5" + a.substr(46, 2) +
+        "2 w 0"
+      );
+    }
+    return { fen: fen, o: {} };
+  }
+
+  getSquareColorClass(x, y) {
+    return "board-sq";
+  }
+
+  get size() {
+    return {x: 9, y: 9};
+  }
+
+  onBoard(x, y) {
+    if (!super.onBoard(x, y))
+      return false;
+    switch (x) {
+      case 0:
+        return [2, 3].includes(y);
+      case 1:
+        return [1, 2, 3, 4].includes(y);
+      case 2:
+        return [1, 2, 3, 4, 5, 6].includes(y);
+      case 3:
+        return y >= 1;
+      case 4:
+        return y != 4;
+      case 5:
+        return y <= 7;
+      case 6:
+        return [2, 3, 4, 5, 6, 7].includes(y);
+      case 7:
+        return [4, 5, 6, 7].includes(y);
+      case 8:
+        return [5, 6].includes(y);
+    }
+    return false; //never reached
+  }
+
+  canIplay() {
+    return this.playerColor == this.turn;
+  }
+
+  doClick(coords) {
+    if (!this.freefill || this.board[coords.x][coords.y] != "")
+      return null;
+    return new Move({
+      start: {x: coords.x, y: coords.y},
+      vanish: [],
+      appear: [new PiPo({x: coords.x, y: coords.y, c: this.turn, p: 'b'})]
+    });
+  }
+
+  getBasicMove([x1, y1], [x2, y2]) {
+    const cp1 = this.board[x1][y1],
+          cp2 = this.board[x2][y2];
+    const newPiece =
+      String.fromCharCode(cp1.charCodeAt(1) + cp2.charCodeAt(1) - 97);
+    return (
+      new Move({
+        vanish: [
+          new PiPo({ x: x1, y: y1, c: cp1.charAt(0), p: cp1.charAt(1) }),
+          new PiPo({ x: x2, y: y2, c: cp2.charAt(0), p: cp2.charAt(1) })
+        ],
+        appear: [
+          new PiPo({ x: x2, y: y2, c: cp1.charAt(0), p: newPiece })
+        ]
+      })
+    );
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const height = this.board[x][y].charCodeAt(1) - 97;
+    if (height == 5)
+      return [];
+    let moves = [];
+    for (let s of this.pieces()['b'].moves[0].steps) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        this.onBoard(i, j) &&
+        this.board[i][j] != "" &&
+        (height + this.board[i][j].charCodeAt(1) - 97 <= 5)
+      ) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+      }
+    }
+    return moves;
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCurrentScore() {
+    let towersCount = {w: 0, b: 0};
+    for (let i = 0; i < this.size.x; i++) {
+      for (let j = 0; j < this.size.y; j++) {
+        if (this.board[i][j] != "") {
+          if (this.getPotentialMovesFrom([i, j]).length > 0)
+            return '*';
+          towersCount[ this.board[i][j][0] ]++;
+        }
+      }
+    }
+    if (towersCount['w'] > towersCount['b'])
+      return "1-0";
+    if (towersCount['b'] > towersCount['w'])
+      return "0-1";
+    return "1/2";
+  }
+
+};
diff --git a/variants/Avalam/rules.html b/variants/Avalam/rules.html
new file mode 100644
index 0000000..c65158e
--- /dev/null
+++ b/variants/Avalam/rules.html
@@ -0,0 +1 @@
+<p>TODO</p>
diff --git a/variants/Avalam/style.css b/variants/Avalam/style.css
new file mode 100644
index 0000000..04463d1
--- /dev/null
+++ b/variants/Avalam/style.css
@@ -0,0 +1,35 @@
+piece.white.stack {
+  background-image: url('/pieces/Avalam/white_stack.svg');
+}
+piece.white.stack2 {
+  background-image: url('/pieces/Avalam/white_stack2.svg');
+}
+piece.white.stack3 {
+  background-image: url('/pieces/Avalam/white_stack3.svg');
+}
+piece.white.stack4 {
+  background-image: url('/pieces/Avalam/white_stack4.svg');
+}
+piece.white.stack5 {
+  background-image: url('/pieces/Avalam/white_stack5.svg');
+}
+
+piece.black.stack {
+  background-image: url('/pieces/Avalam/black_stack.svg');
+}
+piece.black.stack2 {
+  background-image: url('/pieces/Avalam/black_stack2.svg');
+}
+piece.black.stack3 {
+  background-image: url('/pieces/Avalam/black_stack3.svg');
+}
+piece.black.stack4 {
+  background-image: url('/pieces/Avalam/black_stack4.svg');
+}
+piece.black.stack5 {
+  background-image: url('/pieces/Avalam/black_stack5.svg');
+}
+
+.board-sq {
+  fill: grey;
+}
-- 
2.44.0