草庐IT

666行的象棋程序下得不错啊

@->-->-- 2023-03-28 原文

全部666行(不计最后空行),不是只有界面。界面很简陋:

 b2c5写错了,应为b2b9炮七进七打马。截图不改了。9级就是9秒。没空格的如c3c47也行(7秒)。

源码就一个.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include "stdint.h"

enum { KING = 0, SHI, XIANG, MA, CHE, PAO, PAWN }; // 红子为8+x, 黑子为16+x; 8=帅, 22=卒
inline int MyFirstZi(int sd) { return 8 + (sd << 3); } // zi:子; sd:side
inline int YourFirstZi(int sd) { return 16 - (sd << 3); }

char  _brd[256];  // 9x10的棋盘放在16x16的数组中, 左上角为(3,3); brd:board
enum { LEFT = 3, TOP = 3, BEGIN = TOP * 16 + LEFT, END = (TOP + 9) * 16 + LEFT + 9 };
inline int XY2SQ(int x, int y) { return x + (y << 4); } // sq:square, 国际象棋的方格=象棋的交叉点
void PrintBoard() {
  static const char *D = "9876543210", *Z = "·1234567帅仕相马车炮兵15將士象馬車砲卒";
  puts(""); for (int y = 0; y < 10; y++) {
    printf(" %.2s", &D[y * 2]); // %.2s相当于%c%c
    for (int x = 0; x < 9; x++) printf("%.2s", &Z[_brd[XY2SQ(LEFT + x, TOP + y)] * 2]);
    puts("");
  } puts("  abcdefghi\n");
}

#define Z16 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
const char Z_POS_FLAG[256] = { Z16, Z16, Z16, // 棋子(z)位置(Position)标志
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, // 以下红方
  0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1, 0, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 9, 9, 9, 1, 1, 1,
};

inline int InBrd(int sq) { return Z_POS_FLAG[sq]; }
inline int InJiugong(int sq) { return Z_POS_FLAG[sq] == 9; }
inline int Empty(int sq) { return InBrd(sq) && !_brd[sq]; } // 0=没有棋子
inline int GuoHeLe(int sq, int sd) { return (sq & 0x80) == (sd << 7); } // 过河了, (square, side), 兵只能用这个
inline int SameHalf(int from, int to) { return !((from ^ to) & 0x80); } // (square, square), 相用这个
inline int SameX(int sq, int sq2) { return !((sq ^ sq2) & 0x0f); } 
inline int SameY(int sq, int sq2) { return !((sq ^ sq2) & 0xf0); }
inline int Up(int sq, int sd) { return sq - 16 + (sd << 5); } // 红向上, 黑向下
inline int MOVE(int from, int to) { return int(from | (to << 8)); }
inline int FROM(int mv) { return mv & 255; } // mv=move
inline int TO(int mv) { return mv >> 8; }

