Typescript+vite+sass手把手实现五子棋游戏(放置类)

Typescript+vite+sass手把手实现五子棋游戏(放置类)

下面有图片和gif可能没加载出来

上面有图片和gif可能没加载出来

导言

最近练习Typescript,觉得差不多了,就用这个项目练练手,使用Typescript纯面向对象编程。

开源地址

试玩地址:试玩地址 (zou-hong-run.github.io)

代码地址:zou-hong-run/dobang: Typescript+vite+sass拖拽放置五子棋 (github.com)

视频演示地址:www.bilibili.com/video/BV1JX...

功能介绍

用户将棋子放置在棋盘上,首先将五颗棋子连成线的用户胜利

游戏功能

  • 开始游戏
  • 用户开始交替放置棋子
  • 放置棋子后该棋子会被禁用,直到对方下子,方可解
  • 五子连成线胜利
  • 重新游戏

项目介绍

使用Typescript+vite+sass构建项目

typescript:类型提示不要太爽。

vite:轻松编译打包项目,减少配置时间

sass:简化css书写

项目搭建

使用vite初始化项目

这里使用vite作为脚手架搭建 因为可以很好的将Typescript和html等结合到一块 打包压缩更方便 支持热更新

你可以使用npm,yarn或pnpm

sql 复制代码
 npm create vite@latest
 yarn create vite
 pnpm create vite

这里我使用的pnpm

arduino 复制代码
 pnpm create vite
 // 项目名
 √ Project name: ... gobang
 // 原生代码,没有框架支持
 √ Select a framework: >> Vanilla
 // 使用ts
 √ Select a variant: >> TypeScript
   cd gobang   
   pnpm install
   pnpm run dev

安装sass

方便书写scss,-D装开发依赖

csharp 复制代码
 pnpm add sass -D

项目目录结构

  • dist

    • 最终打包文件
  • public

    • 图片资源等
  • src

    • 源码入口

    • css

      • 样式
    • script

      • ts代码放置
    • main.ts

      • 代码主入口
  • index.html

    • 网页文件
  • tsconfig.json

    • ts配置文件
  • package.json

    • 包管理文件

前端页面布局

index.html布局

游戏首页index.html

  • #black_piece左边黑子
  • #white_piece右边白子
  • #container_center棋盘
  • #restart 重新游戏
xml 复制代码
 <!doctype html>
 <html lang="zh">
 ​
 <head>
   <meta charset="UTF-8" />
   <link rel="icon" type="image/svg+xml" href="/vite.svg" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Typescript五子棋</title>
 </head>
 ​
 <body>
   <div id="container">
     <div id="container_left">
       <h1>黑棋</h1>
       <button id="black_piece"></button>
     </div>
     <div id="container_center">
       <div id="title">五子棋对决(等待白棋落子)</div>
       <div id="game"></div>
     </div>
     <div id="container_right">
       <h1>白棋</h1>
       <button id="white_piece"></button>
     </div>
     <div id="restart" class="none">
       <button></button>
     </div>
   </div>
   <script type="module" src="./src/main.ts"></script>
 </body>
 ​
 </html>

sass样式

src/css/style.scss

比原生css简直不要太舒服

css 复制代码
 @use "sass:math";
 ​
 * {
     padding: 0;
     margin: 0;
     box-sizing: border-box;
 }
 ​
 html,
 body {
     min-width: 660px;
     min-height: 660px;
     width: 100%;
     height: 100%;
 }
 ​
 $centerWidthAndHeight: 660px;
 $leftAndRightWidth: calc((100% - $centerWidthAndHeight)/2);
 // $centerWidth: 100% - $leftAndRightWidth * 2;
 // $pieceWidthAndHeight:math.div(100%,1);
 $pieceWidthAndHeight: 60px;
 ​
 .none{
     display: none !important;
 }
 #container {
     width: 100%;
     height: 100%;
     display: flex;
     text-align: center;
     user-select: none;
 ​
     h1 {
         user-select: none;
     }
 ​
     &_left,
     &_right {
         min-width: 100px;
         width: $leftAndRightWidth;
         height: 100%;
         display: flex;
         justify-content: center;
         align-items: center;
         background-color: #C6BA8A;
     }
     // 这里的样式共用
     #black_piece {
         width: $pieceWidthAndHeight;
         height: $pieceWidthAndHeight;
         background-image: url("../public/imgs/blackPiece.png");
         background-size: 100% 100%;
         border-radius: 50%;
         user-select: all;
     }
 ​
     #black_piece:hover {
         border: 2px double white;
     }
 ​
     #white_piece {
         width: $pieceWidthAndHeight;
         height: $pieceWidthAndHeight;
         background-image: url("../public/imgs/whitePiece.png");
         background-size: 100% 100%;
         border-radius: 50%;
         user-select: all;
     }
 ​
     #white_piece:hover {
         border: 2px double black;
     }
 ​
 ​
     &_center {
         width: $centerWidthAndHeight;
         height: 100%;
         background-image: url('../public/imgs/background.png');
         background-repeat: no-repeat;
         background-size: cover;
 ​
         #title {
             background-color: #C6BA8A;
             // opacity: .9;
             height: calc(100% - $centerWidthAndHeight);
         }
 ​
         #game {
             // user-select: all;
             width: $centerWidthAndHeight;
             height: $centerWidthAndHeight;
             position: relative;
             display: flex;
             flex-wrap: wrap;
         }
     }
 ​
     #restart{
         width: 100%;
         height: 100%;
         position: absolute;
         display: flex;
         justify-content: center;
         align-items: center;
         background-color: rgba(133, 132, 132,0.5);
         button{
             width: 25%;
             height: 20%;
             background: url("../../public/imgs/restart.png");
             background-size:  100% 100%;
         }
     }
 }

