背景介绍
因为之前用c#的winform中的gdi+,java图形包做过五子棋,所以做这个逻辑思路也就驾轻就熟,然而最近想温故html5的canvas绘图功能(公司一般不用这些),所以做了个五子棋,当然没参考之前的客户端代码,只用使用之前计算输赢判断算法和电脑ai(网络借取)的算法,当然现在html5做的五子棋百度一下非常多,但是自己实现一边总归是好事情,好了废话不多说了进入正题。^_^
界面功能介绍与后续可增加功能
目前界面功能:
主界面包含
1:人人、人机对战选项 2:棋子外观选择 3:棋盘背景选择 4:棋盘线条颜色选择
游戏界面包含
立即学习“前端免费学习笔记(深入)”;
1:玩家名称 2:玩家棋子 3:当前由谁下棋背景定位 4:玩家比分 5:功能菜单区域(重新开始和无限悔棋) 6:棋盘区域 7.胜利后连环棋子连接 8.最后下棋位置闪烁显示 9.光标定位
游戏结束界面
1:胜利背景图 2:胜利玩家姓名 3:继续下一把按钮
可增加功能
1.返回主界面 2.保存棋局和相关数据 3.读取棋局和相关数据 4.交换角色 5.网络对战(2台机器)6.双方思考总时间记录
界面截图赏析(没花时间美化,比较丑)


