nonebot2聊天机器人插件6:复读机博弈论ban_copyer

nonebot2聊天机器人插件6:复读机博弈论ban_copyer

  • 1. 插件用途
  • 2. 目录结构
  • 3. 实现难点与解决方案
    • 3.1 复读状态转移
    • 3.2 正则匹配替换
  • 4. 代码实现
  • 5. 插件配图
  • 6. 实际效果
  • 7. 下一个插件

该插件涉及知识点:复读状态转移,正则匹配替换
插件合集:nonebot2聊天机器人插件

该系列为用于QQ群聊天机器人的nonebot2相关插件,不保证完全符合标准规范写法,如有差错和改进余地,欢迎大佬指点修正。
前端:nonebot2
后端:go-cqhttp
插件所用语言:python3
前置环境安装过程建议参考零基础2分钟教你搭建QQ机器人——基于nonebot2,但是请注意该教程中的后端版本过旧导致私聊发图异常,需要手动更新go-cqhttp版本。

1. 插件用途

首先,确定一个自定义的游戏规则:
1、在群内连续复读大于等于5人参与时,需要进行复读击杀。
2、每位复读者只计算第一次发言为有效,连续2人参与复读后进入复读计算状态,此时已参与复读者无法再次延续复读或主动打断复读。
3、当未参与复读者打断复读状态,且累计复读参与人数大于等于5人时,将进入复读击杀环节:在只计算第一次发言有效的情况下,倒数第二个复读者将会被禁言1分钟。
4、为了不让第4位复读者只会扣分,不能加分,当被打断时复读次数正好为5次,将会从第4位复读者和第5位复读者中随机抽取一位被禁言,另一位获胜。
5、第一个被复读的人称为复读发起者,被禁言者称为复读受害者,最后一名复读者获胜,被称为复读捍卫者,而打断复读的人,被称为复读猎杀者。
6、在旧一轮复读未完成结算前,不会开始新一轮复读计算。

在完成复读结算的同时,程序需要将四位特殊复读结算的用户添加到数据库中,即记录每一位用户作为发起者、受害者、捍卫者、猎杀者的次数。
程序需要能够在特定命令下查询当前命令用户的所有数据。
程序需要能够列出分数前五位排行榜,分数为(捍卫者次数-受害者次数),次要排序为(捍卫者次数+受害者次数),即在分数相同的用户当中,优先显示参与积极度较高的用户。
程序同时能够列出反向分数排行榜,即分数最低的前五名受害者,但次要排序同样优先显示参与积极度较高的用户,即仍然使用(捍卫者次数+受害者次数)作为次要排序。
为了防止刻意抢人头刷猎杀数据的行为,猎杀次数仅限bot管理员查询。

2. 目录结构

在plugins文件夹中新建一个文件夹ban_copyer,文件夹内目录结构如下:

|-ban_copyer
    |-data
        |-ban_copyer.db
    |-img
        |-所有在信息发送中用到的图片
    |-__init__.py
    |-ban_copyer.py
    |-config.py
    |-model.py

其中img为用于存储发送的图片文件的文件夹,data为储存数据库文件的文件夹,ban_copyer.py为程序主要代码的位置,config.py用于存储配置项,model.py用于封装与数据库交互的SQL语言操作,__init__.py为程序启动位置。
与上一章相同,文件目录中的db文件不需要自己创建,只需要新建好空文件夹,代码运行后如果不存在数据库,会自动新建数据库文件!

3. 实现难点与解决方案

3.1 复读状态转移

根据复读的几条规则,可以罗列出复读计数器的状态转移规则。

  1. 已复读人数小于2且发言不同于复读信息,或,已复读人数小于5且发言不同于复读信息且发言者不是复读参与人员时,不击杀且重置复读计数器与复读信息。
  2. 否则,发言等于复读信息且发言者不是复读参与人员时,复读计数器+1。
  3. 否则,发言不等于复读信息且发言者不是复读参与人员且已复读人数大于等于5,击杀且重置复读计数器与复读信息。
  4. 在击杀时,如果复读数正好为5,进行一次50%概率的随机交换,交换捍卫者与受害者的id。

