首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Gemini 3 打造一个中国象棋 Web 版:从 UI 到 AlphaBeta

Gemini 3 打造一个中国象棋 Web 版:从 UI 到 AlphaBeta

作者头像
井九
发布2025-11-22 08:39:45
发布2025-11-22 08:39:45
450
举报
文章被收录于专栏:四楼没电梯四楼没电梯

项目背景:为什么用 Gemini 3 开发中国象棋

开发一个能下中国象棋的 Web 组件,需要同时处理:

  • UI 绘制(棋盘、棋子、选中态…)
  • 棋规(马腿、过河、将不能照面…)
  • 走法生成器(大量条件判断)
  • 游戏状态管理(撤销/重做)
  • AI 搜索(AlphaBeta)
  • 性能处理(剪枝、评估优化)

Gemini 3 在这里展现出巨大价值:

✔ 自动生成复杂走法规则

比如“炮的走法 + 障碍判断 + 打炮规则”,一句话即可生成稳定代码。

✔ 能从自然语言生成结构化逻辑

例如 “使用 Negamax 的 AlphaBeta 并加入剪枝,保持纯函数风格” Gemini 3 会输出与整份组件完全一致的代码风格。

在这里插入图片描述
在这里插入图片描述

React 单文件组件结构总览

我们采用典型的 React SFC(一个 .jsx 文件) 结构:

代码语言:javascript
复制
export default function Xiangqi() {
  const canvasRef = useRef(null);
  const [board, setBoard] = useState(initBoard());
  const [selected, setSelected] = useState(null);

  useEffect(() => { draw(); }, [board]);

  const draw = () => {};
  const onClick = (e) => {};
  const generateMoves = () => {};
  const evaluate = () => {};
  const alphaBeta = () => {};

  return <canvas ref={canvasRef} onClick={onClick} width={540} height={600} />;
}

所有棋规、走法生成、AI 搜索全部在这个组件内部。

这使得 demo 易于复制,也利于 Gemini 3 自动协助扩展。


棋盘 UI

使用 Canvas:

代码语言:javascript
复制
const drawBoard = (ctx) => {
  ctx.strokeStyle = "#333";
  for (let i = 0; i < 10; i++) {
    ctx.beginPath();
    ctx.moveTo(0, i * grid);
    ctx.lineTo(8 * grid, i * grid);
    ctx.stroke();
  }
  for (let i = 0; i < 9; i++) {
    ctx.beginPath();
    ctx.moveTo(i * grid, 0);
    ctx.lineTo(i * grid, 9 * grid);
    ctx.stroke();
  }
};

绘制棋子:

代码语言:javascript
复制
const drawPiece = (ctx, p, x, y) => {
  ctx.beginPath();
  ctx.arc(x, y, 22, 0, Math.PI * 2);
  ctx.fillStyle = p.side === "r" ? "#fee" : "#eef";
  ctx.fill();
  ctx.stroke();
  ctx.fillStyle = p.side === "r" ? "#b00" : "#008";
  ctx.font = "20px serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText(p.char, x, y);
};

棋子规则

每个棋子在自己的方法中生成合法走法,例如:

马(含“蹩马腿”)
代码语言:javascript
复制
const horseMoves = (x, y) => {
  const moves = [];
  const legs = [
    { b: [0, -1], m: [-1, -2] },
    { b: [0, -1], m: [1, -2] },
    { b: [0, 1], m: [-1, 2] },
    { b: [0, 1], m: [1, 2] },
    { b: [-1, 0], m: [-2, -1] },
    { b: [-1, 0], m: [-2, 1] },
    { b: [1, 0], m: [2, -1] },
    { b: [1, 0], m: [2, 1] },
  ];

  legs.forEach(({ b, m }) => {
    const [bx, by] = [x + b[0], y + b[1]];
    if (board[bx]?.[by]) return; // 蹩马腿

    const [nx, ny] = [x + m[0], y + m[1]];
    if (isInside(nx, ny) && !sameSide(board[nx][ny], board[x][y])) {
      moves.push([x, y, nx, ny]);
    }
  });

  return moves;
};

Gemini 3 在这里最常用: 它能按你的组件结构写 rule method,不会写出“独立函数无法访问 board”之类的问题。


评估函数

为了能在浏览器中轻量运行:

代码语言:javascript
复制
const evaluate = (side) => {
  let score = 0;
  for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 9; j++) {
      const p = board[i][j];
      if (!p) continue;
      const v = value[p.id] + pst[p.id][i][j];
      score += p.side === side ? v : -v;
    }
  }
  return score;
};

