Skip to content

AI 五子棋

玩法

经典五子棋玩法,直接开玩。

TIP

由于前端计算性能有限,计算深度为 2 步(太深直接等到天荒地老);

算法

基于 大小博弈算法,具体简要算法可以看 井字棋游戏 中的介绍。

五子棋相比 井字棋 要困难许多,主要集中在 评估函数上 ;

源码

vue
<template>
  <div class="five-chess vue-component-container">
    <div class="flex-column flex-center-all">
      <div class="text-center mb-sm pos-r">
        <span class="" v-if="gameState !== 2">等待开始</span>
        <span class="" v-else-if="isAiComputing"> 电脑正在思考中... </span>
        <span class="" v-else> 轮到 您 走了 </span>
      </div>
      <div class=" pos-r">
        <div class="chess-container-b" style="z-index: 0">
          <div class="flex-horiz row-b" v-for="i in RC - 1">
            <div class="flex-horiz grid-b" v-for="j in RC - 1"></div>
          </div>
        </div>
        <div class="pos-a chess-container" style="z-index: 10;">
          <div class="flex-horiz row" v-for="(row, i) in chessState.state">
            <div class="grid flex-horiz flex-center-all" v-for="(chess,j) in row"
                 :class="[lastState.state[i][j] !== chess ? 'active':'', chess === EMPTY ? 'cursor-p' : '' ]"
                 @click="onClickChess(i,j)"
            >
              <div class="chess bg-text"
                   :class="[chess === B ? 'bg-black' : 'bg-white']"
                   v-if="chess !== EMPTY"
              ></div>
            </div>
          </div>
        </div>
        <start-over-mask
            :status="gameState"
            @on-click-start="onClickRestart"
        >
          <div class="mb-sm" style="font-size: 30px;">
            <span v-if="gameState === 3 ">你赢了</span>
            <span v-else-if="gameState=== 4">你输了</span>
            <span v-else-if="gameState === 5">棋逢对手</span>
          </div>
        </start-over-mask>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import {defineComponent, ref, getCurrentInstance} from "vue";
import {MaxMinSearch} from "../../utils/ai.util";
import * as _ from "lodash";
import StartOverMask from "../common/start-over-mask.vue";

const DefaultState = () => new Array(RC).fill('').map(r => new Array(RC).fill(EMPTY));
export default defineComponent({
  name: "five-chess",
  components: {StartOverMask},
  props: {},
  setup(props: any, ctx: any) {
    const instance = getCurrentInstance();
    const chessState = ref(new FiveState(DefaultState(), []));
    const gameState = ref(1);
    const lastState = ref(new FiveState(DefaultState(), []));
    const isAiComputing = ref(false);


    const ai = new MaxMinSearch(2)
        .setEvaluateResult(evaluate2)
        .setIsLeafState(isLeafState)
        .setNextLevelState(getNextStates);

    const aiTurn = () => {
      if (gameState.value != 2) return;
      if (!isAiComputing.value) return;
      const best = ai.getBestChoiceByState(_.cloneDeep(chessState.value));
      if (!best) return;
      lastState.value = _.cloneDeep(chessState.value);
      chessState.value = best;
      checkGameOver();
      isAiComputing.value = false;
    }

    const checkGameOver = () => {
      const winner = checkWhoWin(chessState.value);
      if (winner !== EMPTY) {
        gameState.value = winner === 1 ? 3 : 4;
      }
      for (const row of chessState.value.state) {
        for (const c of row) {
          if (c === EMPTY) return;
        }
      }
      gameState.value = 5;
    }

    return {
      W, B, EMPTY, RC, isAiComputing,
      gameState, lastState, chessState,

      onClickChess: (i, j) => {
        const {state, trace} = chessState.value;
        if (gameState.value !== 2) return;
        if (isAiComputing.value || state[i][j] !== EMPTY) return;
        isAiComputing.value = true;
        lastState.value = _.cloneDeep(chessState.value);
        state[i][j] = W;
        trace.push([i, j]);
        checkGameOver();
        setTimeout(aiTurn, 10);
      },
      onClickRestart: () => {
        chessState.value = new FiveState(DefaultState(), []);
        lastState.value = new FiveState(DefaultState(), []);
        gameState.value = 2;
        isAiComputing.value = false;
      },
    }
  }
})


class FiveState {

  constructor(state, trace = [], isAi = true) {
    this.state = state;
    this.trace = trace;
  }

  state: any[];
  trace: any[];
}

const ScoreTable2 = [
  [11111, 100000, 10000],
  [211112, 1000000, 1000],
  [11112, 100000, 100],
  [12111, 100000, 100],
  [11211, 100000, 100],
  [211212, 1000000, 100],
  [21112, 100000, 100],
  [21111, 100000, 100],
  [11121, 100000, 100],
  [212112, 1000000, 100],
  [11122, 100000, 10],
  [22111, 100000, 10],
  [22111, 100000, 10],
  [11122, 100000, 10],
  [11212, 100000, 4],
  [21211, 100000, 4],
  [22112, 100000, 1],
  [21122, 100000, 1]
];

const W = 1, B = 3, EMPTY = 2, RC = 15;

const Rows = [], Columns = [], Slash1 = [], Slash2 = [];

for (let c = 0; c < RC; c++) {
  const line1 = [], line2 = [];
  for (let r = 0; r < RC; r++) {
    line1.push([r, c]);
    line2.push([c, r]);
  }
  Rows.push(line2);
  Columns.push(line1);
}
for (let c = -RC + 1; c < RC; c++) {
  const line1 = [], line2 = [];
  for (let r = 0; r < RC; r++) {
    const j = c + r;
    if (j < 0 || j >= RC) continue;
    line1.push([r, j]);
    line2.push([RC - r - 1, j]);
  }
  Slash1.push(line1);
  Slash2.push(line2);
}

