{"id":97272,"date":"2026-08-29T08:36:31","date_gmt":"2026-08-29T00:36:31","guid":{"rendered":"https:\/\/www.wsisp.com\/helps\/97272.html"},"modified":"2026-08-29T08:36:31","modified_gmt":"2026-08-29T00:36:31","slug":"%e4%b8%ad%e5%9b%bd%e8%b1%a1%e6%a3%8b%e7%bd%91%e9%a1%b5%e7%89%88","status":"publish","type":"post","link":"https:\/\/www.wsisp.com\/helps\/97272.html","title":{"rendered":"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248"},"content":{"rendered":"<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" alt=\"\" height=\"1182\" src=\"https:\/\/www.wsisp.com\/helps\/wp-content\/uploads\/2026\/08\/20260829003629-6a92298d6af15.png\" width=\"995\" \/><\/p>\n<p>1\u7248&#xff08;\u68cb\u5b50\u62d6\u5c3e\u8fc7\u4e8e\u5355\u8c03&#xff09;&#xff1a;<\/p>\n<p>&lt;!DOCTYPE html&gt;<br \/>\n&lt;html lang&#061;&#034;zh-CN&#034;&gt;<br \/>\n&lt;head&gt;<br \/>\n&lt;meta charset&#061;&#034;UTF-8&#034;&gt;<br \/>\n&lt;title&gt;\u4e2d\u56fd\u8c61\u68cb&lt;\/title&gt;<br \/>\n&lt;style&gt;<br \/>\n  * { box-sizing: border-box; margin: 0; padding: 0; }<br \/>\n  body {<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    background: #e8e4dc;<br \/>\n    background-image:<br \/>\n      radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);<br \/>\n    min-height: 100vh;<br \/>\n    display: flex;<br \/>\n    flex-direction: column;<br \/>\n    align-items: center;<br \/>\n    padding: 30px 20px;<br \/>\n    color: #2c2c2c;<br \/>\n  }<br \/>\n  h1 {<br \/>\n    font-size: 38px;<br \/>\n    margin-bottom: 15px;<br \/>\n    letter-spacing: 12px;<br \/>\n    color: #1a1a1a;<br \/>\n    font-weight: normal;<br \/>\n    text-shadow: 1px 1px 0 rgba(0,0,0,0.08);<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  h1::after {<br \/>\n    content: &#039;&#039;;<br \/>\n    display: block;<br \/>\n    width: 60%;<br \/>\n    height: 2px;<br \/>\n    margin: 8px auto 0;<br \/>\n    background: linear-gradient(90deg, transparent, #555, transparent);<br \/>\n  }<br \/>\n  .info {<br \/>\n    display: flex;<br \/>\n    gap: 30px;<br \/>\n    margin-bottom: 20px;<br \/>\n    align-items: center;<br \/>\n    font-size: 18px;<br \/>\n  }<br \/>\n  .turn-indicator {<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    font-weight: bold;<br \/>\n    transition: all 0.3s;<br \/>\n    border: 1px solid #333;<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    letter-spacing: 2px;<br \/>\n  }<br \/>\n  .turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }<br \/>\n  .turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }<br \/>\n  button {<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    border: 1px solid #333;<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    cursor: pointer;<br \/>\n    font-size: 16px;<br \/>\n    font-family: inherit;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 2px;<br \/>\n    transition: all 0.3s;<br \/>\n  }<br \/>\n  button:hover {<br \/>\n    background: #2c2c2c;<br \/>\n    color: #f5f1ea;<br \/>\n    transform: translateY(-2px);<br \/>\n  }<br \/>\n  .canvas-wrap {<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  canvas {<br \/>\n    background: #f5f1ea;<br \/>\n    border: 1px solid #333;<br \/>\n    border-radius: 2px;<br \/>\n    box-shadow:<br \/>\n      0 0 0 8px #f5f1ea,<br \/>\n      0 0 0 9px #333,<br \/>\n      0 15px 40px rgba(0,0,0,0.3);<br \/>\n    cursor: pointer;<br \/>\n  }<br \/>\n  .status {<br \/>\n    margin-top: 18px;<br \/>\n    font-size: 20px;<br \/>\n    min-height: 28px;<br \/>\n    color: #c0392b;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 3px;<br \/>\n  }<br \/>\n  .captured {<br \/>\n    margin-top: 12px;<br \/>\n    display: flex;<br \/>\n    gap: 40px;<br \/>\n    font-size: 16px;<br \/>\n    color: #555;<br \/>\n  }<br \/>\n  .captured div { min-height: 30px; }<br \/>\n  .captured span {<br \/>\n    display: inline-block;<br \/>\n    margin: 0 3px;<br \/>\n    font-size: 22px;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, serif;<br \/>\n  }<br \/>\n  .fx-text {<br \/>\n    position: absolute;<br \/>\n    top: 50%;<br \/>\n    left: 50%;<br \/>\n    transform: translate(-50%, -50%);<br \/>\n    pointer-events: none;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    font-weight: bold;<br \/>\n    opacity: 0;<br \/>\n    z-index: 10;<br \/>\n  }<br \/>\n  .fx-check {<br \/>\n    font-size: 72px;<br \/>\n    color: #c0392b;<br \/>\n    letter-spacing: 20px;<br \/>\n    text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);<br \/>\n  }<br \/>\n  .fx-mate {<br \/>\n    font-size: 96px;<br \/>\n    color: #8b0000;<br \/>\n    letter-spacing: 30px;<br \/>\n    text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);<br \/>\n  }<br \/>\n  &#064;keyframes fxFlash {<br \/>\n    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }<br \/>\n    30%  { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }<br \/>\n    70%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }<br \/>\n    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }<br \/>\n  }<br \/>\n  &#064;keyframes fxShake {<br \/>\n    0%, 100% { transform: translate(0, 0); }<br \/>\n    10%  { transform: translate(-6px, -4px); }<br \/>\n    20%  { transform: translate(5px, 3px); }<br \/>\n    30%  { transform: translate(-4px, 5px); }<br \/>\n    40%  { transform: translate(6px, -3px); }<br \/>\n    50%  { transform: translate(-5px, 4px); }<br \/>\n    60%  { transform: translate(3px, -5px); }<br \/>\n    70%  { transform: translate(-3px, 3px); }<br \/>\n    80%  { transform: translate(4px, -2px); }<br \/>\n    90%  { transform: translate(-2px, 2px); }<br \/>\n  }<br \/>\n  .shake { animation: fxShake 0.5s ease-in-out; }<br \/>\n&lt;\/style&gt;<br \/>\n&lt;\/head&gt;<br \/>\n&lt;body&gt;<br \/>\n  &lt;h1&gt;\u4e2d \u570b \u8c61 \u68cb&lt;\/h1&gt;<br \/>\n  &lt;div class&#061;&#034;info&#034;&gt;<br \/>\n    &lt;div class&#061;&#034;turn-indicator&#034; id&#061;&#034;turnIndicator&#034;&gt;\u7ea2\u65b9\u8d70\u68cb&lt;\/div&gt;<br \/>\n    &lt;button id&#061;&#034;restartBtn&#034;&gt;\u91cd \u65b0 \u5f00 \u59cb&lt;\/button&gt;<br \/>\n    &lt;button id&#061;&#034;undoBtn&#034;&gt;\u6094 \u68cb&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;canvas-wrap&#034; id&#061;&#034;canvasWrap&#034;&gt;<br \/>\n    &lt;canvas id&#061;&#034;board&#034; width&#061;&#034;540&#034; height&#061;&#034;600&#034;&gt;&lt;\/canvas&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-check&#034; id&#061;&#034;fxCheck&#034;&gt;\u5c06 \u8ecd&lt;\/div&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-mate&#034; id&#061;&#034;fxMate&#034;&gt;\u7edd \u6bba&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;status&#034; id&#061;&#034;status&#034;&gt;&lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;captured&#034;&gt;<br \/>\n    &lt;div&gt;\u7ea2\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByRed&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n    &lt;div&gt;\u9ed1\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByBlack&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>&lt;script&gt;<br \/>\nconst CELL &#061; 60;<br \/>\nconst MARGIN_X &#061; 30;<br \/>\nconst MARGIN_Y &#061; 30;<br \/>\nconst COLS &#061; 9;<br \/>\nconst ROWS &#061; 10;<\/p>\n<p>const canvas &#061; document.getElementById(&#039;board&#039;);<br \/>\nconst ctx &#061; canvas.getContext(&#039;2d&#039;);<br \/>\nconst canvasWrap &#061; document.getElementById(&#039;canvasWrap&#039;);<\/p>\n<p>const RED &#061; &#039;red&#039;;<br \/>\nconst BLACK &#061; &#039;black&#039;;<\/p>\n<p>const PIECES &#061; {<br \/>\n  red: {<br \/>\n    king: &#039;\u5e25&#039;, advisor: &#039;\u4ed5&#039;, elephant: &#039;\u76f8&#039;, horse: &#039;\u508c&#039;,<br \/>\n    chariot: &#039;\u4fe5&#039;, cannon: &#039;\u70ae&#039;, pawn: &#039;\u5175&#039;<br \/>\n  },<br \/>\n  black: {<br \/>\n    king: &#039;\u5c07&#039;, advisor: &#039;\u58eb&#039;, elephant: &#039;\u8c61&#039;, horse: &#039;\u99ac&#039;,<br \/>\n    chariot: &#039;\u8eca&#039;, cannon: &#039;\u7832&#039;, pawn: &#039;\u5352&#039;<br \/>\n  }<br \/>\n};<\/p>\n<p>let board &#061; [];<br \/>\nlet currentPlayer &#061; RED;<br \/>\nlet selected &#061; null;<br \/>\nlet legalMoves &#061; [];<br \/>\nlet gameOver &#061; false;<br \/>\nlet history &#061; [];<br \/>\nlet capturedByRed &#061; [];<br \/>\nlet capturedByBlack &#061; [];<\/p>\n<p>let effects &#061; [];<br \/>\nlet animating &#061; false;<br \/>\nlet checkAnimStart &#061; 0;<br \/>\nlet checkKingPos &#061; null;<br \/>\nlet checkmateAnimStart &#061; 0;<\/p>\n<p>function P(type, color) {<br \/>\n  return { type, color };<br \/>\n}<\/p>\n<p>function initBoard() {<br \/>\n  board &#061; [<br \/>\n    [P(&#039;chariot&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;king&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;chariot&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,P(&#039;cannon&#039;,BLACK),null,null,null,null,null,P(&#039;cannon&#039;,BLACK),null],<br \/>\n    [P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED)],<br \/>\n    [null,P(&#039;cannon&#039;,RED),null,null,null,null,null,P(&#039;cannon&#039;,RED),null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;chariot&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;king&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;chariot&#039;,RED)]<br \/>\n  ];<br \/>\n  currentPlayer &#061; RED;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  gameOver &#061; false;<br \/>\n  history &#061; [];<br \/>\n  capturedByRed &#061; [];<br \/>\n  capturedByBlack &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  checkmateAnimStart &#061; 0;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function isInBoard(r, c) {<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt; ROWS &amp;&amp; c &gt;&#061; 0 &amp;&amp; c &lt; COLS;<br \/>\n}<\/p>\n<p>function inPalace(r, c, color) {<br \/>\n  if (c &lt; 3 || c &gt; 5) return false;<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 7 &amp;&amp; r &lt;&#061; 9;<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt;&#061; 2;<br \/>\n}<\/p>\n<p>function onOwnSide(r, color) {<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 5;<br \/>\n  return r &lt;&#061; 4;<br \/>\n}<\/p>\n<p>function getLegalMoves(r, c) {<br \/>\n  const piece &#061; board[r][c];<br \/>\n  if (!piece) return [];<br \/>\n  const color &#061; piece.color;<br \/>\n  const type &#061; piece.type;<br \/>\n  const moves &#061; [];<br \/>\n  const addMove &#061; (nr, nc) &#061;&gt; {<br \/>\n    if (!isInBoard(nr, nc)) return;<br \/>\n    const target &#061; board[nr][nc];<br \/>\n    if (target &amp;&amp; target.color &#061;&#061;&#061; color) return;<br \/>\n    moves.push({ r: nr, c: nc, capture: !!target });<br \/>\n  };<\/p>\n<p>  switch (type) {<br \/>\n    case &#039;king&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;advisor&#039;: {<br \/>\n      const dirs &#061; [[-1,-1],[-1,1],[1,-1],[1,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;elephant&#039;: {<br \/>\n      const dirs &#061; [[-2,-2],[-2,2],[2,-2],[2,2]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        const mr &#061; r &#043; dr\/2, mc &#061; c &#043; dc\/2;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (!onOwnSide(nr, color)) continue;<br \/>\n        if (board[mr][mc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;horse&#039;: {<br \/>\n      const hm &#061; [<br \/>\n        {dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},<br \/>\n        {dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},<br \/>\n        {dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},<br \/>\n        {dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}<br \/>\n      ];<br \/>\n      for (const m of hm) {<br \/>\n        const nr &#061; r &#043; m.dr, nc &#061; c &#043; m.dc;<br \/>\n        const lr &#061; r &#043; m.br, lc &#061; c &#043; m.bc;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (board[lr][lc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;chariot&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!target) {<br \/>\n            moves.push({ r: nr, c: nc, capture: false });<br \/>\n          } else {<br \/>\n            if (target.color !&#061;&#061; color) {<br \/>\n              moves.push({ r: nr, c: nc, capture: true });<br \/>\n            }<br \/>\n            break;<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;cannon&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        let jumped &#061; false;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!jumped) {<br \/>\n            if (!target) {<br \/>\n              moves.push({ r: nr, c: nc, capture: false });<br \/>\n            } else {<br \/>\n              jumped &#061; true;<br \/>\n            }<br \/>\n          } else {<br \/>\n            if (target) {<br \/>\n              if (target.color !&#061;&#061; color) {<br \/>\n                moves.push({ r: nr, c: nc, capture: true });<br \/>\n              }<br \/>\n              break;<br \/>\n            }<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;pawn&#039;: {<br \/>\n      const forward &#061; color &#061;&#061;&#061; RED ? -1 : 1;<br \/>\n      const nr &#061; r &#043; forward;<br \/>\n      if (isInBoard(nr, c)) addMove(nr, c);<br \/>\n      if (!onOwnSide(r, color)) {<br \/>\n        if (isInBoard(r, c &#8211; 1)) addMove(r, c &#8211; 1);<br \/>\n        if (isInBoard(r, c &#043; 1)) addMove(r, c &#043; 1);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n  }<br \/>\n  return moves;<br \/>\n}<\/p>\n<p>function findKing(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].type &#061;&#061;&#061; &#039;king&#039; &amp;&amp; board[r][c].color &#061;&#061;&#061; color) return { r, c };<br \/>\n    }<br \/>\n  }<br \/>\n  return null;<br \/>\n}<\/p>\n<p>function kingsFacing() {<br \/>\n  const rk &#061; findKing(RED);<br \/>\n  const bk &#061; findKing(BLACK);<br \/>\n  if (!rk || !bk || rk.c !&#061;&#061; bk.c) return false;<br \/>\n  const col &#061; rk.c;<br \/>\n  const minR &#061; Math.min(rk.r, bk.r);<br \/>\n  const maxR &#061; Math.max(rk.r, bk.r);<br \/>\n  for (let r &#061; minR &#043; 1; r &lt; maxR; r&#043;&#043;) {<br \/>\n    if (board[r][col]) return false;<br \/>\n  }<br \/>\n  return true;<br \/>\n}<\/p>\n<p>function getLegalMovesFiltered(r, c) {<br \/>\n  const moves &#061; getLegalMoves(r, c);<br \/>\n  return moves.filter(m &#061;&gt; {<br \/>\n    const captured &#061; board[m.r][m.c];<br \/>\n    const moving &#061; board[r][c];<br \/>\n    board[m.r][m.c] &#061; moving;<br \/>\n    board[r][c] &#061; null;<br \/>\n    const inCheck &#061; isKingInCheck(moving.color) || kingsFacing();<br \/>\n    board[r][c] &#061; moving;<br \/>\n    board[m.r][m.c] &#061; captured;<br \/>\n    return !inCheck;<br \/>\n  });<br \/>\n}<\/p>\n<p>function isKingInCheck(color) {<br \/>\n  const king &#061; findKing(color);<br \/>\n  if (!king) return true;<br \/>\n  const oppColor &#061; color &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; oppColor) {<br \/>\n        const moves &#061; getLegalMoves(r, c);<br \/>\n        if (moves.some(m &#061;&gt; m.r &#061;&#061;&#061; king.r &amp;&amp; m.c &#061;&#061;&#061; king.c)) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function hasAnyLegalMove(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; color) {<br \/>\n        if (getLegalMovesFiltered(r, c).length &gt; 0) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function drawBoard() {<br \/>\n  const grad &#061; ctx.createLinearGradient(0, 0, canvas.width, canvas.height);<br \/>\n  grad.addColorStop(0, &#039;#f8f4ec&#039;);<br \/>\n  grad.addColorStop(0.5, &#039;#f0ebe0&#039;);<br \/>\n  grad.addColorStop(1, &#039;#ede6d8&#039;);<br \/>\n  ctx.fillStyle &#061; grad;<br \/>\n  ctx.fillRect(0, 0, canvas.width, canvas.height);<\/p>\n<p>  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.5;<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(MARGIN_X, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n    if (c &#061;&#061;&#061; 0 || c &#061;&#061;&#061; COLS &#8211; 1) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; (ROWS &#8211; 1) * CELL);<br \/>\n      ctx.stroke();<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n      ctx.stroke();<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n      ctx.stroke();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#039;bold 26px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#039;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(&#039;\u695a \u6cb3&#039;, MARGIN_X &#043; 1.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<br \/>\n  ctx.fillText(&#039;\u6f22 \u754c&#039;, MARGIN_X &#043; 6.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<\/p>\n<p>  drawStarPoints();<br \/>\n}<\/p>\n<p>function drawStarPoints() {<br \/>\n  const points &#061; [<br \/>\n    [2,1],[2,7],[7,1],[7,7],<br \/>\n    [3,0],[3,2],[3,4],[3,6],[3,8],<br \/>\n    [6,0],[6,2],[6,4],[6,6],[6,8]<br \/>\n  ];<br \/>\n  for (const [r, c] of points) {<br \/>\n    drawStar(r, c);<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawStar(r, c) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  const s &#061; 6;<br \/>\n  const g &#061; 5;<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.2;<br \/>\n  const dirs &#061; [];<br \/>\n  if (c &gt; 0) dirs.push([-1,-1],[-1,1]);<br \/>\n  if (c &lt; COLS &#8211; 1) dirs.push([1,-1],[1,1]);<br \/>\n  for (const [dx, dy] of dirs) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * (g &#043; s), y &#043; dy * g);<br \/>\n    ctx.stroke();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * g, y &#043; dy * (g &#043; s));<br \/>\n    ctx.stroke();<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawPiece(r, c, type, color, highlight) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  const radius &#061; 26;<\/p>\n<p>  if (highlight) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius &#043; 5, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#039;rgba(192, 57, 43, 0.25)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const inkGrad &#061; ctx.createRadialGradient(x &#8211; 3, y &#8211; 3, 2, x, y, radius);<br \/>\n  if (color &#061;&#061;&#061; RED) {<br \/>\n    inkGrad.addColorStop(0, &#039;#e8e0d4&#039;);<br \/>\n    inkGrad.addColorStop(0.6, &#039;#d4c8b4&#039;);<br \/>\n    inkGrad.addColorStop(1, &#039;#b8a88c&#039;);<br \/>\n  } else {<br \/>\n    inkGrad.addColorStop(0, &#039;#e8e0d4&#039;);<br \/>\n    inkGrad.addColorStop(0.6, &#039;#d0c4b0&#039;);<br \/>\n    inkGrad.addColorStop(1, &#039;#b0a288&#039;);<br \/>\n  }<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 2;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius &#8211; 4, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#039;bold 28px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#039;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(PIECES[color][type], x, y &#043; 1);<br \/>\n}<\/p>\n<p>function draw() {<br \/>\n  drawBoard();<\/p>\n<p>  for (const move of legalMoves) {<br \/>\n    const x &#061; MARGIN_X &#043; move.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; move.r * CELL;<br \/>\n    if (move.capture) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 30, 0, Math.PI * 2);<br \/>\n      ctx.strokeStyle &#061; &#039;rgba(192, 57, 43, 0.7)&#039;;<br \/>\n      ctx.lineWidth &#061; 2.5;<br \/>\n      ctx.setLineDash([4, 3]);<br \/>\n      ctx.stroke();<br \/>\n      ctx.setLineDash([]);<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 6, 0, Math.PI * 2);<br \/>\n      ctx.fillStyle &#061; &#039;rgba(26, 26, 26, 0.5)&#039;;<br \/>\n      ctx.fill();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c]) {<br \/>\n        const p &#061; board[r][c];<br \/>\n        drawPiece(r, c, p.type, p.color, selected &amp;&amp; selected.r &#061;&#061;&#061; r &amp;&amp; selected.c &#061;&#061;&#061; c);<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const now &#061; performance.now();<\/p>\n<p>  if (redKing &amp;&amp; isKingInCheck(RED)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; RED || checkKingPos.r !&#061;&#061; redKing.r || checkKingPos.c !&#061;&#061; redKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: redKing.r, c: redKing.c, color: RED };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; redKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; redKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; RED) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<\/p>\n<p>  if (blackKing &amp;&amp; isKingInCheck(BLACK)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; BLACK || checkKingPos.r !&#061;&#061; blackKing.r || checkKingPos.c !&#061;&#061; blackKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: blackKing.r, c: blackKing.c, color: BLACK };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; blackKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; blackKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; BLACK) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawEffects() {<br \/>\n  const now &#061; performance.now();<br \/>\n  const remaining &#061; [];<\/p>\n<p>  for (const e of effects) {<br \/>\n    const elapsed &#061; now &#8211; e.startTime;<br \/>\n    const progress &#061; elapsed \/ e.duration;<br \/>\n    if (progress &gt;&#061; 1) continue;<\/p>\n<p>    if (e.type &#061;&#061;&#061; &#039;capture&#039;) {<br \/>\n      drawCaptureEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;checkmate&#039;) {<br \/>\n      drawCheckmateEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;inkTrail&#039;) {<br \/>\n      drawInkTrailEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  effects &#061; remaining;<br \/>\n}<\/p>\n<p>function drawCaptureEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<br \/>\n  const inkColor2 &#061; &#039;rgba(40, 40, 40,&#039;;<\/p>\n<p>  for (let i &#061; 0; i &lt; 3; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.08;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 10 &#043; pp * 55;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.8;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; inkColor2 &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.lineWidth &#061; 3 &#8211; pp * 2.5;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 14; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 14) * Math.PI * 2 &#043; p * 0.8;<br \/>\n    const dist &#061; p * 55;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 5 &#043; 1;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor2 &#043; ((1 &#8211; p) * 0.7).toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const coreGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 &#8211; p * 0.6));<br \/>\n  coreGrad.addColorStop(0, inkColor2 &#043; (0.5 * (1 &#8211; p)).toFixed(2) &#043; &#039;)&#039;);<br \/>\n  coreGrad.addColorStop(1, inkColor2 &#043; &#039;0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 30 * (1 &#8211; p * 0.6), 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; coreGrad;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function drawCheckmateEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<\/p>\n<p>  ctx.save();<br \/>\n  ctx.globalAlpha &#061; Math.min(1, p * 3);<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.06;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 15 &#043; pp * 200;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.6;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${alpha.toFixed(2)})&#096;;<br \/>\n    ctx.lineWidth &#061; 4 &#8211; pp * 3;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 5) * Math.PI * 2 &#8211; Math.PI \/ 2;<br \/>\n    const dist &#061; Math.min(300, p * 600);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x, y);<br \/>\n    ctx.lineTo(x &#043; Math.cos(angle) * dist, y &#043; Math.sin(angle) * dist);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${(1 &#8211; p) * 0.5})&#096;;<br \/>\n    ctx.lineWidth &#061; 2;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 30; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 30) * Math.PI * 2 &#043; p * 1.5;<br \/>\n    const dist &#061; p * 80 &#043; Math.sin(i * 3 &#043; p * 10) * 15;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 8 &#043; 2;<br \/>\n    const gray &#061; 30 &#043; (i % 3) * 25;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#096;rgba(${gray}, ${gray}, ${gray}, ${(1 &#8211; p) * 0.8})&#096;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const flashGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 &#8211; p));<br \/>\n  flashGrad.addColorStop(0, &#096;rgba(255, 255, 255, ${Math.max(0, (0.6 &#8211; p * 0.6))})&#096;);<br \/>\n  flashGrad.addColorStop(0.5, &#039;rgba(255, 255, 255, 0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 250, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; flashGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.restore();<br \/>\n}<\/p>\n<p>function drawInkTrailEffect(e, p) {<br \/>\n  const x &#061; e.sx &#043; (e.dx &#8211; e.sx) * p;<br \/>\n  const y &#061; e.sy &#043; (e.dy &#8211; e.sy) * p;<br \/>\n  const inkColor &#061; &#039;rgba(40, 40, 40,&#039;;<\/p>\n<p>  for (let i &#061; 0; i &lt; 6; i&#043;&#043;) {<br \/>\n    const pp &#061; Math.max(0, p &#8211; i * 0.04);<br \/>\n    const tx &#061; e.sx &#043; (e.dx &#8211; e.sx) * pp;<br \/>\n    const ty &#061; e.sy &#043; (e.dy &#8211; e.sy) * pp;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.4;<br \/>\n    const size &#061; (1 &#8211; pp) * 18 &#043; 3;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(tx, ty, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 28, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkColor &#043; &#039;0.6)&#039;;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function animate() {<br \/>\n  draw();<br \/>\n  drawEffects();<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const redChecked &#061; redKing &amp;&amp; isKingInCheck(RED);<br \/>\n  const blackChecked &#061; blackKing &amp;&amp; isKingInCheck(BLACK);<\/p>\n<p>  if (effects.length &gt; 0 || redChecked || blackChecked) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  } else {<br \/>\n    animating &#061; false;<br \/>\n  }<br \/>\n}<\/p>\n<p>function triggerAnimation() {<br \/>\n  if (!animating) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  }<br \/>\n}<\/p>\n<p>function getXY(r, c) {<br \/>\n  return { x: MARGIN_X &#043; c * CELL, y: MARGIN_Y &#043; r * CELL };<br \/>\n}<\/p>\n<p>function addCaptureEffect(r, c, color) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;capture&#039;,<br \/>\n    x, y,<br \/>\n    color,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 800<br \/>\n  });<br \/>\n}<\/p>\n<p>function addInkTrailEffect(sr, sc, dr, dc, color) {<br \/>\n  const { x: sx, y: sy } &#061; getXY(sr, sc);<br \/>\n  const { x: dx, y: dy } &#061; getXY(dr, dc);<br \/>\n  effects.push({<br \/>\n    type: &#039;inkTrail&#039;,<br \/>\n    sx, sy, dx, dy,<br \/>\n    color,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 400<br \/>\n  });<br \/>\n}<\/p>\n<p>function addCheckmateEffect(r, c) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;checkmate&#039;,<br \/>\n    x, y,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 2000<br \/>\n  });<br \/>\n}<\/p>\n<p>function showFx(id, duration) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.offsetHeight;<br \/>\n  el.style.animation &#061; &#096;fxFlash ${duration}ms ease-out forwards&#096;;<br \/>\n}<\/p>\n<p>function hideFx(id) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.style.opacity &#061; &#039;0&#039;;<br \/>\n}<\/p>\n<p>function triggerShake() {<br \/>\n  canvasWrap.classList.remove(&#039;shake&#039;);<br \/>\n  void canvasWrap.offsetWidth;<br \/>\n  canvasWrap.classList.add(&#039;shake&#039;);<br \/>\n}<\/p>\n<p>canvas.addEventListener(&#039;click&#039;, (e) &#061;&gt; {<br \/>\n  if (gameOver) return;<br \/>\n  const rect &#061; canvas.getBoundingClientRect();<br \/>\n  const x &#061; (e.clientX &#8211; rect.left) * (canvas.width \/ rect.width);<br \/>\n  const y &#061; (e.clientY &#8211; rect.top) * (canvas.height \/ rect.height);<br \/>\n  const c &#061; Math.round((x &#8211; MARGIN_X) \/ CELL);<br \/>\n  const r &#061; Math.round((y &#8211; MARGIN_Y) \/ CELL);<br \/>\n  if (!isInBoard(r, c)) return;<\/p>\n<p>  const piece &#061; board[r][c];<\/p>\n<p>  if (selected) {<br \/>\n    const move &#061; legalMoves.find(m &#061;&gt; m.r &#061;&#061;&#061; r &amp;&amp; m.c &#061;&#061;&#061; c);<br \/>\n    if (move) {<br \/>\n      makeMove(selected.r, selected.c, r, c);<br \/>\n      selected &#061; null;<br \/>\n      legalMoves &#061; [];<br \/>\n      checkGameEnd();<br \/>\n      return;<br \/>\n    }<br \/>\n    if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n      selected &#061; { r, c };<br \/>\n      legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n      draw();<br \/>\n      return;<br \/>\n    }<br \/>\n    selected &#061; null;<br \/>\n    legalMoves &#061; [];<br \/>\n    draw();<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n    selected &#061; { r, c };<br \/>\n    legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n    draw();<br \/>\n  }<br \/>\n});<\/p>\n<p>function makeMove(fr, fc, tr, tc) {<br \/>\n  const moved &#061; board[fr][fc];<br \/>\n  const captured &#061; board[tr][tc];<br \/>\n  history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });<\/p>\n<p>  addInkTrailEffect(fr, fc, tr, tc, moved.color);<br \/>\n  if (captured) {<br \/>\n    addCaptureEffect(tr, tc, moved.color);<br \/>\n    triggerShake();<br \/>\n  }<\/p>\n<p>  board[tr][tc] &#061; moved;<br \/>\n  board[fr][fc] &#061; null;<br \/>\n  if (captured) {<br \/>\n    if (currentPlayer &#061;&#061;&#061; RED) capturedByRed.push(captured.type);<br \/>\n    else capturedByBlack.push(captured.type);<br \/>\n  }<br \/>\n  currentPlayer &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  updateUI();<br \/>\n  triggerAnimation();<br \/>\n}<\/p>\n<p>function undo() {<br \/>\n  if (history.length &#061;&#061;&#061; 0) return;<br \/>\n  const last &#061; history.pop();<br \/>\n  board[last.fr][last.fc] &#061; last.moved;<br \/>\n  board[last.tr][last.tc] &#061; last.captured;<br \/>\n  if (last.captured) {<br \/>\n    if (last.player &#061;&#061;&#061; RED) capturedByRed.pop();<br \/>\n    else capturedByBlack.pop();<br \/>\n  }<br \/>\n  currentPlayer &#061; last.player;<br \/>\n  gameOver &#061; false;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function checkGameEnd() {<br \/>\n  const opp &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  const oppKing &#061; findKing(opp);<br \/>\n  const statusEl &#061; document.getElementById(&#039;status&#039;);<br \/>\n  if (!oppKing) {<br \/>\n    gameOver &#061; true;<br \/>\n    const winner &#061; currentPlayer &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;;<br \/>\n    statusEl.textContent &#061; winner &#043; &#039; \u83b7\u80dc \u00b7 \u5c06\u519b\u62bd\u5e05&#039;;<br \/>\n    addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);<br \/>\n    showFx(&#039;fxMate&#039;, 2000);<br \/>\n    triggerShake();<br \/>\n    return;<br \/>\n  }<br \/>\n  if (!hasAnyLegalMove(currentPlayer)) {<br \/>\n    gameOver &#061; true;<br \/>\n    const inCheck &#061; isKingInCheck(currentPlayer);<br \/>\n    if (inCheck) {<br \/>\n      statusEl.textContent &#061; &#039;\u7edd\u6740 \u00b7 &#039; &#043; (opp &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;) &#043; &#039; \u83b7\u80dc&#039;;<br \/>\n      const king &#061; findKing(currentPlayer);<br \/>\n      if (king) {<br \/>\n        addCheckmateEffect(king.r, king.c);<br \/>\n      }<br \/>\n      showFx(&#039;fxMate&#039;, 2000);<br \/>\n      triggerShake();<br \/>\n    } else {<br \/>\n      statusEl.textContent &#061; &#039;\u548c\u68cb \u00b7 \u56f0\u6bd9&#039;;<br \/>\n    }<br \/>\n    return;<br \/>\n  }<br \/>\n  if (isKingInCheck(currentPlayer)) {<br \/>\n    statusEl.textContent &#061; &#039;\u5c06 \u519b&#039;;<br \/>\n    showFx(&#039;fxCheck&#039;, 1500);<br \/>\n  } else {<br \/>\n    statusEl.textContent &#061; &#039;&#039;;<br \/>\n  }<br \/>\n}<\/p>\n<p>function updateUI() {<br \/>\n  const ind &#061; document.getElementById(&#039;turnIndicator&#039;);<br \/>\n  if (gameOver) {<br \/>\n    ind.textContent &#061; &#039;\u6e38\u620f\u7ed3\u675f&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator&#039;;<br \/>\n  } else if (currentPlayer &#061;&#061;&#061; RED) {<br \/>\n    ind.textContent &#061; &#039;\u7ea2\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-red&#039;;<br \/>\n  } else {<br \/>\n    ind.textContent &#061; &#039;\u9ed1\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-black&#039;;<br \/>\n  }<br \/>\n  document.getElementById(&#039;capturedByRed&#039;).textContent &#061; capturedByRed.map(p &#061;&gt; PIECES.black[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n  document.getElementById(&#039;capturedByBlack&#039;).textContent &#061; capturedByBlack.map(p &#061;&gt; PIECES.red[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n}<\/p>\n<p>document.getElementById(&#039;restartBtn&#039;).addEventListener(&#039;click&#039;, initBoard);<br \/>\ndocument.getElementById(&#039;undoBtn&#039;).addEventListener(&#039;click&#039;, undo);<\/p>\n<p>initBoard();<br \/>\n&lt;\/script&gt;<br \/>\n&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n<p>\u601d\u7d22\u518d\u4e09\u540e\u51b3\u5b9a\u91cd\u5199\u68cb\u5b50\u79fb\u52a8&#xff1a;<\/p>\n<p>\u4e8c\u7248(\u65b0\u589e\u4e86\u6c34\u58a8\u7c92\u5b50\u98de\u6e85\u7684\u6548\u679c)&#xff1a;<\/p>\n<p>&lt;!DOCTYPE html&gt;<br \/>\n&lt;html lang&#061;&#034;zh-CN&#034;&gt;<br \/>\n&lt;head&gt;<br \/>\n&lt;meta charset&#061;&#034;UTF-8&#034;&gt;<br \/>\n&lt;title&gt;\u4e2d\u56fd\u8c61\u68cb \u00b7 \u6c34\u58a8\u7279\u6548\u7248&lt;\/title&gt;<br \/>\n&lt;style&gt;<br \/>\n  * { box-sizing: border-box; margin: 0; padding: 0; }<br \/>\n  body {<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    background: #e8e4dc;<br \/>\n    background-image:<br \/>\n      radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);<br \/>\n    min-height: 100vh;<br \/>\n    display: flex;<br \/>\n    flex-direction: column;<br \/>\n    align-items: center;<br \/>\n    padding: 30px 20px;<br \/>\n    color: #2c2c2c;<br \/>\n  }<br \/>\n  h1 {<br \/>\n    font-size: 38px;<br \/>\n    margin-bottom: 15px;<br \/>\n    letter-spacing: 12px;<br \/>\n    color: #1a1a1a;<br \/>\n    font-weight: normal;<br \/>\n    text-shadow: 1px 1px 0 rgba(0,0,0,0.08);<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  h1::after {<br \/>\n    content: &#039;&#039;;<br \/>\n    display: block;<br \/>\n    width: 60%;<br \/>\n    height: 2px;<br \/>\n    margin: 8px auto 0;<br \/>\n    background: linear-gradient(90deg, transparent, #555, transparent);<br \/>\n  }<br \/>\n  .info {<br \/>\n    display: flex;<br \/>\n    gap: 30px;<br \/>\n    margin-bottom: 20px;<br \/>\n    align-items: center;<br \/>\n    font-size: 18px;<br \/>\n  }<br \/>\n  .turn-indicator {<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    font-weight: bold;<br \/>\n    transition: all 0.3s;<br \/>\n    border: 1px solid #333;<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    letter-spacing: 2px;<br \/>\n  }<br \/>\n  .turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }<br \/>\n  .turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }<br \/>\n  button {<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    border: 1px solid #333;<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    cursor: pointer;<br \/>\n    font-size: 16px;<br \/>\n    font-family: inherit;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 2px;<br \/>\n    transition: all 0.3s;<br \/>\n  }<br \/>\n  button:hover {<br \/>\n    background: #2c2c2c;<br \/>\n    color: #f5f1ea;<br \/>\n    transform: translateY(-2px);<br \/>\n  }<br \/>\n  .canvas-wrap {<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  canvas {<br \/>\n    background: #f5f1ea;<br \/>\n    border: 1px solid #333;<br \/>\n    border-radius: 2px;<br \/>\n    box-shadow:<br \/>\n      0 0 0 8px #f5f1ea,<br \/>\n      0 0 0 9px #333,<br \/>\n      0 15px 40px rgba(0,0,0,0.3);<br \/>\n    cursor: pointer;<br \/>\n  }<br \/>\n  .status {<br \/>\n    margin-top: 18px;<br \/>\n    font-size: 20px;<br \/>\n    min-height: 28px;<br \/>\n    color: #c0392b;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 3px;<br \/>\n  }<br \/>\n  .captured {<br \/>\n    margin-top: 12px;<br \/>\n    display: flex;<br \/>\n    gap: 40px;<br \/>\n    font-size: 16px;<br \/>\n    color: #555;<br \/>\n  }<br \/>\n  .captured div { min-height: 30px; }<br \/>\n  .captured span {<br \/>\n    display: inline-block;<br \/>\n    margin: 0 3px;<br \/>\n    font-size: 22px;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, serif;<br \/>\n  }<br \/>\n  .fx-text {<br \/>\n    position: absolute;<br \/>\n    top: 50%;<br \/>\n    left: 50%;<br \/>\n    transform: translate(-50%, -50%);<br \/>\n    pointer-events: none;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    font-weight: bold;<br \/>\n    opacity: 0;<br \/>\n    z-index: 10;<br \/>\n  }<br \/>\n  .fx-check {<br \/>\n    font-size: 72px;<br \/>\n    color: #c0392b;<br \/>\n    letter-spacing: 20px;<br \/>\n    text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);<br \/>\n  }<br \/>\n  .fx-mate {<br \/>\n    font-size: 96px;<br \/>\n    color: #8b0000;<br \/>\n    letter-spacing: 30px;<br \/>\n    text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);<br \/>\n  }<br \/>\n  &#064;keyframes fxFlash {<br \/>\n    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }<br \/>\n    30%  { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }<br \/>\n    70%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }<br \/>\n    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }<br \/>\n  }<br \/>\n  &#064;keyframes fxShake {<br \/>\n    0%, 100% { transform: translate(0, 0); }<br \/>\n    10%  { transform: translate(-6px, -4px); }<br \/>\n    20%  { transform: translate(5px, 3px); }<br \/>\n    30%  { transform: translate(-4px, 5px); }<br \/>\n    40%  { transform: translate(6px, -3px); }<br \/>\n    50%  { transform: translate(-5px, 4px); }<br \/>\n    60%  { transform: translate(3px, -5px); }<br \/>\n    70%  { transform: translate(-3px, 3px); }<br \/>\n    80%  { transform: translate(4px, -2px); }<br \/>\n    90%  { transform: translate(-2px, 2px); }<br \/>\n  }<br \/>\n  .shake { animation: fxShake 0.5s ease-in-out; }<br \/>\n&lt;\/style&gt;<br \/>\n&lt;\/head&gt;<br \/>\n&lt;body&gt;<br \/>\n  &lt;h1&gt;\u4e2d \u570b \u8c61 \u68cb&lt;\/h1&gt;<br \/>\n  &lt;div class&#061;&#034;info&#034;&gt;<br \/>\n    &lt;div class&#061;&#034;turn-indicator&#034; id&#061;&#034;turnIndicator&#034;&gt;\u7ea2\u65b9\u8d70\u68cb&lt;\/div&gt;<br \/>\n    &lt;button id&#061;&#034;restartBtn&#034;&gt;\u91cd \u65b0 \u5f00 \u59cb&lt;\/button&gt;<br \/>\n    &lt;button id&#061;&#034;undoBtn&#034;&gt;\u6094 \u68cb&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;canvas-wrap&#034; id&#061;&#034;canvasWrap&#034;&gt;<br \/>\n    &lt;canvas id&#061;&#034;board&#034; width&#061;&#034;540&#034; height&#061;&#034;600&#034;&gt;&lt;\/canvas&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-check&#034; id&#061;&#034;fxCheck&#034;&gt;\u5c06 \u8ecd&lt;\/div&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-mate&#034; id&#061;&#034;fxMate&#034;&gt;\u7edd \u6bba&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;status&#034; id&#061;&#034;status&#034;&gt;&lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;captured&#034;&gt;<br \/>\n    &lt;div&gt;\u7ea2\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByRed&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n    &lt;div&gt;\u9ed1\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByBlack&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>&lt;script&gt;<br \/>\nconst CELL &#061; 60;<br \/>\nconst MARGIN_X &#061; 30;<br \/>\nconst MARGIN_Y &#061; 30;<br \/>\nconst COLS &#061; 9;<br \/>\nconst ROWS &#061; 10;<\/p>\n<p>const canvas &#061; document.getElementById(&#039;board&#039;);<br \/>\nconst ctx &#061; canvas.getContext(&#039;2d&#039;);<br \/>\nconst canvasWrap &#061; document.getElementById(&#039;canvasWrap&#039;);<\/p>\n<p>const RED &#061; &#039;red&#039;;<br \/>\nconst BLACK &#061; &#039;black&#039;;<\/p>\n<p>const PIECES &#061; {<br \/>\n  red: {<br \/>\n    king: &#039;\u5e25&#039;, advisor: &#039;\u4ed5&#039;, elephant: &#039;\u76f8&#039;, horse: &#039;\u508c&#039;,<br \/>\n    chariot: &#039;\u4fe5&#039;, cannon: &#039;\u70ae&#039;, pawn: &#039;\u5175&#039;<br \/>\n  },<br \/>\n  black: {<br \/>\n    king: &#039;\u5c07&#039;, advisor: &#039;\u58eb&#039;, elephant: &#039;\u8c61&#039;, horse: &#039;\u99ac&#039;,<br \/>\n    chariot: &#039;\u8eca&#039;, cannon: &#039;\u7832&#039;, pawn: &#039;\u5352&#039;<br \/>\n  }<br \/>\n};<\/p>\n<p>let board &#061; [];<br \/>\nlet currentPlayer &#061; RED;<br \/>\nlet selected &#061; null;<br \/>\nlet legalMoves &#061; [];<br \/>\nlet gameOver &#061; false;<br \/>\nlet history &#061; [];<br \/>\nlet capturedByRed &#061; [];<br \/>\nlet capturedByBlack &#061; [];<\/p>\n<p>let effects &#061; [];<br \/>\nlet animating &#061; false;<br \/>\nlet checkAnimStart &#061; 0;<br \/>\nlet checkKingPos &#061; null;<br \/>\nlet checkmateAnimStart &#061; 0;<\/p>\n<p>function P(type, color) {<br \/>\n  return { type, color };<br \/>\n}<\/p>\n<p>function initBoard() {<br \/>\n  board &#061; [<br \/>\n    [P(&#039;chariot&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;king&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;chariot&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,P(&#039;cannon&#039;,BLACK),null,null,null,null,null,P(&#039;cannon&#039;,BLACK),null],<br \/>\n    [P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED)],<br \/>\n    [null,P(&#039;cannon&#039;,RED),null,null,null,null,null,P(&#039;cannon&#039;,RED),null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;chariot&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;king&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;chariot&#039;,RED)]<br \/>\n  ];<br \/>\n  currentPlayer &#061; RED;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  gameOver &#061; false;<br \/>\n  history &#061; [];<br \/>\n  capturedByRed &#061; [];<br \/>\n  capturedByBlack &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  checkmateAnimStart &#061; 0;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function isInBoard(r, c) {<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt; ROWS &amp;&amp; c &gt;&#061; 0 &amp;&amp; c &lt; COLS;<br \/>\n}<\/p>\n<p>function inPalace(r, c, color) {<br \/>\n  if (c &lt; 3 || c &gt; 5) return false;<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 7 &amp;&amp; r &lt;&#061; 9;<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt;&#061; 2;<br \/>\n}<\/p>\n<p>function onOwnSide(r, color) {<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 5;<br \/>\n  return r &lt;&#061; 4;<br \/>\n}<\/p>\n<p>function getLegalMoves(r, c) {<br \/>\n  const piece &#061; board[r][c];<br \/>\n  if (!piece) return [];<br \/>\n  const color &#061; piece.color;<br \/>\n  const type &#061; piece.type;<br \/>\n  const moves &#061; [];<br \/>\n  const addMove &#061; (nr, nc) &#061;&gt; {<br \/>\n    if (!isInBoard(nr, nc)) return;<br \/>\n    const target &#061; board[nr][nc];<br \/>\n    if (target &amp;&amp; target.color &#061;&#061;&#061; color) return;<br \/>\n    moves.push({ r: nr, c: nc, capture: !!target });<br \/>\n  };<\/p>\n<p>  switch (type) {<br \/>\n    case &#039;king&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;advisor&#039;: {<br \/>\n      const dirs &#061; [[-1,-1],[-1,1],[1,-1],[1,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;elephant&#039;: {<br \/>\n      const dirs &#061; [[-2,-2],[-2,2],[2,-2],[2,2]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        const mr &#061; r &#043; dr\/2, mc &#061; c &#043; dc\/2;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (!onOwnSide(nr, color)) continue;<br \/>\n        if (board[mr][mc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;horse&#039;: {<br \/>\n      const hm &#061; [<br \/>\n        {dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},<br \/>\n        {dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},<br \/>\n        {dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},<br \/>\n        {dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}<br \/>\n      ];<br \/>\n      for (const m of hm) {<br \/>\n        const nr &#061; r &#043; m.dr, nc &#061; c &#043; m.dc;<br \/>\n        const lr &#061; r &#043; m.br, lc &#061; c &#043; m.bc;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (board[lr][lc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;chariot&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!target) {<br \/>\n            moves.push({ r: nr, c: nc, capture: false });<br \/>\n          } else {<br \/>\n            if (target.color !&#061;&#061; color) {<br \/>\n              moves.push({ r: nr, c: nc, capture: true });<br \/>\n            }<br \/>\n            break;<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;cannon&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        let jumped &#061; false;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!jumped) {<br \/>\n            if (!target) {<br \/>\n              moves.push({ r: nr, c: nc, capture: false });<br \/>\n            } else {<br \/>\n              jumped &#061; true;<br \/>\n            }<br \/>\n          } else {<br \/>\n            if (target) {<br \/>\n              if (target.color !&#061;&#061; color) {<br \/>\n                moves.push({ r: nr, c: nc, capture: true });<br \/>\n              }<br \/>\n              break;<br \/>\n            }<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;pawn&#039;: {<br \/>\n      const forward &#061; color &#061;&#061;&#061; RED ? -1 : 1;<br \/>\n      const nr &#061; r &#043; forward;<br \/>\n      if (isInBoard(nr, c)) addMove(nr, c);<br \/>\n      if (!onOwnSide(r, color)) {<br \/>\n        if (isInBoard(r, c &#8211; 1)) addMove(r, c &#8211; 1);<br \/>\n        if (isInBoard(r, c &#043; 1)) addMove(r, c &#043; 1);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n  }<br \/>\n  return moves;<br \/>\n}<\/p>\n<p>function findKing(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].type &#061;&#061;&#061; &#039;king&#039; &amp;&amp; board[r][c].color &#061;&#061;&#061; color) return { r, c };<br \/>\n    }<br \/>\n  }<br \/>\n  return null;<br \/>\n}<\/p>\n<p>function kingsFacing() {<br \/>\n  const rk &#061; findKing(RED);<br \/>\n  const bk &#061; findKing(BLACK);<br \/>\n  if (!rk || !bk || rk.c !&#061;&#061; bk.c) return false;<br \/>\n  const col &#061; rk.c;<br \/>\n  const minR &#061; Math.min(rk.r, bk.r);<br \/>\n  const maxR &#061; Math.max(rk.r, bk.r);<br \/>\n  for (let r &#061; minR &#043; 1; r &lt; maxR; r&#043;&#043;) {<br \/>\n    if (board[r][col]) return false;<br \/>\n  }<br \/>\n  return true;<br \/>\n}<\/p>\n<p>function getLegalMovesFiltered(r, c) {<br \/>\n  const moves &#061; getLegalMoves(r, c);<br \/>\n  return moves.filter(m &#061;&gt; {<br \/>\n    const captured &#061; board[m.r][m.c];<br \/>\n    const moving &#061; board[r][c];<br \/>\n    board[m.r][m.c] &#061; moving;<br \/>\n    board[r][c] &#061; null;<br \/>\n    const inCheck &#061; isKingInCheck(moving.color) || kingsFacing();<br \/>\n    board[r][c] &#061; moving;<br \/>\n    board[m.r][m.c] &#061; captured;<br \/>\n    return !inCheck;<br \/>\n  });<br \/>\n}<\/p>\n<p>function isKingInCheck(color) {<br \/>\n  const king &#061; findKing(color);<br \/>\n  if (!king) return true;<br \/>\n  const oppColor &#061; color &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; oppColor) {<br \/>\n        const moves &#061; getLegalMoves(r, c);<br \/>\n        if (moves.some(m &#061;&gt; m.r &#061;&#061;&#061; king.r &amp;&amp; m.c &#061;&#061;&#061; king.c)) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function hasAnyLegalMove(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; color) {<br \/>\n        if (getLegalMovesFiltered(r, c).length &gt; 0) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function drawBoard() {<br \/>\n  const grad &#061; ctx.createLinearGradient(0, 0, canvas.width, canvas.height);<br \/>\n  grad.addColorStop(0, &#039;#f8f4ec&#039;);<br \/>\n  grad.addColorStop(0.5, &#039;#f0ebe0&#039;);<br \/>\n  grad.addColorStop(1, &#039;#ede6d8&#039;);<br \/>\n  ctx.fillStyle &#061; grad;<br \/>\n  ctx.fillRect(0, 0, canvas.width, canvas.height);<\/p>\n<p>  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.5;<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(MARGIN_X, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n    if (c &#061;&#061;&#061; 0 || c &#061;&#061;&#061; COLS &#8211; 1) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; (ROWS &#8211; 1) * CELL);<br \/>\n      ctx.stroke();<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n      ctx.stroke();<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n      ctx.stroke();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#039;bold 26px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#039;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(&#039;\u695a \u6cb3&#039;, MARGIN_X &#043; 1.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<br \/>\n  ctx.fillText(&#039;\u6f22 \u754c&#039;, MARGIN_X &#043; 6.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<\/p>\n<p>  drawStarPoints();<br \/>\n}<\/p>\n<p>function drawStarPoints() {<br \/>\n  const points &#061; [<br \/>\n    [2,1],[2,7],[7,1],[7,7],<br \/>\n    [3,0],[3,2],[3,4],[3,6],[3,8],<br \/>\n    [6,0],[6,2],[6,4],[6,6],[6,8]<br \/>\n  ];<br \/>\n  for (const [r, c] of points) {<br \/>\n    drawStar(r, c);<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawStar(r, c) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  const s &#061; 6;<br \/>\n  const g &#061; 5;<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.2;<br \/>\n  const dirs &#061; [];<br \/>\n  if (c &gt; 0) dirs.push([-1,-1],[-1,1]);<br \/>\n  if (c &lt; COLS &#8211; 1) dirs.push([1,-1],[1,1]);<br \/>\n  for (const [dx, dy] of dirs) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * (g &#043; s), y &#043; dy * g);<br \/>\n    ctx.stroke();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * g, y &#043; dy * (g &#043; s));<br \/>\n    ctx.stroke();<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawPiece(r, c, type, color, highlight) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  const radius &#061; 26;<\/p>\n<p>  if (highlight) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius &#043; 5, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#039;rgba(192, 57, 43, 0.25)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const inkGrad &#061; ctx.createRadialGradient(x &#8211; 3, y &#8211; 3, 2, x, y, radius);<br \/>\n  if (color &#061;&#061;&#061; RED) {<br \/>\n    inkGrad.addColorStop(0, &#039;#e8e0d4&#039;);<br \/>\n    inkGrad.addColorStop(0.6, &#039;#d4c8b4&#039;);<br \/>\n    inkGrad.addColorStop(1, &#039;#b8a88c&#039;);<br \/>\n  } else {<br \/>\n    inkGrad.addColorStop(0, &#039;#e8e0d4&#039;);<br \/>\n    inkGrad.addColorStop(0.6, &#039;#d0c4b0&#039;);<br \/>\n    inkGrad.addColorStop(1, &#039;#b0a288&#039;);<br \/>\n  }<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 2;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius &#8211; 4, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#039;bold 28px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#039;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(PIECES[color][type], x, y &#043; 1);<br \/>\n}<\/p>\n<p>function draw() {<br \/>\n  drawBoard();<\/p>\n<p>  for (const move of legalMoves) {<br \/>\n    const x &#061; MARGIN_X &#043; move.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; move.r * CELL;<br \/>\n    if (move.capture) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 30, 0, Math.PI * 2);<br \/>\n      ctx.strokeStyle &#061; &#039;rgba(192, 57, 43, 0.7)&#039;;<br \/>\n      ctx.lineWidth &#061; 2.5;<br \/>\n      ctx.setLineDash([4, 3]);<br \/>\n      ctx.stroke();<br \/>\n      ctx.setLineDash([]);<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 6, 0, Math.PI * 2);<br \/>\n      ctx.fillStyle &#061; &#039;rgba(26, 26, 26, 0.5)&#039;;<br \/>\n      ctx.fill();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c]) {<br \/>\n        const p &#061; board[r][c];<br \/>\n        drawPiece(r, c, p.type, p.color, selected &amp;&amp; selected.r &#061;&#061;&#061; r &amp;&amp; selected.c &#061;&#061;&#061; c);<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const now &#061; performance.now();<\/p>\n<p>  if (redKing &amp;&amp; isKingInCheck(RED)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; RED || checkKingPos.r !&#061;&#061; redKing.r || checkKingPos.c !&#061;&#061; redKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: redKing.r, c: redKing.c, color: RED };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; redKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; redKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; RED) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<\/p>\n<p>  if (blackKing &amp;&amp; isKingInCheck(BLACK)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; BLACK || checkKingPos.r !&#061;&#061; blackKing.r || checkKingPos.c !&#061;&#061; blackKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: blackKing.r, c: blackKing.c, color: BLACK };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; blackKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; blackKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; BLACK) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawEffects() {<br \/>\n  const now &#061; performance.now();<br \/>\n  const remaining &#061; [];<\/p>\n<p>  for (const e of effects) {<br \/>\n    const elapsed &#061; now &#8211; e.startTime;<br \/>\n    const progress &#061; elapsed \/ e.duration;<br \/>\n    if (progress &gt;&#061; 1) continue;<\/p>\n<p>    if (e.type &#061;&#061;&#061; &#039;capture&#039;) {<br \/>\n      drawCaptureEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;checkmate&#039;) {<br \/>\n      drawCheckmateEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;inkTrail&#039;) {<br \/>\n      drawInkTrailEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  effects &#061; remaining;<br \/>\n}<\/p>\n<p>function drawCaptureEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<br \/>\n  const inkColor2 &#061; &#039;rgba(40, 40, 40,&#039;;<\/p>\n<p>  for (let i &#061; 0; i &lt; 3; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.08;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 10 &#043; pp * 55;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.8;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; inkColor2 &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.lineWidth &#061; 3 &#8211; pp * 2.5;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 14; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 14) * Math.PI * 2 &#043; p * 0.8;<br \/>\n    const dist &#061; p * 55;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 5 &#043; 1;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor2 &#043; ((1 &#8211; p) * 0.7).toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const coreGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 &#8211; p * 0.6));<br \/>\n  coreGrad.addColorStop(0, inkColor2 &#043; (0.5 * (1 &#8211; p)).toFixed(2) &#043; &#039;)&#039;);<br \/>\n  coreGrad.addColorStop(1, inkColor2 &#043; &#039;0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 30 * (1 &#8211; p * 0.6), 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; coreGrad;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function drawCheckmateEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<\/p>\n<p>  ctx.save();<br \/>\n  ctx.globalAlpha &#061; Math.min(1, p * 3);<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.06;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 15 &#043; pp * 200;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.6;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${alpha.toFixed(2)})&#096;;<br \/>\n    ctx.lineWidth &#061; 4 &#8211; pp * 3;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 5) * Math.PI * 2 &#8211; Math.PI \/ 2;<br \/>\n    const dist &#061; Math.min(300, p * 600);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x, y);<br \/>\n    ctx.lineTo(x &#043; Math.cos(angle) * dist, y &#043; Math.sin(angle) * dist);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${(1 &#8211; p) * 0.5})&#096;;<br \/>\n    ctx.lineWidth &#061; 2;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 30; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 30) * Math.PI * 2 &#043; p * 1.5;<br \/>\n    const dist &#061; p * 80 &#043; Math.sin(i * 3 &#043; p * 10) * 15;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 8 &#043; 2;<br \/>\n    const gray &#061; 30 &#043; (i % 3) * 25;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#096;rgba(${gray}, ${gray}, ${gray}, ${(1 &#8211; p) * 0.8})&#096;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const flashGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 &#8211; p));<br \/>\n  flashGrad.addColorStop(0, &#096;rgba(255, 255, 255, ${Math.max(0, (0.6 &#8211; p * 0.6))})&#096;);<br \/>\n  flashGrad.addColorStop(0.5, &#039;rgba(255, 255, 255, 0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 250, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; flashGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.restore();<br \/>\n}<\/p>\n<p>function drawInkTrailEffect(e, p) {<br \/>\n  const x &#061; e.sx &#043; (e.dx &#8211; e.sx) * p;<br \/>\n  const y &#061; e.sy &#043; (e.dy &#8211; e.sy) * p;<br \/>\n  const inkColor &#061; &#039;rgba(40, 40, 40,&#039;;<br \/>\n  const dx &#061; e.dx &#8211; e.sx;<br \/>\n  const dy &#061; e.dy &#8211; e.sy;<br \/>\n  const angle &#061; Math.atan2(dy, dx);<br \/>\n  const splatters &#061; e.splatters;<\/p>\n<p>  for (let i &#061; 0; i &lt; 8; i&#043;&#043;) {<br \/>\n    const pp &#061; Math.max(0, p &#8211; i * 0.035);<br \/>\n    const tx &#061; e.sx &#043; dx * pp;<br \/>\n    const ty &#061; e.sy &#043; dy * pp;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.5;<br \/>\n    const size &#061; (1 &#8211; pp) * 20 &#043; 4;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(tx, ty, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const pp &#061; Math.max(0, p &#8211; i * 0.06);<br \/>\n    const tx &#061; e.sx &#043; dx * pp;<br \/>\n    const ty &#061; e.sy &#043; dy * pp;<br \/>\n    const spread &#061; 8 * (1 &#8211; pp);<br \/>\n    const perpAngle &#061; angle &#043; Math.PI \/ 2;<br \/>\n    const offset &#061; (Math.sin(i * 1.7 &#043; pp * 5) * spread);<br \/>\n    const ox &#061; tx &#043; Math.cos(perpAngle) * offset;<br \/>\n    const oy &#061; ty &#043; Math.sin(perpAngle) * offset;<br \/>\n    const size &#061; (1 &#8211; pp) * 10 &#043; 2;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.35;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(ox, oy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; splatters.length; i&#043;&#043;) {<br \/>\n    const sp &#061; splatters[i];<br \/>\n    const progress &#061; (p &#8211; sp.delay) \/ (1 &#8211; sp.delay);<br \/>\n    if (progress &lt;&#061; 0 || progress &gt; 1) continue;<br \/>\n    const sx &#061; x &#043; Math.cos(sp.angle) * (progress * 35 &#043; 5) &#043; sp.offsetX * progress;<br \/>\n    const sy &#061; y &#043; Math.sin(sp.angle) * (progress * 35 &#043; 5) &#043; sp.offsetY * progress;<br \/>\n    const size &#061; (1 &#8211; progress) * sp.maxSize &#043; sp.minSize;<br \/>\n    const alpha &#061; (1 &#8211; progress) * sp.maxAlpha;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(sx, sy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; e.clusters.length; i&#043;&#043;) {<br \/>\n    const cl &#061; e.clusters[i];<br \/>\n    const progress &#061; (p &#8211; cl.delay) \/ (1 &#8211; cl.delay);<br \/>\n    if (progress &lt;&#061; 0 || progress &gt; 1) continue;<br \/>\n    const cx &#061; x &#043; Math.cos(cl.angle) * (progress * 40);<br \/>\n    const cy &#061; y &#043; Math.sin(cl.angle) * (progress * 40);<br \/>\n    const size &#061; (1 &#8211; progress) * cl.maxSize &#043; cl.minSize;<br \/>\n    const alpha &#061; (1 &#8211; progress) * cl.maxAlpha;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(cx, cy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#096;rgba(${cl.gray}, ${cl.gray}, ${cl.gray}, ${alpha.toFixed(2)})&#096;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const headGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 30);<br \/>\n  headGrad.addColorStop(0, inkColor &#043; &#039;0.7)&#039;);<br \/>\n  headGrad.addColorStop(0.6, inkColor &#043; &#039;0.4)&#039;);<br \/>\n  headGrad.addColorStop(1, inkColor &#043; &#039;0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 30, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; headGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 22, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkColor &#043; &#039;0.55)&#039;;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function animate() {<br \/>\n  draw();<br \/>\n  drawEffects();<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const redChecked &#061; redKing &amp;&amp; isKingInCheck(RED);<br \/>\n  const blackChecked &#061; blackKing &amp;&amp; isKingInCheck(BLACK);<\/p>\n<p>  if (effects.length &gt; 0 || redChecked || blackChecked) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  } else {<br \/>\n    animating &#061; false;<br \/>\n  }<br \/>\n}<\/p>\n<p>function triggerAnimation() {<br \/>\n  if (!animating) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  }<br \/>\n}<\/p>\n<p>function getXY(r, c) {<br \/>\n  return { x: MARGIN_X &#043; c * CELL, y: MARGIN_Y &#043; r * CELL };<br \/>\n}<\/p>\n<p>function addCaptureEffect(r, c, color) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;capture&#039;,<br \/>\n    x, y,<br \/>\n    color,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 800<br \/>\n  });<br \/>\n}<\/p>\n<p>function addInkTrailEffect(sr, sc, dr, dc, color) {<br \/>\n  const { x: sx, y: sy } &#061; getXY(sr, sc);<br \/>\n  const { x: dx, y: dy } &#061; getXY(dr, dc);<br \/>\n  const moveAngle &#061; Math.atan2(dy &#8211; sy, dx &#8211; sx);<\/p>\n<p>  const splatters &#061; [];<br \/>\n  for (let i &#061; 0; i &lt; 12; i&#043;&#043;) {<br \/>\n    const spreadAngle &#061; moveAngle &#043; Math.PI &#043; (Math.random() &#8211; 0.5) * 1.8;<br \/>\n    splatters.push({<br \/>\n      angle: spreadAngle,<br \/>\n      delay: i * 0.07,<br \/>\n      offsetX: (Math.random() &#8211; 0.5) * 10,<br \/>\n      offsetY: (Math.random() &#8211; 0.5) * 10,<br \/>\n      maxSize: 6 &#043; Math.random() * 5,<br \/>\n      minSize: 1 &#043; Math.random() * 1.5,<br \/>\n      maxAlpha: 0.5 &#043; Math.random() * 0.3<br \/>\n    });<br \/>\n  }<\/p>\n<p>  const clusters &#061; [];<br \/>\n  for (let i &#061; 0; i &lt; 4; i&#043;&#043;) {<br \/>\n    const clusterAngle &#061; moveAngle &#043; Math.PI &#043; (i \/ 4) * 0.8 &#8211; 0.4 &#043; (Math.random() &#8211; 0.5) * 0.3;<br \/>\n    clusters.push({<br \/>\n      angle: clusterAngle,<br \/>\n      delay: i * 0.15,<br \/>\n      gray: 25 &#043; Math.floor(Math.random() * 30),<br \/>\n      maxSize: 4 &#043; Math.random() * 4,<br \/>\n      minSize: 0.8 &#043; Math.random() * 1.2,<br \/>\n      maxAlpha: 0.4 &#043; Math.random() * 0.3<br \/>\n    });<br \/>\n  }<\/p>\n<p>  effects.push({<br \/>\n    type: &#039;inkTrail&#039;,<br \/>\n    sx, sy, dx, dy,<br \/>\n    color,<br \/>\n    splatters,<br \/>\n    clusters,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 400<br \/>\n  });<br \/>\n}<\/p>\n<p>function addCheckmateEffect(r, c) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;checkmate&#039;,<br \/>\n    x, y,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 2000<br \/>\n  });<br \/>\n}<\/p>\n<p>function showFx(id, duration) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.offsetHeight;<br \/>\n  el.style.animation &#061; &#096;fxFlash ${duration}ms ease-out forwards&#096;;<br \/>\n}<\/p>\n<p>function hideFx(id) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.style.opacity &#061; &#039;0&#039;;<br \/>\n}<\/p>\n<p>function triggerShake() {<br \/>\n  canvasWrap.classList.remove(&#039;shake&#039;);<br \/>\n  void canvasWrap.offsetWidth;<br \/>\n  canvasWrap.classList.add(&#039;shake&#039;);<br \/>\n}<\/p>\n<p>canvas.addEventListener(&#039;click&#039;, (e) &#061;&gt; {<br \/>\n  if (gameOver) return;<br \/>\n  const rect &#061; canvas.getBoundingClientRect();<br \/>\n  const x &#061; (e.clientX &#8211; rect.left) * (canvas.width \/ rect.width);<br \/>\n  const y &#061; (e.clientY &#8211; rect.top) * (canvas.height \/ rect.height);<br \/>\n  const c &#061; Math.round((x &#8211; MARGIN_X) \/ CELL);<br \/>\n  const r &#061; Math.round((y &#8211; MARGIN_Y) \/ CELL);<br \/>\n  if (!isInBoard(r, c)) return;<\/p>\n<p>  const piece &#061; board[r][c];<\/p>\n<p>  if (selected) {<br \/>\n    const move &#061; legalMoves.find(m &#061;&gt; m.r &#061;&#061;&#061; r &amp;&amp; m.c &#061;&#061;&#061; c);<br \/>\n    if (move) {<br \/>\n      makeMove(selected.r, selected.c, r, c);<br \/>\n      selected &#061; null;<br \/>\n      legalMoves &#061; [];<br \/>\n      checkGameEnd();<br \/>\n      return;<br \/>\n    }<br \/>\n    if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n      selected &#061; { r, c };<br \/>\n      legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n      draw();<br \/>\n      return;<br \/>\n    }<br \/>\n    selected &#061; null;<br \/>\n    legalMoves &#061; [];<br \/>\n    draw();<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n    selected &#061; { r, c };<br \/>\n    legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n    draw();<br \/>\n  }<br \/>\n});<\/p>\n<p>function makeMove(fr, fc, tr, tc) {<br \/>\n  const moved &#061; board[fr][fc];<br \/>\n  const captured &#061; board[tr][tc];<br \/>\n  history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });<\/p>\n<p>  addInkTrailEffect(fr, fc, tr, tc, moved.color);<br \/>\n  if (captured) {<br \/>\n    addCaptureEffect(tr, tc, moved.color);<br \/>\n    triggerShake();<br \/>\n  }<\/p>\n<p>  board[tr][tc] &#061; moved;<br \/>\n  board[fr][fc] &#061; null;<br \/>\n  if (captured) {<br \/>\n    if (currentPlayer &#061;&#061;&#061; RED) capturedByRed.push(captured.type);<br \/>\n    else capturedByBlack.push(captured.type);<br \/>\n  }<br \/>\n  currentPlayer &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  updateUI();<br \/>\n  triggerAnimation();<br \/>\n}<\/p>\n<p>function undo() {<br \/>\n  if (history.length &#061;&#061;&#061; 0) return;<br \/>\n  const last &#061; history.pop();<br \/>\n  board[last.fr][last.fc] &#061; last.moved;<br \/>\n  board[last.tr][last.tc] &#061; last.captured;<br \/>\n  if (last.captured) {<br \/>\n    if (last.player &#061;&#061;&#061; RED) capturedByRed.pop();<br \/>\n    else capturedByBlack.pop();<br \/>\n  }<br \/>\n  currentPlayer &#061; last.player;<br \/>\n  gameOver &#061; false;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function checkGameEnd() {<br \/>\n  const opp &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  const oppKing &#061; findKing(opp);<br \/>\n  const statusEl &#061; document.getElementById(&#039;status&#039;);<br \/>\n  if (!oppKing) {<br \/>\n    gameOver &#061; true;<br \/>\n    const winner &#061; currentPlayer &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;;<br \/>\n    statusEl.textContent &#061; winner &#043; &#039; \u83b7\u80dc \u00b7 \u5c06\u519b\u62bd\u5e05&#039;;<br \/>\n    addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);<br \/>\n    showFx(&#039;fxMate&#039;, 2000);<br \/>\n    triggerShake();<br \/>\n    return;<br \/>\n  }<br \/>\n  if (!hasAnyLegalMove(currentPlayer)) {<br \/>\n    gameOver &#061; true;<br \/>\n    const inCheck &#061; isKingInCheck(currentPlayer);<br \/>\n    if (inCheck) {<br \/>\n      statusEl.textContent &#061; &#039;\u7edd\u6740 \u00b7 &#039; &#043; (opp &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;) &#043; &#039; \u83b7\u80dc&#039;;<br \/>\n      const king &#061; findKing(currentPlayer);<br \/>\n      if (king) {<br \/>\n        addCheckmateEffect(king.r, king.c);<br \/>\n      }<br \/>\n      showFx(&#039;fxMate&#039;, 2000);<br \/>\n      triggerShake();<br \/>\n    } else {<br \/>\n      statusEl.textContent &#061; &#039;\u548c\u68cb \u00b7 \u56f0\u6bd9&#039;;<br \/>\n    }<br \/>\n    return;<br \/>\n  }<br \/>\n  if (isKingInCheck(currentPlayer)) {<br \/>\n    statusEl.textContent &#061; &#039;\u5c06 \u519b&#039;;<br \/>\n    showFx(&#039;fxCheck&#039;, 1500);<br \/>\n  } else {<br \/>\n    statusEl.textContent &#061; &#039;&#039;;<br \/>\n  }<br \/>\n}<\/p>\n<p>function updateUI() {<br \/>\n  const ind &#061; document.getElementById(&#039;turnIndicator&#039;);<br \/>\n  if (gameOver) {<br \/>\n    ind.textContent &#061; &#039;\u6e38\u620f\u7ed3\u675f&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator&#039;;<br \/>\n  } else if (currentPlayer &#061;&#061;&#061; RED) {<br \/>\n    ind.textContent &#061; &#039;\u7ea2\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-red&#039;;<br \/>\n  } else {<br \/>\n    ind.textContent &#061; &#039;\u9ed1\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-black&#039;;<br \/>\n  }<br \/>\n  document.getElementById(&#039;capturedByRed&#039;).textContent &#061; capturedByRed.map(p &#061;&gt; PIECES.black[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n  document.getElementById(&#039;capturedByBlack&#039;).textContent &#061; capturedByBlack.map(p &#061;&gt; PIECES.red[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n}<\/p>\n<p>document.getElementById(&#039;restartBtn&#039;).addEventListener(&#039;click&#039;, initBoard);<br \/>\ndocument.getElementById(&#039;undoBtn&#039;).addEventListener(&#039;click&#039;, undo);<\/p>\n<p>initBoard();<br \/>\n&lt;\/script&gt;<br \/>\n&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n<p>\u968f\u540e\u53d1\u73b0\u4e86\u4e00\u4e2a\u95ee\u9898&#xff0c;\u6c34\u58a8\u62d6\u5c3e\u7279\u6548\u4e0e\u68cb\u5b50\u79fb\u52a8\u5e76\u4e0d\u5339\u914d&#xff1a;<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" alt=\"\" height=\"466\" src=\"https:\/\/www.wsisp.com\/helps\/wp-content\/uploads\/2026\/08\/20260829003630-6a92298e34a85.png\" width=\"876\" \/><\/p>\n<p>\u95ee\u9898\u5728\u4e8e\u68cb\u5b50\u662f\u4e00\u77ac\u95f4\u79fb\u5230\u7ec8\u70b9\u7684&#xff0c;\u800c\u62d6\u5c3e\u5219\u662f\u968f\u540e\u7f13\u6162\u51fa\u73b0\u7684<\/p>\n<p>\u53c8\u5199\u4e86100\u591a\u884c<img loading=\"lazy\" decoding=\"async\" alt=\"\" height=\"40\" src=\"https:\/\/www.wsisp.com\/helps\/wp-content\/uploads\/2026\/08\/20260829003630-6a92298e6e7b0.png\" width=\"114\" \/><\/p>\n<p>\u8fd9\u4e0b\u7b97\u662f\u5b8c\u6210\u4e86&#xff1a;<\/p>\n<p>&lt;!DOCTYPE html&gt;<br \/>\n&lt;html lang&#061;&#034;zh-CN&#034;&gt;<br \/>\n&lt;head&gt;<br \/>\n&lt;meta charset&#061;&#034;UTF-8&#034;&gt;<br \/>\n&lt;title&gt;\u4e2d\u56fd\u8c61\u68cb \u00b7 \u6c34\u58a8\u7279\u6548\u7248&lt;\/title&gt;<br \/>\n&lt;style&gt;<br \/>\n  * { box-sizing: border-box; margin: 0; padding: 0; }<br \/>\n  body {<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    background: #e8e4dc;<br \/>\n    background-image:<br \/>\n      radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),<br \/>\n      radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);<br \/>\n    min-height: 100vh;<br \/>\n    display: flex;<br \/>\n    flex-direction: column;<br \/>\n    align-items: center;<br \/>\n    padding: 30px 20px;<br \/>\n    color: #2c2c2c;<br \/>\n  }<br \/>\n  h1 {<br \/>\n    font-size: 38px;<br \/>\n    margin-bottom: 15px;<br \/>\n    letter-spacing: 12px;<br \/>\n    color: #1a1a1a;<br \/>\n    font-weight: normal;<br \/>\n    text-shadow: 1px 1px 0 rgba(0,0,0,0.08);<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  h1::after {<br \/>\n    content: &#039;&#039;;<br \/>\n    display: block;<br \/>\n    width: 60%;<br \/>\n    height: 2px;<br \/>\n    margin: 8px auto 0;<br \/>\n    background: linear-gradient(90deg, transparent, #555, transparent);<br \/>\n  }<br \/>\n  .info {<br \/>\n    display: flex;<br \/>\n    gap: 30px;<br \/>\n    margin-bottom: 20px;<br \/>\n    align-items: center;<br \/>\n    font-size: 18px;<br \/>\n  }<br \/>\n  .turn-indicator {<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    font-weight: bold;<br \/>\n    transition: all 0.3s;<br \/>\n    border: 1px solid #333;<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    letter-spacing: 2px;<br \/>\n  }<br \/>\n  .turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }<br \/>\n  .turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }<br \/>\n  button {<br \/>\n    background: #f5f1ea;<br \/>\n    color: #2c2c2c;<br \/>\n    border: 1px solid #333;<br \/>\n    padding: 10px 24px;<br \/>\n    border-radius: 4px;<br \/>\n    cursor: pointer;<br \/>\n    font-size: 16px;<br \/>\n    font-family: inherit;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 2px;<br \/>\n    transition: all 0.3s;<br \/>\n  }<br \/>\n  button:hover {<br \/>\n    background: #2c2c2c;<br \/>\n    color: #f5f1ea;<br \/>\n    transform: translateY(-2px);<br \/>\n  }<br \/>\n  .canvas-wrap {<br \/>\n    position: relative;<br \/>\n  }<br \/>\n  canvas {<br \/>\n    background: #f5f1ea;<br \/>\n    border: 1px solid #333;<br \/>\n    border-radius: 2px;<br \/>\n    box-shadow:<br \/>\n      0 0 0 8px #f5f1ea,<br \/>\n      0 0 0 9px #333,<br \/>\n      0 15px 40px rgba(0,0,0,0.3);<br \/>\n    cursor: pointer;<br \/>\n  }<br \/>\n  .status {<br \/>\n    margin-top: 18px;<br \/>\n    font-size: 20px;<br \/>\n    min-height: 28px;<br \/>\n    color: #c0392b;<br \/>\n    font-weight: bold;<br \/>\n    letter-spacing: 3px;<br \/>\n  }<br \/>\n  .captured {<br \/>\n    margin-top: 12px;<br \/>\n    display: flex;<br \/>\n    gap: 40px;<br \/>\n    font-size: 16px;<br \/>\n    color: #555;<br \/>\n  }<br \/>\n  .captured div { min-height: 30px; }<br \/>\n  .captured span {<br \/>\n    display: inline-block;<br \/>\n    margin: 0 3px;<br \/>\n    font-size: 22px;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, serif;<br \/>\n  }<br \/>\n  .fx-text {<br \/>\n    position: absolute;<br \/>\n    top: 50%;<br \/>\n    left: 50%;<br \/>\n    transform: translate(-50%, -50%);<br \/>\n    pointer-events: none;<br \/>\n    font-family: &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif;<br \/>\n    font-weight: bold;<br \/>\n    opacity: 0;<br \/>\n    z-index: 10;<br \/>\n  }<br \/>\n  .fx-check {<br \/>\n    font-size: 72px;<br \/>\n    color: #c0392b;<br \/>\n    letter-spacing: 20px;<br \/>\n    text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);<br \/>\n  }<br \/>\n  .fx-mate {<br \/>\n    font-size: 96px;<br \/>\n    color: #8b0000;<br \/>\n    letter-spacing: 30px;<br \/>\n    text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);<br \/>\n  }<br \/>\n  &#064;keyframes fxFlash {<br \/>\n    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }<br \/>\n    30%  { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }<br \/>\n    70%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }<br \/>\n    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }<br \/>\n  }<br \/>\n  &#064;keyframes fxShake {<br \/>\n    0%, 100% { transform: translate(0, 0); }<br \/>\n    10%  { transform: translate(-6px, -4px); }<br \/>\n    20%  { transform: translate(5px, 3px); }<br \/>\n    30%  { transform: translate(-4px, 5px); }<br \/>\n    40%  { transform: translate(6px, -3px); }<br \/>\n    50%  { transform: translate(-5px, 4px); }<br \/>\n    60%  { transform: translate(3px, -5px); }<br \/>\n    70%  { transform: translate(-3px, 3px); }<br \/>\n    80%  { transform: translate(4px, -2px); }<br \/>\n    90%  { transform: translate(-2px, 2px); }<br \/>\n  }<br \/>\n  .shake { animation: fxShake 0.5s ease-in-out; }<br \/>\n&lt;\/style&gt;<br \/>\n&lt;\/head&gt;<br \/>\n&lt;body&gt;<br \/>\n  &lt;h1&gt;\u4e2d \u570b \u8c61 \u68cb&lt;\/h1&gt;<br \/>\n  &lt;div class&#061;&#034;info&#034;&gt;<br \/>\n    &lt;div class&#061;&#034;turn-indicator&#034; id&#061;&#034;turnIndicator&#034;&gt;\u7ea2\u65b9\u8d70\u68cb&lt;\/div&gt;<br \/>\n    &lt;button id&#061;&#034;restartBtn&#034;&gt;\u91cd \u65b0 \u5f00 \u59cb&lt;\/button&gt;<br \/>\n    &lt;button id&#061;&#034;undoBtn&#034;&gt;\u6094 \u68cb&lt;\/button&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;canvas-wrap&#034; id&#061;&#034;canvasWrap&#034;&gt;<br \/>\n    &lt;canvas id&#061;&#034;board&#034; width&#061;&#034;540&#034; height&#061;&#034;600&#034;&gt;&lt;\/canvas&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-check&#034; id&#061;&#034;fxCheck&#034;&gt;\u5c06 \u8ecd&lt;\/div&gt;<br \/>\n    &lt;div class&#061;&#034;fx-text fx-mate&#034; id&#061;&#034;fxMate&#034;&gt;\u7edd \u6bba&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;status&#034; id&#061;&#034;status&#034;&gt;&lt;\/div&gt;<br \/>\n  &lt;div class&#061;&#034;captured&#034;&gt;<br \/>\n    &lt;div&gt;\u7ea2\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByRed&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n    &lt;div&gt;\u9ed1\u65b9\u88ab\u5403 \u00b7 &lt;span id&#061;&#034;capturedByBlack&#034;&gt;&lt;\/span&gt;&lt;\/div&gt;<br \/>\n  &lt;\/div&gt;<\/p>\n<p>&lt;script&gt;<br \/>\nconst CELL &#061; 60;<br \/>\nconst MARGIN_X &#061; 30;<br \/>\nconst MARGIN_Y &#061; 30;<br \/>\nconst COLS &#061; 9;<br \/>\nconst ROWS &#061; 10;<\/p>\n<p>const canvas &#061; document.getElementById(&#039;board&#039;);<br \/>\nconst ctx &#061; canvas.getContext(&#039;2d&#039;);<br \/>\nconst canvasWrap &#061; document.getElementById(&#039;canvasWrap&#039;);<\/p>\n<p>const RED &#061; &#039;red&#039;;<br \/>\nconst BLACK &#061; &#039;black&#039;;<\/p>\n<p>const PIECES &#061; {<br \/>\n  red: {<br \/>\n    king: &#039;\u5e25&#039;, advisor: &#039;\u4ed5&#039;, elephant: &#039;\u76f8&#039;, horse: &#039;\u508c&#039;,<br \/>\n    chariot: &#039;\u4fe5&#039;, cannon: &#039;\u70ae&#039;, pawn: &#039;\u5175&#039;<br \/>\n  },<br \/>\n  black: {<br \/>\n    king: &#039;\u5c07&#039;, advisor: &#039;\u58eb&#039;, elephant: &#039;\u8c61&#039;, horse: &#039;\u99ac&#039;,<br \/>\n    chariot: &#039;\u8eca&#039;, cannon: &#039;\u7832&#039;, pawn: &#039;\u5352&#039;<br \/>\n  }<br \/>\n};<\/p>\n<p>let board &#061; [];<br \/>\nlet currentPlayer &#061; RED;<br \/>\nlet selected &#061; null;<br \/>\nlet legalMoves &#061; [];<br \/>\nlet gameOver &#061; false;<br \/>\nlet history &#061; [];<br \/>\nlet capturedByRed &#061; [];<br \/>\nlet capturedByBlack &#061; [];<\/p>\n<p>let effects &#061; [];<br \/>\nlet animating &#061; false;<br \/>\nlet checkAnimStart &#061; 0;<br \/>\nlet checkKingPos &#061; null;<br \/>\nlet movingPiece &#061; null;<\/p>\n<p>function P(type, color) {<br \/>\n  return { type, color };<br \/>\n}<\/p>\n<p>function initBoard() {<br \/>\n  board &#061; [<br \/>\n    [P(&#039;chariot&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;king&#039;,BLACK),P(&#039;advisor&#039;,BLACK),P(&#039;elephant&#039;,BLACK),P(&#039;horse&#039;,BLACK),P(&#039;chariot&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,P(&#039;cannon&#039;,BLACK),null,null,null,null,null,P(&#039;cannon&#039;,BLACK),null],<br \/>\n    [P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK),null,P(&#039;pawn&#039;,BLACK)],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED),null,P(&#039;pawn&#039;,RED)],<br \/>\n    [null,P(&#039;cannon&#039;,RED),null,null,null,null,null,P(&#039;cannon&#039;,RED),null],<br \/>\n    [null,null,null,null,null,null,null,null,null],<br \/>\n    [P(&#039;chariot&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;king&#039;,RED),P(&#039;advisor&#039;,RED),P(&#039;elephant&#039;,RED),P(&#039;horse&#039;,RED),P(&#039;chariot&#039;,RED)]<br \/>\n  ];<br \/>\n  currentPlayer &#061; RED;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  gameOver &#061; false;<br \/>\n  history &#061; [];<br \/>\n  capturedByRed &#061; [];<br \/>\n  capturedByBlack &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  movingPiece &#061; null;<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function isInBoard(r, c) {<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt; ROWS &amp;&amp; c &gt;&#061; 0 &amp;&amp; c &lt; COLS;<br \/>\n}<\/p>\n<p>function inPalace(r, c, color) {<br \/>\n  if (c &lt; 3 || c &gt; 5) return false;<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 7 &amp;&amp; r &lt;&#061; 9;<br \/>\n  return r &gt;&#061; 0 &amp;&amp; r &lt;&#061; 2;<br \/>\n}<\/p>\n<p>function onOwnSide(r, color) {<br \/>\n  if (color &#061;&#061;&#061; RED) return r &gt;&#061; 5;<br \/>\n  return r &lt;&#061; 4;<br \/>\n}<\/p>\n<p>function getLegalMoves(r, c) {<br \/>\n  const piece &#061; board[r][c];<br \/>\n  if (!piece) return [];<br \/>\n  const color &#061; piece.color;<br \/>\n  const type &#061; piece.type;<br \/>\n  const moves &#061; [];<br \/>\n  const addMove &#061; (nr, nc) &#061;&gt; {<br \/>\n    if (!isInBoard(nr, nc)) return;<br \/>\n    const target &#061; board[nr][nc];<br \/>\n    if (target &amp;&amp; target.color &#061;&#061;&#061; color) return;<br \/>\n    moves.push({ r: nr, c: nc, capture: !!target });<br \/>\n  };<\/p>\n<p>  switch (type) {<br \/>\n    case &#039;king&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;advisor&#039;: {<br \/>\n      const dirs &#061; [[-1,-1],[-1,1],[1,-1],[1,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        if (inPalace(nr, nc, color)) addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;elephant&#039;: {<br \/>\n      const dirs &#061; [[-2,-2],[-2,2],[2,-2],[2,2]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        const nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        const mr &#061; r &#043; dr\/2, mc &#061; c &#043; dc\/2;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (!onOwnSide(nr, color)) continue;<br \/>\n        if (board[mr][mc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;horse&#039;: {<br \/>\n      const hm &#061; [<br \/>\n        {dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},<br \/>\n        {dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},<br \/>\n        {dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},<br \/>\n        {dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}<br \/>\n      ];<br \/>\n      for (const m of hm) {<br \/>\n        const nr &#061; r &#043; m.dr, nc &#061; c &#043; m.dc;<br \/>\n        const lr &#061; r &#043; m.br, lc &#061; c &#043; m.bc;<br \/>\n        if (!isInBoard(nr, nc)) continue;<br \/>\n        if (board[lr][lc]) continue;<br \/>\n        addMove(nr, nc);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;chariot&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!target) {<br \/>\n            moves.push({ r: nr, c: nc, capture: false });<br \/>\n          } else {<br \/>\n            if (target.color !&#061;&#061; color) {<br \/>\n              moves.push({ r: nr, c: nc, capture: true });<br \/>\n            }<br \/>\n            break;<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;cannon&#039;: {<br \/>\n      const dirs &#061; [[-1,0],[1,0],[0,-1],[0,1]];<br \/>\n      for (const [dr, dc] of dirs) {<br \/>\n        let nr &#061; r &#043; dr, nc &#061; c &#043; dc;<br \/>\n        let jumped &#061; false;<br \/>\n        while (isInBoard(nr, nc)) {<br \/>\n          const target &#061; board[nr][nc];<br \/>\n          if (!jumped) {<br \/>\n            if (!target) {<br \/>\n              moves.push({ r: nr, c: nc, capture: false });<br \/>\n            } else {<br \/>\n              jumped &#061; true;<br \/>\n            }<br \/>\n          } else {<br \/>\n            if (target) {<br \/>\n              if (target.color !&#061;&#061; color) {<br \/>\n                moves.push({ r: nr, c: nc, capture: true });<br \/>\n              }<br \/>\n              break;<br \/>\n            }<br \/>\n          }<br \/>\n          nr &#043;&#061; dr; nc &#043;&#061; dc;<br \/>\n        }<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n    case &#039;pawn&#039;: {<br \/>\n      const forward &#061; color &#061;&#061;&#061; RED ? -1 : 1;<br \/>\n      const nr &#061; r &#043; forward;<br \/>\n      if (isInBoard(nr, c)) addMove(nr, c);<br \/>\n      if (!onOwnSide(r, color)) {<br \/>\n        if (isInBoard(r, c &#8211; 1)) addMove(r, c &#8211; 1);<br \/>\n        if (isInBoard(r, c &#043; 1)) addMove(r, c &#043; 1);<br \/>\n      }<br \/>\n      break;<br \/>\n    }<br \/>\n  }<br \/>\n  return moves;<br \/>\n}<\/p>\n<p>function findKing(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].type &#061;&#061;&#061; &#039;king&#039; &amp;&amp; board[r][c].color &#061;&#061;&#061; color) return { r, c };<br \/>\n    }<br \/>\n  }<br \/>\n  return null;<br \/>\n}<\/p>\n<p>function kingsFacing() {<br \/>\n  const rk &#061; findKing(RED);<br \/>\n  const bk &#061; findKing(BLACK);<br \/>\n  if (!rk || !bk || rk.c !&#061;&#061; bk.c) return false;<br \/>\n  const col &#061; rk.c;<br \/>\n  const minR &#061; Math.min(rk.r, bk.r);<br \/>\n  const maxR &#061; Math.max(rk.r, bk.r);<br \/>\n  for (let r &#061; minR &#043; 1; r &lt; maxR; r&#043;&#043;) {<br \/>\n    if (board[r][col]) return false;<br \/>\n  }<br \/>\n  return true;<br \/>\n}<\/p>\n<p>function getLegalMovesFiltered(r, c) {<br \/>\n  const moves &#061; getLegalMoves(r, c);<br \/>\n  return moves.filter(m &#061;&gt; {<br \/>\n    const captured &#061; board[m.r][m.c];<br \/>\n    const moving &#061; board[r][c];<br \/>\n    board[m.r][m.c] &#061; moving;<br \/>\n    board[r][c] &#061; null;<br \/>\n    const inCheck &#061; isKingInCheck(moving.color) || kingsFacing();<br \/>\n    board[r][c] &#061; moving;<br \/>\n    board[m.r][m.c] &#061; captured;<br \/>\n    return !inCheck;<br \/>\n  });<br \/>\n}<\/p>\n<p>function isKingInCheck(color) {<br \/>\n  const king &#061; findKing(color);<br \/>\n  if (!king) return true;<br \/>\n  const oppColor &#061; color &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; oppColor) {<br \/>\n        const moves &#061; getLegalMoves(r, c);<br \/>\n        if (moves.some(m &#061;&gt; m.r &#061;&#061;&#061; king.r &amp;&amp; m.c &#061;&#061;&#061; king.c)) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function hasAnyLegalMove(color) {<br \/>\n  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; board[r][c].color &#061;&#061;&#061; color) {<br \/>\n        if (getLegalMovesFiltered(r, c).length &gt; 0) return true;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n  return false;<br \/>\n}<\/p>\n<p>function drawBoard() {<br \/>\n  const grad &#061; ctx.createLinearGradient(0, 0, canvas.width, canvas.height);<br \/>\n  grad.addColorStop(0, &#039;#f8f4ec&#039;);<br \/>\n  grad.addColorStop(0.5, &#039;#f0ebe0&#039;);<br \/>\n  grad.addColorStop(1, &#039;#ede6d8&#039;);<br \/>\n  ctx.fillStyle &#061; grad;<br \/>\n  ctx.fillRect(0, 0, canvas.width, canvas.height);<\/p>\n<p>  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.5;<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(MARGIN_X, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; r * CELL);<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n    if (c &#061;&#061;&#061; 0 || c &#061;&#061;&#061; COLS &#8211; 1) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; (ROWS &#8211; 1) * CELL);<br \/>\n      ctx.stroke();<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n      ctx.stroke();<br \/>\n      ctx.beginPath();<br \/>\n      ctx.moveTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n      ctx.lineTo(MARGIN_X &#043; c * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n      ctx.stroke();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 4 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; (COLS &#8211; 1) * CELL, MARGIN_Y &#043; 5 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 2 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.moveTo(MARGIN_X &#043; 5 * CELL, MARGIN_Y &#043; 7 * CELL);<br \/>\n  ctx.lineTo(MARGIN_X &#043; 3 * CELL, MARGIN_Y &#043; 9 * CELL);<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#039;bold 26px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#039;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(&#039;\u695a \u6cb3&#039;, MARGIN_X &#043; 1.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<br \/>\n  ctx.fillText(&#039;\u6f22 \u754c&#039;, MARGIN_X &#043; 6.5 * CELL, MARGIN_Y &#043; 4.5 * CELL);<\/p>\n<p>  drawStarPoints();<br \/>\n}<\/p>\n<p>function drawStarPoints() {<br \/>\n  const points &#061; [<br \/>\n    [2,1],[2,7],[7,1],[7,7],<br \/>\n    [3,0],[3,2],[3,4],[3,6],[3,8],<br \/>\n    [6,0],[6,2],[6,4],[6,6],[6,8]<br \/>\n  ];<br \/>\n  for (const [r, c] of points) {<br \/>\n    drawStar(r, c);<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawStar(r, c) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  const s &#061; 6;<br \/>\n  const g &#061; 5;<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1.2;<br \/>\n  const dirs &#061; [];<br \/>\n  if (c &gt; 0) dirs.push([-1,-1],[-1,1]);<br \/>\n  if (c &lt; COLS &#8211; 1) dirs.push([1,-1],[1,1]);<br \/>\n  for (const [dx, dy] of dirs) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * (g &#043; s), y &#043; dy * g);<br \/>\n    ctx.stroke();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x &#043; dx * g, y &#043; dy * g);<br \/>\n    ctx.lineTo(x &#043; dx * g, y &#043; dy * (g &#043; s));<br \/>\n    ctx.stroke();<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawPiece(r, c, type, color, highlight) {<br \/>\n  const x &#061; MARGIN_X &#043; c * CELL;<br \/>\n  const y &#061; MARGIN_Y &#043; r * CELL;<br \/>\n  drawPieceAtXY(x, y, type, color, highlight, 1);<br \/>\n}<\/p>\n<p>function drawPieceAtXY(x, y, type, color, highlight, scale) {<br \/>\n  const radius &#061; 26 * scale;<\/p>\n<p>  if (highlight) {<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius &#043; 5, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#039;rgba(192, 57, 43, 0.25)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const inkGrad &#061; ctx.createRadialGradient(x &#8211; 3, y &#8211; 3, 2, x, y, radius);<br \/>\n  inkGrad.addColorStop(0, &#039;#e8e0d4&#039;);<br \/>\n  inkGrad.addColorStop(0.6, &#039;#d4c8b4&#039;);<br \/>\n  inkGrad.addColorStop(1, &#039;#b8a88c&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 2;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, radius &#8211; 4, 0, Math.PI * 2);<br \/>\n  ctx.strokeStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.lineWidth &#061; 1;<br \/>\n  ctx.stroke();<\/p>\n<p>  ctx.fillStyle &#061; color &#061;&#061;&#061; RED ? &#039;#c0392b&#039; : &#039;#1a1a1a&#039;;<br \/>\n  ctx.font &#061; &#096;bold ${28 * scale}px &#034;KaiTi&#034;, &#034;STKaiti&#034;, &#034;\u6977\u4f53&#034;, serif&#096;;<br \/>\n  ctx.textAlign &#061; &#039;center&#039;;<br \/>\n  ctx.textBaseline &#061; &#039;middle&#039;;<br \/>\n  ctx.fillText(PIECES[color][type], x, y &#043; 1);<br \/>\n}<\/p>\n<p>function draw() {<br \/>\n  drawBoard();<\/p>\n<p>  for (const move of legalMoves) {<br \/>\n    const x &#061; MARGIN_X &#043; move.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; move.r * CELL;<br \/>\n    if (move.capture) {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 30, 0, Math.PI * 2);<br \/>\n      ctx.strokeStyle &#061; &#039;rgba(192, 57, 43, 0.7)&#039;;<br \/>\n      ctx.lineWidth &#061; 2.5;<br \/>\n      ctx.setLineDash([4, 3]);<br \/>\n      ctx.stroke();<br \/>\n      ctx.setLineDash([]);<br \/>\n    } else {<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(x, y, 6, 0, Math.PI * 2);<br \/>\n      ctx.fillStyle &#061; &#039;rgba(26, 26, 26, 0.5)&#039;;<br \/>\n      ctx.fill();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  for (let r &#061; 0; r &lt; ROWS; r&#043;&#043;) {<br \/>\n    for (let c &#061; 0; c &lt; COLS; c&#043;&#043;) {<br \/>\n      if (board[r][c] &amp;&amp; (!movingPiece || (movingPiece.toR !&#061;&#061; r || movingPiece.toC !&#061;&#061; c) &amp;&amp; (movingPiece.fromR !&#061;&#061; r || movingPiece.fromC !&#061;&#061; c))) {<br \/>\n        const p &#061; board[r][c];<br \/>\n        drawPiece(r, c, p.type, p.color, selected &amp;&amp; selected.r &#061;&#061;&#061; r &amp;&amp; selected.c &#061;&#061;&#061; c);<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  if (movingPiece) {<br \/>\n    const now &#061; performance.now();<br \/>\n    const progress &#061; Math.min(1, (now &#8211; movingPiece.startTime) \/ movingPiece.duration);<br \/>\n    const easeProgress &#061; progress &lt; 0.5 ? 2 * progress * progress : 1 &#8211; Math.pow(-2 * progress &#043; 2, 2) \/ 2;<\/p>\n<p>    if (movingPiece.captured) {<br \/>\n      const tcx &#061; MARGIN_X &#043; movingPiece.toC * CELL;<br \/>\n      const tcy &#061; MARGIN_Y &#043; movingPiece.toR * CELL;<br \/>\n      const capFade &#061; Math.max(0, 1 &#8211; progress * 2.5);<br \/>\n      if (capFade &gt; 0) {<br \/>\n        ctx.save();<br \/>\n        ctx.globalAlpha &#061; capFade;<br \/>\n        drawPiece(movingPiece.toR, movingPiece.toC, movingPiece.captured.type, movingPiece.captured.color, false);<br \/>\n        ctx.restore();<br \/>\n      }<br \/>\n    }<\/p>\n<p>    const sx &#061; MARGIN_X &#043; movingPiece.fromC * CELL;<br \/>\n    const sy &#061; MARGIN_Y &#043; movingPiece.fromR * CELL;<br \/>\n    const tx &#061; MARGIN_X &#043; movingPiece.toC * CELL;<br \/>\n    const ty &#061; MARGIN_Y &#043; movingPiece.toR * CELL;<br \/>\n    const ix &#061; sx &#043; (tx &#8211; sx) * easeProgress;<br \/>\n    const iy &#061; sy &#043; (ty &#8211; sy) * easeProgress;<\/p>\n<p>    const angle &#061; Math.atan2(ty &#8211; sy, tx &#8211; sx);<br \/>\n    ctx.save();<br \/>\n    ctx.translate(ix, iy);<br \/>\n    ctx.rotate(angle * 0.15 * (1 &#8211; easeProgress));<br \/>\n    ctx.translate(-ix, -iy);<br \/>\n    drawPieceAtXY(ix, iy, movingPiece.piece.type, movingPiece.piece.color, false, 1 &#043; (1 &#8211; easeProgress) * 0.1);<br \/>\n    ctx.restore();<\/p>\n<p>    if (easeProgress &lt; 0.9) {<br \/>\n      const shadowGrad &#061; ctx.createRadialGradient(ix, iy &#043; 20, 0, ix, iy &#043; 20, 25);<br \/>\n      shadowGrad.addColorStop(0, &#039;rgba(0,0,0,0.25)&#039;);<br \/>\n      shadowGrad.addColorStop(1, &#039;rgba(0,0,0,0)&#039;);<br \/>\n      ctx.beginPath();<br \/>\n      ctx.arc(ix, iy &#043; 20, 25, 0, Math.PI * 2);<br \/>\n      ctx.fillStyle &#061; shadowGrad;<br \/>\n      ctx.fill();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const now &#061; performance.now();<\/p>\n<p>  if (redKing &amp;&amp; isKingInCheck(RED)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; RED || checkKingPos.r !&#061;&#061; redKing.r || checkKingPos.c !&#061;&#061; redKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: redKing.r, c: redKing.c, color: RED };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; redKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; redKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; RED) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<\/p>\n<p>  if (blackKing &amp;&amp; isKingInCheck(BLACK)) {<br \/>\n    if (!checkKingPos || checkKingPos.color !&#061;&#061; BLACK || checkKingPos.r !&#061;&#061; blackKing.r || checkKingPos.c !&#061;&#061; blackKing.c) {<br \/>\n      checkAnimStart &#061; now;<br \/>\n      checkKingPos &#061; { r: blackKing.r, c: blackKing.c, color: BLACK };<br \/>\n    }<br \/>\n    const elapsed &#061; now &#8211; checkAnimStart;<br \/>\n    const pulse &#061; 0.5 &#043; 0.5 * Math.sin(elapsed \/ 150);<br \/>\n    const x &#061; MARGIN_X &#043; blackKing.c * CELL;<br \/>\n    const y &#061; MARGIN_Y &#043; blackKing.r * CELL;<br \/>\n    const r &#061; 32 &#043; pulse * 8;<br \/>\n    const grad &#061; ctx.createRadialGradient(x, y, 20, x, y, r);<br \/>\n    grad.addColorStop(0, &#096;rgba(192, 57, 43, ${0.15 &#043; pulse * 0.25})&#096;);<br \/>\n    grad.addColorStop(0.7, &#096;rgba(192, 57, 43, ${0.3 &#043; pulse * 0.3})&#096;);<br \/>\n    grad.addColorStop(1, &#039;rgba(192, 57, 43, 0)&#039;);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, r, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; grad;<br \/>\n    ctx.fill();<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, 34 &#043; pulse * 4, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(192, 57, 43, ${0.6 &#043; pulse * 0.4})&#096;;<br \/>\n    ctx.lineWidth &#061; 2.5;<br \/>\n    ctx.stroke();<br \/>\n  } else {<br \/>\n    if (checkKingPos &amp;&amp; checkKingPos.color &#061;&#061;&#061; BLACK) { checkAnimStart &#061; 0; checkKingPos &#061; null; }<br \/>\n  }<br \/>\n}<\/p>\n<p>function drawEffects() {<br \/>\n  const now &#061; performance.now();<br \/>\n  const remaining &#061; [];<\/p>\n<p>  for (const e of effects) {<br \/>\n    const elapsed &#061; now &#8211; e.startTime;<br \/>\n    const progress &#061; elapsed \/ e.duration;<br \/>\n    if (progress &gt;&#061; 1) continue;<\/p>\n<p>    if (e.type &#061;&#061;&#061; &#039;capture&#039;) {<br \/>\n      drawCaptureEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;checkmate&#039;) {<br \/>\n      drawCheckmateEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    } else if (e.type &#061;&#061;&#061; &#039;inkTrail&#039;) {<br \/>\n      drawInkTrailEffect(e, progress);<br \/>\n      remaining.push(e);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  effects &#061; remaining;<br \/>\n}<\/p>\n<p>function drawCaptureEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<br \/>\n  const inkColor2 &#061; &#039;rgba(40, 40, 40,&#039;;<\/p>\n<p>  for (let i &#061; 0; i &lt; 3; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.08;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 10 &#043; pp * 55;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.8;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; inkColor2 &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.lineWidth &#061; 3 &#8211; pp * 2.5;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 14; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 14) * Math.PI * 2 &#043; p * 0.8;<br \/>\n    const dist &#061; p * 55;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 5 &#043; 1;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor2 &#043; ((1 &#8211; p) * 0.7).toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const coreGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 &#8211; p * 0.6));<br \/>\n  coreGrad.addColorStop(0, inkColor2 &#043; (0.5 * (1 &#8211; p)).toFixed(2) &#043; &#039;)&#039;);<br \/>\n  coreGrad.addColorStop(1, inkColor2 &#043; &#039;0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 30 * (1 &#8211; p * 0.6), 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; coreGrad;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function drawCheckmateEffect(e, p) {<br \/>\n  const x &#061; e.x, y &#061; e.y;<\/p>\n<p>  ctx.save();<br \/>\n  ctx.globalAlpha &#061; Math.min(1, p * 3);<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const delay &#061; i * 0.06;<br \/>\n    const pp &#061; Math.max(0, Math.min(1, (p &#8211; delay) \/ (1 &#8211; delay)));<br \/>\n    if (pp &lt;&#061; 0) continue;<br \/>\n    const radius &#061; 15 &#043; pp * 200;<br \/>\n    const alpha &#061; (1 &#8211; pp) * 0.6;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(x, y, radius, 0, Math.PI * 2);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${alpha.toFixed(2)})&#096;;<br \/>\n    ctx.lineWidth &#061; 4 &#8211; pp * 3;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 5) * Math.PI * 2 &#8211; Math.PI \/ 2;<br \/>\n    const dist &#061; Math.min(300, p * 600);<br \/>\n    ctx.beginPath();<br \/>\n    ctx.moveTo(x, y);<br \/>\n    ctx.lineTo(x &#043; Math.cos(angle) * dist, y &#043; Math.sin(angle) * dist);<br \/>\n    ctx.strokeStyle &#061; &#096;rgba(30, 30, 30, ${(1 &#8211; p) * 0.5})&#096;;<br \/>\n    ctx.lineWidth &#061; 2;<br \/>\n    ctx.stroke();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 30; i&#043;&#043;) {<br \/>\n    const angle &#061; (i \/ 30) * Math.PI * 2 &#043; p * 1.5;<br \/>\n    const dist &#061; p * 80 &#043; Math.sin(i * 3 &#043; p * 10) * 15;<br \/>\n    const px &#061; x &#043; Math.cos(angle) * dist;<br \/>\n    const py &#061; y &#043; Math.sin(angle) * dist;<br \/>\n    const size &#061; (1 &#8211; p) * 8 &#043; 2;<br \/>\n    const gray &#061; 30 &#043; (i % 3) * 25;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(px, py, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#096;rgba(${gray}, ${gray}, ${gray}, ${(1 &#8211; p) * 0.8})&#096;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const flashGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 &#8211; p));<br \/>\n  flashGrad.addColorStop(0, &#096;rgba(255, 255, 255, ${Math.max(0, (0.6 &#8211; p * 0.6))})&#096;);<br \/>\n  flashGrad.addColorStop(0.5, &#039;rgba(255, 255, 255, 0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 250, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; flashGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.restore();<br \/>\n}<\/p>\n<p>function easeInOut(t) {<br \/>\n  return t &lt; 0.5 ? 2 * t * t : 1 &#8211; Math.pow(-2 * t &#043; 2, 2) \/ 2;<br \/>\n}<\/p>\n<p>function drawInkTrailEffect(e, p) {<br \/>\n  const easeP &#061; easeInOut(Math.min(1, p));<br \/>\n  const x &#061; e.sx &#043; (e.dx &#8211; e.sx) * easeP;<br \/>\n  const y &#061; e.sy &#043; (e.dy &#8211; e.sy) * easeP;<br \/>\n  const inkColor &#061; &#039;rgba(40, 40, 40,&#039;;<br \/>\n  const dx &#061; e.dx &#8211; e.sx;<br \/>\n  const dy &#061; e.dy &#8211; e.sy;<br \/>\n  const angle &#061; Math.atan2(dy, dx);<br \/>\n  const splatters &#061; e.splatters;<\/p>\n<p>  for (let i &#061; 0; i &lt; 8; i&#043;&#043;) {<br \/>\n    const rawP &#061; Math.max(0, p &#8211; i * 0.035);<br \/>\n    const pp &#061; easeInOut(rawP);<br \/>\n    const tx &#061; e.sx &#043; dx * pp;<br \/>\n    const ty &#061; e.sy &#043; dy * pp;<br \/>\n    const alpha &#061; (1 &#8211; rawP) * 0.5;<br \/>\n    const size &#061; (1 &#8211; rawP) * 20 &#043; 4;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(tx, ty, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; 5; i&#043;&#043;) {<br \/>\n    const rawP &#061; Math.max(0, p &#8211; i * 0.06);<br \/>\n    const pp &#061; easeInOut(rawP);<br \/>\n    const tx &#061; e.sx &#043; dx * pp;<br \/>\n    const ty &#061; e.sy &#043; dy * pp;<br \/>\n    const spread &#061; 8 * (1 &#8211; rawP);<br \/>\n    const perpAngle &#061; angle &#043; Math.PI \/ 2;<br \/>\n    const offset &#061; (Math.sin(i * 1.7 &#043; rawP * 5) * spread);<br \/>\n    const ox &#061; tx &#043; Math.cos(perpAngle) * offset;<br \/>\n    const oy &#061; ty &#043; Math.sin(perpAngle) * offset;<br \/>\n    const size &#061; (1 &#8211; rawP) * 10 &#043; 2;<br \/>\n    const alpha &#061; (1 &#8211; rawP) * 0.35;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(ox, oy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; splatters.length; i&#043;&#043;) {<br \/>\n    const sp &#061; splatters[i];<br \/>\n    const progress &#061; (p &#8211; sp.delay) \/ (1 &#8211; sp.delay);<br \/>\n    if (progress &lt;&#061; 0 || progress &gt; 1) continue;<br \/>\n    const sx &#061; x &#043; Math.cos(sp.angle) * (progress * 35 &#043; 5) &#043; sp.offsetX * progress;<br \/>\n    const sy &#061; y &#043; Math.sin(sp.angle) * (progress * 35 &#043; 5) &#043; sp.offsetY * progress;<br \/>\n    const size &#061; (1 &#8211; progress) * sp.maxSize &#043; sp.minSize;<br \/>\n    const alpha &#061; (1 &#8211; progress) * sp.maxAlpha;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(sx, sy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; inkColor &#043; alpha.toFixed(2) &#043; &#039;)&#039;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  for (let i &#061; 0; i &lt; e.clusters.length; i&#043;&#043;) {<br \/>\n    const cl &#061; e.clusters[i];<br \/>\n    const progress &#061; (p &#8211; cl.delay) \/ (1 &#8211; cl.delay);<br \/>\n    if (progress &lt;&#061; 0 || progress &gt; 1) continue;<br \/>\n    const cx &#061; x &#043; Math.cos(cl.angle) * (progress * 40);<br \/>\n    const cy &#061; y &#043; Math.sin(cl.angle) * (progress * 40);<br \/>\n    const size &#061; (1 &#8211; progress) * cl.maxSize &#043; cl.minSize;<br \/>\n    const alpha &#061; (1 &#8211; progress) * cl.maxAlpha;<br \/>\n    ctx.beginPath();<br \/>\n    ctx.arc(cx, cy, size, 0, Math.PI * 2);<br \/>\n    ctx.fillStyle &#061; &#096;rgba(${cl.gray}, ${cl.gray}, ${cl.gray}, ${alpha.toFixed(2)})&#096;;<br \/>\n    ctx.fill();<br \/>\n  }<\/p>\n<p>  const headGrad &#061; ctx.createRadialGradient(x, y, 0, x, y, 30);<br \/>\n  headGrad.addColorStop(0, inkColor &#043; &#039;0.7)&#039;);<br \/>\n  headGrad.addColorStop(0.6, inkColor &#043; &#039;0.4)&#039;);<br \/>\n  headGrad.addColorStop(1, inkColor &#043; &#039;0)&#039;);<br \/>\n  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 30, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; headGrad;<br \/>\n  ctx.fill();<\/p>\n<p>  ctx.beginPath();<br \/>\n  ctx.arc(x, y, 22, 0, Math.PI * 2);<br \/>\n  ctx.fillStyle &#061; inkColor &#043; &#039;0.55)&#039;;<br \/>\n  ctx.fill();<br \/>\n}<\/p>\n<p>function animate() {<br \/>\n  if (movingPiece) {<br \/>\n    const now &#061; performance.now();<br \/>\n    if (now &#8211; movingPiece.startTime &gt;&#061; movingPiece.duration) {<br \/>\n      finalizeMove();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  draw();<br \/>\n  drawEffects();<\/p>\n<p>  if (movingPiece) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  const redKing &#061; findKing(RED);<br \/>\n  const blackKing &#061; findKing(BLACK);<br \/>\n  const redChecked &#061; redKing &amp;&amp; isKingInCheck(RED);<br \/>\n  const blackChecked &#061; blackKing &amp;&amp; isKingInCheck(BLACK);<\/p>\n<p>  if (effects.length &gt; 0 || redChecked || blackChecked) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  } else {<br \/>\n    animating &#061; false;<br \/>\n  }<br \/>\n}<\/p>\n<p>function triggerAnimation() {<br \/>\n  if (!animating) {<br \/>\n    animating &#061; true;<br \/>\n    requestAnimationFrame(animate);<br \/>\n  }<br \/>\n}<\/p>\n<p>function getXY(r, c) {<br \/>\n  return { x: MARGIN_X &#043; c * CELL, y: MARGIN_Y &#043; r * CELL };<br \/>\n}<\/p>\n<p>function addCaptureEffect(r, c, color) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;capture&#039;,<br \/>\n    x, y,<br \/>\n    color,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 800<br \/>\n  });<br \/>\n}<\/p>\n<p>function addInkTrailEffect(sr, sc, dr, dc, color, duration) {<br \/>\n  const { x: sx, y: sy } &#061; getXY(sr, sc);<br \/>\n  const { x: dx, y: dy } &#061; getXY(dr, dc);<br \/>\n  const moveAngle &#061; Math.atan2(dy &#8211; sy, dx &#8211; sx);<\/p>\n<p>  const splatters &#061; [];<br \/>\n  for (let i &#061; 0; i &lt; 12; i&#043;&#043;) {<br \/>\n    const spreadAngle &#061; moveAngle &#043; Math.PI &#043; (Math.random() &#8211; 0.5) * 1.8;<br \/>\n    splatters.push({<br \/>\n      angle: spreadAngle,<br \/>\n      delay: i * 0.07,<br \/>\n      offsetX: (Math.random() &#8211; 0.5) * 10,<br \/>\n      offsetY: (Math.random() &#8211; 0.5) * 10,<br \/>\n      maxSize: 6 &#043; Math.random() * 5,<br \/>\n      minSize: 1 &#043; Math.random() * 1.5,<br \/>\n      maxAlpha: 0.5 &#043; Math.random() * 0.3<br \/>\n    });<br \/>\n  }<\/p>\n<p>  const clusters &#061; [];<br \/>\n  for (let i &#061; 0; i &lt; 4; i&#043;&#043;) {<br \/>\n    const clusterAngle &#061; moveAngle &#043; Math.PI &#043; (i \/ 4) * 0.8 &#8211; 0.4 &#043; (Math.random() &#8211; 0.5) * 0.3;<br \/>\n    clusters.push({<br \/>\n      angle: clusterAngle,<br \/>\n      delay: i * 0.15,<br \/>\n      gray: 25 &#043; Math.floor(Math.random() * 30),<br \/>\n      maxSize: 4 &#043; Math.random() * 4,<br \/>\n      minSize: 0.8 &#043; Math.random() * 1.2,<br \/>\n      maxAlpha: 0.4 &#043; Math.random() * 0.3<br \/>\n    });<br \/>\n  }<\/p>\n<p>  effects.push({<br \/>\n    type: &#039;inkTrail&#039;,<br \/>\n    sx, sy, dx, dy,<br \/>\n    color,<br \/>\n    splatters,<br \/>\n    clusters,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: duration<br \/>\n  });<br \/>\n}<\/p>\n<p>function addCheckmateEffect(r, c) {<br \/>\n  const { x, y } &#061; getXY(r, c);<br \/>\n  effects.push({<br \/>\n    type: &#039;checkmate&#039;,<br \/>\n    x, y,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: 2000<br \/>\n  });<br \/>\n}<\/p>\n<p>function showFx(id, duration) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.offsetHeight;<br \/>\n  el.style.animation &#061; &#096;fxFlash ${duration}ms ease-out forwards&#096;;<br \/>\n}<\/p>\n<p>function hideFx(id) {<br \/>\n  const el &#061; document.getElementById(id);<br \/>\n  el.style.animation &#061; &#039;none&#039;;<br \/>\n  el.style.opacity &#061; &#039;0&#039;;<br \/>\n}<\/p>\n<p>function triggerShake() {<br \/>\n  canvasWrap.classList.remove(&#039;shake&#039;);<br \/>\n  void canvasWrap.offsetWidth;<br \/>\n  canvasWrap.classList.add(&#039;shake&#039;);<br \/>\n}<\/p>\n<p>canvas.addEventListener(&#039;click&#039;, (e) &#061;&gt; {<br \/>\n  if (gameOver || movingPiece) return;<br \/>\n  const rect &#061; canvas.getBoundingClientRect();<br \/>\n  const x &#061; (e.clientX &#8211; rect.left) * (canvas.width \/ rect.width);<br \/>\n  const y &#061; (e.clientY &#8211; rect.top) * (canvas.height \/ rect.height);<br \/>\n  const c &#061; Math.round((x &#8211; MARGIN_X) \/ CELL);<br \/>\n  const r &#061; Math.round((y &#8211; MARGIN_Y) \/ CELL);<br \/>\n  if (!isInBoard(r, c)) return;<\/p>\n<p>  const piece &#061; board[r][c];<\/p>\n<p>  if (selected) {<br \/>\n    const move &#061; legalMoves.find(m &#061;&gt; m.r &#061;&#061;&#061; r &amp;&amp; m.c &#061;&#061;&#061; c);<br \/>\n    if (move) {<br \/>\n      makeMove(selected.r, selected.c, r, c);<br \/>\n      selected &#061; null;<br \/>\n      legalMoves &#061; [];<br \/>\n      return;<br \/>\n    }<br \/>\n    if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n      selected &#061; { r, c };<br \/>\n      legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n      draw();<br \/>\n      return;<br \/>\n    }<br \/>\n    selected &#061; null;<br \/>\n    legalMoves &#061; [];<br \/>\n    draw();<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  if (piece &amp;&amp; piece.color &#061;&#061;&#061; currentPlayer) {<br \/>\n    selected &#061; { r, c };<br \/>\n    legalMoves &#061; getLegalMovesFiltered(r, c);<br \/>\n    draw();<br \/>\n  }<br \/>\n});<\/p>\n<p>function makeMove(fr, fc, tr, tc) {<br \/>\n  const moved &#061; board[fr][fc];<br \/>\n  const captured &#061; board[tr][tc];<br \/>\n  history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });<\/p>\n<p>  board[fr][fc] &#061; null;<br \/>\n  board[tr][tc] &#061; null;<\/p>\n<p>  const duration &#061; 380;<br \/>\n  movingPiece &#061; {<br \/>\n    piece: moved,<br \/>\n    fromR: fr, fromC: fc,<br \/>\n    toR: tr, toC: tc,<br \/>\n    captured: captured,<br \/>\n    player: currentPlayer,<br \/>\n    startTime: performance.now(),<br \/>\n    duration: duration<br \/>\n  };<\/p>\n<p>  addInkTrailEffect(fr, fc, tr, tc, moved.color, duration);<\/p>\n<p>  if (captured) {<br \/>\n    addCaptureEffect(tr, tc, moved.color);<br \/>\n    triggerShake();<br \/>\n  }<\/p>\n<p>  triggerAnimation();<br \/>\n}<\/p>\n<p>function finalizeMove() {<br \/>\n  if (!movingPiece) return;<br \/>\n  const { piece, toR, toC, captured, player } &#061; movingPiece;<br \/>\n  board[toR][toC] &#061; piece;<br \/>\n  movingPiece &#061; null;<\/p>\n<p>  if (captured) {<br \/>\n    if (player &#061;&#061;&#061; RED) capturedByRed.push(captured.type);<br \/>\n    else capturedByBlack.push(captured.type);<br \/>\n  }<br \/>\n  currentPlayer &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  updateUI();<br \/>\n  checkGameEnd();<br \/>\n}<\/p>\n<p>function undo() {<br \/>\n  if (movingPiece) {<br \/>\n    board[movingPiece.fromR][movingPiece.fromC] &#061; movingPiece.piece;<br \/>\n    board[movingPiece.toR][movingPiece.toC] &#061; movingPiece.captured;<br \/>\n    movingPiece &#061; null;<br \/>\n    history.pop();<br \/>\n    selected &#061; null;<br \/>\n    legalMoves &#061; [];<br \/>\n    effects &#061; [];<br \/>\n    checkAnimStart &#061; 0;<br \/>\n    checkKingPos &#061; null;<br \/>\n    updateUI();<br \/>\n    draw();<br \/>\n    return;<br \/>\n  }<br \/>\n  if (history.length &#061;&#061;&#061; 0) return;<br \/>\n  const last &#061; history.pop();<br \/>\n  board[last.fr][last.fc] &#061; last.moved;<br \/>\n  board[last.tr][last.tc] &#061; last.captured;<br \/>\n  if (last.captured) {<br \/>\n    if (last.player &#061;&#061;&#061; RED) capturedByRed.pop();<br \/>\n    else capturedByBlack.pop();<br \/>\n  }<br \/>\n  currentPlayer &#061; last.player;<br \/>\n  gameOver &#061; false;<br \/>\n  selected &#061; null;<br \/>\n  legalMoves &#061; [];<br \/>\n  effects &#061; [];<br \/>\n  checkAnimStart &#061; 0;<br \/>\n  checkKingPos &#061; null;<br \/>\n  hideFx(&#039;fxCheck&#039;);<br \/>\n  hideFx(&#039;fxMate&#039;);<br \/>\n  updateUI();<br \/>\n  draw();<br \/>\n}<\/p>\n<p>function checkGameEnd() {<br \/>\n  const opp &#061; currentPlayer &#061;&#061;&#061; RED ? BLACK : RED;<br \/>\n  const oppKing &#061; findKing(opp);<br \/>\n  const statusEl &#061; document.getElementById(&#039;status&#039;);<br \/>\n  if (!oppKing) {<br \/>\n    gameOver &#061; true;<br \/>\n    const winner &#061; currentPlayer &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;;<br \/>\n    statusEl.textContent &#061; winner &#043; &#039; \u83b7\u80dc \u00b7 \u5c06\u519b\u62bd\u5e05&#039;;<br \/>\n    addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);<br \/>\n    showFx(&#039;fxMate&#039;, 2000);<br \/>\n    triggerShake();<br \/>\n    return;<br \/>\n  }<br \/>\n  if (!hasAnyLegalMove(currentPlayer)) {<br \/>\n    gameOver &#061; true;<br \/>\n    const inCheck &#061; isKingInCheck(currentPlayer);<br \/>\n    if (inCheck) {<br \/>\n      statusEl.textContent &#061; &#039;\u7edd\u6740 \u00b7 &#039; &#043; (opp &#061;&#061;&#061; RED ? &#039;\u7ea2\u65b9&#039; : &#039;\u9ed1\u65b9&#039;) &#043; &#039; \u83b7\u80dc&#039;;<br \/>\n      const king &#061; findKing(currentPlayer);<br \/>\n      if (king) {<br \/>\n        addCheckmateEffect(king.r, king.c);<br \/>\n      }<br \/>\n      showFx(&#039;fxMate&#039;, 2000);<br \/>\n      triggerShake();<br \/>\n    } else {<br \/>\n      statusEl.textContent &#061; &#039;\u548c\u68cb \u00b7 \u56f0\u6bd9&#039;;<br \/>\n    }<br \/>\n    return;<br \/>\n  }<br \/>\n  if (isKingInCheck(currentPlayer)) {<br \/>\n    statusEl.textContent &#061; &#039;\u5c06 \u519b&#039;;<br \/>\n    showFx(&#039;fxCheck&#039;, 1500);<br \/>\n  } else {<br \/>\n    statusEl.textContent &#061; &#039;&#039;;<br \/>\n  }<br \/>\n}<\/p>\n<p>function updateUI() {<br \/>\n  const ind &#061; document.getElementById(&#039;turnIndicator&#039;);<br \/>\n  if (gameOver) {<br \/>\n    ind.textContent &#061; &#039;\u6e38\u620f\u7ed3\u675f&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator&#039;;<br \/>\n  } else if (currentPlayer &#061;&#061;&#061; RED) {<br \/>\n    ind.textContent &#061; &#039;\u7ea2\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-red&#039;;<br \/>\n  } else {<br \/>\n    ind.textContent &#061; &#039;\u9ed1\u65b9\u8d70\u68cb&#039;;<br \/>\n    ind.className &#061; &#039;turn-indicator turn-black&#039;;<br \/>\n  }<br \/>\n  document.getElementById(&#039;capturedByRed&#039;).textContent &#061; capturedByRed.map(p &#061;&gt; PIECES.black[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n  document.getElementById(&#039;capturedByBlack&#039;).textContent &#061; capturedByBlack.map(p &#061;&gt; PIECES.red[p] || &#039;?&#039;).join(&#039;&#039;);<br \/>\n}<\/p>\n<p>document.getElementById(&#039;restartBtn&#039;).addEventListener(&#039;click&#039;, initBoard);<br \/>\ndocument.getElementById(&#039;undoBtn&#039;).addEventListener(&#039;click&#039;, undo);<\/p>\n<p>initBoard();<br \/>\n&lt;\/script&gt;<br \/>\n&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n<p>\u4fdd\u5b58\u6210.txt\u540e\u7f00\u7c98\u8d34\u540e\u6539\u4e3a.html\u540e\u7f00\u5373\u53ef\u4f7f\u7528\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>1\u7248&#xff08;\u68cb\u5b50\u62d6\u5c3e\u8fc7\u4e8e\u5355\u8c03&#xff09;&#xff1a;<\/p>\n<p><title>\u4e2d\u56fd\u8c61\u68cb<\/title><br \/>\n* { box-sizing: border-box; margin: 0; padding: 0; }body {font-family: \\&#8221;KaiTi\\&#8221;, \\&#8221;STKaiti\\&#8221;, \\&#8221;\u6977\u4f53\\&#8221;, serif;background: <\/p>\n","protected":false},"author":2,"featured_media":97269,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[10336,567],"topic":[],"class_list":["post-97272","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-server","tag-10336","tag-html"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v20.3 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.wsisp.com\/helps\/97272.html\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\" \/>\n<meta property=\"og:description\" content=\"1\u7248&#xff08;\u68cb\u5b50\u62d6\u5c3e\u8fc7\u4e8e\u5355\u8c03&#xff09;&#xff1a;  \u4e2d\u56fd\u8c61\u68cb * { box-sizing: border-box; margin: 0; padding: 0; }body {font-family: &quot;KaiTi&quot;, &quot;STKaiti&quot;, &quot;\u6977\u4f53&quot;, serif;background:\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.wsisp.com\/helps\/97272.html\" \/>\n<meta property=\"og:site_name\" content=\"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-29T00:36:31+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.wsisp.com\/helps\/wp-content\/uploads\/2026\/08\/20260829003629-6a92298d6af15.png\" \/>\n<meta name=\"author\" content=\"admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u4f5c\u8005\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data2\" content=\"59 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/97272.html\",\"url\":\"https:\/\/www.wsisp.com\/helps\/97272.html\",\"name\":\"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\",\"isPartOf\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/#website\"},\"datePublished\":\"2026-08-29T00:36:31+00:00\",\"dateModified\":\"2026-08-29T00:36:31+00:00\",\"author\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/97272.html#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.wsisp.com\/helps\/97272.html\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/97272.html#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"\u9996\u9875\",\"item\":\"https:\/\/www.wsisp.com\/helps\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#website\",\"url\":\"https:\/\/www.wsisp.com\/helps\/\",\"name\":\"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\",\"description\":\"\u9999\u6e2f\u670d\u52a1\u5668_\u9999\u6e2f\u4e91\u670d\u52a1\u5668\u8d44\u8baf_\u670d\u52a1\u5668\u5e2e\u52a9\u6587\u6863_\u670d\u52a1\u5668\u6559\u7a0b\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.wsisp.com\/helps\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41\",\"name\":\"admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery\",\"contentUrl\":\"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery\",\"caption\":\"admin\"},\"sameAs\":[\"http:\/\/wp.wsisp.com\"],\"url\":\"https:\/\/www.wsisp.com\/helps\/author\/admin\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.wsisp.com\/helps\/97272.html","og_locale":"zh_CN","og_type":"article","og_title":"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","og_description":"1\u7248&#xff08;\u68cb\u5b50\u62d6\u5c3e\u8fc7\u4e8e\u5355\u8c03&#xff09;&#xff1a;  \u4e2d\u56fd\u8c61\u68cb * { box-sizing: border-box; margin: 0; padding: 0; }body {font-family: \"KaiTi\", \"STKaiti\", \"\u6977\u4f53\", serif;background:","og_url":"https:\/\/www.wsisp.com\/helps\/97272.html","og_site_name":"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","article_published_time":"2026-08-29T00:36:31+00:00","og_image":[{"url":"https:\/\/www.wsisp.com\/helps\/wp-content\/uploads\/2026\/08\/20260829003629-6a92298d6af15.png"}],"author":"admin","twitter_card":"summary_large_image","twitter_misc":{"\u4f5c\u8005":"admin","\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"59 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.wsisp.com\/helps\/97272.html","url":"https:\/\/www.wsisp.com\/helps\/97272.html","name":"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","isPartOf":{"@id":"https:\/\/www.wsisp.com\/helps\/#website"},"datePublished":"2026-08-29T00:36:31+00:00","dateModified":"2026-08-29T00:36:31+00:00","author":{"@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41"},"breadcrumb":{"@id":"https:\/\/www.wsisp.com\/helps\/97272.html#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.wsisp.com\/helps\/97272.html"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.wsisp.com\/helps\/97272.html#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"\u9996\u9875","item":"https:\/\/www.wsisp.com\/helps"},{"@type":"ListItem","position":2,"name":"\u4e2d\u56fd\u8c61\u68cb\u7f51\u9875\u7248"}]},{"@type":"WebSite","@id":"https:\/\/www.wsisp.com\/helps\/#website","url":"https:\/\/www.wsisp.com\/helps\/","name":"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","description":"\u9999\u6e2f\u670d\u52a1\u5668_\u9999\u6e2f\u4e91\u670d\u52a1\u5668\u8d44\u8baf_\u670d\u52a1\u5668\u5e2e\u52a9\u6587\u6863_\u670d\u52a1\u5668\u6559\u7a0b","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.wsisp.com\/helps\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"zh-Hans"},{"@type":"Person","@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41","name":"admin","image":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/image\/","url":"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery","contentUrl":"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery","caption":"admin"},"sameAs":["http:\/\/wp.wsisp.com"],"url":"https:\/\/www.wsisp.com\/helps\/author\/admin"}]}},"_links":{"self":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts\/97272","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/comments?post=97272"}],"version-history":[{"count":0,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts\/97272\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/media\/97269"}],"wp:attachment":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/media?parent=97272"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/categories?post=97272"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/tags?post=97272"},{"taxonomy":"topic","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/topic?post=97272"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}