React 组件内部 state 不会在搜索期间更新。


AlphaBeta 搜索

使用 Negamax + AlphaBeta

代码语言:javascript
复制
const alphaBeta = (depth, alpha, beta, side) => {
  if (depth === 0) return evaluate(side);

  const moves = generateMoves(side);

  for (const mv of moves) {
    const snap = applyMove(mv);

    const score = -alphaBeta(depth - 1, -beta, -alpha, side === "r" ? "b" : "r");

    undoMove(snap);

    if (score >= beta) return beta;
    if (score > alpha) alpha = score;
  }

  return alpha;
};

React 的单文件组件结构非常适合 Negamax,因为:

  • 所有状态在组件内部
  • 无需全局变量
  • apply/undo 极易维护
  • Gemini 3 能自动生成一致风格的递归逻辑

Gemini 3 如何提升开发效率

1. 生成棋规时极准

例如:

“写一个炮的走法(含打炮规则),按我文件中的方法结构生成。”

Gemini 会:

  • 保持 React 代码风格
  • 使用同样格式返回走法
  • UI / AI / state 都不会乱改
2. 搜索逻辑不会写错

AlphaBeta + Negamax 极易出 bug:

  • 评估符号错
  • undoMove 漏掉
  • alpha 和 beta 混淆
  • side 翻转错误

Gemini 3 写出来的 90% 是可立即运行的。

3. 能在单个组件中维护一致性

你不用拆文件,给它整个 JSX, 它能在 正确的位置 修改你的 methods。


完整代码

代码语言:javascript
复制
import React, { useState, useEffect, useRef } from 'react';
import { RotateCcw, Trophy, Cpu, ArrowRightLeft, Bot } from 'lucide-react';

// ==========================================
//  1. Constants & Config (全局常量)
// ==========================================

// 棋盘尺寸
const BOARD_ROWS = 10;
const BOARD_COLS = 9;

// 坐标系统 (必须在 SVG 和 Game 组件之前定义)
const CELL_SIZE = 50; 
const MARGIN = 30;
const BOARD_WIDTH = 8 * CELL_SIZE;
const BOARD_HEIGHT = 9 * CELL_SIZE;
const SVG_WIDTH = BOARD_WIDTH + MARGIN * 2;
const SVG_HEIGHT = BOARD_HEIGHT + MARGIN * 2;

const TYPES = {
  GENERAL: 'general', ADVISOR: 'advisor', ELEPHANT: 'elephant',
  HORSE: 'horse', CHARIOT: 'chariot', CANNON: 'cannon', SOLDIER: 'soldier'
};

const COLORS = { RED: 'red', BLACK: 'black' };

// UI 显示字符映射
const PIECE_CHARS = {
  red: { [TYPES.GENERAL]: '帅', [TYPES.ADVISOR]: '仕', [TYPES.ELEPHANT]: '相', [TYPES.HORSE]: '马', [TYPES.CHARIOT]: '车', [TYPES.CANNON]: '炮', [TYPES.SOLDIER]: '兵' },
  black: { [TYPES.GENERAL]: '将', [TYPES.ADVISOR]: '士', [TYPES.ELEPHANT]: '象', [TYPES.HORSE]: '马', [TYPES.CHARIOT]: '车', [TYPES.CANNON]: '炮', [TYPES.SOLDIER]: '卒' }
};

// ==========================================
//  2. Engine Constants (引擎常量)
// ==========================================

const R_KING = 8, R_ADV = 9, R_ELE = 10, R_HORSE = 11, R_ROOK = 12, R_CANNON = 13, R_PAWN = 14;
const B_KING = 16, B_ADV = 17, B_ELE = 18, B_HORSE = 19, B_ROOK = 20, B_CANNON = 21, B_PAWN = 22;

const COLOR_RED = 8;
const COLOR_BLACK = 16;

// 棋子价值表
const VAL = {
  0: 0,
  [R_KING]: 15000, [R_ADV]: 20, [R_ELE]: 20, [R_HORSE]: 400, [R_ROOK]: 900, [R_CANNON]: 450, [R_PAWN]: 10,
  [B_KING]: 15000, [B_ADV]: 20, [B_ELE]: 20, [B_HORSE]: 400, [B_ROOK]: 900, [B_CANNON]: 450, [B_PAWN]: 10
};