const getLinesByGrid = (x, y) => {
  return [
    Rows[x], Columns[y],
    Slash1[y - x + RC - 1], Slash2[x + y]
  ];
}

const isLeafState = (fiveState) => {
  const winner = checkWhoWin(fiveState);
  return winner !== EMPTY;
}

const checkWhoWin = ({state}) => {
  const lines: any[] = getLinesByState(state);
  let res: any = true;
  for (const line of lines) {
    if (line.length < 5) continue;
    let w = 0, b = 0;
    for (const [x, y] of line) {
      if (state[x][y] === B) {
        b++;
        w = 0;
      } else if (state[x][y] == W) {
        w++;
        b = 0;
      } else w = b = 0;
      if (state[x][y] === EMPTY && res !== EMPTY) res = EMPTY;
      if (b === 5) return B;
      if (w === 5) return W;
    }
  }
  return res;
}

const Flags = [], EvalRange = 6;
for (let i = 0; i <= EvalRange * 2; i++) {
  Flags[i] = 10 ** i;
}

const DIR = [[0, 1], [1, 0], [1, 1], [1, -1]];
const evaluate2 = (fiveState, isAi) => {
  const {state, trace} = fiveState;
  const map = new Array(RC).fill(true).map(_ => new Array(RC).fill(0));
  let score = 0;
  trace.forEach(g => {
    const [x, y] = g, target = state[x][y], res = [];
    DIR.forEach(dir => {
      const s = getEvaluateLineScore(getEvaluateLineByGridDir(state, dir, g, target), isAi, target);
      score += s;
      if (s) res.push(s);
    })
    res.sort();
    for (let i = 0; i < res.length - 1; i++) {
      score += res[i];
    }
  });
  return score;
}

const getEvaluateLineScore = (line, isAi, player) => {
  let score = 0;
  for (const [st, div, sc] of ScoreTable2) {
    let ln = line;
    while (ln != 0) {
      if (ln - st === Math.floor(ln / div) * div) {
        score += sc;
      }
      ln = Math.floor(ln / 10);
    }
    if (score !== 0) break;
  }
  return player === B ? score : -score;
}

const getEvaluateLineByGridDir = (state, dir, grid, target) => {
  let line = 0, [x, y] = grid;
  for (let i = -EvalRange; i <= EvalRange; i++) {
    const [xx, yy] = [x + dir[0] * i, y + dir[1] * i], f = Flags[EvalRange - i];
    if (!inBoundary(xx, yy)) continue;
    if (state[xx][yy] === target) line += f;
    else if (state[xx][yy] === EMPTY) line += 2 * f;
  }
  return line;
}

const getNextStates = function* (isAi, fiveState) {
  const {state, trace} = fiveState;
  let around = [];
  chessForEach(state, c => c !== EMPTY).forEach(g => {
    getAroundGird(state, g[0], g[1]).forEach(g => {
      if (state[g[0]][g[1]] === EMPTY) around.push(g[0] * RC + g[1]);
    })
  });
  around = _.shuffle(_.uniq(around)).map(g => ([Math.floor(g / RC), g % RC]));
  for (const [x, y] of around) {
    state[x][y] = isAi ? B : W;
    trace.push([x, y]);
    yield fiveState;
    state[x][y] = EMPTY;
    trace.pop([x, y]);
    yield fiveState;
  }
}

const getAroundGird = (state, x, y, radius = 2) => {
  const grids = [];
  for (let i = -radius; i <= radius; i++) {
    let r = i + x, c = y;
    if (inBoundary(r, c) && state[r][c] === EMPTY) grids.push([r, c]);
    r = x;
    c = y + i;
    if (inBoundary(r, c) && state[r][c] === EMPTY) grids.push([r, c]);
    r = x + i;
    c = y + i;
    if (inBoundary(r, c) && state[r][c] === EMPTY) grids.push([r, c]);
    r = x + i;
    c = y - i;
    if (inBoundary(r, c) && state[r][c] === EMPTY) grids.push([r, c]);
  }
  return grids;
}

const inBoundary = (x, y) => {
  return x >= 0 && x < RC && y >= 0 && y < RC;
}

const chessForEach = (state, callBack) => {
  if (!state) throw Error(state);
  const grids = [];
  for (let i = 0; i < RC; i++) {
    for (let j = 0; j < RC; j++) {
      const res = callBack ? !!callBack(state[i][j], i, j) : false;
      if (res) grids.push([i, j]);
    }
  }
  return grids;
}

const getLinesByState = (state) => {
  const ops = chessForEach(state, c => c !== EMPTY);
  const lines = new Set();
  ops.forEach(g => {
    getLinesByGrid(g[0], g[1]).forEach(l => lines.add(l));
  });
  return [...lines];
}

</script>

<style lang="less" scoped>
.five-chess {
}

.chess-container-b {
  padding: 15px;
  background-color: #ad978a;

}

.chess-container {
  top: -1px;
  left: -1px;
}

.grid-b, .grid {
  box-sizing: border-box;
  width: 31px;
  height: 31px;

}

.grid {
  &.cursor-p:hover:not(.active) {
    border-color: #e1e1e1;
  }

  border: 1px solid transparent;

  &.active {
    border-color: #ffffff;
  }
}

.row-b:first-of-type {
  .grid-b {
    border-top: 1px solid #313131;
  }
}

.grid-b {
  border-bottom: 1px solid #313131;
  border-right: 1px solid #313131;

  &:first-of-type {
    border-left: 1px solid #313131;
  }
}

.chess {
  height: 89%;
  width: 89%;
  border-radius: 50%;
}

</style>

MIT Licensed | fangjc1986@qq.com