3.2 正则匹配替换

复读并不仅仅是文字的复读,也可能是图片的复读,甚至可能是更加复杂的问题:图片和文字同时存在,且可能以任意的前后顺序与数量混合在一起。

发现聊天中的图片信息样式为:

[CQ:image,file=94e65b500017f38a6364ce63162f5a80.image,url=...]

进行实验,在群中连续发送两张相同的图片,发现file参数相同,应当为图片的哈希值,而url的参数却会在每一次的发送中都发生变化。

如果将图片与文本分离之后单独比较,那么在考虑M份文本与N份图片以任意比例混合的情况下,比较逻辑将会过于复杂。
一个比较简单易懂的解决方法是将所有聊天信息进行预处理,去除所有图片CQ码中的url信息,这样相同的图片就会具有相同的信息,直接全等比较即可。
该实现需要使用正则匹配找到所有的图片CQ码,并且移除其中的url参数。
代码实现如下:

import re

# 去除图片url
def replace_rule(match):
    return match.group().split(',url=')[0]+r"]}"


# 利用正则匹配所有信息中的图片信息并去除url
def remove_image_num(msg):
    pattern = re.compile('\[CQ:image,file=(\w|\d)*\.image,url=https://gchat.qpic.cn/(\w|\d|\/|-|_|\?|=)*\]')
    result = pattern.sub(replace_rule, msg)
    # 防止操作系统换行符不同导致的问题
    return result.replace('\r\n','').replace('\n','')

4. 代码实现

Config.py

class Config:
    # 记录在哪些群组中使用
    used_in_group = ["131551175"]
    # 插件执行优先级
    priority = 10
    # 机器人QQ号
    bot_id = "123456789"

__init__.py

from .ban_copyer import *

ban_copyer.py

from nonebot import on_message, on_command
from nonebot.rule import to_me
from nonebot.typing import T_State
from nonebot.adapters import Bot, Event
from nonebot.permission import SUPERUSER
from nonebot.adapters.cqhttp import MessageSegment
import re
import os
from .config import Config
from .model import *
import json
from random import randint

__plugin_name__ = 'ban_copyer'
__plugin_usage__ = '用法: 击杀复读者。'

# 创建数据库
create_db()

# 击杀复读者
ban_copyer = on_message(priority=Config.priority)

dir_path = 'file:///' + os.path.split(os.path.realpath(__file__))[0] + '/img/'

# 记录正在被复读的信息{群组id:str}
current_msgs = {
     }
# 记录已经参与复读者QQ号{群组id:list}
already_exist_copyers = {
     }

# 初始化复读内容
for group_id in Config.used_in_group:
    current_msgs[group_id] = ''
    already_exist_copyers[group_id] = []


# 去除图片url
def replace_rule(match):
    return match.group().split(',url=')[0] + r"]}"


# 利用正则匹配所有信息中的图片信息并去除url
def remove_image_num(msg):
    pattern = re.compile('\[CQ:image,file=(\w|\d)*\.image,url=https://gchat.qpic.cn/(\w|\d|\/|-|_|\?|=)*\]')
    result = pattern.sub(replace_rule, msg)
    # 防止操作系统换行符不同导致的问题
    return result.replace('\r\n', '').replace('\n', '')