// Zobrist Hashing 初始化
const ZOBRIST = new Uint32Array(23 * 90);
const ZOBRIST_SIDE = Math.random() * 0xFFFFFFFF >>> 0;
for(let i=0; i<ZOBRIST.length; i++) ZOBRIST[i] = Math.random() * 0xFFFFFFFF >>> 0;

// 位运算辅助
const SRC = (move) => (move >> 8) & 0xFF;
const DST = (move) => move & 0xFF;
const MOVE = (src, dst) => (src << 8) | dst;
const TT_TYPE = { EXACT: 0, LOWERBOUND: 1, UPPERBOUND: 2 };

// ==========================================
//  3. Engine Logic (AI 核心逻辑)
// ==========================================

class Engine {
  constructor() {
    this.board = new Int8Array(90);
    this.color = COLOR_RED;
    this.tt = new Map();
    this.killer = new Array(20).fill(0).map(() => [0, 0]);
    this.historyHeuristic = new Int32Array(90 * 90);
    this.nodes = 0;
    this.startTime = 0;
    this.timeLimit = 1000;
    this.stop = false;
  }

  loadBoard(uiBoard, turnColor) {
    this.board.fill(0);
    for(let r=0; r<10; r++) {
      for(let c=0; c<9; c++) {
        const cell = uiBoard[r][c];
        if(cell) {
           let p = 0;
           if(cell.color === 'red') {
             if(cell.type === 'general') p = R_KING;
             if(cell.type === 'advisor') p = R_ADV;
             if(cell.type === 'elephant') p = R_ELE;
             if(cell.type === 'horse') p = R_HORSE;
             if(cell.type === 'chariot') p = R_ROOK;
             if(cell.type === 'cannon') p = R_CANNON;
             if(cell.type === 'soldier') p = R_PAWN;
           } else {
             if(cell.type === 'general') p = B_KING;
             if(cell.type === 'advisor') p = B_ADV;
             if(cell.type === 'elephant') p = B_ELE;
             if(cell.type === 'horse') p = B_HORSE;
             if(cell.type === 'chariot') p = B_ROOK;
             if(cell.type === 'cannon') p = B_CANNON;
             if(cell.type === 'soldier') p = B_PAWN;
           }
           this.board[r*9 + c] = p;
        }
      }
    }
    this.color = turnColor === 'red' ? COLOR_RED : COLOR_BLACK;
  }

  getHash() {
    let h = 0;
    if (this.color === COLOR_BLACK) h ^= ZOBRIST_SIDE;
    for(let i=0; i<90; i++) {
      if(this.board[i]) h ^= ZOBRIST[this.board[i] * 90 + i];
    }
    return h >>> 0;
  }

