使用原生html编写五子棋小游戏

265 阅读3分钟

 

技术覆盖点:

        使用了原生的html+css+js 不含框架 实现该小游戏 兼容pc与移动端 该例子比较适合刚学前端的朋友巩固知识

        css知识点:

  • css变量的定义与使用 
  • flex布局
  • grid布局
  • 绝对定位的使用

        js知识点:

  • js修改css变量
  • dom的基本操作(增删改查)
  • input的输入监听
  • 二维数组棋盘进行位置记录
  • 通过dom的自定义属性去获取指定dom
  • 根据offsetTop、offsetLeft 鼠标点击的位置设置dom的位置

实现思路:

html的布局

    tools 重置与棋盘大小设置工具栏

    board 棋盘 items 棋盘格子  dots 棋盘黑点 triggers 用于点击响应的对象 chess_list 存放棋子的dom

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="./common.css" />
  </head>

  <body>
    <div class="tools">
      <div>棋盘大小: <input placeholder="请输入棋盘大小" id="num_input" /><button onclick="initBoard()">重置</button></div>
      <div class="current_drop">当前棋权:黑方</div>
    </div>
    <div class="board">
      <div class="items"></div>
      <div class="dots"></div>
      <div class="triggers"></div>
      <div class="chess_list"></div>
    </div>
    <script src="./gomoku.js" />
  </body>
</html>

css编写

        根据棋盘图片我们可以知道 棋盘是一个正方形格子布局为N*N 这时候我们就想到了grid布局

        并且 宽=高 为了在手机上也能正常使用 我们使用vh作为单位 并设置最大宽度和最小宽度 为了方便调试 定义一组css变量

  • body使用flex布局 棋盘始终保持水平与垂直居中
  • 棋盘定义为relative是因为后续的absolute定位的dom都要以board作为父级容器来拿offsetTop、offsetLeft的偏移量
  • 使用grid布局 并且使用var 与 calc 去设置每个格子的大小 --num 是可以用过js外部去设置的这样我们就可以画出不同大小的棋盘
:root {
    --w    : 80vh;
    --w_min: 320px;
    --w_max: 640px;
    --num  : 15;
}

* {
    margin    : 0;
    padding   : 0;
    box-sizing: border-box;
}

body {
    width           : 100%;
    height          : 100vh;
    background-color: #fdf6e3;
    display         : flex;
    flex-direction  : column;
    justify-content : center;
    align-items     : center;
}

.board {
    position     : relative;
    width        : var(--w);
    height       : var(--w);
    min-width    : var(--w_min);
    min-height   : var(--w_min);
    max-width    : var(--w_max);
    max-height   : var(--w_max);
    border       : 1px solid #000;
    border-right : 0;
    border-bottom: 0;
}

.items {
    width                : 100%;
    height               : 100%;
    display              : grid;
    grid-template-columns: repeat(var(--num), calc(100% / var(--num)));
    grid-template-rows   : repeat(var(--num), calc(100% / var(--num)));
}

.item {
    border-right : 1px solid #000;
    border-bottom: 1px solid #000;
}

.dot {
    position        : absolute;
    border-radius   : 50%;
    background-color: #000;
}

.chess {
    position     : absolute;
    border-radius: 50%;
    border       : 1px solid #000;
    z-index      : 2;
    transform    : translate(-40%, -40%);
}

.trigger {
    position        : absolute;
    width           : 8%;
    height          : 8%;
    background-color: transparent;
    transform       : translate(-40%, -40%);
}

.white {
    background-color: #fff;
}

.black {
    background-color: #000;
}

.highlight {
    border: 3px solid red;
}

.tools {
    margin-bottom: 20px;
    font-size    : 2vh;
}

.tools .current_drop {
    margin-top: 10px;
}

js编写

 代码量不多包含了许多注释 看注释即可 都是js的基础知识

// 棋子类型
const CHESS_TYPE = {
  BLACK: 1,
  WHITE: 2,
};

// 方向类型
const DIRECTION = {
  TOP: "top",
  BOTTOM: "bottom",
  LEFT: "left",
  RIGHT: "right",
  TOP_LEFT: "top_left",
  TOP_RIGHT: "top_right",
  BOTTOM_LEFT: "bottom_left",
  BOTTOM_RIGHT: "bottom_right",
};

