【Canvas】使用Vue3+TS+Canvas实现五子棋

1.效果预览

动画.gif

👉 项目地址

2.实现思路

  • 创建画布
<canvas id="chess_canvas" ref="chess_canvas"></canvas>
  • 创建绘制对象和棋盘DOM
let ctx: CanvasRenderingContext2D;

const chess_canvas = ref<HTMLCanvasElement>();
  • 使用二维数组记录棋盘格的信息
let record: number[][] = [];
  • 记录当前要下的棋子是黑还是白
let isBlack = true;
  • 初始化画布,将画布的大小设置为600x600,边缘留出20,棋盘规模为14x14,每个格子的大小为40
  ctx = chess_canvas.value?.getContext("2d") as CanvasRenderingContext2D;
  chess_canvas.value!.width = 600;
  chess_canvas.value!.height = 600;
  chess_canvas.value!.style.background = "#e3cdb0";
  isBlack = true;
  • 绘制棋盘的横线和竖线
  for (let i = 0; i < 15; i++) {
    ctx.beginPath();
    ctx.moveTo(20, 20 + 40 * i);
    ctx.lineTo(580, 20 + 40 * i);
    ctx.stroke();
    ctx.closePath();
  }

  for (let i = 0; i < 15; i++) {
    ctx.beginPath();
    ctx.moveTo(20 + 40 * i, 20);
    ctx.lineTo(20 + 40 * i, 580);
    ctx.stroke();
    ctx.closePath();
  }
  • 初始化棋盘格数组,0代表未下,1代表黑子,2代表白子
  for (let i = 0; i < 15; i++) {
    record[i] = new Array(15).fill(0);
  }
  • 给棋盘添加点击事件,棋子只能落到格子的交叉点上,所以要对x的位置和y的位置进行取整,选取离点击点位置最近的格子,绘制棋子,之后更换棋子颜色
  chess_canvas.value!.onclick = e => {
    let x = e.offsetX - (e.offsetX % 40) + 20;
    let y = e.offsetY - (e.offsetY % 40) + 20;

    let pX = (x - 20) / 40;
    let pY = (y - 20) / 40;
    // 棋盘的X和Y和二维数组的X和Y是相反的
    if (record[pY][pX] != 0) {
      return;
    }

    ctx.beginPath();
    ctx.arc(x, y, 10, 0, 2 * Math.PI);
    ctx.fillStyle = isBlack ? "black" : "white";
    ctx.fill();
    ctx.closePath();

    record[pY][pX] = isBlack ? 1 : 2;
    isBlack = !isBlack;
  };
  • 在下过一个棋子之后,判断这颗棋子是否能和棋盘中的其他棋子连成5颗。
    我的做法是这样的(自己想的,代码量不多,有不足之处请指教):每下过一颗棋子,以该棋子为中心,读取【上4颗+该棋子+下4颗】,【左4颗+该棋子+右4颗】,【左上对角4颗+该棋子+左下对角4颗】,【右上对角4颗+该棋子+右下对角4颗】,存储为4个数组:


    image.png

    然后依次判断这4个数组中,是否有连续5个相同的数字(数字的值等于刚刚下过的棋子的值),判断算法是这样设计的:一个数组最大长度为9,若该数组中含有5个相同的数字,那么该数组的中间数字必定为这5个数字中的一个,然后以该中间数为中心,若左边的数和中间数相等,则一直向左寻找边界,若右边的数和中间的数相等,则一直向右寻找边界,如果找到左右边界,判断左右边界的差值,如果差值为4,那么已经连成了5子。

const isWin = (x: number, y: number) => {
  let res: number[][] = [];
  let xadd = [];
  let yadd = [];
  let zadd = [];
  let wadd = [];
  let flag = false;
  // 上 下 左 右 +4
  for (let i = 4; i >= -4; i--) {
    if (x - i >= 0 && x - i <= 14) {
      xadd.push(record[x - i][y]);
    }
    if (y - i >= 0 && y - i <= 14) {
      yadd.push(record[x][y - i]);
    }
    if (x - i >= 0 && x - i <= 14 && y - i >= 0 && y - i <= 14) {
      zadd.push(record[x - i][y - i]);
    }
    if (x + i >= 0 && x + i <= 14 && y - i >= 0 && y - i <= 14) {
      wadd.push(record[x + i][y - i]);
    }
  }
  res.push(xadd);
  res.push(yadd);
  res.push(zadd);
  res.push(wadd);

  let target = record[x][y];
  res.forEach(arr => {
    let mid = Math.floor(arr.length / 2);
    let left = mid;
    let right = mid;
    while (arr[left] == target) {
      left--;
    }
    while (arr[right] == target) {
      right++;
    }
    if (right - 1 - (left + 1) == 4) {
      flag = true;
    }
  });
  return flag;
};
  • 连成5子后,显示胜利弹窗,可以选择重玩
    setTimeout(() => {
      if (isWin(pY, pX)) {
        const con = confirm(`${!isBlack ? "黑棋" : "白棋"}赢了!是否重新开局?`);
        ctx.clearRect(0, 0, 600, 600);
        con && initCanvas();
      }
    }, 10);