@ban_copyer.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    ids = event.get_session_id()
    # 如果这是一条群聊信息
    if ids.startswith("group"):
        _, group_id, user_id = event.get_session_id().split("_")
        # 对收到的信息进行预处理(去除相同图片中不同的信息,去除换行符)
        msg = remove_image_num(str(event.get_message()))
        # 如果这条群聊信息来自需要执行复读击杀的群组且不是机器人自己发送的
        if group_id in Config.used_in_group and user_id != Config.bot_id:
            # 1. 不击杀且重置复读计数器与复读信息
            # 已复读人数小于2且发言不同于复读信息,或,已复读人数小于5且发言不同于复读信息且发言者不是复读参与人员
            if (len(already_exist_copyers[group_id]) < 2 and msg != current_msgs[group_id]) \
                    or (len(already_exist_copyers[group_id]) < 5 and msg != current_msgs[group_id] and user_id not in
                        already_exist_copyers[group_id]):
                current_msgs[group_id] = msg
                already_exist_copyers[group_id] = [user_id]
                print(f"群{
       group_id}复读计数器已重置,复读者列表:{
       [user_id]}")

            # 2. 复读计数器+1
            # 发言等于复读信息且发言者不是复读参与人员
            elif msg == current_msgs[group_id] and user_id not in already_exist_copyers[group_id]:
                already_exist_copyers[group_id].append(user_id)
                print(
                    f"群{
       group_id}复读计数器增长为{
       str(len(already_exist_copyers[group_id]))},复读者列表:{
       already_exist_copyers[group_id]}")

            # 3. 击杀且重置复读计数器与复读信息
            # 发言不等于复读信息且发言者不是复读参与人员且已复读人数大于等于5
            elif msg != current_msgs[group_id] and user_id not in already_exist_copyers[group_id] and len(
                    already_exist_copyers[group_id]) >= 5:
                # 击杀倒数第二名复读者
                kill_id = already_exist_copyers[group_id][-2]
                # 最终幸存者
                survival_id = already_exist_copyers[group_id][-1]
                # 发起者
                start_id = already_exist_copyers[group_id][0]
                # 复读次数
                copy_num = len(already_exist_copyers[group_id])
                # 如果复读次数正好为5次,为了让第4位复读者不会只有惩罚没有失败,将4和5随机交换身份
                if copy_num == 5:
                    if randint(0, 1):
                        kill_id, survival_id = survival_id, kill_id
                current_msgs[group_id] = msg
                print(f"复读计数器已被清空,击杀{
       str(kill_id)},复读者列表:{
       already_exist_copyers[group_id]}")
                already_exist_copyers[group_id] = [user_id]
                # group_id: 群号, user_id: 要禁言的QQ号, duration: 禁言时长, 单位秒, 0表示取消禁言
                try:
                    await bot.set_group_ban(group_id=group_id, user_id=kill_id, duration=60)
                except:
                    await ban_copyer.send("警告:权限不足,禁言失败")
                # 将数据记录到数据库中
                add_data(victim=kill_id, killer=user_id, surviver=survival_id, start=start_id)
                await ban_copyer.send("处决完成,共计复读" + str(copy_num) + "次\n受害者:" + MessageSegment.at(kill_id) + "\n猎杀者:" \
                                      + MessageSegment.at(user_id) + "\n捍卫者:" + MessageSegment.at(
                    survival_id) + "\n" + MessageSegment.image(dir_path + '禁止复读.png'))


# 查询命令
copyer_helper = on_command("复读帮助", priority=Config.priority)


@copyer_helper.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    await copyer_helper.finish('''复读击杀器指令说明
普通用户指令:
复读帮助——显示帮助信息
复读查询——查询自己的复读数据

管理员指令:
指定复读查询 [QQ号]——查询指定用户的复读数据
复读捍卫者——显示分数【捍卫者-被害者】排名
复读受害者——显示分数【被害者-捍卫者】排名
复读猎杀者——显示猎杀次数排名
复读发起者——显示发起次数排名''')


best_survival = on_command("复读捍卫者", permission=SUPERUSER, priority=Config.priority)


@best_survival.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    members = search_best_survival()
    if not members:
        await best_survival.finish("暂无捍卫者数据。")
    else:
        result = '前五位复读捍卫者为:\n'
        for uid, score, be_killed, survive in members:
            infos = str(await bot.get_stranger_info(user_id=uid))
            nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
            result += nickname + f'\n分数:{
       score} 胜利:{
       survive} 被杀:{
       be_killed}\n\n'
        await best_survival.finish(result[:-1] + MessageSegment.image(dir_path + '捍卫者.jpg'))


best_killer = on_command("复读猎杀者", permission=SUPERUSER, priority=Config.priority)


