const gameDiv = h(
"div",
{
- class: {
+ "class": {
game: true,
clearer: true
}
return h(
"div",
{
- class: {
+ "class": {
row: true
},
style: { opacity: this.choices.length > 0 ? "0.5" : "1" }
if (showPiece(ci, cj)) {
elems.push(
h("img", {
- class: {
+ "class": {
piece: true,
ghost:
!!this.selectedPiece &&
if (this.settings.hints && hintSquares[ci][cj]) {
elems.push(
h("img", {
- class: {
+ "class": {
"mark-square": true
},
attrs: {
return h(
"div",
{
- class: {
+ "class": {
board: true,
["board" + sizeY]: true,
"light-square": lightSquare,
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/" +
".svg"
}
}),
- h("sup", { class: { "reserve-count": true } }, [ qty ])
+ h("sup", { "class": { "reserve-count": true } }, [ qty ])
]
)
);
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/" +
".svg"
}
}),
- h("sup", { class: { "reserve-count": true } }, [ qty ])
+ h("sup", { "class": { "reserve-count": true } }, [ qty ])
]
)
);
h(
"div",
{
- class: {
+ "class": {
game: true,
"reserve-div": true
},
h(
"div",
{
- class: {
+ "class": {
row: true,
"reserve-row": true
}
h(
"div",
{
- class: {
+ "class": {
game: true,
"reserve-div": true
},
h(
"div",
{
- class: {
+ "class": {
row: true,
"reserve-row": true
}
"div",
{
attrs: { id: "choices" },
- class: { row: true },
+ "class": { row: true },
style: {
top: topOffset + "px",
left:
},
[ h(
"div",
- { },
+ {
+ "class": { "full-width": true }
+ },
this.choices.map(m => {
// A "choice" is a move
const applyMove = (e) => {
return h(
"div",
{
- class: {
+ "class": {
board: true,
["board" + sizeY]: true
},
this.vr.getPPpath(m, this.orientation) +
V.IMAGE_EXTENSION
},
- class: { "choice-piece": true },
+ "class": { "choice-piece": true },
on: onClick
})
]
</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
| 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")
- | Four Horsemen of the Apocalypse
- | mythology. Knights are horsemen, and pawns are footmen.
+ | Four Horsemen of the Apocalypse
+ | 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.
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
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
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
}
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];
}
}
getFlagsFen() {
- return this.penaltyFlags.join("");
+ return (
+ this.penaltyFlags['w'].toString() + this.penaltyFlags['b'].toString()
+ );
}
setOtherVariables(fen) {
}
setFlags(fenflags) {
- this.penaltyFlags = [0, 1].map(i => parseInt(fenflags[i]));
+ this.penaltyFlags = {
+ 'w': parseInt(fenflags[0]),
+ 'b': parseInt(fenflags[1])
+ };
}
getWhitemoveFen() {
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;
// 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 &&
}
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;
}
// 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]);
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
});
}
}
}
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);
this.whiteMove = move;
return;
}
-
// A full turn just ended:
const smove = this.resolveSynchroneMove(move);
V.PlayOnBoard(this.board, smove);
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) {
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";