  generateMoves(onlyCaptures = false) {
    const moves = [];
    const selfColor = this.color;
    const enemyColor = selfColor === COLOR_RED ? COLOR_BLACK : COLOR_RED;
    
    for (let src = 0; src < 90; src++) {
      const p = this.board[src];
      if (!p || (p & selfColor) === 0) continue;

      const type = p;
      const r = (src / 9) | 0;
      const c = src % 9;

      const add = (dst) => {
        const target = this.board[dst];
        if (target === 0) {
          if (!onlyCaptures) moves.push(MOVE(src, dst));
        } else if (target & enemyColor) {
          moves.push(MOVE(src, dst));
        }
      };

      if (type === R_ROOK || type === B_ROOK || type === R_CANNON || type === B_CANNON) {
        const dirs = [-1, 1, -9, 9]; 
        for (let d of dirs) {
          let dst = src + d;
          let jump = false;
          while (dst >= 0 && dst < 90) {
             if (Math.abs((dst % 9) - ((dst - d) % 9)) > 1) break;
             const target = this.board[dst];
             if (type === R_ROOK || type === B_ROOK) {
               if (!target) { if(!onlyCaptures) moves.push(MOVE(src, dst)); }
               else {
                 if (target & enemyColor) moves.push(MOVE(src, dst));
                 break;
               }
             } else {
               if (!target) {
                 if (!jump && !onlyCaptures) moves.push(MOVE(src, dst));
               } else {
                 if (jump) {
                   if (target & enemyColor) moves.push(MOVE(src, dst));
                   break;
                 }
                 jump = true;
               }
             }
             dst += d;
          }
        }
      }
      else if (type === R_HORSE || type === B_HORSE) {
        const horseMoves = [
          {d: -19, l: -9, r: r-2, c: c-1}, {d: -17, l: -9, r: r-2, c: c+1},
          {d: 17, l: 9, r: r+2, c: c-1}, {d: 19, l: 9, r: r+2, c: c+1},
          {d: -11, l: -1, r: r-1, c: c-2}, {d: 7, l: -1, r: r+1, c: c-2},
          {d: -7, l: 1, r: r-1, c: c+2}, {d: 11, l: 1, r: r+1, c: c+2}
        ];
        for(let m of horseMoves) {
          if (m.r >= 0 && m.r < 10 && m.c >= 0 && m.c < 9) {
             if (this.board[src + m.l] === 0) add(src + m.d);
          }
        }
      }
      else if (type === R_ELE || type === B_ELE) {
        const offsets = [
          {d: -20, eye: -10, r: r-2, c: c-2}, {d: -16, eye: -8, r: r-2, c: c+2},
          {d: 16, eye: 8, r: r+2, c: c-2}, {d: 20, eye: 10, r: r+2, c: c+2}
        ];
        for(let m of offsets) {
           if (m.r >= 0 && m.r < 10 && m.c >= 0 && m.c < 9) {
             if (selfColor === COLOR_RED && m.r < 5) continue;
             if (selfColor === COLOR_BLACK && m.r > 4) continue;
             if (this.board[src + m.eye] === 0) add(src + m.d);
           }
        }
      }
      else if (type === R_ADV || type === B_ADV) {
         const offsets = [-10, -8, 8, 10];
         for(let o of offsets) {
           const dst = src + o;
           const dr = (dst/9|0), dc = dst%9;
           if (dc < 3 || dc > 5) continue;
           if (selfColor === COLOR_RED && dr < 7) continue;
           if (selfColor === COLOR_BLACK && dr > 2) continue;
           add(dst);
         }
      }
      else if (type === R_KING || type === B_KING) {
         const offsets = [-9, 9, -1, 1];
         for(let o of offsets) {
            const dst = src + o;
            if (Math.abs((dst % 9) - c) > 1) continue;
            const dr = (dst/9|0), dc = dst%9;
            if (dc < 3 || dc > 5) continue;
            if (selfColor === COLOR_RED && dr < 7) continue;
            if (selfColor === COLOR_BLACK && dr > 2) continue;
            add(dst);
         }
      }
      else if (type === R_PAWN || type === B_PAWN) {
         const forward = selfColor === COLOR_RED ? -9 : 9;
         const rDst = r + (selfColor === COLOR_RED ? -1 : 1);
         if (rDst >= 0 && rDst < 10) add(src + forward);
         
         const crossed = selfColor === COLOR_RED ? r < 5 : r > 4;
         if (crossed) {
            if (c > 0) add(src - 1);
            if (c < 8) add(src + 1);
         }
      }
    }
    return moves;
  }

  evaluate() {
    let score = 0;
    for(let i=0; i<90; i++) {
       const p = this.board[i];
       if(p) {
         let val = VAL[p];
         const c = i % 9;
         if (c === 4) val += 5; // Center control bonus
         score += (p & COLOR_RED) ? val : -val;
       }
    }
    return this.color === COLOR_RED ? score : -score;
  }

  makeMove(move) {
    const src = SRC(move);
    const dst = DST(move);
    const capture = this.board[dst];
    const piece = this.board[src];
    
    this.board[dst] = piece;
    this.board[src] = 0;
    this.color = this.color === COLOR_RED ? COLOR_BLACK : COLOR_RED;
    
    return { move, capture };
  }

  undoMove(undo) {
    const src = SRC(undo.move);
    const dst = DST(undo.move);
    this.board[src] = this.board[dst];
    this.board[dst] = undo.capture;
    this.color = this.color === COLOR_RED ? COLOR_BLACK : COLOR_RED;
  }

  orderMoves(moves, bestMove, ply) {
    return moves.map(m => {
      let score = 0;
      if (m === bestMove) score = 2000000;
      else {
         const capture = this.board[DST(m)];
         if (capture) score = 100000 + VAL[capture] - VAL[this.board[SRC(m)]];
         else {
           if (this.killer[ply] && (this.killer[ply][0] === m || this.killer[ply][1] === m)) score += 900;
           score += this.historyHeuristic[m];
         }
      }
      return { m, score };
    }).sort((a, b) => b.score - a.score).map(o => o.m);
  }

