TypeScript 井字棋实战:从交互到逻辑判定的完整实现
本文详细解析使用 TypeScript 实现井字棋游戏的全过程。内容涵盖单元格点击交互、玩家状态切换、枚举类型的应用,以及核心的胜负与平局判定逻辑。通过封装判赢函数、利用数组 some 方法优化遍历,并实现游戏重置功能,帮助开发者掌握 TypeScript 在前端游戏开发中的实际应用技巧。
单元格点击
效果:点击棋盘的任意单元格,单元格显示×(默认) 思路: 1.获取到所有的单元格列表 2.遍历单元格列表,给每一个单元格添加点击事件 3.给当前被点击的单元格添加类名 x
引入 JavaScript 文件:
1. 新建 tsc --watch index.ts 文件,并在终端中执行 tsc --watch index.ts 命令以生成对应的 JavaScript 文件。
2. 在 demo.html 文件中,于 body 结束标签之前引入生成的 index.js 文件。
<script src="./index.js"></script>编写代码
/*
单元格点击
1.获取到所有的单元格列表
2.遍历单元格列表,给每一个单元格添加点击事件
3.给当前被点击的单元格添加类名 x
*/
let cells = document.querySelectorAll('.cell')
cells.forEach(function (item, index) {
let cell = item as HTMLDivElement;
cell.addEventListener('click',function (event) {
let target = event.target as HTMLDivElement
target.classList.add('x')
})
})

代码优化: 1. 防止单元格被重复点击:在添加事件监听器时,使用 {once: true} 属性,确保每个单元格只能被点击一次。 2. 采用函数声明形式定义事件处理程序:当代码量增加时,这种结构能使代码逻辑更加清晰易读。
/*
单元格点击
1.获取到所有的单元格列表
2.遍历单元格列表,给每一个单元格添加点击事件
3.给当前被点击的单元格添加类名 x
*/
let cells = document.querySelectorAll('.cell')
cells.forEach(function (item, index) {
let cell = item as HTMLDivElement;
cell.addEventListener('click', clickCell, {once: true})
})
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add('x')
}
切换玩家
实现效果:实现玩家 'x' 和玩家 'o' 轮流交替下棋的功能。具体思路如下: 1. 创建一个变量 currentPlayer 来存储当前玩家,初始值设为 'x'。 2. 将之前硬编码在单元格类名中的 'x' 替换为变量 currentPlayer 的值。 3. 切换玩家:在添加类名(即完成一步操作)后,根据当前玩家的值,计算出另一位玩家的值。 4. 处理下一步提示:移除游戏面板中现有的 'x' 和 'o' 类名,并添加下一个玩家对应的类名。
let cells = document.querySelectorAll('.cell')
let currentPlayer = 'x'
let gameBoard = document.querySelector('#bord')
cells.forEach(function (item, index) {
let cell = item as HTMLDivElement;
cell.addEventListener('click', clickCell, {once: true})
})
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add(currentPlayer)
currentPlayer = currentPlayer === 'x'?'o':'x'
gameBoard.classList.remove('x','o')
gameBoard.classList.add(currentPlayer)
}

使用枚举
使用变量 currentPlayer 处理当前玩家存在以下问题: 1. 该变量的类型为 string,其值可以是任意字符串。 如果不小心写错(例如将 'o' 误写为 '0'),代码不会报错,但功能将无法实现,且错误难以排查。这说明 string 类型的变量取值范围过宽,无法有效限制值仅为 'x' 和 'o'。
enum Player {
X = 'x',
O = 'o'
}
let cells = document.querySelectorAll('.cell')
let currentPlayer = Player.X
let gameBoard = document.querySelector('#bord')
console.log(cells)
cells.forEach(function (item, index) {
let cell = item as HTMLDivElement;
cell.addEventListener('click', clickCell, {once: true})
})
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add(currentPlayer)
currentPlayer = currentPlayer === Player.X ? Player.O : Player.X
gameBoard.classList.remove(Player.X, Player.O)
gameBoard.classList.add(currentPlayer)
}
游戏判赢
思路
判赢逻辑如下: 判断棋盘上横向、纵向或对角线方向是否存在三个相同的符号('x' 或 'o')。只要满足其中任一条件,即判定该符号获胜。 若所有单元格均已被填充且无人获胜,则判定为平局。
通过打印页面上的单元格对象,可以发现每个单元格都拥有对应的索引值。
console.log(cells)

利用单元格的索引值,可以表示各种获胜组合(例如使用数组存储索引,如 [0, 1, 2])。
所有可能的获胜情况如下: 横向:[0,1,2]、[3,4,5]、[6,7,8] 纵向:[0,3,6]、[1,4,7]、[2,5,8] 斜向:[0,4,8]、[2,4,6]
因此,每次进行胜负判断时,都需要检查这 8 种情况。我们可以使用一个外层数组(大数组)来存储这 8 种获胜组合,因为每次判赢都需要遍历所有情况。
判断过程如下: 遍历该大数组,分别检查每种情况对应的 3 个单元格元素是否都包含相同的 'x' 或 'o' 类名。只要有一种情况满足条件,即判定获胜。
单元格元素列表说明:
单元格元素列表(cells)实际上是一个伪数组(伪数组的特征是具备 length 属性和索引)。它并非真正的数组,将鼠标悬停在 cells 上可查看其类型为 NodeListOf<Element>。

