CHESS LEAGUE
CHESS ร— FOOTBALL
โšฝ
Move pieces onto the ball to kick it ยท First to 5 goals wins
โ˜… Pawns reaching the opponent's back row score instantly!
โšฝ 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; }