TypeScript --- 练习案例 --- 贪吃蛇
0. 开发环境搭建
package.json
{
"name": "typescript_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack server --open chrome.exe"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.38.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.41",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.0.0",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true,
"noEmitOnError": true
}
}
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// 指定入口文件
entry: "./src/main.ts",
mode: "production",
// 打包配置
output: {
// 指定打包后的文件所在目录
path: path.resolve(__dirname, "./dist"),
// 打包后的文件名
filename: "bundle.js",
// 配置打包环境
environment: {
arrowFunction: false,
const: false
}
},
// 指定 webpack 打包时要使用的模块
module: {
// 指定加载的规则
rules: [
{
// test 指定的是规则生效的文件, 正则表达式
test: /\.ts$/,
// 要使用的 loader
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
"chrome": "88",
"ie": "11"
},
"corejs": "3",
"useBuiltIns": "usage"
}]
]
}
},
"ts-loader"
],
// 要排除的文件
exclude: /node_modules/,
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
browsers: ["last 2 versions"],
}
]
]
}
}
},
"less-loader"
]
}
]
},
// 配置 webpack 插件
plugins: [
new HTMLWebpackPlugin({
// 设置模版路径
template: path.resolve(__dirname, "./src/index.html"),
title: "贪吃蛇"
}), // webpack 管理 html 页面的插件
new CleanWebpackPlugin(), // 每次打包前先清除 dist 文件夹下的所有打包文件
],
// 用来配置引用模块, 哪些模块可以作为模块使用
resolve: {
extensions: [".ts", ".js"]
}
}
执行命令
npm i
npm run start
1. 设计图

