export const is_empty = 'isEmpty'; /** * 机器人 */ export const is_white = 'isWhite'; export const is_black = 'isBlack'; /** * [y, x] === [row, col] === [行,列] */ export const directions = [ [1, 0], // 垂直方向 [0, 1], // 水平方向 [1, 1], // \ 方向 [1, -1] // / 方向 ] // 为什么要做这个东西,简化一下下面函数的行数 Array.prototype.group = function (fn) { return this.reduce((r, item, index, arr) => { const key = fn(item, index, arr) if (r[key]?.length) { r[key].push(item) } else { r[key] = [item] } return r }, {}) } /** * 检查 n 是否在 start 与 end 之间,但不包括 end。 * 如果 end 没有指定,那么 start 设置为0。 * 如果 start 大于 end,那么参数会交换以便支持负范围。 * @param {number} n * @param {number} start * @param {number|undefined} end */ function inRange(n, start = 0, end) { if (end === undefined) { [start, end] = [0, start] } if (start > end) { [start, end] = [end, start] } return n >= start && n < end } /** * 检查四个方向连续的棋子数 * @param {object} param * @param {number} param.row 行 y * @param {number} param.col 列 x * @param {(is_empty|is_white|is_black)[][]} param.board 棋盘 * @param {is_white|is_black|is_empty} [param.player] 当前棋子类型 * @param {number} param.win_size 需要几个棋子才赢 * @returns {[number, number][] | false} 赢棋的下标二维数组 或者 不能赢 */ export function checkWin({ row, col, board, player, win_size }) { const ROW = board.length; const COL = board[0].length let res = [] for (let i = 0; i < directions.length; i++) { res = [[row, col]]; const [dy, dx] = directions[i]; let x = col + dx; let y = row + dy; // 向正反两个方向扩展,检查是否有连续的五个相同棋子 while (inRange(x, COL) && inRange(y, ROW) && board[y][x] === player) { res.push([y, x]) x += dx; y += dy; } x = col - dx; y = row - dy; while (inRange(x, COL) && inRange(y, ROW) && board[y][x] === player) { res.push([y, x]) x -= dx; y -= dy; } // 出现五连珠,返回胜利 if (res.length >= win_size) { return res; } } // 当前空位,可以结成多少颗连珠 return false; } /** * 机器人下棋位置 * @param {(is_empty|is_white|is_black)[][]} board 棋盘 * @param {number} win_size 赢棋的棋子颗数 * @returns {number[]} */ export function robotPlay(board, win_size) { // 空位 对每个空位进行评分 const scores = board.map((item, row) => { return item.flatMap((_item, col) => { if (_item === is_empty) { // 评估每个空位置的价值,从八个方向去计算 const score = directions.reduce((r, [y, x]) => { r[is_black] += getDirectionScore(board, row, col, [y, x], win_size, is_black) r[is_white] += getDirectionScore(board, row, col, [y, x], win_size, is_white) return r }, { [is_black]: 0, [is_white]: 0 }) const _b_score = score[is_black] const _w_score = score[is_white] if (_b_score == 0 && _w_score == 0) return [] return [{ [`${row}_${col}`]: Math.max(_b_score, _w_score) }] } else { return [] } }) }).flat(1) // 按分数分组 const scores_group_obj = scores.group((item) => Object.values(item)[0]) // 找到最高分数 const max_key = Math.max(...Object.keys(scores_group_obj)) const max_scores_arr = scores_group_obj[max_key] let res_key = '' if (max_scores_arr.length === 1) { res_key = Object.keys(max_scores_arr[0])[0] } else if (max_scores_arr.length > 1) { const random_index = Math.floor(Math.random() * max_scores_arr.length) res_key = Object.keys(max_scores_arr[random_index])[0] } return res_key.split('_').map(item => parseInt(item, 10)); } /** * 计分 * 零颗棋子记 个位数 的积分 * 一颗棋子记 十位数 的积分 * 两颗棋子记 百位数 的积分 * 三颗棋子记 千位数 的积分 * 以此类推 * 计算公式 10**(n) * * 是以一条线的记录积分,一个位置上正负方向为一条线 * 当一个位置上 横 竖 斜 反斜 位置上都有棋子 * 积分最大为 4 * (10**8) = 400000000 * 积分最小为 4 * (10**0) = 4 * * 连子边界判断,连子分 **** 和 ** ** * 判断单边,双边,连子末尾是否有障碍物 * 相同的棋子数,没有挡住的比挡住的分数要大 10**(n) * * 单边,两边空位置多的那边加分 10**(n) ===== * * 黑白棋棋盘积分判断 * 比较黑白棋的棋盘最高积分,积分大的位置优先下,避免总是在防守 ===== * * 随机下棋 * 如果一个判定回合,棋盘积分中最高的积分有多个,则随机选择一个位置,避免有规律 ===== * */ /** * 获取这一边方向的信息,连珠颗数,延伸出去的空位数 * @param {(is_empty|is_white|is_black)[][]} board 棋盘 * @param {number} row 行 * @param {number} col 列 * @return {{num: number, empty_num: number}} 说明:num: 棋子个数;empty_num: 空格数量 */ function getJoinInfo(board, row, col, [y, x], player) { const ROW = board.length const COL = board[0].length // 连续棋子数 let num = 0 // 一侧到边缘的空位 let empty_num = 0 // 不包括当前位置 let _row = row + y let _col = col + x const villain = player === is_black ? is_white : is_black; while (inRange(_row, ROW) && inRange(_col, COL)) { const item = board[_row][_col] if (item === villain) { break; } // 连珠计数 if (!empty_num && item === player) { num += 1 } // 计算空位数 if (item === is_empty) { empty_num += 1 } _row += y _col += x } return { num, empty_num } } /** * 根据棋子位置信息,计算分数 * @param {(is_empty|is_white|is_black)[]} board 棋盘 * @param {number} row 行 y * @param {number} col 列 x * @param {(is_empty|is_white|is_black)[]} board 棋盘 * @param {number} win_size 需要几个棋子才赢 * @returns */ function getDirectionScore(board, row, col, [y, x], win_size, player) { const { num: r_num, empty_num: r_empty_num } = getJoinInfo(board, row, col, [y, x], player) const { num: l_num, empty_num: l_empty_num } = getJoinInfo(board, row, col, [y * -1, x * -1], player) const SIZE = r_num + l_num // 没有棋子 if (SIZE == 0) return 0 // 两边有障碍物且小于 win_size if (r_empty_num == 0 && l_empty_num == 0 && (SIZE + 1) < win_size) return 0 // 两边都没障碍物 if (r_empty_num != 0 && l_empty_num != 0) { // 单边,哪边空位置多,加分 if ((r_num != 0 && l_num == 0 && (l_empty_num + 1) > r_empty_num) || (l_num != 0 && r_num == 0 && (r_empty_num + 1) > l_empty_num)) { return (10 ** SIZE) * 2 } } // TODO:还有其他的判断,以后再做了 return 10 ** SIZE }