1 import { ChessRules
} from "@/base_rules";
2 import { randInt
, shuffle
} from "@/utils/alea";
3 import { ArrayFun
} from "@/utils/array";
5 export class FootballRules
extends ChessRules
{
7 static get HasEnpassant() {
11 static get HasFlags() {
16 return { x: 9, y: 9 };
33 // 'b' is already taken:
37 // Check that exactly one ball is on the board
38 // + at least one piece per color.
39 static IsGoodPosition(position
) {
40 if (position
.length
== 0) return false;
41 const rows
= position
.split("/");
42 if (rows
.length
!= V
.size
.x
) return false;
43 let pieces
= { "w": 0, "b": 0 };
45 for (let row
of rows
) {
47 for (let i
= 0; i
< row
.length
; i
++) {
48 const lowerRi
= row
[i
].toLowerCase();
49 if (!!lowerRi
.match(/^[a-z]$/)) {
50 if (V
.PIECES
.includes(lowerRi
))
51 pieces
[row
[i
] == lowerRi
? "b" : "w"]++;
52 else if (lowerRi
== 'a') ballCount
++;
57 const num
= parseInt(row
[i
], 10);
58 if (isNaN(num
)) return false;
62 if (sumElts
!= V
.size
.y
) return false;
64 if (ballCount
!= 1 || Object
.values(pieces
).some(v
=> v
== 0))
70 if (b
== V
.BALL
) return 'a';
71 return ChessRules
.board2fen(b
);
75 if (f
== 'a') return V
.BALL
;
76 return ChessRules
.fen2board(f
);
80 if (b
== V
.BALL
) return "Football/ball";
84 canIplay(side
, [x
, y
]) {
87 (this.board
[x
][y
] == V
.BALL
|| this.getColor(x
, y
) == side
)
91 // No checks or king tracking etc. But, track ball
93 // Stack of "kicked by" coordinates, to avoid infinite loops
94 this.kickedBy
= [ {} ];
96 this.ballPos
= [-1, -1];
97 for (let i
=0; i
< V
.size
.x
; i
++) {
98 for (let j
=0; j
< V
.size
.y
; j
++) {
99 if (this.board
[i
][j
] == V
.BALL
) {
100 this.ballPos
= [i
, j
];
107 static GenRandInitFen(options
) {
108 if (options
.randomness
== 0)
109 return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0";
111 let pieces
= { w: new Array(8), b: new Array(8) };
112 for (let c
of ["w", "b"]) {
113 if (c
== 'b' && options
.randomness
== 1) {
114 pieces
['b'] = pieces
['w'];
118 // Get random squares for every piece, totally freely
119 let positions
= shuffle(ArrayFun
.range(8));
120 const composition
= ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
121 // Fix bishops (on different colors)
123 (pos
) => { return (pos
<= 3 ? pos
% 2 : (pos
+ 1) % 2); };
124 const rem2
= realOddity(positions
[0]);
125 if (rem2
== realOddity(positions
[1])) {
126 for (let i
=2; i
<8; i
++) {
127 if (realOddity(positions
[i
]) != rem2
) {
128 [positions
[1], positions
[i
]] = [positions
[i
], positions
[1]];
133 for (let i
= 0; i
< 8; i
++) pieces
[c
][positions
[i
]] = composition
[i
];
135 const piecesB
= pieces
["b"].join("") ;
136 const piecesW
= pieces
["w"].join("").toUpperCase();
138 piecesB
.substr(0, 4) + "1" + piecesB
.substr(4) +
139 "/9/9/9/4a4/9/9/9/" +
140 piecesW
.substr(0, 4) + "1" + piecesW
.substr(4) +
145 tryKickFrom([x
, y
]) {
146 const bp
= this.ballPos
;
147 const emptySquare
= (i
, j
) => {
148 return V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
;
150 // Kick the (adjacent) ball from x, y with current turn:
151 const step
= [bp
[0] - x
, bp
[1] - y
];
152 const piece
= this.getPiece(x
, y
);
154 if (piece
== V
.KNIGHT
) {
155 // The knight case is particular
156 V
.steps
[V
.KNIGHT
].forEach(s
=> {
157 const [i
, j
] = [bp
[0] + s
[0], bp
[1] + s
[1]];
160 this.board
[i
][j
] == V
.EMPTY
&&
162 // In a corner? The, allow all ball moves
163 ([0, 8].includes(bp
[0]) && [0, 8].includes(bp
[1])) ||
164 // Do not end near the knight
165 (Math
.abs(i
- x
) >= 2 || Math
.abs(j
- y
) >= 2)
168 moves
.push(super.getBasicMove(bp
, [i
, j
]));
173 let compatible
= false,
177 compatible
= (step
[0] == 0 || step
[1] == 0);
180 compatible
= (step
[0] != 0 && step
[1] != 0);
190 if (!compatible
) return [];
191 let [i
, j
] = [bp
[0] + step
[0], bp
[1] + step
[1]];
192 const horizontalStepOnGoalRow
=
193 ([0, 8].includes(bp
[0]) && step
.some(s
=> s
== 0));
196 (this.movesCount
>= 2 || j
!= 4 || ![0, 8].includes(i
)) &&
197 (!horizontalStepOnGoalRow
|| j
!= 4)
199 moves
.push(super.getBasicMove(bp
, [i
, j
]));
204 if (!emptySquare(i
, j
)) break;
206 (this.movesCount
>= 2 || j
!= 4 || ![0, 8].includes(i
)) &&
207 (!horizontalStepOnGoalRow
|| j
!= 4)
209 moves
.push(super.getBasicMove(bp
, [i
, j
]));
214 // Try the other direction (TODO: experimental)
215 [i
, j
] = [bp
[0] - 2*step
[0], bp
[1] - 2*step
[1]];
218 (this.movesCount
>= 2 || j
!= 4 || ![0, 8].includes(i
)) &&
219 (!horizontalStepOnGoalRow
|| j
!= 4)
221 moves
.push(super.getBasicMove(bp
, [i
, j
]));
226 if (!emptySquare(i
, j
)) break;
228 (this.movesCount
>= 2 || j
!= 4 || ![0, 8].includes(i
)) &&
229 (!horizontalStepOnGoalRow
|| j
!= 4)
231 moves
.push(super.getBasicMove(bp
, [i
, j
]));
237 const kickedFrom
= x
+ "-" + y
;
238 moves
.forEach(m
=> m
.start
.by
= kickedFrom
)
242 getPotentialMovesFrom([x
, y
], computer
) {
243 const piece
= this.getPiece(x
, y
);
244 if (V
.PIECES
.includes(piece
)) {
245 if (this.subTurn
> 1) return [];
246 const moves
= super.getPotentialMovesFrom([x
, y
])
247 .filter(m
=> m
.end
.y
!= 4 || ![0, 8].includes(m
.end
.x
));
248 // If bishop stuck in a corner: allow to jump over the next obstacle
249 if (moves
.length
== 0 && piece
== V
.BISHOP
) {
252 this.board
[1][1] != V
.EMPTY
&&
253 this.board
[2][2] == V
.EMPTY
255 return [super.getBasicMove([x
, y
], [2, 2])];
259 this.board
[1][7] != V
.EMPTY
&&
260 this.board
[2][6] == V
.EMPTY
262 return [super.getBasicMove([x
, y
], [2, 6])];
266 this.board
[7][1] != V
.EMPTY
&&
267 this.board
[6][2] == V
.EMPTY
269 return [super.getBasicMove([x
, y
], [6, 2])];
273 this.board
[7][7] != V
.EMPTY
&&
274 this.board
[6][6] == V
.EMPTY
276 return [super.getBasicMove([x
, y
], [6, 6])];
281 // Kicking the ball: look for adjacent pieces.
282 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
286 for (let s
of steps
) {
287 const [i
, j
] = [x
+ s
[0], y
+ s
[1]];
290 this.board
[i
][j
] != V
.EMPTY
&&
291 this.getColor(i
, j
) == c
293 const kmoves
= this.tryKickFrom([i
, j
]);
294 kmoves
.forEach(km
=> {
295 const key
= V
.CoordsToSquare(km
.start
) + V
.CoordsToSquare(km
.end
);
303 if (Object
.keys(kicks
).length
> 0) {
304 // And, always add the "end" move. For computer, keep only one
305 outerLoop: for (let i
=0; i
< V
.size
.x
; i
++) {
306 for (let j
=0; j
< V
.size
.y
; j
++) {
307 if (this.board
[i
][j
] != V
.EMPTY
&& this.getColor(i
, j
) == c
) {
308 moves
.push(super.getBasicMove([x
, y
], [i
, j
]));
309 if (!!computer
) break outerLoop
;
321 // Extra arg "computer" to avoid trimming all redundant pass moves:
322 getAllPotentialMoves(computer
) {
323 const color
= this.turn
;
324 let potentialMoves
= [];
325 for (let i
= 0; i
< V
.size
.x
; i
++) {
326 for (let j
= 0; j
< V
.size
.y
; j
++) {
327 if (this.board
[i
][j
] != V
.EMPTY
&& this.getColor(i
, j
) == color
) {
328 Array
.prototype.push
.apply(
330 this.getPotentialMovesFrom([i
, j
], computer
)
335 return potentialMoves
;
339 return this.filterValid(this.getAllPotentialMoves("computer"));
343 const L
= this.kickedBy
.length
;
344 const kb
= this.kickedBy
[L
-1];
345 return moves
.filter(m
=> !m
.start
.by
|| !kb
[m
.start
.by
]);
352 allowAnotherPass(color
) {
353 // Two cases: a piece moved, or the ball moved.
354 // In both cases, check our pieces and ball proximity,
355 // so the move played doesn't matter (if ball position updated)
356 const bp
= this.ballPos
;
357 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
358 for (let s
of steps
) {
359 const [i
, j
] = [this.ballPos
[0] + s
[0], this.ballPos
[1] + s
[1]];
362 this.board
[i
][j
] != V
.EMPTY
&&
363 this.getColor(i
, j
) == color
365 return true; //potentially...
372 if (move.appear
[0].p
== 'a')
373 this.ballPos
= [move.appear
[0].x
, move.appear
[0].y
];
377 // Special message saying "passes are over"
378 const passesOver
= (move.vanish
.length
== 2);
381 V
.PlayOnBoard(this.board
, move);
383 move.turn
= [this.turn
, this.subTurn
]; //easier undo
384 if (passesOver
|| !this.allowAnotherPass(this.turn
)) {
385 this.turn
= V
.GetOppCol(this.turn
);
388 this.kickedBy
.push( {} );
392 if (!!move.start
.by
) {
393 const L
= this.kickedBy
.length
;
394 this.kickedBy
[L
-1][move.start
.by
] = true;
400 const passesOver
= (move.vanish
.length
== 2);
401 if (move.turn
[0] != this.turn
) {
402 [this.turn
, this.subTurn
] = move.turn
;
408 if (!!move.start
.by
) {
409 const L
= this.kickedBy
.length
;
410 delete this.kickedBy
[L
-1][move.start
.by
];
414 V
.UndoOnBoard(this.board
, move);
420 if (move.vanish
[0].p
== 'a')
421 this.ballPos
= [move.vanish
[0].x
, move.vanish
[0].y
];
425 if (this.board
[0][4] == V
.BALL
) return "1-0";
426 if (this.board
[8][4] == V
.BALL
) return "0-1";
431 let initMoves
= this.getAllValidMoves();
432 if (initMoves
.length
== 0) return null;
433 let moves
= JSON
.parse(JSON
.stringify(initMoves
));
436 // Just play random moves (for now at least. TODO?)
438 while (moves
.length
> 0) {
439 mv
= moves
[randInt(moves
.length
)];
442 if (mv
.vanish
.length
== 1 && this.allowAnotherPass(c
))
444 moves
= this.getPotentialMovesFrom(this.ballPos
);
447 for (let i
= mvArray
.length
- 1; i
>= 0; i
--) this.undo(mvArray
[i
]);
448 return (mvArray
.length
> 1 ? mvArray : mvArray
[0]);
451 // NOTE: evalPosition() is wrong, but unused since bot plays at random
454 if (move.vanish
.length
== 2) return "pass";
455 if (move.vanish
[0].p
!= 'a') return super.getNotation(move);
456 // Kick: simple notation (TODO?)
457 return V
.CoordsToSquare(move.end
);