inline int Flip(int sq) { return 254 - sq; } // 黑方查Z_POS_VL时要颠倒。左上51, 右下203
const uint8_t Z_POS_VL[7][256] = { // 棋子在不同位置的价值
  { // 帅(将)
    Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16,
    0,  0,  0,  0,  0,  0,  1,  1,  1,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  2,  2,  2,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0, 11, 15, 11,
  }, { // 仕(士)
    Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16,
    0,  0,  0,  0,  0,  0, 20,  0, 20,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0, 23,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0, 20,  0, 20,
  }, { // 相(象)
    Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16,
    0,  0,  0,  0,  0, 20,  0,  0,  0, 20,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0, 18,  0,  0,  0, 23,  0,  0,  0, 18,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0, 20,  0,  0,  0, 20,  
  }, { //
    Z16, Z16, Z16, // 107的位置既可卧槽又可挂角; 河口象脚101; 中兵头顶103
    0,  0,  0, 90, 90, 90, 96, 90, 96, 90, 90, 90,  0,  0,  0,  0,
    0,  0,  0, 90, 96,103, 97, 94, 97,103, 96, 90,  0,  0,  0,  0,
    0,  0,  0, 92, 98, 99,103, 99,103, 99, 98, 92,  0,  0,  0,  0,
    0,  0,  0, 93,108,100,107,100,107,100,108, 93,  0,  0,  0,  0,
    0,  0,  0, 90,100, 99,103,104,103, 99,100, 90,  0,  0,  0,  0,
    0,  0,  0, 90, 98,101,102,103,102,101, 98, 90,  0,  0,  0,  0,
    0,  0,  0, 92, 94, 98, 95, 98, 95, 98, 94, 92,  0,  0,  0,  0,
    0,  0,  0, 93, 92, 94, 95, 92, 95, 94, 92, 93,  0,  0,  0,  0,
    0,  0,  0, 85, 90, 92, 93, 78, 93, 92, 90, 85,  0,  0,  0,  0,
    0,  0,  0, 88, 85, 90, 88, 90, 88, 90, 85, 88,
  }, { //
    Z16, Z16, Z16,
    0,  0,  0,206,208,207,213,214,213,207,208,206,  0,  0,  0,  0,
    0,  0,  0,206,212,209,216,233,216,209,212,206,  0,  0,  0,  0,
    0,  0,  0,206,208,207,214,216,214,207,208,206,  0,  0,  0,  0,
    0,  0,  0,206,213,213,216,216,216,213,213,206,  0,  0,  0,  0,
    0,  0,  0,208,211,211,214,215,214,211,211,208,  0,  0,  0,  0,
    0,  0,  0,208,212,212,214,215,214,212,212,208,  0,  0,  0,  0,
    0,  0,  0,204,209,204,212,214,212,204,209,204,  0,  0,  0,  0,
    0,  0,  0,198,208,204,212,212,212,204,208,198,  0,  0,  0,  0,
    0,  0,  0,200,208,206,212,200,212,206,208,200,  0,  0,  0,  0,
    0,  0,  0,194,206,204,212,200,212,204,206,194, 
  }, { //
    Z16, Z16, Z16, // 当头炮101; 天炮地炮都100
    0,  0,  0,100,100, 96, 91, 90, 91, 96,100,100,  0,  0,  0,  0,
    0,  0,  0, 98, 98, 96, 92, 89, 92, 96, 98, 98,  0,  0,  0,  0,
    0,  0,  0, 97, 97, 96, 91, 92, 91, 96, 97, 97,  0,  0,  0,  0,
    0,  0,  0, 96, 99, 99, 98,100, 98, 99, 99, 96,  0,  0,  0,  0,
    0,  0,  0, 96, 96, 96, 96,100, 96, 96, 96, 96,  0,  0,  0,  0,
    0,  0,  0, 95, 96, 99, 96,100, 96, 99, 96, 95,  0,  0,  0,  0,
    0,  0,  0, 96, 96, 96, 96, 96, 96, 96, 96, 96,  0,  0,  0,  0,
    0,  0,  0, 97, 96,100, 99,101, 99,100, 96, 97,  0,  0,  0,  0,
    0,  0,  0, 96, 97, 98, 98, 98, 98, 98, 97, 96,  0,  0,  0,  0,
    0,  0,  0, 96, 96, 97, 99, 99, 99, 97, 96, 96,
  }, { // 兵(卒)
    Z16, Z16, Z16,
    0,  0,  0,  9,  9,  9, 11, 13, 11,  9,  9,  9,  0,  0,  0,  0,
    0,  0,  0, 19, 24, 34, 42, 44, 42, 34, 24, 19,  0,  0,  0,  0,
    0,  0,  0, 19, 24, 32, 37, 37, 37, 32, 24, 19,  0,  0,  0,  0,
    0,  0,  0, 19, 23, 27, 29, 30, 29, 27, 23, 19,  0,  0,  0,  0,
    0,  0,  0, 14, 18, 20, 27, 29, 27, 20, 18, 14,  0,  0,  0,  0,
    0,  0,  0,  7,  0, 13,  0, 16,  0, 13,  0,  7,  0,  0,  0,  0,
    0,  0,  0,  7,  0,  7,  0, 15,  0,  7,  0,  7,
  }
};

template<typename T>inline void Swap(T& a, T& b) { T t = a; a = b; b = t; }

struct ZNum { // Invented by Albert Zobrist, Zobrist Hashing is to get an almost unique index number for any position
  uint64_t n;
  ZNum() { static RC4PRGA rc4; n = rc4.Next(); for (int i = 0; i < 7; i++) n = (n << 8) | rc4.Next(); } // n为0的概率非常低
  operator uint64_t () const { return n; }
  void operator^= (const ZNum &z) { n ^= z.n; } // xor-operation is own inverse and can be undone
  struct RC4PRGA { // RC4 Pseudo-Random Generation Algorithm
    int s[256], i, j;
    // 我们只需要ZNum尽量不同。key不变程序的行为不变, 便于调试。搜索结果和搜索时间有关,不一定是改错了程序(但一般是)
    RC4PRGA(const char* key = "https://www.geeksforgeeks.org/what-is-rc4-encryption/") {
      int len = strlen(key);
      for (i = 0; i < 256; i++) s[i] = i;
      for (i = 0; i < 256; i++) { j = (j + s[i] + key[i % len]) & 255; Swap(s[i], s[j]); }
      i = j = 0;
    }
    int Next();
  };
};
int ZNum::RC4PRGA::Next() { i = (i + 1) & 255; j = (j + s[i]) & 255; Swap(s[i], s[j]); return s[(s[i] + s[j]) & 255]; }
ZNum _zn; // 当前局面的ZNum
ZNum BLACK_ZN; // 局面信息含“该谁走”。0=红, 1=黑。^BLACK_ZN得黑, 再^去掉黑——红
// 每方7种棋子。虽然...most recent x86 chips...imul instruction...3 cycles, 但*8(hopefully << 3)总不会比*7慢?
ZNum ZN_TBL[2][8][256]; // [红黑][棋子类型][square]
int _side; // 该谁走
int _redVl, _blackVl; // 双方子力价值。vl:value
int _stop, _bestMove; // 停止搜索, 最佳着法
unsigned  _nTotalMove; // 搜索了多少步, 每步对应1个新局面
clock_t   _startTime, _endTime;
struct Position {
  uint64_t zn;
  int mv; // 从上个局面到本局面的move, 可用于对局结束后print
  int victim; // 从上个局面到本局面时被吃的子, 用于检查重复局面(吃子就不算重复)
  int check; // 本局面要走棋的一方是否被将军
  void Set(int m, int v, int c, uint64_t zn_) { mv = m; victim = v; check = c; zn = zn_; }
} _pstack[200];
// _pstack存放人走-电脑走-人走...的局面。nPos: number of positions. 搜索时不破坏已走局面, 在后面追加distance个, 结束后删除。
// _表示是全局变量。nPos和distance像是stack frame的bp (base pointer)和sp (stack pointer). distance是sp - bp.
int _distance, _nPos;

