利用改bugs 的时间间隙,写了个贪吃蛇,保存文件 index.html, 打开即玩,祝大小朋友六一快乐
<!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, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>Document</title>
<style type="text/css">
div, ul, li {
margin: 0;
padding: 0;
box-sizing: border-box;
}
ul, li {
list-style: none;
}
#playground {
outline: none;
border: 1px solid dimgray
}
.spot {
position: absolute;
width: 10px;
height: 10px;
background: lightblue;
}
.snake-head {
background: orangered;
}
.food {
background: seagreen;
}
.panel {
display: flex;
justify-content: space-around;
list-style: none;
font-size: 10px;
padding-top: 50px;
}
.panel input {
width: 50px;
border: none;
outline: none;
}
</style>
<style>
html {
width: 100%;
height: 100%;
}
@media screen and (orientation: portrait) {
/*竖屏 css*/
body {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.game-box {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
}
@media screen and (orientation: landscape) {
body {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
</style>
</head>
<body>
<div class="game-box">
<form name="panel" class="panel">
<div>状态:<input name="gameState" disabled/></div>
<div>长度:<input name="snakeLength" disabled/></div>
<div>步数:<input name="stepCount" disabled/></div>
</form>
<div id="playground" tabindex="0" border-collapse="none"></div>
</div>
<canvas id="controller"></canvas>
<script type="text/javascript">
function Position (x, y) {
this.x = x; this.y = y;
}
Position.prototype.set = function (x, y) {
this.x = x; this.y = y;
}
Position.prototype.get = function (x, y) {
return [this.x, this.y]
}
const Direction = {
up: 0,
right: 1,
down: 2,
left: 3
}
function Spot (parent, position) {
this.position = position
const el = document.createElement("div")
el.classList.add("spot")
this.parent = parent
parent.el.appendChild(el)
this.el = el
this.move()
this.mark(true)
}
Spot.prototype.mark = function (isOccupied) {
const colIndex = this.position.x / Snake.size
const rowIndex = this.position.y / Snake.size
this.parent.blocks[rowIndex][colIndex] = isOccupied
}
Spot.prototype.move = function () {
const { x, y } = this.position
this.el.style.transform = `translate(${x}px,${y}px)`
}
Spot.prototype.suicide = function () {
this.el.remove()
}
function Food (parent, position) {
Spot.call(this, parent, position)
}
Food.prototype = Object.create(Spot.prototype)
Food.prototype.constructor = Food
const MoveState = {
crash: -1,
back: 0,
forward: 1
}
function Snake (...spots) {
this.spots = spots
this.step = 2
this.deltaX = this.step
this.deltaY = 0
this.lastDeltaX = this.deltaX
this.lastDeltaY = this.deltaY
this.direction = null
}
Snake.size = 10
Snake.prototype.setDelta = function (deltaX, deltaY) {
this.deltaX = deltaX
this.deltaY = deltaY
}
Snake.prototype.setDirection = function (direction) {
const step = this.step
this.direction = direction
switch (direction) {
case Direction.up: this.setDelta(0, -step)
break
case Direction.right: this.setDelta(step, 0)
break
case Direction.down: this.setDelta(0, step)
break
case Direction.left: this.setDelta(-step, 0)
break
default:
}
this.move()
}
Snake.prototype.head = function () {
return this.spots[this.spots.length - 1]
}
Snake.prototype.tail = function () {
return this.spots[0]
}
Snake.prototype.size = function () {
return this.spots.length
}
Snake.prototype.move = function () {
const { deltaX, deltaY } = this
// 判断如果当前方向与之前相反则不执行任何操作
if (this.lastDeltaX + deltaX === 0 && this.lastDeltaY + deltaY === 0) {
this.deltaX = this.lastDeltaX
this.deltaY = this.lastDeltaY
this.move()
return MoveState.back
}
// 尾清除
this.tail().mark()
const depart = Snake.size / this.step
const head = this.head()
const headPosition = new Position(head.position.x + deltaX * depart, head.position.y + deltaY * depart)
// 左右横穿
if (headPosition.x < 0) {
headPosition.x += head.parent.width
}
if (headPosition.x >= head.parent.width) {
headPosition.x -= head.parent.width
}
// 上下横穿
if (headPosition.y < 0) {
headPosition.y += head.parent.height
}
if (headPosition.y >= head.parent.height) {
headPosition.y -= head.parent.height
}
for (let i = 0; i < this.spots.length - 1; i++) {
const prev = this.spots[i + 1]
const current = this.spots[i]
if (
prev.position.x === headPosition.x &&
prev.position.y === headPosition.y
) {
return MoveState.crash
}
current.position.set(prev.position.x, prev.position.y)
current.move()
}
head.position.set(headPosition.x, headPosition.y)
this.lastDeltaX = deltaX
this.lastDeltaY = deltaY
head.move()
// 头标记
head.mark(true)
return MoveState.forward
}
Snake.prototype.eat = function (food) {
const oldHead = this.head()
oldHead.el.classList.remove('snake-head')
const spot = new Spot(food.parent, food.position)
this.spots.push(spot)
food.suicide()
const newHead = this.head()
newHead.el.classList.add('snake-head')
}
function Playground (width = 200, height = 100) {
this.width = width
this.height = height
this.el = document.getElementById("playground");
this.el.style.width = width + "px"
this.el.style.height = height + "px"
const blocks = new Array(this.height / Snake.size)
.fill(undefined)
.map(() => new Array(this.width / Snake.size))
this.blocks = blocks
}
</script>
<script>
const GameState = {
failure: -1,
init: 0,
running: 1,
pause: 2,
stop: 3
}
function Game () {
this.foods = []
this.startTime = null
this.previousTimeStamp = null
this.playground = null
this.snake = null
this.snakeInitialSize = 4
this.state = null
this.normalInterval = 500
this.interval = this.normalInterval
this.lastSetDirectionStamp = Date.now()
// 连续两次间隔时间
this.speedupInterval = 500
// 加速的时间量
this.speedupIntervalDelta = 100
// 加速持续时间
this.speedupIntervalDuration = 2000
this.stepCount = 0
this.keydownHandler = null
}
Game.prototype.init = function () {
this.setState(GameState.init)
// this.playground = new Playground()
this.playground = new Playground(370, 560)
this.snake = this.createSnake()
this.initHandler()
this.createFoods()
return this
}
Game.prototype.destroy = function () {
this.state = GameState.stop
// 解除事件
this.playground.el.removeEventListener('keydown', this.keydownHandler)
// 删除 dom
this.snake.spots.forEach(spots => {
spots.suicide()
})
this.foods.forEach(food => {
food.suicide()
})
// 数据清空
Game.apply(this)
this.init()
this.updatePanel()
}
Game.prototype.setState = function (state) {
this.state = state
this.stateChanged(state)
}
Game.prototype.stateChanged = function (state) {
switch(state) {
case GameState.running:
break
// TODO: game over tooltip
case GameState.failure: alert("game over")
default:
}
}
Game.prototype.updatePanel = function () {
const panel = document.forms.panel
panel.gameState.value = this.state
panel.snakeLength.value = this.snake.size()
panel.stepCount.value = this.stepCount
}
Game.prototype.createSnake = function () {
const playground = this.playground
const body = new Array(this.snakeInitialSize).fill([0, 0]).map(function ([x, y], index) {
const spot = new Spot(playground, new Position(x + index * Snake.size, y))
return spot
})
const snake = new Snake(
...body
)
snake.head().el.classList.add('snake-head')
return snake
}
Game.prototype.setDirection = function (direction) {
// 控制加速时长,加速间隔
if (Date.now() - this.lastSetDirectionStamp < this.speedupInterval) {
const flag = direction === this.snake.direction ? -1 : 1
const interval = this.interval + flag * this.speedupIntervalDelta
if (interval >= this.normalInterval * 0.1) {
this.interval = interval
}
}
this.snake.setDirection(direction)
this.lastSetDirectionStamp = Date.now()
}
Game.prototype.createFoods = function (num = 10) {
let random = Math.round(Math.random() * num)
if (random === 0) {
// 检测食物数量,如果 小于 1 则创建
if (this.foods.length <= 1) {
random = 1
} else {
// 不创建
return
}
}
const blocks = this.playground.blocks
const spareBlocks = []
for (let i = 0; i < blocks.length; i++) {
const rows = blocks[i]
for (let j = 0; j < rows.length; j++) {
if (rows[j]) {
continue
}
spareBlocks.push([j, i])
}
}
for (let i = random; i >= 0; i--) {
this.createFood(spareBlocks)
}
}
// 优先距离蛇头 50 格内生成食物,并且不能生成在蛇身体上, 也不能生成到已有的食物身上
Game.prototype.createFood = function (spareBlocks) {
const randomIndex = Math.floor(Math.random() * spareBlocks.length)
const [x, y] = spareBlocks[randomIndex]
randomX = x * Snake.size
randomY = y * Snake.size
// 查找当前位置是否可用,若不可用,则继续生成
const food = new Food(this.playground, new Position(randomX, randomY))
food.el.classList.add("food")
this.foods.push(food)
}
Game.prototype.initHandler = function () {
this.keydownHandler = this.keydown.bind(this)
this.playground.el.addEventListener("keydown", this.keydownHandler)
}
Game.prototype.keydown = function(e) {
switch(e.key) {
case 'ArrowDown':
this.setDirection(Direction.down)
break
case 'ArrowUp':
this.setDirection(Direction.up)
break;
case 'ArrowLeft':
this.setDirection(Direction.left)
break
case 'ArrowRight':
this.setDirection(Direction.right)
break
default:
}
return false
}
Game.prototype.start = function () {
if (this.state === GameState.running) {
return
}
this.setState(GameState.running)
window.requestAnimationFrame(this.run.bind(this))
this.playground.el.focus()
}
Game.prototype.run = function (timestamp) {
if (this.startTime === null) {
this.startTime = timestamp;
}
const elapsed = timestamp - this.previousTimeStamp;
// 超出加速最大时间,取消加速
if (Date.now() - this.lastSetDirectionStamp > this.speedupIntervalDuration) {
this.interval = this.normalInterval
}
if (this.previousTimeStamp == null || elapsed > this.interval) {
this.previousTimeStamp = timestamp;
const moveState = this.snake.move()
if (moveState === MoveState.crash) {
this.setState(GameState.failure)
}
if (moveState === MoveState.forward) {
this.stepCount++
}
// 检测前方是否存在食物
const head = this.snake.head()
for (let i = this.foods.length - 1; i >= 0; i--) {
const food = this.foods[i]
if (food.position.x === head.position.x && food.position.y === head.position.y) {
// 精神上消灭
this.foods.splice(i, 1)
// 肉体上消灭
this.snake.eat(food)
// 创建新的食物 or 不创建
this.createFoods(Math.random() > 0.5 ? 1 : 0)
}
}
}
// 更新面板数据
this.updatePanel()
if (this.state === GameState.running) {
window.requestAnimationFrame(this.run.bind(this));
}
}
Game.prototype.pause = function () {
this.setState(GameState.pause)
}
Game.prototype.stop = function () {
this.setState(GameState.stop)
}
</script>
<script>
const game = new Game().init()
</script>
<script>
function calculateCube(heart, r) {
const d = Math.sqrt(r**2 / 2)
return [
[heart.x - d, heart.y - d],
[heart.x + d, heart.y - d],
[heart.x + d, heart.y + d],
[heart.x - d, heart.y + d],
]
}
function drawText (ctx, heart, text, color) {
const oldStrokeStyle = ctx.strokeStyle
ctx.strokeStyle = color
ctx.strokeText(text, heart.x - ctx.measureText(text).width / 2, heart.y);
ctx.strokeStyle = oldStrokeStyle
}
(function (canvas, game) {
const height = 220
const width = height * 2
const r1 = 100
const r2 = 20
canvas.width = width
canvas.height = height
canvas.style.height = height + 'px'
const ctx = canvas.getContext('2d')
const heart = new Position(width / 2, height / 2)
ctx.beginPath()
ctx.ellipse(heart.x, heart.y, width / 2, heart.y, 0, 0, Math.PI * 2, true)
ctx.stroke()
ctx.moveTo(heart.x + r1, heart.y);
ctx.beginPath()
ctx.arc(heart.x, heart.y, r1, 0, Math.PI * 2, true);
ctx.stroke()
ctx.moveTo(heart.x + r2, heart.y);
ctx.beginPath()
ctx.fillStyle = 'red'
ctx.arc(heart.x, heart.y, r2, 0, Math.PI * 2, true);
ctx.fill()
drawText(ctx, heart, 'P', '#fff')
// 绘制控制按钮
const startHeart = new Position((heart.x - r1) / 2, height / 2)
ctx.moveTo(startHeart.x + r2, startHeart.y)
ctx.beginPath()
ctx.fillStyle = 'green'
ctx.arc(startHeart.x, startHeart.y, r2, 0, Math.PI * 2, true)
ctx.fill()
drawText(ctx, startHeart, 'S', '#fff')
const stopHeart = new Position(width - (heart.x - r1) / 2, height / 2)
ctx.moveTo(stopHeart.x + r2, stopHeart.y)
ctx.beginPath()
ctx.fillStyle = '#555'
ctx.arc(stopHeart.x, stopHeart.y, r2, 0, Math.PI * 2, true)
ctx.fill()
ctx.save()
ctx.strokeStyle = '#fff'
ctx.strokeText('R', stopHeart.x - ctx.measureText("P").width / 2, stopHeart.y);
ctx.restore()
// 圆的对角线
// line1 y = x - height / 2
// line2 y = heart.x + height / 2 - x
const [ p1, p2, p3, p4 ] = calculateCube(heart, r1)
const [p5, p6, p7, p8] = calculateCube(heart, r2)
ctx.beginPath()
ctx.moveTo(...p1)
ctx.lineTo(...p5)
ctx.moveTo(...p2)
ctx.lineTo(...p6)
ctx.moveTo(...p3)
ctx.lineTo(...p7)
ctx.moveTo(...p4)
ctx.lineTo(...p8)
ctx.stroke()
canvas.addEventListener('click', function (e) {
const { offsetX, offsetY } = e
const leftSide = Math.pow(offsetX - heart.x, 2) + Math.pow(offsetY - heart.y, 2)
if (leftSide < r2 * r2) {
game.stop()
return
}
// 大圆以外, 小圆以内
if (leftSide > r1 * r1) {
if (Math.pow(offsetX - startHeart.x, 2) + Math.pow(offsetY - startHeart.y, 2) < r2 * r2) {
game.start()
}
if (Math.pow(offsetX - stopHeart.x, 2) + Math.pow(offsetY - stopHeart.y, 2) < r2 * r2) {
game.destroy()
}
return
}
if (game.state !== GameState.running) {
return
}
// 判断点击上下左右
if (offsetY > offsetX - height / 2) {
if (offsetY > heart.x + height / 2 - offsetX) {
game.setDirection(Direction.down)
} else {
game.setDirection(Direction.left)
}
} else {
if (offsetY > heart.x + height / 2 - offsetX) {
game.setDirection(Direction.right)
} else {
game.setDirection(Direction.up)
}
}
})
})(document.getElementById('controller'), game)
</script>
</body>
</html>