用TypeScript写的贪吃蛇小游戏(可直接游戏)

最近在学习TypeScript的时候,跟着B站课写的一个小脚本,十分有趣!李立超老师确实深入简出,收获颇丰!

下面是编译后程序,可以使用W、A、S、D控制移动蛇身体,速度会逐渐加快,避免碰壁(网页刷新后可重新开始)

SCORE: 0
LEVEL: 1

下面是关键的源码

> tree
    │   package-lock.json
    │   package.json
    │   tsconfig.json
    │   webpack.config.js
    ├───dist
    │       bundle.js
    │       index.html
    ├───node_modules
    └───src
        │   index.html
        │   index.ts
        ├───modules
        │       Food.ts
        │       GameControl.ts
        │       ScorePanel.ts
        │       Snake.ts
        └───style
                index.less
{
    "compilerOptions": {
        "module": "ES2015",
        "target": "ES2015",
        "strict": true,
        "noEmitOnError": true
    }
}
{
  "name": "greedysnake",
  "version": "1.0.0",
  "description": "",
  "game": "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.22.9",
    "@babel/preset-env": "^7.22.9",
    "babel-loader": "^9.1.3",
    "clean-webpack-plugin": "^4.0.0",
    "core-js": "^3.32.0",
    "css-loader": "^6.8.1",
    "html-webpack-plugin": "^5.5.3",
    "less": "^4.1.3",
    "less-loader": "^11.1.3",
    "postcss": "^8.4.27",
    "postcss-loader": "^7.3.3",
    "postcss-preset-env": "^9.1.0",
    "style-loader": "^3.3.3",
    "ts-loader": "^9.4.4",
    "typescript": "^5.1.6",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
}
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: "./src/index.ts",
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "bundle.js"
    },
    mode:'development', //production
    module:{
        rules: [
            {
                test: /\.ts$/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets:[
                                [
                                    "@babel/preset-env",
                                    {
                                        targets: {
                                            "chrome": "115"
                                        },
                                        "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"
                ]
            }
        ]
    },
    plugins:[
        new CleanWebpackPlugin(),
        new HTMLWebpackPlugin({
            title: "GreedySnake",
            template: "./src/index.html"
        }),
    ],
    resolve: {
        extensions: ['.ts', '.js']
    }
}
<!DOCTYPE html>
<html lang="zh">
    <head>
        <meta charset="UTF-8">
        <title>贪吃蛇</title>
    </head>

    <body>

        <div id="game">
            <div id="stage">
                <div id="snake">
                    <div></div>
                </div>
                <div id="food">
                    <div></div>
                    <div></div>
                    <div></div>
                    <div></div>
                </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>
import './style/index.less'
import GameControl from './modules/GameControl'

const gc = new GameControl();
class Food{
    element: HTMLElement;

    constructor(){
        this.element = document.getElementById("food")!;
    }

    get X(){
        return this.element.offsetLeft;
    }

    get Y(){
        return this.element.offsetTop;
    }

    change(){
        let top = Math.round(Math.random()*29) * 10;
        let left = Math.round(Math.random()*29) * 10;
        this.element.style.top = top+'px';
        this.element.style.left = left+'px';
    }
}

export default Food;
class Snake{
    element: HTMLElement;
    head: HTMLElement;
    bodies: HTMLCollection;

    constructor(){
        this.element = document.getElementById("snake")!;
        this.head = document.querySelector('#snake > div') as HTMLElement;
        this.bodies = document.getElementById("snake")!.getElementsByTagName('div');
    }

    get X(){
        return this.head.offsetLeft;
    }

    get Y(){
        return this.head.offsetTop;
    }

    set X(value){
        if(this.X === value){ return; }
        if(value<0 || value>290){
            throw new Error("蛇撞墙");
        }

        if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value){
            // console.log("水平方向掉头");
            if(value > this.X){
                value = this.X - 10;
            }else{
                value = this.X + 10;
            }
        }

        this.moveBody();
        this.head.style.left = value + 'px';
        this.checkHeadBody();
    }

    set Y(value){
        if(this.Y === value){ return; }
        if(value<0 || value>290){
            throw new Error("蛇撞墙");
        }

        if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value){
            // console.log("水平方向掉头");
            if(value > this.Y){
                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++){
            let body = this.bodies[i] as HTMLElement;
            if (this.X === body.offsetLeft && this.Y === body.offsetTop) {
                throw new Error("撞倒自己了");
                
            }
        }
    }

}

export default Snake;
class ScorePanel{
    score = 0;
    level = 1;
    maxLevel:number;
    upScore:number;
    
    scoreEle: HTMLElement;
    levelEle: HTMLElement;

    constructor(maxLevel:number, upScore:number){
        this.maxLevel = maxLevel;
        this.upScore = upScore;
        this.scoreEle = document.getElementById("score")!;
        this.levelEle = document.getElementById("level")!;
    }

    addScore(){
        this.scoreEle.innerHTML = ++this.score + '';
        if(this.score % this.upScore ==0){
            this.levelup();
        }
    }

    levelup(){
        if(this.level < this.maxLevel){
            this.levelEle.innerHTML = ++this.level +'';
        }
    }
}

export default ScorePanel;
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from './ScorePanel';

class GameControl{
    snake: Snake;
    food: Food;
    scorePanel: ScorePanel;
    direction: string = '';
    isLive: boolean = true;

    constructor(){
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel(10, 3);
        this.init();
    }

    init(){
        document.addEventListener('keydown', this.keydownHandler.bind(this));
        this.run();
    }

    keydownHandler(event: KeyboardEvent){
        // console.log(event.key);
        this.direction = event.key;
    }

    run(){
        let X = this.snake.X;
        let Y = this.snake.Y;
        switch(this.direction){
            case "ArrowUp":
            case "Up":
            case "w":
                Y -= 10;
                break;
            case "ArrowDown":
            case "Down":
            case "s":
                Y += 10;
                break;
            case "ArrowLeft":
            case "Left":
            case "a":
                X -= 10;
                break;
            case "ArrowRight":
            case "Right":
            case "d":
                X += 10;
                break;
        }

        this.checkEat(X, Y);

        try {
            this.snake.X = X;
            this.snake.Y = Y;
        } catch (error) {
            // error.message
            alert("游戏结束");
            this.isLive = false;
        }
        
        this.isLive && setTimeout(this.run.bind(this), 300-(this.scorePanel.level-1)*30);
    }

    checkEat(X:number, Y:number){
        if (X === this.food.X && Y === this.food.Y){
            this.scorePanel.addScore();
            this.snake.addBody();
            this.food.change();
            // console.log(this.snake.bodies);
        }
    }
}

export default GameControl;
@bg-color: #b7d4a8;

*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body{
    font: bold 20px "Courier";
}

#game{
    width: 360px;
    height: 420px;
    background-color: @bg-color;
    margin: 100px auto;
    border: 10px solid black;
    border-radius: 40px;
    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{
                height: 10px;
                width: 10px;
                background-color: black;
                border: 1px solid @bg-color;
                position: absolute;
            }
        }

        #food{
            width: 10px;
            height: 10px;
            // background-color: red;
            position: absolute;
            display: flex;
            flex-flow: row wrap;
            justify-content: space-between;
            align-content: space-between;
            left: 40px;
            top: 40px;

            &>div{
                height: 4px;
                width: 4px;
                background: black;
                transform: rotate(45deg);
            }
        }
    }

    #score-panel{
        width: 300px;
        display: flex;
        justify-content: space-between;
    }
}

这里分享关键代码以及编译后的文件