  alphabeta(depth, alpha, beta, ply) {
    this.nodes++;
    if ((this.nodes & 2047) === 0) {
      if (Date.now() - this.startTime > this.timeLimit) this.stop = true;
    }
    if (this.stop) return 0;

    const h = this.getHash();
    const ttEntry = this.tt.get(h);
    if (ttEntry && ttEntry.depth >= depth) {
       if (ttEntry.flag === 0) return ttEntry.score;
       if (ttEntry.flag === 1 && ttEntry.score <= alpha) return alpha;
       if (ttEntry.flag === 2 && ttEntry.score >= beta) return beta;
    }

    if (depth <= 0) return this.quiescence(alpha, beta);

    let moves = this.generateMoves();
    if (moves.length === 0) return -20000 + ply;

    const bestMoveStored = ttEntry ? ttEntry.move : null;
    moves = this.orderMoves(moves, bestMoveStored, ply);

    let bestMove = 0;
    let flag = 1; 

    for (let i = 0; i < moves.length; i++) {
      const mv = moves[i];
      const undo = this.makeMove(mv);
      
      let score;
      if (i === 0) {
        score = -this.alphabeta(depth - 1, -beta, -alpha, ply + 1);
      } else {
        score = -this.alphabeta(depth - 1, -alpha - 1, -alpha, ply + 1);
        if (score > alpha && score < beta) {
           score = -this.alphabeta(depth - 1, -beta, -alpha, ply + 1);
        }
      }
      
      this.undoMove(undo);
      
      if (this.stop) return 0;

      if (score >= beta) {
        if (!undo.capture) {
           this.killer[ply][1] = this.killer[ply][0];
           this.killer[ply][0] = mv;
           this.historyHeuristic[mv] += depth * depth;
        }
        this.tt.set(h, { score: beta, flag: 2, depth, move: mv });
        return beta; 
      }
      if (score > alpha) {
        alpha = score;
        bestMove = mv;
        flag = 0;
      }
    }
    
    this.tt.set(h, { score: alpha, flag, depth, move: bestMove });
    return alpha;
  }

  quiescence(alpha, beta) {
    this.nodes++;
    const val = this.evaluate();
    if (val >= beta) return beta;
    if (val > alpha) alpha = val;
    
    const moves = this.generateMoves(true); 
    moves.sort((a,b) => {
       const va = VAL[this.board[DST(a)]] - VAL[this.board[SRC(a)]];
       const vb = VAL[this.board[DST(b)]] - VAL[this.board[SRC(b)]];
       return vb - va;
    });

    for(let mv of moves) {
      const undo = this.makeMove(mv);
      const score = -this.quiescence(-beta, -alpha);
      this.undoMove(undo);
      if (score >= beta) return beta;
      if (score > alpha) alpha = score;
    }
    return alpha;
  }
  
  search(depth) {
    this.stop = false;
    this.startTime = Date.now();
    this.nodes = 0;
    let bestMove = 0;
    for(let d=1; d<=depth; d++) {
       const score = this.alphabeta(d, -30000, 30000, 0);
       if (this.stop) break;
       const entry = this.tt.get(this.getHash());
       if (entry && entry.move) bestMove = entry.move;
       console.log(`Depth ${d}: Score ${score}, Nodes ${this.nodes}`);
    }
    return bestMove;
  }
}

// ==========================================
//  4. UI Helpers (UI 辅助函数)
// ==========================================

const initialBoardSetup = () => {
  const board = Array(BOARD_ROWS).fill(null).map(() => Array(BOARD_COLS).fill(null));
  const setupRow = (row, color, pieces) => pieces.forEach((type, col) => { if (type) board[row][col] = { type, color }; });
  setupRow(0, COLORS.BLACK, [TYPES.CHARIOT, TYPES.HORSE, TYPES.ELEPHANT, TYPES.ADVISOR, TYPES.GENERAL, TYPES.ADVISOR, TYPES.ELEPHANT, TYPES.HORSE, TYPES.CHARIOT]);
  setupRow(2, COLORS.BLACK, [null, TYPES.CANNON, null, null, null, null, null, TYPES.CANNON, null]);
  setupRow(3, COLORS.BLACK, [TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER]);
  setupRow(9, COLORS.RED, [TYPES.CHARIOT, TYPES.HORSE, TYPES.ELEPHANT, TYPES.ADVISOR, TYPES.GENERAL, TYPES.ADVISOR, TYPES.ELEPHANT, TYPES.HORSE, TYPES.CHARIOT]);
  setupRow(7, COLORS.RED, [null, TYPES.CANNON, null, null, null, null, null, TYPES.CANNON, null]);
  setupRow(6, COLORS.RED, [TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER]);
  return board;
};

