UNPKG

koishi-plugin-see-color

Version:

Koishi 的找色块游戏插件。找到不同的色块。

541 lines (530 loc) 19.9 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_path = __toESM(require("path")); var name = "see-color"; var inject = { required: ["database", "puppeteer"] // optional: ['markdownToImage'], }; var usage = `## 使用 1. 启动 \`puppeteer\` 服务。 2. 设置指令别名。 ## QQ 群 - 956758505`; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ initialLevel: import_koishi.Schema.number().default(2).description("游戏的初始等级。"), blockGuessTimeLimitInSeconds: import_koishi.Schema.number().min(0).default(0).description("猜测色块的时间限制(秒),值为 0 时则不限制时间。"), blockSize: import_koishi.Schema.number().default(50).description("每个颜色方块的大小(像素)。"), spacingBetweenGrids: import_koishi.Schema.number().default(10).description("色块之间的水平与垂直间距(像素)。"), isNumericGuessMiddlewareEnabled: import_koishi.Schema.boolean().default(true).description("是否启用数字猜测中间件。"), shouldInterruptMiddlewareChainAfterTriggered: import_koishi.Schema.boolean().default(true).description("是否在触发后中断中间件链。") }).description("基础配置"), import_koishi.Schema.object({ isCompressPicture: import_koishi.Schema.boolean().default(false).description("是否压缩图片(不建议)。") }).description("图片配置"), import_koishi.Schema.union([ import_koishi.Schema.object({ isCompressPicture: import_koishi.Schema.const(true).required(), pictureQuality: import_koishi.Schema.number().min(1).max(100).default(80).description("压缩后图片的质量(1 ~ 100)。") }), import_koishi.Schema.object({}) ]) ]); function apply(ctx, config) { const filePath = import_path.default.join(__dirname, "emptyHtml.html").replace(/\\/g, "/"); const pageGotoFilePath = "file://" + filePath; const msg = { start: `🎉 猜色块游戏开启!`, guess: `请输入 '行 列' 来揭示色块。 例如: '2 1'。记得空格哦!😉`, guessRight: `👏 猜中啦!你太棒了!😍`, guessWrong: `哎呀,没猜中。再来一次吧!😊`, continue: `继续游戏,看你的了!😜`, restarted: `👍 游戏重置!`, stopped: `😢 游戏暂停。 新一轮,开始!😘`, isStarted: `😎 游戏进行中...`, isNotStarted: `😮 还没开始。 让我们动起来!😁` }; ctx.database.extend("see_color_games", { id: "unsigned", channelId: "string", isStarted: "boolean", level: "unsigned", block: "unsigned", timestamp: "string" }, { primary: "id", autoInc: true }); ctx.database.extend("see_color_playing_records", { id: "unsigned", channelId: "string", userId: "string", username: "string", score: "unsigned" }, { primary: "id", autoInc: true }); ctx.database.extend("see_color_rank", { id: "unsigned", userId: "string", userName: "string", score: "unsigned" }, { primary: "id", autoInc: true }); ctx.middleware(async (session, next) => { const gameInfo = await getGameInfo(session.channelId); if (!gameInfo.isStarted || !config.isNumericGuessMiddlewareEnabled) { return await next(); } if (checkFormat(session.content, gameInfo.level)) { await session.execute(`seeColor.猜 ${session.content}`); } else if (isValidNumber(session.content, gameInfo.level)) { await session.execute(`seeColor.猜 ${session.content}`); } else { return await next(); } if (config.shouldInterruptMiddlewareChainAfterTriggered) { return; } else { return await next(); } }); ctx.command("seeColor", "给我点颜色看看帮助").action(async ({ session }) => { await session.execute(`seecolor -h`); }); ctx.command("seeColor.开始", "开始游戏").action(async ({ session }) => { const gameInfo = await getGameInfo(session.channelId); if (gameInfo.isStarted) { return msg.isStarted; } await updatePlayingRecord(session, { level: 0 }); const buffer = await generatePictureBuffer(config.initialLevel, session.channelId); await session.send(`${import_koishi.h.at(session.userId)} ~ ${msg.start} ${import_koishi.h.image(buffer, `image/${config.isCompressPicture ? `jpeg` : `png`}`)} ${msg.guess}`); await updateGameState(session.channelId, true, config.initialLevel, String(session.timestamp)); }); ctx.command("seeColor.猜 <numberString:text>", "猜色块").action(async ({ session }, numberString) => { if (!numberString) { return msg.guess; } const gameInfo = await getGameInfo(session.channelId); const isCheckFormat = checkFormat(numberString, gameInfo.level); const isValidNumberString = isValidNumber(numberString, gameInfo.level); if (!isCheckFormat && !isValidNumberString) { return msg.guess; } if (!gameInfo.isStarted) { return msg.isNotStarted; } const lastTimestamp = Number(gameInfo.timestamp); const timeDifference = calculateTimeDifference(lastTimestamp, session.timestamp); if (timeDifference > config.blockGuessTimeLimitInSeconds && config.blockGuessTimeLimitInSeconds > 0) { await session.send(`时间超过 ${config.blockGuessTimeLimitInSeconds} 秒!游戏结束!😢`); await session.execute(`seeColor.结束`); return; } let number = 0; const { row, col } = getRowCol(gameInfo.level, gameInfo.block - 1); if (isValidNumberString) { number = parseFloat(numberString); } else { const [rowStr, colStr] = numberString.split(" "); const rowNumber = parseInt(rowStr); const colNumber = parseInt(colStr); if (rowNumber === row + 1 && colNumber === col + 1) { number = gameInfo.block; } else { return msg.guessWrong; } } if (number === gameInfo.block) { const playingRecord = await updatePlayingRecord(session, gameInfo); await updateRank(session.userId, session.username, playingRecord.score); const buffer = await generatePictureBuffer(gameInfo.level + 1, session.channelId); await session.send(`${import_koishi.h.at(session.userId)} ~ ${msg.guessRight} 赢得 ${gameInfo.level} 点积分!再接再厉喵~😊 ${import_koishi.h.image(buffer, `image/${config.isCompressPicture ? `jpeg` : `png`}`)} ${msg.continue}`); await updateGameState(session.channelId, true, gameInfo.level + 1, String(session.timestamp)); return; } else { return msg.guessWrong; } }); ctx.command("seeColor.结束", "强制结束游戏").action(async ({ session }) => { const gameInfo = await getGameInfo(session.channelId); if (gameInfo.isStarted) { await ctx.database.remove("see_color_playing_records", { channelId: session.channelId }); await ctx.database.set("see_color_games", { channelId: session.channelId }, { isStarted: false }); const { row, col } = getRowCol(gameInfo.level, gameInfo.block - 1); await session.send(`${import_koishi.h.at(session.userId)} ~ 嘿嘿~🤭猜不出来吧~ 答案是块 ${gameInfo.block}(${row + 1} ${col + 1}) 喔~ ${msg.stopped}`); } else { return msg.isNotStarted; } }); ctx.command("seeColor.排行榜", "查看色榜").action(async ({}) => { const rankInfo = await ctx.database.get("see_color_rank", {}); rankInfo.sort((a, b) => b.score - a.score); return generateRankTable(rankInfo.slice(0, 10)); function generateRankTable(rankInfo2) { return ` 给我点颜色看看排行榜: 排名 昵称 积分 -------------------- ${rankInfo2.map((player, index) => ` ${String(index + 1).padStart(2, " ")} ${player.userName.padEnd(6, " ")} ${player.score.toString().padEnd(4, " ")}`).join("\n")} `; } __name(generateRankTable, "generateRankTable"); }); function calculateTimeDifference(previousTimestamp, currentTimestamp) { return (currentTimestamp - previousTimestamp) / 1e3; } __name(calculateTimeDifference, "calculateTimeDifference"); function getLevel(n) { const minLevel = 6; const maxLevel = 12; const range = maxLevel - minLevel + 1; return n % range + minLevel; } __name(getLevel, "getLevel"); function randomSign() { return import_koishi.Random.int(2) * 2 - 1; } __name(randomSign, "randomSign"); function to256(scale) { scale *= 256; return scale > 255 ? "ff" : scale < 0 ? "00" : Math.floor(scale).toString(16).padStart(2, "0"); } __name(to256, "to256"); function createColor(r, g, b) { return `#${to256(r)}${to256(g)}${to256(b)}`; } __name(createColor, "createColor"); function hsv(h2, s = 1, v = 1) { let c = v * s; const hh = h2 / 60; const m = v - c; const x = c * (1 - Math.abs(hh % 2 - 1)) + m; c = c + m; switch (Math.floor(hh)) { case 0: return createColor(c, x, m); case 1: return createColor(x, c, m); case 2: return createColor(m, c, x); case 3: return createColor(m, x, c); case 4: return createColor(x, m, c); case 5: return createColor(c, m, x); } } __name(hsv, "hsv"); function isValidNumber(content, level) { const isValidFormat = /^\d+(\.\d+)?$/.test(content); if (!isValidFormat) { return false; } const number = parseFloat(content); if (number >= 1 && number <= level * level) { return true; } return false; } __name(isValidNumber, "isValidNumber"); function checkFormat(content, level) { const regex = /^\d+\s\d+$/; if (!regex.test(content)) { return false; } const [num1, num2] = content.split(" ").map(Number); if (isNaN(num1) || isNaN(num2)) { return false; } if (num1 < 1 || num1 > level || num2 < 1 || num2 > level) { return false; } return true; } __name(checkFormat, "checkFormat"); function getRowCol(n, diffIndex) { const row = Math.floor(diffIndex / n); const col = diffIndex % n; return { row, col }; } __name(getRowCol, "getRowCol"); function adjustColor(color, percentage, mode, maxIterations = 100) { const rgb = parseInt(color.slice(1), 16); let newColor; let iterations = 0; do { const factor = 1 + Math.random() * (percentage / 200); const adjusted = [rgb >> 16, rgb >> 8 & 255, rgb & 255].map((value) => { let newValue; if (mode === "随机") { newValue = Math.random() < 0.5 ? Math.min(255, Math.round(value * factor)) : Math.max(0, Math.round(value / factor)); } else if (mode === "变浅") { newValue = Math.min(255, Math.round(value * factor)); } else if (mode === "变深") { newValue = Math.max(0, Math.round(value / factor)); } else { newValue = value; } return newValue; }); const adjustedColor = adjusted.map((value) => Math.min(255, Math.max(0, value))); newColor = "#" + adjustedColor.reduce((acc, cur) => (acc << 8) + cur, 0).toString(16).padStart(6, "0"); iterations++; if (iterations >= maxIterations) { newColor = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0"); break; } } while (newColor === color); return newColor; } __name(adjustColor, "adjustColor"); function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } __name(randomInt, "randomInt"); function randomColor() { return "#" + Math.floor(Math.random() * 16777216).toString(16).padStart(6, "0"); } __name(randomColor, "randomColor"); async function updatePlayingRecord(session, gameInfo) { let playingRecord = await ctx.database.get("see_color_playing_records", { userId: session.userId, channelId: session.channelId }); if (playingRecord.length === 0) { return await ctx.database.create("see_color_playing_records", { channelId: session.channelId, userId: session.userId, username: session.username, score: gameInfo.level }); } else { await ctx.database.set("see_color_playing_records", { channelId: session.channelId, userId: session.userId }, { username: session.username, score: playingRecord[0].score + gameInfo.level }); playingRecord = await ctx.database.get("see_color_playing_records", { userId: session.userId, channelId: session.channelId }); } return playingRecord[0]; } __name(updatePlayingRecord, "updatePlayingRecord"); async function updateRank(userId, userName, score) { const rankInfo = await ctx.database.get("see_color_rank", { userId }); if (rankInfo.length === 0) { await ctx.database.create("see_color_rank", { userId, userName, score }); } else if (rankInfo[0].score < score) { await ctx.database.set("see_color_rank", { userId }, { userName, score }); } } __name(updateRank, "updateRank"); function isNumericString(input) { return /^\d+$/.test(input); } __name(isNumericString, "isNumericString"); async function getGameInfo(channelId) { const gameInfo = await ctx.database.get("see_color_games", { channelId }); if (gameInfo.length === 0) { return await ctx.database.create("see_color_games", { channelId, isStarted: false }); } else { return gameInfo[0]; } } __name(getGameInfo, "getGameInfo"); async function updateGameState(channelId, isStarted, level, timestamp) { await ctx.database.set("see_color_games", { channelId }, { isStarted, level, timestamp }); } __name(updateGameState, "updateGameState"); async function generatePictureBuffer(n, channelId) { const { blockSize, isCompressPicture, pictureQuality, spacingBetweenGrids } = config; const h2 = import_koishi.Random.real(360); const s = import_koishi.Random.real(0.2, 1); const v = import_koishi.Random.real(0.2, 1); const baseColor = hsv(h2, s, v); const factorH = Math.random() * 0.3 + 0.1; const residue = 1 - factorH; const factorS = Math.random() * residue * 0.6 + residue * 0.2; const factorV = residue - factorS; const level = getLevel(n); const rangeH = 30 * Math.exp(-0.2 * level); const rangeS = 0.5 * Math.exp(-0.1 * level); const rangeV = 0.2 * Math.exp(-0.1 * level); let deltaS = factorS * rangeS; if (deltaS + s > 1) { deltaS *= -1; } else if (deltaS <= s) { deltaS *= randomSign(); } let deltaV = factorV * rangeV; if (deltaV + v > 1) { deltaV *= -1; } else if (deltaV <= v) { deltaV *= randomSign(); } const factor = s + v + deltaS / 2 + deltaV / 2; const deltaH = factorH * rangeH * randomSign() / factor; let biasedH = h2 + deltaH; if (biasedH < 0) biasedH += 360; else if (biasedH >= 360) biasedH -= 360; const diffColor = hsv(biasedH, s + deltaS, v + deltaV); const diffIndex = randomInt(0, n * n - 1); const { row, col } = getRowCol(n, diffIndex); const offset = blockSize; const canvasSize = n * blockSize + (n - 1) * spacingBetweenGrids + offset * 2; await ctx.database.set("see_color_games", { channelId }, { block: diffIndex + 1 }); let html = ` <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>色块网格</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } canvas { /*border: 1px solid #000;*/ } </style> </head> <body> <canvas id="colorGridCanvas"></canvas> <script> const n = ${n}; // 网格的大小 const blockSize = ${blockSize}; // 色块的边长 const spacingBetweenGrids = ${spacingBetweenGrids}; // 色块间距 const baseColor = '${baseColor}'; // 基础色块颜色 const diffColor = '${diffColor}'; // 不同色块的颜色 const offset = ${offset}; // 行号和列号的偏移量 const canvas = document.getElementById('colorGridCanvas'); const ctx = canvas.getContext('2d'); // 设置 canvas 大小,增加额外空间用于显示行号和列号 canvas.width = ${canvasSize}; canvas.height = ${canvasSize}; // 随机选择一个色块改变颜色 const diffBlockRow = ${row}; const diffBlockCol = ${col}; // 绘制色块网格 for (let row = 0; row < n; row++) { for (let col = 0; col < n; col++) { const color = (row === diffBlockRow && col === diffBlockCol) ? diffColor : baseColor; const x = col * (blockSize + spacingBetweenGrids) + offset; const y = row * (blockSize + spacingBetweenGrids) + offset; ctx.fillStyle = color; ctx.fillRect(x, y, blockSize, blockSize); } } // 设置字体样式 ctx.font = '30px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // 在色块外侧显示行号和列号 for (let i = 0; i < n; i++) { // 显示列号 ctx.fillText(i + 1, i * (blockSize + spacingBetweenGrids) + blockSize / 2 + offset, blockSize / 2); // 显示行号 ctx.fillText(i + 1, blockSize / 2, i * (blockSize + spacingBetweenGrids) + blockSize / 2 + offset); } </script> </body> </html> `; const browser = ctx.puppeteer.browser; const context = await browser.createBrowserContext(); const page = await context.newPage(); await page.setViewport({ width: canvasSize, height: canvasSize, deviceScaleFactor: 1 }); await page.goto(pageGotoFilePath); await page.setContent(html, { waitUntil: "load" }); const canvas = await page.$("#colorGridCanvas"); const buffer = await canvas.screenshot(isCompressPicture ? { type: "jpeg", quality: pictureQuality } : { type: "png" }); await page.close(); await context.close(); return buffer; } __name(generatePictureBuffer, "generatePictureBuffer"); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, usage });