Chess Tutor — Animated Learning Game
by SonicGalaxy24852 lines31.5 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Chess Tutor — Animated Learning Game</title>
<style>
:root{
--light:#f0d9b5;
--dark:#b58863;
--accent:#2b7a78;
--accent-2:#f08a5d;
--board-size: min(84vmin,720px);
}
body{
margin:0;
font-family:Inter,system-ui,Segoe UI,Roboto,Arial;
display:flex;
gap:20px;
align-items:flex-start;
justify-content:center;
padding:24px;
background:linear-gradient(180deg,#0f1724 0%, #071021 100%);
color:#e6eef6;
min-height:100vh;
}
.container{
display:flex;
gap:20px;
align-items:flex-start;
}
.board-wrap{
width:var(--board-size);
height:var(--board-size);
position:relative;
border-radius:12px;
box-shadow:0 10px 30px rgba(0,0,0,0.6);
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.15));
padding:12px;
}
.board{
width:100%;
height:100%;
display:grid;
grid-template-columns:repeat(8,1fr);
grid-template-rows:repeat(8,1fr);
border-radius:8px;
overflow:hidden;
position:relative;
}
.square{
position:relative;
user-select:none;
-webkit-user-select:none;
display:flex;
align-items:center;
justify-content:center;
font-size:calc(var(--board-size) / 12);
transition:background 160ms ease;
}
.square.light{ background: var(--light); }
.square.dark{ background: var(--dark); }
.square.highlight{ outline:4px solid rgba(255,255,0,0.18); box-shadow:0 6px 18px rgba(0,0,0,0.35) inset; }
.square.hint{ box-shadow:0 0 0 4px rgba(43,122,120,0.18) inset; }
.piece{
width:78%;
height:78%;
display:flex;
align-items:center;
justify-content:center;
cursor:grab;
transition:transform 220ms cubic-bezier(.2,.9,.2,1), box-shadow 160ms;
z-index:2;
position:relative;
will-change:transform;
user-select:none;
-webkit-user-select:none;
}
.piece:active{ cursor:grabbing; transform:scale(1.02); }
.piece.white{ filter:drop-shadow(0 6px 10px rgba(0,0,0,0.35)); }
.piece.black{ filter:drop-shadow(0 6px 10px rgba(0,0,0,0.35)); }
.piece svg{ width:100%; height:100%; pointer-events:none; }
.ui{
width:360px;
max-width:36vw;
min-width:260px;
display:flex;
flex-direction:column;
gap:12px;
}
.panel{
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.12));
border-radius:10px;
padding:12px;
box-shadow:0 8px 20px rgba(0,0,0,0.5);
}
h1{ margin:0 0 6px 0; font-size:18px; color:var(--accent); }
p.small{ margin:0; color:#cfe8e6; font-size:13px; }
.controls{ display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; }
button{
background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
border:1px solid rgba(255,255,255,0.06);
color:#eaf6f5;
padding:8px 10px;
border-radius:8px;
cursor:pointer;
font-weight:600;
transition:transform 120ms ease, box-shadow 120ms;
}
button:hover{ transform:translateY(-3px); box-shadow:0 8px 20px rgba(0,0,0,0.45); }
.status{ font-weight:700; color:#fff; margin-top:6px; }
.log{ max-height:240px; overflow:auto; padding:8px; border-radius:8px; background:rgba(0,0,0,0.18); font-size:13px; }
.lesson-step{ padding:10px; border-radius:8px; background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(0,0,0,0.08)); }
.small-muted{ color:#bcd; font-size:13px; }
.controls .primary{ background:linear-gradient(180deg,var(--accent),#1b5f5e); border:0; color:#fff; }
.controls .danger{ background:linear-gradient(180deg,#b33,#8b1f1f); border:0; }
.footer{ font-size:12px; color:#bcd; margin-top:6px; }
/* animation helper for moving piece overlay */
.floating{
position:fixed;
pointer-events:none;
z-index:9999;
transform-origin:center center;
transition:transform 220ms cubic-bezier(.2,.9,.2,1);
}
.legend{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px; }
.chip{ padding:6px 8px; border-radius:999px; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); font-size:13px; }
@media (max-width:900px){
body{ padding:12px; }
.container{ flex-direction:column; align-items:center; }
.ui{ width:100%; max-width:100%; }
}
</style>
</head>
<body>
<div class="container">
<div class="board-wrap panel" aria-label="Chess board">
<div id="board" class="board" role="grid" aria-label="Chessboard"></div>
</div>
<div class="ui">
<div class="panel">
<h1>Chess Tutor</h1>
<p class="small">Animated coach that plays with you, explains moves, and gives hints. Click a piece to move or drag it.</p>
<div class="controls" style="margin-top:10px;">
<button id="newBtn" class="primary">New Game</button>
<button id="undoBtn">Undo</button>
<button id="hintBtn">Hint</button>
<button id="lessonBtn">Lesson Mode</button>
<button id="flipBtn">Flip Board</button>
</div>
<div class="status" id="status">White to move</div>
<div class="legend">
<div class="chip">Animated moves</div>
<div class="chip">Hints & coach</div>
<div class="chip">Simple AI</div>
</div>
</div>
<div class="panel">
<div style="display:flex;justify-content:space-between;align-items:center;">
<strong>Coach Log</strong>
<small class="small-muted">teaching tips appear here</small>
</div>
<div id="log" class="log" aria-live="polite"></div>
</div>
<div class="panel lesson-step" id="lessonPanel">
<strong>Lesson Mode</strong>
<p class="small-muted" id="lessonText">Press <strong>Lesson Mode</strong> to start a guided mini-lesson (opening principles, forks, basic mate patterns).</p>
<div style="display:flex;gap:8px;margin-top:8px;">
<button id="nextLesson">Next</button>
<button id="resetLesson" class="danger">Reset Lesson</button>
</div>
</div>
<div class="panel footer">
<div><strong>Controls</strong></div>
<div class="small-muted">Click a piece, then click a target square. Use Hint for suggestions. Lesson Mode animates examples.</div>
</div>
</div>
</div>
<script>
/* ---------------------------
Simple chess engine + UI
- Board representation: 8x8 array, pieces as objects {t:'p',c:'w'} (type, color)
- Move generation: generates pseudo-legal moves and filters out moves leaving king in check
- AI: minimax depth 2 with material eval
- Animations: CSS transforms for smooth piece movement
- Coach: logs explanations for moves and patterns
--------------------------- */
const PIECE_SVG = {
p_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M22.5 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M12 34c2-2 8-4 10-4s8 2 10 4c-6 2-14 2-20 0z"/></g></svg>`,
r_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M9 39h27v-6H9z"/><path d="M12 33v-6h21v6"/></g></svg>`,
n_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M12 33c6-6 12-6 18 0-6 2-12 2-18 0z"/><path d="M18 12c2-4 8-4 10 0-2 2-6 2-10 0z"/></g></svg>`,
b_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M22.5 11c-4 4-8 8-8 12 4 2 8 2 12 0 0-4-4-8-4-12z"/><path d="M12 33c6-6 18-6 24 0-8 2-16 2-24 0z"/></g></svg>`,
q_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M9 33h27v6H9z"/><path d="M12 12l6 6 6-6 6 6"/></g></svg>`,
k_white: `<svg viewBox="0 0 45 45"><g fill="#fff" stroke="#000" stroke-width="1"><path d="M22.5 9v6"/><path d="M18 18h9"/><path d="M12 33c6-6 18-6 24 0-8 2-16 2-24 0z"/></g></svg>`,
p_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M22.5 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M12 34c2-2 8-4 10-4s8 2 10 4c-6 2-14 2-20 0z"/></g></svg>`,
r_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M9 39h27v-6H9z"/><path d="M12 33v-6h21v6"/></g></svg>`,
n_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M12 33c6-6 12-6 18 0-6 2-12 2-18 0z"/><path d="M18 12c2-4 8-4 10 0-2 2-6 2-10 0z"/></g></svg>`,
b_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M22.5 11c-4 4-8 8-8 12 4 2 8 2 12 0 0-4-4-8-4-12z"/><path d="M12 33c6-6 18-6 24 0-8 2-16 2-24 0z"/></g></svg>`,
q_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M9 33h27v6H9z"/><path d="M12 12l6 6 6-6 6 6"/></g></svg>`,
k_black: `<svg viewBox="0 0 45 45"><g fill="#111" stroke="#000" stroke-width="1"><path d="M22.5 9v6"/><path d="M18 18h9"/><path d="M12 33c6-6 18-6 24 0-8 2-16 2-24 0z"/></g></svg>`
};
/* ---------- Board state ---------- */
let boardEl = document.getElementById('board');
let statusEl = document.getElementById('status');
let logEl = document.getElementById('log');
let hintBtn = document.getElementById('hintBtn');
let newBtn = document.getElementById('newBtn');
let undoBtn = document.getElementById('undoBtn');
let lessonBtn = document.getElementById('lessonBtn');
let flipBtn = document.getElementById('flipBtn');
let nextLessonBtn = document.getElementById('nextLesson');
let resetLessonBtn = document.getElementById('resetLesson');
let lessonPanel = document.getElementById('lessonPanel');
let lessonText = document.getElementById('lessonText');
let board = []; // 8x8 array
let turn = 'w';
let selected = null;
let legalCache = null;
let history = [];
let floating = null;
let flipped = false;
let lessonMode = false;
let lessonStep = 0;
/* initialize board with standard setup */
function initBoard(){
board = Array.from({length:8},()=>Array(8).fill(null));
const back = ['r','n','b','q','k','b','n','r'];
for(let i=0;i<8;i++){
board[0][i] = {t:back[i], c:'b'};
board[1][i] = {t:'p', c:'b'};
board[6][i] = {t:'p', c:'w'};
board[7][i] = {t:back[i], c:'w'};
}
turn = 'w';
history = [];
selected = null;
legalCache = null;
updateBoardUI(true);
log("New game started. Focus on development: bring knights and bishops out early.");
updateStatus();
}
/* ---------- UI rendering ---------- */
function updateBoardUI(reset=false){
boardEl.innerHTML = '';
// create squares a8..h1 (row 0 is 8th rank)
for(let r=0;r<8;r++){
for(let f=0;f<8;f++){
const sq = document.createElement('div');
sq.className = 'square ' + (((r+f)%2===0)?'light':'dark');
sq.dataset.r = r; sq.dataset.f = f;
sq.id = `sq-${r}-${f}`;
// coordinate label small (optional)
// piece
const p = board[r][f];
if(p){
const pieceEl = document.createElement('div');
pieceEl.className = 'piece ' + (p.c==='w'?'white':'black');
pieceEl.dataset.r = r; pieceEl.dataset.f = f;
pieceEl.dataset.type = p.t; pieceEl.dataset.color = p.c;
pieceEl.innerHTML = PIECE_SVG[`${p.t}_${p.c==='w'?'white':'black'}`];
pieceEl.addEventListener('pointerdown', onPiecePointerDown);
pieceEl.addEventListener('click', onPieceClick);
sq.appendChild(pieceEl);
}
sq.addEventListener('click', onSquareClick);
boardEl.appendChild(sq);
}
}
if(reset) clearHighlights();
}
/* ---------- Utilities ---------- */
function coordToAlgebraic(r,f){
return String.fromCharCode(97+f) + (8-r);
}
function algebraicToCoord(s){
const f = s.charCodeAt(0)-97;
const r = 8 - parseInt(s[1],10);
return [r,f];
}
function cloneBoard(b){
return b.map(row=>row.map(cell=>cell?{t:cell.t,c:cell.c}:null));
}
function inBounds(r,f){ return r>=0 && r<8 && f>=0 && f<8; }
function pieceAt(r,f){ return inBounds(r,f)?board[r][f]:null; }
/* ---------- Move generation (pseudo-legal then filter checks) ---------- */
function generatePseudoMovesFor(r,f){
const p = board[r][f];
if(!p) return [];
const moves = [];
const dir = p.c==='w'? -1 : 1;
if(p.t==='p'){
// forward
const r1 = r + dir;
if(inBounds(r1,f) && !board[r1][f]) moves.push({from:[r,f],to:[r1,f],promote:(r1===0||r1===7)});
// double
const startRow = p.c==='w'?6:1;
const r2 = r + 2*dir;
if(r===startRow && !board[r1][f] && !board[r2][f]) moves.push({from:[r,f],to:[r2,f]});
// captures
for(const df of [-1,1]){
const rf = r + dir, ff = f + df;
if(inBounds(rf,ff) && board[rf][ff] && board[rf][ff].c !== p.c){
moves.push({from:[r,f],to:[rf,ff],capture:true,promote:(rf===0||rf===7)});
}
}
} else if(p.t==='n'){
const deltas = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
for(const [dr,df] of deltas){
const rr=r+dr, ff=f+df;
if(inBounds(rr,ff) && (!board[rr][ff] || board[rr][ff].c!==p.c)) moves.push({from:[r,f],to:[rr,ff]});
}
} else if(p.t==='b' || p.t==='r' || p.t==='q'){
const dirs = [];
if(p.t==='b' || p.t==='q') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
if(p.t==='r' || p.t==='q') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
for(const [dr,df] of dirs){
let rr=r+dr, ff=f+df;
while(inBounds(rr,ff)){
if(!board[rr][ff]) moves.push({from:[r,f],to:[rr,ff]});
else { if(board[rr][ff].c!==p.c) moves.push({from:[r,f],to:[rr,ff],capture:true}); break; }
rr+=dr; ff+=df;
}
}
} else if(p.t==='k'){
for(let dr=-1;dr<=1;dr++) for(let df=-1;df<=1;df++){
if(dr===0 && df===0) continue;
const rr=r+dr, ff=f+df;
if(inBounds(rr,ff) && (!board[rr][ff] || board[rr][ff].c!==p.c)) moves.push({from:[r,f],to:[rr,ff]});
}
// castling omitted for simplicity
}
return moves;
}
function findKing(color, b){
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const p = (b||board)[r][f];
if(p && p.t==='k' && p.c===color) return [r,f];
}
return null;
}
function isSquareAttacked(r,f,byColor, b){
// naive: generate pseudo moves for all enemy pieces and see if any target r,f
for(let rr=0;rr<8;rr++) for(let ff=0;ff<8;ff++){
const p = (b||board)[rr][ff];
if(!p || p.c!==byColor) continue;
const moves = generatePseudoMovesFor.call(null, rr, ff);
for(const m of moves){
if(m.to[0]===r && m.to[1]===f) return true;
}
}
return false;
}
function generateLegalMoves(color){
const moves = [];
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const p = board[r][f];
if(!p || p.c!==color) continue;
const pseudo = generatePseudoMovesFor(r,f);
for(const m of pseudo){
// simulate
const bcopy = cloneBoard(board);
const piece = bcopy[m.from[0]][m.from[1]];
bcopy[m.to[0]][m.to[1]] = piece;
bcopy[m.from[0]][m.from[1]] = null;
// promotion auto to queen
if(m.promote) bcopy[m.to[0]][m.to[1]].t = 'q';
const kingPos = findKing(color, bcopy);
if(!kingPos) continue;
if(!isSquareAttacked(kingPos[0], kingPos[1], color==='w'?'b':'w', bcopy)){
moves.push(m);
}
}
}
return moves;
}
/* ---------- Make move with animation ---------- */
function makeMove(move, animate=true, record=true){
if(!move) return;
const [fr,ff] = move.from;
const [tr,tf] = move.to;
const piece = board[fr][ff];
if(!piece) return;
// record
if(record) history.push({move:move, board:cloneBoard(board), turn});
// handle promotion
if(move.promote) piece.t = 'q';
// animate: create floating element from source square to target square
if(animate){
animateMove(fr,ff,tr,tf, piece, ()=>{
board[tr][tf] = piece;
board[fr][ff] = null;
legalCache = null;
updateBoardUI();
afterMove();
});
} else {
board[tr][tf] = piece;
board[fr][ff] = null;
legalCache = null;
updateBoardUI();
afterMove();
}
}
function afterMove(){
turn = (turn==='w')?'b':'w';
updateStatus();
if(checkGameOver()) return;
// if AI to move (we let AI be black)
if(turn==='b'){
setTimeout(()=>{ aiMove(); }, 600);
}
}
/* ---------- Animation helper ---------- */
function animateMove(fr,ff,tr,tf,piece,cb){
// find source and target square elements
const fromEl = document.getElementById(`sq-${fr}-${ff}`);
const toEl = document.getElementById(`sq-${tr}-${tf}`);
if(!fromEl || !toEl){ cb(); return; }
const pieceEl = fromEl.querySelector('.piece');
if(!pieceEl){ cb(); return; }
const rectFrom = pieceEl.getBoundingClientRect();
const rectTo = toEl.getBoundingClientRect();
const clone = pieceEl.cloneNode(true);
clone.classList.add('floating');
clone.style.left = rectFrom.left + 'px';
clone.style.top = rectFrom.top + 'px';
clone.style.width = rectFrom.width + 'px';
clone.style.height = rectFrom.height + 'px';
document.body.appendChild(clone);
// hide original
pieceEl.style.visibility = 'hidden';
// compute transform
const dx = rectTo.left - rectFrom.left;
const dy = rectTo.top - rectFrom.top;
requestAnimationFrame(()=>{
clone.style.transform = `translate(${dx}px, ${dy}px) scale(1.02)`;
clone.style.transition = 'transform 420ms cubic-bezier(.2,.9,.2,1)';
});
setTimeout(()=>{
clone.remove();
pieceEl.style.visibility = '';
cb();
}, 460);
}
/* ---------- UI interactions ---------- */
function clearHighlights(){
document.querySelectorAll('.square').forEach(s=>s.classList.remove('highlight','hint'));
}
function highlightSquares(list, cls='highlight'){
clearHighlights();
for(const [r,f] of list){
const el = document.getElementById(`sq-${r}-${f}`);
if(el) el.classList.add(cls);
}
}
function onSquareClick(e){
const r = parseInt(this.dataset.r,10), f = parseInt(this.dataset.f,10);
if(selected){
// attempt move
const legal = generateLegalMoves(turn);
const found = legal.find(m => m.from[0]===selected[0] && m.from[1]===selected[1] && m.to[0]===r && m.to[1]===f);
if(found){
makeMove(found, true, true);
selected = null;
clearHighlights();
} else {
selected = null;
clearHighlights();
}
}
}
function onPieceClick(e){
e.stopPropagation();
const r = parseInt(this.dataset.r,10), f = parseInt(this.dataset.f,10);
if(board[r][f].c !== turn) return;
selected = [r,f];
const moves = generateLegalMoves(turn).filter(m => m.from[0]===r && m.from[1]===f).map(m=>m.to);
highlightSquares(moves);
}
function onPiecePointerDown(e){
// support drag: create floating clone and follow pointer, on pointerup drop to nearest square
e.preventDefault();
e.stopPropagation();
const pieceEl = this;
const startR = parseInt(pieceEl.dataset.r,10), startF = parseInt(pieceEl.dataset.f,10);
if(board[startR][startF].c !== turn) return;
const rect = pieceEl.getBoundingClientRect();
const clone = pieceEl.cloneNode(true);
clone.classList.add('floating');
clone.style.left = rect.left + 'px';
clone.style.top = rect.top + 'px';
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
document.body.appendChild(clone);
pieceEl.style.visibility = 'hidden';
function onMove(ev){
clone.style.left = (ev.clientX - rect.width/2) + 'px';
clone.style.top = (ev.clientY - rect.height/2) + 'px';
}
function onUp(ev){
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
// find target square
const elems = document.elementsFromPoint(ev.clientX, ev.clientY);
const sq = elems.find(el => el.classList && el.classList.contains('square'));
let tr = startR, tf = startF;
if(sq){
tr = parseInt(sq.dataset.r,10); tf = parseInt(sq.dataset.f,10);
}
// attempt move
const legal = generateLegalMoves(turn);
const found = legal.find(m => m.from[0]===startR && m.from[1]===startF && m.to[0]===tr && m.to[1]===tf);
clone.remove();
pieceEl.style.visibility = '';
if(found){
makeMove(found, true, true);
} else {
// snap back
}
}
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
}
/* ---------- Game status & logging ---------- */
function updateStatus(){
statusEl.textContent = (turn==='w'?'White':'Black') + " to move";
}
function log(msg){
const time = new Date().toLocaleTimeString();
const el = document.createElement('div');
el.innerHTML = `<strong>${time}</strong> — ${msg}`;
logEl.prepend(el);
}
/* ---------- Simple evaluation & AI ---------- */
const PIECE_VALUE = {p:100, n:320, b:330, r:500, q:900, k:20000};
function evaluate(b){
let score = 0;
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const p = b[r][f];
if(!p) continue;
const val = PIECE_VALUE[p.t] || 0;
score += (p.c==='w'? val : -val);
}
return score;
}
function minimax(bd, depth, maximizing, alpha=-Infinity, beta=Infinity){
// bd is board copy, maximizing true for white
const color = maximizing ? 'w' : 'b';
// generate legal moves on bd
function genLegalOn(bdLocal, col){
const moves = [];
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const p = bdLocal[r][f];
if(!p || p.c!==col) continue;
const pseudo = generatePseudoMovesForOn(bdLocal, r, f);
for(const m of pseudo){
// simulate
const bcopy = bdLocal.map(row=>row.map(cell=>cell?{t:cell.t,c:cell.c}:null));
const piece = bcopy[m.from[0]][m.from[1]];
bcopy[m.to[0]][m.to[1]] = piece;
bcopy[m.from[0]][m.from[1]] = null;
if(m.promote) bcopy[m.to[0]][m.to[1]].t = 'q';
const kingPos = findKingOn(bcopy, col);
if(!kingPos) continue;
if(!isSquareAttackedOn(bcopy, kingPos[0], kingPos[1], col==='w'?'b':'w')) moves.push(m);
}
}
return moves;
}
function generatePseudoMovesForOn(bdLocal, r, f){
const p = bdLocal[r][f];
if(!p) return [];
const moves = [];
const dir = p.c==='w'? -1 : 1;
if(p.t==='p'){
const r1 = r + dir;
if(inBounds(r1,f) && !bdLocal[r1][f]) moves.push({from:[r,f],to:[r1,f],promote:(r1===0||r1===7)});
const startRow = p.c==='w'?6:1;
const r2 = r + 2*dir;
if(r===startRow && !bdLocal[r1][f] && !bdLocal[r2][f]) moves.push({from:[r,f],to:[r2,f]});
for(const df of [-1,1]){
const rf = r + dir, ff = f + df;
if(inBounds(rf,ff) && bdLocal[rf][ff] && bdLocal[rf][ff].c !== p.c){
moves.push({from:[r,f],to:[rf,ff],capture:true,promote:(rf===0||rf===7)});
}
}
} else if(p.t==='n'){
const deltas = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
for(const [dr,df] of deltas){
const rr=r+dr, ff=f+df;
if(inBounds(rr,ff) && (!bdLocal[rr][ff] || bdLocal[rr][ff].c!==p.c)) moves.push({from:[r,f],to:[rr,ff]});
}
} else if(p.t==='b' || p.t==='r' || p.t==='q'){
const dirs = [];
if(p.t==='b' || p.t==='q') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
if(p.t==='r' || p.t==='q') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
for(const [dr,df] of dirs){
let rr=r+dr, ff=f+df;
while(inBounds(rr,ff)){
if(!bdLocal[rr][ff]) moves.push({from:[r,f],to:[rr,ff]});
else { if(bdLocal[rr][ff].c!==p.c) moves.push({from:[r,f],to:[rr,ff],capture:true}); break; }
rr+=dr; ff+=df;
}
}
} else if(p.t==='k'){
for(let dr=-1;dr<=1;dr++) for(let df=-1;df<=1;df++){
if(dr===0 && df===0) continue;
const rr=r+dr, ff=f+df;
if(inBounds(rr,ff) && (!bdLocal[rr][ff] || bdLocal[rr][ff].c!==p.c)) moves.push({from:[r,f],to:[rr,ff]});
}
}
return moves;
}
function findKingOn(bdLocal, color){
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const p = bdLocal[r][f];
if(p && p.t==='k' && p.c===color) return [r,f];
}
return null;
}
function isSquareAttackedOn(bdLocal, r,f, byColor){
for(let rr=0;rr<8;rr++) for(let ff=0;ff<8;ff++){
const p = bdLocal[rr][ff];
if(!p || p.c!==byColor) continue;
const moves = generatePseudoMovesForOn(bdLocal, rr, ff);
for(const m of moves) if(m.to[0]===r && m.to[1]===f) return true;
}
return false;
}
// terminal or depth 0
if(depth===0) return {score: evaluate(bd)};
const moves = genLegalOn(bd, color);
if(moves.length===0){
// checkmate or stalemate
const kingPos = findKingOn(bd, color);
if(kingPos && isSquareAttackedOn(bd, kingPos[0], kingPos[1], color==='w'?'b':'w')) {
return {score: maximizing ? -999999 : 999999};
} else return {score: 0};
}
let bestMove = null;
if(maximizing){
let maxEval = -Infinity;
for(const m of moves){
const bcopy = bd.map(row=>row.map(cell=>cell?{t:cell.t,c:cell.c}:null));
const piece = bcopy[m.from[0]][m.from[1]];
bcopy[m.to[0]][m.to[1]] = piece;
bcopy[m.from[0]][m.from[1]] = null;
if(m.promote) bcopy[m.to[0]][m.to[1]].t = 'q';
const res = minimax(bcopy, depth-1, false, alpha, beta);
if(res.score > maxEval){ maxEval = res.score; bestMove = m; }
alpha = Math.max(alpha, res.score);
if(beta <= alpha) break;
}
return {score: maxEval, move: bestMove};
} else {
let minEval = Infinity;
for(const m of moves){
const bcopy = bd.map(row=>row.map(cell=>cell?{t:cell.t,c:cell.c}:null));
const piece = bcopy[m.from[0]][m.from[1]];
bcopy[m.to[0]][m.to[1]] = piece;
bcopy[m.from[0]][m.from[1]] = null;
if(m.promote) bcopy[m.to[0]][m.to[1]].t = 'q';
const res = minimax(bcopy, depth-1, true, alpha, beta);
if(res.score < minEval){ minEval = res.score; bestMove = m; }
beta = Math.min(beta, res.score);
if(beta <= alpha) break;
}
return {score: minEval, move: bestMove};
}
}
function aiMove(){
// simple: use minimax depth 2 (black minimizing)
const bd = cloneBoard(board);
const res = minimax(bd, 2, false);
if(res && res.move){
makeMove(res.move, true, true);
log("AI played a move. Tip: watch for captures and checks.");
} else {
// fallback random
const legal = generateLegalMoves('b');
if(legal.length) {
const m = legal[Math.floor(Math.random()*legal.length)];
makeMove(m, true, true);
}
}
}
/* ---------- Hint & coach ---------- */
hintBtn.addEventListener('click', ()=>{
const legal = generateLegalMoves(turn);
if(legal.length===0) { log("No legal moves available."); return; }
// evaluate each move quickly and pick best for current side
let best = null; let bestScore = (turn==='w')? -Infinity : Infinity;
for(const m of legal){
const bcopy = cloneBoard(board);
const piece = bcopy[m.from[0]][m.from[1]];
bcopy[m.to[0]][m.to[1]] = piece;
bcopy[m.from[0]][m.from[1]] = null;
if(m.promote) bcopy[m.to[0]][m.to[1]].t = 'q';
const sc = evaluate(bcopy);
if(turn==='w' ? sc>bestScore : sc<bestScore){ bestScore = sc; best = m; }
}
if(best){
highlightSquares([best.to], 'hint');
log(`Hint: consider ${coordToAlgebraic(best.from[0],best.from[1])}→${coordToAlgebraic(best.to[0],best.to[1])}`);
// animate hint ghost
setTimeout(()=>{ clearHighlights(); }, 2200);
}
});
/* ---------- Game over detection ---------- */
function checkGameOver(){
const legal = generateLegalMoves(turn);
if(legal.length===0){
const kingPos = findKing(turn);
if(kingPos && isSquareAttacked(kingPos[0], kingPos[1], turn==='w'?'b':'w')){
statusEl.textContent = (turn==='w'?'White':'Black') + " is checkmated. Game over.";
log("Checkmate! Game over.");
} else {
statusEl.textContent = "Stalemate. Game over.";
log("Stalemate.");
}
return true;
}
// check for check
const kingPos = findKing(turn);
if(kingPos && isSquareAttacked(kingPos[0], kingPos[1], turn==='w'?'b':'w')){
statusEl.textContent = (turn==='w'?'White':'Black') + " to move — in check!";
log("Your king is in check. Look for blocking, capturing, or moving the king.");
}
return false;
}
/* ---------- Undo ---------- */
undoBtn.addEventListener('click', ()=>{
if(history.length===0) return;
const last = history.pop();
board = cloneBoard(last.board);
turn = last.turn;
updateBoardUI(true);
updateStatus();
log("Undid last move.");
});
/* ---------- New game ---------- */
newBtn.addEventListener('click', ()=>{ initBoard(); });
/* ---------- Flip board ---------- */
flipBtn.addEventListener('click', ()=>{
flipped = !flipped;
boardEl.style.transform = flipped ? 'rotate(180deg)' : '';
// rotate pieces back to upright
document.querySelectorAll('.piece').forEach(p=> p.style.transform = flipped ? 'rotate(180deg)' : '');
});
/* ---------- Lesson Mode ---------- */
lessonBtn.addEventListener('click', ()=>{
lessonMode = !lessonMode;
lessonBtn.textContent = lessonMode ? 'Exit Lesson' : 'Lesson Mode';
if(lessonMode){
lessonStep = 0;
startLesson();
} else {
lessonText.textContent = 'Lesson mode exited.';
}
});
nextLessonBtn.addEventListener('click', ()=>{ if(lessonMode) nextLesson(); });
resetLessonBtn.addEventListener('click', ()=>{ lessonStep=0; startLesson(); });
function startLesson(){
// simple sequence of 3 mini-lessons
lessonStep = 0;
lessonText.textContent = 'Lesson 1: Opening principles — develop knights before bishops, control center.';
// set up a simple position for lesson 1
initBoard();
// make a couple of moves to illustrate
setTimeout(()=>{ // animate a sample move
const m = generateLegalMoves('w').find(m=>board[m.from[0]][m.from[1]].t==='n');
if(m) makeMove(m,true,true);
log("Lesson: develop knights early. Try moving Nf3 or Nc3 in your games.");
}, 600);
}
function nextLesson(){
lessonStep++;
if(lessonStep===1){
// forks example: set up a simple fork position
lessonText.textContent = 'Lesson 2: Fork — a knight attacking two valuable pieces at once.';
// set up a contrived position
board = Array.from({length:8},()=>Array(8).fill(null));
// white knight on e5, black queen on d7, black rook on f7
board[3][4] = {t:'n', c:'w'}; // e5
board[1][3] = {t:'q', c:'b'}; // d7
board[1][5] = {t:'r', c:'b'}; // f7
board[7][4] = {t:'k', c:'w'};
board[0][4] = {t:'k', c:'b'};
updateBoardUI(true);
log("Fork example: White can play Nf7+ attacking king and rook — a fork.");
// animate suggested fork
setTimeout(()=>{ const m = {from:[3,4], to:[1,5]}; makeMove(m,true,false); }, 900);
} else if(lessonStep===2){
lessonText.textContent = 'Lesson 3: Basic mate pattern — two-rook mate on the back rank.';
// set up simple mate pattern
board = Array.from({length:8},()=>Array(8).fill(null));
board[0][4] = {t:'k', c:'b'};
board[1][4] = {t:'p', c:'b'};
board[7][4] = {t:'k', c:'w'};
board[6][0] = {t:'r', c:'w'};
board[6][7] = {t:'r', c:'w'};
updateBoardUI(true);
log("Two-rook mate: bring rooks to the 7th rank and then to the back rank to deliver mate.");
// animate a rook move
setTimeout(()=>{ makeMove({from:[6,0],to:[1,0]}, true, false); }, 700);
} else {
lessonText.textContent = 'Lessons complete. Toggle Lesson Mode to exit.';
lessonMode = false;
lessonBtn.textContent = 'Lesson Mode';
}
}
/* ---------- Init ---------- */
initBoard();
log("Welcome to Chess Tutor. Use Hint for suggestions and Lesson Mode for guided examples.");
</script>
</body>
</html>
Game Source: Chess Tutor — Animated Learning Game
Creator: SonicGalaxy24
Libraries: none
Complexity: complex (852 lines, 31.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: chess-tutor-animated-learning-game-sonicgalaxy24" to link back to the original. Then publish at arcadelab.ai/publish.