github 传送门

<!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>
html, body {
width: 100%
}
div, ul, li {
margin: 0;
padding: 0;
box-sizing: border-box;
}
ul, li {
list-style: none;
}
#playground {
margin-top: 50px;
outline: none;
border: 1px solid dimgray
}
.dot {
position: absolute;
background: lightblue;
}
body {
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
user-select: none;
}
.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) {
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 id="controller-btns">
<button onclick="game.start()">start</button>
<button onclick="game.pause()">pause</button>
<button onclick="game.destroy()">reset</button>
</div>
<div class="game-box">
<form name="panel" class="panel">
<div>状态:<input name="gameState" disabled/></div>
<div>得分:<input name="score" disabled/></div>
</form>
<div id="playground" tabindex="0" border-collapse="none"></div>
</div>
<canvas id="controller"></canvas>
<script id="common">
const isDev = false
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 () {
return [this.x, this.y]
}
const Direction = {
up: 0,
right: 1,
down: 2,
left: 3
}
</script>
<script id="dot">
function Dot (parent, position) {
this.position = position;
const el = document.createElement("div")
el.classList.add("dot")
el.style.cssText = `width: ${ Dot.size }px; height: ${Dot.size}px`
this.parent = parent
parent.el.appendChild(el)
this.el = el
this.move()
}
Dot.size = isDev ? 10 : 20
Dot.prototype.move = function () {
const { x, y } = this.position
this.el.style.transform = `translate(${x}px,${y}px)`
}
Dot.prototype.suicide = function () {
this.el.remove()
}
Dot.prototype.setPosition = function (x, y) {
this.position.x = x
this.position.y = y
return this
}
Dot.prototype.indexArray = function () {
return [this.position.x / Dot.size, this.position.y / Dot.size]
}
</script>
<script id="cube">
const CubeState = {
unstable: 0,
stable: 1
}
function Cube (parent) {
this.parent = parent
this.dots = []
this.canRotate = true
this.state = CubeState.unstable
}
Cube.prototype.setState = function (state) {
this.state = state
}
Cube.prototype.suicide = function () {
this.dots.forEach(dot => dot.suicide())
}
Cube.prototype.mark = function (bool = true) {
const blocks = this.parent.blocks
this.dots.forEach(dot => {
const [x, y] = dot.indexArray()
blocks[y][x] = dot
})
}
Cube.prototype.tryMove = function (direction = Direction.down) {
const len = this.dots.length
let state = this.state
for (let i = 0; i < len; i++) {
const dot = this.dots[i]
const x = dot.position.x / Dot.size
const y = dot.position.y / Dot.size + 1
const nextY = dot.position.y + Dot.size
if (nextY <= 0) {
continue
}
if (nextY >= this.parent.height || this.parent.blocks[y][x]) {
state = CubeState.stable
break
}
}
this.setState(state)
if (state == CubeState.unstable) {
this.move(direction)
}
return state
}
Cube.prototype.rotate = function () {
if (!this.canRotate) {
return
}
const len = this.dots.length
const { x: x0, y: y0 } = this.getCenter()
const sin = Math.sin(Math.PI / 2)
const cos = Math.cos(Math.PI / 2)
for (let i = 0; i < len; i++) {
const { x, y } = this.dots[i].position
const x1 = (x - x0) * cos - (y -y0) * sin + x0
const y1 = (x - x0) * sin + (y - y0) * cos + y0
this.dots[i].setPosition(x1, y1)
}
this.tryMove(Direction.up)
}
Cube.prototype.move = function (direction = Direction.down) {
let deltaX = deltaY = 0
switch (direction) {
case Direction.up:
break
case Direction.right:
deltaX += Dot.size
break
case Direction.down:
deltaY += Dot.size
break
case Direction.left:
deltaX -= Dot.size
break
default:
}
const len = this.dots.length
const blocks = this.parent.blocks
for (let i = 0; i < len; i++) {
const dot = this.dots[i]
const x = dot.position.x + deltaX
const y = dot.position.y + deltaY
if(y <= 0) continue
if (
x < 0 ||
x > this.parent.width - Dot.size ||
blocks[y/Dot.size][x/Dot.size]
) {
deltaX = 0
break
}
}
for (let i = 0; i < len; i++) {
const dot = this.dots[i]
const x = dot.position.x + deltaX
const y = dot.position.y + deltaY
dot.setPosition(x, y).move()
}
}
function Stone (parent, position) {
position.y -= Dot.size * 2
Cube.call(this, parent)
this.canRotate = false
this.dots = [
new Dot(parent, new Position(position.x, position.y)),
new Dot(parent, new Position(position.x + Dot.size, position.y)),
new Dot(parent, new Position(position.x, position.y + Dot.size)),
new Dot(parent, new Position(position.x + Dot.size, position.y + Dot.size)),
]
}
Stone.prototype.getCenter = function () {
return this.dots[2].position
}
function Snake (parent, position) {
position.y -= Dot.size * 1
Cube.call(this, parent)
this.dots = new Array(5).fill(undefined).map((v, i) => {
return new Dot(parent, new Position(position.x + i * Dot.size, position.y))
})
}
Snake.prototype.getCenter = function () {
const len = this.dots.length
const half = Math.floor(len / 2)
const center = this.dots[half]
return center.position
}
function Bee (parent, position) {
position.y -= Dot.size * 1
Cube.call(this, parent)
this.dots = [
new Dot(parent, new Position(position.x, position.y)),
]
}
Bee.prototype.getCenter = function () {
return this.dots[0].position
}
function Zebra (parent, position) {
position.y -= Dot.size * 2
Cube.call(this, parent)
this.dots = [
new Dot(parent, new Position(position.x, position.y)),
new Dot(parent, new Position(position.x, position.y + Dot.size)),
new Dot(parent, new Position(position.x + Dot.size, position.y + Dot.size)),
new Dot(parent, new Position(position.x + Dot.size, position.y + Dot.size * 2)),
]
}
Zebra.prototype.getCenter = function () {
return this.dots[1].position
}
function SeaHorse (parent, position) {
position.y -= Dot.size * 2
Cube.call(this, parent)
this.dots = [
new Dot(parent, new Position(position.x, position.y)),
].concat(new Array(3).fill(undefined).map((v, i) => {
return new Dot(parent, new Position(position.x + Dot.size * i, position.y + Dot.size))
}))
}
SeaHorse.prototype.getCenter = function () {
return this.dots[1].position
}
function Tank (parent, position) {
position.y -= Dot.size * 2
Cube.call(this, parent)
this.dots = [
new Dot(parent, new Position(position.x, position.y)),
new Dot(parent, new Position(position.x, position.y + Dot.size)),
new Dot(parent, new Position(position.x - Dot.size, position.y + Dot.size)),
new Dot(parent, new Position(position.x + Dot.size, position.y + Dot.size)),
]
}
Tank.prototype.getCenter = function () {
return this.dots[1].position
}
function Human (parent, position) {
position.y -= Dot.size * 3
Cube.call(this, parent)
this.dots = [
...new Array(parent.row).fill(undefined).map((v, i) => {
return new Dot(parent, new Position(i * Dot.size, position.y))
}),
new Dot(parent, new Position(position.x, position.y + 1 * Dot.size)),
...new Array(parent.row).fill(undefined).map((v, i) => {
return new Dot(parent, new Position(i * Dot.size, position.y + Dot.size * 2))
}),
new Dot(parent, new Position(position.x + Dot.size * 3, position.y + 3 * Dot.size)),
...new Array(parent.row).fill(undefined).map((v, i) => {
return new Dot(parent, new Position(i * Dot.size, position.y + Dot.size * 4))
})
]
}
Human.prototype.getCenter = function () {
return this.dots[10].position
}
function extend (targetCtor, ...sourceCtors) {
for (var i = 0; i < sourceCtors.length; i++) {
const sourceCtor = sourceCtors[i];
const oldPrototype = sourceCtor.prototype
sourceCtor.prototype = Object.assign(Object.create(targetCtor.prototype), oldPrototype)
sourceCtor.prototype.constructor = sourceCtor
}
targetCtor.random = function (parent) {
const index = Math.floor(Math.random() * sourceCtors.length)
const position = new Position(Math.round(parent.row / 2) * Dot.size, 0)
const Ctor = sourceCtors[index]
const cube = new Ctor(parent, position)
return cube
}
}
extend(Cube, Stone, Snake, Bee, Zebra, SeaHorse, Tank)
</script>
<script>
function Playground (row = 20, col = 10) {
const width = row * Dot.size
const height = col * Dot.size
this.col = col
this.row = row
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(col)
.fill(false)
.map(() => new Array(row))
this.blocks = blocks
}
</script>
<script id="game">
const GameState = {
failure: -1,
init: 0,
running: 1,
pause: 2,
stop: 3
}
function Game () {
this.score = 0
this.cube = null
this.startTime = null
this.previousTimeStamp = null
this.playground = null
this.state = null
this.normalInterval = 300
this.interval = this.normalInterval
this.lastSetDirectionStamp = Date.now()
this.speedupInterval = 500
this.speedupIntervalDelta = 100
this.speedupIntervalDuration = 2000
this.cubeCount = 0
this.keydownHandler = null
this.animationFrameRes = null
}
Game.prototype.init = function () {
this.setState(GameState.init)
if (isDev) {
this.playground = new Playground()
} else {
this.playground = new Playground(19, 28)
}
this.initHandler()
if (isDev) {
document.getElementById('controller').style.display = 'none'
} else {
document.getElementById('controller-btns').style.display = 'none'
}
return this
}
Game.prototype.initHandler = function () {
this.keydownHandler = this.keydown.bind(this)
this.playground.el.addEventListener("keydown", this.keydownHandler)
}
Game.prototype.setState = function (state) {
this.state = state
this.stateChanged(state)
}
Game.prototype.stateChanged = function (state) {
switch(state) {
case GameState.running:
break
case GameState.failure: alert("game over")
default:
}
}
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)
this.cube === null && this.makeCube()
this.animationFrameRes = window.requestAnimationFrame(this.run.bind(this))
this.playground.el.focus()
}
Game.prototype.clearDots = function () {
const blocks = this.playground.blocks
let count = 0
for (let i = blocks.length - 1; i >= 0; i--) {
const row = blocks[i]
let flag = true
for (let j = 0; j < row.length; j++) {
if (!row[j]) {
flag = false
break
}
}
if (flag) {
count++
for (let j = 0; j < row.length; j++) {
row[j].suicide()
blocks[i][j] = null
}
continue
}
for (let j = 0; j < row.length; j++) {
const dot = row[j]
if (!dot || !count) continue
const [x, y] = dot.indexArray()
dot.setPosition(dot.position.x, dot.position.y + Dot.size * count).move()
blocks[y][x] = null
blocks[y + count][x] = dot
}
}
this.score += count * 10
}
Game.prototype.makeCube = function () {
const cube = Cube.random(this.playground)
this.cube = cube
return cube
}
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;
let cubeState = CubeState.unstable
if (this.cube) {
cubeState = this.cube.tryMove()
}
if (cubeState === CubeState.stable) {
this.cube.mark(true)
const len = this.cube.dots.length
for (let i = 0; i < len; i++) {
const dot = this.cube.dots[i]
if (dot.position.y === 0) {
game.over()
break;
}
}
if (this.state === GameState.running) {
this.clearDots()
this.makeCube()
}
}
}
this.updatePanel()
if (this.state === GameState.running) {
window.requestAnimationFrame(this.run.bind(this));
}
}
Game.prototype.over = function () {
this.setState(GameState.failure)
window.alert('game over')
}
Game.prototype.pause = function () {
this.setState(GameState.pause)
}
Game.prototype.stop = function () {
this.setState(GameState.stop)
}
Game.prototype.destroy = function () {
this.state = GameState.stop
this.playground.el.removeEventListener('keydown', this.keydownHandler)
this.playground.el.querySelectorAll('.dot').forEach(dot => dot.remove())
this.animationFrameRes && window.cancelAnimationFrame(this.animationFrameRes)
Game.apply(this)
this.init()
this.updatePanel()
}
Game.prototype.setDirection = function (direction) {
if (direction == Direction.up) {
this.cube.rotate()
return
}
this.cube.tryMove(direction)
}
Game.prototype.updatePanel = function () {
const panel = document.forms.panel
panel.gameState.value = this.state
panel.score.value = this.score
}
const game = new Game().init()
</script>
<script id="controller">
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()
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>