3.全部代码

  <div class="box">
    <canvas id="chess_canvas" ref="chess_canvas"></canvas>
  </div>
import { onBeforeUnmount, onMounted, ref } from "vue";
/** 绘制对象 */
let ctx: CanvasRenderingContext2D;

/** 棋盘DOM */
const chess_canvas = ref<HTMLCanvasElement>();

/** 记录棋盘格信息的数组 */
let record: number[][] = [];

/** 当前是否要下黑棋 */
let isBlack = true;

/** 初始化 */
const initCanvas = () => {
  ctx = chess_canvas.value?.getContext("2d") as CanvasRenderingContext2D;
  chess_canvas.value!.width = 600;
  chess_canvas.value!.height = 600;
  chess_canvas.value!.style.background = "#e3cdb0";

  isBlack = true;

  drawCheckerboard();
};

/** 绘制棋盘:每个棋格大小40*40 */
const drawCheckerboard = () => {
  for (let i = 0; i < 15; i++) {
    ctx.beginPath();
    ctx.moveTo(20, 20 + 40 * i);
    ctx.lineTo(580, 20 + 40 * i);
    ctx.stroke();
    ctx.closePath();
  }

  for (let i = 0; i < 15; i++) {
    ctx.beginPath();
    ctx.moveTo(20 + 40 * i, 20);
    ctx.lineTo(20 + 40 * i, 580);
    ctx.stroke();
    ctx.closePath();
  }

  /** 初始化棋盘格数组,0代表未下,1代表黑子,2代表白子 */
  for (let i = 0; i < 15; i++) {
    record[i] = new Array(15).fill(0);
  }

  /** 给棋盘添加点击事件 */
  chess_canvas.value!.onclick = e => {
    // 进行取整,确保棋子落在最近的棋盘格交叉点上
    let x = e.offsetX - (e.offsetX % 40) + 20;
    let y = e.offsetY - (e.offsetY % 40) + 20;

    let pX = (x - 20) / 40;
    let pY = (y - 20) / 40;

    if (record[pY][pX] != 0) {
      return;
    }

    ctx.beginPath();
    ctx.arc(x, y, 10, 0, 2 * Math.PI);
    ctx.fillStyle = isBlack ? "black" : "white";
    ctx.fill();
    ctx.closePath();

    record[pY][pX] = isBlack ? 1 : 2;
    isBlack = !isBlack;

    setTimeout(() => {
      if (isWin(pY, pX)) {
        const con = confirm(`${!isBlack ? "黑棋" : "白棋"}赢了!是否重新开局?`);
        ctx.clearRect(0, 0, 600, 600);
        con && initCanvas();
      }
    }, 10);
  };
};

const isWin = (x: number, y: number) => {
  let res: number[][] = [];
  let xadd = [];
  let yadd = [];
  let zadd = [];
  let wadd = [];
  let flag = false;
  // 上 下 左 右 +4
  for (let i = 4; i >= -4; i--) {
    if (x - i >= 0 && x - i <= 14) {
      xadd.push(record[x - i][y]);
    }
    if (y - i >= 0 && y - i <= 14) {
      yadd.push(record[x][y - i]);
    }
    if (x - i >= 0 && x - i <= 14 && y - i >= 0 && y - i <= 14) {
      zadd.push(record[x - i][y - i]);
    }
    if (x + i >= 0 && x + i <= 14 && y - i >= 0 && y - i <= 14) {
      wadd.push(record[x + i][y - i]);
    }
  }
  res.push(xadd);
  res.push(yadd);
  res.push(zadd);
  res.push(wadd);

  let target = record[x][y];
  res.forEach(arr => {
    let mid = Math.floor(arr.length / 2);
    let left = mid;
    let right = mid;
    while (arr[left] == target) {
      left--;
    }
    while (arr[right] == target) {
      right++;
    }
    if (right - 1 - (left + 1) == 4) {
      flag = true;
    }
  });
  return flag;
};

onMounted(() => {
  initCanvas();
});
onBeforeUnmount(() => {
  ctx.clearRect(0, 0, 600, 600);
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容