2. 页面构建
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="container">
<!-- 游戏的舞台 -->
<div class="stage">
<!-- 蛇的容器 -->
<div id="snake">
<!-- 蛇的各个部分 -->
<div></div>
</div>
<!-- 食物 -->
<div id="food">
<!-- 添加四个小 div 来设置食物的样式 -->
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<!-- 游戏的记分牌 -->
<div class="score-panel">
<!-- 左侧记分牌 -->
<div>
SCORE: <span id="score">0</span>
</div>
<!-- 右侧难度等级 -->
<div>
LEVEL: <span id="level">1</span>
</div>
</div>
</div>
</body>
</html>
index.less
// 设置变量
@bg-color: #b7d4a8;
// 清除默认样式
* {
margin: 0;
padding: 0;
// 改变盒子模型的计算方式, 以 border 为基础开始计算
box-sizing: border-box;
}
body {
font: bold 20px "Courier";
}
.container {
width: 360px;
height: 420px;
background-color: @bg-color;
margin: 100px auto;
border: 10px solid black;
border-radius: 30px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
// 游戏舞台样式
.stage {
width: 304px;
height: 304px;
border: 2px solid black;
position: relative;
// 蛇容器
#snake {
& > div {
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
// 开启绝对定位
position: absolute;
}
}
#food {
width: 10px;
height: 10px;
border: 1px solid @bg-color;
// 开启绝对定位
position: absolute;
display: flex;
flex-flow: row wrap; // 设置主轴换行
// 设置主轴和侧轴的空白空间分配到元素中间
justify-content: space-between;
align-content: space-between;
& > div {
width: 4px;
height: 4px;
background-color: black;
transform: rotate(45deg); // 旋转 45 度
}
}
}
// 记分牌
.score-panel {
width: 300px;
height: 50px;
display: flex;
justify-content: space-between;
}
}
3. TS 交互
- 食物的 X Y 轴坐标和蛇的X Y 轴坐标重合时, 证明蛇迟到了这个食物
- 为了保证食物的位置出现在游戏盘中, 食物的坐标位置, 最小为0, 最大是290,蛇每次移动为 10px ,所以要设置食物位置必须为 10 的整倍数
Food.ts
// 定义食物类
class Food {
// 定义一个属性来表示食物所对应的元素
element: HTMLElement
widthDivideTen: number // 舞台的最大宽度 / 10
constructor(widthDivideTen: number = 29) {
this.widthDivideTen = widthDivideTen
// 获取页面中的 food 元素
this.element = document.getElementById("food")!;
}
// 获取食物 X轴 坐标的方法
get X() {
return this.element.offsetLeft
}
// 获取食物 Y轴 坐标的方法
get Y() {
return this.element.offsetTop
}
// 随机生成食物出现位置的方法
change() {
// 食物的坐标位置, 最小为0, 最大是290,
// 蛇每次移动为 10px ,所以要设置食物位置必须为 10 的整倍数
let top = Math.round(Math.random() * this.widthDivideTen) * 10
let left = Math.round(Math.random() * this.widthDivideTen) * 10
this.element.style.left = left + 'px'
this.element.style.top = top + 'px'
}
}
export default Food
ScorePanel.ts
// 定义记分牌的类
class ScorePanel {
private score: number = 0
private level: number = 1
maxLevel: number // 最高等级
upNum: number // 多少分的倍数可以升级
private scoreSpan = HTMLElement
private levelSpan = HTMLElement
constructor(maxLevel: number = 10, upNum: number = 10) {
this.maxLevel = maxLevel
this.upNum = upNum
this.scoreSpan = document.getElementById("score")!;
this.levelSpan = document.getElementById("level")!;
}
// 加分
addScore() {
this.scoreSpan.innerHTML = ++this.score + ''
// 判断分数是否符合升级条件
if (this.score % this.upNum === 0) {
this.levelUp()
}
}
get gameLevel(){
return this.level
}
// 提升等级, 最大等级为 10 级
levelUp() {
if (this.level < this.maxLevel) {
this.levelSpan.innerHTML = ++this.level + ''
}
}
}
export default ScorePanel
Snake.ts
// 定义蛇的类
class Snake {
// 蛇容器
element: HTMLElement;
// 蛇头
head: HTMLElement
// 蛇的身体(包括舌头)
bodies: HTMLCollection
constructor() {
this.element = document.getElementById("snake")!;
this.head = document.querySelector('#snake > div') as HTMLElement
this.bodies = this.element.getElementsByTagName("div")
}
// 获取蛇头的 X轴 坐标
get X() {
return this.head.offsetLeft
}
// 获取蛇头的 Y轴 坐标
get Y() {
return this.head.offsetTop
}
// 设置蛇头的 X轴 坐标
set X(value: number) {
// 如果新值和旧值相同, 则证明蛇是在纵向移动, 不用修改 X 的值
if (this.X === value) return
// 判断蛇移动的横坐标是否在合法范围内( 撞墙, 退出游戏 )
if (value < 0 || value > 290) {
throw new Error("蛇撞墙了! 游戏结束!")
}
// 修改 X 时, 就是在修改 X 轴坐标, 蛇在水平方向运动时, 不允许其向水平的相反方向运动
// 检查 蛇头 和 第二节身体的坐标是否一致
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
// 如果发生了掉头, 需要向按下键盘的相反的方向继续移动
if (value > this.X) {
// 如果新值大于旧值 X, 则说明蛇正在向右运动, 此时如果发生掉头, 应该继续向右移动, 需要将 value 的值 - 10
value = this.X - 10
} else {
value = this.X + 10
}
}
// 移动身体
this.moveBody()
this.head.style.left = value + 'px'
// 检查蛇头的坐标是否和身体的坐标重合
this.checkHeadBody()
}
// 设置蛇头的 Y轴 坐标
set Y(value: number) {
// 如果新值和旧值相同, 则证明蛇是在恒向移动, 不用修改 Y 的值
if (this.Y === value) return
if (value < 0 || value > 290) {
throw new Error("蛇撞墙了! 游戏结束!")
}
// 修改 Y 时, 就是在修改 Y 轴坐标, 蛇在垂直方向运动时, 不允许其向垂直的相反方向运动
// 检查 蛇头 和 第二节身体的坐标是否一致
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
// 如果发生了掉头, 需要向按下键盘的相反的方向继续移动
if (value > this.Y) {
// 如果新值大于旧值 Y, 则说明蛇正在向下运动, 此时如果发生掉头, 应该继续向下移动, 需要将 value 的值 - 10
value = this.Y - 10
} else {
value = this.Y + 10
}
}
// 移动身体
this.moveBody()
this.head.style.top = value + 'px'
// 检查蛇头的坐标是否和身体的坐标重合
this.checkHeadBody()
}
// 设置蛇身体增加一节
addBody() {
this.element.insertAdjacentHTML('beforeend', `<div></div>`)
}
// 移动 蛇 的身体, 后面的每一节身体,要移动到前一节的位置
moveBody() {
// 从后往前改, 第四节的位置 === 第三节的位置
for (let i = this.bodies.length - 1; i > 0; i--) {
// 获取前面身体的位置
let X = (this.bodies[i - 1] as HTMLElement).offsetLeft;
let Y = (this.bodies[i - 1] as HTMLElement).offsetTop;
(this.bodies[i] as HTMLElement).style.left = X + 'px';
(this.bodies[i] as HTMLElement).style.top = Y + 'px';
}
}
// 蛇头是否撞到自己
checkHeadBody(){
// 检查蛇头的坐标是否和身体的坐标重合
for (let i = 1; i < this.bodies.length; i++) {
if (this.X === (this.bodies[i] as HTMLElement).offsetLeft && this.Y === (this.bodies[i] as HTMLElement).offsetTop) {
throw new Error("撞到自己了! ")
}
}
}
}
export default Snake;
GameControl.ts
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器, 控制其他的所有类
class GameControl {
snake: Snake
food: Food
scorePanel: ScorePanel
// 存储用户按键的方向 ( 即蛇的移动方向 )
direction: string = ''
// 游戏是否结束
isLive = true
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.init()
}
// 游戏的初始化方法, 调用后游戏开始
init() {
// 绑定键盘的按下事件, 在回调函数使用 bind() 创建一个新函数, 并传递 GameControl 对象
document.addEventListener('keydown', this.keyDownHandler.bind(this));
// 调用 run() 使蛇进行移动
this.run()
}
keyDownHandler(e: KeyboardEvent) {
// 键盘按下事件得到的字符串 Chrome 和 IE 浏览器中的不一样
/** Chrome IE
* ArrowUp Up
* ArrowDown Down
* ArrowLeft Left
* ArrowRight Right
* /
*/
// 检查 用户按下的键 是否合法
this.direction = e.key;
}
// 控制 蛇 移动的方法
run() {
// 根据 this.direction 来使蛇的位置发生改变
/*
* 向上 top 减少
* 向下 top 增加
* 向左 left 减少
* 向右 left 增加
* */
// 获取蛇现在的坐标
let X = this.snake.X
let Y = this.snake.Y
switch (this.direction) {
case 'ArrowUp':
case 'Up':
Y -= 10
break
case 'ArrowDown':
case 'Down':
Y += 10
break
case 'ArrowLeft':
case 'Left':
X -= 10
break
case 'ArrowRight':
case 'Right':
X += 10
break
}
// 检查 蛇 是否吃到了食物
this.checkEat(X, Y)
try {
this.snake.X = X
this.snake.Y = Y
} catch (e) {
// @ts-ignore
alert(e.message)
// 将 isLIve 设置为 false
this.isLive = false
}
// 开启定时调用, 并且根据等级 提升移动速度
this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.gameLevel - 1) * 30)
}
// 检查蛇是否吃到食物
checkEat(X: number, Y: number) {
if (X === this.food.X && Y === this.food.Y){
// 食物位置重置
this.food.change()
// 分数增加
this.scorePanel.addScore()
// 蛇的身体 +1
this.snake.addBody()
}
}
}
export default GameControl;
main.ts
import './css/index.less'
import GameControl from "./module/GameControl";
const gc = new GameControl();
setInterval(()=>{
console.log(gc.direction)
},1000)
python防脱发技巧

浙公网安备 33010602011771号