工具类封装

src/Utils.ts

此类封装了公用的静态方法

  • clone

    • 克隆元素设置属性
typescript 复制代码
 export default class Utils {
     static clone(
         target: HTMLElement,
         options: Partial<{
             width: string,
             height: string,
             draggable: boolean,
             userSelect: string,
             x:string,
             y:string
         }>
     ): HTMLElement {
         let { width, height, draggable, userSelect,x,y } = options;
         let cloneNode = target.cloneNode(true) as HTMLButtonElement;
         if (width) {
             cloneNode.style.cssText += `
                 width:${width};
             `
         }
         if (height) {
             cloneNode.style.cssText += `
                 height:${height};
             `;
         }
         cloneNode.draggable = draggable as boolean;
         // 根据父元素的坐标记录该元素的坐标
         cloneNode.dataset.x = x;
         cloneNode.dataset.y = y;
         if (userSelect) {
             cloneNode.style.cssText += `
                 user-select:${userSelect};
             `
         }
         return cloneNode;
 ​
 ​
     }
 ​
 }

游戏逻辑

项目入口

main.ts

  • 导入scss样式
  • 实例化Game类
javascript 复制代码
 import './css/style.scss'
 import Game from './script/Game'
 ​
 // 白子优先
 new Game()

Game类

src/Game.ts 游戏控制类,控制各个类的协调工作

  • 初始参数

  • 创建棋盘

    • new Board
  • 创建黑/白棋子

    • new Piece
  • 等待Board触发的回调函数

    • countPieceCallBack

      • 传入最新棋子数和当前的放在棋盘的棋子
  • 判断胜负

    • isWin

      • 根据isPieceFullFive函数判断是否胜利
    • isPieceFullFive

      • 判断落子点的四周是否五子连续
  • 重新游戏功能

  • 改变标题