inline int LastPositionInCheck() { return _pstack[_nPos - 1].check; }

inline void SwitchSide() { _side ^= 1; _zn ^= BLACK_ZN; }

int InCheck() {
  static const int DELTA_SHIZI[4] = { -16, -1, 1, 16 }; // 十字型移动, 帅车炮用
  int i, k = 0; // 将(帅)的位置。TODO: 记录而不是每次找
  if (char* p=(char*)memchr(_brd + 0, MyFirstZi(_side) + KING, 256)) k = p - _brd;
  if (!k) return 0;
  int yours = YourFirstZi(_side), pawn = yours + PAWN;
  if (_brd[Up(k, _side)]==pawn || _brd[k-1]==pawn || _brd[k+1]==pawn) return 1; // 被兵(卒)将
  for (i = 0; i < 4; i++) { // 被马将?
    static const int tui[4] = { -17, -15, 15, 17 }; //
    static const int ma[4][2] = { {-33, -18}, {-31, -14}, {14, 31}, {18, 33} }; //
    if (_brd[k + tui[i]]) continue; // 蹩腿
    for (int j = 0; j < 2; j++) if (_brd[k + ma[i][j]] == yours + MA) return 1;
  }
  for (i = 0; i < 4; i++) {  // 被车或炮将或将帅对脸?
    int to, d = DELTA_SHIZI[i];
    for (to = k + d; Empty(to); to += d);
    if (_brd[to] == yours || _brd[to] == yours + CHE) return 1; // KING=0
    for (to += d; Empty(to); to += d); // 向炮架后找
    if (_brd[to] == yours + PAO) return 1;
  }
  return 0;
}

inline int InCheckAfterMove(int mv) { // 非破坏性判断
  int  from = FROM(mv), to = TO(mv);
  char zFrom = _brd[from], zTo = _brd[to];
  _brd[to] = _brd[from]; _brd[from] = 0;
  int inCheck = InCheck();
  _brd[to] = zTo; _brd[from] = zFrom;
  return inCheck;
}

int GenerateMoves(int* mvs, int jinChizi = 0) { // jinChizi: 只吃对方子; 否则吃子或不吃子(即不吃自己子)
  // !(zi & mine): 不是自己子(即空白或对方子); (zi & yours): 对方子
  static const int DELTA_SHIZI[4] = { -16, -1, 1, 16 }; // 十字型移动, 帅车炮用
  static const int DELTA_XIE[4] = { -17, -15, 15, 17 }; // 斜十字型, 仕相用
  static const int LEG[4] = { -16, -1, 1, 16 }; // 马腿
  static const int TI[4][2] = { {-33, -31}, {-18, 14}, {-14, 18}, {31, 33} }; // 马蹄
  int i, n = 0, mine = MyFirstZi(_side), yours = YourFirstZi(_side);  // 0100=红 1000=黑
  for (int from = BEGIN; from < END; from++) {
    int to, d, zi = _brd[from];
    if ((zi & mine) == 0) continue; // 不是己方棋子。0000=无 01xx=红 10xx=黑
    const int*  delta = DELTA_XIE;
    switch (int type = zi - mine) {
    case KING: delta = DELTA_SHIZI; // 后续处理和仕相同, 只是delta不同。不break
    case SHI:
      for (i = 0; i < 4; i ++) {
        if (!InJiugong(to = from + delta[i])) continue;
        zi = _brd[to];
        if (jinChizi ? (zi & yours) : !(zi & mine)) mvs[n++] = MOVE(from, to);
      }
      break;
    case XIANG:
       for (i = 0; i < 4; i++) {
        if (_brd[to = from + DELTA_XIE[i]]) continue; // 象眼有子
        zi = _brd[to += DELTA_XIE[i]]; // 继续向斜方向移动
        if (InBrd(to) && SameHalf(from, to)) {
          if (jinChizi ? (zi & yours) : !(zi & mine)) mvs[n++] = MOVE(from, to);
        }
      }
      break;
    case MA:
      for (i = 0; i < 4; i++) { // 马最多有4*2=8种走法
        if (_brd[to = from + LEG[i]]) continue; // 马腿有子
        for (int j = 0; j < 2; j++) {
          if (!InBrd(to = from + TI[i][j])) continue;
          zi = _brd[to];
          if (jinChizi ? (zi & yours) : !(zi & mine)) mvs[n++] = MOVE(from, to);
        }
      }
      break;
    case CHE:
      for (i = 0; i < 4; i++) {
        d = DELTA_SHIZI[i];
        for (to = from + d; InBrd(to); to += d) {
          if (const int zi = _brd[to]) {
            if (zi & yours) mvs[n++] = MOVE(from, to); // 吃子
            break; // 前方有子
          }
          else if (!jinChizi) // 不吃子时继续移动
            mvs[n++] = MOVE(from, to);
        }
      }
      break;
    case PAO:
      for (i = 0; i < 4; i++) {
        d = DELTA_SHIZI[i];
        for (to = from + d; Empty(to); to += d) { // 移动和找炮架
          if (!jinChizi) mvs[n++] = MOVE(from, to);
        }
        for (to += d; Empty(to); to += d); // 从炮架往后找
        zi = _brd[to];
        if (zi & yours) mvs[n++] = MOVE(from, to);
      }
      break;
    default: // case PAWN:
      if (InBrd(to = Up(from, _side))) {
        zi = _brd[to];
        if (jinChizi ? (zi & yours) : !(zi & mine)) mvs[n++] = MOVE(from, to);
      }
      if (GuoHeLe(from, _side)) {
        for (d = -1; d <= 1; d += 2) {
          if (InBrd(to = from + d)) {
            zi = _brd[to];
            if (jinChizi ? (zi & yours) : !(zi & mine)) mvs[n++] = MOVE(from, to);
          }
        }
      }
    } // switch
  }
  return n;
}

