โฝ How to Play Chess League
๐ฏ Objective
Kick the red ball off your opponent's end of the board to score goals.
First to 5 goals wins the match.
โ Taking turns
Players alternate turns. White (bottom) always goes first.
Click a piece to select it, then click a destination to move.
Valid destination squares highlight in green.
โฝ Kicking the ball
Move any piece onto the ball's square to kick it. The ball flies 3
squares beyond that square in the same direction. It bounces off the
left and right walls but exits through the top or bottom โ scoring a goal.
๐
บ Blocking the ball
Kings and Rooks block the ball (it stops before them).
All other pieces are destroyed if the ball hits them.
๐ Pawn special rules
- Pawns can kick the ball diagonally (like a normal capture).
- A pawn on its starting row can double-step even if the ball is in between โ jumping over it and kicking it.
- NEW: A pawn that reaches the opponent's back row scores an instant goal!
๐
HEAD goal
If a piece moves the ball directly into the opponent's back row square
(row 0 or row 7) in one step, that counts as a HEAD goal for the mover.
Moving the ball into your own back row is a CLEARED save โ
the ball resets to the centre.
๐ After a goal
The board resets with pieces back in starting positions. The ball
appears randomly in the centre area. The player who conceded kicks off.
// Utility: evaluate if a square is attacked
isSquareAttacked(board, ball, square, bySide) {
for (const [key, piece] of board) {
if (piece[0] !== bySide) continue;
const [cx, cy] = key.split(',').map(Number);
if (this.isLegal(board, ball, [cx, cy], square)) return true;
// Check pawn "jump" kick
if (piece[1] === 'P' && this.isPawnJumpBall(board, ball, [cx, cy], square)) return true;
}
return false;
},
// More careful scoring for CPU; depth = recursion (deeper = harder)
scoreCPUMove(board, ball, from, to, cpu, depth = 1) {
const opp = cpu === 'W' ? 'B' : 'W';
const outcome = this.applyMove(board, ball, from, to);
// Win/loss logic
if (outcome.goal === cpu || outcome.pawnGoal === cpu) return 50000 * depth + Math.random() * 500;
if (outcome.goal === opp || outcome.pawnGoal === opp) return -50000 * depth;
let score = 0;
// Center control value (positional)
const normPos = (p) => ((p[0] - 3.5) ** 2 + (p[1] - 3.5) ** 2);
score -= normPos(to) * 12; // Encourage central squares
// Piece value
const PIECE_VAL = { K: 10000, Q: 900, R: 500, B: 320, N: 300, P: 100 };
for (const [, piece] of outcome.newBoard) {
if (piece[0] === cpu) score += PIECE_VAL[piece[1]] * 0.6;
else score -= PIECE_VAL[piece[1]] * 0.5;
}
// Avoid repeat pawn moves (no retreating)
const movedPiece = board.get(`${from[0]},${from[1]}`);
const prevPawnMove = movedPiece && movedPiece[1] === 'P' && ((cpu === 'W' && to[1] > from[1]) || (cpu === 'B' && to[1] < from[1]));
if (prevPawnMove) score -= 30; // Penalize pawn retreat
// Ball danger: penalize if after this move, our piece is exposed to capture or to the ball
let punishExposed = false;
for (const [key, piece] of outcome.newBoard) {
if (piece[0] !== cpu) continue;
const sq = key.split(',').map(Number);
if (this.isSquareAttacked(outcome.newBoard, outcome.newBall, sq, opp)) {
// Not king
if (piece[1] !== 'K') {
punishExposed = true;
score -= PIECE_VAL[piece[1]] * 0.6;
}
}
}
// Ball position: more advanced ball (offensive)
const ballAdv = (cpu === 'B') ? (outcome.newBall[1] - ball[1]) : (ball[1] - outcome.newBall[1]);
score += ballAdv * 250;
score += (cpu === 'B' ? outcome.newBall[1] : (7 - outcome.newBall[1])) * 80;
// Kicking ball is good if safe
const isBallKick = (to[0] === ball[0] && to[1] === ball[1]) || this.isPawnJumpBall(board, ball, from, to);
if (isBallKick) score += punishExposed ? 10 : 100;
// Minor piece development bonus
if (movedPiece && (movedPiece[1] === 'N' || movedPiece[1] === 'B' || movedPiece[1] === 'Q' || movedPiece[1] === 'R')) {
if ((cpu === 'W' && from[1] === 7) || (cpu === 'B' && from[1] === 0)) score += 80;
}
// Pawn advancement
if (movedPiece && movedPiece[1] === 'P') {
const advance = cpu === 'B' ? to[1] : (7 - to[1]);
score += advance * 27;
}
// Defensive: if the move blocks/checks, bonus
if (this.isSquareAttacked(outcome.newBoard, outcome.newBall, [outcome.newBall[0], outcome.newBall[1]], cpu))
score += 90;
// Try not to lose piece for nothing!
if (punishExposed) score -= 120;
// Discourage repeated board positions (very basic version)
if (this.lastFromTo && from[0] === this.lastFromTo[1][0] && from[1] === this.lastFromTo[1][1] &&
to[0] === this.lastFromTo[0][0] && to[1] === this.lastFromTo[0][1]) {
score -= 300;
}
// Do a shallow minimax if depth > 0
if (depth > 0) {
const oppMoves = this.generateMoves(outcome.newBoard, outcome.newBall, opp);
let worst = 9999999;
for (const om of oppMoves) {
const s = this.scoreCPUMove(outcome.newBoard, outcome.newBall, om.from, om.to, opp, 0);
if (s < worst) worst = s;
}
score -= worst * 0.45; // Defensive weighting
}
return score + (Math.random() - 0.5) * 18;
},
pickCPUMove(board, ball, cpu) {
const moves = this.generateMoves(board, ball, cpu);
if (moves.length === 0) return null;
// If any move scores a goal, pick it immediately
for (const mv of moves) {
if (this.moveScoredGoal(board, ball, mv.from, mv.to, cpu)) return mv;
}
let best = null;
let bestScore = -Infinity;
for (const mv of moves) {
const s = this.scoreCPUMove(board, ball, mv.from, mv.to, cpu, 1); // "1" means do a 2-ply minimax
if (s > bestScore) { bestScore = s; best = mv; }
}
// Save move history for anti-repetition scoring
this.lastFromTo = best ? [best.from, best.to] : null;
return best;
},
// Don't blunder into immediate loss
const oppMoves = this.generateMoves(outcome.newBoard, outcome.newBall, opp);
let oppCanScoreImmediately = false, pawnJustPushedExposed = false;
for (const om of oppMoves) {
if (this.moveScoredGoal(outcome.newBoard, outcome.newBall, om.from, om.to, opp)) {
oppCanScoreImmediately = true;
break;
}
// Penalize if we push pawn and pawn can be captured right away
// Only apply if the moved piece is a pawn and it advanced two squares
if (board.get(`${from[0]},${from[1]}`)?.[1] === 'P') {
const was2step = Math.abs(to[1] - from[1]) === 2;
if (was2step && om.to[0] === to[0] && om.to[1] === to[1]) pawnJustPushedExposed = true;
}
}
if (oppCanScoreImmediately) score -= 800_000;
// Material
const PIECE_VAL = { K: 10000, Q: 900, R: 500, B: 320, N: 300, P: 100 };
for (const [, piece] of outcome.newBoard) {
if (piece[0] === cpu) score += PIECE_VAL[piece[1]] * 0.4;
else score -= PIECE_VAL[piece[1]] * 0.3;
}
// Prefer developing minor pieces (knight, bishop) early, especially from the back row
const sp = board.get(`${from[0]},${from[1]}`);
if (sp && (sp[1] === 'N' || sp[1] === 'B') && ((cpu === 'B' && from[1] === 0) || (cpu === 'W' && from[1] === 7))) {
score += 65; // give a bonus for development move in opening
}
// If pawn pushed two steps from starting row, score less unless safe
if (sp && sp[1] === 'P') {
const startingRow = (cpu === 'B') ? 1 : 6;
const was2step = Math.abs(to[1] - from[1]) === 2 && from[1] === startingRow;
if (was2step) {
if (pawnJustPushedExposed) {
score -= 300;
} else {
score += 15; // modest, not huge
}
}
const advance = cpu === 'B' ? to[1] : (7 - to[1]);
score += advance * 23; // Lower than before (was 35)
if (cpu === 'B' && to[1] >= 5) score += 50; // Lower than before
if (cpu === 'W' && to[1] <= 2) score += 50;
}
// Ball proximity encouragement
if (!isBallKick) {
const distAfter = Math.abs(to[0] - outcome.newBall[0]) + Math.abs(to[1] - outcome.newBall[1]);
score += Math.max(0, 7 - distAfter) * 30;
}
// Add tiny random factor to diversify opening
score += (Math.random() - 0.5) * 60;
return score;
}