const XiangqiBoardSVG = ({ flipped }) => {
  const x = (col) => MARGIN + col * CELL_SIZE;
  const y = (row) => MARGIN + row * CELL_SIZE;
  const CornerMark = ({ r, c }) => {
    const cx = x(c), cy = y(r), len = 10, gap = 4;
    const paths = [];
    if (c > 0) paths.push(`M ${cx-gap-len} ${cy-gap} L ${cx-gap} ${cy-gap} L ${cx-gap} ${cy-gap-len} M ${cx-gap-len} ${cy+gap} L ${cx-gap} ${cy+gap} L ${cx-gap} ${cy+gap+len}`);
    if (c < 8) paths.push(`M ${cx+gap} ${cy-gap-len} L ${cx+gap} ${cy-gap} L ${cx+gap+len} ${cy-gap} M ${cx+gap} ${cy+gap+len} L ${cx+gap} ${cy+gap} L ${cx+gap+len} ${cy+gap}`);
    return <path d={paths.join(' ')} stroke="#8B4513" strokeWidth="2" fill="none" />;
  };
  return (
    <svg width="100%" height="100%" viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`} className="absolute top-0 left-0 pointer-events-none select-none">
      <rect width={SVG_WIDTH} height={SVG_HEIGHT} fill="#DEB887" />
      <rect x={MARGIN - 4} y={MARGIN - 4} width={BOARD_WIDTH + 8} height={BOARD_HEIGHT + 8} stroke="#8B4513" strokeWidth="3" fill="none" />
      {Array.from({ length: 10 }).map((_, i) => <line key={`h-${i}`} x1={x(0)} y1={y(i)} x2={x(8)} y2={y(i)} stroke="#8B4513" strokeWidth="1.5" />)}
      {Array.from({ length: 9 }).map((_, i) => {
        if (i === 0 || i === 8) return <line key={`v-${i}`} x1={x(i)} y1={y(0)} x2={x(i)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" />;
        return <g key={`v-${i}`}><line x1={x(i)} y1={y(0)} x2={x(i)} y2={y(4)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(i)} y1={y(5)} x2={x(i)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" /></g>;
      })}
      <line x1={x(3)} y1={y(0)} x2={x(5)} y2={y(2)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(5)} y1={y(0)} x2={x(3)} y2={y(2)} stroke="#8B4513" strokeWidth="1.5" />
      <line x1={x(3)} y1={y(7)} x2={x(5)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(5)} y1={y(7)} x2={x(3)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" />
      <g transform={flipped ? `rotate(180, ${SVG_WIDTH/2}, ${SVG_HEIGHT/2})` : ""} style={{transformBox:"fill-box",transformOrigin:"center"}}>
        <text x={x(2)} y={y(4.5)} dominantBaseline="middle" textAnchor="middle" fill="#8B4513" fontSize="24" fontWeight="bold" style={{fontFamily: "KaiTi"}}>楚 河</text>
        <text x={x(6)} y={y(4.5)} dominantBaseline="middle" textAnchor="middle" fill="#8B4513" fontSize="24" fontWeight="bold" style={{fontFamily: "KaiTi"}}>汉 界</text>
      </g>
      <CornerMark r={2} c={1} /><CornerMark r={2} c={7} /><CornerMark r={7} c={1} /><CornerMark r={7} c={7} />
      {[0,2,4,6,8].map(c => <React.Fragment key={c}><CornerMark r={3} c={c}/><CornerMark r={6} c={c}/></React.Fragment>)}
    </svg>
  );
};

// ==========================================
//  5. React Component (主组件)
// ==========================================

export default function XiangqiGame() {
  const [uiBoard, setUiBoard] = useState(initialBoardSetup());
  const [turn, setTurn] = useState(COLORS.RED);
  const [selected, setSelected] = useState(null);
  const [lastMove, setLastMove] = useState(null);
  const [winner, setWinner] = useState(null);
  const [isAIThinking, setIsAIThinking] = useState(false);
  const [aiEnabled, setAiEnabled] = useState(true);
  const [demoMode, setDemoMode] = useState(false);
  const [playerColor, setPlayerColor] = useState(COLORS.RED);
  
  const engineRef = useRef(new Engine());

  // --- Game Loop ---
  useEffect(() => {
    if (winner) return;
    
    const engine = engineRef.current;
    const aiColor = playerColor === COLORS.RED ? COLORS.BLACK : COLORS.RED;
    const shouldAIPlay = demoMode || (aiEnabled && turn === aiColor);

    if (shouldAIPlay) {
      setIsAIThinking(true);
      setTimeout(() => {
         engine.loadBoard(uiBoard, turn);
         engine.timeLimit = 1500; 
         const bestMoveInt = engine.search(8); 
         
         if (bestMoveInt) {
           const src = SRC(bestMoveInt);
           const dst = DST(bestMoveInt);
           const r1 = (src/9)|0, c1 = src%9;
           const r2 = (dst/9)|0, c2 = dst%9;
           
           const newBoard = uiBoard.map(r => [...r]);
           newBoard[r2][c2] = newBoard[r1][c1];
           newBoard[r1][c1] = null;
           
           let kingFound = false;
           for(let r=0; r<10; r++) for(let c=0; c<9; c++) {
             if (newBoard[r][c] && newBoard[r][c].type === TYPES.GENERAL && newBoard[r][c].color !== turn) kingFound = true;
           }
           
           setUiBoard(newBoard);
           setLastMove({ from: {r: r1, c: c1}, to: {r: r2, c: c2} });
           setTurn(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
           if (!kingFound) setWinner(turn);
         } else {
           setWinner(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
         }
         setIsAIThinking(false);
      }, 100);
    }
  }, [turn, aiEnabled, demoMode, winner, playerColor, uiBoard]);

  // --- Human Interaction ---
  const handleCellClick = (r, c) => {
    if (winner || isAIThinking || demoMode) return;
    if (aiEnabled && turn !== playerColor) return;

    const cell = uiBoard[r][c];
    
    if (selected) {
      // Check Move Validity using Engine
      engineRef.current.loadBoard(uiBoard, turn);
      const srcIdx = selected.r * 9 + selected.c;
      const dstIdx = r * 9 + c;
      const moves = engineRef.current.generateMoves();
      const valid = moves.includes(MOVE(srcIdx, dstIdx));
      
      if (valid) {
        const newBoard = uiBoard.map(row => [...row]);
        newBoard[r][c] = newBoard[selected.r][selected.c];
        newBoard[selected.r][selected.c] = null;
        
        if (uiBoard[r][c] && uiBoard[r][c].type === TYPES.GENERAL) {
           setWinner(turn);
        }

        setUiBoard(newBoard);
        setLastMove({ from: selected, to: {r, c} });
        setTurn(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
        setSelected(null);
      } else {
        if (cell && cell.color === turn) setSelected({r, c});
        else setSelected(null);
      }
    } else {
      if (cell && cell.color === turn) setSelected({r, c});
    }
  };
  
  const isFlipped = playerColor === COLORS.BLACK;

  // --- Rendering Helpers ---
  const getPosStyle = (r, c) => {
    let left = ((MARGIN + c * CELL_SIZE) / SVG_WIDTH) * 100;
    let top = ((MARGIN + r * CELL_SIZE) / SVG_HEIGHT) * 100;
    if (isFlipped) { left = 100 - left; top = 100 - top; }
    return { left: `${left}%`, top: `${top}%`, transform: 'translate(-50%, -50%)' };
  };

  const restart = () => {
    setUiBoard(initialBoardSetup());
    setTurn(COLORS.RED);
    setWinner(null);
    setLastMove(null);
    setSelected(null);
    setIsAIThinking(false);
    setDemoMode(false);
    engineRef.current = new Engine();
  };

  return (
    <div className="min-h-screen bg-stone-100 flex flex-col items-center justify-center p-2 font-sans select-none">
       {/* HUD */}
       <div className="w-full max-w-md mb-4 flex justify-between items-center bg-white p-4 rounded-xl shadow-sm">
         <div className="flex items-center gap-2">
            <div className={`w-4 h-4 rounded-full ${turn === COLORS.RED ? 'bg-red-600 animate-pulse' : 'bg-gray-300'}`}></div>
            <span className={`font-bold ${turn === COLORS.RED ? 'text-red-600' : 'text-gray-400'}`}>红方</span>
         </div>
         <div className="font-black text-xl text-stone-700">
            {winner ? (winner === COLORS.RED ? '红方胜!' : '黑方胜!') : (isAIThinking ? 'AI思考中...' : '中国象棋')}
         </div>
         <div className="flex items-center gap-2">
            <span className={`font-bold ${turn === COLORS.BLACK ? 'text-black' : 'text-gray-400'}`}>黑方</span>
            <div className={`w-4 h-4 rounded-full ${turn === COLORS.BLACK ? 'bg-black animate-pulse' : 'bg-gray-300'}`}></div>
         </div>
       </div>

       {/* Board */}
       <div className="relative w-full max-w-[460px] aspect-[460/510] rounded shadow-2xl bg-[#DEB887] overflow-hidden transition-all duration-500">
          <XiangqiBoardSVG flipped={isFlipped} />
          <div className="absolute top-0 left-0 w-full h-full">
            {uiBoard.map((row, r) => row.map((cell, c) => {
               const isSel = selected && selected.r === r && selected.c === c;
               const isLast = lastMove && ((lastMove.from.r === r && lastMove.from.c === c) || (lastMove.to.r === r && lastMove.to.c === c));
               return (
                 <div key={`${r}-${c}`} onClick={() => handleCellClick(r, c)} style={getPosStyle(r, c)}
                      className="absolute w-[12%] h-[12%] flex justify-center items-center cursor-pointer z-10">
                    {isSel && <div className="absolute w-full h-full border-2 border-blue-500 rounded-full animate-pulse"></div>}
                    {isLast && <div className="absolute w-3 h-3 bg-blue-500/50 rounded-full"></div>}
                    {cell && (
                      <div className={`relative w-[90%] h-[90%] rounded-full flex items-center justify-center shadow-md border-2 sm:border-[3px] font-serif font-bold text-lg sm:text-2xl
                        ${cell.color === COLORS.RED ? 'bg-[#FDF5E6] text-[#c00] border-[#c00]' : 'bg-[#FDF5E6] text-[#111] border-[#111]'}
                        ${isSel ? '-translate-y-1 scale-110 shadow-xl' : ''}
                      `}>
                        <div className={`absolute w-[82%] h-[82%] rounded-full border ${cell.color === COLORS.RED ? 'border-red-200' : 'border-gray-300'}`}></div>
                        <span className="relative -mt-1">{PIECE_CHARS[cell.color][cell.type]}</span>
                      </div>
                    )}
                 </div>
               )
            }))}
          </div>
       </div>

       {/* Controls */}
       <div className="mt-6 flex flex-wrap justify-center gap-3">
          <button onClick={restart} className="px-4 py-2 bg-stone-700 text-white rounded flex items-center gap-2"><RotateCcw size={16}/> 重开</button>
          <button onClick={() => setPlayerColor(c => c === COLORS.RED ? COLORS.BLACK : COLORS.RED)} className="px-4 py-2 bg-orange-600 text-white rounded flex items-center gap-2"><ArrowRightLeft size={16}/> 换边</button>
          <button onClick={() => setDemoMode(!demoMode)} className={`px-4 py-2 rounded flex items-center gap-2 ${demoMode ? 'bg-purple-600 text-white' : 'bg-gray-300'}`}><Bot size={16}/> 演示</button>
          <button onClick={() => setAiEnabled(!aiEnabled)} className={`px-4 py-2 rounded flex items-center gap-2 ${aiEnabled ? 'bg-blue-600 text-white' : 'bg-gray-300'}`}><Cpu size={16}/> AI: {aiEnabled ? '开' : '关'}</button>
       </div>
    </div>
  );
}

总结

在本项目中:

  • React 单文件组件承担 UI + 状态 + 棋规 + AI 的所有逻辑
  • Gemini 3 让这种复杂度变得可控
  • 棋规、评估函数、AlphaBeta 搜索都能精确生成
  • 最终实现一个 完整可运行的中国象棋 Web 版 + AI

App Store 截图生成器应用图标生成器在线图片压缩utc timestamp, ctf tool 乖猫记账,AI智能分类的最佳聊天学生必备记账App。 Elasticsearch可视化客户端工具 百度网盘免费加速

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目背景:为什么用 Gemini 3 开发中国象棋
    • ✔ 自动生成复杂走法规则
    • ✔ 能从自然语言生成结构化逻辑
  • React 单文件组件结构总览
  • 棋盘 UI
  • 棋子规则
    • 马(含“蹩马腿”)
  • 评估函数
  • AlphaBeta 搜索
  • Gemini 3 如何提升开发效率
    • 1. 生成棋规时极准
    • 2. 搜索逻辑不会写错
    • 3. 能在单个组件中维护一致性
  • 完整代码
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档