// 人走时, c3c4ToSQ检查square是否越界, LegalMove !不! 检查走子后被将军
// 电脑走时, GenerateMoves !不! 检查走子后被将军; 由于Hash冲突, 从表里查来的move可能illegal
int LegalMove(int mv) { // Chess Terminology里用legal而不是valid; LDOCE说legal W1S3 (常用, Written, Spoken)
  static const char SSX[512] = { // 1=帅(将), 2=仕(士), 3=相(象)
    Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 
    0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 
    1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 
    0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 
    1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
    0, 0, 3, 
  };
  static const char BMT[512] = { // 蹩马腿。不是日字走法的为0. 不知道“蹩马腿”咋翻译,但不是Pin: An attack (by a Rook, Bishop or Queen) on a
    // piece that cannot or should not move, because a piece behind the attacked piece is worth even more.
    Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16, Z16,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -16,  
    0,-16,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  0,  
    0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  0,  
    0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 16, 
    0, 16,  
  };
  int mine = MyFirstZi(_side), sq;
  int from = FROM(mv), to = TO(mv), zFrom = _brd[from], zTo = _brd[to]; // z:子
  if (!(zFrom & mine) || (zTo & mine)) return 0; // 不是走自己的子, 或吃自己的子
  switch (int type = zFrom - mine) {
  case KING:  return (SSX[to - from + 256] == 1) && InJiugong(to);
  case SHI:   return (SSX[to - from + 256] == 2) && InJiugong(to);
  case XIANG: return (SSX[to - from + 256] == 3) && SameHalf(from, to) &&
              !_brd[(from + to) >> 1]; // 象眼, “并行计算”(x0+x1)/2和(y0+y1)/2
  case MA:
    sq = from + BMT[to - from + 256];
    return (sq != from) && Empty(sq); // 马腿无子?
  case CHE: case PAO: {
    int d;
    if (SameY(from, to)) d = (to < from ? -1 : 1); // Y相同时在X方向移动
    else if (SameX(from, to)) d = (to < from ? -16 : 16); // X相同时在Y方向移动
    else return 0; // 车炮不走直线
    for (sq = from + d; (sq != to) && Empty(sq); sq += d); // 遇到to或任意一方棋子停止
    if (sq == to) return ((CHE == type) || !zTo); // 无论to处有无子,车都可以到(移动或吃); 无子则炮可移到
    else if ((PAO == type) && zTo) { // 不能隔着炮架打空气
      for (sq += d; (sq != to) && Empty(sq); sq += d); // 找到炮架后第一个子
      return (sq == to);
    } else return 0;
  }
  default: // case PAWN:
    if (GuoHeLe(to, _side) && (to == from - 1 || to == from + 1)) return 1;
    return (to == Up(from, _side));
  }
}

const int MAX_GEN_MOVES = 128; // ELEEYE说“象棋任何局面不超过120种可能走法”。mvs[MAX_GEN_MOVES]是递归函数的局部变量
const int MATE_VALUE = 10000;  // 最高分数,即将死的分。Checkmate可简称mate
const int BAN_VALUE = MATE_VALUE - 100; // 长将判负的分
const int WIN_VALUE = MATE_VALUE - 200; // 超出此值说明已搜索出杀棋
const int DRAW_VALUE = 20; // 和棋时的分

int Checkmate() {
  int mvs[MAX_GEN_MOVES], n = GenerateMoves(mvs);
  for (int i = 0; i < n; i++) if (!InCheckAfterMove(mvs[i])) return 0;
  return 1; // 所有的走法都解不了将
}

inline void PutPiece(int sq, int zi) {
  _brd[sq] = char(zi);
  if (zi < 16) { zi -= 8; _redVl += Z_POS_VL[zi][sq]; _zn ^= ZN_TBL[0][zi][sq]; } 
  else { zi -= 16; _blackVl += Z_POS_VL[zi][Flip(sq)]; _zn ^= ZN_TBL[1][zi][sq]; }
}