整体设计和主代码介绍
整体设计
下棋流程:玩家or电脑AI下棋 ---> 绘制棋子 ---> 设定棋子二维坐标值 ----> logic(逻辑判断) ----> (玩家)一方五子连环 ---> 获胜界面
↑ |
| ↓
<--------------------------------------------------------------------------------------------没有五子
悔棋流程(人人对战):一方玩家悔棋 ----> 弹出下棋记录堆栈并设定为它是最后一枚棋 ---> 清除最后一枚棋子图像 ---> 清除棋子二维坐标值---> 重新定位显示最后下棋位置并闪烁
悔棋流程(人机对战):玩方悔棋 ----> 弹出下棋记录堆栈2次,设定上一次电脑为最后一枚棋 ---> 清除弹出的2次记录图像 ---> 清除棋子2个棋子二维坐标值---> 重新定位显示最后下棋位置并闪烁
主代码介绍
主代码分为二块: 1.界面逻辑块 2.游戏主体块 (界面与游戏代码分离,逻辑清晰,分工明确)
模拟事件通知:游戏主体逻辑块,每次结果都会通知到界面层来进行交互(类似于c#或者java的委托或事件)
界面逻辑代码
<script type="text/javascript">
var gb = null;
var infoboj = document.getElementsByClassName("info")[0];
var pl1obj = document.getElementById("pl1");
var pl2obj = document.getElementById("pl2");
var plname1obj = document.getElementById("plname1");
var plname2obj = document.getElementById("plname2");
var chesstypeobj = document.getElementsByName("chesstype");
var chesscolorobj = document.getElementsByName("chesscolor");
var chessbgObj = document.getElementsByName("chessbg");
var winerpnl = document.getElementById("winer");
document.getElementById("startgame").addEventListener("click", function() {
function initParams() {
var chessTypeValue = 1;
if (chesstypeobj.length > 0) {
for (var i = 0; i < chesstypeobj.length; i++) {
if (chesstypeobj[i].checked) {
chessTypeValue = chesstypeobj[i].value;
break;
}
}
}
var linevalue = "";
if (chesscolorobj.length > 0) {
for (var i = 0; i < chesscolorobj.length; i++) {
if (chesscolorobj[i].checked) {
linevalue = chesscolorobj[i].value;
break;
}
}
}
var bcorimgvalue = "";
if (chessbgObj.length > 0) {
for (var i = 0; i < chessbgObj.length; i++) {
if (chessbgObj[i].checked) {
bcorimgvalue = chessbgObj[i].value;
break;
}
}
}
return {
lineColor: linevalue,
chessType: chessTypeValue, //1 色彩棋子 2 仿真棋子
playAName: plname1Input.value,
playBName: plname2Input.value,
backColorORImg: bcorimgvalue,
playAImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playA.png",
playBImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playB.png",
playerBIsComputer:openComputer.checked
};
}
document.getElementById("cc").style.display = "block";
gb = new gobang(initParams());
/**
* 设置一些界面信息
* @param {Object} opt
*/
gb.info = function(opt) {
infoboj.style.visibility = "visible";
document.getElementsByClassName("startpnl")[0].style.visibility = "hidden";
plname1obj.innerHTML = opt.playAName;
plname2obj.innerHTML = opt.playBName;
if (opt.chessType == 1) {
var span1 = document.createElement("span");
pl1obj.insertBefore(span1, plname1obj);
var span2 = document.createElement("span");
pl2obj.insertBefore(span2, plname2obj);
} else {
var img1 = document.createElement("img");
img1.src = opt.playAImg;
pl1obj.insertBefore(img1, plname1obj);
var img2 = document.createElement("img");
img2.src = opt.playBImg;
pl2obj.insertBefore(img2, plname2obj);
}
}
/**
* 每次下棋后触发事件
* @param {Object} c2d
*/
gb.operate = function(opt, c2d) {
if (!c2d.winer || c2d.winer <= 0) {
pl1obj.removeAttribute("class", "curr");
pl2obj.removeAttribute("class", "curr");
if (c2d.player == 1) {
pl2obj.setAttribute("class", "curr");
} else {
pl1obj.setAttribute("class", "curr");
}
document.getElementById("backChessman").innerHTML="悔棋("+c2d.canBackTimes+")";
} else {
var winname = c2d.winer == 1 ? opt.playAName : opt.playBName;
var str = "恭喜,【" + winname + "】赢了!"
alert(str);
winerpnl.style.display = "block";
document.getElementById("winerName").innerHTML = "恭喜,【" + winname + "】赢了!";
document.getElementById("pl" + c2d.winer).style.backgroundColor = "pink";
document.getElementById("scoreA").innerHTML = c2d.playScoreA;
document.getElementById("scoreB").innerHTML = c2d.playScoreB;
}
}
gb.start();
});
document.getElementById("openComputer").addEventListener("change", function() {
if (this.checked) {
plname2Input.value = "电脑";
plname2Input.disabled = "disabled";
} else {
plname2Input.value = "玩家二";
plname2Input.disabled = "";
}
});
//document.getElementById("openComputer").checked="checked";
//重新开始
function restartgui() {
if (gb) {
winerpnl.style.display = "none";
pl1obj.removeAttribute("class", "curr");
pl2obj.removeAttribute("class", "curr");
document.getElementById("pl1").style.backgroundColor = "";
document.getElementById("pl2").style.backgroundColor = "";
gb.restart();
}
};
</script>游戏主体代码块(只包含函数声明代码)
// ==========
// =name:gobang 游戏
// =anthor:jasnature
// =last modify date:2016-04-13
// ==========
(function(win) {
var gb = function(option) {
var self = this,
canObj = document.getElementById("cc"),
can = canObj.getContext("2d");
self.contextObj = canObj;
self.context = can; if (!self.context) {
alert("浏览器不支持html5"); return;
};
self.Opt = {
lineColor: "green",
chessType: 1, //1 色彩棋子 2 仿真棋子
playAName: "play1",
playBName: "play2",
playAColor: "red",
playBColor: "blue",
playAImg: "img/playA.png",
playBImg: "img/playB.png",
backColorORImg: "default",
playerBIsComputer: false
};
self.operate; //合并属性
for (var a in option) { //console.log(opt[a]);
self.Opt[a] = option[a];
}; //私有变量
var my = {};
my.enableCalcWeightNum = false; //显示AI分数
my.gameover = false; //棋盘相关
my.baseWidth = 30;
my.lastFocusPoint = {}; //鼠标最后移动到的坐标点,计算后的
my.cw = self.contextObj.offsetWidth; //棋盘宽
my.ch = self.contextObj.offsetHeight; //高
my.xlen = Math.ceil(my.cw / my.baseWidth); //行数
my.ylen = Math.ceil(my.ch / my.baseWidth); //列
my.chessRadius = 14; //棋子半径
my.playerBIsComputer = false; //棋手B是否是电脑
my.ComputerThinking = false; //电脑是否在下棋
my.goBackC2dIsComputer = false; //最后下棋是否为电脑
my.switcher = 1; //由谁下棋了 1-a 2-b or computer
my.winer = -1; //赢家,值参考my.switcher
my.playScoreA = 0;
my.playScoreB = 0; //x,y 正方形数量(20*20)
my.rectNum = my.xlen; //存储已下的点
my.rectMap = [];
my.NO_CHESS = -1; //没有棋子标识
my.goBackC2d = {}; //最后下的数组转换坐标
my.downChessmanStackC2d = []; // 记录已下棋子的顺序和位置,堆栈
my.focusFlashInterval = null; //焦点闪烁线程
my.focusChangeColors = ["red", "fuchsia", "#ADFF2F", "yellow", "purple", "blue"];
my.eventBinded = false;
my.currChessBackImg = null;
my.currChessAImg = null;
my.currChessBImg = null;
my.currDrawChessImg = null;
my.ChessDownNum = 0; //2个玩家 下棋总数
/**
* 开始游戏
*/
self.start = function() {
};
/**
* 重新开始游戏
*/
self.restart = function() {
};
/**
* 悔棋一步 ,清棋子,并返回上一次参数
*/
self.back = function() {
}
/**
* 初始化一些数据
*/
function init() {
}
//
self.paint = function() {
//
//
//window.requestAnimationFrame(drawChessboard);
//
};
/**
* 游戏逻辑
*/
function logic(loc, iscomputer) {
};
/**
* 判断是否有玩家胜出
* @param {Object} c2d
*/
function isWin(c2d) {
return false;
} /**
* 连接赢家棋子线
* @param {Object} points
*/
function joinWinLine(points) {
} /**
* 画棋盘 */
function drawChessboard() {
}; /**
* 画棋子
* @param {Object} loc 鼠标点击位置
*/
function drawChessman(c2d) {
} function drawRect(lastRecord, defColor) {
}
/**
* 闪烁最后下棋点
*/
function flashFocusChessman() {
}
/**
* 清棋子
* @param {Object} c2d
*/
function clearChessman() {
}
/**
* @param {Object} loc
* @return {Object} I 二维数组横点(),J二维数组纵点,IX 横点起始坐标,JY纵点起始坐标,player 最后下棋玩, winer 赢家
*/
function calc2dPoint(loc) { var txp = Math.floor(loc.x / my.baseWidth),
typ = Math.floor(loc.y / my.baseWidth)
dxp = txp * my.baseWidth, dyp = typ * my.baseWidth;
loc.I = txp;
loc.J = typ;
loc.IX = dxp;
loc.JY = dyp; return loc;
}
my.isChangeDraw = true; /**
* 位置移动光标
* @param {Object} loc */
function moveFocus(loc) {
}
/**
* 绑定事件
*/
function bindEvent() {
if (!my.eventBinded) {
self.contextObj.addEventListener("touchstart", function(event) {
//console.log(event);
var touchObj = event.touches[0];
eventHandle({
s: "touch",
x: touchObj.clientX - this.offsetLeft,
y: touchObj.clientY - this.offsetTop
})
});
self.contextObj.addEventListener("click", function(event) {
//console.log("click event");
eventHandle({
s: "click",
x: event.offsetX,
y: event.offsetY
})
});
self.contextObj.addEventListener("mousemove", function(event) {
//console.log("mousemove event");
moveFocus({
x: event.offsetX,
y: event.offsetY
});
});
my.eventBinded = true;
} function eventHandle(ps) {
if (!my.gameover && !my.ComputerThinking) {
logic(ps);
if (my.playerBIsComputer && my.switcher == 2) {
my.ComputerThinking = true;
var pp = AI.analysis(my.goBackC2d.I, my.goBackC2d.J);
logic({
I: pp.x,
J: pp.y
}, true);
my.ComputerThinking = false;
}
}
event.preventDefault();
event.stopPropagation();
return false;
}
}
};
win.gobang = gb;
})(window);
主要算法介绍
玩家OR电脑胜出算法
/**
* 判断是否有玩家胜出
* @param {Object} c2d
*/
function isWin(c2d) {
//四个放心计数 竖 横 左斜 右斜
var hcount = 0,
vcount = 0,
lbhcount = 0,
rbhcount = 0,
temp = 0;
var countArray = [];
//左-1
for (var i = c2d.I; i >= 0; i--) {
temp = my.rectMap[i][c2d.J];
if (temp < 0 || temp !== c2d.player) {
break;
}
hcount++;
countArray.push({
I: i,
J: c2d.J
});
}
//右-1
for (var i = c2d.I + 1; i < my.rectMap.length; i++) {
temp = my.rectMap[i][c2d.J];
if (temp < 0 || temp !== c2d.player) {
break;
}
hcount++;
countArray.push({
I: i,
J: c2d.J
});
}
if (countArray.length < 5) {
countArray = [];
//上-2
for (var j = c2d.J; j >= 0; j--) {
temp = my.rectMap[c2d.I][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
vcount++;
countArray.push({
I: c2d.I,
J: j
});
}
//下-2
for (var j = c2d.J + 1; j < my.rectMap[c2d.I].length; j++) {
temp = my.rectMap[c2d.I][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
vcount++;
countArray.push({
I: c2d.I,
J: j
});
}
}
if (countArray.length < 5) {
countArray = [];
//左上
for (var i = c2d.I, j = c2d.J; i >= 0, j >= 0; i--, j--) {
if (i < 0 || j < 0) break;
temp = my.rectMap[i][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
lbhcount++;
countArray.push({
I: i,
J: j
});
}
//右下
if (c2d.I < my.rectMap.length - 1 && c2d.I < my.rectMap[0].length - 1) {
for (var i = c2d.I + 1, j = c2d.J + 1; i < my.rectMap.length, j < my.rectMap[0].length; i++, j++) {
if (i >= my.rectMap.length || j >= my.rectMap.length) break;
temp = my.rectMap[i][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
lbhcount++;
countArray.push({
I: i,
J: j
});
}
}
}
if (countArray.length < 5) {
countArray = [];
//右上
for (var i = c2d.I, j = c2d.J; i < my.rectMap.length, j >= 0; i++, j--) {
if (i >= my.rectMap.length || j < 0) break;
temp = my.rectMap[i][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
rbhcount++;
countArray.push({
I: i,
J: j
});
}
//左下
if (c2d.I >= 1 && c2d.J < my.rectMap[0].length - 1) {
for (var i = c2d.I - 1, j = c2d.J + 1; i > 0, j < my.rectMap[0].length; i--, j++) {
if (j >= my.rectMap.length || i < 0) break;
temp = my.rectMap[i][j];
if (temp < 0 || temp !== c2d.player) {
break;
}
rbhcount++;
countArray.push({
I: i,
J: j
});
}
}
}
if (hcount >= 5 || vcount >= 5 || lbhcount >= 5 || rbhcount >= 5) {
my.winer = c2d.player;
my.gameover = true;
joinWinLine(countArray);
return true;
}
return false;
}算法简介:主要思路是搜索最后落下棋子的位置(二维坐标)计算 米 字形线坐标,看是否有连续5个或以上棋子出现。
连接赢家棋子线
/**
* 连接赢家棋子线
* @param {Object} points
*/
function joinWinLine(points) {
points.sort(function(left, right) {
return (left.I + left.J) > (right.I + right.J);
}); var startP = points.shift();
var endP = points.pop();
var poffset = my.baseWidth / 2;
can.strokeStyle = "#FF0000";
can.lineWidth = 2;
can.beginPath();
var spx = startP.I * my.baseWidth + poffset,
spy = startP.J * my.baseWidth + poffset;
can.arc(spx, spy, my.baseWidth / 4, 0, 2 * Math.PI, false);
can.moveTo(spx, spy);
var epx = endP.I * my.baseWidth + poffset,
epy = endP.J * my.baseWidth + poffset;
can.lineTo(epx, epy);
can.moveTo(epx + my.baseWidth / 4, epy);
can.arc(epx, epy, my.baseWidth / 4, 0, 2 * Math.PI, false);
can.closePath();
can.stroke();
}算法简介:根据赢家返回的连子位置集合,做坐标大小位置排序,直接使用lineto 连接 第一个棋子和最后一个
坐标换算
/**
* 坐标换算
* @param {Object} loc
* @return {Object} I 二维数组横点(),J二维数组纵点,IX 横点起始坐标,JY纵点起始坐标,player 最后下棋玩, winer 赢家
*/
function calc2dPoint(loc) {
var txp = Math.floor(loc.x / my.baseWidth),
typ = Math.floor(loc.y / my.baseWidth)
dxp = txp * my.baseWidth, dyp = typ * my.baseWidth;
loc.I = txp;
loc.J = typ;
loc.IX = dxp;
loc.JY = dyp;
return loc;
}算法简介:这个比较简单,根据每个格子的宽度计算出实际坐标
电脑AI主要代码(修改来源于网络)
/**
* AI棋型分析
*/
AI.analysis = function(x, y) {
//如果为第一步则,在玩家棋周围一格随机下棋,保证每一局棋第一步都不一样
if (my.ChessDownNum == 1) {
return this.getFirstPoint(x, y);
}
var maxX = 0,
maxY = 0,
maxWeight = 0,
i, j, tem;
for (i = BOARD_SIZE - 1; i >= 0; i--) {
for (j = BOARD_SIZE - 1; j >= 0; j--) {
if (my.rectMap[i][j] !== -1) {
continue;
}
tem = this.computerWeight(i, j, 2);
if (tem > maxWeight) {
maxWeight = tem;
maxX = i;
maxY = j;
}
if (my.enableCalcWeightNum) {
can.clearRect(i * 30 + 2, j * 30 + 2, 24, 24);
can.fillText(maxWeight, i * 30 + 5, j * 30 + 15, 30);
}
}
}
return new Point(maxX, maxY);
};
//下子到i,j X方向 结果: 多少连子 两边是否截断
AI.putDirectX = function(i, j, chessColor) {
var m, n,
nums = 1,
side1 = false, //两边是否被截断
side2 = false;
for (m = j - 1; m >= 0; m--) {
if (my.rectMap[i][m] === chessColor) {
nums++;
} else {
if (my.rectMap[i][m] === my.NO_CHESS) {
side1 = true; //如果为空子,则没有截断
}
break;
}
}
for (m = j + 1; m < BOARD_SIZE; m++) {
if (my.rectMap[i][m] === chessColor) {
nums++;
} else {
if (my.rectMap[i][m] === my.NO_CHESS) {
side2 = true;
}
break;
}
}
return {
"nums": nums,
"side1": side1,
"side2": side2
};
};
//下子到i,j Y方向 结果
AI.putDirectY = function(i, j, chessColor) {
var m, n,
nums = 1,
side1 = false,
side2 = false;
for (m = i - 1; m >= 0; m--) {
if (my.rectMap[m][j] === chessColor) {
nums++;
} else {
if (my.rectMap[m][j] === my.NO_CHESS) {
side1 = true;
}
break;
}
}
for (m = i + 1; m < BOARD_SIZE; m++) {
if (my.rectMap[m][j] === chessColor) {
nums++;
} else {
if (my.rectMap[m][j] === my.NO_CHESS) {
side2 = true;
}
break;
}
}
return {
"nums": nums,
"side1": side1,
"side2": side2
};
};
//下子到i,j XY方向 结果
AI.putDirectXY = function(i, j, chessColor) {
var m, n,
nums = 1,
side1 = false,
side2 = false;
for (m = i - 1, n = j - 1; m >= 0 && n >= 0; m--, n--) {
if (my.rectMap[m][n] === chessColor) {
nums++;
} else {
if (my.rectMap[m][n] === my.NO_CHESS) {
side1 = true;
}
break;
}
}
for (m = i + 1, n = j + 1; m < BOARD_SIZE && n < BOARD_SIZE; m++, n++) {
if (my.rectMap[m][n] === chessColor) {
nums++;
} else {
if (my.rectMap[m][n] === my.NO_CHESS) {
side2 = true;
}
break;
}
}
return {
"nums": nums,
"side1": side1,
"side2": side2
};
};
AI.putDirectYX = function(i, j, chessColor) {
var m, n,
nums = 1,
side1 = false,
side2 = false;
for (m = i - 1, n = j + 1; m >= 0 && n < BOARD_SIZE; m--, n++) {
if (my.rectMap[m][n] === chessColor) {
nums++;
} else {
if (my.rectMap[m][n] === my.NO_CHESS) {
side1 = true;
}
break;
}
}
for (m = i + 1, n = j - 1; m < BOARD_SIZE && n >= 0; m++, n--) {
if (my.rectMap[m][n] === chessColor) {
nums++;
} else {
if (my.rectMap[m][n] === my.NO_CHESS) {
side2 = true;
}
break;
}
}
return {
"nums": nums,
"side1": side1,
"side2": side2
};
};
/**
* 计算AI下棋权重
* chessColor 玩家1为玩家2为AI
*/
AI.computerWeight = function(i, j, chessColor) {
//基于棋盘位置权重(越靠近棋盘中心权重越大)
var weight = 19 - (Math.abs(i - 19 / 2) + Math.abs(j - 19 / 2)),
pointInfo = {}; //某点下子后连子信息
//x方向
pointInfo = this.putDirectX(i, j, chessColor);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子权重
pointInfo = this.putDirectX(i, j, chessColor - 1);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子权重
//y方向
pointInfo = this.putDirectY(i, j, chessColor);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子权重
pointInfo = this.putDirectY(i, j, chessColor - 1);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子权重
//左斜方向
pointInfo = this.putDirectXY(i, j, chessColor);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子权重
pointInfo = this.putDirectXY(i, j, chessColor - 1);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子权重
//右斜方向
pointInfo = this.putDirectYX(i, j, chessColor);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子权重
pointInfo = this.putDirectYX(i, j, chessColor - 1);
weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子权重
return weight;
};
//权重方案 活:两边为空可下子,死:一边为空
//其实还有很多种方案,这种是最简单的
AI.weightStatus = function(nums, side1, side2, isAI) {
var weight = 0;
switch (nums) {
case 1:
if (side1 && side2) {
weight = isAI ? 15 : 10; //一
}
break;
case 2:
if (side1 && side2) {
weight = isAI ? 100 : 50; //活二
} else if (side1 || side2) {
weight = isAI ? 10 : 5; //死二
}
break;
case 3:
if (side1 && side2) {
weight = isAI ? 500 : 200; //活三
} else if (side1 || side2) {
weight = isAI ? 30 : 20; //死三
}
break;
case 4:
if (side1 && side2) {
weight = isAI ? 5000 : 2000; //活四
} else if (side1 || side2) {
weight = isAI ? 400 : 100; //死四
}
break;
case 5:
weight = isAI ? 100000 : 10000; //五
break;
default:
weight = isAI ? 500000 : 250000;
break;
}
return weight;
};AI分析:这个只是最简单的算法,其实很简单,计算每个没有下棋坐标的分数,也是按照 米 字形 计算,计算格子8个方向出现的 一个棋子 二个棋子 三个棋子 四个棋子,其中还分为是否被截断,其实就是边缘是否被堵死。
其实这个AI算法后续还有很多可以优化,比如 断跳 二活 其实就是2个交叉的 活二 , 因为是断掉的所以没有纳入算法权重计算,如果加入这个算法,估计很难下赢电脑了。
如符号图:
* *
* *
空位
下这里
因为不是连续的,所有没有纳入。
以上就是HTML5 Canvas实现五子棋游戏的代码分享(图文)的详细内容,更多请关注php中文网其它相关文章!
HTML怎么学习?HTML怎么入门?HTML在哪学?HTML怎么学才快?不用担心,这里为大家提供了HTML速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号