Ⓜ️手动从旧梦 81a191f merge

This commit is contained in:
2024-08-12 11:44:30 +08:00
parent 068feaa591
commit 2d67a703bd
214 changed files with 6457 additions and 10418 deletions

View File

@ -0,0 +1,13 @@
## 版权声明
本插件由 汉钰律许可协议 授权开源,兼容并继承自 MIT 许可协议。
Copyright (c) 2022 MeetWq
版权所有 © 2024 EillesWan & MeetWq
猜成语-睿乐特别版(trimo_plugin_handle)根据 第一版 汉钰律许可协议(“本协议”)授权。\
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。

View File

@ -0,0 +1,274 @@
import asyncio
from asyncio import TimerHandle
from typing import Any, Dict
import nonebot
from nonebot import on_regex, require, on_command
from nonebot.matcher import Matcher
from nonebot.params import RegexDict
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from nonebot.rule import to_me
from nonebot.utils import run_sync
from nonebot.permission import SUPERUSER
from typing_extensions import Annotated
require("nonebot_plugin_alconna")
require("nonebot_plugin_session")
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Image,
Option,
Query,
Text,
UniMessage,
on_alconna,
store_true,
Args,
Arparma,
)
from nonebot_plugin_session import SessionId, SessionIdType
from .config import Config, handle_config
from .data_source import GuessResult, Handle
from .utils import random_idiom
__plugin_meta__ = PluginMetadata(
name="猜成语",
description="猜成语-睿乐特别版",
usage=(
"@我 + “猜成语”开始游戏;\n"
"你有十次的机会猜一个四字词语;\n"
"每次猜测后,汉字与拼音的颜色将会标识其与正确答案的区别;\n"
"青色 表示其出现在答案中且在正确的位置;\n"
"橙色 表示其出现在答案中但不在正确的位置;\n"
"每个格子的 汉字、声母、韵母、声调 都会独立进行颜色的指示。\n"
"当四个格子都为青色时,你便赢得了游戏!\n"
"可发送“结束”结束游戏;可发送“提示”查看提示。\n"
"使用 --strict 选项开启非默认的成语检查,即猜测的短语必须是成语,\n"
"如:@我 猜成语 --strict"
),
type="application",
homepage="https://github.com/noneplugin/nonebot-plugin-handle",
config=Config,
supported_adapters=inherit_supported_adapters(
"nonebot_plugin_alconna", "nonebot_plugin_session"
),
extra={
"example": "@小Q 猜成语",
},
)
games: Dict[str, Handle] = {}
timers: Dict[str, TimerHandle] = {}
UserId = Annotated[str, SessionId(SessionIdType.GROUP)]
def game_is_running(user_id: UserId) -> bool:
return user_id in games
def game_not_running(user_id: UserId) -> bool:
return user_id not in games
handle = on_alconna(
Alconna(
"handle",
Option("-s|--strict", default=False, action=store_true),
Option("-d|--hard", default=False, action=store_true),
),
aliases=("猜成语",),
rule=to_me() & game_not_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_hint = on_alconna(
"提示",
rule=game_is_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_stop = on_alconna(
"结束",
aliases=("结束游戏", "结束猜成语"),
rule=game_is_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_answer = on_alconna(
Alconna(
"答案",
Option(
"-g|--group",
default="Now",
args=Args["group", str, "Now"],
),
Option(
"-l|--list",
default=False,
action=store_true,
),
),
# rule=game_is_running,
use_cmd_start=True,
permission=SUPERUSER,
block=True,
priority=13,
)
# handle_update = on_alconna(
# "更新词库",
# aliases=("刷新词库", "猜成语刷新词库"),
# rule=to_me(),
# use_cmd_start=True,
# block=True,
# priority=13,
# )
handle_idiom = on_regex(
r"^(?P<idiom>[\u4e00-\u9fa5]{4})$",
rule=game_is_running,
block=True,
priority=14,
)
def stop_game(user_id: str):
if timer := timers.pop(user_id, None):
timer.cancel()
games.pop(user_id, None)
async def stop_game_timeout(matcher: Matcher, user_id: str):
game = games.get(user_id, None)
stop_game(user_id)
if game:
msg = "猜成语超时,游戏结束。"
if len(game.guessed_idiom) >= 1:
msg += f"\n{game.result}"
await matcher.finish(msg)
def set_timeout(matcher: Matcher, user_id: str, timeout: float = 300):
if timer := timers.get(user_id, None):
timer.cancel()
loop = asyncio.get_running_loop()
timer = loop.call_later(
timeout, lambda: asyncio.ensure_future(stop_game_timeout(matcher, user_id))
)
timers[user_id] = timer
@handle.handle()
async def _(
result: Arparma,
matcher: Matcher,
user_id: UserId,
):
nonebot.logger.info(result.options)
is_strict = handle_config.handle_strict_mode or result.options["strict"].value
idiom, explanation = random_idiom(result.options["hard"].value)
game = Handle(idiom, explanation, strict=is_strict)
games[user_id] = game
set_timeout(matcher, user_id)
msg = Text(
f"你有{game.times}次机会猜一个四字成语,"
+ ("发送有效成语以参与游戏。" if is_strict else "发送任意四字词语以参与游戏。")
) + Image(raw=await run_sync(game.draw)())
await msg.send()
@handle_hint.handle()
async def _(matcher: Matcher, user_id: UserId):
game = games[user_id]
set_timeout(matcher, user_id)
await UniMessage.image(raw=await run_sync(game.draw_hint)()).send()
@handle_stop.handle()
async def _(matcher: Matcher, user_id: UserId):
game = games[user_id]
stop_game(user_id)
msg = "游戏已结束"
if len(game.guessed_idiom) >= 1:
msg += f"\n{game.result}"
await matcher.finish(msg)
# @handle_update.handle()
@handle_idiom.handle()
async def _(matcher: Matcher, user_id: UserId, matched: Dict[str, Any] = RegexDict()):
game = games[user_id]
set_timeout(matcher, user_id)
idiom = str(matched["idiom"])
result = game.guess(idiom)
if result in [GuessResult.WIN, GuessResult.LOSS]:
stop_game(user_id)
msg = Text(
(
"恭喜你猜出了成语!"
if result == GuessResult.WIN
else "很遗憾,没有人猜出来呢"
)
+ f"\n{game.result}"
) + Image(raw=await run_sync(game.draw)())
await msg.send()
elif result == GuessResult.DUPLICATE:
await matcher.finish("你已经猜过这个成语了呢")
elif result == GuessResult.ILLEGAL:
await matcher.finish(f"你确定“{idiom}”是个成语吗?")
else:
await UniMessage.image(raw=await run_sync(game.draw)()).send()
@handle_answer.handle()
async def _(
result: Arparma,
matcher: Matcher,
user_id: UserId,
):
if result.options["list"].value:
await handle_answer.finish(
UniMessage.text(
"\n".join("{}-{}".format(i, j.idiom) for i, j in games.items())
)
)
return
try:
if result.options["group"].args["group"] == "Now":
session_numstr = user_id
else:
session_numstr = "qq_OneBot V11_2378756507_{}".format(
result.options["group"].args["group"]
)
except:
session_numstr = user_id
if session_numstr in games.keys():
await handle_answer.finish(UniMessage.text(games[session_numstr].idiom))
else:
await handle_answer.finish(
UniMessage.text("{} 不存在开局的游戏".format(session_numstr))
)

View File

@ -0,0 +1,10 @@
from nonebot import get_plugin_config
from pydantic import BaseModel
class Config(BaseModel):
handle_strict_mode: bool = False
handle_color_enhance: bool = False
handle_config = get_plugin_config(Config)

View File

@ -0,0 +1,284 @@
from dataclasses import dataclass
from enum import Enum
from io import BytesIO
from typing import List, Optional, Tuple
from PIL import Image, ImageDraw
from PIL.Image import Image as IMG
from .config import handle_config
from .utils import get_pinyin, legal_idiom, load_font, save_jpg
class GuessResult(Enum):
WIN = 0 # 猜出正确成语
LOSS = 1 # 达到最大可猜次数,未猜出正确成语
DUPLICATE = 2 # 成语重复
ILLEGAL = 3 # 成语不合法
class GuessState(Enum):
CORRECT = 0 # 存在且位置正确
EXIST = 1 # 存在但位置不正确
WRONG = 2 # 不存在
@dataclass
class ColorGroup:
bg_color: str # 背景颜色
block_color: str # 方块颜色
correct_color: str # 存在且位置正确时的颜色
exist_color: str # 存在但位置不正确时的颜色
wrong_color_pinyin: str # 不存在时的颜色
wrong_color_char: str # 不存在时的颜色
NORMAL_COLOR = ColorGroup(
"#ffffff", "#f7f8f9", "#1d9c9c", "#de7525", "#b4b8be", "#5d6673"
)
ENHANCED_COLOR = ColorGroup(
"#ffffff", "#f7f8f9", "#5ba554", "#ff46ff", "#b4b8be", "#5d6673"
)
class Handle:
def __init__(self, idiom: str, explanation: str, strict: bool = False):
self.idiom: str = idiom # 成语
self.explanation: str = explanation # 释义
self.strict: bool = strict # 是否判断输入词语为成语
self.result = f"【成语】:{idiom}\n【释义】:{explanation}"
self.pinyin: List[Tuple[str, str, str]] = get_pinyin(idiom) # 拼音
self.length = 4
self.times: int = 10 # 可猜次数
self.guessed_idiom: List[str] = [] # 记录已猜成语
self.guessed_pinyin: List[List[Tuple[str, str, str]]] = [] # 记录已猜成语的拼音
self.block_size = (160, 160) # 文字块尺寸
self.block_padding = (20, 20) # 文字块之间间距
self.padding = (40, 40) # 边界间距
font_size_char = 60 # 汉字字体大小
font_size_pinyin = 30 # 拼音字体大小
font_size_tone = 22 # 声调字体大小
self.font_char = load_font("NotoSerifSC-Regular.otf", font_size_char)
self.font_pinyin = load_font("NotoSansMono-Regular.ttf", font_size_pinyin)
self.font_tone = load_font("NotoSansMono-Regular.ttf", font_size_tone)
self.colors = (
ENHANCED_COLOR if handle_config.handle_color_enhance else NORMAL_COLOR
)
def guess(self, idiom: str) -> Optional[GuessResult]:
if self.strict and not legal_idiom(idiom):
return GuessResult.ILLEGAL
if idiom in self.guessed_idiom:
return GuessResult.DUPLICATE
self.guessed_idiom.append(idiom)
self.guessed_pinyin.append(get_pinyin(idiom))
if idiom == self.idiom:
return GuessResult.WIN
if len(self.guessed_idiom) == self.times:
return GuessResult.LOSS
def draw_block(
self,
block_color: str,
char: str = "",
char_color: str = "",
initial: str = "",
initial_color: str = "",
final: str = "",
final_color: str = "",
tone: str = "",
tone_color: str = "",
underline: bool = False,
underline_color: str = "",
) -> IMG:
block = Image.new("RGB", self.block_size, block_color)
if not char:
return block
draw = ImageDraw.Draw(block)
char_size = self.font_char.getbbox(char)[2:]
x = (self.block_size[0] - char_size[0]) / 2
y = (self.block_size[1] - char_size[1]) / 5 * 3
draw.text((x, y), char, font=self.font_char, fill=char_color)
space = 5
need_space = bool(initial and final)
py_length = self.font_pinyin.getlength(initial + final)
if need_space:
py_length += space
py_start = (self.block_size[0] - py_length) / 2
x = py_start
y = self.block_size[0] / 8
draw.text((x, y), initial, font=self.font_pinyin, fill=initial_color)
x += self.font_pinyin.getlength(initial)
if need_space:
x += space
draw.text((x, y), final, font=self.font_pinyin, fill=final_color)
tone_size = self.font_tone.getbbox(tone)[2:]
x = (self.block_size[0] + py_length) / 2 + tone_size[0] / 3
y -= tone_size[1] / 3
draw.text((x, y), tone, font=self.font_tone, fill=tone_color)
if underline:
x = py_start
py_size = self.font_pinyin.getbbox(initial + final)[2:]
y = self.block_size[0] / 8 + py_size[1] + 2
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
y += 3
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
return block
def draw(self) -> BytesIO:
rows = min(len(self.guessed_idiom) + 1, self.times)
board_w = self.length * self.block_size[0]
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
board_h = rows * self.block_size[1]
board_h += (rows - 1) * self.block_padding[1] + 2 * self.padding[1]
board_size = (board_w, board_h)
board = Image.new("RGB", board_size, self.colors.bg_color)
def get_states(guessed: List[str], answer: List[str]) -> List[GuessState]:
states = []
incorrect = []
for i in range(self.length):
if guessed[i] != answer[i]:
incorrect.append(answer[i])
else:
incorrect.append("_")
for i in range(self.length):
if guessed[i] == answer[i]:
states.append(GuessState.CORRECT)
elif guessed[i] in incorrect:
states.append(GuessState.EXIST)
incorrect[incorrect.index(guessed[i])] = "_"
else:
states.append(GuessState.WRONG)
return states
def get_pinyin_color(state: GuessState) -> str:
if state == GuessState.CORRECT:
return self.colors.correct_color
elif state == GuessState.EXIST:
return self.colors.exist_color
else:
return self.colors.wrong_color_pinyin
def get_char_color(state: GuessState) -> str:
if state == GuessState.CORRECT:
return self.colors.correct_color
elif state == GuessState.EXIST:
return self.colors.exist_color
else:
return self.colors.wrong_color_char
def block_pos(row: int, col: int) -> Tuple[int, int]:
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * col
y = self.padding[1] + (self.block_size[1] + self.block_padding[1]) * row
return x, y
for i in range(len(self.guessed_idiom)):
idiom = self.guessed_idiom[i]
pinyin = self.guessed_pinyin[i]
char_states = get_states(list(idiom), list(self.idiom))
initial_states = get_states(
[p[0] for p in pinyin], [p[0] for p in self.pinyin]
)
final_states = get_states(
[p[1] for p in pinyin], [p[1] for p in self.pinyin]
)
tone_states = get_states(
[p[2] for p in pinyin], [p[2] for p in self.pinyin]
)
underline_states = get_states(
[p[0] + p[1] for p in pinyin], [p[0] + p[1] for p in self.pinyin]
)
for j in range(self.length):
char = idiom[j]
i2, f2, t2 = pinyin[j]
if char == self.idiom[j]:
block_color = self.colors.correct_color
char_color = initial_color = final_color = tone_color = (
self.colors.bg_color
)
underline = False
underline_color = ""
else:
block_color = self.colors.block_color
char_color = get_char_color(char_states[j])
initial_color = get_pinyin_color(initial_states[j])
final_color = get_pinyin_color(final_states[j])
tone_color = get_pinyin_color(tone_states[j])
underline_color = get_pinyin_color(underline_states[j])
underline = underline_color in (
self.colors.correct_color,
self.colors.exist_color,
)
block = self.draw_block(
block_color,
char,
char_color,
i2,
initial_color,
f2,
final_color,
t2,
tone_color,
underline,
underline_color,
)
board.paste(block, block_pos(i, j))
for i in range(len(self.guessed_idiom), rows):
for j in range(self.length):
block = self.draw_block(self.colors.block_color)
board.paste(block, block_pos(i, j))
return save_jpg(board)
def draw_hint(self) -> BytesIO:
guessed_char = set("".join(self.guessed_idiom))
guessed_initial = set()
guessed_final = set()
guessed_tone = set()
for pinyin in self.guessed_pinyin:
for p in pinyin:
guessed_initial.add(p[0])
guessed_final.add(p[1])
guessed_tone.add(p[2])
board_w = self.length * self.block_size[0]
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
board_h = self.block_size[1] + 2 * self.padding[1]
board = Image.new("RGB", (board_w, board_h), self.colors.bg_color)
for i in range(self.length):
char = self.idiom[i]
hi, hf, ht = self.pinyin[i]
color = char_c = initial_c = final_c = tone_c = self.colors.correct_color
if char not in guessed_char:
char = "?"
color = self.colors.block_color
char_c = self.colors.wrong_color_char
else:
char_c = initial_c = final_c = tone_c = self.colors.bg_color
if hi not in guessed_initial:
hi = "?"
initial_c = self.colors.wrong_color_pinyin
if hf not in guessed_final:
hf = "?"
final_c = self.colors.wrong_color_pinyin
if ht not in guessed_tone:
ht = "?"
tone_c = self.colors.wrong_color_pinyin
block = self.draw_block(
color, char, char_c, hi, initial_c, hf, final_c, ht, tone_c
)
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * i
y = self.padding[1]
board.paste(block, (x, y))
return save_jpg(board)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 MeetWq
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,118 @@
import json
import random
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Tuple
# from watchdog.observers import Observer
# from watchdog.events import FileSystemEventHandler, FileModifiedEvent
from PIL import ImageFont
from PIL.Image import Image as IMG
from PIL.ImageFont import FreeTypeFont
from pypinyin import Style, pinyin
resource_dir = Path(__file__).parent / "resources"
fonts_dir = resource_dir / "fonts"
data_dir = resource_dir / "data"
idiom_path = data_dir / "idioms.txt"
answer_path = data_dir / "answers.json"
answer_hard_path = data_dir / "answers_hard.json"
LEGAL_PHRASES = [
idiom.strip() for idiom in idiom_path.open("r", encoding="utf-8").readlines()
]
ANSWER_PHRASES: List[Dict[str, str]] = json.load(
answer_path.open("r", encoding="utf-8")
)
HARD_ANSWER_PHRASES: List[Dict[str, str]] = json.load(
answer_hard_path.open("r", encoding="utf-8")
)
# class LegalPhrasesModifiedHandler(FileSystemEventHandler):
# """
# Handler for resource file changes
# """
# def on_modified(self, event):
# print(f"{event.src_path} modified, reloading resource...")
# if "idioms.txt" in event.src_path:
# global LEGAL_PHRASES
# LEGAL_PHRASES = [
# idiom.strip()
# for idiom in idiom_path.open("r", encoding="utf-8").readlines()
# ]
# elif "answers.json" in event.src_path:
# global ANSWER_PHRASES
# ANSWER_PHRASES = json.load(
# answer_path.open("r", encoding="utf-8")
# )
# Observer().schedule(
# LegalPhrasesModifiedHandler(),
# data_dir,
# recursive=False,
# event_filter=FileModifiedEvent,
# )
def legal_idiom(word: str) -> bool:
return word in LEGAL_PHRASES
def random_idiom(is_hard: bool = False) -> Tuple[str, str]:
answer = random.choice(HARD_ANSWER_PHRASES if is_hard else ANSWER_PHRASES)
return answer["word"], answer["explanation"]
# fmt: off
# 声母
INITIALS = [
"zh", "z", "y", "x", "w", "t", "sh", "s", "r", "q", "p",
"n", "m", "l", "k", "j", "h", "g", "f", "d", "ch", "c", "b"
]
# 韵母
FINALS = [
"ün", "üe", "üan", "ü", "uo", "un", "ui", "ue", "uang",
"uan", "uai","ua", "ou", "iu", "iong", "ong", "io", "ing",
"in", "ie", "iao", "iang", "ian", "ia", "er", "eng", "en",
"ei", "ao", "ang", "an", "ai", "u", "o", "i", "e", "a"
]
# fmt: on
def get_pinyin(idiom: str) -> List[Tuple[str, str, str]]:
pys = pinyin(idiom, style=Style.TONE3, v_to_u=True)
results = []
for p in pys:
py = p[0]
if py[-1].isdigit():
tone = py[-1]
py = py[:-1]
else:
tone = ""
initial = ""
for i in INITIALS:
if py.startswith(i):
initial = i
break
final = ""
for f in FINALS:
if py.endswith(f):
final = f
break
results.append((initial, final, tone)) # 声母,韵母,声调
return results
def save_jpg(frame: IMG) -> BytesIO:
output = BytesIO()
frame = frame.convert("RGB")
frame.save(output, format="jpeg")
return output
def load_font(name: str, fontsize: int) -> FreeTypeFont:
return ImageFont.truetype(str(fonts_dir / name), fontsize, encoding="utf-8")