inline void TakePiece(int sq, int zi) {
  _brd[sq] = 0;
  if (zi < 16) { zi -= 8; _redVl -= Z_POS_VL[zi][sq]; _zn ^= ZN_TBL[0][zi][sq]; }
  else { zi -= 16; _blackVl -= Z_POS_VL[zi][Flip(sq)]; _zn ^= ZN_TBL[1][zi][sq]; }
}

int MakeMove(int mv) {
  if (!(++_nTotalMove & 0xfffff)) { // 每100万次检查下
    clock_t now = clock(), d = now - _startTime;
    static const int S = CLOCKS_PER_SEC / 1000;
    printf("%d ms %d 千步/秒\r", (d * 1000 / CLOCKS_PER_SEC), _nTotalMove * S / d); // \r\n CR (Carriage Return) LF (Line Feed)/New Line 0d 0a
    if (now >= _endTime) _stop = 1; // 不要return 0也不要throw, 让搜索干净地结束
  }
  if (!mv || InCheckAfterMove(mv)) return 0;
  int from = FROM(mv), to = TO(mv), zi = _brd[from], victim = _brd[to];
  if (victim) TakePiece(to, victim);
  TakePiece(from, zi); PutPiece(to, zi);
  SwitchSide(); // 改变走棋方后重新判断是否将军
  _pstack[_nPos].Set(mv, victim, InCheck(), _zn);
  ++_nPos; ++_distance; return 1;
}

void UnmakeMove() {
  --_distance; --_nPos;
  SwitchSide();
  int mv = _pstack[_nPos].mv, from = FROM(mv), to = TO(mv), victim = _pstack[_nPos].victim, zi = _brd[to];
  TakePiece(to, zi); PutPiece(from, zi); if (victim) PutPiece(to, victim);
}

inline int Evaluate() { return (_side ? (_blackVl - _redVl) : (_redVl - _blackVl)) + 3; }

// 空着(Null-Move)是自己不走让对手连走两次。“在历史表和迭代加深等启发的作用下,空着启发已经意义不大了。”测试好像减少百分之十几的搜索时间。
int NullMoveOK() { return (_side ? _blackVl : _redVl) > 400; } // 根据子力判断是否允许空步裁剪
inline void NullMove() { SwitchSide(); _pstack[_nPos].Set(0, 0, 0, _zn); ++_nPos; ++_distance; }
inline void UndoNullMove() { --_distance; --_nPos; SwitchSide(); }

int DrawValue()  { return (_distance & 1) == 0 ? -DRAW_VALUE : DRAW_VALUE; }

int RepCode(int nRep = 1) { // rep: repetition
  int  side = 0, meCJ = 1, youCJ = 1; // cj:长将(perpetual check)
  for (const Position* p = _pstack + _nPos - 1; p->mv && !p->victim; p--) { // 初始局面和空着产生的局面,mv和victim为0
    if (side) {
      meCJ &= p->check;
      if (p->zn == _zn && !--nRep) return (1 | (meCJ ? 2 : 0) | (youCJ ? 4 : 0));
    } else
      youCJ &= p->check;
    side ^= 1;
  }
  return 0;
}

int GameOver() {
  if (Checkmate()) return puts("绝杀无解");
  if (int c = RepCode(3)) {
    printf("%s", "存在重复局面,");
    switch (c) {
    case 1: return puts("双方都无长将(判和)");
    case 3: return puts("本方长将(判本方负)"); // 1|2
    case 5: return puts("对方长将(判对方负)"); // 1|4
    case 7: return puts("双方长将(判和)"); // 1|2|4
    }
  }
  if (_nPos > 100) return puts("无聊死了");
  return 0;
}

int RepValue(int c) { // 重复局面的分数
  int v = ((c & 2) ? _distance - BAN_VALUE : 0) + ((c & 4) ? BAN_VALUE - _distance : 0);
  return v ? v : DrawValue();
}

struct HashItem {
  uint64_t  zn;
  short vl; // 有很多个所以想省点内存
  uint16_t  mv;
  char  depth, flag;
};

const int MAX_DISTANCE = 20; // 最大搜索深度
// 不同的路可以到达同样局面的现象叫做“置换”(Transposing)。置换表存储已搜过的结果,它通常是个散列表。
const int HASH_SIZE = 1 << 23;
const int HASH_ALPHA = 1; // Alpha节点的置换表项
const int HASH_BETA = 2; // Beta节点的置换表项
const int HASH_PV = 3; // Principal Variation节点的置换表项

