css Audio - Active file-generic CSS - Active Generic - Active HTML - Active JS - Active SVG - Active Text - Active file-generic Video - Active header Love html icon-new-collection icon-person icon-team numbered-list123 pop-out spinner split-screen star tv

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              <html lang="zh-Hans">
<head>
  <meta charset="utf-8">
  <title>🐍 贪吃蛇大作战</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta name="description" content="贪吃蛇是一个产生于1970年代中后期的计算机游戏。此类游戏在1990年代由于一些小屏幕设备引入而再度流行起来,在现在的手机上基本都可安装此小游戏。">
  <meta name="author" content="Ritchie Zhu">
  <link rel="stylesheet" href="snake.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>🐍 贪吃蛇大作战</h1>
    </header>
    <section>
      <div class="panel">
        <a class="btn btn-play" href="javascript:void(0);">开始</a>
        <h2 class="score">0</h2>
        <p class="score-label">分数</p>
        </p>
      </div>
      <svg class="playground" width="400" height="400">
        <svg class="foods"></svg>
        <svg class="snake"></svg>
      </svg>
    </section>
    <footer>
      <p>按任意键开始游戏</p>
    </footer>
  </div>
</body>
</html>

            
          
!
            
              body {
  background-color: #fafafa;
  font-family: -apple-system-font,"Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-font-smoothing: antialiased;
}

.container {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
  width: 800px;
  height: 600px;
  border: 1px solid #e6e6e6;
  border-radius: 5px;
  box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, .2);
  text-align: center;
  background-color: #fff;
}

header {
  margin: 2rem auto;
  color: #333;
}

footer {
  margin: 1rem auto;
  color: #999;
  font-size: .8rem;
  font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;;
}

footer a {
  color: #999;
  text-decoration: none;
}

section {
  position: relative;
}

.panel {
  position: absolute;
  left: 50px;
  box-sizing: border-box;
  width: 300px;
  height: 400px;
  border: 5px solid #00c086;
  border-right: none;
}

.btn-play {
  margin: 5rem auto;
  display: block;
  width: 8rem;
  height: 3rem;
  border: 1px solid #00c086;
  border-radius: 1.5rem;
  color: #00c086;
  font-size: 1.5rem;
  line-height: 3rem;
  text-decoration: none;
}

.score {
  margin: 1rem auto;
  color: #333;
  font-size: 3rem;
  font-weight: bold;
}

.score-label {
  margin: 0 auto;
  padding-top: 1rem;
  width: 100px;
  border-top: 1px solid #999;
  color: #999;
  font-size: 1.5rem;
}

.playground {
  margin-left: 300px;
  background-color: #00c086;
}

            
          