kotlin 复制代码
 import Board from './Board';
 import Piece from './Piece';
 ​
 ​
 export type countPieceCallBack = (count: number, currentPiece: HTMLElement) => void
 type plainArr = ({
     posX: number;
     posY: number;
     name: string;
 } | {
     posX: number;
     posY: number;
     name: null;
 })[]
 ​
 export default class Game {
     // 标题元素
     public titleEle:HTMLElement;
     // 白字优先
     public firstWhite: boolean;
     // 棋盘对象
     public board: Board;
     // 黑子对象
     public blackPiece: Piece;
     // 白子对象
     public whitePiece: Piece;
 ​
     // 当前棋盘棋子数量
     public pieceCount: number;
     // 当前落子
     public currentPiece: HTMLElement | undefined
     constructor() {
         this.titleEle = document.querySelector("#title")!;
         this.firstWhite = true;// 白子优先
         this.board = new Board(this.countPieceCallBack.bind(this)); // 初始化棋盘
         // 初始化白棋子
         this.blackPiece = new Piece("black_piece", this.firstWhite);
         // 初始化黑棋子
         this.whitePiece = new Piece("white_piece", this.firstWhite);
         // 刚开始为零
         this.pieceCount = 0;
     }
     // 传给Board触发的回调函数
     countPieceCallBack(count: number, currentPiece: HTMLElement) {
         // board告诉game棋子数量变化了
         console.log("board计数", count);
         // 实时记录最新棋子数量
         this.pieceCount = count
         // 交换顺序
         this.firstWhite = !this.firstWhite;
         // 通知棋子修改显示状态
         this.blackPiece.setFirstWhite(this.firstWhite)
         this.whitePiece.setFirstWhite(this.firstWhite);
         // 记录当前棋子
         this.currentPiece = currentPiece
         // 当前棋子是什么名字
         let currentPieceName = this.currentPiece?.id;
         // 改变标题
         this.changeTitle(currentPieceName);// 判断胜负
         if(this.isWin()){
             if(currentPieceName==='black_piece'){
                 alert("黑子获胜!!!");
                 this.changeBackGround(currentPieceName)
             }else{
                 alert("白子获胜!!!")
                 this.changeBackGround(currentPieceName)
             }
             this.addRestartPage()
         }
     }
     addRestartPage(){
         (document.querySelector("#restart")as HTMLDivElement).classList.remove("none");
         (document.querySelector("#restart button")as HTMLButtonElement).addEventListener("click",()=>{
             window.location.reload()
         })
     }
     changeBackGround(currentPieceName:string){
         let bodycontainer_center = document.querySelector("#container_center") as HTMLDivElement
         if(currentPieceName==='black_piece'){
             bodycontainer_center.style.background = `url("../imgs/blackWin.png")`
         }else{
             bodycontainer_center.style.background = `url("../imgs/whiteWin.png")`
         }
     }
     changeTitle(currentPieceName:string){
         this.titleEle.innerText = (currentPieceName==='white_piece'?"(等待黑子落子)-":"(等待白子落子)-")+"总步数:"+this.pieceCount;
     }
     // 判断胜负
     isWin():boolean{
         // 两种判断,一种全盘判断,一种判断当前落子及其周围是否连成五子
         // 这里判断当前落子地方及其周围是否连成五子即可
         if (this.pieceCount >= 8) {
             let allPiece = this.board.getAllPiece();
             let dataset = this.currentPiece?.dataset;
             let { x, y } = dataset!;
             let currentPieceName = this.currentPiece?.id;
             let currentPieceposX = parseInt(x!);
             let currentPieceposY = parseInt(y!);
             // 提纯allPiece
             let plainArr = Array.from(allPiece).map(item => {
                 let children = item.children[0] as HTMLButtonElement
                 if (children) {
                     let name = children.id;
                     let { x, y } = children.dataset;
                     return {
                         posX: parseInt(x!),
                         posY: parseInt(y!),
                         name
                     }
                 }
                 return {
                     posX: parseInt(x!),
                     posY: parseInt(y!),
                     name: null
                 }
             })
             // 当前落子的位置
             let currentPiecePos = {
                 X: currentPieceposX,
                 Y: currentPieceposY,
                 name: currentPieceName!
             }
             // 判断是否五子
             // 竖直方向
             if(this.isPieceFullFive(currentPiecePos, plainArr,0,1)){
                 return true;
             }
             // 横向
             if(this.isPieceFullFive(currentPiecePos, plainArr,1,0)){
                 return true
             }
             // 45度向
             if(this.isPieceFullFive(currentPiecePos, plainArr,1,1)){
                 return true
             }
             // 135度向
             if(this.isPieceFullFive(currentPiecePos, plainArr,-1,1)){
                 return true
             }
         }
         if (this.pieceCount == 255) {
             alert("平局");
             return true;
         }
         return false
     }
     // 检查从当前位置的竖向,横向,45度向,135度向,的棋子数量是否大于五
     isPieceFullFive(currentPiecePos: { X: number, Y: number, name: string }, plainArr:plainArr,directX:number,directY:number):boolean {
         let { X, Y, name } = currentPiecePos;
         let tempPos = {
             x:0,
             y:0
         };
         let count = 0;
         // 从落点位置分为 正方向和反方向
         // 反方向
         for(let i=1;i<5;i++){
             tempPos.x = X - directX*i;
             tempPos.y = Y - directY*i;
             if(!plainArr.find(item=>item.name === name&&item.posX === tempPos.x&&item.posY===tempPos.y)){
                 break;
             }
             count++;
         }
         // 正方向
         for(let i=1;i<5;i++){
             tempPos.x = X + directX*i;
             tempPos.y = Y + directY*i;
             if(!plainArr.find(item=>item.name === name&&item.posX === tempPos.x&&item.posY===tempPos.y)){
                 break;
             }
             count++;
         }
         // 
         if(count>=4){
             // 当前棋子+count=5 游戏胜利
             return true;
         }
         return false
     }
 }

Board类

