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

全部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行的象棋程序

posted @ 2022-12-10 21:24  Fun_with_Words  阅读(320)  评论(0编辑  收藏  举报









 张牌。