伪数组的操作方法:
1. 通过索引直接获取元素。
console. log(cells[0])
console. log (cells[1])
2. 使用 for 循环进行遍历(推荐使用 forEach 方法)。
for ( let i = 0; i < cells.length; i++){
console.log(cells[i])
)
封装判赢函数
封装函数时需考虑参数和返回值。 该函数的返回值类型是什么?应为布尔值(表示是否获胜)。 该函数是否有参数?是什么?当前玩家。 何时调用判赢?在玩家点击单元格完成下棋后。
说明:判赢逻辑旨在判断当前玩家下棋后是否获胜(若玩家未下棋,则无需进行判赢操作)。
//声明函数:
function checkwin (player: Player) : boolean { }
//调用函数:
let iswin = checkwin (currentPlayer)
接下来按步骤封装该函数: 1. 声明函数 checkwin,指定参数 player,类型注解为 Player 枚举。 2. 指定返回值:在函数内部先写死返回 true 或 false。 3. 在单元格添加类名(即下棋)后,调用函数 checkwin 并获取返回值。 4. 判断函数返回值是否为 true,若是,则说明当前玩家获胜。
......
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add(currentPlayer)
//调用判赢函数,判断是否获胜
let isWin = checkWin(currentPlayer)
if(isWin){
console.log(currentPlayer+'赢了')
}
//根据当前玩家得到另一个玩家
currentPlayer = currentPlayer === Player.X ? Player.O : Player.X
//处理下一步提示
......
}
//封装判赢函数
function checkWin(player:Player):boolean {
return true
}
接下来实现判赢函数的具体逻辑: 思路: 1. 遍历判赢数组,分别检查每种情况对应的 3 个单元格元素是否同时包含当前玩家的类名。 2. 在 some 方法的回调函数中,获取每种获胜情况对应的 3 个单元格元素。
问题:使用哪种方式遍历数组呢? 只要有一种情况满足,即表示玩家获胜,后续情况无需再遍历,因此数组遍历时可以提前终止。
判赢函数的返回值是布尔类型。如果玩家获胜(即有一种情况满足),则返回 true;否则,返回 false。
数组的 some 方法特点:1. 遍历数组时可终止;2. 方法返回值为 true 或 false。
3. 判断这 3 个单元格元素是否同时包含当前玩家的类名。
4. 如果包含(即玩家获胜),在回调函数中返回 true 以停止循环;否则,返回 false 继续下一次循环。
let winsArr = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
//封装判赢函数
function checkWin(player: Player): boolean {
let isWin = winsArr.some(function (win) {
let cellIndex1 = cells[win[0]] as HTMLDivElement;
let cellIndex2 = cells[win[1]] as HTMLDivElement;
let cellIndex3 = cells[win[2]] as HTMLDivElement;
if (cellIndex1.classList.contains(player) && cellIndex2.classList.contains(player) && cellIndex3.classList.contains(player)) {
return true
} else {
return false
}
})
return isWin;
}

可以优化:
function checkWin(player: Player): boolean {
return winsArr.some(function (win) {
let cellIndex1 = cells[win[0]];
let cellIndex2 = cells[win[1]];
let cellIndex3 = cells[win[2]];
if (hasClass(cellIndex1, player) && hasClass(cellIndex2, player) && hasClass(cellIndex3, player)) {
return true
} else {
return false
}
})
}
function hasClass(el: Element, name: string): boolean {
return el.classList.contains(name)
}
判断平局
思路:创建变量 steps 记录已下棋的次数。判断 steps 是否等于 9,若等于则说明棋盘已满且无人获胜,即为平局。
注意: 必须先进行判赢判断,再进行平局判断。
步骤: 1. 创建变量 steps,默认值为 0。 2. 在玩家下棋后,使 steps 加 1。 3. 在判赢代码之后,判断 steps 是否等于 9。 4. 如果等于 9,说明是平局,游戏结束,直接 return,不再执行后续代码。
//记录已下棋的步数
let steps = 0
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add(currentPlayer)
steps++
//调用判赢函数,判断是否获胜
let isWin = checkWin(currentPlayer)
if (isWin) {
console.log(currentPlayer + '赢了')
return;
}
//判断平局
if (steps === 9) {
console.log('平局了')
return
}
//根据当前玩家得到另一个玩家
......
}
展示获胜信息
实现效果:在获胜或平局时,展示相应的提示信息。
步骤:
1. 获取与获胜信息相关的两个 DOM 元素:① #message ② #winner。
2. 显示获胜信息面板(通过 style 属性实现)。
3. 展示获胜信息:如果获胜,展示“x赢了!” 或 “o赢了!”;如果是平局,展示“平局”。
let messageDiv = document.querySelector('#message') as HTMLDivElement
let winner = document.querySelector('#winner') as HTMLParagraphElement
//棋盘中单元格的click事件处理程序
function clickCell(event) {
......
if (isWin) {
messageDiv.style.display = "block"
winner.innerText = currentPlayer + '赢了'
return;
}
//判断平局
if (steps === 9) {
messageDiv.style.display = "block"
winner.innerText = '平局了'
return
}
......
}

