Files
chatroom/resources/js/chat-room/gomoku-panel.js
T

1002 lines
31 KiB
JavaScript
Raw Normal View History

2026-04-25 18:50:05 +08:00
// 五子棋主面板 Alpine 组件,负责 PvP/PvE 对局、Canvas 棋盘绘制和实时同步。
const DEFAULT_GOMOKU_BASE_URL = "/gomoku";
const DEFAULT_GOMOKU_CONFIG_URL = "/gomoku/config";
const DEFAULT_GOMOKU_ACTIVE_URL = "/gomoku/active";
const DEFAULT_GOMOKU_CREATE_URL = "/gomoku/create";
const BOARD_SIZE = 15;
const DEFAULT_CELL_SIZE = 30;
const DEFAULT_BOARD_PADDING = 22;
const DEFAULT_INVITE_TIMEOUT = 60;
const DEFAULT_PVP_REWARD = 80;
const DEFAULT_AI_ICONS = ["🟢", "🟡", "🔴", "⚡"];
const DEFAULT_AI_COLORS = ["#16a34a", "#ca8a04", "#dc2626", "#7c3aed"];
const DEFAULT_AI_LEVELS = [
{ id: 1, name: "简单", icon: "🟢", reward: 20, fee: 0, color: "#16a34a" },
{ id: 2, name: "普通", icon: "🟡", reward: 50, fee: 10, color: "#ca8a04" },
{ id: 3, name: "困难", icon: "🔴", reward: 120, fee: 30, color: "#dc2626" },
{ id: 4, name: "专家", icon: "⚡", reward: 300, fee: 80, color: "#7c3aed" },
];
/**
* 读取五子棋接口地址,优先使用 Blade 写入的命名路由。
*
* @returns {{base: string, config: string, active: string, create: string}}
*/
function gomokuUrls() {
const panel = document.getElementById("gomoku-panel");
return {
base: panel?.dataset.gomokuBaseUrl || DEFAULT_GOMOKU_BASE_URL,
config: panel?.dataset.gomokuConfigUrl || DEFAULT_GOMOKU_CONFIG_URL,
active: panel?.dataset.gomokuActiveUrl || DEFAULT_GOMOKU_ACTIVE_URL,
create: panel?.dataset.gomokuCreateUrl || DEFAULT_GOMOKU_CREATE_URL,
};
}
/**
* 拼接五子棋对局动态接口路径。
*
* @param {number|string} gameId 对局 ID
* @param {string} action 动作名称
* @returns {string}
*/
function gomokuGameUrl(gameId, action) {
return `${gomokuUrls().base}/${gameId}/${action}`;
}
/**
* 读取 CSRF Token。
*
* @returns {string}
*/
function csrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
}
/**
* 生成空棋盘矩阵。
*
* @returns {number[][]}
*/
function emptyBoard() {
return Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(0));
}
/**
* 请求 JSON,失败时返回 null,保持原组件的静默容错行为。
*
* @param {string} url 请求地址
* @returns {Promise<Record<string, any>|null>}
*/
async function fetchJson(url) {
return fetch(url).then((response) => response.json()).catch(() => null);
}
/**
* 创建五子棋面板 Alpine 组件。
*
* @returns {Record<string, any>}
*/
export function gomokuPanel() {
return {
show: false,
mode: "pvp",
gameStatus: "idle",
gameId: null,
myColor: null,
blackName: "",
whiteName: "",
currentTurn: 1,
stepCount: 0,
aiLevels: DEFAULT_AI_LEVELS.map((level) => ({ ...level })),
board: [],
hoverPos: null,
lastMove: null,
CELL: DEFAULT_CELL_SIZE,
PAD: DEFAULT_BOARD_PADDING,
rewardGold: 0,
resultEmoji: "",
resultText: "",
resultGold: 0,
inviteTimeout: DEFAULT_INVITE_TIMEOUT,
moveTimeout: 0,
_inviteTimer: null,
_moveTimer: null,
_opponentPollTimer: null,
_echoChannel: null,
_pvpReward: DEFAULT_PVP_REWARD,
/**
* 面板标题。
*
* @returns {string}
*/
get title() {
if (this.gameStatus === "idle") {
return "五子棋";
}
if (this.gameStatus === "waiting") {
return "等待对手…";
}
if (this.gameStatus === "finished") {
return "对局结束";
}
return this.mode === "pvp" ? "五子棋 PvP 对战" : "五子棋 AI 对战";
},
/**
* 面板副标题。
*
* @returns {string}
*/
get subtitle() {
if (this.gameStatus === "idle") {
return "选择游戏模式开始";
}
if (this.gameStatus === "waiting") {
return "已发出对战邀请";
}
if (this.gameStatus === "finished") {
return "对局已结束";
}
return this.isMyTurn ? "● 轮到您落子" : "○ 等待对方落子…";
},
/**
* 判断是否轮到当前用户落子。
*
* @returns {boolean}
*/
get isMyTurn() {
if (this.mode === "pve") {
return this.currentTurn === 1;
}
return this.currentTurn === this.myColor;
},
/**
* 当前回合文案。
*
* @returns {string}
*/
get turnText() {
if (this.gameStatus === "finished") {
return "对局结束";
}
return this.currentTurn === 1 ? "● 黑棋回合" : "○ 白棋回合";
},
/**
* 从外部打开面板,并尝试恢复未完成对局。
*
* @returns {Promise<void>}
*/
async open() {
this.show = true;
this.resetToIdle();
this.$nextTick(() => this.initCanvas());
const [config, active] = await Promise.all([
fetchJson(gomokuUrls().config),
fetchJson(gomokuUrls().active),
]);
this.applyConfig(config);
if (active?.has_active) {
await this.confirmActiveGame(active);
}
},
/**
* 初始化面板并直接加入对局。
*
* @param {number|string} gameId 对局 ID
* @returns {Promise<void>}
*/
async openAndJoin(gameId) {
this.show = true;
this.$nextTick(async () => {
this.initCanvas();
await this.joinGame(gameId);
});
},
/**
* 关闭面板,对局中需要二次确认以避免误关。
*
* @returns {void}
*/
closePanel() {
if (this.gameStatus === "playing") {
window.chatDialog?.confirm("对局进行中,关闭面板将不会认输,确定关闭?").then((ok) => {
if (ok) {
this.cleanUp();
this.show = false;
}
});
return;
}
this.cleanUp();
this.show = false;
},
/**
* 重置到选择模式界面。
*
* @returns {void}
*/
resetToIdle() {
this.cleanUp();
this.gameStatus = "idle";
this.gameId = null;
this.myColor = null;
this.board = emptyBoard();
this.lastMove = null;
this.stepCount = 0;
this.rewardGold = 0;
this.resultEmoji = "";
this.resultText = "";
this.resultGold = 0;
this.$nextTick(() => this.redrawBoard());
},
/**
* 清理定时器和 WebSocket 监听。
*
* @returns {void}
*/
cleanUp() {
clearInterval(this._inviteTimer);
clearInterval(this._moveTimer);
clearInterval(this._opponentPollTimer);
this._inviteTimer = null;
this._moveTimer = null;
this._opponentPollTimer = null;
if (this._echoChannel && window.Echo) {
window.Echo.leave(`gomoku.${this.gameId}`);
this._echoChannel = null;
}
},
/**
* 发起 PvP 随机对战。
*
* @returns {Promise<void>}
*/
async startPvP() {
const roomId = window.chatContext?.roomId;
if (!roomId) {
return;
}
const response = await this.post(gomokuUrls().create, {
mode: "pvp",
room_id: roomId,
});
if (!response?.ok) {
window.chatDialog?.alert(response?.message || "发起失败,请稍后重试");
return;
}
this.gameId = response.game_id;
this.mode = "pvp";
this.myColor = 1;
this.blackName = window.chatContext?.username || "我";
this.whiteName = "等待中…";
this.gameStatus = "waiting";
this.inviteTimeout = DEFAULT_INVITE_TIMEOUT;
this.startInviteCountdown();
this.waitForOpponent();
},
/**
* 发起 PvE 人机对战。
*
* @param {Record<string, any>} level AI 难度配置
* @returns {Promise<void>}
*/
async startPvE(level) {
const roomId = window.chatContext?.roomId;
if (!roomId) {
return;
}
const response = await this.post(gomokuUrls().create, {
mode: "pve",
room_id: roomId,
ai_level: level.id,
});
if (!response?.ok) {
window.chatDialog?.alert(response?.message || "创建失败,请稍后重试");
return;
}
this.gameId = response.game_id;
this.mode = "pve";
this.myColor = 1;
this.blackName = window.chatContext?.username || "我";
this.whiteName = `AI${level.name}`;
this.rewardGold = level.reward;
this.board = emptyBoard();
this.currentTurn = 1;
this.gameStatus = "playing";
this.$nextTick(() => this.redrawBoard());
},
/**
* 加入 PvP 对战。
*
* @param {number|string} gameId 对局 ID
* @returns {Promise<void>}
*/
async joinGame(gameId) {
const response = await this.post(gomokuGameUrl(gameId, "join"), {});
if (!response?.ok) {
window.chatDialog?.alert(response?.message || "加入失败");
this.show = false;
return;
}
await this.syncState(gameId);
this.subscribeToGame(gameId);
},
/**
* 等待对手加入,保留轮询兜底。
*
* @returns {void}
*/
waitForOpponent() {
const maxAttempts = 20;
let attempts = 0;
clearInterval(this._opponentPollTimer);
this._opponentPollTimer = setInterval(async () => {
if (this.gameStatus !== "waiting") {
clearInterval(this._opponentPollTimer);
this._opponentPollTimer = null;
return;
}
attempts += 1;
if (attempts > maxAttempts) {
clearInterval(this._opponentPollTimer);
this._opponentPollTimer = null;
return;
}
const response = await fetchJson(gomokuGameUrl(this.gameId, "state"));
if (response?.status === "playing") {
clearInterval(this._opponentPollTimer);
this._opponentPollTimer = null;
this.whiteName = response.opponent_name || "对手";
this.currentTurn = response.current_turn;
this.gameStatus = "playing";
this.subscribeToGame(this.gameId);
this.$nextTick(() => this.redrawBoard());
}
}, 3000);
},
/**
* 同步对局状态。
*
* @param {number|string} gameId 对局 ID
* @returns {Promise<void>}
*/
async syncState(gameId) {
const response = await fetchJson(gomokuGameUrl(gameId, "state"));
if (!response?.ok) {
return;
}
this.gameId = gameId;
this.mode = response.mode;
this.myColor = response.your_color;
this.currentTurn = response.current_turn;
this.board = response.board;
this.gameStatus = response.status;
this.blackName = response.black_name || "黑棋";
this.whiteName = response.white_name || "白棋";
this.rewardGold = response.mode === "pvp" ? DEFAULT_PVP_REWARD : response.reward_gold ?? 0;
this.$nextTick(() => this.redrawBoard());
},
/**
* 订阅对局私有频道。
*
* @param {number|string} gameId 对局 ID
* @returns {void}
*/
subscribeToGame(gameId) {
if (!window.Echo) {
return;
}
this._echoChannel = window.Echo.private(`gomoku.${gameId}`)
.listen(".gomoku.moved", (event) => {
this.onRemoteMove(event);
})
.listen(".gomoku.finished", (event) => {
this.onGameFinished(event);
});
},
/**
* 收到远端落子。
*
* @param {Record<string, any>} event 广播事件
* @returns {void}
*/
onRemoteMove(event) {
if (this.mode !== "pvp") {
return;
}
this.board[event.row][event.col] = event.color;
this.lastMove = {
row: event.row,
col: event.col,
};
this.currentTurn = event.current_turn;
this.stepCount += 1;
this.redrawBoard();
},
/**
* 收到对局结束广播。
*
* @param {Record<string, any>} event 广播事件
* @returns {void}
*/
onGameFinished(event) {
if (event.game_id !== this.gameId) {
return;
}
this.gameStatus = "finished";
this.showResult(event.winner, event.winner_name, event.reason, event.reward_gold);
},
/**
* Canvas 点击事件:计算坐标并落子。
*
* @param {MouseEvent} event 鼠标事件
* @returns {Promise<void>}
*/
async handleCanvasClick(event) {
if (this.gameStatus !== "playing" || !this.isMyTurn) {
return;
}
const position = this.resolveBoardPosition(event);
if (!position || this.board[position.row][position.col] !== 0) {
return;
}
this.placeLocalStone(position.row, position.col, this.myColor);
if (this.mode === "pvp") {
this.currentTurn = this.myColor === 1 ? 2 : 1;
} else {
this.currentTurn = 2;
}
const response = await this.post(gomokuGameUrl(this.gameId, "move"), {
row: position.row,
col: position.col,
});
if (!response?.ok) {
this.rollbackLocalStone(position.row, position.col);
return;
}
if (response.finished) {
this.gameStatus = "finished";
this.showResult(response.winner, "", response.reason, response.reward_gold);
return;
}
if (this.mode === "pve" && response.ai_moved) {
this.applyDelayedAiMove(response.ai_moved);
}
},
/**
* Canvas 鼠标移动:更新悬停预览。
*
* @param {MouseEvent} event 鼠标事件
* @returns {void}
*/
handleCanvasHover(event) {
if (this.gameStatus !== "playing" || !this.isMyTurn) {
return;
}
const position = this.resolveBoardPosition(event);
if (position && this.board[position.row][position.col] === 0) {
this.hoverPos = position;
} else {
this.hoverPos = null;
}
this.redrawBoard();
},
/**
* 认输。
*
* @returns {Promise<void>}
*/
async resign() {
const ok = await window.chatDialog?.confirm("确定认输?").catch(() => false) ?? false;
if (!ok) {
return;
}
const response = await this.post(gomokuGameUrl(this.gameId, "resign"), {});
if (response?.finished) {
this.gameStatus = "finished";
this.showResult(response.winner, "", "resign", 0);
}
},
/**
* 取消邀请。
*
* @returns {Promise<void>}
*/
async cancelInvite() {
await this.post(gomokuGameUrl(this.gameId, "cancel"), {});
clearInterval(this._inviteTimer);
this._inviteTimer = null;
this.gameStatus = "idle";
},
/**
* 显示对局结果。
*
* @param {number} winner 获胜颜色,0 表示平局
* @param {string} winnerName 获胜玩家名称
* @param {string} reason 结束原因
* @param {number} gold 奖励金币
* @returns {void}
*/
showResult(winner, winnerName, reason, gold) {
const isWinner = winner === this.myColor;
const isDraw = winner === 0;
this.resultGold = gold ?? 0;
if (isDraw) {
this.resultEmoji = "🤝";
this.resultText = "平局!势均力敌";
} else if (reason === "resign") {
this.resultEmoji = isWinner ? "🏆" : "😔";
this.resultText = isWinner ? "对手认输,您获胜!" : "您认输了";
} else {
this.resultEmoji = isWinner ? "🏆" : "😔";
this.resultText = isWinner
? (this.mode === "pve" ? "恭喜!您击败了 AI" : "恭喜!您获胜了!")
: (this.mode === "pve" ? "AI 获胜,再接再厉!" : "惜败,再来一局!");
}
if (isWinner && gold > 0 && window.chatContext) {
window.chatContext.userJjb = (window.chatContext.userJjb ?? 0) + gold;
}
},
/**
* 初始化 Canvas 尺寸。
*
* @returns {void}
*/
initCanvas() {
const canvas = document.getElementById("gomoku-canvas");
if (!canvas) {
return;
}
const size = this.CELL * (BOARD_SIZE - 1) + this.PAD * 2;
canvas.width = size;
canvas.height = size;
this.redrawBoard();
},
/**
* 重绘棋盘、棋子、最后落子和悬停预览。
*
* @returns {void}
*/
redrawBoard() {
const canvas = document.getElementById("gomoku-canvas");
if (!canvas) {
return;
}
const context = canvas.getContext("2d");
const { CELL, PAD } = this;
const size = CELL * (BOARD_SIZE - 1) + PAD * 2;
this.drawBoardBase(context, size, CELL, PAD);
this.drawStones(context, CELL, PAD);
this.drawHoverStone(context, CELL, PAD);
},
/**
* 封装 POST 请求。
*
* @param {string} url 请求地址
* @param {Record<string, any>} data 请求数据
* @returns {Promise<Record<string, any>|null>}
*/
async post(url, data) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken(),
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
});
return await response.json();
} catch (error) {
return null;
}
},
/**
* 应用后台五子棋配置。
*
* @param {Record<string, any>|null} config 后台配置
* @returns {void}
*/
applyConfig(config) {
if (config?.ok && Array.isArray(config.pve_levels)) {
this.aiLevels = config.pve_levels.map((level, index) => ({
id: level.level,
name: level.name,
icon: DEFAULT_AI_ICONS[index] ?? "♟️",
reward: level.reward,
fee: level.fee,
color: DEFAULT_AI_COLORS[index] ?? "#336699",
}));
this._pvpReward = config.pvp_reward ?? DEFAULT_PVP_REWARD;
}
},
/**
* 有未完成对局时询问用户是否恢复。
*
* @param {Record<string, any>} active 活跃对局数据
* @returns {Promise<void>}
*/
async confirmActiveGame(active) {
const modeLabel = active.mode === "pve"
? `人机对战(${active.white_name}`
: `PvP 对战(${active.black_name} vs ${active.white_name}`;
const resume = await window.chatDialog.confirm(
`您有一局未完成的${modeLabel},确定继续吗?\n取消则直接认输并结束本局。`,
"♟️ 恢复对局",
"#1e3a5f",
);
if (resume) {
this.restoreActiveGame(active);
return;
}
await fetch(gomokuGameUrl(active.game_id, "resign"), {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken(),
Accept: "application/json",
},
}).catch(() => {});
await window.chatDialog.alert("已认输,对局结束。", "♟️ 对局结束", "#336699");
},
/**
* 恢复未完成对局状态。
*
* @param {Record<string, any>} active 活跃对局数据
* @returns {void}
*/
restoreActiveGame(active) {
this.gameId = active.game_id;
this.mode = active.mode;
this.myColor = active.your_color;
this.currentTurn = active.current_turn;
this.board = active.board;
this.blackName = active.black_name;
this.whiteName = active.white_name;
this.gameStatus = active.status === "waiting" ? "waiting" : "playing";
this.rewardGold = active.mode === "pvp" ? (this._pvpReward ?? DEFAULT_PVP_REWARD) : 0;
this.$nextTick(() => this.redrawBoard());
if (active.mode === "pvp" && active.status === "playing") {
this.subscribeToGame(active.game_id);
}
},
/**
* 启动邀请倒计时。
*
* @returns {void}
*/
startInviteCountdown() {
clearInterval(this._inviteTimer);
this._inviteTimer = setInterval(() => {
this.inviteTimeout -= 1;
if (this.inviteTimeout <= 0) {
clearInterval(this._inviteTimer);
this._inviteTimer = null;
if (this.gameStatus === "waiting") {
this.gameStatus = "idle";
window.chatDialog?.alert("邀请已超时,无人接受挑战。");
}
}
}, 1000);
},
/**
* 计算鼠标在棋盘上的行列。
*
* @param {MouseEvent} event 鼠标事件
* @returns {{row: number, col: number}|null}
*/
resolveBoardPosition(event) {
const canvas = document.getElementById("gomoku-canvas");
if (!canvas) {
return null;
}
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.round((x - this.PAD) / this.CELL);
const row = Math.round((y - this.PAD) / this.CELL);
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
return null;
}
return { row, col };
},
/**
* 本地先行落子,等待服务端确认。
*
* @param {number} row 行
* @param {number} col 列
* @param {number} color 棋子颜色
* @returns {void}
*/
placeLocalStone(row, col, color) {
this.board[row][col] = color;
this.lastMove = { row, col };
this.stepCount += 1;
this.redrawBoard();
},
/**
* 服务端拒绝落子时回滚本地乐观更新。
*
* @param {number} row 行
* @param {number} col 列
* @returns {void}
*/
rollbackLocalStone(row, col) {
this.board[row][col] = 0;
this.stepCount -= 1;
this.currentTurn = this.myColor;
this.redrawBoard();
},
/**
* 延迟展示 AI 落子,保留“思考”节奏。
*
* @param {{row: number, col: number}} aiMove AI 落子
* @returns {void}
*/
applyDelayedAiMove(aiMove) {
setTimeout(() => {
this.board[aiMove.row][aiMove.col] = 2;
this.lastMove = {
row: aiMove.row,
col: aiMove.col,
};
this.stepCount += 1;
this.currentTurn = 1;
this.redrawBoard();
}, 600);
},
/**
* 绘制棋盘背景、网格、星位和坐标。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} size 棋盘尺寸
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawBoardBase(context, size, cell, pad) {
context.fillStyle = "#dcb866";
context.fillRect(0, 0, size, size);
context.strokeStyle = "#a0783a";
context.lineWidth = 0.8;
for (let index = 0; index < BOARD_SIZE; index += 1) {
const position = pad + index * cell;
context.beginPath();
context.moveTo(position, pad);
context.lineTo(position, pad + (BOARD_SIZE - 1) * cell);
context.stroke();
context.beginPath();
context.moveTo(pad, position);
context.lineTo(pad + (BOARD_SIZE - 1) * cell, position);
context.stroke();
}
this.drawStarPoints(context, cell, pad);
this.drawCoordinates(context, cell, pad);
},
/**
* 绘制棋盘星位。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawStarPoints(context, cell, pad) {
context.fillStyle = "#7a5c28";
[[3, 3], [3, 11], [11, 3], [11, 11], [7, 7]].forEach(([row, col]) => {
context.beginPath();
context.arc(pad + col * cell, pad + row * cell, 3.5, 0, Math.PI * 2);
context.fill();
});
},
/**
* 绘制棋盘坐标。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawCoordinates(context, cell, pad) {
const columns = "ABCDEFGHJKLMNOP";
context.fillStyle = "#7a5c28";
context.font = "9px monospace";
context.textAlign = "center";
context.textBaseline = "middle";
for (let index = 0; index < BOARD_SIZE; index += 1) {
context.fillText(columns[index], pad + index * cell, pad - 12);
context.fillText(BOARD_SIZE - index, pad - 14, pad + index * cell);
}
},
/**
* 绘制所有棋子和最后落子标记。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawStones(context, cell, pad) {
this.board.forEach((row, rowIndex) => {
row.forEach((stone, colIndex) => {
if (stone === 0) {
return;
}
this.drawStone(context, rowIndex, colIndex, stone, cell, pad);
});
});
},
/**
* 绘制单枚棋子。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} row 行
* @param {number} col 列
* @param {number} stone 棋子颜色
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawStone(context, row, col, stone, cell, pad) {
const x = pad + col * cell;
const y = pad + row * cell;
const isLast = this.lastMove?.row === row && this.lastMove?.col === col;
const gradient = context.createRadialGradient(x - 3, y - 3, 1, x, y, cell * 0.45);
if (stone === 1) {
gradient.addColorStop(0, "#888");
gradient.addColorStop(1, "#111");
} else {
gradient.addColorStop(0, "#fff");
gradient.addColorStop(1, "#ccc");
}
context.beginPath();
context.arc(x, y, cell * 0.44, 0, Math.PI * 2);
context.fillStyle = gradient;
context.fill();
context.strokeStyle = stone === 1 ? "#000" : "#aaa";
context.lineWidth = 0.5;
context.stroke();
if (isLast) {
context.beginPath();
context.arc(x, y, 4, 0, Math.PI * 2);
context.fillStyle = stone === 1 ? "#fff" : "#666";
context.fill();
}
},
/**
* 绘制悬停预览棋子。
*
* @param {CanvasRenderingContext2D} context Canvas 上下文
* @param {number} cell 格子大小
* @param {number} pad 内边距
* @returns {void}
*/
drawHoverStone(context, cell, pad) {
if (!this.hoverPos || !this.isMyTurn || this.gameStatus !== "playing") {
return;
}
const { row, col } = this.hoverPos;
if (this.board[row][col] !== 0) {
return;
}
const x = pad + col * cell;
const y = pad + row * cell;
context.beginPath();
context.arc(x, y, cell * 0.44, 0, Math.PI * 2);
context.fillStyle = this.myColor === 1 ? "rgba(0,0,0,0.35)" : "rgba(255,255,255,0.55)";
context.fill();
context.strokeStyle = this.myColor === 1 ? "rgba(0,0,0,.5)" : "rgba(150,150,150,.5)";
context.lineWidth = 1;
context.stroke();
},
};
}
/**
* 挂载五子棋主面板全局组件名,兼容 Blade 的 x-data。
*
* @returns {void}
*/
export function bindGomokuPanelControls() {
if (typeof window === "undefined") {
return;
}
window.gomokuPanel = gomokuPanel;
}