// 提前获取需要修改的dom
const board_dom = document.querySelector(".board");
const items = document.querySelector(".items");
const dots = document.querySelector(".dots");
const triggers = document.querySelector(".triggers");
const chess_list = document.querySelector(".chess_list");
const num_input = document.getElementById("num_input");
const text_node = document.querySelector(".current_drop");
// 行列数
let num = 10;
// 存储的棋盘数据
let board = [];
// 当前棋权
let current = CHESS_TYPE.BLACK;
// 是否结束
let is_end = false;
// 一个格子的大小
let grid_size = 0;
// 初始化input的事件与数据
num_input.value = num;
num_input.onchange = (event) => {
  num = event.target.value;
  if (num < 8) {
    num = 8;
  } else if (num > 15) {
    num = 15;
  }
  num_input.value = num;
};
// 设置当前的棋权
function setCurrent(type) {
  current = type;
  text_node.innerText = `当前棋权:${type == CHESS_TYPE.BLACK ? "黑方" : "白方"}`;
}
// 棋盘初始化
function initBoard() {
  // 设置css变量数据
  document.body.style.setProperty("--num", num);
  // 结束标志位置为false
  is_end = false;
  setCurrent(CHESS_TYPE.BLACK);
  // 清空子节点
  for (let father of [items, dots, triggers, chess_list]) {
    father.innerHTML = "";
  }
  board = [];
  // 计算黑点的相对位置 相对边缘距离为3
  const diff = num - 3;
  // 棋盘黑点所在位置 取格子的右上角放置
  const dotsPosition = [
    { column: 3, row: 3 },
    { column: diff, row: 3 },
    { column: Math.ceil(num / 2), row: Math.ceil(num / 2) },
    { column: 3, row: diff },
    { column: diff, row: num - 3 },
  ];

  // 初始化棋盘格子
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      let item = document.createElement("div");
      item.classList.add("item");
      item.setAttribute("grid-column", j + 1);
      item.setAttribute("grid-row", num - i);
      items.append(item);
      grid_size = item.clientHeight;
    }
  }
  // 初始化棋盘黑点
  for (let item of dotsPosition) {
    let target = document.querySelector(`div[grid-column="${item.column}"][grid-row="${item.row}"]`);
    let dom = document.createElement("div");
    let size = board_dom.clientHeight * 0.02;
    let r = size / 2;
    dom.classList.add("dot");
    // 将黑点布置到点位的右上角
    dom.style.top = target.offsetTop - r + "px";
    dom.style.left = target.offsetLeft - r + target.clientHeight + "px";
    dom.style.width = size + "px";
    dom.style.height = size + "px";
    dots.append(dom);
  }
  // 初始化棋盘点击区域 这里比较关键的就是 <=num 而不是<num 
  // 五子棋的棋盘是下在点上的 不是下在格子里面 所以有n+1条横线和竖线 10*10的格子 有121个点位可以下
  // 可以调整tigger样式查看点位的情况
  for (let i = 0; i <= num; i++) {
    let rows = [];
    for (let j = 0; j <= num; j++) {
      rows.push(0);
      let trigger = document.createElement("div");
      trigger.classList.add("trigger");
      trigger.style.left = j * grid_size + "px";
      trigger.style.top = i * grid_size + "px";
      trigger.setAttribute("data-column", j);
      trigger.setAttribute("data-row", i);
      triggers.append(trigger);
      trigger.style.cursor = "pointer";
    }
    board.push(rows);
  }
  console.log("已初始化棋盘");
}

// 落子功能
function drop(event) {
  // 结束的时候不允许点击
  if (is_end) return;
  let target_dom = event.target;
  // 不是点击区域就无反馈
  if (!target_dom.classList.contains("trigger")) return;
  // 落子
  let result = createChess(target_dom);
  if (!result) return;
  let { row, column } = result;
  // 判断胜负
  let win = judge(parseInt(row), parseInt(column));
  if (win) {
    is_end = true;
    // 处理提示胜利 放在timeout里 渲染是异步的
    // 是因为还没渲染完弹窗 高亮会在弹窗后 并被alert阻塞
    setTimeout(() => {
      alert("游戏结束");
    }, 100);
    return;
  }
  // 交换棋权
  setCurrent(current == CHESS_TYPE.BLACK ? CHESS_TYPE.WHITE : CHESS_TYPE.BLACK);
}

// 创建棋子
function createChess(target) {
  let column = target.getAttribute("data-column");
  let row = target.getAttribute("data-row");
  // 判断该位置是否落子
  if (board[row][column]) return null;
  board[row][column] = current;
  // 创建棋子
  let chess = document.createElement("div");
  chess.classList.add("chess", current === CHESS_TYPE.BLACK ? "black" : "white");
  let chess_size = target.clientHeight;
  chess.style.top = row * grid_size + "px";
  chess.style.left = column * grid_size + "px";
  chess.style.width = chess_size + "px";
  chess.style.height = chess_size + "px";
  chess.setAttribute("chess-column", column);
  chess.setAttribute("chess-row", row);
  chess_list.append(chess);
  return { column, row };
}

