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] // / 方向 ] /** * @typedef PanInfo 棋盘信息 * @property {number} ROW * @property {number} COL * @property {{[p:string]: is_empty | is_white | is_black}} OBJ */ /** * 检查 n 是否在 start 与 end 之间,但不包括 end。 * 如果 end 没有指定,那么 start 设置为0。 * 如果 start 大于 end,那么参数会交换以便支持负范围。 * @param {number} n * @param {number} start * @param {number} [end] * @returns {boolean} */ 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 {PanInfo} pan_info 棋盘信息 * @param {number} y 行 * @param {number} x 列 * @returns {boolean} */ function aroundIsEmpty({ ROW, COL, OBJ }, y, x) { let res_arr = 0 for (let i = 0; i < directions.length; i++) { const [_y, _x] = directions[i] for (let j = -1; j < 3; j += 2) { if (inRange(y + _y * j, ROW) && inRange(x + _x * j, COL) && OBJ[`${_y * j + y}_${_x * j + x}`] !== is_empty) { res_arr += 1 } } } return res_arr === 0 } /** * 检查四个方向连续的棋子数 * @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]]; for (let j = -1; j < 3; j += 2) { const [dy, dx] = directions[i]; let x = col + dx * j; let y = row + dy * j; // 向正反两个方向扩展,检查是否有连续的五个相同棋子 while (inRange(x, COL) && inRange(y, ROW) && board[y][x] === player) { res.push([y, x]) x += dx * j; y += dy * j; } // 出现五连珠,返回胜利 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 pan_info = { ROW: board.length, COL: board[0].length, OBJ: board.reduce((r, r_item, r_index) => { return { ...r, ...r_item.reduce((c, c_item, c_index) => { return { ...c, [`${r_index}_${c_index}`]: c_item } }, {}) } }, {}) } // 空位 对每个空位进行评分 const scores = board.map((item, row) => { return item.flatMap((_item, col) => { if (_item === is_empty) { if (aroundIsEmpty(pan_info, row, col)) return [] // 评估每个空位置的价值,从八个方向去计算 const score = directions.reduce((r, [y, x]) => { r[is_black] += getDirectionScore(pan_info, row, col, [y, x], win_size, is_black) r[is_white] += getDirectionScore(pan_info, 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] return [{ [`${row}_${col}`]: Math.max(_b_score, _w_score) }] } return [] }) }).flat(1) // 按分数分组 const scores_group_obj = scores.reduce((r, item) => { const key = Object.values(item)[0] if (r[key]?.length) { r[key].push(item) } else { r[key] = [item] } return r }, {}) // 找到最高分数 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 */ /** * 获取这一边方向的信息,空格数,棋子数 * @param {PanInfo} pan_info 棋盘信息 * @param {number} row 行 * @param {number} col 列 * @return {[number, number, number]} [空位数,棋子数,距离障碍物的距离] */ function getDirectionsInfo({ ROW, COL, OBJ }, row, col, [y, x], player) { // 连续棋子数 let piece_num = 0 // 棋子前面的空格数 let empty_num = 0 // 距离障碍物的距离 let obstacle_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 = OBJ[`${_row}_${_col}`] if (item === villain) { break; } // 连珠计数 if (!empty_num && item === player) { piece_num += 1 } // 计算棋子前面的空格数 if (item === is_empty) { empty_num += 1 } // 计算棋子后面的空格数 _row += y _col += x obstacle_num += 1 } return [ empty_num, piece_num, obstacle_num ] } /** * 根据棋子位置信息,计算分数 * @param {PanInfo} pan_info 棋盘信息 * @param {number} row 行 y * @param {number} col 列 x * @param {(is_empty|is_white|is_black)[]} board 棋盘 * @param {number} win_size 需要几个棋子才赢 * @returns {number} 分数 */ function getDirectionScore(pan_info, row, col, [y, x], win_size, player) { const [r_empty_num, r_piece_num, r_obstacle_num] = getDirectionsInfo(pan_info, row, col, [y, x], player) const [l_empty_num, l_piece_num, l_obstacle_num] = getDirectionsInfo(pan_info, row, col, [y * -1, x * -1], player) /** * 连子边界判断,连子分 **** 和 ** ** * 判断单边,双边,连子末尾是否有障碍物 * 相同的棋子数,没有挡住的比挡住的分数要大 10**(n) * * 单边,两边空位置多的那边加分 10**(n) ===== * * 黑白棋棋盘积分判断 * 比较黑白棋的棋盘最高积分,积分大的位置优先下,避免总是在防守 ===== * * 随机下棋 * 如果一个判定回合,棋盘积分中最高的积分有多个,则随机选择一个位置,避免有规律 ===== */ const SIZE = r_piece_num + l_piece_num // 没有棋子 if (SIZE == 0) return 0 // 空间不够连成直线的 if ((r_obstacle_num + l_obstacle_num + 1) < win_size) return 0 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_piece_num != 0 && l_piece_num == 0 && (l_empty_num + 1) > r_empty_num) || (l_piece_num != 0 && r_piece_num == 0 && (r_empty_num + 1) > l_empty_num)) { return (10 ** SIZE) * 2 } } // TODO:其他判断 return 10 ** SIZE }