@best_killer.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    members = search_best_killer()
    if not members:
        await best_killer.finish("暂无猎杀者数据。")
    else:
        result = '前五位复读猎杀者为:\n'
        for uid, kill in members:
            infos = str(await bot.get_stranger_info(user_id=uid))
            nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
            result += nickname + f'\n击杀:{
       kill}\n\n'
        await best_killer.finish(result[:-1] + MessageSegment.image(dir_path + '猎杀者.jpg'))


best_starter = on_command("复读发起者", permission=SUPERUSER, priority=Config.priority)


@best_starter.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    members = search_best_starter()
    if not members:
        await best_starter.finish("暂无发起者数据。")
    else:
        result = '前五位复读发起者为:\n'
        for uid, start in members:
            infos = str(await bot.get_stranger_info(user_id=uid))
            nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
            result += nickname + f'\n发起:{
       start}\n\n'
        await best_starter.finish(result[:-1] + MessageSegment.image(dir_path + '发起者.jpg'))


worst_survival = on_command("复读受害者", permission=SUPERUSER, priority=Config.priority)


@worst_survival.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    members = search_worst_survival()
    if not members:
        await worst_survival.finish("暂无受害者数据。")
    else:
        result = '前五位复读受害者为:\n'
        for uid, score, be_killed, survive in members:
            infos = str(await bot.get_stranger_info(user_id=uid))
            nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
            result += nickname + f'\n分数:{
       score} 胜利:{
       survive} 被杀:{
       be_killed}\n\n'
        await worst_survival.finish(result[:-1] + MessageSegment.image(dir_path + '受害者.jpg'))


copyer_data = on_command("指定复读查询", permission=SUPERUSER, priority=Config.priority)


@copyer_data.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    uid = str(event.get_message()).strip()
    data = get_exist_data(uid)
    infos = str(await bot.get_stranger_info(user_id=uid))
    nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
    if not data:
        await own_copyer_data.finish(nickname + '\n被杀:0 击杀:0 胜利:0 发起:0')
    else:
        uid, be_killed, kill, survive, start = data[0]
        await copyer_data.finish(nickname + f'\n被杀:{
       be_killed} 击杀:{
       kill} 胜利:{
       survive} 发起:{
       start}')


own_copyer_data = on_command("复读查询", priority=Config.priority)


@own_copyer_data.handle()
async def handle_first_receive(bot: Bot, event: Event, state: T_State):
    ids = event.get_session_id()
    # 如果这是一条群聊信息
    if ids.startswith("group"):
        _, group_id, uid = event.get_session_id().split("_")
    else:
        uid = ids
    data = get_exist_data(uid)
    infos = str(await bot.get_stranger_info(user_id=uid))
    nickname = json.loads(infos.replace("'", '"'))['nickname'] + '(' + str(uid) + ')'
    if not data:
        await own_copyer_data.finish(nickname + '\n被杀:0 胜利:0 发起:0')
    else:
        uid, be_killed, kill, survive, start = data[0]
        await own_copyer_data.finish(nickname + f'\n被杀:{
       be_killed} 胜利:{
       survive} 发起:{
       start}')

model.py

import sqlite3
import os

# 数据库文件的路径位于ban_copyer/data/ban_copyer.db
db_path = os.path.split(os.path.realpath(__file__))[0] + '/data/ban_copyer.db'

# 如果不存在的话,创建数据库,并且创建数据表
def create_db():
    conn = sqlite3.connect(db_path)
    try:
        create_tb_cmd = '''
            CREATE TABLE IF NOT EXISTS ban_copyer
            (uid INT8,
            be_killed INT,
            kill INT,
            survive INT,
            start INT);
            '''
        # 主要就是上面的语句
        conn.execute(create_tb_cmd)
    except:
        pass
    conn.commit()
    conn.close()

# 查询个人分数
def get_exist_data(uid):
    conn = sqlite3.connect(db_path)
    insert_cmd = f'SELECT * from ban_copyer where uid == {
       uid}'
    cursor = list(conn.execute(insert_cmd))
    conn.close()
    # 如果不存在,则返回空列表
    # 否则返回[(uid, be_killed, kill, survive)]
    return cursor