src/Board.ts 棋盘类,控制棋盘格子生成

  • 初始化棋盘参数

  • 初始化棋盘

    • emitGameCountPiece

      • Game传来的回调函数
    • initBoard

      • 创建15*15的棋盘
    • addEventListenerSetGrid

      • 给每个棋盘格子都监听放置事件,棋子放置到网格才触发
    • addEventListenerSetPiece

      • 只要有落子,就会触发该函数
      • 触发Game传来的回调函数emitGameCountPiece
    • getAllPiece

      • 得到棋盘并且包括棋盘中的所有棋子
kotlin 复制代码
 import Utils from './Utils'
 import {type countPieceCallBack} from './Game'
 export default class Board {
     // 棋盘行和列
     private row: number;
     private col: number;
     // 网页游戏区域宽高
     // 游戏区域
     private game: Element;
     private gameWidth: number;
     private gameHeight: number;
     // 棋盘网格中的单个元素宽高
     private oneGridWidth: number
     private oneGridHeight: number
     // 记录棋盘中的棋子数量
     public pieceCount: number;
     // Game传过来的函数,告诉game当前棋盘上的棋子数
     public emitGameCountPiece: countPieceCallBack;
     // 记录当前放置的棋子
     public crrentPiece:HTMLElement|undefined;
     constructor(emitGameCountPiece:countPieceCallBack) {
         this.row = 15;
         this.col = 15;
         this.game = document.querySelector("#game")!;
         this.gameWidth = this.game?.clientWidth!
         this.gameHeight = this.game?.clientHeight!
         this.oneGridWidth = this.gameWidth / this.row
         this.oneGridHeight = this.gameHeight / this.col
         this.pieceCount = 0;
         this.emitGameCountPiece = emitGameCountPiece;
 ​
         this.initBoard()
     }
     initBoard() {
         this.initGrid()
         this.addEventListenerSetPiece()
     }
     // 初始化棋盘网格
     initGrid() {
         let fragment = document.createDocumentFragment();
         for (let i = 0; i < this.col; i++) {
             for (let j = 0; j < this.row; j++) {
                 // 添加网格
                 let grid = document.createElement('div');
                 grid.style.cssText = `
                     border:1px solid black;
                     width:${this.oneGridWidth}px;
                     height:${this.oneGridHeight}px;
                     user-select:none;
                     position:relative;
                 `
                 grid.draggable = false;
                 grid.dataset.x = j + "";
                 grid.dataset.y = i + "";
 ​
                 // 给每个网格监听放置棋子事件
                 this.addEventListenerSetGrid(grid)
                 // 给文档片段添加元素
                 fragment.appendChild(grid);
             }
         }
         this.game.appendChild(fragment)
     }
     // 每一个网格都设置一个放置事件
     addEventListenerSetGrid(ele: Element) {
         let that = this;
         // 我们可以看到对于被拖拽元素,事件触发顺序是 dragstart->drag->dragend;
         // 对于目标元素,事件触发的顺序是 dragenter->dragover->drop/dropleave。
         ele.addEventListener("dragover", (e) => {
             // e.stopPropagation()
             e.preventDefault()
         });
         // 防止一个网格放置多个棋子
         const disableSecondDrop = function () {
             // 棋盘监听放子会加一,所以这里我们减一
             that.pieceCount--;
             // 告诉Game类型
             that.emitGameCountPiece(that.pieceCount,that.crrentPiece!)
             alert("此处已经放置元素");
             return false;
         }
         // 一个网格放置一个棋子
         const drop = function (e: Event) {
             let parent = ele as HTMLElement;
             let parentWidth = parent.style.width;
             let parentHeight = parent.style.height;
             let x = parent.dataset.x;
             let y = parent.dataset.y;
             if (e instanceof DragEvent) {
                 console.log("棋子放置在棋盘上,得到ID");
                 let pieceId = e.dataTransfer?.getData("ID");
                 let pieceEle = document.getElementById(`${pieceId}`)!;
                 // 克隆一个新的棋子
                 let clonePiece = Utils.clone(
                     pieceEle,
                     {
                         width: parentWidth,
                         height: parentHeight, 
                         draggable: false, 
                         userSelect: "none",
                         x:x,
                         y:y
                     });
                 // 添加到网格中
                 parent.appendChild(clonePiece!)
                 // 记录该棋子的坐标
                 that.crrentPiece = clonePiece;
                 // 禁止该网格放置多个元素
                 parent.addEventListener("drop", disableSecondDrop)
                 // 清除放置事件
                 parent.removeEventListener("drop", drop)
             }
         }
         ele.addEventListener("drop", drop)
 ​
     }
     // 监听棋子放置事件
     addEventListenerSetPiece() {
         this.game.addEventListener("dragover", (e) => {
             e.preventDefault()
         })
         this.game.addEventListener("drop", () => {
             console.log("棋盘监听到棋子放下");
             this.pieceCount++;
             console.log("棋盘上的棋子数加一", this.pieceCount);
             // 告诉Game类 数量改变
             this.emitGameCountPiece(this.pieceCount,this.crrentPiece!)
         })
     }
     // 得到棋盘并且包括棋盘中的所有棋子
     getAllPiece(){
         let gameChild = this.game.children;
         return gameChild;
     }
 ​
 }