// 判断胜负
function judge(row, column) {
  // 当前点位为中心点 若任意方向叠加获取到的同色棋子数据为5 就为赢家
  // 获取各方向上相同棋子数据
  let direction_map = {};
  for (let direction of Object.values(DIRECTION)) {
    direction_map[direction] = getDirectionChess(row, column, direction);
  }
  // 获取各方向后 判断每个方向之和是否达到五个
  for (let direction of [DIRECTION.TOP, DIRECTION.LEFT, DIRECTION.TOP_LEFT, DIRECTION.TOP_RIGHT]) {
    if (checkDirection(row, column, direction_map, direction)) {
      return true;
    }
  }
  return false;
}

// 获取方向的向量增加数据
function getDirectionSum(directions) {
  // 因为有斜向的存在 所以我们要分开定义两个位置的对应数据
  const ROW_DIRECTION = {
    top: -1,
    bottom: 1,
  };

  const COLUMN_DRECITION = {
    left: -1,
    right: 1,
  };
  // 会有top_left的存在 所以要拆解他  使用.split
  // 会出现['top']['left]['top','left]
  // 0索引有可能是垂直方向也有可能是水平方向
  // 1索引一定是水平方向的位置
  // 所以当出现left 位置时 根据我们定义的ROW_DIRECTION拿不到里面的存在 就赋值为0 就处理了两个方向的数据
  directions = directions.split("_");
  const column_sum = COLUMN_DRECITION[directions[0]] || (directions[1] && COLUMN_DRECITION[directions[1]]) || 0;
  const row_sum = ROW_DIRECTION[directions[0]] || 0;
  return { column_sum, row_sum };
}

// 获取方向上对应类型数量的棋子
function getDirectionChess(row, column, directions) {
  let count = 0;
  // 获取对应方向的索引 应该加还是减
  let { column_sum, row_sum } = getDirectionSum(directions);
  let nextColumn = column + column_sum;
  let nextRow = row + row_sum;
  // 当遇到不存在的边界或者下一个索引不是相同类型的棋子就中断
  while (board[nextRow] && board[nextRow][nextColumn]) {
    let same = board[nextRow][nextColumn] == current;
    if (same) {
      count++;
      nextColumn += column_sum;
      nextRow += row_sum;
    } else break;
  }
  return count;
}

// 检查是否连成五个并高亮
function checkDirection(row, column, direction_map, direction) {
  const handler = (from, to) => {
    // 1 代表我们落子的位置  由落子从form到to的数量达到五个及以上 就完成了五子 代表有人胜出了
    if (direction_map[from] + direction_map[to] + 1 >= 5) {
      // 拿到往from方向的索引  开始位置=中心点位置+相距棋子数量*向量
      let { column_sum, row_sum } = getDirectionSum(from);
      // 计算出 计算起始点的位置(x,y)
      let start_row = row + row_sum * direction_map[from];
      let start_column = column + column_sum * direction_map[from];
      // 获取从FROM到TO的向量数据 不用担心开始位置索引不对的问题 如果对应的map[from]不存在 拿到的都是0 起始点就是(row,column)
      let { column_sum: next_column_sum, row_sum: next_row_sum } = getDirectionSum(to);
      let dom_list = [];
      // 后记高亮点位上的棋子
      while (dom_list.length < 5) {
        let dom = document.querySelector(`div[chess-column="${start_column}"][chess-row="${start_row}"]`);
        if (!dom) break;
        dom_list.push(dom);
        start_row += next_row_sum;
        start_column += next_column_sum;
      }
      for (let dom of dom_list) {
        dom.classList.add("highlight");
      }
      return true;
    }
    return false;
  };
  // 统一处理 上到下 左到右 左斜到右下 右斜到左下的方向棋子数量判断
  const map = {
    [DIRECTION.TOP]: handler.bind(this, DIRECTION.TOP, DIRECTION.BOTTOM),
    [DIRECTION.LEFT]: handler.bind(this, DIRECTION.LEFT, DIRECTION.RIGHT),
    [DIRECTION.TOP_LEFT]: handler.bind(this, DIRECTION.TOP_LEFT, DIRECTION.BOTTOM_RIGHT),
    [DIRECTION.TOP_RIGHT]: handler.bind(this, DIRECTION.TOP_RIGHT, DIRECTION.BOTTOM_LEFT),
  };

  return map[direction]();
}
// 初始化棋盘
initBoard();
// 初始化棋盘点击事件
// 为什么设置棋盘的点击事件 而不是触发器的点击事件
// 是因为js的事件冒泡机制 https://blog.csdn.net/lph159/article/details/142134303
board_dom.onclick = drop;
window.onresize = initBoard;