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? Then, 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
250 moves
.length
== 0 && piece
== V
.BISHOP
&&
251 [0, 8].includes(x
) && [0, 8].includes(y
)
253 const indX
= x
== 0 ? [1, 2] : [7, 6];
254 const indY
= y
== 0 ? [1, 2] : [7, 6];
256 this.board
[indX
[0]][indY
[0]] != V
.EMPTY
&&
257 this.board
[indX
[1]][indY
[1]] == V
.EMPTY
259 return [super.getBasicMove([x
, y
], [indX
[1], indY
[1]])];
264 // Kicking the ball: look for adjacent pieces.
265 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
269 for (let s
of steps
) {
270 const [i
, j
] = [x
+ s
[0], y
+ s
[1]];
273 this.board
[i
][j
] != V
.EMPTY
&&
274 this.getColor(i
, j
) == c
276 const kmoves
= this.tryKickFrom([i
, j
]);
277 kmoves
.forEach(km
=> {
278 const key
= V
.CoordsToSquare(km
.start
) + V
.CoordsToSquare(km
.end
);
286 if (Object
.keys(kicks
).length
> 0) {
287 // And, always add the "end" move. For computer, keep only one
288 outerLoop: for (let i
=0; i
< V
.size
.x
; i
++) {
289 for (let j
=0; j
< V
.size
.y
; j
++) {
290 if (this.board
[i
][j
] != V
.EMPTY
&& this.getColor(i
, j
) == c
) {
291 moves
.push(super.getBasicMove([x
, y
], [i
, j
]));
292 if (computer
) break outerLoop
;
304 // Extra arg "computer" to avoid trimming all redundant pass moves:
305 getAllPotentialMoves(computer
) {
306 const color
= this.turn
;
307 let potentialMoves
= [];
308 for (let i
= 0; i
< V
.size
.x
; i
++) {
309 for (let j
= 0; j
< V
.size
.y
; j
++) {
310 if (this.board
[i
][j
] != V
.EMPTY
&& this.getColor(i
, j
) == color
) {
311 Array
.prototype.push
.apply(
313 this.getPotentialMovesFrom([i
, j
], computer
)
318 return potentialMoves
;
322 return this.filterValid(this.getAllPotentialMoves("computer"));
326 const L
= this.kickedBy
.length
;
327 const kb
= this.kickedBy
[L
-1];
328 return moves
.filter(m
=> !m
.start
.by
|| !kb
[m
.start
.by
]);
335 allowAnotherPass(color
) {
336 // Two cases: a piece moved, or the ball moved.
337 // In both cases, check our pieces and ball proximity,
338 // so the move played doesn't matter (if ball position updated)
339 const bp
= this.ballPos
;
340 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
341 for (let s
of steps
) {
342 const [i
, j
] = [this.ballPos
[0] + s
[0], this.ballPos
[1] + s
[1]];
345 this.board
[i
][j
] != V
.EMPTY
&&
346 this.getColor(i
, j
) == color
348 return true; //potentially...
355 if (move.appear
[0].p
== 'a')
356 this.ballPos
= [move.appear
[0].x
, move.appear
[0].y
];
360 // Special message saying "passes are over"
361 const passesOver
= (move.vanish
.length
== 2);
364 V
.PlayOnBoard(this.board
, move);
366 move.turn
= [this.turn
, this.subTurn
]; //easier undo
367 if (passesOver
|| !this.allowAnotherPass(this.turn
)) {
368 this.turn
= V
.GetOppCol(this.turn
);
371 this.kickedBy
.push( {} );
375 if (!!move.start
.by
) {
376 const L
= this.kickedBy
.length
;
377 this.kickedBy
[L
-1][move.start
.by
] = true;
383 const passesOver
= (move.vanish
.length
== 2);
384 if (move.turn
[0] != this.turn
) {
385 [this.turn
, this.subTurn
] = move.turn
;
391 if (!!move.start
.by
) {
392 const L
= this.kickedBy
.length
;
393 delete this.kickedBy
[L
-1][move.start
.by
];
397 V
.UndoOnBoard(this.board
, move);
403 if (move.vanish
[0].p
== 'a')
404 this.ballPos
= [move.vanish
[0].x
, move.vanish
[0].y
];
408 if (this.board
[0][4] == V
.BALL
) return "1-0";
409 if (this.board
[8][4] == V
.BALL
) return "0-1";
414 let initMoves
= this.getAllValidMoves();
415 if (initMoves
.length
== 0) return null;
416 let moves
= JSON
.parse(JSON
.stringify(initMoves
));
419 // Just play random moves (for now at least. TODO?)
421 while (moves
.length
> 0) {
422 mv
= moves
[randInt(moves
.length
)];
425 if (mv
.vanish
.length
== 1 && this.allowAnotherPass(c
))
427 moves
= this.getPotentialMovesFrom(this.ballPos
);
430 for (let i
= mvArray
.length
- 1; i
>= 0; i
--) this.undo(mvArray
[i
]);
431 return (mvArray
.length
> 1 ? mvArray : mvArray
[0]);
434 // NOTE: evalPosition() is wrong, but unused since bot plays at random
437 if (move.vanish
.length
== 2) return "pass";
438 if (move.vanish
[0].p
!= 'a') return super.getNotation(move);
439 // Kick: simple notation (TODO?)
440 return V
.CoordsToSquare(move.end
);