# 添加一次复读击杀后的数据
def add_data(victim, killer, surviver, start):
    # 受害者
    victim_date = get_exist_data(victim)
    # 如果用户数据还不存在
    if not victim_date:
        cmd1 = f'INSERT INTO ban_copyer (uid,be_killed,kill,survive,start) VALUES ({
       victim}, 1, 0, 0, 0);'
    # 否则为victim计数+1
    else:
        cmd1 = f'UPDATE ban_copyer SET be_killed = {
       victim_date[0][1]+1} WHERE uid = {
       victim};'

    # 猎杀者
    killer_data = get_exist_data(killer)
    if not killer_data:
        cmd2 = f'INSERT INTO ban_copyer (uid,be_killed,kill,survive,start) VALUES ({
       killer}, 0, 1, 0, 0);'
    else:
        cmd2 = f'UPDATE ban_copyer SET kill = {
       killer_data[0][2]+1} WHERE uid = {
       killer};'

    # 捍卫者
    surviver_data = get_exist_data(surviver)
    if not surviver_data:
        cmd3 = f'INSERT INTO ban_copyer (uid,be_killed,kill,survive,start) VALUES ({
       surviver}, 0, 0, 1, 0);'
    else:
        cmd3 = f'UPDATE ban_copyer SET survive = {
       surviver_data[0][3]+1} WHERE uid = {
       surviver};'

    # 发起者
    start_data = get_exist_data(start)
    if not start_data:
        cmd4 = f'INSERT INTO ban_copyer (uid,be_killed,kill,survive,start) VALUES ({
       start}, 0, 0, 0, 1);'
    else:
        cmd4 = f'UPDATE ban_copyer SET start = {
       start_data[0][4]+1} WHERE uid = {
       start};'


    conn = sqlite3.connect(db_path)
    conn.execute(cmd1)
    conn.execute(cmd2)
    conn.execute(cmd3)
    conn.execute(cmd4)
    conn.commit()
    conn.close()

# 返回(捍卫者-受害者)最高的人,生存正序排名
def search_best_survival():
    conn = sqlite3.connect(db_path)
    insert_cmd = f'SELECT uid, survive - be_killed, be_killed, survive from ban_copyer order by be_killed - survive, -(survive + be_killed) limit 5'
    cursor = list(conn.execute(insert_cmd))
    conn.close()
    return cursor

# 返回(捍卫者-受害者)最低的人,生存倒叙排名
def search_worst_survival():
    conn = sqlite3.connect(db_path)
    insert_cmd = f'SELECT uid, survive - be_killed, be_killed, survive from ban_copyer order by survive - be_killed, -(survive + be_killed) limit 5'
    cursor = list(conn.execute(insert_cmd))
    conn.close()
    return cursor

# 返回击杀排名
def search_best_killer():
    conn = sqlite3.connect(db_path)
    insert_cmd = f'SELECT uid, kill from ban_copyer order by -kill limit 5'
    cursor = list(conn.execute(insert_cmd))
    conn.close()
    return cursor

# 返回发起排名
def search_best_starter():
    conn = sqlite3.connect(db_path)
    insert_cmd = f'SELECT uid, start from ban_copyer order by -start limit 5'
    cursor = list(conn.execute(insert_cmd))
    conn.close()
    return cursor

5. 插件配图

禁止复读.png
nonebot2聊天机器人插件6:复读机博弈论ban_copyer_第1张图片
发起者.jpg
nonebot2聊天机器人插件6:复读机博弈论ban_copyer_第2张图片
受害者.jpg
nonebot2聊天机器人插件6:复读机博弈论ban_copyer_第3张图片
猎杀者.jpg
nonebot2聊天机器人插件6:复读机博弈论ban_copyer_第4张图片

捍卫者.jpg
nonebot2聊天机器人插件6:复读机博弈论ban_copyer_第5张图片

6. 实际效果


7. 下一个插件

nonebot2聊天机器人插件7:随机角色卡mist_star

你可能感兴趣的:(nonebot2聊天机器人插件,python)