🎮ArcadeLab

Chess Tutor — Animated Learning Game

by SonicGalaxy24
852 lines31.5 KB
▶ Play
<!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.