Piece类

src/Piece.ts 棋子类,控制棋子的各种属性

  • 初始化棋子信息

  • addEventListenerDrag

    • 给黑白棋子添加拖拽事件监听
  • togglePiece

    • 切换黑白棋子放子顺序
  • setFirstWhite

    • 修改当前黑白棋子放子顺序
kotlin 复制代码
 ​
 export default class Piece {
     private piece: HTMLButtonElement;
     private firstWhite: boolean;
     public name: string;
     constructor(name: string, firstWhite: boolean) {
         this.name = name;
         this.firstWhite = firstWhite;
         this.piece = document.getElementById(`${name}`) as HTMLButtonElement;
         this.addEventListenerDrag()
         this.togglePiece()
     }
     // 修改当前棋子状态
     setFirstWhite(value: boolean) {
         this.firstWhite = value;
         this.togglePiece()
     }
     // 根据isBlack的值禁用左边或者右边棋盘
     togglePiece() {
         // 判断当前是黑棋还是白棋
         let isBlack = this.name === 'black_piece'
         if (isBlack) {
             // 黑棋,白棋先手禁用黑棋
             this.firstWhite ? (this.piece.draggable = false) : (this.piece.draggable = true);
             this.firstWhite ? (this.piece.disabled = true) : (this.piece.disabled = false);
             this.firstWhite ? (this.piece.style.opacity = "0.5") : (this.piece.style.opacity = "1");
             console.log("黑棋先手","draggable:",this.piece.draggable,"disabled:",this.piece.disabled,this.piece.style.opacity);
 ​
         }else{// 白棋 白棋先手 显示白棋
             this.firstWhite ? (this.piece.draggable = true) : (this.piece.draggable = false)
             this.firstWhite ? (this.piece.disabled = false) : (this.piece.disabled = true);
             this.firstWhite ? (this.piece.style.opacity = "1") : (this.piece.style.opacity = "0.5");
             console.log("白棋先手","draggable:",this.piece.draggable,"disabled:",this.piece.disabled,this.piece.style.opacity);
         }
 ​
     }
     // 监听器棋子被拖拽
     addEventListenerDrag() {
         // 我们可以看到对于被拖拽元素,事件触发顺序是 dragstart->drag->dragend;
         // 对于目标元素,事件触发的顺序是 dragenter->dragover->drop/dropleave。
         this.piece.addEventListener("dragstart", (e) => {
             console.log("棋子开始被拖拽,设置ID");
             if (e instanceof DragEvent) {
                 e.dataTransfer?.setData("ID", (e.target as Element).id)
             }
         });
         this.piece.addEventListener("drag", (e) => {
             // e.stopPropagation()
             e.preventDefault()
         });
         this.piece.addEventListener("dragend", (e) => {
             if (e instanceof DragEvent) {
                 // console.log("棋子被放置");
             }
         })
     }
 ​
 }

总结

  • 练习本项目,可以提高Typescript使用技巧,理解面向对象知识,提示编码能力
  • 项目还很有多不足,请大家多多指教
  • 大佬们觉得不错的话,请三连支持一下!!!!
相关推荐
离&染8 分钟前
vue.js2.x + elementui2.15.6实现el-select滚动条加载数据
前端·javascript·vue.js·el-select滚动加载
kirinlau17 分钟前
pinia状态管理在vue3项目中的用法详解
前端·javascript·vue.js
zhuà!1 小时前
腾讯地图TMap标记反显,新增标记
前端·javascript·vue.js
未知原色1 小时前
web worker使用总结(包含多个worker)
前端·javascript·react.js·架构·node.js
inferno2 小时前
JavaScript 基础
开发语言·前端·javascript
开发者小天2 小时前
React中useMemo的使用
前端·javascript·react.js
1024肥宅2 小时前
JS复杂去重一定要先排序吗?深度解析与性能对比
前端·javascript·面试
趣知岛2 小时前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化
im_AMBER2 小时前
weather-app开发手记 04 AntDesign组件库使用解析 | 项目设计困惑
开发语言·前端·javascript·笔记·学习·react.js
小沐°3 小时前
vue3-ElementPlus出现Uncaught (in promise) cancel 报错
前端·javascript·vue.js