namespace history {
HashItem  _hashTbl[HASH_SIZE];
// 请看main()前注释。车砍砲, 車吃车, 另一个车铁门栓。只看当前局面的评价函数如何写? 得子了, 丢车了, 赢了。
// 国际象棋程序设计(四):“用评估函数对着法打分然后排序。直觉上这会起作用,评估函数越好,这个方法就越有效。不幸的是在Chess中它一点也不起作用,
// 因为下个月我们将了解到,很多局面是不能准确评估的。”(四)和(五)隔了一个月, 可见确实难。:-)
int _vls[65536];  // values
void Clear() { memset(_hashTbl, 0, sizeof(_hashTbl)); memset(_vls, 0, sizeof(_vls)); }

// 如 MATE=100, BAN(长将)=90, WIN(将要将死, 杀棋)=80
void Update(int flag, int vl, int depth, int mv) { // mv可能是0(没有bestMove)
  HashItem& hsh = _hashTbl[_zn & (HASH_SIZE - 1)];
  if (hsh.depth > depth) return; // 冲突时保存更深的
  hsh.flag = char(flag); hsh.depth = char(depth); hsh.mv = uint16_t(mv); hsh.zn = _zn;
  if (vl > WIN_VALUE) { // 红方杀棋
    if (mv || vl > BAN_VALUE) hsh.vl = short(vl + _distance);
  } else if (vl < -WIN_VALUE) {  // 黑方杀棋
    if (mv || vl < -BAN_VALUE) hsh.vl = short(vl - _distance);
  } else
    hsh.vl = short(vl);
  if (mv) _vls[mv] += depth * depth;
}

int Find(int alpha, int beta, int depth, int &mv) {
  const HashItem& h = _hashTbl[_zn & (HASH_SIZE - 1)];
  if (h.zn != _zn) { mv = 0; return -MATE_VALUE; }
  mv = h.mv;
  int vl = h.vl, mate = 0;
  if (vl > WIN_VALUE) {
    if (vl < BAN_VALUE) return -MATE_VALUE;
    vl -= _distance; mate = 1;
  } else if (vl < -WIN_VALUE) {
    if (vl > -BAN_VALUE) return -MATE_VALUE;
    vl += _distance; mate = 1;
  }
  if (mate || h.depth >= depth) { // 杀棋不在乎深度
    if (h.flag == HASH_BETA) return (vl >= beta ? vl : -MATE_VALUE);
    else if (h.flag == HASH_ALPHA) return (vl <= alpha ? vl : -MATE_VALUE);
    return vl;
  }
  return -MATE_VALUE;
}

int GenMoves(int* mvs, int best = 0) {
  mvs[0] = best; int n = GenerateMoves(mvs + 1) + 1; // 生成所有着法
  for (int i = 2; i < n; i++) { // 元素数量几十个时Insertion Sort快
    int t = mvs[i], tv = _vls[t], j;  // 如b12 b11 b21
    for (j = i - 1; j >= 1 && tv > _vls[mvs[j]]; j--) mvs[j + 1] = mvs[j];
    mvs[j + 1] = t;
  }
  if (best) for (int i = 1; i < n; i++) if (best == mvs[i]) mvs[i] = 0;
  return n;
}
} // history

namespace mvvlva { // Most Valuable Victim/Least Valuable Attacker) 卒吃炮: good, 卒吃车: goood, 卒吃帅: goooood!
static const int T[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 5, 1, 1, 3, 4, 3, 2, 0,  5, 1, 1, 3, 4, 3, 2, 0 };
inline int calc(int mv) { return (T[_brd[TO(mv)]] << 3) - T[_brd[FROM(mv)]]; }
int GenMoves(int* mvs) {
  int values[MAX_GEN_MOVES], i;
  int n = GenerateMoves(mvs, 1); // 生成仅吃子走法
  for (i = 0; i < n; i++) values[i] = calc(mvs[i]);
  for (i = 1; i < n; i++) {  // 注意两个排序稍有不同
    int t = mvs[i], v = values[i], j;
    for (j = i - 1; j >= 0 && v > values[j]; j--) mvs[j + 1] = mvs[j];
    mvs[j + 1] = t;
  }
  return n;
};
} // mvvlva

// 《高级搜索方法——静态搜索》、《高级搜索方法——简介(一)》 相对稳定的局面叫做Quiescent局面。
// 静态搜索一般只搜索吃子着法, 因为吃子导致局面剧烈变化(不再inactive; passive; quiet). sanction有类似的情况。
int Quies(int alpha, int beta) {
  if (int c = RepCode()) return RepValue(c); // 重复局面处理
  if (_stop || _distance == MAX_DISTANCE) return Evaluate();
  int best = -MATE_VALUE, mvs[MAX_GEN_MOVES], n;
  if (LastPositionInCheck()) n = history::GenMoves(mvs); // 被将军时生成全部走法
  else {
    int score = Evaluate();
    if (score > best) {
      best = score;
      if (score >= beta) return score;
      if (score > alpha) alpha = score;
    } // Evaluate速度非常快, 尽量避免GenMoves
    n = mvvlva::GenMoves(mvs);
  }
  for (int i = 0; i < n; i++) {
    if (!MakeMove(mvs[i])) continue;
    int score = -Quies(-beta, -alpha);
    UnmakeMove();
    if (score > best) {
      best = score;
      if (score >= beta) return score;
      if (score > alpha) alpha = score;
    }
  }
  return (best == -MATE_VALUE) ? (_distance - MATE_VALUE) : best;
}

