
<!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">
<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 {
height: 100%;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
}
canvas {
user-select: none;
position: absolute;
}
#stage {
position: relative;
height: auto;
}
#background-layer { z-index: 1 }
#buff-layer { z-index: 2 }
#moving-buff-layer { z-index: 3 }
#playground { z-index: 4 }
.controller {
display: flex;
justify-content: space-around;
margin-bottom: 12px;;
}
</style>
</head>
<body>
<div class="controller">
<button onclick="game.destroy()">reset</button>
<button onclick="game.snapShotStack.backward()">上一步</button>
<button onclick="game.snapShotStack.forward()">下一步</button>
</div>
<div id="stage">
<canvas id="background-layer"></canvas>
<canvas id="buff-layer"></canvas>
<canvas id="moving-buff-layer"></canvas>
<canvas id="playground" onselectstart="return false;"></canvas>
</div>
<script>
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]
}
function get (obj, ...props) {
let res = obj
while (res && props.length > 0) {
const key = props.shift()
if (key in res) {
res = res[key]
if (props.length === 0) {
return res
}
} else {
return
}
}
}
</script>
<script>
function Chequer (ctx, position, size) {
this.ctx = ctx
this.position = position
this.selected = false
this.heart = new Position(
this.position.x + size / 2,
this.position.y + size / 2
)
this.render()
}
Chequer.prototype.setHeart = function (heart) {
this.heart.x = heart.x
this.heart.y = heart.y
return this
}
Chequer.prototype.toggleSelect = function (selected) {
if (typeof selected === 'boolean') {
this.selected = selected
} else {
this.selected = !this.selected
}
this.render()
}
Chequer.prototype.render = function () {
this.clear()
this.ctx.beginPath()
const heart = this.heart
this.ctx.arc(heart.x, heart.y, Chequer.radius, 0, Math.PI * 2, true);
if (this.selected) {
this.ctx.fill()
} else {
this.ctx.stroke()
}
const r = Chequer.radius * 0.618
this.ctx.beginPath()
this.ctx.save()
this.ctx.fillStyle = this.selected ? '#fff' : '#000'
this.ctx.arc(heart.x, heart.y, r, 0, Math.PI * 2, true)
this.ctx.fill()
this.ctx.restore()
}
Chequer.prototype.clear = function () {
const lineWidth = this.ctx.lineWidth
const diameter = Chequer.radius * 2 + 2 * lineWidth
this.ctx.clearRect(this.heart.x - Chequer.radius - lineWidth, this.heart.y - Chequer.radius - lineWidth, diameter, diameter)
this.ctx.beginPath()
}
Chequer.radius = 12
</script>
<script>
function Lattice (ctx, position) {
const size = Chequer.radius * 2 + Lattice.padding * 2
this.ctx = ctx
this.size = size
this.x = position.x
this.y = position.y
position.x *= size
position.y *= size
this.position = position
this.selected = false
}
Lattice.prototype.clear = function () {
this.ctx.clearRect(this.position.x, this.position.y, this.size, this.size)
}
Lattice.padding = 10
Lattice.lineWidth = 1
function ChequerLattice (ctx, position, playgroundCtx, isRender = true) {
Lattice.call(this, ctx, position)
this.chequer = new Chequer(playgroundCtx, position, this.size)
isRender && this.render()
}
ChequerLattice.prototype.render = function () {
const { ctx, position, size } = this
ctx.beginPath()
ctx.rect(position.x, position.y, size, size)
ctx.stroke()
}
ChequerLattice.prototype.toggleSelect = function (selected) {
if (typeof selected === 'boolean') {
this.selected = selected
} else {
this.selected = !this.selected
}
this.chequer.toggleSelect(selected)
}
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
}
}
extend(Lattice, ChequerLattice)
function Chessboard (ctx) {
const size = (Chequer.radius * 2 + Lattice.padding * 2) * 7 + 8 * Lattice.lineWidth
this.size = size
this.ctx = ctx
}
Chessboard.prototype.create = function () {
ctx.beginPath()
ctx.rect(0, 0, this.size, this.size);
ctx.stroke()
}
</script>
<script>
function SnapShot (instance) {
this.ctor = instance.constructor
const position = instance.position
const size = instance.size
this.position = new Position(position.x / size, position.y / size)
}
function SnapShotBuilder () {
this._from = null
this._to = null
this._path = null
}
SnapShotBuilder.prototype.from = function (from) {
this._from = from
}
SnapShotBuilder.prototype.to = function (to) {
this._to = to
}
SnapShotBuilder.prototype.path = function (path) {
this._path = path
}
SnapShotBuilder.prototype.build = function () {
return {
from: this._from,
to: this._to,
path: this._path
}
}
function SnapShotStack (game) {
this.index = -1
this.game = game
this.stacks = []
}
SnapShotStack.prototype.push = function (snapshot) {
console.info(`
'from', ${snapshot.from.ctor.name} ${snapshot.from.position.x}, ${snapshot.from.position.y},
'path', ${snapshot.path.ctor.name} ${snapshot.path.position.x}, ${snapshot.path.position.y},
'to', ${snapshot.to.ctor.name} ${snapshot.to.position.x}, ${snapshot.to.position.y}
`)
const len = this.stacks.length
if (len !== this.index + 1) {
this.stacks.splice(this.index, this.stacks.length, snapshot)
} else {
this.stacks.push(snapshot)
}
this.index++
}
SnapShotStack.prototype.forward = function () {
if (this.index >= this.stacks.length) {
console.warn("SnapShotStack", )
return
}
this.index++
this.do(true)
}
SnapShotStack.prototype.backward = function () {
console.info(this.message())
if (this.index < 0) {
console.warn("none can be revert")
return
}
this.do()
this.index--
}
SnapShotStack.prototype.do = function (isForward) {
const stack = this.stacks[this.index]
for (const type in stack) {
const Ctor = stack[type].ctor
const { x, y } = stack[type].position
let lattice
this.game.lattices[x][y]?.chequer?.clear()
const params = [this.game.bgCtx, new Position(x, y), this.game.playgroundCtx]
if (isForward) {
lattice = Ctor === Lattice ? new Lattice(...params) : new ChequerLattice(...params, false)
} else {
lattice = Ctor === Lattice ? new ChequerLattice(...params, false) : new Lattice(...params)
}
this.game.lattices[x][y] = lattice
}
}
SnapShotStack.prototype.message = function () {
return `length: ${this.stacks.length}; index: ${this.index}`
}
</script>
<script>
function Game () {
this.chessboard = this.createBroad()
this.latticeSize = 7
this.lattices = []
this.selectedLattice = null
this.mousedownHandler = null
this.bgCtx = null
this.playgroundCtx = null
this.buffCtx = null
this.buffedAreas = []
this.movingBuffCtx = null
this.snapShotStack = new SnapShotStack(this)
}
Game.prototype.init = function () {
document.getElementById("stage").style.height = this.chessboard.size + 'px'
this.selectedChequer = null
this.initLattices()
this.initHandlers()
this.reset()
return this
}
Game.prototype.initLattices = function () {
this.lattices = new Array(this.latticeSize).fill(null).map(v => new Array(this.latticeSize).fill(null))
}
Game.prototype.initHandlers = function () {
this.mousedownHandler = this.mousedown.bind(this)
const canvas = document.getElementById('playground')
canvas.addEventListener('mousedown', this.mousedownHandler)
}
Game.prototype.reset = function () {
const size = this.chessboard.size
{
const canvas = document.getElementById('background-layer')
this.bgCtx= canvas.getContext('2d')
canvas.width = size
canvas.height = size
}
{
const canvas = document.getElementById('buff-layer')
this.buffCtx= canvas.getContext('2d')
canvas.width = size
canvas.height = size
}
{
const canvas = document.getElementById('moving-buff-layer')
this.movingBuffCtx= canvas.getContext('2d')
canvas.width = size
canvas.height = size
}
{
const canvas = document.getElementById('playground')
canvas.width = size
canvas.height = size
this.playgroundCtx = canvas.getContext('2d')
}
this.resetPlayground()
}
Game.prototype.createBuffLayer = function (lattice) {
const { position, size } = lattice
const [x, y] = [position.x / size, position.y / size]
const tryGet = (x, y) => {
const rows = this.lattices[x]
if (rows) {
return rows[y]
}
}
const up = (step = 1) => tryGet(x, y - step)
const down = (step = 1) => tryGet(x, y + step)
const left = (step = 1) => tryGet(x - step, y)
const right = (step = 1) => tryGet(x + step, y)
const buffed = []
if (up()?.chequer) {
const far = up(2)
if (far && !far.chequer) {
buffed.push(far)
}
}
if (down()?.chequer) {
const far = down(2)
if (far && !far.chequer) {
buffed.push(far)
}
}
if (left()?.chequer) {
const far = left(2)
if (far && !far.chequer) {
buffed.push(far)
}
}
if (right()?.chequer) {
const far = right(2)
if (far && !far.chequer) {
buffed.push(far)
}
}
buffed.forEach((lattice) => this.renderBuff(lattice))
}
Game.prototype.renderBuff = function (lattice) {
const { position, size } = lattice
const ctx = this.buffCtx
const gradient = ctx.createLinearGradient(position.x , position.y, position.x + size, position.y);
gradient.addColorStop(0, "lightgreen");
gradient.addColorStop(0.5, "#c8f8c8");
gradient.addColorStop(1, "lightgreen");
ctx.fillStyle = gradient;
const params = [position.x, position.y, size, size]
this.buffedAreas.push(lattice)
ctx.fillRect(...params);
}
Game.prototype.clearBuffLayer = function () {
const ctx = this.buffCtx
ctx.beginPath()
for (let i = this.buffedAreas.length - 1; i >= 0; i--) {
const { position, size } = this.buffedAreas[i]
const params = [position.x, position.y, size, size]
ctx.clearRect(...params)
this.buffedAreas.splice(i, 1)
}
}
Game.prototype.resetPlayground = function () {
const bgCtx = this.bgCtx
for (let i = 0 ; i < this.latticeSize; i++) {
for (let j = 0 ; j < this.latticeSize; j++) {
const position = new Position(i, j)
if (
((i < 2 || i > 4) && j < 2) ||
((i < 2 || i > 4) && j > 4)
) {
continue
}
if (i === 3 && j === 3) {
this.lattices[i][j] = new Lattice(bgCtx, position, this.playgroundCtx)
} else {
this.lattices[i][j] = new ChequerLattice(bgCtx, position, this.playgroundCtx)
}
}
}
}
Game.prototype.createBroad = function () {
const canvas = document.getElementById('background-layer')
const ctx = canvas.getContext('2d')
this.ctx = ctx
return new Chessboard(ctx)
}
Game.prototype.destroy = function () {
this.clearBuffLayer()
this.lattices.flat().forEach(lattice => lattice?.chequer?.clear())
for (let i = 0; i < this.lattices.length; i++) {
const rows = this.lattices[i]
for (let j = 0 ; j < rows.length; j++) {
rows[j] = null
}
}
this.selectedLattice = null
this.resetPlayground()
}
Game.prototype.toggleSelect = function (lattice, isSelected) {
lattice.toggleSelect(isSelected)
if (isSelected) {
this.createBuffLayer(lattice)
} else {
this.clearBuffLayer()
}
}
Game.prototype.mousedown = function (e) {
const { offsetX, offsetY } = e
let isInBuffedArea = false
rowsLoop:
for (let i = 0; i < this.lattices.length; i++) {
const rows = this.lattices[i]
colsLoop:
for (let j = 0 ; j < rows.length; j++) {
const lattice = rows[j]
if (!lattice) {
continue
}
if (!lattice.chequer) {
for (let k = 0; k < this.buffedAreas.length; k++ ) {
const buffedArea = this.buffedAreas[k];
isInBuffedArea = offsetX > buffedArea.position.x &&
offsetY > buffedArea.position.y &&
offsetX < buffedArea.position.x + buffedArea.size &&
offsetY < buffedArea.position.y + buffedArea.size
if (isInBuffedArea && lattice === buffedArea) {
isEat = true
this.clearBuffLayer()
this.eat(i, j)
this.judge()
break rowsLoop
}
}
continue
}
const chequer = lattice.chequer
const heart = chequer.heart
if ((heart.x - offsetX) ** 2 + (heart.y - offsetY) ** 2 < Chequer.radius**2) {
if (this.selectedLattice === lattice && lattice.selected) {
this.toggleSelect(this.selectedLattice, false)
this.selectedLattice = null
break
}
if (this.selectedLattice) {
this.toggleSelect(this.selectedLattice, false)
}
this.toggleSelect(lattice, true)
this.selectedLattice = lattice
break rowsLoop
}
}
}
return false
}
Game.prototype.judge = function () {
const half = Math.floor(this.latticeSize / 2)
const center = new Position(half, half)
let restCount = 0
let restChequers = []
for (let i = 0; i < this.lattices.length; i++) {
const rows = this.lattices[i]
for (let j = 0 ; j < rows.length; j++) {
if (rows[j]?.chequer) {
restCount++
restChequers.push([i, j])
}
}
}
if (
restCount === 1 &&
(restChequers[0].x === center.x && restChequers[0].y === center.y)) {
console.info('congratulations, you have won the game')
}
}
Game.prototype.hasNeighbor = function (x, y) {
const bool = get(this.lattices, x - 1, y, 'chequer') ||
get(this.lattices, x + 1, y, 'chequer') ||
get(this.lattices, x, y - 1, 'chequer') ||
get(this.lattices, x, y + 1, 'chequer')
return bool
}
Game.prototype.eat = function (x, y) {
const snapShotBuilder = new SnapShotBuilder()
const lattice = this.lattices[x][y]
this.lattices[x][y] = new ChequerLattice(this.bgCtx, new Position(x, y), this.playgroundCtx, false)
snapShotBuilder.to(new SnapShot(this.lattices[x][y]))
if (lattice.position.x === this.selectedLattice.position.x) {
if (lattice.position.y < this.selectedLattice.position.y) {
y++
} else {
y--
}
}
if (lattice.position.y === this.selectedLattice.position.y) {
if (lattice.position.x < this.selectedLattice.position.x) {
x++
} else {
x--
}
}
this.lattices[x][y].chequer.clear()
this.lattices[x][y] = new Lattice(this.bgCtx, new Position(x, y), this.playgroundCtx)
snapShotBuilder.path(new SnapShot(this.lattices[x][y]))
x = this.selectedLattice.x
y = this.selectedLattice.y
this.lattices[x][y] = new Lattice(this.bgCtx, new Position(x, y), this.playgroundCtx)
snapShotBuilder.from(new SnapShot(this.lattices[x][y]))
this.selectedLattice.chequer.clear()
this.selectedLattice = null
this.snapShotStack.push(snapShotBuilder.build())
}
const game = new Game().init()
</script>
</body>
</html>