重新开始游戏
实现效果:点击重新开始按钮,即可重新开始下棋游戏。
说明:重新开始游戏,实际上就是要重置游戏中的所有数据,恢复到初始状态。例如:隐藏获胜信息、重置下棋次数、清空棋盘等。
步骤: 1. 获取到重新开始按钮(#restart),并绑定点击事件。 2. 在点击事件中,重置游戏数据。 3. 隐藏获胜信息、清空棋盘、移除单元格点击事件、重新给单元格绑定点击事件。 4. 重置下棋次数、重置默认玩家为 'x'、重置下棋提示为 'x'。
//获取重新开始按钮
let restart = document.querySelector('#restart') as HTMLButtonElement
restart.addEventListener('click',startGame)
//重新开始游戏
function startGame() {
//隐藏获胜信息
messageDiv.style.display = "none"
cells.forEach(function (item,index) {
let cell = item as HTMLDivElement
//清空棋盘
cell.classList.remove(Player.X, Player.O)
//移除点击事件
cell.removeEventListener('click',clickCell)
//重新给单元格绑定事件
cell.addEventListener('click', clickCell, {once: true})
})
//重置下棋次数
steps = 0
//默认玩家x
currentPlayer = Player.X
//重置下一步提示为x
gameBoard.classList.remove(Player.X,Player.O)
gameBoard.classList.add(currentPlayer)
}
优化重新游戏功能: 原来,下棋分为:①第一次游戏 ②重新开始游戏。 现在,将第一次游戏也看作是“重新开始游戏”,就可以去掉第一次游戏时重复的初始化操作了。
优化步骤: 1. 直接调用函数 startGame,来开始游戏。 2. 移除变量 steps、currentPlayer 的默认值,并添加明确的类型注解。 3. 移除给单元格绑定事件的代码。
完整代码如下:
enum Player {
X = 'x',
O = 'o'
}
//获取所有单元格
let cells = document.querySelectorAll('.cell')
//默认玩家
let currentPlayer:Player
//游戏棋盘dom
let gameBoard = document.querySelector('#bord')
//所有获胜可能性
let winsArr = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
//记录已下棋的步数
let steps:number
//获胜信息展示相关dom
let messageDiv = document.querySelector('#message') as HTMLDivElement
let winner = document.querySelector('#winner') as HTMLParagraphElement
//获取重新开始按钮
let restart = document.querySelector('#restart') as HTMLButtonElement
restart.addEventListener('click',startGame)
//调用该函数来初始化游戏,开始游戏
startGame()
//棋盘中单元格的click事件处理程序
function clickCell(event) {
let target = event.target as HTMLDivElement
target.classList.add(currentPlayer)
steps++
//调用判赢函数,判断是否获胜
let isWin = checkWin(currentPlayer)
if (isWin) {
messageDiv.style.display = "block"
winner.innerText = currentPlayer + '赢了'
return;
}
//判断平局
if (steps === 9) {
messageDiv.style.display = "block"
winner.innerText = '平局了'
return
}
//根据当前玩家得到另一个玩家
currentPlayer = currentPlayer === Player.X ? Player.O : Player.X
//下一步提示
gameBoard.classList.remove(Player.X,Player.O)
gameBoard.classList.add(currentPlayer)
}
//封装判赢函数
function checkWin(player: Player): boolean {
return winsArr.some(function (win) {
let cellIndex1 = cells[win[0]];
let cellIndex2 = cells[win[1]];
let cellIndex3 = cells[win[2]];
if (hasClass(cellIndex1, player) && hasClass(cellIndex2, player) && hasClass(cellIndex3, player)) {
return true
} else {
return false
}
})
}
//是否包含某一样式方法
function hasClass(el: Element, name: string): boolean {
return el.classList.contains(name)
}
//重新开始游戏
function startGame() {
//隐藏获胜信息
messageDiv.style.display = "none"
//重置下棋次数
steps = 0
//默认玩家x
currentPlayer = Player.X
//重置下一步提示为x
gameBoard.classList.remove(Player.X,Player.O)
gameBoard.classList.add(currentPlayer)
cells.forEach(function (item, index) {
let cell = item as HTMLDivElement
//清空棋盘
cell.classList.remove(Player.X, Player.O)
//移除点击事件
cell.removeEventListener('click',clickCell)
//重新给单元格绑定事件
cell.addEventListener('click', clickCell, {once: true})
})
}

到此这篇关于Typescript井字棋的项目实现的文章就介绍到这了,更多相关Typescript 井字棋内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38








