koishi-plugin-see-color
Version:
Koishi 的找色块游戏插件。找到不同的色块。
541 lines (530 loc) • 19.9 kB
JavaScript
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
});