代码
This commit is contained in:
commit
e1dc2551e2
80
README.md
Normal file
80
README.md
Normal file
@ -0,0 +1,80 @@
|
||||
## **贪吃蛇练习**
|
||||
|
||||
使用TypeScript + Webpack + Less实现贪吃蛇的例子;
|
||||
|
||||
### **项目依赖**
|
||||
|
||||
TypeScript:
|
||||
|
||||
- typescript;
|
||||
- ts-loader;
|
||||
|
||||
Webpack:
|
||||
|
||||
- webpack;
|
||||
- webpack-cli;
|
||||
- webpack-dev-server;
|
||||
- html-webpack-plugin;
|
||||
- clean-webpack-plugin;
|
||||
|
||||
Babel:
|
||||
|
||||
- core-js;
|
||||
- babel-loader;
|
||||
- @babel/core;
|
||||
- @babel/preset-env;
|
||||
|
||||
Less & CSS资源:
|
||||
|
||||
- style-loader;
|
||||
- css-loader;
|
||||
- less;
|
||||
- less-loader;
|
||||
- postcss;
|
||||
- postcss-loader;
|
||||
- postcss-preset-env;
|
||||
|
||||
### **项目使用**
|
||||
|
||||
#### **编译运行**
|
||||
|
||||
在确保已经正确安装node和npm的前提下:
|
||||
|
||||
分别执行下面的命令安装依赖并编译项目:
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm i
|
||||
# 编译打包
|
||||
npm run build
|
||||
```
|
||||
|
||||
编译完成后,使用浏览器打开dist目录下的`index.html`即可游玩;
|
||||
|
||||
根据老师的例子优化部分逻辑,及增加食物刷新时与蛇身重叠规避的逻辑,欢迎提出意见或建议
|
||||
|
||||
#### **继续开发**
|
||||
|
||||
使用`npm run start`进入开发模式;
|
||||
|
||||
默认使用Chrome浏览器打开,可以修改`package.json`中的值:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack",
|
||||
"start": "webpack serve --open chrome.exe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **其他**
|
||||
|
||||
视频讲解:
|
||||
|
||||
- [尚硅谷2021版TypeScript教程(李立超老师TS新课)](https://www.bilibili.com/video/BV1Xy4y1v7S2?p=22)
|
||||
|
||||
老师源码:
|
||||
|
||||
- [JasonkayZK/typescript-learn](https://github.com/JasonkayZK/typescript-learn/tree/greedy-snake)
|
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "snake",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --mode development --config webpack.config.js",
|
||||
"start": "webpack serve --open chrome.exe"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"babel-loader": "^8.2.5",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"core-js": "^3.22.8",
|
||||
"css-loader": "^6.7.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^11.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-loader": "^7.0.0",
|
||||
"postcss-preset-env": "^7.7.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.3.0",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.1"
|
||||
}
|
||||
}
|
34
src/index.html
Normal file
34
src/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>贪食蛇</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 游戏的主容器 -->
|
||||
<div id="main">
|
||||
<!-- 游戏的舞台 -->
|
||||
<div id="stage">
|
||||
<!-- 蛇 -->
|
||||
<div id="snake">
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<!-- 食物 -->
|
||||
<div id="food"></div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏的积分牌 -->
|
||||
<div id="score-panel">
|
||||
<div>
|
||||
SCORE:<span id="score">0</span>
|
||||
</div>
|
||||
<div>
|
||||
LEVEL:<span id="level">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// 引入样式
|
||||
import "./style/index.less";
|
||||
import GameController from "./modules/GameController";
|
||||
|
||||
const game = new GameController();
|
||||
game.init();
|
53
src/modules/Food.ts
Normal file
53
src/modules/Food.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 食物
|
||||
*/
|
||||
class Food
|
||||
{
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.element = document.getElementById("food")!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回食物X坐标值
|
||||
*/
|
||||
get X(): number
|
||||
{
|
||||
return this.element.offsetLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回食物Y坐标值
|
||||
*/
|
||||
get Y(): number
|
||||
{
|
||||
return this.element.offsetTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 食物位置重置
|
||||
* @param snakeBodies 蛇身元素集合
|
||||
*/
|
||||
public change(snakeBodies: HTMLCollectionOf<HTMLElement>): void
|
||||
{
|
||||
// 随机生成一个位置,最小是0,最大是290,每次移动间隔大小是10
|
||||
let top = Math.round(Math.random() * 29) * 10;
|
||||
let left = Math.round(Math.random() * 29) * 10;
|
||||
|
||||
// 防止食物重置时位置和蛇身重叠
|
||||
for (let snakeBody of snakeBodies) {
|
||||
if (top === snakeBody.offsetTop && left === snakeBody.offsetLeft) {
|
||||
// 递归调用,生成新的位置,并中止当前程序继续执行(一定要加上中止,否则该逻辑无效)
|
||||
this.change(snakeBodies);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.element.style.top = top + "px";
|
||||
this.element.style.left = left + "px";
|
||||
}
|
||||
}
|
||||
|
||||
export default Food;
|
99
src/modules/GameController.ts
Normal file
99
src/modules/GameController.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import Snake from "./Snake";
|
||||
import Food from "./Food";
|
||||
import ScorePanel from "./ScorePanel";
|
||||
|
||||
/**
|
||||
* 游戏控制器
|
||||
*/
|
||||
class GameController
|
||||
{
|
||||
protected snake: Snake;
|
||||
protected food: Food;
|
||||
protected scorePanel: ScorePanel;
|
||||
|
||||
// 用于防止调头,记录上一次键盘操作
|
||||
protected lastKeyDown = '';
|
||||
// 用于标记游戏是否未结束
|
||||
protected isLive = true;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.snake = new Snake();
|
||||
this.food = new Food();
|
||||
this.scorePanel = new ScorePanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏初始化,运行该方法表示游戏开始
|
||||
*/
|
||||
public init(): void
|
||||
{
|
||||
// 绑定键盘事件,当按下 上/下/左/右 时改变蛇的行动方向
|
||||
document.addEventListener("keydown", this.keyDownHandle.bind(this));
|
||||
this.snakeRun();
|
||||
}
|
||||
|
||||
/**
|
||||
* 键盘事件逻辑
|
||||
* @param event 事件属性
|
||||
*/
|
||||
protected keyDownHandle(event: KeyboardEvent): void
|
||||
{
|
||||
const horizontal = ["ArrowLeft", "Left", "ArrowRight", "Right"];
|
||||
const vertical = ["ArrowUp", "Up", "ArrowDown", "Down"];
|
||||
|
||||
// 防止水平调头
|
||||
if (horizontal.includes(this.lastKeyDown) && horizontal.includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止垂直调头
|
||||
if (vertical.includes(this.lastKeyDown) && vertical.includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置当前蛇的行走方向
|
||||
this.snake.nowDirection = event.key;
|
||||
// 记录此次键盘操作
|
||||
this.lastKeyDown = event.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查蛇是否吃到食物
|
||||
* @param x X坐标
|
||||
* @param y Y坐标
|
||||
*/
|
||||
protected checkEat(x: number, y: number): void
|
||||
{
|
||||
if (x === this.food.X && y === this.food.Y) {
|
||||
// 食物位置重置
|
||||
this.food.change(this.snake.nowBodies);
|
||||
// 分数增加
|
||||
this.scorePanel.addScore();
|
||||
// 蛇身增加一节
|
||||
this.snake.addBody();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理蛇走逻辑
|
||||
*/
|
||||
protected snakeRun(): void
|
||||
{
|
||||
this.checkEat(this.snake.X, this.snake.Y);
|
||||
|
||||
try {
|
||||
this.snake.checkRearEnd();
|
||||
this.snake.run();
|
||||
} catch (e: any) {
|
||||
// 捕获到错误意味着游戏失败
|
||||
alert((e as Error).message + " GAME OVER!");
|
||||
this.isLive = false;
|
||||
}
|
||||
|
||||
// 定时调用自身,并根据当前等级动态改变速度
|
||||
this.isLive && setTimeout(this.snakeRun.bind(this), 300 - (this.scorePanel.nowLevel - 1) * 30);
|
||||
}
|
||||
}
|
||||
|
||||
export default GameController;
|
49
src/modules/ScorePanel.ts
Normal file
49
src/modules/ScorePanel.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 记分牌
|
||||
*/
|
||||
class ScorePanel
|
||||
{
|
||||
protected score = 0;
|
||||
protected level = 1;
|
||||
protected scoreElement: HTMLElement;
|
||||
protected levelElement: HTMLElement;
|
||||
|
||||
constructor(protected maxLavel: number = 10, protected upScore: number = 10)
|
||||
{
|
||||
this.scoreElement = document.getElementById("score")!;
|
||||
this.levelElement = document.getElementById("level")!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加分
|
||||
*/
|
||||
public addScore(): void
|
||||
{
|
||||
this.scoreElement.innerText = (++this.score).toString();
|
||||
|
||||
// 分数达到设置的数则升级
|
||||
if (this.score % this.upScore === 0) {
|
||||
this.levelUp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级
|
||||
*/
|
||||
protected levelUp(): void
|
||||
{
|
||||
if (this.level <= this.maxLavel) {
|
||||
this.levelElement.innerText = (++this.level).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前等级
|
||||
*/
|
||||
get nowLevel(): number
|
||||
{
|
||||
return this.level;
|
||||
}
|
||||
}
|
||||
|
||||
export default ScorePanel;
|
157
src/modules/Snake.ts
Normal file
157
src/modules/Snake.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 蛇
|
||||
*/
|
||||
class Snake
|
||||
{
|
||||
protected element: HTMLElement;
|
||||
// 蛇头元素
|
||||
protected head: HTMLElement;
|
||||
// 蛇的身体(包括蛇头)
|
||||
protected bodies: HTMLCollectionOf<HTMLElement>;
|
||||
// 蛇当前的行走方向
|
||||
protected direction = "";
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.element = document.getElementById("snake")!;
|
||||
this.head = document.querySelector("#snake > div")!;
|
||||
this.bodies = this.element.getElementsByTagName("div");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置蛇头的X坐标值
|
||||
*/
|
||||
set X(value: number)
|
||||
{
|
||||
if (this.X === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value < 0 || value > 290) {
|
||||
throw new Error("蛇撞墙了!");
|
||||
}
|
||||
|
||||
this.moveBody();
|
||||
|
||||
this.head.style.left = value + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回蛇头的X坐标值
|
||||
*/
|
||||
get X(): number
|
||||
{
|
||||
return this.head.offsetLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置蛇头的Y坐标值
|
||||
*/
|
||||
set Y(value: number)
|
||||
{
|
||||
if (this.Y === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value < 0 || value > 290) {
|
||||
throw new Error("蛇撞墙了!");
|
||||
}
|
||||
|
||||
this.moveBody();
|
||||
|
||||
this.head.style.top = value + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回蛇头的Y坐标值
|
||||
*/
|
||||
get Y(): number
|
||||
{
|
||||
return this.head.offsetTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置蛇的行走方向
|
||||
*/
|
||||
set nowDirection(value: string)
|
||||
{
|
||||
this.direction = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取蛇的身体
|
||||
*/
|
||||
get nowBodies(): HTMLCollectionOf<HTMLElement>
|
||||
{
|
||||
return this.bodies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 吃完食物增加身体
|
||||
*/
|
||||
public addBody(): void
|
||||
{
|
||||
const div = document.createElement("div");
|
||||
this.element.appendChild(div);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多节蛇的移动
|
||||
*/
|
||||
public moveBody(): void
|
||||
{
|
||||
for (let i = this.bodies.length - 1; i > 0; i--) {
|
||||
let x = this.bodies[i - 1].offsetLeft;
|
||||
let y = this.bodies[i - 1].offsetTop;
|
||||
|
||||
this.bodies[i].style.left = x + "px";
|
||||
this.bodies[i].style.top = y + "px";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追尾检查
|
||||
*/
|
||||
public checkRearEnd(): void | never
|
||||
{
|
||||
// 只要蛇头与其他任何蛇身重叠则为追尾
|
||||
for (let i = 1; i < this.bodies.length; i++) {
|
||||
if (this.X === this.bodies[i].offsetLeft && this.Y === this.bodies[i].offsetTop) {
|
||||
throw new Error("蛇追尾了!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 蛇头走
|
||||
*/
|
||||
public run(): void
|
||||
{
|
||||
let x = this.X;
|
||||
let y = this.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.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
export default Snake;
|
74
src/style/index.less
Normal file
74
src/style/index.less
Normal file
@ -0,0 +1,74 @@
|
||||
// 颜色变量
|
||||
@bg-color: #b7d4a8;
|
||||
|
||||
// 清除默认样式
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// 改变盒子模型的计算方式
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 页面公共样式
|
||||
body {
|
||||
font: bold 20px "Courier";
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 主窗口的样式
|
||||
#main {
|
||||
width: 360px;
|
||||
height: 420px;
|
||||
background-color: @bg-color;
|
||||
margin: 100px auto;
|
||||
border: 10px solid black;
|
||||
border-radius: 20px;
|
||||
|
||||
// 开启弹性盒模型
|
||||
display: flex;
|
||||
// 设置主轴的方向
|
||||
flex-flow: column;
|
||||
// 设置侧轴的对齐方式
|
||||
align-items: center;
|
||||
// 设置主轴的对齐方式
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
// 游戏舞台
|
||||
#stage {
|
||||
width: 304px;
|
||||
height: 304px;
|
||||
border: 2px solid black;
|
||||
// 开启相对定位(因蛇元素和食物元素需要绝对定位,遵循子绝父相的定律)
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 蛇
|
||||
#snake > div {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: black;
|
||||
border: 1px solid @bg-color;
|
||||
// 开启绝对定位
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// 食物
|
||||
#food {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: black;
|
||||
border: 1px solid @bg-color;
|
||||
border-radius: 50%;
|
||||
// 开启绝对定位
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
// 记分牌
|
||||
#score-panel {
|
||||
width: 300px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ES2015",
|
||||
"target": "ES2015",
|
||||
"strict": true,
|
||||
"noEmitOnError": true
|
||||
}
|
||||
}
|
110
webpack.config.js
Normal file
110
webpack.config.js
Normal file
@ -0,0 +1,110 @@
|
||||
// 引入一个包
|
||||
const path = require("path");
|
||||
// 引入HTML插件
|
||||
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
||||
// 引入Clean插件
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
|
||||
// webpack中的所有配置信息
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
// 指定入口文件
|
||||
entry: "./src/index.ts",
|
||||
|
||||
// 指定打包文件所在目录
|
||||
output: {
|
||||
// 指定打包文件的目录
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
// 打包后的文件名
|
||||
filename: "bundle.js",
|
||||
|
||||
// 不使用箭头函数的方式定义
|
||||
environment: {
|
||||
arrowFunction: false,
|
||||
const: false
|
||||
}
|
||||
},
|
||||
|
||||
// 指定webpack打包时要使用的模块
|
||||
module: {
|
||||
// 指定要加载的规则
|
||||
rules: [
|
||||
{
|
||||
// 指定规则生效的文件
|
||||
test: /\.ts$/,
|
||||
// 要使用的loader
|
||||
use: [
|
||||
// 配置babel
|
||||
{
|
||||
// 指定加载器
|
||||
loader: "babel-loader",
|
||||
// 设置babel
|
||||
options: {
|
||||
// 设置预定义环境
|
||||
presets: [
|
||||
[
|
||||
// 指定环境的插件
|
||||
"@babel/preset-env",
|
||||
// 配置信息
|
||||
{
|
||||
// 要兼容的目标浏览器
|
||||
targets: {
|
||||
"chrome": "100",
|
||||
"ie": "11"
|
||||
},
|
||||
// 指定corejs的版本
|
||||
"corejs": "3",
|
||||
// 使用corejs的方式:usage表示按需加载
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"ts-loader"
|
||||
],
|
||||
// 要排除的文件
|
||||
exclude: /node_modules/
|
||||
},
|
||||
|
||||
// 设置less文件的处理
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
"css-loader",
|
||||
// 引入postcss
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
[
|
||||
"postcss-preset-env",
|
||||
{
|
||||
browsers: "last 2 versions"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"less-loader"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 配置webpack插件
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new HTMLWebpackPlugin({
|
||||
template: "./src/index.html"
|
||||
})
|
||||
],
|
||||
|
||||
// 设置引用模块
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"]
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user