!
            
              (function() {

  /* 定义按键状态 */
  const KEYS = {
    UP: false,
    DOWN: false,
    LEFT: false,
    RIGHT: false
  };


  /* 选择 DOM 元素 */
  let playground = document.getElementsByClassName("playground")[0]; // 舞台
  let score = document.getElementsByClassName("score")[0]; // 记分牌
  let snakeSvg = document.getElementsByClassName("snake")[0]; // 蛇的图层
  let foodsSvg = document.getElementsByClassName("foods")[0]; // 食物图层
  let button = document.getElementsByClassName("btn-play")[0]; // 开始按钮


  /*
   * ## 游戏类
   *
   * 用于接受游戏指令,控制游戏进程
  */
  class Game {

    /* 构造游戏 */
    constructor(stage) {
      this.FPS = 50;
      this.paused = true;
      this.stage = stage;
      this.score = 0;
      this.stageDelay = 1000 / this.FPS;
      this.snakeDelay = Math.round(2000 / this.stage.snake.header.speed);
      this.feedDelay = 10 * this.snakeDelay;
      this.stageTimer = null;
      this.snakeTimer = null;
      this.feedTimer = null;
    }

    /* 初始化游戏 */
    init() {
      button.innerHTML = "开始";
      this.resetScore();
      this.stage.render();

      // 按钮控制
      button.onclick = (event) => {
        event.preventDefault();
        if (this.paused) {
          this.paused = false;
          button.innerHTML = "重玩";
          this.start();
        } else {
          this.paused = true;
          button.innerHTML = "开始";
          this.restart();
        }
      };

      // 键盘控制
      window.onkeydown = (event) => {
        event.preventDefault();
        this.paused = false;
        button.innerHTML = "重玩";
        this.start();
      };
    }

    /* 开始游戏 */
    start() {
      this.stageTimer = window.setInterval(() => {
        this.stage.flashKeys(); // 刷新舞台
      }, this.stageDelay);

      this.snakeTimer = window.setInterval(() => {
        this.snakeRush(); // 开动小蛇
      }, this.snakeDelay);

      this.feedTimer = window.setInterval(() => {
        this.foodRush(); // 开始投食
      }, this.feedDelay);
    }

    /* 暂停游戏 */
    pause() {
      clearInterval(this.snakeTimer);
      clearInterval(this.feedTimer);
    }

    /* 重玩游戏 */
    restart() {
      clearInterval(this.stageTimer);
      clearInterval(this.snakeTimer);
      clearInterval(this.feedTimer);
      this.stage = new Stage(this.stage.width, this.stage.height);
      this.init();
    }

    /* 计分 */
    setScore() {
      score.innerHTML = this.score.toString();
    }

    /* 得分 */
    winScore() {
      this.score++;
      this.setScore(this.score);
    }

    /* 失分 */
    loseScore() {
      this.score--;
      this.setScore(this.score);
    }

    /* 清分 */
    resetScore() {
      this.score = 0;
      this.setScore(this.score);
    }

    /* 蛇的运动函数 */
    snakeRush() {
      let snake = this.stage.snake;
      let foods = this.stage.foods;
      snake.turn(); // 转弯
      snake.move(snake.header.direction); // 移动
      this.stage.renderSnake(); // 渲染蛇

      // 当蛇吃到食物时...
      for (let food of foods) {
        if (snake.header.collideWith(food)) {
          let index = foods.indexOf(food); // 获取被吃食物
          snake.header.move(snake.header.direction);
          snake.body.unshift(new SnakeBody(food.x, food.y, snake.header.color)); // 蛇身++
          snake.header.speedUp(); // 速度++
          this.winScore(); // 积分++

          // 如果食物有彩蛋
          if (food.bonus) {
            if (food.bonus.name === "SLOW") {
              snake.header.speed /= 2; // 减速
            } else if (food.bonus.name === "FAST") {
              snake.header.speed *= 2; // 加速
            } else if (food.bonus.name === "HALF") {
              snake.body = snake.body.slice(0, Math.floor(snake.body.length / 2)); // 二分
            } else if (food.bonus.name == "INVISIBLE") {
              snakeSvg.style.visibility = "hidden"; // 隐形
              setTimeout(() => {
                snakeSvg.style.visibility = "visible";
              }, 3000);
            } else if (food.bonus.name === "POISONOUS") {
              alert("游戏结束,请重新开始!"); // 有毒
              this.restart();
              break;
            }
          }

          this.snakeDelay = Math.round(2000/snake.header.speed); // 更新计时器延迟
          foods.splice(index, 1); // 移除被吃食物
          this.stage.renderSnake(); // 渲染食物
          this.stage.renderFoods(); // 渲染食物
          clearInterval(this.snakeTimer); // 清除老定时器
          this.snakeTimer = window.setInterval(() => {
            this.snakeRush(); // 以新的延迟启动定时器
          }, this.snakeDelay);
          break;
        }
      }

      // 当蛇撞到自己时...
      for (let rect of snake.body) {
        if (rect.collideWith(snake.header)) {
          alert("游戏结束,请重新开始!");
          this.restart();
          break;
        }
      }
    }

    /* 投食函数 */
    foodRush() {
      let snake = this.stage.snake;
      let foods = this.stage.foods;
      let food = this.stage.feed(); // 生成食物
      while (food && (food.collideWiths(snake) || food.collideWiths(foods))) {
        food = this.stage.feed(); // 重新生成食物
        break;
      }
      this.stage.renderFoods(); //渲染食物
      clearInterval(this.feedTimer);
      this.feedTimer = window.setInterval(() => {
        this.foodRush();
      }, this.feedDelay);
    }
  }


  /*
   * ## 舞台类
   *
   * 用于初始化舞台,渲染蛇和食物实例
  */
  class Stage {

    /* 构造舞台 */
    constructor(width, height) {
      this.width = width;
      this.height = height;
      this.maxFoodsCount = 5; // 舞台最大食物数
      this.snake = new Snake(0, 0, "#33495d", 5, "→");
      this.foods = [new Food(Math.floor(this.width / 2), Math.floor(this.width / 2), "#ff002d")];
    }

    /* 坐标预处理 */
    formatCoords(x, y) {
      if (x >= 0) { x = x % this.width; }
      else { x = this.width + x % this.width; }
      if (y >= 0) { y = y % this.height; }
      else { y = this.height + y % this.height; }
      return [x, y];
    }

    /* 刷新方向键状态 */
    flashKeys() {
      window.onkeydown = (event) => {
        let keyCode = event.keyCode;
        event.preventDefault();
        for (let key of Object.keys(KEYS)) {
          KEYS[key] = false;
        }
        if (keyCode === 37 || keyCode === 65) { KEYS.LEFT = true; }
        else if (keyCode === 38 || keyCode === 87) { KEYS.UP = true; }
        else if (keyCode === 39 || keyCode === 68) { KEYS.RIGHT = true; }
        else if (keyCode === 40 || keyCode === 83) { KEYS.DOWN = true; }
      };
    }

    /* 渲染蛇和食物 */
    render() {
      playground.setAttribute("viewBox", `0,0,${this.width},${this.height}`);
      this.renderSnake();
      this.renderFoods();
    }

    /* 渲染蛇 */
    renderSnake() {
      snakeSvg.innerHTML = "";
      let [x, y] = [this.snake.header.x, this.snake.header.y];
      [this.snake.header.x, this.snake.header.y] = this.formatCoords(x, y);
      snakeSvg.appendChild(this.snake.header.renderAsNode());
      if (this.snake.body.length) { // 判断蛇是否有身体
        for (let rect of this.snake.body) {
          snakeSvg.appendChild(rect.renderAsNode());
        }
      }
    }

    /* 渲染食物 */
    renderFoods() {
      foodsSvg.innerHTML = "";
      for (let food of this.foods) {
        foodsSvg.appendChild(food.renderAsNode());
      }
    }

    /* 投放新食物 */
    feed() {
      if (this.foods.length === this.maxFoodsCount) {
        return null;
      }
      let x = Math.floor(Math.random() * this.width);
      let y = Math.floor(Math.random() * this.height);
      let food = new Food(x, y, "#ff002d");
      this.foods.push(food);
      return food;
    }
  }


  /*
   * ## 方块类
   *
   * 蛇和食物类的基类,用于抽象公共方法
  */
  class Rect {

    /* 构造方块 */
    constructor(x, y, color) {
      this.x = x;
      this.y = y;
      this.color = color;
      this.stroke = "rgba(255, 255, 255, .8)";
    }

    /* 判断两个方块是否碰撞 */
    collideWith(rect) {
      if (this.x === rect.x && this.y === rect.y) { return true; }
      return false;
    }

    /* 渲染并返回方块节点 */
    renderAsNode() {
      let rectNode = document.createElementNS("http://www.w3.org/2000/svg", "rect");
      rectNode.setAttribute("x", this.x);
      rectNode.setAttribute("y", this.y);
      rectNode.setAttribute("fill", this.color);
      rectNode.setAttribute("stroke", this.stroke);
      rectNode.setAttribute("stroke-width", "0.1");
      rectNode.setAttribute("width", 1);
      rectNode.setAttribute("height", 1);
      return rectNode;
    }
  }


  /*
   * ## 食物类
   *
   * 方块类的子类,用于生成各种食物
  */
  class Food extends Rect {

    /* 构造食物 */
    constructor(x, y, color="#8a0001", bonus=null) {
      super(x, y, color);
      this.bonus = this.setBonus();
      if (this.bonus) { this.color = this.bonus.color; }
    }

    /* 随机获取彩蛋 */
    random(bonus) {
      if (Math.random() > 0.8) {
        if (bonus.length) {
          return bonus[Math.floor(Math.random() * bonus.length)];
        }
      }
      return null;
    };

    /* 设置食物彩蛋 */
    setBonus() {
      const BONUS = {
        "SLOW": "#ffcc5c",      // 减速
        "HALF": "#f8b195",      // 二分
        "FAST": "#00c8f8",      // 加速
        "INVISIBLE": "#00c086", // 隐形
        "POISONOUS": "#222222"  // 有毒
      }
      let name = this.random(Object.keys(BONUS));
      if (name) {
        return { "name": name, "color": BONUS[name] };
      } else {
        return null;
      }
    }

    /* 判断食物是否靠近其他方块 */
    collideWiths(rects) {
      if (rects instanceof Snake) { // 如果是条蛇
        if (this.collideWith(rects.header)) { return true; }
        if (rects.body.length) {
          for (let rect of rects.body) {
            if (this.collideWith(rect)) { return true; }
          }
        }
      } else { // 如果不是条蛇
        if (rects.length) {
          for (let rect of rects) {
            if (this.collideWith(rect)) { return true; }
          }
        }
      }
      return false;
    }
  }


  /*
   * ## 蛇头类
   *
   * 方块类的子类,控制蛇的方向和移动
  */
  class SnakeHeader extends Rect {

    /* 构造蛇头 */
    constructor(x, y, color, speed, direction) {
      super(x, y, color);
      this.speed = speed;
      this.direction = direction;
    }

    /* 移动蛇头 */
    move(direction) {
      switch (direction) {
        case "↑":
          this.y = this.y - 1;
          break;
        case "↓":
          this.y = this.y + 1;
          break;
        case "←":
          this.x = this.x - 1;
          break;
        case "→":
          this.x = this.x + 1;
          break;
        default:
          console.log("The direction of snake should be in ↑ ↓ ← →.");
      }
    }

    /* 加速蛇头 */
    speedUp() {
      this.speed += 0.1;
    }
  }


  /*
   * ## 蛇身类
   *
   * 方块类的子类,组成蛇的身体
  */
  class SnakeBody extends Rect {

    /* 改造蛇身 */
    constructor(x, y, color) {
      super(x, y, color);
    }
  }


  /*
   * ## 蛇类
   *
   * 由蛇头和蛇身组成的类,完整的一条蛇
  */
  class Snake {

    /* 构造蛇 */
    constructor(x, y, color, speed, direction, ...rects) {
      this.header = new SnakeHeader(x, y, color, speed, direction);
      this.body = [];
      if (rects) {
        for (let rect of rects) {
          this.body.push(new SnakeBody(rect[0], rect[1], color));
        }
      }
    }

    /* 移动蛇 */
    move(direction) {
      if (this.body.length) {
        this.body.pop();
        this.body.unshift(new SnakeBody(this.header.x, this.header.y, this.header.color));
      }
      this.header.move(direction);
    }

    /* 调整方向 */
    turn() {
      if (KEYS.UP && this.header.direction != "↓") { this.header.direction = "↑"; }
      else if (KEYS.DOWN && this.header.direction != "↑") { this.header.direction = "↓"; }
      else if (KEYS.LEFT && this.header.direction != "→") { this.header.direction = "←"; }
      else if (KEYS.RIGHT && this.header.direction != "←") { this.header.direction = "→"; }
      return this.header.direction;
    }
  }


  /* Snake Rush */
  let game = new Game(new Stage(40, 40));
  game.init();

})()

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.
Loading ..................

Console