/* 《高级搜索方法——简介(二)》 https://www.chessprogramming.org/Fail-Soft
Fail-Soft is related to Alpha-Beta. Returned scores might be outside the bounds. */
int Fail_Soft(int alpha, int beta, int depth, int noNullmove) {
  // 《高级搜索方法——简介(一)》 每一步棋都搜索到一个固定的深度, 这个深度叫做“水平线”(Horizon)
  if (depth <= 0) return Quies(alpha, beta); // 到达水平线则调用静态搜索(注意由于空步裁剪,深度可能小于零)
  if (int c = RepCode()) return RepValue(c); // 不要在根节点检查重复局面,否则就没有走法了
  if (_stop || _distance == MAX_DISTANCE) return Evaluate();
  // 尝试置换表裁剪,并得到置换表走法
  int hashMove, score = history::Find(alpha, beta, depth, hashMove);
  if (score > -MATE_VALUE) return score;
  // 尝试空步裁剪(根节点的Beta值是MATE_VALUE,所以不可能发生空步裁剪)
  if (!noNullmove && NullMoveOK() && !InCheck()) {
    NullMove();
    enum { NULL_DEPTH = 2 };
    score = -Fail_Soft(-beta, 1 - beta, depth - NULL_DEPTH - 1, 1);
    UndoNullMove();
    if (score >= beta) return score;
  }
  int hashFlag = HASH_ALPHA, bestMove = 0, bestScore = -MATE_VALUE;
  int mvs[MAX_GEN_MOVES], n = history::GenMoves(mvs, hashMove);
  for (int i = 0; i < n; i++) {
    int mv = mvs[i];
    if (!MakeMove(mv)) continue;
    /* Check Extensions have two distinct forms: one of them extends when giving check, the other - when evading it. 
     In each case, typical depth to extend is one ply. A ply is a half-move, or the move of one player. When both
     players move, that is two ply, or one full move. www.chessprogramming.org/Check_Extensions */
    int newDepth = LastPositionInCheck() ? depth : (depth - 1);
    if (bestScore == -MATE_VALUE) {
      score = -Fail_Soft(-beta, -alpha, newDepth, 0);
    } else {
      score = -Fail_Soft(-alpha - 1, -alpha, newDepth, 0);
      if (score > alpha && score < beta) score = -Fail_Soft(-beta, -alpha, newDepth, 0);
    }
    UnmakeMove();
    // 进行Alpha-Beta大小判断和截断
    if (score > bestScore) { // 找到最佳值(但不能确定是Alpha、PV还是Beta走法)
      bestScore = score; // bestScore是目前要返回的最佳值, 可能超出Alpha-Beta边界
      if (score >= beta) { // 找到一个Beta走法
        hashFlag = HASH_BETA;
        bestMove = mv;  // Beta走法要保存到历史表
        break; // Beta截断(退出循环不再试其它着法)
      }
      if (score > alpha) { // 找到一个Principal Variation走法
        hashFlag = HASH_PV;
        bestMove = mv; // PV走法要保存到历史表
        alpha = score; // 缩小Alpha-Beta边界
      }
    }
  }
  // 搜完所有走法, 把最佳走法(不能是Alpha走法)保存到历史表, 返回最佳值
  if (bestScore == -MATE_VALUE) return _distance - MATE_VALUE; // 杀棋根据步数给出评价
  history::Update(hashFlag, bestScore, depth, bestMove);
  return bestScore;
}

int RootSearch(int depth) {
  int score, best = -MATE_VALUE;
  int mvs[MAX_GEN_MOVES], n = history::GenMoves(mvs, _bestMove);
  for (int i = 0; !_stop && (i < n); i++) {
    int mv = mvs[i];
    if (!MakeMove(mv)) continue;
    int newDepth = LastPositionInCheck() ? depth : depth - 1;
    if (best != -MATE_VALUE) { // https://www.chessprogramming.org/Fail-Hard
      score = -Fail_Soft(-best - 1, -best, newDepth, 0); // Soft比hard放宽一点限制。使用null-move
      if (score > best) score = -Fail_Soft(-MATE_VALUE, -best, newDepth, 1); // No null-move
    } else
      score = -Fail_Soft(-MATE_VALUE, MATE_VALUE, newDepth, 1); // 先有个着法再说
    UnmakeMove();
    if (score > best) { best = score; _bestMove = mv; }
  }
  history::Update(HASH_PV, best, depth, _bestMove);
  return best;
}

void ComputerGo() {
  history::Clear(); _nTotalMove = _distance = _bestMove = _stop = 0;
  _startTime = clock(); _endTime = _startTime  + _endTime * CLOCKS_PER_SEC;
  for (int i = 1; !_stop && i <= MAX_DISTANCE; i++) { // 在满足时间限制的前提下改进。“迭代加深”(Iterated Deepening)
    int score = RootSearch(i);
    if (score > WIN_VALUE || score < -WIN_VALUE) break; // 搜到杀棋提前终止
  }
  MakeMove(_bestMove); puts("");
}

int c3c4ToSQ(int x, int y) {
  x -= 'A'; if (x < 0 || x > 8) throw 0; x += LEFT; 
  y = 9 - (y - '0'); if (y < 0 || y > 9) throw 1; y += TOP;
  return XY2SQ(x, y);
}

void ManGo() {
  for (int mv;;) try {
    char s[80] = ""; printf("%d. 输入着法: ", (_nPos + 1) / 2); strupr(gets(s));
    if (*s == 'Q') exit(0);
    _endTime = atoi(s + 4);
    if (_endTime < 1 || _endTime > 99) _endTime = 2;
    mv = MOVE(c3c4ToSQ(*s, s[1]), c3c4ToSQ(s[2], s[3]));
    if (LegalMove(mv) && MakeMove(mv)) break;
  } catch(int) {}
}

