代码拉取完成,页面将自动刷新
<!DOCTYPE html>
<!-- Copyright © 2021~2022 by wzh -->
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
<title>联机对战 | 2D五子棋</title>
<!--icon-->
<link href="./img/icon.png" rel="icon" type="image/x-icon"/>
<!-- 异步fonts加载 -->
<script src="./js/webfontloader.js"></script>
<script>
if (typeof require != "undefined"){ //electron
console.log("electron")
document.write(`<link href="./css/fonts.css" rel="stylesheet" type="text/css" />`);
window.onload = function(){
console.log("onload")
$("body").addClass("wf-active");
}
}else{
WebFont.load({
google: {
families: ["kt", "st", "xs", "zyyt"],
api: "./css/fonts.css"
}
});
}
</script>
<style>
/* 字体缩放方案 */
/* @media screen and (max-width:320px){
body {font-size: calc(0.018 * 320px + 6px) !important;}
}
body{
font-size: calc(1.8vw + 6px) !important;
}
@media screen and (min-width: 960px){
body {font-size: calc(0.018 * 960px + 6px) !important;}
} */
/* element */
*{
outline: none; /* 无点击线 */
}
html{
/* 禁止选择 */
-moz-user-select: none;
-khtml-user-select: none;
user-select: none;
background-color: #fffae8; /* 背景 */
}
html.outDown{
background-color: #fffae8;
overflow-y: hidden;
}
body{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
margin: 0;
background-color: #fffae8; /* 背景 */
opacity: 0; /* 完全透明 */
transition: opacity 1s, top 0.6s; /* 出入动画 */
}
html.fadeIn > body{
opacity: 1; /* 不透明 */
}
html.outDown > body{
top: 100%; /* 下滑 */
overflow-y: hidden;
}
/* header */
body > header{
display: flex;
width: 100%;
background-color: #aef;
}
body > header > i{
flex: none;
padding: 0.3rem;
font-size: 2em !important;
font-weight: bold;
background-color: #aef;
border: none;
transition: background-color 0.3s;
}
body > header > i:focus{
background-color: #afe;
}
body > header > i:active{
background-color: #9ed;
}
body > header > h1{
display: inline-block;
width: 100%;
margin: 0;
padding: 6px 0;
text-align: center;
font-weight: bold;
}
/* state */
#state{
flex: none;
margin: 0;
padding: 0;
text-align: center;
}
#state.connecting{
background-color: yellow;
color: red;
}
#state.success{
/*display: none;*/
background-color: blue;
color: white;
}
#state.failed{
background-color: red;
color: white;
}
/* main */
#main{
margin: 8px;
}
/* 双方 */
#main > div.side{
display: flex;
align-items: center;
}
#main > div.opposite{
text-align: left;
justify-content: flex-start;
}
#main > div.self{
text-align: right;
justify-content: flex-end;
}
#main > div.side > i{ /* 图标 */
display: inline-block;
width: 1.6rem;
height: 1.6rem;
margin: 0.1rem;
border-radius: 50%;
}
#main > div.black > i{ /* 黑 */
background-color: black;
border: 1px solid white;
}
#main > div.white > i{ /* 白 */
background-color: white;
border: 1px solid black;
}
#main > div.side > div{
display: inline-flex;
flex-direction: column;
}
#main > div.side > div > p.name{ /* 名称 */
display: inline-block;
margin: 0.1rem;
font-weight: bold; /* 粗体 */
}
#main > div.black > div > p.name:before{ /* 黑 */
content: "黑方";
}
#main > div.white > div > p.name:before{ /* 白 */
content: "白方";
}
#main > div.side > div > p.time{ /* 时长 */
display: inline-block;
margin: 0.1rem;
}
/* 棋盘 */
#board{
display: block;
width: 0;
height: 0;
margin: 0 auto;
border: 2px outset #fcce73;
background-color: #fcce73;
}
/* 工具按钮 */
#toolBtn > button{
width: 2.6rem;
height: 2.6rem;
margin: 0.1rem;
font-size: 1em;
background-color: #fc7;
border: 1px solid #c94;
border-radius: 50%;
transition: background-color 0.3s;
box-shadow: 1px 1px 2px #888,
3px 3px 2px #888;
}
#toolBtn > button:active{
background-color: #eb6;
box-shadow: 3px 3px 3px #888;
}
/* 工具 */
#tools > .scores{
border: 1px solid #ddd;
}
#tools > .review > *{
display: inline-block;
width: 2rem;
height: 2rem;
margin: 0.2rem;
padding: 0.3rem;
background-color: #ddd;
border: 3px outset #ddd;
box-shadow: 1px 1px 2px #888,
3px 3px 2px #888;
}
#tools > .review > *:active{
background-color: #ccc;
border: 3px inset #ddd;
}
/* info */
#info{
display: flex;
flex-direction: column;
margin: 8px;
}
#info > .output{
height: 160px;
overflow-y: auto;
}
#info > .output > p{
margin: 0;
font-size: 0.9em;
}
#info > .input{
display: flex;
margin-top: 0.5rem;
}
#info > .input > input{
width: 100%;
margin-right: 0.2rem;
}
#info > .input > button{
flex: none;
margin-left: 0.2rem;
}
</style>
<!-- jquery -->
<script src="./js/jquery.min.js"></script>
<script>
if (typeof require != "undefined") //electron
window.$ = window.jQuery = require("./js/jquery.min.js");
</script>
<!-- layui -->
<!--<link rel="stylesheet" href="https://unpkg.com/layui@2.6.4/dist/css/layui.css">
<script src="https://unpkg.com/layui@2.6.8/dist/layui.js"></script>-->
<script src="./layui/layui.js"></script>
<link rel="stylesheet" href="./layui/css/layui.css" />
<!-- 语言包 -->
<script src="./lang/lang.js" defer></script>
<!-- 调试工具 -->
<!--<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script src="https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js"></script>
<script>
if ( /ipad|iphone|midp|rv:1.2.3.4|ucweb|android|windows ce|windows mobile/.test( navigator.userAgent.toLowerCase() ) ){
//手机
eruda.init();
new VConsole();
}
</script>-->
<script src="./js/js_plus.min.js"></script> <!-- js+ -->
<!-- PianoMusic Bgm -->
<script src="./js/acoustic_grand_piano-ogg.js"></script>
<script src="./js/music.js"></script>
<script src="./js/bgm.js"></script>
<script src="https://wzh.glitch.me/socket.io/socket.io.js"></script> <!-- socket -->
<script src="https://wzh656.github.io/gobang/js/dynamic.js" async></script>
<script src="https://wzh656.github.io/gobang/js/count.js" async></script>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?c6b8f0ad7ec671e54d83e86a67280999";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WTWJEHK1C2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-WTWJEHK1C2');
</script>
</head>
<body>
<header id="header">
<i id="back" class="layui-icon layui-icon-return"></i>
<h1>联机对战 2D</h1>
<i id="restart" class="layui-icon layui-icon-refresh"></i>
</header>
<p id="state" class="connecting">连接服务器中……</p>
<section id="main">
<div class="side opposite">
<i></i>
<div>
<p class="name"></p>
<p class="time">
<span>局时:</span><span class="total">05:00</span>
<span>步时:</span><span class="step">30s</span>
</p>
</div>
</div>
<canvas id="board" width="600" height="600"></canvas>
<div class="side self">
<div>
<p class="name"></p>
<p class="time">
<span>局时:</span><span class="total">05:00</span>
<span>步时:</span><span class="step">30s</span>
</p>
</div>
<i></i>
</div>
<div id="toolBtn">
<button class="giveUp">认输</button>
<button class="drawMatch">求和</button>
<button class="scores">棋局<br>评分</button>
<button class="analyse">棋局<br>分析</button>
<button class="review" disabled>复盘<br>回放</button>
</div>
<div id="tools">
<canvas class="scores" height="36" style="display: none;"></canvas>
<div class="review" style="display: none;">
<svg class="last" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon points="94.65,10 25.35,50 94.65,90 94.65,10" />
<polygon points="5.35,10 25.35,10 25.35,90 5.35,90" />
</svg>
<svg class="play" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon points="6.7,0 93.3,50 6.7,100 6.7,0" />
</svg>
<svg class="stop" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon points="20,0 40,0 40,100 20,100" />
<polygon points="60,0 80,0 80,100 60,100" />
</svg>
<svg class="next" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon points="5.35,10 74.65,50 5.35,90 5.35,10" />
<polygon points="94.65,10 74.65,10 74.65,90 94.75,90" />
</svg>
</div>
</div>
<form id="info" onsubmit="return chat();">
<div class="output layui-textarea" disabled></div>
<div class="input">
<input placeholder="要发送的消息" class="layui-input" />
<button class="send layui-btn layui-btn-primary layui-border-blue">发送</button>
</div>
</form>
</section>
<script>
let WIDTH;
function resize(){
console.log("resize")
WIDTH = Math.min(innerWidth-20, innerHeight - $("#main > .side").height()*2);
$("#board").css("width", WIDTH + "px")
.css("height", WIDTH + "px")
.attr("width", 2*WIDTH)
.attr("height", 2*WIDTH);
$("#tools > .scores").attr("width", innerWidth-18);
}
resize();
//$(window).resize(resize);
document.addEventListener("plusready", function(){
plus.navigator.setStatusBarBackground("#aef");
plus.navigator.setStatusBarStyle("dark");
plus.key.addEventListener("backbutton", function(){
go("room.html");
});
}, false);
</script>
<script>
$("html").addClass("fadeIn");
function go(url){
$("html").addClass("outDown");
setTimeout(function(){
if (typeof plus != "undefined"){
plus.navigator.setStatusBarBackground("#aef");
plus.navigator.setStatusBarStyle("dark");
}
location.href = url;
}, 600);
layer.load(0);
}
$("button, input[type='button'], svg, header > i").click(function(){
playEffect(); //播放音效
});
class Board{
constructor ({canvas, background, rows, columns, picSize}){
this.canvas = canvas;
this.width = canvas.width;
this.height = canvas.height;
this.ctx = canvas.getContext("2d");
this.background = background;
this.rows = rows;
this.columns = columns;
this.pieces = []; //棋子
this.picSize = picSize; //棋子大小
this.spacingX = (this.width - picSize) / (columns-1); //x横向间隔
this.spacingY = (this.height - picSize) / (rows-1); //y纵向间隔
this.updateCallback = { //更新后回调
win: null,
analyse: null
};
}
//棋盘坐标(理想) 转 横纵坐标(理想)
ij2xy(i, j, place){
return [
this.picSize/2 + i * this.spacingX + this.spacingX/2*place,
this.picSize/2 + j * this.spacingY + this.spacingY/2*place
];
}
//横纵坐标(真实) 转 棋盘坐标(理想)
xy2ij(x, y, place){
const scaleX = this.width / $(this.canvas).width(), //理想:真实
scaleY = this.height / $(this.canvas).height(); //理想:真实
return [
Math.round( (x*scaleX - this.picSize/2 - this.spacingX/2*place) / this.spacingX ),
Math.round( (y*scaleY - this.picSize/2 - this.spacingY/2*place) / this.spacingY )
];
}
//添加棋子
add(i, j, place=0, color, borderColor, borderWidth=1, picSize=this.picSize){
const pic = {
i, j,
color,
border: {
color: borderColor, //边框颜色
width: borderWidth //边框粗细
},
place, //格内:1, 格点:0
picSize
};
this.pieces.push(pic);
return this.update(); //重绘
}
//删除棋子 [i~j]
remove(i, j=i+1){
this.pieces.splice(i, j-i);
return this.update(); //重绘
}
//清除棋子
clear(){
this.pieces.splice(0, this.pieces.length);
return this.update(); //重绘
}
//画一个棋子
drawPiece(pic){
const ctx = this.ctx;
ctx.beginPath();
const [x, y] = this.ij2xy(pic.i, pic.j, pic.place);
ctx.arc(x, y, pic.picSize/2, 0, Math.PI*2);
if (pic.color){ //背景
ctx.fillStyle = pic.color;
ctx.fill();
}
if (pic.border.color){ //边框
ctx.strokeStyle = pic.border.color;
ctx.lineWidth = pic.border.width;
ctx.stroke();
}
}
//更新棋盘
update(){
const ctx = this.ctx;
//绘制背景
ctx.beginPath();
ctx.fillStyle = this.background;
ctx.fillRect(0, 0, this.width, this.height);
//绘制网格
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.lineWidth = 1;
for (let x=0; x<this.columns; x++){
ctx.moveTo(...this.ij2xy(x, 0, 0));
ctx.lineTo(...this.ij2xy(x, this.rows-1, 0));
ctx.stroke();
}
for (let y=0; y<this.rows; y++){
ctx.moveTo(...this.ij2xy(0, y, 0));
ctx.lineTo(...this.ij2xy(this.columns-1, y, 0));
ctx.stroke();
}
//绘制棋子
for (const v of this.pieces)
this.drawPiece(v);
//回调
if (this.updateCallback.analyse)
this.updateCallback.analyse();
if (this.updateCallback.win)
this.updateCallback.win();
return this;
}
}
const N = +location.getQueryString("N"),
{cols, rows} = JSON.parse(location.getQueryString("size")),
place = +location.getQueryString("place"),
{mistake=true} = JSON.parse(localStorage.getItem("五子棋_个性化设置") || "{}");
$("#board").css("height", WIDTH *rows/cols + "px")
.attr("height", 2*WIDTH *rows/cols);
const board = new Board({ //棋盘
canvas: $("#board")[0],
background: "#fcce73",
rows: rows + place,
columns: cols + place,
picSize: $("#board").attr("width")/(cols + place) *0.9
}).add((cols-1)/2, (rows-1)/2, place, "#000", undefined, undefined, 10) //多加一个以备删除
.add((cols-1)/2, (rows-1)/2, place, "#000", undefined, undefined, 10);
let waiting = false, //等待中
gameOverMusic; //游戏结束音乐
let socket, //socket.io
send, //send
ID, //分配的ID
oldID = location.getQueryString("id"), //上次分配的ID
ROOM = location.getQueryString("room"), //房间
TYPE = !!+location.getQueryString("type"), //房主: true, 成员: false
FIRST = JSON.parse( location.getQueryString("first") ); //先手
$("body > header > h1").html(ROOM);
let game = {
first: null, //是否先行
turn: 1, //下棋方 1:黑, 0:白
startTime: null, //开始时间
steps: 0, //下棋总步数
winner: false, //获胜者 1:黑, 0:白, null:平 false:未获胜
t0: [null, null], //上次时间
ids: [null, null], //计时器id
time: [
{total: 300, step: 30}, //0: 白
{total: 300, step: 30} //1: 黑
], //剩余时间
timeWarning: { //时间警告
total: {
60: ["#f00", 1.5],
180: ["#fa0", 1.2],
600: ["#000", 1]
},
step: {
10: ["#f00", 1.5],
20: ["#fa0", 1.2],
30: ["#000", 1]
}
},
scores: {
now: [0, 0], //当前
goal: [0, 0], //目标
t0: +new Date(),
k: 0.005
}, //分数 白:0 黑:1
lines: [], //分析连线
linesDisplay: false, //显示连线
//更新时间显示
updateTime(turn){
let total, step,
totalElem, stepElem;
//黑
total = Math.max(this.time[1].total, 0);
step = Math.max(this.time[1].step, 0);
totalElem = $("#main > .black > div > .time > .total"),
stepElem = totalElem.nextAll(".step");
totalElem.html(
( ~~(total/60) +"").padStart(2,"0") + ":" + ( ~~total%60 +"").padStart(2, "0")
);
for (const [i,v] of Object.entries(this.timeWarning.total))
if (total <= i){
totalElem.css("color", v[0])
.css("font-size", v[1] + "em");
break;
}
stepElem.html(
~~step + "s"
);
for (const [i,v] of Object.entries(this.timeWarning.step))
if (step <= i){
stepElem.css("color", v[0])
.css("font-size", v[1] + "em");
break;
}
if (~~step < 10 && ~~step >= 9)
this.flash(this.turn, "#f00"); //背景闪烁
//白
total = Math.max(this.time[0].total, 0);
step = Math.max(this.time[0].step, 0);
totalElem = $("#main > .white > div > .time > .total"),
stepElem = totalElem.nextAll(".step");
totalElem.html(
( ~~(total/60) +"").padStart(2,"0") + ":" + ( ~~total%60 +"").padStart(2, "0")
);
for (const [i,v] of Object.entries(this.timeWarning.total))
if (total <= i){
totalElem.css("color", v[0])
.css("font-size", v[1] + "em");
break;
}
stepElem.html(
~~step + "s"
);
for (const [i,v] of Object.entries(this.timeWarning.step))
if (step <= i){
stepElem.css("color", v[0])
.css("font-size", v[1] + "em");
break;
}
if (~~step < 10 && ~~step >= 9)
this.flash(this.turn, "#f00"); //背景闪烁
},
//背景闪烁
flash(turn, color){
let elem;
if (this.turn == 1){ //黑
elem = $("#main > div.black > i");
}else{ //白
elem = $("#main > div.white > i");
}
elem.css("background-color", color);
setTimeout(()=> elem.css("background-color", ""), 150);
setTimeout(()=> elem.css("background-color", color), 300);
setTimeout(()=> elem.css("background-color", ""), 450);
},
//切换下棋方
set(turn){
if (this.time[this.turn].total <= 0){ //超出局时
this.time[this.turn].step += 5;
}else{
this.time[this.turn].step = 30; //重置步时
//this.time[this.turn].total += 5; //增加局时
}
this.updateTime(this.turn);
clearInterval( this.ids[this.turn] );
this.turn = turn;
this.flash(this.turn, "#af8"); //背景闪烁
this.t0[this.turn] = +new Date();
this.ids[this.turn] = setInterval(()=>{
const interval = (new Date() - this.t0[this.turn]) / 1000;
this.t0[this.turn] = +new Date();
this.time[this.turn].total -= interval;
this.time[this.turn].step -= interval;
this.updateTime(this.turn);
if (this.time[this.turn].step <= 0){ //超出步时
//worker.terminate(); //关闭线程
if (this.turn != this.first) return; //不是自己超时 可能网卡
if (game.winner !== false) return; //游戏已结束
send("gobang_game_timeout", ROOM, oldID); //自己超时 发送timeout
game.over(this.turn!=this.first, this.turn==1?"黑方超时":"白方超时");
}
}, 200);
},
//下棋
play(i, j, direct=false){
console.log("play", i, j, direct)
request({
type: "play", //下棋
i, j, direct
}).then((data)=>{
waiting = false;
console.log("play callback:", data.action, {i, j})
switch (data.action){
case 2: //下黑棋
board.remove(-1)
.add(i, j, place, "#000")
.add(i, j, place, undefined, "#fff", 2, board.picSize+2);
game.set(0);
game.steps++;
send("gobang_game_play", {
room: ROOM,
pos: {i, j},
side: 2, //黑棋
time: +new Date(),
totalTime: this.time[+this.first].total
});
break;
case 1: //下白棋
board.remove(-1)
.add(i, j, place, "#fff", "#000")
.add(i, j, place, undefined, "#fff", 2, board.picSize+2);
game.set(1);
game.steps++;
send("gobang_game_play", {
room: ROOM,
pos: {i, j},
side: 1, //白棋
time: +new Date(),
totalTime: this.time[+this.first].total
});
break;
case -2: //添加标记
board.add(i, j, place, undefined, "#00f", 2);
break;
case -1: //删除标记
board.remove(-1);
break;
}
});
},
//游戏结束
over(win, text){
console.log("game over", win, text)
const t = (new Date() - this.startTime) / 1000;
clearInterval( this.ids[this.turn] );
$("#toolBtn > .review").removeAttr("disabled"); //解除禁用
if (win === true){
this.winner = +this.first;
layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)<span>`,
{title: "胜利!", icon: 6}
);
$("#state").html("胜利!");
output(`<span>胜利:</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)</span>`, true);
if (gameOverMusic)
gameOverMusic.stop();
gameOverMusic = new Player("./music/好运来.mp3").play();
if (gameOverMusic)
gameOverMusic.stop();
gameOverMusic = new Player("./music/凉凉.mp3").play();
}else if (win === false){
this.winner = 1-this.first;
layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)<span>`,
{title: "你失败了!", icon: 5}
);
$("#state").html("你失败了!");
output(`<span>你失败了:</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)</span>`, true);
}else{
this.winner = null;
layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)<span>`,
{title: "游戏结束"}
);
$("#state").html("游戏结束");
output(`<span>游戏结束:</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>(</span>${game.steps/2}<span>回合</span><span>)</span>`, true);
}
},
//更新分数
updateScores(){
const t = new Date()-this.scores.t0,
now = this.scores.now,
goal = this.scores.goal,
k = this.scores.k,
e = $("#tools > .scores");
this.scores.t0 = +new Date();
if (t > 300) return; //跳帧
now[0] += (goal[0] - now[0]) *t *k;
now[1] += (goal[1] - now[1]) *t *k;
const black = now[1] + 0.001,
white = now[0] + 0.001,
total = black + white,
ctx = e[0].getContext("2d"),
width = e.attr("width"),
height = e.attr("height");
//清除背景
ctx.beginPath();
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
//背景
ctx.beginPath();
ctx.fillStyle = "#000";
ctx.moveTo(0, 0);
ctx.lineTo(black / total * width + 5, 0);
ctx.lineTo(black / total * width - 5, height);
ctx.lineTo(0, height);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "#fff";
ctx.moveTo(width, 0);
ctx.lineTo(black / total * width + 5, 0);
ctx.lineTo(black / total * width - 5, height);
ctx.lineTo(width, height);
ctx.fill();
//文字
ctx.beginPath();
ctx.fillStyle = "#fff"; //白字
ctx.textAlign = "left";
ctx.textBaseline = "top"; //基线:左上
ctx.font = height*5/6 + "px xs"; // 5/6vh
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.shadowColor = "#000";
ctx.shadowBlur = 3;
ctx.fillText(Math.round(black-0.001, 2), height/6, height/12); //margin: 0.2vh ;0.1vh
ctx.beginPath();
ctx.fillStyle = "#000"; //黑字
ctx.textAlign = "right";
ctx.textBaseline = "top"; //基线:右上
ctx.font = height*5/6 + "px xs"; // 5/6vh
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.shadowColor = "#fff";
ctx.shadowBlur = 3;
ctx.fillText(Math.round(white-0.001, 2), width-height/6, height/12); //margin: 0.2vh ;0.1vh
},
//更新分析画线
updateLines(display=true){
if (!display){
board.updateCallback.analyse = null;
}else{
board.updateCallback.analyse = ()=>{
const ctx = board.ctx;
for (const v of this.lines){
if (v.width < 3) //连子<3 避免过密
continue;
ctx.beginPath();
ctx.strokeStyle = v.pic==2? "#fff": "#000";
ctx.lineWidth = v.width-3+1;
ctx.moveTo(...v.start);
ctx.lineTo(...v.end);
ctx.stroke();
}
};
}
board.update();
game.linesDisplay = display;
}
},
worker,
workerCallback = { //收到消息回调
//获胜
won(data){
const {winner, steps} = data;
if (game.winner !== false) return; //游戏已结束
game.over(winner-1==game.first, winner==2?"黑方胜":"白方胜");
},
//获胜画线
lines(data){
const {lines} = data;
lines.forEach( v => {
[v.start[0], v.start[1]] = board.ij2xy(...v.start, place);
[v.end[0], v.end[1]] = board.ij2xy(...v.end, place);
});
board.updateCallback.win = ()=>{
const ctx = board.ctx;
ctx.beginPath();
ctx.strokeStyle = "#f00";
ctx.lineWidth = N-3+1;
for (const v of lines){
console.table(v)
ctx.moveTo(v.start[0], v.start[1]);
ctx.lineTo(v.end[0], v.end[1]);
ctx.stroke();
}
};
board.update();
//worker.terminate(); //关闭线程
},
//平局
gameOver(){
//worker.terminate(); //关闭线程
if (game.winner !== false) return; //游戏已结束
game.over(null, "平局");
},
//分析
scores(data){
console.table(data.scores);
const {scores, lines} = data;
lines.forEach( v => {
[v.start[0], v.start[1]] = board.ij2xy(...v.start, place);
[v.end[0], v.end[1]] = board.ij2xy(...v.end, place);
});
game.scores.goal = scores;
game.lines = lines;
if (board.updateCallback.analyse){
game.updateLines(true);
}else{
game.updateLines(false);
}
}
},
scores_id = null, //分数更新id
review_id = null; //复盘id
//初始化
function init(){
//游戏结束音乐
if (gameOverMusic)
gameOverMusic.stop();
//棋盘
board.updateCallback.win = board.updateCallback.analyse = null;
board.clear()
.add((cols-1)/2, (rows-1)/2, place, "#000", undefined, undefined, 10) //多加一个以备删除
.add((cols-1)/2, (rows-1)/2, place, "#000", undefined, undefined, 10);
//游戏
game.steps = 0;
game.turn = 1;
game.winner = false;
clearInterval( game.ids[0] );
clearInterval( game.ids[1] ); //时间暂停
game.t0[0] = game.t0[1] = game.ids[0] = game.ids[1] = null;
game.time[0].total = 300;
game.time[0].step = 30;
game.time[1].total = 300;
game.time[1].step = 30;
game.scores.now[0] = game.scores.now[1] =
game.scores.goal[0] = game.scores.goal[1] = 0;
game.scores.t0 = +new Date();
game.scores.k = 0.005;
setInterval(()=>game.updateScores(), 16); //更新分数
game.lines = []; //清空分析画线
//game.set(1); //黑棋先下
//game.startTime = +new Date(); //开始计时
//关闭工具
scores(false);
analyse(false);
clearInterval(review_id); //停止复盘
review_id = null;
$("#tools > .review > .stop").hide();
$("#tools > .review > .play").show();
$("#toolBtn > .review").attr("disabled", "disabled");
$("#tools > .review").slideUp("fast");
//线程
if (worker)
worker.terminate(); //关闭线程
worker = new Worker("./js/worker.js"); //多线程
worker.postMessage({
type: "init", //初始化
cols,
rows,
D: 2,
N,
mis: mistake
});
worker.onmessage = function(e){
console.log("message: ", e.data.type, e.data)
const type = e.data.type;
if (workerCallback[type])
workerCallback[type](e.data);
};
worker.onerror = function(e){
console.error(`${e.message}\n\tat ${e.filename}:${e.lineno}`)
};
}
//setTimeout(init, 1000); //等待淡入动画结束
/* socket */
if (typeof io == "undefined"){ //未加载socket.io.js
$("#state").html("网络错误或服务器繁忙,请检查网络连接再刷新页面!")
.removeClass().addClass("state failed");
}else{
socket = io("https://wzh.glitch.me/");
send = function (name, ...data){
console.log("↑", name, data);
socket.emit(name, ...data);
};
socket.on("connecting", function(data){
$("#state").html("连接服务器中……")
.removeClass().addClass("state connecting");
});
socket.on("connect", function(data){
send("type", "gobang"); //type
send("gobang_game_room", {
oldID,
room: ROOM,
type: TYPE,
first: FIRST
}); //加入游戏房间
$(".state").html("服务器连接成功")
.removeClass().addClass("state success");
layer.msg("<span>服务器连接成功</span>", {icon:1});
});
socket.on("connect_failed", function(data){
$(".state").html("服务器连接失败")
.removeClass().addClass("state failed");
layer.msg("<span>服务器连接失败</span>", {icon:2});
});
socket.on("disconnect", function(data){
$(".state").html("与服务器连接断开")
.removeClass().addClass("state failed");
layer.msg("<span>与服务器连接断开</span>", {icon:2});
if (game.winner !== false) return; //游戏已结束
if (game.first === null) return; //未开始游戏
game.over(false, "与服务器连接断开,游戏失败");
});
socket.on("reconnecting", function(data){
$(".state").html("重连中……")
.removeClass().addClass("state connecting");
});
socket.on("reconnect", function(data){
send("type", "gobang"); //type
send("gobang_game_room", {
oldID,
room: ROOM,
type: TYPE,
first: FIRST
}); //加入游戏房间
$(".state").html("服务器重连成功")
.removeClass().addClass("state success");
layer.msg("<span>服务器重连成功</span>", {icon:1});
});
socket.on("reconnect_failed", function(data){
$(".state").html("服务器重连失败")
.removeClass().addClass("state failed");
layer.msg("<span>服务器重连失败</span>", {icon:2});
});
socket.on("error", function(data){
console.error(data);
$(".state").html("服务器连接错误!")
.removeClass().addClass("state failed");
layer.msg("<span>服务器连接错误!</span>", {icon:2});
});
//分配新ID
socket.on("gobang_assignID", function(data){
console.log("↓ gobang_assignID", data)
ID = data;
$("#main > div.self > div > p.name").html(oldID);
$("#state").html("<span>等待对方加入中……</span>");
output("<span>id分配成功,等待对方加入中……</span>", true);
});
//准备好了
socket.on("gobang_game_ready", function(data){
console.log("↓ gobang_game_ready", data)
game.first = (data.first && TYPE) || //房主 且 房主先手
(!data.first && !TYPE); //成员 且 房主后手
if (game.first){
$("#main > div.self").removeClass("white").addClass("black");
$("#main > div.opposite").removeClass("black").addClass("white");
$("#main > div.self > div > p.name").html( ":"+data.oldID[1-TYPE] );
$("#main > div.opposite > div > p.name").html( ":"+data.oldID[+TYPE] );
layer.msg("你执黑棋,对方执白棋");
$("#state").html("游戏开始,你执黑棋,对方执白棋");
output("游戏开始,你执黑棋,对方执白棋", true);
}else{
$("#main > div.self").removeClass("black").addClass("white");
$("#main > div.opposite").removeClass("white").addClass("black");
$("#main > div.self > div > p.name").html( ":"+data.oldID[1-TYPE] );
$("#main > div.opposite > div > p.name").html( ":"+data.oldID[+TYPE] );
layer.msg("你执白棋,对方执黑棋");
$("#state").html("游戏开始,你执白棋,对方执黑棋");
output("游戏开始,你执白棋,对方执黑棋", true);
}
init();
game.set(1); //黑棋先下
game.startTime = +new Date(); //开始计时
});
//对方离开
socket.on("gobang_game_leave", function(old_id){
console.log("↓ gobang_game_leave", old_id)
if (old_id != oldID){
$("#state").html(`<span>对方</span>“${old_id}”<span>退出了房间</span>`);
output(`<span>对方</span>“${old_id}”<span>退出了房间</span>`, true);
}
if (game.winner !== false) return; //游戏已结束
game.over(true, `<span>对方</span>“${old_id}”<span>退出了房间,游戏结束!</span>`);
});
//下棋
socket.on("gobang_game_play", function(data={}){
console.log("↓ gobang_game_play", data)
const {pos, side, time, totalTime} = data;
const delay = (new Date()-time)/1000; //延时
game.time[game.turn].total = totalTime; //局时
game.time[game.turn].step += delay;
$("#state").html("<span>下棋延时:</span>" + delay*1000 + "ms")
if (side != game.first+1)
game.play(pos.i, pos.j, true); //直接下
});
//超时
socket.on("gobang_game_timeout", function(old_id){
console.log("↓ gobang_game_timeout", old_id)
if (game.winner !== false) return; //游戏已结束
game.over(true, `<span>对方</span>“${old_id}”<span>超时,游戏结束!</span>`);
});
//认输
socket.on("gobang_game_giveUp", function(old_id){
console.log("↓ gobang_game_giveUp", old_id)
if (game.winner !== false) return; //游戏已结束
game.over(true, `<span>对方</span>“${old_id}”<span>认输,游戏结束!</span>`);
});
//求和
socket.on("gobang_game_drawMatch", function(old_id){
console.log("↓ gobang_game_drawMatch", old_id)
if (game.winner !== false) return; //游戏已结束
layer.confirm(`<span>对方</span>“${old_id}”<span>请求求和,是否同意?</span>`, {
btn: ["同意", "拒绝"],
btn1: function(index){
layer.close(index);
send("gobang_game_drawMatch_agree", ROOM, oldID);
output("你同意了对方的求和请求", true);
game.over(null, "求和成功,游戏结束");
},
btn2: function(index){
layer.close(index);
send("gobang_game_drawMatch_disagree", ROOM, oldID);
output("你拒绝了对方的求和请求", true);
}
});
});
//同意求和
socket.on("gobang_game_drawMatch_agree", function(old_id){
console.log("↓ gobang_game_drawMatch_agree", old_id)
output("对方同意了你的求和请求", true);
game.over(null, "求和成功,游戏结束");
});
//拒绝求和
socket.on("gobang_game_drawMatch_disagree", function(old_id){
console.log("↓ gobang_game_drawMatch_disagree", old_id)
layer.msg("对方拒绝了你的求和请求");
output("对方拒绝了你的求和请求", true);
});
//再来一局
socket.on("gobang_game_replay", function(old_id){
console.log("↓ gobang_game_replay", old_id)
layer.confirm(`<span>对方</span>“${old_id}”<span>请求再来一局,是否同意?</span>`, {
btn: ["同意", "拒绝"],
btn1: function(index){
layer.close(index);
send("gobang_game_replay_agree", ROOM, oldID);
init();
send("gobang_game_room", {
oldID,
room: ROOM,
type: TYPE,
first: FIRST
}); //加入游戏房间
output("你同意了对方的再来一局请求", true);
},
btn2: function(index){
layer.close(index);
send("gobang_game_replay_disagree", ROOM, oldID);
output("你拒绝了对方的再来一局请求", true);
}
});
});
//同意再来一局
socket.on("gobang_game_replay_agree", function(old_id){
console.log("↓ gobang_game_replay_agree", old_id)
layer.msg("对方同意了你再来一局的请求");
output("对方同意了你再来一局的请求", true);
init();
send("gobang_game_room", {
oldID,
room: ROOM,
type: TYPE,
first: FIRST
}); //加入游戏房间
});
//拒绝再来一局
socket.on("gobang_game_replay_disagree", function(old_id){
console.log("↓ gobang_game_replay_disagree", old_id)
layer.msg("对方拒绝了你再来一局的请求");
output("对方拒绝了你再来一局的请求", true);
});
//聊天
socket.on("gobang_game_chat", function(old_id, msg){
console.log("↓ gobang_game_chat", old_id, msg)
output(`${old_id}:${msg}`);
if (old_id == oldID){ //自己发送
layer.tips(msg, $("#main > .self > i")[0], {tips: 1}); //上方
}else{ //对方发送
layer.tips(msg, $("#main > .opposite > i")[0], {tips: 1}); //上方
}
});
//下棋失败
socket.on("gobang_game_play_failed", function(){
console.log("↓ gobang_game_play_failed", room)
layer.msg("<span>下棋失败,房间已被关闭</span>", {icon: 5});
output("下棋失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//超时失败
socket.on("gobang_game_timeout_failed", function(){
console.log("↓ gobang_game_timeout_failed", room)
layer.msg("<span>发送超时失败,房间已被关闭</span>", {icon: 5});
output("发送超时失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败(超时)
});
//认输失败
socket.on("gobang_game_giveUp_failed", function(){
console.log("↓ gobang_game_giveUp_failed", room)
layer.msg("<span>发送认输失败,房间已被关闭</span>", {icon: 5});
output("发送认输失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败(认输)
});
//求和失败
socket.on("gobang_game_drawMatch_failed", function(){
console.log("↓ gobang_game_drawMatch_failed", room)
layer.msg("<span>发送求和失败,房间已被关闭</span>", {icon: 5});
output("发送求和失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//同意求和失败
socket.on("gobang_game_drawMatch_agree_failed", function(){
console.log("↓ gobang_game_drawMatch_agree_failed", room)
layer.msg("<span>同意求和失败,房间已被关闭</span>", {icon: 5});
output("同意求和失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//拒绝求和失败
socket.on("gobang_game_drawMatch_disagree_failed", function(){
console.log("↓ gobang_game_drawMatch_disagree_failed", room)
layer.msg("<span>拒绝求和失败,房间已被关闭</span>", {icon: 5});
output("拒绝求和失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//再来一局失败
socket.on("gobang_game_replay_failed", function(){
console.log("↓ gobang_game_replay_failed", room)
layer.msg("<span>发送再来一局失败,房间已被关闭</span>", {icon: 5});
output("发送再来一局失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//同意再来一局失败
socket.on("gobang_game_replay_agree_failed", function(){
console.log("↓ gobang_game_replay_agree_failed", room)
layer.msg("<span>同意再来一局失败,房间已被关闭</span>", {icon: 5});
output("同意再来一局失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//拒绝再来一局失败
socket.on("gobang_game_replay_disagree_failed", function(){
console.log("↓ gobang_game_replay_disagree_failed", room)
layer.msg("<span>拒绝再来一局失败,房间已被关闭</span>", {icon: 5});
output("拒绝再来一局失败,房间已被关闭", true);
if (game.winner === false)
game.winner = 1-game.first; //自己失败
});
//chat失败
socket.on("gobang_chat_failed", function(room){
console.log("↓ gobang_chat_failed", room)
layer.msg("<span>发送聊天失败,房间已被关闭</span>", {icon: 5});
output("发送聊天失败,房间已被关闭");
});
}
//请求线程
function request(data){
console.log("post: ", data.type, data)
worker.postMessage(data);
return {
then(func){
workerCallback[data.type] = func;
}
};
}
//下棋
$("#board").click(function(e){
if (!worker) return; //未初始化线程
if (game.first != game.turn) return; //未轮到自己
if (game.winner !== false) return; //已结束
if (waiting) return; //上次未响应
playEffect(); //播放音效
const {offsetX: x, offsetY: y} = e;
const [i, j] = board.xy2ij(x,y, place);
console.log("click", [x, y], [i, j])
waiting = true;
game.play(i, j);
});
/* header */
//返回
$("#back").click(function(){
go("rooms.html");
});
//重来
$("#restart").click(function(){
if (game.winner === false) //游戏未结束
return layer.msg("游戏未结束,无法请求再来一局,可以先求和/认输");
layer.confirm("确定请求再来一局吗?", function(index){
layer.close(index);
send("gobang_game_replay", ROOM, oldID);
layer.msg("请求再来一局中……");
output("你发送了再来一局请求", true);
});
});
/* tools */
//认输
$("#toolBtn > .giveUp").click(function(){
if (game.winner !== false) //游戏已结束
return layer.msg("游戏已结束,无法认输");
layer.confirm("真的要认输吗?", function(index){
layer.close(index);
send("gobang_game_giveUp", ROOM, oldID);
game.over(false, "你认输了,游戏结束");
});
});
//求和
$("#toolBtn > .drawMatch").click(function(){
if (game.winner !== false) //游戏已结束
return layer.msg("游戏已结束,无法求和");
layer.confirm("确定请求求和吗?", function(index){
layer.close(index);
send("gobang_game_drawMatch", ROOM, oldID);
layer.msg("请求求和中……");
output("你发送了求和请求", true);
})
});
//评分
function scores(display){
const btn = $("#toolBtn > .scores"),
tool = $("#tools > .scores");
if ( display === true || //欲显示
(display === undefined && tool.css("display") == "none") //已隐藏
){
clearInterval( scores_id ); //停止更新
scores_id = setInterval(()=>game.updateScores(), 30); //更新分数
btn.attr("disabled", "disabled");
tool.slideDown("slow", ()=>btn.removeAttr("disabled"));
}else{
clearInterval( scores_id ); //停止更新
scores_id = null;
btn.attr("disabled", "disabled");
tool.slideUp("slow", ()=>btn.removeAttr("disabled"));
}
}
$("#toolBtn > .scores").click(function(){
if ( $("#tools > .review").css("display") != "none" ){
scores(false);
return layer.msg("复盘时暂不支持棋局评分");
}
scores();
});
//分析
function analyse(display){
if ( display === false || //欲隐藏
(display === undefined && game.linesDisplay) //已显示
){
if (game.linesDisplay) //已显示
layer.msg("棋局分析已关闭");
game.updateLines(false);
}else{
if (!game.linesDisplay) //已隐藏
layer.msg("棋局分析已开启");
game.updateLines(true);
}
}
$("#toolBtn > .analyse").click(function(){
if ($("#tools > .review").css("display") != "none"){
analyse(false);
return layer.msg("复盘时暂不支持棋局分析");
}
analyse();
});
//重放
let reviewRestore; //重置函数
function review(){
if ( $("#tools > .review").css("display") == "none" ){ //需显示
$(this).attr("disabled", "disabled");
$("#tools > .review").slideToggle("slow", ()=>$(this).removeAttr("disabled"));
scores(false); //关闭棋局评分
analyse(false); //关闭棋局分析
$("#tools > .review > .stop").hide();
$("#tools > .review > .play").show(); //暂停
request({
type: "review" //重放
}).then((data)=>{
const {steps, pieces} = data;
const winCallback = board.updateCallback.win;
board.updateCallback.win = board.updateCallback.analyse = null;
board.clear(); //清空
let index = -1;
const next = ()=>{
if (index >= steps.length-1){
board.updateCallback.win = winCallback;
board.update();
return layer.msg("不能再往后了");
}
const {i,j,v} = steps[++index];
board.remove(-1)
.add(i, j, place, v==2? "#000": "#fff", v==2? "#fff":"#000")
.add(i, j, place, undefined, "#fff", 2, board.picSize+2);
return false;
},
last = ()=>{
if (index <= 0)
return layer.msg("不能再往前了");
const {i,j,v} = steps[--index];
board.updateCallback.win = null;
board.remove(-1)
.remove(-1)
.add(i, j, place, undefined, "#fff", 2, board.picSize+2);
return false;
};
reviewRestore = ()=>{
board.clear();
for (const {i,j,v} of steps)
board.add(i, j, place, v==2? "#000": "#fff", v==2? "#fff":"#000")
board.updateCallback.win = winCallback;
board.update();
};
next();
$("#tools > .review > .last").click(last);
$("#tools > .review > .next").click(next);
$("#tools > .review > .play").click(function(){
$(this).hide();
$("#tools > .review > .stop").show();
if (review_id)
clearInterval(review_id);
review_id = setInterval(()=>{
if (next() === false) return;
clearInterval(review_id);
review_id = null;
$(this).show();
$("#tools > .review > .stop").hide();
}, 1000);
});
$("#tools > .review > .stop").click(function(){
clearInterval(review_id);
review_id = null;
$(this).hide();
$("#tools > .review > .play").show();
});
});
}else{
clearInterval(review_id);
review_id = null;
reviewRestore(); //重置
$("#tools > .review > .stop").hide();
$("#tools > .review > .play").show();
$(this).attr("disabled", "disabled");
$("#tools > .review").slideToggle("slow", ()=>$(this).removeAttr("disabled"));
}
}
$("#toolBtn > .review").click(review);
/* 发送消息 */
$("#info > .input > button").click(chat);
function chat(){
send("gobang_game_chat", ROOM, oldID, $("#info > .input > input").val());
$("#info > .input > input").val(""); //清空输入
return false;
}
function output(text, center=false){
$("#info > .output").append(
$("<p></p>").html( text )
.css("text-align", center? "center": "")
);
}
/* 电脑版按键 */
document.onkeydown = function(e){
console.log(e.key)
if (e.key == "Escape"){
go("index.html");
}else if (e.key == "F2"){ //评分
if (!$("#toolBtn > .scores").attr("disabled"))
scores();
}else if (e.key == "F3"){ //分析
if (!$("#toolBtn > .analyse").attr("disabled"))
analyse();
}else if (e.key == "F4"){ //复盘
if (!$("#toolBtn > .review").attr("disabled"))
review();
}else if (e.key == "F5"){ //再来一局
init();
}
e.preventDefault();
return false;
};
</script>
</body>
</html>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。