HTML5实现一笔画游戏
一笔画问题
一笔画是图论科普中一个著名的问题,它起源于柯尼斯堡七桥问题科普。当时的东普鲁士哥尼斯堡城中有一条河,在这条河上有七座桥:
蓝色的代表河,这条河将城市分开成为四个区域,而七个橙色的矩形为座桥。
欧拉把实际的问题抽象为平面上的点与线,每一座桥视为一条线,桥所连接的地区视为点。
“一笔画”问题涉及的核心概念包括连通图、奇点、偶点等。连通图指的是图中任意两个顶点之间都存在一条路径相连且没有重复。奇点则是与奇数个边相连的顶点,偶点则是与偶数个边相连的顶点。
欧拉发现,一个连通图能够一笔画出的条件是:要么图中所有顶点都是偶点,要么图中只有两个奇点。这个规律被称为欧拉定理,它为解决一笔画问题提供了理论基础。
由于哥尼斯堡七桥问题的抽象图中的四个顶点全部是奇顶点,所以它无法实现符合要求的走法,也就是不可能一笔画成。
数学家欧拉在他1736年发表的论文《柯尼斯堡的七桥》中不仅解决了七桥问题,也提出了一笔画定理,顺带解决了一笔画问题。他的这篇论文也成为图论史上第一篇重要文献。
HTML5实现一笔画游戏
HTML5和Canvas API提供了强大的图形处理能力,足以支持一笔画这样的游戏开发。
先给出效果图示:
游戏由两个文件构成:html文件(我这里命名为:一笔画游戏.html)和JavaScript脚本文件 (我这里命名为:game.js),我这里将这两个文件放在同一文件夹中。
html文件“一笔画游戏.html”源码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一笔画游戏</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="300" height="300"></canvas>
<div>
<button onclick="startGame('easy')">简单</button>
<button onclick="startGame('medium')">中等</button>
<button onclick="startGame('hard')">困难</button>
<br>
连接线段时,必须按照一定的顺序来连接点。
<br>
两点间连线方法:从一个点按下鼠标左键拖动到释放。
</div>
<script src="game.js"></script>
</body>
</html>
JavaScript脚本文件 game.js源码如下:
// game.js
let canvas = document.getElementById("gameCanvas");
let ctx = canvas.getContext("2d");
let isDrawing = false;
let startPoint = null;
let lastPointId = null;
let presetPoints = [];
let presetLines = [];
let userLines = [];
const levels = {
easy: {
points: [
{id: 1, x: 50, y: 50},
{id: 2, x: 150, y: 50},
{id: 3, x: 150, y: 150},
{id: 4, x: 50, y: 150}
],
lines: [
{start: 1, end: 2},
{start: 2, end: 3},
{start: 3, end: 4},
{start: 4, end: 1},
{start: 2, end: 4}
]
},
medium: {
points: [
{id: 1, x: 50, y: 100},
{id: 2, x: 150, y: 100},
{id: 3, x: 250, y: 100},
{id: 4, x: 100, y: 200},
{id: 5, x: 200, y: 200}
],
lines: [
{start: 1, end: 2},
{start: 2, end: 3},
{start: 1, end: 4},
{start: 2, end: 5},
{start: 3, end: 5},
{start: 4, end: 5}
]
},
hard: {
points: [
{id: 1, x: 50, y: 50},
{id: 2, x: 150, y: 50},
{id: 3, x: 250, y: 50},
{id: 4, x: 50, y: 150},
{id: 5, x: 150, y: 150},
{id: 6, x: 250, y: 150},
{id: 7, x: 50, y: 250},
{id: 8, x: 150, y: 250},
{id: 9, x: 250, y: 250}
],
lines: [
{start: 1, end: 2},
{start: 2, end: 3},
{start: 1, end: 4},
//{start: 2, end: 5},
{start: 3, end: 6},
{start: 4, end: 5},
//{start: 5, end: 6},
{start: 4, end: 7},
{start: 5, end: 8},
{start: 6, end: 9},
{start: 7, end: 8},
{start: 8, end: 9}
]
}
};
function startGame(difficulty) {
const level = levels[difficulty];
if (!level) {
console.error('未知难度级别');
return;
}
presetPoints = level.points;
presetLines = level.lines;
userLines = [];
lastPointId = null;
draw();
}
canvas.addEventListener("mousedown", (e) => {
isDrawing = true;
startPoint = getPointFromMouseEvent(e);
});
canvas.addEventListener("mouseup", (e) => {
if (!isDrawing || !startPoint) return;
let endPoint = getPointFromMouseEvent(e);
if (endPoint && startPoint.id !== endPoint.id) {
if (lastPointId === null || lastPointId === startPoint.id) {
if (isPresetLine(startPoint.id, endPoint.id)) {
userLines.push({start: startPoint, end: endPoint});
lastPointId = endPoint.id;
draw();
} else {
alert("不能绘制原图中不存在的线段。");
}
} else {
alert("必须按顺序连接点。");
}
}
isDrawing = false;
});
function getPointFromMouseEvent(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
return presetPoints.find(p => Math.sqrt((p.x - x) ** 2 + (p.y - y) ** 2) < 10);
}
function isPresetLine(startId, endId) {
return presetLines.some(line => (line.start === startId && line.end === endId) || (line.start === endId && line.end === startId));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制预设线段
presetLines.forEach(line => {
let start = presetPoints.find(p => p.id === line.start);
let end = presetPoints.find(p => p.id === line.end);
drawLine(start, end, 'black');
});
// 绘制用户线段
userLines.forEach(line => {
drawLine(line.start, line.end, 'red');
});
// 绘制点
presetPoints.forEach(point => {
drawPoint(point);
});
// 检查胜利条件
if (checkWin()) {
alert("恭喜,你完成了这个难度级别的游戏!");
}
}
function checkWin() {
if (userLines.length !== presetLines.length) {
return false;
}
// 检查每个用户线段是否匹配预设线段
for (let userLine of userLines) {
const startId = userLine.start.id;
const endId = userLine.end.id;
const match = presetLines.some(line =>
(line.start === startId && line.end === endId) ||
(line.start === endId && line.end === startId)
);
if (!match) {
return false;
}
}
return true;
}
function drawLine(start, end, color) {
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.strokeStyle = color;
ctx.stroke();
}
function drawPoint(point) {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
}
// 初始化游戏
startGame('easy');
说明,定义不同难度的关卡数据:
const levels = {
easy: {
// 定义简单难度的点和线段
},
medium: {
// 定义中等难度的点和线段
},
hard: {
// 定义高难度的点和线段
}
};
这些点的 x 和 y 坐标是基于画布的尺寸和布局预设的。你可能需要根据你的具体实现调整这些坐标值,以确保点和线在你的游戏界面中正确显示。此外,这些关卡设计仅作为示例,你可以根据需要调整点和线的数量及布局,创造出更多不同难度的关卡。