int fen_atoi(int c) {
  switch (toupper(c)) {
  case 'K': return 0; case 'A': return 1; case 'B': return 2; case 'N': return 3;
  case 'R': return 4; case 'C': return 5; case 'P': return 6; default: abort();
  }
}

void Init() {
  const char* s = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR";
  //const char* s = "r3kab2/4a4/4b4/c3C4/9/R1R6/9/9/9/4K4 ";
  for (int y = TOP, x = LEFT; *s && *s != ' '; ++s) {
    const char c = *s;
    if (c == '/') (x = LEFT, ++y);
    else if (c >= '1' && c <= '9') x += c - '0';
    else _brd[XY2SQ(x++, y)] = char((c >= 'a' ? 16 : 8) + fen_atoi(c));
  }
  for (int sq = BEGIN; sq < END; sq++) { if (char zi = _brd[sq]) PutPiece(sq, zi); }
  _pstack[_nPos++].Set(0, 0, InCheck(), _zn);
}

/* 
9車···將士象··* 《国际象棋程序设计(4)基本搜索方法》《基本搜索方法——简介1,2,最小-最大搜索》 https://www.chessprogramming.org/Minimax
8····士····* 评价函数给每个局面打1个分,如红方子数 - 黑方子数。15对红方是极好的(黑光杆老将), -15对黑方是极好的。-15 < 0 < 15, Min, Max
7····象····* 对方总是极不配合, 红方总选max, 黑方总选min. Minimax搜索时return red ? Max(depth) : Min(depth);
6砲···炮····* -1的n次方不断变号。score = -NegativeMax(depth - 1); 如黑在-15和-7中选min相当于在15和7中选max.
5·········* alpha male: the dominant male animal or person in a group. 希腊字母洋气。完全搜索的结点叫alpha结点。
4车·车······* 如节点F的子节点的分是{11,12,7,9}, 则F的分是12. G是F的兄弟, 子节点是{15,一大堆没搜的}, 那对手会怎么办?
3·········  对手肯定不会选择G, 因为如果他选G, 他知道聪明的你会选至少15那个(G)。他选F, 你用尽洪荒之力也只能选12.
2·········* 程序天然有自己和自己下的能力(靠depth的奇偶区分双方)。到达叶子结点的路叫做“主要变例”(Principal Variation)
1·········* 改伪码是好主意,改别人写好的更好(说我自己)。Don't be cocky, 觉得“这么改咋会错”, 勤测试, 勤备份(还是说自己)。
0····帅····* 以下alpha和beta是分, 不是节点或人。
 abcdefghi
*/
int main() {
  puts("小清象棋——改自象棋小巫师[ http://www.xqbase.com/ ]\n\n例子   说明");
  puts("q      退出\nc3c4   兵七进一,电脑2级\nb2c5 9 炮二进七,电脑9级");
  for (Init(); !GameOver();) PrintBoard(), ManGo(), ComputerGo();
  return 0;
}
View Code

没有stdint.h的话:

typedef unsigned __int64  uint64_t;
typedef unsigned __int16  uint16_t;
typedef unsigned __int8   uint8_t;
View Code

象棋小巫师改的。计算机博弈讲了很多相关知识。

Windows 10自带的杀毒软件歧视用户自己编译的.exe,启动时总要检查一番。可在“病毒和威胁防护”设置—管理设置—添加或删除排除项里把.exe所在的文件夹添加进去。

后记:从VC6改成gcc 10.3.0 (tdm-1)后编译有错误和警告,运行出错,已修正更新。要incude <ctype.h>. const char* s = "...", const是必须的(警告)。short mv必须改成uint16_t mv,因为带符号扩展为int时成了负数。用VC6编译的运行时居然不出错(不是好事啊)。g++ -O3的比VC6 Release的快10%。VC6的.exe 61,440字节,g++的277,948, strip后163,854,不知为啥这么大。

安装gcc时:

 

也许这个mainifest能使Windows安装中心不频繁扫描,但gcc.exe自己呢?安装gdb后有gdb32.exe, 没有gdb.exe, 也没有make.exe

后记2:以为自己搞明白alpha-beta了,其实没有。干脆把注释一删凑了666行。Alpha-Beta算法简介

后记3:可魔改成自己3个车1炮2马等。:-)

后记4: TDM-GCC uses a statically-linked libstdc++ by default! To use the libstdc++ DLL, specify `-shared-libstdc++` on the command line.

后记5: 365行的象棋程序

象棋666spancolorstyle人工智能

有关666行的象棋程序下得不错啊的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  3. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  4. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  5. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  6. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  7. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  8. ruby - 检查是否通过 require 执行或导入了 Ruby 程序 - 2

    如何检查Ruby文件是否是通过“require”或“load”导入的,而不是简单地从命令行执行的?例如:foo.rb的内容:puts"Hello"bar.rb的内容require'foo'输出:$./foo.rbHello$./bar.rbHello基本上,我想调用bar.rb以不执行puts调用。 最佳答案 将foo.rb改为:if__FILE__==$0puts"Hello"end检查__FILE__-当前ruby​​文件的名称-与$0-正在运行的脚本的名称。 关于ruby-检查是否

  9. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

  10. 程序员如何提高代码能力? - 2

    前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源

随机推荐